From dcb002304be124c15a787a341c060be579eb6fef Mon Sep 17 00:00:00 2001 From: Andrii Arsenin Date: Tue, 14 Apr 2026 19:19:39 +0300 Subject: [PATCH] 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 --- client/src/pages/run/RunDetailPage.tsx | 102 +++++++++++++++++++++++-- 1 file changed, 97 insertions(+), 5 deletions(-) diff --git a/client/src/pages/run/RunDetailPage.tsx b/client/src/pages/run/RunDetailPage.tsx index 25aa3aa..0da2860 100644 --- a/client/src/pages/run/RunDetailPage.tsx +++ b/client/src/pages/run/RunDetailPage.tsx @@ -17,6 +17,7 @@ import { Badge, Breadcrumbs, Card, + CodeBlock, DescriptionList, Notification, Pagination, @@ -27,7 +28,7 @@ import { type TableColumn, } 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'; const RUN_STATUS_VARIANT: Record = { @@ -75,6 +76,8 @@ export function RunDetailPage() { const [logPage, setLogPage] = useState(1); const [logSearch, setLogSearch] = useState(''); const [debouncedSearch, setDebouncedSearch] = useState(''); + const [expandAll, setExpandAll] = useState(false); + const [expandedStepIds, setExpandedStepIds] = useState>(new Set()); const logSearchRef = useRef(''); const LOG_PAGE_SIZE = 25; const pollRef = useRef | null>(null); @@ -154,7 +157,7 @@ export function RunDetailPage() { }, [id, runId]); const stepColumns: TableColumn[] = [ - { 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', header: t('runs.step_title'), @@ -171,8 +174,97 @@ export function RunDetailPage() { }, { key: 'description', - header: t('runs.step_description'), - render: (s) => s.description ?? , + header: ( +
+ {t('runs.step_description')} + {run?.stepRuns.some((s) => s.output) && ( + + )} +
+ ), + render: (s) => { + const isExpanded = expandAll || expandedStepIds.has(s.id); + return ( +
+
+ {s.output && ( + + )} + + {s.description ? ( + s.description + ) : s.output ? ( + {s.output.length} bytes + ) : ( + + )} + +
+ {isExpanded && s.output && ( +
+ { + try { + return JSON.stringify(JSON.parse(s.output), null, 2); + } catch { + return s.output; + } + })()} + language="json" + /> +
+ )} +
+ ); + }, }, ]; @@ -183,7 +275,7 @@ export function RunDetailPage() { width: 70, render: (l) => {l.level}, }, - { key: 'message', header: t('runs.log_message'), render: (l) => l.message }, + { key: 'message', header: t('runs.log_message'), render: (l) => {l.message} }, { key: 'time', header: t('runs.log_time'),