tracing-panel.tsx 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. 'use client'
  2. import type { FC } from 'react'
  3. import
  4. React,
  5. {
  6. useCallback,
  7. useState,
  8. } from 'react'
  9. import cn from 'classnames'
  10. import {
  11. RiArrowDownSLine,
  12. RiMenu4Line,
  13. } from '@remixicon/react'
  14. import { useTranslation } from 'react-i18next'
  15. import NodePanel from './node'
  16. import {
  17. BlockEnum,
  18. } from '@/app/components/workflow/types'
  19. import type { IterationDurationMap, NodeTracing } from '@/types/workflow'
  20. type TracingPanelProps = {
  21. list: NodeTracing[]
  22. onShowIterationDetail?: (detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => void
  23. onShowRetryDetail?: (detail: NodeTracing[]) => void
  24. className?: string
  25. hideNodeInfo?: boolean
  26. hideNodeProcessDetail?: boolean
  27. }
  28. type TracingNodeProps = {
  29. id: string
  30. uniqueId: string
  31. isParallel: boolean
  32. data: NodeTracing | null
  33. children: TracingNodeProps[]
  34. parallelTitle?: string
  35. branchTitle?: string
  36. hideNodeInfo?: boolean
  37. hideNodeProcessDetail?: boolean
  38. }
  39. function buildLogTree(nodes: NodeTracing[], t: (key: string) => string): TracingNodeProps[] {
  40. const rootNodes: TracingNodeProps[] = []
  41. const parallelStacks: { [key: string]: TracingNodeProps } = {}
  42. const levelCounts: { [key: string]: number } = {}
  43. const parallelChildCounts: { [key: string]: Set<string> } = {}
  44. let uniqueIdCounter = 0
  45. const getUniqueId = () => {
  46. uniqueIdCounter++
  47. return `unique-${uniqueIdCounter}`
  48. }
  49. const getParallelTitle = (parentId: string | null): string => {
  50. const levelKey = parentId || 'root'
  51. if (!levelCounts[levelKey])
  52. levelCounts[levelKey] = 0
  53. levelCounts[levelKey]++
  54. const parentTitle = parentId ? parallelStacks[parentId]?.parallelTitle : ''
  55. const levelNumber = parentTitle ? parseInt(parentTitle.split('-')[1]) + 1 : 1
  56. const letter = parallelChildCounts[levelKey]?.size > 1 ? String.fromCharCode(64 + levelCounts[levelKey]) : ''
  57. return `${t('workflow.common.parallel')}-${levelNumber}${letter}`
  58. }
  59. const getBranchTitle = (parentId: string | null, branchNum: number): string => {
  60. const levelKey = parentId || 'root'
  61. const parentTitle = parentId ? parallelStacks[parentId]?.parallelTitle : ''
  62. const levelNumber = parentTitle ? parseInt(parentTitle.split('-')[1]) + 1 : 1
  63. const letter = parallelChildCounts[levelKey]?.size > 1 ? String.fromCharCode(64 + levelCounts[levelKey]) : ''
  64. const branchLetter = String.fromCharCode(64 + branchNum)
  65. return `${t('workflow.common.branch')}-${levelNumber}${letter}-${branchLetter}`
  66. }
  67. // Count parallel children (for figuring out if we need to use letters)
  68. for (const node of nodes) {
  69. const parent_parallel_id = node.parent_parallel_id ?? node.execution_metadata?.parent_parallel_id ?? null
  70. const parallel_id = node.parallel_id ?? node.execution_metadata?.parallel_id ?? null
  71. if (parallel_id) {
  72. const parentKey = parent_parallel_id || 'root'
  73. if (!parallelChildCounts[parentKey])
  74. parallelChildCounts[parentKey] = new Set()
  75. parallelChildCounts[parentKey].add(parallel_id)
  76. }
  77. }
  78. for (const node of nodes) {
  79. const parallel_id = node.parallel_id ?? node.execution_metadata?.parallel_id ?? null
  80. const parent_parallel_id = node.parent_parallel_id ?? node.execution_metadata?.parent_parallel_id ?? null
  81. const parallel_start_node_id = node.parallel_start_node_id ?? node.execution_metadata?.parallel_start_node_id ?? null
  82. const parent_parallel_start_node_id = node.parent_parallel_start_node_id ?? node.execution_metadata?.parent_parallel_start_node_id ?? null
  83. if (!parallel_id || node.node_type === BlockEnum.End) {
  84. rootNodes.push({
  85. id: node.id,
  86. uniqueId: getUniqueId(),
  87. isParallel: false,
  88. data: node,
  89. children: [],
  90. })
  91. }
  92. else {
  93. if (!parallelStacks[parallel_id]) {
  94. const newParallelGroup: TracingNodeProps = {
  95. id: parallel_id,
  96. uniqueId: getUniqueId(),
  97. isParallel: true,
  98. data: null,
  99. children: [],
  100. parallelTitle: '',
  101. }
  102. parallelStacks[parallel_id] = newParallelGroup
  103. if (parent_parallel_id && parallelStacks[parent_parallel_id]) {
  104. const sameBranchIndex = parallelStacks[parent_parallel_id].children.findLastIndex(c =>
  105. c.data?.execution_metadata?.parallel_start_node_id === parent_parallel_start_node_id || c.data?.parallel_start_node_id === parent_parallel_start_node_id,
  106. )
  107. parallelStacks[parent_parallel_id].children.splice(sameBranchIndex + 1, 0, newParallelGroup)
  108. newParallelGroup.parallelTitle = getParallelTitle(parent_parallel_id)
  109. }
  110. else {
  111. newParallelGroup.parallelTitle = getParallelTitle(parent_parallel_id)
  112. rootNodes.push(newParallelGroup)
  113. }
  114. }
  115. const branchTitle = parallel_start_node_id === node.node_id ? getBranchTitle(parent_parallel_id, parallelStacks[parallel_id].children.length + 1) : ''
  116. if (branchTitle) {
  117. parallelStacks[parallel_id].children.push({
  118. id: node.id,
  119. uniqueId: getUniqueId(),
  120. isParallel: false,
  121. data: node,
  122. children: [],
  123. branchTitle,
  124. })
  125. }
  126. else {
  127. let sameBranchIndex = parallelStacks[parallel_id].children.findLastIndex(c =>
  128. c.data?.execution_metadata?.parallel_start_node_id === parallel_start_node_id || c.data?.parallel_start_node_id === parallel_start_node_id,
  129. )
  130. if (parallelStacks[parallel_id].children[sameBranchIndex + 1]?.isParallel)
  131. sameBranchIndex++
  132. parallelStacks[parallel_id].children.splice(sameBranchIndex + 1, 0, {
  133. id: node.id,
  134. uniqueId: getUniqueId(),
  135. isParallel: false,
  136. data: node,
  137. children: [],
  138. branchTitle,
  139. })
  140. }
  141. }
  142. }
  143. return rootNodes
  144. }
  145. const TracingPanel: FC<TracingPanelProps> = ({
  146. list,
  147. onShowIterationDetail,
  148. onShowRetryDetail,
  149. className,
  150. hideNodeInfo = false,
  151. hideNodeProcessDetail = false,
  152. }) => {
  153. const { t } = useTranslation()
  154. const treeNodes = buildLogTree(list, t)
  155. const [collapsedNodes, setCollapsedNodes] = useState<Set<string>>(new Set())
  156. const [hoveredParallel, setHoveredParallel] = useState<string | null>(null)
  157. const toggleCollapse = (id: string) => {
  158. setCollapsedNodes((prev) => {
  159. const newSet = new Set(prev)
  160. if (newSet.has(id))
  161. newSet.delete(id)
  162. else
  163. newSet.add(id)
  164. return newSet
  165. })
  166. }
  167. const handleParallelMouseEnter = useCallback((id: string) => {
  168. setHoveredParallel(id)
  169. }, [])
  170. const handleParallelMouseLeave = useCallback((e: React.MouseEvent) => {
  171. const relatedTarget = e.relatedTarget as Element | null
  172. if (relatedTarget && 'closest' in relatedTarget) {
  173. const closestParallel = relatedTarget.closest('[data-parallel-id]')
  174. if (closestParallel)
  175. setHoveredParallel(closestParallel.getAttribute('data-parallel-id'))
  176. else
  177. setHoveredParallel(null)
  178. }
  179. else {
  180. setHoveredParallel(null)
  181. }
  182. }, [])
  183. const renderNode = (node: TracingNodeProps) => {
  184. if (node.isParallel) {
  185. const isCollapsed = collapsedNodes.has(node.id)
  186. const isHovered = hoveredParallel === node.id
  187. return (
  188. <div
  189. key={node.uniqueId}
  190. className="ml-4 mb-2 relative"
  191. data-parallel-id={node.id}
  192. onMouseEnter={() => handleParallelMouseEnter(node.id)}
  193. onMouseLeave={handleParallelMouseLeave}
  194. >
  195. <div className="flex items-center mb-1">
  196. <button
  197. onClick={() => toggleCollapse(node.id)}
  198. className={cn(
  199. 'mr-2 transition-colors',
  200. isHovered ? 'rounded border-components-button-primary-border bg-components-button-primary-bg text-text-primary-on-surface' : 'text-text-secondary hover:text-text-primary',
  201. )}
  202. >
  203. {isHovered ? <RiArrowDownSLine className="w-3 h-3" /> : <RiMenu4Line className="w-3 h-3 text-text-tertiary" />}
  204. </button>
  205. <div className="system-xs-semibold-uppercase text-text-secondary flex items-center">
  206. <span>{node.parallelTitle}</span>
  207. </div>
  208. <div
  209. className="mx-2 flex-grow h-px bg-divider-subtle"
  210. style={{ background: 'linear-gradient(to right, rgba(16, 24, 40, 0.08), rgba(255, 255, 255, 0)' }}
  211. ></div>
  212. </div>
  213. <div className={`pl-2 relative ${isCollapsed ? 'hidden' : ''}`}>
  214. <div className={cn(
  215. 'absolute top-0 bottom-0 left-[5px] w-[2px]',
  216. isHovered ? 'bg-text-accent-secondary' : 'bg-divider-subtle',
  217. )}></div>
  218. {node.children.map(renderNode)}
  219. </div>
  220. </div>
  221. )
  222. }
  223. else {
  224. const isHovered = hoveredParallel === node.id
  225. return (
  226. <div key={node.uniqueId}>
  227. <div className={cn('pl-4 -mb-1.5 system-2xs-medium-uppercase', isHovered ? 'text-text-tertiary' : 'text-text-quaternary')}>
  228. {node.branchTitle}
  229. </div>
  230. <NodePanel
  231. nodeInfo={node.data!}
  232. onShowIterationDetail={onShowIterationDetail}
  233. onShowRetryDetail={onShowRetryDetail}
  234. justShowIterationNavArrow={true}
  235. justShowRetryNavArrow={true}
  236. hideInfo={hideNodeInfo}
  237. hideProcessDetail={hideNodeProcessDetail}
  238. />
  239. </div>
  240. )
  241. }
  242. }
  243. return (
  244. <div className={cn(className || 'bg-components-panel-bg', 'py-2')}>
  245. {treeNodes.map(renderNode)}
  246. </div>
  247. )
  248. }
  249. export default TracingPanel