mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-20 23:18:01 +00:00
lineage: fix expand collapse operation on nodes (#21309)
This commit is contained in:
parent
e55c836cb6
commit
074c2efa10
@ -13,7 +13,9 @@
|
|||||||
import test, { expect } from '@playwright/test';
|
import test, { expect } from '@playwright/test';
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
import { GlobalSettingOptions } from '../../constant/settings';
|
import { GlobalSettingOptions } from '../../constant/settings';
|
||||||
|
import { ContainerClass } from '../../support/entity/ContainerClass';
|
||||||
import { DashboardClass } from '../../support/entity/DashboardClass';
|
import { DashboardClass } from '../../support/entity/DashboardClass';
|
||||||
|
import { MetricClass } from '../../support/entity/MetricClass';
|
||||||
import { MlModelClass } from '../../support/entity/MlModelClass';
|
import { MlModelClass } from '../../support/entity/MlModelClass';
|
||||||
import { SearchIndexClass } from '../../support/entity/SearchIndexClass';
|
import { SearchIndexClass } from '../../support/entity/SearchIndexClass';
|
||||||
import { TableClass } from '../../support/entity/TableClass';
|
import { TableClass } from '../../support/entity/TableClass';
|
||||||
@ -27,6 +29,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
addPipelineBetweenNodes,
|
addPipelineBetweenNodes,
|
||||||
fillLineageConfigForm,
|
fillLineageConfigForm,
|
||||||
|
performCollapse,
|
||||||
performExpand,
|
performExpand,
|
||||||
performZoomOut,
|
performZoomOut,
|
||||||
verifyColumnLayerActive,
|
verifyColumnLayerActive,
|
||||||
@ -48,6 +51,8 @@ test.describe('Lineage Settings Tests', () => {
|
|||||||
const dashboard = new DashboardClass();
|
const dashboard = new DashboardClass();
|
||||||
const mlModel = new MlModelClass();
|
const mlModel = new MlModelClass();
|
||||||
const searchIndex = new SearchIndexClass();
|
const searchIndex = new SearchIndexClass();
|
||||||
|
const container = new ContainerClass();
|
||||||
|
const metric = new MetricClass();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@ -56,12 +61,16 @@ test.describe('Lineage Settings Tests', () => {
|
|||||||
dashboard.create(apiContext),
|
dashboard.create(apiContext),
|
||||||
mlModel.create(apiContext),
|
mlModel.create(apiContext),
|
||||||
searchIndex.create(apiContext),
|
searchIndex.create(apiContext),
|
||||||
|
container.create(apiContext),
|
||||||
|
metric.create(apiContext),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await addPipelineBetweenNodes(page, table, topic);
|
await addPipelineBetweenNodes(page, table, topic);
|
||||||
await addPipelineBetweenNodes(page, topic, dashboard);
|
await addPipelineBetweenNodes(page, topic, dashboard);
|
||||||
await addPipelineBetweenNodes(page, dashboard, mlModel);
|
await addPipelineBetweenNodes(page, dashboard, mlModel);
|
||||||
await addPipelineBetweenNodes(page, mlModel, searchIndex);
|
await addPipelineBetweenNodes(page, mlModel, searchIndex);
|
||||||
|
await addPipelineBetweenNodes(page, searchIndex, container);
|
||||||
|
await addPipelineBetweenNodes(page, container, metric);
|
||||||
|
|
||||||
await test.step(
|
await test.step(
|
||||||
'Lineage config should throw error if upstream depth is less than 0',
|
'Lineage config should throw error if upstream depth is less than 0',
|
||||||
@ -168,7 +177,17 @@ test.describe('Lineage Settings Tests', () => {
|
|||||||
await verifyNodePresent(page, topic);
|
await verifyNodePresent(page, topic);
|
||||||
await verifyNodePresent(page, mlModel);
|
await verifyNodePresent(page, mlModel);
|
||||||
await performExpand(page, mlModel, false, searchIndex);
|
await performExpand(page, mlModel, false, searchIndex);
|
||||||
await performExpand(page, topic, true);
|
await performExpand(page, searchIndex, false, container);
|
||||||
|
await performExpand(page, container, false, metric);
|
||||||
|
await performExpand(page, topic, true, table);
|
||||||
|
|
||||||
|
// perform collapse
|
||||||
|
await performCollapse(page, mlModel, false, [
|
||||||
|
searchIndex,
|
||||||
|
container,
|
||||||
|
metric,
|
||||||
|
]);
|
||||||
|
await performCollapse(page, dashboard, true, [table, topic]);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -275,6 +275,7 @@ test('Verify column lineage between table and topic', async ({ browser }) => {
|
|||||||
|
|
||||||
await table.visitEntityPage(page);
|
await table.visitEntityPage(page);
|
||||||
await visitLineageTab(page);
|
await visitLineageTab(page);
|
||||||
|
await activateColumnLayer(page);
|
||||||
await page.click('[data-testid="edit-lineage"]');
|
await page.click('[data-testid="edit-lineage"]');
|
||||||
|
|
||||||
await removeColumnLineage(page, sourceCol, targetCol);
|
await removeColumnLineage(page, sourceCol, targetCol);
|
||||||
@ -389,6 +390,8 @@ test('Verify function data in edge drawer', async ({ browser }) => {
|
|||||||
await page.reload();
|
await page.reload();
|
||||||
await lineageReq;
|
await lineageReq;
|
||||||
|
|
||||||
|
await activateColumnLayer(page);
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.locator(
|
.locator(
|
||||||
`[data-testid="column-edge-${btoa(sourceColName)}-${btoa(
|
`[data-testid="column-edge-${btoa(sourceColName)}-${btoa(
|
||||||
|
@ -236,6 +236,30 @@ export const performExpand = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const performCollapse = async (
|
||||||
|
page: Page,
|
||||||
|
node: EntityClass,
|
||||||
|
upstream: boolean,
|
||||||
|
hiddenEntity: EntityClass[]
|
||||||
|
) => {
|
||||||
|
const nodeFqn = get(node, 'entityResponseData.fullyQualifiedName');
|
||||||
|
const handleDirection = upstream ? 'left' : 'right';
|
||||||
|
const collapseBtn = page
|
||||||
|
.locator(`[data-testid="lineage-node-${nodeFqn}"]`)
|
||||||
|
.locator(`.react-flow__handle-${handleDirection}`)
|
||||||
|
.getByTestId('minus-icon');
|
||||||
|
|
||||||
|
await collapseBtn.click();
|
||||||
|
|
||||||
|
for (const entity of hiddenEntity) {
|
||||||
|
const hiddenNodeFqn = get(entity, 'entityResponseData.fullyQualifiedName');
|
||||||
|
const hiddenNode = page.locator(
|
||||||
|
`[data-testid="lineage-node-${hiddenNodeFqn}"]`
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(hiddenNode).not.toBeVisible();
|
||||||
|
}
|
||||||
|
};
|
||||||
export const verifyNodePresent = async (page: Page, node: EntityClass) => {
|
export const verifyNodePresent = async (page: Page, node: EntityClass) => {
|
||||||
const nodeFqn = get(node, 'entityResponseData.fullyQualifiedName');
|
const nodeFqn = get(node, 'entityResponseData.fullyQualifiedName');
|
||||||
const name = get(node, 'entityResponseData.name');
|
const name = get(node, 'entityResponseData.name');
|
||||||
|
@ -208,3 +208,22 @@ export const getColumnContent = (
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function getNodeClassNames({
|
||||||
|
isSelected,
|
||||||
|
showDqTracing,
|
||||||
|
isTraced,
|
||||||
|
}: {
|
||||||
|
isSelected: boolean;
|
||||||
|
showDqTracing: boolean;
|
||||||
|
isTraced: boolean;
|
||||||
|
}) {
|
||||||
|
return classNames(
|
||||||
|
'lineage-node p-0',
|
||||||
|
isSelected ? 'custom-node-header-active' : 'custom-node-header-normal',
|
||||||
|
{
|
||||||
|
'data-quality-failed-custom-node-header': showDqTracing,
|
||||||
|
'custom-node-header-tracing': isTraced,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -11,7 +11,6 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { Handle, NodeProps, Position } from 'reactflow';
|
import { Handle, NodeProps, Position } from 'reactflow';
|
||||||
import { useLineageProvider } from '../../../context/LineageProvider/LineageProvider';
|
import { useLineageProvider } from '../../../context/LineageProvider/LineageProvider';
|
||||||
@ -20,7 +19,11 @@ import { LineageDirection } from '../../../generated/api/lineage/lineageDirectio
|
|||||||
import { LineageLayer } from '../../../generated/configuration/lineageSettings';
|
import { LineageLayer } from '../../../generated/configuration/lineageSettings';
|
||||||
import LineageNodeRemoveButton from '../../Lineage/LineageNodeRemoveButton';
|
import LineageNodeRemoveButton from '../../Lineage/LineageNodeRemoveButton';
|
||||||
import './custom-node.less';
|
import './custom-node.less';
|
||||||
import { getCollapseHandle, getExpandHandle } from './CustomNode.utils';
|
import {
|
||||||
|
getCollapseHandle,
|
||||||
|
getExpandHandle,
|
||||||
|
getNodeClassNames,
|
||||||
|
} from './CustomNode.utils';
|
||||||
import './entity-lineage.style.less';
|
import './entity-lineage.style.less';
|
||||||
import {
|
import {
|
||||||
ExpandCollapseHandlesProps,
|
ExpandCollapseHandlesProps,
|
||||||
@ -100,7 +103,8 @@ const ExpandCollapseHandles = memo(
|
|||||||
isDownstreamNode,
|
isDownstreamNode,
|
||||||
isUpstreamNode,
|
isUpstreamNode,
|
||||||
isRootNode,
|
isRootNode,
|
||||||
expandPerformed,
|
upstreamExpandPerformed,
|
||||||
|
downstreamExpandPerformed,
|
||||||
upstreamLineageLength,
|
upstreamLineageLength,
|
||||||
onCollapse,
|
onCollapse,
|
||||||
onExpand,
|
onExpand,
|
||||||
@ -114,18 +118,21 @@ const ExpandCollapseHandles = memo(
|
|||||||
{hasOutgoers &&
|
{hasOutgoers &&
|
||||||
(isDownstreamNode || isRootNode) &&
|
(isDownstreamNode || isRootNode) &&
|
||||||
getCollapseHandle(LineageDirection.Downstream, onCollapse)}
|
getCollapseHandle(LineageDirection.Downstream, onCollapse)}
|
||||||
|
|
||||||
{!hasOutgoers &&
|
{!hasOutgoers &&
|
||||||
!expandPerformed &&
|
!downstreamExpandPerformed &&
|
||||||
getExpandHandle(LineageDirection.Downstream, () =>
|
getExpandHandle(LineageDirection.Downstream, () =>
|
||||||
onExpand(LineageDirection.Downstream)
|
onExpand(LineageDirection.Downstream)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hasIncomers &&
|
{hasIncomers &&
|
||||||
(isUpstreamNode || isRootNode) &&
|
(isUpstreamNode || isRootNode) &&
|
||||||
getCollapseHandle(LineageDirection.Upstream, () =>
|
getCollapseHandle(LineageDirection.Upstream, () =>
|
||||||
onCollapse(LineageDirection.Upstream)
|
onCollapse(LineageDirection.Upstream)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!hasIncomers &&
|
{!hasIncomers &&
|
||||||
!expandPerformed &&
|
!upstreamExpandPerformed &&
|
||||||
upstreamLineageLength > 0 &&
|
upstreamLineageLength > 0 &&
|
||||||
getExpandHandle(LineageDirection.Upstream, () =>
|
getExpandHandle(LineageDirection.Upstream, () =>
|
||||||
onExpand(LineageDirection.Upstream)
|
onExpand(LineageDirection.Upstream)
|
||||||
@ -166,17 +173,23 @@ const CustomNodeV1 = (props: NodeProps) => {
|
|||||||
id,
|
id,
|
||||||
fullyQualifiedName,
|
fullyQualifiedName,
|
||||||
upstreamLineage = [],
|
upstreamLineage = [],
|
||||||
expandPerformed = false,
|
upstreamExpandPerformed = false,
|
||||||
|
downstreamExpandPerformed = false,
|
||||||
} = node;
|
} = node;
|
||||||
const [isTraced, setIsTraced] = useState<boolean>(false);
|
const [isTraced, setIsTraced] = useState(false);
|
||||||
|
|
||||||
const showDqTracing = useMemo(() => {
|
const showDqTracing = useMemo(
|
||||||
return (
|
() =>
|
||||||
(activeLayer.includes(LineageLayer.DataObservability) &&
|
activeLayer.includes(LineageLayer.DataObservability) &&
|
||||||
dataQualityLineage?.nodes?.some((dqNode) => dqNode.id === id)) ??
|
dataQualityLineage?.nodes?.some((dqNode) => dqNode.id === id),
|
||||||
false
|
[activeLayer, dataQualityLineage, id]
|
||||||
);
|
);
|
||||||
}, [activeLayer, dataQualityLineage, id]);
|
|
||||||
|
const containerClass = getNodeClassNames({
|
||||||
|
isSelected,
|
||||||
|
showDqTracing: showDqTracing ?? false,
|
||||||
|
isTraced,
|
||||||
|
});
|
||||||
|
|
||||||
const onExpand = useCallback(
|
const onExpand = useCallback(
|
||||||
(direction: LineageDirection) => {
|
(direction: LineageDirection) => {
|
||||||
@ -197,33 +210,20 @@ const CustomNodeV1 = (props: NodeProps) => {
|
|||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderRemoveBtn =
|
|
||||||
isSelected && isEditMode && !isRootNode ? (
|
|
||||||
<LineageNodeRemoveButton onRemove={() => removeNodeHandler(props)} />
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LineageNodeLabelV1 node={node} />
|
<LineageNodeLabelV1 node={node} />
|
||||||
{renderRemoveBtn}
|
{isSelected && isEditMode && !isRootNode && (
|
||||||
|
<LineageNodeRemoveButton onRemove={() => removeNodeHandler(props)} />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}, [node.id, isNewNode, label, isSelected, isEditMode, isRootNode]);
|
}, [node.id, isNewNode, label, isSelected, isEditMode, isRootNode]);
|
||||||
|
|
||||||
const containerClass = useMemo(() => {
|
const expandCollapseProps = useMemo<ExpandCollapseHandlesProps>(
|
||||||
return classNames(
|
|
||||||
'lineage-node p-0',
|
|
||||||
isSelected ? 'custom-node-header-active' : 'custom-node-header-normal',
|
|
||||||
{
|
|
||||||
'data-quality-failed-custom-node-header': showDqTracing,
|
|
||||||
'custom-node-header-tracing': isTraced,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}, [isSelected, showDqTracing, isTraced]);
|
|
||||||
|
|
||||||
const expandCollapseProps = useMemo(
|
|
||||||
() => ({
|
() => ({
|
||||||
expandPerformed,
|
upstreamExpandPerformed,
|
||||||
|
downstreamExpandPerformed,
|
||||||
hasIncomers,
|
hasIncomers,
|
||||||
hasOutgoers,
|
hasOutgoers,
|
||||||
isDownstreamNode,
|
isDownstreamNode,
|
||||||
@ -235,7 +235,8 @@ const CustomNodeV1 = (props: NodeProps) => {
|
|||||||
onExpand,
|
onExpand,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
expandPerformed,
|
upstreamExpandPerformed,
|
||||||
|
downstreamExpandPerformed,
|
||||||
hasIncomers,
|
hasIncomers,
|
||||||
hasOutgoers,
|
hasOutgoers,
|
||||||
isDownstreamNode,
|
isDownstreamNode,
|
||||||
@ -248,6 +249,11 @@ const CustomNodeV1 = (props: NodeProps) => {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handlesElement = useMemo(
|
||||||
|
() => <ExpandCollapseHandles {...expandCollapseProps} />,
|
||||||
|
[expandCollapseProps]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsTraced(tracedNodes.includes(id));
|
setIsTraced(tracedNodes.includes(id));
|
||||||
}, [tracedNodes, id]);
|
}, [tracedNodes, id]);
|
||||||
@ -257,9 +263,7 @@ const CustomNodeV1 = (props: NodeProps) => {
|
|||||||
className={containerClass}
|
className={containerClass}
|
||||||
data-testid={`lineage-node-${fullyQualifiedName}`}>
|
data-testid={`lineage-node-${fullyQualifiedName}`}>
|
||||||
<NodeHandles
|
<NodeHandles
|
||||||
expandCollapseHandles={
|
expandCollapseHandles={handlesElement}
|
||||||
<ExpandCollapseHandles {...expandCollapseProps} />
|
|
||||||
}
|
|
||||||
id={id}
|
id={id}
|
||||||
isConnectable={isConnectable}
|
isConnectable={isConnectable}
|
||||||
nodeType={nodeType}
|
nodeType={nodeType}
|
||||||
|
@ -88,7 +88,8 @@ export interface ExpandCollapseHandlesProps {
|
|||||||
isDownstreamNode: boolean;
|
isDownstreamNode: boolean;
|
||||||
isUpstreamNode: boolean;
|
isUpstreamNode: boolean;
|
||||||
isRootNode: boolean;
|
isRootNode: boolean;
|
||||||
expandPerformed: boolean;
|
upstreamExpandPerformed: boolean;
|
||||||
|
downstreamExpandPerformed: boolean;
|
||||||
upstreamLineageLength: number;
|
upstreamLineageLength: number;
|
||||||
onCollapse: (direction?: LineageDirection) => void;
|
onCollapse: (direction?: LineageDirection) => void;
|
||||||
onExpand: (direction: LineageDirection) => void;
|
onExpand: (direction: LineageDirection) => void;
|
||||||
|
@ -21,12 +21,7 @@ import React, {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import ReactFlow, {
|
import ReactFlow, { Background, MiniMap, Panel } from 'reactflow';
|
||||||
Background,
|
|
||||||
MiniMap,
|
|
||||||
Panel,
|
|
||||||
ReactFlowProvider,
|
|
||||||
} from 'reactflow';
|
|
||||||
import {
|
import {
|
||||||
MAX_ZOOM_VALUE,
|
MAX_ZOOM_VALUE,
|
||||||
MIN_ZOOM_VALUE,
|
MIN_ZOOM_VALUE,
|
||||||
@ -73,7 +68,6 @@ const Lineage = ({
|
|||||||
onNodeDrop,
|
onNodeDrop,
|
||||||
onNodesChange,
|
onNodesChange,
|
||||||
onEdgesChange,
|
onEdgesChange,
|
||||||
entityLineage,
|
|
||||||
onPaneClick,
|
onPaneClick,
|
||||||
onConnect,
|
onConnect,
|
||||||
onInitReactFlow,
|
onInitReactFlow,
|
||||||
@ -162,7 +156,7 @@ const Lineage = ({
|
|||||||
data-testid="lineage-container"
|
data-testid="lineage-container"
|
||||||
id="lineage-container" // ID is required for export PNG functionality
|
id="lineage-container" // ID is required for export PNG functionality
|
||||||
ref={reactFlowWrapper}>
|
ref={reactFlowWrapper}>
|
||||||
{entityLineage && (
|
{init ? (
|
||||||
<>
|
<>
|
||||||
{isPlatformLineage ? null : (
|
{isPlatformLineage ? null : (
|
||||||
<CustomControlsComponent className="absolute top-1 right-1 p-xs" />
|
<CustomControlsComponent className="absolute top-1 right-1 p-xs" />
|
||||||
@ -178,10 +172,6 @@ const Lineage = ({
|
|||||||
isFullScreen ? onExitFullScreenViewClick : undefined
|
isFullScreen ? onExitFullScreenViewClick : undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{init ? (
|
|
||||||
<ReactFlowProvider>
|
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
elevateEdgesOnSelect
|
elevateEdgesOnSelect
|
||||||
className="custom-react-flow"
|
className="custom-react-flow"
|
||||||
@ -222,7 +212,7 @@ const Lineage = ({
|
|||||||
<LineageLayers entity={entity} entityType={entityType} />
|
<LineageLayers entity={entity} entityType={entityType} />
|
||||||
</Panel>
|
</Panel>
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
</ReactFlowProvider>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="loading-card">
|
<div className="loading-card">
|
||||||
<Loader />
|
<Loader />
|
||||||
|
@ -81,6 +81,7 @@ export interface LineageEntityReference extends EntityReference {
|
|||||||
parentId?: string;
|
parentId?: string;
|
||||||
childrenLength?: number;
|
childrenLength?: number;
|
||||||
};
|
};
|
||||||
expandPerformed?: boolean;
|
upstreamExpandPerformed?: boolean;
|
||||||
|
downstreamExpandPerformed?: boolean;
|
||||||
direction?: LineageDirection;
|
direction?: LineageDirection;
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ import { MOCK_EXPLORE_SEARCH_RESULTS } from '../Explore/Explore.mock';
|
|||||||
import Lineage from './Lineage.component';
|
import Lineage from './Lineage.component';
|
||||||
import { EntityLineageResponse } from './Lineage.interface';
|
import { EntityLineageResponse } from './Lineage.interface';
|
||||||
|
|
||||||
let entityLineage: EntityLineageResponse | undefined = {
|
const entityLineage: EntityLineageResponse | undefined = {
|
||||||
entity: {
|
entity: {
|
||||||
name: 'fact_sale',
|
name: 'fact_sale',
|
||||||
fullyQualifiedName: 'sample_data.ecommerce_db.shopify.fact_sale',
|
fullyQualifiedName: 'sample_data.ecommerce_db.shopify.fact_sale',
|
||||||
@ -90,6 +90,7 @@ jest.mock('../../context/LineageProvider/LineageProvider', () => ({
|
|||||||
activeLayer: [],
|
activeLayer: [],
|
||||||
entityLineage: entityLineage,
|
entityLineage: entityLineage,
|
||||||
updateEntityData: jest.fn(),
|
updateEntityData: jest.fn(),
|
||||||
|
init: true,
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -126,19 +127,7 @@ describe('Lineage', () => {
|
|||||||
const customControlsComponent = screen.getByText('Controls Component');
|
const customControlsComponent = screen.getByText('Controls Component');
|
||||||
const lineageComponent = screen.getByTestId('lineage-container');
|
const lineageComponent = screen.getByTestId('lineage-container');
|
||||||
|
|
||||||
expect(customControlsComponent).toBeInTheDocument();
|
|
||||||
expect(lineageComponent).toBeInTheDocument();
|
expect(lineageComponent).toBeInTheDocument();
|
||||||
});
|
|
||||||
|
|
||||||
it('does not render CustomControlsComponent when entityLineage is falsy', () => {
|
|
||||||
const mockPropsWithoutEntity = {
|
|
||||||
...mockProps,
|
|
||||||
entity: undefined,
|
|
||||||
};
|
|
||||||
entityLineage = undefined;
|
|
||||||
render(<Lineage {...mockPropsWithoutEntity} />);
|
|
||||||
const customControlsComponent = screen.getByText('Controls Component');
|
|
||||||
|
|
||||||
expect(customControlsComponent).toBeInTheDocument();
|
expect(customControlsComponent).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -23,6 +23,7 @@ import React, {
|
|||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -55,6 +56,7 @@ import {
|
|||||||
EntityLineageResponse,
|
EntityLineageResponse,
|
||||||
LineageData,
|
LineageData,
|
||||||
LineageEntityReference,
|
LineageEntityReference,
|
||||||
|
NodeData,
|
||||||
} from '../../components/Lineage/Lineage.interface';
|
} from '../../components/Lineage/Lineage.interface';
|
||||||
import LineageNodeRemoveButton from '../../components/Lineage/LineageNodeRemoveButton';
|
import LineageNodeRemoveButton from '../../components/Lineage/LineageNodeRemoveButton';
|
||||||
import { SourceType } from '../../components/SearchedData/SearchedData.interface';
|
import { SourceType } from '../../components/SearchedData/SearchedData.interface';
|
||||||
@ -152,6 +154,11 @@ const LineageProvider = ({ children }: LineageProviderProps) => {
|
|||||||
{} as SourceType
|
{} as SourceType
|
||||||
);
|
);
|
||||||
const [activeLayer, setActiveLayer] = useState<LineageLayer[]>([]);
|
const [activeLayer, setActiveLayer] = useState<LineageLayer[]>([]);
|
||||||
|
|
||||||
|
// Added this ref to compare the previous active layer with the current active layer.
|
||||||
|
// We need to redraw the lineage if the column level lineage is added or removed.
|
||||||
|
const prevActiveLayerRef = useRef<LineageLayer[]>([]);
|
||||||
|
|
||||||
const [activeNode, setActiveNode] = useState<Node>();
|
const [activeNode, setActiveNode] = useState<Node>();
|
||||||
const [expandAllColumns, setExpandAllColumns] = useState(false);
|
const [expandAllColumns, setExpandAllColumns] = useState(false);
|
||||||
const [selectedColumn, setSelectedColumn] = useState<string>('');
|
const [selectedColumn, setSelectedColumn] = useState<string>('');
|
||||||
@ -264,6 +271,7 @@ const LineageProvider = ({ children }: LineageProviderProps) => {
|
|||||||
allNodes,
|
allNodes,
|
||||||
lineageData.edges ?? [],
|
lineageData.edges ?? [],
|
||||||
decodedFqn,
|
decodedFqn,
|
||||||
|
activeLayer.includes(LineageLayer.ColumnLevelLineage),
|
||||||
isFirstTime ? true : undefined
|
isFirstTime ? true : undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -365,7 +373,7 @@ const LineageProvider = ({ children }: LineageProviderProps) => {
|
|||||||
|
|
||||||
setLineageData(res);
|
setLineageData(res);
|
||||||
|
|
||||||
const { nodes, edges, entity } = parseLineageData(res, '');
|
const { nodes, edges, entity } = parseLineageData(res, '', decodedFqn);
|
||||||
const updatedEntityLineage = {
|
const updatedEntityLineage = {
|
||||||
nodes,
|
nodes,
|
||||||
edges,
|
edges,
|
||||||
@ -409,7 +417,7 @@ const LineageProvider = ({ children }: LineageProviderProps) => {
|
|||||||
});
|
});
|
||||||
setLineageData(res);
|
setLineageData(res);
|
||||||
|
|
||||||
const { nodes, edges, entity } = parseLineageData(res, fqn);
|
const { nodes, edges, entity } = parseLineageData(res, fqn, decodedFqn);
|
||||||
const updatedEntityLineage = {
|
const updatedEntityLineage = {
|
||||||
nodes,
|
nodes,
|
||||||
edges,
|
edges,
|
||||||
@ -510,9 +518,19 @@ const LineageProvider = ({ children }: LineageProviderProps) => {
|
|||||||
direction,
|
direction,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const currentNodes: Record<string, NodeData> = {};
|
||||||
|
entityLineage.nodes?.forEach((node) => {
|
||||||
|
currentNodes[node.fullyQualifiedName ?? ''] = {
|
||||||
|
entity: node,
|
||||||
|
paging: (node as LineageEntityReference).paging ?? {
|
||||||
|
entityDownstreamCount: 0,
|
||||||
|
entityUpstreamCount: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
const concatenatedLineageData = {
|
const concatenatedLineageData = {
|
||||||
nodes: {
|
nodes: {
|
||||||
...lineageData?.nodes,
|
...currentNodes,
|
||||||
...res.nodes,
|
...res.nodes,
|
||||||
},
|
},
|
||||||
downstreamEdges: {
|
downstreamEdges: {
|
||||||
@ -527,20 +545,56 @@ const LineageProvider = ({ children }: LineageProviderProps) => {
|
|||||||
|
|
||||||
const { nodes: newNodes, edges: newEdges } = parseLineageData(
|
const { nodes: newNodes, edges: newEdges } = parseLineageData(
|
||||||
concatenatedLineageData,
|
concatenatedLineageData,
|
||||||
node.fullyQualifiedName ?? ''
|
node.fullyQualifiedName ?? '',
|
||||||
|
decodedFqn
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const uniqueNodes = [...(entityLineage.nodes ?? [])];
|
||||||
|
for (const nNode of newNodes ?? []) {
|
||||||
|
if (
|
||||||
|
!uniqueNodes.some(
|
||||||
|
(n) => n.fullyQualifiedName === nNode.fullyQualifiedName
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
uniqueNodes.push(nNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updatedEntityLineage = {
|
const updatedEntityLineage = {
|
||||||
entity: entityLineage.entity,
|
entity: entityLineage.entity,
|
||||||
nodes: uniqWith(
|
nodes: uniqueNodes,
|
||||||
[...(entityLineage.nodes ?? []), ...newNodes],
|
|
||||||
isEqual
|
|
||||||
),
|
|
||||||
edges: uniqWith(
|
edges: uniqWith(
|
||||||
[...(entityLineage.edges ?? []), ...newEdges],
|
[...(entityLineage.edges ?? []), ...newEdges],
|
||||||
isEqual
|
isEqual
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// remove the nodes and edges from the lineageData
|
||||||
|
const visibleNodes: Record<string, NodeData> = {};
|
||||||
|
uniqueNodes.forEach((node: EntityReference) => {
|
||||||
|
visibleNodes[node.fullyQualifiedName ?? ''] = {
|
||||||
|
entity: node,
|
||||||
|
paging: (node as LineageEntityReference).paging ?? {
|
||||||
|
entityDownstreamCount: 0,
|
||||||
|
entityUpstreamCount: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentNode = updatedEntityLineage.nodes.find(
|
||||||
|
(n) => n.fullyQualifiedName === node.fullyQualifiedName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentNode) {
|
||||||
|
if (direction === LineageDirection.Upstream) {
|
||||||
|
(currentNode as LineageEntityReference).upstreamExpandPerformed =
|
||||||
|
true;
|
||||||
|
} else {
|
||||||
|
(currentNode as LineageEntityReference).downstreamExpandPerformed =
|
||||||
|
true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateLineageData(updatedEntityLineage, {
|
updateLineageData(updatedEntityLineage, {
|
||||||
shouldRedraw: true,
|
shouldRedraw: true,
|
||||||
centerNode: false,
|
centerNode: false,
|
||||||
@ -887,7 +941,8 @@ const LineageProvider = ({ children }: LineageProviderProps) => {
|
|||||||
|
|
||||||
const { nodes: newNodes, edges: newEdges } = parseLineageData(
|
const { nodes: newNodes, edges: newEdges } = parseLineageData(
|
||||||
concatenatedLineageData,
|
concatenatedLineageData,
|
||||||
parentNode.data.node.fullyQualifiedName
|
parentNode.data.node.fullyQualifiedName,
|
||||||
|
decodedFqn
|
||||||
);
|
);
|
||||||
|
|
||||||
updateLineageData(
|
updateLineageData(
|
||||||
@ -1100,7 +1155,12 @@ const LineageProvider = ({ children }: LineageProviderProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { edges: createdEdges, columnsHavingLineage } =
|
const { edges: createdEdges, columnsHavingLineage } =
|
||||||
createEdgesAndEdgeMaps(allNodes, allEdges, decodedFqn);
|
createEdgesAndEdgeMaps(
|
||||||
|
allNodes,
|
||||||
|
allEdges,
|
||||||
|
decodedFqn,
|
||||||
|
activeLayer.includes(LineageLayer.ColumnLevelLineage)
|
||||||
|
);
|
||||||
setEdges(createdEdges);
|
setEdges(createdEdges);
|
||||||
setColumnsHavingLineage(columnsHavingLineage);
|
setColumnsHavingLineage(columnsHavingLineage);
|
||||||
|
|
||||||
@ -1328,6 +1388,31 @@ const LineageProvider = ({ children }: LineageProviderProps) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Find the node in updatedNodes by ID and set expandPerformed: false
|
||||||
|
const currentNodeId = (node as Node).id;
|
||||||
|
const nodeToUpdate = updatedNodes.find((n) => n.id === currentNodeId);
|
||||||
|
if (nodeToUpdate) {
|
||||||
|
if (direction === LineageDirection.Upstream) {
|
||||||
|
(nodeToUpdate as LineageEntityReference).upstreamExpandPerformed =
|
||||||
|
false;
|
||||||
|
} else {
|
||||||
|
(nodeToUpdate as LineageEntityReference).downstreamExpandPerformed =
|
||||||
|
false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the nodes and edges from the lineageData
|
||||||
|
const visibleNodes: Record<string, NodeData> = {};
|
||||||
|
updatedNodes.forEach((node) => {
|
||||||
|
visibleNodes[node.fullyQualifiedName ?? ''] = {
|
||||||
|
entity: node,
|
||||||
|
paging: (node as LineageEntityReference).paging ?? {
|
||||||
|
entityDownstreamCount: 0,
|
||||||
|
entityUpstreamCount: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
updateLineageData(
|
updateLineageData(
|
||||||
{
|
{
|
||||||
...entityLineage,
|
...entityLineage,
|
||||||
@ -1503,7 +1588,20 @@ const LineageProvider = ({ children }: LineageProviderProps) => {
|
|||||||
}, [isEditMode, deletePressed, backspacePressed, activeNode, selectedEdge]);
|
}, [isEditMode, deletePressed, backspacePressed, activeNode, selectedEdge]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
repositionLayout();
|
const prevActiveLayer = prevActiveLayerRef.current;
|
||||||
|
|
||||||
|
const prevHadColumn = prevActiveLayer.includes(
|
||||||
|
LineageLayer.ColumnLevelLineage
|
||||||
|
);
|
||||||
|
const currHasColumn = activeLayer.includes(LineageLayer.ColumnLevelLineage);
|
||||||
|
|
||||||
|
if (prevHadColumn !== currHasColumn) {
|
||||||
|
redraw();
|
||||||
|
} else {
|
||||||
|
repositionLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
prevActiveLayerRef.current = activeLayer;
|
||||||
}, [activeLayer, expandAllColumns]);
|
}, [activeLayer, expandAllColumns]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -16,7 +16,15 @@ import { graphlib, layout } from '@dagrejs/dagre';
|
|||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import ELK, { ElkExtendedEdge, ElkNode } from 'elkjs/lib/elk.bundled.js';
|
import ELK, { ElkExtendedEdge, ElkNode } from 'elkjs/lib/elk.bundled.js';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { get, isEmpty, isNil, isUndefined, uniqueId } from 'lodash';
|
import {
|
||||||
|
get,
|
||||||
|
isEmpty,
|
||||||
|
isEqual,
|
||||||
|
isNil,
|
||||||
|
isUndefined,
|
||||||
|
uniqueId,
|
||||||
|
uniqWith,
|
||||||
|
} from 'lodash';
|
||||||
import { EntityTags, LoadingState } from 'Models';
|
import { EntityTags, LoadingState } from 'Models';
|
||||||
import React, { MouseEvent as ReactMouseEvent } from 'react';
|
import React, { MouseEvent as ReactMouseEvent } from 'react';
|
||||||
import {
|
import {
|
||||||
@ -839,6 +847,7 @@ export const createEdgesAndEdgeMaps = (
|
|||||||
nodes: EntityReference[],
|
nodes: EntityReference[],
|
||||||
edges: EdgeDetails[],
|
edges: EdgeDetails[],
|
||||||
entityFqn: string,
|
entityFqn: string,
|
||||||
|
isColumnLayerActive: boolean,
|
||||||
hidden?: boolean
|
hidden?: boolean
|
||||||
) => {
|
) => {
|
||||||
const lineageEdgesV1: Edge[] = [];
|
const lineageEdgesV1: Edge[] = [];
|
||||||
@ -851,10 +860,6 @@ export const createEdgesAndEdgeMaps = (
|
|||||||
const sourceId = edge.fromEntity.id;
|
const sourceId = edge.fromEntity.id;
|
||||||
const targetId = edge.toEntity.id;
|
const targetId = edge.toEntity.id;
|
||||||
|
|
||||||
// Update edge maps for fast lookup
|
|
||||||
outgoingMap.set(sourceId, (outgoingMap.get(sourceId) ?? 0) + 1);
|
|
||||||
incomingMap.set(targetId, (incomingMap.get(targetId) ?? 0) + 1);
|
|
||||||
|
|
||||||
const sourceType = nodes.find((n) => sourceId === n.id);
|
const sourceType = nodes.find((n) => sourceId === n.id);
|
||||||
const targetType = nodes.find((n) => targetId === n.id);
|
const targetType = nodes.find((n) => targetId === n.id);
|
||||||
|
|
||||||
@ -862,7 +867,11 @@ export const createEdgesAndEdgeMaps = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isUndefined(edge.columns)) {
|
// Update edge maps for fast lookup
|
||||||
|
outgoingMap.set(sourceId, (outgoingMap.get(sourceId) ?? 0) + 1);
|
||||||
|
incomingMap.set(targetId, (incomingMap.get(targetId) ?? 0) + 1);
|
||||||
|
|
||||||
|
if (!isUndefined(edge.columns) && isColumnLayerActive) {
|
||||||
edge.columns?.forEach((e) => {
|
edge.columns?.forEach((e) => {
|
||||||
const toColumn = e.toColumn ?? '';
|
const toColumn = e.toColumn ?? '';
|
||||||
if (toColumn && e.fromColumns?.length) {
|
if (toColumn && e.fromColumns?.length) {
|
||||||
@ -1133,7 +1142,7 @@ export const getConnectedNodesEdges = (
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
nodes: outgoers,
|
nodes: outgoers,
|
||||||
edges: connectedEdges,
|
edges: uniqWith(connectedEdges, isEqual),
|
||||||
nodeFqn: childNodeFqn,
|
nodeFqn: childNodeFqn,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -1406,9 +1415,16 @@ const processNodeArray = (
|
|||||||
.map((node: NodeData) => ({
|
.map((node: NodeData) => ({
|
||||||
...node.entity,
|
...node.entity,
|
||||||
paging: node.paging,
|
paging: node.paging,
|
||||||
expandPerformed:
|
upstreamExpandPerformed:
|
||||||
(node.entity as LineageEntityReference).expandPerformed ||
|
(node.entity as LineageEntityReference).upstreamExpandPerformed !==
|
||||||
node.entity.fullyQualifiedName === entityFqn,
|
undefined
|
||||||
|
? (node.entity as LineageEntityReference).upstreamExpandPerformed
|
||||||
|
: node.entity.fullyQualifiedName === entityFqn,
|
||||||
|
downstreamExpandPerformed:
|
||||||
|
(node.entity as LineageEntityReference).downstreamExpandPerformed !==
|
||||||
|
undefined
|
||||||
|
? (node.entity as LineageEntityReference).downstreamExpandPerformed
|
||||||
|
: node.entity.fullyQualifiedName === entityFqn,
|
||||||
}))
|
}))
|
||||||
.flat();
|
.flat();
|
||||||
};
|
};
|
||||||
@ -1509,7 +1525,8 @@ const processPagination = (
|
|||||||
|
|
||||||
export const parseLineageData = (
|
export const parseLineageData = (
|
||||||
data: LineageData,
|
data: LineageData,
|
||||||
entityFqn: string
|
entityFqn: string, // This contains fqn of node or entity that is being viewed in lineage page
|
||||||
|
rootFqn: string // This contains the fqn of the entity that is being viewed in lineage page
|
||||||
): {
|
): {
|
||||||
nodes: LineageEntityReference[];
|
nodes: LineageEntityReference[];
|
||||||
edges: EdgeDetails[];
|
edges: EdgeDetails[];
|
||||||
@ -1518,7 +1535,7 @@ export const parseLineageData = (
|
|||||||
const { nodes, downstreamEdges, upstreamEdges } = data;
|
const { nodes, downstreamEdges, upstreamEdges } = data;
|
||||||
|
|
||||||
// Process nodes
|
// Process nodes
|
||||||
const nodesArray = processNodeArray(nodes, entityFqn);
|
const nodesArray = processNodeArray(nodes, rootFqn);
|
||||||
const processedNodes: LineageEntityReference[] = [...nodesArray];
|
const processedNodes: LineageEntityReference[] = [...nodesArray];
|
||||||
|
|
||||||
// Process edges
|
// Process edges
|
||||||
|
Loading…
x
Reference in New Issue
Block a user