Skip to content

Commit fb62ff9

Browse files
Group runs by commit SHA, update chip labels with commit time and pass fail overview
1 parent b92cecb commit fb62ff9

File tree

1 file changed

+125
-38
lines changed

1 file changed

+125
-38
lines changed

mcpjam-inspector/client/src/components/evals/overview-panel.tsx

Lines changed: 125 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,6 @@ function formatRelativeTime(timestamp?: number): string {
5454
return new Date(timestamp).toLocaleDateString();
5555
}
5656

57-
function formatShortDate(timestamp: number): string {
58-
return new Date(timestamp).toLocaleDateString(undefined, {
59-
month: "short",
60-
day: "numeric",
61-
});
62-
}
63-
6457
/** Tiny inline sparkline rendered as CSS bars. */
6558
function Sparkline({
6659
data,
@@ -91,11 +84,14 @@ function Sparkline({
9184

9285
interface RunBucket {
9386
id: string;
94-
label: string;
95-
date: string;
87+
commitSha: string | null;
88+
branch: string | null;
9689
timestamp: number;
9790
result: "passed" | "failed" | "mixed" | "running" | "pending";
9891
runs: EvalSuiteRun[];
92+
suiteIds: Set<string>;
93+
passedCount: number;
94+
failedCount: number;
9995
}
10096

10197
function buildRunTimeline(
@@ -108,23 +104,48 @@ function buildRunTimeline(
108104
// Sort by creation time
109105
const sorted = [...allRuns].sort((a, b) => a.createdAt - b.createdAt);
110106

111-
// Group runs that happened within 60s of each other (same batch)
112-
const buckets: EvalSuiteRun[][] = [];
113-
let currentBucket: EvalSuiteRun[] = [sorted[0]];
107+
// Group runs by commit SHA when available, else by 60s time proximity
108+
const commitGroups = new Map<string, EvalSuiteRun[]>();
109+
const manualRuns: EvalSuiteRun[] = [];
114110

115-
for (let i = 1; i < sorted.length; i++) {
116-
const prev = currentBucket[currentBucket.length - 1];
117-
if (sorted[i].createdAt - prev.createdAt < 60_000) {
118-
currentBucket.push(sorted[i]);
111+
for (const run of sorted) {
112+
const sha = run.ciMetadata?.commitSha;
113+
if (sha) {
114+
const group = commitGroups.get(sha) ?? [];
115+
group.push(run);
116+
commitGroups.set(sha, group);
119117
} else {
120-
buckets.push(currentBucket);
121-
currentBucket = [sorted[i]];
118+
manualRuns.push(run);
122119
}
123120
}
124-
buckets.push(currentBucket);
125121

126-
// Dedupe by taking the latest N buckets
127-
const recentBuckets = buckets.slice(-maxBuckets);
122+
// Time-bucket manual runs (no commit SHA)
123+
const manualBuckets: EvalSuiteRun[][] = [];
124+
if (manualRuns.length > 0) {
125+
let currentBucket: EvalSuiteRun[] = [manualRuns[0]];
126+
for (let i = 1; i < manualRuns.length; i++) {
127+
const prev = currentBucket[currentBucket.length - 1];
128+
if (manualRuns[i].createdAt - prev.createdAt < 60_000) {
129+
currentBucket.push(manualRuns[i]);
130+
} else {
131+
manualBuckets.push(currentBucket);
132+
currentBucket = [manualRuns[i]];
133+
}
134+
}
135+
manualBuckets.push(currentBucket);
136+
}
137+
138+
// Merge commit groups + manual buckets, sort by latest timestamp
139+
const allBucketRuns: EvalSuiteRun[][] = [
140+
...Array.from(commitGroups.values()),
141+
...manualBuckets,
142+
].sort(
143+
(a, b) =>
144+
Math.max(...a.map((r) => r.createdAt)) -
145+
Math.max(...b.map((r) => r.createdAt)),
146+
);
147+
148+
const recentBuckets = allBucketRuns.slice(-maxBuckets);
128149

129150
return recentBuckets.map((runs, idx) => {
130151
const hasFailure = runs.some((r) => r.result === "failed");
@@ -142,17 +163,23 @@ function buildRunTimeline(
142163
: "mixed";
143164

144165
const timestamp = Math.max(...runs.map((r) => r.createdAt));
145-
// Use runNumber from first run if available
146-
const runNum = runs[0]?.runNumber;
147-
const label = runNum ? `#${runNum}` : `#${idx + 1}`;
166+
const commitSha = runs[0]?.ciMetadata?.commitSha ?? null;
167+
const branch = runs[0]?.ciMetadata?.branch ?? null;
168+
const suiteIds = new Set(runs.map((r) => r.suiteId));
169+
170+
const passedCount = runs.filter((r) => r.result === "passed").length;
171+
const failedCount = runs.filter((r) => r.result === "failed").length;
148172

149173
return {
150-
id: `bucket-${idx}`,
151-
label,
152-
date: formatShortDate(timestamp),
174+
id: commitSha ?? `manual-${idx}`,
175+
commitSha,
176+
branch,
153177
timestamp,
154178
result,
155179
runs,
180+
suiteIds,
181+
passedCount,
182+
failedCount,
156183
};
157184
});
158185
}
@@ -294,16 +321,21 @@ export function OverviewPanel({
294321
[filteredSuites],
295322
);
296323

297-
// Auto-select latest bucket
298-
const activeBucketId =
299-
selectedBucketId ?? (timeline.length > 0 ? timeline[timeline.length - 1].id : null);
324+
// null = show all suites (no filter)
325+
const activeBucketId = selectedBucketId;
326+
const activeBucket = timeline.find((b) => b.id === activeBucketId) ?? null;
300327

301328
// ---------------------------------------------------------------------------
302329
// Section D: Suite Table — severity-sorted, filtered, searchable
303330
// ---------------------------------------------------------------------------
304331
const tableSuites = useMemo(() => {
305332
let list = [...filteredSuites];
306333

334+
// Filter by selected timeline bucket
335+
if (activeBucket) {
336+
list = list.filter((e) => activeBucket.suiteIds.has(e.suite._id));
337+
}
338+
307339
// Search filter
308340
if (suiteSearch) {
309341
const q = suiteSearch.toLowerCase();
@@ -333,14 +365,18 @@ export function OverviewPanel({
333365
});
334366

335367
return list;
336-
}, [filteredSuites, suiteSearch, failuresOnly]);
368+
}, [filteredSuites, suiteSearch, failuresOnly, activeBucket]);
337369

338-
// Failure feed entries
370+
// Failure feed entries (also filtered by active bucket)
339371
const failureEntries = useMemo(() => {
340-
return filteredSuites.filter(
372+
let list = filteredSuites;
373+
if (activeBucket) {
374+
list = list.filter((e) => activeBucket.suiteIds.has(e.suite._id));
375+
}
376+
return list.filter(
341377
(e) => e.latestRun?.result === "failed" || !e.latestRun,
342378
);
343-
}, [filteredSuites]);
379+
}, [filteredSuites, activeBucket]);
344380

345381
// Auto-collapse failure feed when no failures
346382
const hasFailures = failureEntries.length > 0;
@@ -534,6 +570,24 @@ export function OverviewPanel({
534570
{timeline.length > 0 && (
535571
<div className="rounded-xl border bg-card p-3">
536572
<div className="flex items-center gap-2 overflow-x-auto pb-1">
573+
{/* "All" chip to clear filter */}
574+
<button
575+
onClick={() => setSelectedBucketId(null)}
576+
className={cn(
577+
"flex flex-col items-center gap-1 px-3 py-2 rounded-lg transition-all shrink-0 min-w-[48px]",
578+
activeBucketId === null
579+
? "bg-accent ring-2 ring-primary/30 shadow-sm"
580+
: "hover:bg-accent/50",
581+
)}
582+
>
583+
<span className="text-xs font-medium">All</span>
584+
<span className="text-[10px] text-muted-foreground">
585+
{timeline.reduce((n, b) => n + b.runs.length, 0)} runs
586+
</span>
587+
</button>
588+
589+
<div className="w-px h-6 bg-border shrink-0" />
590+
537591
{timeline.map((bucket) => {
538592
const isActive = bucket.id === activeBucketId;
539593
const chipColor =
@@ -545,12 +599,31 @@ export function OverviewPanel({
545599
? "bg-emerald-500"
546600
: "bg-muted-foreground";
547601

602+
const chipLabel = bucket.commitSha
603+
? bucket.commitSha.slice(0, 7)
604+
: "manual";
605+
606+
const totalRuns = bucket.runs.length;
607+
const summaryParts: string[] = [];
608+
if (bucket.passedCount > 0) summaryParts.push(`${bucket.passedCount}✓`);
609+
if (bucket.failedCount > 0) summaryParts.push(`${bucket.failedCount}✗`);
610+
const summaryText = summaryParts.length > 0
611+
? summaryParts.join(" ")
612+
: `${totalRuns} run${totalRuns !== 1 ? "s" : ""}`;
613+
614+
const tooltipParts = [
615+
bucket.branch ? `${bucket.branch} @ ${chipLabel}` : chipLabel,
616+
`${bucket.passedCount} passed, ${bucket.failedCount} failed of ${totalRuns}`,
617+
new Date(bucket.timestamp).toLocaleString(),
618+
];
619+
548620
return (
549621
<button
550622
key={bucket.id}
551-
onClick={() => setSelectedBucketId(bucket.id)}
623+
onClick={() => setSelectedBucketId(bucket.id === activeBucketId ? null : bucket.id)}
624+
title={tooltipParts.join("\n")}
552625
className={cn(
553-
"flex flex-col items-center gap-1 px-3 py-2 rounded-lg transition-all shrink-0 min-w-[60px]",
626+
"flex flex-col items-center gap-1 px-3 py-2 rounded-lg transition-all shrink-0 min-w-[68px]",
554627
isActive
555628
? "bg-accent ring-2 ring-primary/30 shadow-sm"
556629
: "hover:bg-accent/50",
@@ -560,10 +633,13 @@ export function OverviewPanel({
560633
<div
561634
className={cn("h-2.5 w-2.5 rounded-full", chipColor)}
562635
/>
563-
<span className="text-xs font-medium">{bucket.label}</span>
636+
<span className="text-xs font-mono font-medium">{chipLabel}</span>
564637
</div>
565638
<span className="text-[10px] text-muted-foreground">
566-
{bucket.date}
639+
{formatRelativeTime(bucket.timestamp)}
640+
</span>
641+
<span className="text-[10px] text-muted-foreground">
642+
{summaryText}
567643
</span>
568644
</button>
569645
);
@@ -679,6 +755,17 @@ export function OverviewPanel({
679755
<div className="rounded-xl border bg-card">
680756
{/* Table toolbar */}
681757
<div className="flex items-center gap-2 px-4 py-2.5 border-b flex-wrap">
758+
{activeBucket && (
759+
<button
760+
onClick={() => setSelectedBucketId(null)}
761+
className="text-xs px-2.5 py-1 rounded-full border bg-primary/10 text-primary border-primary/30 hover:bg-primary/20 transition-colors flex items-center gap-1"
762+
>
763+
<span className="font-mono">
764+
{activeBucket.commitSha ? activeBucket.commitSha.slice(0, 7) : "manual"}
765+
</span>
766+
<span>&times;</span>
767+
</button>
768+
)}
682769
<button
683770
onClick={() => setFailuresOnly(!failuresOnly)}
684771
className={cn(

0 commit comments

Comments
 (0)