2026-05-26 11:13:14 +02:00
|
|
|
import { useEffect, useState, type FormEvent } from 'react';
|
|
|
|
|
import { Link, useNavigate, useParams } from 'react-router-dom';
|
feat(frontend): sprint 4 — dark mode + matrix overhaul + tactic selection + done read-only + UI polish
US-17: fix duplicate "Create engagement" button, icon conventions (Save/RotateCcw/Grid2x2), UsersAdminPage form baseline alignment
US-18: done status fully read-only + Reopen button (done → review_required) for all roles
US-19: invalidate engagement queries on simulation PATCH/transition for auto-status propagation
US-20: MitreMatrixModal rewritten — CSS grid 12-column layout, no horizontal scroll, attack.mitre.org compact look
US-21: tactic header clickable in matrix, tactic chips (MitreTacticTag) in field, single atomic PATCH with technique_ids + tactic_ids
US-22: MitreTechniquesField chips-only area + inline search input + matrix icon button; chips show ID-only (name in title=)
US-23: useTheme hook — 3-state light/dark/system, CSS variables, Tailwind darkMode class, localStorage persistence
92/92 tests passing, typecheck and lint clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:06:01 +02:00
|
|
|
import { Save, RotateCcw } from 'lucide-react';
|
2026-05-26 11:13:14 +02:00
|
|
|
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';
|
feat(frontend): sprint 3 — multi-technique MITRE selection + matrix modal
- types: replace mitre_technique_id/name scalars with techniques:MitreTechnique[]
on Simulation; add MitreTactic/MitreMatrixTechnique/MitreMatrixSubtechnique;
SimulationPatchInput now uses technique_ids:string[]
- api/mitre.ts: add getMitreMatrix() → GET /api/mitre/matrix
- hooks/useMitre: add useMitreMatrix(enabled) with staleTime:Infinity
- MitreTechniquePicker: clean rewrite — onSelect(technique) one-shot, resets
input after selection, no incoming value props
- MitreTechniqueTag: chip component with id+name and × remove button
- MitreMatrixModal: tactic columns (220px fixed), expand/collapse subtechniques,
search filter (auto-expands parent on sub match), selection state, focus trap
(Tab wrap, Escape, search autofocus), backdrop click cancel, Apply N techniques
- MitreTechniquesField: orchestrates tags+picker+matrix with auto-save PATCH on
every add/remove/Apply, dedup guard, disabled read-only mode for SOC
- SimulationFormPage: swap MitreTechniquePicker for MitreTechniquesField; remove
technique state from RT form (techniques have independent auto-save cycle)
- SimulationList: MITRE column → T1059 +2 counter format, — when empty
- Tests: 84 passing (13 test files); new suites for Tag, Field, Modal;
MitreTechniquePicker + SimulationFormPage + SimulationList adapted to new API
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 04:04:23 +02:00
|
|
|
import { MitreTechniquesField } from '@/components/MitreTechniquesField';
|
2026-05-26 11:13:14 +02:00
|
|
|
|
|
|
|
|
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<RedteamFormState>(EMPTY_RT);
|
|
|
|
|
const [soc, setSoc] = useState<SocFormState>(EMPTY_SOC);
|
|
|
|
|
const [nameError, setNameError] = useState<string | null>(null);
|
|
|
|
|
const [submitError, setSubmitError] = useState<string | null>(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 ?? '',
|
2026-05-26 11:22:05 +02:00
|
|
|
executed_at: s.executed_at ? s.executed_at.slice(0, 16) : '',
|
2026-05-26 11:13:14 +02:00
|
|
|
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 <LoadingState label="Loading simulation…" />;
|
|
|
|
|
if (!isNew && detail.isError) {
|
|
|
|
|
return (
|
|
|
|
|
<ErrorState
|
|
|
|
|
message={extractApiError(detail.error, 'Could not load simulation')}
|
|
|
|
|
onRetry={() => detail.refetch()}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const simulation = detail.data;
|
|
|
|
|
const status = simulation?.status;
|
|
|
|
|
|
feat(frontend): sprint 4 — dark mode + matrix overhaul + tactic selection + done read-only + UI polish
US-17: fix duplicate "Create engagement" button, icon conventions (Save/RotateCcw/Grid2x2), UsersAdminPage form baseline alignment
US-18: done status fully read-only + Reopen button (done → review_required) for all roles
US-19: invalidate engagement queries on simulation PATCH/transition for auto-status propagation
US-20: MitreMatrixModal rewritten — CSS grid 12-column layout, no horizontal scroll, attack.mitre.org compact look
US-21: tactic header clickable in matrix, tactic chips (MitreTacticTag) in field, single atomic PATCH with technique_ids + tactic_ids
US-22: MitreTechniquesField chips-only area + inline search input + matrix icon button; chips show ID-only (name in title=)
US-23: useTheme hook — 3-state light/dark/system, CSS variables, Tailwind darkMode class, localStorage persistence
92/92 tests passing, typecheck and lint clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:06:01 +02:00
|
|
|
// US-18: Done = fully read-only, Reopen only
|
|
|
|
|
const isDone = status === 'done';
|
|
|
|
|
|
2026-05-26 11:13:14 +02:00
|
|
|
const canEditRT = isAdmin || isRedteam;
|
|
|
|
|
const socCanEdit = isSoc && (status === 'review_required' || status === 'done');
|
|
|
|
|
const socBlocked = isSoc && (status === 'pending' || status === 'in_progress');
|
|
|
|
|
|
feat(frontend): sprint 4 — dark mode + matrix overhaul + tactic selection + done read-only + UI polish
US-17: fix duplicate "Create engagement" button, icon conventions (Save/RotateCcw/Grid2x2), UsersAdminPage form baseline alignment
US-18: done status fully read-only + Reopen button (done → review_required) for all roles
US-19: invalidate engagement queries on simulation PATCH/transition for auto-status propagation
US-20: MitreMatrixModal rewritten — CSS grid 12-column layout, no horizontal scroll, attack.mitre.org compact look
US-21: tactic header clickable in matrix, tactic chips (MitreTacticTag) in field, single atomic PATCH with technique_ids + tactic_ids
US-22: MitreTechniquesField chips-only area + inline search input + matrix icon button; chips show ID-only (name in title=)
US-23: useTheme hook — 3-state light/dark/system, CSS variables, Tailwind darkMode class, localStorage persistence
92/92 tests passing, typecheck and lint clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:06:01 +02:00
|
|
|
const canSaveSoc = !isDone && (socCanEdit || canEditEngagements);
|
|
|
|
|
const rtDisabled = !canEditRT || isDone;
|
|
|
|
|
const socDisabled = isDone || (!canEditEngagements && !socCanEdit);
|
2026-05-26 11:13:14 +02:00
|
|
|
|
|
|
|
|
const showMarkReview =
|
feat(frontend): sprint 4 — dark mode + matrix overhaul + tactic selection + done read-only + UI polish
US-17: fix duplicate "Create engagement" button, icon conventions (Save/RotateCcw/Grid2x2), UsersAdminPage form baseline alignment
US-18: done status fully read-only + Reopen button (done → review_required) for all roles
US-19: invalidate engagement queries on simulation PATCH/transition for auto-status propagation
US-20: MitreMatrixModal rewritten — CSS grid 12-column layout, no horizontal scroll, attack.mitre.org compact look
US-21: tactic header clickable in matrix, tactic chips (MitreTacticTag) in field, single atomic PATCH with technique_ids + tactic_ids
US-22: MitreTechniquesField chips-only area + inline search input + matrix icon button; chips show ID-only (name in title=)
US-23: useTheme hook — 3-state light/dark/system, CSS variables, Tailwind darkMode class, localStorage persistence
92/92 tests passing, typecheck and lint clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:06:01 +02:00
|
|
|
!isDone && canEditEngagements && (status === 'pending' || status === 'in_progress');
|
2026-05-26 11:13:14 +02:00
|
|
|
const showClose =
|
feat(frontend): sprint 4 — dark mode + matrix overhaul + tactic selection + done read-only + UI polish
US-17: fix duplicate "Create engagement" button, icon conventions (Save/RotateCcw/Grid2x2), UsersAdminPage form baseline alignment
US-18: done status fully read-only + Reopen button (done → review_required) for all roles
US-19: invalidate engagement queries on simulation PATCH/transition for auto-status propagation
US-20: MitreMatrixModal rewritten — CSS grid 12-column layout, no horizontal scroll, attack.mitre.org compact look
US-21: tactic header clickable in matrix, tactic chips (MitreTacticTag) in field, single atomic PATCH with technique_ids + tactic_ids
US-22: MitreTechniquesField chips-only area + inline search input + matrix icon button; chips show ID-only (name in title=)
US-23: useTheme hook — 3-state light/dark/system, CSS variables, Tailwind darkMode class, localStorage persistence
92/92 tests passing, typecheck and lint clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:06:01 +02:00
|
|
|
!isDone && (canEditEngagements || isSoc) && status === 'review_required';
|
|
|
|
|
const showReopen = isDone && (isAdmin || isRedteam || isSoc);
|
2026-05-26 11:13:14 +02:00
|
|
|
|
|
|
|
|
const onSubmitNew = async (e: FormEvent) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
setNameError(null);
|
|
|
|
|
setSubmitError(null);
|
feat(frontend): sprint 4 — dark mode + matrix overhaul + tactic selection + done read-only + UI polish
US-17: fix duplicate "Create engagement" button, icon conventions (Save/RotateCcw/Grid2x2), UsersAdminPage form baseline alignment
US-18: done status fully read-only + Reopen button (done → review_required) for all roles
US-19: invalidate engagement queries on simulation PATCH/transition for auto-status propagation
US-20: MitreMatrixModal rewritten — CSS grid 12-column layout, no horizontal scroll, attack.mitre.org compact look
US-21: tactic header clickable in matrix, tactic chips (MitreTacticTag) in field, single atomic PATCH with technique_ids + tactic_ids
US-22: MitreTechniquesField chips-only area + inline search input + matrix icon button; chips show ID-only (name in title=)
US-23: useTheme hook — 3-state light/dark/system, CSS variables, Tailwind darkMode class, localStorage persistence
92/92 tests passing, typecheck and lint clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:06:01 +02:00
|
|
|
if (!rt.name.trim()) { setNameError('Name is required'); return; }
|
2026-05-26 11:13:14 +02:00
|
|
|
try {
|
|
|
|
|
const created = await createMutation.mutateAsync({ name: rt.name.trim() });
|
2026-05-26 16:08:46 +02:00
|
|
|
push('Simulation created', 'success');
|
2026-05-26 11:13:14 +02:00
|
|
|
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);
|
feat(frontend): sprint 4 — dark mode + matrix overhaul + tactic selection + done read-only + UI polish
US-17: fix duplicate "Create engagement" button, icon conventions (Save/RotateCcw/Grid2x2), UsersAdminPage form baseline alignment
US-18: done status fully read-only + Reopen button (done → review_required) for all roles
US-19: invalidate engagement queries on simulation PATCH/transition for auto-status propagation
US-20: MitreMatrixModal rewritten — CSS grid 12-column layout, no horizontal scroll, attack.mitre.org compact look
US-21: tactic header clickable in matrix, tactic chips (MitreTacticTag) in field, single atomic PATCH with technique_ids + tactic_ids
US-22: MitreTechniquesField chips-only area + inline search input + matrix icon button; chips show ID-only (name in title=)
US-23: useTheme hook — 3-state light/dark/system, CSS variables, Tailwind darkMode class, localStorage persistence
92/92 tests passing, typecheck and lint clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:06:01 +02:00
|
|
|
if (!rt.name.trim()) { setNameError('Name is required'); return; }
|
2026-05-26 11:13:14 +02:00
|
|
|
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);
|
2026-05-26 16:08:46 +02:00
|
|
|
push('Simulation updated', 'success');
|
2026-05-26 11:13:14 +02:00
|
|
|
} 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);
|
2026-05-26 16:08:46 +02:00
|
|
|
push('SOC report updated', 'success');
|
2026-05-26 11:13:14 +02:00
|
|
|
} catch (err) {
|
|
|
|
|
setSubmitError(extractApiError(err, 'Could not update SOC fields'));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const onMarkReview = async () => {
|
|
|
|
|
try {
|
|
|
|
|
await transitionMutation.mutateAsync('review_required');
|
2026-05-26 16:08:46 +02:00
|
|
|
push('Simulation marked for review', 'success');
|
2026-05-26 11:13:14 +02:00
|
|
|
} catch (err) {
|
2026-05-26 16:08:46 +02:00
|
|
|
push(extractApiError(err, 'Transition failed'), 'error');
|
2026-05-26 11:13:14 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const onClose = async () => {
|
|
|
|
|
try {
|
|
|
|
|
await transitionMutation.mutateAsync('done');
|
2026-05-26 16:08:46 +02:00
|
|
|
push('Simulation closed', 'success');
|
2026-05-26 11:13:14 +02:00
|
|
|
} catch (err) {
|
2026-05-26 16:08:46 +02:00
|
|
|
push(extractApiError(err, 'Transition failed'), 'error');
|
2026-05-26 11:13:14 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
feat(frontend): sprint 4 — dark mode + matrix overhaul + tactic selection + done read-only + UI polish
US-17: fix duplicate "Create engagement" button, icon conventions (Save/RotateCcw/Grid2x2), UsersAdminPage form baseline alignment
US-18: done status fully read-only + Reopen button (done → review_required) for all roles
US-19: invalidate engagement queries on simulation PATCH/transition for auto-status propagation
US-20: MitreMatrixModal rewritten — CSS grid 12-column layout, no horizontal scroll, attack.mitre.org compact look
US-21: tactic header clickable in matrix, tactic chips (MitreTacticTag) in field, single atomic PATCH with technique_ids + tactic_ids
US-22: MitreTechniquesField chips-only area + inline search input + matrix icon button; chips show ID-only (name in title=)
US-23: useTheme hook — 3-state light/dark/system, CSS variables, Tailwind darkMode class, localStorage persistence
92/92 tests passing, typecheck and lint clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:06:01 +02:00
|
|
|
const onReopen = async () => {
|
|
|
|
|
try {
|
|
|
|
|
await transitionMutation.mutateAsync('review_required');
|
|
|
|
|
push('Simulation reopened', 'success');
|
|
|
|
|
} catch (err) {
|
|
|
|
|
push(extractApiError(err, 'Transition failed'), 'error');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-26 11:13:14 +02:00
|
|
|
const onDelete = async () => {
|
|
|
|
|
setShowDeleteConfirm(false);
|
|
|
|
|
try {
|
|
|
|
|
await deleteMutation.mutateAsync(simulationId as number);
|
2026-05-26 16:08:46 +02:00
|
|
|
push('Simulation deleted', 'success');
|
2026-05-26 11:13:14 +02:00
|
|
|
navigate(`/engagements/${engagementId}`);
|
|
|
|
|
} catch (err) {
|
2026-05-26 16:08:46 +02:00
|
|
|
push(extractApiError(err, 'Could not delete simulation'), 'error');
|
2026-05-26 11:13:14 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
feat(frontend): sprint 4 — dark mode + matrix overhaul + tactic selection + done read-only + UI polish
US-17: fix duplicate "Create engagement" button, icon conventions (Save/RotateCcw/Grid2x2), UsersAdminPage form baseline alignment
US-18: done status fully read-only + Reopen button (done → review_required) for all roles
US-19: invalidate engagement queries on simulation PATCH/transition for auto-status propagation
US-20: MitreMatrixModal rewritten — CSS grid 12-column layout, no horizontal scroll, attack.mitre.org compact look
US-21: tactic header clickable in matrix, tactic chips (MitreTacticTag) in field, single atomic PATCH with technique_ids + tactic_ids
US-22: MitreTechniquesField chips-only area + inline search input + matrix icon button; chips show ID-only (name in title=)
US-23: useTheme hook — 3-state light/dark/system, CSS variables, Tailwind darkMode class, localStorage persistence
92/92 tests passing, typecheck and lint clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:06:01 +02:00
|
|
|
// New simulation form
|
2026-05-26 11:13:14 +02:00
|
|
|
if (isNew) {
|
|
|
|
|
const submitting = createMutation.isPending;
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col gap-xl max-w-2xl">
|
|
|
|
|
<header>
|
|
|
|
|
<Link to={`/engagements/${engagementId}`} className="btn-text-link text-[14px]">
|
|
|
|
|
← Back to engagement
|
|
|
|
|
</Link>
|
2026-05-26 16:08:46 +02:00
|
|
|
<h1 className="text-[44px] font-medium leading-none mt-sm">New simulation</h1>
|
2026-05-26 11:13:14 +02:00
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<form onSubmit={onSubmitNew} noValidate className="card-product flex flex-col gap-md">
|
|
|
|
|
<FormField label="Name" htmlFor="sim-name" required error={nameError}>
|
|
|
|
|
<TextInput
|
|
|
|
|
id="sim-name"
|
|
|
|
|
name="name"
|
|
|
|
|
value={rt.name}
|
|
|
|
|
onChange={(e) => setRt({ ...rt, name: e.target.value })}
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
</FormField>
|
|
|
|
|
|
|
|
|
|
{submitError ? (
|
feat(frontend): sprint 4 — dark mode + matrix overhaul + tactic selection + done read-only + UI polish
US-17: fix duplicate "Create engagement" button, icon conventions (Save/RotateCcw/Grid2x2), UsersAdminPage form baseline alignment
US-18: done status fully read-only + Reopen button (done → review_required) for all roles
US-19: invalidate engagement queries on simulation PATCH/transition for auto-status propagation
US-20: MitreMatrixModal rewritten — CSS grid 12-column layout, no horizontal scroll, attack.mitre.org compact look
US-21: tactic header clickable in matrix, tactic chips (MitreTacticTag) in field, single atomic PATCH with technique_ids + tactic_ids
US-22: MitreTechniquesField chips-only area + inline search input + matrix icon button; chips show ID-only (name in title=)
US-23: useTheme hook — 3-state light/dark/system, CSS variables, Tailwind darkMode class, localStorage persistence
92/92 tests passing, typecheck and lint clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:06:01 +02:00
|
|
|
<div role="alert" className="text-[14px] text-bloom-deep">{submitError}</div>
|
2026-05-26 11:13:14 +02:00
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center gap-md pt-sm">
|
|
|
|
|
<button type="submit" className="btn-primary" disabled={submitting}>
|
|
|
|
|
{submitting ? 'Creating…' : 'Create simulation'}
|
|
|
|
|
</button>
|
|
|
|
|
<Link to={`/engagements/${engagementId}`} className="btn-outline-ink">
|
|
|
|
|
Cancel
|
|
|
|
|
</Link>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const submitting =
|
|
|
|
|
updateMutation.isPending || transitionMutation.isPending || deleteMutation.isPending;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col gap-xl max-w-3xl">
|
|
|
|
|
<header className="flex items-start justify-between gap-md">
|
|
|
|
|
<div className="flex flex-col gap-sm">
|
|
|
|
|
<Link to={`/engagements/${engagementId}`} className="btn-text-link text-[14px]">
|
|
|
|
|
← Back to engagement
|
|
|
|
|
</Link>
|
|
|
|
|
<h1 className="text-[44px] font-medium leading-none">{rt.name || simulation?.name}</h1>
|
|
|
|
|
{status ? (
|
|
|
|
|
<div className="flex items-center gap-md">
|
|
|
|
|
<SimulationStatusBadge status={status} />
|
|
|
|
|
{simulation?.created_by && (
|
|
|
|
|
<span className="text-[14px] text-graphite">
|
|
|
|
|
Created by <span className="text-ink">{simulation.created_by.username}</span>
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
|
feat(frontend): sprint 4 — dark mode + matrix overhaul + tactic selection + done read-only + UI polish
US-17: fix duplicate "Create engagement" button, icon conventions (Save/RotateCcw/Grid2x2), UsersAdminPage form baseline alignment
US-18: done status fully read-only + Reopen button (done → review_required) for all roles
US-19: invalidate engagement queries on simulation PATCH/transition for auto-status propagation
US-20: MitreMatrixModal rewritten — CSS grid 12-column layout, no horizontal scroll, attack.mitre.org compact look
US-21: tactic header clickable in matrix, tactic chips (MitreTacticTag) in field, single atomic PATCH with technique_ids + tactic_ids
US-22: MitreTechniquesField chips-only area + inline search input + matrix icon button; chips show ID-only (name in title=)
US-23: useTheme hook — 3-state light/dark/system, CSS variables, Tailwind darkMode class, localStorage persistence
92/92 tests passing, typecheck and lint clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:06:01 +02:00
|
|
|
{/* Done banner */}
|
|
|
|
|
{isDone && (
|
|
|
|
|
<div
|
|
|
|
|
role="status"
|
|
|
|
|
className="rounded-xl px-xl py-md bg-cloud border border-hairline text-[14px] text-charcoal"
|
|
|
|
|
>
|
|
|
|
|
This simulation is <strong>done</strong> and read-only. Use Reopen to make changes.
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* SOC banner */}
|
2026-05-26 11:13:14 +02:00
|
|
|
{socBlocked && (
|
|
|
|
|
<div
|
|
|
|
|
role="alert"
|
|
|
|
|
data-testid="soc-blocked-banner"
|
|
|
|
|
className="rounded-xl px-xl py-md bg-fog border border-hairline text-[14px] text-charcoal"
|
|
|
|
|
>
|
2026-05-26 16:08:46 +02:00
|
|
|
Simulation not yet ready for review — the red team must mark it as "Review required" before you can fill in the SOC section.
|
2026-05-26 11:13:14 +02:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Red Team card */}
|
|
|
|
|
<form
|
|
|
|
|
id="rt-form"
|
feat(frontend): sprint 4 — dark mode + matrix overhaul + tactic selection + done read-only + UI polish
US-17: fix duplicate "Create engagement" button, icon conventions (Save/RotateCcw/Grid2x2), UsersAdminPage form baseline alignment
US-18: done status fully read-only + Reopen button (done → review_required) for all roles
US-19: invalidate engagement queries on simulation PATCH/transition for auto-status propagation
US-20: MitreMatrixModal rewritten — CSS grid 12-column layout, no horizontal scroll, attack.mitre.org compact look
US-21: tactic header clickable in matrix, tactic chips (MitreTacticTag) in field, single atomic PATCH with technique_ids + tactic_ids
US-22: MitreTechniquesField chips-only area + inline search input + matrix icon button; chips show ID-only (name in title=)
US-23: useTheme hook — 3-state light/dark/system, CSS variables, Tailwind darkMode class, localStorage persistence
92/92 tests passing, typecheck and lint clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:06:01 +02:00
|
|
|
onSubmit={canEditRT && !isDone ? onSaveRT : (e) => e.preventDefault()}
|
2026-05-26 11:13:14 +02:00
|
|
|
noValidate
|
|
|
|
|
className="card-product flex flex-col gap-md"
|
|
|
|
|
>
|
|
|
|
|
<h2 className="text-[20px] font-medium text-ink">Red Team</h2>
|
|
|
|
|
|
|
|
|
|
<FormField label="Name" htmlFor="sim-name" required error={nameError}>
|
|
|
|
|
<TextInput
|
|
|
|
|
id="sim-name"
|
|
|
|
|
name="name"
|
|
|
|
|
value={rt.name}
|
|
|
|
|
onChange={(e) => setRt({ ...rt, name: e.target.value })}
|
|
|
|
|
disabled={rtDisabled}
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
</FormField>
|
|
|
|
|
|
feat(frontend): sprint 3 — multi-technique MITRE selection + matrix modal
- types: replace mitre_technique_id/name scalars with techniques:MitreTechnique[]
on Simulation; add MitreTactic/MitreMatrixTechnique/MitreMatrixSubtechnique;
SimulationPatchInput now uses technique_ids:string[]
- api/mitre.ts: add getMitreMatrix() → GET /api/mitre/matrix
- hooks/useMitre: add useMitreMatrix(enabled) with staleTime:Infinity
- MitreTechniquePicker: clean rewrite — onSelect(technique) one-shot, resets
input after selection, no incoming value props
- MitreTechniqueTag: chip component with id+name and × remove button
- MitreMatrixModal: tactic columns (220px fixed), expand/collapse subtechniques,
search filter (auto-expands parent on sub match), selection state, focus trap
(Tab wrap, Escape, search autofocus), backdrop click cancel, Apply N techniques
- MitreTechniquesField: orchestrates tags+picker+matrix with auto-save PATCH on
every add/remove/Apply, dedup guard, disabled read-only mode for SOC
- SimulationFormPage: swap MitreTechniquePicker for MitreTechniquesField; remove
technique state from RT form (techniques have independent auto-save cycle)
- SimulationList: MITRE column → T1059 +2 counter format, — when empty
- Tests: 84 passing (13 test files); new suites for Tag, Field, Modal;
MitreTechniquePicker + SimulationFormPage + SimulationList adapted to new API
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 04:04:23 +02:00
|
|
|
<div className="flex flex-col gap-xs">
|
feat(frontend): sprint 4 — dark mode + matrix overhaul + tactic selection + done read-only + UI polish
US-17: fix duplicate "Create engagement" button, icon conventions (Save/RotateCcw/Grid2x2), UsersAdminPage form baseline alignment
US-18: done status fully read-only + Reopen button (done → review_required) for all roles
US-19: invalidate engagement queries on simulation PATCH/transition for auto-status propagation
US-20: MitreMatrixModal rewritten — CSS grid 12-column layout, no horizontal scroll, attack.mitre.org compact look
US-21: tactic header clickable in matrix, tactic chips (MitreTacticTag) in field, single atomic PATCH with technique_ids + tactic_ids
US-22: MitreTechniquesField chips-only area + inline search input + matrix icon button; chips show ID-only (name in title=)
US-23: useTheme hook — 3-state light/dark/system, CSS variables, Tailwind darkMode class, localStorage persistence
92/92 tests passing, typecheck and lint clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:06:01 +02:00
|
|
|
<span className="text-[14px] font-medium text-ink">MITRE Techniques & Tactics</span>
|
feat(frontend): sprint 3 — multi-technique MITRE selection + matrix modal
- types: replace mitre_technique_id/name scalars with techniques:MitreTechnique[]
on Simulation; add MitreTactic/MitreMatrixTechnique/MitreMatrixSubtechnique;
SimulationPatchInput now uses technique_ids:string[]
- api/mitre.ts: add getMitreMatrix() → GET /api/mitre/matrix
- hooks/useMitre: add useMitreMatrix(enabled) with staleTime:Infinity
- MitreTechniquePicker: clean rewrite — onSelect(technique) one-shot, resets
input after selection, no incoming value props
- MitreTechniqueTag: chip component with id+name and × remove button
- MitreMatrixModal: tactic columns (220px fixed), expand/collapse subtechniques,
search filter (auto-expands parent on sub match), selection state, focus trap
(Tab wrap, Escape, search autofocus), backdrop click cancel, Apply N techniques
- MitreTechniquesField: orchestrates tags+picker+matrix with auto-save PATCH on
every add/remove/Apply, dedup guard, disabled read-only mode for SOC
- SimulationFormPage: swap MitreTechniquePicker for MitreTechniquesField; remove
technique state from RT form (techniques have independent auto-save cycle)
- SimulationList: MITRE column → T1059 +2 counter format, — when empty
- Tests: 84 passing (13 test files); new suites for Tag, Field, Modal;
MitreTechniquePicker + SimulationFormPage + SimulationList adapted to new API
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 04:04:23 +02:00
|
|
|
<MitreTechniquesField
|
|
|
|
|
value={simulation?.techniques ?? []}
|
feat(frontend): sprint 4 — dark mode + matrix overhaul + tactic selection + done read-only + UI polish
US-17: fix duplicate "Create engagement" button, icon conventions (Save/RotateCcw/Grid2x2), UsersAdminPage form baseline alignment
US-18: done status fully read-only + Reopen button (done → review_required) for all roles
US-19: invalidate engagement queries on simulation PATCH/transition for auto-status propagation
US-20: MitreMatrixModal rewritten — CSS grid 12-column layout, no horizontal scroll, attack.mitre.org compact look
US-21: tactic header clickable in matrix, tactic chips (MitreTacticTag) in field, single atomic PATCH with technique_ids + tactic_ids
US-22: MitreTechniquesField chips-only area + inline search input + matrix icon button; chips show ID-only (name in title=)
US-23: useTheme hook — 3-state light/dark/system, CSS variables, Tailwind darkMode class, localStorage persistence
92/92 tests passing, typecheck and lint clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:06:01 +02:00
|
|
|
tactics={simulation?.tactics ?? []}
|
feat(frontend): sprint 3 — multi-technique MITRE selection + matrix modal
- types: replace mitre_technique_id/name scalars with techniques:MitreTechnique[]
on Simulation; add MitreTactic/MitreMatrixTechnique/MitreMatrixSubtechnique;
SimulationPatchInput now uses technique_ids:string[]
- api/mitre.ts: add getMitreMatrix() → GET /api/mitre/matrix
- hooks/useMitre: add useMitreMatrix(enabled) with staleTime:Infinity
- MitreTechniquePicker: clean rewrite — onSelect(technique) one-shot, resets
input after selection, no incoming value props
- MitreTechniqueTag: chip component with id+name and × remove button
- MitreMatrixModal: tactic columns (220px fixed), expand/collapse subtechniques,
search filter (auto-expands parent on sub match), selection state, focus trap
(Tab wrap, Escape, search autofocus), backdrop click cancel, Apply N techniques
- MitreTechniquesField: orchestrates tags+picker+matrix with auto-save PATCH on
every add/remove/Apply, dedup guard, disabled read-only mode for SOC
- SimulationFormPage: swap MitreTechniquePicker for MitreTechniquesField; remove
technique state from RT form (techniques have independent auto-save cycle)
- SimulationList: MITRE column → T1059 +2 counter format, — when empty
- Tests: 84 passing (13 test files); new suites for Tag, Field, Modal;
MitreTechniquePicker + SimulationFormPage + SimulationList adapted to new API
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 04:04:23 +02:00
|
|
|
simulationId={simulationId as number}
|
|
|
|
|
engagementId={engagementId as number}
|
2026-05-26 11:13:14 +02:00
|
|
|
disabled={rtDisabled}
|
|
|
|
|
/>
|
feat(frontend): sprint 3 — multi-technique MITRE selection + matrix modal
- types: replace mitre_technique_id/name scalars with techniques:MitreTechnique[]
on Simulation; add MitreTactic/MitreMatrixTechnique/MitreMatrixSubtechnique;
SimulationPatchInput now uses technique_ids:string[]
- api/mitre.ts: add getMitreMatrix() → GET /api/mitre/matrix
- hooks/useMitre: add useMitreMatrix(enabled) with staleTime:Infinity
- MitreTechniquePicker: clean rewrite — onSelect(technique) one-shot, resets
input after selection, no incoming value props
- MitreTechniqueTag: chip component with id+name and × remove button
- MitreMatrixModal: tactic columns (220px fixed), expand/collapse subtechniques,
search filter (auto-expands parent on sub match), selection state, focus trap
(Tab wrap, Escape, search autofocus), backdrop click cancel, Apply N techniques
- MitreTechniquesField: orchestrates tags+picker+matrix with auto-save PATCH on
every add/remove/Apply, dedup guard, disabled read-only mode for SOC
- SimulationFormPage: swap MitreTechniquePicker for MitreTechniquesField; remove
technique state from RT form (techniques have independent auto-save cycle)
- SimulationList: MITRE column → T1059 +2 counter format, — when empty
- Tests: 84 passing (13 test files); new suites for Tag, Field, Modal;
MitreTechniquePicker + SimulationFormPage + SimulationList adapted to new API
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 04:04:23 +02:00
|
|
|
</div>
|
2026-05-26 11:13:14 +02:00
|
|
|
|
|
|
|
|
<FormField label="Description" htmlFor="sim-description">
|
|
|
|
|
<TextArea
|
|
|
|
|
id="sim-description"
|
|
|
|
|
name="description"
|
|
|
|
|
value={rt.description}
|
|
|
|
|
onChange={(e) => setRt({ ...rt, description: e.target.value })}
|
|
|
|
|
disabled={rtDisabled}
|
|
|
|
|
/>
|
|
|
|
|
</FormField>
|
|
|
|
|
|
feat(frontend): sprint 4 — dark mode + matrix overhaul + tactic selection + done read-only + UI polish
US-17: fix duplicate "Create engagement" button, icon conventions (Save/RotateCcw/Grid2x2), UsersAdminPage form baseline alignment
US-18: done status fully read-only + Reopen button (done → review_required) for all roles
US-19: invalidate engagement queries on simulation PATCH/transition for auto-status propagation
US-20: MitreMatrixModal rewritten — CSS grid 12-column layout, no horizontal scroll, attack.mitre.org compact look
US-21: tactic header clickable in matrix, tactic chips (MitreTacticTag) in field, single atomic PATCH with technique_ids + tactic_ids
US-22: MitreTechniquesField chips-only area + inline search input + matrix icon button; chips show ID-only (name in title=)
US-23: useTheme hook — 3-state light/dark/system, CSS variables, Tailwind darkMode class, localStorage persistence
92/92 tests passing, typecheck and lint clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:06:01 +02:00
|
|
|
<FormField label="Commands" htmlFor="sim-commands" hint="One command per line">
|
2026-05-26 11:13:14 +02:00
|
|
|
<TextArea
|
|
|
|
|
id="sim-commands"
|
|
|
|
|
name="commands"
|
|
|
|
|
value={rt.commands}
|
|
|
|
|
onChange={(e) => setRt({ ...rt, commands: e.target.value })}
|
|
|
|
|
disabled={rtDisabled}
|
|
|
|
|
className="min-h-[160px] font-mono text-[14px]"
|
|
|
|
|
/>
|
|
|
|
|
</FormField>
|
|
|
|
|
|
|
|
|
|
<FormField label="Prerequisites" htmlFor="sim-prerequisites">
|
|
|
|
|
<TextArea
|
|
|
|
|
id="sim-prerequisites"
|
|
|
|
|
name="prerequisites"
|
|
|
|
|
value={rt.prerequisites}
|
|
|
|
|
onChange={(e) => setRt({ ...rt, prerequisites: e.target.value })}
|
|
|
|
|
disabled={rtDisabled}
|
|
|
|
|
/>
|
|
|
|
|
</FormField>
|
|
|
|
|
|
2026-05-26 16:08:46 +02:00
|
|
|
<FormField label="Executed at" htmlFor="sim-executed-at">
|
|
|
|
|
<TextInput
|
|
|
|
|
id="sim-executed-at"
|
|
|
|
|
type="datetime-local"
|
|
|
|
|
name="executed_at"
|
|
|
|
|
value={rt.executed_at}
|
|
|
|
|
onChange={(e) => setRt({ ...rt, executed_at: e.target.value })}
|
|
|
|
|
disabled={rtDisabled}
|
|
|
|
|
/>
|
|
|
|
|
</FormField>
|
2026-05-26 11:13:14 +02:00
|
|
|
|
2026-05-26 16:08:46 +02:00
|
|
|
<FormField label="Execution result" htmlFor="sim-exec-result">
|
|
|
|
|
<TextArea
|
|
|
|
|
id="sim-exec-result"
|
|
|
|
|
name="execution_result"
|
|
|
|
|
value={rt.execution_result}
|
|
|
|
|
onChange={(e) => setRt({ ...rt, execution_result: e.target.value })}
|
|
|
|
|
disabled={rtDisabled}
|
|
|
|
|
rows={5}
|
|
|
|
|
/>
|
|
|
|
|
</FormField>
|
2026-05-26 11:13:14 +02:00
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
{/* SOC card */}
|
|
|
|
|
<form
|
|
|
|
|
id="soc-form"
|
2026-05-26 11:22:05 +02:00
|
|
|
onSubmit={canSaveSoc ? onSaveSOC : (e) => e.preventDefault()}
|
2026-05-26 11:13:14 +02:00
|
|
|
noValidate
|
|
|
|
|
className="card-product flex flex-col gap-md"
|
|
|
|
|
>
|
|
|
|
|
<h2 className="text-[20px] font-medium text-ink">SOC</h2>
|
|
|
|
|
|
|
|
|
|
<FormField label="Log source" htmlFor="sim-log-source">
|
|
|
|
|
<TextInput
|
|
|
|
|
id="sim-log-source"
|
|
|
|
|
name="log_source"
|
|
|
|
|
value={soc.log_source}
|
|
|
|
|
onChange={(e) => setSoc({ ...soc, log_source: e.target.value })}
|
|
|
|
|
disabled={socDisabled}
|
|
|
|
|
/>
|
|
|
|
|
</FormField>
|
|
|
|
|
|
|
|
|
|
<FormField label="Logs" htmlFor="sim-logs">
|
|
|
|
|
<TextArea
|
|
|
|
|
id="sim-logs"
|
|
|
|
|
name="logs"
|
|
|
|
|
value={soc.logs}
|
|
|
|
|
onChange={(e) => setSoc({ ...soc, logs: e.target.value })}
|
|
|
|
|
disabled={socDisabled}
|
|
|
|
|
/>
|
|
|
|
|
</FormField>
|
|
|
|
|
|
|
|
|
|
<FormField label="SOC comment" htmlFor="sim-soc-comment">
|
|
|
|
|
<TextArea
|
|
|
|
|
id="sim-soc-comment"
|
|
|
|
|
name="soc_comment"
|
|
|
|
|
value={soc.soc_comment}
|
|
|
|
|
onChange={(e) => setSoc({ ...soc, soc_comment: e.target.value })}
|
|
|
|
|
disabled={socDisabled}
|
|
|
|
|
/>
|
|
|
|
|
</FormField>
|
|
|
|
|
|
|
|
|
|
<FormField label="Incident number" htmlFor="sim-incident">
|
|
|
|
|
<TextInput
|
|
|
|
|
id="sim-incident"
|
|
|
|
|
name="incident_number"
|
|
|
|
|
value={soc.incident_number}
|
|
|
|
|
onChange={(e) => setSoc({ ...soc, incident_number: e.target.value })}
|
|
|
|
|
disabled={socDisabled}
|
|
|
|
|
/>
|
|
|
|
|
</FormField>
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
{submitError ? (
|
feat(frontend): sprint 4 — dark mode + matrix overhaul + tactic selection + done read-only + UI polish
US-17: fix duplicate "Create engagement" button, icon conventions (Save/RotateCcw/Grid2x2), UsersAdminPage form baseline alignment
US-18: done status fully read-only + Reopen button (done → review_required) for all roles
US-19: invalidate engagement queries on simulation PATCH/transition for auto-status propagation
US-20: MitreMatrixModal rewritten — CSS grid 12-column layout, no horizontal scroll, attack.mitre.org compact look
US-21: tactic header clickable in matrix, tactic chips (MitreTacticTag) in field, single atomic PATCH with technique_ids + tactic_ids
US-22: MitreTechniquesField chips-only area + inline search input + matrix icon button; chips show ID-only (name in title=)
US-23: useTheme hook — 3-state light/dark/system, CSS variables, Tailwind darkMode class, localStorage persistence
92/92 tests passing, typecheck and lint clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:06:01 +02:00
|
|
|
<div role="alert" className="text-[14px] text-bloom-deep">{submitError}</div>
|
2026-05-26 11:13:14 +02:00
|
|
|
) : null}
|
|
|
|
|
|
2026-05-26 16:08:46 +02:00
|
|
|
{/* Unified sticky action bar */}
|
|
|
|
|
<div className="sticky bottom-0 bg-canvas border-t border-hairline flex items-center gap-md flex-wrap py-md">
|
feat(frontend): sprint 4 — dark mode + matrix overhaul + tactic selection + done read-only + UI polish
US-17: fix duplicate "Create engagement" button, icon conventions (Save/RotateCcw/Grid2x2), UsersAdminPage form baseline alignment
US-18: done status fully read-only + Reopen button (done → review_required) for all roles
US-19: invalidate engagement queries on simulation PATCH/transition for auto-status propagation
US-20: MitreMatrixModal rewritten — CSS grid 12-column layout, no horizontal scroll, attack.mitre.org compact look
US-21: tactic header clickable in matrix, tactic chips (MitreTacticTag) in field, single atomic PATCH with technique_ids + tactic_ids
US-22: MitreTechniquesField chips-only area + inline search input + matrix icon button; chips show ID-only (name in title=)
US-23: useTheme hook — 3-state light/dark/system, CSS variables, Tailwind darkMode class, localStorage persistence
92/92 tests passing, typecheck and lint clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:06:01 +02:00
|
|
|
{/* Done state: Reopen only */}
|
|
|
|
|
{showReopen && (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="btn-outline"
|
|
|
|
|
onClick={onReopen}
|
|
|
|
|
disabled={transitionMutation.isPending}
|
|
|
|
|
data-testid="reopen-btn"
|
|
|
|
|
>
|
|
|
|
|
<RotateCcw size={14} aria-hidden />
|
|
|
|
|
Reopen
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Normal state buttons */}
|
|
|
|
|
{!isDone && canEditRT && (
|
2026-05-26 16:08:46 +02:00
|
|
|
<button type="submit" form="rt-form" className="btn-primary" disabled={submitting}>
|
feat(frontend): sprint 4 — dark mode + matrix overhaul + tactic selection + done read-only + UI polish
US-17: fix duplicate "Create engagement" button, icon conventions (Save/RotateCcw/Grid2x2), UsersAdminPage form baseline alignment
US-18: done status fully read-only + Reopen button (done → review_required) for all roles
US-19: invalidate engagement queries on simulation PATCH/transition for auto-status propagation
US-20: MitreMatrixModal rewritten — CSS grid 12-column layout, no horizontal scroll, attack.mitre.org compact look
US-21: tactic header clickable in matrix, tactic chips (MitreTacticTag) in field, single atomic PATCH with technique_ids + tactic_ids
US-22: MitreTechniquesField chips-only area + inline search input + matrix icon button; chips show ID-only (name in title=)
US-23: useTheme hook — 3-state light/dark/system, CSS variables, Tailwind darkMode class, localStorage persistence
92/92 tests passing, typecheck and lint clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:06:01 +02:00
|
|
|
<Save size={14} aria-hidden />
|
|
|
|
|
{updateMutation.isPending ? 'Saving…' : 'Save'}
|
2026-05-26 16:08:46 +02:00
|
|
|
</button>
|
|
|
|
|
)}
|
feat(frontend): sprint 4 — dark mode + matrix overhaul + tactic selection + done read-only + UI polish
US-17: fix duplicate "Create engagement" button, icon conventions (Save/RotateCcw/Grid2x2), UsersAdminPage form baseline alignment
US-18: done status fully read-only + Reopen button (done → review_required) for all roles
US-19: invalidate engagement queries on simulation PATCH/transition for auto-status propagation
US-20: MitreMatrixModal rewritten — CSS grid 12-column layout, no horizontal scroll, attack.mitre.org compact look
US-21: tactic header clickable in matrix, tactic chips (MitreTacticTag) in field, single atomic PATCH with technique_ids + tactic_ids
US-22: MitreTechniquesField chips-only area + inline search input + matrix icon button; chips show ID-only (name in title=)
US-23: useTheme hook — 3-state light/dark/system, CSS variables, Tailwind darkMode class, localStorage persistence
92/92 tests passing, typecheck and lint clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:06:01 +02:00
|
|
|
{!isDone && canSaveSoc && (
|
2026-05-26 16:08:46 +02:00
|
|
|
<button type="submit" form="soc-form" className="btn-primary" disabled={submitting}>
|
feat(frontend): sprint 4 — dark mode + matrix overhaul + tactic selection + done read-only + UI polish
US-17: fix duplicate "Create engagement" button, icon conventions (Save/RotateCcw/Grid2x2), UsersAdminPage form baseline alignment
US-18: done status fully read-only + Reopen button (done → review_required) for all roles
US-19: invalidate engagement queries on simulation PATCH/transition for auto-status propagation
US-20: MitreMatrixModal rewritten — CSS grid 12-column layout, no horizontal scroll, attack.mitre.org compact look
US-21: tactic header clickable in matrix, tactic chips (MitreTacticTag) in field, single atomic PATCH with technique_ids + tactic_ids
US-22: MitreTechniquesField chips-only area + inline search input + matrix icon button; chips show ID-only (name in title=)
US-23: useTheme hook — 3-state light/dark/system, CSS variables, Tailwind darkMode class, localStorage persistence
92/92 tests passing, typecheck and lint clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:06:01 +02:00
|
|
|
<Save size={14} aria-hidden />
|
2026-05-26 16:08:46 +02:00
|
|
|
{updateMutation.isPending ? 'Saving…' : 'Save SOC'}
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
2026-05-26 11:13:14 +02:00
|
|
|
{showMarkReview && (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="btn-outline"
|
|
|
|
|
onClick={onMarkReview}
|
|
|
|
|
disabled={transitionMutation.isPending}
|
|
|
|
|
>
|
2026-05-26 16:08:46 +02:00
|
|
|
Mark for review
|
2026-05-26 11:13:14 +02:00
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
{showClose && (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="btn-outline"
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
disabled={transitionMutation.isPending}
|
|
|
|
|
>
|
2026-05-26 16:08:46 +02:00
|
|
|
Close
|
2026-05-26 11:13:14 +02:00
|
|
|
</button>
|
|
|
|
|
)}
|
feat(frontend): sprint 4 — dark mode + matrix overhaul + tactic selection + done read-only + UI polish
US-17: fix duplicate "Create engagement" button, icon conventions (Save/RotateCcw/Grid2x2), UsersAdminPage form baseline alignment
US-18: done status fully read-only + Reopen button (done → review_required) for all roles
US-19: invalidate engagement queries on simulation PATCH/transition for auto-status propagation
US-20: MitreMatrixModal rewritten — CSS grid 12-column layout, no horizontal scroll, attack.mitre.org compact look
US-21: tactic header clickable in matrix, tactic chips (MitreTacticTag) in field, single atomic PATCH with technique_ids + tactic_ids
US-22: MitreTechniquesField chips-only area + inline search input + matrix icon button; chips show ID-only (name in title=)
US-23: useTheme hook — 3-state light/dark/system, CSS variables, Tailwind darkMode class, localStorage persistence
92/92 tests passing, typecheck and lint clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:06:01 +02:00
|
|
|
{!isDone && canEditEngagements && simulationId && (
|
2026-05-26 11:13:14 +02:00
|
|
|
<button
|
|
|
|
|
type="button"
|
2026-05-26 16:08:46 +02:00
|
|
|
className="btn-text-link text-bloom-deep ml-auto"
|
2026-05-26 11:13:14 +02:00
|
|
|
onClick={() => setShowDeleteConfirm(true)}
|
|
|
|
|
disabled={submitting}
|
|
|
|
|
>
|
2026-05-26 16:08:46 +02:00
|
|
|
Delete
|
2026-05-26 11:13:14 +02:00
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{showDeleteConfirm && (
|
|
|
|
|
<ConfirmDialog
|
2026-05-26 16:08:46 +02:00
|
|
|
title="Delete simulation"
|
|
|
|
|
description="This action is permanent. The simulation will be deleted forever."
|
|
|
|
|
confirmLabel="Delete"
|
|
|
|
|
cancelLabel="Cancel"
|
2026-05-26 11:13:14 +02:00
|
|
|
destructive
|
|
|
|
|
onConfirm={onDelete}
|
|
|
|
|
onCancel={() => setShowDeleteConfirm(false)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|