diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchListFilter.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchListFilter.java index 708517424644..0e6c1fc84c92 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchListFilter.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchListFilter.java @@ -27,6 +27,7 @@ public SearchListFilter(Include include) { private static final String FIELD_OWNERS_ID = "owners.id"; private static final String FIELD_CREATED_BY = "createdBy"; private static final String FIELD_DOMAINS_FQN = "domains.fullyQualifiedName"; + private static final String FIELD_DATA_PRODUCTS_FQN = "dataProducts.fullyQualifiedName"; private static final String FIELD_SERVICE_NAME = "service.name"; private static final String FIELD_TEST_CASE_STATUS = "testCaseResult.testCaseStatus"; private static final String FIELD_TEST_PLATFORMS = "testPlatforms"; @@ -217,6 +218,7 @@ private String getTestCaseCondition() { String tier = getQueryParam("tier"); String serviceName = getQueryParam("serviceName"); String dataQualityDimension = getQueryParam("dataQualityDimension"); + String dataProductFqn = getQueryParam("dataProductFqn"); String followedBy = getQueryParam("followedBy"); String columnName = getQueryParam("columnName"); @@ -277,6 +279,13 @@ private String getTestCaseCondition() { conditions.add( getDataQualityDimensionCondition(dataQualityDimension, "dataQualityDimension")); + if (dataProductFqn != null) { + conditions.add( + String.format( + "{\"term\": {\"%s\": \"%s\"}}", + FIELD_DATA_PRODUCTS_FQN, escapeDoubleQuotes(dataProductFqn))); + } + if (followedBy != null) { conditions.add( String.format("{\"term\": {\"%s\": \"%s\"}}", FIELD_FOLLOWERS_KEYWORD, followedBy)); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/indexes/TestCaseIndex.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/indexes/TestCaseIndex.java index bc93c329eb48..01847ab81738 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/search/indexes/TestCaseIndex.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/indexes/TestCaseIndex.java @@ -93,6 +93,13 @@ private void setParentRelationships(Map doc, TestCase testCase) && linkedTable.getCertification() != null) { doc.put("certification", linkedTable.getCertification()); } + + if (nullOrEmpty(testCase.getDataProducts()) + && linkedTable != null + && !nullOrEmpty(linkedTable.getDataProducts())) { + doc.put( + Entity.FIELD_DATA_PRODUCTS, getEntitiesWithDisplayName(linkedTable.getDataProducts())); + } } private EntityInterface denormalizeTestSuiteParents(Map doc, TestCase testCase) { diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/indexes/TestSuiteIndex.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/indexes/TestSuiteIndex.java index 3df4904b2aca..c0af71e95c11 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/search/indexes/TestSuiteIndex.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/indexes/TestSuiteIndex.java @@ -69,7 +69,8 @@ static Table addTestSuiteParentEntityRelations( EntityReference testSuiteRef, Map doc) { if (testSuiteRef.getType().equals(Entity.TABLE)) { try { - Table table = Entity.getEntity(testSuiteRef, "domains,certification", Include.ALL); + Table table = + Entity.getEntity(testSuiteRef, "domains,certification,dataProducts", Include.ALL); doc.put("table", table.getEntityReference()); doc.put("database", table.getDatabase()); doc.put("databaseSchema", table.getDatabaseSchema()); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/DataQuality.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/DataQuality.interface.ts index 094f6c590efb..81e0d0bd03f8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/DataQuality.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/DataQuality.interface.ts @@ -44,6 +44,7 @@ export type TestCaseSearchParams = { tags?: string; serviceName?: string; dataQualityDimension?: string; + dataProductFqn?: string; }; export type DataQualityPageParams = TestCaseSearchParams & { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/IncidentManagerPageHeader/IncidentManagerPageHeader.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/IncidentManagerPageHeader/IncidentManagerPageHeader.component.tsx index 55f7ca063af2..55a8c1bddf45 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/IncidentManagerPageHeader/IncidentManagerPageHeader.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/IncidentManagerPageHeader/IncidentManagerPageHeader.component.tsx @@ -14,7 +14,7 @@ import { Typography } from '@openmetadata/ui-core-components'; import { Divider, Skeleton, Space, Tooltip } from 'antd'; import { AxiosError } from 'axios'; import { compare } from 'fast-json-patch'; -import { first, isUndefined, last } from 'lodash'; +import { first, isEmpty, isUndefined, last } from 'lodash'; import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; @@ -39,6 +39,7 @@ import { transitionIncident, updateTestCaseIncidentById, } from '../../../../rest/incidentManagerAPI'; +import { updateTestCaseById } from '../../../../rest/testAPI'; import { getColumnNameFromEntityLink, getEntityName, @@ -53,6 +54,7 @@ import { getTaskDetailPath as getNewTaskDetailPath } from '../../../../utils/Tas import { showErrorToast } from '../../../../utils/ToastUtils'; import { useRequiredParams } from '../../../../utils/useRequiredParams'; import { useActivityFeedProvider } from '../../../ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; +import { DomainLabel } from '../../../common/DomainLabel/DomainLabel.component'; import { OwnerLabel } from '../../../common/OwnerLabel/OwnerLabel.component'; import { ProfilerTabPath } from '../../../Database/Profiler/ProfilerDashboard/profilerDashboard.interface'; import Severity from '../Severity/Severity.component'; @@ -71,7 +73,11 @@ const IncidentManagerPageHeader = ({ const [testCaseStatusData, setTestCaseStatusData] = useState(); const [isLoading, setIsLoading] = useState(true); - const { testCase: testCaseData, testCasePermission } = useTestCaseStore(); + const { + testCase: testCaseData, + testCasePermission, + setTestCase, + } = useTestCaseStore(); const { dimensionKey } = useRequiredParams<{ fqn: string; @@ -223,6 +229,30 @@ const IncidentManagerPageHeader = ({ } }, [testCaseData]); + const handleDomainUpdate = async ( + selectedDomain: EntityReference | EntityReference[] + ) => { + if (!testCaseData) { + return; + } + + const domains = Array.isArray(selectedDomain) + ? selectedDomain + : isEmpty(selectedDomain) + ? [] + : [selectedDomain]; + + const patch = compare(testCaseData, { ...testCaseData, domains }); + if (patch.length && testCaseData.id) { + try { + const updated = await updateTestCaseById(testCaseData.id, patch); + setTestCase(updated); + } catch (error) { + showErrorToast(error as AxiosError); + } + } + }; + const { hasEditStatusPermission, hasEditOwnerPermission } = useMemo(() => { return isVersionPage ? { @@ -358,6 +388,19 @@ const IncidentManagerPageHeader = ({ owners={testCaseData?.owners ?? ownerRef} onUpdate={onOwnerUpdate} /> + + {!isVersionPage && statusDetails} {tableFqn && ( <> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.component.tsx index 5a5b599e1c8a..21512faf7c06 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.component.tsx @@ -30,6 +30,7 @@ import { useParams } from 'react-router-dom'; import { ReactComponent as StarIcon } from '../../../../assets/svg/ic-suggestions.svg'; import { EntityField } from '../../../../constants/Feeds.constants'; import { TagSource } from '../../../../generated/api/domains/createDataProduct'; +import { DataProduct } from '../../../../generated/entity/domains/dataProduct'; import { Operation } from '../../../../generated/entity/policies/policy'; import { ChangeDescription, @@ -57,6 +58,7 @@ import DescriptionV1 from '../../../common/EntityDescription/DescriptionV1'; import { EditIconButton } from '../../../common/IconButtons/EditIconButton'; import TestSummary from '../../../Database/Profiler/TestSummary/TestSummary'; import SchemaEditor from '../../../Database/SchemaEditor/SchemaEditor'; +import DataProductsContainer from '../../../DataProducts/DataProductsContainer/DataProductsContainer.component'; import TagsContainerV2 from '../../../Tag/TagsContainerV2/TagsContainerV2'; import { DisplayType } from '../../../Tag/TagsViewer/TagsViewer.interface'; import EditTestCaseModal from '../../AddDataQualityTest/EditTestCaseModal'; @@ -204,6 +206,37 @@ const TestCaseResultTab = () => { } }; + const handleDataProductsSave = useCallback( + async (dataProducts: DataProduct[]) => { + if (!testCaseData) { + return; + } + + const updatedDataProducts = dataProducts.map((dp) => ({ + id: dp.id ?? '', + type: 'dataProduct', + name: dp.name, + fullyQualifiedName: dp.fullyQualifiedName, + displayName: dp.displayName, + })); + + const patch = compare(testCaseData, { + ...testCaseData, + dataProducts: updatedDataProducts, + }); + + if (patch.length) { + try { + const res = await updateTestCaseById(testCaseData.id ?? '', patch); + setTestCase(res); + } catch (error) { + showErrorToast(error as AxiosError); + } + } + }, + [testCaseData, setTestCase] + ); + const handleDescriptionChange = useCallback( async (description: string) => { if (testCaseData) { @@ -589,6 +622,16 @@ const TestCaseResultTab = () => { onSelectionChange={handleTagSelection} /> +
+ +
)} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.test.tsx index c65288135ef1..4654f96a00d3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.test.tsx @@ -108,6 +108,12 @@ jest.mock('../../../Database/SchemaEditor/SchemaEditor', () => { jest.mock('../../../Database/Profiler/TestSummary/TestSummary', () => { return jest.fn().mockImplementation(() =>
TestSummary
); }); +jest.mock( + '../../../DataProducts/DataProductsContainer/DataProductsContainer.component', + () => { + return jest.fn().mockImplementation(() =>
DataProductsContainer
); + } +); jest.mock('../../AddDataQualityTest/EditTestCaseModal', () => { return jest.fn().mockImplementation(({ onUpdate, testCase, onCancel }) => (
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/TestCases/TestCases.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/TestCases/TestCases.component.tsx index c988df695f78..4814fa19b832 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/TestCases/TestCases.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/TestCases/TestCases.component.tsx @@ -106,6 +106,9 @@ export const TestCases = () => { const [tagOptions, setTagOptions] = useState([]); const [tierOptions, setTierOptions] = useState([]); const [serviceOptions, setServiceOptions] = useState([]); + const [dataProductOptions, setDataProductOptions] = useState< + DefaultOptionType[] + >([]); const [sortOptions, setSortOptions] = useState(DEFAULT_SORT_ORDER); @@ -393,6 +396,44 @@ export const TestCases = () => { } }; + const fetchDataProductOptions = async (search = WILD_CARD_CHAR) => { + setIsOptionsLoading(true); + try { + const response = await searchQuery({ + query: search === WILD_CARD_CHAR ? search : `*${search}*`, + pageNumber: 1, + pageSize: PAGE_SIZE_BASE, + searchIndex: SearchIndex.DATA_PRODUCT, + fetchSource: true, + includeFields: ['name', 'fullyQualifiedName', 'displayName'], + }); + + const options = response.hits.hits.map((hit) => { + return { + label: ( + + + {hit._source.fullyQualifiedName} + + + {getEntityName(hit._source)} + + + ), + value: hit._source.fullyQualifiedName, + }; + }); + setDataProductOptions(options); + } catch { + setDataProductOptions([]); + } finally { + setIsOptionsLoading(false); + } + }; + const getInitialOptions = (key: string, isLengthCheck = false) => { switch (key) { case TEST_CASE_FILTERS.tier: @@ -412,6 +453,12 @@ export const TestCases = () => { break; + case TEST_CASE_FILTERS.dataProduct: + (isEmpty(dataProductOptions) || !isLengthCheck) && + fetchDataProductOptions(); + + break; + default: break; } @@ -512,6 +559,11 @@ export const TestCases = () => { [fetchServiceOptions] ); + const debounceFetchDataProductOptions = useCallback( + debounce(fetchDataProductOptions, 1000), + [fetchDataProductOptions] + ); + const getTestCases = () => { if (!isEmpty(params) || !isEmpty(selectedFilter)) { const updatedValue = uniq([...selectedFilter, ...Object.keys(params)]); @@ -723,6 +775,23 @@ export const TestCases = () => { /> )} + {selectedFilter.includes(TEST_CASE_FILTERS.dataProduct) && ( + +