mirror of
				https://github.com/open-metadata/OpenMetadata.git
				synced 2025-10-31 02:29:03 +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
	 Sachin Chaurasiya
						Sachin Chaurasiya