feat(frontend): sprint 2 — simulations UI + MITRE picker
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
11
frontend/src/hooks/useMitre.ts
Normal file
11
frontend/src/hooks/useMitre.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { searchMitreTechniques } from '@/api/mitre';
|
||||
|
||||
export function useMitreSearch(query: string, enabled: boolean) {
|
||||
return useQuery({
|
||||
queryKey: ['mitre', 'techniques', query],
|
||||
queryFn: () => searchMitreTechniques(query),
|
||||
enabled: enabled && query.trim().length > 0,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
}
|
||||
76
frontend/src/hooks/useSimulations.ts
Normal file
76
frontend/src/hooks/useSimulations.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
createSimulation,
|
||||
deleteSimulation,
|
||||
getSimulation,
|
||||
listSimulations,
|
||||
transitionSimulation,
|
||||
updateSimulation,
|
||||
} from '@/api/simulations';
|
||||
import type { SimulationCreateInput, SimulationPatchInput, SimulationStatus } from '@/api/types';
|
||||
|
||||
function simulationsKey(engagementId: number) {
|
||||
return ['engagements', engagementId, 'simulations'] as const;
|
||||
}
|
||||
|
||||
function simulationKey(id: number) {
|
||||
return ['simulations', id] as const;
|
||||
}
|
||||
|
||||
export function useEngagementSimulations(engagementId: number | undefined) {
|
||||
return useQuery({
|
||||
queryKey: engagementId ? simulationsKey(engagementId) : ['simulations', 'none'],
|
||||
queryFn: () => listSimulations(engagementId as number),
|
||||
enabled: typeof engagementId === 'number' && !Number.isNaN(engagementId),
|
||||
});
|
||||
}
|
||||
|
||||
export function useSimulation(id: number | undefined) {
|
||||
return useQuery({
|
||||
queryKey: id ? simulationKey(id) : ['simulations', 'none'],
|
||||
queryFn: () => getSimulation(id as number),
|
||||
enabled: typeof id === 'number' && !Number.isNaN(id),
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateSimulation(engagementId: number) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (input: SimulationCreateInput) => createSimulation(engagementId, input),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: simulationsKey(engagementId) }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateSimulation(id: number, engagementId: number) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (patch: SimulationPatchInput) => updateSimulation(id, patch),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: simulationKey(id) });
|
||||
qc.invalidateQueries({ queryKey: simulationsKey(engagementId) });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteSimulation(engagementId: number) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => deleteSimulation(id),
|
||||
onSuccess: (_data, id) => {
|
||||
qc.invalidateQueries({ queryKey: simulationKey(id) });
|
||||
qc.invalidateQueries({ queryKey: simulationsKey(engagementId) });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useTransitionSimulation(id: number, engagementId: number) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (to: Extract<SimulationStatus, 'review_required' | 'done'>) =>
|
||||
transitionSimulation(id, to),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: simulationKey(id) });
|
||||
qc.invalidateQueries({ queryKey: simulationsKey(engagementId) });
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user