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:
@@ -1,15 +1,134 @@
|
||||
import { Shield } from "lucide-react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Shield,
|
||||
CheckCircle,
|
||||
AlertTriangle,
|
||||
XCircle,
|
||||
Clock,
|
||||
HelpCircle,
|
||||
Percent,
|
||||
Loader2,
|
||||
AlertCircle,
|
||||
} from "lucide-react";
|
||||
import { getCoverageSummary, getCoverageByTactic } from "../api/metrics";
|
||||
import CoverageSummaryCard from "../components/CoverageSummaryCard";
|
||||
import TacticCoverageChart from "../components/TacticCoverageChart";
|
||||
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-2xl font-bold text-white">Dashboard</h1>
|
||||
<div className="flex items-center gap-3 rounded-xl border border-gray-800 bg-gray-900 p-6">
|
||||
<Shield className="h-8 w-8 text-cyan-400" />
|
||||
<p className="text-gray-300">
|
||||
Coverage metrics will appear here.
|
||||
const {
|
||||
data: summary,
|
||||
isLoading: summaryLoading,
|
||||
error: summaryError,
|
||||
} = useQuery({
|
||||
queryKey: ["metrics", "summary"],
|
||||
queryFn: getCoverageSummary,
|
||||
});
|
||||
|
||||
const {
|
||||
data: tactics,
|
||||
isLoading: tacticsLoading,
|
||||
error: tacticsError,
|
||||
} = useQuery({
|
||||
queryKey: ["metrics", "by-tactic"],
|
||||
queryFn: getCoverageByTactic,
|
||||
});
|
||||
|
||||
if (summaryLoading || tacticsLoading) {
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-cyan-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (summaryError || tacticsError) {
|
||||
return (
|
||||
<div className="flex h-64 flex-col items-center justify-center gap-2">
|
||||
<AlertCircle className="h-10 w-10 text-red-400" />
|
||||
<p className="text-red-400">Failed to load metrics</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
{summaryError?.message || tacticsError?.message || "Unknown error"}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">Dashboard</h1>
|
||||
<p className="mt-1 text-sm text-gray-400">
|
||||
MITRE ATT&CK coverage overview
|
||||
</p>
|
||||
</div>
|
||||
{summary && (
|
||||
<div className="flex items-center gap-2 rounded-lg border border-cyan-500/30 bg-cyan-500/10 px-4 py-2">
|
||||
<Percent className="h-5 w-5 text-cyan-400" />
|
||||
<span className="text-lg font-bold text-cyan-400">
|
||||
{summary.coverage_percentage.toFixed(1)}%
|
||||
</span>
|
||||
<span className="text-sm text-gray-400">Coverage</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
{summary && (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
|
||||
<CoverageSummaryCard
|
||||
title="Total Techniques"
|
||||
value={summary.total_techniques}
|
||||
icon={<Shield className="h-6 w-6 text-cyan-400" />}
|
||||
colorClass="text-cyan-400"
|
||||
bgClass="bg-gray-900"
|
||||
/>
|
||||
<CoverageSummaryCard
|
||||
title="Validated"
|
||||
value={summary.validated}
|
||||
total={summary.total_techniques}
|
||||
icon={<CheckCircle className="h-6 w-6 text-green-400" />}
|
||||
colorClass="text-green-400"
|
||||
bgClass="bg-green-950/20"
|
||||
/>
|
||||
<CoverageSummaryCard
|
||||
title="Partial"
|
||||
value={summary.partial}
|
||||
total={summary.total_techniques}
|
||||
icon={<AlertTriangle className="h-6 w-6 text-yellow-400" />}
|
||||
colorClass="text-yellow-400"
|
||||
bgClass="bg-yellow-950/20"
|
||||
/>
|
||||
<CoverageSummaryCard
|
||||
title="In Progress"
|
||||
value={summary.in_progress}
|
||||
total={summary.total_techniques}
|
||||
icon={<Clock className="h-6 w-6 text-blue-400" />}
|
||||
colorClass="text-blue-400"
|
||||
bgClass="bg-blue-950/20"
|
||||
/>
|
||||
<CoverageSummaryCard
|
||||
title="Not Covered"
|
||||
value={summary.not_covered}
|
||||
total={summary.total_techniques}
|
||||
icon={<XCircle className="h-6 w-6 text-red-400" />}
|
||||
colorClass="text-red-400"
|
||||
bgClass="bg-red-950/20"
|
||||
/>
|
||||
<CoverageSummaryCard
|
||||
title="Not Evaluated"
|
||||
value={summary.not_evaluated}
|
||||
total={summary.total_techniques}
|
||||
icon={<HelpCircle className="h-6 w-6 text-gray-400" />}
|
||||
colorClass="text-gray-400"
|
||||
bgClass="bg-gray-900"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tactic Coverage Table */}
|
||||
{tactics && <TacticCoverageChart data={tactics} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user