mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-01 13:13:10 +00:00
fix(ui): revamp lineage edge layout (#13612)
* revamp lineage edge layout * fix unit test
This commit is contained in:
parent
210a8f9d5c
commit
858d75f9c9
@ -1,98 +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 { Button, Modal, Select } from 'antd';
|
||||
import { t } from 'i18next';
|
||||
import { isUndefined } from 'lodash';
|
||||
import React from 'react';
|
||||
import { EntityReference } from '../../../generated/entity/type';
|
||||
import { getEntityName } from '../../../utils/EntityUtils';
|
||||
|
||||
interface AddPipeLineModalType {
|
||||
showAddEdgeModal: boolean;
|
||||
edgeSearchValue: string;
|
||||
selectedEdgeId: string | undefined;
|
||||
edgeOptions: EntityReference[];
|
||||
onModalCancel: () => void;
|
||||
onSave: () => void;
|
||||
onClear: () => void;
|
||||
onRemoveEdgeClick: (evt: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onSearch: (value: string) => void;
|
||||
onSelect: (value: string) => void;
|
||||
}
|
||||
|
||||
const AddPipeLineModal = ({
|
||||
showAddEdgeModal,
|
||||
edgeOptions,
|
||||
edgeSearchValue,
|
||||
selectedEdgeId,
|
||||
onRemoveEdgeClick,
|
||||
onModalCancel,
|
||||
onSave,
|
||||
onClear,
|
||||
onSearch,
|
||||
onSelect,
|
||||
}: AddPipeLineModalType) => {
|
||||
return (
|
||||
<Modal
|
||||
destroyOnClose
|
||||
data-testid="add-edge-modal"
|
||||
footer={[
|
||||
<Button
|
||||
data-testid="remove-edge-button"
|
||||
key="remove-edge-btn"
|
||||
type="text"
|
||||
onClick={onRemoveEdgeClick}>
|
||||
{t('label.remove-entity', {
|
||||
entity: t('label.edge-lowercase'),
|
||||
})}
|
||||
</Button>,
|
||||
<Button
|
||||
data-testid="save-button"
|
||||
key="save-btn"
|
||||
type="primary"
|
||||
onClick={onSave}>
|
||||
{t('label.save')}
|
||||
</Button>,
|
||||
]}
|
||||
maskClosable={false}
|
||||
open={showAddEdgeModal}
|
||||
title={t(`label.${isUndefined(selectedEdgeId) ? 'add' : 'edit'}-entity`, {
|
||||
entity: t('label.edge'),
|
||||
})}
|
||||
onCancel={onModalCancel}>
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
className="w-full"
|
||||
data-testid="field-select"
|
||||
defaultActiveFirstOption={false}
|
||||
filterOption={false}
|
||||
notFoundContent={false}
|
||||
options={edgeOptions.map((option) => ({
|
||||
label: getEntityName(option),
|
||||
value: option.id,
|
||||
}))}
|
||||
placeholder={t('message.search-for-edge')}
|
||||
searchValue={edgeSearchValue}
|
||||
showArrow={false}
|
||||
value={selectedEdgeId}
|
||||
onClear={onClear}
|
||||
onSearch={onSearch}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddPipeLineModal;
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2022 Collate.
|
||||
* 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
|
||||
@ -26,11 +26,11 @@ const mockProps = {
|
||||
name: 'Pipeline 1',
|
||||
id: 'test-pipeline-1',
|
||||
type: 'pipeline',
|
||||
fullyQualifiedName: 'sample_airflow/presto_etl',
|
||||
},
|
||||
],
|
||||
onModalCancel: jest.fn(),
|
||||
onSave: jest.fn(),
|
||||
onClear: jest.fn(),
|
||||
onRemoveEdgeClick: jest.fn(),
|
||||
onSearch: jest.fn(),
|
||||
onSelect: jest.fn(),
|
||||
@ -41,7 +41,7 @@ describe('Test CustomEdge Component', () => {
|
||||
render(<AddPipeLineModal {...mockProps} />);
|
||||
|
||||
const edgeModal = await screen.findByTestId('add-edge-modal');
|
||||
const fieldSelect = await screen.findByTestId('field-select');
|
||||
const fieldSelect = await screen.findByTestId('field-input');
|
||||
const removeEdge = await screen.findByTestId('remove-edge-button');
|
||||
const saveButton = await screen.findByTestId('save-button');
|
||||
|
@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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 { Button, Input, Modal } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { t } from 'i18next';
|
||||
import { isEmpty, isUndefined } from 'lodash';
|
||||
import React, { useMemo } from 'react';
|
||||
import { ERROR_PLACEHOLDER_TYPE, SIZE } from '../../../../enums/common.enum';
|
||||
import { EntityReference } from '../../../../generated/entity/type';
|
||||
import { getEntityName } from '../../../../utils/EntityUtils';
|
||||
import Fqn from '../../../../utils/Fqn';
|
||||
import { getEntityIcon } from '../../../../utils/TableUtils';
|
||||
import ErrorPlaceHolder from '../../../common/error-with-placeholder/ErrorPlaceHolder';
|
||||
import '../../../FeedEditor/FeedEditor.css';
|
||||
import './add-pipeline-modal.less';
|
||||
|
||||
interface AddPipeLineModalType {
|
||||
showAddEdgeModal: boolean;
|
||||
edgeSearchValue: string;
|
||||
selectedEdgeId: string | undefined;
|
||||
edgeOptions: EntityReference[];
|
||||
onModalCancel: () => void;
|
||||
onSave: () => void;
|
||||
onRemoveEdgeClick: (evt: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onSearch: (value: string) => void;
|
||||
onSelect: (value: string) => void;
|
||||
}
|
||||
|
||||
const AddPipeLineModal = ({
|
||||
showAddEdgeModal,
|
||||
edgeOptions,
|
||||
edgeSearchValue,
|
||||
selectedEdgeId,
|
||||
onRemoveEdgeClick,
|
||||
onModalCancel,
|
||||
onSave,
|
||||
onSearch,
|
||||
onSelect,
|
||||
}: AddPipeLineModalType) => {
|
||||
const errorPlaceholderEdge = useMemo(() => {
|
||||
if (isEmpty(edgeOptions)) {
|
||||
if (edgeSearchValue) {
|
||||
return (
|
||||
<ErrorPlaceHolder
|
||||
className="mt-0-important"
|
||||
size={SIZE.MEDIUM}
|
||||
type={ERROR_PLACEHOLDER_TYPE.FILTER}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <ErrorPlaceHolder />;
|
||||
}
|
||||
|
||||
return;
|
||||
}, [edgeOptions, edgeSearchValue]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
destroyOnClose
|
||||
data-testid="add-edge-modal"
|
||||
footer={[
|
||||
<Button
|
||||
data-testid="remove-edge-button"
|
||||
key="remove-edge-btn"
|
||||
type="text"
|
||||
onClick={onRemoveEdgeClick}>
|
||||
{t('label.remove-entity', {
|
||||
entity: t('label.edge-lowercase'),
|
||||
})}
|
||||
</Button>,
|
||||
<Button
|
||||
data-testid="save-button"
|
||||
key="save-btn"
|
||||
type="primary"
|
||||
onClick={onSave}>
|
||||
{t('label.save')}
|
||||
</Button>,
|
||||
]}
|
||||
maskClosable={false}
|
||||
open={showAddEdgeModal}
|
||||
title={t(`label.${isUndefined(selectedEdgeId) ? 'add' : 'edit'}-entity`, {
|
||||
entity: t('label.edge'),
|
||||
})}
|
||||
onCancel={onModalCancel}>
|
||||
<Input
|
||||
data-testid="field-input"
|
||||
placeholder={t('message.search-for-edge')}
|
||||
value={edgeSearchValue}
|
||||
onChange={(e) => onSearch(e.target.value)}
|
||||
/>
|
||||
|
||||
<div className="edge-option-container">
|
||||
{edgeOptions.map((item) => {
|
||||
const icon = getEntityIcon(item.type);
|
||||
const breadcrumb = Fqn.split(item.fullyQualifiedName).join('/');
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('edge-option-item gap-2', {
|
||||
active: selectedEdgeId === item.id,
|
||||
})}
|
||||
key={item.id}
|
||||
onClick={() => onSelect(item.id)}>
|
||||
<div className="flex-center mention-icon-image">{icon}</div>
|
||||
<div>
|
||||
<div className="d-flex flex-wrap">
|
||||
<span className="truncate breadcrumb">{breadcrumb}</span>
|
||||
</div>
|
||||
<div className="d-flex flex-col">
|
||||
<span className="font-medium truncate">
|
||||
{getEntityName(item)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{errorPlaceholderEdge}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddPipeLineModal;
|
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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 (reference) url('../../../../styles/variables.less');
|
||||
|
||||
.edge-option-container {
|
||||
margin-top: 6px;
|
||||
width: 100%;
|
||||
height: 310px;
|
||||
overflow: scroll;
|
||||
|
||||
.edge-option-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 8px 0;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
background: @primary-color-hover;
|
||||
|
||||
&:hover {
|
||||
background: @primary-color-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: @body-dark-bg-color;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
color: @text-grey-muted;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
}
|
@ -118,7 +118,7 @@ import Loader from '../../Loader/Loader';
|
||||
import { useTourProvider } from '../../TourProvider/TourProvider';
|
||||
import EdgeInfoDrawer from '../EntityInfoDrawer/EdgeInfoDrawer.component';
|
||||
import EntityInfoDrawer from '../EntityInfoDrawer/EntityInfoDrawer.component';
|
||||
import AddPipeLineModal from './AddPipeLineModal';
|
||||
import AddPipeLineModal from './AppPipelineModel/AddPipeLineModal';
|
||||
import CustomControlsComponent from './CustomControls.component';
|
||||
import './entity-lineage.style.less';
|
||||
import {
|
||||
@ -398,6 +398,13 @@ const EntityLineageComponent: FunctionComponent<EntityLineageProp> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleModalCancel = () => {
|
||||
setSelectedEdgeId(undefined);
|
||||
setShowAddEdgeModal(false);
|
||||
setSelectedEdge({} as SelectedEdge);
|
||||
setEdgeOptions([]);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param data selected edge
|
||||
@ -440,6 +447,7 @@ const EntityLineageComponent: FunctionComponent<EntityLineageProp> = ({
|
||||
|
||||
resetSelectedData();
|
||||
setConfirmDelete(false);
|
||||
handleModalCancel();
|
||||
}
|
||||
};
|
||||
|
||||
@ -999,18 +1007,6 @@ const EntityLineageComponent: FunctionComponent<EntityLineageProp> = ({
|
||||
setSelectedEdgeId(value);
|
||||
};
|
||||
|
||||
const handleModalCancel = () => {
|
||||
setSelectedEdgeId(undefined);
|
||||
setShowAddEdgeModal(false);
|
||||
setSelectedEdge({} as SelectedEdge);
|
||||
setEdgeOptions([]);
|
||||
};
|
||||
|
||||
const onEdgeSelectionClear = () => {
|
||||
setSelectedEdgeId(undefined);
|
||||
setEdgeSearchValue('');
|
||||
};
|
||||
|
||||
const handleModalSave = () => {
|
||||
if (selectedEdge.data && updatedLineageData) {
|
||||
setStatus('waiting');
|
||||
@ -1031,7 +1027,8 @@ const EntityLineageComponent: FunctionComponent<EntityLineageProp> = ({
|
||||
selectedEdgeValue,
|
||||
selectedEdgeId,
|
||||
selectedEdge.data,
|
||||
(edgeDetails?.type as EntityType) ?? EntityType.PIPELINE
|
||||
(edgeDetails?.type as EntityType) ?? EntityType.PIPELINE,
|
||||
edgeDetails
|
||||
);
|
||||
|
||||
addLineageHandler(newEdge)
|
||||
@ -1417,14 +1414,29 @@ const EntityLineageComponent: FunctionComponent<EntityLineageProp> = ({
|
||||
SearchIndex.PIPELINE,
|
||||
SearchIndex.STORED_PROCEDURE,
|
||||
]);
|
||||
setEdgeOptions(
|
||||
data.data.hits.hits.map((hit) =>
|
||||
getEntityReferenceFromEntity(
|
||||
hit._source,
|
||||
hit._source.entityType as EntityType
|
||||
)
|
||||
|
||||
const selectedPipeline = selectedEdge.data?.pipeline;
|
||||
|
||||
const edgeOptions = data.data.hits.hits.map((hit) =>
|
||||
getEntityReferenceFromEntity(
|
||||
hit._source,
|
||||
hit._source.entityType as EntityType
|
||||
)
|
||||
);
|
||||
|
||||
const optionContainItem = edgeOptions.find(
|
||||
(item) => item.id === selectedEdgeId
|
||||
);
|
||||
|
||||
setEdgeOptions([
|
||||
...(selectedPipeline &&
|
||||
isEmpty(optionContainItem) &&
|
||||
isEmpty(edgeSearchValue) &&
|
||||
selectedPipeline.id === selectedEdgeId
|
||||
? [selectedPipeline]
|
||||
: []),
|
||||
...edgeOptions,
|
||||
]);
|
||||
} catch (error) {
|
||||
showErrorToast(
|
||||
error as AxiosError,
|
||||
@ -1630,10 +1642,10 @@ const EntityLineageComponent: FunctionComponent<EntityLineageProp> = ({
|
||||
}, [selectedEdge, confirmDelete]);
|
||||
|
||||
useEffect(() => {
|
||||
if (edgeSearchValue) {
|
||||
if (showAddEdgeModal) {
|
||||
getSearchResults(edgeSearchValue);
|
||||
}
|
||||
}, [edgeSearchValue]);
|
||||
}, [edgeSearchValue, showAddEdgeModal]);
|
||||
|
||||
useEffect(() => {
|
||||
edgesRef.current = edges;
|
||||
@ -1775,7 +1787,6 @@ const EntityLineageComponent: FunctionComponent<EntityLineageProp> = ({
|
||||
edgeSearchValue={edgeSearchValue}
|
||||
selectedEdgeId={selectedEdgeId}
|
||||
showAddEdgeModal={showAddEdgeModal}
|
||||
onClear={onEdgeSelectionClear}
|
||||
onModalCancel={handleModalCancel}
|
||||
onRemoveEdgeClick={handleRemoveEdgeClick}
|
||||
onSave={handleModalSave}
|
||||
|
@ -410,10 +410,12 @@ const TagsContainerV2 = ({
|
||||
{header}
|
||||
{tagBody}
|
||||
|
||||
<Space align="baseline" className="m-t-xs w-full" size="middle">
|
||||
{showBottomEditButton && !showInlineEditButton && editTagButton}
|
||||
{children}
|
||||
</Space>
|
||||
{(children || showBottomEditButton) && (
|
||||
<Space align="baseline" className="m-t-xs w-full" size="middle">
|
||||
{showBottomEditButton && !showInlineEditButton && editTagButton}
|
||||
{children}
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -824,7 +824,8 @@ export const getNewLineageConnectionDetails = (
|
||||
selectedEdgeValue: EntityLineageEdge | undefined,
|
||||
selectedPipelineId: string | undefined,
|
||||
customEdgeData: CustomEdgeData,
|
||||
type: EntityType
|
||||
type: EntityType,
|
||||
edgeDetails?: EntityReference
|
||||
) => {
|
||||
const { source, sourceType, target, targetType } = customEdgeData;
|
||||
const updatedLineageDetails: LineageDetails = {
|
||||
@ -836,6 +837,7 @@ export const getNewLineageConnectionDetails = (
|
||||
: {
|
||||
id: selectedPipelineId,
|
||||
type,
|
||||
fullyQualifiedName: edgeDetails?.fullyQualifiedName ?? '',
|
||||
},
|
||||
};
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user