Fix(ui) : search functionality for domain edit in user profile (#22005)

* support search for domain edit in user profile

* handle search for persona edit in profile page

* fix domain update issue

* added test
This commit is contained in:
Shrushti Polekar 2025-06-28 22:52:01 +05:30 committed by GitHub
parent 6077e7348b
commit 64f09e8614
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 91 additions and 15 deletions

View File

@ -12,6 +12,7 @@
*/
import { expect, Page, test as base } from '@playwright/test';
import { Domain } from '../../support/domain/Domain';
import { TeamClass } from '../../support/team/TeamClass';
import { AdminClass } from '../../support/user/AdminClass';
import { UserClass } from '../../support/user/UserClass';
@ -22,6 +23,14 @@ import { redirectToUserPage } from '../../utils/userDetails';
const user1 = new UserClass();
const user2 = new UserClass();
const admin = new AdminClass();
const domain = new Domain({
name: `PW%domain`,
displayName: `PWDomain`,
description: 'playwright domain description',
domainType: 'Aggregate',
// eslint-disable-next-line no-useless-escape
fullyQualifiedName: `PW%domain`,
});
const team = new TeamClass({
name: `a-new-team-${uuid()}`,
displayName: `A New Team ${uuid()}`,
@ -56,6 +65,7 @@ test.describe('User with different Roles', () => {
await user2.create(apiContext);
await team.create(apiContext);
await domain.create(apiContext);
await afterAction();
});
@ -67,6 +77,7 @@ test.describe('User with different Roles', () => {
await user2.delete(apiContext);
await team.delete(apiContext);
await domain.delete(apiContext);
await afterAction();
});
@ -98,6 +109,33 @@ test.describe('User with different Roles', () => {
);
});
test('User can search for a domain', async ({ adminPage }) => {
await redirectToUserPage(adminPage);
await expect(adminPage.getByTestId('edit-domains')).toBeVisible();
await adminPage.getByTestId('edit-domains').click();
await expect(adminPage.locator('.custom-domain-edit-select')).toBeVisible();
await adminPage.locator('.custom-domain-edit-select').click();
const searchPromise = adminPage.waitForResponse('/api/v1/search/query?q=*');
await adminPage
.locator('.custom-domain-edit-select .ant-select-selection-search-input')
.fill('PWDomain');
await searchPromise;
await adminPage.waitForSelector('.domain-custom-dropdown-class', {
state: 'visible',
});
await expect(
adminPage.locator('.domain-custom-dropdown-class')
).toContainText('PWDomain');
});
test('Admin user can get all the roles hierarchy and edit roles', async ({
adminPage,
}) => {

View File

@ -12,6 +12,7 @@
*/
import { Button, Empty, Select, Space, Tree } from 'antd';
import { AxiosError } from 'axios';
import { debounce } from 'lodash';
import React, {
FC,
Key,
@ -25,16 +26,21 @@ import { ReactComponent as IconDown } from '../../../assets/svg/ic-arrow-down.sv
import { ReactComponent as IconRight } from '../../../assets/svg/ic-arrow-right.svg';
import { ReactComponent as ClosePopoverIcon } from '../../../assets/svg/ic-popover-close.svg';
import { ReactComponent as SavePopoverIcon } from '../../../assets/svg/ic-popover-save.svg';
import { DEBOUNCE_TIMEOUT } from '../../../constants/Lineage.constants';
import { EntityType } from '../../../enums/entity.enum';
import { Domain } from '../../../generated/entity/domains/domain';
import { EntityReference } from '../../../generated/tests/testCase';
import { listDomainHierarchy } from '../../../rest/domainAPI';
import { listDomainHierarchy, searchDomains } from '../../../rest/domainAPI';
import {
convertDomainsToTreeOptions,
isDomainExist,
} from '../../../utils/DomainUtils';
import { getEntityReferenceFromEntity } from '../../../utils/EntityUtils';
import { findItemByFqn } from '../../../utils/GlossaryUtils';
import {
escapeESReservedCharacters,
getEncodedFqn,
} from '../../../utils/StringsUtils';
import { showErrorToast } from '../../../utils/ToastUtils';
import Loader from '../Loader/Loader';
import { TagRenderer } from '../TagRenderer/TagRenderer';
@ -56,7 +62,7 @@ const DomainSelectablTreeNew: FC<DomainSelectableTreeProps> = ({
}) => {
const { t } = useTranslation();
const [treeData, setTreeData] = useState<TreeListItem[]>([]);
const [domains, setDomains] = useState<Domain[]>([]);
const [allDomains, setAllDomains] = useState<Domain[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isSubmitLoading, setIsSubmitLoading] = useState(false);
const [selectedDomains, setSelectedDomains] = useState<Domain[]>([]);
@ -64,7 +70,7 @@ const DomainSelectablTreeNew: FC<DomainSelectableTreeProps> = ({
const handleMultiDomainSave = async () => {
const selectedFqns = selectedDomains
.map((domain) => domain.fullyQualifiedName)
.map((domain) => domain?.fullyQualifiedName)
.sort((a, b) => (a ?? '').localeCompare(b ?? ''));
const initialFqns = (value as string[]).sort((a, b) => a.localeCompare(b));
@ -109,7 +115,7 @@ const DomainSelectablTreeNew: FC<DomainSelectableTreeProps> = ({
const combinedData = [...data.data];
initialDomains?.forEach((selectedDomain) => {
const exists = combinedData.some((domain: Domain) =>
isDomainExist(domain, selectedDomain.fullyQualifiedName ?? '')
isDomainExist(domain, selectedDomain?.fullyQualifiedName ?? '')
);
if (!exists) {
combinedData.push(selectedDomain as unknown as Domain);
@ -117,7 +123,7 @@ const DomainSelectablTreeNew: FC<DomainSelectableTreeProps> = ({
});
setTreeData(convertDomainsToTreeOptions(combinedData, 0, isMultiple));
setDomains(combinedData);
setAllDomains(combinedData);
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
@ -130,7 +136,7 @@ const DomainSelectablTreeNew: FC<DomainSelectableTreeProps> = ({
const selectedData = [];
for (const item of selectedKeys) {
selectedData.push(
findItemByFqn(domains, item as string, false) as Domain
findItemByFqn(allDomains, item as string, false) as Domain
);
}
@ -145,14 +151,14 @@ const DomainSelectablTreeNew: FC<DomainSelectableTreeProps> = ({
const selectedData = [];
for (const item of checked) {
selectedData.push(
findItemByFqn(domains, item as string, false) as Domain
findItemByFqn(allDomains, item as string, false) as Domain
);
}
setSelectedDomains(selectedData);
} else {
const selected = checked.checked.map(
(item) => findItemByFqn(domains, item as string, false) as Domain
(item) => findItemByFqn(allDomains, item as string, false) as Domain
);
setSelectedDomains(selected);
@ -163,6 +169,32 @@ const DomainSelectablTreeNew: FC<DomainSelectableTreeProps> = ({
return expanded ? <IconDown /> : <IconRight />;
}, []);
const onSearch = debounce(async (value: string) => {
setSearchTerm(value);
if (value) {
try {
setIsLoading(true);
const encodedValue = getEncodedFqn(escapeESReservedCharacters(value));
const results: Domain[] = await searchDomains(encodedValue);
const updatedTreeData = convertDomainsToTreeOptions(
results,
0,
isMultiple
);
setTreeData(updatedTreeData);
} finally {
setIsLoading(false);
}
} else {
const updatedTreeData = convertDomainsToTreeOptions(
allDomains,
0,
isMultiple
);
setTreeData(updatedTreeData);
}
}, DEBOUNCE_TIMEOUT);
const treeContent = useMemo(() => {
if (isLoading) {
return <Loader />;
@ -206,7 +238,7 @@ const DomainSelectablTreeNew: FC<DomainSelectableTreeProps> = ({
}, [visible]);
const handleSelectChange = (selectedFqns: string[]) => {
const selectedData = selectedFqns.map(
(fqn) => findItemByFqn(domains, fqn, false) as Domain
(fqn) => findItemByFqn(allDomains, fqn, false) as Domain
);
setSelectedDomains(selectedData);
};
@ -215,11 +247,11 @@ const DomainSelectablTreeNew: FC<DomainSelectableTreeProps> = ({
setSelectedDomains(initialDomains as unknown as Domain[]);
} else if (value) {
const selectedData = (value as string[]).map(
(fqn) => findItemByFqn(domains, fqn, false) as Domain
(fqn) => findItemByFqn(allDomains, fqn, false) as Domain
);
setSelectedDomains(selectedData);
}
}, [initialDomains, value, domains]);
}, [initialDomains, value, allDomains]);
return (
<div data-testid="domain-selectable-tree" style={{ width: '339px' }}>
@ -228,6 +260,7 @@ const DomainSelectablTreeNew: FC<DomainSelectableTreeProps> = ({
className="custom-domain-edit-select"
dropdownRender={() => treeContent}
dropdownStyle={{ maxHeight: '200px' }}
filterOption={false}
maxTagCount={3}
maxTagPlaceholder={(omittedValues) => (
<span className="max-tag-text">
@ -235,9 +268,9 @@ const DomainSelectablTreeNew: FC<DomainSelectableTreeProps> = ({
</span>
)}
mode={isMultiple ? 'multiple' : undefined}
options={domains.map((domain) => ({
value: domain.fullyQualifiedName,
label: domain.name,
options={allDomains.map((domain) => ({
value: domain?.fullyQualifiedName,
label: domain?.name,
}))}
placeholder="Select a domain"
popupClassName="domain-custom-dropdown-class"
@ -245,11 +278,12 @@ const DomainSelectablTreeNew: FC<DomainSelectableTreeProps> = ({
tagRender={TagRenderer}
value={
selectedDomains
?.map((domain) => domain.fullyQualifiedName)
?.map((domain) => domain?.fullyQualifiedName)
.filter(Boolean) as string[]
}
onChange={handleSelectChange}
onDropdownVisibleChange={handleDropdownChange}
onSearch={onSearch}
/>
</div>
<Space className="d-flex" size={8}>

View File

@ -40,3 +40,7 @@
.all-domain-container.selected-node {
background-color: @primary-1;
}
.custom-domain-edit-select .ant-select-selection-placeholder,
.custom-domain-edit-select .ant-select-selection-search-input::placeholder {
padding-left: @size-xs;
}