feat(assign): let leads manually assign operators + sync Jira; send platform as Jira label
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

Adds GET /users/operators (leads+admin) and wires a lead-only assign
control into the test detail header, calling the existing but
previously unreachable POST /tests/{id}/assign endpoint. Manual
assignment now also pushes the Jira ticket assignee immediately
instead of waiting for the operator to start execution.

Also adds test.platform as a kebab-case label on Jira ticket creation.
This commit is contained in:
kitos
2026-07-10 08:44:14 +02:00
parent 119db6f91d
commit 2afb886cd9
12 changed files with 487 additions and 4 deletions
+23
View File
@@ -23,9 +23,11 @@ import {
holdTest,
resumeTest,
updateTestClassification,
assignTestOperators,
getTestTimeline,
getRetestChain,
} from "../api/tests";
import { getOperators } from "../api/users";
import { uploadEvidence, getEvidence } from "../api/evidence";
import { useAuth } from "../context/AuthContext";
import type { TestResult, ContainmentResult, AttackSuccessResult, DataClassification, TeamSide, TestTimelineEntry } from "../types/models";
@@ -133,6 +135,14 @@ export default function TestDetailPage() {
enabled: !!testId && !!test && (test.retest_of !== null || test.retest_count > 0),
});
const canAssignOperators = ["red_lead", "blue_lead", "admin"].includes(user?.role ?? "");
const { data: operators = [] } = useQuery({
queryKey: ["operators"],
queryFn: getOperators,
enabled: canAssignOperators,
staleTime: 5 * 60 * 1000,
});
// Hydrate drafts from test data
useEffect(() => {
if (test) {
@@ -368,6 +378,16 @@ export default function TestDetailPage() {
onError: (err: unknown) => showToast(extractError(err), "error"),
});
const assignOperatorMutation = useMutation({
mutationFn: ({ side, userId }: { side: "red" | "blue"; userId: string | null }) =>
assignTestOperators(testId!, side === "red" ? { red_tech_assignee: userId } : { blue_tech_assignee: userId }),
onSuccess: () => {
invalidateAll();
showToast("Assignment updated", "success");
},
onError: (err: unknown) => showToast(extractError(err), "error"),
});
// Evidence upload
const uploadMutation = useMutation({
mutationFn: ({ file, team }: { file: File; team: TeamSide }) =>
@@ -532,6 +552,9 @@ export default function TestDetailPage() {
isTogglingHold={holdMutation.isPending || resumeHoldMutation.isPending}
onUpdateClassification={(value) => updateClassificationMutation.mutate(value)}
isUpdatingClassification={updateClassificationMutation.isPending}
operators={operators}
onAssignOperator={(side, userId) => assignOperatorMutation.mutate({ side, userId })}
isAssigningOperator={assignOperatorMutation.isPending}
/>
{/* Content: Tabs + Sidebar */}