fix(m4): typed MitreSyncResult interface — drop the as cast
Mirrors the backend Pydantic `SyncResultOut` in TS so the mutation result is
properly typed end-to-end. `(res as { duration_ms: number })` cast removed
from MitrePage.tsx; `apiPost<MitreSyncResult>` carries the contract.
Also annotated the unused query-key factories in mitre.ts so the next reader
knows they're parked for M5 template-form consumption (not dead).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -51,6 +51,10 @@ export interface MitreTag {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Query keys. `status` + `matrix` drive the M4 picker; the per-list factories
|
||||||
|
// (`tactics`/`techniques`/`subtechniques`) are unused today but the M5
|
||||||
|
// template forms will consume them for the standalone REST endpoints when
|
||||||
|
// users edit a single test's tags inline.
|
||||||
export const mitreKeys = {
|
export const mitreKeys = {
|
||||||
status: ['mitre', 'status'] as const,
|
status: ['mitre', 'status'] as const,
|
||||||
matrix: ['mitre', 'matrix'] as const,
|
matrix: ['mitre', 'matrix'] as const,
|
||||||
@@ -85,3 +89,17 @@ export interface MatrixTactic {
|
|||||||
export interface MitreMatrix {
|
export interface MitreMatrix {
|
||||||
tactics: MatrixTactic[];
|
tactics: MatrixTactic[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Mirror of backend `SyncResultOut` (`api/mitre.py`). */
|
||||||
|
export interface MitreSyncResult {
|
||||||
|
tactics_upserted: number;
|
||||||
|
techniques_upserted: number;
|
||||||
|
subtechniques_upserted: number;
|
||||||
|
subtechniques_skipped_orphan: number;
|
||||||
|
technique_tactic_links: number;
|
||||||
|
version: string | null;
|
||||||
|
source: string;
|
||||||
|
started_at: string;
|
||||||
|
finished_at: string;
|
||||||
|
duration_ms: number;
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,12 @@ import { SectionHeader } from '@/components/ui/SectionHeader';
|
|||||||
import { Tag } from '@/components/ui/Tag';
|
import { Tag } from '@/components/ui/Tag';
|
||||||
import { ApiError, apiGet, apiPost } from '@/lib/api';
|
import { ApiError, apiGet, apiPost } from '@/lib/api';
|
||||||
import { useAuth } from '@/lib/auth';
|
import { useAuth } from '@/lib/auth';
|
||||||
import { mitreKeys, type MitreStatus, type MitreTag } from '@/lib/mitre';
|
import {
|
||||||
|
mitreKeys,
|
||||||
|
type MitreStatus,
|
||||||
|
type MitreSyncResult,
|
||||||
|
type MitreTag,
|
||||||
|
} from '@/lib/mitre';
|
||||||
|
|
||||||
export function MitrePage() {
|
export function MitrePage() {
|
||||||
const { state } = useAuth();
|
const { state } = useAuth();
|
||||||
@@ -24,14 +29,14 @@ export function MitrePage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const sync = useMutation({
|
const sync = useMutation({
|
||||||
mutationFn: () => apiPost<Record<string, unknown>>('/mitre/sync'),
|
mutationFn: () => apiPost<MitreSyncResult>('/mitre/sync'),
|
||||||
onMutate: () => {
|
onMutate: () => {
|
||||||
setSyncResult(null);
|
setSyncResult(null);
|
||||||
setSyncError(null);
|
setSyncError(null);
|
||||||
},
|
},
|
||||||
onSuccess: async (res) => {
|
onSuccess: async (res) => {
|
||||||
const counts = `${res.tactics_upserted} tactics, ${res.techniques_upserted} techniques, ${res.subtechniques_upserted} subtechniques`;
|
const counts = `${res.tactics_upserted} tactics, ${res.techniques_upserted} techniques, ${res.subtechniques_upserted} subtechniques`;
|
||||||
setSyncResult(`Sync completed in ${(res as { duration_ms: number }).duration_ms / 1000}s — ${counts}.`);
|
setSyncResult(`Sync completed in ${(res.duration_ms / 1000).toFixed(1)}s — ${counts}.`);
|
||||||
await qc.invalidateQueries({ queryKey: ['mitre'] });
|
await qc.invalidateQueries({ queryKey: ['mitre'] });
|
||||||
},
|
},
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user