Skip to content

Commit c5e360a

Browse files
authored
Fix server-side role search behavior across role selectors (#27737)
* Fix server-side role search behavior across role selectors * address gitar * fix failing spec
1 parent 439efc4 commit c5e360a

20 files changed

Lines changed: 409 additions & 168 deletions

File tree

openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/SSOConfiguration.spec.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -780,15 +780,20 @@ test.describe('SSO Configuration Tests', () => {
780780
// Typing filters the visible options
781781
await field.click();
782782
await field.locator('input').fill('Data');
783+
await page.waitForResponse('/api/v1/roles/search?*');
783784
await expect(
784785
dropdown.locator(
785786
'.ant-select-item-option:not(.ant-select-item-option-disabled)'
786787
)
787-
).not.toHaveCount(0);
788+
).not.toHaveCount(0, { timeout: 15000 });
788789

789790
// Pressing Enter on a non-existent value does not create an arbitrary tag
790791
await field.locator('input').clear();
792+
const missingRoleSearchResponse = page.waitForResponse(
793+
'/api/v1/roles/search?*'
794+
);
791795
await field.locator('input').fill('NonExistentRoleXYZ123');
796+
await missingRoleSearchResponse;
792797
await field.locator('input').press('Enter');
793798
await expect(field.locator('.ant-select-selection-item')).toHaveCount(0);
794799
});

openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/AddRoleAndAssignToUser.spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@ test.describe.serial('Add role and assign it to the user', () => {
7979
test('Create new user and assign new role to him', async ({ page }) => {
8080
await settingClick(page, GlobalSettingOptions.USERS);
8181

82+
const initialRolesResponse = page.waitForResponse('/api/v1/roles/search?*');
8283
await page.click('[data-testid="add-user"]');
84+
await initialRolesResponse;
8385

8486
await page.fill('[data-testid="email"]', user.email);
8587
await page.fill('[data-testid="displayName"]', userDisplayName);
@@ -96,7 +98,9 @@ test.describe.serial('Add role and assign it to the user', () => {
9698
await page.locator('.ant-select-dropdown').waitFor({
9799
state: 'visible',
98100
});
101+
const rolesSearchResponse = page.waitForResponse('/api/v1/roles/search?*');
99102
await page.fill('#roles', roleName);
103+
await rolesSearchResponse;
100104
await page.click(`[title="${roleName}"]`);
101105

102106
await page.keyboard.press('Escape');

openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Domains.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2714,7 +2714,9 @@ test.describe('Domains Rbac', () => {
27142714

27152715
// Add domain role to the user
27162716
await visitUserProfilePage(page, user1.responseData.name);
2717+
const initialRolesResponse = page.waitForResponse('/api/v1/roles/search?*');
27172718
await page.getByTestId('edit-roles-button').click();
2719+
await initialRolesResponse;
27182720

27192721
await page.locator('[data-testid="user-profile-edit-popover"]').isVisible();
27202722
const rolesCombobox = page.locator('input[role="combobox"]').nth(1);

openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/UserDetails.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,11 @@ test.describe('User with different Roles', () => {
501501

502502
await expect(adminPage.getByTestId('user-profile-roles')).toBeVisible();
503503

504+
const initialRolesResponse = adminPage.waitForResponse(
505+
'/api/v1/roles/search?*'
506+
);
504507
await adminPage.getByTestId('edit-roles-button').click();
508+
await initialRolesResponse;
505509

506510
await expect(
507511
adminPage.getByTestId('profile-edit-roles-select')
@@ -511,6 +515,15 @@ test.describe('User with different Roles', () => {
511515
state: 'visible',
512516
});
513517

518+
await adminPage
519+
.getByTestId('profile-edit-roles-select')
520+
.locator('input')
521+
.fill('Application');
522+
await adminPage.waitForResponse('/api/v1/roles/search?*');
523+
await adminPage
524+
.locator('.ant-select-item-option-content')
525+
.getByText('Application bot role', { exact: true })
526+
.waitFor({ state: 'visible' });
514527
await adminPage
515528
.locator('.ant-select-item-option-content')
516529
.getByText('Application bot role', { exact: true })

openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -719,7 +719,7 @@ export const addUser = async (
719719
await waitForAllLoadersToDisappear(page);
720720
await page.click('[data-testid="add-user"]');
721721

722-
await page.waitForResponse('/api/v1/roles?default=false&limit=100&fields=');
722+
await page.waitForResponse('/api/v1/roles/search?*');
723723
await page.fill('[data-testid="email"]', email);
724724

725725
await page.fill('[data-testid="displayName"]', name);
@@ -735,7 +735,9 @@ export const addUser = async (
735735
.getByRole('combobox');
736736
await expect(rolesCombobox).toBeVisible({ timeout: 120000 });
737737
await rolesCombobox.click();
738+
const rolesSearchResponse = page.waitForResponse('/api/v1/roles/search?*');
738739
await rolesCombobox.fill(role);
740+
await rolesSearchResponse;
739741
const roleOption = page
740742
.locator('.ant-select-item-option-content')
741743
.filter({ hasText: new RegExp(`^${role}$`) })

openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotDetails/BotDetails.component.tsx

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,16 @@
1414
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
1515
import { Button, Card, Col, Input, Row, Typography } from 'antd';
1616
import { AxiosError } from 'axios';
17-
import { toLower } from 'lodash';
18-
import { FC, useEffect, useMemo, useState } from 'react';
17+
import { debounce, toLower, uniqBy } from 'lodash';
18+
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
1919
import { useTranslation } from 'react-i18next';
2020
import { ReactComponent as IconBotProfile } from '../../../../assets/svg/bot-profile.svg';
21-
import { PAGE_SIZE_LARGE, TERM_ADMIN } from '../../../../constants/constants';
21+
import { TERM_ADMIN } from '../../../../constants/constants';
2222
import { GlobalSettingOptions } from '../../../../constants/GlobalSettings.constants';
2323
import { useLimitStore } from '../../../../context/LimitsProvider/useLimitsStore';
2424
import { EntityType } from '../../../../enums/entity.enum';
2525
import { Role } from '../../../../generated/entity/teams/role';
26-
import { getAllRoles } from '../../../../rest/rolesAPIV1';
26+
import { searchRoles } from '../../../../rest/rolesAPIV1';
2727
import { getEntityName } from '../../../../utils/EntityUtils';
2828
import { getSettingPath } from '../../../../utils/RouterUtils';
2929
import { showErrorToast } from '../../../../utils/ToastUtils';
@@ -48,6 +48,8 @@ const BotDetails: FC<BotsDetailProps> = ({
4848
const [isDisplayNameEdit, setIsDisplayNameEdit] = useState(false);
4949
const [selectedRoles, setSelectedRoles] = useState<Array<string>>([]);
5050
const [roles, setRoles] = useState<Array<Role>>([]);
51+
const [isRolesLoading, setIsRolesLoading] = useState(false);
52+
const selectedRolesRef = useRef<string[]>([]);
5153
const { getResourceLimit, config } = useLimitStore();
5254

5355
const [disableFields, setDisableFields] = useState<string[]>(['token']);
@@ -74,15 +76,29 @@ const BotDetails: FC<BotsDetailProps> = ({
7476
}
7577
};
7678

77-
const fetchRoles = async () => {
79+
const fetchRoles = useCallback(async (query = '') => {
80+
setIsRolesLoading(true);
81+
7882
try {
79-
const data = await getAllRoles('', false, PAGE_SIZE_LARGE);
80-
setRoles(data);
83+
const data = await searchRoles(query);
84+
setRoles((prevRoles) => {
85+
const selectedRoleOptions = prevRoles.filter((role) =>
86+
selectedRolesRef.current.includes(role.id)
87+
);
88+
89+
return uniqBy([...selectedRoleOptions, ...data], 'id');
90+
});
8191
} catch (err) {
82-
setRoles([]);
8392
showErrorToast(err as AxiosError);
93+
} finally {
94+
setIsRolesLoading(false);
8495
}
85-
};
96+
}, []);
97+
98+
const debouncedFetchRoles = useMemo(
99+
() => debounce(fetchRoles, 300),
100+
[fetchRoles]
101+
);
86102

87103
const onDisplayNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
88104
setDisplayName(e.target.value);
@@ -190,7 +206,9 @@ const BotDetails: FC<BotsDetailProps> = ({
190206
</Col>
191207
<Col span={24}>
192208
<RolesCard
209+
isRolesLoading={isRolesLoading}
193210
roles={roles}
211+
searchRolesOptions={debouncedFetchRoles}
194212
selectedRoles={selectedRoles}
195213
setSelectedRoles={(selectedRoles) =>
196214
setSelectedRoles(selectedRoles)
@@ -206,13 +224,36 @@ const BotDetails: FC<BotsDetailProps> = ({
206224
);
207225
};
208226

227+
useEffect(() => {
228+
selectedRolesRef.current = selectedRoles;
229+
}, [selectedRoles]);
230+
209231
useEffect(() => {
210232
fetchRoles();
211233
initLimits();
212234
}, []);
213235

236+
useEffect(() => {
237+
return () => {
238+
debouncedFetchRoles.cancel();
239+
};
240+
}, [debouncedFetchRoles]);
241+
214242
useEffect(() => {
215243
prepareSelectedRoles();
244+
setRoles((prevRoles) =>
245+
uniqBy(
246+
[
247+
...prevRoles,
248+
...((botUserData.roles ?? []).map((role) => ({
249+
id: role.id,
250+
name: role.name ?? '',
251+
displayName: role.displayName,
252+
})) as Role[]),
253+
],
254+
'id'
255+
)
256+
);
216257
}, [botUserData]);
217258

218259
return (

openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotDetails/BotDetails.test.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import { act, render, screen } from '@testing-library/react';
1515
import { MemoryRouter } from 'react-router-dom';
1616
import { OperationPermission } from '../../../../context/PermissionProvider/PermissionProvider.interface';
17+
import { searchRoles } from '../../../../rest/rolesAPIV1';
1718
import { getAuthMechanismForBotUser } from '../../../../rest/userAPI';
1819
import AccessTokenCard from '../../Users/AccessTokenCard/AccessTokenCard.component';
1920
import BotDetails from './BotDetails.component';
@@ -93,6 +94,10 @@ jest.mock('../../../../utils/PermissionsUtils', () => ({
9394
checkPermission: jest.fn().mockReturnValue(true),
9495
}));
9596

97+
jest.mock('../../../../rest/rolesAPIV1', () => ({
98+
searchRoles: jest.fn().mockResolvedValue([]),
99+
}));
100+
96101
const mockGetResourceLimit = jest.fn().mockResolvedValue({
97102
configuredLimit: { disabledFields: [] },
98103
});
@@ -146,6 +151,10 @@ jest.mock('../../../../context/LimitsProvider/useLimitsStore', () => ({
146151
}));
147152

148153
describe('Test BotsDetail Component', () => {
154+
beforeEach(() => {
155+
(searchRoles as jest.Mock).mockResolvedValue([]);
156+
});
157+
149158
it('Should render all child elements', async () => {
150159
await act(async () => {
151160
render(<BotDetails {...mockProp} />, {

openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/CreateUser/CreateUser.component.tsx

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,16 @@ import {
2424
Switch,
2525
} from 'antd';
2626
import { AxiosError } from 'axios';
27-
import { compact, isEmpty, isUndefined, map, trim } from 'lodash';
28-
import { useEffect, useMemo, useState } from 'react';
27+
import {
28+
compact,
29+
debounce,
30+
isEmpty,
31+
isUndefined,
32+
map,
33+
trim,
34+
uniqBy,
35+
} from 'lodash';
36+
import { useCallback, useEffect, useMemo, useState } from 'react';
2937
import { useTranslation } from 'react-i18next';
3038
import { useLocation } from 'react-router-dom';
3139
import { ReactComponent as IconSync } from '../../../../assets/svg/ic-sync.svg';
@@ -55,8 +63,8 @@ import {
5563
} from '../../../../interface/FormUtils.interface';
5664
import { generateRandomPwd } from '../../../../rest/auth-API';
5765
import { getAllPersonas } from '../../../../rest/PersonaAPI';
66+
import { searchRoles } from '../../../../rest/rolesAPIV1';
5867
import { getJWTTokenExpiryOptions } from '../../../../utils/BotsUtils';
59-
import { handleSearchFilterOption } from '../../../../utils/CommonUtils';
6068
import {
6169
getEntityName,
6270
getEntityReferenceListFromEntities,
@@ -72,7 +80,6 @@ import TeamsSelectable from '../../Team/TeamsSelectable/TeamsSelectable';
7280
import { CreateUserProps } from './CreateUser.interface';
7381

7482
const CreateUser = ({
75-
roles,
7683
isLoading,
7784
onCancel,
7885
onSave,
@@ -92,6 +99,10 @@ const CreateUser = ({
9299
const [selectedTeams, setSelectedTeams] = useState<
93100
Array<EntityReference | undefined>
94101
>([]);
102+
const [roleOptions, setRoleOptions] = useState<
103+
Array<{ label: string; value: string }>
104+
>([]);
105+
const [isRolesLoading, setIsRolesLoading] = useState(false);
95106
const [isPasswordGenerating, setIsPasswordGenerating] = useState(false);
96107
const { activeDomainEntityRef } = useDomainStore();
97108
const selectedDomain =
@@ -135,12 +146,35 @@ const CreateUser = ({
135146
const selectedRoles = Form.useWatch('roles', form);
136147
const selectedPersonas = Form.useWatch('personas', form);
137148

138-
const roleOptions = useMemo(() => {
139-
return map(roles, (role) => ({
140-
label: getEntityName(role),
141-
value: role.id,
142-
}));
143-
}, [roles]);
149+
const fetchRoleOptions = useCallback(
150+
async (searchText = '') => {
151+
setIsRolesLoading(true);
152+
153+
try {
154+
const roles = await searchRoles(searchText);
155+
const nextOptions = map(roles, (role) => ({
156+
label: getEntityName(role),
157+
value: role.id,
158+
}));
159+
160+
setRoleOptions((prevOptions) => {
161+
const selectedRoleOptions = prevOptions.filter((option) =>
162+
(selectedRoles ?? []).includes(String(option.value))
163+
);
164+
165+
return uniqBy([...selectedRoleOptions, ...nextOptions], 'value');
166+
});
167+
} catch (error) {
168+
showErrorToast(
169+
error as AxiosError,
170+
t('server.entity-fetch-error', { entity: t('label.role-plural') })
171+
);
172+
} finally {
173+
setIsRolesLoading(false);
174+
}
175+
},
176+
[selectedRoles, t]
177+
);
144178

145179
const fetchPersonaOptions = async (_searchText: string, page?: number) => {
146180
try {
@@ -264,6 +298,23 @@ const CreateUser = ({
264298
generateRandomPassword();
265299
}, []);
266300

301+
useEffect(() => {
302+
if (!forceBot && !isAdminPage) {
303+
fetchRoleOptions();
304+
}
305+
}, [forceBot, isAdminPage]);
306+
307+
const debouncedFetchRoleOptions = useMemo(
308+
() => debounce(fetchRoleOptions, 300),
309+
[fetchRoleOptions]
310+
);
311+
312+
useEffect(() => {
313+
return () => {
314+
debouncedFetchRoleOptions.cancel();
315+
};
316+
}, [debouncedFetchRoleOptions]);
317+
267318
return (
268319
<Form
269320
form={form}
@@ -441,15 +492,18 @@ const CreateUser = ({
441492
</Form.Item>
442493
<Form.Item label={t('label.role-plural')} name="roles">
443494
<Select
495+
showSearch
444496
data-testid="roles-dropdown"
445-
disabled={isEmpty(roles)}
446-
filterOption={handleSearchFilterOption}
497+
disabled={isRolesLoading && isEmpty(roleOptions)}
498+
filterOption={false}
447499
getPopupContainer={(triggerNode) => triggerNode.parentElement}
500+
loading={isRolesLoading}
448501
mode="multiple"
449502
options={roleOptions}
450503
placeholder={t('label.please-select-entity', {
451504
entity: t('label.role-plural'),
452505
})}
506+
onSearch={debouncedFetchRoleOptions}
453507
/>
454508
</Form.Item>
455509
<Form.Item label={t('label.persona-plural')} name="personas">

openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/CreateUser/CreateUser.interface.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,9 @@
1212
*/
1313

1414
import { CreateUser } from '../../../../generated/api/teams/createUser';
15-
import { Role } from '../../../../generated/entity/teams/role';
1615

1716
export interface CreateUserProps {
1817
isLoading?: boolean;
19-
roles: Array<Role>;
2018
onSave: (data: CreateUser) => void;
2119
onCancel: () => void;
2220
forceBot: boolean;

0 commit comments

Comments
 (0)