diff --git a/web/app/components/workflow/run/hooks/useTracingSearch.ts b/web/app/components/workflow/run/hooks/useTracingSearch.ts new file mode 100644 index 0000000000..f5455a67ea --- /dev/null +++ b/web/app/components/workflow/run/hooks/useTracingSearch.ts @@ -0,0 +1,133 @@ +import { useCallback, useMemo, useState } from 'react' +import type { NodeTracing } from '@/types/workflow' + +type UseTracingSearchProps = { + treeNodes: NodeTracing[] +} + +type UseTracingSearchReturn = { + searchQuery: string + setSearchQuery: (query: string) => void + filteredNodes: NodeTracing[] + clearSearch: () => void + searchStats: { + matched: number + total: number + } +} + +export const useTracingSearch = ({ treeNodes }: UseTracingSearchProps): UseTracingSearchReturn => { + const [searchQuery, setSearchQuery] = useState('') + + // Recursively count all nodes including children + const countNodesRecursively = useCallback((nodes: NodeTracing[]): number => { + return nodes.reduce((count, node) => { + let nodeCount = 1 + if (node.parallelDetail?.children) + nodeCount += countNodesRecursively(node.parallelDetail.children) + return count + nodeCount + }, 0) + }, []) + + // Deep recursive search filtering logic + const filteredNodes = useMemo(() => { + if (!searchQuery.trim()) return treeNodes + + const query = searchQuery.toLowerCase().trim() + + // Deep search object content with proper typing + const searchInObject = (obj: unknown): boolean => { + if (!obj) return false + + if (typeof obj === 'string') return obj.toLowerCase().includes(query) + if (typeof obj === 'number') return obj.toString().includes(query) + if (typeof obj === 'boolean') return obj.toString().includes(query) + + if (Array.isArray(obj)) + return obj.some(item => searchInObject(item)) + + if (typeof obj === 'object' && obj !== null) + return Object.values(obj as Record).some(value => searchInObject(value)) + + return false + } + + // Search all content in a single node with safe property access + const searchInNode = (node: NodeTracing): boolean => { + // Safe string search with nullish coalescing + const titleMatch = node.title?.toLowerCase().includes(query) ?? false + const nodeTypeMatch = node.node_type?.toLowerCase().includes(query) ?? false + const statusMatch = (node as NodeTracing & { status?: string }).status?.toLowerCase().includes(query) ?? false + + // Search in node data with proper type checking + const inputsMatch = searchInObject(node.inputs) + const outputsMatch = searchInObject(node.outputs) + const processDataMatch = searchInObject((node as NodeTracing & { process_data?: unknown }).process_data) + const metadataMatch = searchInObject((node as NodeTracing & { execution_metadata?: unknown }).execution_metadata) + + return titleMatch || nodeTypeMatch || statusMatch || inputsMatch || outputsMatch || processDataMatch || metadataMatch + } + + // Recursively search node and all its children + const searchNodeRecursively = (node: NodeTracing): boolean => { + // Search current node + if (searchInNode(node)) return true + + // Search parallel branch children with safe access + if (node.parallelDetail?.children) + return node.parallelDetail.children.some((child: NodeTracing) => searchNodeRecursively(child)) + + return false + } + + // Recursively filter node tree while maintaining hierarchy + const filterNodesRecursively = (nodes: NodeTracing[]): NodeTracing[] => { + return nodes.reduce((acc: NodeTracing[], node: NodeTracing) => { + const nodeMatches = searchInNode(node) + const hasMatchingChildren = node.parallelDetail?.children + ? node.parallelDetail.children.some((child: NodeTracing) => searchNodeRecursively(child)) + : false + + if (nodeMatches || hasMatchingChildren) { + const filteredNode = { ...node } + + // If has parallel children, recursively filter them + if (node.parallelDetail?.children) { + const filteredChildren = filterNodesRecursively(node.parallelDetail.children) + if (filteredChildren.length > 0) { + filteredNode.parallelDetail = { + ...node.parallelDetail, + children: filteredChildren, + } + } + } + + acc.push(filteredNode) + } + + return acc + }, []) + } + + return filterNodesRecursively(treeNodes) + }, [treeNodes, searchQuery]) + + // Clear search function + const clearSearch = useCallback(() => { + setSearchQuery('') + }, []) + + // Calculate search statistics + const searchStats = useMemo(() => ({ + matched: countNodesRecursively(filteredNodes), + total: countNodesRecursively(treeNodes), + }), [filteredNodes, treeNodes, countNodesRecursively]) + + return { + searchQuery, + setSearchQuery, + filteredNodes, + clearSearch, + searchStats, + } +} diff --git a/web/app/components/workflow/run/tracing-panel.tsx b/web/app/components/workflow/run/tracing-panel.tsx index 3ba4c30f5f..ff157653a5 100644 --- a/web/app/components/workflow/run/tracing-panel.tsx +++ b/web/app/components/workflow/run/tracing-panel.tsx @@ -4,7 +4,6 @@ import React, { useCallback, - useMemo, useState, } from 'react' import cn from 'classnames' @@ -20,6 +19,7 @@ import NodePanel from './node' import SpecialResultPanel from './special-result-panel' import type { NodeTracing } from '@/types/workflow' import formatNodeList from '@/app/components/workflow/run/utils/format-log' +import { useTracingSearch } from './hooks/useTracingSearch' type TracingPanelProps = { list: NodeTracing[] @@ -37,93 +37,18 @@ const TracingPanel: FC = ({ enableSearch = false, }) => { const { t } = useTranslation() - const [searchQuery, setSearchQuery] = useState('') const treeNodes = formatNodeList(list, t) - // 递归计算节点总数(包括子节点) - const countNodesRecursively = useCallback((nodes: any[]): number => { - return nodes.reduce((count, node) => { - let nodeCount = 1 - if (node.parallelDetail?.children) - nodeCount += countNodesRecursively(node.parallelDetail.children) - return count + nodeCount - }, 0) - }, []) - - // 深度递归搜索过滤逻辑 - const filteredNodes = useMemo(() => { - if (!searchQuery.trim()) return treeNodes - - const query = searchQuery.toLowerCase().trim() - - // 深度搜索对象内容 - const searchInObject = (obj: any): boolean => { - if (!obj) return false - if (typeof obj === 'string') return obj.toLowerCase().includes(query) - if (typeof obj === 'number') return obj.toString().includes(query) - if (Array.isArray(obj)) return obj.some(item => searchInObject(item)) - if (typeof obj === 'object') - return Object.values(obj).some(value => searchInObject(value)) - return false - } - - // 搜索单个节点的所有内容 - const searchInNode = (node: any): boolean => { - return ( - node.title?.toLowerCase().includes(query) - || node.node_type?.toLowerCase().includes(query) - || node.status?.toLowerCase().includes(query) - || searchInObject(node.inputs) - || searchInObject(node.outputs) - || searchInObject(node.process_data) - || searchInObject(node.execution_metadata) - ) - } - - // 递归搜索节点及其所有子节点 - const searchNodeRecursively = (node: any): boolean => { - // 搜索当前节点 - if (searchInNode(node)) return true - - // 搜索并行分支子节点 - if (node.parallelDetail?.children) - return node.parallelDetail.children.some((child: any) => searchNodeRecursively(child)) - - return false - } - - // 递归过滤节点树,保持层级结构 - const filterNodesRecursively = (nodes: any[]): any[] => { - return nodes.reduce((acc: any[], node: any) => { - const nodeMatches = searchInNode(node) - const hasMatchingChildren = node.parallelDetail?.children - ? node.parallelDetail.children.some((child: any) => searchNodeRecursively(child)) - : false - - if (nodeMatches || hasMatchingChildren) { - const filteredNode = { ...node } - - // 如果有并行子节点,递归过滤它们 - if (node.parallelDetail?.children) { - const filteredChildren = filterNodesRecursively(node.parallelDetail.children) - if (filteredChildren.length > 0) { - filteredNode.parallelDetail = { - ...node.parallelDetail, - children: filteredChildren, - } - } - } - - acc.push(filteredNode) - } - - return acc - }, []) - } + // Search functionality using custom hook + const { + searchQuery, + setSearchQuery, + filteredNodes, + clearSearch, + searchStats, + } = useTracingSearch({ treeNodes }) - return filterNodesRecursively(treeNodes) - }, [treeNodes, searchQuery]) const [collapsedNodes, setCollapsedNodes] = useState>(new Set()) const [hoveredParallel, setHoveredParallel] = useState(null) @@ -282,7 +207,7 @@ const TracingPanel: FC = ({ e.nativeEvent.stopImmediatePropagation() }} > - {/* 搜索框 */} + {/* Search input */} {enableSearch && (
@@ -296,7 +221,7 @@ const TracingPanel: FC = ({ autoComplete="off" /> {searchQuery && ( -
setSearchQuery('')}> +
)} @@ -304,21 +229,21 @@ const TracingPanel: FC = ({
)} - {/* 搜索结果统计 */} + {/* Search results statistics */} {enableSearch && searchQuery && (
{filteredNodes.length === 0 ? t('workflow.common.noSearchResults') : t('workflow.common.searchResults', { - matched: countNodesRecursively(filteredNodes), - total: countNodesRecursively(treeNodes), + matched: searchStats.matched, + total: searchStats.total, })}
)} - {/* 无搜索结果提示 */} + {/* Empty search results hint */} {enableSearch && searchQuery && filteredNodes.length === 0 && (
{t('workflow.common.noSearchResults')}
@@ -328,7 +253,7 @@ const TracingPanel: FC = ({
)} - {/* 追踪内容 */} + {/* Tracing content */}
{filteredNodes.length > 0 && filteredNodes.map(renderNode)}