1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-14 01:16:17 +02:00

Custom roles redesign (#2439)

## About the changes
Visual update to project permissions


![image](https://user-images.githubusercontent.com/2625371/201968786-81d6e068-43e0-43ba-b3d9-d8e550345409.png)

Closes [linear
1-366](https://linear.app/unleash/issue/1-366/frontend-custom-roles-redesign)

Relates to [roadmap](https://github.com/orgs/Unleash/projects/10) item:
#2251
This commit is contained in:
Tymoteusz Czech 2022-11-17 15:33:23 +01:00 committed by GitHub
parent 8e1fc73221
commit 42eadef8da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 400 additions and 570 deletions

View File

@ -17,24 +17,25 @@ const CreateProjectRole = () => {
const { const {
roleName, roleName,
roleDesc, roleDesc,
permissions,
checkedPermissions,
errors,
setRoleName, setRoleName,
setRoleDesc, setRoleDesc,
checkedPermissions,
handlePermissionChange, handlePermissionChange,
checkAllProjectPermissions, onToggleAllProjectPermissions: checkAllProjectPermissions,
checkAllEnvironmentPermissions, onToggleAllEnvironmentPermissions: checkAllEnvironmentPermissions,
getProjectRolePayload, getProjectRolePayload,
validatePermissions, validatePermissions,
validateName, validateName,
validateNameUniqueness, validateNameUniqueness,
errors,
clearErrors, clearErrors,
getRoleKey, getRoleKey,
} = useProjectRoleForm(); } = useProjectRoleForm();
const { createRole, loading } = useProjectRolesApi(); const { createRole, loading } = useProjectRolesApi();
const handleSubmit = async (e: Event) => { const onSubmit = async (e: Event) => {
e.preventDefault(); e.preventDefault();
clearErrors(); clearErrors();
const validName = validateName(); const validName = validateName();
@ -66,7 +67,7 @@ const CreateProjectRole = () => {
--data-raw '${JSON.stringify(getProjectRolePayload(), undefined, 2)}'`; --data-raw '${JSON.stringify(getProjectRolePayload(), undefined, 2)}'`;
}; };
const handleCancel = () => { const onCancel = () => {
navigate(GO_BACK); navigate(GO_BACK);
}; };
@ -83,8 +84,9 @@ const CreateProjectRole = () => {
> >
<ProjectRoleForm <ProjectRoleForm
errors={errors} errors={errors}
handleSubmit={handleSubmit} permissions={permissions}
handleCancel={handleCancel} onSubmit={onSubmit}
onCancel={onCancel}
roleName={roleName} roleName={roleName}
setRoleName={setRoleName} setRoleName={setRoleName}
roleDesc={roleDesc} roleDesc={roleDesc}
@ -93,7 +95,6 @@ const CreateProjectRole = () => {
handlePermissionChange={handlePermissionChange} handlePermissionChange={handlePermissionChange}
checkAllProjectPermissions={checkAllProjectPermissions} checkAllProjectPermissions={checkAllProjectPermissions}
checkAllEnvironmentPermissions={checkAllEnvironmentPermissions} checkAllEnvironmentPermissions={checkAllEnvironmentPermissions}
mode="Create"
clearErrors={clearErrors} clearErrors={clearErrors}
validateNameUniqueness={validateNameUniqueness} validateNameUniqueness={validateNameUniqueness}
getRoleKey={getRoleKey} getRoleKey={getRoleKey}

View File

@ -6,7 +6,6 @@ import useProjectRolesApi from 'hooks/api/actions/useProjectRolesApi/useProjectR
import useProjectRole from 'hooks/api/getters/useProjectRole/useProjectRole'; import useProjectRole from 'hooks/api/getters/useProjectRole/useProjectRole';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import { IPermission } from 'interfaces/project';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import useProjectRoleForm from '../hooks/useProjectRoleForm'; import useProjectRoleForm from '../hooks/useProjectRoleForm';
import ProjectRoleForm from '../ProjectRoleForm/ProjectRoleForm'; import ProjectRoleForm from '../ProjectRoleForm/ProjectRoleForm';
@ -24,38 +23,20 @@ const EditProjectRole = () => {
const { const {
roleName, roleName,
roleDesc, roleDesc,
permissions,
checkedPermissions,
errors,
setRoleName, setRoleName,
setRoleDesc, setRoleDesc,
checkedPermissions,
handlePermissionChange, handlePermissionChange,
checkAllProjectPermissions, onToggleAllProjectPermissions,
checkAllEnvironmentPermissions, onToggleAllEnvironmentPermissions,
getProjectRolePayload, getProjectRolePayload,
validatePermissions, validatePermissions,
validateName, validateName,
errors,
clearErrors, clearErrors,
getRoleKey, getRoleKey,
handleInitialCheckedPermissions, } = useProjectRoleForm(role.name, role.description, role?.permissions);
permissions,
} = useProjectRoleForm(role.name, role.description);
useEffect(() => {
const initialCheckedPermissions = role?.permissions?.reduce(
(acc: { [key: string]: IPermission }, curr: IPermission) => {
acc[getRoleKey(curr)] = curr;
return acc;
},
{}
);
handleInitialCheckedPermissions(initialCheckedPermissions || {});
/* eslint-disable-next-line */
}, [
role?.permissions?.length,
permissions?.project?.length,
permissions?.environments?.length,
]);
const formatApiCode = () => { const formatApiCode = () => {
return `curl --location --request PUT '${ return `curl --location --request PUT '${
@ -69,7 +50,7 @@ const EditProjectRole = () => {
const { refetch } = useProjectRole(projectId); const { refetch } = useProjectRole(projectId);
const { editRole, loading } = useProjectRolesApi(); const { editRole, loading } = useProjectRolesApi();
const handleSubmit = async (e: Event) => { const onSubmit = async (e: Event) => {
e.preventDefault(); e.preventDefault();
const payload = getProjectRolePayload(); const payload = getProjectRolePayload();
@ -93,7 +74,7 @@ const EditProjectRole = () => {
} }
}; };
const handleCancel = () => { const onCancel = () => {
navigate(GO_BACK); navigate(GO_BACK);
}; };
@ -109,17 +90,19 @@ to resources within a project"
formatApiCode={formatApiCode} formatApiCode={formatApiCode}
> >
<ProjectRoleForm <ProjectRoleForm
handleSubmit={handleSubmit} permissions={permissions}
handleCancel={handleCancel} onSubmit={onSubmit}
onCancel={onCancel}
roleName={roleName} roleName={roleName}
setRoleName={setRoleName} setRoleName={setRoleName}
roleDesc={roleDesc} roleDesc={roleDesc}
setRoleDesc={setRoleDesc} setRoleDesc={setRoleDesc}
checkedPermissions={checkedPermissions} checkedPermissions={checkedPermissions}
handlePermissionChange={handlePermissionChange} handlePermissionChange={handlePermissionChange}
checkAllProjectPermissions={checkAllProjectPermissions} checkAllProjectPermissions={onToggleAllProjectPermissions}
checkAllEnvironmentPermissions={checkAllEnvironmentPermissions} checkAllEnvironmentPermissions={
mode="Edit" onToggleAllEnvironmentPermissions
}
errors={errors} errors={errors}
clearErrors={clearErrors} clearErrors={clearErrors}
getRoleKey={getRoleKey} getRoleKey={getRoleKey}

View File

@ -1,35 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
environmentPermissionContainer: {
marginBottom: '1.25rem',
},
accordionSummary: {
boxShadow: 'none',
padding: '0',
},
label: {
minWidth: '300px',
[theme.breakpoints.down(600)]: {
minWidth: 'auto',
},
},
accordionHeader: {
display: 'flex',
alignItems: 'center',
[theme.breakpoints.down(500)]: {
flexDirection: 'column',
alignItems: 'flex-start',
},
},
accordionBody: {
padding: '0',
flexWrap: 'wrap',
},
header: {
color: theme.palette.primary.main,
},
icon: {
fill: theme.palette.primary.main,
},
}));

View File

@ -1,153 +0,0 @@
import {
Accordion,
AccordionDetails,
AccordionSummary,
Checkbox,
FormControlLabel,
} from '@mui/material';
import { ExpandMore } from '@mui/icons-material';
import { useEffect, useState } from 'react';
import {
IPermission,
IProjectEnvironmentPermissions,
} from 'interfaces/project';
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
import { ICheckedPermission } from 'component/admin/projectRoles/hooks/useProjectRoleForm';
import { useStyles } from './EnvironmentPermissionAccordion.styles';
type PermissionMap = { [key: string]: boolean };
interface IEnvironmentPermissionAccordionProps {
environment: IProjectEnvironmentPermissions;
handlePermissionChange: (permission: IPermission, type: string) => void;
checkAllEnvironmentPermissions: (envName: string) => void;
checkedPermissions: ICheckedPermission;
getRoleKey: (permission: { id: number; environment?: string }) => string;
}
const EnvironmentPermissionAccordion = ({
environment,
handlePermissionChange,
checkAllEnvironmentPermissions,
checkedPermissions,
getRoleKey,
}: IEnvironmentPermissionAccordionProps) => {
const [permissionMap, setPermissionMap] = useState<PermissionMap>({});
const [permissionCount, setPermissionCount] = useState(0);
const { classes: styles } = useStyles();
useEffect(() => {
const permissionMap = environment?.permissions?.reduce(
(acc: PermissionMap, curr: IPermission) => {
acc[getRoleKey(curr)] = true;
return acc;
},
{}
);
setPermissionMap(permissionMap);
/* eslint-disable-next-line */
}, [environment?.permissions?.length]);
useEffect(() => {
let count = 0;
Object.keys(checkedPermissions).forEach(key => {
if (permissionMap[key]) {
count = count + 1;
}
});
setPermissionCount(count);
/* eslint-disable-next-line */
}, [checkedPermissions]);
const renderPermissions = () => {
const envPermissions = environment?.permissions?.map(
(permission: IPermission) => {
return (
<FormControlLabel
classes={{ root: styles.label }}
key={getRoleKey(permission)}
control={
<Checkbox
checked={
checkedPermissions[getRoleKey(permission)]
? true
: false
}
onChange={() =>
handlePermissionChange(
permission,
environment.name
)
}
color="primary"
/>
}
label={permission.displayName}
/>
);
}
);
envPermissions.push(
<FormControlLabel
key={`check-all-environment-${environment?.name}`}
classes={{ root: styles.label }}
control={
<Checkbox
checked={
checkedPermissions[
`check-all-environment-${environment?.name}`
]
? true
: false
}
onChange={() =>
checkAllEnvironmentPermissions(environment?.name)
}
color="primary"
/>
}
label={'Select all permissions for this env'}
/>
);
return envPermissions;
};
return (
<div className={styles.environmentPermissionContainer}>
<Accordion style={{ boxShadow: 'none' }}>
<AccordionSummary
className={styles.accordionSummary}
expandIcon={
<ExpandMore
className={styles.icon}
titleAccess="Toggle"
/>
}
>
<div className={styles.accordionHeader}>
<StringTruncator
text={environment.name}
className={styles.header}
maxWidth="120"
maxLength={25}
/>
&nbsp;
<p className={styles.header}>
({permissionCount} /{' '}
{environment?.permissions?.length} permissions)
</p>
</div>
</AccordionSummary>
<AccordionDetails className={styles.accordionBody}>
{renderPermissions()}
</AccordionDetails>
</Accordion>
</div>
);
};
export default EnvironmentPermissionAccordion;

View File

@ -0,0 +1,178 @@
import { ReactNode, useMemo, useState, VFC } from 'react';
import {
Accordion,
AccordionDetails,
AccordionSummary,
Box,
Button,
Checkbox,
Divider,
FormControlLabel,
IconButton,
styled,
Typography,
} from '@mui/material';
import { ExpandMore } from '@mui/icons-material';
import { IPermission } from 'interfaces/project';
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
import { ICheckedPermission } from 'component/admin/projectRoles/hooks/useProjectRoleForm';
interface IEnvironmentPermissionAccordionProps {
permissions: IPermission[];
checkedPermissions: ICheckedPermission;
title: string;
Icon: ReactNode;
isInitiallyExpanded?: boolean;
context: 'project' | 'environment';
onPermissionChange: (permission: IPermission) => void;
onCheckAll: () => void;
getRoleKey: (permission: { id: number; environment?: string }) => string;
}
const AccordionHeader = styled(Box)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
[theme.breakpoints.down(500)]: {
flexDirection: 'column',
alignItems: 'flex-start',
},
}));
const StyledTitle = styled(StringTruncator)(({ theme }) => ({
fontWeight: theme.typography.fontWeightBold,
marginRight: theme.spacing(1),
}));
export const PermissionAccordion: VFC<IEnvironmentPermissionAccordionProps> = ({
title,
permissions,
checkedPermissions,
Icon,
isInitiallyExpanded,
context,
onPermissionChange,
onCheckAll,
getRoleKey,
}) => {
const [expanded, setExpanded] = useState(isInitiallyExpanded);
const permissionMap = useMemo(
() =>
permissions?.reduce(
(acc: Record<string, boolean>, curr: IPermission) => {
acc[getRoleKey(curr)] = true;
return acc;
},
{}
) || {},
[permissions]
);
const permissionCount = useMemo(
() =>
Object.keys(checkedPermissions).filter(key => permissionMap[key])
.length || 0,
[checkedPermissions, permissionMap]
);
const isAllChecked = useMemo(
() => permissionCount === permissions?.length,
[permissionCount, permissions]
);
return (
<Box
sx={{
my: 2,
pb: 1,
}}
>
<Accordion
expanded={expanded}
onChange={() => setExpanded(!expanded)}
sx={{
boxShadow: 'none',
px: 3,
py: 1,
border: theme => `1px solid ${theme.palette.divider}`,
borderRadius: theme => `${theme.shape.borderRadiusLarge}px`,
}}
>
<AccordionSummary
expandIcon={
<IconButton>
<ExpandMore titleAccess="Toggle" />
</IconButton>
}
sx={{
boxShadow: 'none',
padding: '0',
}}
>
<AccordionHeader>
{Icon}
<StyledTitle
text={title}
maxWidth="120"
maxLength={25}
/>{' '}
<Typography variant="body2" color="text.secondary">
({permissionCount} / {permissions?.length}{' '}
permissions)
</Typography>
</AccordionHeader>
</AccordionSummary>
<AccordionDetails
sx={{
px: 0,
py: 1,
flexWrap: 'wrap',
}}
>
<Divider sx={{ mb: 1 }} />
<Button
variant="text"
size="small"
onClick={onCheckAll}
sx={{
fontWeight: theme =>
theme.typography.fontWeightRegular,
}}
>
{isAllChecked ? 'Unselect ' : 'Select '}
all {context} permissions
</Button>
<Box>
{permissions?.map((permission: IPermission) => {
return (
<FormControlLabel
sx={{
minWidth: {
sm: '300px',
xs: 'auto',
},
}}
key={getRoleKey(permission)}
control={
<Checkbox
checked={
checkedPermissions[
getRoleKey(permission)
]
? true
: false
}
onChange={() =>
onPermissionChange(permission)
}
color="primary"
/>
}
label={permission.displayName}
/>
);
})}
</Box>
</AccordionDetails>
</Accordion>
</Box>
);
};

View File

@ -1,41 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
container: {
maxWidth: '400px',
},
input: { width: '100%', marginBottom: '1rem' },
label: {
minWidth: '300px',
[theme.breakpoints.down(600)]: {
minWidth: 'auto',
},
},
buttonContainer: {
marginTop: 'auto',
display: 'flex',
justifyContent: 'flex-end',
},
cancelButton: {
marginLeft: '1.5rem',
},
inputDescription: {
marginBottom: '0.5rem',
},
formHeader: {
fontWeight: 'normal',
marginTop: '0',
},
header: {
fontWeight: 'normal',
},
permissionErrorContainer: {
position: 'relative',
},
errorMessage: {
fontSize: theme.fontSizes.smallBody,
color: theme.palette.error.main,
position: 'absolute',
top: '-8px',
},
}));

View File

@ -1,134 +1,69 @@
import Input from 'component/common/Input/Input'; import React, { Dispatch, FC, ReactNode, SetStateAction } from 'react';
import EnvironmentPermissionAccordion from './EnvironmentPermissionAccordion/EnvironmentPermissionAccordion';
import { Button, Checkbox, FormControlLabel, TextField } from '@mui/material';
import useProjectRolePermissions from 'hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
import { useStyles } from './ProjectRoleForm.styles';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import React, { ReactNode } from 'react';
import { IPermission } from 'interfaces/project';
import { import {
ICheckedPermission, Topic as TopicIcon,
PROJECT_CHECK_ALL_KEY, CloudCircle as CloudCircleIcon,
} from '../hooks/useProjectRoleForm'; } from '@mui/icons-material';
import { Box, Button, TextField, Typography } from '@mui/material';
import Input from 'component/common/Input/Input';
import { PermissionAccordion } from './PermissionAccordion/PermissionAccordion';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import EnvironmentIcon from 'component/common/EnvironmentIcon/EnvironmentIcon';
import {
IPermission,
IProjectEnvironmentPermissions,
IProjectRolePermissions,
} from 'interfaces/project';
import { ICheckedPermission } from '../hooks/useProjectRoleForm';
interface IProjectRoleForm { interface IProjectRoleForm {
roleName: string; roleName: string;
roleDesc: string; roleDesc: string;
setRoleName: React.Dispatch<React.SetStateAction<string>>;
setRoleDesc: React.Dispatch<React.SetStateAction<string>>;
checkedPermissions: ICheckedPermission; checkedPermissions: ICheckedPermission;
handlePermissionChange: (permission: IPermission, type: string) => void; errors: { [key: string]: string };
children: ReactNode;
permissions:
| IProjectRolePermissions
| {
project: IPermission[];
environments: IProjectEnvironmentPermissions[];
};
setRoleName: Dispatch<SetStateAction<string>>;
setRoleDesc: Dispatch<SetStateAction<string>>;
handlePermissionChange: (permission: IPermission) => void;
checkAllProjectPermissions: () => void; checkAllProjectPermissions: () => void;
checkAllEnvironmentPermissions: (envName: string) => void; checkAllEnvironmentPermissions: (envName: string) => void;
handleSubmit: (e: any) => void; onSubmit: (e: any) => void;
handleCancel: () => void; onCancel: () => void;
errors: { [key: string]: string };
mode?: string;
clearErrors: () => void; clearErrors: () => void;
validateNameUniqueness?: () => void; validateNameUniqueness?: () => void;
getRoleKey: (permission: { id: number; environment?: string }) => string; getRoleKey: (permission: { id: number; environment?: string }) => string;
children: ReactNode;
} }
const ProjectRoleForm: React.FC<IProjectRoleForm> = ({ const ProjectRoleForm: FC<IProjectRoleForm> = ({
children, children,
handleSubmit,
handleCancel,
roleName, roleName,
roleDesc, roleDesc,
checkedPermissions,
errors,
permissions,
onSubmit,
onCancel,
setRoleName, setRoleName,
setRoleDesc, setRoleDesc,
checkedPermissions,
handlePermissionChange, handlePermissionChange,
checkAllProjectPermissions, checkAllProjectPermissions,
checkAllEnvironmentPermissions, checkAllEnvironmentPermissions,
errors,
mode,
validateNameUniqueness, validateNameUniqueness,
clearErrors, clearErrors,
getRoleKey, getRoleKey,
}: IProjectRoleForm) => { }: IProjectRoleForm) => {
const { classes: styles } = useStyles();
const { permissions } = useProjectRolePermissions({
revalidateIfStale: false,
revalidateOnReconnect: false,
revalidateOnFocus: false,
});
const { project, environments } = permissions; const { project, environments } = permissions;
const renderProjectPermissions = () => {
const projectPermissions = project.map(permission => {
return (
<FormControlLabel
key={getRoleKey(permission)}
classes={{ root: styles.label }}
control={
<Checkbox
checked={
checkedPermissions[getRoleKey(permission)]
? true
: false
}
onChange={() =>
handlePermissionChange(permission, 'project')
}
color="primary"
/>
}
label={permission.displayName}
/>
);
});
projectPermissions.push(
<FormControlLabel
key={PROJECT_CHECK_ALL_KEY}
classes={{ root: styles.label }}
control={
<Checkbox
checked={
checkedPermissions[PROJECT_CHECK_ALL_KEY]
? true
: false
}
onChange={() => checkAllProjectPermissions()}
color="primary"
/>
}
label={'Select all project permissions'}
/>
);
return projectPermissions;
};
const renderEnvironmentPermissions = () => {
return environments.map(environment => {
return (
<EnvironmentPermissionAccordion
environment={environment}
key={environment.name}
checkedPermissions={checkedPermissions}
handlePermissionChange={handlePermissionChange}
checkAllEnvironmentPermissions={
checkAllEnvironmentPermissions
}
getRoleKey={getRoleKey}
/>
);
});
};
return ( return (
<form onSubmit={handleSubmit}> <form onSubmit={onSubmit}>
<div className={styles.container}> <Box sx={{ maxWidth: '400px' }}>
<p className={styles.inputDescription}> <Typography sx={{ mb: 1 }}>What is your role name?</Typography>
What is your role name?
</p>
<Input <Input
className={styles.input}
label="Role name" label="Role name"
value={roleName} value={roleName}
onChange={e => setRoleName(e.target.value)} onChange={e => setRoleName(e.target.value)}
@ -137,41 +72,76 @@ const ProjectRoleForm: React.FC<IProjectRoleForm> = ({
onFocus={() => clearErrors()} onFocus={() => clearErrors()}
onBlur={validateNameUniqueness} onBlur={validateNameUniqueness}
autoFocus autoFocus
sx={{ width: '100%', marginBottom: '1rem' }}
/> />
<p className={styles.inputDescription}> <Typography sx={{ mb: 1 }}>What is this role for?</Typography>
What is this role for?
</p>
<TextField <TextField
className={styles.input}
label="Role description" label="Role description"
variant="outlined" variant="outlined"
multiline multiline
maxRows={4} maxRows={4}
value={roleDesc} value={roleDesc}
onChange={e => setRoleDesc(e.target.value)} onChange={e => setRoleDesc(e.target.value)}
sx={{ width: '100%', marginBottom: '1rem' }}
/> />
</div> </Box>
<div className={styles.permissionErrorContainer}> <div>
<ConditionallyRender <ConditionallyRender
condition={Boolean(errors.permissions)} condition={Boolean(errors.permissions)}
show={ show={
<span className={styles.errorMessage}> <Typography variant="body2" color="error.main">
You must select at least one permission for a role. You must select at least one permission for a role.
</span> </Typography>
} }
/> />
</div> </div>
<h3 className={styles.header}>Project permissions</h3> <PermissionAccordion
<div>{renderProjectPermissions()}</div> isInitiallyExpanded
<h3 className={styles.header}>Environment permissions</h3> title="Project permissions"
<div>{renderEnvironmentPermissions()}</div> Icon={<TopicIcon color="disabled" sx={{ mr: 1 }} />}
<div className={styles.buttonContainer}> permissions={project}
checkedPermissions={checkedPermissions}
onPermissionChange={(permission: IPermission) =>
handlePermissionChange(permission)
}
onCheckAll={checkAllProjectPermissions}
getRoleKey={getRoleKey}
context="project"
/>
<div>
{environments.map(environment => (
<PermissionAccordion
title={environment.name}
Icon={
<CloudCircleIcon sx={{ mr: 1 }} color="disabled" />
}
permissions={environment.permissions}
key={environment.name}
checkedPermissions={checkedPermissions}
onPermissionChange={(permission: IPermission) =>
handlePermissionChange(permission)
}
onCheckAll={() =>
checkAllEnvironmentPermissions(environment.name)
}
getRoleKey={getRoleKey}
context="environment"
/>
))}
</div>
<Box
sx={{
marginTop: 'auto',
display: 'flex',
justifyContent: 'flex-end',
}}
>
{children} {children}
<Button onClick={handleCancel} className={styles.cancelButton}> <Button onClick={onCancel} sx={{ ml: 2 }}>
Cancel Cancel
</Button> </Button>
</div> </Box>
</form> </form>
); );
}; };

View File

@ -14,13 +14,19 @@ export interface ICheckedPermission {
[key: string]: IPermission; [key: string]: IPermission;
} }
export const PROJECT_CHECK_ALL_KEY = 'check-all-project'; const getRoleKey = (permission: {
export const ENVIRONMENT_CHECK_ALL_KEY = 'check-all-environment'; id: number;
environment?: string;
}): string => {
return permission.environment
? `${permission.id}-${permission.environment}`
: `${permission.id}`;
};
const useProjectRoleForm = ( const useProjectRoleForm = (
initialRoleName = '', initialRoleName = '',
initialRoleDesc = '', initialRoleDesc = '',
initialCheckedPermissions = {} initialCheckedPermissions: IPermission[] = []
) => { ) => {
const { uiConfig } = useUiConfig(); const { uiConfig } = useUiConfig();
const { permissions } = useProjectRolePermissions({ const { permissions } = useProjectRolePermissions({
@ -32,7 +38,25 @@ const useProjectRoleForm = (
const [roleName, setRoleName] = useState(initialRoleName); const [roleName, setRoleName] = useState(initialRoleName);
const [roleDesc, setRoleDesc] = useState(initialRoleDesc); const [roleDesc, setRoleDesc] = useState(initialRoleDesc);
const [checkedPermissions, setCheckedPermissions] = const [checkedPermissions, setCheckedPermissions] =
useState<ICheckedPermission>(initialCheckedPermissions); useState<ICheckedPermission>({});
useEffect(() => {
if (initialCheckedPermissions.length > 0) {
setCheckedPermissions(
initialCheckedPermissions?.reduce(
(
acc: { [key: string]: IPermission },
curr: IPermission
) => {
acc[getRoleKey(curr)] = curr;
return acc;
},
{}
)
);
}
}, [initialCheckedPermissions?.length]);
const [errors, setErrors] = useState({}); const [errors, setErrors] = useState({});
const { validateRole } = useProjectRolesApi(); const { validateRole } = useProjectRolesApi();
@ -45,71 +69,7 @@ const useProjectRoleForm = (
setRoleDesc(initialRoleDesc); setRoleDesc(initialRoleDesc);
}, [initialRoleDesc]); }, [initialRoleDesc]);
const handleInitialCheckedPermissions = ( const handlePermissionChange = (permission: IPermission) => {
initialCheckedPermissions: ICheckedPermission
) => {
const formattedInitialCheckedPermissions =
isAllEnvironmentPermissionsChecked(
// @ts-expect-error
isAllProjectPermissionsChecked(initialCheckedPermissions)
);
setCheckedPermissions(formattedInitialCheckedPermissions || {});
};
const isAllProjectPermissionsChecked = (
initialCheckedPermissions: ICheckedPermission
) => {
const { project } = permissions;
if (!project || project.length === 0) return;
const isAllChecked = project.every((permission: IPermission) => {
return initialCheckedPermissions[getRoleKey(permission)];
});
if (isAllChecked) {
// @ts-expect-error
initialCheckedPermissions[PROJECT_CHECK_ALL_KEY] = true;
} else {
delete initialCheckedPermissions[PROJECT_CHECK_ALL_KEY];
}
return initialCheckedPermissions;
};
const isAllEnvironmentPermissionsChecked = (
initialCheckedPermissions: ICheckedPermission
) => {
const { environments } = permissions;
if (!environments || environments.length === 0) return;
environments.forEach(env => {
const isAllChecked = env.permissions.every(
(permission: IPermission) => {
return initialCheckedPermissions[getRoleKey(permission)];
}
);
const key = `${ENVIRONMENT_CHECK_ALL_KEY}-${env.name}`;
if (isAllChecked) {
// @ts-expect-error
initialCheckedPermissions[key] = true;
} else {
delete initialCheckedPermissions[key];
}
});
return initialCheckedPermissions;
};
const getCheckAllKeys = () => {
const { environments } = permissions;
const envKeys = environments.map(env => {
return `${ENVIRONMENT_CHECK_ALL_KEY}-${env.name}`;
});
return [...envKeys, PROJECT_CHECK_ALL_KEY];
};
const handlePermissionChange = (permission: IPermission, type: string) => {
let checkedPermissionsCopy = cloneDeep(checkedPermissions); let checkedPermissionsCopy = cloneDeep(checkedPermissions);
if (checkedPermissionsCopy[getRoleKey(permission)]) { if (checkedPermissionsCopy[getRoleKey(permission)]) {
@ -118,98 +78,64 @@ const useProjectRoleForm = (
checkedPermissionsCopy[getRoleKey(permission)] = { ...permission }; checkedPermissionsCopy[getRoleKey(permission)] = { ...permission };
} }
if (type === 'project') { setCheckedPermissions(checkedPermissionsCopy);
// @ts-expect-error };
checkedPermissionsCopy = isAllProjectPermissionsChecked(
checkedPermissionsCopy const onToggleAllProjectPermissions = () => {
); const { project } = permissions;
let checkedPermissionsCopy = cloneDeep(checkedPermissions);
const allChecked = project.every(
(permission: IPermission) =>
checkedPermissionsCopy[getRoleKey(permission)]
);
if (allChecked) {
project.forEach((permission: IPermission) => {
delete checkedPermissionsCopy[getRoleKey(permission)];
});
} else { } else {
// @ts-expect-error project.forEach((permission: IPermission) => {
checkedPermissionsCopy = isAllEnvironmentPermissionsChecked( checkedPermissionsCopy[getRoleKey(permission)] = {
checkedPermissionsCopy ...permission,
); };
});
} }
setCheckedPermissions(checkedPermissionsCopy); setCheckedPermissions(checkedPermissionsCopy);
}; };
const checkAllProjectPermissions = () => { const onToggleAllEnvironmentPermissions = (envName: string) => {
const { project } = permissions;
const checkedPermissionsCopy = cloneDeep(checkedPermissions);
const checkedAll = checkedPermissionsCopy[PROJECT_CHECK_ALL_KEY];
project.forEach((permission: IPermission, index: number) => {
const lastItem = project.length - 1 === index;
if (checkedAll) {
if (checkedPermissionsCopy[getRoleKey(permission)]) {
delete checkedPermissionsCopy[getRoleKey(permission)];
}
if (lastItem) {
delete checkedPermissionsCopy[PROJECT_CHECK_ALL_KEY];
}
} else {
checkedPermissionsCopy[getRoleKey(permission)] = {
...permission,
};
if (lastItem) {
// @ts-expect-error
checkedPermissionsCopy[PROJECT_CHECK_ALL_KEY] = true;
}
}
});
setCheckedPermissions(checkedPermissionsCopy);
};
const checkAllEnvironmentPermissions = (envName: string) => {
const { environments } = permissions; const { environments } = permissions;
const checkedPermissionsCopy = cloneDeep(checkedPermissions); const checkedPermissionsCopy = cloneDeep(checkedPermissions);
const environmentCheckAllKey = `${ENVIRONMENT_CHECK_ALL_KEY}-${envName}`;
const env = environments.find(env => env.name === envName); const env = environments.find(env => env.name === envName);
if (!env) return; if (!env) return;
const checkedAll = checkedPermissionsCopy[environmentCheckAllKey];
env.permissions.forEach((permission: IPermission, index: number) => { const allChecked = env.permissions.every(
const lastItem = env.permissions.length - 1 === index; (permission: IPermission) =>
if (checkedAll) { checkedPermissionsCopy[getRoleKey(permission)]
if (checkedPermissionsCopy[getRoleKey(permission)]) { );
delete checkedPermissionsCopy[getRoleKey(permission)];
}
if (lastItem) { if (allChecked) {
delete checkedPermissionsCopy[environmentCheckAllKey]; env.permissions.forEach((permission: IPermission) => {
} delete checkedPermissionsCopy[getRoleKey(permission)];
} else { });
} else {
env.permissions.forEach((permission: IPermission) => {
checkedPermissionsCopy[getRoleKey(permission)] = { checkedPermissionsCopy[getRoleKey(permission)] = {
...permission, ...permission,
}; };
});
if (lastItem) { }
// @ts-expect-error
checkedPermissionsCopy[environmentCheckAllKey] = true;
}
}
});
setCheckedPermissions(checkedPermissionsCopy); setCheckedPermissions(checkedPermissionsCopy);
}; };
const getProjectRolePayload = () => { const getProjectRolePayload = () => ({
const checkAllKeys = getCheckAllKeys(); name: roleName,
const permissions = Object.keys(checkedPermissions) description: roleDesc,
.filter(key => { permissions: Object.values(checkedPermissions),
return !checkAllKeys.includes(key); });
})
.map(permission => {
return checkedPermissions[permission];
});
return {
name: roleName,
description: roleDesc,
permissions,
};
};
const validateNameUniqueness = async () => { const validateNameUniqueness = async () => {
const payload = getProjectRolePayload(); const payload = getProjectRolePayload();
@ -244,15 +170,7 @@ const useProjectRoleForm = (
setErrors({}); setErrors({});
}; };
const getRoleKey = (permission: { // TODO: Clean up when feature is complete - changeRequests
id: number;
environment?: string;
}): string => {
return permission.environment
? `${permission.id}-${permission.environment}`
: `${permission.id}`;
};
// Clean up when feature is complete changeRequests
let filteredPermissions = cloneDeep(permissions); let filteredPermissions = cloneDeep(permissions);
if (!uiConfig?.flags.changeRequests) { if (!uiConfig?.flags.changeRequests) {
@ -278,21 +196,20 @@ const useProjectRoleForm = (
return { return {
roleName, roleName,
roleDesc, roleDesc,
errors,
checkedPermissions,
permissions: filteredPermissions,
setRoleName, setRoleName,
setRoleDesc, setRoleDesc,
handlePermissionChange, handlePermissionChange,
checkAllProjectPermissions, onToggleAllProjectPermissions,
checkAllEnvironmentPermissions, onToggleAllEnvironmentPermissions,
checkedPermissions,
getProjectRolePayload, getProjectRolePayload,
validatePermissions, validatePermissions,
validateName, validateName,
handleInitialCheckedPermissions,
clearErrors, clearErrors,
validateNameUniqueness, validateNameUniqueness,
errors,
getRoleKey, getRoleKey,
permissions: filteredPermissions,
}; };
}; };

View File

@ -25,29 +25,39 @@ interface IChangeRequestProps {
onNavigate?: () => void; onNavigate?: () => void;
} }
const StyledSingleChangeBox = styled(Box)<{ const StyledSingleChangeBox = styled(Box, {
hasConflict: boolean; shouldForwardProp: (prop: string) => !prop.startsWith('$'),
isAfterWarning: boolean; })<{
isLast: boolean; $hasConflict: boolean;
inInConflictFeature: boolean; $isAfterWarning: boolean;
}>(({ theme, hasConflict, inInConflictFeature, isAfterWarning, isLast }) => ({ $isLast: boolean;
borderLeft: '1px solid', $isInConflictFeature: boolean;
borderRight: '1px solid', }>(
borderTop: '1px solid', ({
borderBottom: isLast ? '1px solid' : 'none', theme,
borderRadius: isLast $hasConflict,
? `0 0 $isInConflictFeature,
$isAfterWarning,
$isLast,
}) => ({
borderLeft: '1px solid',
borderRight: '1px solid',
borderTop: '1px solid',
borderBottom: $isLast ? '1px solid' : 'none',
borderRadius: $isLast
? `0 0
${theme.shape.borderRadiusLarge}px ${theme.shape.borderRadiusLarge}px` ${theme.shape.borderRadiusLarge}px ${theme.shape.borderRadiusLarge}px`
: 0, : 0,
borderColor: borderColor:
hasConflict || inInConflictFeature $hasConflict || $isInConflictFeature
? theme.palette.warning.border ? theme.palette.warning.border
: theme.palette.dividerAlternative, : theme.palette.dividerAlternative,
borderTopColor: borderTopColor:
(hasConflict || isAfterWarning) && !inInConflictFeature ($hasConflict || $isAfterWarning) && !$isInConflictFeature
? theme.palette.warning.border ? theme.palette.warning.border
: theme.palette.dividerAlternative, : theme.palette.dividerAlternative,
})); })
);
const StyledAlert = styled(Alert)(({ theme }) => ({ const StyledAlert = styled(Alert)(({ theme }) => ({
borderRadius: 0, borderRadius: 0,
@ -94,14 +104,14 @@ export const ChangeRequest: VFC<IChangeRequestProps> = ({
{featureToggleChange.changes.map((change, index) => ( {featureToggleChange.changes.map((change, index) => (
<StyledSingleChangeBox <StyledSingleChangeBox
key={objectId(change)} key={objectId(change)}
hasConflict={Boolean(change.conflict)} $hasConflict={Boolean(change.conflict)}
inInConflictFeature={Boolean( $isInConflictFeature={Boolean(
featureToggleChange.conflict featureToggleChange.conflict
)} )}
isAfterWarning={Boolean( $isAfterWarning={Boolean(
featureToggleChange.changes[index - 1]?.conflict featureToggleChange.changes[index - 1]?.conflict
)} )}
isLast={ $isLast={
index + 1 === featureToggleChange.changes.length index + 1 === featureToggleChange.changes.length
} }
> >