Fix #5653 UI: Added Lineage support for topic and mlModal (#5888)

This commit is contained in:
Shailesh Parmar 2022-07-07 00:52:30 +05:30 committed by GitHub
parent a3ca37e6a8
commit 09b37d28f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 491 additions and 28 deletions

View File

@ -66,8 +66,8 @@ import {
getColumnType,
getDataLabel,
getDeletedLineagePlaceholder,
getLayoutedElementsV1,
getLineageDataV1,
getLayoutedElements,
getLineageData,
getModalBodyText,
getNodeRemoveButton,
getUniqueFlowElements,
@ -461,7 +461,7 @@ const Entitylineage: FunctionComponent<EntityLineageProp> = ({
setConfirmDelete(false);
};
const setElementsHandleV1 = (data: EntityLineage) => {
const setElementsHandle = (data: EntityLineage) => {
let uniqueElements: CustomeElement = {
node: [],
edge: [],
@ -471,7 +471,7 @@ const Entitylineage: FunctionComponent<EntityLineageProp> = ({
edges: [...(edges || [])],
};
if (!isEmpty(data)) {
const graphElements = getLineageDataV1(
const graphElements = getLineageData(
data,
selectNodeHandler,
loadNodeHandler,
@ -491,7 +491,7 @@ const Entitylineage: FunctionComponent<EntityLineageProp> = ({
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<EntityLineageProp> = ({
useEffect(() => {
if (!deleted && !isEmpty(updatedLineageData)) {
setElementsHandleV1(updatedLineageData);
setElementsHandle(updatedLineageData);
}
}, [isNodeLoading, isEditMode]);
@ -1322,9 +1322,13 @@ const Entitylineage: FunctionComponent<EntityLineageProp> = ({
}, [selectedEdge, confirmDelete]);
useEffect(() => {
if (!isEmpty(entityLineage) && !deleted) {
if (
!isEmpty(entityLineage) &&
!isUndefined(entityLineage.entity) &&
!deleted
) {
setUpdatedLineageData(entityLineage);
setElementsHandleV1(entityLineage);
setElementsHandle(entityLineage);
}
}, [entityLineage]);

View File

@ -161,8 +161,8 @@ jest.mock('../../utils/EntityLineageUtils', () => ({
<p>Lineage data is not available for deleted entities.</p>
),
getHeaderLabel: jest.fn().mockReturnValue(<p>Header label</p>),
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(),

View File

@ -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(<p>RichTextEditorPreviewer</p>);
});
jest.mock('../EntityLineage/EntityLineage.component', () => {
return jest.fn().mockReturnValue(<p>EntityLineage.component</p>);
});
jest.mock('./MlModelFeaturesList', () => {
return jest.fn().mockReturnValue(<p>MlModelFeaturesList</p>);
});
@ -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(
<MlModelDetailComponent {...mockProp} activeTab={3} />,
{
@ -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(
<MlModelDetailComponent {...mockProp} activeTab={4} />,
{
wrapper: MemoryRouter,
}
);
const detailContainer = await findByTestId(container, 'mlmodel-details');
const manageTab = await findByTestId(container, 'manage');

View File

@ -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<HTMLDivElement> {
tagUpdateHandler: (updatedMlModel: Mlmodel) => void;
updateMlModelFeatures: (updatedMlModel: Mlmodel) => void;
settingsUpdateHandler: (updatedMlModel: Mlmodel) => Promise<void>;
lineageTabData: {
loadNodeHandler: (node: EntityReference, pos: LineagePos) => void;
addLineageHandler: (edge: Edge) => Promise<void>;
removeLineageHandler: (data: EdgeData) => void;
entityLineageHandler: (lineage: EntityLineage) => void;
isLineageLoading?: boolean;
entityLineage: EntityLineage;
lineageLeafNodes: LeafNodes;
isNodeLoading: LoadingNodeState;
};
}
const MlModelDetail: FC<MlModelDetailProp> = ({
@ -68,6 +87,7 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
tagUpdateHandler,
settingsUpdateHandler,
updateMlModelFeatures,
lineageTabData,
}) => {
const [followersCount, setFollowersCount] = useState<number>(0);
const [isFollowing, setIsFollowing] = useState<boolean>(false);
@ -184,6 +204,11 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
isProtected: false,
position: 2,
},
{
name: 'Lineage',
isProtected: false,
position: 3,
},
{
name: 'Manage',
icon: {
@ -194,7 +219,7 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
},
isProtected: false,
protectedState: !mlModelDetail.owner || hasEditAccess(),
position: 3,
position: 4,
},
];
@ -452,6 +477,24 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
</div>
)}
{activeTab === 3 && (
<div
className="tw-px-2 tw-h-full"
data-testid="lineage-details">
<EntityLineageComponent
addLineageHandler={lineageTabData.addLineageHandler}
deleted={mlModelDetail.deleted}
entityLineage={lineageTabData.entityLineage}
entityLineageHandler={lineageTabData.entityLineageHandler}
isLoading={lineageTabData.isLineageLoading}
isNodeLoading={lineageTabData.isNodeLoading}
isOwner={hasEditAccess()}
lineageLeafNodes={lineageTabData.lineageLeafNodes}
loadNodeHandler={lineageTabData.loadNodeHandler}
removeLineageHandler={lineageTabData.removeLineageHandler}
/>
</div>
)}
{activeTab === 4 && (
<div>
<ManageTabComponent
allowDelete

View File

@ -93,11 +93,11 @@ jest.mock('../../utils/EntityLineageUtils', () => ({
.fn()
.mockReturnValue(<p>Task data is not available for deleted entities.</p>),
getHeaderLabel: jest.fn().mockReturnValue(<p>Header label</p>),
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(),

View File

@ -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,
});

View File

@ -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<TopicDetailsProps> = ({
sampleData,
updateThreadHandler,
entityFieldTaskCount,
lineageTabData,
}: TopicDetailsProps) => {
const [isEdit, setIsEdit] = useState(false);
const [followersCount, setFollowersCount] = useState(0);
@ -208,6 +210,17 @@ const TopicDetails: React.FC<TopicDetailsProps> = ({
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<TopicDetailsProps> = ({
},
isProtected: true,
protectedState: !owner || hasEditAccess(),
position: 5,
position: 6,
},
];
const extraInfo: Array<ExtraInfo> = [
@ -485,6 +498,24 @@ const TopicDetails: React.FC<TopicDetailsProps> = ({
</div>
)}
{activeTab === 5 && (
<div
className="tw-px-2 tw-h-full"
data-testid="lineage-details">
<EntityLineageComponent
addLineageHandler={lineageTabData.addLineageHandler}
deleted={deleted}
entityLineage={lineageTabData.entityLineage}
entityLineageHandler={lineageTabData.entityLineageHandler}
isLoading={lineageTabData.isLineageLoading}
isNodeLoading={lineageTabData.isNodeLoading}
isOwner={hasEditAccess()}
lineageLeafNodes={lineageTabData.lineageLeafNodes}
loadNodeHandler={lineageTabData.loadNodeHandler}
removeLineageHandler={lineageTabData.removeLineageHandler}
/>
</div>
)}
{activeTab === 6 && (
<div>
<ManageTabComponent
allowDelete

View File

@ -11,16 +11,24 @@
* limitations under the License.
*/
import { EntityFieldThreadCount, EntityTags } from 'Models';
import {
EntityFieldThreadCount,
EntityTags,
LeafNodes,
LineagePos,
LoadingNodeState,
} from 'Models';
import { FeedFilter } from '../../enums/mydata.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';
import { ThreadUpdatedFunc } from '../../interface/feed.interface';
import { TitleBreadcrumbProps } from '../common/title-breadcrumb/title-breadcrumb.interface';
import { Edge, EdgeData } from '../EntityLineage/EntityLineage.interface';
export interface TopicDetailsProps {
topicFQN: string;
@ -66,4 +74,14 @@ export interface TopicDetailsProps {
postFeedHandler: (value: string, id: string) => void;
deletePostHandler: (threadId: string, postId: string) => void;
updateThreadHandler: ThreadUpdatedFunc;
lineageTabData: {
loadNodeHandler: (node: EntityReference, pos: LineagePos) => void;
addLineageHandler: (edge: Edge) => Promise<void>;
removeLineageHandler: (data: EdgeData) => void;
entityLineageHandler: (lineage: EntityLineage) => void;
isLineageLoading?: boolean;
entityLineage: EntityLineage;
lineageLeafNodes: LeafNodes;
isNodeLoading: LoadingNodeState;
};
}

View File

@ -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(<p data-testid="manage">ManageTab</p>);
});
jest.mock('../EntityLineage/EntityLineage.component', () => {
return jest.fn().mockReturnValue(<p>EntityLineage.component</p>);
});
jest.mock('../common/description/Description', () => {
return jest.fn().mockReturnValue(<p>Description Component</p>);
});
@ -220,7 +234,7 @@ describe('Test TopicDetails component', () => {
it('Check if active tab is manage', async () => {
const { container } = render(
<TopicDetails {...TopicDetailsProps} activeTab={5} />,
<TopicDetails {...TopicDetailsProps} activeTab={6} />,
{
wrapper: MemoryRouter,
}
@ -230,6 +244,19 @@ describe('Test TopicDetails component', () => {
expect(manage).toBeInTheDocument();
});
it('Should render lineage tab', async () => {
const { container } = render(
<TopicDetails {...TopicDetailsProps} activeTab={5} />,
{
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(
<TopicDetails {...TopicDetailsProps} activeTab={4} />,

View File

@ -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;

View File

@ -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<number>(getCurrentMlModelTab(tab));
const USERId = getCurrentUserId();
const [entityLineage, setEntityLineage] = useState<EntityLineage>(
{} as EntityLineage
);
const [leafNodes, setLeafNodes] = useState<LeafNodes>({} as LeafNodes);
const [isNodeLoading, setNodeLoading] = useState<LoadingNodeState>({
id: undefined,
state: false,
});
const [isLineageLoading, setIsLineageLoading] = useState<boolean>(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<void> => {
return new Promise<void>((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]);

View File

@ -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<TopicSampleData>();
const [isSampleDataLoading, setIsSampleDataLoading] =
useState<boolean>(false);
const [entityLineage, setEntityLineage] = useState<EntityLineage>(
{} as EntityLineage
);
const [leafNodes, setLeafNodes] = useState<LeafNodes>({} as LeafNodes);
const [isNodeLoading, setNodeLoading] = useState<LoadingNodeState>({
id: undefined,
state: false,
});
const [isLineageLoading, setIsLineageLoading] = useState<boolean>(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<void> => {
return new Promise<void>((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}

View File

@ -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
) => {

View File

@ -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;

View File

@ -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',

View File

@ -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:

View File

@ -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':