Skip to content

Commit 644e3fc

Browse files
authored
Users tab dialogs update (#1398)
1 parent 90752b6 commit 644e3fc

File tree

10 files changed

+324
-44
lines changed

10 files changed

+324
-44
lines changed

web_ui/src/pages/user-management/users/actions/edit-organization-user-dialog.component.tsx

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,22 @@ import {
1010
Button,
1111
ButtonGroup,
1212
Content,
13+
ContextualHelp,
1314
Dialog,
1415
Divider,
1516
Flex,
1617
Form,
1718
Heading,
1819
TextField,
19-
Tooltip,
20-
TooltipTrigger,
2120
} from '@geti/ui';
2221
import { Email } from '@geti/ui/icons';
2322

2423
import { StatusCell } from '../../../../shared/components/table/status-cell/status-cell.component';
24+
import { OrganizationRoleTooltipContent } from '../../../../shared/components/tooltips/organization-role-tooltip';
2525
import { RolePicker } from '../old-project-users/role-picker.component';
26-
import { OrganizationRoleTooltipContent } from '../organization-role-tooltip/organization-role-tooltip';
2726
import { LastLoginCell } from '../users-table/last-login-cell.component';
2827

28+
import tooltipClasses from '../../../../shared/components/tooltips/tooltips.module.scss';
2929
import classes from '../workspace-users/actions/user-summary.module.scss';
3030

3131
interface EditOrganizationUserDialogProps {
@@ -177,18 +177,21 @@ export const EditOrganizationUserDialog = ({
177177
onChange={setLastName}
178178
/>
179179
</Flex>
180-
<TooltipTrigger placement={'bottom'}>
181-
<RolePicker
182-
label='Organization role'
183-
roles={[USER_ROLE.ORGANIZATION_ADMIN, USER_ROLE.ORGANIZATION_CONTRIBUTOR]}
184-
selectedRole={selectedOrgRole as USER_ROLE}
185-
setSelectedRole={setSelectedOrgRole}
186-
isDisabled={isLastRemainingOrgAdmin}
187-
/>
188-
<Tooltip>
189-
<OrganizationRoleTooltipContent />
190-
</Tooltip>
191-
</TooltipTrigger>
180+
<RolePicker
181+
label='Organization role'
182+
roles={[USER_ROLE.ORGANIZATION_ADMIN, USER_ROLE.ORGANIZATION_CONTRIBUTOR]}
183+
selectedRole={selectedOrgRole as USER_ROLE}
184+
setSelectedRole={setSelectedOrgRole}
185+
isDisabled={isLastRemainingOrgAdmin}
186+
contextualHelp={
187+
<ContextualHelp>
188+
<Heading>What roles can there be in an organization?</Heading>
189+
<Content UNSAFE_className={tooltipClasses.organizationRoleContextualHelp}>
190+
<OrganizationRoleTooltipContent />
191+
</Content>
192+
</ContextualHelp>
193+
}
194+
/>
192195
<ButtonGroup align={'end'} marginTop={'size-350'}>
193196
<Button variant='secondary' onPress={closeDialog} id='cancel-edit-org-user'>
194197
Cancel
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
// Copyright (C) 2022-2025 Intel Corporation
2+
// LIMITED EDGE SOFTWARE DISTRIBUTION LICENSE
3+
4+
import { createInMemoryUsersService } from '@geti/core/src/users/services/in-memory-users-service';
5+
import {
6+
ResourceTypeDTO,
7+
RoleOperationDTO,
8+
USER_ROLE,
9+
UserRoleDTO,
10+
type User,
11+
} from '@geti/core/src/users/users.interface';
12+
import { screen, waitFor, within } from '@testing-library/react';
13+
import { userEvent } from '@testing-library/user-event';
14+
15+
import { applicationRender as render } from '../../../../test-utils/application-provider-render';
16+
import {
17+
getMockedOrganizationAdminUser,
18+
getMockedOrganizationContributorUser,
19+
} from '../../../../test-utils/mocked-items-factory/mocked-users';
20+
import { EditOrganizationUserDialog } from './edit-organization-user-dialog.component';
21+
22+
describe('EditOrganizationUserDialog', () => {
23+
const organizationId = 'organization-id';
24+
25+
const createOrgAdmin = (overrides: Partial<User> = {}, workspaceId = 'workspace-id') =>
26+
getMockedOrganizationAdminUser(overrides, workspaceId, organizationId);
27+
28+
const createOrgContributor = (overrides: Partial<User> = {}) =>
29+
getMockedOrganizationContributorUser({ organizationId, ...overrides });
30+
31+
afterEach(() => {
32+
jest.clearAllMocks();
33+
});
34+
35+
it('disables name fields and save button when nothing changed in SaaS environment', async () => {
36+
const admin = createOrgAdmin({
37+
id: 'org-admin',
38+
firstName: 'Alice',
39+
lastName: 'Admin',
40+
41+
});
42+
const secondAdmin = createOrgAdmin({ id: 'org-admin-2' }, 'workspace-id-2');
43+
44+
await render(
45+
<EditOrganizationUserDialog
46+
organizationId={organizationId}
47+
user={admin}
48+
users={[admin, secondAdmin]}
49+
activeUser={admin}
50+
isSaasEnvironment
51+
closeDialog={jest.fn()}
52+
/>
53+
);
54+
55+
expect(screen.getByLabelText('First name')).toBeDisabled();
56+
expect(screen.getByLabelText('Last name')).toBeDisabled();
57+
expect(screen.getByRole('button', { name: 'Save' })).toBeDisabled();
58+
});
59+
60+
it('allows editing names when not in SaaS environment', async () => {
61+
const contributor = createOrgContributor({
62+
id: 'org-contributor',
63+
firstName: 'Cora',
64+
lastName: 'Contributor',
65+
66+
});
67+
const admin = createOrgAdmin({ id: 'active-admin' });
68+
69+
await render(
70+
<EditOrganizationUserDialog
71+
organizationId={organizationId}
72+
user={contributor}
73+
users={[contributor, admin]}
74+
activeUser={contributor}
75+
isSaasEnvironment={false}
76+
closeDialog={jest.fn()}
77+
/>
78+
);
79+
80+
expect(screen.getByLabelText('First name')).toBeEnabled();
81+
expect(screen.getByLabelText('Last name')).toBeEnabled();
82+
});
83+
84+
it('calls updateUser when names change and editing is allowed', async () => {
85+
const admin = createOrgAdmin({ id: 'active-admin' });
86+
const contributor = createOrgContributor({
87+
id: 'edited-user',
88+
firstName: 'Casey',
89+
lastName: 'Contributor',
90+
91+
});
92+
const closeDialog = jest.fn();
93+
const usersService = createInMemoryUsersService();
94+
95+
usersService.updateUser = jest.fn(usersService.updateUser);
96+
97+
await render(
98+
<EditOrganizationUserDialog
99+
organizationId={organizationId}
100+
user={contributor}
101+
users={[contributor, admin]}
102+
activeUser={admin}
103+
isSaasEnvironment={false}
104+
closeDialog={closeDialog}
105+
/>,
106+
{
107+
services: { usersService },
108+
}
109+
);
110+
111+
const firstNameInput = screen.getByLabelText('First name');
112+
const saveButton = screen.getByRole('button', { name: 'Save' });
113+
114+
await userEvent.clear(firstNameInput);
115+
await userEvent.type(firstNameInput, 'Updated');
116+
117+
expect(saveButton).toBeEnabled();
118+
119+
await userEvent.click(saveButton);
120+
121+
await waitFor(() => {
122+
expect(usersService.updateUser).toHaveBeenCalledTimes(1);
123+
});
124+
125+
expect(usersService.updateUser).toHaveBeenCalledWith(
126+
organizationId,
127+
expect.objectContaining({
128+
id: contributor.id,
129+
firstName: 'Updated',
130+
lastName: contributor.lastName,
131+
})
132+
);
133+
expect(closeDialog).toHaveBeenCalled();
134+
});
135+
136+
describe('role updates', () => {
137+
it('uses updateMemberRole when manage users roles feature flag is enabled', async () => {
138+
const userToEdit = createOrgAdmin({
139+
id: 'org-admin',
140+
firstName: 'Olivia',
141+
lastName: 'Operator',
142+
143+
});
144+
const activeAdmin = createOrgAdmin({ id: 'active-admin' }, 'workspace-id-2');
145+
const closeDialog = jest.fn();
146+
const usersService = createInMemoryUsersService();
147+
148+
usersService.updateMemberRole = jest.fn().mockResolvedValue(undefined);
149+
usersService.updateRoles = jest.fn();
150+
151+
await render(
152+
<EditOrganizationUserDialog
153+
organizationId={organizationId}
154+
user={userToEdit}
155+
users={[userToEdit, activeAdmin]}
156+
activeUser={activeAdmin}
157+
isSaasEnvironment={false}
158+
closeDialog={closeDialog}
159+
/>,
160+
{
161+
featureFlags: { FEATURE_FLAG_MANAGE_USERS_ROLES: true },
162+
services: { usersService },
163+
}
164+
);
165+
166+
await userEvent.click(screen.getByTestId('roles-add-user'));
167+
const roleListbox = await screen.findByRole('listbox', { name: /organization role/i });
168+
await userEvent.click(within(roleListbox).getByRole('option', { name: /contributor/i }));
169+
170+
const saveButton = screen.getByRole('button', { name: 'Save' });
171+
await userEvent.click(saveButton);
172+
173+
await waitFor(() => {
174+
expect(usersService.updateMemberRole).toHaveBeenCalledTimes(1);
175+
});
176+
177+
expect(usersService.updateMemberRole).toHaveBeenCalledWith(organizationId, userToEdit.id, {
178+
resourceId: organizationId,
179+
role: USER_ROLE.ORGANIZATION_CONTRIBUTOR,
180+
});
181+
expect(usersService.updateRoles).not.toHaveBeenCalled();
182+
expect(closeDialog).toHaveBeenCalled();
183+
});
184+
185+
it('uses updateRoles when manage users roles feature flag is disabled', async () => {
186+
const userToEdit = createOrgAdmin({
187+
id: 'org-admin',
188+
firstName: 'Nina',
189+
lastName: 'Navigator',
190+
191+
});
192+
const activeAdmin = createOrgAdmin({ id: 'active-admin' }, 'workspace-id-2');
193+
const closeDialog = jest.fn();
194+
const usersService = createInMemoryUsersService();
195+
196+
usersService.updateRoles = jest.fn().mockResolvedValue(undefined);
197+
usersService.updateMemberRole = jest.fn();
198+
199+
await render(
200+
<EditOrganizationUserDialog
201+
organizationId={organizationId}
202+
user={userToEdit}
203+
users={[userToEdit, activeAdmin]}
204+
activeUser={activeAdmin}
205+
isSaasEnvironment={false}
206+
closeDialog={closeDialog}
207+
/>,
208+
{
209+
featureFlags: { FEATURE_FLAG_MANAGE_USERS_ROLES: false },
210+
services: { usersService },
211+
}
212+
);
213+
214+
await userEvent.click(screen.getByTestId('roles-add-user'));
215+
const roleListbox = await screen.findByRole('listbox', { name: /organization role/i });
216+
await userEvent.click(within(roleListbox).getByRole('option', { name: /contributor/i }));
217+
218+
await userEvent.click(screen.getByRole('button', { name: 'Save' }));
219+
220+
await waitFor(() => {
221+
expect(usersService.updateRoles).toHaveBeenCalledTimes(1);
222+
});
223+
224+
expect(usersService.updateRoles).toHaveBeenCalledWith(
225+
organizationId,
226+
userToEdit.id,
227+
expect.arrayContaining([
228+
expect.objectContaining({
229+
operation: RoleOperationDTO.DELETE,
230+
role: expect.objectContaining({
231+
resourceId: organizationId,
232+
resourceType: ResourceTypeDTO.ORGANIZATION,
233+
role: UserRoleDTO.ORGANIZATION_ADMIN,
234+
}),
235+
}),
236+
expect.objectContaining({
237+
operation: RoleOperationDTO.CREATE,
238+
role: expect.objectContaining({
239+
resourceId: organizationId,
240+
resourceType: ResourceTypeDTO.ORGANIZATION,
241+
role: UserRoleDTO.ORGANIZATION_CONTRIBUTOR,
242+
}),
243+
}),
244+
])
245+
);
246+
expect(usersService.updateMemberRole).not.toHaveBeenCalled();
247+
expect(closeDialog).toHaveBeenCalled();
248+
});
249+
});
250+
251+
it('disables role picker when editing the last remaining organization admin', async () => {
252+
const loneAdmin = createOrgAdmin({
253+
id: 'only-admin',
254+
firstName: 'Lara',
255+
lastName: 'Leader',
256+
257+
});
258+
259+
await render(
260+
<EditOrganizationUserDialog
261+
organizationId={organizationId}
262+
user={loneAdmin}
263+
users={[loneAdmin]}
264+
activeUser={loneAdmin}
265+
isSaasEnvironment={false}
266+
closeDialog={jest.fn()}
267+
/>
268+
);
269+
270+
expect(screen.getByTestId('roles-add-user')).toBeDisabled();
271+
});
272+
});

web_ui/src/pages/user-management/users/actions/organization-user-actions.component.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import { ActionMenu } from '../../../../shared/components/action-menu/action-men
1515
import { MenuAction } from '../../../../shared/components/action-menu/menu-action.interface';
1616
import { HasPermission } from '../../../../shared/components/has-permission/has-permission.component';
1717
import { OPERATION } from '../../../../shared/components/has-permission/has-permission.interface';
18-
import { RemoveUserDialog } from '../workspace-users/actions/remove-user-dialog.component';
1918
import { EditOrganizationUserDialog } from './edit-organization-user-dialog.component';
19+
import { RemoveUserDialog } from './remove-user-dialog.component';
2020

2121
enum ORG_USER_ACTIONS_OPTIONS {
2222
DELETE = 'Delete from organization',
Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,11 @@
44
import QUERY_KEYS from '@geti/core/src/requests/query-keys';
55
import { useUsers } from '@geti/core/src/users/hook/use-users.hook';
66
import { User } from '@geti/core/src/users/users.interface';
7-
import { AlertDialog } from '@geti/ui';
7+
import { AlertDialog, Flex, Text } from '@geti/ui';
88
import { useQueryClient } from '@tanstack/react-query';
99
import { isFunction } from 'lodash-es';
1010

11-
import { useHandleSignOut } from '../../../../../hooks/use-handle-sign-out/use-handle-sign-out.hook';
12-
13-
import classes from '../workspace-user.module.scss';
11+
import { useHandleSignOut } from '../../../../hooks/use-handle-sign-out/use-handle-sign-out.hook';
1412

1513
interface UserActionsProps {
1614
organizationId: string;
@@ -40,7 +38,7 @@ export const RemoveUserDialog = ({ organizationId, user, activeUser, onDeleting
4038
} catch (_error: unknown) {}
4139
};
4240

43-
const question = `Are you sure you want to delete "${user.email}"?`;
41+
const email = user.email;
4442

4543
return (
4644
<AlertDialog
@@ -49,9 +47,14 @@ export const RemoveUserDialog = ({ organizationId, user, activeUser, onDeleting
4947
primaryActionLabel='Delete'
5048
onPrimaryAction={deleteUserAction}
5149
cancelLabel={'Cancel'}
52-
UNSAFE_className={classes.removeUserDialog}
5350
>
54-
{question}
51+
<Flex direction={'column'} gap={'size-150'}>
52+
<Text>
53+
This user account of {email} will be permanently deleted from your Geti™ organization. After
54+
deleting the account, the user will not be able to log in again with this account.
55+
</Text>
56+
<Text>Are you sure you want to delete {email}?</Text>
57+
</Flex>
5558
</AlertDialog>
5659
);
5760
};

0 commit comments

Comments
 (0)