From f16516549e1755970e355fdb2662c461c883a6de Mon Sep 17 00:00:00 2001 From: yyh Date: Wed, 28 Jan 2026 22:48:27 +0800 Subject: [PATCH] feat(workflow): add clear button to workflow test run panel Features: - Add refresh button to clear test run history (data, inputs, node highlights) - Persist workflowRunningData when closing panel (from previous commit) Code quality improvements: - Refactor to declarative pattern: effectiveTab derived from state, not set in effects - Replace && with ternary operators for conditional rendering (Vercel best practices) - Fix created_by type: change from string to object to match backend API - Remove `as any` type assertion, use proper type-safe access - Title now declaratively shows status based on workflowRunningData presence Files changed: - use-workflow-interactions.ts: add handleClearWorkflowRunHistory hook - workflow-preview.tsx: declarative tab state, clear button, type-safe props - types.ts: fix created_by type definition - test files: update mock data to match corrected types --- .../components/panel/test-run/index.spec.tsx | 2 +- .../panel/test-run/result/index.spec.tsx | 4 +- .../hooks/use-workflow-interactions.ts | 11 + .../workflow/panel/workflow-preview.tsx | 247 ++++++++++-------- web/app/components/workflow/types.ts | 6 +- web/eslint-suppressions.json | 8 - 6 files changed, 157 insertions(+), 121 deletions(-) diff --git a/web/app/components/rag-pipeline/components/panel/test-run/index.spec.tsx b/web/app/components/rag-pipeline/components/panel/test-run/index.spec.tsx index 7ead398ac1..b421240253 100644 --- a/web/app/components/rag-pipeline/components/panel/test-run/index.spec.tsx +++ b/web/app/components/rag-pipeline/components/panel/test-run/index.spec.tsx @@ -111,7 +111,7 @@ const createMockWorkflowRunningData = (overrides: Partial = elapsed_time: 1000, total_tokens: 100, created_at: Date.now(), - created_by: 'Test User', + created_by: { id: 'test-user-id', name: 'Test User', email: 'test@example.com' }, total_steps: 5, exceptions_count: 0, }, diff --git a/web/app/components/rag-pipeline/components/panel/test-run/result/index.spec.tsx b/web/app/components/rag-pipeline/components/panel/test-run/result/index.spec.tsx index d3204ae29a..8fd4b022c8 100644 --- a/web/app/components/rag-pipeline/components/panel/test-run/result/index.spec.tsx +++ b/web/app/components/rag-pipeline/components/panel/test-run/result/index.spec.tsx @@ -124,7 +124,7 @@ const createMockWorkflowRunningData = ( elapsed_time: 1000, total_tokens: 100, created_at: Date.now(), - created_by: 'test-user', + created_by: { id: 'test-user-id', name: 'Test User', email: 'test@example.com' }, total_steps: 5, exceptions_count: 0, }, @@ -1071,7 +1071,7 @@ describe('Result', () => { elapsed_time: 1500, total_tokens: 200, created_at: 1700000000000, - created_by: { name: 'Test User' } as unknown as string, + created_by: { id: 'test-user-id', name: 'Test User', email: 'test@example.com' }, total_steps: 10, exceptions_count: 2, }, diff --git a/web/app/components/workflow/hooks/use-workflow-interactions.ts b/web/app/components/workflow/hooks/use-workflow-interactions.ts index 9227e7b90b..9fc1c871a4 100644 --- a/web/app/components/workflow/hooks/use-workflow-interactions.ts +++ b/web/app/components/workflow/hooks/use-workflow-interactions.ts @@ -51,8 +51,19 @@ export const useWorkflowInteractions = () => { } }, [workflowStore, handleNodeCancelRunningStatus, handleEdgeCancelRunningStatus]) + const handleClearWorkflowRunHistory = useCallback(() => { + workflowStore.setState({ + workflowRunningData: undefined, + inputs: {}, + files: [], + }) + handleNodeCancelRunningStatus() + handleEdgeCancelRunningStatus() + }, [workflowStore, handleNodeCancelRunningStatus, handleEdgeCancelRunningStatus]) + return { handleCancelDebugAndPreviewPanel, + handleClearWorkflowRunHistory, } } diff --git a/web/app/components/workflow/panel/workflow-preview.tsx b/web/app/components/workflow/panel/workflow-preview.tsx index 3fcec92471..bc0a4b8d62 100644 --- a/web/app/components/workflow/panel/workflow-preview.tsx +++ b/web/app/components/workflow/panel/workflow-preview.tsx @@ -10,8 +10,11 @@ import { useState, } from 'react' import { useTranslation } from 'react-i18next' +import ActionButton from '@/app/components/base/action-button' import Button from '@/app/components/base/button' +import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows' import Loading from '@/app/components/base/loading' +import Tooltip from '@/app/components/base/tooltip' import { cn } from '@/utils/classnames' import Toast from '../../base/toast' import { @@ -29,38 +32,40 @@ import InputsPanel from './inputs-panel' const WorkflowPreview = () => { const { t } = useTranslation() - const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions() + const { handleCancelDebugAndPreviewPanel, handleClearWorkflowRunHistory } = useWorkflowInteractions() const workflowRunningData = useStore(s => s.workflowRunningData) const isListening = useStore(s => s.isListening) const showInputsPanel = useStore(s => s.showInputsPanel) const workflowCanvasWidth = useStore(s => s.workflowCanvasWidth) const panelWidth = useStore(s => s.previewPanelWidth) const setPreviewPanelWidth = useStore(s => s.setPreviewPanelWidth) - const showDebugAndPreviewPanel = useStore(s => s.showDebugAndPreviewPanel) - const [currentTab, setCurrentTab] = useState(showInputsPanel ? 'INPUT' : 'TRACING') + const [userSelectedTab, setUserSelectedTab] = useState(null) - const switchTab = async (tab: string) => { - setCurrentTab(tab) - } - - useEffect(() => { - if (showDebugAndPreviewPanel && showInputsPanel) - setCurrentTab('INPUT') - }, [showDebugAndPreviewPanel, showInputsPanel]) - - useEffect(() => { + const effectiveTab = (() => { if (isListening) - switchTab('DETAIL') - }, [isListening]) + return 'DETAIL' - useEffect(() => { - const status = workflowRunningData?.result.status - if (!workflowRunningData) - return + if (workflowRunningData) { + const status = workflowRunningData.result.status + const isFinishedWithoutOutput = (status === WorkflowRunningStatus.Succeeded || status === WorkflowRunningStatus.Failed) + && !workflowRunningData.resultText + && !workflowRunningData.result.files?.length - if ((status === WorkflowRunningStatus.Succeeded || status === WorkflowRunningStatus.Failed) && !workflowRunningData.resultText && !workflowRunningData.result.files?.length) - switchTab('DETAIL') - }, [workflowRunningData]) + if (isFinishedWithoutOutput && userSelectedTab === null) + return 'DETAIL' + + return userSelectedTab ?? 'RESULT' + } + + if (showInputsPanel) + return 'INPUT' + + return 'TRACING' + })() + + const handleTabChange = (tab: string) => { + setUserSelectedTab(tab) + } const [isResizing, setIsResizing] = useState(false) @@ -104,34 +109,48 @@ const WorkflowPreview = () => { onMouseDown={startResizing} />
- {`Test Run${formatWorkflowRunIdentifier(workflowRunningData?.result.finished_at)}`} -
handleCancelDebugAndPreviewPanel()}> - + {`Test Run${workflowRunningData ? formatWorkflowRunIdentifier(workflowRunningData.result.finished_at) : ''}`} +
+ + { + setUserSelectedTab(null) + handleClearWorkflowRunHistory() + }} + > + + + +
+
handleCancelDebugAndPreviewPanel()}> + +
- {showInputsPanel && ( -
switchTab('INPUT')} - > - {t('input', { ns: 'runLog' })} -
- )} + {showInputsPanel + ? ( +
handleTabChange('INPUT')} + > + {t('input', { ns: 'runLog' })} +
+ ) + : null}
{ if (!workflowRunningData) return - switchTab('RESULT') + handleTabChange('RESULT') }} > {t('result', { ns: 'runLog' })} @@ -139,13 +158,13 @@ const WorkflowPreview = () => {
{ if (!workflowRunningData) return - switchTab('DETAIL') + handleTabChange('DETAIL') }} > {t('detail', { ns: 'runLog' })} @@ -153,13 +172,13 @@ const WorkflowPreview = () => {
{ if (!workflowRunningData) return - switchTab('TRACING') + handleTabChange('TRACING') }} > {t('tracing', { ns: 'runLog' })} @@ -167,75 +186,85 @@ const WorkflowPreview = () => {
- {currentTab === 'INPUT' && showInputsPanel && ( - switchTab('RESULT')} /> - )} - {currentTab === 'RESULT' && ( - <> - switchTab('DETAIL')} - /> - {(workflowRunningData?.result.status === WorkflowRunningStatus.Succeeded && workflowRunningData?.resultText && typeof workflowRunningData?.resultText === 'string') && ( - - )} - - )} - {currentTab === 'DETAIL' && ( - - )} - {currentTab === 'DETAIL' && !workflowRunningData?.result && ( -
- -
- )} - {currentTab === 'TRACING' && ( - - )} - {currentTab === 'TRACING' && !workflowRunningData?.tracing?.length && ( -
- -
- )} + {effectiveTab === 'INPUT' && showInputsPanel + ? handleTabChange('RESULT')} /> + : null} + {effectiveTab === 'RESULT' + ? ( + <> + handleTabChange('DETAIL')} + /> + {(workflowRunningData?.result.status === WorkflowRunningStatus.Succeeded && workflowRunningData?.resultText && typeof workflowRunningData?.resultText === 'string') && ( + + )} + + ) + : null} + {effectiveTab === 'DETAIL' + ? ( + + ) + : null} + {effectiveTab === 'DETAIL' && !workflowRunningData?.result + ? ( +
+ +
+ ) + : null} + {effectiveTab === 'TRACING' + ? ( + + ) + : null} + {effectiveTab === 'TRACING' && !workflowRunningData?.tracing?.length + ? ( +
+ +
+ ) + : null}
diff --git a/web/app/components/workflow/types.ts b/web/app/components/workflow/types.ts index 274d315480..a29ea5f1cd 100644 --- a/web/app/components/workflow/types.ts +++ b/web/app/components/workflow/types.ts @@ -467,7 +467,11 @@ export type WorkflowRunningData = { elapsed_time?: number total_tokens?: number created_at?: number - created_by?: string + created_by?: { + id: string + name: string + email: string + } finished_at?: number steps?: number showSteps?: boolean diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json index bb67061404..62cf6efd1b 100644 --- a/web/eslint-suppressions.json +++ b/web/eslint-suppressions.json @@ -3756,14 +3756,6 @@ "count": 2 } }, - "app/components/workflow/panel/workflow-preview.tsx": { - "react-hooks-extra/no-direct-set-state-in-use-effect": { - "count": 1 - }, - "ts/no-explicit-any": { - "count": 1 - } - }, "app/components/workflow/run/hooks.ts": { "ts/no-explicit-any": { "count": 1