diff --git a/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/CustomEdge.component.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/CustomEdge.component.test.tsx
index 23e77ab4cf9..286ba89baf4 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/CustomEdge.component.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/CustomEdge.component.test.tsx
@@ -11,7 +11,12 @@
* limitations under the License.
*/
-import { findByTestId, queryByTestId, render } from '@testing-library/react';
+import {
+ findAllByTestId,
+ findByTestId,
+ queryByTestId,
+ render,
+} from '@testing-library/react';
import React from 'react';
import { ArrowHeadType, EdgeProps, Position } from 'react-flow-renderer';
import { MemoryRouter } from 'react-router-dom';
@@ -40,6 +45,7 @@ const mockCustomEdgeProp = {
id: 'node1',
},
},
+ selected: true,
} as EdgeProps;
describe('Test CustomEdge Component', () => {
@@ -49,27 +55,24 @@ describe('Test CustomEdge Component', () => {
});
const deleteButton = await findByTestId(container, 'delete-button');
- const edgePathElement = await findByTestId(
+ const edgePathElement = await findAllByTestId(
container,
'react-flow-edge-path'
);
expect(deleteButton).toBeInTheDocument();
- expect(edgePathElement).toBeInTheDocument();
+ expect(edgePathElement).toHaveLength(edgePathElement.length);
});
- it('Check if CustomEdge has selectedNode as empty object', async () => {
+ it('Check if CustomEdge has selected as false', async () => {
const { container } = render(
- ,
+ ,
{
wrapper: MemoryRouter,
}
);
- const edgePathElement = await findByTestId(
+ const edgePathElement = await findAllByTestId(
container,
'react-flow-edge-path'
);
@@ -77,6 +80,6 @@ describe('Test CustomEdge Component', () => {
const deleteButton = queryByTestId(container, 'delete-button');
expect(deleteButton).not.toBeInTheDocument();
- expect(edgePathElement).toBeInTheDocument();
+ expect(edgePathElement).toHaveLength(edgePathElement.length);
});
});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/CustomEdge.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/CustomEdge.component.tsx
index 99401e4aea1..be7bfad468d 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/CustomEdge.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/CustomEdge.component.tsx
@@ -34,8 +34,10 @@ export const CustomEdge = ({
arrowHeadType,
markerEndId,
data,
+ selected,
}: EdgeProps) => {
- const { onEdgeClick, selectedNode, ...rest } = data;
+ const { onEdgeClick, ...rest } = data;
+ const offset = 4;
const edgePath = getBezierPath({
sourceX,
@@ -45,6 +47,22 @@ export const CustomEdge = ({
targetY,
targetPosition,
});
+ const invisibleEdgePath = getBezierPath({
+ sourceX: sourceX + offset,
+ sourceY: sourceY + offset,
+ sourcePosition,
+ targetX: targetX + offset,
+ targetY: targetY + offset,
+ targetPosition,
+ });
+ const invisibleEdgePath1 = getBezierPath({
+ sourceX: sourceX - offset,
+ sourceY: sourceY - offset,
+ sourcePosition,
+ targetX: targetX - offset,
+ targetY: targetY - offset,
+ targetPosition,
+ });
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
const [edgeCenterX, edgeCenterY] = getEdgeCenter({
sourceX,
@@ -53,6 +71,19 @@ export const CustomEdge = ({
targetY,
});
+ const getInvisiblePath = (path: string) => {
+ return (
+
+ );
+ };
+
return (
- {(rest as CustomEdgeData)?.source?.includes(selectedNode?.id) ||
- (rest as CustomEdgeData)?.target?.includes(selectedNode?.id) ? (
+ {getInvisiblePath(invisibleEdgePath)}
+ {getInvisiblePath(invisibleEdgePath1)}
+
+ {selected ? (
+ x={edgeCenterX - foreignObjectSize / offset}
+ y={edgeCenterY - foreignObjectSize / offset}>
);
}
};
-/* eslint-disable-next-line */
-const CustomNode = (props: any) => {
+const CustomNode = (props: NodeProps) => {
+ const { data, type, isConnectable, selected } = props;
/* eslint-disable-next-line */
- const { data, type, isConnectable } = props;
- /* eslint-disable-next-line */
- const { label, columns, isNewNode } = data;
+ const { label, columns, isNewNode, removeNodeHandler, isEditMode } = data;
return (
{getHandle(type, isConnectable, isNewNode)}
{/* Node label could be simple text or reactNode */}
- {label}
+ {label}{' '}
+ {selected && isEditMode
+ ? getNodeRemoveButton(() => {
+ removeNodeHandler?.(props);
+ })
+ : null}
{columns?.length ? (
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 1adb767aae7..58b59da0cda 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
@@ -20,7 +20,9 @@ import React, {
DragEvent,
Fragment,
FunctionComponent,
+ useCallback,
useEffect,
+ useMemo,
useRef,
useState,
} from 'react';
@@ -33,12 +35,16 @@ import ReactFlow, {
Edge,
Elements,
FlowElement,
+ getConnectedEdges,
+ isEdge,
+ isNode,
+ Node,
OnLoadParams,
ReactFlowProvider,
- removeElements,
} 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 { Column } from '../../generated/entity/data/table';
import { Operation } from '../../generated/entity/policies/accessControl/rule';
import {
@@ -55,6 +61,8 @@ import {
getLayoutedElements,
getLineageData,
getModalBodyText,
+ getNodeRemoveButton,
+ getUniqueFlowElements,
onLoad,
onNodeContextMenu,
onNodeMouseEnter,
@@ -75,6 +83,7 @@ import {
CustomEdgeData,
Edge as NewEdge,
EdgeData,
+ ElementLoadingState,
EntityLineageProp,
SelectedEdge,
SelectedNode,
@@ -126,11 +135,27 @@ const Entitylineage: FunctionComponent
= ({
const [status, setStatus] = useState('initial');
const [deletionState, setDeletionState] = useState<{
loading: boolean;
- status: Exclude;
- }>({
- loading: false,
- status: 'initial',
- });
+ status: ElementLoadingState;
+ }>(ELEMENT_DELETE_STATE);
+
+ /**
+ * 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);
+
+ /**
+ * 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
@@ -139,7 +164,7 @@ const Entitylineage: FunctionComponent = ({
*/
const getNodeClass = (node: FlowElement) => {
return `${
- node.id.includes(lineageData.entity?.id) && !isEditMode
+ node.id.includes(updatedLineageData.entity?.id) && !isEditMode
? 'leaf-node core'
: 'leaf-node'
}`;
@@ -166,9 +191,9 @@ const Entitylineage: FunctionComponent = ({
/**
*
* @param node
- * @returns lable for given node
+ * @returns label for given node
*/
- const getNodeLable = (node: EntityReference) => {
+ const getNodeLabel = (node: EntityReference) => {
return (
{node.type === 'table' && !isEditMode ? (
@@ -217,14 +242,16 @@ const Entitylineage: FunctionComponent = ({
removeLineageHandler(edgeData);
// eslint-disable-next-line @typescript-eslint/no-use-before-define
- setElements((es) => es.filter((e) => e.id !== data.id));
+ setElements((es) =>
+ getUniqueFlowElements(es.filter((e) => e.id !== data.id))
+ );
/**
* Get new downstreamEdges
*/
- const newDownStreamEdges = lineageData.downstreamEdges?.filter(
+ const newDownStreamEdges = updatedLineageData.downstreamEdges?.filter(
(dn) =>
- !lineageData.downstreamEdges?.find(
+ !updatedLineageData.downstreamEdges?.find(
() =>
edgeData.fromId === dn.fromEntity && edgeData.toId === dn.toEntity
)
@@ -233,31 +260,22 @@ const Entitylineage: FunctionComponent = ({
/**
* Get new upstreamEdges
*/
- const newUpStreamEdges = lineageData.upstreamEdges?.filter(
+ const newUpStreamEdges = updatedLineageData.upstreamEdges?.filter(
(up) =>
- !lineageData.upstreamEdges?.find(
+ !updatedLineageData.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,
+ setUpdatedLineageData({
+ ...updatedLineageData,
downstreamEdges: newDownStreamEdges,
upstreamEdges: newUpStreamEdges,
});
+ setConfirmDelete(false);
}
};
@@ -273,22 +291,22 @@ const Entitylineage: FunctionComponent = ({
setShowDeleteModal(true);
evt.stopPropagation();
setSelectedEdge(() => {
- let targetNode = lineageData.nodes?.find((n) =>
+ let targetNode = updatedLineageData.nodes?.find((n) =>
data.target?.includes(n.id)
);
- let sourceNode = lineageData.nodes?.find((n) =>
+ let sourceNode = updatedLineageData.nodes?.find((n) =>
data.source?.includes(n.id)
);
if (isUndefined(targetNode)) {
targetNode = isEmpty(selectedEntity)
- ? lineageData.entity
+ ? updatedLineageData.entity
: selectedEntity;
}
if (isUndefined(sourceNode)) {
sourceNode = isEmpty(selectedEntity)
- ? lineageData.entity
+ ? updatedLineageData.entity
: selectedEntity;
}
@@ -301,27 +319,23 @@ const Entitylineage: FunctionComponent = ({
* @returns unique flow elements
*/
const setElementsHandle = () => {
- const flag: { [x: string]: boolean } = {};
- const uniqueElements: Elements = [];
- if (!isEmpty(lineageData)) {
+ let uniqueElements: Elements = [];
+ if (!isEmpty(updatedLineageData)) {
const graphElements = getLineageData(
- lineageData,
+ updatedLineageData,
selectNodeHandler,
loadNodeHandler,
lineageLeafNodes,
isNodeLoading,
- getNodeLable,
+ getNodeLabel,
isEditMode,
'buttonedge',
- onEdgeClick
+ onEdgeClick,
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ removeNodeHandler
) as Elements;
- graphElements.forEach((elem) => {
- if (!flag[elem.id]) {
- flag[elem.id] = true;
- uniqueElements.push(elem);
- }
- });
+ uniqueElements = getUniqueFlowElements(graphElements);
}
return uniqueElements;
@@ -352,159 +366,173 @@ const Entitylineage: FunctionComponent = ({
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 onConnect = useCallback(
+ (params: Edge | Connection) => {
+ setStatus('waiting');
+ setLoading(true);
+ const { target, source } = params;
- const nodes = [
- ...(lineageData.nodes as EntityReference[]),
- lineageData.entity,
- ];
+ const nodes = [
+ ...(updatedLineageData.nodes as EntityReference[]),
+ updatedLineageData.entity,
+ ];
- const sourceDownstreamNode = lineageData.downstreamEdges?.find((d) =>
- source?.includes(d.toEntity as string)
- );
+ const sourceDownstreamNode = updatedLineageData.downstreamEdges?.find(
+ (d) => source?.includes(d.toEntity as string)
+ );
- const sourceUpStreamNode = lineageData.upstreamEdges?.find((u) =>
- source?.includes(u.fromEntity as string)
- );
+ const sourceUpStreamNode = updatedLineageData.upstreamEdges?.find((u) =>
+ source?.includes(u.fromEntity as string)
+ );
- const targetDownStreamNode = lineageData.downstreamEdges?.find((d) =>
- target?.includes(d.toEntity as string)
- );
+ const targetDownStreamNode = updatedLineageData.downstreamEdges?.find(
+ (d) => target?.includes(d.toEntity as string)
+ );
- const targetUpStreamNode = lineageData.upstreamEdges?.find((u) =>
- target?.includes(u.fromEntity as string)
- );
+ const targetUpStreamNode = updatedLineageData.upstreamEdges?.find((u) =>
+ target?.includes(u.fromEntity as string)
+ );
- let targetNode = nodes?.find((n) => target?.includes(n.id));
+ let targetNode = nodes?.find((n) => target?.includes(n.id));
- let sourceNode = nodes?.find((n) => source?.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;
- }
+ if (isUndefined(targetNode)) {
+ targetNode = target?.includes(updatedLineageData.entity?.id)
+ ? updatedLineageData.entity
+ : selectedEntity;
+ }
+ if (isUndefined(sourceNode)) {
+ sourceNode = source?.includes(updatedLineageData.entity?.id)
+ ? updatedLineageData.entity
+ : selectedEntity;
+ }
- const newEdge: NewEdge = {
- edge: {
- fromEntity: {
- id: sourceNode.id,
- type: sourceNode.type,
+ const newEdge: NewEdge = {
+ edge: {
+ fromEntity: {
+ id: sourceNode.id,
+ type: sourceNode.type,
+ },
+ toEntity: {
+ id: targetNode.id,
+ type: targetNode.type,
+ },
},
- toEntity: {
- id: targetNode.id,
- type: targetNode.type,
- },
- },
- };
+ };
- setElements((els) =>
- addEdge({ ...params, arrowHeadType: ArrowHeadType.ArrowClosed }, els)
- );
+ setElements((els) => {
+ const newEdgeData = {
+ id: `edge-${params.source}-${params.target}`,
+ source: `${params.source}`,
+ target: `${params.target}`,
+ type: isEditMode ? 'buttonedge' : 'custom',
+ arrowHeadType: ArrowHeadType.ArrowClosed,
+ data: {
+ id: `edge-${params.source}-${params.target}`,
+ source: `${params.source}`,
+ target: `${params.target}`,
+ sourceType: sourceNode?.type,
+ targetType: targetNode?.type,
+ onEdgeClick,
+ },
+ };
- setTimeout(() => {
- addLineageHandler(newEdge)
- .then(() => {
- setStatus('success');
- setLoading(false);
- setTimeout(() => {
- entityLineageHandler({
- ...lineageData,
- nodes: selectedEntity
- ? [
- ...(lineageData.nodes as Array),
- 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,
- });
+ return getUniqueFlowElements(addEdge(newEdgeData, els));
+ });
+
+ const updatedDownStreamEdges = () => {
+ return !isUndefined(sourceUpStreamNode) ||
+ !isUndefined(targetUpStreamNode) ||
+ targetNode?.id === selectedEntity.id ||
+ nodes?.find((n) => targetNode?.id === n.id)
+ ? [
+ ...(updatedLineageData.downstreamEdges as EntityEdge[]),
+ {
+ fromEntity: sourceNode?.id,
+ toEntity: targetNode?.id,
+ },
+ ]
+ : updatedLineageData.downstreamEdges;
+ };
+
+ const updatedUpStreamEdges = () => {
+ return !isUndefined(sourceDownstreamNode) ||
+ !isUndefined(targetDownStreamNode) ||
+ sourceNode?.id === selectedEntity.id ||
+ nodes?.find((n) => sourceNode?.id === n.id)
+ ? [
+ ...(updatedLineageData.upstreamEdges as EntityEdge[]),
+ {
+ fromEntity: sourceNode?.id,
+ toEntity: targetNode?.id,
+ },
+ ]
+ : updatedLineageData.upstreamEdges;
+ };
+
+ const getUpdatedNodes = () => {
+ return !isEmpty(selectedEntity)
+ ? [
+ ...(updatedLineageData.nodes as Array),
+ 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 FlowElement);
+ setSelectedEntity({} as EntityReference);
+ })
+ .catch(() => {
setStatus('initial');
- }, 100);
- setNewAddedNode({} as FlowElement);
- setSelectedEntity({} as EntityReference);
- })
- .catch(() => {
- setStatus('initial');
- setLoading(false);
- });
- }, 500);
- };
+ setLoading(false);
+ });
+ }, 500);
+ },
+ [selectedNode, updatedLineageData, selectedEntity]
+ );
/**
* take element and perform onClick logic
* @param el
*/
const onElementClick = (el: FlowElement) => {
- const node = [
- ...(lineageData.nodes as Array),
- lineageData.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,
- });
- setElements((prevElements) => {
- return prevElements.map((preEl) => {
- if (preEl.id === el.id) {
- return { ...preEl, className: `${preEl.className} selected-node` };
- } else {
- return {
- ...preEl,
- className: getNodeClass(preEl),
- };
- }
+ if (isNode(el)) {
+ const node = [
+ ...(updatedLineageData.nodes as Array),
+ 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;
+ } else {
+ expandButton.current = null;
+ }
}
};
@@ -519,7 +547,6 @@ const Entitylineage: FunctionComponent = ({
if (preEl.id.includes(expandNode?.id as string)) {
return {
...preEl,
- className: `${preEl.className} selected-node`,
data: { ...preEl.data, columns: tableColumns },
};
} else {
@@ -560,10 +587,51 @@ const Entitylineage: FunctionComponent = ({
* 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);
- };
+ 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
+ );
+ });
+
+ setElements((es) =>
+ getUniqueFlowElements(es.filter((n) => n.id !== node.id))
+ );
+ setNewAddedNode({} as FlowElement);
+ },
+ [elements, updatedLineageData]
+ );
/**
* handle node drag event
@@ -588,64 +656,64 @@ const Entitylineage: FunctionComponent = ({
x: event.clientX - (reactFlowBounds?.left ?? 0),
y: event.clientY - (reactFlowBounds?.top ?? 0),
});
- const [lable, nodeType] = type.split('-');
+ const [label, nodeType] = type.split('-');
const newNode = {
id: uniqueId(),
nodeType,
position,
className: 'leaf-node',
connectable: false,
+ selectable: false,
data: {
label: (
-
+ {getNodeRemoveButton(() => {
+ removeNodeHandler(newNode as Node);
+ })}
),
+ removeNodeHandler,
+ isEditMode,
isNewNode: true,
},
};
setNewAddedNode(newNode as FlowElement);
- setElements((es) => es.concat(newNode as FlowElement));
+ setElements((es) =>
+ getUniqueFlowElements(es.concat(newNode as FlowElement))
+ );
}
};
/**
- * handle onNode select logic
+ * 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 = elements.some((n) =>
- n.id.includes(selectedEntity.id)
- );
+ const isExistingNode = elements.some((n) => n.id === selectedEntity.id);
if (isExistingNode) {
setElements((es) =>
es
.map((n) =>
n.id.includes(selectedEntity.id)
- ? { ...n, className: `${n.className} selected-node` }
+ ? {
+ ...n,
+ selectable: true,
+ className: `${n.className} selected`,
+ }
: n
)
.filter((es) => es.id !== newAddedNode.id)
@@ -659,25 +727,21 @@ const Entitylineage: FunctionComponent = ({
return {
...el,
connectable: true,
+ selectable: true,
id: selectedEntity.id,
data: {
+ ...el.data,
+ removeNodeHandler,
+ isEditMode,
label: (
- {getNodeLable(selectedEntity)}
-
+ {getNodeLabel(selectedEntity)}
+ {getNodeRemoveButton(() => {
+ removeNodeHandler({
+ ...el,
+ id: selectedEntity.id,
+ } as Node);
+ })}
),
},
@@ -691,10 +755,13 @@ const Entitylineage: FunctionComponent = ({
}
};
+ /**
+ * This method will handle the delete edge modal confirmation
+ */
const onRemove = () => {
- setDeletionState({ loading: true, status: 'initial' });
+ setDeletionState({ ...ELEMENT_DELETE_STATE, loading: true });
setTimeout(() => {
- setDeletionState({ loading: false, status: 'success' });
+ setDeletionState({ ...ELEMENT_DELETE_STATE, status: 'success' });
setTimeout(() => {
setShowDeleteModal(false);
setConfirmDelete(true);
@@ -703,13 +770,188 @@ const Entitylineage: FunctionComponent = ({
}, 500);
};
- useEffect(() => {
- setElements(getLayoutedElements(setElementsHandle()));
+ /**
+ *
+ * @returns Custom control elements
+ */
+ const getCustomControlElements = () => {
+ return (
+
+ {!deleted && (
+
+ You do not have permission to edit the lineage
+
+ }
+ permission={Operation.UpdateLineage}>
+ {
+ setEditMode((pre) => !pre && !deleted);
+ setSelectedNode({} as SelectedNode);
+ setIsDrawerOpen(false);
+ setNewAddedNode({} as FlowElement);
+ }}>
+ {loading ? (
+
+ ) : status === 'success' ? (
+
+ ) : (
+
+ )}
+
+
+ )}
+
+ );
+ };
+
+ /**
+ *
+ * @returns Grid background if editmode is enabled otherwise null
+ */
+ const getGraphBackGround = () => {
+ if (!isEditMode) {
+ return null;
+ } else {
+ return ;
+ }
+ };
+
+ /**
+ *
+ * @returns Side drawer if node is selected and view mode is enabled otherwise null
+ */
+ const getEntityDrawer = () => {
+ if (isEmpty(selectedNode) || isEditMode) {
+ return null;
+ } else {
+ return (
+
+ );
+ }
+ };
+
+ const getConfirmationModal = () => {
+ if (!showdeleteModal) {
+ return null;
+ } else {
+ return (
+
+ Cancel
+
+ }
+ confirmText={
+ deletionState.loading ? (
+
+ ) : deletionState.status === 'success' ? (
+
+ ) : (
+ 'Confirm'
+ )
+ }
+ header="Remove lineage edge"
+ onCancel={() => {
+ setShowDeleteModal(false);
+ }}
+ onConfirm={onRemove}
+ />
+ );
+ }
+ };
+
+ /**
+ * Reset State between view and edit mode toggle
+ */
+ const resetViewEditState = () => {
setExpandNode(undefined);
setTableColumns([]);
setConfirmDelete(false);
+ };
+
+ /**
+ * Handle updated linegae nodes
+ * Change newly added node label based on entity:EntityReference
+ */
+ const handleUpdatedLineageNode = () => {
+ const nodes = updatedLineageData.nodes;
+ const newlyAddedNodeElement = elements.find((el) => el?.data?.isNewNode);
+ const newlyAddedNode = nodes?.find(
+ (node) => node.id === newlyAddedNodeElement?.id
+ );
+
+ setElements((els) => {
+ return els.map((el) => {
+ if (el.id === newlyAddedNode?.id) {
+ return {
+ ...el,
+ data: { ...el.data, label: getNodeLabel(newlyAddedNode) },
+ };
+ } else {
+ return el;
+ }
+ });
+ });
+ };
+
+ useEffect(() => {
+ setElements(getLayoutedElements(setElementsHandle()));
+ 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(() => {
onNodeExpand();
getTableColumns(expandNode);
@@ -719,9 +961,6 @@ const Entitylineage: FunctionComponent = ({
if (!isEmpty(selectedNode)) {
setExpandNode(undefined);
}
- setElements((pre) => {
- return pre.map((el) => ({ ...el, data: { ...el.data, selectedNode } }));
- });
}, [selectedNode]);
useEffect(() => {
@@ -741,158 +980,56 @@ const Entitylineage: FunctionComponent = ({
useEffect(() => {
if (!isEmpty(entityLineage)) {
setLineageData(entityLineage);
+ setUpdatedLineageData(entityLineage);
}
}, [entityLineage]);
- return (
+ return deleted ? (
+ getDeletedLineagePlaceholder()
+ ) : (
- {!deleted ? (
-
-
-
- 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}>
-
- {!deleted && (
-
- You do not have permission to edit the lineage
- >
- }
- permission={Operation.UpdateLineage}>
- {
- setEditMode((pre) => !pre && !deleted);
- setSelectedNode({} as SelectedNode);
- setIsDrawerOpen(false);
- setNewAddedNode({} as FlowElement);
- }}>
- {loading ? (
-
- ) : status === 'success' ? (
-
- ) : (
-
- )}
-
-
- )}
-
- {isEditMode ? (
-
- ) : null}
-
-
-
- {!isEmpty(selectedNode) && !isEditMode ? (
-
- ) : null}
-
- {showdeleteModal ? (
-
- Cancel
-
- }
- confirmText={
- deletionState.loading ? (
-
- ) : deletionState.status === 'success' ? (
-
- ) : (
- 'Confirm'
- )
- }
- header="Remove lineage edge"
- onCancel={() => {
- setShowDeleteModal(false);
+
+
+
+ onElementClick(el)}
+ onLoad={(reactFlowInstance: OnLoadParams) => {
+ onLoad(reactFlowInstance);
+ setReactFlowInstance(reactFlowInstance);
}}
- onConfirm={onRemove}
- />
- ) : null}
+ onNodeContextMenu={onNodeContextMenu}
+ onNodeDrag={dragHandle}
+ onNodeDragStart={dragHandle}
+ onNodeDragStop={dragHandle}
+ onNodeMouseEnter={onNodeMouseEnter}
+ onNodeMouseLeave={onNodeMouseLeave}
+ onNodeMouseMove={onNodeMouseMove}>
+ {getCustomControlElements()}
+ {getGraphBackGround()}
+
+
- ) : (
- getDeletedLineagePlaceholder()
- )}
+ {getEntityDrawer()}
+
+ {getConfirmationModal()}
+
);
};
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/EntityLineage.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/EntityLineage.interface.ts
index 272c3236e37..520c17f550a 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/EntityLineage.interface.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/EntityLineage.interface.ts
@@ -11,7 +11,7 @@
* limitations under the License.
*/
-import { LeafNodes, LineagePos, LoadingNodeState } from 'Models';
+import { LeafNodes, LineagePos, LoadingNodeState, LoadingState } from 'Models';
import {
EntityLineage,
EntityReference,
@@ -71,3 +71,5 @@ export interface SelectedEdge {
source: EntityReference;
target: EntityReference;
}
+
+export type ElementLoadingState = Exclude;
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/Entitylineage.component.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/Entitylineage.component.test.tsx
index 3e7b05dad28..1f461cd9b17 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/Entitylineage.component.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/Entitylineage.component.test.tsx
@@ -146,6 +146,7 @@ jest.mock('../../utils/EntityLineageUtils', () => ({
onNodeMouseEnter: jest.fn(),
onNodeMouseLeave: jest.fn(),
onNodeMouseMove: jest.fn(),
+ getUniqueFlowElements: jest.fn().mockReturnValue([]),
}));
jest.mock('../../utils/TableUtils', () => ({
diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/Lineage.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/Lineage.constants.ts
index b1bee503a64..c83570f1b34 100644
--- a/openmetadata-ui/src/main/resources/ui/src/constants/Lineage.constants.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/constants/Lineage.constants.ts
@@ -1,4 +1,5 @@
import { capitalize } from 'lodash';
+import { ElementLoadingState } from '../components/EntityLineage/EntityLineage.interface';
import { EntityType } from '../enums/entity.enum';
export const foreignObjectSize = 40;
@@ -17,3 +18,8 @@ export const positionY = 60;
export const nodeWidth = 300;
export const nodeHeight = 40;
+
+export const ELEMENT_DELETE_STATE = {
+ loading: false,
+ status: 'initial' as ElementLoadingState,
+};
diff --git a/openmetadata-ui/src/main/resources/ui/src/styles/tailwind.css b/openmetadata-ui/src/main/resources/ui/src/styles/tailwind.css
index 4ba0e19a6b3..d3578908525 100644
--- a/openmetadata-ui/src/main/resources/ui/src/styles/tailwind.css
+++ b/openmetadata-ui/src/main/resources/ui/src/styles/tailwind.css
@@ -330,8 +330,8 @@
@apply tw-border-main;
box-shadow: 0 0 0 0.5px #e2dce4;
}
- .leaf-node.selected-node,
- .leaf-node.selected-node:hover {
+ .leaf-node.selected,
+ .leaf-node.selected:hover {
@apply tw-border-primary-active;
box-shadow: 0 0 0 0.5px #7147e8;
}
diff --git a/openmetadata-ui/src/main/resources/ui/src/styles/x-master.css b/openmetadata-ui/src/main/resources/ui/src/styles/x-master.css
index be37cae1060..01b48862418 100644
--- a/openmetadata-ui/src/main/resources/ui/src/styles/x-master.css
+++ b/openmetadata-ui/src/main/resources/ui/src/styles/x-master.css
@@ -741,6 +741,15 @@ body .profiler-graph .recharts-active-dot circle {
.leaf-node.core .react-flow__handle {
background-color: #7147e8;
}
+.react-flow__edge {
+ pointer-events: all;
+ cursor: pointer;
+}
+
+.react-flow__edge .react-flow__edge-path {
+ stroke-width: 2px;
+}
+
.react-flow__edge.selected .react-flow__edge-path {
stroke: #7147e8;
}
diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.tsx
index 944976fea9c..65587fe7952 100644
--- a/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.tsx
@@ -59,6 +59,7 @@ import {
prepareLabel,
} from './CommonUtils';
import { isLeafNode } from './EntityUtils';
+import SVGIcons from './SvgUtils';
import { getEntityLink } from './TableUtils';
export const getHeaderLabel = (
@@ -119,13 +120,14 @@ export const getLineageData = (
loadNodeHandler: (node: EntityReference, pos: LineagePos) => void,
lineageLeafNodes: LeafNodes,
isNodeLoading: LoadingNodeState,
- getNodeLable: (node: EntityReference) => React.ReactNode,
+ getNodeLabel: (node: EntityReference) => React.ReactNode,
isEditMode: boolean,
edgeType: string,
onEdgeClick: (
evt: React.MouseEvent,
data: CustomEdgeData
- ) => void
+ ) => void,
+ removeNodeHandler: (node: Node) => void
) => {
const [x, y] = [0, 0];
const nodes = [
@@ -140,6 +142,7 @@ export const getLineageData = (
isMapped: false,
...down,
})) || [];
+
const mainNode = entityLineage['entity'];
const UPStreamNodes: Elements = [];
@@ -161,8 +164,10 @@ export const getLineageData = (
type: 'default',
className: 'leaf-node',
data: {
- label: getNodeLable(node),
+ label: getNodeLabel(node),
entityType: node.type,
+ removeNodeHandler,
+ isEditMode,
},
position: {
x: pos === 'from' ? -xVal : xVal,
@@ -171,6 +176,10 @@ export const getLineageData = (
};
};
+ const makeEdge = (edge: FlowElement) => {
+ lineageEdges.push(edge);
+ };
+
const getNodes = (
id: string,
pos: LineagePos,
@@ -187,7 +196,7 @@ export const getLineageData = (
if (node) {
UPNodes.push(node);
UPStreamNodes.push(makeNode(node, 'from', depth, upDepth));
- lineageEdges.push({
+ makeEdge({
id: `edge-${up.fromEntity}-${id}-${depth}`,
source: `${node.id}`,
target: edg ? edg.id : `${id}`,
@@ -229,7 +238,7 @@ export const getLineageData = (
if (node) {
DOWNNodes.push(node);
DOWNStreamNodes.push(makeNode(node, 'to', depth, downDepth));
- lineageEdges.push({
+ makeEdge({
id: `edge-${id}-${down.toEntity}`,
source: edg ? edg.id : `${id}`,
target: `${node.id}`,
@@ -328,7 +337,9 @@ export const getLineageData = (
: 'input',
className: `leaf-node ${!isEditMode ? 'core' : ''}`,
data: {
- label: getNodeLable(mainNode),
+ label: getNodeLabel(mainNode),
+ isEditMode,
+ removeNodeHandler,
},
position: { x: x, y: y },
},
@@ -343,6 +354,7 @@ export const getLineageData = (
...up,
type: isEditMode ? 'default' : 'input',
data: {
+ ...up.data,
label: (
{down?.data?.label}
@@ -444,7 +457,7 @@ export const getDataLabel = (
} else {
return (
{type === 'table'
? databaseName && schemaName
@@ -496,8 +509,8 @@ export const getLayoutedElements = (
elements.forEach((el) => {
if (isNode(el)) {
dagreGraph.setNode(el.id, {
- width: el?.__rf?.width ?? nodeWidth,
- height: el?.__rf?.height ?? nodeHeight,
+ width: nodeWidth,
+ height: nodeHeight,
});
} else {
dagreGraph.setEdge(el.source, el.target);
@@ -512,11 +525,8 @@ export const getLayoutedElements = (
el.targetPosition = isHorizontal ? Position.Left : Position.Top;
el.sourcePosition = isHorizontal ? Position.Right : Position.Bottom;
el.position = {
- x:
- nodeWithPosition.x -
- (el?.__rf?.width ?? nodeWidth) / 2 +
- Math.random() / 1000,
- y: nodeWithPosition.y - (el?.__rf?.height ?? nodeHeight) / 2,
+ x: nodeWithPosition.x - nodeWidth / 2,
+ y: nodeWithPosition.y - nodeHeight / 2,
};
}
@@ -527,27 +537,19 @@ export const getLayoutedElements = (
export const getModalBodyText = (selectedEdge: SelectedEdge) => {
let sourceEntity = '';
let targetEntity = '';
+ const sourceFQN = selectedEdge.source.fullyQualifiedName || '';
+ const targetFQN = selectedEdge.target.fullyQualifiedName || '';
if (selectedEdge.source.type === EntityType.TABLE) {
- sourceEntity = getPartialNameFromTableFQN(
- selectedEdge.source.name as string,
- [FqnPart.Table]
- );
+ sourceEntity = getPartialNameFromTableFQN(sourceFQN, [FqnPart.Table]);
} else {
- sourceEntity = getPartialNameFromFQN(selectedEdge.source.name as string, [
- 'database',
- ]);
+ sourceEntity = getPartialNameFromFQN(sourceFQN, ['database']);
}
if (selectedEdge.target.type === EntityType.TABLE) {
- targetEntity = getPartialNameFromTableFQN(
- selectedEdge.target.name as string,
- [FqnPart.Table]
- );
+ targetEntity = getPartialNameFromTableFQN(targetFQN, [FqnPart.Table]);
} else {
- targetEntity = getPartialNameFromFQN(selectedEdge.target.name as string, [
- 'database',
- ]);
+ targetEntity = getPartialNameFromFQN(targetFQN, ['database']);
}
return `Are you sure you want to remove the edge between "${
@@ -560,3 +562,32 @@ export const getModalBodyText = (selectedEdge: SelectedEdge) => {
: targetEntity
}"?`;
};
+
+export const getUniqueFlowElements = (elements: FlowElement[]) => {
+ const flag: { [x: string]: boolean } = {};
+ const uniqueElements: Elements = [];
+
+ elements.forEach((elem) => {
+ if (!flag[elem.id]) {
+ flag[elem.id] = true;
+ uniqueElements.push(elem);
+ }
+ });
+
+ return uniqueElements;
+};
+
+/**
+ *
+ * @param onClick - callback
+ * @returns - Button element with attach callback
+ */
+export const getNodeRemoveButton = (onClick: () => void) => {
+ return (
+
+ );
+};