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
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
if state:
@@ -61,3 +61,24 @@ def test_assignee_username_is_none_when_unassigned(client, db, red_lead_headers,
body = resp.json()
assert body["red_tech_assignee"] 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."} />
</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>
);
}
@@ -721,6 +753,7 @@ function TestTable({
navigate,
formatDate,
emptyMessage,
showAssignees = false,
}: {
tests: Test[];
columns: { key: SortKey; label: string; cls: string }[];
@@ -730,6 +763,9 @@ function TestTable({
navigate: (path: string) => void;
formatDate: (d: string | null | undefined) => 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 (
<div className="overflow-x-auto">
@@ -759,6 +795,12 @@ function TestTable({
</span>
</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>
</tr>
</thead>
@@ -819,6 +861,16 @@ function TestTable({
<td className="py-3 px-4 text-gray-400 text-xs whitespace-nowrap">
{formatDate(lastActivityDate(test))}
</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">
<button
onClick={(e) => {