feat(auth): add loading state for user and role management (#1188)

This commit is contained in:
Rıdvan Akca
2023-08-09 22:51:07 +03:00
committed by GitHub
parent ce8c9906cb
commit 5046c4c911
10 changed files with 308 additions and 171 deletions

View File

@@ -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) => (
<TableRow
key={index}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
>
{[...Array(columnsNumber)].map((cell, index) => (
<TableCell key={index} scope="row">
<Skeleton />
</TableCell>
))}
<TableCell>
<Stack direction="row" gap={1} justifyContent="right">
<IconButton size="small">
<EditIcon />
</IconButton>
<IconButton size="small">
<DeleteIcon />
</IconButton>
</Stack>
</TableCell>
</TableRow>
))}
</>
);
};
export default ListLoader;

View File

@@ -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 (
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell component="th" />
{[...Array(5)].map((row, index) => (
<TableCell key={index} component="th">
<Skeleton />
</TableCell>
))}
<TableCell component="th" />
</TableRow>
</TableHead>
<TableBody>
{[...Array(3)].map((row, index) => (
<TableRow key={index} sx={{ '&:last-child td': { border: 0 } }}>
<TableCell scope="row">
<Skeleton width={40} />
</TableCell>
{[...Array(5)].map((action, index) => (
<TableCell key={index} align="center">
<Typography variant="subtitle2">
<ControlledCheckbox name="value" />
</Typography>
</TableCell>
))}
<TableCell>
<Stack direction="row" gap={1} justifyContent="right">
<IconButton color="info" size="small">
<SettingsIcon />
</IconButton>
</Stack>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
};
export default PermissionCatalogFieldLoader;

View File

@@ -14,17 +14,21 @@ import * as React from 'react';
import ControlledCheckbox from 'components/ControlledCheckbox'; import ControlledCheckbox from 'components/ControlledCheckbox';
import usePermissionCatalog from 'hooks/usePermissionCatalog.ee'; import usePermissionCatalog from 'hooks/usePermissionCatalog.ee';
import PermissionSettings from './PermissionSettings.ee'; import PermissionSettings from './PermissionSettings.ee';
import PermissionCatalogFieldLoader from './PermissionCatalogFieldLoader';
type PermissionCatalogFieldProps = { type PermissionCatalogFieldProps = {
name?: string; name?: string;
disabled?: boolean; disabled?: boolean;
}; };
const PermissionCatalogField = ({ name = 'permissions', disabled = false }: PermissionCatalogFieldProps) => { const PermissionCatalogField = ({
const permissionCatalog = usePermissionCatalog(); name = 'permissions',
disabled = false,
}: PermissionCatalogFieldProps) => {
const { permissionCatalog, loading } = usePermissionCatalog();
const [dialogName, setDialogName] = React.useState<string>(); const [dialogName, setDialogName] = React.useState<string>();
if (!permissionCatalog) return (<React.Fragment />); if (loading) return <PermissionCatalogFieldLoader />;
return ( return (
<TableContainer component={Paper}> <TableContainer component={Paper}>
@@ -33,14 +37,14 @@ const PermissionCatalogField = ({ name = 'permissions', disabled = false }: Perm
<TableRow> <TableRow>
<TableCell component="th" /> <TableCell component="th" />
{permissionCatalog.actions.map(action => ( {permissionCatalog.actions.map((action) => (
<TableCell component="th" key={action.key}> <TableCell component="th" key={action.key}>
<Typography <Typography
variant="subtitle1" variant="subtitle1"
align="center" align="center"
sx={{ sx={{
color: 'text.secondary', color: 'text.secondary',
fontWeight: 700 fontWeight: 700,
}} }}
> >
{action.label} {action.label}
@@ -58,21 +62,12 @@ const PermissionCatalogField = ({ name = 'permissions', disabled = false }: Perm
sx={{ '&:last-child td': { border: 0 } }} sx={{ '&:last-child td': { border: 0 } }}
> >
<TableCell scope="row"> <TableCell scope="row">
<Typography <Typography variant="subtitle2">{subject.label}</Typography>
variant="subtitle2"
>
{subject.label}
</Typography>
</TableCell> </TableCell>
{permissionCatalog.actions.map((action) => ( {permissionCatalog.actions.map((action) => (
<TableCell <TableCell key={`${subject.key}.${action.key}`} align="center">
key={`${subject.key}.${action.key}`} <Typography variant="subtitle2">
align="center"
>
<Typography
variant="subtitle2"
>
{action.subjects.includes(subject.key) && ( {action.subjects.includes(subject.key) && (
<ControlledCheckbox <ControlledCheckbox
disabled={disabled} disabled={disabled}
@@ -86,11 +81,7 @@ const PermissionCatalogField = ({ name = 'permissions', disabled = false }: Perm
))} ))}
<TableCell> <TableCell>
<Stack <Stack direction="row" gap={1} justifyContent="right">
direction="row"
gap={1}
justifyContent="right"
>
<IconButton <IconButton
color="info" color="info"
size="small" size="small"
@@ -116,7 +107,7 @@ const PermissionCatalogField = ({ name = 'permissions', disabled = false }: Perm
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
) );
}; };
export default PermissionCatalogField; export default PermissionCatalogField;

View File

@@ -13,14 +13,14 @@ import Typography from '@mui/material/Typography';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import DeleteRoleButton from 'components/DeleteRoleButton/index.ee'; import DeleteRoleButton from 'components/DeleteRoleButton/index.ee';
import ListLoader from 'components/ListLoader';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import useRoles from 'hooks/useRoles.ee'; import useRoles from 'hooks/useRoles.ee';
import * as URLS from 'config/urls'; import * as URLS from 'config/urls';
// TODO: introduce loading bar
export default function RoleList(): React.ReactElement { export default function RoleList(): React.ReactElement {
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const { roles } = useRoles(); const { roles, loading } = useRoles();
return ( return (
<TableContainer component={Paper}> <TableContainer component={Paper}>
@@ -49,7 +49,9 @@ export default function RoleList(): React.ReactElement {
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{roles.map((role) => ( {loading && <ListLoader rowsNumber={3} columnsNumber={2} />}
{!loading &&
roles.map((role) => (
<TableRow <TableRow
key={role.id} key={role.id}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
@@ -59,7 +61,9 @@ export default function RoleList(): React.ReactElement {
</TableCell> </TableCell>
<TableCell scope="row"> <TableCell scope="row">
<Typography variant="subtitle2">{role.description}</Typography> <Typography variant="subtitle2">
{role.description}
</Typography>
</TableCell> </TableCell>
<TableCell> <TableCell>
@@ -72,7 +76,10 @@ export default function RoleList(): React.ReactElement {
<EditIcon /> <EditIcon />
</IconButton> </IconButton>
<DeleteRoleButton disabled={role.isAdmin} roleId={role.id} /> <DeleteRoleButton
disabled={role.isAdmin}
roleId={role.id}
/>
</Stack> </Stack>
</TableCell> </TableCell>
</TableRow> </TableRow>

View File

@@ -13,11 +13,11 @@ import Typography from '@mui/material/Typography';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import DeleteUserButton from 'components/DeleteUserButton/index.ee'; import DeleteUserButton from 'components/DeleteUserButton/index.ee';
import ListLoader from 'components/ListLoader';
import useUsers from 'hooks/useUsers'; import useUsers from 'hooks/useUsers';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import * as URLS from 'config/urls'; import * as URLS from 'config/urls';
// TODO: introduce loading bar
export default function UserList(): React.ReactElement { export default function UserList(): React.ReactElement {
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const { users, loading } = useUsers(); const { users, loading } = useUsers();
@@ -49,7 +49,9 @@ export default function UserList(): React.ReactElement {
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{users.map((user) => ( {loading && <ListLoader rowsNumber={3} columnsNumber={2} />}
{!loading &&
users.map((user) => (
<TableRow <TableRow
key={user.id} key={user.id}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}

View File

@@ -3,24 +3,33 @@ import { IRole, IPermission } from '@automatisch/types';
type ComputeAction = { type ComputeAction = {
conditions: Record<string, boolean>; conditions: Record<string, boolean>;
value: boolean; value: boolean;
} };
type ComputedActions = Record<string, ComputeAction>; type ComputedActions = Record<string, ComputeAction>;
type ComputedPermissions = Record<string, ComputedActions>; type ComputedPermissions = Record<string, ComputedActions>;
export type RoleWithComputedPermissions = IRole & { computedPermissions: ComputedPermissions }; export type RoleWithComputedPermissions = IRole & {
computedPermissions: ComputedPermissions;
};
export function getRoleWithComputedPermissions(role: IRole): RoleWithComputedPermissions { export function getRoleWithComputedPermissions(
const computedPermissions = role.permissions.reduce((computedPermissions, permission) => ({ role?: IRole
): Partial<RoleWithComputedPermissions> {
if (!role) return {};
const computedPermissions = role.permissions.reduce(
(computedPermissions, permission) => ({
...computedPermissions, ...computedPermissions,
[permission.subject]: { [permission.subject]: {
...(computedPermissions[permission.subject] || {}), ...(computedPermissions[permission.subject] || {}),
[permission.action]: { [permission.action]: {
conditions: Object.fromEntries(permission conditions: Object.fromEntries(
.conditions permission.conditions.map((condition) => [condition, true])
.map(condition => [condition, true])), ),
value: true, value: true,
}, },
} },
}), {} as ComputedPermissions); }),
{} as ComputedPermissions
);
return { return {
...role, ...role,
@@ -31,29 +40,27 @@ export function getRoleWithComputedPermissions(role: IRole): RoleWithComputedPer
export function getPermissions(computedPermissions?: ComputedPermissions) { export function getPermissions(computedPermissions?: ComputedPermissions) {
if (!computedPermissions) return []; if (!computedPermissions) return [];
return Object return Object.entries(computedPermissions).reduce(
.entries(computedPermissions) (permissions, computedPermissionEntry) => {
.reduce((permissions, computedPermissionEntry) => {
const [subject, actionsWithConditions] = computedPermissionEntry; const [subject, actionsWithConditions] = computedPermissionEntry;
for (const action in actionsWithConditions) { for (const action in actionsWithConditions) {
const { const { value: permitted, conditions = {} } =
value: permitted, actionsWithConditions[action];
conditions = {},
} = actionsWithConditions[action];
if (permitted) { if (permitted) {
permissions.push({ permissions.push({
action, action,
subject, subject,
conditions: Object conditions: Object.entries(conditions)
.entries(conditions)
.filter(([, enabled]) => enabled) .filter(([, enabled]) => enabled)
.map(([condition]) => condition), .map(([condition]) => condition),
}) });
} }
} }
return permissions; return permissions;
}, [] as Partial<IPermission>[]); },
[] as Partial<IPermission>[]
);
} }

View File

@@ -3,8 +3,13 @@ import { IPermissionCatalog } from '@automatisch/types';
import { GET_PERMISSION_CATALOG } from 'graphql/queries/get-permission-catalog.ee'; import { GET_PERMISSION_CATALOG } from 'graphql/queries/get-permission-catalog.ee';
export default function usePermissionCatalog(): IPermissionCatalog { type UsePermissionCatalogReturn = {
const { data } = useQuery(GET_PERMISSION_CATALOG); permissionCatalog: IPermissionCatalog;
loading: boolean;
};
return data?.getPermissionCatalog; export default function usePermissionCatalog(): UsePermissionCatalogReturn {
const { data, loading } = useQuery(GET_PERMISSION_CATALOG);
return { permissionCatalog: data?.getPermissionCatalog, loading };
} }

View File

@@ -6,7 +6,7 @@ import { GET_ROLE } from 'graphql/queries/get-role.ee';
type QueryResponse = { type QueryResponse = {
getRole: IRole; getRole: IRole;
} };
export default function useRole(roleId?: string) { export default function useRole(roleId?: string) {
const [getRole, { data, loading }] = useLazyQuery<QueryResponse>(GET_ROLE); const [getRole, { data, loading }] = useLazyQuery<QueryResponse>(GET_ROLE);
@@ -15,14 +15,14 @@ export default function useRole(roleId?: string) {
if (roleId) { if (roleId) {
getRole({ getRole({
variables: { variables: {
id: roleId id: roleId,
} },
}); });
} }
}, [roleId]); }, [roleId]);
return { return {
role: data?.getRole, role: data?.getRole,
loading loading,
}; };
} }

View File

@@ -3,6 +3,7 @@ import LoadingButton from '@mui/lab/LoadingButton';
import Container from '@mui/material/Container'; import Container from '@mui/material/Container';
import Grid from '@mui/material/Grid'; import Grid from '@mui/material/Grid';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import Skeleton from '@mui/material/Skeleton';
import * as React from 'react'; import * as React from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { useSnackbar } from 'notistack'; import { useSnackbar } from 'notistack';
@@ -25,7 +26,6 @@ type EditRoleParams = {
roleId: string; roleId: string;
}; };
// TODO: introduce loading bar
export default function EditRole(): React.ReactElement { export default function EditRole(): React.ReactElement {
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const [updateRole, { loading }] = useMutation(UPDATE_ROLE); const [updateRole, { loading }] = useMutation(UPDATE_ROLE);
@@ -61,8 +61,6 @@ export default function EditRole(): React.ReactElement {
} }
}; };
if (roleLoading || !role) return <React.Fragment />;
const roleWithComputedPermissions = getRoleWithComputedPermissions(role); const roleWithComputedPermissions = getRoleWithComputedPermissions(role);
return ( return (
@@ -78,6 +76,14 @@ export default function EditRole(): React.ReactElement {
onSubmit={handleRoleUpdate} onSubmit={handleRoleUpdate}
> >
<Stack direction="column" gap={2}> <Stack direction="column" gap={2}>
{roleLoading && (
<>
<Skeleton variant="rounded" height={55} />
<Skeleton variant="rounded" height={55} />
</>
)}
{!roleLoading && role && (
<>
<TextField <TextField
disabled={role.isAdmin} disabled={role.isAdmin}
required={true} required={true}
@@ -92,10 +98,12 @@ export default function EditRole(): React.ReactElement {
label={formatMessage('roleForm.description')} label={formatMessage('roleForm.description')}
fullWidth fullWidth
/> />
</>
)}
<PermissionCatalogField <PermissionCatalogField
name="computedPermissions" name="computedPermissions"
disabled={role.isAdmin} disabled={role?.isAdmin}
/> />
<LoadingButton <LoadingButton
@@ -104,7 +112,7 @@ export default function EditRole(): React.ReactElement {
color="primary" color="primary"
sx={{ boxShadow: 2 }} sx={{ boxShadow: 2 }}
loading={loading} loading={loading}
disabled={role.isAdmin} disabled={role?.isAdmin || roleLoading}
> >
{formatMessage('editRole.submit')} {formatMessage('editRole.submit')}
</LoadingButton> </LoadingButton>

View File

@@ -5,6 +5,7 @@ import Container from '@mui/material/Container';
import Grid from '@mui/material/Grid'; import Grid from '@mui/material/Grid';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import MuiTextField from '@mui/material/TextField'; import MuiTextField from '@mui/material/TextField';
import Skeleton from '@mui/material/Skeleton';
import LoadingButton from '@mui/lab/LoadingButton'; import LoadingButton from '@mui/lab/LoadingButton';
import { IUser, IRole } from '@automatisch/types'; import { IUser, IRole } from '@automatisch/types';
import { useSnackbar } from 'notistack'; import { useSnackbar } from 'notistack';
@@ -28,7 +29,6 @@ function generateRoleOptions(roles: IRole[]) {
return roles?.map(({ name: label, id: value }) => ({ label, value })); return roles?.map(({ name: label, id: value }) => ({ label, value }));
} }
// TODO: introduce loading bar
export default function EditUser(): React.ReactElement { export default function EditUser(): React.ReactElement {
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const [updateUser, { loading }] = useMutation(UPDATE_USER); const [updateUser, { loading }] = useMutation(UPDATE_USER);
@@ -63,8 +63,6 @@ export default function EditUser(): React.ReactElement {
} }
}; };
if (userLoading) return <React.Fragment />;
return ( return (
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}> <Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
<Grid container item xs={12} sm={9} md={8} lg={6}> <Grid container item xs={12} sm={9} md={8} lg={6}>
@@ -73,6 +71,16 @@ export default function EditUser(): React.ReactElement {
</Grid> </Grid>
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}> <Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
{userLoading && (
<Stack direction="column" gap={2}>
<Skeleton variant="rounded" height={55} />
<Skeleton variant="rounded" height={55} />
<Skeleton variant="rounded" height={55} />
<Skeleton variant="rounded" height={45} />
</Stack>
)}
{!userLoading && (
<Form defaultValues={user} onSubmit={handleUserUpdate}> <Form defaultValues={user} onSubmit={handleUserUpdate}>
<Stack direction="column" gap={2}> <Stack direction="column" gap={2}>
<TextField <TextField
@@ -117,6 +125,7 @@ export default function EditUser(): React.ReactElement {
</LoadingButton> </LoadingButton>
</Stack> </Stack>
</Form> </Form>
)}
</Grid> </Grid>
</Grid> </Grid>
</Container> </Container>