feat: Refactor tracking panel, implement search function using custom hooks, optimize node filtering logic

pull/22629/head
GuanMu 7 months ago
parent 61a377e386
commit 158a8c0585

@ -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<string, unknown>).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,
}
}

@ -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<TracingPanelProps> = ({
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<Set<string>>(new Set())
const [hoveredParallel, setHoveredParallel] = useState<string | null>(null)
@ -282,7 +207,7 @@ const TracingPanel: FC<TracingPanelProps> = ({
e.nativeEvent.stopImmediatePropagation()
}}
>
{/* 搜索框 */}
{/* Search input */}
{enableSearch && (
<div className="border-b border-divider-subtle px-4 py-3">
<div className="flex h-8 items-center rounded-lg bg-components-input-bg-normal px-2 hover:bg-components-input-bg-hover">
@ -296,7 +221,7 @@ const TracingPanel: FC<TracingPanelProps> = ({
autoComplete="off"
/>
{searchQuery && (
<div className="ml-1 cursor-pointer" onClick={() => setSearchQuery('')}>
<div className="ml-1 cursor-pointer" onClick={clearSearch}>
<RiCloseLine className="h-4 w-4 text-components-input-text-placeholder hover:text-text-tertiary" />
</div>
)}
@ -304,21 +229,21 @@ const TracingPanel: FC<TracingPanelProps> = ({
</div>
)}
{/* 搜索结果统计 */}
{/* Search results statistics */}
{enableSearch && searchQuery && (
<div className="border-b border-divider-subtle px-4 py-2">
<div className="system-xs-regular text-text-tertiary">
{filteredNodes.length === 0
? t('workflow.common.noSearchResults')
: t('workflow.common.searchResults', {
matched: countNodesRecursively(filteredNodes),
total: countNodesRecursively(treeNodes),
matched: searchStats.matched,
total: searchStats.total,
})}
</div>
</div>
)}
{/* 无搜索结果提示 */}
{/* Empty search results hint */}
{enableSearch && searchQuery && filteredNodes.length === 0 && (
<div className="flex flex-1 flex-col items-center justify-center text-text-tertiary">
<div className="system-sm-medium mb-2">{t('workflow.common.noSearchResults')}</div>
@ -328,7 +253,7 @@ const TracingPanel: FC<TracingPanelProps> = ({
</div>
)}
{/* 追踪内容 */}
{/* Tracing content */}
<div className={cn('flex-1 overflow-y-auto py-2', { 'py-0': enableSearch })}>
{filteredNodes.length > 0 && filteredNodes.map(renderNode)}
</div>

Loading…
Cancel
Save