UI : Improve pipeline execution tree view (#8887)

* UI : Improve pipeline execution tree view

* Addressing review comments
This commit is contained in:
Sachin Chaurasiya 2022-11-19 12:35:16 +05:30 committed by GitHub
parent 8ef6b8cf74
commit 6ef9b718ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 197 additions and 47 deletions

View File

@ -37,7 +37,7 @@ import {
EXECUTION_FILTER_RANGE,
MenuOptions,
} from '../../constants/execution.constants';
import { PipelineStatus } from '../../generated/entity/data/pipeline';
import { PipelineStatus, Task } from '../../generated/entity/data/pipeline';
import {
getCurrentDateTimeStamp,
getPastDatesTimeStampFromCurrentDate,
@ -50,9 +50,10 @@ import TreeViewTab from './TreeView/TreeViewTab.component';
interface ExecutionProps {
pipelineFQN: string;
tasks: Task[];
}
const ExecutionsTab = ({ pipelineFQN }: ExecutionProps) => {
const ExecutionsTab = ({ pipelineFQN, tasks }: ExecutionProps) => {
const { t } = useTranslation();
const listViewLabel = t('label.list');
@ -217,6 +218,7 @@ const ExecutionsTab = ({ pipelineFQN }: ExecutionProps) => {
executions={executions}
startTime={startTime}
status={status}
tasks={tasks}
/>
)}
</Col>

View File

@ -10,13 +10,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Card, Col, Empty, Row, Space, Tooltip, Typography } from 'antd';
import { isEmpty, uniqueId } from 'lodash';
import { Card, Col, Empty, Row, Typography } from 'antd';
import Tree from 'antd/lib/tree';
import { isEmpty } from 'lodash';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PipelineStatus } from '../../../generated/entity/data/pipeline';
import { getTreeViewData } from '../../../utils/executionUtils';
import { getStatusBadgeIcon } from '../../../utils/PipelineDetailsUtils';
import { PipelineStatus, Task } from '../../../generated/entity/data/pipeline';
import { getTreeData, getTreeViewData } from '../../../utils/executionUtils';
import SVGIcons, { Icons } from '../../../utils/SvgUtils';
import { formatDateTimeFromSeconds } from '../../../utils/TimeUtils';
import './tree-view-tab.less';
@ -26,6 +26,7 @@ interface TreeViewProps {
status: string;
startTime: number;
endTime: number;
tasks: Task[];
}
const TreeViewTab = ({
@ -33,12 +34,18 @@ const TreeViewTab = ({
status,
startTime,
endTime,
tasks,
}: TreeViewProps) => {
const viewData = useMemo(
() => getTreeViewData(executions as PipelineStatus[], status),
[executions, status]
);
const { treeDataList, treeLabelList } = useMemo(
() => getTreeData(tasks, viewData),
[tasks, viewData]
);
const { t } = useTranslation();
return (
@ -67,41 +74,26 @@ const TreeViewTab = ({
description={t('label.no-execution-runs-found')}
/>
)}
{Object.entries(viewData).map(([key, value]) => {
return (
<Row gutter={16} key={uniqueId()}>
<Col span={5}>
<Space>
<div className="tree-view-dot" />
<Typography.Text type="secondary">{key}</Typography.Text>
</Space>
</Col>
<Col span={19}>
<div className="execution-node-container">
{value.map((status) => (
<Tooltip
key={uniqueId()}
placement="top"
title={
<Space direction="vertical">
<div>{status.timestamp}</div>
<div>{status.executionStatus}</div>
</Space>
}>
<SVGIcons
alt="result"
className="tw-w-6 mr-2 mb-2"
icon={getStatusBadgeIcon(status.executionStatus)}
/>
</Tooltip>
))}
</div>
</Col>
</Row>
);
})}
<Row className="w-full">
<Col span={6}>
<Tree
defaultExpandAll
showIcon
showLine={{ showLeafIcon: false }}
switcherIcon={<></>}
treeData={treeLabelList}
/>
</Col>
<Col span={18}>
<Tree
defaultExpandAll
showIcon
className="tree-without-indent"
switcherIcon={<></>}
treeData={treeDataList}
/>
</Col>
</Row>
</Card>
);
};

View File

@ -22,3 +22,9 @@
border-radius: 6px;
border: 6px solid #cccccc;
}
.tree-without-indent {
.ant-tree-indent {
display: none;
}
}

View File

@ -947,7 +947,7 @@ const PipelineDetails = ({
{t('label.executions')}
</span>
}>
<ExecutionsTab pipelineFQN={pipelineFQN} />
<ExecutionsTab pipelineFQN={pipelineFQN} tasks={tasks} />
</Tabs.TabPane>
<Tabs.TabPane

View File

@ -10,11 +10,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Space } from 'antd';
import { groupBy, isUndefined, toLower } from 'lodash';
import React from 'react';
import { Col, Row, Space, Tooltip } from 'antd';
import { DataNode } from 'antd/lib/tree';
import { groupBy, isUndefined, map, toLower, uniqueId } from 'lodash';
import React, { ReactNode } from 'react';
import { MenuOptions } from '../constants/execution.constants';
import { PipelineStatus, StatusType } from '../generated/entity/data/pipeline';
import {
PipelineStatus,
StatusType,
Task,
} from '../generated/entity/data/pipeline';
import { getStatusBadgeIcon } from './PipelineDetailsUtils';
import SVGIcons from './SvgUtils';
import { formatDateTimeFromSeconds } from './TimeUtils';
@ -106,3 +111,148 @@ export const getStatusLabel = (status: string) => {
return;
}
};
export const getExecutionElementByKey = (
key: string,
viewElements: {
key: string;
value: ReactNode;
}[]
) => viewElements.find((v) => v.key === key);
// check if current task is downstream task of other tasks
const checkIsDownStreamTask = (currentTask: Task, tasks: Task[]) =>
tasks.some((taskData) =>
taskData.downstreamTasks?.includes(currentTask.name)
);
export const getTreeData = (
tasks: Task[],
viewData: Record<string, ViewDataInterface[]>
) => {
const icon = <div className="tree-view-dot" />;
let treeDataList: DataNode[] = [];
let treeLabelList: DataNode[] = [];
// map execution element to task name
const viewElements = map(viewData, (value, key) => ({
key,
value: (
<Row gutter={16} key={uniqueId()}>
<Col>
<div className="execution-node-container">
{value.map((status) => (
<Tooltip
key={uniqueId()}
placement="top"
title={
<Space direction="vertical">
<div>{status.timestamp}</div>
<div>{status.executionStatus}</div>
</Space>
}>
<SVGIcons
alt="result"
className="tw-w-6 mr-2 mb-2"
icon={getStatusBadgeIcon(status.executionStatus)}
/>
</Tooltip>
))}
</div>
</Col>
</Row>
),
}));
for (const task of tasks) {
const taskName = task.name;
// list of downstream tasks
const downstreamTasks = task.downstreamTasks ?? [];
// check has downstream tasks or not
const hasDownStream = Boolean(downstreamTasks.length);
// check if current task is downstream task
const isDownStreamTask = checkIsDownStreamTask(task, tasks);
// check if it's an existing tree data
const existingData = treeDataList.find((tData) => tData.key === taskName);
// check if it's an existing label data
const existingLabel = treeLabelList.find((lData) => lData.key === taskName);
// get the execution element for current task
const currentViewElement = getExecutionElementByKey(taskName, viewElements);
const currentTreeData = {
key: taskName,
title: currentViewElement?.value ?? null,
};
const currentLabelData = {
key: taskName,
title: taskName,
icon,
};
// skip the down stream node as it will be render by the parent task
if (isDownStreamTask) continue;
else if (hasDownStream) {
const dataChildren: DataNode[] = [];
const labelChildren: DataNode[] = [];
// get execution list of downstream tasks
for (const downstreamTask of downstreamTasks) {
const taskElement = getExecutionElementByKey(
downstreamTask,
viewElements
);
dataChildren.push({
key: downstreamTask,
title: taskElement?.value ?? null,
});
labelChildren.push({
key: downstreamTask,
title: downstreamTask,
icon,
});
}
/**
* if not existing data then push current tree data to tree data list
* else modified the existing data
*/
treeDataList = isUndefined(existingData)
? [...treeDataList, { ...currentTreeData, children: dataChildren }]
: treeDataList.map((currentData) => {
if (currentData.key === existingData.key) {
return { ...existingData, children: dataChildren };
} else {
return currentData;
}
});
treeLabelList = isUndefined(existingLabel)
? [...treeLabelList, { ...currentLabelData, children: labelChildren }]
: treeLabelList.map((currentData) => {
if (currentData.key === existingLabel.key) {
return { ...existingLabel, children: labelChildren };
} else {
return currentData;
}
});
} else {
treeDataList = isUndefined(existingData)
? [...treeDataList, currentTreeData]
: treeDataList;
treeLabelList = isUndefined(existingLabel)
? [...treeLabelList, currentLabelData]
: treeLabelList;
}
}
return { treeDataList, treeLabelList };
};