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

View File

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

View File

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

View File

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

View File

@ -10,11 +10,16 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { Space } from 'antd'; import { Col, Row, Space, Tooltip } from 'antd';
import { groupBy, isUndefined, toLower } from 'lodash'; import { DataNode } from 'antd/lib/tree';
import React from 'react'; import { groupBy, isUndefined, map, toLower, uniqueId } from 'lodash';
import React, { ReactNode } from 'react';
import { MenuOptions } from '../constants/execution.constants'; 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 { getStatusBadgeIcon } from './PipelineDetailsUtils';
import SVGIcons from './SvgUtils'; import SVGIcons from './SvgUtils';
import { formatDateTimeFromSeconds } from './TimeUtils'; import { formatDateTimeFromSeconds } from './TimeUtils';
@ -106,3 +111,148 @@ export const getStatusLabel = (status: string) => {
return; 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 };
};