From 09b37d28f2d22d6bdb4416f8ee56762f04e89d2a Mon Sep 17 00:00:00 2001 From: Shailesh Parmar Date: Thu, 7 Jul 2022 00:52:30 +0530 Subject: [PATCH] Fix #5653 UI: Added Lineage support for topic and mlModal (#5888) --- .../EntityLineage/EntityLineage.component.tsx | 20 ++- .../Entitylineage.component.test.tsx | 4 +- .../MlModelDetail.component.test.tsx | 30 +++- .../MlModelDetail/MlModelDetail.component.tsx | 47 +++++- .../TasksDAGView/TasksDAGView.test.tsx | 4 +- .../components/TasksDAGView/TasksDAGView.tsx | 4 +- .../TopicDetails/TopicDetails.component.tsx | 33 +++- .../TopicDetails/TopicDetails.interface.ts | 20 ++- .../TopicDetails/TopicDetails.test.tsx | 29 +++- .../ui/src/constants/Lineage.constants.ts | 2 + .../MlModelPage/MlModelPage.component.tsx | 150 ++++++++++++++++++ .../TopicDetailsPage.component.tsx | 144 ++++++++++++++++- .../ui/src/utils/EntityLineageUtils.tsx | 4 +- .../ui/src/utils/MlModelDetailsUtils.ts | 10 +- .../main/resources/ui/src/utils/SvgUtils.tsx | 2 +- .../resources/ui/src/utils/TableUtils.tsx | 5 + .../ui/src/utils/TopicDetailsUtils.ts | 11 +- 17 files changed, 491 insertions(+), 28 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/EntityLineage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/EntityLineage.component.tsx index f3b03176e00..1672223ba3f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/EntityLineage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/EntityLineage.component.tsx @@ -66,8 +66,8 @@ import { getColumnType, getDataLabel, getDeletedLineagePlaceholder, - getLayoutedElementsV1, - getLineageDataV1, + getLayoutedElements, + getLineageData, getModalBodyText, getNodeRemoveButton, getUniqueFlowElements, @@ -461,7 +461,7 @@ const Entitylineage: FunctionComponent = ({ setConfirmDelete(false); }; - const setElementsHandleV1 = (data: EntityLineage) => { + const setElementsHandle = (data: EntityLineage) => { let uniqueElements: CustomeElement = { node: [], edge: [], @@ -471,7 +471,7 @@ const Entitylineage: FunctionComponent = ({ edges: [...(edges || [])], }; if (!isEmpty(data)) { - const graphElements = getLineageDataV1( + const graphElements = getLineageData( data, selectNodeHandler, loadNodeHandler, @@ -491,7 +491,7 @@ const Entitylineage: FunctionComponent = ({ node: getUniqueFlowElements(graphElements.node) as Node[], edge: getUniqueFlowElements(graphElements.edge) as Edge[], }; - const { node, edge } = getLayoutedElementsV1(uniqueElements); + const { node, edge } = getLayoutedElements(uniqueElements); setNodes(node); setEdges(edge); @@ -1288,7 +1288,7 @@ const Entitylineage: FunctionComponent = ({ useEffect(() => { if (!deleted && !isEmpty(updatedLineageData)) { - setElementsHandleV1(updatedLineageData); + setElementsHandle(updatedLineageData); } }, [isNodeLoading, isEditMode]); @@ -1322,9 +1322,13 @@ const Entitylineage: FunctionComponent = ({ }, [selectedEdge, confirmDelete]); useEffect(() => { - if (!isEmpty(entityLineage) && !deleted) { + if ( + !isEmpty(entityLineage) && + !isUndefined(entityLineage.entity) && + !deleted + ) { setUpdatedLineageData(entityLineage); - setElementsHandleV1(entityLineage); + setElementsHandle(entityLineage); } }, [entityLineage]); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/Entitylineage.component.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/Entitylineage.component.test.tsx index b4d1c2a155c..61b14f8edb9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/Entitylineage.component.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/Entitylineage.component.test.tsx @@ -161,8 +161,8 @@ jest.mock('../../utils/EntityLineageUtils', () => ({

