feat: Phase 8 - Frontend main views (T-026 to T-031)

Implement all main frontend views for the MITRE ATT&CK coverage platform:

- T-026: Dashboard with coverage summary cards and tactic breakdown table

- T-027: Interactive ATT&CK matrix with filtering by status, tactic, platform

- T-028: Technique detail page with tests, intel items, and review actions

- T-029: Test creation form with technique selector and validation

- T-030: Test detail page with drag and drop evidence upload and download

- T-031: System admin panel with MITRE sync and intel scan controls

New components: CoverageSummaryCard, TacticCoverageChart, AttackMatrix, TechniqueCell, TestForm, EvidenceUpload, EvidenceList

New API modules: metrics.ts, techniques.ts, tests.ts, evidence.ts, system.ts

All views use TanStack Query for data fetching with proper loading and error states. Role-based UI controls for admin/lead actions.
This commit is contained in:
2026-02-06 16:21:14 +01:00
parent 591b5df250
commit cb447f3803
22 changed files with 3092 additions and 27 deletions

View File

@@ -0,0 +1,109 @@
import { useNavigate, useSearchParams } from "react-router-dom";
import { useMutation, useQueryClient, useQuery } from "@tanstack/react-query";
import { ArrowLeft, FlaskConical } from "lucide-react";
import TestForm, { type TestFormData } from "../components/TestForm";
import { createTest } from "../api/tests";
import { getTechniqueByMitreId } from "../api/techniques";
export default function TestCreatePage() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const [searchParams] = useSearchParams();
// Get technique ID from URL query param (UUID format)
const techniqueId = searchParams.get("technique");
// If we have a technique ID, try to get its mitre_id for the back link
const { data: technique } = useQuery({
queryKey: ["techniqueById", techniqueId],
queryFn: async () => {
// We need to find the mitre_id from the technique list
// This is a workaround since we get UUID but need mitre_id
const response = await fetch(`http://localhost:8000/api/v1/techniques`, {
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
});
const techniques = await response.json();
return techniques.find((t: { id: string }) => t.id === techniqueId);
},
enabled: !!techniqueId,
});
const createMutation = useMutation({
mutationFn: createTest,
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ["techniques"] });
queryClient.invalidateQueries({ queryKey: ["technique"] });
// Navigate back to the technique detail if we came from there
if (technique?.mitre_id) {
navigate(`/techniques/${technique.mitre_id}`);
} else {
navigate("/tests");
}
},
});
const handleSubmit = (data: TestFormData) => {
createMutation.mutate({
technique_id: data.technique_id,
name: data.name,
description: data.description || undefined,
platform: data.platform || undefined,
procedure_text: data.procedure_text || undefined,
tool_used: data.tool_used || undefined,
});
};
const handleBack = () => {
if (technique?.mitre_id) {
navigate(`/techniques/${technique.mitre_id}`);
} else {
navigate("/tests");
}
};
return (
<div className="space-y-6">
{/* Back button */}
<button
onClick={handleBack}
className="flex items-center gap-1 text-sm text-gray-400 hover:text-cyan-400 transition-colors"
>
<ArrowLeft className="h-4 w-4" />
{technique ? `Back to ${technique.mitre_id}` : "Back to tests"}
</button>
{/* Header */}
<div className="flex items-center gap-4">
<div className="rounded-lg bg-cyan-500/10 p-3">
<FlaskConical className="h-8 w-8 text-cyan-400" />
</div>
<div>
<h1 className="text-2xl font-bold text-white">Create New Test</h1>
<p className="mt-1 text-sm text-gray-400">
Document a security test for technique validation
</p>
</div>
</div>
{/* Error display */}
{createMutation.isError && (
<div className="rounded-lg border border-red-500/30 bg-red-900/20 p-4">
<p className="text-sm text-red-400">
Failed to create test: {(createMutation.error as Error)?.message || "Unknown error"}
</p>
</div>
)}
{/* Form */}
<div className="rounded-xl border border-gray-800 bg-gray-900 p-6">
<TestForm
preselectedTechniqueId={techniqueId || undefined}
onSubmit={handleSubmit}
isSubmitting={createMutation.isPending}
/>
</div>
</div>
);
}