import { useState, useMemo } from "react"; import { useQuery } from "@tanstack/react-query"; import { useNavigate } from "react-router-dom"; import { Loader2, AlertCircle, Plus, Filter, ListChecks, Clock, CheckCircle, XCircle, Eye, Play, Shield, Search, } from "lucide-react"; import { getTests, type TestListFilters } from "../api/tests"; import type { Test, TestState } from "../types/models"; import { useAuth } from "../context/AuthContext"; /* ── Badge colour map ──────────────────────────────────────────────── */ const testStateBadgeColors: Record = { draft: "bg-gray-800/50 text-gray-400 border-gray-600/30", red_executing: "bg-orange-900/50 text-orange-400 border-orange-500/30", blue_evaluating: "bg-indigo-900/50 text-indigo-400 border-indigo-500/30", in_review: "bg-blue-900/50 text-blue-400 border-blue-500/30", validated: "bg-green-900/50 text-green-400 border-green-500/30", rejected: "bg-red-900/50 text-red-400 border-red-500/30", }; const testStateLabels: Record = { draft: "Draft", red_executing: "Red Executing", blue_evaluating: "Blue Evaluating", in_review: "In Review", validated: "Validated", rejected: "Rejected", }; const ALL_STATES: TestState[] = [ "draft", "red_executing", "blue_evaluating", "in_review", "validated", "rejected", ]; /* ── Helper: which team "owns" the current state ────────────────────── */ function currentTeamForState(state: TestState): string { switch (state) { case "draft": case "red_executing": return "Red Team"; case "blue_evaluating": return "Blue Team"; case "in_review": return "Managers"; case "validated": return "-"; case "rejected": return "Red Team"; default: return "-"; } } /* ── Component ──────────────────────────────────────────────────────── */ export default function TestsPage() { const navigate = useNavigate(); const { user } = useAuth(); const canCreate = user?.role === "admin" || user?.role === "red_lead" || user?.role === "blue_lead"; // ── Filter state ────────────────────────────────────────────────── const [stateFilter, setStateFilter] = useState(""); const [platformFilter, setPlatformFilter] = useState(""); const [searchText, setSearchText] = useState(""); const [showMyTasks, setShowMyTasks] = useState(false); // Build API filters const filters = useMemo(() => { const f: TestListFilters = { limit: 200 }; if (showMyTasks && user) { // Role-specific "my tasks" filtering switch (user.role) { case "red_tech": // Tests I created in draft or red_executing f.created_by = user.id; if (!stateFilter) { // Client-side filter for draft + red_executing } else { f.state = stateFilter as TestState; } break; case "blue_tech": f.state = "blue_evaluating"; break; case "red_lead": f.pending_validation_side = "red"; break; case "blue_lead": f.pending_validation_side = "blue"; break; default: // admin: show all if (stateFilter) f.state = stateFilter as TestState; break; } } else { if (stateFilter) f.state = stateFilter as TestState; } if (platformFilter) f.platform = platformFilter; return f; }, [stateFilter, platformFilter, showMyTasks, user]); const { data: allTests, isLoading, error, } = useQuery({ queryKey: ["tests", filters], queryFn: () => getTests(filters), }); // Client-side filtering for search text and "my tasks" for red_tech const tests = useMemo(() => { if (!allTests) return []; let filtered = allTests; // Red tech "my tasks" — client-side filter for draft + red_executing if (showMyTasks && user?.role === "red_tech" && !stateFilter) { filtered = filtered.filter( (t) => t.state === "draft" || t.state === "red_executing" ); } // Search text if (searchText.trim()) { const q = searchText.toLowerCase(); filtered = filtered.filter( (t) => t.name.toLowerCase().includes(q) || (t.technique_mitre_id && t.technique_mitre_id.toLowerCase().includes(q)) || (t.technique_name && t.technique_name.toLowerCase().includes(q)) ); } return filtered; }, [allTests, searchText, showMyTasks, user, stateFilter]); // ── State counters ──────────────────────────────────────────────── // Count from allTests (before client search filter) to show accurate pipeline const stateCounts = useMemo(() => { const counts: Record = {}; for (const s of ALL_STATES) counts[s] = 0; if (allTests) { for (const t of allTests) { counts[t.state] = (counts[t.state] || 0) + 1; } } return counts; }, [allTests]); // Count from unfiltered query for the top cards const { data: allTestsUnfiltered, } = useQuery({ queryKey: ["tests", "unfiltered-counts"], queryFn: () => getTests({ limit: 200 }), }); const globalCounts = useMemo(() => { const counts: Record = {}; for (const s of ALL_STATES) counts[s] = 0; if (allTestsUnfiltered) { for (const t of allTestsUnfiltered) { counts[t.state] = (counts[t.state] || 0) + 1; } } return counts; }, [allTestsUnfiltered]); const totalTests = allTestsUnfiltered?.length || 0; // ── Formatting helpers ───────────────────────────────────────────── const formatDate = (dateStr: string | null) => { if (!dateStr) return "-"; return new Date(dateStr).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", }); }; // ── My tasks label ──────────────────────────────────────────────── const myTasksLabel = useMemo(() => { if (!user) return "My Tasks"; switch (user.role) { case "red_tech": return "My Tests (Draft / Executing)"; case "blue_tech": return "Pending Blue Evaluation"; case "red_lead": return "Pending Red Validation"; case "blue_lead": return "Pending Blue Validation"; default: return "My Tasks"; } }, [user]); // ── Render ───────────────────────────────────────────────────────── if (isLoading) { return (
); } if (error) { return (

Failed to load tests

); } return (
{/* Header */}

Tests

Security tests for technique validation — Red/Blue workflow

{canCreate && ( )}
{/* ── State Counter Cards ───────────────────────────────────────── */}
{ALL_STATES.map((state) => { const icons: Record = { draft: , red_executing: , blue_evaluating: , in_review: , validated: , rejected: , }; const colorMap: Record = { draft: "text-gray-400", red_executing: "text-orange-400", blue_evaluating: "text-indigo-400", in_review: "text-blue-400", validated: "text-green-400", rejected: "text-red-400", }; return ( ); })}
{/* ── Filters Bar ───────────────────────────────────────────────── */}
{/* My tasks toggle */} {user?.role !== "admin" && user?.role !== "viewer" && ( )} {/* State filter */}
{/* Platform filter */} setPlatformFilter(e.target.value)} placeholder="Platform..." className="rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 placeholder-gray-500 focus:border-cyan-500 focus:outline-none w-32" /> {/* Search */}
setSearchText(e.target.value)} placeholder="Search by name or technique..." className="w-full rounded-lg border border-gray-700 bg-gray-800 pl-9 pr-3 py-2 text-sm text-gray-300 placeholder-gray-500 focus:border-cyan-500 focus:outline-none" />
{/* Clear filters */} {(stateFilter || platformFilter || searchText || showMyTasks) && ( )}
{/* Active filter summary */} {(stateFilter || showMyTasks) && (
Showing: {showMyTasks && ( {myTasksLabel} )} {stateFilter && ( {testStateLabels[stateFilter as TestState]} )} ({tests.length} of {totalTests} tests)
)}
{/* ── Tests Table ───────────────────────────────────────────────── */}

{showMyTasks ? myTasksLabel : "All Tests"}

{tests.length} tests
{tests.map((test: Test) => ( navigate(`/tests/${test.id}`)} > ))}
Name Technique State Current Team Platform Updated Action
{test.name} {test.technique_mitre_id ? (
{test.technique_mitre_id} {test.technique_name}
) : ( - )}
{testStateLabels[test.state]} {currentTeamForState(test.state)} {test.platform || "-"} {formatDate(test.created_at)}
{tests.length === 0 && (
{showMyTasks ? "No pending tasks for your role." : "No tests found matching your filters."}
)}
); }