feat(tests): add Validated Tests as dedicated page, remove duplicate sidebar entry
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

- New /tests/validated page with its own route and sidebar link, showing
  only validated tests with Attack and Detection result badges.
- Removed the duplicate "My Pending Tasks" sidebar entry (same as All Tests).
- All Tests table no longer shows validated tests; clicking the Validated
  counter card navigates to the new page instead.
- Validated option removed from the state filter dropdown in All Tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kitos
2026-05-28 17:18:21 +02:00
parent 2eed763f9e
commit 1120d8f2ce
4 changed files with 335 additions and 189 deletions

View File

@@ -103,9 +103,6 @@ export default function TestsPage() {
const [searchText, setSearchText] = useState("");
const [showMyTasks, setShowMyTasks] = useState(false);
// ── Validated section toggle ──────────────────────────────────────
const [showValidated, setShowValidated] = useState(false);
// ── Sort state ────────────────────────────────────────────────────
type SortKey =
| "name"
@@ -124,7 +121,6 @@ export default function TestsPage() {
setSortDir((d) => (d === "asc" ? "desc" : "asc"));
} else {
setSortKey(key);
// For waiting_time, default to asc (oldest = most urgent first)
setSortDir(key === "waiting_time" ? "asc" : "asc");
}
};
@@ -169,12 +165,6 @@ export default function TestsPage() {
queryFn: () => getTests(filters),
});
// Dedicated query for validated tests (shown in separate section)
const { data: validatedTests } = useQuery({
queryKey: ["tests", "validated"],
queryFn: () => getTests({ state: "validated", limit: 200 }),
});
// Client-side filtering
const tests = useMemo(() => {
if (!allTests) return [];
@@ -187,8 +177,7 @@ export default function TestsPage() {
);
}
// Exclude validated from the main active-tests table
// (unless the user explicitly chose to filter by validated)
// Exclude validated from this table — they live in /tests/validated
if (stateFilter !== "validated") {
filtered = filtered.filter((t) => t.state !== "validated");
}
@@ -239,14 +228,12 @@ export default function TestsPage() {
bv = b.updated_at || "";
break;
case "waiting_time": {
// Both blue_evaluating: oldest updated_at = longest wait
const aIsBlue = a.state === "blue_evaluating";
const bIsBlue = b.state === "blue_evaluating";
if (aIsBlue && bIsBlue) {
av = a.updated_at || "";
bv = b.updated_at || "";
} else {
// Blue_evaluating entries come first when sorting by wait
if (aIsBlue && !bIsBlue) return sortDir === "asc" ? -1 : 1;
if (!aIsBlue && bIsBlue) return sortDir === "asc" ? 1 : -1;
av = a.updated_at || "";
@@ -263,18 +250,6 @@ export default function TestsPage() {
}, [allTests, searchText, showMyTasks, user, stateFilter, sortKey, sortDir]);
// ── State counters ────────────────────────────────────────────────
const stateCounts = useMemo(() => {
const counts: Record<string, number> = {};
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 }),
@@ -296,7 +271,8 @@ export default function TestsPage() {
// ── Formatting helpers ─────────────────────────────────────────────
const formatDate = (dateStr: string | null | undefined) => {
if (!dateStr) return "-";
const utc = dateStr.endsWith("Z") || dateStr.includes("+") ? dateStr : dateStr + "Z";
const utc =
dateStr.endsWith("Z") || dateStr.includes("+") ? dateStr : dateStr + "Z";
return new Date(utc).toLocaleDateString("es-ES", {
year: "numeric",
month: "short",
@@ -321,35 +297,6 @@ export default function TestsPage() {
}
}, [user]);
// ── Validated sort helpers ─────────────────────────────────────────
const [valSortKey, setValSortKey] = useState<"name" | "technique" | "created_at" | "updated_at">("updated_at");
const [valSortDir, setValSortDir] = useState<"asc" | "desc">("desc");
const sortedValidatedTests = useMemo(() => {
if (!validatedTests) return [];
return [...validatedTests].sort((a, b) => {
let av = "";
let bv = "";
switch (valSortKey) {
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 "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 valSortDir === "asc" ? cmp : -cmp;
});
}, [validatedTests, valSortKey, valSortDir]);
const handleValSort = (key: typeof valSortKey) => {
if (valSortKey === key) {
setValSortDir((d) => (d === "asc" ? "desc" : "asc"));
} else {
setValSortKey(key);
setValSortDir("desc");
}
};
// ── Render ─────────────────────────────────────────────────────────
if (isLoading) {
@@ -424,6 +371,10 @@ export default function TestsPage() {
<button
key={state}
onClick={() => {
if (state === "validated") {
navigate("/tests/validated");
return;
}
setShowMyTasks(false);
setStateFilter(stateFilter === state ? "" : state);
}}
@@ -473,11 +424,18 @@ export default function TestsPage() {
<Filter className="h-4 w-4 text-gray-500" />
<select
value={stateFilter}
onChange={(e) => setStateFilter(e.target.value as TestState | "")}
onChange={(e) => {
const val = e.target.value as TestState | "";
if (val === "validated") {
navigate("/tests/validated");
return;
}
setStateFilter(val);
}}
className="rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 focus:border-cyan-500 focus:outline-none"
>
<option value="">All States</option>
{ALL_STATES.map((s) => (
{ALL_STATES.filter((s) => s !== "validated").map((s) => (
<option key={s} value={s}>
{testStateLabels[s]}
</option>
@@ -543,19 +501,12 @@ export default function TestsPage() {
)}
</div>
{/* ── Active Tests Table ────────────────────────────────────────── */}
{/* ── Tests Table ───────────────────────────────────────────────── */}
<div className="rounded-xl border border-gray-800 bg-gray-900 p-6">
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-2">
<h2 className="text-lg font-semibold text-white">
{showMyTasks ? myTasksLabel : "All Tests"}
</h2>
{stateFilter !== "validated" && (
<span className="rounded-full border border-gray-700 bg-gray-800 px-2 py-0.5 text-[10px] text-gray-500">
validated shown below
</span>
)}
</div>
<h2 className="text-lg font-semibold text-white">
{showMyTasks ? myTasksLabel : "All Tests"}
</h2>
<span className="text-sm text-gray-400">{tests.length} tests</span>
</div>
@@ -628,7 +579,7 @@ export default function TestsPage() {
<td className="py-3 px-4 text-gray-400 text-xs">
{test.platform || "-"}
</td>
{/* Waiting time — relevant for blue_evaluating */}
{/* Waiting time — meaningful for blue_evaluating */}
<td className="py-3 px-4 text-xs whitespace-nowrap">
{test.state === "blue_evaluating" ? (
<span className="font-mono text-indigo-400">
@@ -669,123 +620,6 @@ export default function TestsPage() {
)}
</div>
</div>
{/* ── Validated / Approved Tests ────────────────────────────────── */}
<div className="rounded-xl border border-green-900/30 bg-gray-900">
{/* Collapsible header */}
<button
onClick={() => setShowValidated((v) => !v)}
className="flex w-full items-center gap-3 px-6 py-4 text-left hover:bg-green-500/5 transition-colors rounded-xl"
>
<CheckCircle className="h-5 w-5 shrink-0 text-green-400" />
<h2 className="text-lg font-semibold text-white flex-1">
Validated Tests
</h2>
<span className="rounded-full border border-green-500/30 bg-green-500/10 px-2.5 py-0.5 text-xs font-medium text-green-400">
{validatedTests?.length ?? 0}
</span>
{showValidated ? (
<ChevronUp className="h-4 w-4 text-gray-500" />
) : (
<ChevronDown className="h-4 w-4 text-gray-500" />
)}
</button>
{showValidated && (
<div className="border-t border-green-900/30 px-6 pb-6 pt-4">
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-gray-800">
{(
[
{ 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 }) => (
<th
key={key}
className={`pb-3 ${cls} font-medium text-gray-400 cursor-pointer select-none hover:text-white transition-colors`}
onClick={() => handleValSort(key)}
>
<span className="inline-flex items-center gap-1">
{label}
{valSortKey === key ? (
valSortDir === "asc" ? (
<ChevronUp className="h-3.5 w-3.5 text-green-400" />
) : (
<ChevronDown className="h-3.5 w-3.5 text-green-400" />
)
) : (
<ChevronsUpDown className="h-3.5 w-3.5 opacity-30" />
)}
</span>
</th>
))}
<th className="pb-3 pl-4 font-medium text-gray-400">Action</th>
</tr>
</thead>
<tbody>
{sortedValidatedTests.map((test: Test) => (
<tr
key={test.id}
className="border-b border-gray-800/50 hover:bg-gray-800/30 transition-colors cursor-pointer"
onClick={() => navigate(`/tests/${test.id}`)}
>
<td className="py-3 pr-4">
<span className="font-medium text-gray-200">{test.name}</span>
</td>
<td className="py-3 px-4">
{test.technique_mitre_id ? (
<div className="flex flex-col">
<span className="font-mono text-xs text-cyan-400">
{test.technique_mitre_id}
</span>
<span className="text-xs text-gray-500 truncate max-w-[160px]">
{test.technique_name}
</span>
</div>
) : (
<span className="text-gray-500">-</span>
)}
</td>
<td className="py-3 px-4 text-gray-400 text-xs">
{test.platform || "-"}
</td>
<td className="py-3 px-4 text-gray-400 text-xs whitespace-nowrap">
{formatDate(test.created_at)}
</td>
<td className="py-3 px-4 text-gray-400 text-xs whitespace-nowrap">
{formatDate(test.updated_at)}
</td>
<td className="py-3 pl-4">
<button
onClick={(e) => {
e.stopPropagation();
navigate(`/tests/${test.id}`);
}}
className="text-sm text-green-400 hover:underline"
>
View
</button>
</td>
</tr>
))}
</tbody>
</table>
{sortedValidatedTests.length === 0 && (
<div className="py-10 text-center text-gray-500 text-sm">
No validated tests yet.
</div>
)}
</div>
</div>
)}
</div>
</div>
);
}