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,97 @@
import { FileIcon, Download, ExternalLink, Copy, Check } from "lucide-react";
import { useState } from "react";
interface Evidence {
id: string;
test_id: string;
file_name: string;
sha256_hash: string;
uploaded_by: string | null;
uploaded_at: string;
download_url?: string;
}
interface EvidenceListProps {
evidences: Evidence[];
onDownload: (evidenceId: string) => void;
}
export default function EvidenceList({ evidences, onDownload }: EvidenceListProps) {
const [copiedHash, setCopiedHash] = useState<string | null>(null);
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
};
const copyHash = async (hash: string) => {
await navigator.clipboard.writeText(hash);
setCopiedHash(hash);
setTimeout(() => setCopiedHash(null), 2000);
};
if (evidences.length === 0) {
return (
<div className="rounded-lg border border-gray-800 bg-gray-800/30 p-6 text-center">
<FileIcon className="mx-auto h-10 w-10 text-gray-600" />
<p className="mt-2 text-sm text-gray-400">No evidence files uploaded yet</p>
</div>
);
}
return (
<div className="space-y-3">
{evidences.map((evidence) => (
<div
key={evidence.id}
className="rounded-lg border border-gray-800 bg-gray-800/30 p-4 transition-colors hover:bg-gray-800/50"
>
<div className="flex items-start justify-between">
<div className="flex items-start gap-3">
<div className="rounded-lg bg-gray-700 p-2">
<FileIcon className="h-5 w-5 text-gray-400" />
</div>
<div>
<p className="font-medium text-gray-200">{evidence.file_name}</p>
<p className="mt-0.5 text-xs text-gray-500">
Uploaded {formatDate(evidence.uploaded_at)}
</p>
</div>
</div>
<button
onClick={() => onDownload(evidence.id)}
className="flex items-center gap-1.5 rounded-lg border border-gray-700 bg-gray-800 px-3 py-1.5 text-sm text-gray-300 hover:border-cyan-500/50 hover:text-cyan-400 transition-colors"
>
<Download className="h-4 w-4" />
Download
</button>
</div>
{/* SHA256 Hash */}
<div className="mt-3 flex items-center gap-2">
<span className="text-xs font-medium text-gray-500">SHA256:</span>
<code className="flex-1 truncate rounded bg-gray-900 px-2 py-1 font-mono text-xs text-gray-400">
{evidence.sha256_hash}
</code>
<button
onClick={() => copyHash(evidence.sha256_hash)}
className="rounded p-1 text-gray-500 hover:bg-gray-700 hover:text-gray-300"
title="Copy hash"
>
{copiedHash === evidence.sha256_hash ? (
<Check className="h-4 w-4 text-green-400" />
) : (
<Copy className="h-4 w-4" />
)}
</button>
</div>
</div>
))}
</div>
);
}