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

View File

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

View File

@ -32,6 +32,7 @@ export interface EntityLineageProp {
entityType: EntityType; entityType: EntityType;
deleted?: boolean; deleted?: boolean;
hasEditAccess?: boolean; hasEditAccess?: boolean;
isFullScreen?: boolean;
} }
export interface Edge { export interface Edge {
@ -123,7 +124,9 @@ export interface ControlProps extends HTMLAttributes<HTMLDivElement> {
status: LoadingState; status: LoadingState;
zoomValue: number; zoomValue: number;
lineageData: EntityLineage; lineageData: EntityLineage;
lineageConfig: LineageConfig;
onOptionSelect: (value?: string) => void; onOptionSelect: (value?: string) => void;
onLineageConfigUpdate: (config: LineageConfig) => void;
} }
export type LineagePos = 'from' | 'to'; export type LineagePos = 'from' | 'to';
@ -151,3 +154,16 @@ export interface NodeIndexMap {
upstream: number[]; upstream: number[];
downstream: 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 [isOpen, setIsOpen] = useState<boolean>(false);
const [searchValue, setSearchValue] = useState<string>(''); const [searchValue, setSearchValue] = useState<string>('');
const getSuggestionLabel = (fqn: string, type: string, name: string) => { const getSuggestionLabelHeading = (fqn: string, type: string) => {
if (type === EntityType.TABLE) { if (type === EntityType.TABLE) {
const database = getPartialNameFromTableFQN(fqn, [FqnPart.Database]); const database = getPartialNameFromTableFQN(fqn, [FqnPart.Database]);
const schema = getPartialNameFromTableFQN(fqn, [FqnPart.Schema]); const schema = getPartialNameFromTableFQN(fqn, [FqnPart.Schema]);
return database && schema return database && schema
? `${database}${FQN_SEPARATOR_CHAR}${schema}${FQN_SEPARATOR_CHAR}${name}` ? `${database}${FQN_SEPARATOR_CHAR}${schema}`
: name; : '';
} else { } else {
return name; return '';
} }
}; };
@ -150,16 +150,15 @@ const NodeSuggestions: FC<EntitySuggestionProps> = ({
<div <div
aria-labelledby="menu-button" aria-labelledby="menu-button"
aria-orientation="vertical" aria-orientation="vertical"
className="tw-origin-top-right tw-absolute tw-z-20 className="suggestion-node-item tw-z-20
tw-w-max tw-mt-1 tw-rounded-md tw-shadow-lg 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" tw-bg-white tw-ring-1 tw-ring-black tw-ring-opacity-5 focus:tw-outline-none text-body"
role="menu"> role="menu">
{data.map((entity) => ( {data.map((entity) => (
<div <>
className="tw-flex tw-items-center hover:tw-bg-body-hover" <div
key={entity.fullyQualifiedName}> className="w-full d-flex items-center tw-px-2 tw-py-2 tw-text-sm hover:tw-bg-body-hover"
<span key={entity.fullyQualifiedName}
className="tw-block tw-px-2 tw-py-2 tw-text-sm tw-break-all"
onClick={() => { onClick={() => {
setIsOpen(false); setIsOpen(false);
onSelectHandler?.({ onSelectHandler?.({
@ -176,13 +175,20 @@ const NodeSuggestions: FC<EntitySuggestionProps> = ({
className="tw-inline tw-h-4 tw-mr-2" className="tw-inline tw-h-4 tw-mr-2"
src={serviceTypeLogo(entity.serviceType as string)} src={serviceTypeLogo(entity.serviceType as string)}
/> />
{getSuggestionLabel( <div className="flex-1 text-left tw-px-2">
entity.fullyQualifiedName, {entity.entityType === EntityType.TABLE && (
entity.entityType as string, <p className="d-block text-xs custom-lineage-heading">
entity.name {getSuggestionLabelHeading(
)} entity.fullyQualifiedName,
</span> entity.entityType as string
</div> )}
</p>
)}
<p className="">{entity.name}</p>
</div>
</div>
<hr className="tw-w-full" />
</>
))} ))}
</div> </div>
) : ( ) : (

View File

@ -86,6 +86,9 @@
.react-flow__handle { .react-flow__handle {
opacity: 0; opacity: 0;
} }
.custom-lineage-heading {
color: @white;
}
} }
} }
@ -114,6 +117,7 @@
display: flex; display: flex;
column-gap: 8px; column-gap: 8px;
height: 32px; height: 32px;
width: 150px;
} }
.custom-node-expand-button { .custom-node-expand-button {
position: absolute; position: absolute;
@ -132,3 +136,23 @@
color: #37352f; 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", "doc-plural": "Docs",
"documentation-lowercase": "documentation", "documentation-lowercase": "documentation",
"domain": "Domain", "domain": "Domain",
"downstream-depth": "Downstream Depth",
"edge-information": "Edge Information", "edge-information": "Edge Information",
"edge-lowercase": "edge", "edge-lowercase": "edge",
"edit": "Edit", "edit": "Edit",
@ -273,6 +274,7 @@
"exclude": "Exclude", "exclude": "Exclude",
"execution-date": "Execution Date", "execution-date": "Execution Date",
"execution-plural": "Executions", "execution-plural": "Executions",
"exit-fit-to-screen": "Exit fit to screen",
"expand-all": "Expand All", "expand-all": "Expand All",
"explore": "Explore", "explore": "Explore",
"explore-data": "Explore Data", "explore-data": "Explore Data",
@ -297,6 +299,7 @@
"first": "First", "first": "First",
"first-lowercase": "first", "first-lowercase": "first",
"first-quartile": "First Quartile", "first-quartile": "First Quartile",
"fit-to-screen": "Fit to screen",
"flush-interval-secs": "Flush Interval (secs)", "flush-interval-secs": "Flush Interval (secs)",
"follow": "Follow", "follow": "Follow",
"followed-lowercase": "followed", "followed-lowercase": "followed",
@ -313,6 +316,7 @@
"friday": "Friday", "friday": "Friday",
"from-lowercase": "from", "from-lowercase": "from",
"full-name": "Full name", "full-name": "Full name",
"full-screen": "Full screen",
"function": "Function", "function": "Function",
"g-chat": "G Chat", "g-chat": "G Chat",
"gcs-config-source": "GCS Config Source", "gcs-config-source": "GCS Config Source",
@ -405,6 +409,7 @@
"leave-team": "Leave Team", "leave-team": "Leave Team",
"less-lowercase": "less", "less-lowercase": "less",
"lineage": "Lineage", "lineage": "Lineage",
"lineage-config": "Lineage Config",
"lineage-ingestion": "Lineage Ingestion", "lineage-ingestion": "Lineage Ingestion",
"lineage-lowercase": "lineage", "lineage-lowercase": "lineage",
"list": "List", "list": "List",
@ -479,6 +484,7 @@
"no-parameter-available": "No Parameter Available", "no-parameter-available": "No Parameter Available",
"no-reviewer": "No reviewer", "no-reviewer": "No reviewer",
"no-tags-added": "No Tags added", "no-tags-added": "No Tags added",
"nodes-per-layer": "Nodes Per Layer",
"none": "None", "none": "None",
"not-found-lowercase": "not found", "not-found-lowercase": "not found",
"not-null": "Not Null", "not-null": "Not Null",
@ -800,6 +806,7 @@
"updated-lowercase": "updated", "updated-lowercase": "updated",
"updated-on": "Updated on", "updated-on": "Updated on",
"upload-csv-uppercase-file": "Upload CSV file", "upload-csv-uppercase-file": "Upload CSV file",
"upstream-depth": "Upstream Depth",
"url-lowercase": "url", "url-lowercase": "url",
"url-uppercase": "URL", "url-uppercase": "URL",
"usage": "Usage", "usage": "Usage",
@ -919,6 +926,8 @@
"delete-team-message": "Any teams under \"{{teamName}}\" will be {{deleteType}} deleted as well.", "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.", "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.", "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", "drag-and-drop-files-here": "Drag & drop files here",
"edit-service-entity-connection": "Edit {{entity}} Service Connection", "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.", "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-username-available": "No user available with name",
"no-users": "There are no users {{text}}", "no-users": "There are no users {{text}}",
"no-version-type-available": "No {{type}} version available", "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.", "not-followed-anything": "You have not followed anything yet.",
"om-description": "Centralized metadata store, to discover, collaborate and get your data right.", "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.", "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", "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-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.", "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.", "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).", "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.", "user-assign-new-task": "{{user}} assigned you a new task.",

View File

@ -230,6 +230,7 @@
"doc-plural": "Docs", "doc-plural": "Docs",
"documentation-lowercase": "documentation", "documentation-lowercase": "documentation",
"domain": "Domain", "domain": "Domain",
"downstream-depth": "Downstream Depth",
"edge-information": "Informations de Bord", "edge-information": "Informations de Bord",
"edge-lowercase": "edge", "edge-lowercase": "edge",
"edit": "Editer", "edit": "Editer",
@ -273,6 +274,7 @@
"exclude": "Exclure", "exclude": "Exclure",
"execution-date": "Date d'Execution", "execution-date": "Date d'Execution",
"execution-plural": "Executions", "execution-plural": "Executions",
"exit-fit-to-screen": "Exit fit to screen",
"expand-all": "Agrandir Tout", "expand-all": "Agrandir Tout",
"explore": "Explorer", "explore": "Explorer",
"explore-data": "Explore Data", "explore-data": "Explore Data",
@ -297,6 +299,7 @@
"first": "First", "first": "First",
"first-lowercase": "first", "first-lowercase": "first",
"first-quartile": "First Quartile", "first-quartile": "First Quartile",
"fit-to-screen": "Fit to screen",
"flush-interval-secs": "Flush Interval (secs)", "flush-interval-secs": "Flush Interval (secs)",
"follow": "Follow", "follow": "Follow",
"followed-lowercase": "followed", "followed-lowercase": "followed",
@ -313,6 +316,7 @@
"friday": "Friday", "friday": "Friday",
"from-lowercase": "de", "from-lowercase": "de",
"full-name": "Full name", "full-name": "Full name",
"full-screen": "Full screen",
"function": "Fonction", "function": "Fonction",
"g-chat": "G Chat", "g-chat": "G Chat",
"gcs-config-source": "GCS Config Source", "gcs-config-source": "GCS Config Source",
@ -405,6 +409,7 @@
"leave-team": "Quitter une Equipe", "leave-team": "Quitter une Equipe",
"less-lowercase": "less", "less-lowercase": "less",
"lineage": "Traçabilité", "lineage": "Traçabilité",
"lineage-config": "Lineage Config",
"lineage-ingestion": "Lineage Ingestion", "lineage-ingestion": "Lineage Ingestion",
"lineage-lowercase": "lineage", "lineage-lowercase": "lineage",
"list": "List", "list": "List",
@ -479,6 +484,7 @@
"no-parameter-available": "No Parameter Available", "no-parameter-available": "No Parameter Available",
"no-reviewer": "No reviewer", "no-reviewer": "No reviewer",
"no-tags-added": "No Tags added", "no-tags-added": "No Tags added",
"nodes-per-layer": "Nodes Per Layer",
"none": "None", "none": "None",
"not-found-lowercase": "not found", "not-found-lowercase": "not found",
"not-null": "Not Null", "not-null": "Not Null",
@ -800,6 +806,7 @@
"updated-lowercase": "updated", "updated-lowercase": "updated",
"updated-on": "Updated on", "updated-on": "Updated on",
"upload-csv-uppercase-file": "Upload CSV file", "upload-csv-uppercase-file": "Upload CSV file",
"upstream-depth": "Upstream Depth",
"url-lowercase": "url", "url-lowercase": "url",
"url-uppercase": "URL", "url-uppercase": "URL",
"usage": "Usage", "usage": "Usage",
@ -919,6 +926,8 @@
"delete-team-message": "Any teams under \"{{teamName}}\" will be {{deleteType}} deleted as well.", "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.", "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", "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", "drag-and-drop-files-here": "Drag & drop files here",
"edit-service-entity-connection": "Edit {{entity}} Service Connection", "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.", "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-username-available": "No user available with name",
"no-users": "Il n'y a pas d'utilisteurs {{text}}", "no-users": "Il n'y a pas d'utilisteurs {{text}}",
"no-version-type-available": "No {{type}} version available", "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.", "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", "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.", "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", "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-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.", "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.", "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).", "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.", "user-assign-new-task": "{{user}} assigned you a new task.",

View File

@ -230,6 +230,7 @@
"doc-plural": "文档", "doc-plural": "文档",
"documentation-lowercase": "documentation", "documentation-lowercase": "documentation",
"domain": "域", "domain": "域",
"downstream-depth": "Downstream Depth",
"edge-information": "边缘信息", "edge-information": "边缘信息",
"edge-lowercase": "edge", "edge-lowercase": "edge",
"edit": "编辑", "edit": "编辑",
@ -273,6 +274,7 @@
"exclude": "排除", "exclude": "排除",
"execution-date": "执行日期", "execution-date": "执行日期",
"execution-plural": "执行", "execution-plural": "执行",
"exit-fit-to-screen": "Exit fit to screen",
"expand-all": "全部展开", "expand-all": "全部展开",
"explore": "Explore", "explore": "Explore",
"explore-data": "Explore Data", "explore-data": "Explore Data",
@ -297,6 +299,7 @@
"first": "First", "first": "First",
"first-lowercase": "first", "first-lowercase": "first",
"first-quartile": "First Quartile", "first-quartile": "First Quartile",
"fit-to-screen": "Fit to screen",
"flush-interval-secs": "刷新间隔 (secs)", "flush-interval-secs": "刷新间隔 (secs)",
"follow": "Follow", "follow": "Follow",
"followed-lowercase": "被关注", "followed-lowercase": "被关注",
@ -313,6 +316,7 @@
"friday": "Friday", "friday": "Friday",
"from-lowercase": "从", "from-lowercase": "从",
"full-name": "Full name", "full-name": "Full name",
"full-screen": "Full screen",
"function": "函数", "function": "函数",
"g-chat": "G Chat", "g-chat": "G Chat",
"gcs-config-source": "GCS Config Source", "gcs-config-source": "GCS Config Source",
@ -405,6 +409,7 @@
"leave-team": "Leave team", "leave-team": "Leave team",
"less-lowercase": "less", "less-lowercase": "less",
"lineage": "血源", "lineage": "血源",
"lineage-config": "Lineage Config",
"lineage-ingestion": "Lineage Ingestion", "lineage-ingestion": "Lineage Ingestion",
"lineage-lowercase": "lineage", "lineage-lowercase": "lineage",
"list": "列表", "list": "列表",
@ -479,6 +484,7 @@
"no-parameter-available": "No Parameter Available", "no-parameter-available": "No Parameter Available",
"no-reviewer": "No reviewer", "no-reviewer": "No reviewer",
"no-tags-added": "No Tags added", "no-tags-added": "No Tags added",
"nodes-per-layer": "Nodes Per Layer",
"none": "None", "none": "None",
"not-found-lowercase": "not found", "not-found-lowercase": "not found",
"not-null": "Not null", "not-null": "Not null",
@ -800,6 +806,7 @@
"updated-lowercase": "updated", "updated-lowercase": "updated",
"updated-on": "Updated on", "updated-on": "Updated on",
"upload-csv-uppercase-file": "Upload CSV file", "upload-csv-uppercase-file": "Upload CSV file",
"upstream-depth": "Upstream Depth",
"url-lowercase": "url", "url-lowercase": "url",
"url-uppercase": "URL", "url-uppercase": "URL",
"usage": "使用", "usage": "使用",
@ -919,6 +926,8 @@
"delete-team-message": "Any teams under \"{{teamName}}\" will be {{deleteType}} deleted as well.", "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.", "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", "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", "drag-and-drop-files-here": "Drag & drop files here",
"edit-service-entity-connection": "Edit {{entity}} Service Connection", "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.", "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-username-available": "No user available with name",
"no-users": "There are no users {{text}}", "no-users": "There are no users {{text}}",
"no-version-type-available": "No {{type}} version available", "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.", "not-followed-anything": "You have not followed anything yet.",
"om-description": "centralized metadata store, discover, collaborate and get your data right", "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.", "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", "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-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.", "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.", "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).", "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.", "user-assign-new-task": "{{user}} assigned you a new task.",

View File

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

View File

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