From c2108cc8ceb794bbdac06c98505ac4ac137346c5 Mon Sep 17 00:00:00 2001 From: "Japheth Louie M. Gofredo" <83058948+japhethLG@users.noreply.github.com> Date: Tue, 21 May 2024 06:34:56 +0800 Subject: [PATCH 01/14] revert: Revert to old pricing page (#2459) # Description Revert to old pricing page ## Type of change Please delete options that are not relevant. - [x] Revert feature (non-breaking change which reverts functionality) # How Has This Been Tested? Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. - [x] Manual Test # Screenshots / Screen recording ![image](https://github.com/zesty-io/website/assets/83058948/ab5a897a-282b-4b32-a82e-0f77d84da9bb) --- src/views/zesty/Pricing.js | 147 +++++++++++++++++++++++++------------ 1 file changed, 99 insertions(+), 48 deletions(-) diff --git a/src/views/zesty/Pricing.js b/src/views/zesty/Pricing.js index 13a65bfc4..83541f4b5 100644 --- a/src/views/zesty/Pricing.js +++ b/src/views/zesty/Pricing.js @@ -32,79 +32,130 @@ */ // Mui Imports -import React from 'react'; +import React, { useEffect, useState } from 'react'; +import { useTheme } from '@mui/material/styles'; +import Box from '@mui/material/Box'; // Components Import -import PricingHero from '../../blocks/pricing/revamp/PricingHero'; +import SimpleCardLogo from 'blocks/zesty/LogoGrid/SimpleCardLogo'; +// import Container from 'components/Container'; +import Container from 'blocks/container/Container'; +import PricingHero from '../../blocks/pricing/PricingHero/PricingHero'; +import SupportBanner from '../../blocks/pricing/SupportBanner/SupportBanner'; +import Faq from '../../blocks/pricing/Faq/Faq'; import useFetch from 'components/hooks/useFetch'; import FillerContent from 'components/globals/FillerContent'; -import { PricingTierCards } from 'blocks/pricing/revamp/PricingTierCards'; -import PricingTable from 'blocks/pricing/revamp/PricingTable'; -import AdditionalFeatures from 'blocks/pricing/revamp/AdditionalFeatures'; -import Testimonial from 'blocks/pricing/revamp/Testimonial'; -import FAQs from 'blocks/pricing/revamp/FAQs'; -import Brands from 'blocks/pricing/revamp/Brands'; - -const filterAdditionalFeatures = (features) => { - return features.filter((feature) => feature.classification[0] === "Add-on's"); -}; +function onlyUnique(value, index, self) { + return self.indexOf(value) === index; +} function Pricing({ content }) { - const { data: levers } = useFetch( - `/-/pricing-levers-revamp.json`, - content.zestyProductionMode, - ); - - const { data: leverClassification } = useFetch( - `/-/pricing-levers-classification.json`, - content.zestyProductionMode, - ); - + const theme = useTheme(); const heroProps = { title: content.title, subtitle: content.instance_definition, tiers: content.tiers.data, }; - const faqsProps = { - faqs: content?.related_faqs?.data, - title: content?.faqs_title, - subtitle: content?.faqs_subtitle, - }; - - const additionalFeaturesProps = { - features: filterAdditionalFeatures(levers), - title: content.additional_features_title, - }; + const [, setCategories] = useState([]); - const pricingTableProps = { - levers: levers, - classification: leverClassification, - tiers: content.pricing_tiers_revamp.data, - }; + const { data: pricingData } = useFetch( + `/-/pricing-levers.json`, + content.zestyProductionMode, + ); - const testimonialProps = { - testimonials: content.testimonials.data, - title: content.testimonial_title, - }; + useEffect(() => { + let leverCategories = []; + pricingData.forEach((item) => { + leverCategories.push(item.classification); + }); + leverCategories.filter(onlyUnique); + let cats = [...new Set(leverCategories)]; + setCategories(cats); + }, [pricingData]); return ( <> - - - - - - + + {/* {Pricing Comparison Table} */} + {/* + + + + {content?.comparison_heading} + + + + {active && ( + + {categories.map((cat, idx) => ( + + ))} + + )} + + */} + + + + + + {/* */} + {/* + + */} + + + + + ); } From d0a85fc16365e799e38d1ae228d3987e7c5816f9 Mon Sep 17 00:00:00 2001 From: Gian Espinosa <44116036+glespinosa@users.noreply.github.com> Date: Mon, 10 Jun 2024 09:08:06 +0800 Subject: [PATCH 02/14] fix(user & domain): fixing invalid date (#2465) fixing invalid date on users and domains tab Will close : https://github.com/zesty-io/manager-ui/issues/2716 --- src/components/accounts/domains/DomainListings.js | 4 +++- src/views/accounts/instances/Users.js | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/accounts/domains/DomainListings.js b/src/components/accounts/domains/DomainListings.js index 9aa0ef5a8..e939ef546 100644 --- a/src/components/accounts/domains/DomainListings.js +++ b/src/components/accounts/domains/DomainListings.js @@ -107,7 +107,9 @@ export default function DomainListings({ renderCell: (params) => { return ( - {dayjs(params.row.createdAt).format('MMM DD, YYYY')} + {!dayjs(params.row.createdAt).isValid() + ? '' + : dayjs(params.row.createdAt).format('MMM DD, YYYY')} ); }, diff --git a/src/views/accounts/instances/Users.js b/src/views/accounts/instances/Users.js index 66fbadb8c..369069a32 100644 --- a/src/views/accounts/instances/Users.js +++ b/src/views/accounts/instances/Users.js @@ -152,7 +152,9 @@ const CustomTable = ({ editable: false, renderHeader: () => Last Active, renderCell: (params) => { - const date = dayjs(params.row.lastLogin).format('MMM DD, YYYY'); + const date = !dayjs(params.row.lastLogin).isValid() + ? '' + : dayjs(params.row.lastLogin).format('MMM DD, YYYY'); return ( {date} From 0c730ebccda2f6593e366d4afeb8db8c81f41937 Mon Sep 17 00:00:00 2001 From: "Japheth Louie M. Gofredo" <83058948+japhethLG@users.noreply.github.com> Date: Sun, 23 Jun 2024 22:47:53 -0400 Subject: [PATCH 03/14] fix: Zesty.io Demo Page spacing edits (#2468) # Description Fixed the whitespace in demo form Fixes #2467 ## Type of change Please delete options that are not relevant. - [x] Bug fix (non-breaking change which fixes an issue) # How Has This Been Tested? Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. - [x] Manual Test - [ ] Unit Test - [ ] E2E Test # Screenshots / Screen recording Please add screenshots or recording if applicable Before: ![image](https://github.com/zesty-io/website/assets/83058948/937f9c4b-fc71-4b24-a9fd-69bd33ee2246) After: ![image](https://github.com/zesty-io/website/assets/83058948/1045d539-5e81-4080-b60d-c0f6038438ce) --- src/revamp/ui/GetDemoSection/index.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/revamp/ui/GetDemoSection/index.js b/src/revamp/ui/GetDemoSection/index.js index c2394a33d..66c559887 100644 --- a/src/revamp/ui/GetDemoSection/index.js +++ b/src/revamp/ui/GetDemoSection/index.js @@ -118,7 +118,7 @@ const GetDemoSection = ({ })} > @@ -138,13 +138,13 @@ const GetDemoSection = ({ p: { component: Typography, props: { - mt: 1, + mt: 2, component: 'p', variant: 'h6', whiteSpace: 'pre-line', color: 'grey.300', fontSize: '18px', - lineHeight: '28px', + lineHeight: '24px', }, }, }, @@ -282,6 +282,9 @@ const GetDemoSection = ({ )} + + + @@ -294,7 +297,7 @@ export default GetDemoSection; function Testimonial({ review }) { return ( - + {review?.review} @@ -320,9 +323,6 @@ function TrustLogos() { return ( - - - ); } From 22d8f34b5d3151e72aa7927ad44ae560a8d32dad Mon Sep 17 00:00:00 2001 From: "Japheth Louie M. Gofredo" <83058948+japhethLG@users.noreply.github.com> Date: Thu, 27 Jun 2024 08:09:23 +0800 Subject: [PATCH 04/14] fix: Zesty.io Mindshare links falling back to old content/not displaying properly (#2470) # Description Fixed Zesty.io Mindshare links falling back to old content/not displaying properly Fixes #2463 ## Type of change Please delete options that are not relevant. - [x] Bug fix (non-breaking change which fixes an issue) # How Has This Been Tested? Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. - [x] Manual Test - [ ] Unit Test - [ ] E2E Test # Screenshots / Screen recording Before: ![image](https://github.com/zesty-io/website/assets/83058948/a86c3600-4c11-4a84-a665-775085c253d5) After: ![image](https://github.com/zesty-io/website/assets/83058948/1ae206ce-91cc-4500-b5d2-984d513c3c09) --- src/layouts/Main/Main.js | 1 + src/lib/ZestyView.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/layouts/Main/Main.js b/src/layouts/Main/Main.js index adccd8689..a151737de 100644 --- a/src/layouts/Main/Main.js +++ b/src/layouts/Main/Main.js @@ -191,6 +191,7 @@ const Main = ({ isAuthenticated={isLoggedIn} userInfo={userInfo?.data} loading={loading} + cta={'Contact Sales'} /> )} diff --git a/src/lib/ZestyView.js b/src/lib/ZestyView.js index d2c07f1bb..9ea1a4bdb 100644 --- a/src/lib/ZestyView.js +++ b/src/lib/ZestyView.js @@ -46,7 +46,7 @@ export function ZestyView(props) { props.content.meta.layout?.json['layout:root:column:0']?.children, ) === '{}' ) { - return true; + return false; } // return only true if the layout is active and has components From aa3c1e57b13635ce5b9652f80a36cab519f3366d Mon Sep 17 00:00:00 2001 From: "Japheth Louie M. Gofredo" <83058948+japhethLG@users.noreply.github.com> Date: Sat, 13 Jul 2024 11:47:40 +0800 Subject: [PATCH 05/14] fix: Error on Homepage Demo CTA (#2474) # Description Fix wrong props of homepage cta; Removed spans in headings for articles Fixes #2472, #2473 ## Type of change Please delete options that are not relevant. - [x] Bug fix (non-breaking change which fixes an issue) # How Has This Been Tested? Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. - [x] Manual Test # Screenshots / Screen recording Before: ![image](https://github.com/user-attachments/assets/ef1994f5-81ee-42cd-b185-a4d166c27417) ![image](https://github.com/user-attachments/assets/f49dd7ff-8872-4adf-a1c7-b866c280a9e2) After: ![image](https://github.com/user-attachments/assets/416038be-33cd-4e60-8a58-1ec2a37fa71d) ![image](https://github.com/user-attachments/assets/0c761b0e-a65c-46cc-aeec-66182d8f5ae3) --- src/views/zesty/Article.js | 25 +++++++++++++++++++- src/views/zesty/Homepage/EnterpriseGrowth.js | 4 ++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/views/zesty/Article.js b/src/views/zesty/Article.js index 40dcaf1e1..3c08d10c5 100644 --- a/src/views/zesty/Article.js +++ b/src/views/zesty/Article.js @@ -89,6 +89,26 @@ function Article({ content }) { // Define a regular expression pattern to match [_CTA_] let regexPattern = /\[CALL TO ACTION (\d+)\]/g; + useEffect(() => { + const removeSpansInHeadings = (html) => { + let tempDiv = document.createElement('div'); + tempDiv.innerHTML = html; + + let headings = tempDiv.querySelectorAll('h1, h2, h3, h4, h5, h6'); + + headings.forEach((heading) => { + let spans = heading.querySelectorAll('span'); + spans.forEach((span) => { + span.replaceWith(...span.childNodes); + }); + }); + + return tempDiv.innerHTML; + }; + + setNewContent(removeSpansInHeadings(newContent)); + }, [newContent]); + useEffect(() => { const validateWysiwyg = () => { if (newContent?.includes('Error hydrating')) { @@ -209,7 +229,10 @@ function Article({ content }) { ? `:is(span, p, h1, h2, h3, h4, h5, h6) :is(img) { width: auto; max-width: 100%; -}` + } + :h1 span, :h2 span { + color: black; + }` : ``; // Match CTA component sort order id from array to return its props diff --git a/src/views/zesty/Homepage/EnterpriseGrowth.js b/src/views/zesty/Homepage/EnterpriseGrowth.js index 59f20d63c..b35efe140 100644 --- a/src/views/zesty/Homepage/EnterpriseGrowth.js +++ b/src/views/zesty/Homepage/EnterpriseGrowth.js @@ -6,12 +6,12 @@ const Child = dynamic(() => import('revamp/ui/EnterpriseGrowth'), { loading: Placeholder, }); -const Index = () => { +const Index = (props) => { const { ref, inView } = useInView({ triggerOnce: true, threshold: 0, }); - return
{inView && }
; + return
{inView && }
; }; export default Index; From 87e4da6a9957f21f5069da4af495ea30d88b35bf Mon Sep 17 00:00:00 2001 From: Andres Galindo Date: Wed, 24 Jul 2024 15:09:33 -0700 Subject: [PATCH 06/14] feat: Add new system roles (#2475) Adds new system roles: Developer Contributor, Access Admin PR for adding them to legacy accounts: https://github.com/zesty-io/accounts-ui/pull/248 --- src/components/accounts/users/baseroles.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/components/accounts/users/baseroles.js b/src/components/accounts/users/baseroles.js index 2b410d008..546100b98 100644 --- a/src/components/accounts/users/baseroles.js +++ b/src/components/accounts/users/baseroles.js @@ -47,4 +47,20 @@ export const baseroles = [ label: 'Contributor', value: 'contributor', }, + { + ZUID: '31-71cfc74-d3vc0n', + desc: '', + name: '6 - Developer Contributor', + accessLevel: 6, + label: 'Developer Contributor', + value: 'developer-contributor', + }, + { + ZUID: '31-71cfc74-4cc4dm13', + desc: '', + name: '7 - Access Admin', + accessLevel: 7, + label: 'Access Admin', + value: 'access-admin', + }, ]; From a95eaa5c46ccb44913ab59b11b9ff0b238d683c8 Mon Sep 17 00:00:00 2001 From: "Japheth Louie M. Gofredo" <83058948+japhethLG@users.noreply.github.com> Date: Mon, 5 Aug 2024 09:55:04 +0800 Subject: [PATCH 07/14] fix: Announcement is not reflecting the latest announcement (#2477) # Description Add a sort logic for product announcements. Remove "Good Night" when the user logs in at night. Fixes #2476 [#250](https://github.com/zesty-io/accounts-ui/issues/250) ## Type of change Please delete options that are not relevant. - [x] Bug fix (non-breaking change which fixes an issue) # How Has This Been Tested? - [x] Manual Test # Screenshots / Screen recording Please add screenshots or recording if applicable Before: ![image](https://github.com/user-attachments/assets/ca977a94-786b-4219-9522-4ecddbbac870) After: ![image](https://github.com/user-attachments/assets/a76aeac6-8961-4e56-9b16-bba34f2a968a) --- .../accounts/dashboard/ui/ZInstancesContainer.js | 6 ++---- src/pages/login/index.js | 10 +++++++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/components/accounts/dashboard/ui/ZInstancesContainer.js b/src/components/accounts/dashboard/ui/ZInstancesContainer.js index 48f358685..63ec39ff3 100644 --- a/src/components/accounts/dashboard/ui/ZInstancesContainer.js +++ b/src/components/accounts/dashboard/ui/ZInstancesContainer.js @@ -12,11 +12,9 @@ const ZInstancesContainer = ({ }) => { const dayTime = () => { const hour = new Date().getHours(); - if (hour >= 5 && hour <= 12) return 'Good Morning, '; + if (hour >= 0 && hour <= 12) return 'Good Morning, '; else if (hour > 12 && hour <= 17) return 'Good Afternoon, '; - else if (hour > 17 && hour <= 21) return 'Good Evening, '; - else if ((hour > 21 && hour <= 23) || (hour >= 0 && hour <= 4)) - return 'Good Night, '; + else if (hour > 17 && hour <= 23) return 'Good Evening, '; }; return ( <> diff --git a/src/pages/login/index.js b/src/pages/login/index.js index 365518cd2..62d5cab91 100644 --- a/src/pages/login/index.js +++ b/src/pages/login/index.js @@ -15,7 +15,15 @@ const site = 'https://www.zesty.io'; const Login = (props) => { const router = useRouter(); - const content = props.data.data[0].content; + const content = + props.data.data + .sort( + (a, b) => + new Date(b.content.start_date_and_time) - + new Date(a.content.start_date_and_time), + ) + .map((item) => item.content)[0] || null; + const loginContent = props.loginData.data[0].content; const ogimage = content?.feature_image?.data[0]?.url; From 444fffb5c8db45a29dfc36aa3e4470d0d21cb49b Mon Sep 17 00:00:00 2001 From: "Japheth Louie M. Gofredo" <83058948+japhethLG@users.noreply.github.com> Date: Sat, 9 Nov 2024 01:33:24 +0800 Subject: [PATCH 08/14] feat: docs redirects (#2488) # Description Redirects zesty docs to readme Fixes #2487 ## Type of change Please delete options that are not relevant. - [x] New feature (non-breaking change which adds functionality) # How Has This Been Tested? Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. - [x] Manual Test # Screenshots / Screen recording https://github.com/user-attachments/assets/05a4fb40-231b-41e1-9a5c-46288f1b06d2 --------- Signed-off-by: Japheth Louie M. Gofredo <83058948+japhethLG@users.noreply.github.com> --- .vscode/settings.json | 2 +- next.config.js | 5 +++++ src/config/redirects.js | 49 +++++++++++++++++++++++++++++++++++++++++ src/pages/docs/index.js | 6 ++--- 4 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 src/config/redirects.js diff --git a/.vscode/settings.json b/.vscode/settings.json index 5f21e24fb..278c412c5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,5 @@ "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { "source.fixAll.format": "never" - } + }, } diff --git a/next.config.js b/next.config.js index 77288aee4..4c398e613 100644 --- a/next.config.js +++ b/next.config.js @@ -1,4 +1,5 @@ const zestyConfig = require('./zesty.config.json'); +const { docsRedirects } = require('./src/config/redirects'); module.exports = { trailingSlash: true, @@ -14,4 +15,8 @@ module.exports = { ], }, swcMinify: true, + + async redirects() { + return [...docsRedirects]; + }, }; diff --git a/src/config/redirects.js b/src/config/redirects.js new file mode 100644 index 000000000..c27a3184d --- /dev/null +++ b/src/config/redirects.js @@ -0,0 +1,49 @@ +const docsRedirects = [ + { + source: '/docs/instances/api-reference/:path*', + destination: 'https://docs.zesty.io/reference/instances-api-reference', + permanent: true, + }, + { + source: '/docs/authentication/api-reference/:path*', + destination: 'https://docs.zesty.io/reference/authentication-api-reference', + permanent: true, + }, + { + source: '/docs/accounts/api-reference/:path*', + destination: 'https://docs.zesty.io/reference/accounts-api-reference', + permanent: true, + }, + { + source: '/docs/parsley/api-reference/:path*', + destination: 'https://docs.zesty.io/docs/parsley', + permanent: true, + }, + { + source: '/docs/media/api-reference/:path*', + destination: 'https://docs.zesty.io/reference/media-api-reference', + permanent: true, + }, + { + source: '/docs/media/api-reference/manager/:path*', + destination: 'https://docs.zesty.io/reference/media-manager-api-reference', + permanent: true, + }, + { + source: '/docs/media/api-reference/storage/:path*', + destination: 'https://docs.zesty.io/reference/media-storage-api-reference', + permanent: true, + }, + { + source: '/docs/media/api-reference/modify/:path*', + destination: 'https://docs.zesty.io/reference/media-modify-api-reference', + permanent: true, + }, + { + source: '/docs/media/api-reference/resolver/:path*', + destination: 'https://docs.zesty.io/reference/media-resolver-api-reference', + permanent: true, + }, +]; + +module.exports = { docsRedirects }; diff --git a/src/pages/docs/index.js b/src/pages/docs/index.js index 76b6c95f5..4dfa00c8c 100644 --- a/src/pages/docs/index.js +++ b/src/pages/docs/index.js @@ -165,19 +165,19 @@ const cardData = [ title: 'Instances API', description: 'A collection of available REST endpoints scoped to your unique instance.', - link: '/docs/instances/api-reference/', + link: 'https://docs.zesty.io/reference/instances-api-reference', }, { title: 'Authentication API', description: 'Auth API is used to authenticate users with Zesty.io, which returns a token that grants to access services like Instances API, Accounts API, and Media API. Auth was setup as a stand alone service so it can connect to many services in our infrastructure.', - link: '/docs/authentication/api-reference/', + link: 'https://docs.zesty.io/reference/authentication-api-reference', }, { title: 'Accounts API', description: 'API used to control management of users, roles, instances, and teams.', - link: '/docs/accounts/api-reference/', + link: 'https://docs.zesty.io/reference/accounts-api-reference', }, { title: 'Guides', From 4d1865c4021998075046f5ca82a839de82b57f1d Mon Sep 17 00:00:00 2001 From: "Japheth Louie M. Gofredo" <83058948+japhethLG@users.noreply.github.com> Date: Wed, 20 Nov 2024 03:54:45 +0800 Subject: [PATCH 09/14] feat: docs redirects (#2490) # Description This is a fix for the previous redirects of zesty docs to readme. Also added small fix for removing acorns logo Fixes #2487 #2484 ## Type of change Please delete options that are not relevant. - [x] New feature (non-breaking change which adds functionality) # How Has This Been Tested? Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. - [x] Manual Test # Screenshots / Screen recording https://github.com/user-attachments/assets/05a4fb40-231b-41e1-9a5c-46288f1b06d2 --------- Signed-off-by: Japheth Louie M. Gofredo <83058948+japhethLG@users.noreply.github.com> --- src/config/redirects.js | 10 +++++----- src/revamp/ui/GetDemoSection/index.js | 12 +----------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/config/redirects.js b/src/config/redirects.js index c27a3184d..3fea22eee 100644 --- a/src/config/redirects.js +++ b/src/config/redirects.js @@ -19,11 +19,6 @@ const docsRedirects = [ destination: 'https://docs.zesty.io/docs/parsley', permanent: true, }, - { - source: '/docs/media/api-reference/:path*', - destination: 'https://docs.zesty.io/reference/media-api-reference', - permanent: true, - }, { source: '/docs/media/api-reference/manager/:path*', destination: 'https://docs.zesty.io/reference/media-manager-api-reference', @@ -44,6 +39,11 @@ const docsRedirects = [ destination: 'https://docs.zesty.io/reference/media-resolver-api-reference', permanent: true, }, + { + source: '/docs/media/api-reference/:path*', + destination: 'https://docs.zesty.io/reference/media-api-reference', + permanent: true, + }, ]; module.exports = { docsRedirects }; diff --git a/src/revamp/ui/GetDemoSection/index.js b/src/revamp/ui/GetDemoSection/index.js index 66c559887..01005ad6e 100644 --- a/src/revamp/ui/GetDemoSection/index.js +++ b/src/revamp/ui/GetDemoSection/index.js @@ -19,9 +19,7 @@ import useGetDynamicData from './useGetDynamicData'; import { useRouter } from 'next/router'; import ZestyImage from 'blocks/Image/ZestyImage'; -const acorns = - 'https://storage.googleapis.com/assets.zesty.io/website/images/assets/demo/Acorns%20Logo.svg', - bjs = `https://storage.googleapis.com/assets.zesty.io/website/images/assets/demo/BJ's%20Logo.svg`, +const bjs = `https://storage.googleapis.com/assets.zesty.io/website/images/assets/demo/BJ's%20Logo.svg`, rocketLeague = `https://storage.googleapis.com/assets.zesty.io/website/images/assets/demo/Horizontal_Text.svg`, cornershop = `https://storage.googleapis.com/assets.zesty.io/website/images/assets/demo/Logo_de_Cornershop%201.svg`, phoenixSuns = `https://storage.googleapis.com/assets.zesty.io/website/images/assets/demo/Phoenix%20Suns.svg`, @@ -369,14 +367,6 @@ export function Logos({ invert = false, alignLeft }) { height="32px" alt={generateAlt('Singlife')} /> - {generateAlt('Acorns')} Date: Thu, 21 Nov 2024 00:58:33 +0800 Subject: [PATCH 10/14] fix: Remove Acorn, BJs and Cornershop logos from all components in the marketing website (#2491) # Description Removed Acorn, BJs and Cornershop logos from all components in the marketing website (Homepage, Demo, Insurance CMS). Fixes #2484 ## Type of change Please delete options that are not relevant. - [x] Bug fix (non-breaking change which fixes an issue) # How Has This Been Tested? - [x] Manual Test # Screenshots / Screen recording ![image](https://github.com/user-attachments/assets/6ac7e196-bc3a-4d01-afec-a364f66e1e59) ![image](https://github.com/user-attachments/assets/ee5b6d8c-3c75-466b-93b1-64f4f8b98d8d) ![image](https://github.com/user-attachments/assets/33221a89-a95a-4598-80d9-1b48288bc56a) --- src/revamp/ui/GetDemoSection/index.js | 20 +------------------- src/revamp/ui/HeroV2/index.js | 16 ---------------- 2 files changed, 1 insertion(+), 35 deletions(-) diff --git a/src/revamp/ui/GetDemoSection/index.js b/src/revamp/ui/GetDemoSection/index.js index 01005ad6e..98644fc75 100644 --- a/src/revamp/ui/GetDemoSection/index.js +++ b/src/revamp/ui/GetDemoSection/index.js @@ -19,9 +19,7 @@ import useGetDynamicData from './useGetDynamicData'; import { useRouter } from 'next/router'; import ZestyImage from 'blocks/Image/ZestyImage'; -const bjs = `https://storage.googleapis.com/assets.zesty.io/website/images/assets/demo/BJ's%20Logo.svg`, - rocketLeague = `https://storage.googleapis.com/assets.zesty.io/website/images/assets/demo/Horizontal_Text.svg`, - cornershop = `https://storage.googleapis.com/assets.zesty.io/website/images/assets/demo/Logo_de_Cornershop%201.svg`, +const rocketLeague = `https://storage.googleapis.com/assets.zesty.io/website/images/assets/demo/Horizontal_Text.svg`, phoenixSuns = `https://storage.googleapis.com/assets.zesty.io/website/images/assets/demo/Phoenix%20Suns.svg`, singlife = `https://storage.googleapis.com/assets.zesty.io/website/images/assets/demo/Singlife%20Logo.svg`, sony = `https://storage.googleapis.com/assets.zesty.io/website/images/assets/demo/Sony%20Logo.svg`, @@ -383,22 +381,6 @@ export function Logos({ invert = false, alignLeft }) { height="32px" alt={generateAlt('Wattpad')} /> - {generateAlt('Corner - {generateAlt('Bjs')}
); diff --git a/src/revamp/ui/HeroV2/index.js b/src/revamp/ui/HeroV2/index.js index 94e7b05fb..ba466aca5 100644 --- a/src/revamp/ui/HeroV2/index.js +++ b/src/revamp/ui/HeroV2/index.js @@ -9,8 +9,6 @@ import Logos from './Logos'; const media = 'https://kfg6bckb.media.zestyio.com/Zesty-io-2023-Homepage-Graphic.webp', - acorns = 'https://kfg6bckb.media.zestyio.com/acornsHero.svg', - bjs = 'https://kfg6bckb.media.zestyio.com/bjsHero.svg', phoenixSuns = 'https://kfg6bckb.media.zestyio.com/phoenixSunsHero.svg', rocketLeague = 'https://kfg6bckb.media.zestyio.com/rocketLeagueHero.svg', singlife = 'https://kfg6bckb.media.zestyio.com/singlifeHero.svg', @@ -38,20 +36,6 @@ const logos = [ title: 'Singlife', alt: generateAlt('Singlife'), }, - { - src: acorns, - width: 94, - height: 32, - title: 'Acorns', - alt: generateAlt('Acorns'), - }, - { - src: bjs, - width: 36.48, - height: 32, - title: 'Bjs', - alt: generateAlt('Bjs'), - }, { src: phoenixSuns, width: 31.59, From 7a9aa5704f5421eb62cda980a84f705849ddc3f2 Mon Sep 17 00:00:00 2001 From: Nar -- <28705606+finnar-bin@users.noreply.github.com> Date: Sat, 7 Dec 2024 02:27:00 +0800 Subject: [PATCH 11/14] feat: Update homepage logos (#2486) # Description Update homepage company logos Resolves #2483 ## Type of change Please delete options that are not relevant. - [x] New feature (non-breaking change which adds functionality) # How Has This Been Tested? - [x] Manual Test # Screenshots / Screen recording ![image](https://github.com/user-attachments/assets/36e5031b-da92-446a-b198-080181fc5387) --------- Signed-off-by: Stuart Runyan Co-authored-by: Stuart Runyan --- src/revamp/ui/HeroV2/index.js | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/revamp/ui/HeroV2/index.js b/src/revamp/ui/HeroV2/index.js index ba466aca5..d223a3f31 100644 --- a/src/revamp/ui/HeroV2/index.js +++ b/src/revamp/ui/HeroV2/index.js @@ -10,9 +10,10 @@ import Logos from './Logos'; const media = 'https://kfg6bckb.media.zestyio.com/Zesty-io-2023-Homepage-Graphic.webp', phoenixSuns = 'https://kfg6bckb.media.zestyio.com/phoenixSunsHero.svg', - rocketLeague = 'https://kfg6bckb.media.zestyio.com/rocketLeagueHero.svg', singlife = 'https://kfg6bckb.media.zestyio.com/singlifeHero.svg', - sony = 'https://kfg6bckb.media.zestyio.com/sonyHero.svg'; + sony = 'https://kfg6bckb.media.zestyio.com/sonyHero.svg', + wattpad = 'https://kfg6bckb.media.zestyio.com/wattpadHero.png?height=32', + tsa = 'https://kfg6bckb.media.zestyio.com/theSalvationArmyHero.png?height=32'; const logos = [ { @@ -22,13 +23,6 @@ const logos = [ title: 'Sony', alt: generateAlt('Sony'), }, - { - src: rocketLeague, - width: 88.35, - height: 32, - title: 'Rocket League', - alt: generateAlt('Rocket League'), - }, { src: singlife, width: 102.12, @@ -36,6 +30,20 @@ const logos = [ title: 'Singlife', alt: generateAlt('Singlife'), }, + { + src: wattpad, + width: 'auto', + height: 32, + title: 'Wattpad', + alt: generateAlt('Wattpad'), + }, + { + src: tsa, + width: 'auto', + height: 32, + title: 'The Salvation Army', + alt: generateAlt('The Salvation Army'), + }, { src: phoenixSuns, width: 31.59, From f0b61b64609d46ea65ee0407c350bf7012f39d02 Mon Sep 17 00:00:00 2001 From: Nar -- <28705606+finnar-bin@users.noreply.github.com> Date: Sat, 7 Dec 2024 02:49:07 +0800 Subject: [PATCH 12/14] feat: Custom Roles (#2481) # Description Added a page to manage an instance's custom roles. Closes #2479 ## Type of change - [x] New feature (non-breaking change which adds functionality) # Dependencies - [ ] https://github.com/zesty-io/fetch-wrapper/pull/66 - [x] https://github.com/zesty-io/material/pull/109 - [ ] https://github.com/zesty-io/accounts-api/pull/285 - [ ] https://github.com/zesty-io/base-api/pull/207 # How Has This Been Tested? - [x] Manual Test # Screenshots / Screen recording [screen-recorder-tue-oct-29-2024-15-02-58.webm](https://github.com/user-attachments/assets/e75765ef-8ab1-4936-b925-55f39977591a) --------- Co-authored-by: Stuart Runyan --- next-env.d.ts | 5 + package-lock.json | 202 +++++-- package.json | 3 +- public/assets/images/add_user.svg | 90 +++ public/assets/images/data_table.svg | 63 ++ public/assets/images/no_search_results.svg | 23 + public/assets/images/shield.svg | 13 + public/styles/custom.css | 82 +-- .../revamp/layoutComponent/Images/index.js | 14 +- .../revamp/layoutComponent/Row/index.js | 44 +- .../revamp/layoutComponent/Text/index.js | 19 +- src/components/accounts/instances/lang.js | 1 + src/components/accounts/instances/tabs.js | 25 +- src/components/accounts/roles/BaseRoles.tsx | 146 +++++ .../accounts/roles/CreateCustomRoleDialog.tsx | 569 ++++++++++++++++++ src/components/accounts/roles/CustomRoles.tsx | 148 +++++ .../accounts/roles/DeleteCustomRoleDialog.tsx | 128 ++++ .../roles/EditCustomRoleDialog/index.tsx | 492 +++++++++++++++ .../EditCustomRoleDialog/tabs/Details.tsx | 161 +++++ .../tabs/Permissions/AddRule.tsx | 194 ++++++ .../tabs/Permissions/Loading.tsx | 44 ++ .../tabs/Permissions/NoFilterMatches.tsx | 60 ++ .../tabs/Permissions/NoRules.tsx | 88 +++ .../tabs/Permissions/ResourceSelector.tsx | 138 +++++ .../tabs/Permissions/Table.tsx | 217 +++++++ .../tabs/Permissions/index.tsx | 164 +++++ .../roles/EditCustomRoleDialog/tabs/Users.tsx | 189 ++++++ .../accounts/roles/NoCustomRoles.tsx | 55 ++ .../accounts/ui/NoSearchResults.tsx | 56 ++ .../accounts/ui/VirtualizedList.tsx | 58 ++ src/components/accounts/ui/dialogs/index.js | 9 + src/components/accounts/ui/header/index.js | 60 +- src/components/globals/NoPermission.jsx | 80 +++ src/mui.d.ts | 27 + src/pages/instances/[zuid]/roles.tsx | 59 ++ src/store/instance.ts | 58 ++ src/store/roles.ts | 244 ++++++++ src/store/types.ts | 134 +++++ src/views/accounts/instances/Roles.tsx | 181 ++++++ src/views/accounts/instances/index.js | 1 + tsconfig.json | 19 + 41 files changed, 4204 insertions(+), 159 deletions(-) create mode 100644 next-env.d.ts create mode 100644 public/assets/images/add_user.svg create mode 100644 public/assets/images/data_table.svg create mode 100644 public/assets/images/no_search_results.svg create mode 100644 public/assets/images/shield.svg create mode 100644 src/components/accounts/roles/BaseRoles.tsx create mode 100644 src/components/accounts/roles/CreateCustomRoleDialog.tsx create mode 100644 src/components/accounts/roles/CustomRoles.tsx create mode 100644 src/components/accounts/roles/DeleteCustomRoleDialog.tsx create mode 100644 src/components/accounts/roles/EditCustomRoleDialog/index.tsx create mode 100644 src/components/accounts/roles/EditCustomRoleDialog/tabs/Details.tsx create mode 100644 src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/AddRule.tsx create mode 100644 src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/Loading.tsx create mode 100644 src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/NoFilterMatches.tsx create mode 100644 src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/NoRules.tsx create mode 100644 src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/ResourceSelector.tsx create mode 100644 src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/Table.tsx create mode 100644 src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/index.tsx create mode 100644 src/components/accounts/roles/EditCustomRoleDialog/tabs/Users.tsx create mode 100644 src/components/accounts/roles/NoCustomRoles.tsx create mode 100644 src/components/accounts/ui/NoSearchResults.tsx create mode 100644 src/components/accounts/ui/VirtualizedList.tsx create mode 100644 src/components/globals/NoPermission.jsx create mode 100644 src/mui.d.ts create mode 100644 src/pages/instances/[zuid]/roles.tsx create mode 100644 src/store/instance.ts create mode 100644 src/store/roles.ts create mode 100644 src/store/types.ts create mode 100644 src/views/accounts/instances/Roles.tsx create mode 100644 tsconfig.json diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 000000000..4f11a03dc --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/package-lock.json b/package-lock.json index bd3c08ead..a78f537e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "@uiw/codemirror-theme-github": "^4.21.18", "@uiw/react-codemirror": "^4.21.18", "@zesty-io/live-editor": "^2.0.30", - "@zesty-io/material": "^0.12.0", + "@zesty-io/material": "^0.15.6", "@zesty-io/react-autolayout": "^1.0.0-beta.16", "algoliasearch": "^4.20.0", "aos": "^2.3.4", @@ -74,6 +74,7 @@ "@next/bundle-analyzer": "^14.0.1", "@testing-library/jest-dom": "^6.1.3", "@testing-library/react": "^14.0.0", + "@types/react-window": "^1.8.8", "@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/parser": "^6.7.4", "babel-eslint": "^10.1.0", @@ -808,7 +809,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.23.4", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", + "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" @@ -1318,7 +1321,9 @@ "license": "MIT" }, "node_modules/@emotion/is-prop-valid": { - "version": "1.2.1", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", "license": "MIT", "dependencies": { "@emotion/memoize": "^0.8.1" @@ -2902,11 +2907,13 @@ } }, "node_modules/@mui/private-theming": { - "version": "5.14.18", + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.6.tgz", + "integrity": "sha512-rAk+Rh8Clg7Cd7shZhyt2HGTTE5wYKNSJ5sspf28Fqm/PZ69Er9o6KX25g03/FG2dfpg5GCwZh/xOojiTfm3hw==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.23.2", - "@mui/utils": "^5.14.18", + "@babel/runtime": "^7.23.9", + "@mui/utils": "^5.16.6", "prop-types": "^15.8.1" }, "engines": { @@ -2914,7 +2921,7 @@ }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/mui" + "url": "https://opencollective.com/mui-org" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0", @@ -2957,17 +2964,19 @@ } }, "node_modules/@mui/styles": { - "version": "5.14.18", + "version": "5.16.7", + "resolved": "https://registry.npmjs.org/@mui/styles/-/styles-5.16.7.tgz", + "integrity": "sha512-FfXhHP/2MlqH+vLs2tIHMeCChmqSRgkOALVNLKkPrDsvtoq5J8OraOutCn1scpvRjr9mO8ZhW6jKx2t/vUDxtQ==", "license": "MIT", "peer": true, "dependencies": { - "@babel/runtime": "^7.23.2", + "@babel/runtime": "^7.23.9", "@emotion/hash": "^0.9.1", - "@mui/private-theming": "^5.14.18", - "@mui/types": "^7.2.9", - "@mui/utils": "^5.14.18", - "clsx": "^2.0.0", - "csstype": "^3.1.2", + "@mui/private-theming": "^5.16.6", + "@mui/types": "^7.2.15", + "@mui/utils": "^5.16.6", + "clsx": "^2.1.0", + "csstype": "^3.1.3", "hoist-non-react-statics": "^3.3.2", "jss": "^10.10.0", "jss-plugin-camel-case": "^10.10.0", @@ -2984,7 +2993,7 @@ }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/mui" + "url": "https://opencollective.com/mui-org" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0", @@ -2997,7 +3006,9 @@ } }, "node_modules/@mui/styles/node_modules/clsx": { - "version": "2.0.0", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "license": "MIT", "peer": true, "engines": { @@ -3050,10 +3061,12 @@ } }, "node_modules/@mui/types": { - "version": "7.2.9", + "version": "7.2.19", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.19.tgz", + "integrity": "sha512-6XpZEM/Q3epK9RN8ENoXuygnqUQxE+siN/6rGRi2iwJPgBUR25mphYQ9ZI87plGh58YoZ5pp40bFvKYOCDJ3tA==", "license": "MIT", "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0" + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -3062,20 +3075,24 @@ } }, "node_modules/@mui/utils": { - "version": "5.14.18", + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.6.tgz", + "integrity": "sha512-tWiQqlhxAt3KENNiSRL+DIn9H5xNVK6Jjf70x3PnfQPz1MPBdh7yyIcAyVBT9xiw7hP3SomRhPR7hzBMBCjqEA==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.23.2", - "@types/prop-types": "^15.7.10", + "@babel/runtime": "^7.23.9", + "@mui/types": "^7.2.15", + "@types/prop-types": "^15.7.12", + "clsx": "^2.1.1", "prop-types": "^15.8.1", - "react-is": "^18.2.0" + "react-is": "^18.3.1" }, "engines": { "node": ">=12.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/mui" + "url": "https://opencollective.com/mui-org" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0", @@ -3087,6 +3104,15 @@ } } }, + "node_modules/@mui/utils/node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/@mui/x-data-grid": { "version": "6.18.1", "license": "MIT", @@ -4007,7 +4033,9 @@ "license": "MIT" }, "node_modules/@types/prop-types": { - "version": "15.7.11", + "version": "15.7.13", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", + "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", "license": "MIT" }, "node_modules/@types/react": { @@ -4034,6 +4062,16 @@ "@types/react": "*" } }, + "node_modules/@types/react-window": { + "version": "1.8.8", + "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz", + "integrity": "sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/scheduler": { "version": "0.16.7", "license": "MIT" @@ -4057,7 +4095,9 @@ "license": "MIT" }, "node_modules/@types/stylis": { - "version": "4.2.3", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", + "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==", "dev": true, "license": "MIT", "peer": true @@ -4812,7 +4852,9 @@ } }, "node_modules/@zesty-io/material": { - "version": "0.12.0", + "version": "0.15.6", + "resolved": "https://registry.npmjs.org/@zesty-io/material/-/material-0.15.6.tgz", + "integrity": "sha512-zMWI1J+ArxhD7VX+A6JGRtwMO4oVNIPm+ZcjqugbGK1u/aaRRkRzhQ/ZsvTwHxOgK3rsatv6QWfYCttBTZF1KQ==", "license": "MIT", "dependencies": { "@emotion/react": "^11.9.0", @@ -5818,6 +5860,8 @@ }, "node_modules/camelize": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", "dev": true, "license": "MIT", "peer": true, @@ -6373,6 +6417,8 @@ }, "node_modules/css-color-keywords": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", "dev": true, "license": "ISC", "peer": true, @@ -6382,6 +6428,8 @@ }, "node_modules/css-to-react-native": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", "dev": true, "license": "MIT", "peer": true, @@ -6393,6 +6441,8 @@ }, "node_modules/css-vendor": { "version": "2.0.8", + "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz", + "integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==", "license": "MIT", "peer": true, "dependencies": { @@ -6424,7 +6474,9 @@ "license": "MIT" }, "node_modules/csstype": { - "version": "3.1.2", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, "node_modules/cypress": { @@ -9086,7 +9138,9 @@ } }, "node_modules/hyphenate-style-name": { - "version": "1.0.4", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", + "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==", "license": "BSD-3-Clause", "peer": true }, @@ -9517,6 +9571,8 @@ }, "node_modules/is-in-browser": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", + "integrity": "sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g==", "license": "MIT", "peer": true }, @@ -10991,6 +11047,8 @@ }, "node_modules/jquery": { "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", "license": "MIT", "peer": true }, @@ -11168,6 +11226,8 @@ }, "node_modules/jss": { "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss/-/jss-10.10.0.tgz", + "integrity": "sha512-cqsOTS7jqPsPMjtKYDUpdFC0AbhYFLTcuGRqymgmdJIeQ8cH7+AgX7YSgQy79wXloZq2VvATYxUOUQEvS1V/Zw==", "license": "MIT", "peer": true, "dependencies": { @@ -11183,6 +11243,8 @@ }, "node_modules/jss-plugin-camel-case": { "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.10.0.tgz", + "integrity": "sha512-z+HETfj5IYgFxh1wJnUAU8jByI48ED+v0fuTuhKrPR+pRBYS2EDwbusU8aFOpCdYhtRc9zhN+PJ7iNE8pAWyPw==", "license": "MIT", "peer": true, "dependencies": { @@ -11193,6 +11255,8 @@ }, "node_modules/jss-plugin-default-unit": { "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.10.0.tgz", + "integrity": "sha512-SvpajxIECi4JDUbGLefvNckmI+c2VWmP43qnEy/0eiwzRUsafg5DVSIWSzZe4d2vFX1u9nRDP46WCFV/PXVBGQ==", "license": "MIT", "peer": true, "dependencies": { @@ -11202,6 +11266,8 @@ }, "node_modules/jss-plugin-global": { "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.10.0.tgz", + "integrity": "sha512-icXEYbMufiNuWfuazLeN+BNJO16Ge88OcXU5ZDC2vLqElmMybA31Wi7lZ3lf+vgufRocvPj8443irhYRgWxP+A==", "license": "MIT", "peer": true, "dependencies": { @@ -11211,6 +11277,8 @@ }, "node_modules/jss-plugin-nested": { "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.10.0.tgz", + "integrity": "sha512-9R4JHxxGgiZhurDo3q7LdIiDEgtA1bTGzAbhSPyIOWb7ZubrjQe8acwhEQ6OEKydzpl8XHMtTnEwHXCARLYqYA==", "license": "MIT", "peer": true, "dependencies": { @@ -11221,6 +11289,8 @@ }, "node_modules/jss-plugin-props-sort": { "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.10.0.tgz", + "integrity": "sha512-5VNJvQJbnq/vRfje6uZLe/FyaOpzP/IH1LP+0fr88QamVrGJa0hpRRyAa0ea4U/3LcorJfBFVyC4yN2QC73lJg==", "license": "MIT", "peer": true, "dependencies": { @@ -11230,6 +11300,8 @@ }, "node_modules/jss-plugin-rule-value-function": { "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.10.0.tgz", + "integrity": "sha512-uEFJFgaCtkXeIPgki8ICw3Y7VMkL9GEan6SqmT9tqpwM+/t+hxfMUdU4wQ0MtOiMNWhwnckBV0IebrKcZM9C0g==", "license": "MIT", "peer": true, "dependencies": { @@ -11240,6 +11312,8 @@ }, "node_modules/jss-plugin-vendor-prefixer": { "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.10.0.tgz", + "integrity": "sha512-UY/41WumgjW8r1qMCO8l1ARg7NHnfRVWRhZ2E2m0DMYsr2DD91qIXLyNhiX83hHswR7Wm4D+oDYNC1zWCJWtqg==", "license": "MIT", "peer": true, "dependencies": { @@ -13870,6 +13944,8 @@ }, "node_modules/postcss-value-parser": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true, "license": "MIT", "peer": true @@ -14693,7 +14769,9 @@ } }, "node_modules/react-is": { - "version": "18.2.0", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, "node_modules/react-json-view": { @@ -15497,7 +15575,9 @@ } }, "node_modules/search-insights": { - "version": "2.11.0", + "version": "2.17.2", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.2.tgz", + "integrity": "sha512-zFNpOpUO+tY2D85KrxJ+aqwnIfdEGi06UH2+xEb+Bp9Mwznmauqc9djbnBibJO5mpfUPPa8st6Sx65+vbeO45g==", "license": "MIT", "peer": true }, @@ -15577,6 +15657,8 @@ }, "node_modules/shallowequal": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", "dev": true, "license": "MIT", "peer": true @@ -15769,7 +15851,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -16149,20 +16233,22 @@ } }, "node_modules/styled-components": { - "version": "6.1.1", + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.13.tgz", + "integrity": "sha512-M0+N2xSnAtwcVAQeFEsGWFFxXDftHUD7XrKla06QbpUMmbmtFBMMTcKWvFXtWxuD5qQkB8iU5gk6QASlx2ZRMw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@emotion/is-prop-valid": "^1.2.1", - "@emotion/unitless": "^0.8.0", - "@types/stylis": "^4.0.2", - "css-to-react-native": "^3.2.0", - "csstype": "^3.1.2", - "postcss": "^8.4.31", - "shallowequal": "^1.1.0", - "stylis": "^4.3.0", - "tslib": "^2.5.0" + "@emotion/is-prop-valid": "1.2.2", + "@emotion/unitless": "0.8.1", + "@types/stylis": "4.2.5", + "css-to-react-native": "3.2.0", + "csstype": "3.1.3", + "postcss": "8.4.38", + "shallowequal": "1.1.0", + "stylis": "4.3.2", + "tslib": "2.6.2" }, "engines": { "node": ">= 16" @@ -16176,8 +16262,40 @@ "react-dom": ">= 16.8.0" } }, + "node_modules/styled-components/node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/styled-components/node_modules/stylis": { - "version": "4.3.0", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==", "dev": true, "license": "MIT", "peer": true diff --git a/package.json b/package.json index d210b4e99..7a000606d 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "@uiw/codemirror-theme-github": "^4.21.18", "@uiw/react-codemirror": "^4.21.18", "@zesty-io/live-editor": "^2.0.30", - "@zesty-io/material": "^0.12.0", + "@zesty-io/material": "^0.15.6", "@zesty-io/react-autolayout": "^1.0.0-beta.16", "algoliasearch": "^4.20.0", "aos": "^2.3.4", @@ -122,6 +122,7 @@ "@next/bundle-analyzer": "^14.0.1", "@testing-library/jest-dom": "^6.1.3", "@testing-library/react": "^14.0.0", + "@types/react-window": "^1.8.8", "@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/parser": "^6.7.4", "babel-eslint": "^10.1.0", diff --git a/public/assets/images/add_user.svg b/public/assets/images/add_user.svg new file mode 100644 index 000000000..de76486fa --- /dev/null +++ b/public/assets/images/add_user.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/images/data_table.svg b/public/assets/images/data_table.svg new file mode 100644 index 000000000..6414f3990 --- /dev/null +++ b/public/assets/images/data_table.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/images/no_search_results.svg b/public/assets/images/no_search_results.svg new file mode 100644 index 000000000..f2bb006a5 --- /dev/null +++ b/public/assets/images/no_search_results.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/images/shield.svg b/public/assets/images/shield.svg new file mode 100644 index 000000000..ef9969b31 --- /dev/null +++ b/public/assets/images/shield.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/styles/custom.css b/public/styles/custom.css index 242c797c0..8b046daa4 100644 --- a/public/styles/custom.css +++ b/public/styles/custom.css @@ -1,6 +1,6 @@ html { scroll-behavior: smooth; - font-family: Mulish, Arial, Helvetica,sans-serif; + font-family: Mulish, Arial, Helvetica, sans-serif; } h1 span { @@ -28,7 +28,7 @@ h2 span { flex-shrink: 0; width: 40px; height: 40px; - font-family: 'Mulish', Arial, Helvetica,sans-serif; + font-family: 'Mulish', Arial, Helvetica, sans-serif; font-size: 1.25rem; line-height: 1; border-radius: 4px; @@ -46,74 +46,74 @@ h2 span { margin-right: 10px; } - input:-webkit-autofill, -input:-webkit-autofill:hover, -input:-webkit-autofill:focus, -input:-webkit-autofill:active{ - -webkit-box-shadow: 0 0 0 30px white inset !important; - font-family: Mulish, Arial, sans-serif; - font-size: 14px +input:-webkit-autofill:hover, +input:-webkit-autofill:focus, +input:-webkit-autofill:active { + -webkit-box-shadow: 0 0 0 30px white inset !important; + font-family: Mulish, Arial, sans-serif; + font-size: 14px; } @font-face { - font-family: 'Mulish'; - src: url(../fonts/Mulish-Regular.woff2) format(woff2); - font-style: normal; - font-weight: 400; + font-family: 'Mulish'; + src: url(../fonts/Mulish-Regular.woff2) format(woff2); + font-style: normal; + font-weight: 400; } - @font-face { - font-family: 'Mulish'; - src: url(../fonts/Mulish-Medium.woff2) format(woff2); - font-style: normal; - font-weight: 500; + font-family: 'Mulish'; + src: url(../fonts/Mulish-Medium.woff2) format(woff2); + font-style: normal; + font-weight: 500; } @font-face { - font-family: 'Mulish'; - src: url(../fonts/Mulish-SemiBold.woff2) format(woff2); - font-style: normal; - font-weight: 600; + font-family: 'Mulish'; + src: url(../fonts/Mulish-SemiBold.woff2) format(woff2); + font-style: normal; + font-weight: 600; } @font-face { - font-family: 'Mulish'; - src: url(../fonts/Mulish-Bold.woff2) format(woff2); - font-style: normal; - font-weight: 700; + font-family: 'Mulish'; + src: url(../fonts/Mulish-Bold.woff2) format(woff2); + font-style: normal; + font-weight: 700; } @font-face { - font-family: 'Mulish'; - src: url(../fonts/Mulish-ExtraBold.woff2) format(woff2); - font-style: normal; - font-weight: 800; + font-family: 'Mulish'; + src: url(../fonts/Mulish-ExtraBold.woff2) format(woff2); + font-style: normal; + font-weight: 800; } @font-face { - font-family: 'Mulish'; - src: url(../fonts/Mulish-Black.woff2) format(woff2); - font-style: normal; - font-weight: 900; + font-family: 'Mulish'; + src: url(../fonts/Mulish-Black.woff2) format(woff2); + font-style: normal; + font-weight: 900; } /* Algolia DocSearch CSS */ /*! @docsearch/css 3.5.2 | MIT License | © Algolia, Inc. and contributors | https://docsearch.algolia.com */ :root { - --docsearch-primary-color: #FF5D0A !important; - --docsearch-text-color: #5b5b5b !important; - --docsearch-logo-color:#FF5D0A !important; - + --docsearch-primary-color: #ff5d0a !important; + --docsearch-text-color: #5b5b5b !important; + --docsearch-logo-color: #ff5d0a !important; } - -.DocSearch-Container { - z-index: 9999 !important +.DocSearch-Container { + z-index: 9999 !important; } .DocSearch-Button { width: 100% !important; max-width: 350px !important; } +/* Makes sure that the swal is on top of the MUI modals */ +.swal-zindex-override { + z-index: 1301; +} diff --git a/src/blocks/layoutsBlocks/revamp/layoutComponent/Images/index.js b/src/blocks/layoutsBlocks/revamp/layoutComponent/Images/index.js index 07965b791..2ba5cfbbd 100644 --- a/src/blocks/layoutsBlocks/revamp/layoutComponent/Images/index.js +++ b/src/blocks/layoutsBlocks/revamp/layoutComponent/Images/index.js @@ -1,7 +1,13 @@ -import ZestyImage from "blocks/Image/ZestyImage" +import ZestyImage from 'blocks/Image/ZestyImage'; function Images(props) { - - return + return ( + + ); } -export default Images \ No newline at end of file +export default Images; diff --git a/src/blocks/layoutsBlocks/revamp/layoutComponent/Row/index.js b/src/blocks/layoutsBlocks/revamp/layoutComponent/Row/index.js index 1b70290f9..22ff56995 100644 --- a/src/blocks/layoutsBlocks/revamp/layoutComponent/Row/index.js +++ b/src/blocks/layoutsBlocks/revamp/layoutComponent/Row/index.js @@ -1,32 +1,20 @@ -import { Box, Container, Grid, Stack } from "@mui/material" +import { Box, Container, Grid, Stack } from '@mui/material'; function Row(props) { - - if(props.data.name === 'container') { - return ( - - {props.children} - - ) - } - if(props.data.name === 'section') { - return ( - - {props.children} - - ) - } - else { - return ( - - {props.children} - - ) - } - + if (props.data.name === 'container') { + return ( + {props.children} + ); + } + if (props.data.name === 'section') { + return {props.children}; + } else { + return ( + + {props.children} + + ); + } } - -export default Row \ No newline at end of file +export default Row; diff --git a/src/blocks/layoutsBlocks/revamp/layoutComponent/Text/index.js b/src/blocks/layoutsBlocks/revamp/layoutComponent/Text/index.js index fab881b32..df9628e11 100644 --- a/src/blocks/layoutsBlocks/revamp/layoutComponent/Text/index.js +++ b/src/blocks/layoutsBlocks/revamp/layoutComponent/Text/index.js @@ -1,11 +1,18 @@ -import { Typography } from "@mui/material"; +import { Typography } from '@mui/material'; function Text(props) { - /** - * Fix me! - * the !important in attributes break the styles and disregard it + * Fix me! + * the !important in attributes break the styles and disregard it */ - return {props.data.content}; + return ( + + {props.data.content} + + ); } -export default Text \ No newline at end of file +export default Text; diff --git a/src/components/accounts/instances/lang.js b/src/components/accounts/instances/lang.js index ac11d15cc..5ac609245 100644 --- a/src/components/accounts/instances/lang.js +++ b/src/components/accounts/instances/lang.js @@ -3,6 +3,7 @@ export const lang = { tabs: { '': 'Overview', users: 'Users', + roles: 'Roles & Permissions', teams: 'Teams', domains: 'Domains', usage: 'Usage', diff --git a/src/components/accounts/instances/tabs.js b/src/components/accounts/instances/tabs.js index b52aa842d..734cdc0e7 100644 --- a/src/components/accounts/instances/tabs.js +++ b/src/components/accounts/instances/tabs.js @@ -9,6 +9,7 @@ import SupportAgentIcon from '@mui/icons-material/SupportAgent'; // import SettingsIcon from '@mui/icons-material/Settings'; import CreditCardIcon from '@mui/icons-material/CreditCard'; import LeaderboardIcon from '@mui/icons-material/Leaderboard'; +import AccountBoxRoundedIcon from '@mui/icons-material/AccountBoxRounded'; export const instanceTabs = [ { @@ -17,53 +18,59 @@ export const instanceTabs = [ label: 'Overview', sort: 0, }, + { + icon: , + filename: 'roles', + label: 'Roles & Permissions', + sort: 1, + }, { icon: , filename: 'users', label: 'Users', - sort: 1, + sort: 2, }, { icon: , filename: 'teams', label: 'Teams', - sort: 2, + sort: 3, }, { icon: , filename: 'domains', label: 'Domains', - sort: 3, + sort: 4, }, { icon: , filename: 'usage', label: 'Usage', - sort: 4, + sort: 5, }, { icon: , filename: 'locales', label: 'Locales', - sort: 5, + sort: 6, }, { icon: , filename: 'apis', label: 'APIs & Tokens', - sort: 6, + sort: 7, }, { icon: , filename: 'webhooks', label: 'Webhooks', - sort: 7, + sort: 8, }, { icon: , filename: 'support', label: 'Support', - sort: 8, + sort: 9, }, // comment out for now // { @@ -76,6 +83,6 @@ export const instanceTabs = [ icon: , filename: 'settings', label: 'Settings', - sort: 7, + sort: 10, }, ]; diff --git a/src/components/accounts/roles/BaseRoles.tsx b/src/components/accounts/roles/BaseRoles.tsx new file mode 100644 index 000000000..37b673617 --- /dev/null +++ b/src/components/accounts/roles/BaseRoles.tsx @@ -0,0 +1,146 @@ +import { + Box, + Typography, + List, + ListItem, + ListItemText, + ListItemAvatar, + Avatar, +} from '@mui/material'; +import { + AdminPanelSettingsRounded, + CodeRounded, + RecommendRounded, + BorderColorRounded, + PublicRounded, +} from '@mui/icons-material'; +import { Role } from 'store/types'; + +const BASE_ROLES_CONFIG = Object.freeze({ + owner: { + name: 'Owner', + description: + 'Full access to all sections: Content, Schema, Media, Code, Leads, Redirects, Reports, Apps, and Settings. In Accounts they have full access as well which includes the ability to: launch instances, add domains, invite new users and set their roles, add a team, and create tokens.', + avatar: ( + + + + ), + }, + admin: { + name: 'Admin', + description: + 'Have the same privileges as the Owner role except for deleting other users.', + avatar: ( + + theme.palette.success.dark }} + /> + + ), + }, + 'access admin': { + name: 'Access Admin', + description: + 'Have the same privileges as Admins, except they cannot create, update, delete, or publish content.', + avatar: ( + + theme.palette.pink[600] }} + /> + + ), + }, + developer: { + name: 'Developer', + description: + 'Access to Content, Schema, Media, Code, Leads, Redirects, Reports, Apps, and Setting section.', + avatar: ( + + theme.palette.blue[500] }} /> + + ), + }, + 'developer contributor': { + name: 'Developer Contributor', + description: + 'Have the same privileges as Developers except they cannot delete or publish content.', + avatar: ( + + theme.palette.yellow[500] }} /> + + ), + }, + seo: { + name: 'SEO', + description: + 'Access to Content, Media, Leads, Redirects, Reports, and Apps section.', + avatar: ( + + theme.palette.purple[600] }} /> + + ), + }, + publisher: { + name: 'Publisher', + description: 'Access to Content, Media, Leads, Reports, and Apps section.', + avatar: ( + + theme.palette.pink[600] }} /> + + ), + }, + contributor: { + name: 'Contributor', + description: 'Access to Content, Media, and Apps section.', + avatar: ( + + theme.palette.yellow[500] }} + /> + + ), + }, +}); + +type BaseRolesProps = { + baseRoles: Role[]; +}; +export const BaseRoles = ({ baseRoles }: BaseRolesProps) => { + return ( + + + Base Roles + + + {baseRoles?.map((role, index) => ( + `1px solid ${theme.palette.border}`, + borderRadius: 2, + mb: index + 1 < baseRoles?.length ? 1 : 0, + }} + > + + {BASE_ROLES_CONFIG[role.name.toLowerCase()]?.avatar} + + + {BASE_ROLES_CONFIG[role.name.toLowerCase()]?.name} +
+ } + secondary={ + + {BASE_ROLES_CONFIG[role.name.toLowerCase()]?.description} + + } + /> + + ))} + + + ); +}; diff --git a/src/components/accounts/roles/CreateCustomRoleDialog.tsx b/src/components/accounts/roles/CreateCustomRoleDialog.tsx new file mode 100644 index 000000000..e146e96a8 --- /dev/null +++ b/src/components/accounts/roles/CreateCustomRoleDialog.tsx @@ -0,0 +1,569 @@ +import { useReducer, useState, useMemo } from 'react'; +import { + Typography, + Avatar, + Stack, + Box, + TextField, + Autocomplete, + IconButton, + Dialog, + DialogContent, + DialogActions, + Button, + InputLabel, + Tooltip, +} from '@mui/material'; +import { + LocalPoliceOutlined, + Close, + InfoRounded, + Check, + EditRounded, + ImageRounded, + CodeRounded, + RecentActorsRounded, + BarChartRounded, + HistoryRounded, + SettingsRounded, + ShuffleRounded, + ExtensionRounded, +} from '@mui/icons-material'; +import { LoadingButton } from '@mui/lab'; +import { Database, Block } from '@zesty-io/material'; +import { useRouter } from 'next/router'; + +import { useZestyStore } from 'store'; +import { useRoles } from 'store/roles'; +import { ErrorMsg } from '../ui'; + +export const BASE_ROLE_PERMISSIONS = Object.freeze({ + '31-71cfc74-0wn3r': { + actions: { + create: true, + read: true, + update: true, + delete: true, + publish: true, + grant: true, + super: true, + }, + products: { + content: true, + blocks: true, + schema: true, + media: true, + code: true, + leads: true, + analytics: true, + redirects: true, + activityLog: true, + apps: true, + settings: true, + }, + }, + '31-71cfc74-4dm13': { + actions: { + create: true, + read: true, + update: true, + delete: true, + publish: true, + grant: true, + super: false, + }, + products: { + content: true, + blocks: true, + schema: true, + media: true, + code: true, + leads: true, + analytics: true, + redirects: true, + activityLog: true, + apps: true, + settings: true, + }, + }, + '31-71cfc74-4cc4dm13': { + actions: { + create: false, + read: true, + update: false, + delete: false, + publish: false, + grant: true, + super: true, + }, + products: { + content: true, + blocks: true, + schema: true, + media: true, + code: true, + leads: true, + analytics: true, + redirects: true, + activityLog: true, + apps: true, + settings: true, + }, + }, + '31-71cfc74-d3v3l0p3r': { + actions: { + create: true, + read: true, + update: true, + delete: true, + publish: true, + grant: false, + super: false, + }, + products: { + content: true, + blocks: true, + schema: true, + media: true, + code: true, + leads: true, + analytics: false, + redirects: true, + activityLog: false, + apps: true, + settings: true, + }, + }, + '31-71cfc74-d3vc0n': { + actions: { + create: true, + read: true, + update: true, + delete: false, + publish: false, + grant: false, + super: false, + }, + products: { + content: true, + blocks: true, + schema: true, + media: true, + code: true, + leads: true, + analytics: false, + redirects: true, + activityLog: false, + apps: true, + settings: true, + }, + }, + '31-71cfc74-p0bl1shr': { + actions: { + create: true, + read: true, + update: true, + delete: true, + publish: true, + grant: false, + super: false, + }, + products: { + content: true, + blocks: true, + schema: false, + media: true, + code: false, + leads: true, + analytics: false, + redirects: false, + activityLog: false, + apps: true, + settings: false, + }, + }, + '31-71cfc74-c0ntr1b0t0r': { + actions: { + create: true, + read: true, + update: true, + delete: false, + publish: false, + grant: false, + super: false, + }, + products: { + content: true, + blocks: true, + schema: true, + media: true, + code: true, + leads: true, + analytics: true, + redirects: true, + activityLog: true, + apps: true, + settings: true, + }, + }, + '31-71cfc74-s30': { + actions: { + create: true, + read: true, + update: true, + delete: true, + publish: true, + grant: false, + super: false, + }, + products: { + content: true, + blocks: true, + schema: false, + media: true, + code: false, + leads: true, + analytics: false, + redirects: true, + activityLog: false, + apps: true, + settings: false, + }, + }, +}); +export const PRODUCT_DETAILS = Object.freeze({ + content: { + name: 'Content', + icon: , + }, + blocks: { + name: 'Blocks', + // FIXME: Icon is too small + icon: , + }, + schema: { + name: 'Schema', + icon: , + }, + media: { + name: 'Media', + icon: , + }, + code: { + name: 'Code (Zesty IDE)', + icon: , + }, + leads: { + name: 'Leads', + icon: , + }, + analytics: { + name: 'Analytics', + icon: , + }, + redirects: { + name: 'Redirects', + icon: , + }, + activityLog: { + name: 'Activity Log', + icon: , + }, + apps: { + name: 'Apps', + icon: , + }, + settings: { + name: 'Settings', + icon: , + }, +}); + +export type RoleDetails = { + name: string; + description: string; + systemRoleZUID: string; +}; +type FieldErrors = { + name: string; + description: string; +}; + +type CreateCustomRoleDialogProps = { + onClose: () => void; + onRoleCreated: (ZUID: string) => void; +}; +export const CreateCustomRoleDialog = ({ + onClose, + onRoleCreated, +}: CreateCustomRoleDialogProps) => { + const router = useRouter(); + const { instance } = useZestyStore((state) => state); + const { createRole, getRoles, baseRoles } = useRoles((state) => state); + const [isCreatingRole, setIsCreatingRole] = useState(false); + const [fieldErrors, updateFieldErrors] = useReducer( + (state: FieldErrors, data: Partial) => { + return { + ...state, + ...data, + }; + }, + { name: null, description: null }, + ); + + const [fieldData, updateFieldData] = useReducer( + (state: RoleDetails, data: Partial) => { + return { + ...state, + ...data, + }; + }, + { + name: '', + description: '', + systemRoleZUID: '31-71cfc74-4dm13', + }, + ); + + const baseRoleOptions = useMemo(() => { + if (!baseRoles?.length) return []; + + return baseRoles?.map((role) => ({ + label: role.name, + value: role.systemRoleZUID, + })); + }, [baseRoles]); + + const handleCreateRole = () => { + const instanceZUID = String(router?.query?.zuid); + + if (!fieldData.name) { + updateFieldErrors({ + name: 'Role name is required', + }); + return; + } + + setIsCreatingRole(true); + createRole({ + name: fieldData.name.replace(/[^\w\s\n]/g, ''), + description: fieldData.description.replace(/[^\w\s\n]/g, ''), + systemRoleZUID: fieldData.systemRoleZUID, + instanceZUID, + }) + .then((response: any) => { + getRoles(instanceZUID) + .then(() => { + onRoleCreated(response?.ZUID); + onClose?.(); + }) + .catch(() => ErrorMsg({ title: 'Failed to fetch roles' })); + }) + .catch(() => ErrorMsg({ title: 'Failed to create role' })) + .finally(() => { + setIsCreatingRole(false); + }); + }; + + return ( + onClose?.()} + PaperProps={{ + sx: { + maxWidth: 960, + width: 960, + minHeight: 800, + }, + }} + > + + + + + + + + Create Custom Role + + + Creates a custom role that can have granular permissions applied + to it + + + + onClose?.()}> + + + + + + + Role Name * + + + + + { + updateFieldData({ name: evt.target.value }); + + if (!!evt.target.value) { + updateFieldErrors({ + name: null, + }); + } + }} + placeholder="e.g. Lawyer" + fullWidth + disabled={isCreatingRole} + error={!!fieldErrors?.name} + helperText={fieldErrors?.name} + /> + + + + Role Description + + + + + + updateFieldData({ description: evt.target.value }) + } + placeholder="What is this role going to be used for" + multiline + fullWidth + rows={4} + disabled={isCreatingRole} + /> + + + + Base Role + + + + + role.value === fieldData.systemRoleZUID, + )} + onChange={(_, value) => + updateFieldData({ systemRoleZUID: value.value }) + } + options={baseRoleOptions} + renderInput={(params) => } + disabled={isCreatingRole} + /> + + + + {instance?.name} Base Permissions + + + {Object.entries( + BASE_ROLE_PERMISSIONS[fieldData.systemRoleZUID]?.actions || {}, + )?.map(([name, permission], index) => ( + + + {name} + + {!!permission ? ( + + ) : ( + + )} + + ))} + + + + + + Has access to: + + + {Object.entries( + BASE_ROLE_PERMISSIONS[fieldData.systemRoleZUID]?.products || {}, + )?.map(([product, hasAccess]) => { + if (hasAccess && !!PRODUCT_DETAILS[product]) { + return ( + + {PRODUCT_DETAILS[product]?.icon} + + {PRODUCT_DETAILS[product]?.name} + + + ); + } + })} + + + + + {fieldData.systemRoleZUID === '31-71cfc74-0wn3r' + ? 'Can delete users' + : 'Cannot delete other users'} + + + + + + + + Create + + + + ); +}; diff --git a/src/components/accounts/roles/CustomRoles.tsx b/src/components/accounts/roles/CustomRoles.tsx new file mode 100644 index 000000000..c232fa01a --- /dev/null +++ b/src/components/accounts/roles/CustomRoles.tsx @@ -0,0 +1,148 @@ +import { forwardRef, useState, useImperativeHandle } from 'react'; +import { + List, + ListItemButton, + ListItemText, + ListItemAvatar, + Avatar, + Typography, + Box, + IconButton, + Menu, + MenuItem, + ListItemIcon, +} from '@mui/material'; +import { + LocalPoliceOutlined, + MoreHorizRounded, + EditRounded, + DeleteRounded, +} from '@mui/icons-material'; + +import { EditCustomRoleDialog } from './EditCustomRoleDialog'; +import { DeleteCustomRoleDialog } from './DeleteCustomRoleDialog'; +import { Role } from 'store/types'; + +type CustomRolesProps = { + customRoles: Role[]; +}; +export const CustomRoles = forwardRef( + ({ customRoles }: CustomRolesProps, ref) => { + const [anchorEl, setAnchorEl] = useState(null); + const [ZUIDToEdit, setZUIDToEdit] = useState(null); + const [ZUIDToDelete, setZUIDToDelete] = useState(null); + const [activeZUID, setActiveZUID] = useState(null); + + useImperativeHandle(ref, () => ({ + updateZUIDToEdit: (ZUID: string) => setZUIDToEdit(ZUID), + })); + + return ( + <> + + + Custom Roles + + + {customRoles?.map((role, index) => ( + + `1px solid ${theme.palette.border}`, + borderRadius: 2, + mb: index + 1 < customRoles?.length ? 1 : 0, + }} + onClick={() => { + setZUIDToEdit(role.ZUID); + }} + > + + + + + + + {role.name} + + } + secondary={ + + {role.description || ''} + + } + /> + { + evt.stopPropagation(); + setAnchorEl(evt.currentTarget); + setActiveZUID(role.ZUID); + }} + > + + + + setAnchorEl(null)} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'right', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'right', + }} + > + { + setAnchorEl(null); + setZUIDToEdit(role.ZUID); + }} + > + + + + Edit + + { + setAnchorEl(null); + setZUIDToDelete(role.ZUID); + }} + > + + + + Delete + + + + ))} + + + {!!ZUIDToEdit && ( + setZUIDToEdit(null)} + /> + )} + {!!ZUIDToDelete && ( + setZUIDToDelete(null)} + /> + )} + + ); + }, +); diff --git a/src/components/accounts/roles/DeleteCustomRoleDialog.tsx b/src/components/accounts/roles/DeleteCustomRoleDialog.tsx new file mode 100644 index 000000000..849cd2c00 --- /dev/null +++ b/src/components/accounts/roles/DeleteCustomRoleDialog.tsx @@ -0,0 +1,128 @@ +import { useMemo, useState } from 'react'; +import { + Avatar, + Box, + Dialog, + DialogContent, + DialogActions, + Button, + TextField, + Typography, + InputLabel, + Autocomplete, +} from '@mui/material'; +import { DeleteRounded } from '@mui/icons-material'; +import { LoadingButton } from '@mui/lab'; +import { useRouter } from 'next/router'; + +import { useRoles } from 'store/roles'; +import { ErrorMsg } from '../ui'; + +type DeleteCustomRoleDialogProps = { + ZUID: string; + onClose: () => void; +}; +export const DeleteCustomRoleDialog = ({ + ZUID, + onClose, +}: DeleteCustomRoleDialogProps) => { + const router = useRouter(); + const { customRoles, baseRoles, deleteRole, getUsersWithRoles, getRoles } = + useRoles((state) => state); + const [isDeleting, setIsDeleting] = useState(false); + + const roleData = useMemo(() => { + return customRoles?.find((role) => role.ZUID === ZUID); + }, [ZUID, customRoles]); + + const roleOptions = useMemo(() => { + const customRolesOpts = customRoles + ?.filter((role) => role.ZUID !== ZUID) + ?.map((role) => ({ + label: role.name, + value: role.ZUID, + })); + const baseRolesOpts = baseRoles?.map((role) => ({ + label: role.name, + value: role.ZUID, + })); + + return [...customRolesOpts, ...baseRolesOpts]; + }, [customRoles, baseRoles]); + + const defaultBaseRole = baseRoles?.find( + (role) => role.systemRoleZUID === roleData?.systemRoleZUID, + ); + + const [selectedTransferRole, setSelectedTransferRole] = useState<{ + label: string; + value: string; + }>({ + label: defaultBaseRole?.name, + value: defaultBaseRole?.ZUID, + }); + + const { zuid: instanceZUID } = router.query; + + const handleConfirmDelete = () => { + setIsDeleting(true); + deleteRole({ + roleZUIDToDelete: ZUID, + roleZUIDToTransferUsers: selectedTransferRole?.value, + }) + .catch(() => ErrorMsg({ title: 'Failed to delete role' })) + .finally(() => { + setIsDeleting(false); + getRoles(String(instanceZUID)); + getUsersWithRoles(String(instanceZUID)); + onClose(); + }); + }; + + return ( + onClose?.()} + PaperProps={{ sx: { width: 480 } }} + > + + + + + + Delete Custom Role:{' '} + + {roleData?.name || ''} + + + + This role and its permissions will be immediately deactivated.
+ Please reassign users currently assigned to this role to a new role. +
+
+ + Role to reassign users to + setSelectedTransferRole(value)} + renderInput={(params) => } + /> + + + + + Delete Custom Role + + +
+ ); +}; diff --git a/src/components/accounts/roles/EditCustomRoleDialog/index.tsx b/src/components/accounts/roles/EditCustomRoleDialog/index.tsx new file mode 100644 index 000000000..cf1607fdd --- /dev/null +++ b/src/components/accounts/roles/EditCustomRoleDialog/index.tsx @@ -0,0 +1,492 @@ +import { useMemo, useState, useReducer, useEffect } from 'react'; +import { + Typography, + Avatar, + Stack, + Box, + IconButton, + Dialog, + DialogContent, + DialogActions, + Button, + Tabs, + Tab, +} from '@mui/material'; +import { LoadingButton } from '@mui/lab'; +import { + LocalPoliceOutlined, + Close, + InfoRounded, + RuleRounded, + GroupsRounded, +} from '@mui/icons-material'; +import { useRouter } from 'next/router'; + +import { useRoles } from 'store/roles'; +import { Details } from './tabs/Details'; +import { Permissions } from './tabs/Permissions'; +import { Users } from './tabs/Users'; +import { GranularRole } from 'store/types'; +import { useZestyStore } from 'store'; +import { ErrorMsg } from 'components/accounts/ui'; + +type FieldErrors = { + detailsTab: { + roleName: string; + roleDescription: string; + }; + permissionsTab: string[]; + usersTab: string[]; +}; +export type RoleDetails = { + name: string; + description: string; + systemRoleZUID: string; +}; + +type EditCustomRoleDialogProps = { + ZUID: string; + onClose: () => void; +}; +export const EditCustomRoleDialog = ({ + ZUID, + onClose, +}: EditCustomRoleDialogProps) => { + const router = useRouter(); + const { ZestyAPI } = useZestyStore((state: any) => state); + const { + getRoles, + customRoles, + baseRoles, + updateGranularRole, + updateRole, + createGranularRole, + deleteGranularRole, + usersWithRoles, + updateUserRole, + getUsersWithRoles, + } = useRoles((state) => state); + const [activeTab, setActiveTab] = useState< + 'details' | 'permissions' | 'users' + >('details'); + const [isSaving, setIsSaving] = useState(false); + const [fieldErrors, updateFieldErrors] = useReducer( + ( + state: FieldErrors, + action: { + tab: keyof FieldErrors; + data: Partial; + }, + ) => { + return { + ...state, + [action.tab]: { + ...state[action.tab], + ...action.data, + }, + }; + }, + { + detailsTab: { + roleName: null, + roleDescription: null, + }, + permissionsTab: [], + usersTab: [], + }, + ); + + const roleUsers = useMemo(() => { + if (!usersWithRoles?.length) return []; + + return usersWithRoles?.filter((user) => user.role?.ZUID === ZUID); + }, [usersWithRoles, customRoles]); + const [userEmails, setUserEmails] = useState( + roleUsers?.map((user) => user.email) || [], + ); + + const { zuid: instanceZUID } = router.query; + + const roleData = customRoles?.find((role) => role.ZUID === ZUID); + const [granularRoles, setGranularRoles] = useState[]>( + [], + ); + const [resourceZUIDsToDelete, setResourceZUIDsToDelete] = useState( + [], + ); + + useEffect(() => { + setUserEmails(roleUsers?.map((user) => user.email) || []); + }, [roleUsers]); + + const [detailsData, updateDetailsData] = useReducer( + (state: RoleDetails, data: Partial) => { + return { + ...state, + ...data, + }; + }, + { + name: roleData?.name || '', + description: roleData?.description || '', + systemRoleZUID: roleData?.systemRoleZUID || '31-71cfc74-4dm13', + }, + ); + + useEffect(() => { + if (!ZUID) return; + + getPermissions(ZUID); + }, [ZUID]); + + const getPermissions = async (ZUID: string) => { + const res = await ZestyAPI.getAllGranularRoles(ZUID); + + if (res.error) { + ErrorMsg({ text: res.error }); + setGranularRoles([]); + } else { + setGranularRoles(res.data); + } + }; + + const saveGranularRoleUpdates = async () => { + const granularRolesClone: GranularRole[] = JSON.parse( + JSON.stringify(granularRoles || []), + ); + const payload = granularRolesClone?.map((role) => ({ + resourceZUID: role.resourceZUID, + create: role.create, + read: role.read, + update: role.update, + delete: role.delete, + publish: role.publish, + name: '', + })); + + if (!!roleData?.granularRoleZUID) { + // If a granularRoleZUID is already attached to the role, we can just + // do an update to add the new granular roles + if (!!payload?.length) { + return updateGranularRole({ + roleZUID: ZUID, + granularRoles: payload, + }); + } + } else { + // If the role doesn't have any granularRoleZUID attached, we need to create a + // granular role first + const granularRoleInitiator = payload?.[0]; + + if (granularRoleInitiator) { + return createGranularRole({ + roleZUID: ZUID, + data: granularRoleInitiator, + }).then(() => { + // If there are any other granular roles aside from the one we used to + // initiate a new granular role zuid, we then use the update endpoint to + // add those in as well + if (payload?.length > 1) { + return updateGranularRole({ + roleZUID: ZUID, + granularRoles: payload, + }); + } + }); + } + } + }; + + const saveUsersUpdate = () => { + const baseRoleZUID = baseRoles?.find( + (role) => role.systemRoleZUID === roleData?.systemRoleZUID, + )?.ZUID; + const alreadyExistingUsers: string[] = roleUsers?.reduce( + (prev, curr) => [...prev, curr.email], + [], + ); + const usersToRemove = alreadyExistingUsers?.filter( + (email) => !userEmails?.includes(email), + ); + const usersToAdd = userEmails?.filter( + (email) => !alreadyExistingUsers.includes(email), + ); + + // TODO: Do something with emails that are not yet instance members + const users = usersWithRoles?.reduce( + (prev, curr) => { + if (usersToAdd?.includes(curr.email)) { + return { + ...prev, + toAdd: [ + ...prev.toAdd, + { + userZUID: curr.ZUID, + oldRoleZUID: curr.role?.ZUID, + newRoleZUID: ZUID, + }, + ], + }; + } + + if (usersToRemove?.includes(curr.email)) { + return { + ...prev, + toRemove: [ + ...prev.toRemove, + { + userZUID: curr.ZUID, + oldRoleZUID: curr.role?.ZUID, + newRoleZUID: baseRoleZUID, + }, + ], + }; + } + + return prev; + }, + { + toAdd: [], + toRemove: [], + }, + ); + + return Promise.all([ + updateUserRole(users.toAdd), + updateUserRole(users.toRemove), + ]); + }; + + const saveDetailsUpdate = () => { + if (!detailsData?.name?.trim()) { + updateFieldErrors({ + tab: 'detailsTab', + data: { + roleName: 'Role name is required', + }, + }); + } else { + return updateRole({ + roleZUID: ZUID, + name: detailsData.name?.replace(/[^\w\s\n]/g, ''), + description: detailsData.description?.replace(/[^\w\s\n]/g, ''), + systemRoleZUID: detailsData.systemRoleZUID, + }); + } + }; + + const handleSave = () => { + setIsSaving(true); + + Promise.all([ + // Update role details + saveDetailsUpdate(), + // Delete a granular role if there's any to delete + ...(!!resourceZUIDsToDelete && [ + deleteGranularRole({ + roleZUID: ZUID, + resourceZUIDs: resourceZUIDsToDelete, + }), + ]), + // Perform all granular role updates + saveGranularRoleUpdates(), + // Save all user updates + saveUsersUpdate(), + ]) + .then((responses) => console.log(responses)) + .catch(() => ErrorMsg({ title: 'Failed to update role' })) + .finally(() => { + getUsersWithRoles(String(instanceZUID)); + getRoles(String(instanceZUID)); + getPermissions(ZUID); + setIsSaving(false); + setResourceZUIDsToDelete([]); + + // Navigate to the tab if that tab has errors + if ( + !!fieldErrors?.detailsTab?.roleName || + !!fieldErrors?.detailsTab?.roleDescription + ) { + setActiveTab('details'); + } else if (!!fieldErrors?.permissionsTab?.length) { + setActiveTab('permissions'); + } else if (!!fieldErrors?.usersTab?.length) { + setActiveTab('users'); + } + }); + }; + + return ( + onClose?.()} + PaperProps={{ + sx: { + maxWidth: 960, + width: 960, + minHeight: 800, + }, + }} + > + + + + + + + + + Edit {roleData?.name} + + + Edit your custom role that can have granular permissions applied + to it + + + + onClose?.()}> + + + + setActiveTab(value)} + sx={{ + position: 'relative', + top: '2px', + px: 2.5, + }} + > + } + iconPosition="start" + /> + } + iconPosition="start" + /> + } + iconPosition="start" + /> + + + + {activeTab === 'details' && ( +
{ + updateDetailsData(data); + + if (data?.name?.length) { + updateFieldErrors({ + tab: 'detailsTab', + data: { + roleName: null, + }, + }); + } + }} + errors={fieldErrors.detailsTab} + /> + )} + {activeTab === 'permissions' && ( + { + setGranularRoles((prev) => [...prev, newRoleData]); + }} + onDeleteGranularRole={(resourceZUID) => { + setResourceZUIDsToDelete((prevState) => { + try { + const resourceZUIDsToDeleteClone = JSON.parse( + JSON.stringify(prevState), + ); + resourceZUIDsToDeleteClone.push(resourceZUID); + + return Array.from(new Set(resourceZUIDsToDeleteClone)); + } catch (err) { + console.error(err); + } + }); + setGranularRoles( + (prevState) => + prevState?.filter( + (role) => role.resourceZUID !== resourceZUID, + ), + ); + }} + onUpdateGranularRole={(updatedRoleData) => { + setGranularRoles((prevState) => { + try { + const granularRolesClone: GranularRole[] = JSON.parse( + JSON.stringify(prevState), + ); + const index = granularRolesClone?.findIndex( + (role) => + role.resourceZUID === updatedRoleData?.resourceZUID, + ); + + if (index === -1) return prevState; + + granularRolesClone[index] = { + ...granularRolesClone[index], + ...updatedRoleData, + }; + + return granularRolesClone; + } catch (err) { + console.error(err); + } + }); + }} + /> + )} + {activeTab === 'users' && ( + setUserEmails(emails)} + /> + )} + + + + + Save + + +
+ ); +}; diff --git a/src/components/accounts/roles/EditCustomRoleDialog/tabs/Details.tsx b/src/components/accounts/roles/EditCustomRoleDialog/tabs/Details.tsx new file mode 100644 index 000000000..b84923763 --- /dev/null +++ b/src/components/accounts/roles/EditCustomRoleDialog/tabs/Details.tsx @@ -0,0 +1,161 @@ +import { useMemo } from 'react'; +import { + Typography, + Stack, + Box, + TextField, + Autocomplete, + InputLabel, + Tooltip, +} from '@mui/material'; +import { Close, InfoRounded, Check } from '@mui/icons-material'; + +import { useZestyStore } from 'store'; +import { + BASE_ROLE_PERMISSIONS, + PRODUCT_DETAILS, +} from '../../CreateCustomRoleDialog'; +import { RoleDetails } from '../index'; +import { useRoles } from 'store/roles'; + +type DetailsProps = { + data: RoleDetails; + onUpdateData: (data: Partial) => void; + errors: { + roleName: string; + roleDescription: string; + }; +}; +export const Details = ({ data, onUpdateData, errors }: DetailsProps) => { + const { instance } = useZestyStore((state) => state); + const { baseRoles } = useRoles((state) => state); + + const baseRoleOptions = useMemo(() => { + if (!baseRoles?.length) return []; + + return baseRoles?.map((role) => ({ + label: role.name, + value: role.systemRoleZUID, + })); + }, [baseRoles]); + + return ( + + + + Role Name * + + + + + onUpdateData({ name: evt.target.value })} + placeholder="e.g. Lawyer" + fullWidth + error={!!errors?.roleName} + helperText={errors?.roleName} + /> + + + + Role Description + + + + + onUpdateData({ description: evt.target.value })} + placeholder="What is this role going to be used for" + multiline + fullWidth + rows={4} + error={!!errors?.roleDescription} + helperText={errors?.roleDescription} + /> + + + + Base Role + + + + + role.value === data?.systemRoleZUID, + )} + onChange={(_, value) => onUpdateData({ systemRoleZUID: value.value })} + options={baseRoleOptions} + renderInput={(params) => } + /> + + + + {instance?.name} Base Permissions + + + {Object.entries( + BASE_ROLE_PERMISSIONS[data?.systemRoleZUID]?.actions || {}, + )?.map(([name, permission]) => ( + + + {name} + + {!!permission ? ( + + ) : ( + + )} + + ))} + + + + + + Has access to: + + + {Object.entries( + BASE_ROLE_PERMISSIONS[data?.systemRoleZUID]?.products || {}, + )?.map(([product, hasAccess]) => { + if (hasAccess && !!PRODUCT_DETAILS[product]) { + return ( + + {PRODUCT_DETAILS[product]?.icon} + + {PRODUCT_DETAILS[product]?.name} + + + ); + } + })} + + + + + {data?.systemRoleZUID === '31-71cfc74-0wn3r' + ? 'Can delete users' + : 'Cannot delete other users'} + + + + + ); +}; diff --git a/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/AddRule.tsx b/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/AddRule.tsx new file mode 100644 index 000000000..ee09668e2 --- /dev/null +++ b/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/AddRule.tsx @@ -0,0 +1,194 @@ +import { Box, Stack, Typography, Checkbox, Button } from '@mui/material'; +import { Check } from '@mui/icons-material'; +import dynamic from 'next/dynamic'; +import { useMemo, useReducer } from 'react'; + +import { NewGranularRole } from './index'; +import { ResourceSelector } from './ResourceSelector'; +import { GranularRole } from 'store/types'; + +const DataGrid = dynamic(() => + import('@mui/x-data-grid').then((e) => e.DataGrid), +); + +type AddRuleProps = { + onAddRuleClick: (data: NewGranularRole) => void; + onCancel: () => void; + granularRoles: Partial[]; +}; +export const AddRule = ({ + onAddRuleClick, + onCancel, + granularRoles, +}: AddRuleProps) => { + const [ruleData, updateRuleData] = useReducer( + (state: NewGranularRole, data: Partial) => { + return { + ...state, + ...data, + }; + }, + { + resourceZUID: '', + create: false, + read: false, + update: false, + delete: false, + publish: false, + }, + ); + + const resourcesToFilter = granularRoles?.map((role) => role.resourceZUID); + + const COLUMNS = useMemo( + () => [ + { + field: 'resourceZUID', + headerName: 'Resource Name', + width: 380, + sortable: false, + renderCell: () => ( + + updateRuleData({ + resourceZUID: zuid, + }) + } + resourcesToFilter={resourcesToFilter} + /> + ), + }, + { + field: 'create', + headerName: 'Create', + sortable: false, + renderCell: () => ( + updateRuleData({ create: evt.target.checked })} + /> + ), + }, + { + field: 'read', + headerName: 'Read', + sortable: false, + renderCell: () => ( + updateRuleData({ read: evt.target.checked })} + /> + ), + }, + { + field: 'update', + headerName: 'Update', + sortable: false, + renderCell: () => ( + updateRuleData({ update: evt.target.checked })} + /> + ), + }, + { + field: 'delete', + headerName: 'Delete', + sortable: false, + renderCell: () => ( + updateRuleData({ delete: evt.target.checked })} + /> + ), + }, + { + field: 'publish', + headerName: 'Publish', + sortable: false, + renderCell: () => ( + updateRuleData({ publish: evt.target.checked })} + /> + ), + }, + ], + [], + ); + const ROWS = useMemo( + () => [ + { + id: 1, + resourceZUID: '', + create: false, + read: false, + update: false, + delete: false, + publish: false, + }, + ], + [], + ); + + return ( + + + Add Rule + + + Assign specific permissions (create, read, update, delete, publish) for + the items of any model + + + + + + + + ); +}; diff --git a/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/Loading.tsx b/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/Loading.tsx new file mode 100644 index 000000000..0387d77f3 --- /dev/null +++ b/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/Loading.tsx @@ -0,0 +1,44 @@ +import { Box, Stack, Typography, Skeleton } from '@mui/material'; + +export const Loading = () => { + return ( + + + + Resource Name + + + Create + + + Read + + + Update + + + Delete + + + Publish + + + + + + + + + + + ); +}; diff --git a/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/NoFilterMatches.tsx b/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/NoFilterMatches.tsx new file mode 100644 index 000000000..55a3bf40a --- /dev/null +++ b/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/NoFilterMatches.tsx @@ -0,0 +1,60 @@ +import { Typography, Stack, Box } from '@mui/material'; +import { NoSearchResults } from 'components/accounts/ui/NoSearchResults'; + +type NoFilterMatchesProps = { + keyword: string; + onSearchAgain: () => void; +}; +export const NoFilterMatches = ({ + keyword, + onSearchAgain, +}: NoFilterMatchesProps) => { + return ( + + + + Resource Name + + + Create + + + Read + + + Update + + + Delete + + + Publish + + + + + + + + ); +}; diff --git a/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/NoRules.tsx b/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/NoRules.tsx new file mode 100644 index 000000000..0d445c708 --- /dev/null +++ b/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/NoRules.tsx @@ -0,0 +1,88 @@ +import { Stack, Typography, Box, Button } from '@mui/material'; +import { AddRounded } from '@mui/icons-material'; + +import dataTable from '../../../../../../../public/assets/images/data_table.svg'; + +type NoRulesProps = { + onAddRulesClick: () => void; +}; +export const NoRules = ({ onAddRulesClick }: NoRulesProps) => { + return ( + + + + Resource Name + + + Create + + + Read + + + Update + + + Delete + + + Publish + + + + + + + Add Rules + + + Assign specific rules (create, read, update, delete, publish) for any + resource + + + + + ); +}; diff --git a/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/ResourceSelector.tsx b/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/ResourceSelector.tsx new file mode 100644 index 000000000..a32a071d6 --- /dev/null +++ b/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/ResourceSelector.tsx @@ -0,0 +1,138 @@ +import { useMemo, forwardRef, useState } from 'react'; +import { + TextField, + Autocomplete, + ListItem, + ListItemIcon, + ListItemText, + InputAdornment, +} from '@mui/material'; +import { EditRounded } from '@mui/icons-material'; +import { Database } from '@zesty-io/material'; + +import { useInstance } from 'store/instance'; +import { VirtualizedList } from 'components/accounts/ui/VirtualizedList'; +import { ContentItem } from 'store/types'; + +const VirtualizedListComp = (defaultprops: any, ref) => { + return ; +}; + +const getLangCode = (content: ContentItem) => { + if (!content || !Object.keys(content)?.length) { + return ''; + } + + const { languages } = useInstance.getState(); + + return languages?.find((lang) => lang.ID === content?.meta?.langID)?.code; +}; + +type ResourceSelectorProps = { + onChange: (zuid: string) => void; + initialValue?: string; + resourcesToFilter: string[]; +}; +export const ResourceSelector = ({ + onChange, + initialValue, + resourcesToFilter, +}: ResourceSelectorProps) => { + const { instanceModels, instanceContentItems } = useInstance( + (state) => state, + ); + + const options = useMemo(() => { + if (!instanceModels?.length && !instanceContentItems?.length) return []; + + const models = instanceModels?.map((model) => ({ + label: model.label, + value: model.ZUID, + type: 'model', + sortText: model.label, + })); + const items = instanceContentItems?.map((item) => { + const label = item?.web?.metaTitle || 'Missing Meta Title'; + + return { + label: getLangCode(item) ? `(${getLangCode(item)}) ${label}` : label, + value: item?.meta?.ZUID, + type: 'item', + sortText: label, + }; + }); + + return [...models, ...items].sort( + (a, b) => a?.sortText?.localeCompare(b?.sortText), + ); + }, [instanceModels, instanceContentItems]); + + const filteredOptions = useMemo(() => { + return options?.filter( + (option) => !resourcesToFilter.includes(option.value), + ); + }, [options, resourcesToFilter]); + + const [value, setValue] = useState( + options?.find((option) => option.value === initialValue), + ); + + return ( + ( + + {value?.value?.startsWith('6-') && } + {value?.value?.startsWith('7-') && } + + ), + }} + placeholder="Select Resource" + /> + )} + renderOption={(props, option) => { + return ( + + + {option.type === 'model' ? : } + + + {option.label} + + + ); + }} + onChange={(_, value) => { + onChange(value?.value || ''); + setValue(value); + }} + ListboxComponent={forwardRef(VirtualizedListComp)} + onKeyDown={(evt) => { + evt.stopPropagation(); + }} + /> + ); +}; diff --git a/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/Table.tsx b/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/Table.tsx new file mode 100644 index 000000000..2981ba554 --- /dev/null +++ b/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/Table.tsx @@ -0,0 +1,217 @@ +import { useMemo } from 'react'; +import dynamic from 'next/dynamic'; +import { Checkbox, Typography, IconButton, Tooltip } from '@mui/material'; +import { Database } from '@zesty-io/material'; +import { EditRounded, InfoRounded, DeleteRounded } from '@mui/icons-material'; +import { GridRenderCellParams, GridValueGetterParams } from '@mui/x-data-grid'; + +import { GranularRoleWithResourceName, UpdateGranularRole } from './index'; +import { useInstance } from 'store/instance'; + +const DataGrid = dynamic(() => + import('@mui/x-data-grid').then((e) => e.DataGrid), +); + +type TableProps = { + granularRoles: Partial[]; + onDataChange: (roleData: UpdateGranularRole) => void; + onDelete: (resourceZUID: string) => void; +}; +export const Table = ({ + granularRoles, + onDataChange, + onDelete, +}: TableProps) => { + const { instanceModels, instanceContentItems } = useInstance( + (state) => state, + ); + + const COLUMNS = useMemo( + () => [ + { + field: 'resourceName', + headerName: 'Resource Name', + width: 300, + sortable: false, + renderCell: (params: GridValueGetterParams) => ( + <> + {params.row.id?.startsWith('6-') ? ( + + ) : ( + + )} + + {params.value} + + + ), + }, + { + field: 'create', + headerName: 'Create', + sortable: false, + renderCell: (params: GridRenderCellParams) => ( + + onDataChange({ + resourceZUID: params.row?.resourceZUID, + create: evt.target.checked, + }) + } + /> + ), + }, + { + field: 'read', + headerName: 'Read', + sortable: false, + renderCell: (params: GridRenderCellParams) => ( + + onDataChange({ + resourceZUID: params.row?.resourceZUID, + read: evt.target.checked, + }) + } + /> + ), + }, + { + field: 'update', + headerName: 'Update', + sortable: false, + renderCell: (params: GridRenderCellParams) => ( + + onDataChange({ + resourceZUID: params.row?.resourceZUID, + update: evt.target.checked, + }) + } + /> + ), + }, + { + field: 'delete', + headerName: 'Delete', + sortable: false, + renderCell: (params: GridRenderCellParams) => ( + + onDataChange({ + resourceZUID: params.row?.resourceZUID, + delete: evt.target.checked, + }) + } + /> + ), + }, + { + field: 'publish', + headerName: 'Publish', + sortable: false, + renderCell: (params: GridRenderCellParams) => ( + + onDataChange({ + resourceZUID: params.row?.resourceZUID, + publish: evt.target.checked, + }) + } + /> + ), + }, + { + field: 'actions', + headerName: '', + sortable: false, + renderCell: (params: GridRenderCellParams) => { + const title = `Name: ${params.row?.resourceName} \n ${ + params.value?.startsWith('6-') + ? 'Model' + : !!params.value?.startsWith('7-') + ? 'Item' + : '' + } ZUID: ${params.value}`; + + return ( + <> + + + + onDelete(params.value)} + > + + + + ); + }, + }, + ], + [instanceModels, instanceContentItems], + ); + const rows = granularRoles?.map((role) => { + return { + id: role.resourceZUID, + resourceName: role.resourceName, + create: role.create, + read: role.read, + update: role.update, + delete: role.delete, + publish: role.publish, + actions: role.resourceZUID, + }; + }); + + return ( + + ); +}; diff --git a/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/index.tsx b/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/index.tsx new file mode 100644 index 000000000..bc083ba8f --- /dev/null +++ b/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/index.tsx @@ -0,0 +1,164 @@ +import { useRef, useState, useDeferredValue, useMemo, useEffect } from 'react'; +import { + Box, + Stack, + Typography, + TextField, + Button, + InputAdornment, +} from '@mui/material'; +import { Search, AddRounded } from '@mui/icons-material'; + +import { NoRules } from './NoRules'; +import { GranularRole } from 'store/types'; +import { AddRule } from './AddRule'; +import { Table } from './Table'; +import { useInstance } from 'store/instance'; +import { NoFilterMatches } from './NoFilterMatches'; + +export type GranularRoleWithResourceName = GranularRole & { + resourceName: string; +}; +export type UpdateGranularRole = Pick & + Partial>; +export type NewGranularRole = Pick< + GranularRole, + 'resourceZUID' | 'create' | 'read' | 'update' | 'delete' | 'publish' +>; +type PermissionsProps = { + granularRoles: Partial[]; + onAddNewGranularRole: (roleData: NewGranularRole) => void; + onUpdateGranularRole: (roleData: UpdateGranularRole) => void; + onDeleteGranularRole: (resourceZUID: string) => void; +}; +export const Permissions = ({ + granularRoles, + onAddNewGranularRole, + onUpdateGranularRole, + onDeleteGranularRole, +}: PermissionsProps) => { + const { instanceModels, instanceContentItems, languages } = useInstance( + (state) => state, + ); + const [filterKeyword, setFilterKeyword] = useState(''); + const [showAddRule, setShowAddRule] = useState(false); + const deferredFilterKeyword = useDeferredValue(filterKeyword); + const searchFieldRef = useRef(null); + const addGranularRoleRef = useRef(null); + + const resolveResourceZUID = (zuid: string) => { + if (zuid?.startsWith('6-')) { + return ( + instanceModels?.find((model) => model.ZUID === zuid)?.label || zuid + ); + } else if (zuid?.startsWith('7-')) { + const contentItem = instanceContentItems?.find( + (item) => item.meta.ZUID === zuid, + ); + const name = contentItem?.web?.metaTitle || zuid; + const langCode = languages?.find( + (lang) => lang.ID === contentItem?.meta?.langID, + )?.code; + + return langCode ? `(${langCode}) ${name}` : name; + } else { + return zuid; + } + }; + + const granularRolesWithResourceNames = useMemo(() => { + return granularRoles?.map((role) => ({ + ...role, + resourceName: resolveResourceZUID(role.resourceZUID), + })); + }, [granularRoles]); + + const filteredGranularRoles = useMemo(() => { + if (!deferredFilterKeyword) return granularRolesWithResourceNames; + + return granularRolesWithResourceNames?.filter( + (role) => + role.resourceName + ?.toLowerCase() + ?.includes(deferredFilterKeyword.toLowerCase()), + ); + }, [granularRolesWithResourceNames, deferredFilterKeyword]); + + useEffect(() => { + if (showAddRule) { + setTimeout(() => { + addGranularRoleRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, 300); + } + }, [showAddRule, addGranularRoleRef]); + + return ( + + + + + Resource Permissions + + + Grant users access only to resources you specify + + + + setFilterKeyword(evt.target.value)} + size="small" + placeholder="Filter Resources" + InputProps={{ + startAdornment: ( + + + + ), + }} + /> + + + + {!granularRoles?.length && !showAddRule && !deferredFilterKeyword && ( + setShowAddRule(true)} /> + )} + {!!filteredGranularRoles?.length && ( + onUpdateGranularRole(roleData)} + onDelete={onDeleteGranularRole} + /> + )} + {!filteredGranularRoles?.length && !!deferredFilterKeyword && ( + { + setFilterKeyword(''); + searchFieldRef.current?.querySelector('input')?.focus(); + }} + /> + )} + {showAddRule && ( + + setShowAddRule(false)} + onAddRuleClick={(newRoleData) => { + onAddNewGranularRole(newRoleData); + setShowAddRule(false); + }} + granularRoles={granularRoles} + /> + + )} + + ); +}; diff --git a/src/components/accounts/roles/EditCustomRoleDialog/tabs/Users.tsx b/src/components/accounts/roles/EditCustomRoleDialog/tabs/Users.tsx new file mode 100644 index 000000000..0a2ba0b1b --- /dev/null +++ b/src/components/accounts/roles/EditCustomRoleDialog/tabs/Users.tsx @@ -0,0 +1,189 @@ +import { useState, useRef, useMemo } from 'react'; +import { + Chip, + Box, + Stack, + InputLabel, + Tooltip, + TextField, + Autocomplete, + Typography, +} from '@mui/material'; +import { InfoRounded } from '@mui/icons-material'; +import { useRoles } from 'store/roles'; + +const emailAddressRegexp = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + +type UsersProps = { + userEmails: string[]; + onUpdateUserEmails: (emails: string[]) => void; +}; +export const Users = ({ userEmails, onUpdateUserEmails }: UsersProps) => { + const { usersWithRoles } = useRoles((state) => state); + const [inputValue, setInputValue] = useState(''); + const [emailError, setEmailError] = useState(false); + const emailChipsRef = useRef([]); + const autocompleteRef = useRef(null); + + const nonInstanceMembers = useMemo(() => { + if (!usersWithRoles?.length || !userEmails?.length) return []; + const instanceUserEmails = usersWithRoles?.map((user) => user.email); + + return userEmails?.filter((email) => !instanceUserEmails?.includes(email)); + }, [userEmails, usersWithRoles]); + + return ( + + + Users + + + + + ( + { + setEmailError(false); + if ( + event.key === 'Enter' || + event.key === ',' || + event.key === ' ' + ) { + if (inputValue && !inputValue.match(/^\s+$/)) { + if (inputValue.trim().match(emailAddressRegexp)) { + event.preventDefault(); + onUpdateUserEmails( + Array.from( + new Set([ + ...userEmails, + inputValue?.toLowerCase()?.trim(), + ]), + ), + ); + setInputValue(''); + } else { + setEmailError(true); + } + } else { + setEmailError(false); + } + } + + if (event.key === 'Backspace' && !inputValue) { + event.stopPropagation(); + + // HACK: Needed to prevent the default behavior of autocomplete which autodeletes the right most tag on backspace + setTimeout(() => { + emailChipsRef.current?.[ + emailChipsRef.current?.filter((ref) => !!ref)?.length - 1 + ]?.focus({ visible: true }); + }, 100); + } + }} + onChange={(event) => { + if (event.target.value?.split('').pop() === ',') return; + + // Handle pasted value if it contains comma or space separated emails + const pastedValue = event.target?.value.replaceAll(',', ' '); + + if (pastedValue.includes(' ')) { + event.preventDefault(); + const validEmails: string[] = []; + const invalidEmails: string[] = []; + + pastedValue.split(' ').forEach((email) => { + if (email) { + if (email.match(emailAddressRegexp)) { + validEmails.push(email); + } else { + invalidEmails.push(email); + } + } + }); + + onUpdateUserEmails( + Array.from(new Set([...userEmails, ...validEmails])), + ); + + if (invalidEmails?.length) { + setEmailError(true); + setInputValue(invalidEmails.join(', ')); + } + } else { + setInputValue(event.target.value); + } + }} + value={inputValue} + /> + )} + renderTags={(tagValue, getTagProps) => + userEmails.map((email, index) => ( + (emailChipsRef.current[index] = el)} + size="small" + color="default" + clickable={false} + sx={{ + backgroundColor: 'common.white', + borderColor: 'grey.300', + borderWidth: 1, + borderStyle: 'solid', + }} + label={email} + onDelete={(evt) => { + if (evt.type === 'click') { + onUpdateUserEmails(userEmails.filter((_, i) => i !== index)); + } + }} + onKeyDown={(event) => { + if (event.key === 'Backspace') { + // HACK: Needed to override the default behavior of autocomplete where it automatically selects the next tag after deleting a diff tag via backspace + setTimeout(() => { + onUpdateUserEmails( + userEmails.filter((_, i) => i !== index), + ); + autocompleteRef.current?.querySelector('input')?.focus(); + }, 150); + } + }} + /> + )) + } + /> + {!!nonInstanceMembers?.length && ( + + These users are not part of this instance:{' '} + {nonInstanceMembers?.join(', ')}. To assign them a custom role, first + invite them with a system role. Once added, you can return to assign + them a custom role. + + )} + + ); +}; diff --git a/src/components/accounts/roles/NoCustomRoles.tsx b/src/components/accounts/roles/NoCustomRoles.tsx new file mode 100644 index 000000000..bef4d03e9 --- /dev/null +++ b/src/components/accounts/roles/NoCustomRoles.tsx @@ -0,0 +1,55 @@ +import { Box, Stack, Typography, Button } from '@mui/material'; +import { Add } from '@mui/icons-material'; + +import addUser from '../../../../public/assets/images/add_user.svg'; + +type NoCustomRolesProps = { + onCreateCustomRoleClick: () => void; +}; +export const NoCustomRoles = ({ + onCreateCustomRoleClick, +}: NoCustomRolesProps) => { + return ( + + + Custom Roles + + + + + + + Create your first Custom Role + + + Custom roles allow you to tailor permissions to fit specific needs. + Click the button below to define a role name, set permissions, and + assign users. + + + + + + ); +}; diff --git a/src/components/accounts/ui/NoSearchResults.tsx b/src/components/accounts/ui/NoSearchResults.tsx new file mode 100644 index 000000000..56777302b --- /dev/null +++ b/src/components/accounts/ui/NoSearchResults.tsx @@ -0,0 +1,56 @@ +import { Stack, Typography, Button, Box } from '@mui/material'; +import { SearchRounded } from '@mui/icons-material'; + +import noSearchResults from '../../../../public/assets/images/no_search_results.svg'; + +type NoSearchResultsProps = { + onSearchAgain: () => void; + keyword: string; +}; +export const NoSearchResults = ({ + onSearchAgain, + keyword, +}: NoSearchResultsProps) => { + return ( + + + + + Your filter{' '} + + "{keyword}" + {' '} + could not find any results + + + Try adjusting your search. We suggest check all words are spelled + correctly or try using different keywords. + + + + + ); +}; diff --git a/src/components/accounts/ui/VirtualizedList.tsx b/src/components/accounts/ui/VirtualizedList.tsx new file mode 100644 index 000000000..c2d81ee98 --- /dev/null +++ b/src/components/accounts/ui/VirtualizedList.tsx @@ -0,0 +1,58 @@ +import { + useRef, + useEffect, + createContext, + forwardRef, + useContext, +} from 'react'; +import { VariableSizeList } from 'react-window'; + +const Row = ({ data, index, style }) => { + const elem = data[index]; + return ( +
+ <>{elem} +
+ ); +}; + +const useResetCache = (data: any) => { + const ref = useRef(null); + useEffect(() => { + if (ref.current !== null) { + ref.current.resetAfterIndex(0, true); + } + }, [data]); + return ref; +}; + +const OuterElementContext = createContext({}); +const OuterElementType = forwardRef((props, ref) => { + const outerProps = useContext(OuterElementContext); + return
; +}); +export const VirtualizedList = forwardRef((props: any, ref: any) => { + const itemCount = props.children.length; + const gridRef = useResetCache(itemCount); + const outerProps = { ...props }; + delete outerProps.children; + return ( +
+ + props.rowheight} + overscanCount={5} + itemData={{ ...props.children }} + > + {Row} + + +
+ ); +}); diff --git a/src/components/accounts/ui/dialogs/index.js b/src/components/accounts/ui/dialogs/index.js index 4fbf5bd8d..41df76df5 100644 --- a/src/components/accounts/ui/dialogs/index.js +++ b/src/components/accounts/ui/dialogs/index.js @@ -17,6 +17,9 @@ export const SuccessMsg = ({ title, timer: 2500, confirmButtonColor: light.primary.main, + customClass: { + container: 'swal-zindex-override', + }, }).then(() => action()); }; @@ -36,6 +39,9 @@ export const ErrorMsg = ({ confirmButtonColor: light.zesty.zestyRose, timer, timerProgressBar, + customClass: { + container: 'swal-zindex-override', + }, }); }; @@ -85,6 +91,9 @@ export const DeleteMsg = ({ confirmButtonText: 'Yes', confirmButtonColor: '#3085d6', cancelButtonColor: '#d33', + customClass: { + container: 'swal-zindex-override', + }, }).then((result) => { if (result.isConfirmed) { action(); diff --git a/src/components/accounts/ui/header/index.js b/src/components/accounts/ui/header/index.js index a9edf6533..9ff68ffb6 100644 --- a/src/components/accounts/ui/header/index.js +++ b/src/components/accounts/ui/header/index.js @@ -1,39 +1,43 @@ import React from 'react'; -import { Grid, Stack, Typography } from '@mui/material'; +import { Grid, Stack, Typography, ThemeProvider } from '@mui/material'; import HelpOutlineIcon from '@mui/icons-material/HelpOutline'; +import { theme } from '@zesty-io/material'; const Index = ({ title, description, info, children }) => { return ( - - - - - - {title} - - + + + + + + + {title} + + + + + + {description} + + - - - {description} - + + {children} - - {children} - - - + + ); }; export const AccountsHeader = React.memo(Index); diff --git a/src/components/globals/NoPermission.jsx b/src/components/globals/NoPermission.jsx new file mode 100644 index 000000000..a64fb533c --- /dev/null +++ b/src/components/globals/NoPermission.jsx @@ -0,0 +1,80 @@ +import { useMemo } from 'react'; +import { + Stack, + Box, + Typography, + Avatar, + List, + ListItem, + ListItemText, + ListItemAvatar, +} from '@mui/material'; + +import { hashMD5 } from 'utils/Md5Hash'; +import shield from '../../../public/assets/images/shield.svg'; + +export const NoPermission = ({ users }) => { + const ownersAndAdmins = useMemo(() => { + if (!users && !users?.length) return []; + + const owners = users + .filter((user) => user.role?.name?.toLowerCase() === 'owner') + .sort((a, b) => a.firstName.localeCompare(b.firstName)); + const admins = users + .filter((user) => user.role?.name?.toLowerCase() === 'admin') + .sort((a, b) => a.firstName.localeCompare(b.firstName)); + + return [...owners, ...admins]; + }, [users]); + + return ( + + + + You need permission to view and edit Roles & Permissions + + + Contact the instance owner or administrators listed below to upgrade + your role to Admin or Owner for this capability. + + + {ownersAndAdmins?.map((user) => ( + + + + + + + ))} + + + + + ); +}; diff --git a/src/mui.d.ts b/src/mui.d.ts new file mode 100644 index 000000000..9af9d5043 --- /dev/null +++ b/src/mui.d.ts @@ -0,0 +1,27 @@ +import { Color } from '@mui/material'; + +declare module '@mui/material/Typography' { + export interface TypographyPropsVariantOverrides { + body3: true; + } +} + +declare module '@mui/material/styles' { + export interface Palette { + red: Color; + deepPurple: Color; + deepOrange: Color; + pink: Color; + blue: Color; + green: Color; + purple: Color; + yellow: Color; + } +} + +declare module '@mui/material/IconButton' { + interface IconButtonPropsSizeOverrides { + xsmall: true; + xxsmall: true; + } +} diff --git a/src/pages/instances/[zuid]/roles.tsx b/src/pages/instances/[zuid]/roles.tsx new file mode 100644 index 000000000..729c25f6c --- /dev/null +++ b/src/pages/instances/[zuid]/roles.tsx @@ -0,0 +1,59 @@ +import { useState, useEffect, useMemo } from 'react'; +import { useRouter } from 'next/router'; + +import { useZestyStore } from 'store'; +import { useRoles } from 'store/roles'; +import { useInstance } from 'store/instance'; +import InstanceContainer from 'components/accounts/instances/InstanceContainer'; +import { Roles } from 'views/accounts'; +import { ErrorMsg } from 'components/accounts'; + +export { default as getServerSideProps } from 'lib/accounts/protectedRouteGetServerSideProps'; + +export default function RolesPage() { + const router = useRouter(); + const { userInfo, loading } = useZestyStore((state) => state); + const { usersWithRoles, getRoles, getUsersWithRoles } = useRoles( + (state) => state, + ); + const { getInstanceModels, getInstanceContentItems, getLanguages } = + useInstance((state) => state); + const [isInitializingData, setIsInitializingData] = useState(true); + + const { zuid } = router.query; + + const hasPermission = useMemo(() => { + if (!userInfo?.ZUID || !usersWithRoles?.length) return false; + + return ['admin', 'owner'].includes( + usersWithRoles + ?.find((user) => user.ZUID === userInfo?.ZUID) + ?.role?.name?.toLowerCase(), + ); + }, [userInfo, usersWithRoles]); + + useEffect(() => { + if (router.isReady) { + const instanceZUID = String(zuid); + + Promise.all([ + getUsersWithRoles(instanceZUID), + getRoles(instanceZUID), + getInstanceModels(), + getInstanceContentItems(), + getLanguages('all'), + ]) + .catch(() => ErrorMsg({ title: 'Failed to fetch page data' })) + .finally(() => setIsInitializingData(false)); + } + }, [router.isReady]); + + return ( + + + + ); +} diff --git a/src/store/instance.ts b/src/store/instance.ts new file mode 100644 index 000000000..606a6294f --- /dev/null +++ b/src/store/instance.ts @@ -0,0 +1,58 @@ +import { create } from 'zustand'; +import { ContentItem, ContentModel, Language } from './types'; +import { getZestyAPI } from 'store'; + +const ZestyAPI = getZestyAPI(); + +type InstanceState = { + instanceModels: ContentModel[]; + instanceContentItems: ContentItem[]; + languages: Language[]; +}; +type InstanceAction = { + getInstanceModels: () => Promise; + getInstanceContentItems: () => Promise; + getLanguages: (type: 'all' | 'active') => Promise; +}; + +export const useInstance = create((set) => ({ + instanceModels: [], + getInstanceModels: async () => { + const response = await ZestyAPI.getModels(); + + if (response.error) { + console.error('getInstanceModels error: ', response.error); + throw new Error(response.error); + } else { + set({ + instanceModels: response.data, + }); + } + }, + + instanceContentItems: [], + getInstanceContentItems: async () => { + const response = await ZestyAPI.searchItems(); + + if (response.error) { + console.error('getInstanceContentItems error: ', response.error); + throw new Error(response.error); + } else { + set({ + instanceContentItems: response.data, + }); + } + }, + + languages: [], + getLanguages: async (type) => { + const response = await ZestyAPI.getLocales(type); + + if (response.error) { + console.error('getLanguages error: ', response.error); + throw new Error(response.error); + } else { + set({ languages: response.data }); + } + }, +})); diff --git a/src/store/roles.ts b/src/store/roles.ts new file mode 100644 index 000000000..25c31e5a6 --- /dev/null +++ b/src/store/roles.ts @@ -0,0 +1,244 @@ +import { create } from 'zustand'; + +import { UserRole, Role, GranularRole, RoleWithSort } from './types'; +import { getZestyAPI } from 'store'; +import { RoleDetails } from 'components/accounts/roles/CreateCustomRoleDialog'; +import { NewGranularRole } from 'components/accounts/roles/EditCustomRoleDialog/tabs/Permissions'; + +const BASE_ROLE_SORT_ORDER = [ + '31-71cfc74-0wn3r', + '31-71cfc74-4dm13', + '31-71cfc74-4cc4dm13', + '31-71cfc74-d3v3l0p3r', + '31-71cfc74-d3vc0n', + '31-71cfc74-s30', + '31-71cfc74-p0bl1shr', + '31-71cfc74-c0ntr1b0t0r', +] as const; + +const ZestyAPI = getZestyAPI(); + +type RolesState = { + usersWithRoles: UserRole[]; + baseRoles: RoleWithSort[]; + customRoles: Role[]; +}; +type RolesAction = { + getUsersWithRoles: (instanceZUID: string) => Promise; + updateUserRole: ( + data: { userZUID: string; oldRoleZUID: string; newRoleZUID: string }[], + ) => Promise; + getRoles: (instanceZUID: string) => Promise; + createRole: (data: RoleDetails & { instanceZUID: string }) => Promise; + updateRole: ({ + roleZUID, + name, + description, + systemRoleZUID, + }: { + roleZUID: string; + name: string; + description: string; + systemRoleZUID: string; + }) => Promise; + deleteRole: (data: { + roleZUIDToDelete: string; + roleZUIDToTransferUsers: string; + }) => Promise; + createGranularRole: ({ + roleZUID, + data, + }: { + roleZUID: string; + data: NewGranularRole & { name: string }; + }) => Promise; + updateGranularRole: ({ + roleZUID, + granularRoles, + }: { + roleZUID: string; + granularRoles: Partial[]; + }) => Promise; + deleteGranularRole: ({ + roleZUID, + resourceZUIDs, + }: { + roleZUID: string; + resourceZUIDs: string[]; + }) => Promise; +}; + +export const useRoles = create((set) => ({ + usersWithRoles: [], + getUsersWithRoles: async (instanceZUID) => { + const response = await ZestyAPI.getInstanceUsersWithRoles(instanceZUID); + + if (response.error) { + console.error('getUsersWithRoles error: ', response.error); + throw new Error(response.error); + } else { + set({ usersWithRoles: response.data }); + return response.data; + } + }, + updateUserRole: async (data) => { + if (!data?.length) return; + + Promise.all([ + data?.forEach(({ userZUID, oldRoleZUID, newRoleZUID }) => + ZestyAPI.updateUserRole(userZUID, oldRoleZUID, newRoleZUID), + ), + ]) + .then((response) => response) + .catch((error) => { + console.error('updateUserRole error: ', error); + throw new Error(error); + }); + }, + + baseRoles: [], + customRoles: [], + getRoles: async (instanceZUID) => { + const response = await ZestyAPI.getInstanceRoles(instanceZUID); + + if (response.error) { + console.error('getRoles error: ', response.error); + throw new Error(response.error); + } else { + const _baseRoles: RoleWithSort[] = []; + const _customRoles: Role[] = []; + + // Separate base roles from custom roles + response.data?.forEach((role: Role) => { + if (role.static) { + _baseRoles.push({ + ...role, + sort: BASE_ROLE_SORT_ORDER.findIndex( + (systemRoleZUID) => systemRoleZUID === role.systemRoleZUID, + ), + }); + } else { + _customRoles.push(role); + } + }); + + set({ + baseRoles: _baseRoles.sort((a, b) => a.sort - b.sort), + customRoles: _customRoles, + }); + } + }, + createRole: async ({ name, description, systemRoleZUID, instanceZUID }) => { + if (!name && !systemRoleZUID) return; + + const res = await ZestyAPI.createRole( + name, + instanceZUID, + systemRoleZUID, + description, + ); + + if (res.error) { + console.error('Failed to create role: ', res.error); + throw new Error(res.error); + } else { + return res.data; + } + }, + updateRole: async ({ roleZUID, name, description, systemRoleZUID }) => { + if (!roleZUID || !name) return; + + const res = await ZestyAPI.updateRole(roleZUID, { + name, + description, + systemRoleZUID, + }); + + if (res.error) { + console.error('Failed to update role: ', res.error); + throw new Error(res.error); + } else { + return res.data; + } + }, + deleteRole: async ({ roleZUIDToDelete, roleZUIDToTransferUsers }) => { + if (!roleZUIDToDelete || !roleZUIDToTransferUsers) return; + + // Transfer the existing users to a new role + const transferResponse = await ZestyAPI.bulkReassignUsersRole({ + oldRoleZUID: roleZUIDToDelete, + newRoleZUID: roleZUIDToTransferUsers, + }); + + if (transferResponse.error) { + console.error('Failed to reassign users role: ', transferResponse.error); + throw new Error(transferResponse.error); + } else { + // Once users have been reassigned, delete the role + const deleteRoleResponse = await ZestyAPI.deleteRole(roleZUIDToDelete); + + if (deleteRoleResponse.error) { + console.error( + `Failed to delete role ${roleZUIDToDelete}: `, + transferResponse.error, + ); + throw new Error(transferResponse.error); + } else { + return deleteRoleResponse.data; + } + } + }, + + createGranularRole: async ({ roleZUID, data }) => { + if (!roleZUID || !data || !Object.keys(data)?.length) return; + + const res = await ZestyAPI.createGranularRole( + roleZUID, + data.resourceZUID, + data.create, + data.read, + data.update, + data.delete, + data.publish, + ); + + if (res.error) { + console.error('Failed to update role: ', res.error); + throw new Error(res.error); + } else { + return res.data; + } + }, + updateGranularRole: async ({ roleZUID, granularRoles }) => { + if (!roleZUID || !granularRoles) return; + + const res = await ZestyAPI.batchUpdateGranularRoles( + roleZUID, + granularRoles, + ); + + if (res.error) { + console.error('Failed to update granular role: ', res.error); + throw new Error(res.error); + } else { + return res.data; + } + }, + deleteGranularRole: async ({ roleZUID, resourceZUIDs }) => { + if (!roleZUID || !resourceZUIDs || !resourceZUIDs?.length) return; + + Promise.all([ + resourceZUIDs.forEach((zuid) => + ZestyAPI.deleteGranularRole(roleZUID, zuid), + ), + ]) + .then((res) => { + console.log('delete response', res); + return res; + }) + .catch((err) => { + console.error('Failed to delete granular role: ', err); + throw new Error(err); + }); + }, +})); diff --git a/src/store/types.ts b/src/store/types.ts new file mode 100644 index 000000000..b0e45f4a6 --- /dev/null +++ b/src/store/types.ts @@ -0,0 +1,134 @@ +export type UserRole = { + ID: number; + ZUID: string; + authSource: string | null; + authyEnabled?: boolean; + authyPhoneCountryCode: string | null; + authyPhoneNumber: string | null; + authyUserID: string | null; + createdAt: string; + email: string; + firstName: string; + lastLogin: string; + lastName: string; + prefs: string | null; + role: Role; + signupInfo: string | null; + staff: boolean; + unverifiedEmails: string | null; + updatedAt: string; + verifiedEmails: string | null; + websiteCreator: boolean; +}; + +export type Role = { + ZUID: string; + createdAt: string; + createdByUserZUID: string; + entityZUID: string; + expiry: string | null; + granularRoleZUID: string | null; + granularRoles: GranularRole[] | null; + name: string; + static: boolean; + systemRole: SystemRole; + systemRoleZUID: string; + updatedAt: string; + description?: string; +}; + +export type RoleWithSort = Role & { sort?: number }; + +export type SystemRole = { + ZUID: string; + create: boolean; + createdAt: string; + delete: boolean; + grant: boolean; + name: string; + publish: boolean; + read: boolean; + super: boolean; + update: boolean; + updatedAt: string; +}; + +export type GranularRole = SystemRole & { resourceZUID: string }; + +export type ContentModel = { + ZUID: string; + masterZUID: string; + parentZUID: string; + description: string; + label: string; + metaTitle?: any; + metaDescription?: any; + metaKeywords?: any; + type: ModelType; + name: string; + sort: number; + listed: boolean; + createdByUserZUID: string; + updatedByUserZUID: string; + createdAt: string; + updatedAt: string; + module?: number; + plugin?: number; +}; + +export type ModelType = 'pageset' | 'templateset' | 'dataset'; + +export type ContentItem = { + web: Web; + meta: Meta; + siblings: [{ [key: number]: { value: string; id: number } }] | []; + data: Data; + publishAt?: any; +}; + +export type Web = { + version: number; + versionZUID: string; + metaDescription: string; + metaTitle: string; + metaLinkText: string; + metaKeywords?: any; + parentZUID?: any; + pathPart: string; + path: string; + sitemapPriority: number; + canonicalTagMode: number; + canonicalQueryParamWhitelist?: any; + canonicalTagCustomValue?: any; + createdByUserZUID: string; + createdAt: string; + updatedAt: string; +}; + +export type Meta = { + ZUID: string; + zid: number; + masterZUID: string; + contentModelZUID: string; + sort: number; + listed: boolean; + version: number; + langID: number; + createdAt: string; + updatedAt: string; + createdByUserZUID: string; +}; + +export type Data = { + [key: string]: number | string | null | undefined; +}; + +export type Language = { + ID: number; + code: string; + name: string; + default: boolean; + active: boolean; + createdAt: string; + updatedAt: string; +}; diff --git a/src/views/accounts/instances/Roles.tsx b/src/views/accounts/instances/Roles.tsx new file mode 100644 index 000000000..e55fa3609 --- /dev/null +++ b/src/views/accounts/instances/Roles.tsx @@ -0,0 +1,181 @@ +import { useMemo, useState, useRef, useDeferredValue } from 'react'; +import { + Button, + TextField, + Stack, + InputAdornment, + ThemeProvider, + CircularProgress, +} from '@mui/material'; +import { Search, AddRounded } from '@mui/icons-material'; +import { theme } from '@zesty-io/material'; + +import { useRoles } from 'store/roles'; +import { AccountsHeader } from 'components/accounts'; +import { NoPermission } from 'components/globals/NoPermission'; +import { BaseRoles } from 'components/accounts/roles/BaseRoles'; +import { NoCustomRoles } from 'components/accounts/roles/NoCustomRoles'; +import { CustomRoles } from 'components/accounts/roles/CustomRoles'; +import { CreateCustomRoleDialog } from 'components/accounts/roles/CreateCustomRoleDialog'; +import { NoSearchResults } from 'components/accounts/ui/NoSearchResults'; + +type RolesProps = { + isLoading: boolean; + hasPermission: boolean; +}; +export const Roles = ({ isLoading, hasPermission }: RolesProps) => { + const { usersWithRoles, customRoles, baseRoles } = useRoles((state) => state); + const customRolesRef = useRef(null); + const searchFieldRef = useRef(null); + const [isCreateCustomRoleDialogOpen, setIsCreateCustomRoleDialogOpen] = + useState(false); + const [filterKeyword, setFilterKeyword] = useState(''); + const deferredFilterKeyword = useDeferredValue(filterKeyword); + + const filteredRoles = useMemo(() => { + const keyword = deferredFilterKeyword?.toLowerCase(); + + if (!keyword) { + return { + baseRoles, + customRoles, + }; + } + + return { + baseRoles: baseRoles?.filter((role) => + role.name.toLowerCase().includes(keyword), + ), + customRoles: customRoles?.filter((role) => + role.name.toLowerCase().includes(keyword), + ), + }; + }, [baseRoles, customRoles, deferredFilterKeyword]); + + if (isLoading) { + return ( + + + {/* @ts-expect-error untyped component */} + + + + + + + ); + } + + if (!hasPermission) { + return ( + + + {/* @ts-expect-error untyped component */} + + + + + + + ); + } + + return ( + + + + + setFilterKeyword(evt.target.value)} + ref={searchFieldRef} + InputProps={{ + startAdornment: ( + + + + ), + }} + /> + + + + + {!filteredRoles?.customRoles?.length && + !filteredRoles?.baseRoles?.length && + deferredFilterKeyword ? ( + { + setFilterKeyword(''); + searchFieldRef.current?.querySelector('input')?.focus(); + }} + /> + ) : ( + <> + {filteredRoles?.customRoles?.length || + (!filteredRoles?.customRoles?.length && + !!deferredFilterKeyword) ? ( + + ) : ( + + setIsCreateCustomRoleDialogOpen(true) + } + /> + )} + + + )} + + + {isCreateCustomRoleDialogOpen && ( + setIsCreateCustomRoleDialogOpen(false)} + onRoleCreated={(ZUID) => + customRolesRef.current?.updateZUIDToEdit?.(ZUID) + } + /> + )} + + ); +}; diff --git a/src/views/accounts/instances/index.js b/src/views/accounts/instances/index.js index 06f20b328..00845c59b 100644 --- a/src/views/accounts/instances/index.js +++ b/src/views/accounts/instances/index.js @@ -5,3 +5,4 @@ export * from './Apis'; export * from './Webhooks'; export * from './Overview'; export * from './Usage'; +export * from './Roles'; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..01c2da4b5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "noEmit": true, + "incremental": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "baseUrl": "src" + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "src"], + "exclude": ["node_modules"] +} From 88108377b2dd3382fb698553813d063cb3eee107 Mon Sep 17 00:00:00 2001 From: Nar -- <28705606+finnar-bin@users.noreply.github.com> Date: Tue, 17 Dec 2024 05:02:10 +0800 Subject: [PATCH 13/14] fix: Open permissions tab upon successful creation of custom role (#2496) # Description Automatically opens the permissions tab upon successful creation of a new custom role ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) # How Has This Been Tested? Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. - [x] Manual Test # Screenshots / Screen recording [screen-recorder-tue-dec-10-2024-09-28-21.webm](https://github.com/user-attachments/assets/95d107ee-a993-4824-b553-1fce444c8cdc) --- src/components/accounts/roles/CustomRoles.tsx | 15 +++++++++++++-- .../roles/EditCustomRoleDialog/index.tsx | 18 ++++++++++++------ .../tabs/Permissions/Table.tsx | 10 +++++----- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/components/accounts/roles/CustomRoles.tsx b/src/components/accounts/roles/CustomRoles.tsx index c232fa01a..569ab6ba1 100644 --- a/src/components/accounts/roles/CustomRoles.tsx +++ b/src/components/accounts/roles/CustomRoles.tsx @@ -19,7 +19,11 @@ import { DeleteRounded, } from '@mui/icons-material'; -import { EditCustomRoleDialog } from './EditCustomRoleDialog'; +import { + EditCustomRoleDialog, + TabNames, + TabName, +} from './EditCustomRoleDialog'; import { DeleteCustomRoleDialog } from './DeleteCustomRoleDialog'; import { Role } from 'store/types'; @@ -32,9 +36,13 @@ export const CustomRoles = forwardRef( const [ZUIDToEdit, setZUIDToEdit] = useState(null); const [ZUIDToDelete, setZUIDToDelete] = useState(null); const [activeZUID, setActiveZUID] = useState(null); + const [tabToOpen, setTabToOpen] = useState(TabNames.details); useImperativeHandle(ref, () => ({ - updateZUIDToEdit: (ZUID: string) => setZUIDToEdit(ZUID), + updateZUIDToEdit: (ZUID: string) => { + setZUIDToEdit(ZUID); + setTabToOpen(TabNames.permissions); + }, })); return ( @@ -57,6 +65,7 @@ export const CustomRoles = forwardRef( }} onClick={() => { setZUIDToEdit(role.ZUID); + setTabToOpen(TabNames.details); }} > @@ -107,6 +116,7 @@ export const CustomRoles = forwardRef( onClick={() => { setAnchorEl(null); setZUIDToEdit(role.ZUID); + setTabToOpen(TabNames.details); }} > @@ -134,6 +144,7 @@ export const CustomRoles = forwardRef( setZUIDToEdit(null)} + tabToOpen={tabToOpen} /> )} {!!ZUIDToDelete && ( diff --git a/src/components/accounts/roles/EditCustomRoleDialog/index.tsx b/src/components/accounts/roles/EditCustomRoleDialog/index.tsx index cf1607fdd..49a4675aa 100644 --- a/src/components/accounts/roles/EditCustomRoleDialog/index.tsx +++ b/src/components/accounts/roles/EditCustomRoleDialog/index.tsx @@ -30,6 +30,12 @@ import { GranularRole } from 'store/types'; import { useZestyStore } from 'store'; import { ErrorMsg } from 'components/accounts/ui'; +export const TabNames = { + details: 'details', + permissions: 'permissions', + users: 'users', +} as const; +export type TabName = (typeof TabNames)[keyof typeof TabNames]; type FieldErrors = { detailsTab: { roleName: string; @@ -47,10 +53,12 @@ export type RoleDetails = { type EditCustomRoleDialogProps = { ZUID: string; onClose: () => void; + tabToOpen?: TabName; }; export const EditCustomRoleDialog = ({ ZUID, onClose, + tabToOpen = TabNames.details, }: EditCustomRoleDialogProps) => { const router = useRouter(); const { ZestyAPI } = useZestyStore((state: any) => state); @@ -66,9 +74,7 @@ export const EditCustomRoleDialog = ({ updateUserRole, getUsersWithRoles, } = useRoles((state) => state); - const [activeTab, setActiveTab] = useState< - 'details' | 'permissions' | 'users' - >('details'); + const [activeTab, setActiveTab] = useState(tabToOpen); const [isSaving, setIsSaving] = useState(false); const [fieldErrors, updateFieldErrors] = useReducer( ( @@ -395,7 +401,7 @@ export const EditCustomRoleDialog = ({ bgcolor: 'grey.50', }} > - {activeTab === 'details' && ( + {activeTab === TabNames.details && (
{ @@ -413,7 +419,7 @@ export const EditCustomRoleDialog = ({ errors={fieldErrors.detailsTab} /> )} - {activeTab === 'permissions' && ( + {activeTab === TabNames.permissions && ( { @@ -465,7 +471,7 @@ export const EditCustomRoleDialog = ({ }} /> )} - {activeTab === 'users' && ( + {activeTab === TabNames.users && ( setUserEmails(emails)} diff --git a/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/Table.tsx b/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/Table.tsx index 2981ba554..539eb06b0 100644 --- a/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/Table.tsx +++ b/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/Table.tsx @@ -55,7 +55,7 @@ export const Table = ({ defaultChecked={!!params.value} onChange={(evt) => onDataChange({ - resourceZUID: params.row?.resourceZUID, + resourceZUID: params.row?.id, create: evt.target.checked, }) } @@ -71,7 +71,7 @@ export const Table = ({ defaultChecked={!!params.value} onChange={(evt) => onDataChange({ - resourceZUID: params.row?.resourceZUID, + resourceZUID: params.row?.id, read: evt.target.checked, }) } @@ -87,7 +87,7 @@ export const Table = ({ defaultChecked={!!params.value} onChange={(evt) => onDataChange({ - resourceZUID: params.row?.resourceZUID, + resourceZUID: params.row?.id, update: evt.target.checked, }) } @@ -103,7 +103,7 @@ export const Table = ({ defaultChecked={!!params.value} onChange={(evt) => onDataChange({ - resourceZUID: params.row?.resourceZUID, + resourceZUID: params.row?.id, delete: evt.target.checked, }) } @@ -119,7 +119,7 @@ export const Table = ({ defaultChecked={!!params.value} onChange={(evt) => onDataChange({ - resourceZUID: params.row?.resourceZUID, + resourceZUID: params.row?.id, publish: evt.target.checked, }) } From dd1da72d677eb2e9c21e5f35b6b53ec3d3e579e4 Mon Sep 17 00:00:00 2001 From: Nar -- <28705606+finnar-bin@users.noreply.github.com> Date: Sat, 21 Dec 2024 07:42:38 +0800 Subject: [PATCH 14/14] feat: resolve vqa comments for custom roles (#2499) # Description Resolve vqa comments for custom roles Requires https://github.com/zesty-io/material/pull/111 ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update # How Has This Been Tested? - [x] Manual Test - [ ] Unit Test - [ ] E2E Test --------- Co-authored-by: shrunyan --- package-lock.json | 8 ++--- package.json | 2 +- src/components/accounts/instances/lang.js | 4 +-- src/components/accounts/instances/tabs.js | 32 +++++++++---------- src/components/accounts/roles/BaseRoles.tsx | 5 ++- .../accounts/roles/CreateCustomRoleDialog.tsx | 9 +++--- src/components/accounts/roles/CustomRoles.tsx | 8 +++-- .../accounts/roles/DeleteCustomRoleDialog.tsx | 4 ++- .../roles/EditCustomRoleDialog/index.tsx | 26 +++++++++++---- .../tabs/Permissions/ResourceSelector.tsx | 19 ++++++++--- .../tabs/Permissions/index.tsx | 9 ++++-- src/components/accounts/ui/header/index.js | 2 +- src/components/globals/NoPermission.jsx | 1 + src/views/accounts/instances/Roles.tsx | 2 +- 14 files changed, 85 insertions(+), 46 deletions(-) diff --git a/package-lock.json b/package-lock.json index a78f537e6..309bbc14b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "@uiw/codemirror-theme-github": "^4.21.18", "@uiw/react-codemirror": "^4.21.18", "@zesty-io/live-editor": "^2.0.30", - "@zesty-io/material": "^0.15.6", + "@zesty-io/material": "^0.15.7", "@zesty-io/react-autolayout": "^1.0.0-beta.16", "algoliasearch": "^4.20.0", "aos": "^2.3.4", @@ -4852,9 +4852,9 @@ } }, "node_modules/@zesty-io/material": { - "version": "0.15.6", - "resolved": "https://registry.npmjs.org/@zesty-io/material/-/material-0.15.6.tgz", - "integrity": "sha512-zMWI1J+ArxhD7VX+A6JGRtwMO4oVNIPm+ZcjqugbGK1u/aaRRkRzhQ/ZsvTwHxOgK3rsatv6QWfYCttBTZF1KQ==", + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/@zesty-io/material/-/material-0.15.7.tgz", + "integrity": "sha512-cfqKrX3MeB4d5rJAqRbasaG+8xQ1hIjH92KBG9LUl3oyNeH5KHkWaC7Ek5CnMlXlmJanoeEZRDkl8Qk/2JwKIg==", "license": "MIT", "dependencies": { "@emotion/react": "^11.9.0", diff --git a/package.json b/package.json index 7a000606d..b6a3181c4 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "@uiw/codemirror-theme-github": "^4.21.18", "@uiw/react-codemirror": "^4.21.18", "@zesty-io/live-editor": "^2.0.30", - "@zesty-io/material": "^0.15.6", + "@zesty-io/material": "^0.15.7", "@zesty-io/react-autolayout": "^1.0.0-beta.16", "algoliasearch": "^4.20.0", "aos": "^2.3.4", diff --git a/src/components/accounts/instances/lang.js b/src/components/accounts/instances/lang.js index 5ac609245..1373cd35c 100644 --- a/src/components/accounts/instances/lang.js +++ b/src/components/accounts/instances/lang.js @@ -4,11 +4,11 @@ export const lang = { '': 'Overview', users: 'Users', roles: 'Roles & Permissions', - teams: 'Teams', + teams: 'Team Access', domains: 'Domains', usage: 'Usage', support: 'Support', - apis: 'APIs', + apis: 'API Tokens', webhooks: 'Webhooks', settings: 'Settings', billing: 'Billing', diff --git a/src/components/accounts/instances/tabs.js b/src/components/accounts/instances/tabs.js index 734cdc0e7..c1094b589 100644 --- a/src/components/accounts/instances/tabs.js +++ b/src/components/accounts/instances/tabs.js @@ -18,59 +18,59 @@ export const instanceTabs = [ label: 'Overview', sort: 0, }, - { - icon: , - filename: 'roles', - label: 'Roles & Permissions', - sort: 1, - }, { icon: , filename: 'users', label: 'Users', sort: 2, }, + { + icon: , + filename: 'roles', + label: 'Roles & Permissions', + sort: 3, + }, { icon: , filename: 'teams', - label: 'Teams', - sort: 3, + label: 'Team Access', + sort: 4, }, { icon: , filename: 'domains', label: 'Domains', - sort: 4, + sort: 5, }, { icon: , filename: 'usage', label: 'Usage', - sort: 5, + sort: 6, }, { icon: , filename: 'locales', label: 'Locales', - sort: 6, + sort: 7, }, { icon: , filename: 'apis', - label: 'APIs & Tokens', - sort: 7, + label: 'API Tokens', + sort: 8, }, { icon: , filename: 'webhooks', label: 'Webhooks', - sort: 8, + sort: 9, }, { icon: , filename: 'support', label: 'Support', - sort: 9, + sort: 10, }, // comment out for now // { @@ -83,6 +83,6 @@ export const instanceTabs = [ icon: , filename: 'settings', label: 'Settings', - sort: 10, + sort: 11, }, ]; diff --git a/src/components/accounts/roles/BaseRoles.tsx b/src/components/accounts/roles/BaseRoles.tsx index 37b673617..c1ce2e8c4 100644 --- a/src/components/accounts/roles/BaseRoles.tsx +++ b/src/components/accounts/roles/BaseRoles.tsx @@ -120,7 +120,7 @@ export const BaseRoles = ({ baseRoles }: BaseRolesProps) => { p: 2, border: (theme) => `1px solid ${theme.palette.border}`, borderRadius: 2, - mb: index + 1 < baseRoles?.length ? 1 : 0, + mb: index + 1 < baseRoles?.length ? 2 : 0, }} > @@ -137,6 +137,9 @@ export const BaseRoles = ({ baseRoles }: BaseRolesProps) => { {BASE_ROLES_CONFIG[role.name.toLowerCase()]?.description} } + sx={{ + my: 0, + }} /> ))} diff --git a/src/components/accounts/roles/CreateCustomRoleDialog.tsx b/src/components/accounts/roles/CreateCustomRoleDialog.tsx index e146e96a8..e3ed621b4 100644 --- a/src/components/accounts/roles/CreateCustomRoleDialog.tsx +++ b/src/components/accounts/roles/CreateCustomRoleDialog.tsx @@ -374,7 +374,8 @@ export const CreateCustomRoleDialog = ({ sx: { maxWidth: 960, width: 960, - minHeight: 800, + maxHeight: 'calc(100% - 40px)', + my: 2.5, }, }} > @@ -392,7 +393,7 @@ export const CreateCustomRoleDialog = ({ - + Create Custom Role @@ -400,10 +401,10 @@ export const CreateCustomRoleDialog = ({ Creates a custom role that can have granular permissions applied to it - + onClose?.()}> - + `1px solid ${theme.palette.border}`, borderRadius: 2, - mb: index + 1 < customRoles?.length ? 1 : 0, + mb: index + 1 < customRoles?.length ? 2 : 0, }} onClick={() => { setZUIDToEdit(role.ZUID); @@ -88,15 +88,19 @@ export const CustomRoles = forwardRef( {role.description || ''} } + sx={{ + my: 0, + }} /> { evt.stopPropagation(); setAnchorEl(evt.currentTarget); setActiveZUID(role.ZUID); }} > - + a.label?.localeCompare(b.label), + ); }, [customRoles, baseRoles]); const defaultBaseRole = baseRoles?.find( diff --git a/src/components/accounts/roles/EditCustomRoleDialog/index.tsx b/src/components/accounts/roles/EditCustomRoleDialog/index.tsx index 49a4675aa..df51bacac 100644 --- a/src/components/accounts/roles/EditCustomRoleDialog/index.tsx +++ b/src/components/accounts/roles/EditCustomRoleDialog/index.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState, useReducer, useEffect } from 'react'; +import { useMemo, useState, useReducer, useEffect, useRef } from 'react'; import { Typography, Avatar, @@ -19,6 +19,7 @@ import { InfoRounded, RuleRounded, GroupsRounded, + SaveRounded, } from '@mui/icons-material'; import { useRouter } from 'next/router'; @@ -74,6 +75,8 @@ export const EditCustomRoleDialog = ({ updateUserRole, getUsersWithRoles, } = useRoles((state) => state); + const containerRef = useRef(null); + const [minHeight, setMinHeight] = useState(0); const [activeTab, setActiveTab] = useState(tabToOpen); const [isSaving, setIsSaving] = useState(false); const [fieldErrors, updateFieldErrors] = useReducer( @@ -102,6 +105,12 @@ export const EditCustomRoleDialog = ({ }, ); + useEffect(() => { + setTimeout(() => { + setMinHeight(containerRef.current?.clientHeight); + }); + }, []); + const roleUsers = useMemo(() => { if (!usersWithRoles?.length) return []; @@ -218,7 +227,6 @@ export const EditCustomRoleDialog = ({ (email) => !alreadyExistingUsers.includes(email), ); - // TODO: Do something with emails that are not yet instance members const users = usersWithRoles?.reduce( (prev, curr) => { if (usersToAdd?.includes(curr.email)) { @@ -318,6 +326,8 @@ export const EditCustomRoleDialog = ({ setActiveTab('permissions'); } else if (!!fieldErrors?.usersTab?.length) { setActiveTab('users'); + } else { + onClose(); } }); }; @@ -328,10 +338,13 @@ export const EditCustomRoleDialog = ({ fullWidth onClose={() => onClose?.()} PaperProps={{ + ref: containerRef, sx: { maxWidth: 960, width: 960, - minHeight: 800, + minHeight: minHeight, + maxHeight: 'calc(100% - 40px)', + my: 2.5, }, }} > @@ -348,7 +361,7 @@ export const EditCustomRoleDialog = ({ - + Edit {roleData?.name} @@ -360,10 +373,10 @@ export const EditCustomRoleDialog = ({ Edit your custom role that can have granular permissions applied to it - + onClose?.()}> - + } > Save diff --git a/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/ResourceSelector.tsx b/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/ResourceSelector.tsx index a32a071d6..a44e7e6a1 100644 --- a/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/ResourceSelector.tsx +++ b/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/ResourceSelector.tsx @@ -107,7 +107,15 @@ export const ResourceSelector = ({ height: 36, }} > - + {option.type === 'model' ? : } - {option.label} - + primaryTypographyProps={{ + variant: 'body2', + color: 'text.secondary', + }} + primary={option.label} + /> ); }} diff --git a/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/index.tsx b/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/index.tsx index bc083ba8f..08953c9b2 100644 --- a/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/index.tsx +++ b/src/components/accounts/roles/EditCustomRoleDialog/tabs/Permissions/index.tsx @@ -95,14 +95,14 @@ export const Permissions = ({ return ( - + Resource Permissions Grant users access only to resources you specify - + ), }} + sx={{ + width: 240, + }} />