From 2781ce411755a46dd3074a08b8f46e96a2eaa553 Mon Sep 17 00:00:00 2001 From: Knacky Date: Tue, 12 May 2026 19:57:41 +0200 Subject: [PATCH] feat(m5): admin SPA pages for the template catalogue - AdminTestsPage with filters (q, tactic, opsec, tag), modal-based CRUD, markdown textareas for procedure/result/detection, embedded MitreTagPicker for tagging. - AdminScenariosPage with @dnd-kit/sortable drag-and-drop on the ordered test list, two-step save (PATCH metadata + PUT tests), catalogue picker excluding soft-deleted items. - lib/templates.ts typed client + queryKey factory. - MarkdownField helper (textarea with markdown hint label). - Layout adds Tests + Scenarios admin nav links; App.tsx routes both behind RequireAdmin. Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/package.json | 3 + frontend/src/App.tsx | 18 + frontend/src/components/Layout.tsx | 4 +- frontend/src/components/MarkdownField.tsx | 45 +++ frontend/src/lib/templates.ts | 136 +++++++ frontend/src/pages/AdminScenariosPage.tsx | 434 ++++++++++++++++++++++ frontend/src/pages/AdminTestsPage.tsx | 403 ++++++++++++++++++++ 7 files changed, 1042 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/MarkdownField.tsx create mode 100644 frontend/src/lib/templates.ts create mode 100644 frontend/src/pages/AdminScenariosPage.tsx create mode 100644 frontend/src/pages/AdminTestsPage.tsx diff --git a/frontend/package.json b/frontend/package.json index 1f7e91f..2da9cbc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,9 @@ "format:check": "prettier --check \"src/**/*.{ts,tsx,css,json,html}\"" }, "dependencies": { + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/sortable": "^8.0.0", + "@dnd-kit/utilities": "^3.2.2", "@fontsource/ibm-plex-sans": "^5.0.20", "@fontsource/jetbrains-mono": "^5.0.20", "@tanstack/react-query": "^5.51.0", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index db48353..44f8731 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,6 +6,8 @@ import { RequireAdmin } from '@/components/RequireAdmin'; import { RequireAuth } from '@/components/RequireAuth'; import { AdminGroupsPage } from '@/pages/AdminGroupsPage'; import { AdminInvitationsPage } from '@/pages/AdminInvitationsPage'; +import { AdminScenariosPage } from '@/pages/AdminScenariosPage'; +import { AdminTestsPage } from '@/pages/AdminTestsPage'; import { AdminUsersPage } from '@/pages/AdminUsersPage'; import { HomePage } from '@/pages/HomePage'; import { MitrePage } from '@/pages/MitrePage'; @@ -82,6 +84,22 @@ function App() { } /> + + + + } + /> + + + + } + /> } /> diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 0eab999..a9b269b 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -42,6 +42,8 @@ export function Layout() { {navItem('/admin/users', 'Users')} {navItem('/admin/groups', 'Groups')} {navItem('/admin/invitations', 'Invitations')} + {navItem('/admin/tests', 'Tests')} + {navItem('/admin/scenarios', 'Scenarios')} )} @@ -69,7 +71,7 @@ export function Layout() {
- metamorph · M0 bootstrap · M1 db schema · M2 auth · M3 rbac · M4 mitre · design system from tasks/design.md + metamorph · M0 bootstrap · M1 db schema · M2 auth · M3 rbac · M4 mitre · M5 templates · design system from tasks/design.md
diff --git a/frontend/src/components/MarkdownField.tsx b/frontend/src/components/MarkdownField.tsx new file mode 100644 index 0000000..635793a --- /dev/null +++ b/frontend/src/components/MarkdownField.tsx @@ -0,0 +1,45 @@ +import { useId, type TextareaHTMLAttributes } from 'react'; + +import { cn } from '@/lib/cn'; + +interface MarkdownFieldProps extends Omit, 'value' | 'onChange'> { + label: string; + value: string; + onChange: (next: string) => void; + rows?: number; + hint?: string; +} + +/** + * Markdown-content textarea. We deliberately keep it textarea-only (no fancy + * WYSIWYG editor) — markdown lives well in plain text and the saved blob is + * rendered to HTML at display time (M6/M7 mission pages). The label exposes + * "markdown" so the user knows the field accepts MD syntax. + */ +export function MarkdownField({ label, value, onChange, rows = 6, hint, id, className, ...rest }: MarkdownFieldProps) { + const fallbackId = useId(); + const inputId = id ?? fallbackId; + return ( +
+ +