((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: (
+
+
+
+ ),
+ }}
+ />
+ }
+ onClick={() => setIsCreateCustomRoleDialogOpen(true)}
+ >
+ Create Custom Role
+
+
+
+
+ {!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);
}}
>
-
+