iteration-result-panel.tsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. 'use client'
  2. import type { FC } from 'react'
  3. import React, { useCallback, useState } from 'react'
  4. import { useTranslation } from 'react-i18next'
  5. import {
  6. RiArrowRightSLine,
  7. RiCloseLine,
  8. RiErrorWarningLine,
  9. RiLoader2Line,
  10. } from '@remixicon/react'
  11. import { ArrowNarrowLeft } from '../../base/icons/src/vender/line/arrows'
  12. import { NodeRunningStatus } from '../types'
  13. import TracingPanel from './tracing-panel'
  14. import RetryResultPanel from './retry-result-panel'
  15. import { Iteration } from '@/app/components/base/icons/src/vender/workflow'
  16. import cn from '@/utils/classnames'
  17. import type { IterationDurationMap, NodeTracing } from '@/types/workflow'
  18. const i18nPrefix = 'workflow.singleRun'
  19. type Props = {
  20. list: NodeTracing[][]
  21. onHide: () => void
  22. onBack: () => void
  23. noWrap?: boolean
  24. iterDurationMap?: IterationDurationMap
  25. }
  26. const IterationResultPanel: FC<Props> = ({
  27. list,
  28. onHide,
  29. onBack,
  30. noWrap,
  31. iterDurationMap,
  32. }) => {
  33. const { t } = useTranslation()
  34. const [expandedIterations, setExpandedIterations] = useState<Record<number, boolean>>({})
  35. const toggleIteration = useCallback((index: number) => {
  36. setExpandedIterations(prev => ({
  37. ...prev,
  38. [index]: !prev[index],
  39. }))
  40. }, [])
  41. const countIterDuration = (iteration: NodeTracing[], iterDurationMap: IterationDurationMap): string => {
  42. const IterRunIndex = iteration[0]?.execution_metadata?.iteration_index as number
  43. const iterRunId = iteration[0]?.execution_metadata?.parallel_mode_run_id
  44. const iterItem = iterDurationMap[iterRunId || IterRunIndex]
  45. const duration = iterItem
  46. return `${(duration && duration > 0.01) ? duration.toFixed(2) : 0.01}s`
  47. }
  48. const iterationStatusShow = (index: number, iteration: NodeTracing[], iterDurationMap?: IterationDurationMap) => {
  49. const hasFailed = iteration.some(item => item.status === NodeRunningStatus.Failed)
  50. const isRunning = iteration.some(item => item.status === NodeRunningStatus.Running)
  51. const hasDurationMap = iterDurationMap && Object.keys(iterDurationMap).length !== 0
  52. if (hasFailed)
  53. return <RiErrorWarningLine className='w-4 h-4 text-text-destructive' />
  54. if (isRunning)
  55. return <RiLoader2Line className='w-3.5 h-3.5 text-primary-600 animate-spin' />
  56. return (
  57. <>
  58. {hasDurationMap && (
  59. <div className='system-xs-regular text-text-tertiary'>
  60. {countIterDuration(iteration, iterDurationMap)}
  61. </div>
  62. )}
  63. <RiArrowRightSLine
  64. className={cn(
  65. 'w-4 h-4 text-text-tertiary transition-transform duration-200 flex-shrink-0',
  66. expandedIterations[index] && 'transform rotate-90',
  67. )}
  68. />
  69. </>
  70. )
  71. }
  72. const [retryRunResult, setRetryRunResult] = useState<Record<string, NodeTracing[]> | undefined>()
  73. const handleRetryDetail = (v: number, detail?: NodeTracing[]) => {
  74. setRetryRunResult({ ...retryRunResult, [v]: detail })
  75. }
  76. const main = (
  77. <>
  78. <div className={cn(!noWrap && 'shrink-0 ', 'px-4 pt-3')}>
  79. <div className='shrink-0 flex justify-between items-center h-8'>
  80. <div className='system-xl-semibold text-text-primary truncate'>
  81. {t(`${i18nPrefix}.testRunIteration`)}
  82. </div>
  83. <div className='ml-2 shrink-0 p-1 cursor-pointer' onClick={onHide}>
  84. <RiCloseLine className='w-4 h-4 text-text-tertiary' />
  85. </div>
  86. </div>
  87. <div className='flex items-center py-2 space-x-1 text-text-accent-secondary cursor-pointer' onClick={onBack}>
  88. <ArrowNarrowLeft className='w-4 h-4' />
  89. <div className='system-sm-medium'>{t(`${i18nPrefix}.back`)}</div>
  90. </div>
  91. </div>
  92. {/* List */}
  93. <div className={cn(!noWrap ? 'flex-grow overflow-auto' : 'max-h-full', 'p-2 bg-components-panel-bg')}>
  94. {list.map((iteration, index) => (
  95. <div key={index} className={cn('mb-1 overflow-hidden rounded-xl bg-background-section-burn border-none')}>
  96. <div
  97. className={cn(
  98. 'flex items-center justify-between w-full px-3 cursor-pointer',
  99. expandedIterations[index] ? 'pt-3 pb-2' : 'py-3',
  100. 'rounded-xl text-left',
  101. )}
  102. onClick={() => toggleIteration(index)}
  103. >
  104. <div className={cn('flex items-center gap-2 flex-grow')}>
  105. <div className='flex items-center justify-center w-4 h-4 rounded-[5px] border-divider-subtle bg-util-colors-cyan-cyan-500 flex-shrink-0'>
  106. <Iteration className='w-3 h-3 text-text-primary-on-surface' />
  107. </div>
  108. <span className='system-sm-semibold-uppercase text-text-primary flex-grow'>
  109. {t(`${i18nPrefix}.iteration`)} {index + 1}
  110. </span>
  111. {iterationStatusShow(index, iteration, iterDurationMap)}
  112. </div>
  113. </div>
  114. {expandedIterations[index] && <div
  115. className="flex-grow h-px bg-divider-subtle"
  116. ></div>}
  117. {
  118. !retryRunResult?.[index] && (
  119. <div className={cn(
  120. 'overflow-hidden transition-all duration-200',
  121. expandedIterations[index] ? 'max-h-[1000px] opacity-100' : 'max-h-0 opacity-0',
  122. )}>
  123. <TracingPanel
  124. list={iteration}
  125. className='bg-background-section-burn'
  126. onShowRetryDetail={v => handleRetryDetail(index, v)}
  127. />
  128. </div>
  129. )
  130. }
  131. {
  132. retryRunResult?.[index] && (
  133. <RetryResultPanel
  134. list={retryRunResult[index]}
  135. onBack={() => handleRetryDetail(index, undefined)}
  136. />
  137. )
  138. }
  139. </div>
  140. ))}
  141. </div>
  142. </>
  143. )
  144. const handleNotBubble = useCallback((e: React.MouseEvent) => {
  145. // if not do this, it will trigger the message log modal disappear(useClickAway)
  146. e.stopPropagation()
  147. e.nativeEvent.stopImmediatePropagation()
  148. }, [])
  149. if (noWrap)
  150. return main
  151. return (
  152. <div
  153. className='absolute inset-0 z-10 rounded-2xl pt-10'
  154. style={{
  155. backgroundColor: 'rgba(16, 24, 40, 0.20)',
  156. }}
  157. onClick={handleNotBubble}
  158. >
  159. <div className='h-full rounded-2xl bg-components-panel-bg flex flex-col'>
  160. {main}
  161. </div>
  162. </div >
  163. )
  164. }
  165. export default React.memo(IterationResultPanel)