feat(tests): add team queue overview section for leads
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled

red_lead/blue_lead never saw a plain "All Tests" list — they were always
routed into their own review two-queue view or "My Tasks", with no way to
see every test and who's actually working it. Adds an always-visible
section at the end of the Tests page for leads showing every test with
both Red and Blue assignee columns, so they can monitor the queues and
spot a stuck ticket or an overloaded operator.

The list endpoint (GET /tests) now joinedloads the assignee relationships
and resolves usernames the same way the detail endpoint already does, so
the new section doesn't depend on GET /users/operators (lead/manager-only).
This commit is contained in:
kitos
2026-07-14 11:03:17 +02:00
parent c6bdded3f6
commit 9d66c30127
3 changed files with 77 additions and 1 deletions
+4 -1
View File
@@ -70,7 +70,10 @@ def _build_test_query(
Kept as a single source of truth so the displayed page of results and Kept as a single source of truth so the displayed page of results and
the total count it's paginated against can never drift apart. the total count it's paginated against can never drift apart.
""" """
query = db.query(Test).options(joinedload(Test.technique)) query = db.query(Test).options(
joinedload(Test.technique),
joinedload(Test.red_tech_assigned_user), joinedload(Test.blue_tech_assigned_user),
)
# Check: state # Check: state
if state: if state:
@@ -61,3 +61,24 @@ def test_assignee_username_is_none_when_unassigned(client, db, red_lead_headers,
body = resp.json() body = resp.json()
assert body["red_tech_assignee"] is None assert body["red_tech_assignee"] is None
assert body["red_tech_assignee_username"] is None assert body["red_tech_assignee_username"] is None
def test_list_endpoint_resolves_assignee_usernames_for_team_queue_overview(
client, db, red_lead_headers, red_lead_user, red_tech_user, blue_tech_user,
):
"""The leads' 'All Tests — Team Queue Overview' section reads assignee
usernames off GET /tests (the list endpoint), not just the detail
endpoint — both must resolve them the same way."""
technique = _seed_technique(db)
_seed_test(
db, technique, red_lead_user.id,
red_tech_assignee=red_tech_user.id, blue_tech_assignee=blue_tech_user.id,
)
resp = client.get("/api/v1/tests", headers=red_lead_headers)
assert resp.status_code == 200, resp.text
body = resp.json()
match = next(t for t in body if t["red_tech_assignee"] == str(red_tech_user.id))
assert match["red_tech_assignee_username"] == red_tech_user.username
assert match["blue_tech_assignee_username"] == blue_tech_user.username
+52
View File
@@ -706,6 +706,38 @@ export default function TestsPage() {
<TestTable tests={tests} columns={mainTableColumns} sortKey={sortKey} sortDir={sortDir} handleSort={handleSort} navigate={navigate} formatDate={formatDate} emptyMessage={showMyTasks ? "No pending tasks for your role." : "No tests found matching your filters."} /> <TestTable tests={tests} columns={mainTableColumns} sortKey={sortKey} sortDir={sortDir} handleSort={handleSort} navigate={navigate} formatDate={formatDate} emptyMessage={showMyTasks ? "No pending tasks for your role." : "No tests found matching your filters."} />
</div> </div>
)} )}
{/* ── Team queue overview for leads ─────────────────────────────
red_lead/blue_lead never hit the plain "All Tests" branch above —
they're always routed into the review two-queue view or their
own "My Tasks" — so they had no way to see every test and who's
working it, which is what they need to spot a stuck ticket or an
overloaded operator. Always shown at the end, independent of the
my-tasks toggle above, with assignees visible on both sides. */}
{isReviewLead && (
<div className="rounded-xl border border-gray-800 bg-gray-900 p-6">
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-2">
<h2 className="text-lg font-semibold text-white">All Tests Team Queue Overview</h2>
<span className="rounded-full border border-gray-600 bg-gray-800 px-2 py-0.5 text-xs font-medium text-gray-300">
{tests.length}
</span>
</div>
<span className="text-xs text-gray-500">Every test, with who's assigned on each side</span>
</div>
<TestTable
tests={tests}
columns={mainTableColumns}
sortKey={sortKey}
sortDir={sortDir}
handleSort={handleSort}
navigate={navigate}
formatDate={formatDate}
emptyMessage="No tests found matching your filters."
showAssignees
/>
</div>
)}
</div> </div>
); );
} }
@@ -721,6 +753,7 @@ function TestTable({
navigate, navigate,
formatDate, formatDate,
emptyMessage, emptyMessage,
showAssignees = false,
}: { }: {
tests: Test[]; tests: Test[];
columns: { key: SortKey; label: string; cls: string }[]; columns: { key: SortKey; label: string; cls: string }[];
@@ -730,6 +763,9 @@ function TestTable({
navigate: (path: string) => void; navigate: (path: string) => void;
formatDate: (d: string | null | undefined) => string; formatDate: (d: string | null | undefined) => string;
emptyMessage: string; emptyMessage: string;
/** Adds Red/Blue assignee columns — for the leads' team queue overview,
* where seeing who's working each side is the whole point. */
showAssignees?: boolean;
}) { }) {
return ( return (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
@@ -759,6 +795,12 @@ function TestTable({
</span> </span>
</th> </th>
))} ))}
{showAssignees && (
<>
<th className="pb-3 px-4 font-medium text-gray-400">Red Assignee</th>
<th className="pb-3 px-4 font-medium text-gray-400">Blue Assignee</th>
</>
)}
<th className="pb-3 pl-4 font-medium text-gray-400">Action</th> <th className="pb-3 pl-4 font-medium text-gray-400">Action</th>
</tr> </tr>
</thead> </thead>
@@ -819,6 +861,16 @@ function TestTable({
<td className="py-3 px-4 text-gray-400 text-xs whitespace-nowrap"> <td className="py-3 px-4 text-gray-400 text-xs whitespace-nowrap">
{formatDate(lastActivityDate(test))} {formatDate(lastActivityDate(test))}
</td> </td>
{showAssignees && (
<>
<td className="py-3 px-4 text-xs text-gray-400 whitespace-nowrap">
{test.red_tech_assignee_username || "-"}
</td>
<td className="py-3 px-4 text-xs text-gray-400 whitespace-nowrap">
{test.blue_tech_assignee_username || "-"}
</td>
</>
)}
<td className="py-3 pl-4"> <td className="py-3 pl-4">
<button <button
onClick={(e) => { onClick={(e) => {