feat(frontend): c2 tasks panel + history import (sprint 8 phase 2)

- Add getC2Tasks / listCallbackHistory / importC2 API functions + types
- useC2Tasks with 2500ms polling (stops when all tasks completed)
- useC2CallbackHistory, useImportC2 hooks
- C2TaskStatusBadge, C2TasksPanel (expandable output rows, polling indicator)
- C2CallbackPicker extracted as shared component (reused in both modals)
- ImportC2HistoryModal: 2-step callback picker → paginated history table
- SimulationFormPage: RT card + tasks panel share left grid column; Import C2 history button
- 37 new tests (api/c2, C2TasksPanel, ImportC2HistoryModal, SimulationFormPage panel visibility)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Knacky
2026-06-10 20:11:12 +02:00
parent 8f23f59601
commit 7ff153905b
13 changed files with 1437 additions and 75 deletions

View File

@@ -12,7 +12,7 @@ import {
useTransitionSimulation,
useUpdateSimulation,
} from '@/hooks/useSimulations';
import { useC2Config } from '@/hooks/useC2';
import { useC2Config, useC2Tasks } from '@/hooks/useC2';
import { FormField, TextArea, TextInput } from '@/components/FormField';
import { LoadingState } from '@/components/LoadingState';
import { ErrorState } from '@/components/ErrorState';
@@ -20,6 +20,8 @@ import { SimulationStatusBadge } from '@/components/SimulationStatusBadge';
import { ConfirmDialog } from '@/components/ConfirmDialog';
import { MitreTechniquesField } from '@/components/MitreTechniquesField';
import { ExecuteViaC2Modal } from '@/components/ExecuteViaC2Modal';
import { ImportC2HistoryModal } from '@/components/ImportC2HistoryModal';
import { C2TasksPanel } from '@/components/C2TasksPanel';
interface RedteamFormState {
name: string;
@@ -70,6 +72,13 @@ export function SimulationFormPage(): JSX.Element {
);
const hasC2Config = c2ConfigQuery.data !== null && c2ConfigQuery.data !== undefined;
const c2TasksQuery = useC2Tasks(!isNew ? simulationId : undefined, {
enabled: !isNew && canEditRT,
});
const hasTasks = (c2TasksQuery.data?.tasks?.length ?? 0) > 0;
// Show panel when: has C2 config (so Execute button is visible) OR already has tasks
const showTasksPanel = !isNew && canEditRT && (hasC2Config || hasTasks);
const detail = useSimulation(isNew ? undefined : simulationId);
const createMutation = useCreateSimulation(engagementId ?? 0);
const updateMutation = useUpdateSimulation(simulationId ?? 0, engagementId ?? 0);
@@ -82,6 +91,7 @@ export function SimulationFormPage(): JSX.Element {
const [submitError, setSubmitError] = useState<string | null>(null);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [showC2Modal, setShowC2Modal] = useState(false);
const [showImportModal, setShowImportModal] = useState(false);
useEffect(() => {
if (!isNew && detail.data) {
@@ -307,8 +317,10 @@ export function SimulationFormPage(): JSX.Element {
</div>
)}
{/* 2-column grid: RT left, SOC right. Stacks vertically below lg. */}
{/* 2-column grid: RT+tasks left, SOC right. Stacks vertically below lg. */}
<div className="grid gap-xl lg:grid-cols-2 items-start">
{/* Left column: RT card + C2 tasks panel */}
<div className="flex flex-col gap-xl">
{/* Red Team card */}
<form
id="rt-form"
@@ -394,7 +406,7 @@ export function SimulationFormPage(): JSX.Element {
</FormField>
{!isDone && canEditRT && hasC2Config && (
<div className="pt-xs">
<div className="pt-xs flex items-center gap-md flex-wrap">
<button
type="button"
data-testid="c2-execute-btn"
@@ -403,10 +415,24 @@ export function SimulationFormPage(): JSX.Element {
>
Execute via C2
</button>
<button
type="button"
data-testid="c2-import-trigger-btn"
className="btn-outline"
onClick={() => setShowImportModal(true)}
>
Import C2 history
</button>
</div>
)}
</form>
{/* C2 tasks panel — under RT card, same left column */}
{showTasksPanel && simulationId && (
<C2TasksPanel simulationId={simulationId} />
)}
</div>{/* end left column */}
{/* SOC card */}
<form
id="soc-form"
@@ -543,6 +569,14 @@ export function SimulationFormPage(): JSX.Element {
onClose={() => setShowC2Modal(false)}
/>
)}
{showImportModal && simulationId && typeof engagementId === 'number' && (
<ImportC2HistoryModal
simulationId={simulationId}
engagementId={engagementId}
onClose={() => setShowImportModal(false)}
/>
)}
</div>
);
}