From 83d59c3af31a75884d16d2b196e95f73d3c0f903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Tue, 13 Jun 2023 18:44:36 +0100 Subject: [PATCH] adapt new role logic in all the places --- .../admin/groups/GroupForm/GroupForm.tsx | 28 ++---- .../groups/GroupsList/GroupCard/GroupCard.tsx | 16 +-- .../admin/groups/GroupsList/GroupsList.tsx | 8 -- .../EditProjectRole/EditProjectRole.tsx | 9 +- .../admin/roles/RoleForm/RoleForm.tsx | 1 + .../admin/roles/RoleModal/RoleModal.tsx | 15 ++- .../RoleDeleteDialog/RoleDeleteDialog.tsx | 27 ++++- .../RoleDeleteDialogGroups.tsx | 86 ++++++++++++++++ .../RoleDeleteDialogServiceAccounts.tsx | 7 +- .../RoleDeleteDialogUsers.tsx | 8 +- .../RolePermissionsCell.tsx | 31 ++++++ .../admin/roles/RolesTable/RolesTable.tsx | 9 +- .../ServiceAccountsTable.tsx | 4 + .../admin/users/UserForm/UserForm.tsx | 76 ++------------ .../admin/users/UsersList/UsersList.tsx | 4 + .../admin/users/hooks/useAddUserForm.ts | 14 ++- .../component/common/RoleBadge/RoleBadge.tsx | 23 +++++ .../RoleDescription/RoleDescription.tsx | 98 +++++++++++++++++++ .../common/RoleSelect/RoleSelect.tsx | 10 +- .../common/Table/cells/RoleCell/RoleCell.tsx | 17 ++++ .../ProjectRoleDescription.tsx | 17 ++-- .../user/Profile/ProfileTab/ProfileTab.tsx | 28 +++--- .../useProjectRole.ts => useRole/useRole.ts} | 29 ++++-- frontend/src/interfaces/role.ts | 2 +- 24 files changed, 394 insertions(+), 173 deletions(-) create mode 100644 frontend/src/component/admin/roles/RolesTable/RoleDeleteDialog/RoleDeleteDialogGroups/RoleDeleteDialogGroups.tsx create mode 100644 frontend/src/component/admin/roles/RolesTable/RolePermissionsCell/RolePermissionsCell.tsx create mode 100644 frontend/src/component/common/RoleBadge/RoleBadge.tsx create mode 100644 frontend/src/component/common/RoleDescription/RoleDescription.tsx create mode 100644 frontend/src/component/common/Table/cells/RoleCell/RoleCell.tsx rename frontend/src/hooks/api/getters/{useProjectRole/useProjectRole.ts => useRole/useRole.ts} (55%) diff --git a/frontend/src/component/admin/groups/GroupForm/GroupForm.tsx b/frontend/src/component/admin/groups/GroupForm/GroupForm.tsx index 80830401a351..389fce959681 100644 --- a/frontend/src/component/admin/groups/GroupForm/GroupForm.tsx +++ b/frontend/src/component/admin/groups/GroupForm/GroupForm.tsx @@ -1,5 +1,5 @@ import React, { FC } from 'react'; -import { Autocomplete, Box, Button, styled, TextField } from '@mui/material'; +import { Box, Button, styled } from '@mui/material'; import { UG_DESC_ID, UG_NAME_ID } from 'utils/testIds'; import Input from 'component/common/Input/Input'; import { IGroupUser } from 'interfaces/group'; @@ -10,9 +10,10 @@ import { ItemList } from 'component/common/ItemList/ItemList'; import useAuthSettings from 'hooks/api/getters/useAuthSettings/useAuthSettings'; import { Link } from 'react-router-dom'; import { HelpIcon } from 'component/common/HelpIcon/HelpIcon'; -import { IProjectRole } from 'interfaces/role'; +import IRole, { IProjectRole } from 'interfaces/role'; import { useUsers } from 'hooks/api/getters/useUsers/useUsers'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; +import { RoleSelect } from 'component/common/RoleSelect/RoleSelect'; const StyledForm = styled('form')(() => ({ display: 'flex', @@ -128,10 +129,8 @@ export const GroupForm: FC = ({ const groupRootRolesEnabled = Boolean(uiConfig.flags.groupRootRoles); - const roleIdToRole = (rootRoleId: number | null): IProjectRole | null => { - return ( - roles.find((role: IProjectRole) => role.id === rootRoleId) || null - ); + const roleIdToRole = (rootRoleId: number | null): IRole | null => { + return roles.find((role: IRole) => role.id === rootRoleId) || null; }; const renderRoleOption = ( @@ -214,23 +213,12 @@ export const GroupForm: FC = ({ - - setRootRole(newValue?.id || null) + setValue={role => + setRootRole(role?.id || null) } - options={roles.filter( - (role: IProjectRole) => - role.name !== 'Viewer' - )} - renderOption={renderRoleOption} - getOptionLabel={option => option.name} - renderInput={params => ( - - )} /> diff --git a/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCard.tsx b/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCard.tsx index 25ec159fa615..bc74ce3d2010 100644 --- a/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCard.tsx +++ b/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCard.tsx @@ -6,7 +6,7 @@ import { GroupCardAvatars } from './GroupCardAvatars/GroupCardAvatars'; import { Badge } from 'component/common/Badge/Badge'; import { GroupCardActions } from './GroupCardActions/GroupCardActions'; import TopicOutlinedIcon from '@mui/icons-material/TopicOutlined'; -import { IProjectRole } from 'interfaces/role'; +import { RoleBadge } from 'component/common/RoleBadge/RoleBadge'; const StyledLink = styled(Link)(({ theme }) => ({ textDecoration: 'none', @@ -86,14 +86,12 @@ const InfoBadgeDescription = styled('span')(({ theme }) => ({ interface IGroupCardProps { group: IGroup; - rootRoles: IProjectRole[]; onEditUsers: (group: IGroup) => void; onRemoveGroup: (group: IGroup) => void; } export const GroupCard = ({ group, - rootRoles, onEditUsers, onRemoveGroup, }: IGroupCardProps) => { @@ -117,17 +115,7 @@ export const GroupCard = ({ show={

Root role:

- } - > - { - rootRoles.find( - (role: IProjectRole) => - role.id === group.rootRole - )?.name - } - +
} /> diff --git a/frontend/src/component/admin/groups/GroupsList/GroupsList.tsx b/frontend/src/component/admin/groups/GroupsList/GroupsList.tsx index 02e475d03f78..11b1ff1594d6 100644 --- a/frontend/src/component/admin/groups/GroupsList/GroupsList.tsx +++ b/frontend/src/component/admin/groups/GroupsList/GroupsList.tsx @@ -18,8 +18,6 @@ import { Add } from '@mui/icons-material'; import { NAVIGATE_TO_CREATE_GROUP } from 'utils/testIds'; import { EditGroupUsers } from '../Group/EditGroupUsers/EditGroupUsers'; import { RemoveGroup } from '../RemoveGroup/RemoveGroup'; -import { useUsers } from 'hooks/api/getters/useUsers/useUsers'; -import { IProjectRole } from 'interfaces/role'; type PageQueryType = Partial>; @@ -51,7 +49,6 @@ export const GroupsList: VFC = () => { const [searchValue, setSearchValue] = useState( searchParams.get('search') || '' ); - const { roles } = useUsers(); const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); @@ -85,10 +82,6 @@ export const GroupsList: VFC = () => { setRemoveOpen(true); }; - const getBindableRootRoles = () => { - return roles.filter((role: IProjectRole) => role.type === 'root'); - }; - return ( { diff --git a/frontend/src/component/admin/projectRoles/EditProjectRole/EditProjectRole.tsx b/frontend/src/component/admin/projectRoles/EditProjectRole/EditProjectRole.tsx index 17416c12e575..13aa6169b974 100644 --- a/frontend/src/component/admin/projectRoles/EditProjectRole/EditProjectRole.tsx +++ b/frontend/src/component/admin/projectRoles/EditProjectRole/EditProjectRole.tsx @@ -2,7 +2,7 @@ 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 useProjectRole from 'hooks/api/getters/useProjectRole/useProjectRole'; +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'; @@ -16,7 +16,7 @@ const EditProjectRole = () => { const { uiConfig } = useUiConfig(); const { setToastData, setToastApiError } = useToast(); const roleId = useRequiredPathParam('id'); - const { role } = useProjectRole(roleId); + const { role, refetch } = useRole(roleId); const navigate = useNavigate(); const { @@ -35,18 +35,17 @@ const EditProjectRole = () => { validateName, clearErrors, getRoleKey, - } = useProjectRoleForm(role.name, role.description, role?.permissions); + } = useProjectRoleForm(role?.name, role?.description, role?.permissions); const formatApiCode = () => { return `curl --location --request PUT '${ uiConfig.unleashUrl - }/api/admin/roles/${role.id}' \\ + }/api/admin/roles/${role?.id}' \\ --header 'Authorization: INSERT_API_KEY' \\ --header 'Content-Type: application/json' \\ --data-raw '${JSON.stringify(getProjectRolePayload(), undefined, 2)}'`; }; - const { refetch } = useProjectRole(roleId); const { updateRole, loading } = useRolesApi(); const onSubmit = async (e: Event) => { diff --git a/frontend/src/component/admin/roles/RoleForm/RoleForm.tsx b/frontend/src/component/admin/roles/RoleForm/RoleForm.tsx index 0e6e0ede7860..47b757ba4b8f 100644 --- a/frontend/src/component/admin/roles/RoleForm/RoleForm.tsx +++ b/frontend/src/component/admin/roles/RoleForm/RoleForm.tsx @@ -117,6 +117,7 @@ export const RoleForm = ({ {[...categories].map(category => ( } diff --git a/frontend/src/component/admin/roles/RoleModal/RoleModal.tsx b/frontend/src/component/admin/roles/RoleModal/RoleModal.tsx index 8ad97d5307ca..5b8fa7bd553c 100644 --- a/frontend/src/component/admin/roles/RoleModal/RoleModal.tsx +++ b/frontend/src/component/admin/roles/RoleModal/RoleModal.tsx @@ -1,6 +1,5 @@ import { Button, styled } from '@mui/material'; import { SidebarModal } from 'component/common/SidebarModal/SidebarModal'; -import IRole from 'interfaces/role'; import { useRoleForm } from '../RoleForm/useRoleForm'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import FormTemplate from 'component/common/FormTemplate/FormTemplate'; @@ -10,6 +9,7 @@ import useToast from 'hooks/useToast'; 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'; const StyledForm = styled('form')(() => ({ display: 'flex', @@ -29,12 +29,14 @@ const StyledCancelButton = styled(Button)(({ theme }) => ({ })); interface IRoleModalProps { - role?: IRole; + roleId?: number; open: boolean; setOpen: React.Dispatch>; } -export const RoleModal = ({ role, open, setOpen }: IRoleModalProps) => { +export const RoleModal = ({ roleId, open, setOpen }: IRoleModalProps) => { + const { role, refetch: refetchRole } = useRole(roleId?.toString()); + const { name, setName, @@ -53,7 +55,7 @@ export const RoleModal = ({ role, open, setOpen }: IRoleModalProps) => { clearError, ErrorField, } = useRoleForm(role?.name, role?.description, role?.permissions); - const { refetch } = useRoles(); + const { refetch: refetchRoles } = useRoles(); const { addRole, updateRole, loading } = useRolesApi(); const { setToastData, setToastApiError } = useToast(); const { uiConfig } = useUiConfig(); @@ -82,6 +84,11 @@ export const RoleModal = ({ role, open, setOpen }: IRoleModalProps) => { setName(name); }; + const refetch = () => { + refetchRoles(); + refetchRole(); + }; + const onSubmit = async (e: FormEvent) => { e.preventDefault(); diff --git a/frontend/src/component/admin/roles/RolesTable/RoleDeleteDialog/RoleDeleteDialog.tsx b/frontend/src/component/admin/roles/RolesTable/RoleDeleteDialog/RoleDeleteDialog.tsx index ed5c49d68432..f3c8aef3c8fa 100644 --- a/frontend/src/component/admin/roles/RolesTable/RoleDeleteDialog/RoleDeleteDialog.tsx +++ b/frontend/src/component/admin/roles/RolesTable/RoleDeleteDialog/RoleDeleteDialog.tsx @@ -6,6 +6,8 @@ import { useUsers } from 'hooks/api/getters/useUsers/useUsers'; import IRole from 'interfaces/role'; import { RoleDeleteDialogUsers } from './RoleDeleteDialogUsers/RoleDeleteDialogUsers'; import { RoleDeleteDialogServiceAccounts } from './RoleDeleteDialogServiceAccounts/RoleDeleteDialogServiceAccounts'; +import { useGroups } from 'hooks/api/getters/useGroups/useGroups'; +import { RoleDeleteDialogGroups } from './RoleDeleteDialogGroups/RoleDeleteDialogGroups'; const StyledTableContainer = styled('div')(({ theme }) => ({ marginTop: theme.spacing(1.5), @@ -30,11 +32,13 @@ export const RoleDeleteDialog = ({ }: IRoleDeleteDialogProps) => { const { users } = useUsers(); const { serviceAccounts } = useServiceAccounts(); + const { groups } = useGroups(); const roleUsers = users.filter(({ rootRole }) => rootRole === role?.id); const roleServiceAccounts = serviceAccounts.filter( ({ rootRole }) => rootRole === role?.id ); + const roleGroups = groups?.filter(({ rootRole }) => rootRole === role?.id); const deleteMessage = ( <> @@ -55,14 +59,16 @@ export const RoleDeleteDialog = ({ > - If you delete this role, all current accounts + If you delete this role, all current entities associated with it will be automatically assigned - the preconfigured Viewer role. + the predefined Viewer role. {deleteMessage} } /> + + + Groups ({roleGroups?.length}): + + + + + + } + /> } elseShow={

{deleteMessage}

} diff --git a/frontend/src/component/admin/roles/RolesTable/RoleDeleteDialog/RoleDeleteDialogGroups/RoleDeleteDialogGroups.tsx b/frontend/src/component/admin/roles/RolesTable/RoleDeleteDialog/RoleDeleteDialogGroups/RoleDeleteDialogGroups.tsx new file mode 100644 index 000000000000..52e00bf4a83b --- /dev/null +++ b/frontend/src/component/admin/roles/RolesTable/RoleDeleteDialog/RoleDeleteDialogGroups/RoleDeleteDialogGroups.tsx @@ -0,0 +1,86 @@ +import { VirtualizedTable } from 'component/common/Table'; +import { DateCell } from 'component/common/Table/cells/DateCell/DateCell'; +import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell'; +import { useMemo, useState } from 'react'; +import { useTable, useSortBy, useFlexLayout, Column } from 'react-table'; +import { sortTypes } from 'utils/sortTypes'; +import { IGroup } from 'interfaces/group'; +import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; + +export type PageQueryType = Partial< + Record<'sort' | 'order' | 'search', string> +>; + +interface IRoleDeleteDialogGroupsProps { + groups: IGroup[]; +} + +export const RoleDeleteDialogGroups = ({ + groups, +}: IRoleDeleteDialogGroupsProps) => { + const [initialState] = useState(() => ({ + sortBy: [{ id: 'createdAt' }], + })); + + console.log(groups); + + const columns = useMemo( + () => + [ + { + id: 'name', + Header: 'Name', + accessor: (row: any) => row.name || '', + minWidth: 200, + Cell: ({ row: { original: group } }: any) => ( + + ), + }, + { + Header: 'Created', + accessor: 'createdAt', + Cell: DateCell, + sortType: 'date', + width: 120, + maxWidth: 120, + }, + { + id: 'users', + Header: 'Users', + accessor: (row: IGroup) => + row.users.length === 1 + ? '1 user' + : `${row.users.length} users`, + Cell: TextCell, + maxWidth: 150, + }, + ] as Column[], + [] + ); + + const { headerGroups, rows, prepareRow } = useTable( + { + columns, + data: groups, + initialState, + sortTypes, + autoResetHiddenColumns: false, + autoResetSortBy: false, + disableSortRemove: true, + disableMultiSort: true, + }, + useSortBy, + useFlexLayout + ); + + return ( + + ); +}; diff --git a/frontend/src/component/admin/roles/RolesTable/RoleDeleteDialog/RoleDeleteDialogServiceAccounts/RoleDeleteDialogServiceAccounts.tsx b/frontend/src/component/admin/roles/RolesTable/RoleDeleteDialog/RoleDeleteDialogServiceAccounts/RoleDeleteDialogServiceAccounts.tsx index 9906bd6fabe7..a50b116643bf 100644 --- a/frontend/src/component/admin/roles/RolesTable/RoleDeleteDialog/RoleDeleteDialogServiceAccounts/RoleDeleteDialogServiceAccounts.tsx +++ b/frontend/src/component/admin/roles/RolesTable/RoleDeleteDialog/RoleDeleteDialogServiceAccounts/RoleDeleteDialogServiceAccounts.tsx @@ -6,7 +6,6 @@ import { useTable, useSortBy, useFlexLayout, Column } from 'react-table'; import { sortTypes } from 'utils/sortTypes'; import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell'; import { IServiceAccount } from 'interfaces/service-account'; -import { useServiceAccounts } from 'hooks/api/getters/useServiceAccounts/useServiceAccounts'; import { ServiceAccountTokensCell } from 'component/admin/serviceAccounts/ServiceAccountsTable/ServiceAccountTokensCell/ServiceAccountTokensCell'; export type PageQueryType = Partial< @@ -20,8 +19,6 @@ interface IRoleDeleteDialogServiceAccountsProps { export const RoleDeleteDialogServiceAccounts = ({ serviceAccounts, }: IRoleDeleteDialogServiceAccountsProps) => { - const { roles } = useServiceAccounts(); - const [initialState] = useState(() => ({ sortBy: [{ id: 'seenAt' }], })); @@ -40,7 +37,6 @@ export const RoleDeleteDialogServiceAccounts = ({ subtitle={serviceAccount.username} /> ), - searchable: true, }, { id: 'tokens', @@ -61,7 +57,6 @@ export const RoleDeleteDialogServiceAccounts = ({ value={value} /> ), - searchable: true, maxWidth: 100, }, { @@ -86,7 +81,7 @@ export const RoleDeleteDialogServiceAccounts = ({ maxWidth: 150, }, ] as Column[], - [roles] + [] ); const { headerGroups, rows, prepareRow } = useTable( diff --git a/frontend/src/component/admin/roles/RolesTable/RoleDeleteDialog/RoleDeleteDialogUsers/RoleDeleteDialogUsers.tsx b/frontend/src/component/admin/roles/RolesTable/RoleDeleteDialog/RoleDeleteDialogUsers/RoleDeleteDialogUsers.tsx index a23eb636dde8..0ca7f991b1d9 100644 --- a/frontend/src/component/admin/roles/RolesTable/RoleDeleteDialog/RoleDeleteDialogUsers/RoleDeleteDialogUsers.tsx +++ b/frontend/src/component/admin/roles/RolesTable/RoleDeleteDialog/RoleDeleteDialogUsers/RoleDeleteDialogUsers.tsx @@ -6,8 +6,6 @@ import { useTable, useSortBy, useFlexLayout, Column } from 'react-table'; import { sortTypes } from 'utils/sortTypes'; import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell'; import { IUser } from 'interfaces/user'; -import { useUsers } from 'hooks/api/getters/useUsers/useUsers'; -import { ServiceAccountTokensCell } from 'component/admin/serviceAccounts/ServiceAccountsTable/ServiceAccountTokensCell/ServiceAccountTokensCell'; export type PageQueryType = Partial< Record<'sort' | 'order' | 'search', string> @@ -20,8 +18,6 @@ interface IRoleDeleteDialogUsersProps { export const RoleDeleteDialogUsers = ({ users, }: IRoleDeleteDialogUsersProps) => { - const { roles } = useUsers(); - const [initialState] = useState(() => ({ sortBy: [{ id: 'last-login' }], })); @@ -40,7 +36,6 @@ export const RoleDeleteDialogUsers = ({ subtitle={user.email || user.username} /> ), - searchable: true, }, { Header: 'Created', @@ -61,12 +56,11 @@ export const RoleDeleteDialogUsers = ({ title={date => `Last login: ${date}`} /> ), - disableGlobalFilter: true, sortType: 'date', maxWidth: 150, }, ] as Column[], - [roles] + [] ); const { headerGroups, rows, prepareRow } = useTable( diff --git a/frontend/src/component/admin/roles/RolesTable/RolePermissionsCell/RolePermissionsCell.tsx b/frontend/src/component/admin/roles/RolesTable/RolePermissionsCell/RolePermissionsCell.tsx new file mode 100644 index 000000000000..0b600640170a --- /dev/null +++ b/frontend/src/component/admin/roles/RolesTable/RolePermissionsCell/RolePermissionsCell.tsx @@ -0,0 +1,31 @@ +import { VFC } from 'react'; +import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; +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'; + +interface IRolePermissionsCellProps { + row: { original: IRole }; +} + +export const RolePermissionsCell: VFC = ({ + row, +}) => { + const { original: rowRole } = row; + const { role } = useRole(rowRole.id.toString()); + + if (!role || role.type === 'root') return null; + + return ( + + } + > + {role.permissions?.length === 1 + ? '1 permission' + : `${role.permissions?.length} permissions`} + + + ); +}; diff --git a/frontend/src/component/admin/roles/RolesTable/RolesTable.tsx b/frontend/src/component/admin/roles/RolesTable/RolesTable.tsx index 2bf103b565dc..0f26103dc27d 100644 --- a/frontend/src/component/admin/roles/RolesTable/RolesTable.tsx +++ b/frontend/src/component/admin/roles/RolesTable/RolesTable.tsx @@ -23,6 +23,7 @@ import { RoleDeleteDialog } from './RoleDeleteDialog/RoleDeleteDialog'; 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'; export const RolesTable = () => { const { setToastData, setToastApiError } = useToast(); @@ -72,6 +73,12 @@ export const RolesTable = () => { searchable: true, minWidth: 100, }, + { + id: 'permissions', + Header: 'Permissions', + Cell: RolePermissionsCell, + maxWidth: 140, + }, { Header: 'Actions', id: 'Actions', @@ -211,7 +218,7 @@ export const RolesTable = () => { } /> diff --git a/frontend/src/component/admin/serviceAccounts/ServiceAccountsTable/ServiceAccountsTable.tsx b/frontend/src/component/admin/serviceAccounts/ServiceAccountsTable/ServiceAccountsTable.tsx index a2c0761e2d50..8013a338aab0 100644 --- a/frontend/src/component/admin/serviceAccounts/ServiceAccountsTable/ServiceAccountsTable.tsx +++ b/frontend/src/component/admin/serviceAccounts/ServiceAccountsTable/ServiceAccountsTable.tsx @@ -28,6 +28,7 @@ import { ServiceAccountTokenDialog } from './ServiceAccountTokenDialog/ServiceAc import { ServiceAccountTokensCell } from './ServiceAccountTokensCell/ServiceAccountTokensCell'; import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell'; import { IServiceAccount } from 'interfaces/service-account'; +import { RoleCell } from 'component/common/Table/cells/RoleCell/RoleCell'; export const ServiceAccountsTable = () => { const { setToastData, setToastApiError } = useToast(); @@ -92,6 +93,9 @@ export const ServiceAccountsTable = () => { accessor: (row: any) => roles.find((role: IRole) => role.id === row.rootRole) ?.name || '', + Cell: ({ row: { original: serviceAccount }, value }: any) => ( + + ), maxWidth: 120, }, { diff --git a/frontend/src/component/admin/users/UserForm/UserForm.tsx b/frontend/src/component/admin/users/UserForm/UserForm.tsx index 706fbee66713..6125ea605df9 100644 --- a/frontend/src/component/admin/users/UserForm/UserForm.tsx +++ b/frontend/src/component/admin/users/UserForm/UserForm.tsx @@ -1,19 +1,11 @@ import Input from 'component/common/Input/Input'; -import { - FormControlLabel, - Button, - RadioGroup, - FormControl, - Typography, - Radio, - Switch, - styled, -} from '@mui/material'; +import { Button, FormControl, Typography, Switch, styled } from '@mui/material'; import React from 'react'; -import { useUsers } from 'hooks/api/getters/useUsers/useUsers'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { EDIT } from 'constants/misc'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; +import { RoleSelect } from 'component/common/RoleSelect/RoleSelect'; +import IRole from 'interfaces/role'; const StyledForm = styled('form')(() => ({ display: 'flex', @@ -38,16 +30,6 @@ const StyledRoleSubtitle = styled(Typography)(({ theme }) => ({ margin: theme.spacing(1, 0), })); -const StyledRoleBox = styled(FormControlLabel)(({ theme }) => ({ - margin: theme.spacing(0.5, 0), - border: `1px solid ${theme.palette.divider}`, - padding: theme.spacing(2), -})); - -const StyledRoleRadio = styled(Radio)(({ theme }) => ({ - marginRight: theme.spacing(2), -})); - const StyledFlexRow = styled('div')(() => ({ display: 'flex', alignItems: 'center', @@ -66,12 +48,12 @@ const StyledCancelButton = styled(Button)(({ theme }) => ({ interface IUserForm { email: string; name: string; - rootRole: number; + rootRole: IRole | null; sendEmail: boolean; setEmail: React.Dispatch>; setName: React.Dispatch>; setSendEmail: React.Dispatch>; - setRootRole: React.Dispatch>; + setRootRole: React.Dispatch>; handleSubmit: (e: any) => void; handleCancel: () => void; errors: { [key: string]: string }; @@ -95,19 +77,8 @@ const UserForm: React.FC = ({ clearErrors, mode, }) => { - const { roles } = useUsers(); const { uiConfig } = useUiConfig(); - // @ts-expect-error - const sortRoles = (a, b) => { - if (b.name[0] < a.name[0]) { - return 1; - } else if (a.name[0] < b.name[0]) { - return -1; - } - return 0; - }; - return ( @@ -132,39 +103,10 @@ const UserForm: React.FC = ({ errorText={errors.email} onFocus={() => clearErrors()} /> - - - What is your team member allowed to do? - - setRootRole(+e.target.value)} - data-loading - > - {/* @ts-expect-error */} - {roles.sort(sortRoles).map(role => ( - - {role.name} - - {role.description} - - - } - control={ - - } - value={role.id} - /> - ))} - - + + What is your team member allowed to do? + + { const navigate = useNavigate(); @@ -126,6 +127,9 @@ const UsersList = () => { accessor: (row: any) => roles.find((role: IRole) => role.id === row.rootRole) ?.name || '', + Cell: ({ row: { original: user }, value }: any) => ( + + ), disableGlobalFilter: true, maxWidth: 120, }, diff --git a/frontend/src/component/admin/users/hooks/useAddUserForm.ts b/frontend/src/component/admin/users/hooks/useAddUserForm.ts index 607f1e30e17c..83db52a2c773 100644 --- a/frontend/src/component/admin/users/hooks/useAddUserForm.ts +++ b/frontend/src/component/admin/users/hooks/useAddUserForm.ts @@ -1,17 +1,22 @@ import { useEffect, useState } from 'react'; import { useUsers } from 'hooks/api/getters/useUsers/useUsers'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; +import IRole from 'interfaces/role'; +import { useRoles } from 'hooks/api/getters/useRoles/useRoles'; const useCreateUserForm = ( initialName = '', initialEmail = '', - initialRootRole = 1 + initialRootRole = null ) => { const { uiConfig } = useUiConfig(); + const { roles } = useRoles(); const [name, setName] = useState(initialName); const [email, setEmail] = useState(initialEmail); const [sendEmail, setSendEmail] = useState(false); - const [rootRole, setRootRole] = useState(initialRootRole); + const [rootRole, setRootRole] = useState( + roles.find(({ id }) => id === initialRootRole) || null + ); const [errors, setErrors] = useState({}); const { users } = useUsers(); @@ -29,7 +34,7 @@ const useCreateUserForm = ( }, [uiConfig?.emailEnabled]); useEffect(() => { - setRootRole(initialRootRole); + setRootRole(roles.find(({ id }) => id === initialRootRole) || null); }, [initialRootRole]); const getAddUserPayload = () => { @@ -37,7 +42,7 @@ const useCreateUserForm = ( name: name, email: email, sendEmail: sendEmail, - rootRole: rootRole, + rootRole: rootRole?.id || 0, }; }; @@ -54,7 +59,6 @@ const useCreateUserForm = ( }; const validateEmail = () => { - // @ts-expect-error if (users.some(user => user['email'] === email)) { setErrors(prev => ({ ...prev, email: 'Email already exists' })); return false; diff --git a/frontend/src/component/common/RoleBadge/RoleBadge.tsx b/frontend/src/component/common/RoleBadge/RoleBadge.tsx new file mode 100644 index 000000000000..681103b19c70 --- /dev/null +++ b/frontend/src/component/common/RoleBadge/RoleBadge.tsx @@ -0,0 +1,23 @@ +import { Badge } from 'component/common/Badge/Badge'; +import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip'; +import { useRole } from 'hooks/api/getters/useRole/useRole'; +import { Person as UserIcon } from '@mui/icons-material'; +import { RoleDescription } from 'component/common/RoleDescription/RoleDescription'; + +interface IRoleBadgeProps { + roleId: number; +} + +export const RoleBadge = ({ roleId }: IRoleBadgeProps) => { + const { role } = useRole(roleId.toString()); + + if (!role) return null; + + return ( + }> + }> + {role.name} + + + ); +}; diff --git a/frontend/src/component/common/RoleDescription/RoleDescription.tsx b/frontend/src/component/common/RoleDescription/RoleDescription.tsx new file mode 100644 index 000000000000..085dab680273 --- /dev/null +++ b/frontend/src/component/common/RoleDescription/RoleDescription.tsx @@ -0,0 +1,98 @@ +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'; + +const StyledDescription = styled('div', { + shouldForwardProp: prop => prop !== 'tooltip', +})<{ tooltip?: boolean }>(({ theme, tooltip }) => ({ + width: '100%', + maxWidth: theme.spacing(50), + padding: tooltip ? theme.spacing(1) : theme.spacing(3), + backgroundColor: tooltip + ? theme.palette.background.paper + : theme.palette.neutral.light, + color: theme.palette.text.secondary, + fontSize: theme.fontSizes.smallBody, + borderRadius: theme.shape.borderRadiusMedium, +})); + +const StyledDescriptionBlock = styled('div')(({ theme }) => ({ + marginTop: theme.spacing(2), +})); + +const StyledDescriptionHeader = styled('p')(({ theme }) => ({ + color: theme.palette.text.primary, + fontSize: theme.fontSizes.smallBody, + fontWeight: theme.fontWeight.bold, + marginBottom: theme.spacing(1), +})); + +const StyledDescriptionSubHeader = styled('p')(({ theme }) => ({ + fontSize: theme.fontSizes.smallBody, + marginTop: theme.spacing(1), +})); + +interface IRoleDescriptionProps { + roleId: number; + tooltip?: boolean; + className?: string; + sx?: SxProps; +} + +export const RoleDescription = ({ + roleId, + tooltip, + ...rest +}: IRoleDescriptionProps) => { + const { role } = useRole(roleId.toString()); + + if (!role) return null; + + const { name, description, permissions } = role; + + const categorizedPermissions = [...new Set(permissions)].map(permission => { + const category = ROOT_PERMISSION_CATEGORIES.find(category => + category.permissions.includes(permission.name) + ); + + return { + category: category ? category.label : 'Other', + permission, + }; + }); + + const categories = new Set( + categorizedPermissions.map(({ category }) => category).sort() + ); + + return ( + + + {name} + + + {description} + + + [...categories].map(category => ( + + + {category} + + {categorizedPermissions + .filter(({ category: c }) => c === category) + .map(({ permission }) => ( +

+ {permission.displayName} +

+ ))} +
+ )) + } + /> +
+ ); +}; diff --git a/frontend/src/component/common/RoleSelect/RoleSelect.tsx b/frontend/src/component/common/RoleSelect/RoleSelect.tsx index 3e175539d842..0f3df308b8b7 100644 --- a/frontend/src/component/common/RoleSelect/RoleSelect.tsx +++ b/frontend/src/component/common/RoleSelect/RoleSelect.tsx @@ -6,6 +6,8 @@ import { } from '@mui/material'; import { useRoles } from 'hooks/api/getters/useRoles/useRoles'; import IRole from 'interfaces/role'; +import { RoleDescription } from '../RoleDescription/RoleDescription'; +import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender'; const StyledRoleOption = styled('div')(({ theme }) => ({ display: 'flex', @@ -16,7 +18,7 @@ const StyledRoleOption = styled('div')(({ theme }) => ({ }, })); -export interface IRoleSelectProps +interface IRoleSelectProps extends Partial> { value: IRole | null; setValue: (role: IRole | null) => void; @@ -58,6 +60,12 @@ export const RoleSelect = ({ )} {...rest} /> + ( + + )} + /> ); }; diff --git a/frontend/src/component/common/Table/cells/RoleCell/RoleCell.tsx b/frontend/src/component/common/Table/cells/RoleCell/RoleCell.tsx new file mode 100644 index 000000000000..3bc1f3440ffb --- /dev/null +++ b/frontend/src/component/common/Table/cells/RoleCell/RoleCell.tsx @@ -0,0 +1,17 @@ +import { VFC } from 'react'; +import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; +import { TooltipLink } from 'component/common/TooltipLink/TooltipLink'; +import { RoleDescription } from 'component/common/RoleDescription/RoleDescription'; + +interface IRoleCellProps { + roleId: number; + value: string; +} + +export const RoleCell: VFC = ({ roleId, value }) => ( + + }> + {value} + + +); diff --git a/frontend/src/component/project/ProjectAccess/ProjectAccessAssign/ProjectRoleDescription/ProjectRoleDescription.tsx b/frontend/src/component/project/ProjectAccess/ProjectAccessAssign/ProjectRoleDescription/ProjectRoleDescription.tsx index 19b62b40a64d..a33f37429834 100644 --- a/frontend/src/component/project/ProjectAccess/ProjectAccessAssign/ProjectRoleDescription/ProjectRoleDescription.tsx +++ b/frontend/src/component/project/ProjectAccess/ProjectAccessAssign/ProjectRoleDescription/ProjectRoleDescription.tsx @@ -1,6 +1,6 @@ import { styled, SxProps, Theme } from '@mui/material'; import { ForwardedRef, forwardRef, useMemo, VFC } from 'react'; -import useProjectRole from 'hooks/api/getters/useProjectRole/useProjectRole'; +import { useRole } from 'hooks/api/getters/useRole/useRole'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import useProjectAccess from 'hooks/api/getters/useProjectAccess/useProjectAccess'; import { ProjectRoleDescriptionProjectPermissions } from './ProjectRoleDescriptionProjectPermissions/ProjectRoleDescriptionProjectPermissions'; @@ -64,13 +64,13 @@ export const ProjectRoleDescription: VFC = }: IProjectRoleDescriptionProps, ref: ForwardedRef ) => { - const { role } = useProjectRole(roleId.toString()); + const { role } = useRole(roleId.toString()); const { access } = useProjectAccess(projectId); const accessRole = access?.roles.find(role => role.id === roleId); const environments = useMemo(() => { const environments = new Set(); - role.permissions + role?.permissions ?.filter((permission: any) => permission.environment) .forEach((permission: any) => { environments.add(permission.environment); @@ -79,7 +79,7 @@ export const ProjectRoleDescription: VFC = }, [role]); const projectPermissions = useMemo(() => { - return role.permissions?.filter( + return role?.permissions?.filter( (permission: any) => !permission.environment ); }, [role]); @@ -92,7 +92,9 @@ export const ProjectRoleDescription: VFC = ref={ref} > 0} + condition={Boolean( + role?.permissions && role?.permissions?.length > 0 + )} show={ <> = @@ -132,7 +134,8 @@ export const ProjectRoleDescription: VFC = environment } permissions={ - role.permissions + role?.permissions || + [] } /> diff --git a/frontend/src/component/user/Profile/ProfileTab/ProfileTab.tsx b/frontend/src/component/user/Profile/ProfileTab/ProfileTab.tsx index 27ba8709a7ab..c2fab4922605 100644 --- a/frontend/src/component/user/Profile/ProfileTab/ProfileTab.tsx +++ b/frontend/src/component/user/Profile/ProfileTab/ProfileTab.tsx @@ -14,11 +14,11 @@ import { UserAvatar } from 'component/common/UserAvatar/UserAvatar'; import { useProfile } from 'hooks/api/getters/useProfile/useProfile'; import { useLocationSettings } from 'hooks/useLocationSettings'; import { IUser } from 'interfaces/user'; -import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; import TopicOutlinedIcon from '@mui/icons-material/TopicOutlined'; import { useNavigate } from 'react-router-dom'; import { PageContent } from 'component/common/PageContent/PageContent'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { RoleBadge } from 'component/common/RoleBadge/RoleBadge'; const StyledHeader = styled('div')(({ theme }) => ({ display: 'flex', @@ -134,21 +134,17 @@ export const ProfileTab = ({ user }: IProfileTabProps) => { Access - Your root role - - } - iconRight - > - {profile?.rootRole.name} - - + ( + <> + + Your root role + + + + )} + /> Projects diff --git a/frontend/src/hooks/api/getters/useProjectRole/useProjectRole.ts b/frontend/src/hooks/api/getters/useRole/useRole.ts similarity index 55% rename from frontend/src/hooks/api/getters/useProjectRole/useProjectRole.ts rename to frontend/src/hooks/api/getters/useRole/useRole.ts index 5763ae6ebf20..c47739e546a7 100644 --- a/frontend/src/hooks/api/getters/useProjectRole/useProjectRole.ts +++ b/frontend/src/hooks/api/getters/useRole/useRole.ts @@ -2,20 +2,35 @@ import { mutate, SWRConfiguration } from 'swr'; import { useState, useEffect } from 'react'; import { formatApiPath } from 'utils/formatPath'; import handleErrorResponses from '../httpErrorResponseHandler'; -import { useEnterpriseSWR } from '../useEnterpriseSWR/useEnterpriseSWR'; +import IRole from 'interfaces/role'; +import useUiConfig from '../useUiConfig/useUiConfig'; +import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR'; + +export interface IUseRoleOutput { + role?: IRole; + refetch: () => void; + loading: boolean; + error?: Error; +} + +export const useRole = ( + id?: string, + options: SWRConfiguration = {} +): IUseRoleOutput => { + const { isEnterprise } = useUiConfig(); -const useProjectRole = (id: string, options: SWRConfiguration = {}) => { const fetcher = () => { const path = formatApiPath(`api/admin/roles/${id}`); return fetch(path, { method: 'GET', }) - .then(handleErrorResponses('project role')) + .then(handleErrorResponses('role')) .then(res => res.json()); }; - const { data, error } = useEnterpriseSWR( - {}, + const { data, error } = useConditionalSWR( + Boolean(id) && isEnterprise(), + undefined, `api/admin/roles/${id}`, fetcher, options @@ -31,11 +46,9 @@ const useProjectRole = (id: string, options: SWRConfiguration = {}) => { }, [data, error]); return { - role: data ? data : {}, + role: data as IRole, error, loading, refetch, }; }; - -export default useProjectRole; diff --git a/frontend/src/interfaces/role.ts b/frontend/src/interfaces/role.ts index fba3a51d556b..284a3c6a7a22 100644 --- a/frontend/src/interfaces/role.ts +++ b/frontend/src/interfaces/role.ts @@ -6,7 +6,7 @@ interface IRole { project: string | null; description: string; type: string; - permissions: IPermission[]; + permissions?: IPermission[]; } export interface IProjectRole {