mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-12-31 01:15:44 +00:00
1341 lines
40 KiB
TypeScript
1341 lines
40 KiB
TypeScript
/*
|
|
* 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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
import { AxiosError, AxiosResponse } from 'axios';
|
|
import classNames from 'classnames';
|
|
import {
|
|
isEmpty,
|
|
isNil,
|
|
isUndefined,
|
|
lowerCase,
|
|
uniqueId,
|
|
upperCase,
|
|
} from 'lodash';
|
|
import { LoadingState } from 'Models';
|
|
import React, {
|
|
DragEvent,
|
|
Fragment,
|
|
FunctionComponent,
|
|
useCallback,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from 'react';
|
|
import ReactFlow, {
|
|
addEdge,
|
|
Background,
|
|
BackgroundVariant,
|
|
Connection,
|
|
Edge,
|
|
getConnectedEdges,
|
|
isNode,
|
|
MarkerType,
|
|
Node,
|
|
ReactFlowInstance,
|
|
ReactFlowProvider,
|
|
useEdgesState,
|
|
useNodesState,
|
|
} from 'react-flow-renderer';
|
|
import { useAuthContext } from '../../authentication/auth-provider/AuthProvider';
|
|
import { getTableDetails } from '../../axiosAPIs/tableAPI';
|
|
import { ELEMENT_DELETE_STATE } from '../../constants/Lineage.constants';
|
|
import {
|
|
AddLineage,
|
|
ColumnLineage,
|
|
} from '../../generated/api/lineage/addLineage';
|
|
import { Column } from '../../generated/entity/data/table';
|
|
import { Operation } from '../../generated/entity/policies/accessControl/rule';
|
|
import {
|
|
Edge as EntityEdge,
|
|
EntityLineage,
|
|
} from '../../generated/type/entityLineage';
|
|
import { EntityReference } from '../../generated/type/entityReference';
|
|
import { withLoader } from '../../hoc/withLoader';
|
|
import { useAuth } from '../../hooks/authHooks';
|
|
import {
|
|
dragHandle,
|
|
getColumnType,
|
|
getDataLabel,
|
|
getDeletedLineagePlaceholder,
|
|
getLayoutedElementsV1,
|
|
getLineageDataV1,
|
|
getModalBodyText,
|
|
getNodeRemoveButton,
|
|
getUniqueFlowElements,
|
|
onLoad,
|
|
onNodeContextMenu,
|
|
onNodeMouseEnter,
|
|
onNodeMouseLeave,
|
|
onNodeMouseMove,
|
|
} from '../../utils/EntityLineageUtils';
|
|
import SVGIcons from '../../utils/SvgUtils';
|
|
import { getEntityIcon } from '../../utils/TableUtils';
|
|
import { showErrorToast } from '../../utils/ToastUtils';
|
|
import NonAdminAction from '../common/non-admin-action/NonAdminAction';
|
|
import EntityInfoDrawer from '../EntityInfoDrawer/EntityInfoDrawer.component';
|
|
import Loader from '../Loader/Loader';
|
|
import ConfirmationModal from '../Modals/ConfirmationModal/ConfirmationModal';
|
|
import CustomControls, { ControlButton } from './CustomControls.component';
|
|
import { CustomEdge } from './CustomEdge.component';
|
|
import CustomNode from './CustomNode.component';
|
|
import {
|
|
CustomEdgeData,
|
|
CustomeElement,
|
|
EdgeData,
|
|
ElementLoadingState,
|
|
EntityLineageProp,
|
|
ModifiedColumn,
|
|
SelectedEdge,
|
|
SelectedNode,
|
|
} from './EntityLineage.interface';
|
|
import EntityLineageSidebar from './EntityLineageSidebar.component';
|
|
import NodeSuggestions from './NodeSuggestions.component';
|
|
|
|
const Entitylineage: FunctionComponent<EntityLineageProp> = ({
|
|
entityLineage,
|
|
loadNodeHandler,
|
|
lineageLeafNodes,
|
|
isNodeLoading,
|
|
isLoading,
|
|
deleted,
|
|
addLineageHandler,
|
|
removeLineageHandler,
|
|
entityLineageHandler,
|
|
}: EntityLineageProp) => {
|
|
const { userPermissions, isAdminUser } = useAuth();
|
|
const { isAuthDisabled } = useAuthContext();
|
|
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
|
const [lineageData, setLineageData] = useState<EntityLineage>(entityLineage);
|
|
const [reactFlowInstance, setReactFlowInstance] =
|
|
useState<ReactFlowInstance>();
|
|
const [isDrawerOpen, setIsDrawerOpen] = useState<boolean>(false);
|
|
const [selectedNode, setSelectedNode] = useState<SelectedNode>(
|
|
{} as SelectedNode
|
|
);
|
|
const expandButton = useRef<HTMLButtonElement | null>(null);
|
|
const [isEditMode, setEditMode] = useState<boolean>(false);
|
|
|
|
const tableColumnsRef = useRef<{ [key: string]: Column[] }>(
|
|
{} as { [key: string]: Column[] }
|
|
);
|
|
const [newAddedNode, setNewAddedNode] = useState<Node>({} as Node);
|
|
const [selectedEntity, setSelectedEntity] = useState<EntityReference>(
|
|
{} as EntityReference
|
|
);
|
|
const [confirmDelete, setConfirmDelete] = useState<boolean>(false);
|
|
|
|
const [showdeleteModal, setShowDeleteModal] = useState<boolean>(false);
|
|
|
|
const [selectedEdge, setSelectedEdge] = useState<SelectedEdge>(
|
|
{} as SelectedEdge
|
|
);
|
|
|
|
const [loading, setLoading] = useState<boolean>(false);
|
|
const [status, setStatus] = useState<LoadingState>('initial');
|
|
const [deletionState, setDeletionState] = useState<{
|
|
loading: boolean;
|
|
status: ElementLoadingState;
|
|
}>(ELEMENT_DELETE_STATE);
|
|
|
|
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
|
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
|
|
|
/**
|
|
* this state will maintain the updated state and
|
|
* it will be sent back to parent when the user came out from edit mode to view mode
|
|
*/
|
|
const [updatedLineageData, setUpdatedLineageData] =
|
|
useState<EntityLineage>(entityLineage);
|
|
|
|
/**
|
|
* Custom Node Type Object
|
|
*/
|
|
const nodeTypes = useMemo(
|
|
() => ({
|
|
output: CustomNode,
|
|
input: CustomNode,
|
|
default: CustomNode,
|
|
}),
|
|
[]
|
|
);
|
|
|
|
/**
|
|
* take node as input and check if node is main entity or not
|
|
* @param node
|
|
* @returns class `leaf-node core` for main node and `leaf-node` for leaf node
|
|
*/
|
|
const getNodeClass = (node: Node) => {
|
|
return `${
|
|
node.id.includes(updatedLineageData.entity?.id) && !isEditMode
|
|
? 'leaf-node core'
|
|
: 'leaf-node'
|
|
}`;
|
|
};
|
|
|
|
/**
|
|
* take entity as input and set it as selected entity
|
|
* @param entity
|
|
*/
|
|
const selectedEntityHandler = (entity: EntityReference) => {
|
|
setSelectedEntity(entity);
|
|
};
|
|
|
|
/**
|
|
* take state and value to set selected node
|
|
* @param state
|
|
* @param value
|
|
*/
|
|
const selectNodeHandler = (state: boolean, value: SelectedNode) => {
|
|
setIsDrawerOpen(state);
|
|
setSelectedNode(value);
|
|
};
|
|
|
|
/**
|
|
*
|
|
* @param node
|
|
* @returns label for given node
|
|
*/
|
|
const getNodeLabel = (node: EntityReference, isExpanded = false) => {
|
|
return (
|
|
<Fragment>
|
|
{node.type === 'table' ? (
|
|
<button
|
|
className="tw-absolute tw--top-3.5 tw--left-2 tw-cursor-pointer tw-z-9999"
|
|
onClick={(e) => {
|
|
expandButton.current = expandButton.current
|
|
? null
|
|
: e.currentTarget;
|
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
handleNodeExpand(!isExpanded, node);
|
|
setIsDrawerOpen(false);
|
|
}}>
|
|
<SVGIcons
|
|
alt="plus"
|
|
icon={isExpanded ? 'icon-minus' : 'icon-plus'}
|
|
width="16px"
|
|
/>
|
|
</button>
|
|
) : null}
|
|
<p className="tw-flex tw-m-0 tw-py-3">
|
|
<span className="tw-mr-2">{getEntityIcon(node.type)}</span>
|
|
{getDataLabel(
|
|
node.displayName,
|
|
node.fullyQualifiedName,
|
|
false,
|
|
node.type
|
|
)}
|
|
</p>
|
|
</Fragment>
|
|
);
|
|
};
|
|
|
|
/**
|
|
*
|
|
* @param data selected edge
|
|
* @param confirmDelete confirmation state for deleting seslected edge
|
|
*/
|
|
const removeEdgeHandler = (data: SelectedEdge, confirmDelete: boolean) => {
|
|
if (confirmDelete) {
|
|
const edgeData: EdgeData = {
|
|
fromEntity: data.source.type,
|
|
fromId: data.source.id,
|
|
toEntity: data.target.type,
|
|
toId: data.target.id,
|
|
};
|
|
removeLineageHandler(edgeData);
|
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
setEdges((es) => {
|
|
return es.filter((e) => e.id !== data.id);
|
|
});
|
|
|
|
/**
|
|
* Get new downstreamEdges
|
|
*/
|
|
const newDownStreamEdges = updatedLineageData.downstreamEdges?.filter(
|
|
(dn) =>
|
|
!updatedLineageData.downstreamEdges?.find(
|
|
() =>
|
|
edgeData.fromId === dn.fromEntity && edgeData.toId === dn.toEntity
|
|
)
|
|
);
|
|
|
|
/**
|
|
* Get new upstreamEdges
|
|
*/
|
|
const newUpStreamEdges = updatedLineageData.upstreamEdges?.filter(
|
|
(up) =>
|
|
!updatedLineageData.upstreamEdges?.find(
|
|
() =>
|
|
edgeData.fromId === up.fromEntity && edgeData.toId === up.toEntity
|
|
)
|
|
);
|
|
|
|
setNewAddedNode({} as Node);
|
|
setSelectedEntity({} as EntityReference);
|
|
setUpdatedLineageData({
|
|
...updatedLineageData,
|
|
downstreamEdges: newDownStreamEdges,
|
|
upstreamEdges: newUpStreamEdges,
|
|
});
|
|
setConfirmDelete(false);
|
|
}
|
|
};
|
|
|
|
const removeColumnEdge = (data: SelectedEdge, confirmDelete: boolean) => {
|
|
if (confirmDelete) {
|
|
const upStreamEdge = updatedLineageData.upstreamEdges?.find(
|
|
(up) =>
|
|
up.fromEntity === data.source.id && up.toEntity === data.target.id
|
|
);
|
|
|
|
const downStreamEdge = updatedLineageData.downstreamEdges?.find(
|
|
(down) =>
|
|
down.fromEntity === data.source.id && down.toEntity === data.target.id
|
|
);
|
|
|
|
const selectedEdge: AddLineage = {
|
|
edge: {
|
|
fromEntity: {
|
|
id: data.source.id,
|
|
type: data.source.type,
|
|
},
|
|
toEntity: {
|
|
id: data.target.id,
|
|
type: data.target.type,
|
|
},
|
|
},
|
|
};
|
|
|
|
if (!isUndefined(upStreamEdge)) {
|
|
const upColumnsLineage: ColumnLineage[] =
|
|
upStreamEdge.lineageDetails?.columnsLineage?.reduce((col, curr) => {
|
|
if (curr.toColumn === data.data?.targetHandle) {
|
|
const newCol = {
|
|
...curr,
|
|
fromColumns:
|
|
curr.fromColumns?.filter(
|
|
(c) => c !== data.data?.sourceHandle
|
|
) || [],
|
|
};
|
|
if (newCol.fromColumns?.length) {
|
|
return [...col, newCol];
|
|
} else {
|
|
return col;
|
|
}
|
|
}
|
|
|
|
return [...col, curr];
|
|
}, [] as ColumnLineage[]) || [];
|
|
selectedEdge.edge.lineageDetails = {
|
|
sqlQuery: upStreamEdge.lineageDetails?.sqlQuery || '',
|
|
columnsLineage: upColumnsLineage,
|
|
};
|
|
|
|
setUpdatedLineageData({
|
|
...updatedLineageData,
|
|
upstreamEdges: updatedLineageData.upstreamEdges?.map((up) => {
|
|
if (
|
|
up.fromEntity === data.source.id &&
|
|
up.toEntity === data.target.id
|
|
) {
|
|
return {
|
|
...up,
|
|
lineageDetails: selectedEdge.edge.lineageDetails,
|
|
};
|
|
}
|
|
|
|
return up;
|
|
}),
|
|
});
|
|
}
|
|
|
|
if (!isUndefined(downStreamEdge)) {
|
|
const downColumnsLineage: ColumnLineage[] =
|
|
downStreamEdge.lineageDetails?.columnsLineage?.reduce((col, curr) => {
|
|
if (curr.toColumn === data.data?.targetHandle) {
|
|
const newCol: ColumnLineage = {
|
|
...curr,
|
|
fromColumns:
|
|
curr.fromColumns?.filter(
|
|
(c) => c !== data.data?.sourceHandle
|
|
) || [],
|
|
};
|
|
if (newCol.fromColumns?.length) {
|
|
return [...col, newCol];
|
|
}
|
|
}
|
|
|
|
return [...col, curr];
|
|
}, [] as ColumnLineage[]) || [];
|
|
selectedEdge.edge.lineageDetails = {
|
|
sqlQuery: downStreamEdge.lineageDetails?.sqlQuery || '',
|
|
columnsLineage: downColumnsLineage,
|
|
};
|
|
|
|
setUpdatedLineageData({
|
|
...updatedLineageData,
|
|
downstreamEdges: updatedLineageData.downstreamEdges?.map((down) => {
|
|
if (
|
|
down.fromEntity === data.source.id &&
|
|
down.toEntity === data.target.id
|
|
) {
|
|
return {
|
|
...down,
|
|
lineageDetails: selectedEdge.edge.lineageDetails,
|
|
};
|
|
}
|
|
|
|
return down;
|
|
}),
|
|
});
|
|
}
|
|
setEdges((pre) =>
|
|
pre.filter(
|
|
(e) =>
|
|
e.sourceHandle !== data.data?.sourceHandle &&
|
|
e.targetHandle !== data.data?.targetHandle
|
|
)
|
|
);
|
|
addLineageHandler(selectedEdge);
|
|
setNewAddedNode({} as Node);
|
|
setSelectedEntity({} as EntityReference);
|
|
setConfirmDelete(false);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* take edge data and set it as selected edge
|
|
* @param evt
|
|
* @param data
|
|
*/
|
|
const onEdgeClick = (
|
|
evt: React.MouseEvent<HTMLButtonElement>,
|
|
data: CustomEdgeData
|
|
) => {
|
|
setShowDeleteModal(true);
|
|
evt.stopPropagation();
|
|
setSelectedEdge(() => {
|
|
const allNode = [
|
|
...(updatedLineageData.nodes || []),
|
|
updatedLineageData.entity,
|
|
];
|
|
let targetNode = allNode.find((n) => data.target?.includes(n.id));
|
|
|
|
let sourceNode = allNode.find((n) => data.source?.includes(n.id));
|
|
|
|
if (isUndefined(targetNode)) {
|
|
targetNode = isEmpty(selectedEntity)
|
|
? updatedLineageData.entity
|
|
: selectedEntity;
|
|
}
|
|
if (isUndefined(sourceNode)) {
|
|
sourceNode = isEmpty(selectedEntity)
|
|
? updatedLineageData.entity
|
|
: selectedEntity;
|
|
}
|
|
|
|
return { id: data.id, source: sourceNode, target: targetNode, data };
|
|
});
|
|
};
|
|
|
|
const setElementsHandleV1 = () => {
|
|
let uniqueElements: CustomeElement = {
|
|
node: [],
|
|
edge: [],
|
|
};
|
|
const currentData = {
|
|
nodes: [...nodes],
|
|
edges: [...edges],
|
|
};
|
|
if (!isEmpty(updatedLineageData)) {
|
|
const graphElements = getLineageDataV1(
|
|
updatedLineageData,
|
|
selectNodeHandler,
|
|
loadNodeHandler,
|
|
lineageLeafNodes,
|
|
isNodeLoading,
|
|
getNodeLabel,
|
|
isEditMode,
|
|
'buttonedge',
|
|
onEdgeClick,
|
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
removeNodeHandler,
|
|
tableColumnsRef.current,
|
|
currentData
|
|
) as CustomeElement;
|
|
|
|
uniqueElements = {
|
|
node: getUniqueFlowElements(graphElements.node) as Node[],
|
|
edge: getUniqueFlowElements(graphElements.edge) as Edge[],
|
|
};
|
|
}
|
|
|
|
return uniqueElements;
|
|
};
|
|
|
|
/**
|
|
* take boolean value as input and reset selected node
|
|
* @param value
|
|
*/
|
|
const closeDrawer = (value: boolean) => {
|
|
setIsDrawerOpen(value);
|
|
setNodes((prevElements) => {
|
|
return prevElements.map((el) => {
|
|
if (el.id === selectedNode.id) {
|
|
return {
|
|
...el,
|
|
className: getNodeClass(el),
|
|
};
|
|
} else {
|
|
return el;
|
|
}
|
|
});
|
|
});
|
|
setSelectedNode({} as SelectedNode);
|
|
};
|
|
|
|
/**
|
|
* take edge or connection to add new element in the graph
|
|
* @param params
|
|
*/
|
|
const onConnect = useCallback(
|
|
(params: Edge | Connection) => {
|
|
const { target, source, sourceHandle, targetHandle } = params;
|
|
|
|
if (target === source) return;
|
|
|
|
const columnConnection = !isNil(sourceHandle) && !isNil(targetHandle);
|
|
const normalConnection = isNil(sourceHandle) && isNil(targetHandle);
|
|
const mainEntity = updatedLineageData.entity;
|
|
if (columnConnection || normalConnection) {
|
|
setStatus('waiting');
|
|
setLoading(true);
|
|
|
|
let edgeType: 'upstream' | 'downstream' | '' = '';
|
|
|
|
const nodes = [
|
|
...(updatedLineageData.nodes as EntityReference[]),
|
|
updatedLineageData.entity,
|
|
];
|
|
|
|
const sourceDownstreamNode = updatedLineageData.downstreamEdges?.find(
|
|
(d) =>
|
|
(source?.includes(d.fromEntity) || source?.includes(d.toEntity)) &&
|
|
source !== mainEntity.id
|
|
);
|
|
|
|
const sourceUpStreamNode = updatedLineageData.upstreamEdges?.find(
|
|
(u) =>
|
|
(source?.includes(u.fromEntity) || source?.includes(u.toEntity)) &&
|
|
source !== mainEntity.id
|
|
);
|
|
|
|
const targetDownStreamNode = updatedLineageData.downstreamEdges?.find(
|
|
(d) =>
|
|
(target?.includes(d.toEntity) || target?.includes(d.fromEntity)) &&
|
|
target !== mainEntity.id
|
|
);
|
|
|
|
const targetUpStreamNode = updatedLineageData.upstreamEdges?.find(
|
|
(u) =>
|
|
(target?.includes(u.toEntity) || target?.includes(u.fromEntity)) &&
|
|
target !== mainEntity.id
|
|
);
|
|
|
|
const isUpstream =
|
|
(!isNil(sourceUpStreamNode) && !isNil(targetDownStreamNode)) ||
|
|
!isNil(sourceUpStreamNode) ||
|
|
!isNil(targetUpStreamNode) ||
|
|
target?.includes(mainEntity.id);
|
|
|
|
const isDownstream =
|
|
(!isNil(sourceDownstreamNode) && !isNil(targetUpStreamNode)) ||
|
|
!isNil(sourceDownstreamNode) ||
|
|
!isNil(targetDownStreamNode) ||
|
|
source?.includes(mainEntity.id);
|
|
|
|
if (isUpstream) {
|
|
edgeType = 'upstream';
|
|
} else if (isDownstream) {
|
|
edgeType = 'downstream';
|
|
}
|
|
|
|
let targetNode = nodes?.find((n) => target?.includes(n.id));
|
|
|
|
let sourceNode = nodes?.find((n) => source?.includes(n.id));
|
|
|
|
if (isUndefined(targetNode) && sourceNode?.id !== selectedEntity?.id) {
|
|
targetNode = target?.includes(updatedLineageData.entity?.id)
|
|
? updatedLineageData.entity
|
|
: selectedEntity;
|
|
}
|
|
if (isUndefined(sourceNode) && targetNode?.id !== selectedEntity?.id) {
|
|
sourceNode = source?.includes(updatedLineageData.entity?.id)
|
|
? updatedLineageData.entity
|
|
: selectedEntity;
|
|
}
|
|
|
|
if (!isUndefined(sourceNode) && !isUndefined(targetNode)) {
|
|
const newEdge: AddLineage = {
|
|
edge: {
|
|
fromEntity: {
|
|
id: sourceNode.id,
|
|
type: sourceNode.type,
|
|
},
|
|
toEntity: {
|
|
id: targetNode.id,
|
|
type: targetNode.type,
|
|
},
|
|
},
|
|
};
|
|
|
|
if (columnConnection) {
|
|
const allEdge = [
|
|
...(entityLineage.downstreamEdges || []),
|
|
...(entityLineage.upstreamEdges || []),
|
|
];
|
|
const currentEdge = allEdge.find(
|
|
(e) => e.fromEntity === source && e.toEntity === target
|
|
)?.lineageDetails;
|
|
|
|
if (isUndefined(currentEdge)) {
|
|
newEdge.edge.lineageDetails = {
|
|
sqlQuery: '',
|
|
columnsLineage: [
|
|
{
|
|
fromColumns: [sourceHandle || ''],
|
|
toColumn: targetHandle || '',
|
|
},
|
|
],
|
|
};
|
|
} else {
|
|
const updatedColumnsLineage: ColumnLineage[] =
|
|
currentEdge.columnsLineage?.map((l) => {
|
|
if (l.toColumn === targetHandle) {
|
|
return {
|
|
...l,
|
|
fromColumns: [
|
|
...(l.fromColumns || []),
|
|
sourceHandle || '',
|
|
],
|
|
};
|
|
}
|
|
|
|
return l;
|
|
}) || [];
|
|
if (
|
|
!updatedColumnsLineage.find((l) => l.toColumn === targetHandle)
|
|
) {
|
|
updatedColumnsLineage.push({
|
|
fromColumns: [sourceHandle || ''],
|
|
toColumn: targetHandle || '',
|
|
});
|
|
}
|
|
newEdge.edge.lineageDetails = {
|
|
sqlQuery: currentEdge.sqlQuery || '',
|
|
columnsLineage: updatedColumnsLineage,
|
|
};
|
|
}
|
|
|
|
setEdges((els) => {
|
|
const newEdgeData = {
|
|
id: `column-${sourceHandle}-${targetHandle}-edge-${params.source}-${params.target}`,
|
|
source: source || '',
|
|
target: target || '',
|
|
sourceHandle: sourceHandle,
|
|
targetHandle: targetHandle,
|
|
type: isEditMode ? 'buttonedge' : 'custom',
|
|
markerEnd: {
|
|
type: MarkerType.ArrowClosed,
|
|
},
|
|
data: {
|
|
id: `column-${sourceHandle}-${targetHandle}-edge-${params.source}-${params.target}`,
|
|
source: params.source,
|
|
target: params.target,
|
|
sourceHandle: sourceHandle,
|
|
targetHandle: targetHandle,
|
|
sourceType: sourceNode?.type,
|
|
targetType: targetNode?.type,
|
|
isColumnLineage: true,
|
|
onEdgeClick,
|
|
},
|
|
};
|
|
|
|
return getUniqueFlowElements(addEdge(newEdgeData, els)) as Edge[];
|
|
});
|
|
}
|
|
|
|
setEdges((els) => {
|
|
const newEdgeData = {
|
|
id: `edge-${params.source}-${params.target}`,
|
|
source: `${params.source}`,
|
|
target: `${params.target}`,
|
|
type: isEditMode ? 'buttonedge' : 'custom',
|
|
style: { strokeWidth: '2px' },
|
|
markerEnd: {
|
|
type: MarkerType.ArrowClosed,
|
|
},
|
|
data: {
|
|
id: `edge-${params.source}-${params.target}`,
|
|
source: params.source,
|
|
target: params.target,
|
|
sourceType: sourceNode?.type,
|
|
targetType: targetNode?.type,
|
|
isColumnLineage: false,
|
|
onEdgeClick,
|
|
},
|
|
};
|
|
|
|
return getUniqueFlowElements(addEdge(newEdgeData, els)) as Edge[];
|
|
});
|
|
|
|
const updatedDownStreamEdges = () => {
|
|
return edgeType === 'downstream'
|
|
? [
|
|
...(updatedLineageData.downstreamEdges as EntityEdge[]),
|
|
{
|
|
fromEntity: sourceNode?.id as string,
|
|
toEntity: targetNode?.id as string,
|
|
lineageDetails: newEdge.edge.lineageDetails,
|
|
},
|
|
]
|
|
: updatedLineageData.downstreamEdges;
|
|
};
|
|
|
|
const updatedUpStreamEdges = () => {
|
|
return edgeType === 'upstream'
|
|
? [
|
|
...(updatedLineageData.upstreamEdges as EntityEdge[]),
|
|
{
|
|
fromEntity: sourceNode?.id as string,
|
|
toEntity: targetNode?.id as string,
|
|
lineageDetails: newEdge.edge.lineageDetails,
|
|
},
|
|
]
|
|
: updatedLineageData.upstreamEdges;
|
|
};
|
|
|
|
const getUpdatedNodes = () => {
|
|
return !isEmpty(selectedEntity)
|
|
? [
|
|
...(updatedLineageData.nodes as Array<EntityReference>),
|
|
selectedEntity,
|
|
]
|
|
: updatedLineageData.nodes;
|
|
};
|
|
|
|
setTimeout(() => {
|
|
addLineageHandler(newEdge)
|
|
.then(() => {
|
|
setStatus('success');
|
|
setLoading(false);
|
|
setTimeout(() => {
|
|
setUpdatedLineageData({
|
|
...updatedLineageData,
|
|
nodes: getUpdatedNodes(),
|
|
downstreamEdges: updatedDownStreamEdges(),
|
|
upstreamEdges: updatedUpStreamEdges(),
|
|
});
|
|
setStatus('initial');
|
|
}, 100);
|
|
setNewAddedNode({} as Node);
|
|
setSelectedEntity({} as EntityReference);
|
|
})
|
|
.catch(() => {
|
|
setStatus('initial');
|
|
setLoading(false);
|
|
});
|
|
}, 500);
|
|
}
|
|
}
|
|
},
|
|
[selectedNode, updatedLineageData, selectedEntity]
|
|
);
|
|
|
|
/**
|
|
* take element and perform onClick logic
|
|
* @param el
|
|
*/
|
|
const onNodeClick = (el: Node) => {
|
|
if (isNode(el)) {
|
|
const node = [
|
|
...(updatedLineageData.nodes as Array<EntityReference>),
|
|
updatedLineageData.entity,
|
|
].find((n) => el.id.includes(n.id));
|
|
|
|
if (!expandButton.current) {
|
|
selectNodeHandler(true, {
|
|
name: node?.name as string,
|
|
fqn: node?.fullyQualifiedName as string,
|
|
id: el.id,
|
|
displayName: node?.displayName,
|
|
type: node?.type as string,
|
|
entityId: node?.id as string,
|
|
});
|
|
} else {
|
|
expandButton.current = null;
|
|
}
|
|
}
|
|
};
|
|
|
|
// ToDo: remove below code once design flow finalized for column expand and colaps
|
|
|
|
const updateColumnsToNode = (columns: Column[], id: string) => {
|
|
setNodes((node) => {
|
|
const updatedNode = node.map((n) => {
|
|
if (n.id === id) {
|
|
const cols: { [key: string]: ModifiedColumn } = {};
|
|
columns.forEach((col) => {
|
|
cols[col.fullyQualifiedName || col.name] = {
|
|
...col,
|
|
type: isEditMode
|
|
? 'default'
|
|
: getColumnType(edges, col.fullyQualifiedName || col.name),
|
|
};
|
|
});
|
|
n.data.columns = cols;
|
|
}
|
|
|
|
return n;
|
|
});
|
|
|
|
return updatedNode;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* take node and get the columns for that node
|
|
* @param expandNode
|
|
*/
|
|
const getTableColumns = (expandNode?: EntityReference) => {
|
|
if (expandNode) {
|
|
getTableDetails(expandNode.id, ['columns'])
|
|
.then((res: AxiosResponse) => {
|
|
const tableId = expandNode.id;
|
|
const { columns } = res.data;
|
|
tableColumnsRef.current[tableId] = columns;
|
|
updateColumnsToNode(columns, tableId);
|
|
})
|
|
.catch((error: AxiosError) => {
|
|
showErrorToast(
|
|
error,
|
|
`Error while fetching ${getDataLabel(
|
|
expandNode.displayName,
|
|
expandNode.name,
|
|
true
|
|
)} columns`
|
|
);
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleNodeExpand = (isExpanded: boolean, node: EntityReference) => {
|
|
if (isExpanded) {
|
|
setNodes((prevState) => {
|
|
const newNodes = prevState.map((n) => {
|
|
if (n.id === node.id) {
|
|
const nodeId = node.id;
|
|
n.data.label = getNodeLabel(node, true);
|
|
n.data.isExpanded = true;
|
|
if (isUndefined(tableColumnsRef.current[nodeId])) {
|
|
getTableColumns(node);
|
|
} else {
|
|
const cols: { [key: string]: ModifiedColumn } = {};
|
|
tableColumnsRef.current[nodeId]?.forEach((col) => {
|
|
cols[col.fullyQualifiedName || col.name] = {
|
|
...col,
|
|
type: isEditMode
|
|
? 'default'
|
|
: getColumnType(edges, col.fullyQualifiedName || col.name),
|
|
};
|
|
});
|
|
n.data.columns = cols;
|
|
}
|
|
}
|
|
|
|
return n;
|
|
});
|
|
|
|
return newNodes;
|
|
});
|
|
} else {
|
|
setNodes((prevState) => {
|
|
const newNodes = prevState.map((n) => {
|
|
if (n.id === node.id) {
|
|
n.data.label = getNodeLabel(node);
|
|
n.data.isExpanded = false;
|
|
n.data.columns = undefined;
|
|
}
|
|
|
|
return n;
|
|
});
|
|
|
|
return newNodes;
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* take node and remove it from the graph
|
|
* @param node
|
|
*/
|
|
const removeNodeHandler = useCallback(
|
|
(node: Node) => {
|
|
// Get all edges for the flow
|
|
// const edges = elements.filter((element) => isEdge(element));
|
|
|
|
// Get edges connected to selected node
|
|
const edgesToRemove = getConnectedEdges([node], edges as Edge[]);
|
|
|
|
edgesToRemove.forEach((edge) => {
|
|
let targetNode = updatedLineageData.nodes?.find((n) =>
|
|
edge.target?.includes(n.id)
|
|
);
|
|
|
|
let sourceNode = updatedLineageData.nodes?.find((n) =>
|
|
edge.source?.includes(n.id)
|
|
);
|
|
|
|
if (isUndefined(targetNode)) {
|
|
targetNode = isEmpty(selectedEntity)
|
|
? updatedLineageData.entity
|
|
: selectedEntity;
|
|
}
|
|
if (isUndefined(sourceNode)) {
|
|
sourceNode = isEmpty(selectedEntity)
|
|
? updatedLineageData.entity
|
|
: selectedEntity;
|
|
}
|
|
|
|
removeEdgeHandler(
|
|
{
|
|
id: edge.id,
|
|
source: sourceNode,
|
|
target: targetNode,
|
|
},
|
|
true
|
|
);
|
|
});
|
|
|
|
setNodes(
|
|
(es) =>
|
|
getUniqueFlowElements(es.filter((n) => n.id !== node.id)) as Node[]
|
|
);
|
|
setNewAddedNode({} as Node);
|
|
},
|
|
[nodes, updatedLineageData]
|
|
);
|
|
|
|
/**
|
|
* handle node drag event
|
|
* @param event
|
|
*/
|
|
const onDragOver = (event: DragEvent) => {
|
|
event.preventDefault();
|
|
event.dataTransfer.dropEffect = 'move';
|
|
};
|
|
|
|
/**
|
|
* handle node drop event
|
|
* @param event
|
|
*/
|
|
const onDrop = (event: DragEvent) => {
|
|
event.preventDefault();
|
|
|
|
const reactFlowBounds = reactFlowWrapper.current?.getBoundingClientRect();
|
|
const type = event.dataTransfer.getData('application/reactflow');
|
|
if (type.trim()) {
|
|
const position = reactFlowInstance?.project({
|
|
x: event.clientX - (reactFlowBounds?.left ?? 0),
|
|
y: event.clientY - (reactFlowBounds?.top ?? 0),
|
|
});
|
|
const [label, nodeType] = type.split('-');
|
|
const newNode = {
|
|
id: uniqueId(),
|
|
nodeType,
|
|
position,
|
|
className: 'leaf-node',
|
|
connectable: false,
|
|
selectable: false,
|
|
type: 'default',
|
|
data: {
|
|
label: (
|
|
<div className="tw-relative">
|
|
{getNodeRemoveButton(() => {
|
|
removeNodeHandler(newNode as Node);
|
|
})}
|
|
<div className="tw-flex">
|
|
<SVGIcons
|
|
alt="entity-icon"
|
|
className="tw-mr-2"
|
|
icon={`${lowerCase(label)}-grey`}
|
|
width="16px"
|
|
/>
|
|
<NodeSuggestions
|
|
entityType={upperCase(label)}
|
|
onSelectHandler={selectedEntityHandler}
|
|
/>
|
|
</div>
|
|
</div>
|
|
),
|
|
removeNodeHandler,
|
|
isEditMode,
|
|
isNewNode: true,
|
|
},
|
|
};
|
|
setNewAddedNode(newNode as Node);
|
|
|
|
setNodes(
|
|
(es) => getUniqueFlowElements(es.concat(newNode as Node)) as Node[]
|
|
);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* After dropping node to graph user will search and select entity
|
|
* and this method will take care of changing node information based on selected entity.
|
|
*/
|
|
const onEntitySelect = () => {
|
|
if (!isEmpty(selectedEntity)) {
|
|
const isExistingNode = nodes.some((n) => n.id === selectedEntity.id);
|
|
if (isExistingNode) {
|
|
setNodes((es) =>
|
|
es
|
|
.map((n) =>
|
|
n.id.includes(selectedEntity.id)
|
|
? {
|
|
...n,
|
|
selectable: true,
|
|
className: `${n.className} selected`,
|
|
}
|
|
: n
|
|
)
|
|
.filter((es) => es.id !== newAddedNode.id)
|
|
);
|
|
setNewAddedNode({} as Node);
|
|
setSelectedEntity({} as EntityReference);
|
|
} else {
|
|
setNodes((es) => {
|
|
return es.map((el) => {
|
|
if (el.id === newAddedNode.id) {
|
|
return {
|
|
...el,
|
|
connectable: true,
|
|
selectable: true,
|
|
id: selectedEntity.id,
|
|
data: {
|
|
...el.data,
|
|
removeNodeHandler,
|
|
isEditMode,
|
|
label: (
|
|
<Fragment>
|
|
{getNodeLabel(selectedEntity)}
|
|
{getNodeRemoveButton(() => {
|
|
removeNodeHandler({
|
|
...el,
|
|
id: selectedEntity.id,
|
|
} as Node);
|
|
})}
|
|
</Fragment>
|
|
),
|
|
},
|
|
};
|
|
} else {
|
|
return el;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This method will handle the delete edge modal confirmation
|
|
*/
|
|
const onRemove = () => {
|
|
setDeletionState({ ...ELEMENT_DELETE_STATE, loading: true });
|
|
setTimeout(() => {
|
|
setDeletionState({ ...ELEMENT_DELETE_STATE, status: 'success' });
|
|
setTimeout(() => {
|
|
setShowDeleteModal(false);
|
|
setConfirmDelete(true);
|
|
setDeletionState((pre) => ({ ...pre, status: 'initial' }));
|
|
}, 500);
|
|
}, 500);
|
|
};
|
|
|
|
/**
|
|
*
|
|
* @returns Custom control elements
|
|
*/
|
|
const getCustomControlElements = () => {
|
|
return (
|
|
<CustomControls
|
|
className="tw-absolute tw-top-1 tw-right-3 tw-bottom-full tw-ml-4 tw-mt-4"
|
|
fitViewParams={{ minZoom: 0.5, maxZoom: 2.5 }}>
|
|
{!deleted && (
|
|
<NonAdminAction
|
|
html={
|
|
<Fragment>
|
|
<p>You do not have permission to edit the lineage</p>
|
|
</Fragment>
|
|
}
|
|
permission={Operation.UpdateLineage}>
|
|
<ControlButton
|
|
className={classNames(
|
|
'tw-h-9 tw-w-9 tw-rounded-full tw-px-1 tw-shadow-lg tw-cursor-pointer',
|
|
{
|
|
'tw-bg-primary': isEditMode,
|
|
'tw-bg-primary-hover-lite': !isEditMode,
|
|
},
|
|
{
|
|
'tw-opacity-40':
|
|
!userPermissions[Operation.UpdateLineage] &&
|
|
!isAuthDisabled &&
|
|
!isAdminUser,
|
|
}
|
|
)}
|
|
onClick={() => {
|
|
setEditMode((pre) => !pre && !deleted);
|
|
setSelectedNode({} as SelectedNode);
|
|
setIsDrawerOpen(false);
|
|
setNewAddedNode({} as Node);
|
|
}}>
|
|
{loading ? (
|
|
<Loader size="small" type="white" />
|
|
) : status === 'success' ? (
|
|
<FontAwesomeIcon className="tw-text-white" icon="check" />
|
|
) : (
|
|
<SVGIcons
|
|
alt="icon-edit-lineag"
|
|
className="tw--mt-1"
|
|
data-testid="edit-lineage"
|
|
icon={
|
|
!isEditMode
|
|
? 'icon-edit-lineage-color'
|
|
: 'icon-edit-lineage'
|
|
}
|
|
width="14"
|
|
/>
|
|
)}
|
|
</ControlButton>
|
|
</NonAdminAction>
|
|
)}
|
|
</CustomControls>
|
|
);
|
|
};
|
|
|
|
/**
|
|
*
|
|
* @returns Grid background if editmode is enabled otherwise null
|
|
*/
|
|
const getGraphBackGround = () => {
|
|
if (!isEditMode) {
|
|
return null;
|
|
} else {
|
|
return <Background gap={12} size={1} variant={BackgroundVariant.Lines} />;
|
|
}
|
|
};
|
|
|
|
/**
|
|
*
|
|
* @returns Side drawer if node is selected and view mode is enabled otherwise null
|
|
*/
|
|
const getEntityDrawer = () => {
|
|
if (isEmpty(selectedNode) || isEditMode) {
|
|
return null;
|
|
} else {
|
|
return (
|
|
<EntityInfoDrawer
|
|
isMainNode={selectedNode.name === updatedLineageData.entity?.name}
|
|
selectedNode={selectedNode}
|
|
show={isDrawerOpen && !isEditMode}
|
|
onCancel={closeDrawer}
|
|
/>
|
|
);
|
|
}
|
|
};
|
|
|
|
const getConfirmationModal = () => {
|
|
if (!showdeleteModal) {
|
|
return null;
|
|
} else {
|
|
return (
|
|
<ConfirmationModal
|
|
bodyText={getModalBodyText(selectedEdge)}
|
|
cancelText={
|
|
<span
|
|
className={classNames({
|
|
'tw-pointer-events-none tw-opacity-70': deletionState.loading,
|
|
})}>
|
|
Cancel
|
|
</span>
|
|
}
|
|
confirmText={
|
|
deletionState.loading ? (
|
|
<Loader size="small" type="white" />
|
|
) : deletionState.status === 'success' ? (
|
|
<FontAwesomeIcon className="tw-text-white" icon="check" />
|
|
) : (
|
|
'Confirm'
|
|
)
|
|
}
|
|
header="Remove lineage edge"
|
|
onCancel={() => {
|
|
setShowDeleteModal(false);
|
|
}}
|
|
onConfirm={onRemove}
|
|
/>
|
|
);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Reset State between view and edit mode toggle
|
|
*/
|
|
const resetViewEditState = () => {
|
|
setConfirmDelete(false);
|
|
};
|
|
|
|
/**
|
|
* Handle updated linegae nodes
|
|
* Change newly added node label based on entity:EntityReference
|
|
*/
|
|
const handleUpdatedLineageNode = () => {
|
|
const uNodes = updatedLineageData.nodes;
|
|
const newlyAddedNodeElement = nodes.find((el) => el?.data?.isNewNode);
|
|
const newlyAddedNode = uNodes?.find(
|
|
(node) => node.id === newlyAddedNodeElement?.id
|
|
);
|
|
|
|
setNodes((els) => {
|
|
return (els || []).map((el) => {
|
|
if (el.id === newlyAddedNode?.id) {
|
|
return {
|
|
...el,
|
|
data: { ...el.data, label: getNodeLabel(newlyAddedNode) },
|
|
};
|
|
} else {
|
|
return el;
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!deleted) {
|
|
const { node, edge } = getLayoutedElementsV1(setElementsHandleV1());
|
|
setNodes(node);
|
|
setEdges(edge);
|
|
|
|
resetViewEditState();
|
|
}
|
|
}, [lineageData, isNodeLoading, isEditMode]);
|
|
|
|
useEffect(() => {
|
|
const newNodes = updatedLineageData.nodes?.filter(
|
|
(n) =>
|
|
!isUndefined(
|
|
updatedLineageData.downstreamEdges?.find((d) => d.toEntity === n.id)
|
|
) ||
|
|
!isUndefined(
|
|
updatedLineageData.upstreamEdges?.find((u) => u.fromEntity === n.id)
|
|
)
|
|
);
|
|
entityLineageHandler({ ...updatedLineageData, nodes: newNodes });
|
|
}, [isEditMode]);
|
|
|
|
useEffect(() => {
|
|
handleUpdatedLineageNode();
|
|
}, [updatedLineageData]);
|
|
|
|
useEffect(() => {
|
|
onEntitySelect();
|
|
}, [selectedEntity]);
|
|
|
|
useEffect(() => {
|
|
if (selectedEdge.data?.isColumnLineage) {
|
|
removeColumnEdge(selectedEdge, confirmDelete);
|
|
} else {
|
|
removeEdgeHandler(selectedEdge, confirmDelete);
|
|
}
|
|
}, [selectedEdge, confirmDelete]);
|
|
|
|
useEffect(() => {
|
|
if (!isEmpty(entityLineage)) {
|
|
setLineageData(entityLineage);
|
|
setUpdatedLineageData(entityLineage);
|
|
}
|
|
}, [entityLineage]);
|
|
|
|
if (isLoading) {
|
|
return <Loader />;
|
|
}
|
|
|
|
return deleted ? (
|
|
getDeletedLineagePlaceholder()
|
|
) : (
|
|
<Fragment>
|
|
<div
|
|
className={classNames(
|
|
'tw-relative tw-h-full tw--ml-4 tw--mr-7 tw--mt-4'
|
|
)}
|
|
data-testid="lineage-container">
|
|
<div className="tw-w-full tw-h-full" ref={reactFlowWrapper}>
|
|
<ReactFlowProvider>
|
|
<ReactFlow
|
|
data-testid="react-flow-component"
|
|
edgeTypes={{ buttonedge: CustomEdge }}
|
|
edges={edges}
|
|
maxZoom={2}
|
|
minZoom={0.5}
|
|
nodeTypes={nodeTypes}
|
|
nodes={nodes}
|
|
nodesConnectable={isEditMode}
|
|
selectNodesOnDrag={false}
|
|
zoomOnDoubleClick={false}
|
|
zoomOnScroll={false}
|
|
onConnect={onConnect}
|
|
onDragOver={onDragOver}
|
|
onDrop={onDrop}
|
|
onEdgesChange={onEdgesChange}
|
|
onInit={(reactFlowInstance: ReactFlowInstance) => {
|
|
onLoad(reactFlowInstance, nodes.length);
|
|
setReactFlowInstance(reactFlowInstance);
|
|
}}
|
|
onNodeClick={(_e, node) => onNodeClick(node)}
|
|
onNodeContextMenu={onNodeContextMenu}
|
|
onNodeDrag={dragHandle}
|
|
onNodeDragStart={dragHandle}
|
|
onNodeDragStop={dragHandle}
|
|
onNodeMouseEnter={onNodeMouseEnter}
|
|
onNodeMouseLeave={onNodeMouseLeave}
|
|
onNodeMouseMove={onNodeMouseMove}
|
|
onNodesChange={onNodesChange}>
|
|
{getCustomControlElements()}
|
|
{getGraphBackGround()}
|
|
</ReactFlow>
|
|
</ReactFlowProvider>
|
|
</div>
|
|
{getEntityDrawer()}
|
|
<EntityLineageSidebar newAddedNode={newAddedNode} show={isEditMode} />
|
|
{getConfirmationModal()}
|
|
</div>
|
|
</Fragment>
|
|
);
|
|
};
|
|
|
|
export default withLoader<EntityLineageProp>(Entitylineage);
|