import { useEffect, useRef, useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { ChevronDown, Plus } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { extractApiError } from '@/api/client'; import type { SimulationTemplate } from '@/api/types'; import { useAuth } from '@/hooks/useAuth'; import { useEngagementSimulations, useCreateSimulation } from '@/hooks/useSimulations'; import { useToast } from '@/hooks/useToast'; import { formatDateTime } from '@/lib/format'; import { LoadingState } from './LoadingState'; import { ErrorState } from './ErrorState'; import { EmptyState } from './EmptyState'; import { SimulationStatusBadge } from './SimulationStatusBadge'; import { TemplatePickerModal } from './TemplatePickerModal'; interface SimulationListProps { engagementId: number; } function NewSimulationDropdown({ engagementId }: { engagementId: number }): JSX.Element { const { t } = useTranslation(); const navigate = useNavigate(); const { push } = useToast(); const [open, setOpen] = useState(false); const [showPicker, setShowPicker] = useState(false); const ref = useRef(null); const createMutation = useCreateSimulation(engagementId); useEffect(() => { if (!open) return; const onPointerDown = (e: PointerEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) { setOpen(false); } }; const onKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') setOpen(false); }; document.addEventListener('pointerdown', onPointerDown); document.addEventListener('keydown', onKeyDown); return () => { document.removeEventListener('pointerdown', onPointerDown); document.removeEventListener('keydown', onKeyDown); }; }, [open]); const handleBlank = () => { setOpen(false); navigate(`/engagements/${engagementId}/simulations/new`); }; const handleFromTemplate = () => { setOpen(false); setShowPicker(true); }; const handleSelectTemplate = async (template: SimulationTemplate) => { try { const sim = await createMutation.mutateAsync({ name: template.name, template_id: template.id }); setShowPicker(false); push(t('simulation.form.toast.created'), 'success'); navigate(`/engagements/${engagementId}/simulations/${sim.id}/edit`); } catch (err) { push(extractApiError(err, t('simulation.form.error.create')), 'error'); } }; return (
{open ? (
) : null} {showPicker ? ( setShowPicker(false)} onInstantiated={(simId) => { setShowPicker(false); navigate(`/engagements/${engagementId}/simulations/${simId}/edit`); }} onSelectTemplate={handleSelectTemplate} isPending={createMutation.isPending} /> ) : null}
); } export function SimulationList({ engagementId }: SimulationListProps): JSX.Element { const { t } = useTranslation(); const { data, isLoading, isError, error, refetch } = useEngagementSimulations(engagementId); const { canEditEngagements } = useAuth(); const navigate = useNavigate(); if (isLoading) return ; if (isError) { return ( refetch()} /> ); } if (!data || data.length === 0) { return ( ) : undefined } /> ); } return (

{t('engagement.detail.tabs.simulations')}

{canEditEngagements ? ( ) : null}
{data.map((sim) => ( navigate(`/engagements/${engagementId}/simulations/${sim.id}/edit`) } > ))}
{t('simulation.list.col.name')} {t('simulation.list.col.mitre')} {t('simulation.list.col.status')} {t('simulation.list.col.executedAt')}
e.stopPropagation()} > {sim.name} {(() => { const items = [ ...(sim.tactics ?? []).map((tactic) => tactic.id), ...sim.techniques.map((tech) => tech.id), ]; if (items.length === 0) return '—'; if (items.length === 1) return items[0]; return `${items[0]} +${items.length - 1}`; })()} {sim.executed_at ? formatDateTime(sim.executed_at) : '—'}
); }