#14023 Unable to Zoom on graph view of tasks under airflow dag (#14154)

* #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:
Shailesh Parmar 2024-01-04 21:30:41 +05:30 committed by GitHub
parent 4b1befacaf
commit 65a910f885
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 171 additions and 53 deletions

View File

@ -75,6 +75,7 @@ import EntityRightPanel from '../Entity/EntityRightPanel/EntityRightPanel';
import { ModalWithMarkdownEditor } from '../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
import { usePermissionProvider } from '../PermissionProvider/PermissionProvider';
import { ResourceEntity } from '../PermissionProvider/PermissionProvider.interface';
import './pipeline-details.style.less';
import { PipeLineDetailsProp } from './PipelineDetails.interface';
const PipelineDetails = ({
@ -524,7 +525,7 @@ const PipelineDetails = ({
const tasksDAGView = useMemo(
() =>
!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">
<TasksDAGView
selectedExec={selectedExecution}

View File

@ -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;
}
}

View File

@ -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);
});
});

View File

@ -11,16 +11,17 @@
* limitations under the License.
*/
import { Space } from 'antd';
import classNames from 'classnames';
import React, { CSSProperties, Fragment } from 'react';
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 = {
width: '8px',
height: '8px',
borderRadius: '50%',
position: 'absolute',
top: 10,
opacity: 0,
height: '1px',
width: '1px',
};
const renderHandle = (position: Position, isConnectable: boolean) => {
@ -28,8 +29,10 @@ const renderHandle = (position: Position, isConnectable: boolean) => {
let type: HandleType;
if (position === Position.Left) {
type = 'target';
styles.left = '10px';
} else {
type = 'source';
styles.right = '10px';
}
return (
@ -59,15 +62,24 @@ const getHandle = (nodeType: string, isConnectable: boolean) => {
const TaskNode = (props: NodeProps) => {
const { data, type, isConnectable } = props;
const { label } = data;
const { label, taskStatus } = data;
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)}
{/* 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}
</div>
</Space>
</div>
);
};

View File

@ -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;
}

View File

@ -11,21 +11,27 @@
* limitations under the License.
*/
import classNames from 'classnames';
import React, { Fragment, useCallback, useEffect, useMemo } from 'react';
import ReactFlow, {
Background,
Controls,
Edge,
MarkerType,
useEdgesState,
useNodesState,
} from 'reactflow';
import {
MAX_ZOOM_VALUE,
MIN_ZOOM_VALUE,
} from '../../constants/Lineage.constants';
import { EntityLineageNodeType } from '../../enums/entity.enum';
import { PipelineStatus, Task } from '../../generated/entity/data/pipeline';
import { replaceSpaceWith_ } from '../../utils/CommonUtils';
import { getLayoutedElements, onLoad } from '../../utils/EntityLineageUtils';
import { getEntityName } from '../../utils/EntityUtils';
import { getTaskExecStatus } from '../../utils/PipelineDetailsUtils';
import TaskNode from './TaskNode';
import TaskNode from './TaskNode/TaskNode';
import './tasks-dag-view.style.less';
export interface Props {
tasks: Task[];
@ -72,11 +78,12 @@ const TasksDAGView = ({ tasks, selectedExec }: Props) => {
);
return {
className: classNames('leaf-node', taskStatus),
className: 'leaf-node',
id: replaceSpaceWith_(task.name),
type: getNodeType(task),
data: {
label: getEntityName(task),
taskStatus,
},
position: { x: 0, y: 0 },
isConnectable: false,
@ -114,8 +121,8 @@ const TasksDAGView = ({ tasks, selectedExec }: Props) => {
<ReactFlow
data-testid="react-flow-component"
edges={edgesData}
maxZoom={2}
minZoom={0.5}
maxZoom={MAX_ZOOM_VALUE}
minZoom={MIN_ZOOM_VALUE}
nodeTypes={nodeTypes}
nodes={nodesData}
selectNodesOnDrag={false}
@ -125,8 +132,14 @@ const TasksDAGView = ({ tasks, selectedExec }: Props) => {
onInit={(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 />
);

View File

@ -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;
}
}

View File

@ -605,35 +605,6 @@ a[href].link-text-grey,
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 {
color: @text-grey-muted !important;
}
@ -658,12 +629,6 @@ a[href].link-text-grey,
.react-flow__node {
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 {
pointer-events: all;
cursor: pointer;

View File

@ -87,6 +87,9 @@
.top-1 {
top: 4px;
}
.top-4 {
top: 16px;
}
.top-6 {
top: 24px;
}