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