From a430284aa21e3ae1f0d5654e55b2ad2852519cc2 Mon Sep 17 00:00:00 2001
From: wwf <yearningwang@iqtogether.com>
Date: 星期三, 04 六月 2025 15:17:49 +0800
Subject: [PATCH] 初始化

---
 app/components/workflow/run/index.tsx |  181 +++++++++++++++++++++++++++++++++++++++++++--
 1 files changed, 172 insertions(+), 9 deletions(-)

diff --git a/app/components/workflow/run/index.tsx b/app/components/workflow/run/index.tsx
index 8b99603..8b0319c 100644
--- a/app/components/workflow/run/index.tsx
+++ b/app/components/workflow/run/index.tsx
@@ -3,16 +3,21 @@
 import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
 import { useContext } from 'use-context-selector'
 import { useTranslation } from 'react-i18next'
+import { useBoolean } from 'ahooks'
+import { BlockEnum } from '../types'
 import OutputPanel from './output-panel'
 import ResultPanel from './result-panel'
 import TracingPanel from './tracing-panel'
+import IterationResultPanel from './iteration-result-panel'
+import RetryResultPanel from './retry-result-panel'
 import cn from '@/utils/classnames'
 import { ToastContext } from '@/app/components/base/toast'
 import Loading from '@/app/components/base/loading'
 import { fetchRunDetail, fetchTracingList } from '@/service/log'
-import type { NodeTracing } from '@/types/workflow'
+import type { IterationDurationMap, NodeTracing } from '@/types/workflow'
 import type { WorkflowRunDetailResponse } from '@/models/log'
 import { useStore as useAppStore } from '@/app/components/app/store'
+
 export type RunProps = {
   hideResult?: boolean
   activeTab?: 'RESULT' | 'DETAIL' | 'TRACING'
@@ -55,12 +60,124 @@
     }
   }, [notify, getResultCallback])
 
