feat(frontend): sprint 5 — templates CRUD pages + nav + picker modal + dropdown
- types.ts: SimulationTemplate, SimulationTemplateCreateInput, SimulationTemplatePatchInput, extend SimulationCreateInput with template_id - api/templates.ts: listTemplates, getTemplate, createTemplate, updateTemplate, deleteTemplate - hooks/useTemplates.ts: useTemplates, useTemplate, useCreateTemplate, useUpdateTemplate, useDeleteTemplate (TanStack Query, invalidates ["templates"]) - TemplatesListPage: /admin/templates — table (name, MITRE count, created by, updated), New/Edit/Delete actions, loading/error/empty states - TemplateFormPage: /admin/templates/new + /admin/templates/:id/edit — controlled form with inline MITRE field (picker + matrix modal), ConfirmDialog for delete - TemplatePickerModal: reusable modal listing templates with empty state (AC-27.6) - SimulationList: replace "New simulation" link with split-button dropdown (Blank → /simulations/new | From template… → TemplatePickerModal + POST template_id) - Layout: "Templates" nav link (admin | redteam, before "Users") - App.tsx: /admin/templates routes gated roles=["admin","redteam"] - 26 new Vitest tests (118 total, 92 original preserved) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
35
frontend/src/api/templates.ts
Normal file
35
frontend/src/api/templates.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { apiClient } from './client';
|
||||
import type {
|
||||
SimulationTemplate,
|
||||
SimulationTemplateCreateInput,
|
||||
SimulationTemplatePatchInput,
|
||||
} from './types';
|
||||
|
||||
export async function listTemplates(): Promise<SimulationTemplate[]> {
|
||||
const { data } = await apiClient.get<SimulationTemplate[]>('/simulation-templates');
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getTemplate(id: number): Promise<SimulationTemplate> {
|
||||
const { data } = await apiClient.get<SimulationTemplate>(`/simulation-templates/${id}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function createTemplate(
|
||||
input: SimulationTemplateCreateInput,
|
||||
): Promise<SimulationTemplate> {
|
||||
const { data } = await apiClient.post<SimulationTemplate>('/simulation-templates', input);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function updateTemplate(
|
||||
id: number,
|
||||
patch: SimulationTemplatePatchInput,
|
||||
): Promise<SimulationTemplate> {
|
||||
const { data } = await apiClient.patch<SimulationTemplate>(`/simulation-templates/${id}`, patch);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function deleteTemplate(id: number): Promise<void> {
|
||||
await apiClient.delete(`/simulation-templates/${id}`);
|
||||
}
|
||||
@@ -104,8 +104,40 @@ export interface Simulation {
|
||||
created_by: { id: number; username: string };
|
||||
}
|
||||
|
||||
export interface SimulationTemplate {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string | null;
|
||||
commands: string | null;
|
||||
prerequisites: string | null;
|
||||
techniques: MitreTechnique[];
|
||||
tactics: MitreTacticRef[];
|
||||
created_at: string;
|
||||
updated_at: string | null;
|
||||
created_by: { id: number; username: string };
|
||||
}
|
||||
|
||||
export interface SimulationTemplateCreateInput {
|
||||
name: string;
|
||||
description?: string | null;
|
||||
commands?: string | null;
|
||||
prerequisites?: string | null;
|
||||
technique_ids?: string[];
|
||||
tactic_ids?: string[];
|
||||
}
|
||||
|
||||
export interface SimulationTemplatePatchInput {
|
||||
name?: string;
|
||||
description?: string | null;
|
||||
commands?: string | null;
|
||||
prerequisites?: string | null;
|
||||
technique_ids?: string[];
|
||||
tactic_ids?: string[];
|
||||
}
|
||||
|
||||
export interface SimulationCreateInput {
|
||||
name: string;
|
||||
template_id?: number;
|
||||
}
|
||||
|
||||
export interface SimulationPatchInput {
|
||||
|
||||
Reference in New Issue
Block a user