Skip to content

Commit 9a671d4

Browse files
ActiveChooNCopilot
andauthored
Enhance user management tab (#1394)
Co-authored-by: Copilot <[email protected]>
1 parent 01a3627 commit 9a671d4

23 files changed

+951
-231
lines changed

web_ui/packages/core/src/services/use-deployment-config-query.hook.test.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ describe('useDeploymentConfigQuery', () => {
3737
const renderDeploymentConfigHook = ({ isAdmin = false }: { isAdmin?: boolean } = {}) => {
3838
jest.mocked(isAdminLocation).mockImplementation(() => isAdmin);
3939

40-
return renderHookWithProviders(useDeploymentConfigQuery);
40+
return renderHookWithProviders(useDeploymentConfigQuery, {
41+
providerProps: { skipPrefillDeploymentConfig: true },
42+
});
4143
};
4244

4345
describe('not having a deployment config', () => {

web_ui/packages/core/src/users/hook/use-users.hook.ts

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,20 @@ import { validate } from 'uuid';
1010

1111
import { AccountStatusDTO } from '../../../../../src/core/organizations/dtos/organizations.interface';
1212
import { redirectTo } from '../../../../../src/shared/utils';
13+
import { useFeatureFlags } from '../../feature-flags/hooks/use-feature-flags.hook';
1314
import QUERY_KEYS from '../../requests/query-keys';
1415
import { useApplicationServices } from '../../services/application-services-provider.component';
1516
import { paths } from '../../services/routes';
1617
import { getErrorMessage } from '../../services/utils';
1718
import { ForgotPasswordDTO, ResetPasswordDTO, UpdatePasswordDTO, UserRegistrationDTO } from '../dtos/members.interface';
18-
import { getUsersQueryParamsDTO } from '../services/utils';
19-
import { RoleResource, User, UsersResponse } from '../users.interface';
19+
import { getRoleCreationPayload, getRoleDeletionPayload, getUsersQueryParamsDTO } from '../services/utils';
20+
import { RoleResource, UpdateRolePayload, User, UsersResponse } from '../users.interface';
2021
import {
2122
UseCreateUserPayload,
2223
UseDeleteUserPayload,
2324
UseDeleteUserPhotoPayload,
2425
UseInviteUserPayload,
26+
UseUpdateRolePayload,
2527
UseUpdateUserPayload,
2628
UseUpdateUserStatusesPayload,
2729
UseUploadUserPhotoPayload,
@@ -325,6 +327,55 @@ export const useUsers = (): UseUsers => {
325327
});
326328
};
327329

330+
const useUpdateRole: UseUsers['useUpdateRole'] = () => {
331+
const { FEATURE_FLAG_MANAGE_USERS_ROLES } = useFeatureFlags();
332+
333+
return useMutation<void, AxiosError, UseUpdateRolePayload>({
334+
mutationFn: async ({ organizationId, userId, newRole, previousRole, resourceId, resourceType }) => {
335+
if (FEATURE_FLAG_MANAGE_USERS_ROLES) {
336+
await usersService.updateMemberRole(organizationId, userId, {
337+
role: newRole,
338+
resourceId,
339+
});
340+
341+
return;
342+
}
343+
344+
const roleUpdates: UpdateRolePayload[] = [];
345+
346+
if (previousRole) {
347+
roleUpdates.push(
348+
getRoleDeletionPayload({
349+
role: previousRole,
350+
resourceId,
351+
resourceType,
352+
})
353+
);
354+
}
355+
356+
roleUpdates.push(
357+
getRoleCreationPayload({
358+
role: newRole,
359+
resourceId,
360+
resourceType,
361+
})
362+
);
363+
364+
await usersService.updateRoles(organizationId, userId, roleUpdates);
365+
},
366+
onSuccess: async (_, { organizationId, userId, resourceType }) => {
367+
if (FEATURE_FLAG_MANAGE_USERS_ROLES) {
368+
await queryClient.invalidateQueries({ queryKey: QUERY_KEYS.USERS(organizationId) });
369+
await queryClient.invalidateQueries({ queryKey: QUERY_KEYS.ACTIVE_USER(organizationId) });
370+
} else {
371+
await queryClient.invalidateQueries({
372+
queryKey: QUERY_KEYS.USER_ROLES(organizationId, userId, resourceType),
373+
});
374+
}
375+
},
376+
});
377+
};
378+
328379
return {
329380
useActiveUser,
330381
useGetUsersQuery,
@@ -338,7 +389,7 @@ export const useUsers = (): UseUsers => {
338389
useUploadUserPhoto,
339390
useDeleteUserPhoto,
340391
useUserRoles,
341-
392+
useUpdateRole,
342393
useUpdateMemberRole,
343394
useDeleteMemberRole,
344395
};

web_ui/packages/core/src/users/hook/use-users.interface.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ interface UseUpdateMemberRolePayload {
7878
role: MemberRole;
7979
}
8080

81+
export interface UseUpdateRolePayload extends UseUsersBasePayload {
82+
resourceId: string;
83+
resourceType: RESOURCE_TYPE;
84+
newRole: MemberRole['role'];
85+
previousRole?: MemberRole['role'];
86+
}
87+
8188
export interface UseUsers {
8289
useActiveUser: (organizationId: string, resource?: Resource) => UseQueryResult<User, AxiosError>;
8390
useGetUsersQuery: (organizationId: string, queryParams?: UsersQueryParams) => UseGetUsersQuery;
@@ -92,6 +99,7 @@ export interface UseUsers {
9299
useUpdateUserRoles: () => UseMutationResult<void, AxiosError, UseUpdateUserRolesPayload>;
93100
useInviteUserMutation: (organizationId: string) => UseMutationResult<void, AxiosError, UseInviteUserPayload>;
94101

102+
useUpdateRole: () => UseMutationResult<void, AxiosError, UseUpdateRolePayload>;
95103
useUpdateMemberRole: () => UseMutationResult<void, AxiosError, UseUpdateMemberRolePayload>;
96104
useDeleteMemberRole: () => UseMutationResult<void, AxiosError, UseUpdateMemberRolePayload>;
97105
}
Lines changed: 89 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,50 @@
11
// Copyright (C) 2022-2025 Intel Corporation
22
// LIMITED EDGE SOFTWARE DISTRIBUTION LICENSE
33

4-
import { ReactNode } from 'react';
5-
6-
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
7-
import { renderHook, waitFor } from '@testing-library/react';
4+
import { act, waitFor } from '@testing-library/react';
85

96
import { getMockedUser } from '../../../../../src/test-utils/mocked-items-factory/mocked-users';
10-
import { ApplicationServicesProvider } from '../../services/application-services-provider.component';
7+
import { renderHookWithProviders } from '../../../../../src/test-utils/render-hook-with-providers';
118
import { createInMemoryUsersService } from '../services/in-memory-users-service';
9+
import { getRoleCreationPayload, getRoleDeletionPayload } from '../services/utils';
10+
import { RESOURCE_TYPE, USER_ROLE } from '../users.interface';
1211
import { useActiveUser, useUsers } from './use-users.hook';
1312

1413
jest.mock('react-router-dom', () => ({
1514
...jest.requireActual('react-router-dom'),
1615
useParams: () => ({ workspaceId: 'workspace-id', projectId: 'project-id', organizationId: 'organization-123' }),
1716
}));
1817

19-
const queryClient = new QueryClient({
20-
defaultOptions: {
21-
queries: {
22-
retry: false, // Disable query retries (default is 3)
23-
},
24-
},
25-
});
26-
2718
const mockedUser = getMockedUser();
28-
const mockedUsersService = createInMemoryUsersService();
19+
let mockedUsersService = createInMemoryUsersService();
2920
mockedUsersService.getUser = jest.fn(async () => mockedUser);
21+
mockedUsersService.updateRoles = jest.fn(mockedUsersService.updateRoles);
22+
mockedUsersService.updateMemberRole = jest.fn(mockedUsersService.updateMemberRole);
3023

31-
const wrapper = ({ children }: { children?: ReactNode }) => {
32-
return (
33-
<QueryClientProvider client={queryClient}>
34-
<ApplicationServicesProvider usersService={mockedUsersService} useInMemoryEnvironment>
35-
{children}
36-
</ApplicationServicesProvider>
37-
</QueryClientProvider>
38-
);
39-
};
24+
const getProviderProps = (featureFlags?: Record<string, boolean>) => ({
25+
usersService: mockedUsersService,
26+
useInMemoryEnvironment: true as const,
27+
featureFlags,
28+
});
4029

4130
describe('useUsers', () => {
31+
beforeEach(() => {
32+
mockedUsersService = createInMemoryUsersService();
33+
mockedUsersService.getUser = jest.fn(async () => mockedUser);
34+
const originalUpdateRoles = mockedUsersService.updateRoles;
35+
mockedUsersService.updateRoles = jest.fn(originalUpdateRoles);
36+
const originalUpdateMemberRole = mockedUsersService.updateMemberRole;
37+
mockedUsersService.updateMemberRole = jest.fn(originalUpdateMemberRole);
38+
});
39+
4240
afterEach(() => {
4341
jest.clearAllMocks();
4442
});
4543

4644
it('gets activeUser', async () => {
47-
const { result } = renderHook(() => useActiveUser('organization-id'), { wrapper });
45+
const { result } = renderHookWithProviders(() => useActiveUser('organization-id'), {
46+
providerProps: getProviderProps(),
47+
});
4848

4949
await waitFor(() => {
5050
expect(result.current).not.toBeNull();
@@ -56,26 +56,83 @@ describe('useUsers', () => {
5656
});
5757

5858
it('query is not executed if the user id is "undefined"', async () => {
59-
const { result } = renderHook(() => useUsers(), { wrapper });
60-
61-
renderHook(() => result.current.useGetUserQuery('organization-id', undefined), {
62-
wrapper,
59+
renderHookWithProviders(() => useUsers().useGetUserQuery('organization-id', undefined), {
60+
providerProps: getProviderProps(),
6361
});
6462

6563
expect(mockedUsersService.getUser).not.toHaveBeenCalled();
6664
});
6765

6866
it('query is not executed if the user id is invalid', async () => {
69-
const { result } = renderHook(() => useUsers(), { wrapper });
67+
renderHookWithProviders(() => useUsers().useGetUserQuery('organization-id', '[email protected]'), {
68+
providerProps: getProviderProps(),
69+
});
70+
71+
expect(mockedUsersService.getUser).not.toHaveBeenCalled();
72+
});
73+
74+
it('updates member role when feature flag is enabled', async () => {
75+
const { result } = renderHookWithProviders(() => useUsers().useUpdateRole(), {
76+
providerProps: getProviderProps({ FEATURE_FLAG_MANAGE_USERS_ROLES: true }),
77+
});
7078

7179
await waitFor(() => {
7280
expect(result.current).not.toBeNull();
7381
});
7482

75-
renderHook(() => result.current.useGetUserQuery('organization-id', '[email protected]'), {
76-
wrapper,
83+
await act(async () => {
84+
await result.current.mutateAsync({
85+
organizationId: 'organization-id',
86+
userId: 'user-id',
87+
resourceId: 'organization-id',
88+
resourceType: RESOURCE_TYPE.ORGANIZATION,
89+
newRole: USER_ROLE.ORGANIZATION_CONTRIBUTOR,
90+
});
7791
});
7892

79-
expect(mockedUsersService.getUser).not.toHaveBeenCalled();
93+
expect(mockedUsersService.updateMemberRole).toHaveBeenCalledTimes(1);
94+
expect(mockedUsersService.updateMemberRole).toHaveBeenCalledWith('organization-id', 'user-id', {
95+
role: USER_ROLE.ORGANIZATION_CONTRIBUTOR,
96+
resourceId: 'organization-id',
97+
});
98+
expect(mockedUsersService.updateRoles).not.toHaveBeenCalled();
99+
});
100+
101+
it('updates roles when feature flag is disabled', async () => {
102+
const { result } = renderHookWithProviders(() => useUsers().useUpdateRole(), {
103+
providerProps: getProviderProps({ FEATURE_FLAG_MANAGE_USERS_ROLES: false }),
104+
});
105+
106+
const payload = {
107+
organizationId: 'organization-id',
108+
userId: 'user-id',
109+
resourceId: 'organization-id',
110+
resourceType: RESOURCE_TYPE.ORGANIZATION,
111+
newRole: USER_ROLE.ORGANIZATION_CONTRIBUTOR,
112+
previousRole: USER_ROLE.ORGANIZATION_ADMIN,
113+
} as const;
114+
115+
await waitFor(() => {
116+
expect(result.current).not.toBeNull();
117+
});
118+
119+
await act(async () => {
120+
await result.current.mutateAsync(payload);
121+
});
122+
123+
expect(mockedUsersService.updateRoles).toHaveBeenCalledTimes(1);
124+
expect(mockedUsersService.updateRoles).toHaveBeenCalledWith('organization-id', 'user-id', [
125+
getRoleDeletionPayload({
126+
role: payload.previousRole,
127+
resourceId: payload.resourceId,
128+
resourceType: payload.resourceType,
129+
}),
130+
getRoleCreationPayload({
131+
role: payload.newRole,
132+
resourceId: payload.resourceId,
133+
resourceType: payload.resourceType,
134+
}),
135+
]);
136+
expect(mockedUsersService.updateMemberRole).not.toHaveBeenCalled();
80137
});
81138
});

web_ui/packages/core/src/users/services/utils.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,18 @@ const resourceMapDTO: Record<ResourceTypeDTO, RESOURCE_TYPE> = {
8686
[ResourceTypeDTO.ORGANIZATION]: RESOURCE_TYPE.ORGANIZATION,
8787
};
8888

89+
export const mapResourceTypeToDTO = (
90+
resourceType?: RESOURCE_TYPE | RESOURCE_TYPE[]
91+
): ResourceTypeDTO | ResourceTypeDTO[] | undefined => {
92+
if (resourceType == null) {
93+
return undefined;
94+
}
95+
96+
return Array.isArray(resourceType)
97+
? resourceType.map((type) => USER_RESOURCE_TYPE_MAPPING_DTO[type])
98+
: USER_RESOURCE_TYPE_MAPPING_DTO[resourceType];
99+
};
100+
89101
const getRoles = (roles: RoleResourceDTO[]): RoleResource[] => {
90102
return roles.reduce<Role[]>((prev, curr) => {
91103
const { resourceId, resourceType, role } = curr;
@@ -220,7 +232,7 @@ export const getUsersQueryParamsDTO = (queryParams: UsersQueryParams): UsersQuer
220232
secondName: lastName,
221233
externalId: externalIdentitySystemId,
222234
role: role ? USER_ROLE_MAPPING_DTO[role] : undefined,
223-
resourceType: resourceType ? USER_RESOURCE_TYPE_MAPPING_DTO[resourceType] : undefined,
235+
resourceType: mapResourceTypeToDTO(resourceType),
224236
sortDirection: sortDirection ? (sortDirection === 'ASC' ? 'asc' : 'desc') : undefined,
225237

226238
sortBy:

web_ui/packages/core/src/users/users.interface.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@ export interface Role {
103103
}
104104

105105
export enum USER_ROLE {
106-
WORKSPACE_ADMIN = 'Admin',
107-
WORKSPACE_CONTRIBUTOR = 'Contributor',
106+
WORKSPACE_ADMIN = 'Workspace admin',
107+
WORKSPACE_CONTRIBUTOR = 'Workspace contributor',
108108
PROJECT_MANAGER = 'Project manager',
109109
PROJECT_CONTRIBUTOR = 'Project contributor',
110110
ORGANIZATION_ADMIN = 'Organization admin',
@@ -123,8 +123,9 @@ export interface UsersQueryParamsDTO
123123
| 'userPhoto'
124124
>
125125
>,
126-
Partial<RoleResourceDTO>,
126+
Omit<Partial<RoleResourceDTO>, 'resourceType'>,
127127
QueryParametersDTO<keyof UserDTO> {
128+
resourceType?: ResourceTypeDTO | ResourceTypeDTO[];
128129
lastSuccessfulLoginFrom?: string;
129130
lastSuccessfulLoginTo?: string;
130131
}
@@ -193,8 +194,9 @@ export interface UsersQueryParams
193194
| 'userPhoto'
194195
> & { name: string }
195196
>,
196-
Partial<RoleResource>,
197+
Omit<Partial<RoleResource>, 'resourceType'>,
197198
QueryParameters<keyof User> {
199+
resourceType?: RESOURCE_TYPE | RESOURCE_TYPE[];
198200
lastSuccessfulLoginFrom?: string;
199201
lastSuccessfulLoginTo?: string;
200202
}

0 commit comments

Comments
 (0)