diff --git a/frontend/src/component/admin/Admin.tsx b/frontend/src/component/admin/Admin.tsx index 26448a35f0..ffffd82d96 100644 --- a/frontend/src/component/admin/Admin.tsx +++ b/frontend/src/component/admin/Admin.tsx @@ -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 = () => ( } /> - } /> - } - /> } /> } /> } /> @@ -46,8 +38,7 @@ export const Admin = () => ( element={} /> } /> - } /> - } /> + } /> } /> } /> } /> diff --git a/frontend/src/component/admin/menu/AdminMenu.tsx b/frontend/src/component/admin/menu/AdminMenu.tsx index 5c3e5097e3..fe820fdab2 100644 --- a/frontend/src/component/admin/menu/AdminMenu.tsx +++ b/frontend/src/component/admin/menu/AdminMenu.tsx @@ -55,7 +55,7 @@ function AdminMenu() { } /> )} - {flags.customRootRoles && ( + {isEnterprise() && ( )} - {flags.RE && ( - - Project roles - - } - /> - )} { - 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 ( - - - - - - ); -}; - -export default CreateProjectRole; diff --git a/frontend/src/component/admin/projectRoles/EditProjectRole/EditProjectRole.tsx b/frontend/src/component/admin/projectRoles/EditProjectRole/EditProjectRole.tsx deleted file mode 100644 index 13aa6169b9..0000000000 --- a/frontend/src/component/admin/projectRoles/EditProjectRole/EditProjectRole.tsx +++ /dev/null @@ -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 ( - - - - - - ); -}; - -export default EditProjectRole; diff --git a/frontend/src/component/admin/projectRoles/ProjectRoleForm/ProjectRoleForm.tsx b/frontend/src/component/admin/projectRoles/ProjectRoleForm/ProjectRoleForm.tsx deleted file mode 100644 index 412575ab62..0000000000 --- a/frontend/src/component/admin/projectRoles/ProjectRoleForm/ProjectRoleForm.tsx +++ /dev/null @@ -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>; - setRoleDesc: Dispatch>; - 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 = ({ - children, - roleName, - roleDesc, - checkedPermissions, - errors, - permissions, - onSubmit, - onCancel, - setRoleName, - setRoleDesc, - handlePermissionChange, - checkAllProjectPermissions, - checkAllEnvironmentPermissions, - validateNameUniqueness, - clearErrors, - getRoleKey, -}: IProjectRoleForm) => { - const { project, environments } = permissions; - - return ( -
- - What is your role name? - setRoleName(e.target.value)} - error={Boolean(errors.name)} - errorText={errors.name} - onFocus={() => clearErrors()} - onBlur={validateNameUniqueness} - autoFocus - sx={{ width: '100%', marginBottom: '1rem' }} - /> - - What is this role for? - setRoleDesc(e.target.value)} - sx={{ width: '100%', marginBottom: '1rem' }} - /> - -
- - You must select at least one permission for a role. - - } - /> -
- } - permissions={project} - checkedPermissions={checkedPermissions} - onPermissionChange={(permission: IPermission) => - handlePermissionChange(permission) - } - onCheckAll={checkAllProjectPermissions} - getRoleKey={getRoleKey} - context="project" - /> -
- {environments.map(environment => ( - - } - permissions={environment.permissions} - key={environment.name} - checkedPermissions={checkedPermissions} - onPermissionChange={(permission: IPermission) => - handlePermissionChange(permission) - } - onCheckAll={() => - checkAllEnvironmentPermissions(environment.name) - } - getRoleKey={getRoleKey} - context="environment" - /> - ))} -
- - {children} - - - - ); -}; - -export default ProjectRoleForm; diff --git a/frontend/src/component/admin/projectRoles/ProjectRoles/ProjectRoleDeleteConfirm/ProjectRoleDeleteConfirm.tsx b/frontend/src/component/admin/projectRoles/ProjectRoles/ProjectRoleDeleteConfirm/ProjectRoleDeleteConfirm.tsx deleted file mode 100644 index cc1c5eba52..0000000000 --- a/frontend/src/component/admin/projectRoles/ProjectRoles/ProjectRoleDeleteConfirm/ProjectRoleDeleteConfirm.tsx +++ /dev/null @@ -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>; - handleDeleteRole: (id: number) => Promise; - confirmName: string; - setConfirmName: React.Dispatch>; -} - -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) => - setConfirmName(e.currentTarget.value); - - const handleCancel = () => { - setDialogue(false); - setConfirmName(''); - }; - const formId = 'delete-project-role-confirmation-form'; - return ( - handleDeleteRole(role.id)} - disabledPrimaryButton={role?.name !== confirmName} - onClose={handleCancel} - formId={formId} - > - - Danger. Deleting this role will result in removing all - permissions that are active in this environment across all - feature toggles. - - - - In order to delete this role, please enter the name of the role - in the textfield below: {role?.name} - - -
- - -
- ); -}; - -export default ProjectRoleDeleteConfirm; diff --git a/frontend/src/component/admin/projectRoles/ProjectRoles/ProjectRoleList/ProjectRoleList.tsx b/frontend/src/component/admin/projectRoles/ProjectRoles/ProjectRoleList/ProjectRoleList.tsx deleted file mode 100644 index 972c765301..0000000000 --- a/frontend/src/component/admin/projectRoles/ProjectRoles/ProjectRoleList/ProjectRoleList.tsx +++ /dev/null @@ -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(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: () => ( - } - /> - ), - 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) => ( - - { - navigate(`/admin/project-roles/${id}/edit`); - }} - permission={ADMIN} - tooltipProps={{ - title: - type === BUILTIN_ROLE_TYPE - ? 'You cannot edit role' - : 'Edit role', - }} - > - - - { - setCurrentRole({ - id, - name, - description, - } as IProjectRole); - setDelDialog(true); - }} - permission={ADMIN} - tooltipProps={{ - title: - type === BUILTIN_ROLE_TYPE - ? 'You cannot remove role' - : 'Remove role', - }} - > - - - - ), - 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 ( - - - - - - } - /> - } - > - - - - - {rows.map(row => { - prepareRow(row); - return ( - - {row.cells.map(cell => ( - - {cell.render('Cell')} - - ))} - - ); - })} - -
-
- 0} - show={ - - No project roles found matching “ - {globalFilter} - ” - - } - elseShow={ - - No project roles available. Get started by - adding one. - - } - /> - } - /> - - -
- ); -}; - -export default ProjectRoleList; diff --git a/frontend/src/component/admin/projectRoles/ProjectRoles/ProjectRoles.styles.ts b/frontend/src/component/admin/projectRoles/ProjectRoles/ProjectRoles.styles.ts deleted file mode 100644 index fb25df6238..0000000000 --- a/frontend/src/component/admin/projectRoles/ProjectRoles/ProjectRoles.styles.ts +++ /dev/null @@ -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', - }, -})); diff --git a/frontend/src/component/admin/projectRoles/ProjectRoles/ProjectRoles.tsx b/frontend/src/component/admin/projectRoles/ProjectRoles/ProjectRoles.tsx deleted file mode 100644 index 8fdc168476..0000000000 --- a/frontend/src/component/admin/projectRoles/ProjectRoles/ProjectRoles.tsx +++ /dev/null @@ -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 ( -
- } - elseShow={} - /> -
- ); -}; - -export default ProjectRoles; diff --git a/frontend/src/component/admin/projectRoles/hooks/useProjectRoleForm.ts b/frontend/src/component/admin/projectRoles/hooks/useProjectRoleForm.ts deleted file mode 100644 index 205e3daf79..0000000000 --- a/frontend/src/component/admin/projectRoles/hooks/useProjectRoleForm.ts +++ /dev/null @@ -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({}); - - 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; diff --git a/frontend/src/component/admin/projectRoles/ProjectRoleForm/PermissionAccordion/PermissionAccordion.tsx b/frontend/src/component/admin/roles/RoleForm/PermissionAccordion/PermissionAccordion.tsx similarity index 71% rename from frontend/src/component/admin/projectRoles/ProjectRoleForm/PermissionAccordion/PermissionAccordion.tsx rename to frontend/src/component/admin/roles/RoleForm/PermissionAccordion/PermissionAccordion.tsx index a6268fddc5..59d73e7f24 100644 --- a/frontend/src/component/admin/projectRoles/ProjectRoleForm/PermissionAccordion/PermissionAccordion.tsx +++ b/frontend/src/component/admin/roles/RoleForm/PermissionAccordion/PermissionAccordion.tsx @@ -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 = ({ context, onPermissionChange, onCheckAll, - getRoleKey = permission => permission.id.toString(), }) => { const [expanded, setExpanded] = useState(isInitiallyExpanded); const permissionMap = useMemo( @@ -141,35 +139,32 @@ export const PermissionAccordion: VFC = ({ all {context} permissions - {permissions?.map((permission: IPermission) => { - return ( - - onPermissionChange(permission) - } - color="primary" - /> - } - label={permission.displayName} - /> - ); - })} + {permissions?.map((permission: IPermission) => ( + + onPermissionChange(permission) + } + color="primary" + /> + } + label={permission.displayName} + /> + ))} diff --git a/frontend/src/component/admin/roles/RoleForm/RoleForm.tsx b/frontend/src/component/admin/roles/RoleForm/RoleForm.tsx index f764a49c50..fe458a0e30 100644 --- a/frontend/src/component/admin/roles/RoleForm/RoleForm.tsx +++ b/frontend/src/component/admin/roles/RoleForm/RoleForm.tsx @@ -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 >; - 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 = ({ What is your role allowed to do? - {[...categories].map(category => ( + {categories.map(({ label, type, permissions }) => ( } - permissions={categorizedPermissions - .filter( - ({ category: pCategory }) => pCategory === category + key={label} + title={`${label} permissions`} + context={label.toLowerCase()} + Icon={ + type === PROJECT_PERMISSION_TYPE ? ( + + ) : type === ENVIRONMENT_PERMISSION_TYPE ? ( + + ) : ( + ) - .map(({ permission }) => permission)} + } + permissions={permissions} checkedPermissions={checkedPermissions} onPermissionChange={(permission: IPermission) => onPermissionChange(permission) } - onCheckAll={() => onCheckAll(category)} + onCheckAll={() => onCheckAll(permissions)} /> ))} diff --git a/frontend/src/component/admin/roles/RoleForm/useRoleForm.ts b/frontend/src/component/admin/roles/RoleForm/useRoleForm.ts index 709e6541f2..4619bbcd9b 100644 --- a/frontend/src/component/admin/roles/RoleForm/useRoleForm.ts +++ b/frontend/src/component/admin/roles/RoleForm/useRoleForm.ts @@ -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), }); diff --git a/frontend/src/component/admin/roles/RoleModal/RoleModal.tsx b/frontend/src/component/admin/roles/RoleModal/RoleModal.tsx index af50d584c7..23af41740d 100644 --- a/frontend/src/component/admin/roles/RoleModal/RoleModal.tsx +++ b/frontend/src/component/admin/roles/RoleModal/RoleModal.tsx @@ -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>; } -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 ( { setOpen(false); }} - label={editing ? 'Edit role' : 'New role'} + label={editing ? `Edit ${type} role` : `New ${type} role`} > diff --git a/frontend/src/component/admin/roles/Roles.tsx b/frontend/src/component/admin/roles/Roles.tsx index 2fea3bfd38..561888b907 100644 --- a/frontend/src/component/admin/roles/Roles.tsx +++ b/frontend/src/component/admin/roles/Roles.tsx @@ -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 ( +
+ } + elseShow={} + /> +
+ ); + } return (
} + show={ + + {tabs.map(({ label, path }) => ( + + {label} + + } + /> + ))} + + } + > + + + } + /> + } /> + + + } elseShow={} />
diff --git a/frontend/src/component/admin/roles/RolesTable/RolePermissionsCell/RolePermissionsCell.tsx b/frontend/src/component/admin/roles/RolesTable/RolePermissionsCell/RolePermissionsCell.tsx index 0b60064017..0560e85911 100644 --- a/frontend/src/component/admin/roles/RolesTable/RolePermissionsCell/RolePermissionsCell.tsx +++ b/frontend/src/component/admin/roles/RolesTable/RolePermissionsCell/RolePermissionsCell.tsx @@ -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 = ({ 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 ( diff --git a/frontend/src/component/admin/roles/RolesTable/RolesActionsCell/RolesActionsCell.tsx b/frontend/src/component/admin/roles/RolesTable/RolesActionsCell/RolesActionsCell.tsx index 1353c20f43..e8ee369a3d 100644 --- a/frontend/src/component/admin/roles/RolesTable/RolesActionsCell/RolesActionsCell.tsx +++ b/frontend/src/component/admin/roles/RolesTable/RolesActionsCell/RolesActionsCell.tsx @@ -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 = ({ onEdit, onDelete, }) => { - const defaultRole = role.type === DEFAULT_ROOT_ROLE; + const defaultRole = PREDEFINED_ROLE_TYPES.includes(role.type); return ( diff --git a/frontend/src/component/admin/roles/RolesTable/RolesCell/RolesCell.tsx b/frontend/src/component/admin/roles/RolesTable/RolesCell/RolesCell.tsx index fda9123324..8131e3b257 100644 --- a/frontend/src/component/admin/roles/RolesTable/RolesCell/RolesCell.tsx +++ b/frontend/src/component/admin/roles/RolesTable/RolesCell/RolesCell.tsx @@ -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={ Predefined} /> } diff --git a/frontend/src/component/admin/roles/RolesTable/RolesTable.tsx b/frontend/src/component/admin/roles/RolesTable/RolesTable.tsx index 0a787249e8..7659eec744 100644 --- a/frontend/src/component/admin/roles/RolesTable/RolesTable.tsx +++ b/frontend/src/component/admin/roles/RolesTable/RolesTable.tsx @@ -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 ( { setModalOpen(true); }} > - New role + New {type} role } @@ -204,20 +215,22 @@ export const RolesTable = () => { condition={searchValue?.length > 0} show={ - No roles found matching “ + No {type} roles found matching “ {searchValue} ” } elseShow={ - No roles available. Get started by adding one. + No {type} roles available. Get started by adding + one. } /> } /> 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 ( @@ -75,22 +73,18 @@ export const RoleDescription = ({ {description} 0 && role.type !== 'root' - } + condition={!PREDEFINED_ROLE_TYPES.includes(role.type)} show={() => - [...categories].map(category => ( - + categories.map(({ label, permissions }) => ( + - {category} + {label} - {categorizedPermissions - .filter(({ category: c }) => c === category) - .map(({ permission }) => ( -

- {permission.displayName} -

- ))} + {permissions.map(permission => ( +

+ {permission.displayName} +

+ ))}
)) } diff --git a/frontend/src/component/menu/routes.ts b/frontend/src/component/menu/routes.ts index 5e9a341d66..ae165d44e8 100644 --- a/frontend/src/component/menu/routes.ts +++ b/frontend/src/component/menu/routes.ts @@ -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'] }, }, { diff --git a/frontend/src/hooks/api/getters/useRoles/useRoles.ts b/frontend/src/hooks/api/getters/useRoles/useRoles.ts index 5f11a04a45..70723795fc 100644 --- a/frontend/src/hooks/api/getters/useRoles/useRoles.ts +++ b/frontend/src/hooks/api/getters/useRoles/useRoles.ts @@ -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); diff --git a/frontend/src/interfaces/permissions.ts b/frontend/src/interfaces/permissions.ts index baf43dbf84..cd7a4fd058 100644 --- a/frontend/src/interfaces/permissions.ts +++ b/frontend/src/interfaces/permissions.ts @@ -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[]; +} diff --git a/frontend/src/interfaces/role.ts b/frontend/src/interfaces/role.ts index 0373e3c0fd..e2916413b3 100644 --- a/frontend/src/interfaces/role.ts +++ b/frontend/src/interfaces/role.ts @@ -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; diff --git a/frontend/src/utils/permissions.ts b/frontend/src/utils/permissions.ts index a112c2b06a..8cea8ba5f4 100644 --- a/frontend/src/utils/permissions.ts +++ b/frontend/src/utils/permissions.ts @@ -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); + } +}; diff --git a/src/lib/util/constants.ts b/src/lib/util/constants.ts index d317034a79..bb00b4416b 100644 --- a/src/lib/util/constants.ts +++ b/src/lib/util/constants.ts @@ -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 */