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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user