import { useEffect, useState, type FormEvent } from 'react'; import { Link, useNavigate, useParams } from 'react-router-dom'; import { Save, RotateCcw } from 'lucide-react'; import { extractApiError } from '@/api/client'; import type { SimulationPatchInput } from '@/api/types'; import { useAuth } from '@/hooks/useAuth'; import { useToast } from '@/hooks/useToast'; import { useCreateSimulation, useDeleteSimulation, useSimulation, useTransitionSimulation, useUpdateSimulation, } from '@/hooks/useSimulations'; import { FormField, TextArea, TextInput } from '@/components/FormField'; import { LoadingState } from '@/components/LoadingState'; import { ErrorState } from '@/components/ErrorState'; import { SimulationStatusBadge } from '@/components/SimulationStatusBadge'; import { ConfirmDialog } from '@/components/ConfirmDialog'; import { MitreTechniquesField } from '@/components/MitreTechniquesField'; interface RedteamFormState { name: string; description: string; commands: string; prerequisites: string; executed_at: string; execution_result: string; } interface SocFormState { log_source: string; logs: string; soc_comment: string; incident_number: string; } const EMPTY_RT: RedteamFormState = { name: '', description: '', commands: '', prerequisites: '', executed_at: '', execution_result: '', }; const EMPTY_SOC: SocFormState = { log_source: '', logs: '', soc_comment: '', incident_number: '', }; export function SimulationFormPage(): JSX.Element { const { eid, sid } = useParams<{ eid: string; sid: string }>(); const engagementId = eid ? Number(eid) : undefined; const simulationId = sid ? Number(sid) : undefined; const isNew = !simulationId; const navigate = useNavigate(); const { push } = useToast(); const { isAdmin, isRedteam, isSoc, canEditEngagements } = useAuth(); const detail = useSimulation(isNew ? undefined : simulationId); const createMutation = useCreateSimulation(engagementId ?? 0); const updateMutation = useUpdateSimulation(simulationId ?? 0, engagementId ?? 0); const deleteMutation = useDeleteSimulation(engagementId ?? 0); const transitionMutation = useTransitionSimulation(simulationId ?? 0, engagementId ?? 0); const [rt, setRt] = useState(EMPTY_RT); const [soc, setSoc] = useState(EMPTY_SOC); const [nameError, setNameError] = useState(null); const [submitError, setSubmitError] = useState(null); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); useEffect(() => { if (!isNew && detail.data) { const s = detail.data; setRt({ name: s.name, description: s.description ?? '', commands: s.commands ?? '', prerequisites: s.prerequisites ?? '', executed_at: s.executed_at ? s.executed_at.slice(0, 16) : '', execution_result: s.execution_result ?? '', }); setSoc({ log_source: s.log_source ?? '', logs: s.logs ?? '', soc_comment: s.soc_comment ?? '', incident_number: s.incident_number ?? '', }); } }, [isNew, detail.data]); if (!isNew && detail.isLoading) return ; if (!isNew && detail.isError) { return ( detail.refetch()} /> ); } const simulation = detail.data; const status = simulation?.status; // US-18: Done = fully read-only, Reopen only const isDone = status === 'done'; const canEditRT = isAdmin || isRedteam; const socCanEdit = isSoc && (status === 'review_required' || status === 'done'); const socBlocked = isSoc && (status === 'pending' || status === 'in_progress'); const canSaveSoc = !isDone && (socCanEdit || canEditEngagements); const rtDisabled = !canEditRT || isDone; const socDisabled = isDone || (!canEditEngagements && !socCanEdit); const showMarkReview = !isDone && canEditEngagements && (status === 'pending' || status === 'in_progress'); const showClose = !isDone && (canEditEngagements || isSoc) && status === 'review_required'; const showReopen = isDone && (isAdmin || isRedteam || isSoc); const onSubmitNew = async (e: FormEvent) => { e.preventDefault(); setNameError(null); setSubmitError(null); if (!rt.name.trim()) { setNameError('Name is required'); return; } try { const created = await createMutation.mutateAsync({ name: rt.name.trim() }); push('Simulation created', 'success'); navigate(`/engagements/${engagementId}/simulations/${created.id}/edit`); } catch (err) { setSubmitError(extractApiError(err, 'Could not create simulation')); } }; const onSaveRT = async (e: FormEvent) => { e.preventDefault(); setNameError(null); setSubmitError(null); if (!rt.name.trim()) { setNameError('Name is required'); return; } const patch: SimulationPatchInput = { name: rt.name.trim(), description: rt.description.trim() || null, commands: rt.commands.trim() || null, prerequisites: rt.prerequisites.trim() || null, executed_at: rt.executed_at || null, execution_result: rt.execution_result.trim() || null, }; try { await updateMutation.mutateAsync(patch); push('Simulation updated', 'success'); } catch (err) { setSubmitError(extractApiError(err, 'Could not update simulation')); } }; const onSaveSOC = async (e: FormEvent) => { e.preventDefault(); setSubmitError(null); const patch: SimulationPatchInput = { log_source: soc.log_source.trim() || null, logs: soc.logs.trim() || null, soc_comment: soc.soc_comment.trim() || null, incident_number: soc.incident_number.trim() || null, }; try { await updateMutation.mutateAsync(patch); push('SOC report updated', 'success'); } catch (err) { setSubmitError(extractApiError(err, 'Could not update SOC fields')); } }; const onMarkReview = async () => { try { await transitionMutation.mutateAsync('review_required'); push('Simulation marked for review', 'success'); } catch (err) { push(extractApiError(err, 'Transition failed'), 'error'); } }; const onClose = async () => { try { await transitionMutation.mutateAsync('done'); push('Simulation closed', 'success'); } catch (err) { push(extractApiError(err, 'Transition failed'), 'error'); } }; const onReopen = async () => { try { await transitionMutation.mutateAsync('review_required'); push('Simulation reopened', 'success'); } catch (err) { push(extractApiError(err, 'Transition failed'), 'error'); } }; const onDelete = async () => { setShowDeleteConfirm(false); try { await deleteMutation.mutateAsync(simulationId as number); push('Simulation deleted', 'success'); navigate(`/engagements/${engagementId}`); } catch (err) { push(extractApiError(err, 'Could not delete simulation'), 'error'); } }; // New simulation form if (isNew) { const submitting = createMutation.isPending; return (
← Back to engagement

New simulation

setRt({ ...rt, name: e.target.value })} required /> {submitError ? (
{submitError}
) : null}
Cancel
); } const submitting = updateMutation.isPending || transitionMutation.isPending || deleteMutation.isPending; return (
← Back to engagement

{rt.name || simulation?.name}

{status ? (
{simulation?.created_by && ( Created by {simulation.created_by.username} )}
) : null}
{/* Done banner */} {isDone && (
This simulation is done and read-only. Use Reopen to make changes.
)} {/* SOC banner */} {socBlocked && (
Simulation not yet ready for review — the red team must mark it as "Review required" before you can fill in the SOC section.
)} {/* Red Team card */}
e.preventDefault()} noValidate className="card-product flex flex-col gap-md" >

Red Team

setRt({ ...rt, name: e.target.value })} disabled={rtDisabled} required />
MITRE Techniques & Tactics