-
-
- {showMyTasks ? myTasksLabel : "All Tests"}
-
- {stateFilter !== "validated" && (
-
- validated shown below
-
- )}
-
+
+ {showMyTasks ? myTasksLabel : "All Tests"}
+
{tests.length} tests
@@ -628,7 +579,7 @@ export default function TestsPage() {
{test.platform || "-"}
|
- {/* Waiting time — relevant for blue_evaluating */}
+ {/* Waiting time — meaningful for blue_evaluating */}
{test.state === "blue_evaluating" ? (
@@ -669,123 +620,6 @@ export default function TestsPage() {
)}
-
- {/* ── Validated / Approved Tests ────────────────────────────────── */}
-
- {/* Collapsible header */}
-
-
- {showValidated && (
-
-
-
-
-
- {(
- [
- { key: "name" as const, label: "Name", cls: "pr-4" },
- { key: "technique" as const, label: "Technique", cls: "px-4" },
- { key: "platform", label: "Platform", cls: "px-4" },
- { key: "created_at" as const, label: "Created", cls: "px-4" },
- { key: "updated_at" as const, label: "Validated", cls: "px-4" },
- ] as { key: typeof valSortKey; label: string; cls: string }[]
- ).map(({ key, label, cls }) => (
- | handleValSort(key)}
- >
-
- {label}
- {valSortKey === key ? (
- valSortDir === "asc" ? (
-
- ) : (
-
- )
- ) : (
-
- )}
-
- |
- ))}
- Action |
-
-
-
- {sortedValidatedTests.map((test: Test) => (
- navigate(`/tests/${test.id}`)}
- >
- |
- {test.name}
- |
-
- {test.technique_mitre_id ? (
-
-
- {test.technique_mitre_id}
-
-
- {test.technique_name}
-
-
- ) : (
- -
- )}
- |
-
- {test.platform || "-"}
- |
-
- {formatDate(test.created_at)}
- |
-
- {formatDate(test.updated_at)}
- |
-
-
- |
-
- ))}
-
-
-
- {sortedValidatedTests.length === 0 && (
-
- No validated tests yet.
-
- )}
-
-
- )}
-
);
}
diff --git a/frontend/src/pages/ValidatedTestsPage.tsx b/frontend/src/pages/ValidatedTestsPage.tsx
new file mode 100644
index 0000000..8dc89cf
--- /dev/null
+++ b/frontend/src/pages/ValidatedTestsPage.tsx
@@ -0,0 +1,310 @@
+import { useState, useMemo } from "react";
+import { useQuery } from "@tanstack/react-query";
+import { useNavigate } from "react-router-dom";
+import {
+ Loader2,
+ AlertCircle,
+ CheckCircle,
+ Search,
+ ChevronUp,
+ ChevronDown,
+ ChevronsUpDown,
+ ShieldCheck,
+ Swords,
+} from "lucide-react";
+import { getTests } from "../api/tests";
+import type { Test, TestResult } from "../types/models";
+
+/* ── Result helpers ─────────────────────────────────────────────────── */
+
+const detectionColors: Record = {
+ detected: "bg-green-500/10 text-green-400 border-green-500/20",
+ partially_detected: "bg-yellow-500/10 text-yellow-400 border-yellow-500/20",
+ not_detected: "bg-red-500/10 text-red-400 border-red-500/20",
+};
+
+const detectionLabels: Record = {
+ detected: "Detected",
+ partially_detected: "Partial",
+ not_detected: "Not Detected",
+};
+
+function DetectionBadge({ result }: { result: TestResult | null | undefined }) {
+ if (!result) return —;
+ return (
+
+
+ {detectionLabels[result]}
+
+ );
+}
+
+function AttackBadge({ success }: { success: boolean | null | undefined }) {
+ if (success === null || success === undefined)
+ return —;
+ return (
+
+
+ {success ? "Success" : "Blocked"}
+
+ );
+}
+
+/* ── Component ──────────────────────────────────────────────────────── */
+
+export default function ValidatedTestsPage() {
+ const navigate = useNavigate();
+
+ const [searchText, setSearchText] = useState("");
+
+ type SortKey =
+ | "name"
+ | "technique"
+ | "platform"
+ | "attack_success"
+ | "detection_result"
+ | "created_at"
+ | "updated_at";
+
+ const [sortKey, setSortKey] = useState("updated_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("desc");
+ }
+ };
+
+ const { data: validatedTests, isLoading, error } = useQuery({
+ queryKey: ["tests", "validated"],
+ queryFn: () => getTests({ state: "validated", limit: 200 }),
+ });
+
+ 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",
+ });
+ };
+
+ const tests = useMemo(() => {
+ if (!validatedTests) return [];
+ let list = validatedTests;
+
+ if (searchText.trim()) {
+ const q = searchText.toLowerCase();
+ list = list.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 [...list].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 "platform":
+ av = (a.platform || "").toLowerCase();
+ bv = (b.platform || "").toLowerCase();
+ break;
+ case "attack_success":
+ av = String(a.attack_success ?? "");
+ bv = String(b.attack_success ?? "");
+ break;
+ case "detection_result":
+ av = a.detection_result ?? "";
+ bv = b.detection_result ?? "";
+ 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;
+ }
+ const cmp = av < bv ? -1 : av > bv ? 1 : 0;
+ return sortDir === "asc" ? cmp : -cmp;
+ });
+ }, [validatedTests, searchText, sortKey, sortDir]);
+
+ const SortIcon = ({ col }: { col: SortKey }) =>
+ sortKey === col ? (
+ sortDir === "asc" ? (
+
+ ) : (
+
+ )
+ ) : (
+
+ );
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+ Failed to load validated tests
+
+ );
+ }
+
+ const columns: { key: SortKey; label: string }[] = [
+ { key: "name", label: "Name" },
+ { key: "technique", label: "Technique" },
+ { key: "platform", label: "Platform" },
+ { key: "attack_success", label: "Attack" },
+ { key: "detection_result", label: "Detection" },
+ { key: "created_at", label: "Created" },
+ { key: "updated_at", label: "Validated" },
+ ];
+
+ return (
+
+ {/* Header */}
+
+
+
+
+ Validated Tests
+
+ Tests that have completed the full Red/Blue validation workflow
+
+
+
+
+ {validatedTests?.length ?? 0} validated
+
+
+
+ {/* Search */}
+
+
+ setSearchText(e.target.value)}
+ placeholder="Search by name or technique…"
+ className="w-full rounded-lg border border-gray-700 bg-gray-900 pl-9 pr-3 py-2 text-sm text-gray-300 placeholder-gray-500 focus:border-green-500 focus:outline-none"
+ />
+
+
+ {/* Table */}
+
+
+
+
+
+ {columns.map(({ key, label }) => (
+ | handleSort(key)}
+ >
+
+ {label}
+
+
+ |
+ ))}
+ Action |
+
+
+
+ {tests.map((test: Test) => (
+ navigate(`/tests/${test.id}`)}
+ >
+ |
+ {test.name}
+ |
+
+ {test.technique_mitre_id ? (
+
+
+ {test.technique_mitre_id}
+
+
+ {test.technique_name}
+
+
+ ) : (
+ —
+ )}
+ |
+
+ {test.platform || "—"}
+ |
+
+
+ |
+
+
+ |
+
+ {formatDate(test.created_at)}
+ |
+
+ {formatDate(test.updated_at)}
+ |
+
+
+ |
+
+ ))}
+
+
+
+ {tests.length === 0 && (
+
+ {searchText ? "No validated tests match your search." : "No validated tests yet."}
+
+ )}
+
+
+
+ );
+}
|