feat(tests): on-hold button with reason modal, Jira comment + On Hold transition
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

This commit is contained in:
kitos
2026-06-19 09:53:05 +02:00
parent 6147f15238
commit 4e1f35c250
9 changed files with 333 additions and 0 deletions
@@ -13,6 +13,8 @@ import {
MessageSquare,
X,
UserCheck,
PauseCircle,
PlayCircle,
} from "lucide-react";
import { useMutation } from "@tanstack/react-query";
import { requestDiscussion } from "../../api/tests";
@@ -66,6 +68,9 @@ interface TestDetailHeaderProps {
onPauseTimer: () => void;
onResumeTimer: () => void;
isTogglingTimer: boolean;
onHold: () => void;
onResume: () => void;
isTogglingHold: boolean;
}
// ── Component ──────────────────────────────────────────────────────
@@ -83,6 +88,9 @@ export default function TestDetailHeader({
onPauseTimer,
onResumeTimer,
isTogglingTimer,
onHold,
onResume,
isTogglingHold,
}: TestDetailHeaderProps) {
const role = user?.role ?? "";
const currentIdx = STATE_INDEX[test.state];
@@ -116,9 +124,30 @@ export default function TestDetailHeader({
// ── Contextual action buttons ────────────────────────────────────
const HOLDABLE_STATES: TestState[] = ["draft", "red_executing", "blue_evaluating"];
const canHold =
HOLDABLE_STATES.includes(test.state) &&
(role === "red_tech" || role === "blue_tech" || role === "red_lead" || role === "blue_lead" || role === "admin");
const renderActions = () => {
const buttons: React.ReactNode[] = [];
// On Hold banner + Resume button (shown first when test is on hold)
if (test.is_on_hold && canHold) {
buttons.push(
<button
key="resume"
onClick={onResume}
disabled={isTogglingHold}
className="flex items-center gap-1.5 rounded-lg bg-green-700 px-4 py-2 text-sm font-medium text-white hover:bg-green-600 transition-colors disabled:opacity-50"
>
{isTogglingHold ? <Loader2 className="h-4 w-4 animate-spin" /> : <PlayCircle className="h-4 w-4" />}
Resume Test
</button>,
);
return <div className="flex flex-wrap items-center gap-2">{buttons}</div>;
}
// Red Team in draft -> Start Execution
if (
test.state === "draft" &&
@@ -327,6 +356,21 @@ export default function TestDetailHeader({
);
}
// On Hold button — appears alongside action buttons in pre-validation states
if (canHold && !test.is_on_hold) {
buttons.push(
<button
key="hold"
onClick={onHold}
disabled={isTogglingHold}
className="flex items-center gap-1.5 rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 py-2 text-sm font-medium text-amber-400 hover:bg-amber-500/20 transition-colors disabled:opacity-50"
>
{isTogglingHold ? <Loader2 className="h-4 w-4 animate-spin" /> : <PauseCircle className="h-4 w-4" />}
On Hold
</button>,
);
}
return buttons.length > 0 ? (
<div className="flex flex-wrap items-center gap-2">{buttons}</div>
) : null;
@@ -469,6 +513,24 @@ export default function TestDetailHeader({
</div>
)}
{/* On Hold banner */}
{test.is_on_hold && (
<div className="flex items-start gap-3 rounded-xl border border-amber-500/40 bg-amber-500/8 p-4">
<PauseCircle className="mt-0.5 h-5 w-5 shrink-0 text-amber-400" />
<div className="flex-1 min-w-0">
<p className="text-sm font-semibold text-amber-300">Test On Hold</p>
{test.hold_reason && (
<p className="mt-0.5 text-xs text-amber-400/80">
<span className="font-medium">Reason:</span> {test.hold_reason}
</p>
)}
<p className="mt-1 text-[10px] text-amber-400/60">
This test is paused. No action required until it is resumed.
</p>
</div>
</div>
)}
{/* Progress bar */}
{test.state !== "rejected" && (
<div className="pt-2">