From c74d7612bcd9f2bb4e32646c3de399663da3e2a9 Mon Sep 17 00:00:00 2001 From: Sachin Chaurasiya Date: Fri, 26 Nov 2021 20:37:33 +0530 Subject: [PATCH] Adding Lineage node expansion to show columns . (#1393) * Adding Lineage node expansion to show columns . * Adding support for getting columns of expanded node. * Refactoring. * Reafctoring and style changes. * Minor changes * Changing prop type. * minor style changes. --- .../resources/ui/src/assets/svg/minus.svg | 4 + .../EntityInfoDrawer.component.tsx | 4 +- .../EntityLineage/CustomNode.component.jsx | 77 ++++ .../EntityLineage/EntityLineage.component.tsx | 375 +++++++++++------- .../ui/src/components/Loader/Loader.tsx | 7 +- .../main/resources/ui/src/styles/x-master.css | 3 + .../main/resources/ui/src/utils/SvgUtils.tsx | 12 + 7 files changed, 337 insertions(+), 145 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/svg/minus.svg create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/CustomNode.component.jsx diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/minus.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/minus.svg new file mode 100644 index 00000000000..1e4f4998767 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/minus.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/openmetadata-ui/src/main/resources/ui/src/components/EntityInfoDrawer/EntityInfoDrawer.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/EntityInfoDrawer/EntityInfoDrawer.component.tsx index af170284c17..c9493ba43e0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/EntityInfoDrawer/EntityInfoDrawer.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/EntityInfoDrawer/EntityInfoDrawer.component.tsx @@ -187,7 +187,7 @@ const EntityInfoDrawer = ({ serviceType ).map((d) => { return ( -

+

{d.name && {d.name}:} -

+
); })} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/CustomNode.component.jsx b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/CustomNode.component.jsx new file mode 100644 index 00000000000..e09fcaf0040 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/CustomNode.component.jsx @@ -0,0 +1,77 @@ +import classNames from 'classnames'; +import React, { Fragment } from 'react'; +import { Handle } from 'react-flow-renderer'; + +const handleStyles = { borderRadius: '50%', position: 'absolute', top: 10 }; +const getHandle = (nodeType, isConnectable) => { + if (nodeType === 'output') { + return ( + + ); + } else if (nodeType === 'input') { + return ( + + ); + } else { + return ( + + + + + ); + } +}; + +const CustomNode = (props) => { + /* eslint-disable-next-line */ + const { data, type, isConnectable } = props; + /* eslint-disable-next-line */ + const { label, columns } = data; + + return ( +
+ {getHandle(type, isConnectable)} + {/* Node label could be simple text or reactNode */} +
{label}
+ + {columns?.length ?
: null} +
+
+ {columns?.map((c) => ( +

+ {c.name} +

+ ))} +
+
+
+ ); +}; + +export default CustomNode; 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 1493f9cd17d..49d201b2713 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 @@ -1,8 +1,11 @@ +import { AxiosResponse } from 'axios'; +import { isEmpty } from 'lodash'; import { LeafNodes, LineagePos, LoadingNodeState } from 'Models'; import React, { FunctionComponent, MouseEvent as ReactMouseEvent, useEffect, + useRef, useState, } from 'react'; import ReactFlow, { @@ -20,15 +23,20 @@ import ReactFlow, { removeElements, } from 'react-flow-renderer'; import { Link } from 'react-router-dom'; +import { getTableDetails } from '../../axiosAPIs/tableAPI'; +import { Column } from '../../generated/entity/data/table'; import { Edge as LineageEdge, EntityLineage, } from '../../generated/type/entityLineage'; import { EntityReference } from '../../generated/type/entityReference'; +import useToastContext from '../../hooks/useToastContext'; import { isLeafNode } from '../../utils/EntityUtils'; +import SVGIcons from '../../utils/SvgUtils'; import { getEntityIcon } from '../../utils/TableUtils'; import EntityInfoDrawer from '../EntityInfoDrawer/EntityInfoDrawer.component'; import Loader from '../Loader/Loader'; +import CustomNode from './CustomNode.component'; import { EntityLineageProp, SelectedNode } from './EntityLineage.interface'; const onLoad = (reactFlowInstance: OnLoadParams) => { reactFlowInstance.fitView(); @@ -51,8 +59,15 @@ const onNodeContextMenu = (_event: ReactMouseEvent, _node: Node | Edge) => { _event.preventDefault(); }; -const getDataLabel = (v = '', separator = '.') => { +const dragHandle = (event: ReactMouseEvent) => { + event.stopPropagation(); +}; + +const getDataLabel = (v = '', separator = '.', isTextOnly = false) => { const length = v.split(separator).length; + if (isTextOnly) { + return v.split(separator)[length - 1]; + } return ( { ); }; +const getNoLineageDataPlaceholder = () => { + return ( +
+ + Lineage is currently supported for Airflow. To enable lineage collection + from Airflow, Please follow the documentation + + + here + +
+ ); +}; + const positionX = 150; const positionY = 60; @@ -71,7 +106,8 @@ const getLineageData = ( onSelect: (state: boolean, value: SelectedNode) => void, loadNodeHandler: (node: EntityReference, pos: LineagePos) => void, lineageLeafNodes: LeafNodes, - isNodeLoading: LoadingNodeState + isNodeLoading: LoadingNodeState, + getNodeLable: (node: EntityReference) => React.ReactNode ) => { const [x, y] = [0, 0]; const nodes = entityLineage['nodes']; @@ -89,9 +125,33 @@ const getLineageData = ( const DOWNStreamNodes: Elements = []; const lineageEdges: Elements = []; + const makeNode = ( + node: EntityReference, + pos: LineagePos, + depth: number, + posDepth: number + ) => { + const [xVal, yVal] = [positionX * 2 * depth, y + positionY * posDepth]; + + return { + id: `node-${node.id}-${depth}`, + sourcePosition: Position.Right, + targetPosition: Position.Left, + type: 'default', + className: 'leaf-node', + data: { + label: getNodeLable(node), + }, + position: { + x: pos === 'from' ? -xVal : xVal, + y: yVal, + }, + }; + }; + const getNodes = ( id: string, - pos: 'from' | 'to', + pos: LineagePos, depth: number, NodesArr: Array = [] ): Array => { @@ -104,25 +164,7 @@ const getLineageData = ( const node = nodes?.find((nd) => nd.id === up.fromEntity); if (node) { UPNodes.push(node); - UPStreamNodes.push({ - id: `node-${node.id}-${depth}`, - sourcePosition: Position.Right, - targetPosition: Position.Left, - type: 'default', - className: 'leaf-node', - data: { - label: ( -

- {getEntityIcon(node.type)} - {getDataLabel(node.name as string)} -

- ), - }, - position: { - x: -positionX * 2 * depth, - y: y + positionY * upDepth, - }, - }); + UPStreamNodes.push(makeNode(node, 'from', depth, upDepth)); lineageEdges.push({ id: `edge-${up.fromEntity}-${id}-${depth}`, source: `node-${node.id}-${depth}`, @@ -156,25 +198,7 @@ const getLineageData = ( const node = nodes?.find((nd) => nd.id === down.toEntity); if (node) { DOWNNodes.push(node); - DOWNStreamNodes.push({ - id: `node-${node.id}-${depth}`, - sourcePosition: Position.Right, - targetPosition: Position.Left, - type: 'default', - className: 'leaf-node', - data: { - label: ( -

- {getEntityIcon(node.type)} - {getDataLabel(node.name as string)} -

- ), - }, - position: { - x: positionX * 2 * depth, - y: y + positionY * downDepth, - }, - }); + DOWNStreamNodes.push(makeNode(node, 'to', depth, downDepth)); lineageEdges.push({ id: `edge-${id}-${down.toEntity}`, source: edg ? edg.id : `node-${id}-${depth}`, @@ -258,19 +282,7 @@ const getLineageData = ( : 'input', className: 'leaf-node core', data: { - label: ( -

- onSelect(true, { - name: mainNode.name as string, - type: mainNode.type, - }) - }> - {getEntityIcon(mainNode.type)} - {getDataLabel(mainNode.name as string)} -

- ), + label: getNodeLable(mainNode), }, position: { x: x, y: y }, }, @@ -287,25 +299,28 @@ const getLineageData = ( data: { label: (
- {!isLeafNode(lineageLeafNodes, node?.id as string, 'from') && - !up.id.includes(isNodeLoading.id as string) ? ( -

{ - e.stopPropagation(); - onSelect(false, {} as SelectedNode); - if (node) { - loadNodeHandler(node, 'from'); - } - }} - /> - ) : null} - {isNodeLoading.state && - up.id.includes(isNodeLoading.id as string) ? ( -

+
{ + e.stopPropagation(); + onSelect(false, {} as SelectedNode); + if (node) { + loadNodeHandler(node, 'from'); + } + }}> + {!isLeafNode( + lineageLeafNodes, + node?.id as string, + 'from' + ) && !up.id.includes(isNodeLoading.id as string) ? ( + + ) : null} + {isNodeLoading.state && + up.id.includes(isNodeLoading.id as string) ? ( -
- ) : null} + ) : null} +
+
{up?.data?.label}
), @@ -327,25 +342,24 @@ const getLineageData = (
{down?.data?.label}
- {!isLeafNode(lineageLeafNodes, node?.id as string, 'to') && - !down.id.includes(isNodeLoading.id as string) ? ( -

{ - e.stopPropagation(); - onSelect(false, {} as SelectedNode); - if (node) { - loadNodeHandler(node, 'to'); - } - }} - /> - ) : null} - {isNodeLoading.state && - down.id.includes(isNodeLoading.id as string) ? ( -

+
{ + e.stopPropagation(); + onSelect(false, {} as SelectedNode); + if (node) { + loadNodeHandler(node, 'to'); + } + }}> + {!isLeafNode(lineageLeafNodes, node?.id as string, 'to') && + !down.id.includes(isNodeLoading.id as string) ? ( + + ) : null} + {isNodeLoading.state && + down.id.includes(isNodeLoading.id as string) ? ( -
- ) : null} + ) : null} +
), }, @@ -363,25 +377,63 @@ const Entitylineage: FunctionComponent = ({ lineageLeafNodes, isNodeLoading, }: EntityLineageProp) => { + const showToast = useToastContext(); const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [selectedNode, setSelectedNode] = useState( {} as SelectedNode ); + const expandButton = useRef(null); + const [expandNode, setExpandNode] = useState( + undefined + ); + + const [tableColumns, setTableColumns] = useState([] as Column[]); const selectNodeHandler = (state: boolean, value: SelectedNode) => { setIsDrawerOpen(state); setSelectedNode(value); }; - const [elements, setElements] = useState( - getLineageData( + + const getNodeLable = (node: EntityReference) => { + return ( + <> + {node.type === 'table' ? ( + + ) : null} +

+ {getEntityIcon(node.type)} + {getDataLabel(node.name as string)} +

+ + ); + }; + + const setElementsHandle = () => { + return getLineageData( entityLineage, selectNodeHandler, loadNodeHandler, lineageLeafNodes, - isNodeLoading - ) as Elements - ); + isNodeLoading, + getNodeLable + ) as Elements; + }; + const [elements, setElements] = useState(setElementsHandle()); const closeDrawer = (value: boolean) => { setIsDrawerOpen(value); @@ -395,66 +447,119 @@ const Entitylineage: FunctionComponent = ({ }); }); }; - const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); const onConnect = (params: Edge | Connection) => setElements((els) => addEdge(params, els)); const onElementClick = (el: FlowElement) => { - const node = entityLineage.nodes?.find((n) => el.id.includes(n.id)); - selectNodeHandler(true, { - name: node?.name as string, - id: el.id, - type: node?.type as string, - }); - setElements((prevElements) => { - return prevElements.map((preEl) => { - if (preEl.id === el.id) { - return { ...preEl, className: `${preEl.className} selected-node` }; + const node = [ + ...(entityLineage.nodes as Array), + entityLineage.entity, + ].find((n) => el.id.includes(n.id)); + if (!expandButton.current) { + selectNodeHandler(true, { + name: node?.name as string, + id: el.id, + type: node?.type as string, + }); + setElements((prevElements) => { + return prevElements.map((preEl) => { + if (preEl.id === el.id) { + return { ...preEl, className: `${preEl.className} selected-node` }; + } else { + return { ...preEl, className: 'leaf-node' }; + } + }); + }); + } else { + expandButton.current = null; + } + }; + + const onNodeExpand = (tableColumns?: Column[]) => { + const elements = 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: 'leaf-node' }; } - }); - }); + }) + ); + }; + + 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.name, + '.', + true + )} columns`, + }); + }); + } }; useEffect(() => { - setElements( - getLineageData( - entityLineage, - selectNodeHandler, - loadNodeHandler, - lineageLeafNodes, - isNodeLoading - ) as Elements - ); + setElements(setElementsHandle()); + setExpandNode(undefined); + setTableColumns([]); }, [entityLineage, isNodeLoading]); + useEffect(() => { + onNodeExpand(); + getTableColumns(expandNode); + }, [expandNode]); + + useEffect(() => { + if (!isEmpty(selectedNode)) { + setExpandNode(undefined); + } + }, [selectedNode]); + + useEffect(() => { + if (tableColumns.length) { + onNodeExpand(tableColumns); + } + }, [tableColumns]); + return (
{(entityLineage?.downstreamEdges ?? []).length > 0 || - (entityLineage.upstreamEdges ?? []).length ? ( + (entityLineage?.upstreamEdges ?? []).length > 0 ? ( onElementClick(el)} onElementsRemove={onElementsRemove} onLoad={onLoad} onNodeContextMenu={onNodeContextMenu} - onNodeDrag={(e) => { - e.stopPropagation(); - }} - onNodeDragStart={(e) => { - e.stopPropagation(); - }} - onNodeDragStop={(e) => { - e.stopPropagation(); - }} + onNodeDrag={dragHandle} + onNodeDragStart={dragHandle} + onNodeDragStop={dragHandle} onNodeMouseEnter={onNodeMouseEnter} onNodeMouseLeave={onNodeMouseLeave} onNodeMouseMove={onNodeMouseMove}> @@ -465,21 +570,7 @@ const Entitylineage: FunctionComponent = ({ ) : ( -
- - Lineage is currently supported for Airflow. To enable lineage - collection from Airflow, Please follow the documentation - - - here - -
+ getNoLineageDataPlaceholder() )}
= ({ size = 'default', type = 'default', + className = '', }: Props): JSX.Element => { let classes = 'loader'; switch (size) { @@ -54,7 +57,9 @@ const Loader: FunctionComponent = ({ break; } - return
; + return ( +
+ ); }; export default Loader; 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 aa082c973cf..fe944d312a6 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 @@ -715,6 +715,9 @@ body .profiler-graph .recharts-active-dot circle { } /* React flow */ +.react-flow__node { + min-width: max-content; +} .leaf-node .react-flow__handle { background-color: #6b7280; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx index 065edaa98cf..4825223a46c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx @@ -54,8 +54,10 @@ import IconInfo from '../assets/svg/info.svg'; import IconIngestion from '../assets/svg/ingestion.svg'; import LogoMonogram from '../assets/svg/logo-monogram.svg'; import Logo from '../assets/svg/logo.svg'; +import IconMinus from '../assets/svg/minus.svg'; import IconPipelineGrey from '../assets/svg/pipeline-grey.svg'; import IconPipeline from '../assets/svg/pipeline.svg'; +import IconPlus from '../assets/svg/plus.svg'; import IconProfiler from '../assets/svg/profiler.svg'; import IconHelpCircle from '../assets/svg/question-circle.svg'; import IconSetting from '../assets/svg/service.svg'; @@ -150,6 +152,8 @@ export const Icons = { VERSION: 'icon-version', VERSION_WHITE: 'icon-version-white', ICON_DEPLOY: 'icon-deploy', + ICON_PLUS: 'icon-plus', + ICON_MINUS: 'icon-minus', }; const SVGIcons: FunctionComponent = ({ @@ -447,6 +451,14 @@ const SVGIcons: FunctionComponent = ({ case Icons.ICON_DEPLOY: IconComponent = IconDeploy; + break; + case Icons.ICON_PLUS: + IconComponent = IconPlus; + + break; + case Icons.ICON_MINUS: + IconComponent = IconMinus; + break; default: