| | |
| | | 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' |
| | |
| | | } |
| | | }, [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({ |
| | |
| | | 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')} |
| | |
| | | )} |
| | | <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 /> |
| | |
| | | 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> |
| | | ) |