feat(run-detail): add expandable step output blocks with json pretty-printing
- add collapsible code blocks for step output with JSON syntax highlighting - display byte count when output exists but no description available - support both individual row expansion and header-level expand all toggle - pretty-print JSON output for improved readability - wrap log messages in code tags for monospace display - fix step order numbering to start from 1 instead of 2
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
|||||||
Badge,
|
Badge,
|
||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
Card,
|
Card,
|
||||||
|
CodeBlock,
|
||||||
DescriptionList,
|
DescriptionList,
|
||||||
Notification,
|
Notification,
|
||||||
Pagination,
|
Pagination,
|
||||||
@@ -27,7 +28,7 @@ import {
|
|||||||
type TableColumn,
|
type TableColumn,
|
||||||
} from '../../ui';
|
} from '../../ui';
|
||||||
import type { BadgeVariant } from '../../ui';
|
import type { BadgeVariant } from '../../ui';
|
||||||
import { ClipboardList, Activity } from 'lucide-react';
|
import { ChevronDown, ChevronRight, ClipboardList, Activity } from 'lucide-react';
|
||||||
import styles from '../Page.module.css';
|
import styles from '../Page.module.css';
|
||||||
|
|
||||||
const RUN_STATUS_VARIANT: Record<ScenarioRunStatus, BadgeVariant> = {
|
const RUN_STATUS_VARIANT: Record<ScenarioRunStatus, BadgeVariant> = {
|
||||||
@@ -75,6 +76,8 @@ export function RunDetailPage() {
|
|||||||
const [logPage, setLogPage] = useState(1);
|
const [logPage, setLogPage] = useState(1);
|
||||||
const [logSearch, setLogSearch] = useState('');
|
const [logSearch, setLogSearch] = useState('');
|
||||||
const [debouncedSearch, setDebouncedSearch] = useState('');
|
const [debouncedSearch, setDebouncedSearch] = useState('');
|
||||||
|
const [expandAll, setExpandAll] = useState(false);
|
||||||
|
const [expandedStepIds, setExpandedStepIds] = useState<Set<string>>(new Set());
|
||||||
const logSearchRef = useRef('');
|
const logSearchRef = useRef('');
|
||||||
const LOG_PAGE_SIZE = 25;
|
const LOG_PAGE_SIZE = 25;
|
||||||
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
@@ -154,7 +157,7 @@ export function RunDetailPage() {
|
|||||||
}, [id, runId]);
|
}, [id, runId]);
|
||||||
|
|
||||||
const stepColumns: TableColumn<ScenarioRunStep>[] = [
|
const stepColumns: TableColumn<ScenarioRunStep>[] = [
|
||||||
{ key: 'order', header: t('runs.step_order'), render: (s) => s.order + 1, width: 50 },
|
{ key: 'order', header: t('runs.step_order'), render: (s) => s.order, width: 50 },
|
||||||
{
|
{
|
||||||
key: 'title',
|
key: 'title',
|
||||||
header: t('runs.step_title'),
|
header: t('runs.step_title'),
|
||||||
@@ -171,8 +174,97 @@ export function RunDetailPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'description',
|
key: 'description',
|
||||||
header: t('runs.step_description'),
|
header: (
|
||||||
render: (s) => s.description ?? <span className={styles.muted}>–</span>,
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px' }}>
|
||||||
|
<span>{t('runs.step_description')}</span>
|
||||||
|
{run?.stepRuns.some((s) => s.output) && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (expandAll) {
|
||||||
|
setExpandAll(false);
|
||||||
|
setExpandedStepIds(new Set());
|
||||||
|
} else {
|
||||||
|
setExpandAll(true);
|
||||||
|
setExpandedStepIds(new Set(run?.stepRuns.filter((s) => s.output).map((s) => s.id) ?? []));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
padding: '0 4px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
color: 'inherit',
|
||||||
|
}}
|
||||||
|
title={expandAll ? 'Collapse all' : 'Expand all'}
|
||||||
|
>
|
||||||
|
{expandAll ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
render: (s) => {
|
||||||
|
const isExpanded = expandAll || expandedStepIds.has(s.id);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginBottom: isExpanded && s.output ? '8px' : 0 }}>
|
||||||
|
{s.output && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const newSet = new Set(expandedStepIds);
|
||||||
|
if (newSet.has(s.id)) {
|
||||||
|
newSet.delete(s.id);
|
||||||
|
} else {
|
||||||
|
newSet.add(s.id);
|
||||||
|
}
|
||||||
|
setExpandedStepIds(newSet);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
padding: 0,
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexShrink: 0,
|
||||||
|
marginTop: '2px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isExpanded ? (
|
||||||
|
<ChevronDown size={16} />
|
||||||
|
) : (
|
||||||
|
<ChevronRight size={16} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<code style={{ wordBreak: 'break-word' }}>
|
||||||
|
{s.description ? (
|
||||||
|
s.description
|
||||||
|
) : s.output ? (
|
||||||
|
<span title={`${s.output.length} bytes`}>{s.output.length} bytes</span>
|
||||||
|
) : (
|
||||||
|
<span className={styles.muted}>–</span>
|
||||||
|
)}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
{isExpanded && s.output && (
|
||||||
|
<div style={{ marginLeft: '24px' }}>
|
||||||
|
<CodeBlock
|
||||||
|
code={(() => {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(JSON.parse(s.output), null, 2);
|
||||||
|
} catch {
|
||||||
|
return s.output;
|
||||||
|
}
|
||||||
|
})()}
|
||||||
|
language="json"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -183,7 +275,7 @@ export function RunDetailPage() {
|
|||||||
width: 70,
|
width: 70,
|
||||||
render: (l) => <Badge variant={LOG_LEVEL_VARIANT[l.level]}>{l.level}</Badge>,
|
render: (l) => <Badge variant={LOG_LEVEL_VARIANT[l.level]}>{l.level}</Badge>,
|
||||||
},
|
},
|
||||||
{ key: 'message', header: t('runs.log_message'), render: (l) => l.message },
|
{ key: 'message', header: t('runs.log_message'), render: (l) => <code style={{ wordBreak: 'break-word' }}>{l.message}</code> },
|
||||||
{
|
{
|
||||||
key: 'time',
|
key: 'time',
|
||||||
header: t('runs.log_time'),
|
header: t('runs.log_time'),
|
||||||
|
|||||||
Reference in New Issue
Block a user