+  const formatNodeList = useCallback((list: NodeTracing[]) => {
+    const allItems = [...list].reverse()
+    const result: NodeTracing[] = []
+    const nodeGroupMap = new Map<string, Map<string, NodeTracing[]>>()
+
+    const processIterationNode = (item: NodeTracing) => {
+      result.push({
+        ...item,
+        details: [],
+      })
+    }
+
+    const updateParallelModeGroup = (runId: string, item: NodeTracing, iterationNode: NodeTracing) => {
+      if (!nodeGroupMap.has(iterationNode.node_id))
+        nodeGroupMap.set(iterationNode.node_id, new Map())
+
+      const groupMap = nodeGroupMap.get(iterationNode.node_id)!
+
+      if (!groupMap.has(runId)) {
+        groupMap.set(runId, [item])
+      }
+      else {
+        if (item.status === 'retry') {
+          const retryNode = groupMap.get(runId)!.find(node => node.node_id === item.node_id)
+
+          if (retryNode) {
+            if (retryNode?.retryDetail)
+              retryNode.retryDetail.push(item)
+            else
+              retryNode.retryDetail = [item]
+          }
+        }
+        else {
+          groupMap.get(runId)!.push(item)
+        }
+      }
+
+      if (item.status === 'failed') {
+        iterationNode.status = 'failed'
+        iterationNode.error = item.error
+      }
+
+      iterationNode.details = Array.from(groupMap.values())
+    }
+    const updateSequentialModeGroup = (index: number, item: NodeTracing, iterationNode: NodeTracing) => {
+      const { details } = iterationNode
+      if (details) {
+        if (!details[index]) {
+          details[index] = [item]
+        }
+        else {
+          if (item.status === 'retry') {
+            const retryNode = details[index].find(node => node.node_id === item.node_id)
+
+            if (retryNode) {
+              if (retryNode?.retryDetail)
+                retryNode.retryDetail.push(item)
+              else
+                retryNode.retryDetail = [item]
+            }
+          }
+          else {
+            details[index].push(item)
+          }
+        }
+      }
+
+      if (item.status === 'failed') {
+        iterationNode.status = 'failed'
+        iterationNode.error = item.error
+      }
+    }
+    const processNonIterationNode = (item: NodeTracing) => {
+      const { execution_metadata } = item
+      if (!execution_metadata?.iteration_id) {
+        if (item.status === 'retry') {
+          const retryNode = result.find(node => node.node_id === item.node_id)
+
+          if (retryNode) {
+            if (retryNode?.retryDetail)
+              retryNode.retryDetail.push(item)
+            else
+              retryNode.retryDetail = [item]
+          }
+
+          return
+        }
+        result.push(item)
+        return
+      }
+
+      const iterationNode = result.find(node => node.node_id === execution_metadata.iteration_id)
+      if (!iterationNode || !Array.isArray(iterationNode.details))
+        return
+
+      const { parallel_mode_run_id, iteration_index = 0 } = execution_metadata
+
+      if (parallel_mode_run_id)
+        updateParallelModeGroup(parallel_mode_run_id, item, iterationNode)
+      else
+        updateSequentialModeGroup(iteration_index, item, iterationNode)
+    }
+
+    allItems.forEach((item) => {
+      item.node_type === BlockEnum.Iteration
+        ? processIterationNode(item)
+        : processNonIterationNode(item)
+    })
+
+    return result
+  }, [])
+
   const getTracingList = useCallback(async (appID: string, runID: string) => {
     try {
       const { data: nodeList } = await fetchTracingList({
         url: `/apps/${appID}/workflow-runs/${runID}/node-executions`,
       })
-      setList(nodeList)
+      setList(formatNodeList(nodeList))
     }
     catch (err) {
       notify({
@@ -102,14 +219,50 @@
     adjustResultHeight()
   }, [loading])
 
+  const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[][]>([])
+  const [iterDurationMap, setIterDurationMap] = useState<IterationDurationMap>({})
+  const [retryRunResult, setRetryRunResult] = useState<NodeTracing[]>([])
+  const [isShowIterationDetail, {
+    setTrue: doShowIterationDetail,
+    setFalse: doHideIterationDetail,
+  }] = useBoolean(false)
+  const [isShowRetryDetail, {
+    setTrue: doShowRetryDetail,
+    setFalse: doHideRetryDetail,
+  }] = useBoolean(false)
+
+  const handleShowIterationDetail = useCallback((detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => {
+    setIterationRunResult(detail)
+    doShowIterationDetail()
+    setIterDurationMap(iterDurationMap)
+  }, [doShowIterationDetail, setIterationRunResult, setIterDurationMap])
+
+  const handleShowRetryDetail = useCallback((detail: NodeTracing[]) => {
+    setRetryRunResult(detail)
+    doShowRetryDetail()
+  }, [doShowRetryDetail, setRetryRunResult])
+
+  if (isShowIterationDetail) {
+    return (
+      <div className='grow relative flex flex-col'>
+        <IterationResultPanel
+          list={iterationRunResult}
+          onHide={doHideIterationDetail}
+          onBack={doHideIterationDetail}
+          iterDurationMap={iterDurationMap}
+        />
+      </div>
+    )
+  }
+
   return (
-    <div className='relative flex grow flex-col'>
+    <div className='grow relative flex flex-col'>
       {/* tab */}
-      <div className='flex shrink-0 items-center border-b-[0.5px] border-divider-subtle px-4'>
+      <div className='shrink-0 flex items-center px-4 border-b-[0.5px] border-divider-subtle'>
         {!hideResult && (
           <div
             className={cn(
-              'system-sm-semibold-uppercase mr-6 cursor-pointer border-b-2 border-transparent py-3 text-text-tertiary',
+              'mr-6 py-3 border-b-2 border-transparent system-sm-semibold-uppercase text-text-tertiary cursor-pointer',
               currentTab === 'RESULT' && '!border-util-colors-blue-brand-blue-brand-600 text-text-primary',
             )}
             onClick={() => switchTab('RESULT')}
@@ -117,21 +270,21 @@
         )}
         <div
           className={cn(
-            'system-sm-semibold-uppercase mr-6 cursor-pointer border-b-2 border-transparent py-3 text-text-tertiary',
+            'mr-6 py-3 border-b-2 border-transparent system-sm-semibold-uppercase text-text-tertiary cursor-pointer',
             currentTab === 'DETAIL' && '!border-util-colors-blue-brand-blue-brand-600 text-text-primary',
           )}
           onClick={() => switchTab('DETAIL')}
         >{t('runLog.detail')}</div>
         <div
           className={cn(
-            'system-sm-semibold-uppercase mr-6 cursor-pointer border-b-2 border-transparent py-3 text-text-tertiary',
+            'mr-6 py-3 border-b-2 border-transparent system-sm-semibold-uppercase text-text-tertiary cursor-pointer',
             currentTab === 'TRACING' && '!border-util-colors-blue-brand-blue-brand-600 text-text-primary',
           )}
           onClick={() => switchTab('TRACING')}
         >{t('runLog.tracing')}</div>
       </div>
       {/* panel detail */}
-      <div ref={ref} className={cn('relative h-0 grow overflow-y-auto rounded-b-2xl bg-components-panel-bg')}>
+      <div ref={ref} className={cn('grow bg-components-panel-bg h-0 overflow-y-auto rounded-b-2xl', currentTab !== 'DETAIL' && '!bg-background-section-burn')}>
         {loading && (
           <div className='flex h-full items-center justify-center bg-components-panel-bg'>
             <Loading />
@@ -158,12 +311,22 @@
             exceptionCounts={runDetail.exceptions_count}
           />
         )}
-        {!loading && currentTab === 'TRACING' && (
+        {!loading && currentTab === 'TRACING' && !isShowRetryDetail && (
           <TracingPanel
             className='bg-background-section-burn'
             list={list}
+            onShowIterationDetail={handleShowIterationDetail}
+            onShowRetryDetail={handleShowRetryDetail}
           />
         )}
+        {
+          !loading && currentTab === 'TRACING' && isShowRetryDetail && (
+            <RetryResultPanel
+              list={retryRunResult}
+              onBack={doHideRetryDetail}
+            />
+          )
+        }
       </div>
     </div>
   )

--
Gitblit v1.8.0