Skip to content

Commit 6a1d050

Browse files
ok
1 parent e9ecd6b commit 6a1d050

File tree

1 file changed

+125
-39
lines changed

1 file changed

+125
-39
lines changed

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

Lines changed: 125 additions & 39 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,17 +321,21 @@ export function OverviewPanel({
294321
[filteredSuites],
295322
);
296323

297-
// Auto-select latest bucket
298-
const activeBucketId =
299-
selectedBucketId ??
300-
(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;
301327

302328
// ---------------------------------------------------------------------------
303329
// Section D: Suite Table — severity-sorted, filtered, searchable
304330
// ---------------------------------------------------------------------------
305331
const tableSuites = useMemo(() => {
306332
let list = [...filteredSuites];
307333

334+
// Filter by selected timeline bucket
335+
if (activeBucket) {
336+
list = list.filter((e) => activeBucket.suiteIds.has(e.suite._id));
337+
}
338+
308339
// Search filter
309340
if (suiteSearch) {
310341
const q = suiteSearch.toLowerCase();
@@ -334,14 +365,18 @@ export function OverviewPanel({
334365
});
335366

336367
return list;
337-
}, [filteredSuites, suiteSearch, failuresOnly]);
368+
}, [filteredSuites, suiteSearch, failuresOnly, activeBucket]);
338369

339-
// Failure feed entries
370+
// Failure feed entries (also filtered by active bucket)
340371
const failureEntries = useMemo(() => {
341-
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(
342377
(e) => e.latestRun?.result === "failed" || !e.latestRun,
343378
);
344-
}, [filteredSuites]);
379+
}, [filteredSuites, activeBucket]);
345380

346381
// Auto-collapse failure feed when no failures
347382
const hasFailures = failureEntries.length > 0;
@@ -536,6 +571,24 @@ export function OverviewPanel({
536571
{timeline.length > 0 && (
537572
<div className="rounded-xl border bg-card p-3">
538573
<div className="flex items-center gap-2 overflow-x-auto pb-1">
574+
{/* "All" chip to clear filter */}
575+
<button
576+
onClick={() => setSelectedBucketId(null)}
577+
className={cn(
578+
"flex flex-col items-center gap-1 px-3 py-2 rounded-lg transition-all shrink-0 min-w-[48px]",
579+
activeBucketId === null
580+
? "bg-accent ring-2 ring-primary/30 shadow-sm"
581+
: "hover:bg-accent/50",
582+
)}
583+
>
584+
<span className="text-xs font-medium">All</span>
585+
<span className="text-[10px] text-muted-foreground">
586+
{timeline.reduce((n, b) => n + b.runs.length, 0)} runs
587+
</span>
588+
</button>
589+
590+
<div className="w-px h-6 bg-border shrink-0" />
591+
539592
{timeline.map((bucket) => {
540593
const isActive = bucket.id === activeBucketId;
541594
const chipColor =
@@ -547,12 +600,31 @@ export function OverviewPanel({
547600
? "bg-emerald-500"
548601
: "bg-muted-foreground";
549602

603+
const chipLabel = bucket.commitSha
604+
? bucket.commitSha.slice(0, 7)
605+
: "manual";
606+
607+
const totalRuns = bucket.runs.length;
608+
const summaryParts: string[] = [];
609+
if (bucket.passedCount > 0) summaryParts.push(`${bucket.passedCount}✓`);
610+
if (bucket.failedCount > 0) summaryParts.push(`${bucket.failedCount}✗`);
611+
const summaryText = summaryParts.length > 0
612+
? summaryParts.join(" ")
613+
: `${totalRuns} run${totalRuns !== 1 ? "s" : ""}`;
614+
615+
const tooltipParts = [
616+
bucket.branch ? `${bucket.branch} @ ${chipLabel}` : chipLabel,
617+
`${bucket.passedCount} passed, ${bucket.failedCount} failed of ${totalRuns}`,
618+
new Date(bucket.timestamp).toLocaleString(),
619+
];
620+
550621
return (
551622
<button
552623
key={bucket.id}
553-
onClick={() => setSelectedBucketId(bucket.id)}
624+
onClick={() => setSelectedBucketId(bucket.id === activeBucketId ? null : bucket.id)}
625+
title={tooltipParts.join("\n")}
554626
className={cn(
555-
"flex flex-col items-center gap-1 px-3 py-2 rounded-lg transition-all shrink-0 min-w-[60px]",
627+
"flex flex-col items-center gap-1 px-3 py-2 rounded-lg transition-all shrink-0 min-w-[68px]",
556628
isActive
557629
? "bg-accent ring-2 ring-primary/30 shadow-sm"
558630
: "hover:bg-accent/50",
@@ -562,10 +634,13 @@ export function OverviewPanel({
562634
<div
563635
className={cn("h-2.5 w-2.5 rounded-full", chipColor)}
564636
/>
565-
<span className="text-xs font-medium">{bucket.label}</span>
637+
<span className="text-xs font-mono font-medium">{chipLabel}</span>
566638
</div>
567639
<span className="text-[10px] text-muted-foreground">
568-
{bucket.date}
640+
{formatRelativeTime(bucket.timestamp)}
641+
</span>
642+
<span className="text-[10px] text-muted-foreground">
643+
{summaryText}
569644
</span>
570645
</button>
571646
);
@@ -688,6 +763,17 @@ export function OverviewPanel({
688763
<div className="rounded-xl border bg-card">
689764
{/* Table toolbar */}
690765
<div className="flex items-center gap-2 px-4 py-2.5 border-b flex-wrap">
766+
{activeBucket && (
767+
<button
768+
onClick={() => setSelectedBucketId(null)}
769+
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"
770+
>
771+
<span className="font-mono">
772+
{activeBucket.commitSha ? activeBucket.commitSha.slice(0, 7) : "manual"}
773+
</span>
774+
<span>&times;</span>
775+
</button>
776+
)}
691777
<button
692778
onClick={() => setFailuresOnly(!failuresOnly)}
693779
className={cn(

0 commit comments

Comments
 (0)