116 lines
3.6 KiB
TypeScript
116 lines
3.6 KiB
TypeScript
/**
|
|
* RunTracingPanel
|
|
*
|
|
* Shows AutoStepLog entries for a workflow run with live-update capability.
|
|
* Displays per-node status (running/completed/failed/skipped) with timing info.
|
|
*/
|
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
import { useApiRequest } from '../../../hooks/useApi';
|
|
import type { AutoStepLog } from '../../../api/workflowApi';
|
|
|
|
interface RunTracingPanelProps {
|
|
instanceId: string;
|
|
runId: string | null;
|
|
onNodeSelect?: (nodeId: string) => void;
|
|
}
|
|
|
|
const STATUS_COLORS: Record<string, string> = {
|
|
pending: '#999',
|
|
running: '#f0ad4e',
|
|
completed: '#28a745',
|
|
failed: '#dc3545',
|
|
skipped: '#6c757d',
|
|
};
|
|
|
|
const STATUS_ICONS: Record<string, string> = {
|
|
pending: '○',
|
|
running: '◉',
|
|
completed: '✓',
|
|
failed: '✗',
|
|
skipped: '—',
|
|
};
|
|
|
|
export const RunTracingPanel: React.FC<RunTracingPanelProps> = ({
|
|
instanceId,
|
|
runId,
|
|
onNodeSelect,
|
|
}) => {
|
|
const [steps, setSteps] = useState<AutoStepLog[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const { request } = useApiRequest();
|
|
|
|
const loadSteps = useCallback(async () => {
|
|
if (!runId || !instanceId) return;
|
|
setLoading(true);
|
|
try {
|
|
const data = await request({
|
|
url: `/api/workflows/${instanceId}/runs/${runId}/steps`,
|
|
method: 'get',
|
|
});
|
|
setSteps(data?.steps || []);
|
|
} catch (e) {
|
|
console.error('[RunTracing] Failed to load steps:', e);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [runId, instanceId, request]);
|
|
|
|
useEffect(() => {
|
|
loadSteps();
|
|
const interval = setInterval(loadSteps, 3000);
|
|
return () => clearInterval(interval);
|
|
}, [loadSteps]);
|
|
|
|
if (!runId) {
|
|
return (
|
|
<div style={{ padding: '16px', color: 'var(--text-secondary, #888)', fontSize: '13px' }}>
|
|
Select a run to see tracing details.
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div style={{ padding: '12px', overflowY: 'auto', height: '100%' }}>
|
|
<div style={{ fontWeight: 600, fontSize: '14px', marginBottom: '12px' }}>
|
|
Run Steps {loading && <span style={{ fontWeight: 400, fontSize: '12px', color: '#888' }}>(loading...)</span>}
|
|
</div>
|
|
{steps.length === 0 && !loading && (
|
|
<div style={{ color: 'var(--text-secondary, #888)', fontSize: '13px' }}>No steps recorded yet.</div>
|
|
)}
|
|
{steps.map((step) => (
|
|
<div
|
|
key={step.id}
|
|
onClick={() => onNodeSelect?.(step.nodeId)}
|
|
style={{
|
|
padding: '8px 12px',
|
|
marginBottom: '6px',
|
|
borderRadius: '6px',
|
|
border: `1px solid ${STATUS_COLORS[step.status] || '#ddd'}`,
|
|
background: 'var(--bg-primary, #fff)',
|
|
cursor: 'pointer',
|
|
fontSize: '13px',
|
|
}}
|
|
>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
<span>
|
|
<span style={{ color: STATUS_COLORS[step.status] || '#999', marginRight: '6px' }}>
|
|
{STATUS_ICONS[step.status] || '?'}
|
|
</span>
|
|
<strong>{step.nodeType}</strong>
|
|
<span style={{ color: '#888', marginLeft: '6px' }}>({step.nodeId})</span>
|
|
</span>
|
|
{step.durationMs != null && (
|
|
<span style={{ color: '#888', fontSize: '12px' }}>{step.durationMs}ms</span>
|
|
)}
|
|
</div>
|
|
{step.error && (
|
|
<div style={{ color: '#dc3545', fontSize: '12px', marginTop: '4px' }}>{step.error}</div>
|
|
)}
|
|
{step.tokensUsed > 0 && (
|
|
<div style={{ color: '#888', fontSize: '11px', marginTop: '2px' }}>{step.tokensUsed} tokens</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
};
|