mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-28 02:13:09 +00:00
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:
parent
6077e7348b
commit
64f09e8614
@ -12,6 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect, Page, test as base } from '@playwright/test';
|
import { expect, Page, test as base } from '@playwright/test';
|
||||||
|
import { Domain } from '../../support/domain/Domain';
|
||||||
import { TeamClass } from '../../support/team/TeamClass';
|
import { TeamClass } from '../../support/team/TeamClass';
|
||||||
import { AdminClass } from '../../support/user/AdminClass';
|
import { AdminClass } from '../../support/user/AdminClass';
|
||||||
import { UserClass } from '../../support/user/UserClass';
|
import { UserClass } from '../../support/user/UserClass';
|
||||||
@ -22,6 +23,14 @@ import { redirectToUserPage } from '../../utils/userDetails';
|
|||||||
const user1 = new UserClass();
|
const user1 = new UserClass();
|
||||||
const user2 = new UserClass();
|
const user2 = new UserClass();
|
||||||
const admin = new AdminClass();
|
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({
|
const team = new TeamClass({
|
||||||
name: `a-new-team-${uuid()}`,
|
name: `a-new-team-${uuid()}`,
|
||||||
displayName: `A New Team ${uuid()}`,
|
displayName: `A New Team ${uuid()}`,
|
||||||
@ -56,6 +65,7 @@ test.describe('User with different Roles', () => {
|
|||||||
await user2.create(apiContext);
|
await user2.create(apiContext);
|
||||||
|
|
||||||
await team.create(apiContext);
|
await team.create(apiContext);
|
||||||
|
await domain.create(apiContext);
|
||||||
|
|
||||||
await afterAction();
|
await afterAction();
|
||||||
});
|
});
|
||||||
@ -67,6 +77,7 @@ test.describe('User with different Roles', () => {
|
|||||||
await user2.delete(apiContext);
|
await user2.delete(apiContext);
|
||||||
|
|
||||||
await team.delete(apiContext);
|
await team.delete(apiContext);
|
||||||
|
await domain.delete(apiContext);
|
||||||
|
|
||||||
await afterAction();
|
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 ({
|
test('Admin user can get all the roles hierarchy and edit roles', async ({
|
||||||
adminPage,
|
adminPage,
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
import { Button, Empty, Select, Space, Tree } from 'antd';
|
import { Button, Empty, Select, Space, Tree } from 'antd';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
import React, {
|
import React, {
|
||||||
FC,
|
FC,
|
||||||
Key,
|
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 IconRight } from '../../../assets/svg/ic-arrow-right.svg';
|
||||||
import { ReactComponent as ClosePopoverIcon } from '../../../assets/svg/ic-popover-close.svg';
|
import { ReactComponent as ClosePopoverIcon } from '../../../assets/svg/ic-popover-close.svg';
|
||||||
import { ReactComponent as SavePopoverIcon } from '../../../assets/svg/ic-popover-save.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 { EntityType } from '../../../enums/entity.enum';
|
||||||
import { Domain } from '../../../generated/entity/domains/domain';
|
import { Domain } from '../../../generated/entity/domains/domain';
|
||||||
import { EntityReference } from '../../../generated/tests/testCase';
|
import { EntityReference } from '../../../generated/tests/testCase';
|
||||||
import { listDomainHierarchy } from '../../../rest/domainAPI';
|
import { listDomainHierarchy, searchDomains } from '../../../rest/domainAPI';
|
||||||
import {
|
import {
|
||||||
convertDomainsToTreeOptions,
|
convertDomainsToTreeOptions,
|
||||||
isDomainExist,
|
isDomainExist,
|
||||||
} from '../../../utils/DomainUtils';
|
} from '../../../utils/DomainUtils';
|
||||||
import { getEntityReferenceFromEntity } from '../../../utils/EntityUtils';
|
import { getEntityReferenceFromEntity } from '../../../utils/EntityUtils';
|
||||||
import { findItemByFqn } from '../../../utils/GlossaryUtils';
|
import { findItemByFqn } from '../../../utils/GlossaryUtils';
|
||||||
|
import {
|
||||||
|
escapeESReservedCharacters,
|
||||||
|
getEncodedFqn,
|
||||||
|
} from '../../../utils/StringsUtils';
|
||||||
import { showErrorToast } from '../../../utils/ToastUtils';
|
import { showErrorToast } from '../../../utils/ToastUtils';
|
||||||
import Loader from '../Loader/Loader';
|
import Loader from '../Loader/Loader';
|
||||||
import { TagRenderer } from '../TagRenderer/TagRenderer';
|
import { TagRenderer } from '../TagRenderer/TagRenderer';
|
||||||
@ -56,7 +62,7 @@ const DomainSelectablTreeNew: FC<DomainSelectableTreeProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [treeData, setTreeData] = useState<TreeListItem[]>([]);
|
const [treeData, setTreeData] = useState<TreeListItem[]>([]);
|
||||||
const [domains, setDomains] = useState<Domain[]>([]);
|
const [allDomains, setAllDomains] = useState<Domain[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
const [isSubmitLoading, setIsSubmitLoading] = useState(false);
|
const [isSubmitLoading, setIsSubmitLoading] = useState(false);
|
||||||
const [selectedDomains, setSelectedDomains] = useState<Domain[]>([]);
|
const [selectedDomains, setSelectedDomains] = useState<Domain[]>([]);
|
||||||
@ -64,7 +70,7 @@ const DomainSelectablTreeNew: FC<DomainSelectableTreeProps> = ({
|
|||||||
|
|
||||||
const handleMultiDomainSave = async () => {
|
const handleMultiDomainSave = async () => {
|
||||||
const selectedFqns = selectedDomains
|
const selectedFqns = selectedDomains
|
||||||
.map((domain) => domain.fullyQualifiedName)
|
.map((domain) => domain?.fullyQualifiedName)
|
||||||
.sort((a, b) => (a ?? '').localeCompare(b ?? ''));
|
.sort((a, b) => (a ?? '').localeCompare(b ?? ''));
|
||||||
const initialFqns = (value as string[]).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];
|
const combinedData = [...data.data];
|
||||||
initialDomains?.forEach((selectedDomain) => {
|
initialDomains?.forEach((selectedDomain) => {
|
||||||
const exists = combinedData.some((domain: Domain) =>
|
const exists = combinedData.some((domain: Domain) =>
|
||||||
isDomainExist(domain, selectedDomain.fullyQualifiedName ?? '')
|
isDomainExist(domain, selectedDomain?.fullyQualifiedName ?? '')
|
||||||
);
|
);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
combinedData.push(selectedDomain as unknown as Domain);
|
combinedData.push(selectedDomain as unknown as Domain);
|
||||||
@ -117,7 +123,7 @@ const DomainSelectablTreeNew: FC<DomainSelectableTreeProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
setTreeData(convertDomainsToTreeOptions(combinedData, 0, isMultiple));
|
setTreeData(convertDomainsToTreeOptions(combinedData, 0, isMultiple));
|
||||||
setDomains(combinedData);
|
setAllDomains(combinedData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showErrorToast(error as AxiosError);
|
showErrorToast(error as AxiosError);
|
||||||
} finally {
|
} finally {
|
||||||
@ -130,7 +136,7 @@ const DomainSelectablTreeNew: FC<DomainSelectableTreeProps> = ({
|
|||||||
const selectedData = [];
|
const selectedData = [];
|
||||||
for (const item of selectedKeys) {
|
for (const item of selectedKeys) {
|
||||||
selectedData.push(
|
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 = [];
|
const selectedData = [];
|
||||||
for (const item of checked) {
|
for (const item of checked) {
|
||||||
selectedData.push(
|
selectedData.push(
|
||||||
findItemByFqn(domains, item as string, false) as Domain
|
findItemByFqn(allDomains, item as string, false) as Domain
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelectedDomains(selectedData);
|
setSelectedDomains(selectedData);
|
||||||
} else {
|
} else {
|
||||||
const selected = checked.checked.map(
|
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);
|
setSelectedDomains(selected);
|
||||||
@ -163,6 +169,32 @@ const DomainSelectablTreeNew: FC<DomainSelectableTreeProps> = ({
|
|||||||
return expanded ? <IconDown /> : <IconRight />;
|
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(() => {
|
const treeContent = useMemo(() => {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <Loader />;
|
return <Loader />;
|
||||||
@ -206,7 +238,7 @@ const DomainSelectablTreeNew: FC<DomainSelectableTreeProps> = ({
|
|||||||
}, [visible]);
|
}, [visible]);
|
||||||
const handleSelectChange = (selectedFqns: string[]) => {
|
const handleSelectChange = (selectedFqns: string[]) => {
|
||||||
const selectedData = selectedFqns.map(
|
const selectedData = selectedFqns.map(
|
||||||
(fqn) => findItemByFqn(domains, fqn, false) as Domain
|
(fqn) => findItemByFqn(allDomains, fqn, false) as Domain
|
||||||
);
|
);
|
||||||
setSelectedDomains(selectedData);
|
setSelectedDomains(selectedData);
|
||||||
};
|
};
|
||||||
@ -215,11 +247,11 @@ const DomainSelectablTreeNew: FC<DomainSelectableTreeProps> = ({
|
|||||||
setSelectedDomains(initialDomains as unknown as Domain[]);
|
setSelectedDomains(initialDomains as unknown as Domain[]);
|
||||||
} else if (value) {
|
} else if (value) {
|
||||||
const selectedData = (value as string[]).map(
|
const selectedData = (value as string[]).map(
|
||||||
(fqn) => findItemByFqn(domains, fqn, false) as Domain
|
(fqn) => findItemByFqn(allDomains, fqn, false) as Domain
|
||||||
);
|
);
|
||||||
setSelectedDomains(selectedData);
|
setSelectedDomains(selectedData);
|
||||||
}
|
}
|
||||||
}, [initialDomains, value, domains]);
|
}, [initialDomains, value, allDomains]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-testid="domain-selectable-tree" style={{ width: '339px' }}>
|
<div data-testid="domain-selectable-tree" style={{ width: '339px' }}>
|
||||||
@ -228,6 +260,7 @@ const DomainSelectablTreeNew: FC<DomainSelectableTreeProps> = ({
|
|||||||
className="custom-domain-edit-select"
|
className="custom-domain-edit-select"
|
||||||
dropdownRender={() => treeContent}
|
dropdownRender={() => treeContent}
|
||||||
dropdownStyle={{ maxHeight: '200px' }}
|
dropdownStyle={{ maxHeight: '200px' }}
|
||||||
|
filterOption={false}
|
||||||
maxTagCount={3}
|
maxTagCount={3}
|
||||||
maxTagPlaceholder={(omittedValues) => (
|
maxTagPlaceholder={(omittedValues) => (
|
||||||
<span className="max-tag-text">
|
<span className="max-tag-text">
|
||||||
@ -235,9 +268,9 @@ const DomainSelectablTreeNew: FC<DomainSelectableTreeProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
mode={isMultiple ? 'multiple' : undefined}
|
mode={isMultiple ? 'multiple' : undefined}
|
||||||
options={domains.map((domain) => ({
|
options={allDomains.map((domain) => ({
|
||||||
value: domain.fullyQualifiedName,
|
value: domain?.fullyQualifiedName,
|
||||||
label: domain.name,
|
label: domain?.name,
|
||||||
}))}
|
}))}
|
||||||
placeholder="Select a domain"
|
placeholder="Select a domain"
|
||||||
popupClassName="domain-custom-dropdown-class"
|
popupClassName="domain-custom-dropdown-class"
|
||||||
@ -245,11 +278,12 @@ const DomainSelectablTreeNew: FC<DomainSelectableTreeProps> = ({
|
|||||||
tagRender={TagRenderer}
|
tagRender={TagRenderer}
|
||||||
value={
|
value={
|
||||||
selectedDomains
|
selectedDomains
|
||||||
?.map((domain) => domain.fullyQualifiedName)
|
?.map((domain) => domain?.fullyQualifiedName)
|
||||||
.filter(Boolean) as string[]
|
.filter(Boolean) as string[]
|
||||||
}
|
}
|
||||||
onChange={handleSelectChange}
|
onChange={handleSelectChange}
|
||||||
onDropdownVisibleChange={handleDropdownChange}
|
onDropdownVisibleChange={handleDropdownChange}
|
||||||
|
onSearch={onSearch}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Space className="d-flex" size={8}>
|
<Space className="d-flex" size={8}>
|
||||||
|
@ -40,3 +40,7 @@
|
|||||||
.all-domain-container.selected-node {
|
.all-domain-container.selected-node {
|
||||||
background-color: @primary-1;
|
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;
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user