From 39fc6f86e17a1c86bef55de802eccc421f068fc5 Mon Sep 17 00:00:00 2001 From: karanh37 <33024356+karanh37@users.noreply.github.com> Date: Thu, 16 Mar 2023 18:38:09 +0530 Subject: [PATCH] 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 --- .../CustomControls.component.tsx | 242 ++++++++++-------- .../EntityLineage/EntityLineage.component.tsx | 98 ++++--- .../EntityLineage/EntityLineage.interface.ts | 16 ++ .../EntityLineage/LineageConfigModal.test.tsx | 66 +++++ .../EntityLineage/LineageConfigModal.tsx | 121 +++++++++ .../NodeSuggestions.component.tsx | 42 +-- .../EntityLineage/entityLineage.style.less | 24 ++ .../ui/src/locale/languages/en-us.json | 13 + .../ui/src/locale/languages/fr-fr.json | 13 + .../ui/src/locale/languages/zh-cn.json | 13 + .../ui/src/pages/LineagePage/LineagePage.tsx | 6 +- .../ui/src/utils/EntityLineageUtils.tsx | 109 ++++---- 12 files changed, 563 insertions(+), 200 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/LineageConfigModal.test.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/LineageConfigModal.tsx diff --git a/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/CustomControls.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/CustomControls.component.tsx index aff0d0e4973..afc4b80f9d0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/CustomControls.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/CustomControls.component.tsx @@ -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> = ({ children, @@ -69,11 +71,14 @@ const CustomControls: FC = ({ status, zoomValue, lineageData, + lineageConfig, onOptionSelect, + onLineageConfigUpdate, }: ControlProps) => { const { t } = useTranslation(); const { fitView, zoomTo } = useReactFlow(); const [zoom, setZoom] = useState(zoomValue); + const [dialogVisible, setDialogVisible] = useState(false); const onZoomHandler = useCallback( (zoomLevel: number) => { @@ -146,124 +151,151 @@ const CustomControls: FC = ({ ); }, [isEditMode]); + const handleDialogSave = useCallback( + (config: LineageConfig) => { + onLineageConfigUpdate(config); + setDialogVisible(false); + }, + [onLineageConfigUpdate, setDialogVisible] + ); + return ( - - - + + + + - {showZoom && ( -
+ {showZoom && ( +
+ + + + + + + + +
+ )} + {showFitView && ( + className="custom-control-basic-button custom-control-fit-screen-button" + title={t('label.fit-to-screen')} + onClick={onFitViewHandler}> + + + )} + {handleFullScreenViewClick && ( + - - + )} + {onExitFullScreenViewClick && ( + className="custom-control-basic-button custom-control-fit-screen-button" + title={t('label.exit-fit-to-screen')} + onClick={onExitFullScreenViewClick}> -
- )} - {showFitView && ( + )} + - + title={t('label.setting-plural')} + onClick={() => setDialogVisible(true)}> + - )} - {handleFullScreenViewClick && ( - - - - )} - {onExitFullScreenViewClick && ( - - - - )} - {!deleted && ( - - {getLoadingStatusValue(editIcon, loading, status)} - - )} -
- -
+ onClick={onEditLinageClick}> + {getLoadingStatusValue(editIcon, loading, status)} + + )} + + + + setDialogVisible(false)} + onSave={handleDialogSave} + /> + ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/EntityLineage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/EntityLineage.component.tsx index 9ce5fc73ee4..0b01f467c9f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/EntityLineage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/EntityLineage.component.tsx @@ -134,6 +134,7 @@ import { EntityLineageProp, EntityReferenceChild, LeafNodes, + LineageConfig, LineagePos, LoadingNodeState, ModifiedColumn, @@ -150,6 +151,7 @@ const EntityLineageComponent: FunctionComponent = ({ deleted, hasEditAccess, entityType, + isFullScreen = false, }: EntityLineageProp) => { const { t } = useTranslation(); const reactFlowWrapper = useRef(null); @@ -203,6 +205,11 @@ const EntityLineageComponent: FunctionComponent = ({ state: false, }); const [leafNodes, setLeafNodes] = useState({} as LeafNodes); + const [lineageConfig, setLineageConfig] = useState({ + upstreamDepth: 3, + downstreamDepth: 3, + nodesPerLayer: 50, + }); const params = useParams>(); const entityFQN = @@ -213,21 +220,34 @@ const EntityLineageComponent: FunctionComponent = ({ 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 = ({ * 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 = ({ /** * 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 = ({ 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, - edge: Edge - ) => { - setSelectedEdgeInfo(edge); - setIsDrawerOpen(true); - }; + const handleEdgeClick = useCallback( + (_e: React.MouseEvent, edge: Edge) => { + setSelectedEdgeInfo(edge); + setIsDrawerOpen(true); + }, + [] + ); const toggleColumnView = (value: boolean) => { setExpandAllColumns(value); @@ -1409,6 +1429,10 @@ const EntityLineageComponent: FunctionComponent = ({ } }; + 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 = ({ const { nodes, edges } = getPaginatedChildMap( updatedLineageData, childMap, - paginationData + paginationData, + lineageConfig.nodesPerLayer ); const newNodes = union(nodes, path); setElementsHandle( @@ -1498,7 +1523,8 @@ const EntityLineageComponent: FunctionComponent = ({ const { nodes: newNodes, edges } = getPaginatedChildMap( lineageData, childMapObj, - paginationObj + paginationObj, + lineageConfig.nodesPerLayer ); setElementsHandle({ ...lineageData, @@ -1509,7 +1535,7 @@ const EntityLineageComponent: FunctionComponent = ({ }; useEffect(() => { - fetchLineageData(); + fetchLineageData(lineageConfig); }, []); useEffect(() => { @@ -1614,17 +1640,23 @@ const EntityLineageComponent: FunctionComponent = ({ 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} /> )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/EntityLineage.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/EntityLineage.interface.ts index aa1a6ae1a82..6a276e871d6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/EntityLineage.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/EntityLineage.interface.ts @@ -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 { 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; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/LineageConfigModal.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/LineageConfigModal.test.tsx new file mode 100644 index 00000000000..b7a3e84f554 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/LineageConfigModal.test.tsx @@ -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( + + ); + + 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( + + ); + + fireEvent.click(screen.getByRole('button', { name: 'Cancel' })); + + expect(onCancel).toHaveBeenCalledTimes(1); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/LineageConfigModal.tsx b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/LineageConfigModal.tsx new file mode 100644 index 00000000000..ac0cb10b563 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/LineageConfigModal.tsx @@ -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) => ( + + {value} + +)); + +const LineageConfigModal: React.FC = ({ + visible, + config, + onCancel, + onSave, +}) => { + const { t } = useTranslation(); + const [form] = Form.useForm(); + const [upstreamDepth, setUpstreamDepth] = useState( + config.upstreamDepth || 1 + ); + const [downstreamDepth, setDownstreamDepth] = useState( + config.downstreamDepth || 1 + ); + const [nodesPerLayer, setNodesPerLayer] = useState( + config.nodesPerLayer || 1 + ); + + const handleSave = () => { + const updatedConfig: LineageConfig = { + upstreamDepth, + downstreamDepth, + nodesPerLayer, + }; + onSave(updatedConfig); + }; + + return ( + +
+ + + + + + + + + + setNodesPerLayer(value as number)} + /> + +
+
+ ); +}; + +export default LineageConfigModal; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/NodeSuggestions.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/NodeSuggestions.component.tsx index 3a411650d6a..ed95a216814 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/NodeSuggestions.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/NodeSuggestions.component.tsx @@ -50,16 +50,16 @@ const NodeSuggestions: FC = ({ const [isOpen, setIsOpen] = useState(false); const [searchValue, setSearchValue] = useState(''); - 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 = ({
{data.map((entity) => ( -
- +
{ setIsOpen(false); onSelectHandler?.({ @@ -176,13 +175,20 @@ const NodeSuggestions: FC = ({ className="tw-inline tw-h-4 tw-mr-2" src={serviceTypeLogo(entity.serviceType as string)} /> - {getSuggestionLabel( - entity.fullyQualifiedName, - entity.entityType as string, - entity.name - )} - -
+
+ {entity.entityType === EntityType.TABLE && ( +

+ {getSuggestionLabelHeading( + entity.fullyQualifiedName, + entity.entityType as string + )} +

+ )} +

{entity.name}

+
+
+
+ ))}
) : ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/entityLineage.style.less b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/entityLineage.style.less index df1d824a2eb..e6ea6cfebc6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/entityLineage.style.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/EntityLineage/entityLineage.style.less @@ -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; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json index 1abf0fe140b..a885c4853f1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json @@ -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 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.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json index 65668007158..ca8d4ccd931 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json @@ -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 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.", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json index 72ec9af8780..d6b4dde237d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json @@ -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 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.", diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/LineagePage/LineagePage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/LineagePage/LineagePage.tsx index 9bcf047883f..ae630ae762b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/LineagePage/LineagePage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/LineagePage/LineagePage.tsx @@ -202,7 +202,11 @@ const LineagePage = () => {
- +
diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.tsx index bb3971ebd02..77358257397 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.tsx @@ -497,11 +497,14 @@ export const getDataLabel = ( - {type === 'table' - ? databaseName && schemaName - ? `${databaseName}${FQN_SEPARATOR_CHAR}${schemaName}${FQN_SEPARATOR_CHAR}${label}` - : label - : label} + {type === 'table' && databaseName && schemaName ? ( + + {databaseName} + {FQN_SEPARATOR_CHAR} + {schemaName} + + ) : null} + {label} ); } @@ -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 + pagination_data: Record, + 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 + pagination_data: Record, + 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, 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, - 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); };