mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-09 07:42:41 +00:00
Fix : UI manual Lineage Editor Issues (#4532)
* Fix : UI manual Lineage Editor Issues * Keep node if only edge is deleted. * Adding hidden handler * Add invisible handle on custom node * Fix funtion name typo * Fix node overlapping issue * Fix #3508 Manual Lineage Editor: Do not reorganize the graph as the user is connecting the nodes * Fix code smell * Minor Fix * Styling fix * Fix Flaky state issue * Refactor onConnect Method * Fix duplicate edge and node issue * Fix Failing Unit test * Fix confirmation modal source and target node name issue * Add check for isNode in Element Click Handler * Add makeEdge Helper * Add JSDoc for helper methods * Remove onElementsRemove prop * Refactor node remove button * Move util method to util file * Allow users to delete edge and node separately * Add unit test * Fix Node Styling * Minor Fix * Add invisble edges
This commit is contained in:
parent
8511f9f0b2
commit
241df76cae
@ -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(
|
||||
<CustomEdge
|
||||
{...mockCustomEdgeProp}
|
||||
data={{ ...mockCustomEdgeProp.data, selectedNode: {} }}
|
||||
/>,
|
||||
<CustomEdge {...mockCustomEdgeProp} selected={false} />,
|
||||
{
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
@ -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 (
|
||||
<path
|
||||
className="react-flow__edge-path"
|
||||
d={path}
|
||||
data-testid="react-flow-edge-path"
|
||||
id={id}
|
||||
markerEnd={markerEnd}
|
||||
style={{ ...style, strokeWidth: '6px', opacity: 0 }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<path
|
||||
@ -63,15 +94,17 @@ export const CustomEdge = ({
|
||||
markerEnd={markerEnd}
|
||||
style={style}
|
||||
/>
|
||||
{(rest as CustomEdgeData)?.source?.includes(selectedNode?.id) ||
|
||||
(rest as CustomEdgeData)?.target?.includes(selectedNode?.id) ? (
|
||||
{getInvisiblePath(invisibleEdgePath)}
|
||||
{getInvisiblePath(invisibleEdgePath1)}
|
||||
|
||||
{selected ? (
|
||||
<foreignObject
|
||||
data-testid="delete-button"
|
||||
height={foreignObjectSize}
|
||||
requiredExtensions="http://www.w3.org/1999/xhtml"
|
||||
width={foreignObjectSize}
|
||||
x={edgeCenterX - foreignObjectSize / 4}
|
||||
y={edgeCenterY - foreignObjectSize / 4}>
|
||||
x={edgeCenterX - foreignObjectSize / offset}
|
||||
y={edgeCenterY - foreignObjectSize / offset}>
|
||||
<button
|
||||
className="tw-cursor-pointer tw-flex tw-z-9999"
|
||||
onClick={(event) => onEdgeClick?.(event, rest as CustomEdgeData)}>
|
||||
|
@ -88,7 +88,9 @@ const mockTableColumns = [
|
||||
];
|
||||
|
||||
const mockCustomNodeProp = {
|
||||
id: 'node-1',
|
||||
type: 'default',
|
||||
selected: false,
|
||||
isConnectable: false,
|
||||
data: {
|
||||
label: <p>label</p>,
|
||||
|
@ -13,7 +13,8 @@
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, { CSSProperties, Fragment } from 'react';
|
||||
import { Handle, HandleProps, Position } from 'react-flow-renderer';
|
||||
import { Handle, HandleProps, NodeProps, Position } from 'react-flow-renderer';
|
||||
import { getNodeRemoveButton } from '../../utils/EntityLineageUtils';
|
||||
import { getConstraintIcon } from '../../utils/TableUtils';
|
||||
|
||||
const handleStyles = {
|
||||
@ -29,23 +30,97 @@ const getHandle = (
|
||||
isConnectable: HandleProps['isConnectable'],
|
||||
isNewNode = false
|
||||
) => {
|
||||
const getLeftRightHandleStyles = () => {
|
||||
return {
|
||||
opacity: 0,
|
||||
borderRadius: '0px',
|
||||
height: '162%',
|
||||
};
|
||||
};
|
||||
|
||||
const getTopBottomHandleStyles = () => {
|
||||
return {
|
||||
opacity: 0,
|
||||
borderRadius: '0px',
|
||||
width: '110%',
|
||||
};
|
||||
};
|
||||
|
||||
if (nodeType === 'output') {
|
||||
return (
|
||||
<Fragment>
|
||||
<Handle
|
||||
isConnectable={isConnectable}
|
||||
position={Position.Left}
|
||||
style={{ ...handleStyles, left: '-14px' } as CSSProperties}
|
||||
type="target"
|
||||
/>
|
||||
<Handle
|
||||
isConnectable={isConnectable}
|
||||
position={Position.Left}
|
||||
style={{
|
||||
...getLeftRightHandleStyles(),
|
||||
marginLeft: '-10px',
|
||||
}}
|
||||
type="target"
|
||||
/>
|
||||
<Handle
|
||||
isConnectable={isConnectable}
|
||||
position={Position.Bottom}
|
||||
style={{
|
||||
...getTopBottomHandleStyles(),
|
||||
marginBottom: '-6px',
|
||||
}}
|
||||
type="target"
|
||||
/>
|
||||
<Handle
|
||||
isConnectable={isConnectable}
|
||||
position={Position.Top}
|
||||
style={{
|
||||
...getTopBottomHandleStyles(),
|
||||
marginTop: '-6px',
|
||||
}}
|
||||
type="target"
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
} else if (nodeType === 'input') {
|
||||
return (
|
||||
<Fragment>
|
||||
<Handle
|
||||
isConnectable={isConnectable}
|
||||
position={Position.Right}
|
||||
style={{ ...handleStyles, right: '-14px' } as CSSProperties}
|
||||
type="source"
|
||||
/>
|
||||
<Handle
|
||||
isConnectable={isConnectable}
|
||||
position={Position.Right}
|
||||
style={{
|
||||
...getLeftRightHandleStyles(),
|
||||
marginRight: '-10px',
|
||||
}}
|
||||
type="source"
|
||||
/>
|
||||
<Handle
|
||||
isConnectable={isConnectable}
|
||||
position={Position.Bottom}
|
||||
style={{
|
||||
...getTopBottomHandleStyles(),
|
||||
marginBottom: '-6px',
|
||||
}}
|
||||
type="target"
|
||||
/>
|
||||
<Handle
|
||||
isConnectable={isConnectable}
|
||||
position={Position.Top}
|
||||
style={{
|
||||
...getTopBottomHandleStyles(),
|
||||
marginTop: '-6px',
|
||||
}}
|
||||
type="target"
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
@ -74,24 +149,63 @@ const getHandle = (
|
||||
}
|
||||
type="source"
|
||||
/>
|
||||
<Handle
|
||||
isConnectable={isConnectable}
|
||||
position={Position.Left}
|
||||
style={{
|
||||
...getLeftRightHandleStyles(),
|
||||
marginLeft: '-10px',
|
||||
}}
|
||||
type="target"
|
||||
/>
|
||||
<Handle
|
||||
isConnectable={isConnectable}
|
||||
position={Position.Right}
|
||||
style={{
|
||||
...getLeftRightHandleStyles(),
|
||||
marginRight: '-10px',
|
||||
}}
|
||||
type="source"
|
||||
/>
|
||||
<Handle
|
||||
isConnectable={isConnectable}
|
||||
position={Position.Bottom}
|
||||
style={{
|
||||
...getTopBottomHandleStyles(),
|
||||
marginBottom: '-6px',
|
||||
}}
|
||||
type="target"
|
||||
/>
|
||||
<Handle
|
||||
isConnectable={isConnectable}
|
||||
position={Position.Top}
|
||||
style={{
|
||||
...getTopBottomHandleStyles(),
|
||||
marginTop: '-6px',
|
||||
}}
|
||||
type="target"
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const CustomNode = (props: NodeProps) => {
|
||||
const { data, type, isConnectable, selected } = props;
|
||||
/* eslint-disable-next-line */
|
||||
const CustomNode = (props: any) => {
|
||||
/* 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 (
|
||||
<div className="tw-relative nowheel ">
|
||||
{getHandle(type, isConnectable, isNewNode)}
|
||||
{/* Node label could be simple text or reactNode */}
|
||||
<div className={classNames('tw-px-2')} data-testid="node-label">
|
||||
{label}
|
||||
{label}{' '}
|
||||
{selected && isEditMode
|
||||
? getNodeRemoveButton(() => {
|
||||
removeNodeHandler?.(props);
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
|
||||
{columns?.length ? (
|
||||
|
@ -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<EntityLineageProp> = ({
|
||||
const [status, setStatus] = useState<LoadingState>('initial');
|
||||
const [deletionState, setDeletionState] = useState<{
|
||||
loading: boolean;
|
||||
status: Exclude<LoadingState, 'waiting'>;
|
||||
}>({
|
||||
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>(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<EntityLineageProp> = ({
|
||||
*/
|
||||
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<EntityLineageProp> = ({
|
||||
/**
|
||||
*
|
||||
* @param node
|
||||
* @returns lable for given node
|
||||
* @returns label for given node
|
||||
*/
|
||||
const getNodeLable = (node: EntityReference) => {
|
||||
const getNodeLabel = (node: EntityReference) => {
|
||||
return (
|
||||
<Fragment>
|
||||
{node.type === 'table' && !isEditMode ? (
|
||||
@ -217,14 +242,16 @@ const Entitylineage: FunctionComponent<EntityLineageProp> = ({
|
||||
|
||||
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<EntityLineageProp> = ({
|
||||
/**
|
||||
* 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<EntityLineageProp> = ({
|
||||
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<EntityLineageProp> = ({
|
||||
* @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,41 +366,34 @@ const Entitylineage: FunctionComponent<EntityLineageProp> = ({
|
||||
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) => {
|
||||
const onConnect = useCallback(
|
||||
(params: Edge | Connection) => {
|
||||
setStatus('waiting');
|
||||
setLoading(true);
|
||||
const { target, source } = params;
|
||||
|
||||
const nodes = [
|
||||
...(lineageData.nodes as EntityReference[]),
|
||||
lineageData.entity,
|
||||
...(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) =>
|
||||
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) =>
|
||||
const targetUpStreamNode = updatedLineageData.upstreamEdges?.find((u) =>
|
||||
target?.includes(u.fromEntity as string)
|
||||
);
|
||||
|
||||
@ -395,13 +402,13 @@ const Entitylineage: FunctionComponent<EntityLineageProp> = ({
|
||||
let sourceNode = nodes?.find((n) => source?.includes(n.id));
|
||||
|
||||
if (isUndefined(targetNode)) {
|
||||
targetNode = target?.includes(lineageData.entity?.id)
|
||||
? lineageData.entity
|
||||
targetNode = target?.includes(updatedLineageData.entity?.id)
|
||||
? updatedLineageData.entity
|
||||
: selectedEntity;
|
||||
}
|
||||
if (isUndefined(sourceNode)) {
|
||||
sourceNode = source?.includes(lineageData.entity?.id)
|
||||
? lineageData.entity
|
||||
sourceNode = source?.includes(updatedLineageData.entity?.id)
|
||||
? updatedLineageData.entity
|
||||
: selectedEntity;
|
||||
}
|
||||
|
||||
@ -418,9 +425,64 @@ const Entitylineage: FunctionComponent<EntityLineageProp> = ({
|
||||
},
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
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<EntityReference>),
|
||||
selectedEntity,
|
||||
]
|
||||
: updatedLineageData.nodes;
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
addLineageHandler(newEdge)
|
||||
@ -428,38 +490,11 @@ const Entitylineage: FunctionComponent<EntityLineageProp> = ({
|
||||
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,
|
||||
setUpdatedLineageData({
|
||||
...updatedLineageData,
|
||||
nodes: getUpdatedNodes(),
|
||||
downstreamEdges: updatedDownStreamEdges(),
|
||||
upstreamEdges: updatedUpStreamEdges(),
|
||||
});
|
||||
setStatus('initial');
|
||||
}, 100);
|
||||
@ -471,17 +506,21 @@ const Entitylineage: FunctionComponent<EntityLineageProp> = ({
|
||||
setLoading(false);
|
||||
});
|
||||
}, 500);
|
||||
};
|
||||
},
|
||||
[selectedNode, updatedLineageData, selectedEntity]
|
||||
);
|
||||
|
||||
/**
|
||||
* take element and perform onClick logic
|
||||
* @param el
|
||||
*/
|
||||
const onElementClick = (el: FlowElement) => {
|
||||
if (isNode(el)) {
|
||||
const node = [
|
||||
...(lineageData.nodes as Array<EntityReference>),
|
||||
lineageData.entity,
|
||||
...(updatedLineageData.nodes as Array<EntityReference>),
|
||||
updatedLineageData.entity,
|
||||
].find((n) => el.id.includes(n.id));
|
||||
|
||||
if (!expandButton.current) {
|
||||
selectNodeHandler(true, {
|
||||
name: node?.name as string,
|
||||
@ -491,21 +530,10 @@ const Entitylineage: FunctionComponent<EntityLineageProp> = ({
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@ -519,7 +547,6 @@ const Entitylineage: FunctionComponent<EntityLineageProp> = ({
|
||||
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<EntityLineageProp> = ({
|
||||
* take node and remove it from the graph
|
||||
* @param node
|
||||
*/
|
||||
const removeNodeHandler = (node: FlowElement) => {
|
||||
setElements((es) => es.filter((n) => n.id !== node.id));
|
||||
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<EntityLineageProp> = ({
|
||||
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: (
|
||||
<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>
|
||||
{getNodeRemoveButton(() => {
|
||||
removeNodeHandler(newNode as Node);
|
||||
})}
|
||||
<div className="tw-flex">
|
||||
<SVGIcons
|
||||
alt="entity-icon"
|
||||
className="tw-mr-2"
|
||||
icon={`${lowerCase(lable)}-grey`}
|
||||
icon={`${lowerCase(label)}-grey`}
|
||||
width="16px"
|
||||
/>
|
||||
<NodeSuggestions
|
||||
entityType={upperCase(lable)}
|
||||
entityType={upperCase(label)}
|
||||
onSelectHandler={selectedEntityHandler}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
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<EntityLineageProp> = ({
|
||||
return {
|
||||
...el,
|
||||
connectable: true,
|
||||
selectable: true,
|
||||
id: selectedEntity.id,
|
||||
data: {
|
||||
...el.data,
|
||||
removeNodeHandler,
|
||||
isEditMode,
|
||||
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={() => {
|
||||
{getNodeLabel(selectedEntity)}
|
||||
{getNodeRemoveButton(() => {
|
||||
removeNodeHandler({
|
||||
...el,
|
||||
id: selectedEntity.id,
|
||||
} as FlowElement);
|
||||
}}>
|
||||
<SVGIcons
|
||||
alt="times-circle"
|
||||
icon="icon-times-circle"
|
||||
width="16px"
|
||||
/>
|
||||
</button>
|
||||
} as Node);
|
||||
})}
|
||||
</Fragment>
|
||||
),
|
||||
},
|
||||
@ -691,10 +755,13 @@ const Entitylineage: FunctionComponent<EntityLineageProp> = ({
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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,99 +770,21 @@ const Entitylineage: FunctionComponent<EntityLineageProp> = ({
|
||||
}, 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]);
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns Custom control elements
|
||||
*/
|
||||
const getCustomControlElements = () => {
|
||||
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={
|
||||
<>
|
||||
<Fragment>
|
||||
<p>You do not have permission to edit the lineage</p>
|
||||
</>
|
||||
</Fragment>
|
||||
}
|
||||
permission={Operation.UpdateLineage}>
|
||||
<ControlButton
|
||||
@ -821,10 +810,7 @@ const Entitylineage: FunctionComponent<EntityLineageProp> = ({
|
||||
{loading ? (
|
||||
<Loader size="small" type="white" />
|
||||
) : status === 'success' ? (
|
||||
<FontAwesomeIcon
|
||||
className="tw-text-white"
|
||||
icon="check"
|
||||
/>
|
||||
<FontAwesomeIcon className="tw-text-white" icon="check" />
|
||||
) : (
|
||||
<SVGIcons
|
||||
alt="icon-edit-lineag"
|
||||
@ -842,33 +828,51 @@ const Entitylineage: FunctionComponent<EntityLineageProp> = ({
|
||||
</NonAdminAction>
|
||||
)}
|
||||
</CustomControls>
|
||||
{isEditMode ? (
|
||||
<Background
|
||||
gap={12}
|
||||
size={1}
|
||||
variant={BackgroundVariant.Lines}
|
||||
/>
|
||||
) : null}
|
||||
</ReactFlow>
|
||||
</ReactFlowProvider>
|
||||
</div>
|
||||
{!isEmpty(selectedNode) && !isEditMode ? (
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @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 === lineageData.entity?.name}
|
||||
isMainNode={selectedNode.name === updatedLineageData.entity?.name}
|
||||
selectedNode={selectedNode}
|
||||
show={isDrawerOpen && !isEditMode}
|
||||
onCancel={closeDrawer}
|
||||
/>
|
||||
) : null}
|
||||
<EntityLineageSidebar newAddedNode={newAddedNode} show={isEditMode} />
|
||||
{showdeleteModal ? (
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
'tw-pointer-events-none tw-opacity-70': deletionState.loading,
|
||||
})}>
|
||||
Cancel
|
||||
</span>
|
||||
@ -888,11 +892,144 @@ const Entitylineage: FunctionComponent<EntityLineageProp> = ({
|
||||
}}
|
||||
onConfirm={onRemove}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}, [expandNode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEmpty(selectedNode)) {
|
||||
setExpandNode(undefined);
|
||||
}
|
||||
}, [selectedNode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tableColumns.length) {
|
||||
onNodeExpand(tableColumns);
|
||||
}
|
||||
}, [tableColumns]);
|
||||
|
||||
useEffect(() => {
|
||||
onEntitySelect();
|
||||
}, [selectedEntity]);
|
||||
|
||||
useEffect(() => {
|
||||
removeEdgeHandler(selectedEdge, confirmDelete);
|
||||
}, [selectedEdge, confirmDelete]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEmpty(entityLineage)) {
|
||||
setLineageData(entityLineage);
|
||||
setUpdatedLineageData(entityLineage);
|
||||
}
|
||||
}, [entityLineage]);
|
||||
|
||||
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 }}
|
||||
elements={elements as Elements}
|
||||
maxZoom={2}
|
||||
minZoom={0.5}
|
||||
nodeTypes={nodeTypes}
|
||||
nodesConnectable={isEditMode}
|
||||
selectNodesOnDrag={false}
|
||||
zoomOnDoubleClick={false}
|
||||
zoomOnScroll={false}
|
||||
onConnect={onConnect}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={onDrop}
|
||||
onElementClick={(_e, el) => onElementClick(el)}
|
||||
onLoad={(reactFlowInstance: OnLoadParams) => {
|
||||
onLoad(reactFlowInstance);
|
||||
setReactFlowInstance(reactFlowInstance);
|
||||
}}
|
||||
onNodeContextMenu={onNodeContextMenu}
|
||||
onNodeDrag={dragHandle}
|
||||
onNodeDragStart={dragHandle}
|
||||
onNodeDragStop={dragHandle}
|
||||
onNodeMouseEnter={onNodeMouseEnter}
|
||||
onNodeMouseLeave={onNodeMouseLeave}
|
||||
onNodeMouseMove={onNodeMouseMove}>
|
||||
{getCustomControlElements()}
|
||||
{getGraphBackGround()}
|
||||
</ReactFlow>
|
||||
</ReactFlowProvider>
|
||||
</div>
|
||||
{getEntityDrawer()}
|
||||
<EntityLineageSidebar newAddedNode={newAddedNode} show={isEditMode} />
|
||||
{getConfirmationModal()}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
@ -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<LoadingState, 'waiting'>;
|
||||
|
@ -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', () => ({
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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<HTMLButtonElement>,
|
||||
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: (
|
||||
<div className="tw-flex">
|
||||
<div
|
||||
@ -387,6 +399,7 @@ export const getLineageData = (
|
||||
...down,
|
||||
type: isEditMode ? 'default' : 'output',
|
||||
data: {
|
||||
...down.data,
|
||||
label: (
|
||||
<div className="tw-flex tw-justify-between">
|
||||
<div>{down?.data?.label}</div>
|
||||
@ -444,7 +457,7 @@ export const getDataLabel = (
|
||||
} else {
|
||||
return (
|
||||
<span
|
||||
className="tw-break-words description-text tw-self-center"
|
||||
className="tw-break-words tw-self-center tw-w-60"
|
||||
data-testid="lineage-entity">
|
||||
{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 (
|
||||
<button
|
||||
className="tw-absolute tw--top-4 tw--right-6 tw-cursor-pointer tw-z-9999 tw-bg-body-hover tw-rounded-full"
|
||||
onClick={() => onClick()}>
|
||||
<SVGIcons alt="times-circle" icon="icon-times-circle" width="16px" />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user