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, ChevronUp, ChevronDown, ChevronsUpDown, Timer, } 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 "-"; } } /* ── Helper: elapsed time since a date ─────────────────────────────── */ function formatElapsed(dateStr: string | null | undefined): string { if (!dateStr) return "—"; const utc = dateStr.endsWith("Z") || dateStr.includes("+") ? dateStr : dateStr + "Z"; const ms = Date.now() - new Date(utc).getTime(); const minutes = Math.floor(ms / 60000); if (minutes < 1) return "<1m"; if (minutes < 60) return `${minutes}m`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ${minutes % 60}m`; const days = Math.floor(hours / 24); return `${days}d ${hours % 24}h`; } /* ── 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); // ── Sort state ──────────────────────────────────────────────────── type SortKey = | "name" | "technique" | "state" | "team" | "platform" | "created_at" | "updated_at" | "waiting_time"; const [sortKey, setSortKey] = useState("created_at"); const [sortDir, setSortDir] = useState<"asc" | "desc">("desc"); const handleSort = (key: SortKey) => { if (sortKey === key) { setSortDir((d) => (d === "asc" ? "desc" : "asc")); } else { setSortKey(key); setSortDir(key === "waiting_time" ? "asc" : "asc"); } }; // Build API filters const filters = useMemo(() => { const f: TestListFilters = { limit: 200 }; if (showMyTasks && user) { switch (user.role) { case "red_tech": f.created_by = user.id; if (stateFilter) 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: 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 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" ); } // Exclude validated from this table — they live in /tests/validated if (stateFilter !== "validated") { filtered = filtered.filter((t) => t.state !== "validated"); } // 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)) ); } // Sort filtered = [...filtered].sort((a, b) => { let av = ""; let bv = ""; switch (sortKey) { case "name": av = a.name.toLowerCase(); bv = b.name.toLowerCase(); break; case "technique": av = (a.technique_mitre_id || "").toLowerCase(); bv = (b.technique_mitre_id || "").toLowerCase(); break; case "state": av = a.state; bv = b.state; break; case "team": av = currentTeamForState(a.state); bv = currentTeamForState(b.state); break; case "platform": av = (a.platform || "").toLowerCase(); bv = (b.platform || "").toLowerCase(); break; case "created_at": av = a.created_at || ""; bv = b.created_at || ""; break; case "updated_at": av = a.updated_at || ""; bv = b.updated_at || ""; break; case "waiting_time": { const aIsBlue = a.state === "blue_evaluating"; const bIsBlue = b.state === "blue_evaluating"; if (aIsBlue && bIsBlue) { av = a.updated_at || ""; bv = b.updated_at || ""; } else { if (aIsBlue && !bIsBlue) return sortDir === "asc" ? -1 : 1; if (!aIsBlue && bIsBlue) return sortDir === "asc" ? 1 : -1; av = a.updated_at || ""; bv = b.updated_at || ""; } break; } } const cmp = av < bv ? -1 : av > bv ? 1 : 0; return sortDir === "asc" ? cmp : -cmp; }); return filtered; }, [allTests, searchText, showMyTasks, user, stateFilter, sortKey, sortDir]); // ── State counters ──────────────────────────────────────────────── 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 | undefined) => { if (!dateStr) return "-"; const utc = dateStr.endsWith("Z") || dateStr.includes("+") ? dateStr : dateStr + "Z"; return new Date(utc).toLocaleDateString("es-ES", { 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

); } const mainTableColumns: { key: SortKey; label: string; cls: string }[] = [ { key: "name", label: "Name", cls: "pr-4" }, { key: "technique", label: "Technique", cls: "px-4" }, { key: "state", label: "State", cls: "px-4" }, { key: "team", label: "Current Team", cls: "px-4" }, { key: "platform", label: "Platform", cls: "px-4" }, { key: "waiting_time", label: "Waiting", cls: "px-4" }, { key: "created_at", label: "Created", cls: "px-4" }, { key: "updated_at", label: "Updated", cls: "px-4" }, ]; 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
{mainTableColumns.map(({ key, label, cls }) => ( ))} {tests.map((test: Test) => ( navigate(`/tests/${test.id}`)} > {/* Waiting time — meaningful for blue_evaluating */} ))}
handleSort(key)} > {key === "waiting_time" && ( )} {label} {sortKey === key ? ( sortDir === "asc" ? ( ) : ( ) ) : ( )} Action
{test.name} {test.technique_mitre_id ? (
{test.technique_mitre_id} {test.technique_name}
) : ( - )}
{testStateLabels[test.state]} {currentTeamForState(test.state)} {test.platform || "-"} {test.state === "blue_evaluating" ? ( {formatElapsed(test.updated_at)} ) : ( )} {formatDate(test.created_at)} {formatDate(test.updated_at)}
{tests.length === 0 && (
{showMyTasks ? "No pending tasks for your role." : "No tests found matching your filters."}
)}
); }