| | 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); |
| |
|
| | |
| | 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> |
| | ); |
| | } |
| |
|