mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-12 17:02:23 +00:00
* #14023 Unable to Zoom on graph view of tasks under airflow dag * added dependency * updated style as per the mock * updated controls as per new style and addressing review comment
This commit is contained in:
parent
4b1befacaf
commit
65a910f885
@ -75,6 +75,7 @@ import EntityRightPanel from '../Entity/EntityRightPanel/EntityRightPanel';
|
|||||||
import { ModalWithMarkdownEditor } from '../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
|
import { ModalWithMarkdownEditor } from '../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
|
||||||
import { usePermissionProvider } from '../PermissionProvider/PermissionProvider';
|
import { usePermissionProvider } from '../PermissionProvider/PermissionProvider';
|
||||||
import { ResourceEntity } from '../PermissionProvider/PermissionProvider.interface';
|
import { ResourceEntity } from '../PermissionProvider/PermissionProvider.interface';
|
||||||
|
import './pipeline-details.style.less';
|
||||||
import { PipeLineDetailsProp } from './PipelineDetails.interface';
|
import { PipeLineDetailsProp } from './PipelineDetails.interface';
|
||||||
|
|
||||||
const PipelineDetails = ({
|
const PipelineDetails = ({
|
||||||
@ -524,7 +525,7 @@ const PipelineDetails = ({
|
|||||||
const tasksDAGView = useMemo(
|
const tasksDAGView = useMemo(
|
||||||
() =>
|
() =>
|
||||||
!isEmpty(pipelineDetails.tasks) && !isUndefined(pipelineDetails.tasks) ? (
|
!isEmpty(pipelineDetails.tasks) && !isUndefined(pipelineDetails.tasks) ? (
|
||||||
<Card headStyle={{ background: '#fafafa' }} title={t('label.dag-view')}>
|
<Card className="task-dag-view-card" title={t('label.dag-view')}>
|
||||||
<div className="h-100">
|
<div className="h-100">
|
||||||
<TasksDAGView
|
<TasksDAGView
|
||||||
selectedExec={selectedExecution}
|
selectedExec={selectedExecution}
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* 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');
|
||||||
|
.task-dag-view-card {
|
||||||
|
.ant-card-body {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
.ant-card-head {
|
||||||
|
background: @grey-1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* 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 { render, screen } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { NodeProps } from 'reactflow';
|
||||||
|
import TaskNode from './TaskNode';
|
||||||
|
|
||||||
|
jest.mock('reactflow', () => ({
|
||||||
|
...jest.requireActual('reactflow'),
|
||||||
|
Handle: jest.fn().mockImplementation(() => <div>Handle</div>),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockProps = {
|
||||||
|
data: {
|
||||||
|
label: 'Test Label',
|
||||||
|
taskStatus: 'Success',
|
||||||
|
},
|
||||||
|
} as NodeProps;
|
||||||
|
|
||||||
|
describe('TaskNode', () => {
|
||||||
|
it('component should render', async () => {
|
||||||
|
render(<TaskNode {...mockProps} />);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await screen.findByTestId('task-node-container')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(await screen.findByTestId('node-label')).toBeInTheDocument();
|
||||||
|
expect(await screen.findByTestId('node-label-status')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render correct label', async () => {
|
||||||
|
render(<TaskNode {...mockProps} />);
|
||||||
|
|
||||||
|
expect(await screen.findByText(mockProps.data.label)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should append class based on status', async () => {
|
||||||
|
render(<TaskNode {...mockProps} />);
|
||||||
|
const status = await screen.findByTestId('node-label-status');
|
||||||
|
|
||||||
|
expect(status).toHaveClass(mockProps.data.taskStatus);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -11,16 +11,17 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Space } from 'antd';
|
||||||
|
import classNames from 'classnames';
|
||||||
import React, { CSSProperties, Fragment } from 'react';
|
import React, { CSSProperties, Fragment } from 'react';
|
||||||
import { Handle, HandleType, NodeProps, Position } from 'reactflow';
|
import { Handle, HandleType, NodeProps, Position } from 'reactflow';
|
||||||
import { EntityLineageNodeType } from '../../enums/entity.enum';
|
import { EntityLineageNodeType } from '../../../enums/entity.enum';
|
||||||
|
import './task-node.style.less';
|
||||||
|
|
||||||
const handleStyles = {
|
const handleStyles = {
|
||||||
width: '8px',
|
opacity: 0,
|
||||||
height: '8px',
|
height: '1px',
|
||||||
borderRadius: '50%',
|
width: '1px',
|
||||||
position: 'absolute',
|
|
||||||
top: 10,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderHandle = (position: Position, isConnectable: boolean) => {
|
const renderHandle = (position: Position, isConnectable: boolean) => {
|
||||||
@ -28,8 +29,10 @@ const renderHandle = (position: Position, isConnectable: boolean) => {
|
|||||||
let type: HandleType;
|
let type: HandleType;
|
||||||
if (position === Position.Left) {
|
if (position === Position.Left) {
|
||||||
type = 'target';
|
type = 'target';
|
||||||
|
styles.left = '10px';
|
||||||
} else {
|
} else {
|
||||||
type = 'source';
|
type = 'source';
|
||||||
|
styles.right = '10px';
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -59,15 +62,24 @@ const getHandle = (nodeType: string, isConnectable: boolean) => {
|
|||||||
|
|
||||||
const TaskNode = (props: NodeProps) => {
|
const TaskNode = (props: NodeProps) => {
|
||||||
const { data, type, isConnectable } = props;
|
const { data, type, isConnectable } = props;
|
||||||
const { label } = data;
|
const { label, taskStatus } = data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="task-node relative nowheel bg-primary-lite border border-primary rounded-6 p-x-sm">
|
<div
|
||||||
|
className="task-node relative nowheel border rounded-6 p-x-sm"
|
||||||
|
data-testid="task-node-container">
|
||||||
{getHandle(type, isConnectable)}
|
{getHandle(type, isConnectable)}
|
||||||
{/* Node label could be simple text or reactNode */}
|
{/* Node label could be simple text or reactNode */}
|
||||||
<div className="p-x-sm p-y-sm" data-testid="node-label">
|
<Space className="p-x-sm p-y-sm w-full" data-testid="node-label">
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'custom-node-label',
|
||||||
|
taskStatus ? taskStatus : 'bg-primary'
|
||||||
|
)}
|
||||||
|
data-testid="node-label-status"
|
||||||
|
/>
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* 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');
|
||||||
|
|
||||||
|
.custom-node-label {
|
||||||
|
border-radius: 50%;
|
||||||
|
height: 10px;
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaf-node .task-node .Successful {
|
||||||
|
background-color: @green-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaf-node .task-node .Failed {
|
||||||
|
background-color: @red-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaf-node .task-node .Pending {
|
||||||
|
background-color: @yellow-2;
|
||||||
|
}
|
||||||
@ -11,21 +11,27 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import React, { Fragment, useCallback, useEffect, useMemo } from 'react';
|
import React, { Fragment, useCallback, useEffect, useMemo } from 'react';
|
||||||
import ReactFlow, {
|
import ReactFlow, {
|
||||||
|
Background,
|
||||||
|
Controls,
|
||||||
Edge,
|
Edge,
|
||||||
MarkerType,
|
MarkerType,
|
||||||
useEdgesState,
|
useEdgesState,
|
||||||
useNodesState,
|
useNodesState,
|
||||||
} from 'reactflow';
|
} from 'reactflow';
|
||||||
|
import {
|
||||||
|
MAX_ZOOM_VALUE,
|
||||||
|
MIN_ZOOM_VALUE,
|
||||||
|
} from '../../constants/Lineage.constants';
|
||||||
import { EntityLineageNodeType } from '../../enums/entity.enum';
|
import { EntityLineageNodeType } from '../../enums/entity.enum';
|
||||||
import { PipelineStatus, Task } from '../../generated/entity/data/pipeline';
|
import { PipelineStatus, Task } from '../../generated/entity/data/pipeline';
|
||||||
import { replaceSpaceWith_ } from '../../utils/CommonUtils';
|
import { replaceSpaceWith_ } from '../../utils/CommonUtils';
|
||||||
import { getLayoutedElements, onLoad } from '../../utils/EntityLineageUtils';
|
import { getLayoutedElements, onLoad } from '../../utils/EntityLineageUtils';
|
||||||
import { getEntityName } from '../../utils/EntityUtils';
|
import { getEntityName } from '../../utils/EntityUtils';
|
||||||
import { getTaskExecStatus } from '../../utils/PipelineDetailsUtils';
|
import { getTaskExecStatus } from '../../utils/PipelineDetailsUtils';
|
||||||
import TaskNode from './TaskNode';
|
import TaskNode from './TaskNode/TaskNode';
|
||||||
|
import './tasks-dag-view.style.less';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
tasks: Task[];
|
tasks: Task[];
|
||||||
@ -72,11 +78,12 @@ const TasksDAGView = ({ tasks, selectedExec }: Props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
className: classNames('leaf-node', taskStatus),
|
className: 'leaf-node',
|
||||||
id: replaceSpaceWith_(task.name),
|
id: replaceSpaceWith_(task.name),
|
||||||
type: getNodeType(task),
|
type: getNodeType(task),
|
||||||
data: {
|
data: {
|
||||||
label: getEntityName(task),
|
label: getEntityName(task),
|
||||||
|
taskStatus,
|
||||||
},
|
},
|
||||||
position: { x: 0, y: 0 },
|
position: { x: 0, y: 0 },
|
||||||
isConnectable: false,
|
isConnectable: false,
|
||||||
@ -114,8 +121,8 @@ const TasksDAGView = ({ tasks, selectedExec }: Props) => {
|
|||||||
<ReactFlow
|
<ReactFlow
|
||||||
data-testid="react-flow-component"
|
data-testid="react-flow-component"
|
||||||
edges={edgesData}
|
edges={edgesData}
|
||||||
maxZoom={2}
|
maxZoom={MAX_ZOOM_VALUE}
|
||||||
minZoom={0.5}
|
minZoom={MIN_ZOOM_VALUE}
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
nodes={nodesData}
|
nodes={nodesData}
|
||||||
selectNodesOnDrag={false}
|
selectNodesOnDrag={false}
|
||||||
@ -125,8 +132,14 @@ const TasksDAGView = ({ tasks, selectedExec }: Props) => {
|
|||||||
onInit={(reactFlowInstance) => {
|
onInit={(reactFlowInstance) => {
|
||||||
onLoad(reactFlowInstance);
|
onLoad(reactFlowInstance);
|
||||||
}}
|
}}
|
||||||
onNodesChange={onNodesChange}
|
onNodesChange={onNodesChange}>
|
||||||
|
<Background gap={12} size={1} />
|
||||||
|
<Controls
|
||||||
|
className="task-dag-control-btn"
|
||||||
|
position="bottom-right"
|
||||||
|
showInteractive={false}
|
||||||
/>
|
/>
|
||||||
|
</ReactFlow>
|
||||||
) : (
|
) : (
|
||||||
<Fragment />
|
<Fragment />
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 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.
|
||||||
|
*/
|
||||||
|
.task-dag-control-btn {
|
||||||
|
.react-flow__controls-button {
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -605,35 +605,6 @@ a[href].link-text-grey,
|
|||||||
padding-right: 60px !important;
|
padding-right: 60px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ********* Leaf Node **********
|
|
||||||
|
|
||||||
.leaf-node.Successful .task-node {
|
|
||||||
border-color: @green-1;
|
|
||||||
background-color: @success-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaf-node.Failed .task-node {
|
|
||||||
border-color: @failed-color;
|
|
||||||
background-color: @error-light-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaf-node.Pending .task-node {
|
|
||||||
border-color: @primary-color;
|
|
||||||
background-color: @link-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaf-node.Successful .react-flow__handle {
|
|
||||||
background-color: @green-1 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaf-node.Failed .react-flow__handle {
|
|
||||||
background-color: @failed-color !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaf-node.Pending .react-flow__handle {
|
|
||||||
background-color: @primary-color !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror .placeholder {
|
.ProseMirror .placeholder {
|
||||||
color: @text-grey-muted !important;
|
color: @text-grey-muted !important;
|
||||||
}
|
}
|
||||||
@ -658,12 +629,6 @@ a[href].link-text-grey,
|
|||||||
.react-flow__node {
|
.react-flow__node {
|
||||||
min-width: max-content;
|
min-width: max-content;
|
||||||
}
|
}
|
||||||
.leaf-node .react-flow__handle {
|
|
||||||
background-color: @text-grey-muted;
|
|
||||||
}
|
|
||||||
.leaf-node.core .react-flow__handle {
|
|
||||||
background-color: @primary-color;
|
|
||||||
}
|
|
||||||
.react-flow__edge {
|
.react-flow__edge {
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@ -87,6 +87,9 @@
|
|||||||
.top-1 {
|
.top-1 {
|
||||||
top: 4px;
|
top: 4px;
|
||||||
}
|
}
|
||||||
|
.top-4 {
|
||||||
|
top: 16px;
|
||||||
|
}
|
||||||
.top-6 {
|
.top-6 {
|
||||||
top: 24px;
|
top: 24px;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user