1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +02:00

feat: use new role components in project access (#4018)

https://linear.app/unleash/issue/2-1143/adapt-project-roles-to-use-the-new-role-selector-and-role-description

This PR further unifies roles, by having a single `IRole` interface to
cover both types, and re-using the same components for project roles.
This commit is contained in:
Nuno Góis 2023-06-21 08:16:37 +01:00 committed by GitHub
parent a5ee50cfc9
commit a9e9ae8c3e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 49 additions and 331 deletions

View File

@ -10,7 +10,7 @@ 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 IRole from 'interfaces/role';
import { IRole } 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';

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import { IPermission, ICheckedPermissions } from 'interfaces/permissions';
import IRole, { PredefinedRoleType } 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';

View File

@ -17,7 +17,7 @@ import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { Add } from '@mui/icons-material';
import { UPDATE_ROLE } from '@server/types/permissions';
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
import IRole from 'interfaces/role';
import { IRole } from 'interfaces/role';
const StyledPageContent = styled(PageContent)(({ theme }) => ({
'& .page-header': {

View File

@ -3,7 +3,7 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { useServiceAccounts } from 'hooks/api/getters/useServiceAccounts/useServiceAccounts';
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
import IRole from 'interfaces/role';
import { IRole } from 'interfaces/role';
import { RoleDeleteDialogUsers } from './RoleDeleteDialogUsers/RoleDeleteDialogUsers';
import { RoleDeleteDialogServiceAccounts } from './RoleDeleteDialogServiceAccounts/RoleDeleteDialogServiceAccounts';
import { useGroups } from 'hooks/api/getters/useGroups/useGroups';

View File

@ -1,7 +1,7 @@
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 { 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';

View File

@ -3,7 +3,7 @@ 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';
import { IRole } from 'interfaces/role';
import { VFC } from 'react';
const StyledBox = styled(Box)(() => ({

View File

@ -1,7 +1,7 @@
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Badge } from 'component/common/Badge/Badge';
import { styled } from '@mui/material';
import IRole from 'interfaces/role';
import { IRole } from 'interfaces/role';
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
import { PREDEFINED_ROLE_TYPES } from '@server/util/constants';

View File

@ -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, { PredefinedRoleType } 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';

View File

@ -33,7 +33,7 @@ import { INewPersonalAPIToken } from 'interfaces/personalAPIToken';
import { ServiceAccountTokens } from './ServiceAccountTokens/ServiceAccountTokens';
import { IServiceAccount } from 'interfaces/service-account';
import { RoleSelect } from 'component/common/RoleSelect/RoleSelect';
import IRole from 'interfaces/role';
import { IRole } from 'interfaces/role';
const StyledForm = styled('form')(() => ({
display: 'flex',

View File

@ -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 } from 'interfaces/role';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import { PageContent } from 'component/common/PageContent/PageContent';

View File

@ -5,7 +5,7 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
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';
import { IRole } from 'interfaces/role';
const StyledForm = styled('form')(() => ({
display: 'flex',

View File

@ -7,7 +7,7 @@ import ConfirmUserAdded from '../ConfirmUserAdded/ConfirmUserAdded';
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
import useAdminUsersApi from 'hooks/api/actions/useAdminUsersApi/useAdminUsersApi';
import { IUser } from 'interfaces/user';
import IRole from 'interfaces/role';
import { IRole } from 'interfaces/role';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import { useUsersPlan } from 'hooks/useUsersPlan';

View File

@ -1,7 +1,7 @@
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 { IRole } from 'interfaces/role';
import { useRoles } from 'hooks/api/getters/useRoles/useRoles';
const useCreateUserForm = (

View File

@ -5,9 +5,10 @@ import {
styled,
} from '@mui/material';
import { useRoles } from 'hooks/api/getters/useRoles/useRoles';
import IRole from 'interfaces/role';
import { IRole, PredefinedRoleType } from 'interfaces/role';
import { RoleDescription } from '../RoleDescription/RoleDescription';
import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender';
import { ROOT_ROLE_TYPE } from '@server/util/constants';
const StyledRoleOption = styled('div')(({ theme }) => ({
display: 'flex',
@ -20,18 +21,22 @@ const StyledRoleOption = styled('div')(({ theme }) => ({
interface IRoleSelectProps
extends Partial<AutocompleteProps<IRole, false, false, false>> {
type?: PredefinedRoleType;
value: IRole | null;
setValue: (role: IRole | null) => void;
required?: boolean;
}
export const RoleSelect = ({
type = ROOT_ROLE_TYPE,
value,
setValue,
required,
...rest
}: IRoleSelectProps) => {
const { roles } = useRoles();
const { roles: rootRoles, projectRoles } = useRoles();
const roles = type === ROOT_ROLE_TYPE ? rootRoles : projectRoles;
const renderRoleOption = (
props: React.HTMLAttributes<HTMLLIElement>,

View File

@ -16,7 +16,7 @@ import useProjectAccess, {
ENTITY_TYPE,
IProjectAccess,
} from 'hooks/api/getters/useProjectAccess/useProjectAccess';
import { IProjectRole } from 'interfaces/role';
import { IRole } from 'interfaces/role';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
@ -25,7 +25,6 @@ import { formatUnknownError } from 'utils/formatUnknownError';
import { IUser } from 'interfaces/user';
import { IGroup } from 'interfaces/group';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { ProjectRoleDescription } from './ProjectRoleDescription/ProjectRoleDescription';
import { useNavigate } from 'react-router-dom';
import { GO_BACK } from 'constants/navigate';
import {
@ -36,6 +35,8 @@ import {
} from 'utils/testIds';
import { caseInsensitiveSearch } from 'utils/search';
import { IServiceAccount } from 'interfaces/service-account';
import { RoleSelect } from 'component/common/RoleSelect/RoleSelect';
import { PROJECT_ROLE_TYPE } from '@server/util/constants';
const StyledForm = styled('form')(() => ({
display: 'flex',
@ -82,15 +83,6 @@ const StyledUserOption = styled('div')(({ theme }) => ({
},
}));
const StyledRoleOption = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
'& > span:last-of-type': {
fontSize: theme.fontSizes.smallerBody,
color: theme.palette.text.secondary,
},
}));
interface IAccessOption {
id: number;
entity: IUser | IGroup;
@ -103,7 +95,7 @@ interface IProjectAccessAssignProps {
users: IUser[];
serviceAccounts: IServiceAccount[];
groups: IGroup[];
roles: IProjectRole[];
roles: IRole[];
}
export const ProjectAccessAssign = ({
@ -190,7 +182,7 @@ export const ProjectAccessAssign = ({
id === selected?.entity.id && type === selected?.type
)
);
const [role, setRole] = useState<IProjectRole | null>(
const [role, setRole] = useState<IRole | null>(
roles.find(({ id }) => id === selected?.entity.roleId) ?? null
);
@ -317,18 +309,6 @@ export const ProjectAccessAssign = ({
);
};
const renderRoleOption = (
props: React.HTMLAttributes<HTMLLIElement>,
option: IProjectRole
) => (
<li {...props}>
<StyledRoleOption>
<span>{option.name}</span>
<span>{option.description}</span>
</StyledRoleOption>
</li>
);
const isValid = selectedOptions.length > 0 && role;
return (
@ -451,29 +431,13 @@ export const ProjectAccessAssign = ({
Select the role to assign for this project
</StyledInputDescription>
<StyledAutocompleteWrapper>
<Autocomplete
<RoleSelect
data-testid={PA_ROLE_ID}
size="small"
openOnFocus
type={PROJECT_ROLE_TYPE}
value={role}
onChange={(_, newValue) => setRole(newValue)}
options={roles}
renderOption={renderRoleOption}
getOptionLabel={option => option.name}
renderInput={params => (
<TextField {...params} label="Role" />
)}
setValue={role => setRole(role || null)}
/>
</StyledAutocompleteWrapper>
<ConditionallyRender
condition={Boolean(role?.id)}
show={
<ProjectRoleDescription
roleId={role?.id!}
projectId={projectId}
/>
}
/>
</div>
<StyledButtonContainer>

View File

@ -1,163 +0,0 @@
import { styled, SxProps, Theme } from '@mui/material';
import { ForwardedRef, forwardRef, useMemo, VFC } from 'react';
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';
import { ProjectRoleDescriptionEnvironmentPermissions } from './ProjectRoleDescriptionEnvironmentPermissions/ProjectRoleDescriptionEnvironmentPermissions';
const StyledDescription = styled('div', {
shouldForwardProp: prop =>
prop !== 'roleId' && prop !== 'popover' && prop !== 'sx',
})<IProjectRoleDescriptionStyleProps>(({ theme, popover }) => ({
width: '100%',
maxWidth: theme.spacing(50),
padding: theme.spacing(3),
backgroundColor: popover
? 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 }) => ({
'& p:last-child': {
marginBottom: theme.spacing(2),
},
}));
const StyledDescriptionHeader = styled('p')(({ theme }) => ({
color: theme.palette.text.primary,
fontSize: theme.fontSizes.smallBody,
fontWeight: theme.fontWeight.bold,
marginBottom: theme.spacing(2),
}));
const StyledDescriptionSubHeader = styled('p')(({ theme }) => ({
color: theme.palette.text.primary,
fontSize: theme.fontSizes.smallBody,
marginBottom: theme.spacing(1),
}));
interface IProjectRoleDescriptionStyleProps {
popover?: boolean;
className?: string;
sx?: SxProps<Theme>;
}
interface IProjectRoleDescriptionProps
extends IProjectRoleDescriptionStyleProps {
roleId: number;
projectId: string;
}
export const ProjectRoleDescription: VFC<IProjectRoleDescriptionProps> =
forwardRef(
(
{
roleId,
projectId,
className,
sx,
...props
}: IProjectRoleDescriptionProps,
ref: ForwardedRef<HTMLDivElement>
) => {
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<string>();
role?.permissions
?.filter((permission: any) => permission.environment)
.forEach((permission: any) => {
environments.add(permission.environment);
});
return [...environments].sort();
}, [role]);
const projectPermissions = useMemo(() => {
return role?.permissions?.filter(
(permission: any) => !permission.environment
);
}, [role]);
return (
<StyledDescription
className={className}
sx={sx}
{...props}
ref={ref}
>
<ConditionallyRender
condition={Boolean(
role?.permissions && role?.permissions?.length > 0
)}
show={
<>
<ConditionallyRender
condition={Boolean(
projectPermissions?.length
)}
show={
<>
<StyledDescriptionHeader>
Project permissions
</StyledDescriptionHeader>
<StyledDescriptionBlock>
<ProjectRoleDescriptionProjectPermissions
permissions={
role?.permissions || []
}
/>
</StyledDescriptionBlock>
</>
}
/>
<ConditionallyRender
condition={Boolean(environments.length)}
show={
<>
<StyledDescriptionHeader>
Environment permissions
</StyledDescriptionHeader>
{environments.map(environment => (
<div key={environment}>
<StyledDescriptionSubHeader>
{environment}
</StyledDescriptionSubHeader>
<StyledDescriptionBlock>
<ProjectRoleDescriptionEnvironmentPermissions
environment={
environment
}
permissions={
role?.permissions ||
[]
}
/>
</StyledDescriptionBlock>
</div>
))}
</>
}
/>
</>
}
elseShow={
<>
<StyledDescriptionSubHeader>
{accessRole?.name}
</StyledDescriptionSubHeader>
<StyledDescriptionBlock>
{accessRole?.description}
</StyledDescriptionBlock>
</>
}
/>
</StyledDescription>
);
}
);

View File

@ -1,26 +0,0 @@
interface IProjectRoleDescriptionEnvironmentPermissionsProps {
environment: string;
permissions: any[];
}
export const ProjectRoleDescriptionEnvironmentPermissions = ({
environment,
permissions,
}: IProjectRoleDescriptionEnvironmentPermissionsProps) => (
<>
{[
...new Set(
permissions
.filter(
(permission: any) =>
permission.environment === environment
)
.map((permission: any) => permission.displayName)
),
]
.sort()
.map((permission: any) => (
<p key={`${environment}-${permission}`}>{permission}</p>
))}
</>
);

View File

@ -1,17 +0,0 @@
interface IProjectRoleDescriptionProjectPermissionsProps {
permissions: any[];
}
export const ProjectRoleDescriptionProjectPermissions = ({
permissions,
}: IProjectRoleDescriptionProjectPermissionsProps) => (
<>
{permissions
?.filter((permission: any) => !permission.environment)
.map((permission: any) => permission.displayName)
.sort()
.map((permission: any) => (
<p key={permission}>{permission}</p>
))}
</>
);

View File

@ -1,40 +0,0 @@
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { VFC } from 'react';
import { ProjectRoleDescription } from 'component/project/ProjectAccess/ProjectAccessAssign/ProjectRoleDescription/ProjectRoleDescription';
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
interface IProjectAccessRoleCellProps {
roleId: number;
projectId: string;
value?: string;
emptyText?: string;
}
export const ProjectAccessRoleCell: VFC<IProjectAccessRoleCellProps> = ({
roleId,
projectId,
value,
emptyText,
}) => {
if (!value) return <TextCell>{emptyText}</TextCell>;
return (
<TextCell>
<TooltipLink
tooltip={
<ProjectRoleDescription
roleId={roleId}
projectId={projectId}
popover
/>
}
tooltipProps={{
maxWidth: 500,
maxHeight: 600,
}}
>
{value}
</TooltipLink>
</TextCell>
);
};

View File

@ -43,7 +43,7 @@ import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton
import { ProjectAccessCreate } from 'component/project/ProjectAccess/ProjectAccessCreate/ProjectAccessCreate';
import { ProjectAccessEditUser } from 'component/project/ProjectAccess/ProjectAccessEditUser/ProjectAccessEditUser';
import { ProjectAccessEditGroup } from 'component/project/ProjectAccess/ProjectAccessEditGroup/ProjectAccessEditGroup';
import { ProjectAccessRoleCell } from './ProjectAccessRoleCell/ProjectAccessRoleCell';
import { RoleCell } from 'component/common/Table/cells/RoleCell/RoleCell';
import {
PA_ASSIGN_BUTTON_ID,
PA_EDIT_BUTTON_ID,
@ -168,11 +168,7 @@ export const ProjectAccessTable: VFC = () => {
access?.roles.find(({ id }) => id === row.entity.roleId)
?.name,
Cell: ({ value, row: { original: row } }: any) => (
<ProjectAccessRoleCell
roleId={row.entity.roleId}
projectId={projectId}
value={value}
/>
<RoleCell roleId={row.entity.roleId} value={value} />
),
maxWidth: 125,
filterName: 'role',

View File

@ -2,7 +2,7 @@ import useSWR, { mutate, SWRConfiguration } from 'swr';
import { useState, useEffect, useMemo } from 'react';
import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler';
import { IProjectRole } from 'interfaces/role';
import { IRole } from 'interfaces/role';
import { IGroup } from 'interfaces/group';
import { IUser } from 'interfaces/user';
import { mapGroupUsers } from '../useGroup/useGroup';
@ -30,7 +30,7 @@ export interface IProjectAccessGroup extends IGroup {
export interface IProjectAccessOutput {
users: IProjectAccessUser[];
groups: IProjectAccessGroup[];
roles: IProjectRole[];
roles: IRole[];
rows: IProjectAccess[];
}

View File

@ -2,7 +2,7 @@ import { SWRConfiguration } from 'swr';
import { useMemo } from 'react';
import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler';
import IRole, { IRoleWithPermissions } from 'interfaces/role';
import { IRole, IRoleWithPermissions } from 'interfaces/role';
import useUiConfig from '../useUiConfig/useUiConfig';
import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';

View File

@ -1,4 +1,4 @@
import IRole, { IProjectRole } from 'interfaces/role';
import { IRole } from 'interfaces/role';
import { useMemo } from 'react';
import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler';
@ -11,7 +11,15 @@ import {
PREDEFINED_ROLE_TYPES,
} from '@server/util/constants';
export const useRoles = () => {
interface IUseRolesOutput {
roles: IRole[];
projectRoles: IRole[];
loading: boolean;
refetch: () => void;
error?: Error;
}
export const useRoles = (): IUseRolesOutput => {
const { isEnterprise, uiConfig } = useUiConfig();
const { data, error, mutate } = useConditionalSWR(
@ -56,7 +64,7 @@ export const useRoles = () => {
.filter(({ type }: IRole) =>
PROJECT_ROLE_TYPES.includes(type)
)
.sort(sortRoles) ?? []) as IProjectRole[],
.sort(sortRoles) ?? []) as IRole[],
loading: !error && !data,
refetch: () => mutate(),
error,

View File

@ -1,4 +1,4 @@
import IRole from 'interfaces/role';
import { IRole } from 'interfaces/role';
import { IServiceAccount } from 'interfaces/service-account';
import { useMemo } from 'react';
import { formatApiPath } from 'utils/formatPath';

View File

@ -3,7 +3,7 @@ import { useMemo } from 'react';
import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler';
import { IUser } from 'interfaces/user';
import IRole from 'interfaces/role';
import { IRole } from 'interfaces/role';
interface IUseUsersOutput {
users: IUser[];

View File

@ -1,4 +1,4 @@
import IRole from './role';
import { IRole } from './role';
export interface IProfile {
rootRole: IRole;

View File

@ -1,4 +1,4 @@
import IRole from './role';
import { IRole } from './role';
import { IUser } from './user';
export interface ICreateInvitedUser {

View File

@ -5,7 +5,7 @@ export type PredefinedRoleType =
| typeof ROOT_ROLE_TYPE
| typeof PROJECT_ROLE_TYPE;
interface IRole {
export interface IRole {
id: number;
name: string;
project: string | null;
@ -16,12 +16,3 @@ interface IRole {
export interface IRoleWithPermissions extends IRole {
permissions: IPermission[];
}
export interface IProjectRole {
id: number;
name: string;
description: string;
type: string;
}
export default IRole;