feat(tests): add Detect Procedure UI, template suggested-procedure editing, lead review panel
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
Blue Team gets a Detect Procedure textarea in the test detail view, mirroring Red's Attack Procedure field. Template create/edit forms gain a Detect Suggested Procedure field for leads to curate directly. Leads also get a Procedure Suggestions panel on the Tests page, scoped to their own team by the backend, to approve or reject suggestions extracted from submitted rounds.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
Loader2,
|
||||
@@ -19,8 +19,14 @@ import {
|
||||
ChevronsUpDown,
|
||||
Timer,
|
||||
AlertTriangle,
|
||||
Lightbulb,
|
||||
} from "lucide-react";
|
||||
import { getTests, type TestListFilters } from "../api/tests";
|
||||
import {
|
||||
getProcedureSuggestions,
|
||||
approveProcedureSuggestion,
|
||||
rejectProcedureSuggestion,
|
||||
} from "../api/procedure-suggestions";
|
||||
import type { Test, TestState } from "../types/models";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
|
||||
@@ -707,6 +713,12 @@ export default function TestsPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Procedure suggestions for leads ───────────────────────────
|
||||
GET /procedure-suggestions is already scoped server-side to the
|
||||
caller's own team (red_lead sees only red, blue_lead only blue),
|
||||
so no client-side filtering is needed here. */}
|
||||
{isReviewLead && <ProcedureSuggestionsPanel />}
|
||||
|
||||
{/* ── 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
|
||||
@@ -742,6 +754,76 @@ export default function TestsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Procedure suggestions panel (leads only) ─────────────────────── */
|
||||
|
||||
function ProcedureSuggestionsPanel() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: suggestions = [] } = useQuery({
|
||||
queryKey: ["procedure-suggestions"],
|
||||
queryFn: getProcedureSuggestions,
|
||||
});
|
||||
|
||||
const invalidate = () => queryClient.invalidateQueries({ queryKey: ["procedure-suggestions"] });
|
||||
const approveMutation = useMutation({ mutationFn: approveProcedureSuggestion, onSuccess: invalidate });
|
||||
const rejectMutation = useMutation({ mutationFn: rejectProcedureSuggestion, onSuccess: invalidate });
|
||||
const isBusy = approveMutation.isPending || rejectMutation.isPending;
|
||||
|
||||
if (suggestions.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-gray-800 bg-gray-900 p-6">
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<Lightbulb className="h-5 w-5 text-yellow-400" />
|
||||
<h2 className="text-lg font-semibold text-white">Procedure Suggestions</h2>
|
||||
<span className="rounded-full border border-gray-600 bg-gray-800 px-2 py-0.5 text-xs font-medium text-gray-300">
|
||||
{suggestions.length}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
Commands extracted from submitted rounds, proposed as template updates
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{suggestions.map((s) => (
|
||||
<div key={s.id} className="rounded-lg border border-gray-700 bg-gray-800/50 p-4">
|
||||
<p className="text-sm font-medium text-gray-200">{s.template_name || "Unknown template"}</p>
|
||||
{s.template_current_text && (
|
||||
<div className="mt-2">
|
||||
<p className="text-xs font-medium uppercase text-gray-500">Current</p>
|
||||
<pre className="mt-1 whitespace-pre-wrap rounded bg-gray-900 p-2 font-mono text-xs text-gray-400">
|
||||
{s.template_current_text}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-2">
|
||||
<p className="text-xs font-medium uppercase text-gray-500">Suggested</p>
|
||||
<pre className="mt-1 whitespace-pre-wrap rounded bg-gray-900 p-2 font-mono text-xs text-green-400">
|
||||
{s.suggested_text}
|
||||
</pre>
|
||||
</div>
|
||||
<div className="mt-3 flex gap-2">
|
||||
<button
|
||||
onClick={() => approveMutation.mutate(s.id)}
|
||||
disabled={isBusy}
|
||||
className="flex items-center gap-1 rounded-lg border border-green-500/30 bg-green-900/20 px-3 py-1.5 text-xs font-medium text-green-400 hover:bg-green-900/40 disabled:opacity-50"
|
||||
>
|
||||
<CheckCircle className="h-3.5 w-3.5" /> Approve
|
||||
</button>
|
||||
<button
|
||||
onClick={() => rejectMutation.mutate(s.id)}
|
||||
disabled={isBusy}
|
||||
className="flex items-center gap-1 rounded-lg border border-red-500/30 bg-red-900/20 px-3 py-1.5 text-xs font-medium text-red-400 hover:bg-red-900/40 disabled:opacity-50"
|
||||
>
|
||||
<XCircle className="h-3.5 w-3.5" /> Reject
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Shared test table component ────────────────────────────────────── */
|
||||
|
||||
function TestTable({
|
||||
|
||||
Reference in New Issue
Block a user