Skip to content

Commit 3769430

Browse files
Clean up sidebar and other top improves
1 parent 5562168 commit 3769430

File tree

5 files changed

+370
-172
lines changed

5 files changed

+370
-172
lines changed
Lines changed: 238 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect } from "react";
1+
import { useState, useEffect, useMemo } from "react";
22
import { BarChart3 } from "lucide-react";
33
import { cn } from "@/lib/utils";
44
import type { CommitGroup, EvalSuiteOverviewEntry } from "./types";
@@ -94,25 +94,37 @@ export function CiSuiteListSidebar({
9494
? suites.filter((e) => e.suite.tags?.includes(filterTag))
9595
: suites;
9696

97+
// Group suites by name, keeping the most recent one as the "primary" entry
98+
const groupedSuites = useMemo(() => {
99+
const groups = new Map<string, EvalSuiteOverviewEntry[]>();
100+
for (const entry of filteredSuites) {
101+
const name = entry.suite.name || "Untitled suite";
102+
if (!groups.has(name)) {
103+
groups.set(name, []);
104+
}
105+
groups.get(name)!.push(entry);
106+
}
107+
// Sort each group by latest run time (most recent first)
108+
for (const entries of groups.values()) {
109+
entries.sort((a, b) => {
110+
const aTime = a.latestRun?.completedAt ?? a.latestRun?.createdAt ?? a.suite.updatedAt ?? 0;
111+
const bTime = b.latestRun?.completedAt ?? b.latestRun?.createdAt ?? b.suite.updatedAt ?? 0;
112+
return bTime - aTime;
113+
});
114+
}
115+
return groups;
116+
}, [filteredSuites]);
117+
118+
const uniqueSuiteCount = groupedSuites.size;
119+
120+
const failCount = suites.filter(
121+
(e) => e.latestRun?.result === "failed",
122+
).length;
123+
97124
return (
98125
<div className="flex h-full flex-col">
99-
<div className="border-b px-4 py-3">
100-
<div className="flex items-center justify-between">
101-
<h2 className="text-sm font-semibold">
102-
{sidebarMode === "suites" ? "Eval suites" : "Runs by commit"}
103-
</h2>
104-
{sidebarMode === "suites" && filteredSuites.length > 0 && (
105-
<span className="text-[10px] text-muted-foreground tabular-nums">
106-
{filteredSuites.length}
107-
</span>
108-
)}
109-
{sidebarMode === "runs" && commitGroups.length > 0 && (
110-
<span className="text-[10px] text-muted-foreground tabular-nums">
111-
{commitGroups.length}
112-
</span>
113-
)}
114-
</div>
115-
<div className="mt-2 flex rounded-md border bg-muted/50 p-0.5">
126+
<div className="border-b px-4 py-3 space-y-2">
127+
<div className="flex rounded-md border bg-muted/50 p-0.5">
116128
<button
117129
onClick={() => onSidebarModeChange("runs")}
118130
className={cn(
@@ -122,7 +134,7 @@ export function CiSuiteListSidebar({
122134
: "text-muted-foreground hover:text-foreground",
123135
)}
124136
>
125-
Runs
137+
By Commit
126138
</button>
127139
<button
128140
onClick={() => onSidebarModeChange("suites")}
@@ -133,39 +145,31 @@ export function CiSuiteListSidebar({
133145
: "text-muted-foreground hover:text-foreground",
134146
)}
135147
>
136-
Suites
148+
By Suite
137149
</button>
138150
</div>
139151
</div>
140152

141-
{/* Overview button — always visible regardless of sidebar mode */}
142-
<button
143-
onClick={onSelectOverview}
144-
className={cn(
145-
"w-full px-4 py-2.5 text-left transition-colors hover:bg-accent/50 border-b",
146-
isOverviewSelected && "bg-accent",
147-
)}
148-
>
149-
<div className="flex items-center gap-2.5">
150-
<BarChart3 className="h-4 w-4 shrink-0 text-muted-foreground" />
151-
<div className="min-w-0 flex-1">
152-
<div className="text-sm font-medium">Overview</div>
153-
<div className="text-[11px] text-muted-foreground">
154-
Suite health & status
155-
</div>
156-
</div>
157-
{(() => {
158-
const failCount = suites.filter(
159-
(e) => e.latestRun?.result === "failed",
160-
).length;
161-
return failCount > 0 ? (
162-
<span className="shrink-0 flex h-5 min-w-[20px] items-center justify-center rounded-full bg-destructive px-1.5 text-[10px] font-bold text-destructive-foreground">
163-
{failCount}
164-
</span>
165-
) : null;
166-
})()}
167-
</div>
168-
</button>
153+
{/* Dashboard button — always visible regardless of sidebar mode */}
154+
<div className="px-3 py-2 border-b">
155+
<button
156+
onClick={onSelectOverview}
157+
className={cn(
158+
"w-full flex items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-xs font-medium transition-colors cursor-pointer border border-transparent",
159+
isOverviewSelected
160+
? "bg-primary/15 text-primary border-primary/30"
161+
: "text-muted-foreground hover:bg-accent hover:text-foreground hover:border-border",
162+
)}
163+
>
164+
<BarChart3 className="h-3.5 w-3.5 shrink-0" />
165+
<span className="flex-1">Dashboard</span>
166+
{failCount > 0 && (
167+
<span className="shrink-0 flex h-4 min-w-[16px] items-center justify-center rounded-full bg-destructive px-1 text-[9px] font-bold text-destructive-foreground">
168+
{failCount}
169+
</span>
170+
)}
171+
</button>
172+
</div>
169173

170174
{sidebarMode === "runs" ? (
171175
<CommitListSidebar
@@ -186,71 +190,196 @@ export function CiSuiteListSidebar({
186190
</div>
187191
) : (
188192
<div>
189-
{filteredSuites.map((entry) => {
190-
const latestRun = entry.latestRun;
191-
const status = getStatusInfo(entry);
192-
const trend = entry.passRateTrend
193-
.slice(-12)
194-
.map((value) => toPercent(value));
195-
const timestamp = formatRelativeTime(
196-
latestRun?.completedAt ??
197-
latestRun?.createdAt ??
198-
entry.suite.updatedAt,
199-
);
200-
201-
return (
202-
<button
203-
key={entry.suite._id}
204-
onClick={() => onSelectSuite(entry.suite._id)}
205-
className={cn(
206-
"w-full px-4 py-2.5 text-left transition-colors hover:bg-accent/50",
207-
selectedSuiteId === entry.suite._id && "bg-accent",
208-
)}
209-
>
210-
<div className="flex items-center gap-2.5">
211-
<div className="flex flex-col items-center gap-0.5 shrink-0 w-[3.25rem]">
212-
<div
213-
className={cn(
214-
"h-2 w-2 rounded-full",
215-
status.dotClass,
216-
)}
217-
/>
218-
<span className={cn("text-[9px] font-medium leading-none", status.labelClass)}>
219-
{status.label}
220-
</span>
221-
</div>
222-
<div className="min-w-0 flex-1">
223-
<div className="truncate text-sm font-medium">
224-
{entry.suite.name || "Untitled suite"}
225-
</div>
226-
{entry.suite.tags && entry.suite.tags.length > 0 && (
227-
<TagBadges tags={entry.suite.tags} className="mt-0.5" />
228-
)}
229-
<div className="text-[11px] text-muted-foreground">
230-
{timestamp}
231-
</div>
232-
</div>
233-
{trend.length > 0 && (
234-
<div className="flex h-5 shrink-0 items-end gap-px">
235-
{trend.map((value, idx) => (
236-
<div
237-
key={`${entry.suite._id}-t-${idx}`}
238-
className="w-1 rounded-sm bg-primary/70"
239-
style={{
240-
height: `${Math.max(3, (value / 100) * 20)}px`,
241-
}}
242-
/>
243-
))}
244-
</div>
245-
)}
246-
</div>
247-
</button>
248-
);
249-
})}
193+
{Array.from(groupedSuites.entries()).map(([suiteName, entries]) => (
194+
<SuiteGroupItem
195+
key={suiteName}
196+
suiteName={suiteName}
197+
entries={entries}
198+
selectedSuiteId={selectedSuiteId}
199+
onSelectSuite={onSelectSuite}
200+
/>
201+
))}
250202
</div>
251203
)}
252204
</div>
253205
)}
254206
</div>
255207
);
256208
}
209+
210+
function SuiteGroupItem({
211+
suiteName,
212+
entries,
213+
selectedSuiteId,
214+
onSelectSuite,
215+
}: {
216+
suiteName: string;
217+
entries: EvalSuiteOverviewEntry[];
218+
selectedSuiteId: string | null;
219+
onSelectSuite: (suiteId: string) => void;
220+
}) {
221+
const primary = entries[0]; // most recent
222+
const hasMultiple = entries.length > 1;
223+
const isAnySelected = entries.some((e) => e.suite._id === selectedSuiteId);
224+
const [expanded, setExpanded] = useState(false);
225+
226+
const latestRun = primary.latestRun;
227+
const status = getStatusInfo(primary);
228+
const trend = primary.passRateTrend.slice(-12).map((value) => toPercent(value));
229+
const timestamp = formatRelativeTime(
230+
latestRun?.completedAt ?? latestRun?.createdAt ?? primary.suite.updatedAt,
231+
);
232+
233+
// For single-entry groups, render directly
234+
if (!hasMultiple) {
235+
return (
236+
<SuiteEntryButton
237+
entry={primary}
238+
isSelected={selectedSuiteId === primary.suite._id}
239+
onSelect={() => onSelectSuite(primary.suite._id)}
240+
status={status}
241+
trend={trend}
242+
timestamp={timestamp}
243+
/>
244+
);
245+
}
246+
247+
// For multi-entry groups, render as expandable group
248+
return (
249+
<div>
250+
<button
251+
onClick={() => {
252+
if (!isAnySelected) {
253+
// Click selects the most recent entry
254+
onSelectSuite(primary.suite._id);
255+
} else {
256+
setExpanded(!expanded);
257+
}
258+
}}
259+
className={cn(
260+
"w-full px-4 py-2.5 text-left transition-colors hover:bg-accent/50",
261+
isAnySelected && "bg-accent shadow-sm",
262+
)}
263+
>
264+
<div className="flex items-center gap-2.5">
265+
<div className="flex flex-col items-center gap-0.5 shrink-0 w-[3.25rem]">
266+
<div className={cn("h-2 w-2 rounded-full", status.dotClass)} />
267+
<span className={cn("text-[9px] font-medium leading-none", status.labelClass)}>
268+
{status.label}
269+
</span>
270+
</div>
271+
<div className="min-w-0 flex-1">
272+
<div className="flex items-center gap-1.5">
273+
<span className={cn("truncate text-sm font-medium", isAnySelected && "font-semibold")}>
274+
{suiteName}
275+
</span>
276+
<span className="shrink-0 flex h-4 min-w-[16px] items-center justify-center rounded-full bg-muted px-1 text-[9px] font-medium text-muted-foreground">
277+
{entries.length}
278+
</span>
279+
</div>
280+
{primary.suite.tags && primary.suite.tags.length > 0 && (
281+
<TagBadges tags={primary.suite.tags} className="mt-0.5" />
282+
)}
283+
<div className="text-[11px] text-muted-foreground">{timestamp}</div>
284+
</div>
285+
{trend.length >= 3 && (
286+
<div className="flex h-5 shrink-0 items-end gap-px">
287+
{trend.map((value, idx) => (
288+
<div
289+
key={`${primary.suite._id}-t-${idx}`}
290+
className={cn("w-1 rounded-sm", value >= 80 ? "bg-emerald-500/70" : value >= 50 ? "bg-amber-500/70" : "bg-destructive/70")}
291+
style={{ height: `${Math.max(3, (value / 100) * 20)}px` }}
292+
/>
293+
))}
294+
</div>
295+
)}
296+
</div>
297+
</button>
298+
{(expanded || isAnySelected) && entries.length > 1 && (
299+
<div className="border-l-2 border-muted ml-6">
300+
{entries.map((entry) => {
301+
const entryStatus = getStatusInfo(entry);
302+
const entryTimestamp = formatRelativeTime(
303+
entry.latestRun?.completedAt ?? entry.latestRun?.createdAt ?? entry.suite.updatedAt,
304+
);
305+
return (
306+
<button
307+
key={entry.suite._id}
308+
onClick={() => onSelectSuite(entry.suite._id)}
309+
className={cn(
310+
"w-full px-3 py-1.5 text-left transition-colors hover:bg-accent/50",
311+
selectedSuiteId === entry.suite._id && "bg-primary/10 border-r-2 border-r-primary",
312+
)}
313+
>
314+
<div className="flex items-center gap-2">
315+
<div className={cn("h-1.5 w-1.5 rounded-full shrink-0", entryStatus.dotClass)} />
316+
<span className="text-[11px] text-muted-foreground truncate flex-1">
317+
{entryTimestamp}
318+
</span>
319+
<span className={cn("text-[10px] font-medium", entryStatus.labelClass)}>
320+
{entryStatus.label}
321+
</span>
322+
</div>
323+
</button>
324+
);
325+
})}
326+
</div>
327+
)}
328+
</div>
329+
);
330+
}
331+
332+
function SuiteEntryButton({
333+
entry,
334+
isSelected,
335+
onSelect,
336+
status,
337+
trend,
338+
timestamp,
339+
}: {
340+
entry: EvalSuiteOverviewEntry;
341+
isSelected: boolean;
342+
onSelect: () => void;
343+
status: { label: string; dotClass: string; labelClass: string };
344+
trend: number[];
345+
timestamp: string;
346+
}) {
347+
return (
348+
<button
349+
onClick={onSelect}
350+
className={cn(
351+
"w-full px-4 py-2.5 text-left transition-colors hover:bg-accent/50",
352+
isSelected && "bg-accent shadow-sm",
353+
)}
354+
>
355+
<div className="flex items-center gap-2.5">
356+
<div className="flex flex-col items-center gap-0.5 shrink-0 w-[3.25rem]">
357+
<div className={cn("h-2 w-2 rounded-full", status.dotClass)} />
358+
<span className={cn("text-[9px] font-medium leading-none", status.labelClass)}>
359+
{status.label}
360+
</span>
361+
</div>
362+
<div className="min-w-0 flex-1">
363+
<div className={cn("truncate text-sm font-medium", isSelected && "font-semibold")}>
364+
{entry.suite.name || "Untitled suite"}
365+
</div>
366+
{entry.suite.tags && entry.suite.tags.length > 0 && (
367+
<TagBadges tags={entry.suite.tags} className="mt-0.5" />
368+
)}
369+
<div className="text-[11px] text-muted-foreground">{timestamp}</div>
370+
</div>
371+
{trend.length >= 3 && (
372+
<div className="flex h-5 shrink-0 items-end gap-px">
373+
{trend.map((value, idx) => (
374+
<div
375+
key={`${entry.suite._id}-t-${idx}`}
376+
className="w-1 rounded-sm bg-primary/70"
377+
style={{ height: `${Math.max(3, (value / 100) * 20)}px` }}
378+
/>
379+
))}
380+
</div>
381+
)}
382+
</div>
383+
</button>
384+
);
385+
}

0 commit comments

Comments
 (0)