1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-15 17:50:48 +02:00

refactor: address custom root roles PR comments (#3994)

https://linear.app/unleash/issue/2-1135/address-3975-pr-comments-by-refactoring-some-of-the-new-custom-root

This pull request addresses the majority of the comments raised in issue
#3975 and lays the groundwork for unifying roles. The idea is for
project roles to also be managed in the "Roles" tab, and several
components, such as `RoleForm` and the `useRoleForm` can potentially be
reused.

I'll leave the further investigation and implementation of unifying
roles to be addressed in a separate task.

As a mostly unrelated UI fix, this also adds an arrow to the tooltip in
the `RoleBadge` component.
This commit is contained in:
Nuno Góis 2023-06-15 14:03:47 +01:00 committed by GitHub
parent c7ff3b472e
commit 58607f7f48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 119 additions and 112 deletions

View File

@ -48,7 +48,7 @@ export const PermissionAccordion: VFC<IEnvironmentPermissionAccordionProps> = ({
permissions, permissions,
checkedPermissions, checkedPermissions,
Icon, Icon,
isInitiallyExpanded, isInitiallyExpanded = false,
context, context,
onPermissionChange, onPermissionChange,
onCheckAll, onCheckAll,

View File

@ -5,7 +5,7 @@ import { Person as UserIcon } from '@mui/icons-material';
import { ICheckedPermissions, IPermission } from 'interfaces/permissions'; import { ICheckedPermissions, IPermission } from 'interfaces/permissions';
import { IRoleFormErrors } from './useRoleForm'; import { IRoleFormErrors } from './useRoleForm';
import { ROOT_PERMISSION_CATEGORIES } from '@server/types/permissions'; import { ROOT_PERMISSION_CATEGORIES } from '@server/types/permissions';
import cloneDeep from 'lodash.clonedeep'; import { toggleAllPermissions, togglePermission } from 'utils/permissions';
const StyledInputDescription = styled('p')(({ theme }) => ({ const StyledInputDescription = styled('p')(({ theme }) => ({
display: 'flex', display: 'flex',
@ -30,7 +30,6 @@ interface IRoleFormProps {
setCheckedPermissions: React.Dispatch< setCheckedPermissions: React.Dispatch<
React.SetStateAction<ICheckedPermissions> React.SetStateAction<ICheckedPermissions>
>; >;
handlePermissionChange: (permission: IPermission) => void;
permissions: IPermission[]; permissions: IPermission[];
errors: IRoleFormErrors; errors: IRoleFormErrors;
} }
@ -42,7 +41,6 @@ export const RoleForm = ({
setDescription, setDescription,
checkedPermissions, checkedPermissions,
setCheckedPermissions, setCheckedPermissions,
handlePermissionChange,
permissions, permissions,
errors, errors,
}: IRoleFormProps) => { }: IRoleFormProps) => {
@ -61,30 +59,25 @@ export const RoleForm = ({
categorizedPermissions.map(({ category }) => category).sort() categorizedPermissions.map(({ category }) => category).sort()
); );
const onToggleAllPermissions = (category: string) => { const onPermissionChange = (permission: IPermission) => {
let checkedPermissionsCopy = cloneDeep(checkedPermissions); const newCheckedPermissions = togglePermission(
checkedPermissions,
permission
);
setCheckedPermissions(newCheckedPermissions);
};
const onCheckAll = (category: string) => {
const categoryPermissions = categorizedPermissions const categoryPermissions = categorizedPermissions
.filter(({ category: pCategory }) => pCategory === category) .filter(({ category: pCategory }) => pCategory === category)
.map(({ permission }) => permission); .map(({ permission }) => permission);
const allChecked = categoryPermissions.every( const newCheckedPermissions = toggleAllPermissions(
(permission: IPermission) => checkedPermissionsCopy[permission.id] checkedPermissions,
categoryPermissions
); );
if (allChecked) { setCheckedPermissions(newCheckedPermissions);
categoryPermissions.forEach((permission: IPermission) => {
delete checkedPermissionsCopy[permission.id];
});
} else {
categoryPermissions.forEach((permission: IPermission) => {
checkedPermissionsCopy[permission.id] = {
...permission,
};
});
}
setCheckedPermissions(checkedPermissionsCopy);
}; };
return ( return (
@ -128,9 +121,9 @@ export const RoleForm = ({
.map(({ permission }) => permission)} .map(({ permission }) => permission)}
checkedPermissions={checkedPermissions} checkedPermissions={checkedPermissions}
onPermissionChange={(permission: IPermission) => onPermissionChange={(permission: IPermission) =>
handlePermissionChange(permission) onPermissionChange(permission)
} }
onCheckAll={() => onToggleAllPermissions(category)} onCheckAll={() => onCheckAll(category)}
/> />
))} ))}
</div> </div>

View File

@ -1,9 +1,8 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { IPermission, ICheckedPermissions } from 'interfaces/permissions'; import { IPermission, ICheckedPermissions } from 'interfaces/permissions';
import cloneDeep from 'lodash.clonedeep';
import usePermissions from 'hooks/api/getters/usePermissions/usePermissions';
import IRole from 'interfaces/role'; import IRole from 'interfaces/role';
import { useRoles } from 'hooks/api/getters/useRoles/useRoles'; import { useRoles } from 'hooks/api/getters/useRoles/useRoles';
import { permissionsToCheckedPermissions } from 'utils/permissions';
enum ErrorField { enum ErrorField {
NAME = 'name', NAME = 'name',
@ -19,33 +18,11 @@ export const useRoleForm = (
initialPermissions: IPermission[] = [] initialPermissions: IPermission[] = []
) => { ) => {
const { roles } = useRoles(); const { roles } = useRoles();
const { permissions } = usePermissions({
revalidateIfStale: false,
revalidateOnReconnect: false,
revalidateOnFocus: false,
});
const rootPermissions = permissions.root.filter(
({ name }) => name !== 'ADMIN'
);
const [name, setName] = useState(initialName); const [name, setName] = useState(initialName);
const [description, setDescription] = useState(initialDescription); const [description, setDescription] = useState(initialDescription);
const [checkedPermissions, setCheckedPermissions] = const [checkedPermissions, setCheckedPermissions] =
useState<ICheckedPermissions>({}); useState<ICheckedPermissions>({});
useEffect(() => {
setCheckedPermissions(
initialPermissions.reduce(
(acc: { [key: string]: IPermission }, curr: IPermission) => {
acc[curr.id] = curr;
return acc;
},
{}
)
);
}, [initialPermissions.length]);
const [errors, setErrors] = useState<IRoleFormErrors>({}); const [errors, setErrors] = useState<IRoleFormErrors>({});
useEffect(() => { useEffect(() => {
@ -56,44 +33,16 @@ export const useRoleForm = (
setDescription(initialDescription); setDescription(initialDescription);
}, [initialDescription]); }, [initialDescription]);
const handlePermissionChange = (permission: IPermission) => { useEffect(() => {
let checkedPermissionsCopy = cloneDeep(checkedPermissions); const newCheckedPermissions =
permissionsToCheckedPermissions(initialPermissions);
setCheckedPermissions(newCheckedPermissions);
}, [initialPermissions.length]);
if (checkedPermissionsCopy[permission.id]) { const getRolePayload = (type: 'root-custom' | 'custom' = 'custom') => ({
delete checkedPermissionsCopy[permission.id];
} else {
checkedPermissionsCopy[permission.id] = { ...permission };
}
setCheckedPermissions(checkedPermissionsCopy);
};
const onToggleAllPermissions = () => {
let checkedPermissionsCopy = cloneDeep(checkedPermissions);
const allChecked = rootPermissions.every(
(permission: IPermission) => checkedPermissionsCopy[permission.id]
);
if (allChecked) {
rootPermissions.forEach((permission: IPermission) => {
delete checkedPermissionsCopy[permission.id];
});
} else {
rootPermissions.forEach((permission: IPermission) => {
checkedPermissionsCopy[permission.id] = {
...permission,
};
});
}
setCheckedPermissions(checkedPermissionsCopy);
};
const getRolePayload = () => ({
name, name,
description, description,
type: 'root-custom', type,
permissions: Object.values(checkedPermissions), permissions: Object.values(checkedPermissions),
}); });
@ -121,14 +70,11 @@ export const useRoleForm = (
return { return {
name, name,
description, description,
errors,
checkedPermissions, checkedPermissions,
rootPermissions, errors,
setName, setName,
setDescription, setDescription,
setCheckedPermissions, setCheckedPermissions,
handlePermissionChange,
onToggleAllPermissions,
getRolePayload, getRolePayload,
clearError, clearError,
setError, setError,

View File

@ -10,6 +10,7 @@ import { formatUnknownError } from 'utils/formatUnknownError';
import { FormEvent } from 'react'; import { FormEvent } from 'react';
import { useRolesApi } from 'hooks/api/actions/useRolesApi/useRolesApi'; import { useRolesApi } from 'hooks/api/actions/useRolesApi/useRolesApi';
import { useRole } from 'hooks/api/getters/useRole/useRole'; import { useRole } from 'hooks/api/getters/useRole/useRole';
import usePermissions from 'hooks/api/getters/usePermissions/usePermissions';
const StyledForm = styled('form')(() => ({ const StyledForm = styled('form')(() => ({
display: 'flex', display: 'flex',
@ -44,12 +45,10 @@ export const RoleModal = ({ roleId, open, setOpen }: IRoleModalProps) => {
setDescription, setDescription,
checkedPermissions, checkedPermissions,
setCheckedPermissions, setCheckedPermissions,
handlePermissionChange,
getRolePayload, getRolePayload,
isNameUnique, isNameUnique,
isNotEmpty, isNotEmpty,
hasPermissions, hasPermissions,
rootPermissions,
errors, errors,
setError, setError,
clearError, clearError,
@ -57,9 +56,18 @@ export const RoleModal = ({ roleId, open, setOpen }: IRoleModalProps) => {
} = useRoleForm(role?.name, role?.description, role?.permissions); } = useRoleForm(role?.name, role?.description, role?.permissions);
const { refetch: refetchRoles } = useRoles(); const { refetch: refetchRoles } = useRoles();
const { addRole, updateRole, loading } = useRolesApi(); const { addRole, updateRole, loading } = useRolesApi();
const { permissions } = usePermissions({
revalidateIfStale: false,
revalidateOnReconnect: false,
revalidateOnFocus: false,
});
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
const { uiConfig } = useUiConfig(); const { uiConfig } = useUiConfig();
const rootPermissions = permissions.root.filter(
({ name }) => name !== 'ADMIN'
);
const editing = role !== undefined; const editing = role !== undefined;
const isValid = const isValid =
isNameUnique(name) && isNameUnique(name) &&
@ -67,13 +75,15 @@ export const RoleModal = ({ roleId, open, setOpen }: IRoleModalProps) => {
isNotEmpty(description) && isNotEmpty(description) &&
hasPermissions(checkedPermissions); hasPermissions(checkedPermissions);
const payload = getRolePayload('root-custom');
const formatApiCode = () => { const formatApiCode = () => {
return `curl --location --request ${editing ? 'PUT' : 'POST'} '${ return `curl --location --request ${editing ? 'PUT' : 'POST'} '${
uiConfig.unleashUrl uiConfig.unleashUrl
}/api/admin/roles${editing ? `/${role.id}` : ''}' \\ }/api/admin/roles${editing ? `/${role.id}` : ''}' \\
--header 'Authorization: INSERT_API_KEY' \\ --header 'Authorization: INSERT_API_KEY' \\
--header 'Content-Type: application/json' \\ --header 'Content-Type: application/json' \\
--data-raw '${JSON.stringify(getRolePayload(), undefined, 2)}'`; --data-raw '${JSON.stringify(payload, undefined, 2)}'`;
}; };
const onSetName = (name: string) => { const onSetName = (name: string) => {
@ -96,9 +106,9 @@ export const RoleModal = ({ roleId, open, setOpen }: IRoleModalProps) => {
try { try {
if (editing) { if (editing) {
await updateRole(role.id, getRolePayload()); await updateRole(role.id, payload);
} else { } else {
await addRole(getRolePayload()); await addRole(payload);
} }
setToastData({ setToastData({
title: `Role ${editing ? 'updated' : 'added'} successfully`, title: `Role ${editing ? 'updated' : 'added'} successfully`,
@ -136,7 +146,6 @@ export const RoleModal = ({ roleId, open, setOpen }: IRoleModalProps) => {
setDescription={setDescription} setDescription={setDescription}
checkedPermissions={checkedPermissions} checkedPermissions={checkedPermissions}
setCheckedPermissions={setCheckedPermissions} setCheckedPermissions={setCheckedPermissions}
handlePermissionChange={handlePermissionChange}
permissions={rootPermissions} permissions={rootPermissions}
errors={errors} errors={errors}
/> />

View File

@ -14,7 +14,7 @@ export const RoleBadge = ({ roleId }: IRoleBadgeProps) => {
if (!role) return null; if (!role) return null;
return ( return (
<HtmlTooltip title={<RoleDescription roleId={roleId} tooltip />}> <HtmlTooltip title={<RoleDescription roleId={roleId} tooltip />} arrow>
<Badge <Badge
color="success" color="success"
icon={<UserIcon />} icon={<UserIcon />}

View File

@ -2,21 +2,11 @@ import useSWR, { mutate, SWRConfiguration } from 'swr';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { formatApiPath } from 'utils/formatPath'; import { formatApiPath } from 'utils/formatPath';
import { import { IPermissions } from 'interfaces/permissions';
IProjectEnvironmentPermissions,
IPermissions,
IPermission,
} from 'interfaces/permissions';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
interface IUsePermissions { interface IUsePermissions {
permissions: permissions: IPermissions;
| IPermissions
| {
root: IPermission[];
project: IPermission[];
environments: IProjectEnvironmentPermissions[];
};
loading: boolean; loading: boolean;
refetch: () => void; refetch: () => void;
error: any; error: any;

View File

@ -2,12 +2,12 @@ import { SWRConfiguration } from 'swr';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { formatApiPath } from 'utils/formatPath'; import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
import IRole from 'interfaces/role'; import IRole, { IRoleWithPermissions } from 'interfaces/role';
import useUiConfig from '../useUiConfig/useUiConfig'; import useUiConfig from '../useUiConfig/useUiConfig';
import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR'; import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
export interface IUseRoleOutput { export interface IUseRoleOutput {
role?: IRole; role?: IRoleWithPermissions;
refetch: () => void; refetch: () => void;
loading: boolean; loading: boolean;
error?: Error; error?: Error;
@ -42,16 +42,19 @@ export const useRole = (
return useMemo(() => { return useMemo(() => {
if (!isEnterprise()) { if (!isEnterprise()) {
return { return {
role: ((ossData?.rootRoles ?? []) as IRole[]).find( role: {
({ id: rId }) => rId === +id! ...((ossData?.rootRoles ?? []) as IRole[]).find(
), ({ id: rId }) => rId === +id!
),
permissions: [],
} as IRoleWithPermissions,
loading: !ossError && !ossData, loading: !ossError && !ossData,
refetch: () => ossMutate(), refetch: () => ossMutate(),
error: ossError, error: ossError,
}; };
} else { } else {
return { return {
role: data as IRole, role: data as IRoleWithPermissions,
loading: !error && !data, loading: !error && !data,
refetch: () => mutate(), refetch: () => mutate(),
error, error,

View File

@ -6,7 +6,10 @@ interface IRole {
project: string | null; project: string | null;
description: string; description: string;
type: string; type: string;
permissions?: IPermission[]; }
export interface IRoleWithPermissions extends IRole {
permissions: IPermission[];
} }
export interface IProjectRole { export interface IProjectRole {

View File

@ -0,0 +1,63 @@
import { IPermission, ICheckedPermissions } from 'interfaces/permissions';
import cloneDeep from 'lodash.clonedeep';
const getRoleKey = (permission: IPermission): string => {
return permission.environment
? `${permission.id}-${permission.environment}`
: `${permission.id}`;
};
export const permissionsToCheckedPermissions = (
permissions: IPermission[]
): ICheckedPermissions =>
permissions.reduce(
(
checkedPermissions: { [key: string]: IPermission },
permission: IPermission
) => {
checkedPermissions[getRoleKey(permission)] = permission;
return checkedPermissions;
},
{}
);
export const togglePermission = (
checkedPermissions: ICheckedPermissions,
permission: IPermission
): ICheckedPermissions => {
let checkedPermissionsCopy = cloneDeep(checkedPermissions);
if (checkedPermissionsCopy[getRoleKey(permission)]) {
delete checkedPermissionsCopy[getRoleKey(permission)];
} else {
checkedPermissionsCopy[getRoleKey(permission)] = { ...permission };
}
return checkedPermissionsCopy;
};
export const toggleAllPermissions = (
checkedPermissions: ICheckedPermissions,
toggledPermissions: IPermission[]
): ICheckedPermissions => {
let checkedPermissionsCopy = cloneDeep(checkedPermissions);
const allChecked = toggledPermissions.every(
(permission: IPermission) =>
checkedPermissionsCopy[getRoleKey(permission)]
);
if (allChecked) {
toggledPermissions.forEach((permission: IPermission) => {
delete checkedPermissionsCopy[getRoleKey(permission)];
});
} else {
toggledPermissions.forEach((permission: IPermission) => {
checkedPermissionsCopy[getRoleKey(permission)] = {
...permission,
};
});
}
return checkedPermissionsCopy;
};