mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-07-25 18:30:00 +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,
|
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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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 };
|
||||||
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user