Lineage data is not available for deleted entities.

), getHeaderLabel: jest.fn().mockReturnValue(

Header label

), - getLayoutedElementsV1: jest.fn().mockImplementation(() => mockFlowData), - getLineageDataV1: jest.fn().mockImplementation(() => mockFlowData), + getLayoutedElements: jest.fn().mockImplementation(() => mockFlowData), + getLineageData: jest.fn().mockImplementation(() => mockFlowData), getModalBodyText: jest.fn(), onLoad: jest.fn(), onNodeContextMenu: jest.fn(), diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MlModelDetail/MlModelDetail.component.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MlModelDetail/MlModelDetail.component.test.tsx index 76e361273bb..f80dea65635 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MlModelDetail/MlModelDetail.component.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MlModelDetail/MlModelDetail.component.test.tsx @@ -12,6 +12,7 @@ */ import { findByTestId, findByText, render } from '@testing-library/react'; +import { LeafNodes } from 'Models'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import { Mlmodel } from '../../generated/entity/data/mlmodel'; @@ -141,6 +142,16 @@ const mockProp = { tagUpdateHandler, updateMlModelFeatures, settingsUpdateHandler, + lineageTabData: { + loadNodeHandler: jest.fn(), + addLineageHandler: jest.fn(), + removeLineageHandler: jest.fn(), + entityLineageHandler: jest.fn(), + isLineageLoading: false, + entityLineage: { entity: { id: 'test', type: 'mlmodel' } }, + lineageLeafNodes: {} as LeafNodes, + isNodeLoading: { id: undefined, state: false }, + }, }; jest.mock('../ManageTab/ManageTab.component', () => { @@ -159,6 +170,10 @@ jest.mock('../common/rich-text-editor/RichTextEditorPreviewer', () => { return jest.fn().mockReturnValue(

RichTextEditorPreviewer

); }); +jest.mock('../EntityLineage/EntityLineage.component', () => { + return jest.fn().mockReturnValue(

EntityLineage.component

); +}); + jest.mock('./MlModelFeaturesList', () => { return jest.fn().mockReturnValue(

MlModelFeaturesList

); }); @@ -224,7 +239,7 @@ describe('Test MlModel entity detail component', () => { expect(mlStoreTable).toBeInTheDocument(); }); - it('Should render manage component for manage tab', async () => { + it('Should render lineage tab', async () => { const { container } = render( , { @@ -232,6 +247,19 @@ describe('Test MlModel entity detail component', () => { } ); + const detailContainer = await findByTestId(container, 'lineage-details'); + + expect(detailContainer).toBeInTheDocument(); + }); + + it('Should render manage component for manage tab', async () => { + const { container } = render( + , + { + wrapper: MemoryRouter, + } + ); + const detailContainer = await findByTestId(container, 'mlmodel-details'); const manageTab = await findByTestId(container, 'manage'); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MlModelDetail/MlModelDetail.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MlModelDetail/MlModelDetail.component.tsx index c1402c105b3..ce51d71819e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MlModelDetail/MlModelDetail.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MlModelDetail/MlModelDetail.component.tsx @@ -14,7 +14,13 @@ import classNames from 'classnames'; import { startCase, uniqueId } from 'lodash'; import { observer } from 'mobx-react'; -import { EntityTags, ExtraInfo } from 'Models'; +import { + EntityTags, + ExtraInfo, + LeafNodes, + LineagePos, + LoadingNodeState, +} from 'Models'; import React, { FC, Fragment, @@ -33,6 +39,7 @@ import { EntityType } from '../../enums/entity.enum'; import { ServiceCategory } from '../../enums/service.enum'; import { OwnerType } from '../../enums/user.enum'; import { Mlmodel } from '../../generated/entity/data/mlmodel'; +import { EntityLineage } from '../../generated/type/entityLineage'; import { EntityReference } from '../../generated/type/entityReference'; import { LabelType, State, TagLabel } from '../../generated/type/tagLabel'; import { getEntityName, getEntityPlaceHolder } from '../../utils/CommonUtils'; @@ -43,6 +50,8 @@ import EntityPageInfo from '../common/entityPageInfo/EntityPageInfo'; import TabsPane from '../common/TabsPane/TabsPane'; import { TitleBreadcrumbProps } from '../common/title-breadcrumb/title-breadcrumb.interface'; import PageContainer from '../containers/PageContainer'; +import EntityLineageComponent from '../EntityLineage/EntityLineage.component'; +import { Edge, EdgeData } from '../EntityLineage/EntityLineage.interface'; import ManageTabComponent from '../ManageTab/ManageTab.component'; import MlModelFeaturesList from './MlModelFeaturesList'; @@ -56,6 +65,16 @@ interface MlModelDetailProp extends HTMLAttributes { tagUpdateHandler: (updatedMlModel: Mlmodel) => void; updateMlModelFeatures: (updatedMlModel: Mlmodel) => void; settingsUpdateHandler: (updatedMlModel: Mlmodel) => Promise; + lineageTabData: { + loadNodeHandler: (node: EntityReference, pos: LineagePos) => void; + addLineageHandler: (edge: Edge) => Promise; + removeLineageHandler: (data: EdgeData) => void; + entityLineageHandler: (lineage: EntityLineage) => void; + isLineageLoading?: boolean; + entityLineage: EntityLineage; + lineageLeafNodes: LeafNodes; + isNodeLoading: LoadingNodeState; + }; } const MlModelDetail: FC = ({ @@ -68,6 +87,7 @@ const MlModelDetail: FC = ({ tagUpdateHandler, settingsUpdateHandler, updateMlModelFeatures, + lineageTabData, }) => { const [followersCount, setFollowersCount] = useState(0); const [isFollowing, setIsFollowing] = useState(false); @@ -184,6 +204,11 @@ const MlModelDetail: FC = ({ isProtected: false, position: 2, }, + { + name: 'Lineage', + isProtected: false, + position: 3, + }, { name: 'Manage', icon: { @@ -194,7 +219,7 @@ const MlModelDetail: FC = ({ }, isProtected: false, protectedState: !mlModelDetail.owner || hasEditAccess(), - position: 3, + position: 4, }, ]; @@ -452,6 +477,24 @@ const MlModelDetail: FC = ({ )} {activeTab === 3 && ( +
+ +
+ )} + {activeTab === 4 && (
({ .fn() .mockReturnValue(

Task data is not available for deleted entities.

), getHeaderLabel: jest.fn().mockReturnValue(

Header label

), - getLayoutedElementsV1: jest.fn().mockImplementation(() => ({ + getLayoutedElements: jest.fn().mockImplementation(() => ({ node: mockNodes, edge: mockEdges, })), - getLineageDataV1: jest.fn().mockReturnValue([]), + getLineageData: jest.fn().mockReturnValue([]), getModalBodyText: jest.fn(), onLoad: jest.fn(), onNodeContextMenu: jest.fn(), diff --git a/openmetadata-ui/src/main/resources/ui/src/components/TasksDAGView/TasksDAGView.tsx b/openmetadata-ui/src/main/resources/ui/src/components/TasksDAGView/TasksDAGView.tsx index 0315e588693..a1105d753a6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/TasksDAGView/TasksDAGView.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/TasksDAGView/TasksDAGView.tsx @@ -22,7 +22,7 @@ import ReactFlow, { import { PipelineStatus, Task } from '../../generated/entity/data/pipeline'; import { EntityReference } from '../../generated/type/entityReference'; import { getEntityName, replaceSpaceWith_ } from '../../utils/CommonUtils'; -import { getLayoutedElementsV1, onLoad } from '../../utils/EntityLineageUtils'; +import { getLayoutedElements, onLoad } from '../../utils/EntityLineageUtils'; import { getTaskExecStatus } from '../../utils/PipelineDetailsUtils'; import TaskNode from './TaskNode'; @@ -92,7 +92,7 @@ const TasksDAGView = ({ tasks, selectedExec }: Props) => { return [...prev, ...taskEdges]; }, [] as Edge[]); - const { node: nodeValue, edge: edgeValue } = getLayoutedElementsV1({ + const { node: nodeValue, edge: edgeValue } = getLayoutedElements({ node: nodes, edge: edges, }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/TopicDetails/TopicDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/TopicDetails/TopicDetails.component.tsx index be3946940af..85c8190b81c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/TopicDetails/TopicDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/TopicDetails/TopicDetails.component.tsx @@ -48,6 +48,7 @@ import Description from '../common/description/Description'; import EntityPageInfo from '../common/entityPageInfo/EntityPageInfo'; import TabsPane from '../common/TabsPane/TabsPane'; import PageContainer from '../containers/PageContainer'; +import EntityLineageComponent from '../EntityLineage/EntityLineage.component'; import Loader from '../Loader/Loader'; import ManageTabComponent from '../ManageTab/ManageTab.component'; import RequestDescriptionModal from '../Modals/RequestDescriptionModal/RequestDescriptionModal'; @@ -95,6 +96,7 @@ const TopicDetails: React.FC = ({ sampleData, updateThreadHandler, entityFieldTaskCount, + lineageTabData, }: TopicDetailsProps) => { const [isEdit, setIsEdit] = useState(false); const [followersCount, setFollowersCount] = useState(0); @@ -208,6 +210,17 @@ const TopicDetails: React.FC = ({ isProtected: false, position: 4, }, + { + name: 'Lineage', + icon: { + alt: 'lineage', + name: 'icon-lineage', + title: 'Lineage', + selectedName: 'icon-lineagecolor', + }, + isProtected: false, + position: 5, + }, { name: 'Manage', icon: { @@ -218,7 +231,7 @@ const TopicDetails: React.FC = ({ }, isProtected: true, protectedState: !owner || hasEditAccess(), - position: 5, + position: 6, }, ]; const extraInfo: Array = [ @@ -485,6 +498,24 @@ const TopicDetails: React.FC = ({
)} {activeTab === 5 && ( +
+ +
+ )} + {activeTab === 6 && (
void; deletePostHandler: (threadId: string, postId: string) => void; updateThreadHandler: ThreadUpdatedFunc; + lineageTabData: { + loadNodeHandler: (node: EntityReference, pos: LineagePos) => void; + addLineageHandler: (edge: Edge) => Promise; + removeLineageHandler: (data: EdgeData) => void; + entityLineageHandler: (lineage: EntityLineage) => void; + isLineageLoading?: boolean; + entityLineage: EntityLineage; + lineageLeafNodes: LeafNodes; + isNodeLoading: LoadingNodeState; + }; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/TopicDetails/TopicDetails.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/TopicDetails/TopicDetails.test.tsx index 1d435be8dd9..362f685e58f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/TopicDetails/TopicDetails.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/TopicDetails/TopicDetails.test.tsx @@ -95,6 +95,16 @@ const TopicDetailsProps = { paging: {} as Paging, fetchFeedHandler: jest.fn(), updateThreadHandler: jest.fn(), + lineageTabData: { + loadNodeHandler: jest.fn(), + addLineageHandler: jest.fn(), + removeLineageHandler: jest.fn(), + entityLineageHandler: jest.fn(), + isLineageLoading: false, + entityLineage: { entity: { id: 'test', type: 'topic' } }, + lineageLeafNodes: {} as LeafNodes, + isNodeLoading: { id: undefined, state: false }, + }, }; const mockObserve = jest.fn(); @@ -109,6 +119,10 @@ jest.mock('../ManageTab/ManageTab.component', () => { return jest.fn().mockReturnValue(

ManageTab

); }); +jest.mock('../EntityLineage/EntityLineage.component', () => { + return jest.fn().mockReturnValue(

EntityLineage.component

); +}); + jest.mock('../common/description/Description', () => { return jest.fn().mockReturnValue(

Description Component

); }); @@ -220,7 +234,7 @@ describe('Test TopicDetails component', () => { it('Check if active tab is manage', async () => { const { container } = render( - , + , { wrapper: MemoryRouter, } @@ -230,6 +244,19 @@ describe('Test TopicDetails component', () => { expect(manage).toBeInTheDocument(); }); + it('Should render lineage tab', async () => { + const { container } = render( + , + { + wrapper: MemoryRouter, + } + ); + + const detailContainer = await findByTestId(container, 'lineage-details'); + + expect(detailContainer).toBeInTheDocument(); + }); + it('Should create an observer if IntersectionObserver is available', async () => { const { container } = render( , diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/Lineage.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/Lineage.constants.ts index 2a1551f872c..c1bd91c5260 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/Lineage.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/Lineage.constants.ts @@ -12,6 +12,8 @@ export const entityData = [ }, { type: EntityType.PIPELINE, label: capitalize(EntityType.PIPELINE) }, { type: EntityType.DASHBOARD, label: capitalize(EntityType.DASHBOARD) }, + { type: EntityType.TOPIC, label: capitalize(EntityType.TOPIC) }, + { type: EntityType.MLMODEL, label: capitalize(EntityType.MLMODEL) }, ]; export const positionX = 150; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/MlModelPage/MlModelPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/MlModelPage/MlModelPage.component.tsx index 8bb82ff1d21..b36d17f3c29 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/MlModelPage/MlModelPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/MlModelPage/MlModelPage.component.tsx @@ -15,8 +15,11 @@ import { AxiosError, AxiosResponse } from 'axios'; import { compare } from 'fast-json-patch'; import { isEmpty, isNil } from 'lodash'; import { observer } from 'mobx-react'; +import { LeafNodes, LineagePos, LoadingNodeState } from 'Models'; import React, { Fragment, useEffect, useState } from 'react'; import { useHistory, useParams } from 'react-router-dom'; +import { getLineageByFQN } from '../../axiosAPIs/lineageAPI'; +import { addLineage, deleteLineageEdge } from '../../axiosAPIs/miscAPI'; import { addFollower, getMlModelByFQN, @@ -24,15 +27,25 @@ import { removeFollower, } from '../../axiosAPIs/mlModelAPI'; import ErrorPlaceHolder from '../../components/common/error-with-placeholder/ErrorPlaceHolder'; +import { + Edge, + EdgeData, +} from '../../components/EntityLineage/EntityLineage.interface'; import Loader from '../../components/Loader/Loader'; import MlModelDetailComponent from '../../components/MlModelDetail/MlModelDetail.component'; import { getMlModelPath } from '../../constants/constants'; +import { EntityType, TabSpecificField } from '../../enums/entity.enum'; import { Mlmodel } from '../../generated/entity/data/mlmodel'; +import { + EntityLineage, + EntityReference, +} from '../../generated/type/entityLineage'; import jsonData from '../../jsons/en'; import { getCurrentUserId, getEntityMissingError, } from '../../utils/CommonUtils'; +import { getEntityLineage } from '../../utils/EntityUtils'; import { defaultFields, getCurrentMlModelTab, @@ -48,6 +61,110 @@ const MlModelPage = () => { const [activeTab, setActiveTab] = useState(getCurrentMlModelTab(tab)); const USERId = getCurrentUserId(); + const [entityLineage, setEntityLineage] = useState( + {} as EntityLineage + ); + const [leafNodes, setLeafNodes] = useState({} as LeafNodes); + const [isNodeLoading, setNodeLoading] = useState({ + id: undefined, + state: false, + }); + const [isLineageLoading, setIsLineageLoading] = useState(false); + + const getLineageData = () => { + setIsLineageLoading(true); + getLineageByFQN(mlModelDetail.fullyQualifiedName, EntityType.MLMODEL) + .then((res: AxiosResponse) => { + if (res.data) { + setEntityLineage(res.data); + } else { + showErrorToast(jsonData['api-error-messages']['fetch-lineage-error']); + } + }) + .catch((err: AxiosError) => { + showErrorToast( + err, + jsonData['api-error-messages']['fetch-lineage-error'] + ); + }) + .finally(() => { + setIsLineageLoading(false); + }); + }; + + const setLeafNode = (val: EntityLineage, pos: LineagePos) => { + if (pos === 'to' && val.downstreamEdges?.length === 0) { + setLeafNodes((prev) => ({ + ...prev, + downStreamNode: [...(prev.downStreamNode ?? []), val.entity.id], + })); + } + if (pos === 'from' && val.upstreamEdges?.length === 0) { + setLeafNodes((prev) => ({ + ...prev, + upStreamNode: [...(prev.upStreamNode ?? []), val.entity.id], + })); + } + }; + + const entityLineageHandler = (lineage: EntityLineage) => { + setEntityLineage(lineage); + }; + + const loadNodeHandler = (node: EntityReference, pos: LineagePos) => { + setNodeLoading({ id: node.id, state: true }); + getLineageByFQN(node.fullyQualifiedName, node.type) + .then((res: AxiosResponse) => { + if (res.data) { + setLeafNode(res.data, pos); + setEntityLineage(getEntityLineage(entityLineage, res.data, pos)); + } else { + showErrorToast( + jsonData['api-error-messages']['fetch-lineage-node-error'] + ); + } + setTimeout(() => { + setNodeLoading((prev) => ({ ...prev, state: false })); + }, 500); + }) + .catch((err: AxiosError) => { + showErrorToast( + err, + jsonData['api-error-messages']['fetch-lineage-node-error'] + ); + }); + }; + + const addLineageHandler = (edge: Edge): Promise => { + return new Promise((resolve, reject) => { + addLineage(edge) + .then(() => { + resolve(); + }) + .catch((err: AxiosError) => { + showErrorToast( + err, + jsonData['api-error-messages']['add-lineage-error'] + ); + reject(); + }); + }); + }; + + const removeLineageHandler = (data: EdgeData) => { + deleteLineageEdge( + data.fromEntity, + data.fromId, + data.toEntity, + data.toId + ).catch((err: AxiosError) => { + showErrorToast( + err, + jsonData['api-error-messages']['delete-lineage-error'] + ); + }); + }; + const activeTabHandler = (tabValue: number) => { const currentTabIndex = tabValue - 1; if (mlModelTabs[currentTabIndex].path !== tab) { @@ -58,6 +175,25 @@ const MlModelPage = () => { } }; + const fetchTabSpecificData = (tabField = '') => { + switch (tabField) { + case TabSpecificField.LINEAGE: { + if (!isEmpty(mlModelDetail) && !mlModelDetail.deleted) { + if (isEmpty(entityLineage)) { + getLineageData(); + } + + break; + } + + break; + } + + default: + break; + } + }; + const fetchMlModelDetails = (name: string) => { setIsDetailLoading(true); getMlModelByFQN(name, defaultFields) @@ -227,6 +363,16 @@ const MlModelPage = () => { activeTab={activeTab} descriptionUpdateHandler={descriptionUpdateHandler} followMlModelHandler={followMlModel} + lineageTabData={{ + loadNodeHandler, + addLineageHandler, + removeLineageHandler, + entityLineageHandler, + isLineageLoading, + entityLineage, + lineageLeafNodes: leafNodes, + isNodeLoading, + }} mlModelDetail={mlModelDetail} setActiveTabHandler={activeTabHandler} settingsUpdateHandler={settingsUpdateHandler} @@ -244,6 +390,10 @@ const MlModelPage = () => { } }; + useEffect(() => { + fetchTabSpecificData(mlModelTabs[activeTab - 1].field); + }, [activeTab, mlModelDetail]); + useEffect(() => { fetchMlModelDetails(mlModelFqn); }, [mlModelFqn]); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TopicDetails/TopicDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TopicDetails/TopicDetailsPage.component.tsx index de38d024092..ba6847dadbf 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TopicDetails/TopicDetailsPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TopicDetails/TopicDetailsPage.component.tsx @@ -13,9 +13,15 @@ import { AxiosError, AxiosResponse } from 'axios'; import { compare, Operation } from 'fast-json-patch'; -import { isUndefined } from 'lodash'; +import { isEmpty, isUndefined } from 'lodash'; import { observer } from 'mobx-react'; -import { EntityFieldThreadCount, EntityTags } from 'Models'; +import { + EntityFieldThreadCount, + EntityTags, + LeafNodes, + LineagePos, + LoadingNodeState, +} from 'Models'; import React, { FunctionComponent, useEffect, useState } from 'react'; import { useHistory, useParams } from 'react-router-dom'; import AppState from '../../AppState'; @@ -24,6 +30,8 @@ import { postFeedById, postThread, } from '../../axiosAPIs/feedsAPI'; +import { getLineageByFQN } from '../../axiosAPIs/lineageAPI'; +import { addLineage, deleteLineageEdge } from '../../axiosAPIs/miscAPI'; import { addFollower, getTopicByFqn, @@ -32,6 +40,10 @@ import { } from '../../axiosAPIs/topicsAPI'; import ErrorPlaceHolder from '../../components/common/error-with-placeholder/ErrorPlaceHolder'; import { TitleBreadcrumbProps } from '../../components/common/title-breadcrumb/title-breadcrumb.interface'; +import { + Edge, + EdgeData, +} from '../../components/EntityLineage/EntityLineage.interface'; import Loader from '../../components/Loader/Loader'; import TopicDetails from '../../components/TopicDetails/TopicDetails.component'; import { @@ -45,6 +57,7 @@ import { ServiceCategory } from '../../enums/service.enum'; import { CreateThread } from '../../generated/api/feed/createThread'; import { Topic, TopicSampleData } from '../../generated/entity/data/topic'; import { Thread, ThreadType } from '../../generated/entity/feed/thread'; +import { EntityLineage } from '../../generated/type/entityLineage'; import { EntityReference } from '../../generated/type/entityReference'; import { Paging } from '../../generated/type/paging'; import { TagLabel } from '../../generated/type/tagLabel'; @@ -56,7 +69,7 @@ import { getEntityName, getFeedCounts, } from '../../utils/CommonUtils'; -import { getEntityFeedLink } from '../../utils/EntityUtils'; +import { getEntityFeedLink, getEntityLineage } from '../../utils/EntityUtils'; import { deletePost, getUpdatedThread, @@ -114,6 +127,109 @@ const TopicDetailsPage: FunctionComponent = () => { const [sampleData, setSampleData] = useState(); const [isSampleDataLoading, setIsSampleDataLoading] = useState(false); + const [entityLineage, setEntityLineage] = useState( + {} as EntityLineage + ); + const [leafNodes, setLeafNodes] = useState({} as LeafNodes); + const [isNodeLoading, setNodeLoading] = useState({ + id: undefined, + state: false, + }); + const [isLineageLoading, setIsLineageLoading] = useState(false); + + const getLineageData = () => { + setIsLineageLoading(true); + getLineageByFQN(topicFQN, EntityType.TOPIC) + .then((res: AxiosResponse) => { + if (res.data) { + setEntityLineage(res.data); + } else { + showErrorToast(jsonData['api-error-messages']['fetch-lineage-error']); + } + }) + .catch((err: AxiosError) => { + showErrorToast( + err, + jsonData['api-error-messages']['fetch-lineage-error'] + ); + }) + .finally(() => { + setIsLineageLoading(false); + }); + }; + + const setLeafNode = (val: EntityLineage, pos: LineagePos) => { + if (pos === 'to' && val.downstreamEdges?.length === 0) { + setLeafNodes((prev) => ({ + ...prev, + downStreamNode: [...(prev.downStreamNode ?? []), val.entity.id], + })); + } + if (pos === 'from' && val.upstreamEdges?.length === 0) { + setLeafNodes((prev) => ({ + ...prev, + upStreamNode: [...(prev.upStreamNode ?? []), val.entity.id], + })); + } + }; + + const entityLineageHandler = (lineage: EntityLineage) => { + setEntityLineage(lineage); + }; + + const loadNodeHandler = (node: EntityReference, pos: LineagePos) => { + setNodeLoading({ id: node.id, state: true }); + getLineageByFQN(node.fullyQualifiedName, node.type) + .then((res: AxiosResponse) => { + if (res.data) { + setLeafNode(res.data, pos); + setEntityLineage(getEntityLineage(entityLineage, res.data, pos)); + } else { + showErrorToast( + jsonData['api-error-messages']['fetch-lineage-node-error'] + ); + } + setTimeout(() => { + setNodeLoading((prev) => ({ ...prev, state: false })); + }, 500); + }) + .catch((err: AxiosError) => { + showErrorToast( + err, + jsonData['api-error-messages']['fetch-lineage-node-error'] + ); + }); + }; + + const addLineageHandler = (edge: Edge): Promise => { + return new Promise((resolve, reject) => { + addLineage(edge) + .then(() => { + resolve(); + }) + .catch((err: AxiosError) => { + showErrorToast( + err, + jsonData['api-error-messages']['add-lineage-error'] + ); + reject(); + }); + }); + }; + + const removeLineageHandler = (data: EdgeData) => { + deleteLineageEdge( + data.fromEntity, + data.fromId, + data.toEntity, + data.toId + ).catch((err: AxiosError) => { + showErrorToast( + err, + jsonData['api-error-messages']['delete-lineage-error'] + ); + }); + }; const activeTabHandler = (tabValue: number) => { const currentTabIndex = tabValue - 1; @@ -215,6 +331,18 @@ const TopicDetailsPage: FunctionComponent = () => { } } + case TabSpecificField.LINEAGE: { + if (!deleted) { + if (isEmpty(entityLineage)) { + getLineageData(); + } + + break; + } + + break; + } + default: break; } @@ -583,6 +711,16 @@ const TopicDetailsPage: FunctionComponent = () => { followers={followers} isSampleDataLoading={isSampleDataLoading} isentityThreadLoading={isentityThreadLoading} + lineageTabData={{ + loadNodeHandler, + addLineageHandler, + removeLineageHandler, + entityLineageHandler, + isLineageLoading, + entityLineage, + lineageLeafNodes: leafNodes, + isNodeLoading, + }} maximumMessageSize={maximumMessageSize} owner={owner as EntityReference} paging={paging} diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.tsx index af344190afe..010c21c6d53 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.tsx @@ -154,7 +154,7 @@ export const getColumnType = (edges: Edge[], id: string) => { return EntityLineageNodeType.NOT_CONNECTED; }; -export const getLineageDataV1 = ( +export const getLineageData = ( entityLineage: EntityLineage, onSelect: (state: boolean, value: SelectedNode) => void, loadNodeHandler: (node: EntityReference, pos: LineagePos) => void, @@ -425,7 +425,7 @@ export const getDeletedLineagePlaceholder = () => { const dagreGraph = new dagre.graphlib.Graph(); dagreGraph.setDefaultEdgeLabel(() => ({})); -export const getLayoutedElementsV1 = ( +export const getLayoutedElements = ( elements: CustomeElement, direction = EntityLineageDirection.LEFT_RIGHT ) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/MlModelDetailsUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/MlModelDetailsUtils.ts index 6cdd11f9445..a4a3ee65463 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/MlModelDetailsUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/MlModelDetailsUtils.ts @@ -25,6 +25,11 @@ export const mlModelTabs = [ name: 'Details', path: 'details', }, + { + name: 'Lineage', + path: 'lineage', + field: TabSpecificField.LINEAGE, + }, { name: 'Manage', path: 'manage', @@ -38,11 +43,14 @@ export const getCurrentMlModelTab = (tab: string) => { currentTab = 2; break; - case 'manage': + case 'lineage': currentTab = 3; break; + case 'manage': + currentTab = 4; + break; case 'features': currentTab = 1; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx index bf0ab10a714..cb0a0cb1d1c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx @@ -249,7 +249,7 @@ export const Icons = { EXTERNAL_LINK_GREY: 'external-link-grey', PROFILER: 'icon-profiler', PIPELINE: 'pipeline', - MLMODAL: 'mlmodal', + MLMODAL: 'mlmodel-grey', PIPELINE_GREY: 'pipeline-grey', DBTMODEL_GREY: 'dbtmodel-grey', DBTMODEL_LIGHT_GREY: 'dbtmodel-light-grey', diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx index 256677691e8..380b1983714 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx @@ -229,6 +229,11 @@ export const getEntityIcon = (indexType: string) => { case EntityType.DASHBOARD: icon = 'dashboard-grey'; + break; + case SearchIndex.MLMODEL: + case EntityType.MLMODEL: + icon = 'mlmodel-grey'; + break; case SearchIndex.PIPELINE: case EntityType.PIPELINE: diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TopicDetailsUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TopicDetailsUtils.ts index ea589f60e30..eb5fbb94bec 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TopicDetailsUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TopicDetailsUtils.ts @@ -32,6 +32,11 @@ export const topicDetailsTabs = [ name: 'Config', path: 'config', }, + { + name: 'Lineage', + path: 'lineage', + field: TabSpecificField.LINEAGE, + }, { name: 'Manage', path: 'manage', @@ -53,9 +58,13 @@ export const getCurrentTopicTab = (tab: string) => { currentTab = 4; break; - case 'manage': + case 'lineage': currentTab = 5; + break; + case 'manage': + currentTab = 6; + break; case 'schema':