From 64f09e86148db12bf7b50e148b5e7bef4e905c88 Mon Sep 17 00:00:00 2001 From: Shrushti Polekar Date: Sat, 28 Jun 2025 22:52:01 +0530 Subject: [PATCH] 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 --- .../playwright/e2e/Pages/UserDetails.spec.ts | 38 +++++++++++ .../DomainSelectableTreeNew.tsx | 64 ++++++++++++++----- .../domain-selectable.less | 4 ++ 3 files changed, 91 insertions(+), 15 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/UserDetails.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/UserDetails.spec.ts index 3b99a3e5038..9ed5e3169f5 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/UserDetails.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/UserDetails.spec.ts @@ -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, }) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx index df0512974b5..b975e86a765 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx @@ -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 = ({ }) => { const { t } = useTranslation(); const [treeData, setTreeData] = useState([]); - const [domains, setDomains] = useState([]); + const [allDomains, setAllDomains] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isSubmitLoading, setIsSubmitLoading] = useState(false); const [selectedDomains, setSelectedDomains] = useState([]); @@ -64,7 +70,7 @@ const DomainSelectablTreeNew: FC = ({ 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 = ({ 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 = ({ }); setTreeData(convertDomainsToTreeOptions(combinedData, 0, isMultiple)); - setDomains(combinedData); + setAllDomains(combinedData); } catch (error) { showErrorToast(error as AxiosError); } finally { @@ -130,7 +136,7 @@ const DomainSelectablTreeNew: FC = ({ 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 = ({ 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 = ({ return expanded ? : ; }, []); + 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 ; @@ -206,7 +238,7 @@ const DomainSelectablTreeNew: FC = ({ }, [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 = ({ 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 (
@@ -228,6 +260,7 @@ const DomainSelectablTreeNew: FC = ({ className="custom-domain-edit-select" dropdownRender={() => treeContent} dropdownStyle={{ maxHeight: '200px' }} + filterOption={false} maxTagCount={3} maxTagPlaceholder={(omittedValues) => ( @@ -235,9 +268,9 @@ const DomainSelectablTreeNew: FC = ({ )} 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 = ({ tagRender={TagRenderer} value={ selectedDomains - ?.map((domain) => domain.fullyQualifiedName) + ?.map((domain) => domain?.fullyQualifiedName) .filter(Boolean) as string[] } onChange={handleSelectChange} onDropdownVisibleChange={handleDropdownChange} + onSearch={onSearch} />
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/domain-selectable.less b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/domain-selectable.less index a2e27e5b0c1..6357cc914a6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/domain-selectable.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/domain-selectable.less @@ -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; +}