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
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:
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user