diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Execution/Execution.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Execution/Execution.component.tsx index ab1a88563bc..d6d9efe044d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Execution/Execution.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Execution/Execution.component.tsx @@ -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} /> )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Execution/TreeView/TreeViewTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Execution/TreeView/TreeViewTab.component.tsx index aff6f7f83db..ae2a0de09ff 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Execution/TreeView/TreeViewTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Execution/TreeView/TreeViewTab.component.tsx @@ -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 ( - - - -
- {key} - - - - -
- {value.map((status) => ( - -
{status.timestamp}
-
{status.executionStatus}
- - }> - -
- ))} -
- - - ); - })} + + + } + treeData={treeLabelList} + /> + + + } + treeData={treeDataList} + /> + + ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Execution/TreeView/tree-view-tab.less b/openmetadata-ui/src/main/resources/ui/src/components/Execution/TreeView/tree-view-tab.less index 89d03e87db1..7a061fcada8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Execution/TreeView/tree-view-tab.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/Execution/TreeView/tree-view-tab.less @@ -22,3 +22,9 @@ border-radius: 6px; border: 6px solid #cccccc; } + +.tree-without-indent { + .ant-tree-indent { + display: none; + } +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/PipelineDetails/PipelineDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/PipelineDetails/PipelineDetails.component.tsx index 9d520925e60..627632f2535 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/PipelineDetails/PipelineDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/PipelineDetails/PipelineDetails.component.tsx @@ -947,7 +947,7 @@ const PipelineDetails = ({ {t('label.executions')} }> - + { 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 +) => { + const icon =
; + let treeDataList: DataNode[] = []; + let treeLabelList: DataNode[] = []; + + // map execution element to task name + const viewElements = map(viewData, (value, key) => ({ + key, + value: ( + + +
+ {value.map((status) => ( + +
{status.timestamp}
+
{status.executionStatus}
+ + }> + +
+ ))} +
+ +
+ ), + })); + + 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 }; +};