feat(tests): review panels, blind visibility UX, red_review/blue_review badges
This commit is contained in:
@@ -59,7 +59,9 @@ const typeLabels: Record<string, string> = {
|
||||
const testStateColors: Record<string, string> = {
|
||||
draft: "bg-gray-800/50 text-gray-400 border-gray-600/30",
|
||||
red_executing: "bg-orange-900/50 text-orange-400 border-orange-500/30",
|
||||
red_review: "bg-amber-900/50 text-amber-400 border-amber-500/30",
|
||||
blue_evaluating: "bg-indigo-900/50 text-indigo-400 border-indigo-500/30",
|
||||
blue_review: "bg-purple-900/50 text-purple-400 border-purple-500/30",
|
||||
in_review: "bg-blue-900/50 text-blue-400 border-blue-500/30",
|
||||
validated: "bg-green-900/50 text-green-400 border-green-500/30",
|
||||
rejected: "bg-red-900/50 text-red-400 border-red-500/30",
|
||||
|
||||
@@ -50,7 +50,9 @@ import type { TestState } from "../types/models";
|
||||
const testStateBadgeColors: Record<string, string> = {
|
||||
draft: "bg-gray-800/50 text-gray-400 border-gray-600/30",
|
||||
red_executing: "bg-orange-900/50 text-orange-400 border-orange-500/30",
|
||||
red_review: "bg-amber-900/50 text-amber-400 border-amber-500/30",
|
||||
blue_evaluating: "bg-indigo-900/50 text-indigo-400 border-indigo-500/30",
|
||||
blue_review: "bg-purple-900/50 text-purple-400 border-purple-500/30",
|
||||
in_review: "bg-blue-900/50 text-blue-400 border-blue-500/30",
|
||||
validated: "bg-green-900/50 text-green-400 border-green-500/30",
|
||||
rejected: "bg-red-900/50 text-red-400 border-red-500/30",
|
||||
@@ -60,7 +62,9 @@ const testStateBadgeColors: Record<string, string> = {
|
||||
const testStateLabels: Record<string, string> = {
|
||||
draft: "Draft",
|
||||
red_executing: "Red Executing",
|
||||
red_review: "Red Review",
|
||||
blue_evaluating: "Blue Evaluating",
|
||||
blue_review: "Blue Review",
|
||||
in_review: "In Review",
|
||||
validated: "Validated",
|
||||
rejected: "Rejected",
|
||||
|
||||
@@ -213,7 +213,9 @@ export default function ReportsPage() {
|
||||
<option value="">All states</option>
|
||||
<option value="draft">Draft</option>
|
||||
<option value="red_executing">Red Executing</option>
|
||||
<option value="red_review">Red Review</option>
|
||||
<option value="blue_evaluating">Blue Evaluating</option>
|
||||
<option value="blue_review">Blue Review</option>
|
||||
<option value="in_review">In Review</option>
|
||||
<option value="validated">Validated</option>
|
||||
<option value="rejected">Rejected</option>
|
||||
@@ -452,7 +454,9 @@ const statusColors: Record<string, string> = {
|
||||
not_evaluated: "bg-gray-500/10 text-gray-400 border-gray-500/30",
|
||||
draft: "bg-gray-500/10 text-gray-400 border-gray-500/30",
|
||||
red_executing: "bg-orange-500/10 text-orange-400 border-orange-500/30",
|
||||
red_review: "bg-amber-500/10 text-amber-400 border-amber-500/30",
|
||||
blue_evaluating: "bg-indigo-500/10 text-indigo-400 border-indigo-500/30",
|
||||
blue_review: "bg-purple-500/10 text-purple-400 border-purple-500/30",
|
||||
in_review: "bg-yellow-500/10 text-yellow-400 border-yellow-500/30",
|
||||
rejected: "bg-red-500/10 text-red-400 border-red-500/30",
|
||||
};
|
||||
|
||||
@@ -42,7 +42,9 @@ const statusBadgeColors: Record<TechniqueStatus, string> = {
|
||||
const testStateBadgeColors: Record<TestState, string> = {
|
||||
draft: "bg-gray-800/50 text-gray-400 border-gray-600/30",
|
||||
red_executing: "bg-orange-900/50 text-orange-400 border-orange-500/30",
|
||||
red_review: "bg-amber-900/50 text-amber-400 border-amber-500/30",
|
||||
blue_evaluating: "bg-indigo-900/50 text-indigo-400 border-indigo-500/30",
|
||||
blue_review: "bg-purple-900/50 text-purple-400 border-purple-500/30",
|
||||
in_review: "bg-blue-900/50 text-blue-400 border-blue-500/30",
|
||||
validated: "bg-green-900/50 text-green-400 border-green-500/30",
|
||||
rejected: "bg-red-900/50 text-red-400 border-red-500/30",
|
||||
@@ -493,7 +495,7 @@ export default function TechniqueDetailPage() {
|
||||
|
||||
// Any test currently in a non-terminal state
|
||||
const ACTIVE_STATES: TestState[] = [
|
||||
"draft", "red_executing", "blue_evaluating", "in_review",
|
||||
"draft", "red_executing", "red_review", "blue_evaluating", "blue_review", "in_review",
|
||||
];
|
||||
const activeTest = allTests.find(
|
||||
(t: { state: TestState }) => ACTIVE_STATES.includes(t.state)
|
||||
@@ -520,7 +522,9 @@ export default function TechniqueDetailPage() {
|
||||
const ACTIVE_LABEL: Partial<Record<TestState, string>> = {
|
||||
draft: "Draft",
|
||||
red_executing: "Executing",
|
||||
red_review: "Red Review",
|
||||
blue_evaluating: "Evaluating",
|
||||
blue_review: "Blue Review",
|
||||
in_review: "In Review",
|
||||
};
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
startBlueWork,
|
||||
validateAsRedLead,
|
||||
validateAsBlueLead,
|
||||
reviewAsRedLead,
|
||||
reviewAsBlueLead,
|
||||
reopenTest,
|
||||
pauseTimer,
|
||||
resumeTimer,
|
||||
@@ -29,6 +31,7 @@ import type { TestResult, ContainmentResult, TeamSide, TestTimelineEntry } from
|
||||
import TestDetailHeader from "../components/test-detail/TestDetailHeader";
|
||||
import TeamTabs from "../components/test-detail/TeamTabs";
|
||||
import ValidationModal from "../components/test-detail/ValidationModal";
|
||||
import ReviewModal from "../components/test-detail/ReviewModal";
|
||||
import ConfirmDialog from "../components/ConfirmDialog";
|
||||
import JiraLinkPanel from "../components/JiraLinkPanel";
|
||||
import TestPhaseTimeline from "../components/TestPhaseTimeline";
|
||||
@@ -56,6 +59,11 @@ export default function TestDetailPage() {
|
||||
side: "red" | "blue";
|
||||
}>({ open: false, side: "red" });
|
||||
|
||||
const [reviewModal, setReviewModal] = useState<{
|
||||
open: boolean;
|
||||
side: "red" | "blue";
|
||||
}>({ open: false, side: "red" });
|
||||
|
||||
const [confirmReopen, setConfirmReopen] = useState(false);
|
||||
const [holdModal, setHoldModal] = useState(false);
|
||||
const [holdReason, setHoldReason] = useState("");
|
||||
@@ -254,6 +262,28 @@ export default function TestDetailPage() {
|
||||
onError: (err: unknown) => showToast(extractError(err), "error"),
|
||||
});
|
||||
|
||||
const reviewRedMutation = useMutation({
|
||||
mutationFn: (payload: { decision: "approve" | "reopen"; notes?: string }) =>
|
||||
reviewAsRedLead(testId!, payload),
|
||||
onSuccess: () => {
|
||||
invalidateAll();
|
||||
setReviewModal({ open: false, side: "red" });
|
||||
showToast("Red review submitted", "success");
|
||||
},
|
||||
onError: (err: unknown) => showToast(extractError(err), "error"),
|
||||
});
|
||||
|
||||
const reviewBlueMutation = useMutation({
|
||||
mutationFn: (payload: { decision: "approve" | "reopen" | "gap"; notes?: string; system_gaps?: string }) =>
|
||||
reviewAsBlueLead(testId!, payload),
|
||||
onSuccess: () => {
|
||||
invalidateAll();
|
||||
setReviewModal({ open: false, side: "blue" });
|
||||
showToast("Blue review submitted", "success");
|
||||
},
|
||||
onError: (err: unknown) => showToast(extractError(err), "error"),
|
||||
});
|
||||
|
||||
const reopenMutation = useMutation({
|
||||
mutationFn: () => reopenTest(testId!),
|
||||
onSuccess: () => {
|
||||
@@ -360,6 +390,22 @@ export default function TestDetailPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleReviewRedSubmit = (decision: "approve" | "reopen", notes: string) => {
|
||||
reviewRedMutation.mutate({ decision, notes: notes || undefined });
|
||||
};
|
||||
|
||||
const handleReviewBlueSubmit = (
|
||||
decision: "approve" | "reopen" | "gap",
|
||||
notes: string,
|
||||
systemGaps: string,
|
||||
) => {
|
||||
reviewBlueMutation.mutate({
|
||||
decision,
|
||||
notes: notes || undefined,
|
||||
system_gaps: decision === "gap" ? systemGaps : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const isTransitioning =
|
||||
startExecMutation.isPending ||
|
||||
submitRedMutation.isPending ||
|
||||
@@ -439,6 +485,7 @@ export default function TestDetailPage() {
|
||||
onSubmitBlue={() => submitBlueMutation.mutate()}
|
||||
onStartBlueWork={() => startBlueWorkMutation.mutate()}
|
||||
onOpenValidateModal={(side) => setValidationModal({ open: true, side })}
|
||||
onOpenReviewModal={(side) => setReviewModal({ open: true, side })}
|
||||
onReopen={() => setConfirmReopen(true)}
|
||||
onPauseTimer={() => pauseTimerMutation.mutate()}
|
||||
onResumeTimer={() => resumeTimerMutation.mutate()}
|
||||
@@ -717,6 +764,22 @@ export default function TestDetailPage() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Review Modal (red_review / blue_review lead gate) */}
|
||||
{reviewModal.open && (
|
||||
<ReviewModal
|
||||
side={reviewModal.side}
|
||||
test={test}
|
||||
isSubmitting={
|
||||
reviewModal.side === "red"
|
||||
? reviewRedMutation.isPending
|
||||
: reviewBlueMutation.isPending
|
||||
}
|
||||
onSubmitRed={reviewModal.side === "red" ? handleReviewRedSubmit : undefined}
|
||||
onSubmitBlue={reviewModal.side === "blue" ? handleReviewBlueSubmit : undefined}
|
||||
onClose={() => setReviewModal({ open: false, side: "red" })}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Save as Template Modal */}
|
||||
{showTemplateModal && (
|
||||
<SaveAsTemplateModal
|
||||
|
||||
@@ -29,7 +29,9 @@ import { useAuth } from "../context/AuthContext";
|
||||
const testStateBadgeColors: Record<TestState, string> = {
|
||||
draft: "bg-gray-800/50 text-gray-400 border-gray-600/30",
|
||||
red_executing: "bg-orange-900/50 text-orange-400 border-orange-500/30",
|
||||
red_review: "bg-amber-900/50 text-amber-400 border-amber-500/30",
|
||||
blue_evaluating: "bg-indigo-900/50 text-indigo-400 border-indigo-500/30",
|
||||
blue_review: "bg-purple-900/50 text-purple-400 border-purple-500/30",
|
||||
in_review: "bg-blue-900/50 text-blue-400 border-blue-500/30",
|
||||
validated: "bg-green-900/50 text-green-400 border-green-500/30",
|
||||
rejected: "bg-red-900/50 text-red-400 border-red-500/30",
|
||||
@@ -39,7 +41,9 @@ const testStateBadgeColors: Record<TestState, string> = {
|
||||
const testStateLabels: Record<TestState, string> = {
|
||||
draft: "Draft",
|
||||
red_executing: "Red Executing",
|
||||
red_review: "Red Review",
|
||||
blue_evaluating: "Blue Evaluating",
|
||||
blue_review: "Blue Review",
|
||||
in_review: "In Review",
|
||||
validated: "Validated",
|
||||
rejected: "Rejected",
|
||||
@@ -49,7 +53,9 @@ const testStateLabels: Record<TestState, string> = {
|
||||
const ALL_STATES: TestState[] = [
|
||||
"draft",
|
||||
"red_executing",
|
||||
"red_review",
|
||||
"blue_evaluating",
|
||||
"blue_review",
|
||||
"in_review",
|
||||
"validated",
|
||||
"rejected",
|
||||
@@ -63,8 +69,12 @@ function currentTeamForState(state: TestState): string {
|
||||
case "draft":
|
||||
case "red_executing":
|
||||
return "Red Team";
|
||||
case "red_review":
|
||||
return "Red Lead";
|
||||
case "blue_evaluating":
|
||||
return "Blue Team";
|
||||
case "blue_review":
|
||||
return "Blue Lead";
|
||||
case "in_review":
|
||||
return "Managers";
|
||||
case "validated":
|
||||
@@ -429,7 +439,9 @@ export default function TestsPage() {
|
||||
const icons: Record<TestState, React.ReactNode> = {
|
||||
draft: <Clock className="h-5 w-5 text-gray-400" />,
|
||||
red_executing: <Play className="h-5 w-5 text-orange-400" />,
|
||||
red_review: <Shield className="h-5 w-5 text-amber-400" />,
|
||||
blue_evaluating: <Shield className="h-5 w-5 text-indigo-400" />,
|
||||
blue_review: <Shield className="h-5 w-5 text-purple-400" />,
|
||||
in_review: <Eye className="h-5 w-5 text-blue-400" />,
|
||||
validated: <CheckCircle className="h-5 w-5 text-green-400" />,
|
||||
rejected: <XCircle className="h-5 w-5 text-red-400" />,
|
||||
@@ -438,7 +450,9 @@ export default function TestsPage() {
|
||||
const colorMap: Record<TestState, string> = {
|
||||
draft: "text-gray-400",
|
||||
red_executing: "text-orange-400",
|
||||
red_review: "text-amber-400",
|
||||
blue_evaluating: "text-indigo-400",
|
||||
blue_review: "text-purple-400",
|
||||
in_review: "text-blue-400",
|
||||
validated: "text-green-400",
|
||||
rejected: "text-red-400",
|
||||
@@ -708,7 +722,9 @@ function TestTable({
|
||||
testStateBadgeColors[test.state]
|
||||
}`}
|
||||
>
|
||||
{testStateLabels[test.state]}
|
||||
{test.state === "blue_evaluating" && !test.blue_work_started_at
|
||||
? "Queued Blue Team"
|
||||
: testStateLabels[test.state]}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4 text-gray-400 text-xs">
|
||||
|
||||
Reference in New Issue
Block a user