Skip to content

Commit 6feed8e

Browse files
authored
feat(ui): highlight failed executions and persist modal state in URL (#213)
## Summary - Highlights failed execution rows in the test detail modal with a red-tinted background for better visibility - Persists the active step tab (test/setup/cleanup) and expanded execution row indices as URL search params (`testStep`, `testExec`), making modal state shareable and preserved across refreshes - Clears step/expanded state when closing the modal, switching tests, or switching tabs ## Test plan - [ ] Open a test modal with failed executions — verify FAIL rows have red background in both light and dark mode - [ ] Switch between Test/Setup/Cleanup tabs — verify `testStep` param updates in URL - [ ] Expand execution rows — verify `testExec` param updates in URL - [ ] Refresh the page — verify tab and expanded rows are restored - [ ] Share the URL — verify it opens with the same state
1 parent 8108432 commit 6feed8e

3 files changed

Lines changed: 65 additions & 9 deletions

File tree

ui/src/components/run-detail/ExecutionsList.tsx

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ interface ExecutionsListProps {
6767
suiteHash: string
6868
testName: string
6969
stepType: StepType
70+
expandedRows?: Set<number>
71+
onExpandedRowsChange?: (rows: Set<number>) => void
7072
}
7173

7274
function parseMethod(request: string): string {
@@ -142,8 +144,13 @@ function StatusIndicator({ status }: { status?: number }) {
142144

143145
const MAX_LAZY_LINE_SIZE = 1_000_000 // 1MB — lazy-load lines up to this size
144146

145-
function ExecutionRow({ index, request, requestSize, methodName, requestLineInfo, response, responseSize, time, status, mgasPerSec, gasUsed, responseViewerUrl, requestViewerUrl }: ExecutionRowProps) {
146-
const [expanded, setExpanded] = useState(false)
147+
function ExecutionRow({ index, request, requestSize, methodName, requestLineInfo, response, responseSize, time, status, mgasPerSec, gasUsed, responseViewerUrl, requestViewerUrl, expanded: expandedProp, onExpandedChange }: ExecutionRowProps & { expanded?: boolean; onExpandedChange?: (index: number, expanded: boolean) => void }) {
148+
const [expandedLocal, setExpandedLocal] = useState(false)
149+
const expanded = expandedProp ?? expandedLocal
150+
const setExpanded = (v: boolean) => {
151+
setExpandedLocal(v)
152+
onExpandedChange?.(index, v)
153+
}
147154
const method = request ? parseMethod(request) : methodName
148155

149156
// Lazy-load request content for lines that are small enough but weren't in the full fetch
@@ -162,15 +169,25 @@ function ExecutionRow({ index, request, requestSize, methodName, requestLineInfo
162169
const effectiveRequestSize = requestSize ?? (request ? new Blob([request]).size : undefined)
163170
const effectiveResponseSize = responseSize ?? (response ? new Blob([response]).size : undefined)
164171
const canExpand = !!effectiveRequest || !!response || !!responseViewerUrl || !!requestViewerUrl || canLazyLoad
172+
const isFail = status !== undefined && status !== 0
165173

166174
return (
167-
<div className="max-w-full overflow-hidden border-b border-gray-200 last:border-b-0 dark:border-gray-700">
175+
<div className={clsx(
176+
'max-w-full overflow-hidden border-b last:border-b-0',
177+
isFail
178+
? 'border-red-300 bg-red-100 dark:border-red-800/50 dark:bg-red-900/30'
179+
: 'border-gray-200 dark:border-gray-700',
180+
)}>
168181
<button
169182
onClick={() => canExpand && setExpanded(!expanded)}
170183
className={clsx(
171184
'flex w-full items-center gap-3 px-3 py-2 text-left transition-colors',
172-
canExpand ? 'cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800' : 'cursor-default',
173-
expanded && 'bg-gray-100 dark:bg-gray-800',
185+
canExpand
186+
? isFail
187+
? 'cursor-pointer hover:bg-red-200 dark:hover:bg-red-900/40'
188+
: 'cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800'
189+
: 'cursor-default',
190+
expanded && (isFail ? 'bg-red-200 dark:bg-red-900/40' : 'bg-gray-100 dark:bg-gray-800'),
174191
)}
175192
>
176193
{canExpand ? (
@@ -306,7 +323,7 @@ function ExecutionRow({ index, request, requestSize, methodName, requestLineInfo
306323

307324
const EXECUTIONS_PAGE_SIZE = 100
308325

309-
export function ExecutionsList({ runId, suiteHash, testName, stepType }: ExecutionsListProps) {
326+
export function ExecutionsList({ runId, suiteHash, testName, stepType, expandedRows, onExpandedRowsChange }: ExecutionsListProps) {
310327
const { data: requests, isLoading: requestsLoading, error: requestsError } = useTestRequests(suiteHash, testName, stepType)
311328
const { data: responses, error: responsesError } = useTestResponses(runId, testName, stepType)
312329
const { data: resultDetails, isLoading: detailsLoading, error: detailsError } = useTestResultDetails(runId, testName, stepType)
@@ -405,6 +422,12 @@ export function ExecutionsList({ runId, suiteHash, testName, stepType }: Executi
405422
requestViewerUrl={!safeRequests?.[index] && requestSummaries?.[index] && requestSummaries[index].size > 1_000_000
406423
? `/runs/${runId}/fileviewer?base=${encodeURIComponent(`suites/${suiteHash}`)}&file=${encodeURIComponent(`${testName}/${stepType}.request`)}&lines=${index + 1}`
407424
: undefined}
425+
expanded={expandedRows?.has(index)}
426+
onExpandedChange={(idx, exp) => {
427+
const next = new Set(expandedRows)
428+
if (exp) next.add(idx); else next.delete(idx)
429+
onExpandedRowsChange?.(next)
430+
}}
408431
/>
409432
)
410433
})}

ui/src/components/run-detail/TestHeatmap.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ interface TestHeatmapProps {
120120
onGroupModeChange?: (mode: GroupMode) => void
121121
onThresholdChange?: (threshold: number) => void
122122
onSearchChange?: (query: string) => void
123+
activeStepTab?: 'test' | 'setup' | 'cleanup'
124+
onActiveStepTabChange?: (tab: 'test' | 'setup' | 'cleanup') => void
125+
expandedExecRows?: Set<number>
126+
onExpandedExecRowsChange?: (rows: Set<number>) => void
123127
}
124128

125129
const COLORS = [
@@ -357,13 +361,22 @@ export function TestHeatmap({
357361
onSortModeChange,
358362
onGroupModeChange,
359363
onThresholdChange,
364+
activeStepTab: activeStepTabProp,
365+
onActiveStepTabChange,
366+
expandedExecRows,
367+
onExpandedExecRowsChange,
360368
}: TestHeatmapProps) {
361369
const sortMode = sortModeProp ?? 'order'
362370
const groupMode = groupModeProp ?? 'none'
363371
const threshold = thresholdProp ?? DEFAULT_THRESHOLD
364372
const [tooltip, setTooltip] = useState<{ test: TestData; x: number; y: number } | null>(null)
365373
const [opcodeSort, setOpcodeSort] = useState<OpcodeSortMode>('name')
366-
const [activeStepTab, setActiveStepTab] = useState<'test' | 'setup' | 'cleanup'>('test')
374+
const [activeStepTabLocal, setActiveStepTabLocal] = useState<'test' | 'setup' | 'cleanup'>('test')
375+
const activeStepTab = activeStepTabProp ?? activeStepTabLocal
376+
const setActiveStepTab = (tab: 'test' | 'setup' | 'cleanup') => {
377+
setActiveStepTabLocal(tab)
378+
onActiveStepTabChange?.(tab)
379+
}
367380
const { data: blockLogs } = useBlockLogs(runId)
368381

369382
// Pop-in stagger state for newly-completed tiles. Populated below by
@@ -1005,6 +1018,8 @@ export function TestHeatmap({
10051018
suiteHash={suiteHash}
10061019
testName={selectedTest}
10071020
stepType={activeStep.key}
1021+
expandedRows={expandedExecRows}
1022+
onExpandedRowsChange={onExpandedExecRowsChange}
10081023
/>
10091024
</div>
10101025
)}

ui/src/pages/RunDetailPage.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,16 @@ export function RunDetailPage() {
137137
blFs?: boolean // Block Logs fullscreen
138138
dlModal?: boolean // Download list modal
139139
dlFmt?: string // Download list format
140+
testStep?: string // Active step tab in test modal (test/setup/cleanup)
141+
testExec?: string // Expanded execution row indices (comma-separated)
140142
}
141143
const page = Number(search.page) || 1
142144
const pageSize = Number(search.pageSize) || 20
143145
const heatmapThreshold = search.heatmapThreshold ? Number(search.heatmapThreshold) : undefined
144146
const stepFilter = parseStepFilter(search.steps)
145-
const { sortBy = 'order', sortDir = 'asc', q = '', status = 'all', testModal, preRunModal, heatmapGroup, heatmapSort, ohFs = false, blFs = false, dlModal = false, dlFmt } = search
147+
const { sortBy = 'order', sortDir = 'asc', q = '', status = 'all', testModal, preRunModal, heatmapGroup, heatmapSort, ohFs = false, blFs = false, dlModal = false, dlFmt, testStep, testExec } = search
148+
const activeStepTab = (testStep === 'setup' || testStep === 'cleanup') ? testStep : testStep === 'test' ? testStep : undefined
149+
const expandedExecRows = testExec ? new Set(testExec.split(',').map(Number).filter(n => !isNaN(n))) : undefined
146150

147151
const { data: liveRuns, isLoading: liveRunsLoading } = useLiveRuns()
148152
const liveRun = liveRuns?.find((lr) => lr.run_id === runId)
@@ -251,7 +255,17 @@ export function RunDetailPage() {
251255
}
252256

253257
const handleTestModalChange = (testName: string | undefined) => {
254-
updateSearch({ testModal: testName })
258+
// Clear step tab and expanded rows when closing or switching test
259+
updateSearch({ testModal: testName, testStep: undefined, testExec: undefined })
260+
}
261+
262+
const handleStepTabChange = (tab: 'test' | 'setup' | 'cleanup') => {
263+
// Clear expanded rows when switching tabs
264+
updateSearch({ testStep: tab !== 'test' ? tab : undefined, testExec: undefined })
265+
}
266+
267+
const handleExpandedExecRowsChange = (rows: Set<number>) => {
268+
updateSearch({ testExec: rows.size > 0 ? [...rows].sort((a, b) => a - b).join(',') : undefined })
255269
}
256270

257271
const handlePreRunModalChange = (stepName: string | undefined) => {
@@ -680,6 +694,10 @@ export function RunDetailPage() {
680694
onGroupModeChange={handleHeatmapGroupChange}
681695
onThresholdChange={handleHeatmapThresholdChange}
682696
onSearchChange={handleSearchChange}
697+
activeStepTab={activeStepTab}
698+
onActiveStepTabChange={handleStepTabChange}
699+
expandedExecRows={expandedExecRows}
700+
onExpandedExecRowsChange={handleExpandedExecRowsChange}
683701
/>
684702
</div>
685703

0 commit comments

Comments
 (0)