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  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:
parent
8e1fc73221
commit
42eadef8da
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
},
|
||||
}));
|
@ -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}
|
||||
/>
|
||||
|
||||
<p className={styles.header}>
|
||||
({permissionCount} /{' '}
|
||||
{environment?.permissions?.length} permissions)
|
||||
</p>
|
||||
</div>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails className={styles.accordionBody}>
|
||||
{renderPermissions()}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnvironmentPermissionAccordion;
|
@ -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>
|
||||
);
|
||||
};
|
@ -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',
|
||||
},
|
||||
}));
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
>
|
||||
|
Loading…
Reference in New Issue
Block a user