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) <noreply@anthropic.com>
This commit is contained in:
@@ -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')}
|
||||
</>
|
||||
)}
|
||||
<span className="font-mono text-2xs text-text-dim ml-2" data-testid="me-email">
|
||||
@@ -69,7 +71,7 @@ export function Layout() {
|
||||
<Outlet />
|
||||
|
||||
<footer className="mt-[60px] py-8 border-t border-border text-center font-mono text-xs text-text-dim">
|
||||
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
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
45
frontend/src/components/MarkdownField.tsx
Normal file
45
frontend/src/components/MarkdownField.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useId, type TextareaHTMLAttributes } from 'react';
|
||||
|
||||
import { cn } from '@/lib/cn';
|
||||
|
||||
interface MarkdownFieldProps extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, '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 (
|
||||
<div className="block">
|
||||
<label
|
||||
htmlFor={inputId}
|
||||
className="block font-mono text-3xs font-semibold uppercase tracking-wider2 text-text-dim"
|
||||
>
|
||||
{label} <span className="text-text-dim/60">· markdown</span>
|
||||
</label>
|
||||
<textarea
|
||||
id={inputId}
|
||||
rows={rows}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className={cn(
|
||||
'mt-1 w-full rounded-md border border-border bg-bg-card px-3 py-2 font-mono text-xs text-text-bright placeholder:text-text-dim',
|
||||
'focus:border-cyan focus:outline-none',
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
{hint && <p className="mt-1 font-mono text-2xs text-text-dim">{hint}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user