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:
@@ -3,11 +3,19 @@ import {
|
||||
deleteC2Config,
|
||||
executeC2,
|
||||
getC2Config,
|
||||
getC2Tasks,
|
||||
importC2,
|
||||
listCallbackHistory,
|
||||
listCallbacks,
|
||||
putC2Config,
|
||||
testC2Config,
|
||||
} from '@/api/c2';
|
||||
import type { C2ConfigInput, C2ExecuteInput } from '@/api/types';
|
||||
import type {
|
||||
C2ConfigInput,
|
||||
C2ExecuteInput,
|
||||
C2ImportInput,
|
||||
C2TasksResponse,
|
||||
} from '@/api/types';
|
||||
|
||||
function c2ConfigKey(engagementId: number) {
|
||||
return ['c2-config', engagementId] as const;
|
||||
@@ -17,6 +25,14 @@ function c2CallbacksKey(engagementId: number) {
|
||||
return ['c2-callbacks', engagementId] as const;
|
||||
}
|
||||
|
||||
function c2TasksKey(simulationId: number) {
|
||||
return ['c2-tasks', simulationId] as const;
|
||||
}
|
||||
|
||||
function c2HistoryKey(engagementId: number, callbackDisplayId: number, page: number, pageSize: number) {
|
||||
return ['c2-history', engagementId, callbackDisplayId, page, pageSize] as const;
|
||||
}
|
||||
|
||||
function simulationKey(id: number) {
|
||||
return ['simulations', id] as const;
|
||||
}
|
||||
@@ -69,6 +85,22 @@ export function useC2Callbacks(engagementId: number | undefined, options?: { ena
|
||||
});
|
||||
}
|
||||
|
||||
export function useC2Tasks(simulationId: number | undefined, options?: { enabled?: boolean }) {
|
||||
const enabled =
|
||||
typeof simulationId === 'number' &&
|
||||
!Number.isNaN(simulationId) &&
|
||||
(options?.enabled !== false);
|
||||
|
||||
return useQuery({
|
||||
queryKey: simulationId ? c2TasksKey(simulationId) : ['c2-tasks', 'none'],
|
||||
queryFn: () => getC2Tasks(simulationId as number),
|
||||
enabled,
|
||||
// Poll every 2500 ms while any task is incomplete; stop when all done.
|
||||
refetchInterval: (query: { state: { data?: C2TasksResponse } }) =>
|
||||
query.state.data?.tasks?.some((t) => !t.completed) ? 2500 : false,
|
||||
});
|
||||
}
|
||||
|
||||
export function useExecuteC2(simulationId: number, engagementId: number) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
@@ -76,6 +108,45 @@ export function useExecuteC2(simulationId: number, engagementId: number) {
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: simulationKey(simulationId) });
|
||||
qc.invalidateQueries({ queryKey: ['engagements', engagementId, 'simulations'] });
|
||||
qc.invalidateQueries({ queryKey: c2TasksKey(simulationId) });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useC2CallbackHistory(
|
||||
engagementId: number | undefined,
|
||||
callbackDisplayId: number | null,
|
||||
options?: { page?: number; pageSize?: number; enabled?: boolean },
|
||||
) {
|
||||
const page = options?.page ?? 1;
|
||||
const pageSize = options?.pageSize ?? 25;
|
||||
const enabled =
|
||||
typeof engagementId === 'number' &&
|
||||
!Number.isNaN(engagementId) &&
|
||||
callbackDisplayId !== null &&
|
||||
(options?.enabled !== false);
|
||||
|
||||
return useQuery({
|
||||
queryKey:
|
||||
engagementId && callbackDisplayId !== null
|
||||
? c2HistoryKey(engagementId, callbackDisplayId, page, pageSize)
|
||||
: ['c2-history', 'none'],
|
||||
queryFn: () =>
|
||||
listCallbackHistory(engagementId as number, callbackDisplayId as number, {
|
||||
page,
|
||||
pageSize,
|
||||
}),
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
|
||||
export function useImportC2(simulationId: number) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (input: C2ImportInput) => importC2(simulationId, input),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: c2TasksKey(simulationId) });
|
||||
qc.invalidateQueries({ queryKey: simulationKey(simulationId) });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user