Refactor(UI): Refactor lineage component code (#8269)

* Refactor(UI): Refactor lineage component code

* addressing all the comments

* Added unit test for lineage utils file

* fixing failed unit test
This commit is contained in:
Shailesh Parmar 2022-10-21 11:41:45 +05:30 committed by GitHub
parent 7db929a5bc
commit 1a0147b16b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1360 additions and 638 deletions

View File

@ -0,0 +1,64 @@
/*
* Copyright 2022 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import classNames from 'classnames';
import React, { useCallback } from 'react';
import { NO_PERMISSION_FOR_ACTION } from '../../constants/HelperTextUtil';
import { LoadingStatus } from '../../utils/EntityLineageUtils';
import SVGIcons from '../../utils/SvgUtils';
import CustomControls, { ControlButton } from './CustomControls.component';
import { CustomControlElementsProps } from './EntityLineage.interface';
const CustomControlElements = ({
deleted,
isEditMode,
hasEditAccess,
onClick,
loading,
status,
}: CustomControlElementsProps) => {
const getLoadingStatus = useCallback(() => {
const editIcon = (
<SVGIcons
alt="icon-edit-lineag"
className="m--t-xss"
icon={isEditMode ? 'icon-edit-lineage' : 'icon-edit-lineage-color'}
width="14"
/>
);
return LoadingStatus(editIcon, loading, status);
}, [loading, status, isEditMode]);
return (
<CustomControls
className="absolute top-1 right-3 bottom-full m-l-md m-t-md"
fitViewParams={{ minZoom: 0.5, maxZoom: 2.5 }}>
{!deleted && (
<ControlButton
className={classNames('h-9 w-9 rounded-full p-x-xss tw-shadow-lg', {
'bg-primary': isEditMode,
'bg-primary-hover-lite': !isEditMode,
})}
data-testid="edit-lineage"
disabled={!hasEditAccess}
title={hasEditAccess ? 'Edit Lineage' : NO_PERMISSION_FOR_ACTION}
onClick={onClick}>
{getLoadingStatus()}
</ControlButton>
)}
</CustomControls>
);
};
export default CustomControlElements;

View File

@ -83,8 +83,23 @@ export interface SelectedEdge {
export type ElementLoadingState = Exclude<LoadingState, 'waiting'>;
export type CustomeElement = { node: Node[]; edge: FlowEdge[] };
export type CustomeFlow = Node | FlowEdge;
export type CustomElement = { node: Node[]; edge: FlowEdge[] };
export type CustomFlow = Node | FlowEdge;
export type ModifiedColumn = Column & {
type: string;
};
export interface CustomControlElementsProps {
deleted: boolean | undefined;
isEditMode: boolean;
hasEditAccess: boolean | undefined;
onClick: () => void;
loading: boolean;
status: LoadingState;
}
export enum EdgeTypeEnum {
UP_STREAM = 'upstream',
DOWN_STREAM = 'downstream',
NO_STREAM = '',
}

View File

@ -150,6 +150,7 @@ jest.mock('../../utils/EntityLineageUtils', () => ({
<p>Lineage data is not available for deleted entities.</p>
),
getHeaderLabel: jest.fn().mockReturnValue(<p>Header label</p>),
LoadingStatus: jest.fn().mockReturnValue(<p>Confirm</p>),
getLayoutedElements: jest.fn().mockImplementation(() => mockFlowData),
getLineageData: jest.fn().mockImplementation(() => mockFlowData),
getModalBodyText: jest.fn(),

View File

@ -0,0 +1,420 @@
/* eslint-disable max-len */
export const MOCK_LINEAGE_DATA = {
entity: {
id: 'f80de28c-ecce-46fb-88c7-152cc111f9ec',
type: 'table',
name: 'fact_session',
fullyQualifiedName: 'sample_data.ecommerce_db.shopify.fact_session',
description:
'This fact table contains information about the visitors to your online store. This table has one row per session, where one session can contain many page views. If you use Urchin Traffic Module (UTM) parameters in marketing campaigns, then you can use this table to track how many customers they direct to your store.',
deleted: false,
href: 'http://localhost:8585/api/v1/tables/f80de28c-ecce-46fb-88c7-152cc111f9ec',
},
nodes: [
{
id: '5f2eee5d-1c08-4756-af31-dabce7cb26fd',
type: 'table',
name: 'dim_customer',
fullyQualifiedName: 'sample_data.ecommerce_db.shopify.dim_customer',
description:
'The dimension table contains data about your customers. The customers table contains one row per customer. It includes historical metrics (such as the total amount that each customer has spent in your store) as well as forward-looking metrics (such as the predicted number of days between future orders and the expected order value in the next 30 days). This table also includes columns that segment customers into various categories (such as new, returning, promising, at risk, dormant, and loyal), which you can use to target marketing activities.',
deleted: false,
href: 'http://localhost:8585/api/v1/tables/5f2eee5d-1c08-4756-af31-dabce7cb26fd',
},
{
id: '92d7cb90-cc49-497a-9b01-18f4c6a61951',
type: 'table',
name: 'storage_service_entity',
fullyQualifiedName:
'mysql.default.openmetadata_db.storage_service_entity',
deleted: false,
href: 'http://localhost:8585/api/v1/tables/92d7cb90-cc49-497a-9b01-18f4c6a61951',
},
{
id: '2d30f754-05de-4372-af27-f221997bfe9a',
type: 'table',
name: 'dim_address',
fullyQualifiedName: 'sample_data.ecommerce_db.shopify.dim_address',
description:
'This dimension table contains the billing and shipping addresses of customers. You can join this table with the sales table to generate lists of the billing and shipping addresses. Customers can enter their addresses more than once, so the same address can appear in more than one row in this table. This table contains one row per customer address.',
deleted: false,
href: 'http://localhost:8585/api/v1/tables/2d30f754-05de-4372-af27-f221997bfe9a',
},
{
id: 'b5d520fd-a4a5-4173-85d5-f804ddab452a',
type: 'table',
name: 'dashboard_service_entity',
fullyQualifiedName:
'mysql.default.openmetadata_db.dashboard_service_entity',
deleted: false,
href: 'http://localhost:8585/api/v1/tables/b5d520fd-a4a5-4173-85d5-f804ddab452a',
},
{
id: 'bf99a241-76e9-4947-86a7-c9bf3c326974',
type: 'table',
name: 'dim.product',
fullyQualifiedName: 'sample_data.ecommerce_db.shopify."dim.product"',
description:
'This dimension table contains information about each of the products in your store. This table contains one row per product. This table reflects the current state of products in your Shopify admin.',
deleted: false,
href: 'http://localhost:8585/api/v1/tables/bf99a241-76e9-4947-86a7-c9bf3c326974',
},
{
id: 'd4aab894-5877-44f1-840c-b08a2dc664a4',
type: 'pipeline',
name: 'dim_address_etl',
fullyQualifiedName: 'sample_airflow.dim_address_etl',
description: 'dim_address ETL pipeline',
displayName: 'dim_address etl',
deleted: false,
href: 'http://localhost:8585/api/v1/pipelines/d4aab894-5877-44f1-840c-b08a2dc664a4',
},
{
id: '5a51ea54-8304-4fa8-a7b2-1f083ff1580c',
type: 'pipeline',
name: 'presto_etl',
fullyQualifiedName: 'sample_airflow.presto_etl',
description: 'Presto ETL pipeline',
displayName: 'Presto ETL',
deleted: false,
href: 'http://localhost:8585/api/v1/pipelines/5a51ea54-8304-4fa8-a7b2-1f083ff1580c',
},
],
upstreamEdges: [
{
fromEntity: '2d30f754-05de-4372-af27-f221997bfe9a',
toEntity: '5f2eee5d-1c08-4756-af31-dabce7cb26fd',
lineageDetails: {
sqlQuery: '',
columnsLineage: [
{
fromColumns: [
'sample_data.ecommerce_db.shopify.dim_address.address_id',
],
toColumn:
'sample_data.ecommerce_db.shopify.dim_customer.total_order_value',
},
],
},
},
{
fromEntity: '5f2eee5d-1c08-4756-af31-dabce7cb26fd',
toEntity: 'f80de28c-ecce-46fb-88c7-152cc111f9ec',
lineageDetails: {
sqlQuery: '',
columnsLineage: [
{
fromColumns: [
'sample_data.ecommerce_db.shopify.dim_customer.customer_id',
],
toColumn:
'sample_data.ecommerce_db.shopify.fact_session.derived_session_token',
},
],
},
},
{
fromEntity: '92d7cb90-cc49-497a-9b01-18f4c6a61951',
toEntity: 'f80de28c-ecce-46fb-88c7-152cc111f9ec',
},
{
fromEntity: 'b5d520fd-a4a5-4173-85d5-f804ddab452a',
toEntity: '2d30f754-05de-4372-af27-f221997bfe9a',
lineageDetails: {
sqlQuery: '',
columnsLineage: [
{
fromColumns: [
'mysql.default.openmetadata_db.dashboard_service_entity.id',
],
toColumn: 'sample_data.ecommerce_db.shopify.dim_address.shop_id',
},
],
},
},
{
fromEntity: 'bf99a241-76e9-4947-86a7-c9bf3c326974',
toEntity: '2d30f754-05de-4372-af27-f221997bfe9a',
lineageDetails: {
sqlQuery: '',
columnsLineage: [
{
fromColumns: [
'sample_data.ecommerce_db.shopify."dim.product".shop_id',
],
toColumn: 'sample_data.ecommerce_db.shopify.dim_address.first_name',
},
],
},
},
{
fromEntity: 'd4aab894-5877-44f1-840c-b08a2dc664a4',
toEntity: '2d30f754-05de-4372-af27-f221997bfe9a',
},
{
fromEntity: '5a51ea54-8304-4fa8-a7b2-1f083ff1580c',
toEntity: '92d7cb90-cc49-497a-9b01-18f4c6a61951',
},
],
downstreamEdges: [
{
toEntity: '92d7cb90-cc49-497a-9b01-18f4c6a61951',
fromEntity: 'f80de28c-ecce-46fb-88c7-152cc111f9ec',
},
],
};
export const SELECTED_EDGE = {
id: 'column-sample_data.ecommerce_db.shopify.dim_customer.customer_id-sample_data.ecommerce_db.shopify.fact_session.derived_session_token-edge-5f2eee5d-1c08-4756-af31-dabce7cb26fd-f80de28c-ecce-46fb-88c7-152cc111f9ec',
source: {
id: '5f2eee5d-1c08-4756-af31-dabce7cb26fd',
type: 'table',
name: 'dim_customer',
fullyQualifiedName: 'sample_data.ecommerce_db.shopify.dim_customer',
description:
'The dimension table contains data about your customers. The customers table contains one row per customer. It includes historical metrics (such as the total amount that each customer has spent in your store) as well as forward-looking metrics (such as the predicted number of days between future orders and the expected order value in the next 30 days). This table also includes columns that segment customers into various categories (such as new, returning, promising, at risk, dormant, and loyal), which you can use to target marketing activities.',
deleted: false,
href: 'http://localhost:8585/api/v1/tables/5f2eee5d-1c08-4756-af31-dabce7cb26fd',
},
target: {
id: 'f80de28c-ecce-46fb-88c7-152cc111f9ec',
type: 'table',
name: 'fact_session',
fullyQualifiedName: 'sample_data.ecommerce_db.shopify.fact_session',
description:
'This fact table contains information about the visitors to your online store. This table has one row per session, where one session can contain many page views. If you use Urchin Traffic Module (UTM) parameters in marketing campaigns, then you can use this table to track how many customers they direct to your store.',
deleted: false,
href: 'http://localhost:8585/api/v1/tables/f80de28c-ecce-46fb-88c7-152cc111f9ec',
},
data: {
id: 'column-sample_data.ecommerce_db.shopify.dim_customer.customer_id-sample_data.ecommerce_db.shopify.fact_session.derived_session_token-edge-5f2eee5d-1c08-4756-af31-dabce7cb26fd-f80de28c-ecce-46fb-88c7-152cc111f9ec',
source: '5f2eee5d-1c08-4756-af31-dabce7cb26fd',
target: 'f80de28c-ecce-46fb-88c7-152cc111f9ec',
targetHandle:
'sample_data.ecommerce_db.shopify.fact_session.derived_session_token',
sourceHandle: 'sample_data.ecommerce_db.shopify.dim_customer.customer_id',
isColumnLineage: true,
},
};
export const UP_STREAM_EDGE = {
fromEntity: '5f2eee5d-1c08-4756-af31-dabce7cb26fd',
toEntity: 'f80de28c-ecce-46fb-88c7-152cc111f9ec',
lineageDetails: {
sqlQuery: '',
columnsLineage: [
{
fromColumns: [
'sample_data.ecommerce_db.shopify.dim_customer.customer_id',
],
toColumn:
'sample_data.ecommerce_db.shopify.fact_session.derived_session_token',
},
],
},
};
export const COLUMN_LINEAGE_DETAILS = {
sqlQuery: '',
columnsLineage: [
{
fromColumns: ['sample_data.ecommerce_db.shopify.dim_address.address_id'],
toColumn:
'sample_data.ecommerce_db.shopify.dim_customer.total_order_value',
},
],
};
export const UPDATED_LINEAGE_EDGE = [
{
fromEntity: '2d30f754-05de-4372-af27-f221997bfe9a',
lineageDetails: {
columnsLineage: [
{
fromColumns: [
'sample_data.ecommerce_db.shopify.dim_address.address_id',
],
toColumn:
'sample_data.ecommerce_db.shopify.dim_customer.total_order_value',
},
],
sqlQuery: '',
},
toEntity: '5f2eee5d-1c08-4756-af31-dabce7cb26fd',
},
{
fromEntity: '5f2eee5d-1c08-4756-af31-dabce7cb26fd',
lineageDetails: {
columnsLineage: [
{
fromColumns: [
'sample_data.ecommerce_db.shopify.dim_address.address_id',
],
toColumn:
'sample_data.ecommerce_db.shopify.dim_customer.total_order_value',
},
],
sqlQuery: '',
},
toEntity: 'f80de28c-ecce-46fb-88c7-152cc111f9ec',
},
{
fromEntity: '92d7cb90-cc49-497a-9b01-18f4c6a61951',
toEntity: 'f80de28c-ecce-46fb-88c7-152cc111f9ec',
},
{
fromEntity: 'b5d520fd-a4a5-4173-85d5-f804ddab452a',
lineageDetails: {
columnsLineage: [
{
fromColumns: [
'mysql.default.openmetadata_db.dashboard_service_entity.id',
],
toColumn: 'sample_data.ecommerce_db.shopify.dim_address.shop_id',
},
],
sqlQuery: '',
},
toEntity: '2d30f754-05de-4372-af27-f221997bfe9a',
},
{
fromEntity: 'bf99a241-76e9-4947-86a7-c9bf3c326974',
lineageDetails: {
columnsLineage: [
{
fromColumns: [
'sample_data.ecommerce_db.shopify."dim.product".shop_id',
],
toColumn: 'sample_data.ecommerce_db.shopify.dim_address.first_name',
},
],
sqlQuery: '',
},
toEntity: '2d30f754-05de-4372-af27-f221997bfe9a',
},
{
fromEntity: 'd4aab894-5877-44f1-840c-b08a2dc664a4',
toEntity: '2d30f754-05de-4372-af27-f221997bfe9a',
},
{
fromEntity: '5a51ea54-8304-4fa8-a7b2-1f083ff1580c',
toEntity: '92d7cb90-cc49-497a-9b01-18f4c6a61951',
},
];
export const EDGE_TO_BE_REMOVED = {
id: 'edge-5a51ea54-8304-4fa8-a7b2-1f083ff1580c-92d7cb90-cc49-497a-9b01-18f4c6a61951',
source: '5a51ea54-8304-4fa8-a7b2-1f083ff1580c',
target: '92d7cb90-cc49-497a-9b01-18f4c6a61951',
type: 'buttonedge',
style: {
strokeWidth: '2px',
},
markerEnd: {
type: 'arrowclosed',
},
data: {
id: 'edge-5a51ea54-8304-4fa8-a7b2-1f083ff1580c-92d7cb90-cc49-497a-9b01-18f4c6a61951',
source: '5a51ea54-8304-4fa8-a7b2-1f083ff1580c',
target: '92d7cb90-cc49-497a-9b01-18f4c6a61951',
sourceType: 'pipeline',
targetType: 'table',
isColumnLineage: false,
},
};
export const MOCK_REMOVED_NODE = {
id: 'edge-5a51ea54-8304-4fa8-a7b2-1f083ff1580c-92d7cb90-cc49-497a-9b01-18f4c6a61951',
source: {
id: '5a51ea54-8304-4fa8-a7b2-1f083ff1580c',
type: 'pipeline',
name: 'presto_etl',
fullyQualifiedName: 'sample_airflow.presto_etl',
description: 'Presto ETL pipeline',
displayName: 'Presto ETL',
deleted: false,
href: 'http://localhost:8585/api/v1/pipelines/5a51ea54-8304-4fa8-a7b2-1f083ff1580c',
},
target: {
id: '92d7cb90-cc49-497a-9b01-18f4c6a61951',
type: 'table',
name: 'storage_service_entity',
fullyQualifiedName: 'mysql.default.openmetadata_db.storage_service_entity',
deleted: false,
href: 'http://localhost:8585/api/v1/tables/92d7cb90-cc49-497a-9b01-18f4c6a61951',
},
};
export const MOCK_PARAMS_FOR_UP_STREAM = {
source: '5a51ea54-8304-4fa8-a7b2-1f083ff1580c',
sourceHandle: null,
target: '92d7cb90-cc49-497a-9b01-18f4c6a61951',
targetHandle: null,
};
export const MOCK_PARAMS_FOR_DOWN_STREAM = {
source: 'f80de28c-ecce-46fb-88c7-152cc111f9ec',
sourceHandle: null,
target: 'b32555fc-f38b-4e4b-9dbf-f156fa8ba3c9',
targetHandle: null,
};
export const UPDATED_COLUMN_LINEAGE = {
sqlQuery: '',
columnsLineage: [
{
fromColumns: [
'sample_data.ecommerce_db.shopify.dim_customer.customer_id',
],
toColumn:
'sample_data.ecommerce_db.shopify.fact_session.derived_session_token',
},
{
fromColumns: ['sample_data.ecommerce_db.shopify.dim_customer.shop_id'],
toColumn: 'sample_data.ecommerce_db.shopify.fact_session.shop_id',
},
],
};
export const UPDATED_EDGE_PARAM = {
source: '5f2eee5d-1c08-4756-af31-dabce7cb26fd',
sourceHandle: 'sample_data.ecommerce_db.shopify.dim_customer.shop_id',
target: 'f80de28c-ecce-46fb-88c7-152cc111f9ec',
targetHandle: 'sample_data.ecommerce_db.shopify.fact_session.shop_id',
};
export const MOCK_COLUMN_LINEAGE_EDGE = {
data: {
id: 'column-sample_data.ecommerce_db.shopify.dim_customer.shop_id-sample_data.ecommerce_db.shopify.fact_session.shop_id-edge-5f2eee5d-1c08-4756-af31-dabce7cb26fd-f80de28c-ecce-46fb-88c7-152cc111f9ec',
isColumnLineage: true,
source: '5f2eee5d-1c08-4756-af31-dabce7cb26fd',
sourceHandle: 'sample_data.ecommerce_db.shopify.dim_customer.shop_id',
sourceType: 'table',
target: 'f80de28c-ecce-46fb-88c7-152cc111f9ec',
targetHandle: 'sample_data.ecommerce_db.shopify.fact_session.shop_id',
targetType: 'table',
},
id: 'column-sample_data.ecommerce_db.shopify.dim_customer.shop_id-sample_data.ecommerce_db.shopify.fact_session.shop_id-edge-5f2eee5d-1c08-4756-af31-dabce7cb26fd-f80de28c-ecce-46fb-88c7-152cc111f9ec',
markerEnd: { type: 'arrowclosed' },
source: '5f2eee5d-1c08-4756-af31-dabce7cb26fd',
sourceHandle: 'sample_data.ecommerce_db.shopify.dim_customer.shop_id',
style: undefined,
target: 'f80de28c-ecce-46fb-88c7-152cc111f9ec',
targetHandle: 'sample_data.ecommerce_db.shopify.fact_session.shop_id',
type: 'buttonedge',
};
export const MOCK_NORMAL_LINEAGE_EDGE = {
id: 'edge-5f2eee5d-1c08-4756-af31-dabce7cb26fd-f80de28c-ecce-46fb-88c7-152cc111f9ec',
source: '5f2eee5d-1c08-4756-af31-dabce7cb26fd',
target: 'f80de28c-ecce-46fb-88c7-152cc111f9ec',
type: 'buttonedge',
style: { strokeWidth: '2px' },
markerEnd: { type: 'arrowclosed' },
data: {
id: 'edge-5f2eee5d-1c08-4756-af31-dabce7cb26fd-f80de28c-ecce-46fb-88c7-152cc111f9ec',
source: '5f2eee5d-1c08-4756-af31-dabce7cb26fd',
target: 'f80de28c-ecce-46fb-88c7-152cc111f9ec',
sourceType: 'table',
targetType: 'table',
isColumnLineage: false,
},
};

View File

@ -15,6 +15,8 @@
@primary: #7147e8;
@primary-light: rgb(244, 240, 253);
@primary-hover-light: rgba(219, 209, 249);
@white: #fff;
//font weight
.font-medium {
@ -43,6 +45,9 @@
.error-text {
color: #ff4c3b;
}
.text-white {
color: @white;
}
// text alignment
@ -60,6 +65,9 @@
.w-8 {
width: 32px;
}
.w-9 {
width: 36px;
}
.w-16 {
width: 64px;
}
@ -110,6 +118,9 @@
.h-7 {
height: 28px;
}
.h-9 {
height: 36px;
}
.h-min-100 {
min-height: 100vh;
}
@ -180,9 +191,19 @@
border-color: @primary;
}
.rounded-full {
border-radius: 9999px;
}
.bg-primary-lite {
background: @primary-light;
}
.bg-primary {
background: @primary;
}
.bg-primary-hover-lite {
background-color: @primary-hover-light;
}
.activeCategory {
border-left: 2px solid @primary;

View File

@ -36,3 +36,18 @@
.flex-1 {
flex: 1;
}
//top
.top-1 {
top: 4px;
}
//right
.right-3 {
right: 12px;
}
//bottom
.bottom-full {
bottom: 100%;
}

View File

@ -251,6 +251,10 @@
.p-lg {
padding: @padding-lg;
}
.p-x-xss {
padding-right: @padding-xss;
padding-left: @padding-xss;
}
.p-x-xs {
padding-right: @padding-xs;
padding-left: @padding-xs;

View File

@ -69,10 +69,6 @@ pre {
color: #c45296 !important;
}
.bg-primary {
color: #ffffff;
background-color: #2eaadc !important;
}
.bg-success {
color: #ffffff;
background-color: #28a745 !important;

View File

@ -0,0 +1,170 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Edge } from 'reactflow';
import {
EdgeTypeEnum,
SelectedEdge,
} from '../components/EntityLineage/EntityLineage.interface';
import { LineageDetails } from '../generated/api/lineage/addLineage';
import { EntityReference } from '../generated/type/entityReference';
import {
COLUMN_LINEAGE_DETAILS,
EDGE_TO_BE_REMOVED,
MOCK_COLUMN_LINEAGE_EDGE,
MOCK_LINEAGE_DATA,
MOCK_NORMAL_LINEAGE_EDGE,
MOCK_PARAMS_FOR_DOWN_STREAM,
MOCK_PARAMS_FOR_UP_STREAM,
MOCK_REMOVED_NODE,
SELECTED_EDGE,
UPDATED_COLUMN_LINEAGE,
UPDATED_EDGE_PARAM,
UPDATED_LINEAGE_EDGE,
UP_STREAM_EDGE,
} from '../mocks/Lineage.mock';
import {
createNewEdge,
findUpstreamDownStreamEdge,
getEdgeType,
getRemovedNodeData,
getUpdatedEdge,
getUpdatedUpstreamDownStreamEdgeArr,
getUpStreamDownStreamColumnLineageArr,
} from './EntityLineageUtils';
describe('Test EntityLineageUtils utility', () => {
it('findUpstreamDownStreamEdge function should work properly', () => {
const upstreamData = findUpstreamDownStreamEdge(
MOCK_LINEAGE_DATA.upstreamEdges,
SELECTED_EDGE as SelectedEdge
);
const nodata = findUpstreamDownStreamEdge(
undefined,
SELECTED_EDGE as SelectedEdge
);
expect(upstreamData).toStrictEqual(UP_STREAM_EDGE);
expect(nodata).toStrictEqual(undefined);
});
it('getUpStreamDownStreamColumnLineageArr function should work properly', () => {
const columnLineageData = getUpStreamDownStreamColumnLineageArr(
MOCK_LINEAGE_DATA.upstreamEdges[0].lineageDetails as LineageDetails,
SELECTED_EDGE as SelectedEdge
);
const nodata = getUpStreamDownStreamColumnLineageArr(
MOCK_LINEAGE_DATA.upstreamEdges[1].lineageDetails as LineageDetails,
SELECTED_EDGE as SelectedEdge
);
expect(columnLineageData).toStrictEqual(COLUMN_LINEAGE_DETAILS);
expect(nodata).toStrictEqual({ sqlQuery: '', columnsLineage: [] });
});
it('getUpdatedUpstreamDownStreamEdgeArr function should work properly', () => {
const columnLineageData = getUpdatedUpstreamDownStreamEdgeArr(
MOCK_LINEAGE_DATA.upstreamEdges,
SELECTED_EDGE as SelectedEdge,
COLUMN_LINEAGE_DETAILS
);
const nodata = getUpdatedUpstreamDownStreamEdgeArr(
[],
SELECTED_EDGE as SelectedEdge,
COLUMN_LINEAGE_DETAILS
);
expect(columnLineageData).toStrictEqual(UPDATED_LINEAGE_EDGE);
expect(nodata).toStrictEqual([]);
});
it('getRemovedNodeData function should work properly', () => {
const data = getRemovedNodeData(
MOCK_LINEAGE_DATA.nodes,
EDGE_TO_BE_REMOVED as Edge,
MOCK_LINEAGE_DATA.entity,
MOCK_LINEAGE_DATA.nodes[0]
);
const nodata = getRemovedNodeData(
[],
SELECTED_EDGE.data,
MOCK_LINEAGE_DATA.entity,
{} as EntityReference
);
expect(data).toStrictEqual(MOCK_REMOVED_NODE);
expect(nodata).toStrictEqual({
id: SELECTED_EDGE.data.id,
source: MOCK_LINEAGE_DATA.entity,
target: MOCK_LINEAGE_DATA.entity,
});
});
it('getEdgeType function should work properly', () => {
const upStreamData = getEdgeType(
MOCK_LINEAGE_DATA,
MOCK_PARAMS_FOR_UP_STREAM
);
const downStreamData = getEdgeType(
MOCK_LINEAGE_DATA,
MOCK_PARAMS_FOR_DOWN_STREAM
);
expect(upStreamData).toStrictEqual(EdgeTypeEnum.UP_STREAM);
expect(downStreamData).toStrictEqual(EdgeTypeEnum.DOWN_STREAM);
});
it('getUpdatedEdge function should work properly', () => {
const node = MOCK_LINEAGE_DATA.upstreamEdges[1];
const data = getUpdatedEdge(
[node],
UPDATED_EDGE_PARAM,
UPDATED_COLUMN_LINEAGE
);
expect(data).toStrictEqual([
{
...node,
lineageDetails: UPDATED_COLUMN_LINEAGE,
},
]);
});
it('createNewEdge function should work properly', () => {
const columnLineageEdge = createNewEdge(
UPDATED_EDGE_PARAM,
true,
'table',
'table',
true,
jest.fn
);
const normalLineageEdge = createNewEdge(
UPDATED_EDGE_PARAM,
true,
'table',
'table',
false,
jest.fn
);
const updatedColLineageEdge = MOCK_COLUMN_LINEAGE_EDGE as Edge;
updatedColLineageEdge.data.onEdgeClick = jest.fn;
const updatedNormalLineageEdge = MOCK_NORMAL_LINEAGE_EDGE as Edge;
updatedNormalLineageEdge.data.onEdgeClick = jest.fn;
expect(columnLineageEdge).toMatchObject(updatedColLineageEdge);
expect(normalLineageEdge).toMatchObject(updatedNormalLineageEdge);
});
});

View File

@ -17,15 +17,24 @@ import {
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import dagre from 'dagre';
import { isUndefined } from 'lodash';
import { LeafNodes, LineagePos, LoadingNodeState } from 'Models';
import { isEmpty, isNil, isUndefined } from 'lodash';
import { LeafNodes, LineagePos, LoadingNodeState, LoadingState } from 'Models';
import React, { Fragment, MouseEvent as ReactMouseEvent } from 'react';
import { Link } from 'react-router-dom';
import { Edge, MarkerType, Node, Position, ReactFlowInstance } from 'reactflow';
import {
Connection,
Edge,
MarkerType,
Node,
Position,
ReactFlowInstance,
} from 'reactflow';
import {
CustomEdgeData,
CustomeElement,
CustomeFlow,
CustomElement,
CustomFlow,
EdgeData,
EdgeTypeEnum,
ModifiedColumn,
SelectedEdge,
SelectedNode,
@ -44,7 +53,12 @@ import {
FqnPart,
} from '../enums/entity.enum';
import { Column } from '../generated/entity/data/table';
import { EntityLineage } from '../generated/type/entityLineage';
import {
ColumnLineage,
Edge as EntityLineageEdge,
EntityLineage,
LineageDetails,
} from '../generated/type/entityLineage';
import { EntityReference } from '../generated/type/entityReference';
import {
getPartialNameFromFQN,
@ -420,7 +434,7 @@ const dagreGraph = new dagre.graphlib.Graph();
dagreGraph.setDefaultEdgeLabel(() => ({}));
export const getLayoutedElements = (
elements: CustomeElement,
elements: CustomElement,
direction = EntityLineageDirection.LEFT_RIGHT
) => {
const { node, edge } = elements;
@ -487,9 +501,9 @@ export const getModalBodyText = (selectedEdge: SelectedEdge) => {
} and ${target.displayName ? target.displayName : targetEntity}"?`;
};
export const getUniqueFlowElements = (elements: CustomeFlow[]) => {
export const getUniqueFlowElements = (elements: CustomFlow[]) => {
const flag: { [x: string]: boolean } = {};
const uniqueElements: CustomeFlow[] = [];
const uniqueElements: CustomFlow[] = [];
elements.forEach((elem) => {
if (!flag[elem.id]) {
@ -515,3 +529,292 @@ export const getNodeRemoveButton = (onClick: () => void) => {
</button>
);
};
export const getSelectedEdgeArr = (
edgeArr: EntityLineageEdge[],
edgeData: EdgeData
) => {
return edgeArr.filter(
(edge) =>
!edgeArr.find(
() =>
edgeData.fromId === edge.fromEntity && edgeData.toId === edge.toEntity
)
);
};
/**
* Finds the upstream/downstream edge based on selected edge
* @param edgeArr edge[]
* @param data selected edge
* @returns edge
*/
export const findUpstreamDownStreamEdge = (
edgeArr: EntityLineageEdge[] | undefined,
data: SelectedEdge
) => {
return edgeArr?.find(
(edge) =>
edge.fromEntity === data.source.id && edge.toEntity === data.target.id
);
};
/**
* Get upstream/downstream column lineage array
* @param lineageDetails LineageDetails
* @param data SelectedEdge
* @returns Updated LineageDetails
*/
export const getUpStreamDownStreamColumnLineageArr = (
lineageDetails: LineageDetails,
data: SelectedEdge
) => {
const columnsLineage = lineageDetails.columnsLineage.reduce((col, curr) => {
if (curr.toColumn === data.data?.targetHandle) {
const newCol = {
...curr,
fromColumns:
curr.fromColumns?.filter(
(column) => column !== data.data?.sourceHandle
) || [],
};
if (newCol.fromColumns?.length) {
return [...col, newCol];
} else {
return col;
}
}
return [...col, curr];
}, [] as ColumnLineage[]);
return {
sqlQuery: lineageDetails.sqlQuery || '',
columnsLineage: columnsLineage,
};
};
/**
* Get updated EntityLineageEdge Array based on selected data
* @param edge EntityLineageEdge[]
* @param data SelectedEdge
* @param lineageDetails updated LineageDetails
* @returns updated EntityLineageEdge[]
*/
export const getUpdatedUpstreamDownStreamEdgeArr = (
edge: EntityLineageEdge[],
data: SelectedEdge,
lineageDetails: LineageDetails
) => {
return edge.map((down) => {
if (
down.fromEntity === data.source.id &&
down.toEntity === data.target.id
) {
return {
...down,
lineageDetails: lineageDetails,
};
}
return down;
});
};
/**
* Get array of the removed node
* @param nodes All the node
* @param edge selected edge
* @param entity main entity
* @param selectedEntity selected entity
* @returns details of removed node
*/
export const getRemovedNodeData = (
nodes: EntityReference[],
edge: Edge,
entity: EntityReference,
selectedEntity: EntityReference
) => {
let targetNode = nodes.find((node) => edge.target?.includes(node.id));
let sourceNode = nodes.find((node) => edge.source?.includes(node.id));
const selectedNode = isEmpty(selectedEntity) ? entity : selectedEntity;
if (isUndefined(targetNode)) {
targetNode = selectedNode;
}
if (isUndefined(sourceNode)) {
sourceNode = selectedNode;
}
return {
id: edge.id,
source: sourceNode,
target: targetNode,
};
};
/**
* Get source/target edge based on query string
* @param edge upstream/downstream edge array
* @param queryStr source/target string
* @param id main entity id
* @returns source/target edge
*/
const getSourceTargetNode = (
edge: EntityLineageEdge[],
queryStr: string | null,
id: string
) => {
return edge.find(
(d) =>
(queryStr?.includes(d.fromEntity) || queryStr?.includes(d.toEntity)) &&
queryStr !== id
);
};
export const getEdgeType = (
updatedLineageData: EntityLineage,
params: Edge | Connection
) => {
const { entity } = updatedLineageData;
const { target, source } = params;
const sourceDownstreamNode = getSourceTargetNode(
updatedLineageData.downstreamEdges || [],
source,
entity.id
);
const sourceUpStreamNode = getSourceTargetNode(
updatedLineageData.upstreamEdges || [],
source,
entity.id
);
const targetDownStreamNode = getSourceTargetNode(
updatedLineageData.downstreamEdges || [],
target,
entity.id
);
const targetUpStreamNode = getSourceTargetNode(
updatedLineageData.upstreamEdges || [],
target,
entity.id
);
const isUpstream =
(!isNil(sourceUpStreamNode) && !isNil(targetDownStreamNode)) ||
!isNil(sourceUpStreamNode) ||
!isNil(targetUpStreamNode) ||
target?.includes(entity.id);
const isDownstream =
(!isNil(sourceDownstreamNode) && !isNil(targetUpStreamNode)) ||
!isNil(sourceDownstreamNode) ||
!isNil(targetDownStreamNode) ||
source?.includes(entity.id);
if (isUpstream) {
return EdgeTypeEnum.UP_STREAM;
} else if (isDownstream) {
return EdgeTypeEnum.DOWN_STREAM;
}
return EdgeTypeEnum.NO_STREAM;
};
/**
* Get updated Edge with lineageDetails
* @param edges Array of Edge
* @param params new connected edge
* @param lineageDetails updated lineage details
* @returns updated edge array
*/
export const getUpdatedEdge = (
edges: EntityLineageEdge[],
params: Edge | Connection,
lineageDetails: LineageDetails | undefined
) => {
const updatedEdge: EntityLineageEdge[] = [];
const { target, source } = params;
edges.forEach((edge) => {
if (edge.fromEntity === source && edge.toEntity === target) {
updatedEdge.push({
...edge,
lineageDetails: lineageDetails,
});
} else {
updatedEdge.push(edge);
}
});
return updatedEdge;
};
// create new edge
export const createNewEdge = (
params: Edge | Connection,
isEditMode: boolean,
sourceNodeType: string,
targetNodeType: string,
isColumnLineage: boolean,
onEdgeClick: (
evt: React.MouseEvent<HTMLButtonElement>,
data: CustomEdgeData
) => void
) => {
const { target, source, sourceHandle, targetHandle } = params;
let data: Edge = {
id: `edge-${source}-${target}`,
source: `${source}`,
target: `${target}`,
type: isEditMode ? 'buttonedge' : 'custom',
style: { strokeWidth: '2px' },
markerEnd: {
type: MarkerType.ArrowClosed,
},
data: {
id: `edge-${source}-${target}`,
source: source,
target: target,
sourceType: sourceNodeType,
targetType: targetNodeType,
isColumnLineage: isColumnLineage,
onEdgeClick,
},
};
if (isColumnLineage) {
data = {
...data,
id: `column-${sourceHandle}-${targetHandle}-edge-${source}-${target}`,
sourceHandle: sourceHandle,
targetHandle: targetHandle,
style: undefined,
data: {
...data.data,
id: `column-${sourceHandle}-${targetHandle}-edge-${source}-${target}`,
sourceHandle: sourceHandle,
targetHandle: targetHandle,
},
};
}
return data;
};
export const LoadingStatus = (
defaultState: string | JSX.Element,
loading: boolean,
status: LoadingState
) => {
if (loading) {
return <Loader size="small" type="white" />;
} else if (status === 'success') {
return <FontAwesomeIcon className="text-white" icon="check" />;
} else {
return defaultState;
}
};