import { useState, useMemo } from "react"; import { useNavigate } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import { Loader2, AlertCircle, ArrowUp, ArrowDown, Minus, GitCompareArrows, Camera, TrendingUp, TrendingDown, } from "lucide-react"; import { listSnapshots, compareSnapshots, type SnapshotSummary, type SnapshotComparison, } from "../api/snapshots"; type Tab = "improved" | "worsened" | "unchanged"; const statusColors: Record = { validated: "text-green-400", partial: "text-yellow-400", not_covered: "text-red-400", in_progress: "text-blue-400", not_evaluated: "text-gray-500", }; const statusDots: Record = { validated: "bg-green-400", partial: "bg-yellow-400", not_covered: "bg-red-400", in_progress: "bg-blue-400", not_evaluated: "bg-gray-500", }; function StatusBadge({ status }: { status: string }) { return ( {status.replace(/_/g, " ")} ); } function DeltaArrow({ delta }: { delta: number }) { if (delta > 0) return ; if (delta < 0) return ; return ; } function MetricCard({ label, valueA, valueB, suffix, }: { label: string; valueA: number; valueB: number; suffix?: string; }) { const delta = valueB - valueA; const deltaColor = delta > 0 ? "text-green-400" : delta < 0 ? "text-red-400" : "text-gray-500"; return (
{label}
{valueB} {suffix} {delta !== 0 && ( {delta > 0 ? "+" : ""} {delta} )}
); } export default function ComparisonPage() { const navigate = useNavigate(); const [snapA, setSnapA] = useState(""); const [snapB, setSnapB] = useState(""); const [activeTab, setActiveTab] = useState("improved"); // Fetch all snapshots for the dropdowns const { data: snapshotsData, isLoading: isLoadingSnapshots } = useQuery({ queryKey: ["snapshots", "all"], queryFn: () => listSnapshots({ limit: 200 }), }); const snapshots = snapshotsData?.items || []; // Comparison query const { data: comparison, isLoading: isComparing, error: compareError, } = useQuery({ queryKey: ["snapshot-compare", snapA, snapB], queryFn: () => compareSnapshots(snapA, snapB), enabled: !!snapA && !!snapB && snapA !== snapB, }); const formatDate = (dateStr: string | null) => { if (!dateStr) return "—"; return new Date(dateStr).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); }; // For the "unchanged" tab, we don't get individual rows from the API, // just a count, so we show the count. const tabData = useMemo(() => { if (!comparison) return { improved: [], worsened: [], unchanged_count: 0 }; return { improved: comparison.improved, worsened: comparison.worsened, unchanged_count: comparison.unchanged_count, }; }, [comparison]); return (
{/* Header */}

Temporal Comparison

Compare coverage snapshots over time

{/* Snapshot selectors */}
{/* Snapshot A */}
{/* Snapshot B */}
{isLoadingSnapshots && (
Loading snapshots...
)}
{/* Loading / Error */} {isComparing && (
)} {compareError && (
Failed to compare snapshots
)} {/* Comparison results */} {comparison && ( <> {/* Side-by-side score cards */}
{/* Snapshot A card */}
{comparison.snapshot_a.name || "Snapshot A"} {formatDate(comparison.snapshot_a.created_at)}
{comparison.snapshot_a.organization_score}
{/* Snapshot B card */}
{comparison.snapshot_b.name || "Snapshot B"} {formatDate(comparison.snapshot_b.created_at)}
{comparison.snapshot_b.organization_score} {comparison.score_delta !== 0 && ( 0 ? "text-green-400" : "text-red-400" }`} > {comparison.score_delta > 0 ? ( ) : ( )} {comparison.score_delta > 0 ? "+" : ""} {comparison.score_delta} )}
{/* Tabs */}
{activeTab === "unchanged" ? (

{comparison.unchanged_count} techniques unchanged

These techniques had the same status and score in both snapshots.

) : ( <> {(activeTab === "improved" ? tabData.improved : tabData.worsened ).length === 0 ? (

No techniques {activeTab} between snapshots.

) : (
{(activeTab === "improved" ? tabData.improved : tabData.worsened ).map((item) => ( navigate(`/techniques/${item.mitre_id}`) } > ))}
MITRE ID Before After Score Before Score After Delta
{item.mitre_id} {item.old_score} {item.new_score} item.old_score ? "text-green-400" : item.new_score < item.old_score ? "text-red-400" : "text-gray-500" }`} > {item.new_score > item.old_score ? "+" : ""} {Math.round((item.new_score - item.old_score) * 10) / 10}
)} )}
)} {/* No selection prompt */} {!comparison && !isComparing && !compareError && (

Select two snapshots to compare

Choose a baseline and current snapshot from the dropdowns above.

)}
); }