Skip to content

Commit 39a7245

Browse files
authored
feat(ui): make suite detail compare buttons open in new tab (#217)
## Summary On the suite detail page's **Recent Runs by Client** section, the compare buttons were `<button onClick={navigate(...)}>`, so cmd+click / middle-click did nothing. Converts them to `<a href>` so you can open comparisons in a new tab. Covers: - Per-group compare (heatmap section header) - Group-vs-group averaged compare (heatmap section header) - Per-client cross-group compare (client row) - Per-client averaged-groups compare (client row) - "Compare latest successful run per client" heatmap header button (both render branches) `RunsHeatmap`'s four compare props are now href-builders (`get*Href`) that return `string | undefined`; URL construction moved up to `SuiteDetailPage`. Matches the existing `groupCompareUrl` → `<a>` pattern already on the adjacent group-compare button. ## Test plan - [ ] Click a compare button normally — navigates to `/compare` as before. - [ ] Cmd+click (macOS) / ctrl+click (Linux/Windows) / middle-click — opens the comparison in a new tab. - [ ] With fewer than 2 recent successful runs, the "Compare latest successful run per client" control is disabled (no href) in both the heatmap header and the alternate render path. - [ ] With `group by` set, verify all four in-heatmap compare icons produce the same URLs they did before (`/compare?runs=...` and `/compare/groups?suite=...&groups=...`).
1 parent f2d1c28 commit 39a7245

2 files changed

Lines changed: 75 additions & 68 deletions

File tree

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

Lines changed: 30 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,14 @@ interface RunsHeatmapProps {
9696
runs: IndexEntry[]
9797
/** When set, runs are grouped by this label key (or 'instance_id') before client grouping. */
9898
groupBy?: string
99-
/** Called with the runs of a group when the per-group compare button is clicked. */
100-
onCompareGroup?: (runs: IndexEntry[]) => void
101-
/** Called with a client name to compare its latest successful run across all groups. */
102-
onCompareClientAcrossGroups?: (client: string) => void
103-
/** Called with the group label + clients to open the averaged group comparison page. */
104-
onGroupCompareGroup?: (groupLabel: string, clients: string[]) => void
105-
/** Called with a client name to open the averaged group comparison across label groups. */
106-
onGroupCompareClientAcrossGroups?: (client: string) => void
99+
/** Returns the URL for the per-group compare button, or undefined to render it inert. */
100+
getCompareGroupHref?: (runs: IndexEntry[]) => string | undefined
101+
/** Returns the URL for comparing a client's latest successful run across groups. */
102+
getCompareClientAcrossGroupsHref?: (client: string) => string | undefined
103+
/** Returns the URL for the averaged group-vs-group comparison page. */
104+
getGroupCompareGroupHref?: (groupLabel: string, clients: string[]) => string | undefined
105+
/** Returns the URL for a client's averaged comparison across label groups. */
106+
getGroupCompareClientAcrossGroupsHref?: (client: string) => string | undefined
107107
isDark: boolean
108108
colorNormalization?: ColorNormalization
109109
onColorNormalizationChange?: (mode: ColorNormalization) => void
@@ -134,10 +134,10 @@ interface TooltipData {
134134
export function RunsHeatmap({
135135
runs,
136136
groupBy,
137-
onCompareGroup,
138-
onCompareClientAcrossGroups,
139-
onGroupCompareGroup,
140-
onGroupCompareClientAcrossGroups,
137+
getCompareGroupHref,
138+
getCompareClientAcrossGroupsHref,
139+
getGroupCompareGroupHref,
140+
getGroupCompareClientAcrossGroupsHref,
141141
isDark,
142142
colorNormalization = 'suite',
143143
onColorNormalizationChange,
@@ -468,26 +468,23 @@ export function RunsHeatmap({
468468
<span>=</span>
469469
<span>{section.label}</span>
470470
</span>
471-
{onCompareGroup && (
472-
<button
473-
onClick={() => {
474-
const allGroupRuns = section.clients.flatMap((c) => section.clientRuns[c])
475-
onCompareGroup(allGroupRuns)
476-
}}
471+
{getCompareGroupHref && (
472+
<a
473+
href={getCompareGroupHref(section.clients.flatMap((c) => section.clientRuns[c]))}
477474
className="flex shrink-0 cursor-pointer items-center justify-center rounded-xs p-1 shadow-xs ring-1 ring-inset transition-colors bg-white text-gray-500 ring-gray-300 hover:bg-gray-50 hover:text-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:ring-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-200"
478475
title="Compare latest successful run per client in this group"
479476
>
480477
<GitCompareArrows className="size-3.5" />
481-
</button>
478+
</a>
482479
)}
483-
{onGroupCompareGroup && (
484-
<button
485-
onClick={() => onGroupCompareGroup(section.label, section.clients)}
480+
{getGroupCompareGroupHref && (
481+
<a
482+
href={getGroupCompareGroupHref(section.label, section.clients)}
486483
className="flex shrink-0 cursor-pointer items-center justify-center rounded-xs p-1 shadow-xs ring-1 ring-inset transition-colors bg-white text-gray-500 ring-gray-300 hover:bg-gray-50 hover:text-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:ring-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-200"
487484
title="Compare averaged groups for clients in this group"
488485
>
489486
<Layers className="size-3.5" />
490-
</button>
487+
</a>
491488
)}
492489
<div className="h-px grow bg-gray-200 dark:bg-gray-700" />
493490
</div>
@@ -496,7 +493,7 @@ export function RunsHeatmap({
496493
{/* Stats header */}
497494
{sectionIdx === 0 && (
498495
<div className="flex items-center gap-2 sm:gap-3">
499-
<div className={clsx('hidden shrink-0 sm:block', onGroupCompareClientAcrossGroups && groupSections ? 'w-38' : onCompareClientAcrossGroups && groupSections ? 'w-32' : 'w-28')} />
496+
<div className={clsx('hidden shrink-0 sm:block', getGroupCompareClientAcrossGroupsHref && groupSections ? 'w-38' : getCompareClientAcrossGroupsHref && groupSections ? 'w-32' : 'w-28')} />
500497
<div className="flex-1" />
501498
<div className="hidden shrink-0 gap-3 border-l border-transparent pl-3 font-mono text-xs/5 font-medium text-gray-400 md:flex dark:text-gray-500">
502499
<span className="w-10 text-center">Min</span>
@@ -518,30 +515,30 @@ export function RunsHeatmap({
518515
}
519516
return (
520517
<div key={`${section.label}-${client}`} className="flex items-center gap-2 sm:gap-3">
521-
<div className={clsx('flex shrink-0 items-center gap-1', onGroupCompareClientAcrossGroups && groupSections ? 'sm:w-38' : onCompareClientAcrossGroups && groupSections ? 'sm:w-32' : 'sm:w-28')}>
518+
<div className={clsx('flex shrink-0 items-center gap-1', getGroupCompareClientAcrossGroupsHref && groupSections ? 'sm:w-38' : getCompareClientAcrossGroupsHref && groupSections ? 'sm:w-32' : 'sm:w-28')}>
522519
<span className="sm:hidden">
523520
<ClientBadge client={client} hideLabel />
524521
</span>
525522
<span className="hidden sm:inline-flex">
526523
<ClientBadge client={client} />
527524
</span>
528-
{onCompareClientAcrossGroups && groupSections && (
529-
<button
530-
onClick={() => onCompareClientAcrossGroups(client)}
525+
{getCompareClientAcrossGroupsHref && groupSections && (
526+
<a
527+
href={getCompareClientAcrossGroupsHref(client)}
531528
className="flex shrink-0 items-center justify-center rounded-xs p-0.5 text-gray-400 transition-colors hover:text-gray-700 dark:text-gray-500 dark:hover:text-gray-200"
532529
title={`Compare latest successful ${client} run across groups`}
533530
>
534531
<GitCompareArrows className="size-3" />
535-
</button>
532+
</a>
536533
)}
537-
{onGroupCompareClientAcrossGroups && groupSections && (
538-
<button
539-
onClick={() => onGroupCompareClientAcrossGroups(client)}
534+
{getGroupCompareClientAcrossGroupsHref && groupSections && (
535+
<a
536+
href={getGroupCompareClientAcrossGroupsHref(client)}
540537
className="flex shrink-0 items-center justify-center rounded-xs p-0.5 text-gray-400 transition-colors hover:text-gray-700 dark:text-gray-500 dark:hover:text-gray-200"
541538
title={`Compare ${client} averaged across groups (group comparison)`}
542539
>
543540
<Layers className="size-3" />
544-
</button>
541+
</a>
545542
)}
546543
</div>
547544
<div className="flex min-w-0 flex-1 flex-wrap gap-1">

ui/src/pages/SuiteDetailPage.tsx

Lines changed: 45 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,17 +1029,23 @@ export function SuiteDetailPage() {
10291029
>
10301030
<SquareStack className="size-3.5" />
10311031
</button>
1032-
<button
1033-
disabled={recentSuccessfulPerClient.length < MIN_COMPARE_RUNS}
1034-
onClick={() => {
1035-
const ids = recentSuccessfulPerClient.map((r) => r.run_id)
1036-
navigate({ to: '/compare', search: { runs: ids.join(',') } })
1037-
}}
1038-
className="flex cursor-pointer items-center justify-center rounded-xs p-1 shadow-xs ring-1 ring-inset transition-colors bg-white text-gray-500 ring-gray-300 hover:bg-gray-50 hover:text-gray-700 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-800 dark:text-gray-400 dark:ring-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-200"
1039-
title="Compare latest successful run per client"
1040-
>
1041-
<GitCompareArrows className="size-3.5" />
1042-
</button>
1032+
{recentSuccessfulPerClient.length >= MIN_COMPARE_RUNS ? (
1033+
<a
1034+
href={`/compare?runs=${encodeURIComponent(recentSuccessfulPerClient.map((r) => r.run_id).join(','))}`}
1035+
className="flex items-center justify-center rounded-xs p-1 shadow-xs ring-1 ring-inset transition-colors bg-white text-gray-500 ring-gray-300 hover:bg-gray-50 hover:text-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:ring-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-200"
1036+
title="Compare latest successful run per client"
1037+
>
1038+
<GitCompareArrows className="size-3.5" />
1039+
</a>
1040+
) : (
1041+
<button
1042+
disabled
1043+
className="flex cursor-not-allowed items-center justify-center rounded-xs p-1 opacity-50 shadow-xs ring-1 ring-inset bg-white text-gray-500 ring-gray-300 dark:bg-gray-800 dark:text-gray-400 dark:ring-gray-600"
1044+
title="Compare latest successful run per client"
1045+
>
1046+
<GitCompareArrows className="size-3.5" />
1047+
</button>
1048+
)}
10431049
{groupCompareUrl && (
10441050
<a
10451051
href={groupCompareUrl}
@@ -1056,7 +1062,7 @@ export function SuiteDetailPage() {
10561062
<RunsHeatmap
10571063
runs={suiteRunsAll}
10581064
groupBy={effectiveGroupBy}
1059-
onCompareGroup={effectiveGroupBy ? (groupRuns) => {
1065+
getCompareGroupHref={effectiveGroupBy ? (groupRuns) => {
10601066
const sorted = [...groupRuns].sort((a, b) => b.timestamp - a.timestamp)
10611067
const seen = new Set<string>()
10621068
const ids: string[] = []
@@ -1071,11 +1077,10 @@ export function SuiteDetailPage() {
10711077
}
10721078
if (ids.length >= MAX_COMPARE_RUNS) break
10731079
}
1074-
if (ids.length >= MIN_COMPARE_RUNS) {
1075-
navigate({ to: '/compare', search: { runs: ids.join(',') } })
1076-
}
1080+
if (ids.length < MIN_COMPARE_RUNS) return undefined
1081+
return `/compare?runs=${encodeURIComponent(ids.join(','))}`
10771082
} : undefined}
1078-
onCompareClientAcrossGroups={effectiveGroupBy ? (client) => {
1083+
getCompareClientAcrossGroupsHref={effectiveGroupBy ? (client) => {
10791084
// Find the latest successful run for this client in each group
10801085
const sorted = [...suiteRunsAll]
10811086
.filter((r) => r.instance.client === client)
@@ -1095,17 +1100,16 @@ export function SuiteDetailPage() {
10951100
}
10961101
if (ids.length >= MAX_COMPARE_RUNS) break
10971102
}
1098-
if (ids.length >= MIN_COMPARE_RUNS) {
1099-
const labels = effectiveGroupBy === 'instance_id' ? 'instance-id' : `label:${effectiveGroupBy}`
1100-
navigate({ to: '/compare', search: { runs: ids.join(','), labels } })
1101-
}
1103+
if (ids.length < MIN_COMPARE_RUNS) return undefined
1104+
const labels = effectiveGroupBy === 'instance_id' ? 'instance-id' : `label:${effectiveGroupBy}`
1105+
return `/compare?runs=${encodeURIComponent(ids.join(','))}&labels=${encodeURIComponent(labels)}`
11021106
} : undefined}
1103-
onGroupCompareGroup={effectiveGroupBy ? (groupLabel, groupClients) => {
1107+
getGroupCompareGroupHref={effectiveGroupBy ? (groupLabel, groupClients) => {
11041108
const labelFilter = effectiveGroupBy !== 'instance_id' ? `${effectiveGroupBy}=${groupLabel}` : ''
11051109
const groups = groupClients.map((c) => `${c}:${labelFilter}`).join(';')
1106-
navigate({ to: '/compare/groups', search: { suite: suiteHash, groups } as Record<string, string> })
1110+
return `/compare/groups?suite=${encodeURIComponent(suiteHash)}&groups=${encodeURIComponent(groups)}`
11071111
} : undefined}
1108-
onGroupCompareClientAcrossGroups={effectiveGroupBy ? (client) => {
1112+
getGroupCompareClientAcrossGroupsHref={effectiveGroupBy ? (client) => {
11091113
// Build one group per label-value for this client.
11101114
const labelValues = new Set<string>()
11111115
for (const run of suiteRunsAll) {
@@ -1119,7 +1123,7 @@ export function SuiteDetailPage() {
11191123
if (effectiveGroupBy === 'instance_id') return `${client}:`
11201124
return `${client}:${effectiveGroupBy}=${val}`
11211125
}).join(';')
1122-
navigate({ to: '/compare/groups', search: { suite: suiteHash, groups } as Record<string, string> })
1126+
return `/compare/groups?suite=${encodeURIComponent(suiteHash)}&groups=${encodeURIComponent(groups)}`
11231127
} : undefined}
11241128
isDark={isDark}
11251129
colorNormalization={heatmapColor}
@@ -1292,17 +1296,23 @@ export function SuiteDetailPage() {
12921296
>
12931297
<SquareStack className="size-4" />
12941298
</button>
1295-
<button
1296-
disabled={recentSuccessfulPerClient.length < MIN_COMPARE_RUNS}
1297-
onClick={() => {
1298-
const ids = recentSuccessfulPerClient.map((r) => r.run_id)
1299-
navigate({ to: '/compare', search: { runs: ids.join(',') } })
1300-
}}
1301-
className="flex cursor-pointer items-center justify-center rounded-xs p-1.5 shadow-xs ring-1 ring-inset transition-colors bg-white text-gray-500 ring-gray-300 hover:bg-gray-50 hover:text-gray-700 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-800 dark:text-gray-400 dark:ring-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-200"
1302-
title="Compare latest successful run per client"
1303-
>
1304-
<GitCompareArrows className="size-4" />
1305-
</button>
1299+
{recentSuccessfulPerClient.length >= MIN_COMPARE_RUNS ? (
1300+
<a
1301+
href={`/compare?runs=${encodeURIComponent(recentSuccessfulPerClient.map((r) => r.run_id).join(','))}`}
1302+
className="flex items-center justify-center rounded-xs p-1.5 shadow-xs ring-1 ring-inset transition-colors bg-white text-gray-500 ring-gray-300 hover:bg-gray-50 hover:text-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:ring-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-200"
1303+
title="Compare latest successful run per client"
1304+
>
1305+
<GitCompareArrows className="size-4" />
1306+
</a>
1307+
) : (
1308+
<button
1309+
disabled
1310+
className="flex cursor-not-allowed items-center justify-center rounded-xs p-1.5 opacity-50 shadow-xs ring-1 ring-inset bg-white text-gray-500 ring-gray-300 dark:bg-gray-800 dark:text-gray-400 dark:ring-gray-600"
1311+
title="Compare latest successful run per client"
1312+
>
1313+
<GitCompareArrows className="size-4" />
1314+
</button>
1315+
)}
13061316
{groupCompareUrl && (
13071317
<a
13081318
href={groupCompareUrl}

0 commit comments

Comments
 (0)