File size: 4,894 Bytes
8b41737 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | 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>
);
}
|