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 77b1f6df3f1..b12c7802cc9 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
@@ -13,376 +13,37 @@
import { AxiosResponse } from 'axios';
import { isEmpty } from 'lodash';
-import { LeafNodes, LineagePos, LoadingNodeState } from 'Models';
-import React, {
- FunctionComponent,
- MouseEvent as ReactMouseEvent,
- useEffect,
- useRef,
- useState,
-} from 'react';
+import React, { FunctionComponent, useEffect, useRef, useState } from 'react';
import ReactFlow, {
addEdge,
- ArrowHeadType,
Connection,
Controls,
Edge,
Elements,
FlowElement,
- Node,
- OnLoadParams,
- Position,
ReactFlowProvider,
removeElements,
} from 'react-flow-renderer';
-import { Link } from 'react-router-dom';
import { getTableDetails } from '../../axiosAPIs/tableAPI';
import { Column } from '../../generated/entity/data/table';
-import {
- Edge as LineageEdge,
- EntityLineage,
-} from '../../generated/type/entityLineage';
import { EntityReference } from '../../generated/type/entityReference';
import useToastContext from '../../hooks/useToastContext';
-import { isLeafNode } from '../../utils/EntityUtils';
+import {
+ dragHandle,
+ getDataLabel,
+ getLineageData,
+ getNoLineageDataPlaceholder,
+ onLoad,
+ onNodeContextMenu,
+ onNodeMouseEnter,
+ onNodeMouseLeave,
+ onNodeMouseMove,
+} from '../../utils/EntityLineageUtils';
import SVGIcons from '../../utils/SvgUtils';
import { getEntityIcon } from '../../utils/TableUtils';
import EntityInfoDrawer from '../EntityInfoDrawer/EntityInfoDrawer.component';
-import Loader from '../Loader/Loader';
import CustomNode from './CustomNode.component';
import { EntityLineageProp, SelectedNode } from './EntityLineage.interface';
-const onLoad = (reactFlowInstance: OnLoadParams) => {
- reactFlowInstance.fitView();
- reactFlowInstance.zoomTo(1);
-};
-/* eslint-disable-next-line */
-const onNodeMouseEnter = (_event: ReactMouseEvent, _node: Node | Edge) => {
- return;
-};
-/* eslint-disable-next-line */
-const onNodeMouseMove = (_event: ReactMouseEvent, _node: Node | Edge) => {
- return;
-};
-/* eslint-disable-next-line */
-const onNodeMouseLeave = (_event: ReactMouseEvent, _node: Node | Edge) => {
- return;
-};
-/* eslint-disable-next-line */
-const onNodeContextMenu = (_event: ReactMouseEvent, _node: Node | Edge) => {
- _event.preventDefault();
-};
-
-const dragHandle = (event: ReactMouseEvent) => {
- event.stopPropagation();
-};
-
-const getDataLabel = (v = '', separator = '.', isTextOnly = false) => {
- const length = v.split(separator).length;
- if (isTextOnly) {
- return v.split(separator)[length - 1];
- }
-
- return (
-
- {v.split(separator)[length - 1]}
-
- );
-};
-
-const getNoLineageDataPlaceholder = () => {
- return (
-
-
- Lineage is currently supported for Airflow. To enable lineage collection
- from Airflow, please follow the documentation
-
-
- here.
-
-
- );
-};
-
-const positionX = 150;
-const positionY = 60;
-
-const getLineageData = (
- entityLineage: EntityLineage,
- onSelect: (state: boolean, value: SelectedNode) => void,
- loadNodeHandler: (node: EntityReference, pos: LineagePos) => void,
- lineageLeafNodes: LeafNodes,
- isNodeLoading: LoadingNodeState,
- getNodeLable: (node: EntityReference) => React.ReactNode
-) => {
- const [x, y] = [0, 0];
- const nodes = entityLineage['nodes'];
- let upstreamEdges: Array =
- entityLineage['upstreamEdges']?.map((up) => ({ isMapped: false, ...up })) ||
- [];
- let downstreamEdges: Array =
- entityLineage['downstreamEdges']?.map((down) => ({
- isMapped: false,
- ...down,
- })) || [];
- const mainNode = entityLineage['entity'];
-
- const UPStreamNodes: Elements = [];
- const DOWNStreamNodes: Elements = [];
- const lineageEdges: Elements = [];
-
- const makeNode = (
- node: EntityReference,
- pos: LineagePos,
- depth: number,
- posDepth: number
- ) => {
- const [xVal, yVal] = [positionX * 2 * depth, y + positionY * posDepth];
-
- return {
- id: `node-${node.id}-${depth}`,
- sourcePosition: Position.Right,
- targetPosition: Position.Left,
- type: 'default',
- className: 'leaf-node',
- data: {
- label: getNodeLable(node),
- },
- position: {
- x: pos === 'from' ? -xVal : xVal,
- y: yVal,
- },
- };
- };
-
- const getNodes = (
- id: string,
- pos: LineagePos,
- depth: number,
- NodesArr: Array = []
- ): Array => {
- if (pos === 'to') {
- let upDepth = NodesArr.filter((nd) => nd.lDepth === depth).length;
- const UPNodes: Array = [];
- const updatedUpStreamEdge = upstreamEdges.map((up) => {
- if (up.toEntity === id) {
- const edg = UPStreamNodes.find((up) => up.id.includes(`node-${id}`));
- const node = nodes?.find((nd) => nd.id === up.fromEntity);
- if (node) {
- UPNodes.push(node);
- UPStreamNodes.push(makeNode(node, 'from', depth, upDepth));
- lineageEdges.push({
- id: `edge-${up.fromEntity}-${id}-${depth}`,
- source: `node-${node.id}-${depth}`,
- target: edg ? edg.id : `node-${id}-${depth}`,
- type: 'custom',
- arrowHeadType: ArrowHeadType.ArrowClosed,
- });
- }
- upDepth += 1;
-
- return {
- ...up,
- isMapped: true,
- };
- } else {
- return up;
- }
- });
-
- upstreamEdges = updatedUpStreamEdge;
-
- return UPNodes?.map((upNd) => ({ lDepth: depth, ...upNd })) || [];
- } else {
- let downDepth = NodesArr.filter((nd) => nd.lDepth === depth).length;
- const DOWNNodes: Array = [];
- const updatedDownStreamEdge = downstreamEdges.map((down) => {
- if (down.fromEntity === id) {
- const edg = DOWNStreamNodes.find((down) =>
- down.id.includes(`node-${id}`)
- );
- const node = nodes?.find((nd) => nd.id === down.toEntity);
- if (node) {
- DOWNNodes.push(node);
- DOWNStreamNodes.push(makeNode(node, 'to', depth, downDepth));
- lineageEdges.push({
- id: `edge-${id}-${down.toEntity}`,
- source: edg ? edg.id : `node-${id}-${depth}`,
- target: `node-${node.id}-${depth}`,
- type: 'custom',
- arrowHeadType: ArrowHeadType.ArrowClosed,
- });
- }
- downDepth += 1;
-
- return {
- ...down,
- isMapped: true,
- };
- } else {
- return down;
- }
- });
-
- downstreamEdges = updatedDownStreamEdge;
-
- return DOWNNodes?.map((downNd) => ({ lDepth: depth, ...downNd })) || [];
- }
- };
-
- const getUpStreamData = (
- Entity: EntityReference,
- depth = 1,
- upNodesArr: Array = []
- ) => {
- const upNodes = getNodes(Entity.id, 'to', depth, upNodesArr);
- upNodesArr.push(...upNodes);
- upNodes.forEach((up) => {
- if (
- upstreamEdges.some((upE) => upE.toEntity === up.id && !upE.isMapped)
- ) {
- getUpStreamData(up, depth + 1, upNodesArr);
- }
- });
-
- return upNodesArr;
- };
-
- const getDownStreamData = (
- Entity: EntityReference,
- depth = 1,
- downNodesArr: Array = []
- ) => {
- const downNodes = getNodes(Entity.id, 'from', depth, downNodesArr);
- downNodesArr.push(...downNodes);
- downNodes.forEach((down) => {
- if (
- downstreamEdges.some(
- (downE) => downE.fromEntity === down.id && !downE.isMapped
- )
- ) {
- getDownStreamData(down, depth + 1, downNodesArr);
- }
- });
-
- return downNodesArr;
- };
-
- getUpStreamData(mainNode);
-
- getDownStreamData(mainNode);
-
- const lineageData = [
- {
- id: `node-${mainNode.id}-1`,
- sourcePosition: 'right',
- targetPosition: 'left',
- type: lineageEdges.find((ed: FlowElement) =>
- (ed as Edge).target.includes(mainNode.id)
- )
- ? lineageEdges.find((ed: FlowElement) =>
- (ed as Edge).source.includes(mainNode.id)
- )
- ? 'default'
- : 'output'
- : 'input',
- className: 'leaf-node core',
- data: {
- label: getNodeLable(mainNode),
- },
- position: { x: x, y: y },
- },
- ...UPStreamNodes.map((up) => {
- const node = entityLineage?.nodes?.find((d) => up.id.includes(d.id));
-
- return lineageEdges.find(
- (ed: FlowElement) => (ed as Edge).target === up.id
- )
- ? up
- : {
- ...up,
- type: 'input',
- data: {
- label: (
-
-
{
- e.stopPropagation();
- onSelect(false, {} as SelectedNode);
- if (node) {
- loadNodeHandler(node, 'from');
- }
- }}>
- {!isLeafNode(
- lineageLeafNodes,
- node?.id as string,
- 'from'
- ) && !up.id.includes(isNodeLoading.id as string) ? (
-
- ) : null}
- {isNodeLoading.state &&
- up.id.includes(isNodeLoading.id as string) ? (
-
- ) : null}
-
-
-
{up?.data?.label}
-
- ),
- },
- };
- }),
- ...DOWNStreamNodes.map((down) => {
- const node = entityLineage?.nodes?.find((d) => down.id.includes(d.id));
-
- return lineageEdges.find((ed: FlowElement) =>
- (ed as Edge).source.includes(down.id)
- )
- ? down
- : {
- ...down,
- type: 'output',
- data: {
- label: (
-
-
{down?.data?.label}
-
-
{
- e.stopPropagation();
- onSelect(false, {} as SelectedNode);
- if (node) {
- loadNodeHandler(node, 'to');
- }
- }}>
- {!isLeafNode(lineageLeafNodes, node?.id as string, 'to') &&
- !down.id.includes(isNodeLoading.id as string) ? (
-
- ) : null}
- {isNodeLoading.state &&
- down.id.includes(isNodeLoading.id as string) ? (
-
- ) : null}
-
-
- ),
- },
- };
- }),
- ...lineageEdges,
- ];
-
- return lineageData;
-};
const Entitylineage: FunctionComponent = ({
entityLineage,
diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts
index 1ad4e52c0cd..ae5d3a5e214 100644
--- a/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts
@@ -286,3 +286,7 @@ export const TITLE_FOR_NON_OWNER_ACTION =
export const TITLE_FOR_NON_ADMIN_ACTION =
'Only Admin is allowed for the action';
+
+// Entity Lineage Constant
+export const positionX = 150;
+export const positionY = 60;
diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.tsx
new file mode 100644
index 00000000000..82256215a1d
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.tsx
@@ -0,0 +1,364 @@
+/*
+ * Copyright 2021 Collate
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { LeafNodes, LineagePos, LoadingNodeState } from 'Models';
+import React, { MouseEvent as ReactMouseEvent } from 'react';
+import {
+ OnLoadParams,
+ Edge,
+ Elements,
+ Position,
+ FlowElement,
+ ArrowHeadType,
+ Node,
+} from 'react-flow-renderer';
+import { Link } from 'react-router-dom';
+import { SelectedNode } from '../components/EntityLineage/EntityLineage.interface';
+import Loader from '../components/Loader/Loader';
+import { positionX, positionY } from '../constants/constants';
+import {
+ EntityLineage,
+ Edge as LineageEdge,
+} from '../generated/type/entityLineage';
+import { EntityReference } from '../generated/type/entityReference';
+import { isLeafNode } from './EntityUtils';
+
+export const onLoad = (reactFlowInstance: OnLoadParams) => {
+ reactFlowInstance.fitView();
+ reactFlowInstance.zoomTo(1);
+};
+/* eslint-disable-next-line */
+export const onNodeMouseEnter = (_event: ReactMouseEvent, _node: Node) => {
+ return;
+};
+/* eslint-disable-next-line */
+export const onNodeMouseMove = (_event: ReactMouseEvent, _node: Node) => {
+ return;
+};
+/* eslint-disable-next-line */
+export const onNodeMouseLeave = (_event: ReactMouseEvent, _node: Node) => {
+ return;
+};
+/* eslint-disable-next-line */
+export const onNodeContextMenu = (event: ReactMouseEvent, _node: Node) => {
+ event.preventDefault();
+};
+
+export const dragHandle = (event: ReactMouseEvent) => {
+ event.stopPropagation();
+};
+
+export const getLineageData = (
+ entityLineage: EntityLineage,
+ onSelect: (state: boolean, value: SelectedNode) => void,
+ loadNodeHandler: (node: EntityReference, pos: LineagePos) => void,
+ lineageLeafNodes: LeafNodes,
+ isNodeLoading: LoadingNodeState,
+ getNodeLable: (node: EntityReference) => React.ReactNode
+) => {
+ const [x, y] = [0, 0];
+ const nodes = entityLineage['nodes'];
+ let upstreamEdges: Array =
+ entityLineage['upstreamEdges']?.map((up) => ({ isMapped: false, ...up })) ||
+ [];
+ let downstreamEdges: Array =
+ entityLineage['downstreamEdges']?.map((down) => ({
+ isMapped: false,
+ ...down,
+ })) || [];
+ const mainNode = entityLineage['entity'];
+
+ const UPStreamNodes: Elements = [];
+ const DOWNStreamNodes: Elements = [];
+ const lineageEdges: Elements = [];
+
+ const makeNode = (
+ node: EntityReference,
+ pos: LineagePos,
+ depth: number,
+ posDepth: number
+ ) => {
+ const [xVal, yVal] = [positionX * 2 * depth, y + positionY * posDepth];
+
+ return {
+ id: `node-${node.id}-${depth}`,
+ sourcePosition: Position.Right,
+ targetPosition: Position.Left,
+ type: 'default',
+ className: 'leaf-node',
+ data: {
+ label: getNodeLable(node),
+ },
+ position: {
+ x: pos === 'from' ? -xVal : xVal,
+ y: yVal,
+ },
+ };
+ };
+
+ const getNodes = (
+ id: string,
+ pos: LineagePos,
+ depth: number,
+ NodesArr: Array = []
+ ): Array => {
+ if (pos === 'to') {
+ let upDepth = NodesArr.filter((nd) => nd.lDepth === depth).length;
+ const UPNodes: Array = [];
+ const updatedUpStreamEdge = upstreamEdges.map((up) => {
+ if (up.toEntity === id) {
+ const edg = UPStreamNodes.find((up) => up.id.includes(`node-${id}`));
+ const node = nodes?.find((nd) => nd.id === up.fromEntity);
+ if (node) {
+ UPNodes.push(node);
+ UPStreamNodes.push(makeNode(node, 'from', depth, upDepth));
+ lineageEdges.push({
+ id: `edge-${up.fromEntity}-${id}-${depth}`,
+ source: `node-${node.id}-${depth}`,
+ target: edg ? edg.id : `node-${id}-${depth}`,
+ type: 'custom',
+ arrowHeadType: ArrowHeadType.ArrowClosed,
+ });
+ }
+ upDepth += 1;
+
+ return {
+ ...up,
+ isMapped: true,
+ };
+ } else {
+ return up;
+ }
+ });
+
+ upstreamEdges = updatedUpStreamEdge;
+
+ return UPNodes?.map((upNd) => ({ lDepth: depth, ...upNd })) || [];
+ } else {
+ let downDepth = NodesArr.filter((nd) => nd.lDepth === depth).length;
+ const DOWNNodes: Array = [];
+ const updatedDownStreamEdge = downstreamEdges.map((down) => {
+ if (down.fromEntity === id) {
+ const edg = DOWNStreamNodes.find((down) =>
+ down.id.includes(`node-${id}`)
+ );
+ const node = nodes?.find((nd) => nd.id === down.toEntity);
+ if (node) {
+ DOWNNodes.push(node);
+ DOWNStreamNodes.push(makeNode(node, 'to', depth, downDepth));
+ lineageEdges.push({
+ id: `edge-${id}-${down.toEntity}`,
+ source: edg ? edg.id : `node-${id}-${depth}`,
+ target: `node-${node.id}-${depth}`,
+ type: 'custom',
+ arrowHeadType: ArrowHeadType.ArrowClosed,
+ });
+ }
+ downDepth += 1;
+
+ return {
+ ...down,
+ isMapped: true,
+ };
+ } else {
+ return down;
+ }
+ });
+
+ downstreamEdges = updatedDownStreamEdge;
+
+ return DOWNNodes?.map((downNd) => ({ lDepth: depth, ...downNd })) || [];
+ }
+ };
+
+ const getUpStreamData = (
+ Entity: EntityReference,
+ depth = 1,
+ upNodesArr: Array = []
+ ) => {
+ const upNodes = getNodes(Entity.id, 'to', depth, upNodesArr);
+ upNodesArr.push(...upNodes);
+ upNodes.forEach((up) => {
+ if (
+ upstreamEdges.some((upE) => upE.toEntity === up.id && !upE.isMapped)
+ ) {
+ getUpStreamData(up, depth + 1, upNodesArr);
+ }
+ });
+
+ return upNodesArr;
+ };
+
+ const getDownStreamData = (
+ Entity: EntityReference,
+ depth = 1,
+ downNodesArr: Array = []
+ ) => {
+ const downNodes = getNodes(Entity.id, 'from', depth, downNodesArr);
+ downNodesArr.push(...downNodes);
+ downNodes.forEach((down) => {
+ if (
+ downstreamEdges.some(
+ (downE) => downE.fromEntity === down.id && !downE.isMapped
+ )
+ ) {
+ getDownStreamData(down, depth + 1, downNodesArr);
+ }
+ });
+
+ return downNodesArr;
+ };
+
+ getUpStreamData(mainNode);
+
+ getDownStreamData(mainNode);
+
+ const lineageData = [
+ {
+ id: `node-${mainNode.id}-1`,
+ sourcePosition: 'right',
+ targetPosition: 'left',
+ type: lineageEdges.find((ed: FlowElement) =>
+ (ed as Edge).target.includes(mainNode.id)
+ )
+ ? lineageEdges.find((ed: FlowElement) =>
+ (ed as Edge).source.includes(mainNode.id)
+ )
+ ? 'default'
+ : 'output'
+ : 'input',
+ className: 'leaf-node core',
+ data: {
+ label: getNodeLable(mainNode),
+ },
+ position: { x: x, y: y },
+ },
+ ...UPStreamNodes.map((up) => {
+ const node = entityLineage?.nodes?.find((d) => up.id.includes(d.id));
+
+ return lineageEdges.find(
+ (ed: FlowElement) => (ed as Edge).target === up.id
+ )
+ ? up
+ : {
+ ...up,
+ type: 'input',
+ data: {
+ label: (
+
+
{
+ e.stopPropagation();
+ onSelect(false, {} as SelectedNode);
+ if (node) {
+ loadNodeHandler(node, 'from');
+ }
+ }}>
+ {!isLeafNode(
+ lineageLeafNodes,
+ node?.id as string,
+ 'from'
+ ) && !up.id.includes(isNodeLoading.id as string) ? (
+
+ ) : null}
+ {isNodeLoading.state &&
+ up.id.includes(isNodeLoading.id as string) ? (
+
+ ) : null}
+
+
+
{up?.data?.label}
+
+ ),
+ },
+ };
+ }),
+ ...DOWNStreamNodes.map((down) => {
+ const node = entityLineage?.nodes?.find((d) => down.id.includes(d.id));
+
+ return lineageEdges.find((ed: FlowElement) =>
+ (ed as Edge).source.includes(down.id)
+ )
+ ? down
+ : {
+ ...down,
+ type: 'output',
+ data: {
+ label: (
+
+
{down?.data?.label}
+
+
{
+ e.stopPropagation();
+ onSelect(false, {} as SelectedNode);
+ if (node) {
+ loadNodeHandler(node, 'to');
+ }
+ }}>
+ {!isLeafNode(lineageLeafNodes, node?.id as string, 'to') &&
+ !down.id.includes(isNodeLoading.id as string) ? (
+
+ ) : null}
+ {isNodeLoading.state &&
+ down.id.includes(isNodeLoading.id as string) ? (
+
+ ) : null}
+
+
+ ),
+ },
+ };
+ }),
+ ...lineageEdges,
+ ];
+
+ return lineageData;
+};
+
+export const getDataLabel = (v = '', separator = '.', isTextOnly = false) => {
+ const length = v.split(separator).length;
+ if (isTextOnly) {
+ return v.split(separator)[length - 1];
+ }
+
+ return (
+
+ {v.split(separator)[length - 1]}
+
+ );
+};
+
+export const getNoLineageDataPlaceholder = () => {
+ return (
+
+
+ Lineage is currently supported for Airflow. To enable lineage collection
+ from Airflow, please follow the documentation
+
+
+ here.
+
+
+ );
+};