import { useState, useMemo, useEffect } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { X, Search, Plus, Loader2, CheckCircle, FlaskConical, BookOpen, ArrowLeft, ChevronRight, Filter, } from "lucide-react"; import { getTests } from "../api/tests"; import { addTestToCampaign } from "../api/campaigns"; import { getTemplates, getTemplateById } from "../api/test-templates"; import { createTestFromTemplate } from "../api/tests"; import type { Test, TestState, TestTemplateSummary } from "../types/models"; /* ── helpers ─────────────────────────────────────────────────────── */ const stateBadge: Record = { 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", blue_evaluating: "bg-indigo-900/50 text-indigo-400 border-indigo-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", }; const SEVERITY_COLORS: Record = { critical: "text-red-400 border-red-500/30 bg-red-500/10", high: "text-orange-400 border-orange-500/30 bg-orange-500/10", medium: "text-yellow-400 border-yellow-500/30 bg-yellow-500/10", low: "text-blue-400 border-blue-500/30 bg-blue-500/10", }; const SOURCE_LABELS: Record = { atomic_red_team: "Atomic Red Team", mitre: "MITRE", custom: "Custom", }; type Tab = "existing" | "template"; type TemplateStep = "list" | "form"; /* ── props ───────────────────────────────────────────────────────── */ interface AddTestToCampaignModalProps { campaignId: string; existingTestIds: string[]; open: boolean; onClose: () => void; onSuccess: () => void; } /* ── component ───────────────────────────────────────────────────── */ export default function AddTestToCampaignModal({ campaignId, existingTestIds, open, onClose, onSuccess, }: AddTestToCampaignModalProps) { const queryClient = useQueryClient(); // ── shared state ──────────────────────────────────────────────── const [tab, setTab] = useState("template"); const [addedCount, setAddedCount] = useState(0); // ── existing-test tab ─────────────────────────────────────────── const [existingSearch, setExistingSearch] = useState(""); const [addedIds, setAddedIds] = useState>(new Set()); // ── template tab ──────────────────────────────────────────────── const [templateStep, setTemplateStep] = useState("list"); const [templateSearch, setTemplateSearch] = useState(""); const [filterPlatform, setFilterPlatform] = useState(""); const [filterSeverity, setFilterSeverity] = useState(""); const [selectedTemplateId, setSelectedTemplateId] = useState(null); // form fields (pre-filled from selected template) const [formName, setFormName] = useState(""); const [formDescription, setFormDescription] = useState(""); const [formPlatform, setFormPlatform] = useState(""); const [formProcedure, setFormProcedure] = useState(""); const [formTool, setFormTool] = useState(""); // ── reset when closed ──────────────────────────────────────────── useEffect(() => { if (!open) { setTab("template"); setExistingSearch(""); setAddedIds(new Set()); setAddedCount(0); setTemplateStep("list"); setTemplateSearch(""); setFilterPlatform(""); setFilterSeverity(""); setSelectedTemplateId(null); } }, [open]); // ── queries ────────────────────────────────────────────────────── const { data: allTests, isLoading: testsLoading } = useQuery({ queryKey: ["tests", "for-campaign-picker"], queryFn: () => getTests({ limit: 200 }), enabled: open && tab === "existing", }); const { data: templates, isLoading: templatesLoading } = useQuery({ queryKey: ["templates", "picker", filterPlatform, filterSeverity, templateSearch], queryFn: () => getTemplates({ search: templateSearch || undefined, platform: filterPlatform || undefined, severity: filterSeverity || undefined, limit: 100, }), enabled: open && tab === "template", staleTime: 60_000, }); const { data: fullTemplate, isLoading: fullTemplateLoading } = useQuery({ queryKey: ["template-detail", selectedTemplateId], queryFn: () => getTemplateById(selectedTemplateId!), enabled: !!selectedTemplateId, }); // Pre-fill form when full template loads useEffect(() => { if (fullTemplate) { setFormName(fullTemplate.name); setFormDescription(fullTemplate.description || ""); setFormPlatform(fullTemplate.platform || ""); setFormProcedure(fullTemplate.attack_procedure || ""); setFormTool(fullTemplate.tool_suggested || ""); } }, [fullTemplate]); // ── mutations ───────────────────────────────────────────────────── /** Add an existing test directly to the campaign */ const addExistingMutation = useMutation({ mutationFn: (testId: string) => addTestToCampaign(campaignId, { test_id: testId }), onSuccess: (_data, testId) => { setAddedIds((prev) => new Set(prev).add(testId)); setAddedCount((n) => n + 1); queryClient.invalidateQueries({ queryKey: ["campaign", campaignId] }); onSuccess(); }, }); /** Create test from template, then add to campaign */ const createAndAddMutation = useMutation({ mutationFn: async () => { if (!fullTemplate) throw new Error("No template loaded"); const test = await createTestFromTemplate( selectedTemplateId!, fullTemplate.mitre_technique_id, { name: formName.trim() || undefined, description: formDescription.trim() || undefined, platform: formPlatform.trim() || undefined, procedure_text: formProcedure.trim() || undefined, tool_used: formTool.trim() || undefined, }, ); await addTestToCampaign(campaignId, { test_id: test.id }); return test; }, onSuccess: () => { setAddedCount((n) => n + 1); queryClient.invalidateQueries({ queryKey: ["campaign", campaignId] }); onSuccess(); // Back to template list, ready to add another setTemplateStep("list"); setSelectedTemplateId(null); }, }); // ── derived data ────────────────────────────────────────────────── const filteredExisting = useMemo(() => { if (!allTests) return []; const alreadyIn = new Set([...existingTestIds, ...addedIds]); let list = allTests.filter((t) => !alreadyIn.has(t.id)); if (existingSearch.trim()) { const q = existingSearch.toLowerCase(); list = list.filter( (t) => t.name.toLowerCase().includes(q) || (t.technique_mitre_id && t.technique_mitre_id.toLowerCase().includes(q)) || (t.technique_name && t.technique_name.toLowerCase().includes(q)), ); } return list; }, [allTests, existingSearch, existingTestIds, addedIds]); const canSubmitForm = formName.trim().length > 0 && !!fullTemplate && !createAndAddMutation.isPending; if (!open) return null; /* ── render ──────────────────────────────────────────────────── */ return (
{/* ── Header ── */}
{templateStep === "form" ? ( ) : (

Add Test to Campaign

)}
{/* ── Tab bar (only on list step) ── */} {templateStep === "list" && (
{(["template", "existing"] as Tab[]).map((t) => ( ))}
)} {/* ── Body ── */}
{/* ═══ TEMPLATE TAB — LIST ══════════════════════════════ */} {tab === "template" && templateStep === "list" && ( <> {/* Filters */}
setTemplateSearch(e.target.value)} placeholder="Search templates by name or technique…" className="w-full rounded-lg border border-gray-700 bg-gray-800 pl-9 pr-3 py-2 text-sm text-gray-300 placeholder-gray-500 focus:border-cyan-500 focus:outline-none" />
{/* Template list */}
{templatesLoading ? (
) : !templates?.length ? (

No templates match your filters.

) : (
{templates.map((tmpl: TestTemplateSummary) => ( ))}
)}
)} {/* ═══ TEMPLATE TAB — FORM ══════════════════════════════ */} {tab === "template" && templateStep === "form" && (
{fullTemplateLoading ? (
) : ( <> {/* Template info banner */} {fullTemplate && (
Template: {fullTemplate.name} {fullTemplate.mitre_technique_id} {fullTemplate.source && ( {SOURCE_LABELS[fullTemplate.source] ?? fullTemplate.source} )}
)} {/* Name */}
setFormName(e.target.value)} className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 placeholder-gray-500 focus:border-cyan-500 focus:outline-none" placeholder="Test name" />
{/* Platform */}
setFormPlatform(e.target.value)} className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 placeholder-gray-500 focus:border-cyan-500 focus:outline-none" placeholder="e.g. windows, linux, macos" />
{/* Description */}