mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-07-24 09:50:01 +00:00
UI : Improve pipeline execution tree view (#8887)
* UI : Improve pipeline execution tree view * Addressing review comments
This commit is contained in:
parent
8ef6b8cf74
commit
6ef9b718ab
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -22,3 +22,9 @@
|
||||
border-radius: 6px;
|
||||
border: 6px solid #cccccc;
|
||||
}
|
||||
|
||||
.tree-without-indent {
|
||||
.ant-tree-indent {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -947,7 +947,7 @@ const PipelineDetails = ({
|
||||
{t('label.executions')}
|
||||
</span>
|
||||
}>
|
||||
<ExecutionsTab pipelineFQN={pipelineFQN} />
|
||||
<ExecutionsTab pipelineFQN={pipelineFQN} tasks={tasks} />
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane
|
||||
|
@ -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 };
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user