diff --git a/packages/web/src/components/ListLoader/index.tsx b/packages/web/src/components/ListLoader/index.tsx new file mode 100644 index 00000000..43945185 --- /dev/null +++ b/packages/web/src/components/ListLoader/index.tsx @@ -0,0 +1,47 @@ +import { + IconButton, + Skeleton, + Stack, + TableCell, + TableRow, +} from '@mui/material'; +import EditIcon from '@mui/icons-material/Edit'; +import DeleteIcon from '@mui/icons-material/Delete'; + +type ListLoaderProps = { + rowsNumber: number; + columnsNumber: number; +}; + +const ListLoader = ({ rowsNumber, columnsNumber }: ListLoaderProps) => { + return ( + <> + {[...Array(rowsNumber)].map((row, index) => ( + + {[...Array(columnsNumber)].map((cell, index) => ( + + + + ))} + + + + + + + + + + + + + + ))} + + ); +}; + +export default ListLoader; diff --git a/packages/web/src/components/PermissionCatalogField/PermissionCatalogFieldLoader/index.tsx b/packages/web/src/components/PermissionCatalogField/PermissionCatalogFieldLoader/index.tsx new file mode 100644 index 00000000..79bce0b5 --- /dev/null +++ b/packages/web/src/components/PermissionCatalogField/PermissionCatalogFieldLoader/index.tsx @@ -0,0 +1,61 @@ +import { + IconButton, + Skeleton, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, +} from '@mui/material'; +import SettingsIcon from '@mui/icons-material/Settings'; +import ControlledCheckbox from 'components/ControlledCheckbox'; + +const PermissionCatalogFieldLoader = () => { + return ( + + + + + + {[...Array(5)].map((row, index) => ( + + + + ))} + + + + + {[...Array(3)].map((row, index) => ( + + + + + + {[...Array(5)].map((action, index) => ( + + + + + + ))} + + + + + + + + + + ))} + +
+
+ ); +}; + +export default PermissionCatalogFieldLoader; diff --git a/packages/web/src/components/PermissionCatalogField/index.ee.tsx b/packages/web/src/components/PermissionCatalogField/index.ee.tsx index b3ace5e1..6180104a 100644 --- a/packages/web/src/components/PermissionCatalogField/index.ee.tsx +++ b/packages/web/src/components/PermissionCatalogField/index.ee.tsx @@ -14,17 +14,21 @@ import * as React from 'react'; import ControlledCheckbox from 'components/ControlledCheckbox'; import usePermissionCatalog from 'hooks/usePermissionCatalog.ee'; import PermissionSettings from './PermissionSettings.ee'; +import PermissionCatalogFieldLoader from './PermissionCatalogFieldLoader'; type PermissionCatalogFieldProps = { name?: string; disabled?: boolean; }; -const PermissionCatalogField = ({ name = 'permissions', disabled = false }: PermissionCatalogFieldProps) => { - const permissionCatalog = usePermissionCatalog(); +const PermissionCatalogField = ({ + name = 'permissions', + disabled = false, +}: PermissionCatalogFieldProps) => { + const { permissionCatalog, loading } = usePermissionCatalog(); const [dialogName, setDialogName] = React.useState(); - if (!permissionCatalog) return (); + if (loading) return ; return ( @@ -33,14 +37,14 @@ const PermissionCatalogField = ({ name = 'permissions', disabled = false }: Perm - {permissionCatalog.actions.map(action => ( + {permissionCatalog.actions.map((action) => ( {action.label} @@ -58,21 +62,12 @@ const PermissionCatalogField = ({ name = 'permissions', disabled = false }: Perm sx={{ '&:last-child td': { border: 0 } }} > - - {subject.label} - + {subject.label} {permissionCatalog.actions.map((action) => ( - - + + {action.subjects.includes(subject.key) && ( - + - ) + ); }; export default PermissionCatalogField; diff --git a/packages/web/src/components/RoleList/index.ee.tsx b/packages/web/src/components/RoleList/index.ee.tsx index 94d02b53..285873ac 100644 --- a/packages/web/src/components/RoleList/index.ee.tsx +++ b/packages/web/src/components/RoleList/index.ee.tsx @@ -13,14 +13,14 @@ import Typography from '@mui/material/Typography'; import EditIcon from '@mui/icons-material/Edit'; import DeleteRoleButton from 'components/DeleteRoleButton/index.ee'; +import ListLoader from 'components/ListLoader'; import useFormatMessage from 'hooks/useFormatMessage'; import useRoles from 'hooks/useRoles.ee'; import * as URLS from 'config/urls'; -// TODO: introduce loading bar export default function RoleList(): React.ReactElement { const formatMessage = useFormatMessage(); - const { roles } = useRoles(); + const { roles, loading } = useRoles(); return ( @@ -49,34 +49,41 @@ export default function RoleList(): React.ReactElement { - {roles.map((role) => ( - - - {role.name} - + {loading && } + {!loading && + roles.map((role) => ( + + + {role.name} + - - {role.description} - + + + {role.description} + + - - - - - + + + + + - - - - - ))} + + + + + ))} diff --git a/packages/web/src/components/UserList/index.tsx b/packages/web/src/components/UserList/index.tsx index f394936c..956c2ad3 100644 --- a/packages/web/src/components/UserList/index.tsx +++ b/packages/web/src/components/UserList/index.tsx @@ -13,11 +13,11 @@ import Typography from '@mui/material/Typography'; import EditIcon from '@mui/icons-material/Edit'; import DeleteUserButton from 'components/DeleteUserButton/index.ee'; +import ListLoader from 'components/ListLoader'; import useUsers from 'hooks/useUsers'; import useFormatMessage from 'hooks/useFormatMessage'; import * as URLS from 'config/urls'; -// TODO: introduce loading bar export default function UserList(): React.ReactElement { const formatMessage = useFormatMessage(); const { users, loading } = useUsers(); @@ -49,34 +49,36 @@ export default function UserList(): React.ReactElement { - {users.map((user) => ( - - - {user.fullName} - + {loading && } + {!loading && + users.map((user) => ( + + + {user.fullName} + - - {user.email} - + + {user.email} + - - - - - + + + + + - - - - - ))} + + + + + ))} diff --git a/packages/web/src/helpers/computePermissions.ee.ts b/packages/web/src/helpers/computePermissions.ee.ts index f57b4cd8..6313ec77 100644 --- a/packages/web/src/helpers/computePermissions.ee.ts +++ b/packages/web/src/helpers/computePermissions.ee.ts @@ -3,24 +3,33 @@ import { IRole, IPermission } from '@automatisch/types'; type ComputeAction = { conditions: Record; value: boolean; -} +}; type ComputedActions = Record; type ComputedPermissions = Record; -export type RoleWithComputedPermissions = IRole & { computedPermissions: ComputedPermissions }; +export type RoleWithComputedPermissions = IRole & { + computedPermissions: ComputedPermissions; +}; -export function getRoleWithComputedPermissions(role: IRole): RoleWithComputedPermissions { - const computedPermissions = role.permissions.reduce((computedPermissions, permission) => ({ - ...computedPermissions, - [permission.subject]: { - ...(computedPermissions[permission.subject] || {}), - [permission.action]: { - conditions: Object.fromEntries(permission - .conditions - .map(condition => [condition, true])), - value: true, +export function getRoleWithComputedPermissions( + role?: IRole +): Partial { + if (!role) return {}; + + const computedPermissions = role.permissions.reduce( + (computedPermissions, permission) => ({ + ...computedPermissions, + [permission.subject]: { + ...(computedPermissions[permission.subject] || {}), + [permission.action]: { + conditions: Object.fromEntries( + permission.conditions.map((condition) => [condition, true]) + ), + value: true, + }, }, - } - }), {} as ComputedPermissions); + }), + {} as ComputedPermissions + ); return { ...role, @@ -31,29 +40,27 @@ export function getRoleWithComputedPermissions(role: IRole): RoleWithComputedPer export function getPermissions(computedPermissions?: ComputedPermissions) { if (!computedPermissions) return []; - return Object - .entries(computedPermissions) - .reduce((permissions, computedPermissionEntry) => { + return Object.entries(computedPermissions).reduce( + (permissions, computedPermissionEntry) => { const [subject, actionsWithConditions] = computedPermissionEntry; for (const action in actionsWithConditions) { - const { - value: permitted, - conditions = {}, - } = actionsWithConditions[action]; + const { value: permitted, conditions = {} } = + actionsWithConditions[action]; if (permitted) { permissions.push({ action, subject, - conditions: Object - .entries(conditions) + conditions: Object.entries(conditions) .filter(([, enabled]) => enabled) .map(([condition]) => condition), - }) + }); } } return permissions; - }, [] as Partial[]); + }, + [] as Partial[] + ); } diff --git a/packages/web/src/hooks/usePermissionCatalog.ee.ts b/packages/web/src/hooks/usePermissionCatalog.ee.ts index d9496ceb..0292e4f0 100644 --- a/packages/web/src/hooks/usePermissionCatalog.ee.ts +++ b/packages/web/src/hooks/usePermissionCatalog.ee.ts @@ -3,8 +3,13 @@ import { IPermissionCatalog } from '@automatisch/types'; import { GET_PERMISSION_CATALOG } from 'graphql/queries/get-permission-catalog.ee'; -export default function usePermissionCatalog(): IPermissionCatalog { - const { data } = useQuery(GET_PERMISSION_CATALOG); +type UsePermissionCatalogReturn = { + permissionCatalog: IPermissionCatalog; + loading: boolean; +}; - return data?.getPermissionCatalog; +export default function usePermissionCatalog(): UsePermissionCatalogReturn { + const { data, loading } = useQuery(GET_PERMISSION_CATALOG); + + return { permissionCatalog: data?.getPermissionCatalog, loading }; } diff --git a/packages/web/src/hooks/useRole.ee.ts b/packages/web/src/hooks/useRole.ee.ts index 5263254d..ff6c63bb 100644 --- a/packages/web/src/hooks/useRole.ee.ts +++ b/packages/web/src/hooks/useRole.ee.ts @@ -6,7 +6,7 @@ import { GET_ROLE } from 'graphql/queries/get-role.ee'; type QueryResponse = { getRole: IRole; -} +}; export default function useRole(roleId?: string) { const [getRole, { data, loading }] = useLazyQuery(GET_ROLE); @@ -15,14 +15,14 @@ export default function useRole(roleId?: string) { if (roleId) { getRole({ variables: { - id: roleId - } + id: roleId, + }, }); } }, [roleId]); return { role: data?.getRole, - loading + loading, }; } diff --git a/packages/web/src/pages/EditRole/index.ee.tsx b/packages/web/src/pages/EditRole/index.ee.tsx index 64915cfd..58cd878a 100644 --- a/packages/web/src/pages/EditRole/index.ee.tsx +++ b/packages/web/src/pages/EditRole/index.ee.tsx @@ -3,6 +3,7 @@ import LoadingButton from '@mui/lab/LoadingButton'; import Container from '@mui/material/Container'; import Grid from '@mui/material/Grid'; import Stack from '@mui/material/Stack'; +import Skeleton from '@mui/material/Skeleton'; import * as React from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useSnackbar } from 'notistack'; @@ -25,7 +26,6 @@ type EditRoleParams = { roleId: string; }; -// TODO: introduce loading bar export default function EditRole(): React.ReactElement { const formatMessage = useFormatMessage(); const [updateRole, { loading }] = useMutation(UPDATE_ROLE); @@ -61,8 +61,6 @@ export default function EditRole(): React.ReactElement { } }; - if (roleLoading || !role) return ; - const roleWithComputedPermissions = getRoleWithComputedPermissions(role); return ( @@ -78,24 +76,34 @@ export default function EditRole(): React.ReactElement { onSubmit={handleRoleUpdate} > - + {roleLoading && ( + <> + + + + )} + {!roleLoading && role && ( + <> + - + + + )} {formatMessage('editRole.submit')} diff --git a/packages/web/src/pages/EditUser/index.tsx b/packages/web/src/pages/EditUser/index.tsx index 8c663868..c4253b07 100644 --- a/packages/web/src/pages/EditUser/index.tsx +++ b/packages/web/src/pages/EditUser/index.tsx @@ -5,6 +5,7 @@ import Container from '@mui/material/Container'; import Grid from '@mui/material/Grid'; import Stack from '@mui/material/Stack'; import MuiTextField from '@mui/material/TextField'; +import Skeleton from '@mui/material/Skeleton'; import LoadingButton from '@mui/lab/LoadingButton'; import { IUser, IRole } from '@automatisch/types'; import { useSnackbar } from 'notistack'; @@ -28,7 +29,6 @@ function generateRoleOptions(roles: IRole[]) { return roles?.map(({ name: label, id: value }) => ({ label, value })); } -// TODO: introduce loading bar export default function EditUser(): React.ReactElement { const formatMessage = useFormatMessage(); const [updateUser, { loading }] = useMutation(UPDATE_USER); @@ -63,8 +63,6 @@ export default function EditUser(): React.ReactElement { } }; - if (userLoading) return ; - return ( @@ -73,50 +71,61 @@ export default function EditUser(): React.ReactElement { -
+ {userLoading && ( - - - - - - ( - - )} - loading={rolesLoading} - /> - - - - {formatMessage('editUser.submit')} - + + + + -
+ )} + + {!userLoading && ( +
+ + + + + + + ( + + )} + loading={rolesLoading} + /> + + + + {formatMessage('editUser.submit')} + + +
+ )}