mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-10 01:16:39 +02:00
feat: roles unification (#3999)
https://linear.app/unleash/issue/2-1137/roles-unification-on-the-ui Root and project roles should be managed in a similar manner, which means using the same roles route and tab for both. Additionally, this includes a big revamp to the project roles to align them more closely with the modern and standardized custom root roles that were recently developed. They mostly use the same components. There are still more things we want to improve and unify, but we've left some of that out of this PR due to PR size concerns.
This commit is contained in:
parent
60f4ce31f7
commit
eb8f16da8d
@ -13,10 +13,7 @@ import { InstanceAdmin } from './instance-admin/InstanceAdmin';
|
||||
import { MaintenanceAdmin } from './maintenance';
|
||||
import AdminMenu from './menu/AdminMenu';
|
||||
import { Network } from './network/Network';
|
||||
import CreateProjectRole from './projectRoles/CreateProjectRole/CreateProjectRole';
|
||||
import EditProjectRole from './projectRoles/EditProjectRole/EditProjectRole';
|
||||
import { Roles } from './roles/Roles';
|
||||
import ProjectRoles from './projectRoles/ProjectRoles/ProjectRoles';
|
||||
import { ServiceAccounts } from './serviceAccounts/ServiceAccounts';
|
||||
import CreateUser from './users/CreateUser/CreateUser';
|
||||
import EditUser from './users/EditUser/EditUser';
|
||||
@ -28,11 +25,6 @@ export const Admin = () => (
|
||||
<AdminMenu />
|
||||
<Routes>
|
||||
<Route path="users" element={<UsersAdmin />} />
|
||||
<Route path="project-roles/new" element={<CreateProjectRole />} />
|
||||
<Route
|
||||
path="project-roles/:id/edit"
|
||||
element={<EditProjectRole />}
|
||||
/>
|
||||
<Route path="api" element={<ApiTokenPage />} />
|
||||
<Route path="api/create-token" element={<CreateApiToken />} />
|
||||
<Route path="users/:id/edit" element={<EditUser />} />
|
||||
@ -46,8 +38,7 @@ export const Admin = () => (
|
||||
element={<EditGroupContainer />}
|
||||
/>
|
||||
<Route path="groups/:groupId" element={<Group />} />
|
||||
<Route path="roles" element={<Roles />} />
|
||||
<Route path="project-roles" element={<ProjectRoles />} />
|
||||
<Route path="roles/*" element={<Roles />} />
|
||||
<Route path="instance" element={<InstanceAdmin />} />
|
||||
<Route path="network/*" element={<Network />} />
|
||||
<Route path="maintenance" element={<MaintenanceAdmin />} />
|
||||
|
@ -55,7 +55,7 @@ function AdminMenu() {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{flags.customRootRoles && (
|
||||
{isEnterprise() && (
|
||||
<Tab
|
||||
value="roles"
|
||||
label={
|
||||
@ -65,16 +65,6 @@ function AdminMenu() {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{flags.RE && (
|
||||
<Tab
|
||||
value="project-roles"
|
||||
label={
|
||||
<CenteredNavLink to="/admin/project-roles">
|
||||
<span>Project roles</span>
|
||||
</CenteredNavLink>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Tab
|
||||
value="api"
|
||||
label={
|
||||
|
@ -1,108 +0,0 @@
|
||||
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
||||
import { useRolesApi } from 'hooks/api/actions/useRolesApi/useRolesApi';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import ProjectRoleForm from '../ProjectRoleForm/ProjectRoleForm';
|
||||
import useProjectRoleForm from '../hooks/useProjectRoleForm';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { CreateButton } from 'component/common/CreateButton/CreateButton';
|
||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { GO_BACK } from 'constants/navigate';
|
||||
|
||||
const CreateProjectRole = () => {
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { uiConfig } = useUiConfig();
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
roleName,
|
||||
roleDesc,
|
||||
permissions,
|
||||
checkedPermissions,
|
||||
errors,
|
||||
setRoleName,
|
||||
setRoleDesc,
|
||||
handlePermissionChange,
|
||||
onToggleAllProjectPermissions: checkAllProjectPermissions,
|
||||
onToggleAllEnvironmentPermissions: checkAllEnvironmentPermissions,
|
||||
getProjectRolePayload,
|
||||
validatePermissions,
|
||||
validateName,
|
||||
validateNameUniqueness,
|
||||
clearErrors,
|
||||
getRoleKey,
|
||||
} = useProjectRoleForm();
|
||||
|
||||
const { addRole, loading } = useRolesApi();
|
||||
|
||||
const onSubmit = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
clearErrors();
|
||||
const validName = validateName();
|
||||
const validPermissions = validatePermissions();
|
||||
|
||||
if (validName && validPermissions) {
|
||||
const payload = getProjectRolePayload();
|
||||
try {
|
||||
await addRole(payload);
|
||||
navigate('/admin/project-roles');
|
||||
setToastData({
|
||||
title: 'Project role created',
|
||||
text: 'Now you can start assigning your project roles to project members.',
|
||||
confetti: true,
|
||||
type: 'success',
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const formatApiCode = () => {
|
||||
return `curl --location --request POST '${
|
||||
uiConfig.unleashUrl
|
||||
}/api/admin/roles' \\
|
||||
--header 'Authorization: INSERT_API_KEY' \\
|
||||
--header 'Content-Type: application/json' \\
|
||||
--data-raw '${JSON.stringify(getProjectRolePayload(), undefined, 2)}'`;
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
navigate(GO_BACK);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormTemplate
|
||||
loading={loading}
|
||||
title="Create project role"
|
||||
description="A project role can be
|
||||
customised to limit access
|
||||
to resources within a project"
|
||||
documentationLink="https://docs.getunleash.io/reference/rbac#custom-project-roles"
|
||||
documentationLinkLabel="Project roles documentation"
|
||||
formatApiCode={formatApiCode}
|
||||
>
|
||||
<ProjectRoleForm
|
||||
errors={errors}
|
||||
permissions={permissions}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={onCancel}
|
||||
roleName={roleName}
|
||||
setRoleName={setRoleName}
|
||||
roleDesc={roleDesc}
|
||||
setRoleDesc={setRoleDesc}
|
||||
checkedPermissions={checkedPermissions}
|
||||
handlePermissionChange={handlePermissionChange}
|
||||
checkAllProjectPermissions={checkAllProjectPermissions}
|
||||
checkAllEnvironmentPermissions={checkAllEnvironmentPermissions}
|
||||
clearErrors={clearErrors}
|
||||
validateNameUniqueness={validateNameUniqueness}
|
||||
getRoleKey={getRoleKey}
|
||||
>
|
||||
<CreateButton name="role" permission={ADMIN} />
|
||||
</ProjectRoleForm>
|
||||
</FormTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateProjectRole;
|
@ -1,114 +0,0 @@
|
||||
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
||||
import { UpdateButton } from 'component/common/UpdateButton/UpdateButton';
|
||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||
import { useRolesApi } from 'hooks/api/actions/useRolesApi/useRolesApi';
|
||||
import { useRole } from 'hooks/api/getters/useRole/useRole';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import useProjectRoleForm from '../hooks/useProjectRoleForm';
|
||||
import ProjectRoleForm from '../ProjectRoleForm/ProjectRoleForm';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { GO_BACK } from 'constants/navigate';
|
||||
|
||||
const EditProjectRole = () => {
|
||||
const { uiConfig } = useUiConfig();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const roleId = useRequiredPathParam('id');
|
||||
const { role, refetch } = useRole(roleId);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
roleName,
|
||||
roleDesc,
|
||||
permissions,
|
||||
checkedPermissions,
|
||||
errors,
|
||||
setRoleName,
|
||||
setRoleDesc,
|
||||
handlePermissionChange,
|
||||
onToggleAllProjectPermissions,
|
||||
onToggleAllEnvironmentPermissions,
|
||||
getProjectRolePayload,
|
||||
validatePermissions,
|
||||
validateName,
|
||||
clearErrors,
|
||||
getRoleKey,
|
||||
} = useProjectRoleForm(role?.name, role?.description, role?.permissions);
|
||||
|
||||
const formatApiCode = () => {
|
||||
return `curl --location --request PUT '${
|
||||
uiConfig.unleashUrl
|
||||
}/api/admin/roles/${role?.id}' \\
|
||||
--header 'Authorization: INSERT_API_KEY' \\
|
||||
--header 'Content-Type: application/json' \\
|
||||
--data-raw '${JSON.stringify(getProjectRolePayload(), undefined, 2)}'`;
|
||||
};
|
||||
|
||||
const { updateRole, loading } = useRolesApi();
|
||||
|
||||
const onSubmit = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
const payload = getProjectRolePayload();
|
||||
|
||||
const validName = validateName();
|
||||
const validPermissions = validatePermissions();
|
||||
|
||||
if (validName && validPermissions) {
|
||||
try {
|
||||
await updateRole(+roleId, payload);
|
||||
refetch();
|
||||
navigate('/admin/project-roles');
|
||||
setToastData({
|
||||
type: 'success',
|
||||
title: 'Project role updated',
|
||||
text: 'Your role changes will automatically be applied to the users with this role.',
|
||||
confetti: true,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
navigate(GO_BACK);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormTemplate
|
||||
loading={loading}
|
||||
title="Edit project role"
|
||||
description="A project role can be
|
||||
customised to limit access
|
||||
to resources within a project"
|
||||
documentationLink="https://docs.getunleash.io/reference/rbac#custom-project-roles"
|
||||
documentationLinkLabel="Project roles documentation"
|
||||
formatApiCode={formatApiCode}
|
||||
>
|
||||
<ProjectRoleForm
|
||||
permissions={permissions}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={onCancel}
|
||||
roleName={roleName}
|
||||
setRoleName={setRoleName}
|
||||
roleDesc={roleDesc}
|
||||
setRoleDesc={setRoleDesc}
|
||||
checkedPermissions={checkedPermissions}
|
||||
handlePermissionChange={handlePermissionChange}
|
||||
checkAllProjectPermissions={onToggleAllProjectPermissions}
|
||||
checkAllEnvironmentPermissions={
|
||||
onToggleAllEnvironmentPermissions
|
||||
}
|
||||
errors={errors}
|
||||
clearErrors={clearErrors}
|
||||
getRoleKey={getRoleKey}
|
||||
>
|
||||
<UpdateButton permission={ADMIN} />
|
||||
</ProjectRoleForm>
|
||||
</FormTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditProjectRole;
|
@ -1,148 +0,0 @@
|
||||
import React, { Dispatch, FC, ReactNode, SetStateAction } from 'react';
|
||||
import {
|
||||
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 {
|
||||
IPermission,
|
||||
IProjectEnvironmentPermissions,
|
||||
IPermissions,
|
||||
} from 'interfaces/permissions';
|
||||
import { ICheckedPermission } from '../hooks/useProjectRoleForm';
|
||||
|
||||
interface IProjectRoleForm {
|
||||
roleName: string;
|
||||
roleDesc: string;
|
||||
checkedPermissions: ICheckedPermission;
|
||||
errors: { [key: string]: string };
|
||||
children: ReactNode;
|
||||
permissions:
|
||||
| IPermissions
|
||||
| {
|
||||
project: IPermission[];
|
||||
environments: IProjectEnvironmentPermissions[];
|
||||
};
|
||||
setRoleName: Dispatch<SetStateAction<string>>;
|
||||
setRoleDesc: Dispatch<SetStateAction<string>>;
|
||||
handlePermissionChange: (permission: IPermission) => void;
|
||||
checkAllProjectPermissions: () => void;
|
||||
checkAllEnvironmentPermissions: (envName: string) => void;
|
||||
onSubmit: (e: any) => void;
|
||||
onCancel: () => void;
|
||||
clearErrors: () => void;
|
||||
validateNameUniqueness?: () => void;
|
||||
getRoleKey: (permission: { id: number; environment?: string }) => string;
|
||||
}
|
||||
|
||||
const ProjectRoleForm: FC<IProjectRoleForm> = ({
|
||||
children,
|
||||
roleName,
|
||||
roleDesc,
|
||||
checkedPermissions,
|
||||
errors,
|
||||
permissions,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
setRoleName,
|
||||
setRoleDesc,
|
||||
handlePermissionChange,
|
||||
checkAllProjectPermissions,
|
||||
checkAllEnvironmentPermissions,
|
||||
validateNameUniqueness,
|
||||
clearErrors,
|
||||
getRoleKey,
|
||||
}: IProjectRoleForm) => {
|
||||
const { project, environments } = permissions;
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit}>
|
||||
<Box sx={{ maxWidth: '400px' }}>
|
||||
<Typography sx={{ mb: 1 }}>What is your role name?</Typography>
|
||||
<Input
|
||||
label="Role name"
|
||||
value={roleName}
|
||||
onChange={e => setRoleName(e.target.value)}
|
||||
error={Boolean(errors.name)}
|
||||
errorText={errors.name}
|
||||
onFocus={() => clearErrors()}
|
||||
onBlur={validateNameUniqueness}
|
||||
autoFocus
|
||||
sx={{ width: '100%', marginBottom: '1rem' }}
|
||||
/>
|
||||
|
||||
<Typography sx={{ mb: 1 }}>What is this role for?</Typography>
|
||||
<TextField
|
||||
label="Role description"
|
||||
variant="outlined"
|
||||
multiline
|
||||
maxRows={4}
|
||||
value={roleDesc}
|
||||
onChange={e => setRoleDesc(e.target.value)}
|
||||
sx={{ width: '100%', marginBottom: '1rem' }}
|
||||
/>
|
||||
</Box>
|
||||
<div>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(errors.permissions)}
|
||||
show={
|
||||
<Typography variant="body2" color="error.main">
|
||||
You must select at least one permission for a role.
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<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={onCancel} sx={{ marginLeft: 2 }}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectRoleForm;
|
@ -1,74 +0,0 @@
|
||||
import { Alert, styled } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { IProjectRole } from 'interfaces/role';
|
||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||
import Input from 'component/common/Input/Input';
|
||||
|
||||
interface IProjectRoleDeleteConfirmProps {
|
||||
role: IProjectRole;
|
||||
open: boolean;
|
||||
setDialogue: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
handleDeleteRole: (id: number) => Promise<void>;
|
||||
confirmName: string;
|
||||
setConfirmName: React.Dispatch<React.SetStateAction<string>>;
|
||||
}
|
||||
|
||||
const DeleteParagraph = styled('p')(({ theme }) => ({
|
||||
marginTop: theme.spacing(3),
|
||||
}));
|
||||
|
||||
const RoleDeleteInput = styled(Input)(({ theme }) => ({
|
||||
marginTop: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const ProjectRoleDeleteConfirm = ({
|
||||
role,
|
||||
open,
|
||||
setDialogue,
|
||||
handleDeleteRole,
|
||||
confirmName,
|
||||
setConfirmName,
|
||||
}: IProjectRoleDeleteConfirmProps) => {
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setConfirmName(e.currentTarget.value);
|
||||
|
||||
const handleCancel = () => {
|
||||
setDialogue(false);
|
||||
setConfirmName('');
|
||||
};
|
||||
const formId = 'delete-project-role-confirmation-form';
|
||||
return (
|
||||
<Dialogue
|
||||
title="Are you sure you want to delete this role?"
|
||||
open={open}
|
||||
primaryButtonText="Delete project role"
|
||||
secondaryButtonText="Cancel"
|
||||
onClick={() => handleDeleteRole(role.id)}
|
||||
disabledPrimaryButton={role?.name !== confirmName}
|
||||
onClose={handleCancel}
|
||||
formId={formId}
|
||||
>
|
||||
<Alert severity="error">
|
||||
Danger. Deleting this role will result in removing all
|
||||
permissions that are active in this environment across all
|
||||
feature toggles.
|
||||
</Alert>
|
||||
|
||||
<DeleteParagraph>
|
||||
In order to delete this role, please enter the name of the role
|
||||
in the textfield below: <strong>{role?.name}</strong>
|
||||
</DeleteParagraph>
|
||||
|
||||
<form id={formId}>
|
||||
<RoleDeleteInput
|
||||
autoFocus
|
||||
onChange={handleChange}
|
||||
value={confirmName}
|
||||
label="Role name"
|
||||
/>
|
||||
</form>
|
||||
</Dialogue>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectRoleDeleteConfirm;
|
@ -1,270 +0,0 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
Table,
|
||||
SortableTableHeader,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableRow,
|
||||
TablePlaceholder,
|
||||
} from 'component/common/Table';
|
||||
import { useTable, useGlobalFilter, useSortBy } from 'react-table';
|
||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||
import { useRoles } from 'hooks/api/getters/useRoles/useRoles';
|
||||
import { IProjectRole } from 'interfaces/role';
|
||||
import { useRolesApi } from 'hooks/api/actions/useRolesApi/useRolesApi';
|
||||
import useToast from 'hooks/useToast';
|
||||
import ProjectRoleDeleteConfirm from '../ProjectRoleDeleteConfirm/ProjectRoleDeleteConfirm';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { Box, Button, useMediaQuery } from '@mui/material';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||
import { Delete, Edit, SupervisedUserCircle } from '@mui/icons-material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||
import { sortTypes } from 'utils/sortTypes';
|
||||
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
|
||||
import theme from 'themes/theme';
|
||||
import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
|
||||
import { Search } from 'component/common/Search/Search';
|
||||
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
|
||||
|
||||
const BUILTIN_ROLE_TYPE = 'project';
|
||||
|
||||
const ProjectRoleList = () => {
|
||||
const navigate = useNavigate();
|
||||
const { projectRoles: data, refetch, loading } = useRoles();
|
||||
|
||||
const isExtraSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
|
||||
const { removeRole } = useRolesApi();
|
||||
const [currentRole, setCurrentRole] = useState<IProjectRole | null>(null);
|
||||
const [delDialog, setDelDialog] = useState(false);
|
||||
const [confirmName, setConfirmName] = useState('');
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
|
||||
const deleteProjectRole = async () => {
|
||||
if (!currentRole?.id) return;
|
||||
try {
|
||||
await removeRole(currentRole?.id);
|
||||
refetch();
|
||||
setToastData({
|
||||
type: 'success',
|
||||
title: 'Successfully deleted role',
|
||||
text: 'Your role is now deleted',
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
setDelDialog(false);
|
||||
setConfirmName('');
|
||||
};
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'Icon',
|
||||
Cell: () => (
|
||||
<IconCell
|
||||
icon={<SupervisedUserCircle color="disabled" />}
|
||||
/>
|
||||
),
|
||||
disableGlobalFilter: true,
|
||||
},
|
||||
{
|
||||
Header: 'Project role',
|
||||
accessor: 'name',
|
||||
},
|
||||
{
|
||||
Header: 'Description',
|
||||
accessor: 'description',
|
||||
width: '90%',
|
||||
},
|
||||
{
|
||||
Header: 'Actions',
|
||||
id: 'Actions',
|
||||
align: 'center',
|
||||
Cell: ({
|
||||
row: {
|
||||
original: { id, type, name, description },
|
||||
},
|
||||
}: any) => (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<PermissionIconButton
|
||||
data-loading
|
||||
disabled={type === BUILTIN_ROLE_TYPE}
|
||||
onClick={() => {
|
||||
navigate(`/admin/project-roles/${id}/edit`);
|
||||
}}
|
||||
permission={ADMIN}
|
||||
tooltipProps={{
|
||||
title:
|
||||
type === BUILTIN_ROLE_TYPE
|
||||
? 'You cannot edit role'
|
||||
: 'Edit role',
|
||||
}}
|
||||
>
|
||||
<Edit />
|
||||
</PermissionIconButton>
|
||||
<PermissionIconButton
|
||||
data-loading
|
||||
disabled={type === BUILTIN_ROLE_TYPE}
|
||||
onClick={() => {
|
||||
setCurrentRole({
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
} as IProjectRole);
|
||||
setDelDialog(true);
|
||||
}}
|
||||
permission={ADMIN}
|
||||
tooltipProps={{
|
||||
title:
|
||||
type === BUILTIN_ROLE_TYPE
|
||||
? 'You cannot remove role'
|
||||
: 'Remove role',
|
||||
}}
|
||||
>
|
||||
<Delete />
|
||||
</PermissionIconButton>
|
||||
</Box>
|
||||
),
|
||||
width: 100,
|
||||
disableGlobalFilter: true,
|
||||
disableSortBy: true,
|
||||
},
|
||||
],
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const initialState = useMemo(
|
||||
() => ({
|
||||
sortBy: [{ id: 'name', desc: false }],
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
rows,
|
||||
prepareRow,
|
||||
state: { globalFilter },
|
||||
setGlobalFilter,
|
||||
setHiddenColumns,
|
||||
} = useTable(
|
||||
{
|
||||
columns: columns as any[], // TODO: fix after `react-table` v8 update
|
||||
data,
|
||||
initialState,
|
||||
sortTypes,
|
||||
autoResetGlobalFilter: false,
|
||||
autoResetHiddenColumns: false,
|
||||
autoResetSortBy: false,
|
||||
disableSortRemove: true,
|
||||
defaultColumn: {
|
||||
Cell: HighlightCell,
|
||||
},
|
||||
},
|
||||
useGlobalFilter,
|
||||
useSortBy
|
||||
);
|
||||
|
||||
useConditionallyHiddenColumns(
|
||||
[
|
||||
{
|
||||
condition: isExtraSmallScreen,
|
||||
columns: ['Icon'],
|
||||
},
|
||||
],
|
||||
setHiddenColumns,
|
||||
columns
|
||||
);
|
||||
|
||||
let count =
|
||||
data.length < rows.length
|
||||
? `(${data.length} of ${rows.length})`
|
||||
: `(${rows.length})`;
|
||||
return (
|
||||
<PageContent
|
||||
isLoading={loading}
|
||||
header={
|
||||
<PageHeader
|
||||
title={`Project roles ${count}`}
|
||||
actions={
|
||||
<>
|
||||
<Search
|
||||
initialValue={globalFilter}
|
||||
onChange={setGlobalFilter}
|
||||
/>
|
||||
<PageHeader.Divider />
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() =>
|
||||
navigate('/admin/project-roles/new')
|
||||
}
|
||||
>
|
||||
New project role
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<SearchHighlightProvider value={globalFilter}>
|
||||
<Table {...getTableProps()}>
|
||||
<SortableTableHeader headerGroups={headerGroups} />
|
||||
<TableBody {...getTableBodyProps()}>
|
||||
{rows.map(row => {
|
||||
prepareRow(row);
|
||||
return (
|
||||
<TableRow hover {...row.getRowProps()}>
|
||||
{row.cells.map(cell => (
|
||||
<TableCell {...cell.getCellProps()}>
|
||||
{cell.render('Cell')}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</SearchHighlightProvider>
|
||||
<ConditionallyRender
|
||||
condition={rows.length === 0}
|
||||
show={
|
||||
<ConditionallyRender
|
||||
condition={globalFilter?.length > 0}
|
||||
show={
|
||||
<TablePlaceholder>
|
||||
No project roles found matching “
|
||||
{globalFilter}
|
||||
”
|
||||
</TablePlaceholder>
|
||||
}
|
||||
elseShow={
|
||||
<TablePlaceholder>
|
||||
No project roles available. Get started by
|
||||
adding one.
|
||||
</TablePlaceholder>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<ProjectRoleDeleteConfirm
|
||||
role={currentRole!}
|
||||
open={delDialog}
|
||||
setDialogue={setDelDialog}
|
||||
handleDeleteRole={deleteProjectRole}
|
||||
confirmName={confirmName}
|
||||
setConfirmName={setConfirmName}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectRoleList;
|
@ -1,10 +0,0 @@
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
export const useStyles = makeStyles()(theme => ({
|
||||
rolesListBody: {
|
||||
padding: theme.spacing(4),
|
||||
paddingBottom: '4rem',
|
||||
minHeight: '50vh',
|
||||
position: 'relative',
|
||||
},
|
||||
}));
|
@ -1,22 +0,0 @@
|
||||
import { useContext } from 'react';
|
||||
import AccessContext from 'contexts/AccessContext';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||
import ProjectRoleList from './ProjectRoleList/ProjectRoleList';
|
||||
import { AdminAlert } from 'component/common/AdminAlert/AdminAlert';
|
||||
|
||||
const ProjectRoles = () => {
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(ADMIN)}
|
||||
show={<ProjectRoleList />}
|
||||
elseShow={<AdminAlert />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectRoles;
|
@ -1,187 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { IPermission } from 'interfaces/permissions';
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
import usePermissions from 'hooks/api/getters/usePermissions/usePermissions';
|
||||
import { useRolesApi } from 'hooks/api/actions/useRolesApi/useRolesApi';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
|
||||
export interface ICheckedPermission {
|
||||
[key: string]: IPermission;
|
||||
}
|
||||
|
||||
const getRoleKey = (permission: {
|
||||
id: number;
|
||||
environment?: string;
|
||||
}): string => {
|
||||
return permission.environment
|
||||
? `${permission.id}-${permission.environment}`
|
||||
: `${permission.id}`;
|
||||
};
|
||||
|
||||
const useProjectRoleForm = (
|
||||
initialRoleName = '',
|
||||
initialRoleDesc = '',
|
||||
initialCheckedPermissions: IPermission[] = []
|
||||
) => {
|
||||
const { permissions } = usePermissions({
|
||||
revalidateIfStale: false,
|
||||
revalidateOnReconnect: false,
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
|
||||
const [roleName, setRoleName] = useState(initialRoleName);
|
||||
const [roleDesc, setRoleDesc] = useState(initialRoleDesc);
|
||||
const [checkedPermissions, setCheckedPermissions] =
|
||||
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 } = useRolesApi();
|
||||
|
||||
useEffect(() => {
|
||||
setRoleName(initialRoleName);
|
||||
}, [initialRoleName]);
|
||||
|
||||
useEffect(() => {
|
||||
setRoleDesc(initialRoleDesc);
|
||||
}, [initialRoleDesc]);
|
||||
|
||||
const handlePermissionChange = (permission: IPermission) => {
|
||||
let checkedPermissionsCopy = cloneDeep(checkedPermissions);
|
||||
|
||||
if (checkedPermissionsCopy[getRoleKey(permission)]) {
|
||||
delete checkedPermissionsCopy[getRoleKey(permission)];
|
||||
} else {
|
||||
checkedPermissionsCopy[getRoleKey(permission)] = { ...permission };
|
||||
}
|
||||
|
||||
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 {
|
||||
project.forEach((permission: IPermission) => {
|
||||
checkedPermissionsCopy[getRoleKey(permission)] = {
|
||||
...permission,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
setCheckedPermissions(checkedPermissionsCopy);
|
||||
};
|
||||
|
||||
const onToggleAllEnvironmentPermissions = (envName: string) => {
|
||||
const { environments } = permissions;
|
||||
const checkedPermissionsCopy = cloneDeep(checkedPermissions);
|
||||
const env = environments.find(env => env.name === envName);
|
||||
if (!env) return;
|
||||
|
||||
const allChecked = env.permissions.every(
|
||||
(permission: IPermission) =>
|
||||
checkedPermissionsCopy[getRoleKey(permission)]
|
||||
);
|
||||
|
||||
if (allChecked) {
|
||||
env.permissions.forEach((permission: IPermission) => {
|
||||
delete checkedPermissionsCopy[getRoleKey(permission)];
|
||||
});
|
||||
} else {
|
||||
env.permissions.forEach((permission: IPermission) => {
|
||||
checkedPermissionsCopy[getRoleKey(permission)] = {
|
||||
...permission,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
setCheckedPermissions(checkedPermissionsCopy);
|
||||
};
|
||||
|
||||
const getProjectRolePayload = () => ({
|
||||
name: roleName,
|
||||
description: roleDesc,
|
||||
permissions: Object.values(checkedPermissions),
|
||||
});
|
||||
|
||||
const validateNameUniqueness = async () => {
|
||||
const payload = getProjectRolePayload();
|
||||
|
||||
try {
|
||||
await validateRole(payload);
|
||||
} catch (error: unknown) {
|
||||
setErrors(prev => ({ ...prev, name: formatUnknownError(error) }));
|
||||
}
|
||||
};
|
||||
|
||||
const validateName = () => {
|
||||
if (roleName.length === 0) {
|
||||
setErrors(prev => ({ ...prev, name: 'Name can not be empty.' }));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const validatePermissions = () => {
|
||||
if (Object.keys(checkedPermissions).length === 0) {
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
permissions: 'You must include at least one permission.',
|
||||
}));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const clearErrors = () => {
|
||||
setErrors({});
|
||||
};
|
||||
|
||||
return {
|
||||
roleName,
|
||||
roleDesc,
|
||||
errors,
|
||||
checkedPermissions,
|
||||
permissions,
|
||||
setRoleName,
|
||||
setRoleDesc,
|
||||
handlePermissionChange,
|
||||
onToggleAllProjectPermissions,
|
||||
onToggleAllEnvironmentPermissions,
|
||||
getProjectRolePayload,
|
||||
validatePermissions,
|
||||
validateName,
|
||||
clearErrors,
|
||||
validateNameUniqueness,
|
||||
getRoleKey,
|
||||
};
|
||||
};
|
||||
|
||||
export default useProjectRoleForm;
|
@ -13,20 +13,19 @@ import {
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { ExpandMore } from '@mui/icons-material';
|
||||
import { IPermission } from 'interfaces/permissions';
|
||||
import { ICheckedPermissions, IPermission } from 'interfaces/permissions';
|
||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||
import { ICheckedPermission } from 'component/admin/projectRoles/hooks/useProjectRoleForm';
|
||||
import { getRoleKey } from 'utils/permissions';
|
||||
|
||||
interface IEnvironmentPermissionAccordionProps {
|
||||
permissions: IPermission[];
|
||||
checkedPermissions: ICheckedPermission;
|
||||
checkedPermissions: ICheckedPermissions;
|
||||
title: string;
|
||||
Icon: ReactNode;
|
||||
isInitiallyExpanded?: boolean;
|
||||
context: string;
|
||||
onPermissionChange: (permission: IPermission) => void;
|
||||
onCheckAll: () => void;
|
||||
getRoleKey?: (permission: { id: number; environment?: string }) => string;
|
||||
}
|
||||
|
||||
const AccordionHeader = styled(Box)(({ theme }) => ({
|
||||
@ -52,7 +51,6 @@ export const PermissionAccordion: VFC<IEnvironmentPermissionAccordionProps> = ({
|
||||
context,
|
||||
onPermissionChange,
|
||||
onCheckAll,
|
||||
getRoleKey = permission => permission.id.toString(),
|
||||
}) => {
|
||||
const [expanded, setExpanded] = useState(isInitiallyExpanded);
|
||||
const permissionMap = useMemo(
|
||||
@ -141,35 +139,32 @@ export const PermissionAccordion: VFC<IEnvironmentPermissionAccordionProps> = ({
|
||||
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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{permissions?.map((permission: IPermission) => (
|
||||
<FormControlLabel
|
||||
sx={{
|
||||
minWidth: {
|
||||
sm: '300px',
|
||||
xs: 'auto',
|
||||
},
|
||||
}}
|
||||
data-testid={getRoleKey(permission)}
|
||||
key={getRoleKey(permission)}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={Boolean(
|
||||
checkedPermissions[
|
||||
getRoleKey(permission)
|
||||
]
|
||||
)}
|
||||
onChange={() =>
|
||||
onPermissionChange(permission)
|
||||
}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={permission.displayName}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
@ -1,11 +1,28 @@
|
||||
import { styled } from '@mui/material';
|
||||
import Input from 'component/common/Input/Input';
|
||||
import { PermissionAccordion } from 'component/admin/projectRoles/ProjectRoleForm/PermissionAccordion/PermissionAccordion';
|
||||
import { Person as UserIcon } from '@mui/icons-material';
|
||||
import { PermissionAccordion } from './PermissionAccordion/PermissionAccordion';
|
||||
import {
|
||||
Person as UserIcon,
|
||||
Topic as TopicIcon,
|
||||
CloudCircle as CloudCircleIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { ICheckedPermissions, IPermission } from 'interfaces/permissions';
|
||||
import { IRoleFormErrors } from './useRoleForm';
|
||||
import { ROOT_PERMISSION_CATEGORIES } from '@server/types/permissions';
|
||||
import { toggleAllPermissions, togglePermission } from 'utils/permissions';
|
||||
import {
|
||||
flattenProjectPermissions,
|
||||
getCategorizedProjectPermissions,
|
||||
getCategorizedRootPermissions,
|
||||
toggleAllPermissions,
|
||||
togglePermission,
|
||||
} from 'utils/permissions';
|
||||
import usePermissions from 'hooks/api/getters/usePermissions/usePermissions';
|
||||
import { PredefinedRoleType } from 'interfaces/role';
|
||||
import {
|
||||
ENVIRONMENT_PERMISSION_TYPE,
|
||||
PROJECT_PERMISSION_TYPE,
|
||||
PROJECT_ROLE_TYPES,
|
||||
ROOT_ROLE_TYPE,
|
||||
} from '@server/util/constants';
|
||||
|
||||
const StyledInputDescription = styled('p')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
@ -22,6 +39,7 @@ const StyledInput = styled(Input)(({ theme }) => ({
|
||||
}));
|
||||
|
||||
interface IRoleFormProps {
|
||||
type?: PredefinedRoleType;
|
||||
name: string;
|
||||
onSetName: (name: string) => void;
|
||||
description: string;
|
||||
@ -30,34 +48,35 @@ interface IRoleFormProps {
|
||||
setCheckedPermissions: React.Dispatch<
|
||||
React.SetStateAction<ICheckedPermissions>
|
||||
>;
|
||||
permissions: IPermission[];
|
||||
errors: IRoleFormErrors;
|
||||
}
|
||||
|
||||
export const RoleForm = ({
|
||||
type = ROOT_ROLE_TYPE,
|
||||
name,
|
||||
onSetName,
|
||||
description,
|
||||
setDescription,
|
||||
checkedPermissions,
|
||||
setCheckedPermissions,
|
||||
permissions,
|
||||
errors,
|
||||
}: IRoleFormProps) => {
|
||||
const categorizedPermissions = permissions.map(permission => {
|
||||
const category = ROOT_PERMISSION_CATEGORIES.find(category =>
|
||||
category.permissions.includes(permission.name)
|
||||
);
|
||||
|
||||
return {
|
||||
category: category ? category.label : 'Other',
|
||||
permission,
|
||||
};
|
||||
const { permissions } = usePermissions({
|
||||
revalidateIfStale: false,
|
||||
revalidateOnReconnect: false,
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
|
||||
const categories = new Set(
|
||||
categorizedPermissions.map(({ category }) => category).sort()
|
||||
);
|
||||
const isProjectRole = PROJECT_ROLE_TYPES.includes(type);
|
||||
|
||||
const categories = isProjectRole
|
||||
? getCategorizedProjectPermissions(
|
||||
flattenProjectPermissions(
|
||||
permissions.project,
|
||||
permissions.environments
|
||||
)
|
||||
)
|
||||
: getCategorizedRootPermissions(permissions.root);
|
||||
|
||||
const onPermissionChange = (permission: IPermission) => {
|
||||
const newCheckedPermissions = togglePermission(
|
||||
@ -67,14 +86,10 @@ export const RoleForm = ({
|
||||
setCheckedPermissions(newCheckedPermissions);
|
||||
};
|
||||
|
||||
const onCheckAll = (category: string) => {
|
||||
const categoryPermissions = categorizedPermissions
|
||||
.filter(({ category: pCategory }) => pCategory === category)
|
||||
.map(({ permission }) => permission);
|
||||
|
||||
const onCheckAll = (permissions: IPermission[]) => {
|
||||
const newCheckedPermissions = toggleAllPermissions(
|
||||
checkedPermissions,
|
||||
categoryPermissions
|
||||
permissions
|
||||
);
|
||||
|
||||
setCheckedPermissions(newCheckedPermissions);
|
||||
@ -108,22 +123,26 @@ export const RoleForm = ({
|
||||
<StyledInputDescription>
|
||||
What is your role allowed to do?
|
||||
</StyledInputDescription>
|
||||
{[...categories].map(category => (
|
||||
{categories.map(({ label, type, permissions }) => (
|
||||
<PermissionAccordion
|
||||
key={category}
|
||||
title={`${category} permissions`}
|
||||
context={category.toLowerCase()}
|
||||
Icon={<UserIcon color="disabled" sx={{ mr: 1 }} />}
|
||||
permissions={categorizedPermissions
|
||||
.filter(
|
||||
({ category: pCategory }) => pCategory === category
|
||||
key={label}
|
||||
title={`${label} permissions`}
|
||||
context={label.toLowerCase()}
|
||||
Icon={
|
||||
type === PROJECT_PERMISSION_TYPE ? (
|
||||
<TopicIcon color="disabled" sx={{ mr: 1 }} />
|
||||
) : type === ENVIRONMENT_PERMISSION_TYPE ? (
|
||||
<CloudCircleIcon color="disabled" sx={{ mr: 1 }} />
|
||||
) : (
|
||||
<UserIcon color="disabled" sx={{ mr: 1 }} />
|
||||
)
|
||||
.map(({ permission }) => permission)}
|
||||
}
|
||||
permissions={permissions}
|
||||
checkedPermissions={checkedPermissions}
|
||||
onPermissionChange={(permission: IPermission) =>
|
||||
onPermissionChange(permission)
|
||||
}
|
||||
onCheckAll={() => onCheckAll(category)}
|
||||
onCheckAll={() => onCheckAll(permissions)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { IPermission, ICheckedPermissions } from 'interfaces/permissions';
|
||||
import IRole from 'interfaces/role';
|
||||
import IRole, { PredefinedRoleType } from 'interfaces/role';
|
||||
import { useRoles } from 'hooks/api/getters/useRoles/useRoles';
|
||||
import { permissionsToCheckedPermissions } from 'utils/permissions';
|
||||
import { ROOT_ROLE_TYPE } from '@server/util/constants';
|
||||
|
||||
enum ErrorField {
|
||||
NAME = 'name',
|
||||
@ -39,10 +40,10 @@ export const useRoleForm = (
|
||||
setCheckedPermissions(newCheckedPermissions);
|
||||
}, [initialPermissions.length]);
|
||||
|
||||
const getRolePayload = (type: 'root-custom' | 'custom' = 'custom') => ({
|
||||
const getRolePayload = (type: PredefinedRoleType = ROOT_ROLE_TYPE) => ({
|
||||
name,
|
||||
description,
|
||||
type,
|
||||
type: type === ROOT_ROLE_TYPE ? 'root-custom' : 'custom',
|
||||
permissions: Object.values(checkedPermissions),
|
||||
});
|
||||
|
||||
|
@ -10,7 +10,8 @@ import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { FormEvent } from 'react';
|
||||
import { useRolesApi } from 'hooks/api/actions/useRolesApi/useRolesApi';
|
||||
import { useRole } from 'hooks/api/getters/useRole/useRole';
|
||||
import usePermissions from 'hooks/api/getters/usePermissions/usePermissions';
|
||||
import { PredefinedRoleType } from 'interfaces/role';
|
||||
import { ROOT_ROLE_TYPE } from '@server/util/constants';
|
||||
|
||||
const StyledForm = styled('form')(() => ({
|
||||
display: 'flex',
|
||||
@ -30,12 +31,18 @@ const StyledCancelButton = styled(Button)(({ theme }) => ({
|
||||
}));
|
||||
|
||||
interface IRoleModalProps {
|
||||
type?: PredefinedRoleType;
|
||||
roleId?: number;
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export const RoleModal = ({ roleId, open, setOpen }: IRoleModalProps) => {
|
||||
export const RoleModal = ({
|
||||
type = ROOT_ROLE_TYPE,
|
||||
roleId,
|
||||
open,
|
||||
setOpen,
|
||||
}: IRoleModalProps) => {
|
||||
const { role, refetch: refetchRole } = useRole(roleId?.toString());
|
||||
|
||||
const {
|
||||
@ -56,18 +63,9 @@ export const RoleModal = ({ roleId, open, setOpen }: IRoleModalProps) => {
|
||||
} = useRoleForm(role?.name, role?.description, role?.permissions);
|
||||
const { refetch: refetchRoles } = useRoles();
|
||||
const { addRole, updateRole, loading } = useRolesApi();
|
||||
const { permissions } = usePermissions({
|
||||
revalidateIfStale: false,
|
||||
revalidateOnReconnect: false,
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { uiConfig } = useUiConfig();
|
||||
|
||||
const rootPermissions = permissions.root.filter(
|
||||
({ name }) => name !== 'ADMIN'
|
||||
);
|
||||
|
||||
const editing = role !== undefined;
|
||||
const isValid =
|
||||
isNameUnique(name) &&
|
||||
@ -75,7 +73,7 @@ export const RoleModal = ({ roleId, open, setOpen }: IRoleModalProps) => {
|
||||
isNotEmpty(description) &&
|
||||
hasPermissions(checkedPermissions);
|
||||
|
||||
const payload = getRolePayload('root-custom');
|
||||
const payload = getRolePayload(type);
|
||||
|
||||
const formatApiCode = () => {
|
||||
return `curl --location --request ${editing ? 'PUT' : 'POST'} '${
|
||||
@ -121,32 +119,34 @@ export const RoleModal = ({ roleId, open, setOpen }: IRoleModalProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
const titleCasedType = type[0].toUpperCase() + type.slice(1);
|
||||
|
||||
return (
|
||||
<SidebarModal
|
||||
open={open}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
label={editing ? 'Edit role' : 'New role'}
|
||||
label={editing ? `Edit ${type} role` : `New ${type} role`}
|
||||
>
|
||||
<FormTemplate
|
||||
loading={loading}
|
||||
modal
|
||||
title={editing ? 'Edit role' : 'New role'}
|
||||
description="Roles allow you to control access to global root resources. Besides the built-in roles, you can create and manage custom roles to fit your needs."
|
||||
documentationLink="https://docs.getunleash.io/reference/rbac#standard-roles"
|
||||
title={editing ? `Edit ${type} role` : `New ${type} role`}
|
||||
description={`${titleCasedType} roles allow you to control access to ${type} resources. Besides the built-in ${type} roles, you can create and manage custom ${type} roles to fit your needs.`}
|
||||
documentationLink="https://docs.getunleash.io/reference/rbac"
|
||||
documentationLinkLabel="Roles documentation"
|
||||
formatApiCode={formatApiCode}
|
||||
>
|
||||
<StyledForm onSubmit={onSubmit}>
|
||||
<RoleForm
|
||||
type={type}
|
||||
name={name}
|
||||
onSetName={onSetName}
|
||||
description={description}
|
||||
setDescription={setDescription}
|
||||
checkedPermissions={checkedPermissions}
|
||||
setCheckedPermissions={setCheckedPermissions}
|
||||
permissions={rootPermissions}
|
||||
errors={errors}
|
||||
/>
|
||||
<StyledButtonContainer>
|
||||
|
@ -4,15 +4,88 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
|
||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||
import { RolesTable } from './RolesTable/RolesTable';
|
||||
import { AdminAlert } from 'component/common/AdminAlert/AdminAlert';
|
||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||
import { Tab, Tabs, styled } from '@mui/material';
|
||||
import { Route, Routes, useLocation } from 'react-router-dom';
|
||||
import { CenteredNavLink } from '../menu/CenteredNavLink';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { PROJECT_ROLE_TYPE } from '@server/util/constants';
|
||||
|
||||
const StyledPageContent = styled(PageContent)(({ theme }) => ({
|
||||
'.page-header': {
|
||||
padding: 0,
|
||||
},
|
||||
}));
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
label: 'Root',
|
||||
path: '/admin/roles',
|
||||
},
|
||||
{
|
||||
label: 'Project',
|
||||
path: '/admin/roles/project-roles',
|
||||
},
|
||||
];
|
||||
|
||||
export const Roles = () => {
|
||||
const { uiConfig } = useUiConfig();
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const { pathname } = useLocation();
|
||||
|
||||
if (!uiConfig.flags.customRootRoles) {
|
||||
return (
|
||||
<div>
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(ADMIN)}
|
||||
show={<RolesTable type={PROJECT_ROLE_TYPE} />}
|
||||
elseShow={<AdminAlert />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(ADMIN)}
|
||||
show={<RolesTable />}
|
||||
show={
|
||||
<StyledPageContent
|
||||
headerClass="page-header"
|
||||
bodyClass="page-body"
|
||||
header={
|
||||
<Tabs
|
||||
value={pathname}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
variant="scrollable"
|
||||
allowScrollButtonsMobile
|
||||
>
|
||||
{tabs.map(({ label, path }) => (
|
||||
<Tab
|
||||
key={label}
|
||||
value={path}
|
||||
label={
|
||||
<CenteredNavLink to={path}>
|
||||
<span>{label}</span>
|
||||
</CenteredNavLink>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
}
|
||||
>
|
||||
<Routes>
|
||||
<Route
|
||||
path="project-roles"
|
||||
element={
|
||||
<RolesTable type={PROJECT_ROLE_TYPE} />
|
||||
}
|
||||
/>
|
||||
<Route path="*" element={<RolesTable />} />
|
||||
</Routes>
|
||||
</StyledPageContent>
|
||||
}
|
||||
elseShow={<AdminAlert />}
|
||||
/>
|
||||
</div>
|
||||
|
@ -4,6 +4,7 @@ import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
|
||||
import IRole from 'interfaces/role';
|
||||
import { useRole } from 'hooks/api/getters/useRole/useRole';
|
||||
import { RoleDescription } from 'component/common/RoleDescription/RoleDescription';
|
||||
import { PREDEFINED_ROLE_TYPES } from '@server/util/constants';
|
||||
|
||||
interface IRolePermissionsCellProps {
|
||||
row: { original: IRole };
|
||||
@ -15,7 +16,7 @@ export const RolePermissionsCell: VFC<IRolePermissionsCellProps> = ({
|
||||
const { original: rowRole } = row;
|
||||
const { role } = useRole(rowRole.id.toString());
|
||||
|
||||
if (!role || role.type === 'root') return null;
|
||||
if (!role || PREDEFINED_ROLE_TYPES.includes(role.type)) return null;
|
||||
|
||||
return (
|
||||
<TextCell>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Delete, Edit } from '@mui/icons-material';
|
||||
import { Box, styled } from '@mui/material';
|
||||
import { PREDEFINED_ROLE_TYPES } from '@server/util/constants';
|
||||
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||
import IRole from 'interfaces/role';
|
||||
@ -10,8 +11,6 @@ const StyledBox = styled(Box)(() => ({
|
||||
justifyContent: 'center',
|
||||
}));
|
||||
|
||||
const DEFAULT_ROOT_ROLE = 'root';
|
||||
|
||||
interface IRolesActionsCellProps {
|
||||
role: IRole;
|
||||
onEdit: (event: React.SyntheticEvent) => void;
|
||||
@ -23,7 +22,7 @@ export const RolesActionsCell: VFC<IRolesActionsCellProps> = ({
|
||||
onEdit,
|
||||
onDelete,
|
||||
}) => {
|
||||
const defaultRole = role.type === DEFAULT_ROOT_ROLE;
|
||||
const defaultRole = PREDEFINED_ROLE_TYPES.includes(role.type);
|
||||
|
||||
return (
|
||||
<StyledBox>
|
||||
|
@ -3,6 +3,7 @@ import { Badge } from 'component/common/Badge/Badge';
|
||||
import { styled } from '@mui/material';
|
||||
import IRole from 'interfaces/role';
|
||||
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
|
||||
import { PREDEFINED_ROLE_TYPES } from '@server/util/constants';
|
||||
|
||||
const StyledBadge = styled(Badge)(({ theme }) => ({
|
||||
marginLeft: theme.spacing(1),
|
||||
@ -18,7 +19,7 @@ export const RolesCell = ({ role }: IRolesCellProps) => (
|
||||
subtitle={role.description}
|
||||
afterTitle={
|
||||
<ConditionallyRender
|
||||
condition={role.type === 'root'}
|
||||
condition={PREDEFINED_ROLE_TYPES.includes(role.type)}
|
||||
show={<StyledBadge color="success">Predefined</StyledBadge>}
|
||||
/>
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import IRole from 'interfaces/role';
|
||||
import IRole, { PredefinedRoleType } from 'interfaces/role';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||
@ -24,11 +24,16 @@ import { useRolesApi } from 'hooks/api/actions/useRolesApi/useRolesApi';
|
||||
import { useRoles } from 'hooks/api/getters/useRoles/useRoles';
|
||||
import { RoleModal } from '../RoleModal/RoleModal';
|
||||
import { RolePermissionsCell } from './RolePermissionsCell/RolePermissionsCell';
|
||||
import { ROOT_ROLE_TYPE } from '@server/util/constants';
|
||||
|
||||
export const RolesTable = () => {
|
||||
interface IRolesTableProps {
|
||||
type?: PredefinedRoleType;
|
||||
}
|
||||
|
||||
export const RolesTable = ({ type = ROOT_ROLE_TYPE }: IRolesTableProps) => {
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
|
||||
const { roles, refetch, loading } = useRoles();
|
||||
const { roles, projectRoles, refetch, loading } = useRoles();
|
||||
const { removeRole } = useRolesApi();
|
||||
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
@ -114,7 +119,11 @@ export const RolesTable = () => {
|
||||
hiddenColumns: ['description'],
|
||||
});
|
||||
|
||||
const { data, getSearchText } = useSearch(columns, searchValue, roles);
|
||||
const { data, getSearchText } = useSearch(
|
||||
columns,
|
||||
searchValue,
|
||||
type === ROOT_ROLE_TYPE ? roles : projectRoles
|
||||
);
|
||||
|
||||
const { headerGroups, rows, prepareRow, setHiddenColumns } = useTable(
|
||||
{
|
||||
@ -145,12 +154,14 @@ export const RolesTable = () => {
|
||||
columns
|
||||
);
|
||||
|
||||
const titledCaseType = type[0].toUpperCase() + type.slice(1);
|
||||
|
||||
return (
|
||||
<PageContent
|
||||
isLoading={loading}
|
||||
header={
|
||||
<PageHeader
|
||||
title={`Roles (${rows.length})`}
|
||||
title={`${titledCaseType} roles (${rows.length})`}
|
||||
actions={
|
||||
<>
|
||||
<ConditionallyRender
|
||||
@ -173,7 +184,7 @@ export const RolesTable = () => {
|
||||
setModalOpen(true);
|
||||
}}
|
||||
>
|
||||
New role
|
||||
New {type} role
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
@ -204,20 +215,22 @@ export const RolesTable = () => {
|
||||
condition={searchValue?.length > 0}
|
||||
show={
|
||||
<TablePlaceholder>
|
||||
No roles found matching “
|
||||
No {type} roles found matching “
|
||||
{searchValue}
|
||||
”
|
||||
</TablePlaceholder>
|
||||
}
|
||||
elseShow={
|
||||
<TablePlaceholder>
|
||||
No roles available. Get started by adding one.
|
||||
No {type} roles available. Get started by adding
|
||||
one.
|
||||
</TablePlaceholder>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<RoleModal
|
||||
type={type}
|
||||
roleId={selectedRole?.id}
|
||||
open={modalOpen}
|
||||
setOpen={setModalOpen}
|
||||
|
@ -1,7 +1,14 @@
|
||||
import { SxProps, Theme, styled } from '@mui/material';
|
||||
import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender';
|
||||
import { ROOT_PERMISSION_CATEGORIES } from '@server/types/permissions';
|
||||
import { useRole } from 'hooks/api/getters/useRole/useRole';
|
||||
import {
|
||||
PREDEFINED_ROLE_TYPES,
|
||||
PROJECT_ROLE_TYPES,
|
||||
} from '@server/util/constants';
|
||||
import {
|
||||
getCategorizedProjectPermissions,
|
||||
getCategorizedRootPermissions,
|
||||
} from 'utils/permissions';
|
||||
|
||||
const StyledDescription = styled('div', {
|
||||
shouldForwardProp: prop => prop !== 'tooltip',
|
||||
@ -49,22 +56,13 @@ export const RoleDescription = ({
|
||||
|
||||
if (!role) return null;
|
||||
|
||||
const { name, description, permissions } = role;
|
||||
const { name, description, permissions, type } = role;
|
||||
|
||||
const categorizedPermissions = [...new Set(permissions)].map(permission => {
|
||||
const category = ROOT_PERMISSION_CATEGORIES.find(category =>
|
||||
category.permissions.includes(permission.name)
|
||||
);
|
||||
const isProjectRole = PROJECT_ROLE_TYPES.includes(type);
|
||||
|
||||
return {
|
||||
category: category ? category.label : 'Other',
|
||||
permission,
|
||||
};
|
||||
});
|
||||
|
||||
const categories = new Set(
|
||||
categorizedPermissions.map(({ category }) => category).sort()
|
||||
);
|
||||
const categories = isProjectRole
|
||||
? getCategorizedProjectPermissions(permissions)
|
||||
: getCategorizedRootPermissions(permissions);
|
||||
|
||||
return (
|
||||
<StyledDescription tooltip={tooltip} {...rest}>
|
||||
@ -75,22 +73,18 @@ export const RoleDescription = ({
|
||||
{description}
|
||||
</StyledDescriptionSubHeader>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
categorizedPermissions.length > 0 && role.type !== 'root'
|
||||
}
|
||||
condition={!PREDEFINED_ROLE_TYPES.includes(role.type)}
|
||||
show={() =>
|
||||
[...categories].map(category => (
|
||||
<StyledDescriptionBlock key={category}>
|
||||
categories.map(({ label, permissions }) => (
|
||||
<StyledDescriptionBlock key={label}>
|
||||
<StyledDescriptionHeader>
|
||||
{category}
|
||||
{label}
|
||||
</StyledDescriptionHeader>
|
||||
{categorizedPermissions
|
||||
.filter(({ category: c }) => c === category)
|
||||
.map(({ permission }) => (
|
||||
<p key={permission.id}>
|
||||
{permission.displayName}
|
||||
</p>
|
||||
))}
|
||||
{permissions.map(permission => (
|
||||
<p key={permission.id}>
|
||||
{permission.displayName}
|
||||
</p>
|
||||
))}
|
||||
</StyledDescriptionBlock>
|
||||
))
|
||||
}
|
||||
|
@ -464,15 +464,8 @@ export const adminMenuRoutes: INavigationMenuItem[] = [
|
||||
flag: UG,
|
||||
},
|
||||
{
|
||||
path: '/admin/roles',
|
||||
path: '/admin/roles/*',
|
||||
title: 'Roles',
|
||||
flag: 'customRootRoles',
|
||||
menu: { adminSettings: true, mode: ['enterprise'] },
|
||||
},
|
||||
{
|
||||
path: '/admin/project-roles',
|
||||
title: 'Project roles',
|
||||
flag: RE,
|
||||
menu: { adminSettings: true, mode: ['enterprise'] },
|
||||
},
|
||||
{
|
||||
|
@ -4,10 +4,12 @@ import { formatApiPath } from 'utils/formatPath';
|
||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||
import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
|
||||
import useUiConfig from '../useUiConfig/useUiConfig';
|
||||
|
||||
const ROOT_ROLE = 'root';
|
||||
const ROOT_ROLES = [ROOT_ROLE, 'root-custom'];
|
||||
const PROJECT_ROLES = ['project', 'custom'];
|
||||
import {
|
||||
PROJECT_ROLE_TYPES,
|
||||
ROOT_ROLE_TYPE,
|
||||
ROOT_ROLE_TYPES,
|
||||
PREDEFINED_ROLE_TYPES,
|
||||
} from '@server/util/constants';
|
||||
|
||||
export const useRoles = () => {
|
||||
const { isEnterprise, uiConfig } = useUiConfig();
|
||||
@ -34,7 +36,7 @@ export const useRoles = () => {
|
||||
if (!isEnterprise()) {
|
||||
return {
|
||||
roles: ossData?.rootRoles
|
||||
.filter(({ type }: IRole) => type === ROOT_ROLE)
|
||||
.filter(({ type }: IRole) => type === ROOT_ROLE_TYPE)
|
||||
.sort(sortRoles) as IRole[],
|
||||
projectRoles: [],
|
||||
loading: !ossError && !ossData,
|
||||
@ -46,12 +48,14 @@ export const useRoles = () => {
|
||||
roles: (data?.roles
|
||||
.filter(({ type }: IRole) =>
|
||||
uiConfig.flags.customRootRoles
|
||||
? ROOT_ROLES.includes(type)
|
||||
: type === ROOT_ROLE
|
||||
? ROOT_ROLE_TYPES.includes(type)
|
||||
: type === ROOT_ROLE_TYPE
|
||||
)
|
||||
.sort(sortRoles) ?? []) as IRole[],
|
||||
projectRoles: (data?.roles
|
||||
.filter(({ type }: IRole) => PROJECT_ROLES.includes(type))
|
||||
.filter(({ type }: IRole) =>
|
||||
PROJECT_ROLE_TYPES.includes(type)
|
||||
)
|
||||
.sort(sortRoles) ?? []) as IProjectRole[],
|
||||
loading: !error && !data,
|
||||
refetch: () => mutate(),
|
||||
@ -68,9 +72,15 @@ const fetcher = (path: string) => {
|
||||
};
|
||||
|
||||
export const sortRoles = (a: IRole, b: IRole) => {
|
||||
if (a.type === 'root' && b.type !== 'root') {
|
||||
if (
|
||||
PREDEFINED_ROLE_TYPES.includes(a.type) &&
|
||||
!PREDEFINED_ROLE_TYPES.includes(b.type)
|
||||
) {
|
||||
return -1;
|
||||
} else if (a.type !== 'root' && b.type === 'root') {
|
||||
} else if (
|
||||
!PREDEFINED_ROLE_TYPES.includes(a.type) &&
|
||||
PREDEFINED_ROLE_TYPES.includes(b.type)
|
||||
) {
|
||||
return 1;
|
||||
} else {
|
||||
return a.name.localeCompare(b.name);
|
||||
|
@ -1,7 +1,19 @@
|
||||
import {
|
||||
ROOT_PERMISSION_TYPE,
|
||||
PROJECT_PERMISSION_TYPE,
|
||||
ENVIRONMENT_PERMISSION_TYPE,
|
||||
} from '@server/util/constants';
|
||||
|
||||
export type PermissionType =
|
||||
| typeof ROOT_PERMISSION_TYPE
|
||||
| typeof PROJECT_PERMISSION_TYPE
|
||||
| typeof ENVIRONMENT_PERMISSION_TYPE;
|
||||
|
||||
export interface IPermission {
|
||||
id: number;
|
||||
name: string;
|
||||
displayName: string;
|
||||
type: PermissionType;
|
||||
environment?: string;
|
||||
}
|
||||
|
||||
@ -19,3 +31,9 @@ export interface IProjectEnvironmentPermissions {
|
||||
export interface ICheckedPermissions {
|
||||
[key: string]: IPermission;
|
||||
}
|
||||
|
||||
export interface IPermissionCategory {
|
||||
label: string;
|
||||
type: PermissionType;
|
||||
permissions: IPermission[];
|
||||
}
|
||||
|
@ -1,5 +1,10 @@
|
||||
import { ROOT_ROLE_TYPE, PROJECT_ROLE_TYPE } from '@server/util/constants';
|
||||
import { IPermission } from './permissions';
|
||||
|
||||
export type PredefinedRoleType =
|
||||
| typeof ROOT_ROLE_TYPE
|
||||
| typeof PROJECT_ROLE_TYPE;
|
||||
|
||||
interface IRole {
|
||||
id: number;
|
||||
name: string;
|
||||
|
@ -1,7 +1,17 @@
|
||||
import { IPermission, ICheckedPermissions } from 'interfaces/permissions';
|
||||
import { ROOT_PERMISSION_CATEGORIES } from '@server/types/permissions';
|
||||
import {
|
||||
ENVIRONMENT_PERMISSION_TYPE,
|
||||
PROJECT_PERMISSION_TYPE,
|
||||
} from '@server/util/constants';
|
||||
import {
|
||||
IPermission,
|
||||
ICheckedPermissions,
|
||||
IPermissionCategory,
|
||||
IProjectEnvironmentPermissions,
|
||||
} from 'interfaces/permissions';
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
|
||||
const getRoleKey = (permission: IPermission): string => {
|
||||
export const getRoleKey = (permission: IPermission): string => {
|
||||
return permission.environment
|
||||
? `${permission.id}-${permission.environment}`
|
||||
: `${permission.id}`;
|
||||
@ -61,3 +71,101 @@ export const toggleAllPermissions = (
|
||||
|
||||
return checkedPermissionsCopy;
|
||||
};
|
||||
|
||||
export const getCategorizedRootPermissions = (permissions: IPermission[]) => {
|
||||
const rootPermissions = permissions.filter(({ name }) => name !== 'ADMIN');
|
||||
|
||||
return rootPermissions
|
||||
.reduce((categories: IPermissionCategory[], permission) => {
|
||||
const categoryLabel =
|
||||
ROOT_PERMISSION_CATEGORIES.find(category =>
|
||||
category.permissions.includes(permission.name)
|
||||
)?.label || 'Other';
|
||||
|
||||
const category = categories.find(
|
||||
({ label }) => label === categoryLabel
|
||||
);
|
||||
|
||||
if (category) {
|
||||
category.permissions.push(permission);
|
||||
} else {
|
||||
categories.push({
|
||||
label: categoryLabel,
|
||||
type: 'root',
|
||||
permissions: [permission],
|
||||
});
|
||||
}
|
||||
|
||||
return categories;
|
||||
}, [])
|
||||
.sort(sortCategories);
|
||||
};
|
||||
|
||||
export const getCategorizedProjectPermissions = (
|
||||
permissions: IPermission[]
|
||||
) => {
|
||||
const projectPermissions = permissions.filter(
|
||||
({ type }) => type === PROJECT_PERMISSION_TYPE
|
||||
);
|
||||
const environmentPermissions = permissions.filter(
|
||||
({ type }) => type === ENVIRONMENT_PERMISSION_TYPE
|
||||
);
|
||||
|
||||
const categories = [];
|
||||
|
||||
if (projectPermissions.length) {
|
||||
categories.push({
|
||||
label: 'Project',
|
||||
type: 'project',
|
||||
permissions: projectPermissions,
|
||||
});
|
||||
}
|
||||
|
||||
categories.push(
|
||||
...environmentPermissions.reduce(
|
||||
(categories: IPermissionCategory[], permission) => {
|
||||
const categoryLabel = permission.environment;
|
||||
|
||||
const category = categories.find(
|
||||
({ label }) => label === categoryLabel
|
||||
);
|
||||
|
||||
if (category) {
|
||||
category.permissions.push(permission);
|
||||
} else {
|
||||
categories.push({
|
||||
label: categoryLabel!,
|
||||
type: 'environment',
|
||||
permissions: [permission],
|
||||
});
|
||||
}
|
||||
|
||||
return categories;
|
||||
},
|
||||
[]
|
||||
)
|
||||
);
|
||||
|
||||
return categories;
|
||||
};
|
||||
|
||||
export const flattenProjectPermissions = (
|
||||
projectPermissions: IPermission[],
|
||||
environmentPermissions: IProjectEnvironmentPermissions[]
|
||||
) => [
|
||||
...projectPermissions,
|
||||
...environmentPermissions.flatMap(({ permissions }) => permissions),
|
||||
];
|
||||
|
||||
const sortCategories = (
|
||||
{ label: aLabel }: IPermissionCategory,
|
||||
{ label: bLabel }: IPermissionCategory
|
||||
) => {
|
||||
if (aLabel === 'Other' && bLabel !== 'Other') {
|
||||
return 1;
|
||||
} else if (aLabel !== 'Other' && bLabel === 'Other') {
|
||||
return -1;
|
||||
} else {
|
||||
return aLabel.localeCompare(bLabel);
|
||||
}
|
||||
};
|
||||
|
@ -7,8 +7,13 @@ export const ROOT_PERMISSION_TYPE = 'root';
|
||||
export const ENVIRONMENT_PERMISSION_TYPE = 'environment';
|
||||
export const PROJECT_PERMISSION_TYPE = 'project';
|
||||
|
||||
export const ROOT_ROLE_TYPE = 'root';
|
||||
export const PROJECT_ROLE_TYPE = 'project';
|
||||
export const CUSTOM_ROOT_ROLE_TYPE = 'root-custom';
|
||||
export const CUSTOM_PROJECT_ROLE_TYPE = 'custom';
|
||||
export const PREDEFINED_ROLE_TYPES = [ROOT_ROLE_TYPE, PROJECT_ROLE_TYPE];
|
||||
export const ROOT_ROLE_TYPES = [ROOT_ROLE_TYPE, CUSTOM_ROOT_ROLE_TYPE];
|
||||
export const PROJECT_ROLE_TYPES = [PROJECT_ROLE_TYPE, CUSTOM_PROJECT_ROLE_TYPE];
|
||||
|
||||
/* CONTEXT FIELD OPERATORS */
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user