feat: add lineage config dialog (#10496)

* feat: initial commit for lineage pagination

* fix: optimize the entitylineage component

removing all props that were passed from datasetdetails

* fix: remove code from details pages

* fix: update styling of load more node

* fix: update lineage DS

* fix: pagination issue

* feat: add lineage config dialog

* test: add unit tests for lineage config modal

* fix: update node styles for lineage

* fix: update localization text for other languages

* fix: resolve merge conflicts

* fix: added localization messages

* fix: add localization for other languages

---------

Co-authored-by: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com>
Co-authored-by: Shailesh Parmar <shailesh.parmar.webdev@gmail.com>
This commit is contained in:
karanh37 2023-03-16 18:38:09 +05:30 committed by GitHub
parent e706c81fcd
commit 39fc6f86e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 563 additions and 200 deletions

View File

@ -11,6 +11,7 @@
* limitations under the License.
*/
import { SettingOutlined } from '@ant-design/icons';
import { Button, Col, Row, Select, Space } from 'antd';
import classNames from 'classnames';
import React, {
@ -35,7 +36,8 @@ import {
} from '../../constants/Lineage.constants';
import { getLoadingStatusValue } from '../../utils/EntityLineageUtils';
import SVGIcons, { Icons } from '../../utils/SvgUtils';
import { ControlProps } from './EntityLineage.interface';
import { ControlProps, LineageConfig } from './EntityLineage.interface';
import LineageConfigModal from './LineageConfigModal';
export const ControlButton: FC<ButtonHTMLAttributes<HTMLButtonElement>> = ({
children,
@ -69,11 +71,14 @@ const CustomControls: FC<ControlProps> = ({
status,
zoomValue,
lineageData,
lineageConfig,
onOptionSelect,
onLineageConfigUpdate,
}: ControlProps) => {
const { t } = useTranslation();
const { fitView, zoomTo } = useReactFlow();
const [zoom, setZoom] = useState<number>(zoomValue);
const [dialogVisible, setDialogVisible] = useState<boolean>(false);
const onZoomHandler = useCallback(
(zoomLevel: number) => {
@ -146,124 +151,151 @@ const CustomControls: FC<ControlProps> = ({
);
}, [isEditMode]);
const handleDialogSave = useCallback(
(config: LineageConfig) => {
onLineageConfigUpdate(config);
setDialogVisible(false);
},
[onLineageConfigUpdate, setDialogVisible]
);
return (
<Row
className={classNames('z-10 w-full', className)}
gutter={[8, 8]}
style={style}>
<Col span={12}>
<Select
allowClear
showSearch
className={classNames('custom-control-search-box', {
'custom-control-search-box-edit-mode': isEditMode,
})}
filterOption={handleSearchFilterOption}
options={nodeOptions}
placeholder={t('label.search-entity', {
entity: t('label.lineage'),
})}
onChange={onOptionSelect}
/>
</Col>
<Col span={12}>
<Space className="justify-end w-full" size={16}>
<Button
ghost
data-testid="expand-column"
type="primary"
onClick={onExpandColumnClick}>
{isColumnsExpanded
? t('label.collapse-all')
: t('label.expand-all')}
</Button>
<>
<Row
className={classNames('z-10 w-full', className)}
gutter={[8, 8]}
style={style}>
<Col span={12}>
<Select
allowClear
showSearch
className={classNames('custom-control-search-box', {
'custom-control-search-box-edit-mode': isEditMode,
})}
filterOption={handleSearchFilterOption}
options={nodeOptions}
placeholder={t('label.search-entity', {
entity: t('label.lineage'),
})}
onChange={onOptionSelect}
/>
</Col>
<Col span={12}>
<Space className="justify-end w-full" size={16}>
<Button
ghost
data-testid="expand-column"
type="primary"
onClick={onExpandColumnClick}>
{isColumnsExpanded
? t('label.collapse-all')
: t('label.expand-all')}
</Button>
{showZoom && (
<div className="flow-control custom-control-fit-screen-button custom-control-zoom-slide">
{showZoom && (
<div className="flow-control custom-control-fit-screen-button custom-control-zoom-slide">
<ControlButton
className="custom-control-basic-button"
onClick={onZoomOutHandler}>
<SVGIcons
alt="minus-icon"
className="tw--mt-0.5"
icon="icon-control-minus"
width="12"
/>
</ControlButton>
<input
className="tw-bg-body-hover"
max={MAX_ZOOM_VALUE}
min={MIN_ZOOM_VALUE}
step={ZOOM_SLIDER_STEP}
type="range"
value={zoom}
onChange={onRangeChange}
/>
<ControlButton
className="custom-control-basic-button"
onClick={onZoomInHandler}>
<SVGIcons
alt="plus-icon"
className="tw--mt-0.5"
icon="icon-control-plus"
width="12"
/>
</ControlButton>
</div>
)}
{showFitView && (
<ControlButton
className="custom-control-basic-button"
onClick={onZoomOutHandler}>
className="custom-control-basic-button custom-control-fit-screen-button"
title={t('label.fit-to-screen')}
onClick={onFitViewHandler}>
<SVGIcons alt="fit-view" icon={Icons.FITVEW} width="16" />
</ControlButton>
)}
{handleFullScreenViewClick && (
<ControlButton
className="custom-control-basic-button custom-control-fit-screen-button"
title={t('label.full-screen')}
onClick={handleFullScreenViewClick}>
<SVGIcons
alt="minus-icon"
className="tw--mt-0.5"
icon="icon-control-minus"
width="12"
alt="fullScreenViewicon"
icon={Icons.FULL_SCREEN}
width="16"
/>
</ControlButton>
<input
className="tw-bg-body-hover"
max={MAX_ZOOM_VALUE}
min={MIN_ZOOM_VALUE}
step={ZOOM_SLIDER_STEP}
type="range"
value={zoom}
onChange={onRangeChange}
/>
)}
{onExitFullScreenViewClick && (
<ControlButton
className="custom-control-basic-button"
onClick={onZoomInHandler}>
className="custom-control-basic-button custom-control-fit-screen-button"
title={t('label.exit-fit-to-screen')}
onClick={onExitFullScreenViewClick}>
<SVGIcons
alt="plus-icon"
className="tw--mt-0.5"
icon="icon-control-plus"
width="12"
alt="exitFullScreenViewIcon"
icon={Icons.EXIT_FULL_SCREEN}
width="16"
/>
</ControlButton>
</div>
)}
{showFitView && (
)}
<ControlButton
className="custom-control-basic-button custom-control-fit-screen-button"
onClick={onFitViewHandler}>
<SVGIcons alt="fit-view" icon={Icons.FITVEW} width="16" />
title={t('label.setting-plural')}
onClick={() => setDialogVisible(true)}>
<SettingOutlined style={{ fontSize: '16px', color: '#7147E8' }} />
</ControlButton>
)}
{handleFullScreenViewClick && (
<ControlButton
className="custom-control-basic-button custom-control-fit-screen-button"
onClick={handleFullScreenViewClick}>
<SVGIcons
alt="fullScreenViewicon"
icon={Icons.FULL_SCREEN}
width="16"
/>
</ControlButton>
)}
{onExitFullScreenViewClick && (
<ControlButton
className="custom-control-basic-button custom-control-fit-screen-button"
onClick={onExitFullScreenViewClick}>
<SVGIcons
alt="exitFullScreenViewIcon"
icon={Icons.EXIT_FULL_SCREEN}
width="16"
/>
</ControlButton>
)}
{!deleted && (
<ControlButton
className={classNames(
'h-8 w-8 rounded-full p-x-xss tw-shadow-lg',
{
'bg-primary': !isEditMode,
'bg-primary-hover-lite': isEditMode,
{!deleted && (
<ControlButton
className={classNames(
'custom-control-edit-button h-8 w-8 rounded-full p-x-xss tw-shadow-lg',
{
'bg-primary': !isEditMode,
'bg-primary-hover-lite': isEditMode,
}
)}
data-testid="edit-lineage"
disabled={!hasEditAccess}
title={
hasEditAccess
? t('label.edit-entity', { entity: t('label.lineage') })
: NO_PERMISSION_FOR_ACTION
}
)}
data-testid="edit-lineage"
disabled={!hasEditAccess}
title={
hasEditAccess
? t('label.edit-entity', { entity: t('label.lineage') })
: NO_PERMISSION_FOR_ACTION
}
onClick={onEditLinageClick}>
{getLoadingStatusValue(editIcon, loading, status)}
</ControlButton>
)}
</Space>
</Col>
</Row>
onClick={onEditLinageClick}>
{getLoadingStatusValue(editIcon, loading, status)}
</ControlButton>
)}
</Space>
</Col>
</Row>
<LineageConfigModal
config={lineageConfig}
visible={dialogVisible}
onCancel={() => setDialogVisible(false)}
onSave={handleDialogSave}
/>
</>
);
};

View File

@ -134,6 +134,7 @@ import {
EntityLineageProp,
EntityReferenceChild,
LeafNodes,
LineageConfig,
LineagePos,
LoadingNodeState,
ModifiedColumn,
@ -150,6 +151,7 @@ const EntityLineageComponent: FunctionComponent<EntityLineageProp> = ({
deleted,
hasEditAccess,
entityType,
isFullScreen = false,
}: EntityLineageProp) => {
const { t } = useTranslation();
const reactFlowWrapper = useRef<HTMLDivElement>(null);
@ -203,6 +205,11 @@ const EntityLineageComponent: FunctionComponent<EntityLineageProp> = ({
state: false,
});
const [leafNodes, setLeafNodes] = useState<LeafNodes>({} as LeafNodes);
const [lineageConfig, setLineageConfig] = useState<LineageConfig>({
upstreamDepth: 3,
downstreamDepth: 3,
nodesPerLayer: 50,
});
const params = useParams<Record<string, string>>();
const entityFQN =
@ -213,21 +220,34 @@ const EntityLineageComponent: FunctionComponent<EntityLineageProp> = ({
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 fetchLineageData = useCallback(
async (config: LineageConfig) => {
setIsLineageLoading(true);
try {
const res = await getLineageByFQN(
entityFQN,
entityType,
config.upstreamDepth,
config.downstreamDepth
);
if (res) {
setPaginationData({});
setEntityLineage(res);
setUpdatedLineageData(res);
} else {
showErrorToast(jsonData['api-error-messages']['fetch-lineage-error']);
}
} 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) => {
@ -1174,10 +1194,10 @@ const EntityLineageComponent: FunctionComponent<EntityLineageProp> = ({
* handle node drag event
* @param event
*/
const onDragOver = (event: DragEvent) => {
const onDragOver = useCallback((event: DragEvent) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
};
}, []);
/**
* handle node drop event
@ -1299,7 +1319,7 @@ const EntityLineageComponent: FunctionComponent<EntityLineageProp> = ({
/**
* This method will handle the delete edge modal confirmation
*/
const onRemove = () => {
const onRemove = useCallback(() => {
setDeletionState({ ...ELEMENT_DELETE_STATE, loading: true });
setTimeout(() => {
setDeletionState({ ...ELEMENT_DELETE_STATE, status: 'success' });
@ -1309,21 +1329,21 @@ const EntityLineageComponent: FunctionComponent<EntityLineageProp> = ({
setDeletionState((pre) => ({ ...pre, status: 'initial' }));
}, 500);
}, 500);
};
}, []);
const handleEditLineageClick = () => {
const handleEditLineageClick = useCallback(() => {
setEditMode((pre) => !pre && !deleted);
resetSelectedData();
setIsDrawerOpen(false);
};
}, [deleted]);
const handleEdgeClick = (
_e: React.MouseEvent<Element, MouseEvent>,
edge: Edge
) => {
setSelectedEdgeInfo(edge);
setIsDrawerOpen(true);
};
const handleEdgeClick = useCallback(
(_e: React.MouseEvent<Element, MouseEvent>, edge: Edge) => {
setSelectedEdgeInfo(edge);
setIsDrawerOpen(true);
},
[]
);
const toggleColumnView = (value: boolean) => {
setExpandAllColumns(value);
@ -1409,6 +1429,10 @@ const EntityLineageComponent: FunctionComponent<EntityLineageProp> = ({
}
};
const handleLineageConfigUpdate = useCallback((config: LineageConfig) => {
setLineageConfig(config);
fetchLineageData(config);
}, []);
const selectNode = (node: Node) => {
const { position } = node;
onNodeClick(node);
@ -1433,7 +1457,8 @@ const EntityLineageComponent: FunctionComponent<EntityLineageProp> = ({
const { nodes, edges } = getPaginatedChildMap(
updatedLineageData,
childMap,
paginationData
paginationData,
lineageConfig.nodesPerLayer
);
const newNodes = union(nodes, path);
setElementsHandle(
@ -1498,7 +1523,8 @@ const EntityLineageComponent: FunctionComponent<EntityLineageProp> = ({
const { nodes: newNodes, edges } = getPaginatedChildMap(
lineageData,
childMapObj,
paginationObj
paginationObj,
lineageConfig.nodesPerLayer
);
setElementsHandle({
...lineageData,
@ -1509,7 +1535,7 @@ const EntityLineageComponent: FunctionComponent<EntityLineageProp> = ({
};
useEffect(() => {
fetchLineageData();
fetchLineageData(lineageConfig);
}, []);
useEffect(() => {
@ -1614,17 +1640,23 @@ const EntityLineageComponent: FunctionComponent<EntityLineageProp> = ({
minZoom: MIN_ZOOM_VALUE,
maxZoom: MAX_ZOOM_VALUE,
}}
handleFullScreenViewClick={onFullScreenClick}
handleFullScreenViewClick={
!isFullScreen ? onFullScreenClick : undefined
}
hasEditAccess={hasEditAccess}
isColumnsExpanded={expandAllColumns}
isEditMode={isEditMode}
lineageConfig={lineageConfig}
lineageData={updatedLineageData}
loading={loading}
status={status}
zoomValue={zoomValue}
onEditLinageClick={handleEditLineageClick}
onExitFullScreenViewClick={onExitFullScreenViewClick}
onExitFullScreenViewClick={
isFullScreen ? onExitFullScreenViewClick : undefined
}
onExpandColumnClick={handleExpandColumnClick}
onLineageConfigUpdate={handleLineageConfigUpdate}
onOptionSelect={handleOptionSelect}
/>
)}

View File

@ -32,6 +32,7 @@ export interface EntityLineageProp {
entityType: EntityType;
deleted?: boolean;
hasEditAccess?: boolean;
isFullScreen?: boolean;
}
export interface Edge {
@ -123,7 +124,9 @@ export interface ControlProps extends HTMLAttributes<HTMLDivElement> {
status: LoadingState;
zoomValue: number;
lineageData: EntityLineage;
lineageConfig: LineageConfig;
onOptionSelect: (value?: string) => void;
onLineageConfigUpdate: (config: LineageConfig) => void;
}
export type LineagePos = 'from' | 'to';
@ -151,3 +154,16 @@ export interface NodeIndexMap {
upstream: number[];
downstream: number[];
}
export interface LineageConfig {
upstreamDepth: number;
downstreamDepth: number;
nodesPerLayer: number;
}
export interface LineageConfigModalProps {
visible: boolean;
config: LineageConfig;
onCancel: () => void;
onSave: (config: LineageConfig) => void;
}

View File

@ -0,0 +1,66 @@
/*
* Copyright 2023 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 { fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import LineageConfigModal from './LineageConfigModal';
const onCancel = jest.fn();
const onSave = jest.fn();
const config = {
upstreamDepth: 2,
downstreamDepth: 3,
nodesPerLayer: 4,
};
describe('LineageConfigModal', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('renders the modal with pre-populated values', async () => {
render(
<LineageConfigModal
visible
config={config}
onCancel={onCancel}
onSave={onSave}
/>
);
const fieldUpstream = await screen.findByTestId('field-upstream');
const fieldDownstream = await screen.findByTestId('field-downstream');
const fieldNodesPerLayer = await screen.findByTestId(
'field-nodes-per-layer'
);
expect(fieldUpstream).toBeInTheDocument();
expect(fieldDownstream).toBeInTheDocument();
expect(fieldNodesPerLayer).toBeInTheDocument();
});
it('calls onCancel when Cancel button is clicked', () => {
render(
<LineageConfigModal
visible
config={config}
onCancel={onCancel}
onSave={onSave}
/>
);
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
expect(onCancel).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,121 @@
/*
* Copyright 2023 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 { Form, InputNumber, Modal, Select } from 'antd';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
LineageConfig,
LineageConfigModalProps,
} from './EntityLineage.interface';
const SELECT_OPTIONS = [1, 2, 3].map((value) => (
<Select.Option key={value} value={value}>
{value}
</Select.Option>
));
const LineageConfigModal: React.FC<LineageConfigModalProps> = ({
visible,
config,
onCancel,
onSave,
}) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const [upstreamDepth, setUpstreamDepth] = useState<number>(
config.upstreamDepth || 1
);
const [downstreamDepth, setDownstreamDepth] = useState<number>(
config.downstreamDepth || 1
);
const [nodesPerLayer, setNodesPerLayer] = useState<number>(
config.nodesPerLayer || 1
);
const handleSave = () => {
const updatedConfig: LineageConfig = {
upstreamDepth,
downstreamDepth,
nodesPerLayer,
};
onSave(updatedConfig);
};
return (
<Modal
title={t('label.lineage-config')}
visible={visible}
onCancel={onCancel}
onOk={form.submit}>
<Form
form={form}
initialValues={config}
layout="vertical"
onFinish={handleSave}>
<Form.Item
label={t('label.upstream-depth')}
name="upstreamDepth"
rules={[
{
required: true,
message: t('message.upstream-depth-message'),
},
]}
tooltip={t('message.upstream-depth-tooltip')}>
<Select
data-testid="field-upstream"
onChange={(value) => setUpstreamDepth(value as number)}>
{SELECT_OPTIONS}
</Select>
</Form.Item>
<Form.Item
label={t('label.downstream-depth')}
name="downstreamDepth"
rules={[
{
required: true,
message: t('message.downstream-depth-message'),
},
]}
tooltip={t('message.downstream-depth-tooltip')}>
<Select
data-testid="field-downstream"
onChange={(value) => setDownstreamDepth(value as number)}>
{SELECT_OPTIONS}
</Select>
</Form.Item>
<Form.Item
label={t('label.nodes-per-layer')}
name="nodesPerLayer"
rules={[
{
required: true,
message: t('message.nodes-per-layer-message'),
},
]}
tooltip={t('message.nodes-per-layer-tooltip')}>
<InputNumber
className="w-full"
data-testid="field-nodes-per-layer"
min={5}
onChange={(value) => setNodesPerLayer(value as number)}
/>
</Form.Item>
</Form>
</Modal>
);
};
export default LineageConfigModal;

View File

@ -50,16 +50,16 @@ const NodeSuggestions: FC<EntitySuggestionProps> = ({
const [isOpen, setIsOpen] = useState<boolean>(false);
const [searchValue, setSearchValue] = useState<string>('');
const getSuggestionLabel = (fqn: string, type: string, name: string) => {
const getSuggestionLabelHeading = (fqn: string, type: string) => {
if (type === EntityType.TABLE) {
const database = getPartialNameFromTableFQN(fqn, [FqnPart.Database]);
const schema = getPartialNameFromTableFQN(fqn, [FqnPart.Schema]);
return database && schema
? `${database}${FQN_SEPARATOR_CHAR}${schema}${FQN_SEPARATOR_CHAR}${name}`
: name;
? `${database}${FQN_SEPARATOR_CHAR}${schema}`
: '';
} else {
return name;
return '';
}
};
@ -150,16 +150,15 @@ const NodeSuggestions: FC<EntitySuggestionProps> = ({
<div
aria-labelledby="menu-button"
aria-orientation="vertical"
className="tw-origin-top-right tw-absolute tw-z-20
tw-w-max tw-mt-1 tw-rounded-md tw-shadow-lg
className="suggestion-node-item tw-z-20
tw-mt-1 tw-rounded-md tw-shadow-lg
tw-bg-white tw-ring-1 tw-ring-black tw-ring-opacity-5 focus:tw-outline-none text-body"
role="menu">
{data.map((entity) => (
<div
className="tw-flex tw-items-center hover:tw-bg-body-hover"
key={entity.fullyQualifiedName}>
<span
className="tw-block tw-px-2 tw-py-2 tw-text-sm tw-break-all"
<>
<div
className="w-full d-flex items-center tw-px-2 tw-py-2 tw-text-sm hover:tw-bg-body-hover"
key={entity.fullyQualifiedName}
onClick={() => {
setIsOpen(false);
onSelectHandler?.({
@ -176,13 +175,20 @@ const NodeSuggestions: FC<EntitySuggestionProps> = ({
className="tw-inline tw-h-4 tw-mr-2"
src={serviceTypeLogo(entity.serviceType as string)}
/>
{getSuggestionLabel(
entity.fullyQualifiedName,
entity.entityType as string,
entity.name
)}
</span>
</div>
<div className="flex-1 text-left tw-px-2">
{entity.entityType === EntityType.TABLE && (
<p className="d-block text-xs custom-lineage-heading">
{getSuggestionLabelHeading(
entity.fullyQualifiedName,
entity.entityType as string
)}
</p>
)}
<p className="">{entity.name}</p>
</div>
</div>
<hr className="tw-w-full" />
</>
))}
</div>
) : (

View File

@ -86,6 +86,9 @@
.react-flow__handle {
opacity: 0;
}
.custom-lineage-heading {
color: @white;
}
}
}
@ -114,6 +117,7 @@
display: flex;
column-gap: 8px;
height: 32px;
width: 150px;
}
.custom-node-expand-button {
position: absolute;
@ -132,3 +136,23 @@
color: #37352f;
}
}
.control-button {
display: flex;
align-items: center;
justify-content: center;
}
.custom-control-edit-button {
display: block !important;
}
.custom-lineage-heading {
color: @text-grey-muted;
}
.suggestion-node-item {
position: absolute;
right: 0;
width: 210px;
}

View File

@ -230,6 +230,7 @@
"doc-plural": "Docs",
"documentation-lowercase": "documentation",
"domain": "Domain",
"downstream-depth": "Downstream Depth",
"edge-information": "Edge Information",
"edge-lowercase": "edge",
"edit": "Edit",
@ -273,6 +274,7 @@
"exclude": "Exclude",
"execution-date": "Execution Date",
"execution-plural": "Executions",
"exit-fit-to-screen": "Exit fit to screen",
"expand-all": "Expand All",
"explore": "Explore",
"explore-data": "Explore Data",
@ -297,6 +299,7 @@
"first": "First",
"first-lowercase": "first",
"first-quartile": "First Quartile",
"fit-to-screen": "Fit to screen",
"flush-interval-secs": "Flush Interval (secs)",
"follow": "Follow",
"followed-lowercase": "followed",
@ -313,6 +316,7 @@
"friday": "Friday",
"from-lowercase": "from",
"full-name": "Full name",
"full-screen": "Full screen",
"function": "Function",
"g-chat": "G Chat",
"gcs-config-source": "GCS Config Source",
@ -405,6 +409,7 @@
"leave-team": "Leave Team",
"less-lowercase": "less",
"lineage": "Lineage",
"lineage-config": "Lineage Config",
"lineage-ingestion": "Lineage Ingestion",
"lineage-lowercase": "lineage",
"list": "List",
@ -479,6 +484,7 @@
"no-parameter-available": "No Parameter Available",
"no-reviewer": "No reviewer",
"no-tags-added": "No Tags added",
"nodes-per-layer": "Nodes Per Layer",
"none": "None",
"not-found-lowercase": "not found",
"not-null": "Not Null",
@ -800,6 +806,7 @@
"updated-lowercase": "updated",
"updated-on": "Updated on",
"upload-csv-uppercase-file": "Upload CSV file",
"upstream-depth": "Upstream Depth",
"url-lowercase": "url",
"url-uppercase": "URL",
"usage": "Usage",
@ -919,6 +926,8 @@
"delete-team-message": "Any teams under \"{{teamName}}\" will be {{deleteType}} deleted as well.",
"delete-webhook-permanently": "You want to delete webhook {{webhookName}} permanently? This action cannot be reverted.",
"discover-your-data-and-unlock-the-value-of-data-assets": "Discover your data and unlock the value of data assets.",
"downstream-depth-message": "Please select a value for downstream depth",
"downstream-depth-tooltip": "Display up to 3 nodes of downstream lineage to identify the target (child levels).",
"drag-and-drop-files-here": "Drag & drop files here",
"edit-service-entity-connection": "Edit {{entity}} Service Connection",
"elastic-search-message": "Ensure that your Elasticsearch indexes are up-to-date by syncing, or recreating all indexes.",
@ -1057,6 +1066,8 @@
"no-username-available": "No user available with name",
"no-users": "There are no users {{text}}",
"no-version-type-available": "No {{type}} version available",
"nodes-per-layer-message": "Please enter a value for nodes per layer",
"nodes-per-layer-tooltip": "Choose to display n number of nodes per layer. If the existing nodes exceed the defined number of nodes, then pagination will be shown.",
"not-followed-anything": "You have not followed anything yet.",
"om-description": "Centralized metadata store, to discover, collaborate and get your data right.",
"onboarding-claim-ownership-description": "Data works well when it is owned. Take a look at the data assets that you own and claim ownership.",
@ -1143,6 +1154,8 @@
"type-delete-to-confirm": "Type <0>DELETE</0> to confirm",
"unable-to-connect-to-your-dbt-cloud-instance": "URL to connect to your dbt cloud instance. E.g., \n https://cloud.getdbt.com or https://emea.dbt.com/",
"unable-to-error-elasticsearch": "We are unable to {{error}} Elasticsearch for entity indexes.",
"upstream-depth-message": "Please select a value for upstream depth",
"upstream-depth-tooltip": "Display up to 3 nodes of upstream lineage to identify the source (parent levels).",
"usage-ingestion-description": "Usage ingestion can be configured and deployed after a metadata ingestion has been set up. The usage ingestion workflow obtains the query log and table creation details from the underlying database and feeds it to OpenMetadata. Metadata and usage can have only one pipeline for a database service. Define the Query Log Duration (in days), Stage File Location, and Result Limit to start.",
"use-fqn-for-filtering-message": "Regex will be applied on fully qualified name (e.g service_name.db_name.schema_name.table_name) instead of raw name (e.g. table_name).",
"user-assign-new-task": "{{user}} assigned you a new task.",

View File

@ -230,6 +230,7 @@
"doc-plural": "Docs",
"documentation-lowercase": "documentation",
"domain": "Domain",
"downstream-depth": "Downstream Depth",
"edge-information": "Informations de Bord",
"edge-lowercase": "edge",
"edit": "Editer",
@ -273,6 +274,7 @@
"exclude": "Exclure",
"execution-date": "Date d'Execution",
"execution-plural": "Executions",
"exit-fit-to-screen": "Exit fit to screen",
"expand-all": "Agrandir Tout",
"explore": "Explorer",
"explore-data": "Explore Data",
@ -297,6 +299,7 @@
"first": "First",
"first-lowercase": "first",
"first-quartile": "First Quartile",
"fit-to-screen": "Fit to screen",
"flush-interval-secs": "Flush Interval (secs)",
"follow": "Follow",
"followed-lowercase": "followed",
@ -313,6 +316,7 @@
"friday": "Friday",
"from-lowercase": "de",
"full-name": "Full name",
"full-screen": "Full screen",
"function": "Fonction",
"g-chat": "G Chat",
"gcs-config-source": "GCS Config Source",
@ -405,6 +409,7 @@
"leave-team": "Quitter une Equipe",
"less-lowercase": "less",
"lineage": "Traçabilité",
"lineage-config": "Lineage Config",
"lineage-ingestion": "Lineage Ingestion",
"lineage-lowercase": "lineage",
"list": "List",
@ -479,6 +484,7 @@
"no-parameter-available": "No Parameter Available",
"no-reviewer": "No reviewer",
"no-tags-added": "No Tags added",
"nodes-per-layer": "Nodes Per Layer",
"none": "None",
"not-found-lowercase": "not found",
"not-null": "Not Null",
@ -800,6 +806,7 @@
"updated-lowercase": "updated",
"updated-on": "Updated on",
"upload-csv-uppercase-file": "Upload CSV file",
"upstream-depth": "Upstream Depth",
"url-lowercase": "url",
"url-uppercase": "URL",
"usage": "Usage",
@ -919,6 +926,8 @@
"delete-team-message": "Any teams under \"{{teamName}}\" will be {{deleteType}} deleted as well.",
"delete-webhook-permanently": "Vous voulez supprimer le webhook {{webhookName}} de manière permanente ? Cette action ne peut pas être annulée.",
"discover-your-data-and-unlock-the-value-of-data-assets": "Découvrez vos données et libérez la valeur de vos resources de données",
"downstream-depth-message": "Please select a value for downstream depth",
"downstream-depth-tooltip": "Display up to 3 nodes of downstream lineage to identify the target (child levels).",
"drag-and-drop-files-here": "Drag & drop files here",
"edit-service-entity-connection": "Edit {{entity}} Service Connection",
"elastic-search-message": "Ensure that your Elasticsearch indexes are up-to-date by syncing, or recreating all indexes.",
@ -1057,6 +1066,8 @@
"no-username-available": "No user available with name",
"no-users": "Il n'y a pas d'utilisteurs {{text}}",
"no-version-type-available": "No {{type}} version available",
"nodes-per-layer-message": "Please enter a value for nodes per layer",
"nodes-per-layer-tooltip": "Choose to display n number of nodes per layer. If the existing nodes exceed the defined number of nodes, then pagination will be shown.",
"not-followed-anything": "You have not followed anything yet.",
"om-description": "Entrepôt centralisé des métadonnées, découvrez, collaborez et soyez sûr que vos données sont correctes",
"onboarding-claim-ownership-description": "Data works well when it is owned. Take a look at the data assets that you own and claim ownership.",
@ -1143,6 +1154,8 @@
"type-delete-to-confirm": "Type <0>DELETE</0> to confirm",
"unable-to-connect-to-your-dbt-cloud-instance": "URL to connect to your dbt cloud instance. E.g., \n https://cloud.getdbt.com or https://emea.dbt.com/",
"unable-to-error-elasticsearch": "We are unable to {{error}} Elasticsearch for entity indexes.",
"upstream-depth-message": "Please select a value for upstream depth",
"upstream-depth-tooltip": "Display up to 3 nodes of upstream lineage to identify the source (parent levels).",
"usage-ingestion-description": "Usage ingestion can be configured and deployed after a metadata ingestion has been set up. The usage ingestion workflow obtains the query log and table creation details from the underlying database and feeds it to OpenMetadata. Metadata and usage can have only one pipeline for a database service. Define the Query Log Duration (in days), Stage File Location, and Result Limit to start.",
"use-fqn-for-filtering-message": "Regex will be applied on fully qualified name (e.g service_name.db_name.schema_name.table_name) instead of raw name (e.g. table_name).",
"user-assign-new-task": "{{user}} assigned you a new task.",

View File

@ -230,6 +230,7 @@
"doc-plural": "文档",
"documentation-lowercase": "documentation",
"domain": "域",
"downstream-depth": "Downstream Depth",
"edge-information": "边缘信息",
"edge-lowercase": "edge",
"edit": "编辑",
@ -273,6 +274,7 @@
"exclude": "排除",
"execution-date": "执行日期",
"execution-plural": "执行",
"exit-fit-to-screen": "Exit fit to screen",
"expand-all": "全部展开",
"explore": "Explore",
"explore-data": "Explore Data",
@ -297,6 +299,7 @@
"first": "First",
"first-lowercase": "first",
"first-quartile": "First Quartile",
"fit-to-screen": "Fit to screen",
"flush-interval-secs": "刷新间隔 (secs)",
"follow": "Follow",
"followed-lowercase": "被关注",
@ -313,6 +316,7 @@
"friday": "Friday",
"from-lowercase": "从",
"full-name": "Full name",
"full-screen": "Full screen",
"function": "函数",
"g-chat": "G Chat",
"gcs-config-source": "GCS Config Source",
@ -405,6 +409,7 @@
"leave-team": "Leave team",
"less-lowercase": "less",
"lineage": "血源",
"lineage-config": "Lineage Config",
"lineage-ingestion": "Lineage Ingestion",
"lineage-lowercase": "lineage",
"list": "列表",
@ -479,6 +484,7 @@
"no-parameter-available": "No Parameter Available",
"no-reviewer": "No reviewer",
"no-tags-added": "No Tags added",
"nodes-per-layer": "Nodes Per Layer",
"none": "None",
"not-found-lowercase": "not found",
"not-null": "Not null",
@ -800,6 +806,7 @@
"updated-lowercase": "updated",
"updated-on": "Updated on",
"upload-csv-uppercase-file": "Upload CSV file",
"upstream-depth": "Upstream Depth",
"url-lowercase": "url",
"url-uppercase": "URL",
"usage": "使用",
@ -919,6 +926,8 @@
"delete-team-message": "Any teams under \"{{teamName}}\" will be {{deleteType}} deleted as well.",
"delete-webhook-permanently": "You want to delete webhook {{webhookName}} permanently? This action cannot be reverted.",
"discover-your-data-and-unlock-the-value-of-data-assets": "Discover your data and unlock the value of data assets",
"downstream-depth-message": "Please select a value for downstream depth",
"downstream-depth-tooltip": "Display up to 3 nodes of downstream lineage to identify the target (child levels).",
"drag-and-drop-files-here": "Drag & drop files here",
"edit-service-entity-connection": "Edit {{entity}} Service Connection",
"elastic-search-message": "Ensure that your Elasticsearch indexes are up-to-date by syncing, or recreating all indexes.",
@ -1057,6 +1066,8 @@
"no-username-available": "No user available with name",
"no-users": "There are no users {{text}}",
"no-version-type-available": "No {{type}} version available",
"nodes-per-layer-message": "Please enter a value for nodes per layer",
"nodes-per-layer-tooltip": "Choose to display n number of nodes per layer. If the existing nodes exceed the defined number of nodes, then pagination will be shown.",
"not-followed-anything": "You have not followed anything yet.",
"om-description": "centralized metadata store, discover, collaborate and get your data right",
"onboarding-claim-ownership-description": "Data works well when it is owned. Take a look at the data assets that you own and claim ownership.",
@ -1143,6 +1154,8 @@
"type-delete-to-confirm": "Type <0>DELETE</0> to confirm",
"unable-to-connect-to-your-dbt-cloud-instance": "URL to connect to your dbt cloud instance. E.g., \n https://cloud.getdbt.com or https://emea.dbt.com/",
"unable-to-error-elasticsearch": "We are unable to {{error}} Elasticsearch for entity indexes.",
"upstream-depth-message": "Please select a value for upstream depth",
"upstream-depth-tooltip": "Display up to 3 nodes of upstream lineage to identify the source (parent levels).",
"usage-ingestion-description": "Usage ingestion can be configured and deployed after a metadata ingestion has been set up. The usage ingestion workflow obtains the query log and table creation details from the underlying database and feeds it to OpenMetadata. Metadata and usage can have only one pipeline for a database service. Define the Query Log Duration (in days), Stage File Location, and Result Limit to start.",
"use-fqn-for-filtering-message": "Regex will be applied on fully qualified name (e.g service_name.db_name.schema_name.table_name) instead of raw name (e.g. table_name).",
"user-assign-new-task": "{{user}} assigned you a new task.",

View File

@ -202,7 +202,11 @@ const LineagePage = () => {
<div className="lineage-page-container">
<TitleBreadcrumb titleLinks={titleBreadcrumb} />
<Card className="h-full" size="default">
<EntityLineageComponent hasEditAccess entityType={entityType} />
<EntityLineageComponent
hasEditAccess
isFullScreen
entityType={entityType}
/>
</Card>
</div>
</PageLayoutV1>

View File

@ -497,11 +497,14 @@ export const getDataLabel = (
<span
className="tw-break-words tw-self-center w-72"
data-testid="lineage-entity">
{type === 'table'
? databaseName && schemaName
? `${databaseName}${FQN_SEPARATOR_CHAR}${schemaName}${FQN_SEPARATOR_CHAR}${label}`
: label
: label}
{type === 'table' && databaseName && schemaName ? (
<span className="d-block text-xs custom-lineage-heading">
{databaseName}
{FQN_SEPARATOR_CHAR}
{schemaName}
</span>
) : null}
<span className="">{label}</span>
</span>
);
}
@ -1199,16 +1202,18 @@ export const getChildMap = (obj: EntityLineage) => {
newData.downstreamEdges = removeDuplicates(newData.downstreamEdges || []);
newData.upstreamEdges = removeDuplicates(newData.upstreamEdges || []);
const childMap: EntityReferenceChild[] = getChildren(
const childMap: EntityReferenceChild[] = getLineageChildParents(
newData,
nodeSet,
obj.entity.id
obj.entity.id,
false
);
const parentsMap: EntityReferenceChild[] = getParents(
const parentsMap: EntityReferenceChild[] = getLineageChildParents(
newData,
nodeSet,
obj.entity.id
obj.entity.id,
true
);
const map: EntityReferenceChild = {
@ -1223,14 +1228,33 @@ export const getChildMap = (obj: EntityLineage) => {
export const getPaginatedChildMap = (
obj: EntityLineage,
map: EntityReferenceChild | undefined,
pagination_data: Record<string, NodeIndexMap>
pagination_data: Record<string, NodeIndexMap>,
maxLineageLength: number
) => {
const nodes = [];
const edges: EntityLineageEdge[] = [];
nodes.push(obj.entity);
if (map) {
flattenObj(obj, map, true, obj.entity.id, nodes, edges, pagination_data);
flattenObj(obj, map, false, obj.entity.id, nodes, edges, pagination_data);
flattenObj(
obj,
map,
true,
obj.entity.id,
nodes,
edges,
pagination_data,
maxLineageLength
);
flattenObj(
obj,
map,
false,
obj.entity.id,
nodes,
edges,
pagination_data,
maxLineageLength
);
}
return { nodes, edges };
@ -1243,7 +1267,8 @@ export const flattenObj = (
id: string,
nodes: EntityReference[],
edges: EntityLineageEdge[],
pagination_data: Record<string, NodeIndexMap>
pagination_data: Record<string, NodeIndexMap>,
maxLineageLength = 50
) => {
const children = downwards ? childMapObj.children : childMapObj.parents;
if (!children) {
@ -1251,8 +1276,8 @@ export const flattenObj = (
}
const startIndex =
pagination_data[id]?.[downwards ? 'downstream' : 'upstream'][0] ?? 0;
const hasMoreThanLimit = children.length > startIndex + MAX_LINEAGE_LENGTH;
const endIndex = startIndex + MAX_LINEAGE_LENGTH;
const hasMoreThanLimit = children.length > startIndex + maxLineageLength;
const endIndex = startIndex + maxLineageLength;
children.slice(0, endIndex).forEach((item) => {
if (item) {
@ -1294,49 +1319,47 @@ export const flattenObj = (
}
};
export const getChildren = (
export const getLineageChildParents = (
obj: EntityLineage,
nodeSet: Set<string>,
id: string,
isParent = false,
index = 0
) => {
const downStreamEdges = obj.downstreamEdges || [];
const filtered = downStreamEdges.filter((edge) => edge.fromEntity === id);
const edges = isParent ? obj.upstreamEdges || [] : obj.downstreamEdges || [];
const filtered = edges.filter((edge) => {
return isParent ? edge.toEntity === id : edge.fromEntity === id;
});
return filtered.reduce((childMap: EntityReferenceChild[], edge, i) => {
const node = obj.nodes?.find((node) => node.id === edge.toEntity);
const node = obj.nodes?.find((node) => {
return isParent ? node.id === edge.fromEntity : node.id === edge.toEntity;
});
if (node && !nodeSet.has(node.id)) {
nodeSet.add(node.id);
const childNodes = getChildren(obj, nodeSet, node.id, i);
childMap.push({ ...node, children: childNodes, pageIndex: index + i });
const childNodes = getLineageChildParents(
obj,
nodeSet,
node.id,
isParent,
i
);
const lineage: EntityReferenceChild = { ...node, pageIndex: index + i };
if (isParent) {
lineage.parents = childNodes;
} else {
lineage.children = childNodes;
}
childMap.push(lineage);
}
return childMap;
}, []);
};
export const getParents = (
obj: EntityLineage,
nodeSet: Set<string>,
id: string,
index = 0
) => {
const upstreamEdges = obj.upstreamEdges || [];
return upstreamEdges
.filter((edge) => edge.toEntity === id)
.reduce((childMap: EntityReferenceChild[], edge, i) => {
const node = obj.nodes?.find((node) => node.id === edge.fromEntity);
if (node && !nodeSet.has(node.id)) {
nodeSet.add(node.id);
const childNodes = getParents(obj, nodeSet, node.id, i);
childMap.push({ ...node, parents: childNodes, pageIndex: index + i });
}
return childMap;
}, []);
};
export const removeDuplicates = (arr: EntityLineageEdge[]) => {
return uniqWith(arr, isEqual);
};