From 1ddcc94ccd00160310ec99f0fa981df042efca55 Mon Sep 17 00:00:00 2001 From: Sachin Chaurasiya Date: Sat, 19 Mar 2022 01:02:47 +0530 Subject: [PATCH] Fix #2797 Lineage: Issue while adding downstream Table/Dashboard/Pipeline (#3506) --- .../EntityInfoDrawer.component.tsx | 6 +- .../EntityLineage/EntityLineage.component.tsx | 115 +++++++++- .../EntityLineage/EntityLineage.interface.ts | 1 + .../Entitylineage.component.test.tsx | 200 ++++++++++++++++++ .../ui/src/utils/EntityLineageUtils.tsx | 26 ++- .../resources/ui/src/utils/EntityUtils.tsx | 12 +- 6 files changed, 339 insertions(+), 21 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/Entitylineage.component.test.tsx 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 d14cdcf8ac8..a91a8667382 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 @@ -153,7 +153,11 @@ const EntityInfoDrawer = ({

{getEntityIcon(selectedNode.type)} - {getHeaderLabel(selectedNode.name, selectedNode.type, isMainNode)} + {getHeaderLabel( + selectedNode.displayName ?? selectedNode.name, + selectedNode.type, + isMainNode + )}

= ({ status: 'initial', }); + /** + * take node as input and check if node is main entity or not + * @param node + * @returns class `leaf-node core` for main node and `leaf-node` for leaf node + */ const getNodeClass = (node: FlowElement) => { return `${ node.id.includes(lineageData.entity?.id) && !isEditMode @@ -141,14 +146,29 @@ const Entitylineage: FunctionComponent = ({ }`; }; + /** + * take entity as input and set it as selected entity + * @param entity + */ const selectedEntityHandler = (entity: EntityReference) => { setSelectedEntity(entity); }; + + /** + * take state and value to set selected node + * @param state + * @param value + */ const selectNodeHandler = (state: boolean, value: SelectedNode) => { setIsDrawerOpen(state); setSelectedNode(value); }; + /** + * + * @param node + * @returns lable for given node + */ const getNodeLable = (node: EntityReference) => { return ( <> @@ -177,6 +197,11 @@ const Entitylineage: FunctionComponent = ({ ); }; + /** + * + * @param data selected edge + * @param confirmDelete confirmation state for deleting seslected edge + */ const removeEdgeHandler = (data: SelectedEdge, confirmDelete: boolean) => { if (confirmDelete) { const edgeData: EdgeData = { @@ -215,6 +240,11 @@ const Entitylineage: FunctionComponent = ({ } }; + /** + * take edge data and set it as selected edge + * @param evt + * @param data + */ const onEdgeClick = ( evt: React.MouseEvent, data: CustomEdgeData @@ -245,6 +275,10 @@ const Entitylineage: FunctionComponent = ({ }); }; + /** + * + * @returns unique flow elements + */ const setElementsHandle = () => { const flag: { [x: string]: boolean } = {}; const uniqueElements: Elements = []; @@ -276,6 +310,10 @@ const Entitylineage: FunctionComponent = ({ getLayoutedElements(setElementsHandle()) ); + /** + * take boolean value as input and reset selected node + * @param value + */ const closeDrawer = (value: boolean) => { setIsDrawerOpen(value); setElements((prevElements) => { @@ -293,21 +331,47 @@ 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 downstreamNode = lineageData.downstreamEdges?.find((d) => + const nodes = [ + ...(lineageData.nodes as EntityReference[]), + lineageData.entity, + ]; + + const sourceDownstreamNode = lineageData.downstreamEdges?.find((d) => source?.includes(d.toEntity as string) ); - let targetNode = lineageData.nodes?.find((n) => target?.includes(n.id)); + const sourceUpStreamNode = lineageData.upstreamEdges?.find((u) => + source?.includes(u.fromEntity as string) + ); - let sourceNode = lineageData.nodes?.find((n) => source?.includes(n.id)); + const targetDownStreamNode = lineageData.downstreamEdges?.find((d) => + target?.includes(d.toEntity as string) + ); + + const targetUpStreamNode = lineageData.upstreamEdges?.find((u) => + target?.includes(u.fromEntity as string) + ); + + let targetNode = nodes?.find((n) => target?.includes(n.id)); + + let sourceNode = nodes?.find((n) => source?.includes(n.id)); if (isUndefined(targetNode)) { targetNode = target?.includes(lineageData.entity?.id) @@ -332,9 +396,11 @@ const Entitylineage: FunctionComponent = ({ }, }, }; + setElements((els) => addEdge({ ...params, arrowHeadType: ArrowHeadType.ArrowClosed }, els) ); + setTimeout(() => { addLineageHandler(newEdge) .then(() => { @@ -350,8 +416,9 @@ const Entitylineage: FunctionComponent = ({ ] : lineageData.nodes, downstreamEdges: - !isUndefined(downstreamNode) || - sourceNode?.id === lineageData.entity?.id + !isUndefined(sourceUpStreamNode) || + !isUndefined(targetUpStreamNode) || + targetNode?.id === selectedEntity.id ? [ ...(lineageData.downstreamEdges as EntityEdge[]), { @@ -361,8 +428,9 @@ const Entitylineage: FunctionComponent = ({ ] : lineageData.downstreamEdges, upstreamEdges: - isUndefined(downstreamNode) && - sourceNode?.id !== lineageData.entity?.id + !isUndefined(sourceDownstreamNode) || + !isUndefined(targetDownStreamNode) || + sourceNode?.id === selectedEntity.id ? [ ...(lineageData.upstreamEdges as EntityEdge[]), { @@ -384,6 +452,10 @@ const Entitylineage: FunctionComponent = ({ }, 500); }; + /** + * take element and perform onClick logic + * @param el + */ const onElementClick = (el: FlowElement) => { const node = [ ...(lineageData.nodes as Array), @@ -393,6 +465,7 @@ const Entitylineage: FunctionComponent = ({ selectNodeHandler(true, { name: node?.name as string, id: el.id, + displayName: node?.displayName, type: node?.type as string, entityId: node?.id as string, }); @@ -413,6 +486,10 @@ const Entitylineage: FunctionComponent = ({ } }; + /** + * this method is used for table entity to show table columns + * @param tableColumns + */ const onNodeExpand = (tableColumns?: Column[]) => { const elements = getLayoutedElements(setElementsHandle()); setElements( @@ -433,6 +510,10 @@ const Entitylineage: FunctionComponent = ({ ); }; + /** + * take node and get the columns for that node + * @param expandNode + */ const getTableColumns = (expandNode?: EntityReference) => { if (expandNode) { getTableDetails(expandNode.id, ['columns']) @@ -454,16 +535,28 @@ 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); }; + /** + * handle node drag event + * @param event + */ const onDragOver = (event: DragEvent) => { event.preventDefault(); event.dataTransfer.dropEffect = 'move'; }; + /** + * handle node drop event + * @param event + */ const onDrop = (event: DragEvent) => { event.preventDefault(); @@ -518,6 +611,9 @@ const Entitylineage: FunctionComponent = ({ } }; + /** + * handle onNode select logic + */ const onEntitySelect = () => { if (!isEmpty(selectedEntity)) { const isExistingNode = elements.some((n) => @@ -630,10 +726,12 @@ const Entitylineage: FunctionComponent = ({
+ )} + data-testid="lineage-container">
= ({ ) : null} - :
({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); + +const mockLineageData = { + entity: { + id: 'efcc334a-41c8-483e-b779-464a88a7ece3', + type: 'table', + name: 'bigquery_gcp.shopify.raw_product_catalog', + description: '1234', + displayName: 'raw_product_catalog', + href: 'http://localhost:8585/api/v1/tables/efcc334a-41c8-483e-b779-464a88a7ece3', + }, + nodes: [ + { + description: 'dim_address ETL pipeline', + displayName: 'dim_address etl', + id: 'c14d78eb-dc17-4bc4-b54b-227318116da3', + type: 'pipeline', + name: 'sample_airflow.dim_address_etl', + }, + { + description: '', + displayName: 'deck.gl Demo', + id: '7408172f-bd78-4c60-a270-f9d5ed1490ab', + type: 'dashboard', + name: 'sample_superset.10', + }, + { + description: '', + displayName: 'dim_address', + id: 'c3cb016a-dc6e-4d22-9aa5-be8b32999a6b', + type: 'table', + name: 'bigquery_gcp.shopify.dim_address', + }, + { + description: 'diim_location ETL pipeline', + displayName: 'dim_location etl', + id: 'bb1c2c56-9b0e-4f8e-920d-02819e5ee288', + type: 'pipeline', + name: 'sample_airflow.dim_location_etl', + }, + { + description: '', + displayName: 'dim_api_client', + id: 'abb6567e-fbd9-47d9-95f6-29a80a5a0a52', + type: 'table', + name: 'bigquery_gcp.shopify.dim_api_client', + }, + ], + upstreamEdges: [ + { + fromEntity: 'c14d78eb-dc17-4bc4-b54b-227318116da3', + toEntity: 'efcc334a-41c8-483e-b779-464a88a7ece3', + }, + { + fromEntity: 'bb1c2c56-9b0e-4f8e-920d-02819e5ee288', + toEntity: '7408172f-bd78-4c60-a270-f9d5ed1490ab', + }, + { + fromEntity: '7408172f-bd78-4c60-a270-f9d5ed1490ab', + toEntity: 'abb6567e-fbd9-47d9-95f6-29a80a5a0a52', + }, + ], + downstreamEdges: [ + { + fromEntity: 'efcc334a-41c8-483e-b779-464a88a7ece3', + toEntity: '7408172f-bd78-4c60-a270-f9d5ed1490ab', + }, + { + fromEntity: 'c14d78eb-dc17-4bc4-b54b-227318116da3', + toEntity: 'c3cb016a-dc6e-4d22-9aa5-be8b32999a6b', + }, + { + fromEntity: '7408172f-bd78-4c60-a270-f9d5ed1490ab', + toEntity: 'abb6567e-fbd9-47d9-95f6-29a80a5a0a52', + }, + ], +}; + +const mockEntityLineageProp = { + entityLineage: mockLineageData, + lineageLeafNodes: { + upStreamNode: [], + downStreamNode: [], + }, + isNodeLoading: { + id: 'id1', + state: false, + }, + deleted: false, + loadNodeHandler: jest.fn(), + addLineageHandler: jest.fn(), + removeLineageHandler: jest.fn(), + entityLineageHandler: jest.fn(), +}; + +jest.mock('../../utils/EntityLineageUtils', () => ({ + dragHandle: jest.fn(), + getDataLabel: jest + .fn() + .mockReturnValue(datalabel), + getDeletedLineagePlaceholder: jest + .fn() + .mockReturnValue( +

Lineage data is not available for deleted entities.

+ ), + getHeaderLabel: jest.fn().mockReturnValue(

Header label

), + getLayoutedElements: jest.fn().mockReturnValue([]), + getLineageData: jest.fn().mockReturnValue([]), + getModalBodyText: jest.fn(), + onLoad: jest.fn(), + onNodeContextMenu: jest.fn(), + onNodeMouseEnter: jest.fn(), + onNodeMouseLeave: jest.fn(), + onNodeMouseMove: jest.fn(), +})); + +jest.mock('../../utils/TableUtils', () => ({ + getEntityIcon: jest.fn(), +})); + +jest.mock('../../auth-provider/AuthProvider', () => ({ + useAuthContext: jest.fn().mockReturnValue({ isAuthDisabled: true }), +})); + +jest.mock('../../hooks/authHooks', () => ({ + useAuth: jest.fn().mockReturnValue({ + userPermissions: {}, + isAdminUser: false, + }), +})); + +describe('Test EntityLineage Component', () => { + it('Check if EntityLineage is rendering all the nodes', async () => { + const { container } = render(, { + wrapper: MemoryRouter, + }); + + const lineageContainer = await findByTestId(container, 'lineage-container'); + const reactFlowElement = await findByTestId( + container, + 'react-flow-component' + ); + + expect(reactFlowElement).toBeInTheDocument(); + + expect(lineageContainer).toBeInTheDocument(); + }); + + it('Check if EntityLineage has deleted as true', async () => { + const { container } = render( + , + { + wrapper: MemoryRouter, + } + ); + + const lineageContainer = queryByTestId(container, 'lineage-container'); + const reactFlowElement = queryByTestId(container, 'react-flow-component'); + const deletedMessage = await findByText( + container, + /Lineage data is not available for deleted entities/i + ); + + expect(deletedMessage).toBeInTheDocument(); + + expect(reactFlowElement).not.toBeInTheDocument(); + + expect(lineageContainer).not.toBeInTheDocument(); + }); +}); 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 07ef1f673a6..4f99805cccb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.tsx @@ -11,6 +11,11 @@ * limitations under the License. */ +import { + faChevronLeft, + faChevronRight, +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import dagre from 'dagre'; import { LeafNodes, LineagePos, LoadingNodeState } from 'Models'; import React, { MouseEvent as ReactMouseEvent } from 'react'; @@ -46,11 +51,6 @@ import { EntityReference } from '../generated/type/entityReference'; import { getPartialNameFromFQN } from './CommonUtils'; import { isLeafNode } from './EntityUtils'; import { getEntityLink } from './TableUtils'; -import { - faChevronLeft, - faChevronRight, -} from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; export const getHeaderLabel = ( v = '', @@ -121,7 +121,10 @@ export const getLineageData = ( ) => void ) => { const [x, y] = [0, 0]; - const nodes = entityLineage['nodes']; + const nodes = [ + ...(entityLineage['nodes'] as EntityReference[]), + entityLineage['entity'], + ]; let upstreamEdges: Array = entityLineage['upstreamEdges']?.map((up) => ({ isMapped: false, ...up })) || []; @@ -290,9 +293,16 @@ export const getLineageData = ( return downNodesArr; }; - getUpStreamData(mainNode); + /** + * Get upstream and downstream of each node and store it in + * UPStreamNodes + * DOWNStreamNodes + */ + nodes?.forEach((node) => { + getUpStreamData(node); - getDownStreamData(mainNode); + getDownStreamData(node); + }); const lineageData = [ { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx index 10b68943d36..d6b44568dc5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx @@ -228,8 +228,14 @@ export const getEntityOverview = ( return overview; } case EntityType.DASHBOARD: { - const { owner, tags, dashboardUrl, service, fullyQualifiedName } = - entityDetail; + const { + owner, + tags, + dashboardUrl, + service, + fullyQualifiedName, + displayName, + } = entityDetail; const ownerValue = getOwnerFromId(owner?.id); const tier = getTierFromTableTags(tags || []); @@ -260,7 +266,7 @@ export const getEntityOverview = ( }, { name: `${serviceType} url`, - value: fullyQualifiedName?.split('.')[1] as string, + value: displayName || (fullyQualifiedName?.split('.')[1] as string), url: dashboardUrl as string, isLink: true, isExternal: true,