diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.component.tsx index 68fab3527e8..3e676d8c006 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.component.tsx @@ -26,7 +26,7 @@ import React, { useState, } from 'react'; import { useTranslation } from 'react-i18next'; -import { Link, useHistory } from 'react-router-dom'; +import { Link } from 'react-router-dom'; import { restoreDashboard } from 'rest/dashboardAPI'; import { getEntityName } from 'utils/EntityUtils'; import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants'; @@ -53,7 +53,6 @@ import { getGlossaryTermlist, } from '../../utils/GlossaryUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; -import { getLineageViewPath } from '../../utils/RouterUtils'; import SVGIcons from '../../utils/SvgUtils'; import { getTagsWithoutTier } from '../../utils/TableUtils'; import { getClassifications, getTaglist } from '../../utils/TagsUtils'; @@ -97,17 +96,9 @@ const DashboardDetails = ({ charts, chartDescriptionUpdateHandler, chartTagUpdateHandler, - entityLineage, - isNodeLoading, - lineageLeafNodes, - loadNodeHandler, versionHandler, version, deleted, - addLineageHandler, - removeLineageHandler, - entityLineageHandler, - isLineageLoading, entityThread, isentityThreadLoading, postFeedHandler, @@ -123,7 +114,6 @@ const DashboardDetails = ({ onExtensionUpdate, }: DashboardDetailsProps) => { const { t } = useTranslation(); - const history = useHistory(); const [isEdit, setIsEdit] = useState(false); const [followersCount, setFollowersCount] = useState(0); const [isFollowing, setIsFollowing] = useState(false); @@ -476,10 +466,6 @@ const DashboardDetails = ({ }); }; - const handleFullScreenClick = () => { - history.push(getLineageViewPath(EntityType.DASHBOARD, dashboardFQN)); - }; - const onThreadLinkSelect = (link: string, threadType?: ThreadType) => { setThreadLink(link); if (threadType) { @@ -788,21 +774,11 @@ const DashboardDetails = ({ {activeTab === 3 && ( )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.interface.ts index ce5498bb9d6..a52aa057490 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.interface.ts @@ -18,7 +18,6 @@ import { CreateThread } from '../../generated/api/feed/createThread'; import { Chart } from '../../generated/entity/data/chart'; import { Dashboard } from '../../generated/entity/data/dashboard'; import { Thread, ThreadType } from '../../generated/entity/feed/thread'; -import { EntityLineage } from '../../generated/type/entityLineage'; import { EntityReference } from '../../generated/type/entityReference'; import { Paging } from '../../generated/type/paging'; import { TagLabel } from '../../generated/type/tagLabel'; @@ -27,13 +26,6 @@ import { ThreadUpdatedFunc, } from '../../interface/feed.interface'; import { TitleBreadcrumbProps } from '../common/title-breadcrumb/title-breadcrumb.interface'; -import { - Edge, - EdgeData, - LeafNodes, - LineagePos, - LoadingNodeState, -} from '../EntityLineage/EntityLineage.interface'; export interface ChartType extends Chart { displayName: string; @@ -42,9 +34,6 @@ export interface ChartType extends Chart { export interface DashboardDetailsProps { dashboardFQN: string; version: string; - isNodeLoading: LoadingNodeState; - lineageLeafNodes: LeafNodes; - entityLineage: EntityLineage; charts: Array; serviceType: string; dashboardUrl: string; @@ -59,7 +48,6 @@ export interface DashboardDetailsProps { slashedDashboardName: TitleBreadcrumbProps['titleLinks']; entityThread: Thread[]; deleted?: boolean; - isLineageLoading?: boolean; isentityThreadLoading: boolean; feedCount: number; entityFieldThreadCount: EntityFieldThreadCount[]; @@ -87,11 +75,7 @@ export interface DashboardDetailsProps { patch: Array ) => void; tagUpdateHandler: (updatedDashboard: Dashboard) => void; - loadNodeHandler: (node: EntityReference, pos: LineagePos) => void; versionHandler: () => void; - addLineageHandler: (edge: Edge) => Promise; - removeLineageHandler: (data: EdgeData) => void; - entityLineageHandler: (lineage: EntityLineage) => void; postFeedHandler: (value: string, id: string) => void; deletePostHandler: ( threadId: string, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DatasetDetails/DatasetDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DatasetDetails/DatasetDetails.component.tsx index 1c4c2e2669e..188bd641456 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DatasetDetails/DatasetDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DatasetDetails/DatasetDetails.component.tsx @@ -24,7 +24,6 @@ import React, { useState, } from 'react'; import { useTranslation } from 'react-i18next'; -import { useHistory } from 'react-router-dom'; import { restoreTable } from 'rest/tableAPI'; import { getEntityId, getEntityName } from 'utils/EntityUtils'; import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants'; @@ -56,7 +55,6 @@ import { } from '../../utils/CommonUtils'; import { getEntityFieldThreadCounts } from '../../utils/FeedUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; -import { getLineageViewPath } from '../../utils/RouterUtils'; import { getTagsWithoutTier, getUsagePercentile } from '../../utils/TableUtils'; import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; import ActivityFeedList from '../ActivityFeed/ActivityFeedList/ActivityFeedList'; @@ -95,7 +93,6 @@ const DatasetDetails: React.FC = ({ tableProfile, columns, tier, - entityLineage, followTableHandler, unfollowTableHandler, followers, @@ -110,16 +107,9 @@ const DatasetDetails: React.FC = ({ tableType, version, versionHandler, - loadNodeHandler, - lineageLeafNodes, - isNodeLoading, dataModel, deleted, tagUpdateHandler, - addLineageHandler, - removeLineageHandler, - entityLineageHandler, - isLineageLoading, entityThread, isentityThreadLoading, postFeedHandler, @@ -135,7 +125,6 @@ const DatasetDetails: React.FC = ({ isTableProfileLoading, }: DatasetDetailsProps) => { const { t } = useTranslation(); - const history = useHistory(); const [isEdit, setIsEdit] = useState(false); const [followersCount, setFollowersCount] = useState(0); const [isFollowing, setIsFollowing] = useState(false); @@ -593,10 +582,6 @@ const DatasetDetails: React.FC = ({ setThreadLink(''); }; - const handleFullScreenClick = () => { - history.push(getLineageViewPath(EntityType.TABLE, datasetFQN)); - }; - const getLoader = () => { return isentityThreadLoading ? : null; }; @@ -830,20 +815,11 @@ const DatasetDetails: React.FC = ({ )} id="lineageDetails"> )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DatasetDetails/DatasetDetails.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/DatasetDetails/DatasetDetails.interface.ts index 486c2dfad6f..4b423b8815f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DatasetDetails/DatasetDetails.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/DatasetDetails/DatasetDetails.interface.ts @@ -23,7 +23,6 @@ import { UsageDetails, } from '../../generated/entity/data/table'; import { Thread, ThreadType } from '../../generated/entity/feed/thread'; -import { EntityLineage } from '../../generated/type/entityLineage'; import { EntityReference } from '../../generated/type/entityReference'; import { Paging } from '../../generated/type/paging'; import { TagLabel } from '../../generated/type/tagLabel'; @@ -32,17 +31,8 @@ import { ThreadUpdatedFunc, } from '../../interface/feed.interface'; import { TitleBreadcrumbProps } from '../common/title-breadcrumb/title-breadcrumb.interface'; -import { - Edge, - EdgeData, - LeafNodes, - LineagePos, - LoadingNodeState, -} from '../EntityLineage/EntityLineage.interface'; export interface DatasetDetailsProps { - isNodeLoading: LoadingNodeState; - lineageLeafNodes: LeafNodes; version?: string; entityId?: string; joins: TableJoins; @@ -60,14 +50,12 @@ export interface DatasetDetailsProps { columns: Column[]; tier: TagLabel; sampleData: TableData; - entityLineage: EntityLineage; followers: Array; tableTags: Array; slashedTableName: TitleBreadcrumbProps['titleLinks']; entityThread: Thread[]; deleted?: boolean; isTableProfileLoading?: boolean; - isLineageLoading?: boolean; isSampleDataLoading?: boolean; isQueriesLoading?: boolean; isentityThreadLoading: boolean; @@ -84,10 +72,6 @@ export interface DatasetDetailsProps { descriptionUpdateHandler: (updatedTable: Table) => Promise; tagUpdateHandler: (updatedTable: Table) => void; versionHandler: () => void; - loadNodeHandler: (node: EntityReference, pos: LineagePos) => void; - addLineageHandler: (edge: Edge) => Promise; - removeLineageHandler: (data: EdgeData) => void; - entityLineageHandler: (lineage: EntityLineage) => void; postFeedHandler: (value: string, id: string) => void; deletePostHandler: ( threadId: string, 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 397fd12f9a2..9ce5fc73ee4 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 @@ -13,11 +13,13 @@ import { Modal, Space } from 'antd'; import { AxiosError } from 'axios'; +import jsonData from 'jsons/en'; import { debounce, isEmpty, isNil, isUndefined, + union, uniqueId, upperCase, } from 'lodash'; @@ -28,11 +30,11 @@ import React, { FunctionComponent, useCallback, useEffect, - useMemo, useRef, useState, } from 'react'; import { useTranslation } from 'react-i18next'; +import { useHistory, useParams } from 'react-router-dom'; import ReactFlow, { addEdge, Background, @@ -47,9 +49,11 @@ import ReactFlow, { useEdgesState, useNodesState, } from 'reactflow'; +import { getLineageByFQN } from 'rest/lineageAPI'; import { searchData } from 'rest/miscAPI'; import { getTableDetails } from 'rest/tableAPI'; -import { getEntityName } from 'utils/EntityUtils'; +import { getEntityLineage, getEntityName } from 'utils/EntityUtils'; +import { getLineageViewPath } from 'utils/RouterUtils'; import { PAGE_SIZE } from '../../constants/constants'; import { ELEMENT_DELETE_STATE, @@ -72,23 +76,31 @@ import { import { EntityReference } from '../../generated/type/entityReference'; import { withLoader } from '../../hoc/withLoader'; import { + addLineageHandler, createNewEdge, + customEdges, dragHandle, + findNodeById, findUpstreamDownStreamEdge, getAllTracedColumnEdge, getAllTracedNodes, + getChildMap, getClassifiedEdge, getColumnType, getDeletedLineagePlaceholder, getEdgeStyle, getEdgeType, + getEntityLineagePath, getEntityNodeIcon, getLayoutedElements, getLineageData, getLoadingStatusValue, getModalBodyText, getNewLineageConnectionDetails, + getNewNodes, getNodeRemoveButton, + getPaginatedChildMap, + getParamByEntityType, getRemovedNodeData, getSelectedEdgeArr, getUniqueFlowElements, @@ -98,11 +110,13 @@ import { getUpStreamDownStreamColumnLineageArr, isColumnLineageTraced, isTracedEdge, + nodeTypes, onLoad, onNodeContextMenu, onNodeMouseEnter, onNodeMouseLeave, onNodeMouseMove, + removeLineageHandler, } from '../../utils/EntityLineageUtils'; import { getEntityReferenceFromPipeline } from '../../utils/PipelineServiceUtils'; import { showErrorToast } from '../../utils/ToastUtils'; @@ -111,8 +125,6 @@ import EntityInfoDrawer from '../EntityInfoDrawer/EntityInfoDrawer.component'; import Loader from '../Loader/Loader'; import AddPipeLineModal from './AddPipeLineModal'; import CustomControlsComponent from './CustomControls.component'; -import { CustomEdge } from './CustomEdge.component'; -import CustomNode from './CustomNode.component'; import { CustomEdgeData, CustomElement, @@ -120,7 +132,12 @@ import { EdgeTypeEnum, ElementLoadingState, EntityLineageProp, + EntityReferenceChild, + LeafNodes, + LineagePos, + LoadingNodeState, ModifiedColumn, + NodeIndexMap, SelectedEdge, SelectedNode, } from './EntityLineage.interface'; @@ -130,18 +147,9 @@ import LineageNodeLabel from './LineageNodeLabel'; import NodeSuggestions from './NodeSuggestions.component'; const EntityLineageComponent: FunctionComponent = ({ - entityLineage, - loadNodeHandler, - lineageLeafNodes, - isNodeLoading, - isLoading, deleted, - addLineageHandler, - removeLineageHandler, - entityLineageHandler, - onFullScreenClick, hasEditAccess, - onExitFullScreenViewClick, + entityType, }: EntityLineageProp) => { const { t } = useTranslation(); const reactFlowWrapper = useRef(null); @@ -185,26 +193,90 @@ const EntityLineageComponent: FunctionComponent = ({ const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); + const [paginationData, setPaginationData] = useState({}); + const [entityLineage, setEntityLineage] = useState(); + const [updatedLineageData, setUpdatedLineageData] = useState(); + const [childMap, setChildMap] = useState(); + const [isLineageLoading, setIsLineageLoading] = useState(false); + const [isNodeLoading, setNodeLoading] = useState({ + id: undefined, + state: false, + }); + const [leafNodes, setLeafNodes] = useState({} as LeafNodes); - /** - * 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); + const params = useParams>(); + const entityFQN = + params[getParamByEntityType(entityType)] ?? params['entityFQN']; + const history = useHistory(); - /** - * Custom Node Type Object - */ - const nodeTypes = useMemo( - () => ({ - output: CustomNode, - input: CustomNode, - default: CustomNode, - }), - [] + const onFullScreenClick = useCallback(() => { + history.push(getLineageViewPath(entityType, entityFQN)); + }, [entityType, entityFQN]); + + const fetchLineageData = useCallback(async () => { + setIsLineageLoading(true); + try { + const res = await getLineageByFQN(entityFQN, entityType); + setEntityLineage(res); + setUpdatedLineageData(res); + } catch (err) { + showErrorToast( + err as AxiosError, + jsonData['api-error-messages']['fetch-lineage-error'] + ); + } finally { + setIsLineageLoading(false); + } + }, [entityFQN, entityType]); + + const loadNodeHandler = useCallback( + async (node: EntityReference, pos: LineagePos) => { + setNodeLoading((prev) => ({ ...prev, id: node.id, state: true })); + try { + const res = await getLineageByFQN( + node.fullyQualifiedName ?? '', + node.type + ); + if (res && entityLineage) { + setNodeLoading((prev) => ({ ...prev, id: node.id, state: false })); + setLeafNode(res, pos); + setEntityLineage(getEntityLineage(entityLineage, res, pos)); + } + } catch (err) { + setNodeLoading((prev) => ({ ...prev, id: node.id, state: false })); + showErrorToast( + err as AxiosError, + jsonData['api-error-messages']['fetch-lineage-node-error'] + ); + } + }, + [entityLineage, setNodeLoading] ); - const customEdges = useMemo(() => ({ buttonedge: CustomEdge }), []); + + const setLeafNode = useCallback( + (val: EntityLineage, pos: LineagePos) => { + if (pos === 'to' && val.downstreamEdges?.length === 0) { + setLeafNodes((prev) => ({ + ...prev, + downStreamNode: [...(prev.downStreamNode ?? []), val.entity.id], + })); + } + if (pos === 'from' && val.upstreamEdges?.length === 0) { + setLeafNodes((prev) => ({ + ...prev, + upStreamNode: [...(prev.upStreamNode ?? []), val.entity.id], + })); + } + }, + [setLeafNodes] + ); + + const onExitFullScreenViewClick = useCallback(() => { + const path = getEntityLineagePath(entityType, entityFQN); + if (path !== '') { + history.push(path); + } + }, [entityType, entityFQN, history]); /** * take state and value to set selected node @@ -221,23 +293,62 @@ const EntityLineageComponent: FunctionComponent = ({ setSelectedEntity({} as EntityReference); }; - const handleNodeSelection = (node: Node) => { - const selectedNode = [ - ...(updatedLineageData.nodes || []), - updatedLineageData.entity, - ].find((n) => node.id.includes(n.id)); + const selectLoadMoreNode = (node: Node) => { + const { pagination_data, edgeType } = node.data.node; + setPaginationData( + (prevState: { + [key: string]: { upstream: number[]; downstream: number[] }; + }) => { + const { parentId, index } = pagination_data; + const updatedParentData = prevState[parentId] || { + upstream: [], + downstream: [], + }; + const updatedIndexList = + edgeType === EdgeTypeEnum.DOWN_STREAM + ? { + upstream: updatedParentData.upstream, + downstream: [index], + } + : { + upstream: [index], + downstream: updatedParentData.downstream, + }; - if (!expandButton.current) { - selectNodeHandler(true, { - name: selectedNode?.name as string, - fqn: selectedNode?.fullyQualifiedName as string, - id: node.id, - displayName: selectedNode?.displayName, - type: selectedNode?.type as string, - entityId: selectedNode?.id as string, - }); + const retnObj = { + ...prevState, + [parentId]: updatedIndexList, + }; + if (updatedLineageData) { + initLineageChildMaps(updatedLineageData, childMap, retnObj); + } + + return retnObj; + } + ); + }; + + const handleNodeSelection = (node: Node) => { + if (node.type === EntityLineageNodeType.LOAD_MORE) { + selectLoadMoreNode(node); } else { - expandButton.current = null; + const selectedNode = [ + ...(updatedLineageData?.nodes || []), + updatedLineageData?.entity, + ].find((n) => n && node.id.includes(n.id)); + + if (!expandButton.current) { + selectNodeHandler(true, { + name: selectedNode?.name as string, + fqn: selectedNode?.fullyQualifiedName as string, + id: node.id, + displayName: selectedNode?.displayName, + type: selectedNode?.type as string, + entityId: selectedNode?.id as string, + }); + } else { + expandButton.current = null; + } } }; @@ -246,44 +357,48 @@ const EntityLineageComponent: FunctionComponent = ({ * @param data selected edge * @param confirmDelete confirmation state for deleting selected edge */ - const removeEdgeHandler = (data: SelectedEdge, confirmDelete: boolean) => { - if (confirmDelete) { + const removeEdgeHandler = ( + { source, target }: SelectedEdge, + confirmDelete: boolean + ) => { + if (confirmDelete && updatedLineageData) { const edgeData: EdgeData = { - fromEntity: data.source.type, - fromId: data.source.id, - toEntity: data.target.type, - toId: data.target.id, + fromEntity: source.type, + fromId: source.id, + toEntity: target.type, + toId: target.id, }; removeLineageHandler(edgeData); setEdges((prevEdges) => { return prevEdges.filter((edge) => { const isRemovedEdge = - edge.source === data.source.id && edge.target === data.target.id; + edge.source === source.id && edge.target === target.id; return !isRemovedEdge; }); }); const newDownStreamEdges = getSelectedEdgeArr( - updatedLineageData.downstreamEdges || [], + updatedLineageData?.downstreamEdges || [], edgeData ); const newUpStreamEdges = getSelectedEdgeArr( - updatedLineageData.upstreamEdges || [], + updatedLineageData?.upstreamEdges || [], edgeData ); - resetSelectedData(); setUpdatedLineageData({ ...updatedLineageData, downstreamEdges: newDownStreamEdges, upstreamEdges: newUpStreamEdges, }); + + resetSelectedData(); setConfirmDelete(false); } }; const removeColumnEdge = (data: SelectedEdge, confirmDelete: boolean) => { - if (confirmDelete) { + if (confirmDelete && updatedLineageData) { const upStreamEdge = findUpstreamDownStreamEdge( updatedLineageData.upstreamEdges, data @@ -404,6 +519,9 @@ const EntityLineageComponent: FunctionComponent = ({ evt: React.MouseEvent, data: CustomEdgeData ) => { + if (!updatedLineageData) { + return; + } setShowDeleteModal(true); evt.stopPropagation(); setSelectedEdge(() => { @@ -452,6 +570,9 @@ const EntityLineageComponent: FunctionComponent = ({ const removeNodeHandler = useCallback( (node: Node) => { + if (!updatedLineageData) { + return; + } // Get edges connected to selected node const edgesToRemove = getConnectedEdges([node], edges); @@ -560,13 +681,13 @@ const EntityLineageComponent: FunctionComponent = ({ } }; - const setElementsHandle = (data: EntityLineage) => { + const setElementsHandle = (data: EntityLineage, activeNodeId?: string) => { if (!isEmpty(data)) { const graphElements = getLineageData( data, selectNodeHandler, loadNodeHandler, - lineageLeafNodes, + leafNodes, isNodeLoading, isEditMode, 'buttonedge', @@ -588,6 +709,12 @@ const EntityLineageComponent: FunctionComponent = ({ setEdges(edge); setConfirmDelete(false); + if (activeNodeId) { + const activeNode = node.find((item) => item.id === activeNodeId); + if (activeNode) { + selectNode(activeNode); + } + } } }; @@ -601,13 +728,14 @@ const EntityLineageComponent: FunctionComponent = ({ }; const getSourceOrTargetNode = (queryStr: string) => { - return queryStr.includes(updatedLineageData.entity?.id) + return updatedLineageData && + queryStr.includes(updatedLineageData.entity?.id) ? updatedLineageData.entity : selectedEntity; }; const getUpdatedNodes = (entityLineage: EntityLineage) => { - return !isEmpty(selectedEntity) + return entityLineage && !isEmpty(selectedEntity) ? [...(entityLineage.nodes || []), selectedEntity] : entityLineage.nodes; }; @@ -618,6 +746,9 @@ const EntityLineageComponent: FunctionComponent = ({ */ const onConnect = useCallback( (params: Edge | Connection) => { + if (!updatedLineageData) { + return; + } const { target, source, sourceHandle, targetHandle } = params; if (target === source) { @@ -777,18 +908,24 @@ const EntityLineageComponent: FunctionComponent = ({ setTimeout(() => { addLineageHandler(newEdge) .then(() => { + if (!updatedLineageData) { + return; + } setStatus('success'); setLoading(false); setUpdatedLineageData((pre) => { + if (!pre) { + return; + } const newData = { ...pre, nodes: getUpdatedNodes(pre), downstreamEdges: updatedStreamEdges( - pre.downstreamEdges, + pre?.downstreamEdges, EdgeTypeEnum.DOWN_STREAM ), upstreamEdges: updatedStreamEdges( - pre.upstreamEdges, + pre?.upstreamEdges, EdgeTypeEnum.UP_STREAM ), }; @@ -827,7 +964,7 @@ const EntityLineageComponent: FunctionComponent = ({ }; const handleModalSave = () => { - if (selectedEdge.data) { + if (selectedEdge.data && updatedLineageData) { setStatus('waiting'); setLoading(true); const { source, target } = selectedEdge.data; @@ -855,7 +992,7 @@ const EntityLineageComponent: FunctionComponent = ({ setStatus('success'); setLoading(false); setUpdatedLineageData((pre) => { - if (selectedEdge.data) { + if (selectedEdge.data && pre) { const newData = { ...pre, downstreamEdges: getUpdatedEdgeWithPipeline( @@ -1225,6 +1362,9 @@ const EntityLineageComponent: FunctionComponent = ({ }; const handleExpandColumnClick = () => { + if (!updatedLineageData) { + return; + } if (expandAllColumns) { toggleColumnView(false); } else { @@ -1269,21 +1409,45 @@ const EntityLineageComponent: FunctionComponent = ({ } }; + const selectNode = (node: Node) => { + const { position } = node; + onNodeClick(node); + // moving selected node in center + reactFlowInstance && + reactFlowInstance.setCenter(position.x, position.y, { + duration: ZOOM_TRANSITION_DURATION, + zoom: zoomValue, + }); + }; + const handleOptionSelect = (value?: string) => { if (value) { const selectedNode = nodes.find((node) => node.id === value); if (selectedNode) { - const { position } = selectedNode; - onNodeClick(selectedNode); - // moving selected node in center - reactFlowInstance && - reactFlowInstance.setCenter(position.x, position.y, { - duration: ZOOM_TRANSITION_DURATION, - zoom: MIN_ZOOM_VALUE, - }); + selectNode(selectedNode); } else { - onPaneClick(); + const path = findNodeById(value, childMap?.children, []) || []; + const lastNode = path[path?.length - 1]; + if (updatedLineageData) { + const { nodes, edges } = getPaginatedChildMap( + updatedLineageData, + childMap, + paginationData + ); + const newNodes = union(nodes, path); + setElementsHandle( + { + ...updatedLineageData, + nodes: newNodes, + downstreamEdges: [ + ...(updatedLineageData.downstreamEdges || []), + ...edges, + ], + }, + lastNode.id + ); + } } } }; @@ -1293,7 +1457,7 @@ const EntityLineageComponent: FunctionComponent = ({ * Change newly added node label based on entity:EntityReference */ const handleUpdatedLineageNode = () => { - const uNodes = updatedLineageData.nodes; + const uNodes = updatedLineageData?.nodes; const newlyAddedNodeElement = nodes.find((el) => el?.data?.isNewNode); const newlyAddedNode = uNodes?.find( (node) => node.id === newlyAddedNodeElement?.id @@ -1325,23 +1489,52 @@ const EntityLineageComponent: FunctionComponent = ({ setZoomValue(value); }, 150); - useEffect(() => { - if (!deleted && !isEmpty(updatedLineageData)) { - setElementsHandle(updatedLineageData); + const initLineageChildMaps = ( + lineageData: EntityLineage, + childMapObj: EntityReferenceChild | undefined, + paginationObj: Record + ) => { + if (lineageData && childMapObj) { + const { nodes: newNodes, edges } = getPaginatedChildMap( + lineageData, + childMapObj, + paginationObj + ); + setElementsHandle({ + ...lineageData, + nodes: newNodes, + downstreamEdges: [...(lineageData.downstreamEdges || []), ...edges], + }); } - }, [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 }); + fetchLineageData(); + }, []); + + useEffect(() => { + if (!entityLineage) { + return; + } + if ( + !isEmpty(entityLineage) && + !isUndefined(entityLineage.entity) && + !deleted + ) { + const childMapObj: EntityReferenceChild = getChildMap(entityLineage); + setChildMap(childMapObj); + initLineageChildMaps(entityLineage, childMapObj, paginationData); + } + }, [entityLineage]); + + useEffect(() => { + if (!updatedLineageData) { + return; + } + setEntityLineage({ + ...updatedLineageData, + nodes: getNewNodes(updatedLineageData), + }); }, [isEditMode]); useEffect(() => { @@ -1360,24 +1553,13 @@ const EntityLineageComponent: FunctionComponent = ({ } }, [selectedEdge, confirmDelete]); - useEffect(() => { - if ( - !isEmpty(entityLineage) && - !isUndefined(entityLineage.entity) && - !deleted - ) { - setUpdatedLineageData(entityLineage); - setElementsHandle(entityLineage); - } - }, [entityLineage]); - useEffect(() => { if (pipelineSearchValue) { getSearchResults(pipelineSearchValue); } }, [pipelineSearchValue]); - if (isLoading || (nodes.length === 0 && !deleted)) { + if (isLineageLoading || (nodes.length === 0 && !deleted)) { return ; } @@ -1390,6 +1572,7 @@ const EntityLineageComponent: FunctionComponent = ({
= ({ setReactFlowInstance(reactFlowInstance); }} onMove={(_e, viewPort) => handleZoomLevel(viewPort.zoom)} - onNodeClick={(_e, node) => onNodeClick(node)} + onNodeClick={(_e, node) => { + onNodeClick(node); + _e.stopPropagation(); + }} onNodeContextMenu={onNodeContextMenu} onNodeDrag={dragHandle} onNodeDragStart={dragHandle} @@ -1420,26 +1606,28 @@ const EntityLineageComponent: FunctionComponent = ({ onNodeMouseMove={onNodeMouseMove} onNodesChange={onNodesChange} onPaneClick={onPaneClick}> - + {updatedLineageData && ( + + )} {isEditMode && ( )} @@ -1460,7 +1648,7 @@ const EntityLineageComponent: FunctionComponent = ({ /> ) : ( void; - addLineageHandler: (edge: Edge) => Promise; - removeLineageHandler: (data: EdgeData) => void; - entityLineageHandler: (lineage: EntityLineage) => void; - onFullScreenClick?: () => void; - onExitFullScreenViewClick?: () => void; } export interface Edge { @@ -146,3 +136,18 @@ export interface LoadingNodeState { id: string | undefined; state: boolean; } + +export interface EntityReferenceChild extends EntityReference { + /** + * Children of this entity, if any. + */ + children?: EntityReferenceChild[]; + parents?: EntityReferenceChild[]; + pageIndex?: number; + edgeType?: EdgeTypeEnum; +} + +export interface NodeIndexMap { + upstream: number[]; + downstream: number[]; +} 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 2c167129ea9..aafca3ad3fa 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 @@ -11,10 +11,11 @@ * limitations under the License. */ -import { findByTestId, render, screen } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; +import { MOCK_CHILD_MAP, MOCK_LINEAGE_DATA } from 'mocks/Lineage.mock'; import React from 'react'; +import { act } from 'react-dom/test-utils'; import { MemoryRouter } from 'react-router-dom'; -import { act } from 'react-test-renderer'; import { EntityType } from '../../enums/entity.enum'; import EntityLineage from './EntityLineage.component'; @@ -22,98 +23,10 @@ jest.mock('../common/rich-text-editor/RichTextEditorPreviewer', () => { return jest.fn().mockReturnValue(

RichTextEditorPreviewer

); }); -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, entityType: EntityType.TABLE, - loadNodeHandler: jest.fn(), - addLineageHandler: jest.fn(), - removeLineageHandler: jest.fn(), - entityLineageHandler: jest.fn(), + hasEditAccess: true, }; const mockFlowData = { @@ -139,6 +52,11 @@ const mockFlowData = { edge: [], }; +const mockPaginatedData = { + nodes: [...mockFlowData.node], + edges: [], +}; + jest.mock('../../utils/EntityLineageUtils', () => ({ dragHandle: jest.fn(), getDataLabel: jest @@ -153,6 +71,8 @@ jest.mock('../../utils/EntityLineageUtils', () => ({ getLoadingStatusValue: jest.fn().mockReturnValue(

Confirm

), getLayoutedElements: jest.fn().mockImplementation(() => mockFlowData), getLineageData: jest.fn().mockImplementation(() => mockFlowData), + getPaginatedChildMap: jest.fn().mockImplementation(() => mockPaginatedData), + getChildMap: jest.fn().mockImplementation(() => MOCK_CHILD_MAP), getModalBodyText: jest.fn(), onLoad: jest.fn(), onNodeContextMenu: jest.fn(), @@ -160,6 +80,7 @@ jest.mock('../../utils/EntityLineageUtils', () => ({ onNodeMouseLeave: jest.fn(), onNodeMouseMove: jest.fn(), getUniqueFlowElements: jest.fn().mockReturnValue([]), + getParamByEntityType: jest.fn().mockReturnValue('entityFQN'), })); jest.mock('../../utils/TableUtils', () => ({ @@ -173,6 +94,14 @@ jest.mock('../../hooks/authHooks', () => ({ }), })); +jest.mock('rest/lineageAPI', () => ({ + getLineageByFQN: jest.fn().mockImplementation(() => + Promise.resolve({ + ...MOCK_LINEAGE_DATA, + }) + ), +})); + jest.mock('../../utils/PermissionsUtils', () => ({ hasPermission: jest.fn().mockReturnValue(false), })); @@ -183,16 +112,17 @@ jest.mock('../EntityInfoDrawer/EntityInfoDrawer.component', () => { describe('Test EntityLineage Component', () => { it('Check if EntityLineage is rendering all the nodes', async () => { - const { container } = render(, { - wrapper: MemoryRouter, + act(() => { + render(, { + wrapper: MemoryRouter, + }); }); - const lineageContainer = await findByTestId(container, 'lineage-container'); - const reactFlowElement = await findByTestId(container, 'rf__wrapper'); - - expect(reactFlowElement).toBeInTheDocument(); + const lineageContainer = await screen.findByTestId('lineage-container'); + const reactFlowElement = await screen.findByTestId('rf__wrapper'); expect(lineageContainer).toBeInTheDocument(); + expect(reactFlowElement).toBeInTheDocument(); }); it('Check if EntityLineage has deleted as true', async () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/LineageNodeLabel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/LineageNodeLabel.tsx index 63832a28323..dfa0de42813 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/LineageNodeLabel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/LineageNodeLabel.tsx @@ -12,7 +12,10 @@ */ import { Button } from 'antd'; +import { EntityLineageNodeType } from 'enums/entity.enum'; +import { get } from 'lodash'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import SVGIcons, { Icons } from 'utils/SvgUtils'; import { EntityReference } from '../../generated/type/entityReference'; import { getDataLabel } from '../../utils/EntityLineageUtils'; @@ -24,6 +27,63 @@ interface LineageNodeLabelProps { isExpanded?: boolean; } +const TableExpandButton = ({ + node, + onNodeExpand, + isExpanded, +}: LineageNodeLabelProps) => { + if (node.type !== 'table') { + return null; + } + + return ( +