Skip to content

Commit

Permalink
refactor: address custom root roles PR comments (#3994)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
nunogois committed Jun 15, 2023
1 parent c7ff3b4 commit 58607f7
Show file tree
Hide file tree
Showing 9 changed files with 119 additions and 112 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const PermissionAccordion: VFC<IEnvironmentPermissionAccordionProps> = ({
permissions,
checkedPermissions,
Icon,
isInitiallyExpanded,
isInitiallyExpanded = false,
context,
onPermissionChange,
onCheckAll,
Expand Down
37 changes: 15 additions & 22 deletions frontend/src/component/admin/roles/RoleForm/RoleForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Person as UserIcon } from '@mui/icons-material';
import { ICheckedPermissions, IPermission } from 'interfaces/permissions';
import { IRoleFormErrors } from './useRoleForm';
import { ROOT_PERMISSION_CATEGORIES } from '@server/types/permissions';
import cloneDeep from 'lodash.clonedeep';
import { toggleAllPermissions, togglePermission } from 'utils/permissions';

const StyledInputDescription = styled('p')(({ theme }) => ({
display: 'flex',
Expand All @@ -30,7 +30,6 @@ interface IRoleFormProps {
setCheckedPermissions: React.Dispatch<
React.SetStateAction<ICheckedPermissions>
>;
handlePermissionChange: (permission: IPermission) => void;
permissions: IPermission[];
errors: IRoleFormErrors;
}
Expand All @@ -42,7 +41,6 @@ export const RoleForm = ({
setDescription,
checkedPermissions,
setCheckedPermissions,
handlePermissionChange,
permissions,
errors,
}: IRoleFormProps) => {
Expand All @@ -61,30 +59,25 @@ export const RoleForm = ({
categorizedPermissions.map(({ category }) => category).sort()
);

const onToggleAllPermissions = (category: string) => {
let checkedPermissionsCopy = cloneDeep(checkedPermissions);
const onPermissionChange = (permission: IPermission) => {
const newCheckedPermissions = togglePermission(
checkedPermissions,
permission
);
setCheckedPermissions(newCheckedPermissions);
};

const onCheckAll = (category: string) => {
const categoryPermissions = categorizedPermissions
.filter(({ category: pCategory }) => pCategory === category)
.map(({ permission }) => permission);

const allChecked = categoryPermissions.every(
(permission: IPermission) => checkedPermissionsCopy[permission.id]
const newCheckedPermissions = toggleAllPermissions(
checkedPermissions,
categoryPermissions
);

if (allChecked) {
categoryPermissions.forEach((permission: IPermission) => {
delete checkedPermissionsCopy[permission.id];
});
} else {
categoryPermissions.forEach((permission: IPermission) => {
checkedPermissionsCopy[permission.id] = {
...permission,
};
});
}

setCheckedPermissions(checkedPermissionsCopy);
setCheckedPermissions(newCheckedPermissions);
};

return (
Expand Down Expand Up @@ -128,9 +121,9 @@ export const RoleForm = ({
.map(({ permission }) => permission)}
checkedPermissions={checkedPermissions}
onPermissionChange={(permission: IPermission) =>
handlePermissionChange(permission)
onPermissionChange(permission)
}
onCheckAll={() => onToggleAllPermissions(category)}
onCheckAll={() => onCheckAll(category)}
/>
))}
</div>
Expand Down
72 changes: 9 additions & 63 deletions frontend/src/component/admin/roles/RoleForm/useRoleForm.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { useEffect, useState } from 'react';
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 { useRoles } from 'hooks/api/getters/useRoles/useRoles';
import { permissionsToCheckedPermissions } from 'utils/permissions';

enum ErrorField {
NAME = 'name',
Expand All @@ -19,33 +18,11 @@ export const useRoleForm = (
initialPermissions: IPermission[] = []
) => {
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 [description, setDescription] = useState(initialDescription);
const [checkedPermissions, setCheckedPermissions] =
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>({});

useEffect(() => {
Expand All @@ -56,44 +33,16 @@ export const useRoleForm = (
setDescription(initialDescription);
}, [initialDescription]);

const handlePermissionChange = (permission: IPermission) => {
let checkedPermissionsCopy = cloneDeep(checkedPermissions);

if (checkedPermissionsCopy[permission.id]) {
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);
};
useEffect(() => {
const newCheckedPermissions =
permissionsToCheckedPermissions(initialPermissions);
setCheckedPermissions(newCheckedPermissions);
}, [initialPermissions.length]);

const getRolePayload = () => ({
const getRolePayload = (type: 'root-custom' | 'custom' = 'custom') => ({
name,
description,
type: 'root-custom',
type,
permissions: Object.values(checkedPermissions),
});

Expand Down Expand Up @@ -121,14 +70,11 @@ export const useRoleForm = (
return {
name,
description,
errors,
checkedPermissions,
rootPermissions,
errors,
setName,
setDescription,
setCheckedPermissions,
handlePermissionChange,
onToggleAllPermissions,
getRolePayload,
clearError,
setError,
Expand Down
21 changes: 15 additions & 6 deletions frontend/src/component/admin/roles/RoleModal/RoleModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ 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';

const StyledForm = styled('form')(() => ({
display: 'flex',
Expand Down Expand Up @@ -44,36 +45,45 @@ export const RoleModal = ({ roleId, open, setOpen }: IRoleModalProps) => {
setDescription,
checkedPermissions,
setCheckedPermissions,
handlePermissionChange,
getRolePayload,
isNameUnique,
isNotEmpty,
hasPermissions,
rootPermissions,
errors,
setError,
clearError,
ErrorField,
} = 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) &&
isNotEmpty(name) &&
isNotEmpty(description) &&
hasPermissions(checkedPermissions);

const payload = getRolePayload('root-custom');

const formatApiCode = () => {
return `curl --location --request ${editing ? 'PUT' : 'POST'} '${
uiConfig.unleashUrl
}/api/admin/roles${editing ? `/${role.id}` : ''}' \\
--header 'Authorization: INSERT_API_KEY' \\
--header 'Content-Type: application/json' \\
--data-raw '${JSON.stringify(getRolePayload(), undefined, 2)}'`;
--data-raw '${JSON.stringify(payload, undefined, 2)}'`;
};

const onSetName = (name: string) => {
Expand All @@ -96,9 +106,9 @@ export const RoleModal = ({ roleId, open, setOpen }: IRoleModalProps) => {

try {
if (editing) {
await updateRole(role.id, getRolePayload());
await updateRole(role.id, payload);
} else {
await addRole(getRolePayload());
await addRole(payload);
}
setToastData({
title: `Role ${editing ? 'updated' : 'added'} successfully`,
Expand Down Expand Up @@ -136,7 +146,6 @@ export const RoleModal = ({ roleId, open, setOpen }: IRoleModalProps) => {
setDescription={setDescription}
checkedPermissions={checkedPermissions}
setCheckedPermissions={setCheckedPermissions}
handlePermissionChange={handlePermissionChange}
permissions={rootPermissions}
errors={errors}
/>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/component/common/RoleBadge/RoleBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const RoleBadge = ({ roleId }: IRoleBadgeProps) => {
if (!role) return null;

return (
<HtmlTooltip title={<RoleDescription roleId={roleId} tooltip />}>
<HtmlTooltip title={<RoleDescription roleId={roleId} tooltip />} arrow>
<Badge
color="success"
icon={<UserIcon />}
Expand Down
14 changes: 2 additions & 12 deletions frontend/src/hooks/api/getters/usePermissions/usePermissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,11 @@ import useSWR, { mutate, SWRConfiguration } from 'swr';
import { useState, useEffect } from 'react';
import { formatApiPath } from 'utils/formatPath';

import {
IProjectEnvironmentPermissions,
IPermissions,
IPermission,
} from 'interfaces/permissions';
import { IPermissions } from 'interfaces/permissions';
import handleErrorResponses from '../httpErrorResponseHandler';

interface IUsePermissions {
permissions:
| IPermissions
| {
root: IPermission[];
project: IPermission[];
environments: IProjectEnvironmentPermissions[];
};
permissions: IPermissions;
loading: boolean;
refetch: () => void;
error: any;
Expand Down
15 changes: 9 additions & 6 deletions frontend/src/hooks/api/getters/useRole/useRole.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { SWRConfiguration } from 'swr';
import { useMemo } from 'react';
import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler';
import IRole from 'interfaces/role';
import IRole, { IRoleWithPermissions } from 'interfaces/role';
import useUiConfig from '../useUiConfig/useUiConfig';
import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';

export interface IUseRoleOutput {
role?: IRole;
role?: IRoleWithPermissions;
refetch: () => void;
loading: boolean;
error?: Error;
Expand Down Expand Up @@ -42,16 +42,19 @@ export const useRole = (
return useMemo(() => {
if (!isEnterprise()) {
return {
role: ((ossData?.rootRoles ?? []) as IRole[]).find(
({ id: rId }) => rId === +id!
),
role: {
...((ossData?.rootRoles ?? []) as IRole[]).find(
({ id: rId }) => rId === +id!
),
permissions: [],
} as IRoleWithPermissions,
loading: !ossError && !ossData,
refetch: () => ossMutate(),
error: ossError,
};
} else {
return {
role: data as IRole,
role: data as IRoleWithPermissions,
loading: !error && !data,
refetch: () => mutate(),
error,
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/interfaces/role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ interface IRole {
project: string | null;
description: string;
type: string;
permissions?: IPermission[];
}

export interface IRoleWithPermissions extends IRole {
permissions: IPermission[];
}

export interface IProjectRole {
Expand Down
Loading

0 comments on commit 58607f7

Please sign in to comment.