mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-02 03:29:03 +00:00
fix: lineage full screen view (#12473)
* fix: remove redirection to separate page * fix: remove unused lineage page component * fix: position full screen container * fix: add unit tests * fix: add cypress for full screen toggle * fix: lineage style issue * fix: address comments
This commit is contained in:
parent
940ab3d183
commit
6b46f4e1cb
@ -48,3 +48,38 @@ describe('Entity Details Page', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Lineage functionality', () => {
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it('toggle fullscreen mode', () => {
|
||||
visitEntityDetailsPage(
|
||||
tableEntity.term,
|
||||
tableEntity.serviceName,
|
||||
tableEntity.entity
|
||||
);
|
||||
cy.get('[data-testid="lineage"]').click();
|
||||
|
||||
// Enable fullscreen
|
||||
cy.get('[data-testid="full-screen"]').click();
|
||||
cy.url().should('include', 'fullscreen=true');
|
||||
cy.get('[data-testid="breadcrumb"]')
|
||||
.should('be.visible')
|
||||
.and('contain', 'Lineage');
|
||||
cy.get('[data-testid="lineage-details"]').should(
|
||||
'have.class',
|
||||
'full-screen-lineage'
|
||||
);
|
||||
|
||||
// Exit fullscreen
|
||||
cy.get('[data-testid="exit-full-screen"]').click();
|
||||
cy.url().should('not.include', 'fullscreen=true');
|
||||
cy.get('[data-testid="breadcrumb"]').should('not.contain', 'Lineage');
|
||||
cy.get('[data-testid="lineage-details"]').should(
|
||||
'not.have.class',
|
||||
'full-screen-lineage'
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -11,7 +11,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Card, Col, Row, Space, Table, Tabs, Typography } from 'antd';
|
||||
import { Col, Row, Space, Table, Tabs, Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import { AxiosError } from 'axios';
|
||||
import ActivityFeedProvider, {
|
||||
@ -661,14 +661,13 @@ const DashboardDetails = ({
|
||||
label: <TabsLabel id={EntityTabs.LINEAGE} name={t('label.lineage')} />,
|
||||
key: EntityTabs.LINEAGE,
|
||||
children: (
|
||||
<Card className="lineage-card card-body-full w-auto border-none">
|
||||
<EntityLineageComponent
|
||||
entityType={EntityType.DASHBOARD}
|
||||
hasEditAccess={
|
||||
dashboardPermissions.EditAll || dashboardPermissions.EditLineage
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
<EntityLineageComponent
|
||||
entity={dashboardDetails}
|
||||
entityType={EntityType.DASHBOARD}
|
||||
hasEditAccess={
|
||||
dashboardPermissions.EditAll || dashboardPermissions.EditLineage
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@ -23,6 +23,7 @@ import { DataAssetsHeader } from 'components/DataAssets/DataAssetsHeader/DataAss
|
||||
import EntityLineageComponent from 'components/EntityLineage/EntityLineage.component';
|
||||
import { EntityName } from 'components/Modals/EntityNameModal/EntityNameModal.interface';
|
||||
import SchemaEditor from 'components/schema-editor/SchemaEditor';
|
||||
import { SourceType } from 'components/searched-data/SearchedData.interface';
|
||||
import TabsLabel from 'components/TabsLabel/TabsLabel.component';
|
||||
import TagsContainerV2 from 'components/Tag/TagsContainerV2/TagsContainerV2';
|
||||
import { getDataModelDetailsPath, getVersionPath } from 'constants/constants';
|
||||
@ -313,15 +314,12 @@ const DataModelDetails = ({
|
||||
),
|
||||
key: EntityTabs.LINEAGE,
|
||||
children: (
|
||||
<Card
|
||||
className="card-body-full m-md w-auto h-60vh"
|
||||
data-testid="lineage-details">
|
||||
<EntityLineageComponent
|
||||
deleted={deleted}
|
||||
entityType={EntityType.DASHBOARD_DATA_MODEL}
|
||||
hasEditAccess={hasEditLineagePermission}
|
||||
/>
|
||||
</Card>
|
||||
<EntityLineageComponent
|
||||
deleted={deleted}
|
||||
entity={dataModelData as SourceType}
|
||||
entityType={EntityType.DASHBOARD_DATA_MODEL}
|
||||
hasEditAccess={hasEditLineagePermission}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@ -175,6 +175,7 @@ const CustomControls: FC<ControlProps> = ({
|
||||
className={classNames('custom-control-search-box', {
|
||||
'custom-control-search-box-edit-mode': isEditMode,
|
||||
})}
|
||||
data-testid="lineage-search"
|
||||
filterOption={handleSearchFilterOption}
|
||||
options={nodeOptions}
|
||||
placeholder={t('label.search-entity', {
|
||||
@ -200,6 +201,7 @@ const CustomControls: FC<ControlProps> = ({
|
||||
<div className="flow-control custom-control-fit-screen-button custom-control-zoom-slide">
|
||||
<ControlButton
|
||||
className="custom-control-basic-button"
|
||||
data-testid="zoom-in-button"
|
||||
onClick={onZoomOutHandler}>
|
||||
<SVGIcons
|
||||
alt="minus-icon"
|
||||
@ -211,6 +213,7 @@ const CustomControls: FC<ControlProps> = ({
|
||||
|
||||
<input
|
||||
className="tw-bg-body-hover"
|
||||
data-testid="lineage-zoom-slider"
|
||||
max={MAX_ZOOM_VALUE}
|
||||
min={MIN_ZOOM_VALUE}
|
||||
step={ZOOM_SLIDER_STEP}
|
||||
@ -220,6 +223,7 @@ const CustomControls: FC<ControlProps> = ({
|
||||
/>
|
||||
<ControlButton
|
||||
className="custom-control-basic-button"
|
||||
data-testid="zoom-out-button"
|
||||
onClick={onZoomInHandler}>
|
||||
<SVGIcons
|
||||
alt="plus-icon"
|
||||
@ -233,6 +237,7 @@ const CustomControls: FC<ControlProps> = ({
|
||||
{showFitView && (
|
||||
<ControlButton
|
||||
className="custom-control-basic-button custom-control-fit-screen-button"
|
||||
data-testid="fit-to-screen"
|
||||
title={t('label.fit-to-screen')}
|
||||
onClick={onFitViewHandler}>
|
||||
<SVGIcons alt="fit-view" icon={Icons.FITVEW} width="16" />
|
||||
@ -241,6 +246,7 @@ const CustomControls: FC<ControlProps> = ({
|
||||
{handleFullScreenViewClick && (
|
||||
<ControlButton
|
||||
className="custom-control-basic-button custom-control-fit-screen-button"
|
||||
data-testid="full-screen"
|
||||
title={t('label.full-screen')}
|
||||
onClick={handleFullScreenViewClick}>
|
||||
<FullScreen color={PRIMERY_COLOR} height={16} width={16} />
|
||||
@ -249,6 +255,7 @@ const CustomControls: FC<ControlProps> = ({
|
||||
{onExitFullScreenViewClick && (
|
||||
<ControlButton
|
||||
className="custom-control-basic-button custom-control-fit-screen-button"
|
||||
data-testid="exit-full-screen"
|
||||
title={t('label.exit-fit-to-screen')}
|
||||
onClick={onExitFullScreenViewClick}>
|
||||
<ExitFullScreen color={PRIMERY_COLOR} height={16} width={16} />
|
||||
@ -257,6 +264,7 @@ const CustomControls: FC<ControlProps> = ({
|
||||
|
||||
<ControlButton
|
||||
className="custom-control-basic-button custom-control-fit-screen-button"
|
||||
data-testid="lineage-config"
|
||||
disabled={isEditMode}
|
||||
title={t('label.setting-plural')}
|
||||
onClick={() => setDialogVisible(true)}>
|
||||
@ -270,7 +278,7 @@ const CustomControls: FC<ControlProps> = ({
|
||||
className={classNames(
|
||||
'custom-control-edit-button h-8 w-8 rounded-full p-x-xss',
|
||||
{
|
||||
'bg-primary': isEditMode,
|
||||
active: isEditMode,
|
||||
}
|
||||
)}
|
||||
data-testid="edit-lineage"
|
||||
|
||||
@ -0,0 +1,163 @@
|
||||
/*
|
||||
* 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 } from '@testing-library/react';
|
||||
import { LOADING_STATE } from 'enums/common.enum';
|
||||
import { MOCK_LINEAGE_DATA } from 'mocks/Lineage.mock';
|
||||
import React from 'react';
|
||||
import CustomControlsComponent from './CustomControls.component';
|
||||
|
||||
const mockFitView = jest.fn();
|
||||
const mockZoomTo = jest.fn();
|
||||
const mockOnOptionSelect = jest.fn();
|
||||
const mockOnLineageConfigUpdate = jest.fn();
|
||||
const mockOnEditLinageClick = jest.fn();
|
||||
const mockOnExpandColumnClick = jest.fn();
|
||||
const mockHandleFullScreenViewClick = jest.fn();
|
||||
const mockOnExitFullScreenViewClick = jest.fn();
|
||||
const mockOnZoomHandler = jest.fn();
|
||||
const mockZoomValue = 1;
|
||||
|
||||
jest.mock('reactflow', () => ({
|
||||
useReactFlow: () => ({
|
||||
fitView: mockFitView,
|
||||
zoomTo: mockZoomTo,
|
||||
}),
|
||||
Position: () => ({
|
||||
Left: 'left',
|
||||
Top: 'top',
|
||||
Right: 'right',
|
||||
Bottom: 'bottom',
|
||||
}),
|
||||
MarkerType: () => ({
|
||||
Arrow: 'arrow',
|
||||
ArrowClosed: 'arrowclosed',
|
||||
}),
|
||||
}));
|
||||
|
||||
const customProps = {
|
||||
fitView: mockFitView,
|
||||
zoomTo: mockZoomTo,
|
||||
onOptionSelect: mockOnOptionSelect,
|
||||
onLineageConfigUpdate: mockOnLineageConfigUpdate,
|
||||
onEditLinageClick: mockOnEditLinageClick,
|
||||
onExpandColumnClick: mockOnExpandColumnClick,
|
||||
handleFullScreenViewClick: mockHandleFullScreenViewClick,
|
||||
onExitFullScreenViewClick: mockOnExitFullScreenViewClick,
|
||||
onZoomHandler: mockOnZoomHandler,
|
||||
zoomValue: mockZoomValue,
|
||||
deleted: false,
|
||||
hasEditAccess: true,
|
||||
isEditMode: false,
|
||||
lineageData: MOCK_LINEAGE_DATA,
|
||||
isColumnsExpanded: false,
|
||||
loading: false,
|
||||
status: LOADING_STATE.INITIAL,
|
||||
lineageConfig: {
|
||||
upstreamDepth: 1,
|
||||
downstreamDepth: 1,
|
||||
nodesPerLayer: 50,
|
||||
},
|
||||
};
|
||||
|
||||
describe('CustomControls', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('calls fitView on Fit View button click', () => {
|
||||
const { getByTestId } = render(
|
||||
<CustomControlsComponent {...customProps} />
|
||||
);
|
||||
const fitViewButton = getByTestId('fit-to-screen');
|
||||
fireEvent.click(fitViewButton);
|
||||
|
||||
expect(mockFitView).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls zoomTo with zoomInValue on Zoom In button click', () => {
|
||||
const { getByTestId } = render(
|
||||
<CustomControlsComponent {...customProps} />
|
||||
);
|
||||
const zoomInButton = getByTestId('zoom-in-button');
|
||||
fireEvent.click(zoomInButton);
|
||||
const zoomRangeInput = getByTestId(
|
||||
'lineage-zoom-slider'
|
||||
) as HTMLInputElement;
|
||||
|
||||
expect(zoomRangeInput.value).toBe('0.75');
|
||||
});
|
||||
|
||||
it('calls zoomTo with zoomOutValue on Zoom Out button click', () => {
|
||||
const { getByTestId } = render(
|
||||
<CustomControlsComponent {...customProps} />
|
||||
);
|
||||
const zoomOutButton = getByTestId('zoom-out-button');
|
||||
fireEvent.click(zoomOutButton);
|
||||
const zoomRangeInput = getByTestId(
|
||||
'lineage-zoom-slider'
|
||||
) as HTMLInputElement;
|
||||
|
||||
expect(zoomRangeInput.value).toBe('1.25');
|
||||
});
|
||||
|
||||
it('calls onEditLinageClick on Edit Lineage button click', () => {
|
||||
const { getByTestId } = render(
|
||||
<CustomControlsComponent {...customProps} />
|
||||
);
|
||||
const editLineageButton = getByTestId('edit-lineage');
|
||||
fireEvent.click(editLineageButton);
|
||||
|
||||
expect(mockOnEditLinageClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onExpandColumnClick on Expand Column button click', () => {
|
||||
const { getByTestId } = render(
|
||||
<CustomControlsComponent {...customProps} />
|
||||
);
|
||||
const expandColumnButton = getByTestId('expand-column');
|
||||
fireEvent.click(expandColumnButton);
|
||||
|
||||
expect(mockOnExpandColumnClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls mockHandleFullScreenViewClick on Full Screen button click', () => {
|
||||
const { getByTestId } = render(
|
||||
<CustomControlsComponent {...customProps} />
|
||||
);
|
||||
const fullScreenButton = getByTestId('full-screen');
|
||||
fireEvent.click(fullScreenButton);
|
||||
|
||||
expect(mockHandleFullScreenViewClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls mockOnExitFullScreenViewClick on Exit Full Screen button click', () => {
|
||||
const { getByTestId } = render(
|
||||
<CustomControlsComponent {...customProps} />
|
||||
);
|
||||
const exitFullScreenButton = getByTestId('exit-full-screen');
|
||||
fireEvent.click(exitFullScreenButton);
|
||||
|
||||
expect(mockOnExitFullScreenViewClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show lineage config dialog on setting button click', () => {
|
||||
const { getByTestId, getByRole } = render(
|
||||
<CustomControlsComponent {...customProps} />
|
||||
);
|
||||
const settingButton = getByTestId('lineage-config');
|
||||
fireEvent.click(settingButton);
|
||||
const dialog = getByRole('dialog');
|
||||
|
||||
expect(dialog).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@ -11,8 +11,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Modal, Space } from 'antd';
|
||||
import { Card, Modal, Space } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import classNames from 'classnames';
|
||||
import TitleBreadcrumb from 'components/common/title-breadcrumb/title-breadcrumb.component';
|
||||
import { useTourProvider } from 'components/TourProvider/TourProvider';
|
||||
import { mockDatasetData } from 'constants/mockTourData.constants';
|
||||
import {
|
||||
@ -25,16 +27,18 @@ import {
|
||||
upperCase,
|
||||
} from 'lodash';
|
||||
import { LoadingState } from 'Models';
|
||||
import Qs from 'qs';
|
||||
import React, {
|
||||
DragEvent,
|
||||
FunctionComponent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { useHistory, useLocation, useParams } from 'react-router-dom';
|
||||
import ReactFlow, {
|
||||
addEdge,
|
||||
Background,
|
||||
@ -52,8 +56,11 @@ import { getDataModelDetails } from 'rest/dataModelsAPI';
|
||||
import { getLineageByFQN } from 'rest/lineageAPI';
|
||||
import { searchData } from 'rest/miscAPI';
|
||||
import { getTableDetails } from 'rest/tableAPI';
|
||||
import { getEntityLineage, getEntityName } from 'utils/EntityUtils';
|
||||
import { getLineageViewPath } from 'utils/RouterUtils';
|
||||
import {
|
||||
getEntityBreadcrumbs,
|
||||
getEntityLineage,
|
||||
getEntityName,
|
||||
} from 'utils/EntityUtils';
|
||||
import { PAGE_SIZE } from '../../constants/constants';
|
||||
import {
|
||||
ELEMENT_DELETE_STATE,
|
||||
@ -90,7 +97,6 @@ import {
|
||||
getDeletedLineagePlaceholder,
|
||||
getEdgeStyle,
|
||||
getEdgeType,
|
||||
getEntityLineagePath,
|
||||
getEntityNodeIcon,
|
||||
getLayoutedElements,
|
||||
getLineageData,
|
||||
@ -150,9 +156,10 @@ const EntityLineageComponent: FunctionComponent<EntityLineageProp> = ({
|
||||
deleted,
|
||||
hasEditAccess,
|
||||
entityType,
|
||||
isFullScreen = false,
|
||||
entity,
|
||||
}: EntityLineageProp) => {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const { isTourOpen } = useTourProvider();
|
||||
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||
const [reactFlowInstance, setReactFlowInstance] =
|
||||
@ -213,14 +220,34 @@ const EntityLineageComponent: FunctionComponent<EntityLineageProp> = ({
|
||||
});
|
||||
|
||||
const params = useParams<Record<string, string>>();
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
|
||||
const isFullScreen = queryParams.get('fullscreen') === 'true';
|
||||
const entityFQN =
|
||||
params[getParamByEntityType(entityType)] ?? params['entityFQN'];
|
||||
const history = useHistory();
|
||||
|
||||
const onFullScreenClick = useCallback(() => {
|
||||
history.push(getLineageViewPath(entityType, entityFQN));
|
||||
history.push({
|
||||
search: Qs.stringify({ fullscreen: true }),
|
||||
});
|
||||
}, [entityType, entityFQN]);
|
||||
|
||||
const breadcrumbs = useMemo(
|
||||
() =>
|
||||
entity
|
||||
? [
|
||||
...getEntityBreadcrumbs(entity, entityType),
|
||||
{
|
||||
name: t('label.lineage'),
|
||||
url: '',
|
||||
activeTitle: true,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
[entity]
|
||||
);
|
||||
|
||||
const fetchLineageData = useCallback(
|
||||
async (config: LineageConfig) => {
|
||||
if (isTourOpen) {
|
||||
@ -311,10 +338,9 @@ const EntityLineageComponent: FunctionComponent<EntityLineageProp> = ({
|
||||
);
|
||||
|
||||
const onExitFullScreenViewClick = useCallback(() => {
|
||||
const path = getEntityLineagePath(entityType, entityFQN);
|
||||
if (path !== '') {
|
||||
history.push(path);
|
||||
}
|
||||
history.push({
|
||||
search: '',
|
||||
});
|
||||
}, [entityType, entityFQN, history]);
|
||||
|
||||
/**
|
||||
@ -1587,128 +1613,140 @@ const EntityLineageComponent: FunctionComponent<EntityLineageProp> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative h-full" data-testid="lineage-container">
|
||||
<div className="w-full h-full" ref={reactFlowWrapper}>
|
||||
<ReactFlowProvider>
|
||||
<ReactFlow
|
||||
onlyRenderVisibleElements
|
||||
className="custom-react-flow"
|
||||
data-testid="react-flow-component"
|
||||
edgeTypes={customEdges}
|
||||
edges={edges}
|
||||
maxZoom={MAX_ZOOM_VALUE}
|
||||
minZoom={MIN_ZOOM_VALUE}
|
||||
nodeTypes={nodeTypes}
|
||||
nodes={nodes}
|
||||
nodesConnectable={isEditMode}
|
||||
selectNodesOnDrag={false}
|
||||
onConnect={onConnect}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={onDrop}
|
||||
onEdgeClick={handleEdgeClick}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onInit={(reactFlowInstance: ReactFlowInstance) => {
|
||||
onLoad(reactFlowInstance);
|
||||
setReactFlowInstance(reactFlowInstance);
|
||||
}}
|
||||
onMove={(_e, viewPort) => handleZoomLevel(viewPort.zoom)}
|
||||
onNodeClick={(_e, node) => {
|
||||
onNodeClick(node);
|
||||
_e.stopPropagation();
|
||||
}}
|
||||
onNodeContextMenu={onNodeContextMenu}
|
||||
onNodeDrag={dragHandle}
|
||||
onNodeDragStart={dragHandle}
|
||||
onNodeDragStop={dragHandle}
|
||||
onNodeMouseEnter={onNodeMouseEnter}
|
||||
onNodeMouseLeave={onNodeMouseLeave}
|
||||
onNodeMouseMove={onNodeMouseMove}
|
||||
onNodesChange={onNodesChange}
|
||||
onPaneClick={onPaneClick}>
|
||||
{updatedLineageData && (
|
||||
<CustomControlsComponent
|
||||
className="absolute top-1 right-1 bottom-full p-md"
|
||||
deleted={deleted}
|
||||
fitViewParams={{
|
||||
minZoom: MIN_ZOOM_VALUE,
|
||||
maxZoom: MAX_ZOOM_VALUE,
|
||||
}}
|
||||
handleFullScreenViewClick={
|
||||
!isFullScreen ? onFullScreenClick : undefined
|
||||
}
|
||||
hasEditAccess={hasEditAccess}
|
||||
isColumnsExpanded={expandAllColumns}
|
||||
isEditMode={isEditMode}
|
||||
lineageConfig={lineageConfig}
|
||||
lineageData={updatedLineageData}
|
||||
loading={loading}
|
||||
status={status}
|
||||
zoomValue={zoomValue}
|
||||
onEditLinageClick={handleEditLineageClick}
|
||||
onExitFullScreenViewClick={
|
||||
isFullScreen ? onExitFullScreenViewClick : undefined
|
||||
}
|
||||
onExpandColumnClick={handleExpandColumnClick}
|
||||
onLineageConfigUpdate={handleLineageConfigUpdate}
|
||||
onOptionSelect={handleOptionSelect}
|
||||
/>
|
||||
)}
|
||||
<Background gap={12} size={1} />
|
||||
</ReactFlow>
|
||||
</ReactFlowProvider>
|
||||
</div>
|
||||
{isDrawerOpen &&
|
||||
!isEditMode &&
|
||||
(selectedEdgeInfo ? (
|
||||
<EdgeInfoDrawer
|
||||
edge={selectedEdgeInfo}
|
||||
nodes={nodes}
|
||||
visible={isDrawerOpen}
|
||||
onClose={() => {
|
||||
setIsDrawerOpen(false);
|
||||
setSelectedEdgeInfo(undefined);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<EntityInfoDrawer
|
||||
isMainNode={selectedNode.name === updatedLineageData?.entity?.name}
|
||||
selectedNode={selectedNode}
|
||||
show={isDrawerOpen}
|
||||
onCancel={closeDrawer}
|
||||
/>
|
||||
))}
|
||||
<EntityLineageSidebar newAddedNode={newAddedNode} show={isEditMode} />
|
||||
{showDeleteModal && (
|
||||
<Modal
|
||||
maskClosable={false}
|
||||
okText={getLoadingStatusValue(
|
||||
t('label.confirm'),
|
||||
deletionState.loading,
|
||||
deletionState.status
|
||||
)}
|
||||
open={showDeleteModal}
|
||||
title={t('message.remove-lineage-edge')}
|
||||
onCancel={() => {
|
||||
setShowDeleteModal(false);
|
||||
}}
|
||||
onOk={onRemove}>
|
||||
{getModalBodyText(selectedEdge)}
|
||||
</Modal>
|
||||
<Card
|
||||
className={classNames('lineage-card card-body-full w-auto border-none', {
|
||||
'full-screen-lineage': isFullScreen,
|
||||
})}
|
||||
data-testid="lineage-details"
|
||||
id="lineageDetails">
|
||||
{isFullScreen && (
|
||||
<TitleBreadcrumb className="p-md" titleLinks={breadcrumbs} />
|
||||
)}
|
||||
<div className="relative h-full" data-testid="lineage-container">
|
||||
<div className="w-full h-full" ref={reactFlowWrapper}>
|
||||
<ReactFlowProvider>
|
||||
<ReactFlow
|
||||
onlyRenderVisibleElements
|
||||
className="custom-react-flow"
|
||||
data-testid="react-flow-component"
|
||||
edgeTypes={customEdges}
|
||||
edges={edges}
|
||||
maxZoom={MAX_ZOOM_VALUE}
|
||||
minZoom={MIN_ZOOM_VALUE}
|
||||
nodeTypes={nodeTypes}
|
||||
nodes={nodes}
|
||||
nodesConnectable={isEditMode}
|
||||
selectNodesOnDrag={false}
|
||||
onConnect={onConnect}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={onDrop}
|
||||
onEdgeClick={handleEdgeClick}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onInit={(reactFlowInstance: ReactFlowInstance) => {
|
||||
onLoad(reactFlowInstance);
|
||||
setReactFlowInstance(reactFlowInstance);
|
||||
}}
|
||||
onMove={(_e, viewPort) => handleZoomLevel(viewPort.zoom)}
|
||||
onNodeClick={(_e, node) => {
|
||||
onNodeClick(node);
|
||||
_e.stopPropagation();
|
||||
}}
|
||||
onNodeContextMenu={onNodeContextMenu}
|
||||
onNodeDrag={dragHandle}
|
||||
onNodeDragStart={dragHandle}
|
||||
onNodeDragStop={dragHandle}
|
||||
onNodeMouseEnter={onNodeMouseEnter}
|
||||
onNodeMouseLeave={onNodeMouseLeave}
|
||||
onNodeMouseMove={onNodeMouseMove}
|
||||
onNodesChange={onNodesChange}
|
||||
onPaneClick={onPaneClick}>
|
||||
{updatedLineageData && (
|
||||
<CustomControlsComponent
|
||||
className="absolute top-1 right-1 bottom-full p-md"
|
||||
deleted={deleted}
|
||||
fitViewParams={{
|
||||
minZoom: MIN_ZOOM_VALUE,
|
||||
maxZoom: MAX_ZOOM_VALUE,
|
||||
}}
|
||||
handleFullScreenViewClick={
|
||||
!isFullScreen ? onFullScreenClick : undefined
|
||||
}
|
||||
hasEditAccess={hasEditAccess}
|
||||
isColumnsExpanded={expandAllColumns}
|
||||
isEditMode={isEditMode}
|
||||
lineageConfig={lineageConfig}
|
||||
lineageData={updatedLineageData}
|
||||
loading={loading}
|
||||
status={status}
|
||||
zoomValue={zoomValue}
|
||||
onEditLinageClick={handleEditLineageClick}
|
||||
onExitFullScreenViewClick={
|
||||
isFullScreen ? onExitFullScreenViewClick : undefined
|
||||
}
|
||||
onExpandColumnClick={handleExpandColumnClick}
|
||||
onLineageConfigUpdate={handleLineageConfigUpdate}
|
||||
onOptionSelect={handleOptionSelect}
|
||||
/>
|
||||
)}
|
||||
<Background gap={12} size={1} />
|
||||
</ReactFlow>
|
||||
</ReactFlowProvider>
|
||||
</div>
|
||||
{isDrawerOpen &&
|
||||
!isEditMode &&
|
||||
(selectedEdgeInfo ? (
|
||||
<EdgeInfoDrawer
|
||||
edge={selectedEdgeInfo}
|
||||
nodes={nodes}
|
||||
visible={isDrawerOpen}
|
||||
onClose={() => {
|
||||
setIsDrawerOpen(false);
|
||||
setSelectedEdgeInfo(undefined);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<EntityInfoDrawer
|
||||
isMainNode={
|
||||
selectedNode.name === updatedLineageData?.entity?.name
|
||||
}
|
||||
selectedNode={selectedNode}
|
||||
show={isDrawerOpen}
|
||||
onCancel={closeDrawer}
|
||||
/>
|
||||
))}
|
||||
<EntityLineageSidebar newAddedNode={newAddedNode} show={isEditMode} />
|
||||
{showDeleteModal && (
|
||||
<Modal
|
||||
maskClosable={false}
|
||||
okText={getLoadingStatusValue(
|
||||
t('label.confirm'),
|
||||
deletionState.loading,
|
||||
deletionState.status
|
||||
)}
|
||||
open={showDeleteModal}
|
||||
title={t('message.remove-lineage-edge')}
|
||||
onCancel={() => {
|
||||
setShowDeleteModal(false);
|
||||
}}
|
||||
onOk={onRemove}>
|
||||
{getModalBodyText(selectedEdge)}
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
<AddPipeLineModal
|
||||
pipelineOptions={pipelineOptions}
|
||||
pipelineSearchValue={pipelineSearchValue}
|
||||
selectedPipelineId={selectedPipelineId}
|
||||
showAddPipelineModal={showAddPipelineModal}
|
||||
onClear={onPipelineSelectionClear}
|
||||
onModalCancel={handleModalCancel}
|
||||
onRemoveEdgeClick={handleRemoveEdgeClick}
|
||||
onSave={handleModalSave}
|
||||
onSearch={(value) => setPipelineSearchValue(value)}
|
||||
onSelect={handlePipelineSelection}
|
||||
/>
|
||||
</div>
|
||||
<AddPipeLineModal
|
||||
pipelineOptions={pipelineOptions}
|
||||
pipelineSearchValue={pipelineSearchValue}
|
||||
selectedPipelineId={selectedPipelineId}
|
||||
showAddPipelineModal={showAddPipelineModal}
|
||||
onClear={onPipelineSelectionClear}
|
||||
onModalCancel={handleModalCancel}
|
||||
onRemoveEdgeClick={handleRemoveEdgeClick}
|
||||
onSave={handleModalSave}
|
||||
onSearch={(value) => setPipelineSearchValue(value)}
|
||||
onSelect={handlePipelineSelection}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { SourceType } from 'components/searched-data/SearchedData.interface';
|
||||
import { LoadingState } from 'Models';
|
||||
import { HTMLAttributes } from 'react';
|
||||
import { Edge as FlowEdge, FitViewOptions, Node } from 'reactflow';
|
||||
@ -33,6 +34,7 @@ export interface EntityLineageProp {
|
||||
deleted?: boolean;
|
||||
hasEditAccess?: boolean;
|
||||
isFullScreen?: boolean;
|
||||
entity?: SourceType;
|
||||
}
|
||||
|
||||
export interface Edge {
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { MOCK_CHILD_MAP, MOCK_LINEAGE_DATA } from 'mocks/Lineage.mock';
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
@ -107,6 +107,15 @@ jest.mock('../EntityInfoDrawer/EntityInfoDrawer.component', () => {
|
||||
return jest.fn().mockReturnValue(<p>EntityInfoDrawerComponent</p>);
|
||||
});
|
||||
|
||||
const mockPush = jest.fn();
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useHistory: () => ({
|
||||
push: mockPush,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Test EntityLineage Component', () => {
|
||||
it('Check if EntityLineage is rendering all the nodes', async () => {
|
||||
act(() => {
|
||||
@ -141,4 +150,52 @@ describe('Test EntityLineage Component', () => {
|
||||
|
||||
expect(lineageContainer).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should add fullscreen true in url on fullscreen button click', async () => {
|
||||
render(<EntityLineage {...mockEntityLineageProp} />, {
|
||||
wrapper: MemoryRouter,
|
||||
});
|
||||
|
||||
const lineageContainer = await screen.findByTestId('lineage-container');
|
||||
const reactFlowElement = await screen.findByTestId('rf__wrapper');
|
||||
const fullscreenButton = await screen.getByTestId('full-screen');
|
||||
|
||||
expect(lineageContainer).toBeInTheDocument();
|
||||
expect(reactFlowElement).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(fullscreenButton);
|
||||
});
|
||||
|
||||
expect(mockPush).toHaveBeenCalledTimes(1);
|
||||
expect(mockPush).toHaveBeenCalledWith({
|
||||
search: 'fullscreen=true',
|
||||
});
|
||||
});
|
||||
|
||||
it('should show breadcrumbs when URL has fullscreen=true', async () => {
|
||||
act(() => {
|
||||
render(
|
||||
<MemoryRouter
|
||||
initialEntries={[
|
||||
'/table/sample_data.ecommerce_db.shopify.raw_customer/lineage?fullscreen=true',
|
||||
]}>
|
||||
<EntityLineage {...mockEntityLineageProp} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
});
|
||||
const lineageContainer = await screen.findByTestId('lineage-container');
|
||||
const reactFlowElement = await screen.findByTestId('rf__wrapper');
|
||||
|
||||
expect(lineageContainer).toBeInTheDocument();
|
||||
expect(reactFlowElement).toBeInTheDocument();
|
||||
|
||||
const breadcrumbs = await screen.getByTestId('breadcrumb');
|
||||
|
||||
expect(breadcrumbs).toBeInTheDocument();
|
||||
|
||||
const mainRootElement = await screen.getByTestId('lineage-details');
|
||||
|
||||
expect(mainRootElement).toHaveClass('full-screen-lineage');
|
||||
});
|
||||
});
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
|
||||
@import url('../../styles/variables.less');
|
||||
|
||||
@sidebar-width: 100px;
|
||||
@lineage-sidebar-width: 100px;
|
||||
|
||||
.custom-react-flow {
|
||||
.react-flow__node-input.selectable.selected {
|
||||
@ -87,7 +87,7 @@
|
||||
height: 32px;
|
||||
}
|
||||
.ant-select.custom-control-search-box-edit-mode {
|
||||
margin-left: @sidebar-width;
|
||||
margin-left: @lineage-sidebar-width;
|
||||
}
|
||||
.custom-control-basic-button {
|
||||
padding-left: 4px;
|
||||
@ -134,6 +134,9 @@
|
||||
display: block !important;
|
||||
border: 1px solid @border-color;
|
||||
background-color: @body-bg-color;
|
||||
&.active {
|
||||
background-color: @primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-lineage-heading {
|
||||
@ -160,7 +163,7 @@
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: @sidebar-width;
|
||||
width: @lineage-sidebar-width;
|
||||
z-index: 200;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
@ -174,3 +177,11 @@
|
||||
transform: translateX(0);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.lineage-card.full-screen-lineage {
|
||||
top: @navbar-height;
|
||||
left: @sidebar-width;
|
||||
position: fixed !important;
|
||||
width: calc(100vw - @sidebar-width);
|
||||
height: calc(100vh - @navbar-height);
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Card, Col, Row, Space, Table, Tabs, Typography } from 'antd';
|
||||
import { Col, Row, Space, Table, Tabs, Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import { AxiosError } from 'axios';
|
||||
import ActivityFeedProvider, {
|
||||
@ -495,16 +495,13 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
|
||||
label: <TabsLabel id={EntityTabs.LINEAGE} name={t('label.lineage')} />,
|
||||
key: EntityTabs.LINEAGE,
|
||||
children: (
|
||||
<Card
|
||||
className="lineage-card card-body-full w-auto border-none"
|
||||
data-testid="lineage-details">
|
||||
<EntityLineageComponent
|
||||
entityType={EntityType.MLMODEL}
|
||||
hasEditAccess={
|
||||
mlModelPermissions.EditAll || mlModelPermissions.EditLineage
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
<EntityLineageComponent
|
||||
entity={mlModelDetail}
|
||||
entityType={EntityType.MLMODEL}
|
||||
hasEditAccess={
|
||||
mlModelPermissions.EditAll || mlModelPermissions.EditLineage
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@ -672,18 +672,14 @@ const PipelineDetails = ({
|
||||
label: <TabsLabel id={EntityTabs.LINEAGE} name={t('label.lineage')} />,
|
||||
key: EntityTabs.LINEAGE,
|
||||
children: (
|
||||
<Card
|
||||
className="lineage-card card-body-full w-auto border-none"
|
||||
data-testid="lineage-details"
|
||||
id="lineageDetails">
|
||||
<EntityLineageComponent
|
||||
deleted={deleted}
|
||||
entityType={EntityType.PIPELINE}
|
||||
hasEditAccess={
|
||||
pipelinePermissions.EditAll || pipelinePermissions.EditLineage
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
<EntityLineageComponent
|
||||
deleted={deleted}
|
||||
entity={pipelineDetails}
|
||||
entityType={EntityType.PIPELINE}
|
||||
hasEditAccess={
|
||||
pipelinePermissions.EditAll || pipelinePermissions.EditLineage
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Card, Col, Row, Space, Tabs } from 'antd';
|
||||
import { Col, Row, Space, Tabs } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import ActivityFeedProvider, {
|
||||
useActivityFeedProvider,
|
||||
@ -397,17 +397,13 @@ const TopicDetails: React.FC<TopicDetailsProps> = ({
|
||||
label: <TabsLabel id={EntityTabs.LINEAGE} name={t('label.lineage')} />,
|
||||
key: EntityTabs.LINEAGE,
|
||||
children: (
|
||||
<Card
|
||||
className="lineage-card card-body-full w-auto border-none"
|
||||
data-testid="lineage-details"
|
||||
id="lineageDetails">
|
||||
<EntityLineageComponent
|
||||
entityType={EntityType.TOPIC}
|
||||
hasEditAccess={
|
||||
topicPermissions.EditAll || topicPermissions.EditLineage
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
<EntityLineageComponent
|
||||
entity={topicDetails}
|
||||
entityType={EntityType.TOPIC}
|
||||
hasEditAccess={
|
||||
topicPermissions.EditAll || topicPermissions.EditLineage
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@ -12,7 +12,6 @@
|
||||
*/
|
||||
|
||||
import DataQualityPage from 'pages/DataQuality/DataQualityPage';
|
||||
import LineagePage from 'pages/LineagePage/LineagePage';
|
||||
import React, { FunctionComponent, useMemo } from 'react';
|
||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
||||
import { ROUTES } from '../../constants/constants';
|
||||
@ -562,11 +561,6 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
|
||||
|
||||
<Route exact component={RequestTagsPage} path={ROUTES.REQUEST_TAGS} />
|
||||
<Route exact component={UpdateTagsPage} path={ROUTES.UPDATE_TAGS} />
|
||||
<Route
|
||||
exact
|
||||
component={LineagePage}
|
||||
path={ROUTES.LINEAGE_FULL_SCREEN_VIEW}
|
||||
/>
|
||||
|
||||
{/* keep these route above the setting route always */}
|
||||
<AdminProtectedRoute
|
||||
|
||||
@ -286,7 +286,6 @@ export const ROUTES = {
|
||||
PROFILER_DASHBOARD: `/profiler-dashboard/${PLACEHOLDER_DASHBOARD_TYPE}/${PLACEHOLDER_ENTITY_TYPE_FQN}`,
|
||||
PROFILER_DASHBOARD_WITH_TAB: `/profiler-dashboard/${PLACEHOLDER_DASHBOARD_TYPE}/${PLACEHOLDER_ENTITY_TYPE_FQN}/${PLACEHOLDER_ROUTE_TAB}`,
|
||||
ADD_DATA_QUALITY_TEST_CASE: `/data-quality-test/${PLACEHOLDER_DASHBOARD_TYPE}/${PLACEHOLDER_ENTITY_TYPE_FQN}`,
|
||||
LINEAGE_FULL_SCREEN_VIEW: `/lineage-view/${PLACEHOLDER_ROUTE_ENTITY_TYPE}/${PLACEHOLDER_ROUTE_ENTITY_FQN}`,
|
||||
|
||||
// Query Routes
|
||||
QUERY_FULL_SCREEN_VIEW: `/query-view/${PLACEHOLDER_ROUTE_TABLE_FQN}/${PLACEHOLDER_ROUTE_QUERY_ID}`,
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Card, Col, Row, Space, Tabs } from 'antd';
|
||||
import { Col, Row, Space, Tabs } from 'antd';
|
||||
import AppState from 'AppState';
|
||||
import { AxiosError } from 'axios';
|
||||
import ActivityFeedProvider, {
|
||||
@ -27,13 +27,6 @@ import ContainerDataModel from 'components/ContainerDetail/ContainerDataModel/Co
|
||||
import PageLayoutV1 from 'components/containers/PageLayoutV1';
|
||||
import { DataAssetsHeader } from 'components/DataAssets/DataAssetsHeader/DataAssetsHeader.component';
|
||||
import EntityLineageComponent from 'components/EntityLineage/EntityLineage.component';
|
||||
import {
|
||||
Edge,
|
||||
EdgeData,
|
||||
LeafNodes,
|
||||
LineagePos,
|
||||
LoadingNodeState,
|
||||
} from 'components/EntityLineage/EntityLineage.interface';
|
||||
import Loader from 'components/Loader/Loader';
|
||||
import { EntityName } from 'components/Modals/EntityNameModal/EntityNameModal.interface';
|
||||
import { usePermissionProvider } from 'components/PermissionProvider/PermissionProvider';
|
||||
@ -50,8 +43,6 @@ import { EntityTabs, EntityType } from 'enums/entity.enum';
|
||||
import { compare } from 'fast-json-patch';
|
||||
import { CreateThread, ThreadType } from 'generated/api/feed/createThread';
|
||||
import { Container } from 'generated/entity/data/container';
|
||||
import { EntityLineage } from 'generated/type/entityLineage';
|
||||
import { EntityReference } from 'generated/type/entityReference';
|
||||
import { Include } from 'generated/type/include';
|
||||
import { LabelType, State, TagLabel, TagSource } from 'generated/type/tagLabel';
|
||||
import { EntityFieldThreadCount } from 'interface/feed.interface';
|
||||
@ -62,8 +53,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { postThread } from 'rest/feedsAPI';
|
||||
import { getLineageByFQN } from 'rest/lineageAPI';
|
||||
import { addLineage, deleteLineageEdge } from 'rest/miscAPI';
|
||||
import {
|
||||
addContainerFollower,
|
||||
getContainerByName,
|
||||
@ -79,14 +68,9 @@ import {
|
||||
refreshPage,
|
||||
sortTagsCaseInsensitive,
|
||||
} from 'utils/CommonUtils';
|
||||
import {
|
||||
getEntityLineage,
|
||||
getEntityName,
|
||||
getEntityThreadLink,
|
||||
} from 'utils/EntityUtils';
|
||||
import { getEntityName, getEntityThreadLink } from 'utils/EntityUtils';
|
||||
import { getEntityFieldThreadCounts } from 'utils/FeedUtils';
|
||||
import { DEFAULT_ENTITY_PERMISSION } from 'utils/PermissionsUtils';
|
||||
import { getLineageViewPath } from 'utils/RouterUtils';
|
||||
import { getTagsWithoutTier, getTierTags } from 'utils/TableUtils';
|
||||
import { showErrorToast, showSuccessToast } from 'utils/ToastUtils';
|
||||
|
||||
@ -103,7 +87,6 @@ const ContainerPage = () => {
|
||||
const [isChildrenLoading, setIsChildrenLoading] = useState<boolean>(false);
|
||||
const [hasError, setHasError] = useState<boolean>(false);
|
||||
const [isEditDescription, setIsEditDescription] = useState<boolean>(false);
|
||||
const [isLineageLoading, setIsLineageLoading] = useState<boolean>(false);
|
||||
|
||||
const [containerData, setContainerData] = useState<Container>();
|
||||
const [containerChildrenData, setContainerChildrenData] = useState<
|
||||
@ -111,14 +94,6 @@ const ContainerPage = () => {
|
||||
>([]);
|
||||
const [containerPermissions, setContainerPermissions] =
|
||||
useState<OperationPermission>(DEFAULT_ENTITY_PERMISSION);
|
||||
const [entityLineage, setEntityLineage] = useState<EntityLineage>(
|
||||
{} as EntityLineage
|
||||
);
|
||||
const [leafNodes, setLeafNodes] = useState<LeafNodes>({} as LeafNodes);
|
||||
const [isNodeLoading, setNodeLoading] = useState<LoadingNodeState>({
|
||||
id: undefined,
|
||||
state: false,
|
||||
});
|
||||
|
||||
const [feedCount, setFeedCount] = useState<number>(0);
|
||||
const [entityFieldThreadCount, setEntityFieldThreadCount] = useState<
|
||||
@ -170,22 +145,6 @@ const ContainerPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchLineageData = async (containerFQN: string) => {
|
||||
setIsLineageLoading(true);
|
||||
try {
|
||||
const response = await getLineageByFQN(
|
||||
containerFQN,
|
||||
EntityType.CONTAINER
|
||||
);
|
||||
|
||||
setEntityLineage(response);
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
} finally {
|
||||
setIsLineageLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchResourcePermission = async (containerFQN: string) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
@ -431,67 +390,6 @@ const ContainerPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Lineage handlers
|
||||
const handleAddLineage = async (edge: Edge) => {
|
||||
try {
|
||||
await addLineage(edge);
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveLineage = async (data: EdgeData) => {
|
||||
try {
|
||||
await deleteLineageEdge(
|
||||
data.fromEntity,
|
||||
data.fromId,
|
||||
data.toEntity,
|
||||
data.toId
|
||||
);
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetLeafNode = (val: EntityLineage, pos: LineagePos) => {
|
||||
if (pos === 'to' && val.downstreamEdges?.length === 0) {
|
||||
setLeafNodes((prev) => ({
|
||||
...prev,
|
||||
downStreamNode: [...(prev.downStreamNode ?? []), val.entity.id],
|
||||
}));
|
||||
}
|
||||
if (pos === 'from' && val.upstreamEdges?.length === 0) {
|
||||
setLeafNodes((prev) => ({
|
||||
...prev,
|
||||
upStreamNode: [...(prev.upStreamNode ?? []), val.entity.id],
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadLineageNode = async (
|
||||
node: EntityReference,
|
||||
pos: LineagePos
|
||||
) => {
|
||||
setNodeLoading({ id: node.id, state: true });
|
||||
|
||||
try {
|
||||
const response = await getLineageByFQN(
|
||||
node.fullyQualifiedName ?? '',
|
||||
node.type
|
||||
);
|
||||
handleSetLeafNode(response, pos);
|
||||
setEntityLineage(getEntityLineage(entityLineage, response, pos));
|
||||
setTimeout(() => {
|
||||
setNodeLoading((prev) => ({ ...prev, state: false }));
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFullScreenClick = () =>
|
||||
history.push(getLineageViewPath(EntityType.CONTAINER, containerName));
|
||||
|
||||
const handleExtensionUpdate = async (updatedContainer: Container) => {
|
||||
try {
|
||||
const response = await handleUpdateContainerData(updatedContainer);
|
||||
@ -691,24 +589,11 @@ const ContainerPage = () => {
|
||||
label: <TabsLabel id={EntityTabs.LINEAGE} name={t('label.lineage')} />,
|
||||
key: EntityTabs.LINEAGE,
|
||||
children: (
|
||||
<Card className="lineage-card card-body-full m-md w-auto">
|
||||
<EntityLineageComponent
|
||||
addLineageHandler={handleAddLineage}
|
||||
deleted={deleted}
|
||||
entityLineage={entityLineage}
|
||||
entityLineageHandler={(lineage: EntityLineage) =>
|
||||
setEntityLineage(lineage)
|
||||
}
|
||||
entityType={EntityType.CONTAINER}
|
||||
hasEditAccess={hasEditLineagePermission}
|
||||
isLoading={isLineageLoading}
|
||||
isNodeLoading={isNodeLoading}
|
||||
lineageLeafNodes={leafNodes}
|
||||
loadNodeHandler={handleLoadLineageNode}
|
||||
removeLineageHandler={handleRemoveLineage}
|
||||
onFullScreenClick={handleFullScreenClick}
|
||||
/>
|
||||
</Card>
|
||||
<EntityLineageComponent
|
||||
entity={containerData}
|
||||
entityType={EntityType.CONTAINER}
|
||||
hasEditAccess={hasEditLineagePermission}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
@ -745,24 +630,16 @@ const ContainerPage = () => {
|
||||
hasEditCustomFieldsPermission,
|
||||
deleted,
|
||||
owner,
|
||||
isNodeLoading,
|
||||
leafNodes,
|
||||
isLineageLoading,
|
||||
isChildrenLoading,
|
||||
entityFieldThreadCount,
|
||||
tags,
|
||||
entityLineage,
|
||||
feedCount,
|
||||
containerChildrenData,
|
||||
handleAddLineage,
|
||||
handleUpdateDataModel,
|
||||
handleUpdateDescription,
|
||||
getEntityFieldThreadCounts,
|
||||
handleTagSelection,
|
||||
onThreadLinkSelect,
|
||||
handleLoadLineageNode,
|
||||
handleRemoveLineage,
|
||||
handleFullScreenClick,
|
||||
handleExtensionUpdate,
|
||||
]
|
||||
);
|
||||
@ -779,9 +656,6 @@ const ContainerPage = () => {
|
||||
}, [containerName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tab === EntityTabs.LINEAGE) {
|
||||
fetchLineageData(containerName);
|
||||
}
|
||||
if (tab === EntityTabs.CHILDREN) {
|
||||
fetchContainerChildren(containerName);
|
||||
}
|
||||
|
||||
@ -1,189 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 Collate.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AxiosError } from 'axios';
|
||||
import TitleBreadcrumb from 'components/common/title-breadcrumb/title-breadcrumb.component';
|
||||
import { TitleBreadcrumbProps } from 'components/common/title-breadcrumb/title-breadcrumb.interface';
|
||||
import PageLayoutV1 from 'components/containers/PageLayoutV1';
|
||||
import EntityLineageComponent from 'components/EntityLineage/EntityLineage.component';
|
||||
import { Container } from 'generated/entity/data/container';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { getDashboardByFqn } from 'rest/dashboardAPI';
|
||||
import { getMlModelByFQN } from 'rest/mlModelAPI';
|
||||
import { getPipelineByFqn } from 'rest/pipelineAPI';
|
||||
import { getContainerByName } from 'rest/storageAPI';
|
||||
import { getTableDetailsByFQN } from 'rest/tableAPI';
|
||||
import { getTopicByFqn } from 'rest/topicsAPI';
|
||||
import {
|
||||
getContainerDetailPath,
|
||||
getDashboardDetailsPath,
|
||||
getMlModelPath,
|
||||
getPipelineDetailsPath,
|
||||
getTableTabPath,
|
||||
getTopicDetailsPath,
|
||||
} from '../../constants/constants';
|
||||
import { EntityTabs, EntityType } from '../../enums/entity.enum';
|
||||
import { Dashboard } from '../../generated/entity/data/dashboard';
|
||||
import { Mlmodel } from '../../generated/entity/data/mlmodel';
|
||||
import { Pipeline } from '../../generated/entity/data/pipeline';
|
||||
import { Topic } from '../../generated/entity/data/topic';
|
||||
import { getEntityBreadcrumbs, getEntityName } from '../../utils/EntityUtils';
|
||||
import { showErrorToast } from '../../utils/ToastUtils';
|
||||
import './lineagePage.style.less';
|
||||
|
||||
const LineagePage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { entityType, entityFQN } =
|
||||
useParams<{ entityType: EntityType; entityFQN: string }>();
|
||||
|
||||
const [titleBreadcrumb, setTitleBreadcrumb] = useState<
|
||||
TitleBreadcrumbProps['titleLinks']
|
||||
>([]);
|
||||
|
||||
const updateBreadcrumb = (
|
||||
apiRes: Topic | Dashboard | Pipeline | Mlmodel | Container,
|
||||
currentEntityPath: string,
|
||||
entityType: EntityType
|
||||
) => {
|
||||
setTitleBreadcrumb([
|
||||
...getEntityBreadcrumbs(apiRes, entityType),
|
||||
{
|
||||
name: getEntityName(apiRes),
|
||||
url: currentEntityPath,
|
||||
},
|
||||
{
|
||||
name: t('label.lineage'),
|
||||
url: '',
|
||||
activeTitle: true,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const fetchEntityDetails = async () => {
|
||||
try {
|
||||
switch (entityType) {
|
||||
case EntityType.TABLE:
|
||||
{
|
||||
const tableRes = await getTableDetailsByFQN(entityFQN, '');
|
||||
setTitleBreadcrumb([
|
||||
...getEntityBreadcrumbs(tableRes, EntityType.TABLE),
|
||||
{
|
||||
name: getEntityName(tableRes),
|
||||
url: getTableTabPath(entityFQN, EntityTabs.LINEAGE),
|
||||
},
|
||||
{
|
||||
name: t('label.lineage'),
|
||||
url: '',
|
||||
activeTitle: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case EntityType.TOPIC:
|
||||
{
|
||||
const topicRes = await getTopicByFqn(entityFQN, '');
|
||||
updateBreadcrumb(
|
||||
topicRes,
|
||||
getTopicDetailsPath(entityFQN, EntityTabs.LINEAGE),
|
||||
EntityType.TOPIC
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case EntityType.DASHBOARD:
|
||||
{
|
||||
const dashboardRes = await getDashboardByFqn(entityFQN, '');
|
||||
updateBreadcrumb(
|
||||
dashboardRes,
|
||||
getDashboardDetailsPath(entityFQN, EntityTabs.LINEAGE),
|
||||
EntityType.DASHBOARD
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case EntityType.PIPELINE:
|
||||
{
|
||||
const pipelineRes = await getPipelineByFqn(entityFQN, '');
|
||||
updateBreadcrumb(
|
||||
pipelineRes,
|
||||
getPipelineDetailsPath(entityFQN, EntityTabs.LINEAGE),
|
||||
EntityType.PIPELINE
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case EntityType.MLMODEL:
|
||||
{
|
||||
const mlmodelRes = await getMlModelByFQN(entityFQN, '');
|
||||
updateBreadcrumb(
|
||||
mlmodelRes,
|
||||
getMlModelPath(entityFQN, EntityTabs.LINEAGE),
|
||||
EntityType.MLMODEL
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case EntityType.CONTAINER:
|
||||
{
|
||||
const containerRes = await getContainerByName(entityFQN, '');
|
||||
updateBreadcrumb(
|
||||
containerRes,
|
||||
getContainerDetailPath(entityFQN, EntityTabs.LINEAGE),
|
||||
EntityType.CONTAINER
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
showErrorToast(
|
||||
error as AxiosError,
|
||||
t('server.entity-fetch-error', {
|
||||
entity: entityType,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (entityFQN && entityType) {
|
||||
fetchEntityDetails();
|
||||
}
|
||||
}, [entityFQN, entityType]);
|
||||
|
||||
return (
|
||||
<PageLayoutV1 className="p-x-lg" pageTitle={t('label.lineage')}>
|
||||
<div className="lineage-page-container page-container">
|
||||
<TitleBreadcrumb titleLinks={titleBreadcrumb} />
|
||||
<EntityLineageComponent
|
||||
hasEditAccess
|
||||
isFullScreen
|
||||
entityType={entityType}
|
||||
/>
|
||||
</div>
|
||||
</PageLayoutV1>
|
||||
);
|
||||
};
|
||||
|
||||
export default LineagePage;
|
||||
@ -1,23 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 Collate.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.lineage-page-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
height: 100%;
|
||||
|
||||
.ant-card-body {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
@ -10,7 +10,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Card, Col, Row, Space, Tabs, Typography } from 'antd';
|
||||
import { Col, Row, Space, Tabs, Typography } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import classNames from 'classnames';
|
||||
import ActivityFeedProvider, {
|
||||
@ -35,6 +35,7 @@ import {
|
||||
} from 'components/PermissionProvider/PermissionProvider.interface';
|
||||
import SampleDataTableComponent from 'components/SampleDataTable/SampleDataTable.component';
|
||||
import SchemaTab from 'components/SchemaTab/SchemaTab.component';
|
||||
import { SourceType } from 'components/searched-data/SearchedData.interface';
|
||||
import TableProfilerV1 from 'components/TableProfiler/TableProfilerV1';
|
||||
import TableQueries from 'components/TableQueries/TableQueries';
|
||||
import TabsLabel from 'components/TabsLabel/TabsLabel.component';
|
||||
@ -624,17 +625,14 @@ const TableDetailsPageV1 = () => {
|
||||
label: <TabsLabel id={EntityTabs.LINEAGE} name={t('label.lineage')} />,
|
||||
key: EntityTabs.LINEAGE,
|
||||
children: (
|
||||
<Card
|
||||
className="lineage-card card-body-full w-auto border-none"
|
||||
id="lineageDetails">
|
||||
<EntityLineageComponent
|
||||
deleted={tableDetails?.deleted}
|
||||
entityType={EntityType.TABLE}
|
||||
hasEditAccess={
|
||||
tablePermissions.EditAll || tablePermissions.EditLineage
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
<EntityLineageComponent
|
||||
deleted={tableDetails?.deleted}
|
||||
entity={tableDetails as SourceType}
|
||||
entityType={EntityType.TABLE}
|
||||
hasEditAccess={
|
||||
tablePermissions.EditAll || tablePermissions.EditLineage
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
|
||||
@ -78,6 +78,9 @@
|
||||
@grey-bg-with-alpha: #7575751a;
|
||||
@btn-shadow: none;
|
||||
|
||||
@navbar-height: 64px;
|
||||
@sidebar-width: 60px;
|
||||
|
||||
// Sizing
|
||||
@page-height: calc(100vh - 64px);
|
||||
@left-side-panel-width: 230px;
|
||||
|
||||
@ -25,8 +25,6 @@ import {
|
||||
PLACEHOLDER_ENTITY_TYPE_FQN,
|
||||
PLACEHOLDER_GLOSSARY_NAME,
|
||||
PLACEHOLDER_GLOSSARY_TERMS_FQN,
|
||||
PLACEHOLDER_ROUTE_ENTITY_FQN,
|
||||
PLACEHOLDER_ROUTE_ENTITY_TYPE,
|
||||
PLACEHOLDER_ROUTE_FQN,
|
||||
PLACEHOLDER_ROUTE_INGESTION_FQN,
|
||||
PLACEHOLDER_ROUTE_INGESTION_TYPE,
|
||||
@ -48,7 +46,7 @@ import {
|
||||
GlobalSettingsMenuCategory,
|
||||
} from '../constants/GlobalSettings.constants';
|
||||
import { arrServiceTypes } from '../constants/Services.constant';
|
||||
import { EntityAction, EntityType } from '../enums/entity.enum';
|
||||
import { EntityAction } from '../enums/entity.enum';
|
||||
import { PipelineType } from '../generated/api/services/ingestionPipelines/createIngestionPipeline';
|
||||
import { getServiceRouteFromServiceType } from './ServiceUtils';
|
||||
import { getEncodedFqn } from './StringsUtils';
|
||||
@ -395,16 +393,6 @@ export const getLogEntityPath = (
|
||||
);
|
||||
};
|
||||
|
||||
export const getLineageViewPath = (entity: EntityType, fqn: string) => {
|
||||
let path = ROUTES.LINEAGE_FULL_SCREEN_VIEW;
|
||||
|
||||
path = path
|
||||
.replace(PLACEHOLDER_ROUTE_ENTITY_TYPE, entity)
|
||||
.replace(PLACEHOLDER_ROUTE_ENTITY_FQN, fqn);
|
||||
|
||||
return path;
|
||||
};
|
||||
|
||||
export const getGlossaryPathWithAction = (
|
||||
fqn: string,
|
||||
action: EntityAction
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user