mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-07 14:31:08 +00:00
895 lines
27 KiB
TypeScript
895 lines
27 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 { AxiosResponse } from 'axios';
|
|
import classNames from 'classnames';
|
|
import { isEmpty, isUndefined, lowerCase, uniqueId, upperCase } from 'lodash';
|
|
import { LoadingState } from 'Models';
|
|
import React, {
|
|
DragEvent,
|
|
Fragment,
|
|
FunctionComponent,
|
|
useEffect,
|
|
useRef,
|
|
useState,
|
|
} from 'react';
|
|
import ReactFlow, {
|
|
addEdge,
|
|
ArrowHeadType,
|
|
Background,
|
|
BackgroundVariant,
|
|
Connection,
|
|
Edge,
|
|
Elements,
|
|
FlowElement,
|
|
OnLoadParams,
|
|
ReactFlowProvider,
|
|
removeElements,
|
|
} from 'react-flow-renderer';
|
|
import { useAuthContext } from '../../auth-provider/AuthProvider';
|
|
import { getTableDetails } from '../../axiosAPIs/tableAPI';
|
|
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 useToastContext from '../../hooks/useToastContext';
|
|
import {
|
|
dragHandle,
|
|
getDataLabel,
|
|
getDeletedLineagePlaceholder,
|
|
getLayoutedElements,
|
|
getLineageData,
|
|
getModalBodyText,
|
|
onLoad,
|
|
onNodeContextMenu,
|
|
onNodeMouseEnter,
|
|
onNodeMouseLeave,
|
|
onNodeMouseMove,
|
|
} from '../../utils/EntityLineageUtils';
|
|
import SVGIcons from '../../utils/SvgUtils';
|
|
import { getEntityIcon } from '../../utils/TableUtils';
|
|
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,
|
|
Edge as NewEdge,
|
|
EdgeData,
|
|
EntityLineageProp,
|
|
SelectedEdge,
|
|
SelectedNode,
|
|
} from './EntityLineage.interface';
|
|
import EntityLineageSidebar from './EntityLineageSidebar.component';
|
|
import NodeSuggestions from './NodeSuggestions.component';
|
|
|
|
const Entitylineage: FunctionComponent<EntityLineageProp> = ({
|
|
entityLineage,
|
|
loadNodeHandler,
|
|
lineageLeafNodes,
|
|
isNodeLoading,
|
|
deleted,
|
|
addLineageHandler,
|
|
removeLineageHandler,
|
|
entityLineageHandler,
|
|
}: EntityLineageProp) => {
|
|
const showToast = useToastContext();
|
|
const { userPermissions, isAdminUser } = useAuth();
|
|
const { isAuthDisabled } = useAuthContext();
|
|
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
|
const [lineageData, setLineageData] = useState<EntityLineage>(entityLineage);
|
|
const [reactFlowInstance, setReactFlowInstance] = useState<OnLoadParams>();
|
|
const [isDrawerOpen, setIsDrawerOpen] = useState<boolean>(false);
|
|
const [selectedNode, setSelectedNode] = useState<SelectedNode>(
|
|
{} as SelectedNode
|
|
);
|
|
const expandButton = useRef<HTMLButtonElement | null>(null);
|
|
const [expandNode, setExpandNode] = useState<EntityReference | undefined>(
|
|
undefined
|
|
);
|
|
const [isEditMode, setEditMode] = useState<boolean>(false);
|
|
|
|
const [tableColumns, setTableColumns] = useState<Column[]>([] as Column[]);
|
|
const [newAddedNode, setNewAddedNode] = useState<FlowElement>(
|
|
{} as FlowElement
|
|
);
|
|
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: Exclude<LoadingState, 'waiting'>;
|
|
}>({
|
|
loading: false,
|
|
status: 'initial',
|
|
});
|
|
|
|
/**
|
|
* 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: FlowElement) => {
|
|
return `${
|
|
node.id.includes(lineageData.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 lable for given node
|
|
*/
|
|
const getNodeLable = (node: EntityReference) => {
|
|
return (
|
|
<>
|
|
{node.type === 'table' && !isEditMode ? (
|
|
<button
|
|
className="tw-absolute tw--top-4 tw--left-5 tw-cursor-pointer tw-z-9999"
|
|
onClick={(e) => {
|
|
expandButton.current = expandButton.current
|
|
? null
|
|
: e.currentTarget;
|
|
setExpandNode(expandNode ? undefined : node);
|
|
setIsDrawerOpen(false);
|
|
}}>
|
|
<SVGIcons
|
|
alt="plus"
|
|
icon={expandNode?.id === node.id ? 'icon-minus' : 'icon-plus'}
|
|
width="16px"
|
|
/>
|
|
</button>
|
|
) : null}
|
|
<p className="tw-flex">
|
|
<span className="tw-mr-2">{getEntityIcon(node.type)}</span>
|
|
{getDataLabel(node.displayName, node.name, '.', false, node.type)}
|
|
</p>
|
|
</>
|
|
);
|
|
};
|
|
|
|
/**
|
|
*
|
|
* @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
|
|
setElements((es) => es.filter((e) => e.id !== data.id));
|
|
|
|
/**
|
|
* Get new downstreamEdges
|
|
*/
|
|
const newDownStreamEdges = lineageData.downstreamEdges?.filter(
|
|
(dn) =>
|
|
!lineageData.downstreamEdges?.find(
|
|
() =>
|
|
edgeData.fromId === dn.fromEntity && edgeData.toId === dn.toEntity
|
|
)
|
|
);
|
|
|
|
/**
|
|
* Get new upstreamEdges
|
|
*/
|
|
const newUpStreamEdges = lineageData.upstreamEdges?.filter(
|
|
(up) =>
|
|
!lineageData.upstreamEdges?.find(
|
|
() =>
|
|
edgeData.fromId === up.fromEntity && edgeData.toId === up.toEntity
|
|
)
|
|
);
|
|
|
|
/**
|
|
* Get new nodes that have either downstreamEdge or upstreamEdge
|
|
*/
|
|
const newNodes = lineageData.nodes?.filter(
|
|
(n) =>
|
|
!isUndefined(newDownStreamEdges?.find((d) => d.toEntity === n.id)) ||
|
|
!isUndefined(newUpStreamEdges?.find((u) => u.fromEntity === n.id))
|
|
);
|
|
|
|
setNewAddedNode({} as FlowElement);
|
|
setSelectedEntity({} as EntityReference);
|
|
entityLineageHandler({
|
|
...lineageData,
|
|
nodes: newNodes,
|
|
downstreamEdges: newDownStreamEdges,
|
|
upstreamEdges: newUpStreamEdges,
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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(() => {
|
|
let targetNode = lineageData.nodes?.find((n) =>
|
|
data.target?.includes(n.id)
|
|
);
|
|
|
|
let sourceNode = lineageData.nodes?.find((n) =>
|
|
data.source?.includes(n.id)
|
|
);
|
|
|
|
if (isUndefined(targetNode)) {
|
|
targetNode = isEmpty(selectedEntity)
|
|
? lineageData.entity
|
|
: selectedEntity;
|
|
}
|
|
if (isUndefined(sourceNode)) {
|
|
sourceNode = isEmpty(selectedEntity)
|
|
? lineageData.entity
|
|
: selectedEntity;
|
|
}
|
|
|
|
return { id: data.id, source: sourceNode, target: targetNode };
|
|
});
|
|
};
|
|
|
|
/**
|
|
*
|
|
* @returns unique flow elements
|
|
*/
|
|
const setElementsHandle = () => {
|
|
const flag: { [x: string]: boolean } = {};
|
|
const uniqueElements: Elements = [];
|
|
if (!isEmpty(lineageData)) {
|
|
const graphElements = getLineageData(
|
|
lineageData,
|
|
selectNodeHandler,
|
|
loadNodeHandler,
|
|
lineageLeafNodes,
|
|
isNodeLoading,
|
|
getNodeLable,
|
|
isEditMode,
|
|
'buttonedge',
|
|
onEdgeClick
|
|
) as Elements;
|
|
|
|
graphElements.forEach((elem) => {
|
|
if (!flag[elem.id]) {
|
|
flag[elem.id] = true;
|
|
uniqueElements.push(elem);
|
|
}
|
|
});
|
|
}
|
|
|
|
return uniqueElements;
|
|
};
|
|
|
|
const [elements, setElements] = useState<Elements>(
|
|
getLayoutedElements(setElementsHandle())
|
|
);
|
|
|
|
/**
|
|
* take boolean value as input and reset selected node
|
|
* @param value
|
|
*/
|
|
const closeDrawer = (value: boolean) => {
|
|
setIsDrawerOpen(value);
|
|
setElements((prevElements) => {
|
|
return prevElements.map((el) => {
|
|
if (el.id === selectedNode.id) {
|
|
return {
|
|
...el,
|
|
className: getNodeClass(el),
|
|
};
|
|
} else {
|
|
return el;
|
|
}
|
|
});
|
|
});
|
|
setSelectedNode({} as SelectedNode);
|
|
};
|
|
|
|
/**
|
|
* take list of elements to remove it from the graph
|
|
* @param elementsToRemove
|
|
* @returns updated elements list
|
|
*/
|
|
const onElementsRemove = (elementsToRemove: Elements) =>
|
|
setElements((els) => removeElements(elementsToRemove, els));
|
|
|
|
/**
|
|
* take edge or connection to add new element in the graph
|
|
* @param params
|
|
*/
|
|
const onConnect = (params: Edge | Connection) => {
|
|
setStatus('waiting');
|
|
setLoading(true);
|
|
const { target, source } = params;
|
|
|
|
const nodes = [
|
|
...(lineageData.nodes as EntityReference[]),
|
|
lineageData.entity,
|
|
];
|
|
|
|
const sourceDownstreamNode = lineageData.downstreamEdges?.find((d) =>
|
|
source?.includes(d.toEntity as string)
|
|
);
|
|
|
|
const sourceUpStreamNode = lineageData.upstreamEdges?.find((u) =>
|
|
source?.includes(u.fromEntity as string)
|
|
);
|
|
|
|
const targetDownStreamNode = lineageData.downstreamEdges?.find((d) =>
|
|
target?.includes(d.toEntity as string)
|
|
);
|
|
|
|
const targetUpStreamNode = lineageData.upstreamEdges?.find((u) =>
|
|
target?.includes(u.fromEntity as string)
|
|
);
|
|
|
|
let targetNode = nodes?.find((n) => target?.includes(n.id));
|
|
|
|
let sourceNode = nodes?.find((n) => source?.includes(n.id));
|
|
|
|
if (isUndefined(targetNode)) {
|
|
targetNode = target?.includes(lineageData.entity?.id)
|
|
? lineageData.entity
|
|
: selectedEntity;
|
|
}
|
|
if (isUndefined(sourceNode)) {
|
|
sourceNode = source?.includes(lineageData.entity?.id)
|
|
? lineageData.entity
|
|
: selectedEntity;
|
|
}
|
|
|
|
const newEdge: NewEdge = {
|
|
edge: {
|
|
fromEntity: {
|
|
id: sourceNode.id,
|
|
type: sourceNode.type,
|
|
},
|
|
toEntity: {
|
|
id: targetNode.id,
|
|
type: targetNode.type,
|
|
},
|
|
},
|
|
};
|
|
|
|
setElements((els) =>
|
|
addEdge({ ...params, arrowHeadType: ArrowHeadType.ArrowClosed }, els)
|
|
);
|
|
|
|
setTimeout(() => {
|
|
addLineageHandler(newEdge)
|
|
.then(() => {
|
|
setStatus('success');
|
|
setLoading(false);
|
|
setTimeout(() => {
|
|
entityLineageHandler({
|
|
...lineageData,
|
|
nodes: selectedEntity
|
|
? [
|
|
...(lineageData.nodes as Array<EntityReference>),
|
|
selectedEntity,
|
|
]
|
|
: lineageData.nodes,
|
|
downstreamEdges:
|
|
!isUndefined(sourceUpStreamNode) ||
|
|
!isUndefined(targetUpStreamNode) ||
|
|
targetNode?.id === selectedEntity.id
|
|
? [
|
|
...(lineageData.downstreamEdges as EntityEdge[]),
|
|
{
|
|
fromEntity: sourceNode?.id,
|
|
toEntity: targetNode?.id,
|
|
},
|
|
]
|
|
: lineageData.downstreamEdges,
|
|
upstreamEdges:
|
|
!isUndefined(sourceDownstreamNode) ||
|
|
!isUndefined(targetDownStreamNode) ||
|
|
sourceNode?.id === selectedEntity.id
|
|
? [
|
|
...(lineageData.upstreamEdges as EntityEdge[]),
|
|
{
|
|
fromEntity: sourceNode?.id,
|
|
toEntity: targetNode?.id,
|
|
},
|
|
]
|
|
: lineageData.upstreamEdges,
|
|
});
|
|
setStatus('initial');
|
|
}, 100);
|
|
setNewAddedNode({} as FlowElement);
|
|
setSelectedEntity({} as EntityReference);
|
|
})
|
|
.catch(() => {
|
|
setStatus('initial');
|
|
setLoading(false);
|
|
});
|
|
}, 500);
|
|
};
|
|
|
|
/**
|
|
* take element and perform onClick logic
|
|
* @param el
|
|
*/
|
|
const onElementClick = (el: FlowElement) => {
|
|
const node = [
|
|
...(lineageData.nodes as Array<EntityReference>),
|
|
lineageData.entity,
|
|
].find((n) => el.id.includes(n.id));
|
|
if (!expandButton.current) {
|
|
selectNodeHandler(true, {
|
|
name: node?.name as string,
|
|
id: el.id,
|
|
displayName: node?.displayName,
|
|
type: node?.type as string,
|
|
entityId: node?.id as string,
|
|
});
|
|
setElements((prevElements) => {
|
|
return prevElements.map((preEl) => {
|
|
if (preEl.id === el.id) {
|
|
return { ...preEl, className: `${preEl.className} selected-node` };
|
|
} else {
|
|
return {
|
|
...preEl,
|
|
className: getNodeClass(preEl),
|
|
};
|
|
}
|
|
});
|
|
});
|
|
} else {
|
|
expandButton.current = null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* this method is used for table entity to show table columns
|
|
* @param tableColumns
|
|
*/
|
|
const onNodeExpand = (tableColumns?: Column[]) => {
|
|
const elements = getLayoutedElements(setElementsHandle());
|
|
setElements(
|
|
elements.map((preEl) => {
|
|
if (preEl.id.includes(expandNode?.id as string)) {
|
|
return {
|
|
...preEl,
|
|
className: `${preEl.className} selected-node`,
|
|
data: { ...preEl.data, columns: tableColumns },
|
|
};
|
|
} else {
|
|
return {
|
|
...preEl,
|
|
className: getNodeClass(preEl),
|
|
};
|
|
}
|
|
})
|
|
);
|
|
};
|
|
|
|
/**
|
|
* 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 { columns } = res.data;
|
|
setTableColumns(columns);
|
|
})
|
|
.catch(() => {
|
|
showToast({
|
|
variant: 'error',
|
|
body: `Error while fetching ${getDataLabel(
|
|
expandNode.displayName,
|
|
expandNode.name,
|
|
'.',
|
|
true
|
|
)} columns`,
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* take node and remove it from the graph
|
|
* @param node
|
|
*/
|
|
const removeNodeHandler = (node: FlowElement) => {
|
|
setElements((es) => es.filter((n) => n.id !== node.id));
|
|
setNewAddedNode({} as FlowElement);
|
|
};
|
|
|
|
/**
|
|
* 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 [lable, nodeType] = type.split('-');
|
|
const newNode = {
|
|
id: uniqueId(),
|
|
nodeType,
|
|
position,
|
|
className: 'leaf-node',
|
|
connectable: false,
|
|
data: {
|
|
label: (
|
|
<div className="tw-relative">
|
|
<button
|
|
className="tw-absolute tw--top-4 tw--right-6 tw-cursor-pointer tw-z-9999 tw-bg-body-hover tw-rounded-full"
|
|
onClick={() => {
|
|
removeNodeHandler(newNode as FlowElement);
|
|
}}>
|
|
<SVGIcons
|
|
alt="times-circle"
|
|
icon="icon-times-circle"
|
|
width="16px"
|
|
/>
|
|
</button>
|
|
<div className="tw-flex">
|
|
<SVGIcons
|
|
alt="entity-icon"
|
|
className="tw-mr-2"
|
|
icon={`${lowerCase(lable)}-grey`}
|
|
width="16px"
|
|
/>
|
|
<NodeSuggestions
|
|
entityType={upperCase(lable)}
|
|
onSelectHandler={selectedEntityHandler}
|
|
/>
|
|
</div>
|
|
</div>
|
|
),
|
|
isNewNode: true,
|
|
},
|
|
};
|
|
setNewAddedNode(newNode as FlowElement);
|
|
|
|
setElements((es) => es.concat(newNode as FlowElement));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* handle onNode select logic
|
|
*/
|
|
const onEntitySelect = () => {
|
|
if (!isEmpty(selectedEntity)) {
|
|
const isExistingNode = elements.some((n) =>
|
|
n.id.includes(selectedEntity.id)
|
|
);
|
|
if (isExistingNode) {
|
|
setElements((es) =>
|
|
es
|
|
.map((n) =>
|
|
n.id.includes(selectedEntity.id)
|
|
? { ...n, className: `${n.className} selected-node` }
|
|
: n
|
|
)
|
|
.filter((es) => es.id !== newAddedNode.id)
|
|
);
|
|
setNewAddedNode({} as FlowElement);
|
|
setSelectedEntity({} as EntityReference);
|
|
} else {
|
|
setElements((es) => {
|
|
return es.map((el) => {
|
|
if (el.id === newAddedNode.id) {
|
|
return {
|
|
...el,
|
|
connectable: true,
|
|
id: selectedEntity.id,
|
|
data: {
|
|
label: (
|
|
<Fragment>
|
|
{getNodeLable(selectedEntity)}
|
|
<button
|
|
className="tw-absolute tw--top-5 tw--right-4 tw-cursor-pointer tw-z-9999 tw-bg-body-hover tw-rounded-full"
|
|
onClick={() => {
|
|
removeNodeHandler({
|
|
...el,
|
|
id: selectedEntity.id,
|
|
} as FlowElement);
|
|
}}>
|
|
<SVGIcons
|
|
alt="times-circle"
|
|
icon="icon-times-circle"
|
|
width="16px"
|
|
/>
|
|
</button>
|
|
</Fragment>
|
|
),
|
|
},
|
|
};
|
|
} else {
|
|
return el;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
const onRemove = () => {
|
|
setDeletionState({ loading: true, status: 'initial' });
|
|
setTimeout(() => {
|
|
setDeletionState({ loading: false, status: 'success' });
|
|
setTimeout(() => {
|
|
setShowDeleteModal(false);
|
|
setConfirmDelete(true);
|
|
setDeletionState((pre) => ({ ...pre, status: 'initial' }));
|
|
}, 500);
|
|
}, 500);
|
|
};
|
|
|
|
useEffect(() => {
|
|
setElements(getLayoutedElements(setElementsHandle()));
|
|
setExpandNode(undefined);
|
|
setTableColumns([]);
|
|
setConfirmDelete(false);
|
|
}, [lineageData, isNodeLoading, isEditMode]);
|
|
|
|
useEffect(() => {
|
|
onNodeExpand();
|
|
getTableColumns(expandNode);
|
|
}, [expandNode]);
|
|
|
|
useEffect(() => {
|
|
if (!isEmpty(selectedNode)) {
|
|
setExpandNode(undefined);
|
|
}
|
|
setElements((pre) => {
|
|
return pre.map((el) => ({ ...el, data: { ...el.data, selectedNode } }));
|
|
});
|
|
}, [selectedNode]);
|
|
|
|
useEffect(() => {
|
|
if (tableColumns.length) {
|
|
onNodeExpand(tableColumns);
|
|
}
|
|
}, [tableColumns]);
|
|
|
|
useEffect(() => {
|
|
onEntitySelect();
|
|
}, [selectedEntity]);
|
|
|
|
useEffect(() => {
|
|
removeEdgeHandler(selectedEdge, confirmDelete);
|
|
}, [selectedEdge, confirmDelete]);
|
|
|
|
useEffect(() => {
|
|
if (!isEmpty(entityLineage)) {
|
|
setLineageData(entityLineage);
|
|
}
|
|
}, [entityLineage]);
|
|
|
|
return (
|
|
<Fragment>
|
|
{!deleted ? (
|
|
<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 }}
|
|
elements={elements as Elements}
|
|
elementsSelectable={!isEditMode}
|
|
maxZoom={2}
|
|
minZoom={0.5}
|
|
nodeTypes={{
|
|
output: CustomNode,
|
|
input: CustomNode,
|
|
default: CustomNode,
|
|
}}
|
|
nodesConnectable={isEditMode}
|
|
selectNodesOnDrag={false}
|
|
zoomOnDoubleClick={false}
|
|
zoomOnPinch={false}
|
|
zoomOnScroll={false}
|
|
onConnect={onConnect}
|
|
onDragOver={onDragOver}
|
|
onDrop={onDrop}
|
|
onElementClick={(_e, el) => onElementClick(el)}
|
|
onElementsRemove={onElementsRemove}
|
|
onLoad={(reactFlowInstance: OnLoadParams) => {
|
|
onLoad(reactFlowInstance);
|
|
setReactFlowInstance(reactFlowInstance);
|
|
}}
|
|
onNodeContextMenu={onNodeContextMenu}
|
|
onNodeDrag={dragHandle}
|
|
onNodeDragStart={dragHandle}
|
|
onNodeDragStop={dragHandle}
|
|
onNodeMouseEnter={onNodeMouseEnter}
|
|
onNodeMouseLeave={onNodeMouseLeave}
|
|
onNodeMouseMove={onNodeMouseMove}>
|
|
<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={
|
|
<>
|
|
<p>You do not have permission to edit the lineage</p>
|
|
</>
|
|
}
|
|
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 FlowElement);
|
|
}}>
|
|
{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>
|
|
{isEditMode ? (
|
|
<Background
|
|
gap={12}
|
|
size={1}
|
|
variant={BackgroundVariant.Lines}
|
|
/>
|
|
) : null}
|
|
</ReactFlow>
|
|
</ReactFlowProvider>
|
|
</div>
|
|
<EntityInfoDrawer
|
|
isMainNode={selectedNode.name === lineageData.entity?.name}
|
|
selectedNode={selectedNode}
|
|
show={isDrawerOpen && !isEditMode}
|
|
onCancel={closeDrawer}
|
|
/>
|
|
<EntityLineageSidebar newAddedNode={newAddedNode} show={isEditMode} />
|
|
{showdeleteModal ? (
|
|
<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}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
) : (
|
|
getDeletedLineagePlaceholder()
|
|
)}
|
|
</Fragment>
|
|
);
|
|
};
|
|
|
|
export default withLoader<EntityLineageProp>(Entitylineage);
|