Zayne Rea Sprague
Initial deploy: aggregate trace visualizer
8b41737
import type { GroupedInstance } from "../types";
interface Props {
groups: GroupedInstance[];
totalGroups: number;
onSelect: (instanceId: string) => void;
filterResolved: "all" | "resolved" | "unresolved";
onFilterChange: (f: "all" | "resolved" | "unresolved") => void;
searchQuery: string;
onSearchChange: (q: string) => void;
loading: boolean;
}
export function InstanceList({
groups,
totalGroups,
onSelect,
filterResolved,
onFilterChange,
searchQuery,
onSearchChange,
loading,
}: Props) {
return (
<div className="flex-1 flex flex-col overflow-hidden">
{/* Toolbar */}
<div className="flex items-center gap-3 px-4 py-3 border-b border-gray-800 bg-gray-900/50">
<input
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
placeholder="Search instances..."
className="flex-1 max-w-sm bg-gray-800 border border-gray-700 rounded px-3 py-1.5 text-sm text-gray-200 placeholder-gray-500 focus:outline-none focus:border-blue-500"
/>
<div className="flex gap-1">
{(["all", "resolved", "unresolved"] as const).map((f) => (
<button
key={f}
onClick={() => onFilterChange(f)}
className={`px-2.5 py-1 rounded text-xs font-medium ${
filterResolved === f
? "bg-blue-600 text-white"
: "bg-gray-800 text-gray-400 hover:text-gray-200"
}`}
>
{f}
</button>
))}
</div>
<span className="text-xs text-gray-500">
{groups.length === totalGroups
? `${groups.length} instances`
: `${groups.length} / ${totalGroups} instances`}
</span>
{loading && (
<span className="text-xs text-blue-400 animate-pulse">Loading...</span>
)}
</div>
{/* Instance grid */}
<div className="flex-1 overflow-y-auto p-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
{groups.map((g) => (
<InstanceCard key={g.instance_id} group={g} onClick={() => onSelect(g.instance_id)} />
))}
</div>
{groups.length === 0 && !loading && (
<div className="text-center text-gray-500 mt-12">
No instances match your filters
</div>
)}
</div>
</div>
);
}
function InstanceCard({
group,
onClick,
}: {
group: GroupedInstance;
onClick: () => void;
}) {
const anyResolved = group.datasets.some((d) => d.summary.resolved);
const allResolved = group.datasets.every((d) => d.summary.resolved);
// Parse instance_id: "repo__issue-number"
const parts = group.instance_id.split("__");
const repo = parts[0] || "";
const issue = parts.slice(1).join("__") || "";
return (
<button
onClick={onClick}
className="text-left bg-gray-900 border border-gray-800 rounded-lg p-3 hover:border-gray-600 hover:bg-gray-800/50 transition-colors"
>
<div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex-1">
<div className="text-xs text-gray-500 truncate">{repo}</div>
<div className="text-sm text-gray-200 font-medium truncate mt-0.5">
{issue}
</div>
</div>
<span
className={`shrink-0 px-1.5 py-0.5 rounded text-xs font-medium ${
allResolved
? "bg-emerald-900/50 text-emerald-300"
: anyResolved
? "bg-yellow-900/50 text-yellow-300"
: "bg-red-900/50 text-red-300"
}`}
>
{allResolved ? "pass" : anyResolved ? "partial" : "fail"}
</span>
</div>
<div className="mt-2 flex flex-wrap gap-1">
{group.datasets.map((d) => (
<span
key={d.ds_id}
className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs ${
d.summary.resolved
? "bg-emerald-900/30 text-emerald-400"
: "bg-gray-800 text-gray-500"
}`}
title={d.repo}
>
<span
className={`w-1.5 h-1.5 rounded-full ${
d.summary.resolved ? "bg-emerald-400" : "bg-gray-600"
}`}
/>
{d.name.length > 30 ? d.name.slice(0, 28) + ".." : d.name}
</span>
))}
</div>
{group.datasets.length > 0 && (
<div className="mt-2 text-xs text-gray-500">
{group.datasets.length} agent{group.datasets.length > 1 ? "s" : ""}
{group.datasets[0].summary.duration_seconds > 0 && (
<span className="ml-2">
{Math.round(group.datasets[0].summary.duration_seconds)}s
</span>
)}
</div>
)}
</button>
);
}