Skip to content

Commit 3e7b5b9

Browse files
author
zhougongyan
committed
fix(workflow): add copy buttons for runtime payloads
Add copy actions for runtime input/output payloads in workflow detail views so users can reuse the latest JSON more easily during debugging and testing. Made-with: Cursor
1 parent cd89e40 commit 3e7b5b9

4 files changed

Lines changed: 58 additions & 10 deletions

File tree

webui/src/pages/WorkflowDetail/NodeInfoPanel.test.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ vi.mock('@/api/workflow', async () => {
2020
};
2121
});
2222

23+
vi.mock('@/components/common/CopyButton', () => ({
24+
default: ({ text }: { text: string }) => (
25+
<button type="button" data-testid="copy-button" aria-label={`copy:${text}`}>
26+
copy
27+
</button>
28+
),
29+
}));
30+
2331
vi.mock('react-i18next', () => ({
2432
useTranslation: () => ({
2533
t: (key: string, params?: Record<string, unknown>) => {
@@ -149,6 +157,10 @@ describe('NodeInfoPanel', () => {
149157
expect(screen.getByText('真实输出')).toBeInTheDocument();
150158
expect(screen.getByDisplayValue(/demo.local/)).toBeInTheDocument();
151159
expect(screen.getByText(/"result": "ok"/)).toBeInTheDocument();
160+
const copyButtons = screen.getAllByTestId('copy-button');
161+
expect(copyButtons).toHaveLength(2);
162+
expect(copyButtons[0]).toHaveAttribute('aria-label', 'copy:{\n "host": "demo.local"\n}');
163+
expect(copyButtons[1]).toHaveAttribute('aria-label', 'copy:{\n "result": "ok"\n}');
152164
});
153165

154166
it('shows empty runtime hint when there is no latest execution', () => {

webui/src/pages/WorkflowDetail/NodeInfoPanel.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useState, useEffect, useMemo, useRef } from 'react';
66
import { useTranslation } from 'react-i18next';
77
import { X, AlertCircle, Save, Loader2, ChevronDown, ChevronRight, Play, RotateCcw, Maximize2 } from 'lucide-react';
88
import { workflowAPI, Workflow, WorkflowEdge, WorkflowExecution, WorkflowNode, WorkflowNodeExecution } from '@/api/workflow';
9+
import CopyButton from '@/components/common/CopyButton';
910

1011
// ─────────────────────────────────────────────
1112
// Constants
@@ -296,13 +297,17 @@ function RuntimeJsonBlock({ label, value, tone }: {
296297
tone: 'amber' | 'green';
297298
}) {
298299
const bgClass = tone === 'amber' ? 'bg-amber-50 border-amber-100 text-amber-900' : 'bg-green-50 border-green-100 text-green-900';
300+
const formattedValue = JSON.stringify(value, null, 2);
299301

300302
return (
301303
<div className="space-y-1.5">
302-
<FL>{label}</FL>
304+
<div className="flex items-center justify-between gap-2">
305+
<FL>{label}</FL>
306+
<CopyButton text={formattedValue} size="w-3 h-3" />
307+
</div>
303308
<div className={`rounded-lg border px-2.5 py-2 ${bgClass}`}>
304309
<pre className="text-[11px] font-mono whitespace-pre-wrap break-all">
305-
{JSON.stringify(value, null, 2)}
310+
{formattedValue}
306311
</pre>
307312
</div>
308313
</div>

webui/src/pages/WorkflowDetail/tabs/RunTab.test.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ vi.mock('@/api/workflow', () => ({
2626
}));
2727

2828
vi.mock('@/components/common/CopyButton', () => ({
29-
default: () => null,
29+
default: ({ text }: { text: string }) => (
30+
<button type="button" data-testid="copy-button" aria-label={`copy:${text}`}>
31+
copy
32+
</button>
33+
),
3034
}));
3135

3236
vi.mock('@/components/common/WorkflowStatusBadge', () => ({
@@ -204,4 +208,25 @@ describe('RunTab', () => {
204208
});
205209
});
206210

211+
it('shows a copy button when output results are available', async () => {
212+
render(
213+
<ControlledRunTab
214+
initialExecution={{
215+
id: 'exec-output',
216+
workflowId: 'wf-1',
217+
inputParams: { topic: 'demo' },
218+
outputResults: { result: 'ok' },
219+
status: 'success',
220+
startedAt: Date.now(),
221+
executionLog: [],
222+
}}
223+
/>
224+
);
225+
226+
await waitFor(() => {
227+
const copyButtons = screen.getAllByTestId('copy-button');
228+
expect(copyButtons.some((button) => button.getAttribute('aria-label') === 'copy:{\n "result": "ok"\n}')).toBe(true);
229+
});
230+
});
231+
207232
});

webui/src/pages/WorkflowDetail/tabs/RunTab.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -429,13 +429,19 @@ function TestSection({
429429

430430
{execution.outputResults && (
431431
<div>
432-
<button
433-
onClick={() => setOutputExpanded(v => !v)}
434-
className="w-full flex items-center justify-between px-3 py-2 hover:bg-gray-50 transition-colors"
435-
>
436-
<span className="text-xs font-medium text-gray-600">{t('detail.run.outputResults')}</span>
437-
{outputExpanded ? <ChevronDown className="w-3 h-3 text-gray-400" /> : <ChevronRight className="w-3 h-3 text-gray-400" />}
438-
</button>
432+
<div className="flex items-center justify-between gap-2 px-3 py-2 hover:bg-gray-50 transition-colors">
433+
<div className="flex items-center gap-2 min-w-0">
434+
<span className="text-xs font-medium text-gray-600">{t('detail.run.outputResults')}</span>
435+
<CopyButton text={JSON.stringify(execution.outputResults, null, 2)} size="w-3 h-3" />
436+
</div>
437+
<button
438+
onClick={() => setOutputExpanded(v => !v)}
439+
className="flex items-center rounded p-0.5 hover:bg-gray-100"
440+
aria-label={t('detail.run.outputResults')}
441+
>
442+
{outputExpanded ? <ChevronDown className="w-3 h-3 text-gray-400" /> : <ChevronRight className="w-3 h-3 text-gray-400" />}
443+
</button>
444+
</div>
439445
{outputExpanded && (
440446
<div className="bg-gray-900 px-3 py-2 max-h-48 overflow-y-auto">
441447
<pre className="text-xs text-green-300 font-mono whitespace-pre-wrap">

0 commit comments

Comments
 (0)