Skip to content

Commit 04de092

Browse files
authored
Users role cell refactor (#1403)
1 parent a7bf9e8 commit 04de092

File tree

10 files changed

+172
-241
lines changed

10 files changed

+172
-241
lines changed

web_ui/src/pages/user-management/users/users-table/user-name-cell/user-name-cell.component.tsx

Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
// Copyright (C) 2022-2025 Intel Corporation
22
// LIMITED EDGE SOFTWARE DISTRIBUTION LICENSE
33

4-
import { USER_ROLE } from '@geti/core/src/users/users.interface';
5-
import { Flex, PressableElement, Tooltip, TooltipTrigger } from '@geti/ui';
6-
import { UserCircleFilled as AdminIcon } from '@geti/ui/icons';
7-
import { COLOR_MODE } from '@geti/ui/theme';
4+
import { Flex } from '@geti/ui';
85

96
import { UserPhotoPresentation } from '../../../profile-page/user-photo-container/user-photo-presentation.component';
107

@@ -17,36 +14,20 @@ interface EmailCellProps {
1714
id: string;
1815
userPhoto: string | null;
1916
fullName: string;
20-
isOrgAdmin: boolean;
2117
}
2218

23-
export const UserNameCell = ({ cellData, dataKey, id, userPhoto, fullName, email, isOrgAdmin }: EmailCellProps) => {
19+
export const UserNameCell = ({ cellData, dataKey, id, userPhoto, fullName, email }: EmailCellProps) => {
2420
return (
2521
<Flex alignItems='center' gap='size-200' id={`${id}-${dataKey}`} width={'100%'}>
26-
<TooltipTrigger placement={'bottom'}>
27-
<PressableElement aria-label='label-relation'>
28-
<>
29-
<UserPhotoPresentation
30-
key={id}
31-
userName={fullName}
32-
email={email}
33-
userPhoto={userPhoto}
34-
handleUploadClick={null}
35-
width={'size-300'}
36-
height={'size-300'}
37-
/>
38-
{isOrgAdmin && (
39-
<AdminIcon
40-
color={COLOR_MODE.NEGATIVE}
41-
fill='white'
42-
data-testid={'organization-admin-indicator'}
43-
className={classes.orgAdminIndicator}
44-
/>
45-
)}
46-
</>
47-
</PressableElement>
48-
<Tooltip>{isOrgAdmin ? USER_ROLE.ORGANIZATION_ADMIN : ''}</Tooltip>
49-
</TooltipTrigger>
22+
<UserPhotoPresentation
23+
key={id}
24+
userName={fullName}
25+
email={email}
26+
userPhoto={userPhoto}
27+
handleUploadClick={null}
28+
width={'size-300'}
29+
height={'size-300'}
30+
/>
5031

5132
<span title={cellData} id={'user-name-cell'} className={classes.emailCellTitle}>
5233
{cellData}
Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,3 @@
11
.emailCellTitle {
22
flex-basis: calc(100% - var(--spectrum-global-dimension-size-225) - var(--spectrum-global-dimension-size-300));
33
}
4-
5-
.orgAdminIndicator {
6-
width: var(--spectrum-global-dimension-size-175);
7-
height: var(--spectrum-global-dimension-size-175);
8-
position: absolute;
9-
left: var(--spectrum-global-dimension-size-400);
10-
top: var(--spectrum-global-dimension-size-175);
11-
}

web_ui/src/pages/user-management/users/users-table/user-name-cell/user-name-cell.test.tsx

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,35 +12,18 @@ describe('UsernameCell', () => {
1212
const mockedUser = getMockedUser({ id: 'user-id' });
1313
const fullName = getFullNameFromUser(mockedUser);
1414

15-
it('Check if organization admin is marked in the table', async () => {
15+
it('shows the user full name within the cell', async () => {
1616
render(
1717
<UserNameCell
1818
dataKey={mockedUser.id}
1919
id={mockedUser.id}
2020
userPhoto={null}
2121
fullName={fullName}
2222
email={mockedUser.email}
23-
isOrgAdmin
2423
cellData={fullName}
2524
/>
2625
);
2726

28-
expect(screen.getByTestId('organization-admin-indicator')).toBeInTheDocument();
29-
});
30-
31-
it('Check if not organization admin is not marked in the table', async () => {
32-
render(
33-
<UserNameCell
34-
dataKey={mockedUser.id}
35-
id={mockedUser.id}
36-
userPhoto={null}
37-
fullName={fullName}
38-
email={mockedUser.email}
39-
isOrgAdmin={false}
40-
cellData={fullName}
41-
/>
42-
);
43-
44-
expect(screen.queryByTestId('organization-admin-indicator')).not.toBeInTheDocument();
27+
expect(screen.getByText(fullName)).toBeInTheDocument();
4528
});
4629
});
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright (C) 2022-2025 Intel Corporation
2+
// LIMITED EDGE SOFTWARE DISTRIBUTION LICENSE
3+
4+
import { RESOURCE_TYPE, USER_ROLE } from '@geti/core/src/users/users.interface';
5+
import { screen } from '@testing-library/react';
6+
7+
import { getMockedUser } from '../../../../test-utils/mocked-items-factory/mocked-users';
8+
import { providersRender as render } from '../../../../test-utils/required-providers-render';
9+
import { UserRoleCell } from './user-role-cell.component';
10+
import { USERS_TABLE_COLUMNS } from './users-table.component';
11+
12+
describe('UserRoleCell', () => {
13+
const workspaceId = 'workspace-id';
14+
const organizationId = 'organization-id';
15+
16+
it('shows organization roles when no resourceId is provided', () => {
17+
const roles = [
18+
{
19+
role: USER_ROLE.ORGANIZATION_ADMIN,
20+
resourceId: organizationId,
21+
resourceType: RESOURCE_TYPE.ORGANIZATION,
22+
},
23+
];
24+
25+
render(
26+
<UserRoleCell
27+
resourceId={undefined}
28+
cellData={roles}
29+
columnIndex={0}
30+
dataKey={USERS_TABLE_COLUMNS.ROLES}
31+
rowIndex={0}
32+
rowData={getMockedUser({ id: 'user-id', roles })}
33+
isScrolling={false}
34+
/>
35+
);
36+
37+
expect(screen.getByTestId('user-id-roles')).toHaveTextContent('Organization admin');
38+
});
39+
40+
it('shows workspace role for the selected workspace', () => {
41+
const roles = [
42+
{
43+
role: USER_ROLE.WORKSPACE_ADMIN,
44+
resourceId: workspaceId,
45+
resourceType: RESOURCE_TYPE.WORKSPACE,
46+
},
47+
];
48+
49+
render(
50+
<UserRoleCell
51+
resourceId={workspaceId}
52+
cellData={roles}
53+
columnIndex={0}
54+
dataKey={USERS_TABLE_COLUMNS.ROLES}
55+
rowIndex={0}
56+
rowData={getMockedUser({ id: 'user-id', roles })}
57+
isScrolling={false}
58+
/>
59+
);
60+
61+
expect(screen.getByTestId('user-id-roles')).toHaveTextContent('Workspace admin');
62+
});
63+
64+
it('falls back to N/A when no matching workspace role exists', () => {
65+
const roles = [
66+
{
67+
role: USER_ROLE.WORKSPACE_CONTRIBUTOR,
68+
resourceId: 'different-workspace-id',
69+
resourceType: RESOURCE_TYPE.WORKSPACE,
70+
},
71+
];
72+
73+
render(
74+
<UserRoleCell
75+
resourceId={workspaceId}
76+
cellData={roles}
77+
columnIndex={0}
78+
dataKey={USERS_TABLE_COLUMNS.ROLES}
79+
rowIndex={0}
80+
rowData={getMockedUser({ id: 'user-id', roles })}
81+
isScrolling={false}
82+
/>
83+
);
84+
85+
expect(screen.getByTestId('user-id-roles')).toHaveTextContent('N/A');
86+
});
87+
});
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (C) 2022-2025 Intel Corporation
2+
// LIMITED EDGE SOFTWARE DISTRIBUTION LICENSE
3+
4+
import { RESOURCE_TYPE, Role } from '@geti/core/src/users/users.interface';
5+
6+
import { CasualCell } from '../../../../shared/components/table/components/casual-cell/casual-cell.component';
7+
import { TableCellProps } from '../../../../shared/components/table/table.interface';
8+
import { OrganizationRoleTooltipContent } from '../../../../shared/components/tooltips/organization-role-tooltip';
9+
import { WorkspaceRoleTooltipContent } from '../../../../shared/components/tooltips/workspace-role-tooltip';
10+
import { ProjectRoleCell } from './project-role-cell.component';
11+
12+
interface UserRoleCellProps extends TableCellProps {
13+
resourceId: string | undefined;
14+
isProjectUsersTable?: boolean;
15+
}
16+
17+
export const UserRoleCell = ({
18+
resourceId,
19+
isProjectUsersTable = false,
20+
rowData,
21+
cellData: _unused,
22+
...rest
23+
}: UserRoleCellProps) => {
24+
const roles = (rowData.roles as Role[]) ?? [];
25+
26+
if (isProjectUsersTable && resourceId) {
27+
return <ProjectRoleCell {...rest} rowData={rowData} roles={roles} projectId={resourceId} />;
28+
}
29+
30+
if (!resourceId) {
31+
const organizationRole = roles.find((role) => role.resourceType === RESOURCE_TYPE.ORGANIZATION)?.role ?? 'N/A';
32+
33+
return (
34+
<CasualCell
35+
{...rest}
36+
rowData={rowData}
37+
cellData={organizationRole}
38+
tooltip={<OrganizationRoleTooltipContent />}
39+
tooltipProps={{
40+
width: 'calc(size-4600 + size-100)',
41+
}}
42+
/>
43+
);
44+
}
45+
46+
const workspaceRole =
47+
roles.find((role) => role.resourceType === RESOURCE_TYPE.WORKSPACE && role.resourceId === resourceId)?.role ??
48+
'N/A';
49+
50+
return (
51+
<CasualCell
52+
{...rest}
53+
rowData={rowData}
54+
cellData={workspaceRole}
55+
tooltip={<WorkspaceRoleTooltipContent />}
56+
tooltipProps={{
57+
width: 'calc(size-4600 + size-100)',
58+
}}
59+
/>
60+
);
61+
};

web_ui/src/pages/user-management/users/users-table/users-table.component.tsx

Lines changed: 11 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,21 @@
33

44
import { Dispatch, ReactNode, SetStateAction, useMemo } from 'react';
55

6-
import { isOrganizationAdmin } from '@geti/core/src/users/user-role-utils';
76
import { User, UsersQueryParams } from '@geti/core/src/users/users.interface';
8-
import { Workspace } from '@geti/core/src/workspaces/services/workspaces.interface';
97
import { Cell, Column, Flex, Row, TableBody, TableHeader, TableView, View } from '@geti/ui';
108
import { get, isEmpty } from 'lodash-es';
11-
import { useLocation } from 'react-router-dom';
129

1310
import { SortDirection } from '../../../../core/shared/query-parameters';
1411
import { useSortTable } from '../../../../hooks/use-sort-table/use-sort-table.hook';
1512
import { NotFound } from '../../../../shared/components/not-found/not-found.component';
1613
import { CasualCell } from '../../../../shared/components/table/components/casual-cell/casual-cell.component';
1714
import { StatusCell } from '../../../../shared/components/table/status-cell/status-cell.component';
1815
import { TableCellProps } from '../../../../shared/components/table/table.interface';
19-
import { WorkspaceRoleTooltipContent } from '../../../../shared/components/tooltips/workspace-role-tooltip';
2016
import { SpectrumTableLoadingState } from '../../../../shared/utils';
2117
import { LastLoginCell } from './last-login-cell.component';
22-
import { ProjectRoleCell } from './project-role-cell.component';
2318
import { UserNameCell } from './user-name-cell/user-name-cell.component';
19+
import { UserRoleCell } from './user-role-cell.component';
2420
import { getUserFullName } from './utils';
25-
import { WorkspacesRoleCell } from './workspaces-role-cell.component';
2621

2722
export const enum USERS_TABLE_COLUMNS {
2823
EMAIL_ADDRESS = 'email',
@@ -46,11 +41,8 @@ interface UsersTableProps {
4641
UserActions: (props: { activeUser: User; user: User; users: User[] }) => ReactNode;
4742
ignoredColumns?: USERS_TABLE_COLUMNS[];
4843
resourceId: string | undefined;
49-
workspaces: Workspace[];
5044
isProjectUsersTable?: boolean;
51-
organizationId: string;
5245
tableId?: string;
53-
overrideRoleColumn?: (props: TableCellProps) => ReactNode;
5446
}
5547

5648
export const UsersTable = ({
@@ -64,16 +56,11 @@ export const UsersTable = ({
6456
resourceId,
6557
isLoading,
6658
isFetchingNextPage,
67-
organizationId,
6859
isProjectUsersTable,
69-
workspaces,
7060
getNextPage,
7161
tableId,
72-
overrideRoleColumn,
7362
}: UsersTableProps) => {
7463
const shouldShowNotFound = hasFilters && isEmpty(users);
75-
const location = useLocation();
76-
const isAccountWorkspacesLocation = location.pathname.includes('account/workspaces');
7764

7865
const columns = useMemo(() => {
7966
const tableColumns = [
@@ -87,7 +74,6 @@ export const UsersTable = ({
8774

8875
const fullName = getUserFullName(firstName, lastName);
8976
const cellData = isEmpty(fullName) ? '-' : activeUser?.id === id ? `${fullName} (You)` : fullName;
90-
const isOrgAdmin = isOrganizationAdmin(rowData, organizationId);
9177

9278
return (
9379
<UserNameCell
@@ -98,7 +84,6 @@ export const UsersTable = ({
9884
dataKey={dataKey}
9985
userPhoto={userPhoto}
10086
fullName={`${firstName} ${lastName}`}
101-
isOrgAdmin={isOrgAdmin}
10287
/>
10388
);
10489
},
@@ -113,33 +98,17 @@ export const UsersTable = ({
11398
},
11499
},
115100
{
116-
label: isEmpty(resourceId) ? 'Workspace' : isAccountWorkspacesLocation ? 'Workspace role' : 'Role',
101+
label: isEmpty(resourceId)
102+
? 'Organization role'
103+
: isProjectUsersTable
104+
? 'Project role'
105+
: 'Workspace role',
117106
dataKey: USERS_TABLE_COLUMNS.ROLES,
118-
width: 150,
107+
width: 180,
119108
isSortable: false,
120-
tooltip: <WorkspaceRoleTooltipContent />,
121-
component: (data: TableCellProps) => {
122-
if (overrideRoleColumn) {
123-
return overrideRoleColumn(data);
124-
}
125-
// Organization-level view (no specific workspace selected)
126-
if (isEmpty(resourceId)) {
127-
const isOrgAdminUser = isOrganizationAdmin(data.rowData, organizationId);
128-
if (isOrgAdminUser) {
129-
return <CasualCell {...data} cellData='N/A' />;
130-
}
131-
}
132-
return isProjectUsersTable ? (
133-
<ProjectRoleCell {...data} roles={data.rowData.roles} projectId={resourceId as string} />
134-
) : (
135-
<WorkspacesRoleCell
136-
{...data}
137-
workspaceId={resourceId}
138-
workspaces={workspaces}
139-
cellData={data.rowData.roles}
140-
/>
141-
);
142-
},
109+
component: (data: TableCellProps) => (
110+
<UserRoleCell {...data} resourceId={resourceId} isProjectUsersTable={isProjectUsersTable} />
111+
),
143112
},
144113
{
145114
label: 'Last login',
@@ -175,18 +144,7 @@ export const UsersTable = ({
175144
];
176145

177146
return tableColumns.filter(({ dataKey }) => !ignoredColumns.includes(dataKey as USERS_TABLE_COLUMNS));
178-
}, [
179-
ignoredColumns,
180-
resourceId,
181-
UserActions,
182-
activeUser,
183-
isProjectUsersTable,
184-
organizationId,
185-
workspaces,
186-
users,
187-
overrideRoleColumn,
188-
isAccountWorkspacesLocation,
189-
]);
147+
}, [ignoredColumns, resourceId, UserActions, activeUser, isProjectUsersTable, users]);
190148

191149
const [sortingOptions, sort] = useSortTable<UsersQueryParams>({
192150
queryOptions: usersQueryParams,

0 commit comments

Comments
 (0)