1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-23 00:22:19 +01: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 {
roleName,
roleDesc,
permissions,
checkedPermissions,
errors,
setRoleName,
setRoleDesc,
checkedPermissions,
handlePermissionChange,
checkAllProjectPermissions,
checkAllEnvironmentPermissions,
onToggleAllProjectPermissions: checkAllProjectPermissions,
onToggleAllEnvironmentPermissions: checkAllEnvironmentPermissions,
getProjectRolePayload,
validatePermissions,
validateName,
validateNameUniqueness,
errors,
clearErrors,
getRoleKey,
} = useProjectRoleForm();
const { createRole, loading } = useProjectRolesApi();
const handleSubmit = async (e: Event) => {
const onSubmit = async (e: Event) => {
e.preventDefault();
clearErrors();
const validName = validateName();
@ -66,7 +67,7 @@ const CreateProjectRole = () => {
--data-raw '${JSON.stringify(getProjectRolePayload(), undefined, 2)}'`;
};
const handleCancel = () => {
const onCancel = () => {
navigate(GO_BACK);
};
@ -83,8 +84,9 @@ const CreateProjectRole = () => {
>
<ProjectRoleForm
errors={errors}
handleSubmit={handleSubmit}
handleCancel={handleCancel}
permissions={permissions}
onSubmit={onSubmit}
onCancel={onCancel}
roleName={roleName}
setRoleName={setRoleName}
roleDesc={roleDesc}
@ -93,7 +95,6 @@ const CreateProjectRole = () => {
handlePermissionChange={handlePermissionChange}
checkAllProjectPermissions={checkAllProjectPermissions}
checkAllEnvironmentPermissions={checkAllEnvironmentPermissions}
mode="Create"
clearErrors={clearErrors}
validateNameUniqueness={validateNameUniqueness}
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 useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useToast from 'hooks/useToast';
import { IPermission } from 'interfaces/project';
import { useNavigate } from 'react-router-dom';
import useProjectRoleForm from '../hooks/useProjectRoleForm';
import ProjectRoleForm from '../ProjectRoleForm/ProjectRoleForm';
@ -24,38 +23,20 @@ const EditProjectRole = () => {
const {
roleName,
roleDesc,
permissions,
checkedPermissions,
errors,
setRoleName,
setRoleDesc,
checkedPermissions,
handlePermissionChange,
checkAllProjectPermissions,
checkAllEnvironmentPermissions,
onToggleAllProjectPermissions,
onToggleAllEnvironmentPermissions,
getProjectRolePayload,
validatePermissions,
validateName,
errors,
clearErrors,
getRoleKey,
handleInitialCheckedPermissions,
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,
]);
} = useProjectRoleForm(role.name, role.description, role?.permissions);
const formatApiCode = () => {
return `curl --location --request PUT '${
@ -69,7 +50,7 @@ const EditProjectRole = () => {
const { refetch } = useProjectRole(projectId);
const { editRole, loading } = useProjectRolesApi();
const handleSubmit = async (e: Event) => {
const onSubmit = async (e: Event) => {
e.preventDefault();
const payload = getProjectRolePayload();
@ -93,7 +74,7 @@ const EditProjectRole = () => {
}
};
const handleCancel = () => {
const onCancel = () => {
navigate(GO_BACK);
};
@ -109,17 +90,19 @@ to resources within a project"
formatApiCode={formatApiCode}
>
<ProjectRoleForm
handleSubmit={handleSubmit}
handleCancel={handleCancel}
permissions={permissions}
onSubmit={onSubmit}
onCancel={onCancel}
roleName={roleName}
setRoleName={setRoleName}
roleDesc={roleDesc}
setRoleDesc={setRoleDesc}
checkedPermissions={checkedPermissions}
handlePermissionChange={handlePermissionChange}
checkAllProjectPermissions={checkAllProjectPermissions}
checkAllEnvironmentPermissions={checkAllEnvironmentPermissions}
mode="Edit"
checkAllProjectPermissions={onToggleAllProjectPermissions}
checkAllEnvironmentPermissions={
onToggleAllEnvironmentPermissions
}
errors={errors}
clearErrors={clearErrors}
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 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 React, { Dispatch, FC, ReactNode, SetStateAction } from 'react';
import {
ICheckedPermission,
PROJECT_CHECK_ALL_KEY,
} from '../hooks/useProjectRoleForm';
Topic as TopicIcon,
CloudCircle as CloudCircleIcon,
} 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 {
roleName: string;
roleDesc: string;
setRoleName: React.Dispatch<React.SetStateAction<string>>;
setRoleDesc: React.Dispatch<React.SetStateAction<string>>;
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;
checkAllEnvironmentPermissions: (envName: string) => void;
handleSubmit: (e: any) => void;
handleCancel: () => void;
errors: { [key: string]: string };
mode?: string;
onSubmit: (e: any) => void;
onCancel: () => void;
clearErrors: () => void;
validateNameUniqueness?: () => void;
getRoleKey: (permission: { id: number; environment?: string }) => string;
children: ReactNode;
}
const ProjectRoleForm: React.FC<IProjectRoleForm> = ({
const ProjectRoleForm: FC<IProjectRoleForm> = ({
children,
handleSubmit,
handleCancel,
roleName,
roleDesc,
checkedPermissions,
errors,
permissions,
onSubmit,
onCancel,
setRoleName,
setRoleDesc,
checkedPermissions,
handlePermissionChange,
checkAllProjectPermissions,
checkAllEnvironmentPermissions,
errors,
mode,
validateNameUniqueness,
clearErrors,
getRoleKey,
}: IProjectRoleForm) => {
const { classes: styles } = useStyles();
const { permissions } = useProjectRolePermissions({
revalidateIfStale: false,
revalidateOnReconnect: false,
revalidateOnFocus: false,
});
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 (
<form onSubmit={handleSubmit}>
<div className={styles.container}>
<p className={styles.inputDescription}>
What is your role name?
</p>
<form onSubmit={onSubmit}>
<Box sx={{ maxWidth: '400px' }}>
<Typography sx={{ mb: 1 }}>What is your role name?</Typography>
<Input
className={styles.input}
label="Role name"
value={roleName}
onChange={e => setRoleName(e.target.value)}
@ -137,41 +72,76 @@ const ProjectRoleForm: React.FC<IProjectRoleForm> = ({
onFocus={() => clearErrors()}
onBlur={validateNameUniqueness}
autoFocus
sx={{ width: '100%', marginBottom: '1rem' }}
/>
<p className={styles.inputDescription}>
What is this role for?
</p>
<Typography sx={{ mb: 1 }}>What is this role for?</Typography>
<TextField
className={styles.input}
label="Role description"
variant="outlined"
multiline
maxRows={4}
value={roleDesc}
onChange={e => setRoleDesc(e.target.value)}
sx={{ width: '100%', marginBottom: '1rem' }}
/>
</div>
<div className={styles.permissionErrorContainer}>
</Box>
<div>
<ConditionallyRender
condition={Boolean(errors.permissions)}
show={
<span className={styles.errorMessage}>
<Typography variant="body2" color="error.main">
You must select at least one permission for a role.
</span>
</Typography>
}
/>
</div>
<h3 className={styles.header}>Project permissions</h3>
<div>{renderProjectPermissions()}</div>
<h3 className={styles.header}>Environment permissions</h3>
<div>{renderEnvironmentPermissions()}</div>
<div className={styles.buttonContainer}>
<PermissionAccordion
isInitiallyExpanded
title="Project permissions"
Icon={<TopicIcon color="disabled" sx={{ mr: 1 }} />}
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}
<Button onClick={handleCancel} className={styles.cancelButton}>
<Button onClick={onCancel} sx={{ ml: 2 }}>
Cancel
</Button>
</div>
</Box>
</form>
);
};

View File

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

View File

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