import { useEffect, useState, type FormEvent } from 'react'; import { Link, useNavigate, useParams } from 'react-router-dom'; import { Save, Grid2x2 } from 'lucide-react'; import { extractApiError } from '@/api/client'; import type { MitreTechnique, MitreTacticRef } from '@/api/types'; import { useToast } from '@/hooks/useToast'; import { useCreateTemplate, useDeleteTemplate, useTemplate, useUpdateTemplate } from '@/hooks/useTemplates'; import { FormField, TextArea, TextInput } from '@/components/FormField'; import { LoadingState } from '@/components/LoadingState'; import { ErrorState } from '@/components/ErrorState'; import { ConfirmDialog } from '@/components/ConfirmDialog'; import { MitreTechniqueTag, MitreTacticTag } from '@/components/MitreTechniqueTag'; import { MitreTechniquePicker } from '@/components/MitreTechniquePicker'; import { MitreMatrixModal } from '@/components/MitreMatrixModal'; import type { MatrixSelection } from '@/components/MitreMatrixModal'; interface FormState { name: string; description: string; commands: string; prerequisites: string; } const EMPTY: FormState = { name: '', description: '', commands: '', prerequisites: '' }; export function TemplateFormPage(): JSX.Element { const { id } = useParams<{ id: string }>(); const templateId = id ? Number(id) : undefined; const isNew = !templateId; const navigate = useNavigate(); const { push } = useToast(); const existing = useTemplate(templateId); const createMutation = useCreateTemplate(); const updateMutation = useUpdateTemplate(templateId ?? 0); const deleteMutation = useDeleteTemplate(); const [form, setForm] = useState(EMPTY); const [techniques, setTechniques] = useState([]); const [tactics, setTactics] = useState([]); const [formError, setFormError] = useState(null); const [showMatrix, setShowMatrix] = useState(false); const [showPicker, setShowPicker] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); useEffect(() => { if (existing.data) { const t = existing.data; setForm({ name: t.name, description: t.description ?? '', commands: t.commands ?? '', prerequisites: t.prerequisites ?? '', }); setTechniques(t.techniques); setTactics(t.tactics); } }, [existing.data]); const isPending = createMutation.isPending || updateMutation.isPending; const onSubmit = async (e: FormEvent) => { e.preventDefault(); setFormError(null); if (!form.name.trim()) { setFormError('Name is required'); return; } const payload = { name: form.name.trim(), description: form.description.trim() || null, commands: form.commands.trim() || null, prerequisites: form.prerequisites.trim() || null, technique_ids: techniques.map((t) => t.id), tactic_ids: tactics.map((t) => t.id), }; try { if (isNew) { const created = await createMutation.mutateAsync(payload); push('Template created', 'success'); navigate(`/admin/templates/${created.id}/edit`, { replace: true }); } else { await updateMutation.mutateAsync(payload); push('Template saved', 'success'); } } catch (err) { setFormError(extractApiError(err, 'Could not save template')); } }; const onDelete = async () => { if (!templateId) return; try { await deleteMutation.mutateAsync(templateId); push('Template deleted', 'success'); navigate('/admin/templates', { replace: true }); } catch (err) { push(extractApiError(err, 'Could not delete template'), 'error'); } setShowDeleteConfirm(false); }; const handleMatrixApply = ({ techniques: newTech, tactics: newTac }: MatrixSelection) => { setShowMatrix(false); setTechniques(newTech); setTactics(newTac); }; const handlePickerSelect = (technique: MitreTechnique) => { if (techniques.some((t) => t.id === technique.id)) return; setTechniques((prev) => [...prev, technique]); setShowPicker(false); }; if (!isNew && existing.isLoading) return ; if (!isNew && existing.isError) { return ( existing.refetch()} /> ); } return (
← Back to templates

{isNew ? 'New template' : (existing.data?.name ?? 'Edit template')}

{!isNew ? ( ) : null}
setForm({ ...form, name: e.target.value })} disabled={isPending} />