Skip to content

Commit f94c75d

Browse files
authored
fix(ui): render live runs distinctly in "Recent Runs by Client" (#203)
## Summary On the suite detail page's "Recent Runs by Client" heatmap, live runs were indistinguishable from cancelled runs (grey cell + red ring + X) and the tooltip was counting tests-not-yet-executed as failed. This PR makes live runs a first-class state. ### Cell rendering - Solid blue background, blue inset ring, small pulsing white dot in the center. - No `!` marker for "not-yet-executed" tests (only actually-reported failures count). - In compare mode the cell is `cursor-default` — clicking a live cell falls through to navigate to the run detail page instead of toggling selection. ### Tooltip - Shows "● In progress (N/total)" in blue instead of "Cancelled". - Hides Duration and MGas/s (not meaningful yet). - Pass/fail summary uses reported `tests_failed` (not `total - passed`) for live runs. - Hint in compare mode reads: *"Live runs can't be compared — click for details"*. ### Compare helpers - `onCompareGroup` and `onCompareClientAcrossGroups` on SuiteDetailPage now explicitly skip runs with `status=running`, so a live run sitting at exactly 100% progress won't be swept into a comparison. - The "latest successful per client" button already used `completedRuns`, which excludes live runs — no change needed. ## Test plan - [x] TypeScript + ESLint clean - [x] Manual: with a live run reporting, open the suite detail page — confirm the live row shows a blue pulsing cell and a blue "In progress" tooltip - [x] Manual: hover a live cell: no "failed" count for not-yet-executed tests - [x] Manual: enter compare mode, click a live cell: should navigate to run detail, not add to selection - [x] Manual: use "Compare group" / "Compare across groups" buttons with a live run present: confirm only finished runs are included
1 parent 2e92bad commit f94c75d

2 files changed

Lines changed: 80 additions & 16 deletions

File tree

ui/src/components/suite-detail/RunsHeatmap.tsx

Lines changed: 75 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ function isRunCompleted(run: IndexEntry): boolean {
1111
return !run.status || run.status === 'completed'
1212
}
1313

14+
// Check if a run is still in progress (being reported by an active runner).
15+
function isRunLive(run: IndexEntry): boolean {
16+
return run.status === 'running'
17+
}
18+
1419
const MAX_RUNS_PER_CLIENT = 30
1520

1621
// 5-level discrete color scale (green to red for duration, reversed for MGas/s)
@@ -350,6 +355,13 @@ export function RunsHeatmap({
350355

351356
const handleRunClick = (run: IndexEntry) => {
352357
if (selectable) {
358+
// Live runs are not selectable for compare — their per-test
359+
// results don't exist yet. Let the click fall through to nav.
360+
if (isRunLive(run)) {
361+
navigate({ to: '/runs/$runId', params: { runId: run.run_id } })
362+
return
363+
}
364+
353365
onSelectionChange?.(run.run_id, !selectedRunIds?.has(run.run_id))
354366
} else {
355367
navigate({
@@ -512,27 +524,53 @@ export function RunsHeatmap({
512524
{section.clientRuns[client].map((run) => {
513525
const runStats = getIndexAggregatedStats(run, stepFilter)
514526
const completed = isRunCompleted(run)
527+
const live = isRunLive(run)
528+
// For live runs, "failed" is the reported count (not
529+
// total - passed, which would count not-yet-executed
530+
// tests as failed).
531+
const reportedFailed = live
532+
? run.tests.tests_failed
533+
: run.tests.tests_total - run.tests.tests_passed
515534
return (
516535
<button
517536
key={run.run_id}
518537
onClick={() => handleRunClick(run)}
519538
onMouseEnter={(e) => handleMouseEnter(run, e)}
520539
onMouseLeave={handleMouseLeave}
521540
className={clsx(
522-
'relative size-5 shrink-0 cursor-pointer rounded-xs transition-all hover:scale-110 hover:ring-2 hover:ring-gray-400 dark:hover:ring-gray-500',
541+
'relative size-5 shrink-0 rounded-xs transition-all hover:scale-110 hover:ring-2 hover:ring-gray-400 dark:hover:ring-gray-500',
542+
// Live cells in compare mode aren't selectable:
543+
// clicking falls through to nav instead.
544+
selectable && live ? 'cursor-default' : 'cursor-pointer',
523545
selectable && selectedRunIds?.has(run.run_id) && 'scale-110 ring-2 ring-blue-500 dark:ring-blue-400',
524-
run.tests.tests_total - run.tests.tests_passed > 0 && completed && !selectedRunIds?.has(run.run_id) && 'ring-2 ring-inset ring-orange-500',
525-
!completed && !selectedRunIds?.has(run.run_id) && 'ring-2 ring-inset ring-red-600 dark:ring-red-500',
546+
!live && reportedFailed > 0 && completed && !selectedRunIds?.has(run.run_id) && 'ring-2 ring-inset ring-orange-500',
547+
!live && !completed && !selectedRunIds?.has(run.run_id) && 'ring-2 ring-inset ring-red-600 dark:ring-red-500',
548+
live && !selectedRunIds?.has(run.run_id) && 'ring-2 ring-inset ring-blue-500 dark:ring-blue-400',
526549
)}
527-
style={{ backgroundColor: completed ? getColorForRun(run, colorOverrides) : '#6b7280' }}
528-
title={`${formatTimestamp(run.timestamp)} - ${completed ? formatDurationMinSec(runStats.duration) : run.status}`}
550+
style={{
551+
backgroundColor: live
552+
? '#3b82f6' // blue-500
553+
: completed
554+
? getColorForRun(run, colorOverrides)
555+
: '#6b7280',
556+
}}
557+
title={
558+
live
559+
? `${formatTimestamp(run.timestamp)} - in progress (${run.tests.tests_passed}/${run.tests.tests_total})`
560+
: `${formatTimestamp(run.timestamp)} - ${completed ? formatDurationMinSec(runStats.duration) : run.status}`
561+
}
529562
>
530-
{completed && run.tests.tests_total - run.tests.tests_passed > 0 && (
563+
{live && (
564+
<span className="absolute inset-0 flex items-center justify-center">
565+
<span className="size-1.5 animate-pulse rounded-full bg-white" />
566+
</span>
567+
)}
568+
{!live && completed && reportedFailed > 0 && (
531569
<svg className="absolute inset-0 size-5" viewBox="0 0 20 20" fill="none">
532570
<text x="10" y="15" textAnchor="middle" fill="white" fontSize="13" fontWeight="bold" fontFamily="system-ui">!</text>
533571
</svg>
534572
)}
535-
{!completed && (
573+
{!live && !completed && (
536574
<svg className="absolute inset-0 size-5 text-red-600 dark:text-red-400" viewBox="0 0 20 20" fill="currentColor">
537575
<path d="M4 4l12 12M4 16L16 4" stroke="currentColor" strokeWidth="2" fill="none" />
538576
</svg>
@@ -642,11 +680,18 @@ export function RunsHeatmap({
642680
{(() => {
643681
const tooltipStats = getIndexAggregatedStats(tooltip.run, stepFilter)
644682
const completed = isRunCompleted(tooltip.run)
683+
const live = isRunLive(tooltip.run)
645684
return (
646685
<div className="flex flex-col gap-1">
647686
<div className="font-medium">{tooltip.run.instance.client}</div>
648687
<div>{formatTimestamp(tooltip.run.timestamp)}</div>
649-
{!completed && (
688+
{live && (
689+
<div className="flex items-center gap-1.5 font-medium text-blue-600 dark:text-blue-400">
690+
<span className="size-1.5 animate-pulse rounded-full bg-blue-500 dark:bg-blue-400" />
691+
In progress ({tooltip.run.tests.tests_passed}/{tooltip.run.tests.tests_total})
692+
</div>
693+
)}
694+
{!live && !completed && (
650695
<div className="flex items-center gap-1 font-medium text-red-600 dark:text-red-400">
651696
<AlertTriangle className="size-3.5" />
652697
{tooltip.run.status === 'container_died' ? 'Container Died' : 'Cancelled'}
@@ -657,8 +702,8 @@ export function RunsHeatmap({
657702
{tooltip.run.termination_reason}
658703
</div>
659704
)}
660-
<div>Duration: {formatDurationMinSec(tooltipStats.duration)}</div>
661-
{(() => {
705+
{!live && <div>Duration: {formatDurationMinSec(tooltipStats.duration)}</div>}
706+
{!live && (() => {
662707
const mgas = calculateMGasPerSec(tooltipStats.gasUsed, tooltipStats.gasUsedDuration)
663708
return mgas !== undefined ? <div>MGas/s: {mgas.toFixed(2)}</div> : null
664709
})()}
@@ -686,16 +731,30 @@ export function RunsHeatmap({
686731
<span className="text-green-600 dark:text-green-400">
687732
{tooltip.run.tests.tests_passed} passed
688733
</span>
689-
{tooltip.run.tests.tests_total - tooltip.run.tests.tests_passed > 0 && (
690-
<span className="text-red-600 dark:text-red-400">
691-
{tooltip.run.tests.tests_total - tooltip.run.tests.tests_passed} failed
692-
</span>
693-
)}
734+
{(() => {
735+
// Live runs report their failures directly; for
736+
// completed runs we fall back to total - passed so
737+
// older rows without tests_failed still render.
738+
const failed = live
739+
? tooltip.run.tests.tests_failed
740+
: tooltip.run.tests.tests_total - tooltip.run.tests.tests_passed
741+
return failed > 0 ? (
742+
<span className="text-red-600 dark:text-red-400">
743+
{failed} failed
744+
</span>
745+
) : null
746+
})()}
694747
<span className="text-gray-500 dark:text-gray-400">
695748
({tooltip.run.tests.tests_total} total)
696749
</span>
697750
</div>
698-
<div className="mt-1 text-gray-400 dark:text-gray-500">{selectable ? 'Click to select' : 'Click for details'}</div>
751+
<div className="mt-1 text-gray-400 dark:text-gray-500">
752+
{selectable && live
753+
? 'Live runs can\'t be compared — click for details'
754+
: selectable
755+
? 'Click to select'
756+
: 'Click for details'}
757+
</div>
699758
</div>
700759
)
701760
})()}

ui/src/pages/SuiteDetailPage.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,6 +1037,9 @@ export function SuiteDetailPage() {
10371037
const ids: string[] = []
10381038
for (const run of sorted) {
10391039
if (seen.has(run.instance.client)) continue
1040+
// Skip live runs — comparison needs finished
1041+
// per-test results which don't exist yet.
1042+
if (run.status === 'running') continue
10401043
if (run.tests.tests_total > 0 && run.tests.tests_passed === run.tests.tests_total) {
10411044
seen.add(run.instance.client)
10421045
ids.push(run.run_id)
@@ -1055,6 +1058,8 @@ export function SuiteDetailPage() {
10551058
const seenGroups = new Set<string>()
10561059
const ids: string[] = []
10571060
for (const run of sorted) {
1061+
// Skip live runs — can't compare in-progress runs.
1062+
if (run.status === 'running') continue
10581063
const groupValue = groupBy === 'instance_id'
10591064
? run.instance.id
10601065
: (run.metadata?.[groupBy] ?? '(none)')

0 commit comments

Comments
 (0)