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