Skip to content

Commit 0343906

Browse files
Copilotkaranh37gitar-botCopilotmohityadav766
authored
Add persona dropdown to user creation form (#25243)
* Initial plan * Add persona dropdown to user creation form Co-authored-by: karanh37 <[email protected]> * Refactor personas to use Form.useWatch for consistency Co-authored-by: karanh37 <[email protected]> * Add loading state and better error handling for personas Co-authored-by: karanh37 <[email protected]> * fix: pass entity references for personas in CreateUser component Co-authored-by: Copilot <[email protected]> * fix: add closing brace in PersonaAPI jest.mock and implement infinite scroll for personas list Co-authored-by: Copilot <[email protected]> * increase fetch option limit for persona * Added test for persona flow * nit * nit * nit * fix e2e test --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: karanh37 <[email protected]> Co-authored-by: Gitar <[email protected]> Co-authored-by: Copilot <[email protected]> Co-authored-by: Mohit Yadav <[email protected]> Co-authored-by: Anujkumar Yadav <[email protected]>
1 parent 9666071 commit 0343906

File tree

5 files changed

+235
-43
lines changed

5 files changed

+235
-43
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright 2026 Collate.
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
* limitations under the License.
12+
*/
13+
import { expect } from '@playwright/test';
14+
import { DATA_STEWARD_RULES } from '../../constant/permission';
15+
import { PolicyClass } from '../../support/access-control/PoliciesClass';
16+
import { RolesClass } from '../../support/access-control/RolesClass';
17+
import { PersonaClass } from '../../support/persona/PersonaClass';
18+
import { UserClass } from '../../support/user/UserClass';
19+
import { performAdminLogin } from '../../utils/admin';
20+
import { redirectToHomePage, uuid } from '../../utils/common';
21+
import {
22+
addUser,
23+
permanentDeleteUser,
24+
visitUserListPage,
25+
visitUserProfilePage,
26+
} from '../../utils/user';
27+
28+
import { test } from '../fixtures/pages';
29+
30+
const adminUser = new UserClass();
31+
const persona1 = new PersonaClass();
32+
const policy = new PolicyClass();
33+
const role = new RolesClass();
34+
35+
test.beforeAll('Setup pre-requests', async ({ browser }) => {
36+
const { apiContext, afterAction } = await performAdminLogin(browser);
37+
await adminUser.create(apiContext);
38+
await adminUser.setAdminRole(apiContext);
39+
await policy.create(apiContext, DATA_STEWARD_RULES);
40+
await role.create(apiContext, [policy.responseData.name]);
41+
await persona1.create(apiContext, [adminUser.responseData.id]);
42+
await afterAction();
43+
});
44+
45+
test.describe('Create user with persona', async () => {
46+
test('Create user with persona and verify on profile', async ({ page }) => {
47+
await redirectToHomePage(page);
48+
await visitUserListPage(page);
49+
50+
const userWithPersonaName = `pw-user-persona-${uuid()}`;
51+
await addUser(page, {
52+
name: userWithPersonaName,
53+
email: `${userWithPersonaName}@gmail.com`,
54+
password: `User@${uuid()}`,
55+
role: role.responseData.displayName,
56+
personas: [
57+
persona1.responseData.displayName ?? persona1.responseData.name,
58+
],
59+
});
60+
61+
await visitUserProfilePage(page, userWithPersonaName);
62+
const expectedPersonaName =
63+
persona1.responseData.displayName ?? persona1.responseData.name;
64+
await expect(
65+
page
66+
.getByTestId('persona-details-card')
67+
.getByTestId('tag-chip')
68+
.filter({ hasText: expectedPersonaName })
69+
).toBeVisible();
70+
71+
await visitUserListPage(page);
72+
await permanentDeleteUser(
73+
page,
74+
userWithPersonaName,
75+
userWithPersonaName,
76+
false
77+
);
78+
});
79+
});

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

Lines changed: 37 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@
1212
*/
1313
import { test as base, expect, Page } from '@playwright/test';
1414
import {
15-
DATA_CONSUMER_RULES,
16-
DATA_STEWARD_RULES,
17-
EDIT_DESCRIPTION_RULE,
18-
EDIT_GLOSSARY_TERM_RULE,
19-
EDIT_TAGS_RULE,
20-
EDIT_USER_FOR_TEAM_RULES,
21-
OWNER_TEAM_RULES,
22-
VIEW_ALL_RULE,
23-
VIEW_ALL_WITH_MATCH_TAG_CONDITION,
15+
DATA_CONSUMER_RULES,
16+
DATA_STEWARD_RULES,
17+
EDIT_DESCRIPTION_RULE,
18+
EDIT_GLOSSARY_TERM_RULE,
19+
EDIT_TAGS_RULE,
20+
EDIT_USER_FOR_TEAM_RULES,
21+
OWNER_TEAM_RULES,
22+
VIEW_ALL_RULE,
23+
VIEW_ALL_WITH_MATCH_TAG_CONDITION,
2424
} from '../../constant/permission';
2525
import { GlobalSettingOptions } from '../../constant/settings';
2626
import { SidebarItem } from '../../constant/sidebar';
@@ -34,36 +34,36 @@ import { TeamClass } from '../../support/team/TeamClass';
3434
import { UserClass } from '../../support/user/UserClass';
3535
import { performAdminLogin } from '../../utils/admin';
3636
import {
37-
getApiContext,
38-
redirectToHomePage,
39-
toastNotification,
40-
uuid,
41-
visitOwnProfilePage,
37+
getApiContext,
38+
redirectToHomePage,
39+
toastNotification,
40+
uuid,
41+
visitOwnProfilePage,
4242
} from '../../utils/common';
4343
import { addOwner, waitForAllLoadersToDisappear } from '../../utils/entity';
4444
import { settingClick, sidebarClick } from '../../utils/sidebar';
4545
import {
46-
addUser,
47-
checkDataConsumerPermissions,
48-
checkEditOwnerButtonPermission,
49-
checkForUserExistError,
50-
checkStewardPermissions,
51-
checkStewardServicesPermissions,
52-
generateToken,
53-
hardDeleteUserProfilePage,
54-
performUserLogin,
55-
permanentDeleteUser,
56-
resetPassword,
57-
restoreUser,
58-
restoreUserProfilePage,
59-
revokeToken,
60-
settingPageOperationPermissionCheck,
61-
softDeleteUser,
62-
softDeleteUserProfilePage,
63-
updateExpiration,
64-
updateUserDetails,
65-
visitUserListPage,
66-
visitUserProfilePage,
46+
addUser,
47+
checkDataConsumerPermissions,
48+
checkEditOwnerButtonPermission,
49+
checkForUserExistError,
50+
checkStewardPermissions,
51+
checkStewardServicesPermissions,
52+
generateToken,
53+
hardDeleteUserProfilePage,
54+
performUserLogin,
55+
permanentDeleteUser,
56+
resetPassword,
57+
restoreUser,
58+
restoreUserProfilePage,
59+
revokeToken,
60+
settingPageOperationPermissionCheck,
61+
softDeleteUser,
62+
softDeleteUserProfilePage,
63+
updateExpiration,
64+
updateUserDetails,
65+
visitUserListPage,
66+
visitUserProfilePage,
6767
} from '../../utils/user';
6868

6969
const userName = `pw-user-${uuid()}`;
@@ -218,7 +218,7 @@ test.describe('User with Admin Roles', () => {
218218
'/api/v1/users?**include=non-deleted'
219219
);
220220
await adminPage.fill('[data-testid="searchbar"]', '');
221-
await fetchUsers
221+
await fetchUsers;
222222

223223
await restoreUser(
224224
adminPage,
@@ -992,7 +992,7 @@ test.describe('User Profile Dropdown Persona Interactions', () => {
992992
await adminPage.waitForSelector(
993993
'[data-testid="default-persona-select-list"]'
994994
);
995-
995+
996996
await adminPage.waitForSelector('.ant-select-dropdown', {
997997
state: 'visible',
998998
});
@@ -1356,7 +1356,6 @@ base.describe(
13561356
const user = new UserClass();
13571357

13581358
base.beforeAll('Setup pre-requests', async ({ browser }) => {
1359-
13601359
const { apiContext, afterAction } = await performAdminLogin(browser);
13611360

13621361
await user.create(apiContext);

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

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,13 @@ import {
1919
import { VISIT_SERVICE_PAGE_DETAILS } from '../constant/service';
2020
import {
2121
GlobalSettingOptions,
22-
SETTINGS_OPTIONS_PATH,
2322
SETTING_CUSTOM_PROPERTIES_PATH,
23+
SETTINGS_OPTIONS_PATH,
2424
} from '../constant/settings';
2525
import { SidebarItem } from '../constant/sidebar';
2626
import { UserClass } from '../support/user/UserClass';
2727
import {
28+
clickOutside,
2829
descriptionBox,
2930
descriptionBoxReadOnly,
3031
getAuthContext,
@@ -669,11 +670,13 @@ export const addUser = async (
669670
email,
670671
password,
671672
role,
673+
personas,
672674
}: {
673675
name: string;
674676
email: string;
675677
password: string;
676678
role: string;
679+
personas?: string[];
677680
}
678681
) => {
679682
await page.click('[data-testid="add-user"]');
@@ -690,8 +693,37 @@ export const addUser = async (
690693

691694
await page.click('[data-testid="roles-dropdown"] > .ant-select-selector');
692695
await page.getByTestId('roles-dropdown').getByRole('combobox').fill(role);
693-
await page.click('.ant-select-item-option-content');
694-
await page.click('[data-testid="roles-dropdown"] > .ant-select-selector');
696+
await page.waitForSelector('.ant-select-dropdown:visible', {
697+
state: 'visible',
698+
});
699+
const roleOption = page
700+
.locator('.ant-select-dropdown:visible')
701+
.locator('.ant-select-item-option')
702+
.filter({ hasText: role })
703+
.first();
704+
await roleOption.waitFor({ state: 'visible' });
705+
await clickOutside(page);
706+
707+
if (personas?.length) {
708+
await page
709+
.locator('[data-testid="personas-dropdown"] .ant-select-selector')
710+
.click();
711+
await page
712+
.getByTestId('personas-dropdown')
713+
.getByRole('combobox')
714+
.fill(personas[0]);
715+
await page.waitForSelector('.ant-select-dropdown:visible', {
716+
state: 'visible',
717+
});
718+
const personaOption = page
719+
.locator('.ant-select-dropdown:visible')
720+
.locator('.ant-select-item-option')
721+
.filter({ hasText: personas[0] })
722+
.first();
723+
await personaOption.waitFor({ state: 'visible' });
724+
await personaOption.click();
725+
await clickOutside(page);
726+
}
695727

696728
const saveResponse = page.waitForResponse('/api/v1/users');
697729
await page.click('[data-testid="save-user"]');

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

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ import { useEffect, useMemo, useState } from 'react';
2929
import { useTranslation } from 'react-i18next';
3030
import { useLocation } from 'react-router-dom';
3131
import { ReactComponent as IconSync } from '../../../../assets/svg/ic-sync.svg';
32-
import { VALIDATION_MESSAGES } from '../../../../constants/constants';
32+
import {
33+
AGGREGATE_PAGE_SIZE_LARGE,
34+
VALIDATION_MESSAGES,
35+
} from '../../../../constants/constants';
3336
import {
3437
EMAIL_REG_EX,
3538
passwordRegex,
@@ -51,11 +54,16 @@ import {
5154
FormItemLayout,
5255
} from '../../../../interface/FormUtils.interface';
5356
import { generateRandomPwd } from '../../../../rest/auth-API';
57+
import { getAllPersonas } from '../../../../rest/PersonaAPI';
5458
import { getJWTTokenExpiryOptions } from '../../../../utils/BotsUtils';
5559
import { handleSearchFilterOption } from '../../../../utils/CommonUtils';
56-
import { getEntityName } from '../../../../utils/EntityUtils';
60+
import {
61+
getEntityName,
62+
getEntityReferenceListFromEntities,
63+
} from '../../../../utils/EntityUtils';
5764
import { getField } from '../../../../utils/formUtils';
5865
import { showErrorToast } from '../../../../utils/ToastUtils';
66+
import { AsyncSelect } from '../../../common/AsyncSelect/AsyncSelect';
5967
import CopyToClipboardButton from '../../../common/CopyToClipboardButton/CopyToClipboardButton';
6068
import { DomainLabel } from '../../../common/DomainLabel/DomainLabel.component';
6169
import InlineAlert from '../../../common/InlineAlert/InlineAlert';
@@ -125,6 +133,7 @@ const CreateUser = ({
125133
);
126134

127135
const selectedRoles = Form.useWatch('roles', form);
136+
const selectedPersonas = Form.useWatch('personas', form);
128137

129138
const roleOptions = useMemo(() => {
130139
return map(roles, (role) => ({
@@ -133,6 +142,40 @@ const CreateUser = ({
133142
}));
134143
}, [roles]);
135144

145+
const fetchPersonaOptions = async (_searchText: string, page?: number) => {
146+
try {
147+
const params: Record<string, unknown> = {
148+
limit: AGGREGATE_PAGE_SIZE_LARGE,
149+
};
150+
151+
if (page && page > 1) {
152+
params.after = String((page - 1) * AGGREGATE_PAGE_SIZE_LARGE);
153+
}
154+
155+
const response = await getAllPersonas(params);
156+
const personaRefs = getEntityReferenceListFromEntities(
157+
response.data,
158+
EntityType.PERSONA
159+
);
160+
161+
return {
162+
data: personaRefs.map((persona) => ({
163+
label: getEntityName(persona),
164+
value: persona.id,
165+
data: persona,
166+
})),
167+
paging: response.paging,
168+
};
169+
} catch (error) {
170+
showErrorToast(
171+
error as AxiosError,
172+
t('server.entity-fetch-error', { entity: t('label.persona-plural') })
173+
);
174+
175+
return { data: [], paging: {} };
176+
}
177+
};
178+
136179
const generatedPassword = Form.useWatch('generatedPassword', form);
137180
const passwordGenerator = Form.useWatch('passwordGenerator', form);
138181
const password = Form.useWatch('password', form);
@@ -158,6 +201,13 @@ const CreateUser = ({
158201
const isPasswordGenerated =
159202
passwordGenerator === CreatePasswordGenerator.AutomaticGenerate;
160203
const validTeam = compact(selectedTeams).map((team) => team.id);
204+
const validPersonas = selectedPersonas?.map(
205+
(personaId: string) =>
206+
({
207+
id: personaId,
208+
type: EntityType.PERSONA,
209+
} as EntityReference)
210+
);
161211

162212
const { email, displayName, tokenExpiry, confirmPassword, description } =
163213
values;
@@ -168,6 +218,7 @@ const CreateUser = ({
168218
displayName: trim(displayName),
169219
roles: selectedRoles,
170220
teams: validTeam.length ? validTeam : undefined,
221+
personas: validPersonas,
171222
email: email,
172223
isAdmin: isAdmin,
173224
domains: selectedDomain.map((domain) => domain.fullyQualifiedName ?? ''),
@@ -401,6 +452,26 @@ const CreateUser = ({
401452
})}
402453
/>
403454
</Form.Item>
455+
<Form.Item label={t('label.persona-plural')} name="personas">
456+
<AsyncSelect
457+
enableInfiniteScroll
458+
showSearch
459+
api={fetchPersonaOptions}
460+
data-testid="personas-dropdown"
461+
filterOption={(input, option) => {
462+
const label = String(option?.label ?? option?.value ?? '');
463+
464+
return (
465+
!input ||
466+
label.toLowerCase().includes(input.toLowerCase())
467+
);
468+
}}
469+
mode="multiple"
470+
placeholder={t('label.please-select-entity', {
471+
entity: t('label.persona-plural'),
472+
})}
473+
/>
474+
</Form.Item>
404475
</>
405476
)}
406477

0 commit comments

Comments
 (0)