diff --git a/artifacts/skillguard/public/opengraph.jpg b/artifacts/skillguard/public/opengraph.jpg index a40c476..e738dc5 100644 Binary files a/artifacts/skillguard/public/opengraph.jpg and b/artifacts/skillguard/public/opengraph.jpg differ diff --git a/artifacts/skillguard/src/pages/scan-history.tsx b/artifacts/skillguard/src/pages/scan-history.tsx index cd6ec5e..61fd37c 100644 --- a/artifacts/skillguard/src/pages/scan-history.tsx +++ b/artifacts/skillguard/src/pages/scan-history.tsx @@ -1,22 +1,63 @@ +import { useMemo, useState } from "react"; import { Link } from "wouter"; import { useQueryClient } from "@tanstack/react-query"; import { useListScans, getListScansQueryKey, useDeleteScan } from "@workspace/api-client-react"; -import { Card, CardContent } from "@/components/ui/card"; +import { Card } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Skeleton } from "@/components/ui/skeleton"; import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog"; import { VerdictBadge, RelationBadge } from "@/components/ui-helpers"; import { formatDate } from "@/lib/format"; -import { Search, Trash2, ArrowRight } from "lucide-react"; +import { Search, Trash2, ArrowRight, X } from "lucide-react"; import { useToast } from "@/hooks/use-toast"; +const VERDICT_OPTIONS = [ + { value: "pass", label: "Freigabe" }, + { value: "review", label: "Manuelle Prüfung" }, + { value: "block", label: "Blockieren" }, +] as const; + +const SOURCE_OPTIONS = [ + { value: "zip", label: "ZIP" }, + { value: "file", label: "Datei" }, + { value: "text", label: "Text" }, +] as const; + export default function ScanHistory() { const { data: scans, isLoading } = useListScans(); const queryClient = useQueryClient(); const deleteScan = useDeleteScan(); const { toast } = useToast(); + const [query, setQuery] = useState(""); + const [verdictFilters, setVerdictFilters] = useState([]); + const [sourceFilters, setSourceFilters] = useState([]); + + const hasActiveFilters = query.trim() !== "" || verdictFilters.length > 0 || sourceFilters.length > 0; + + const resetFilters = () => { + setQuery(""); + setVerdictFilters([]); + setSourceFilters([]); + }; + + const filteredScans = useMemo(() => { + if (!scans) return []; + const q = query.trim().toLowerCase(); + return scans.filter((scan) => { + if (verdictFilters.length > 0 && !verdictFilters.includes(scan.verdict)) return false; + if (sourceFilters.length > 0 && !sourceFilters.includes(scan.source)) return false; + if (q) { + const haystack = `${scan.name ?? ""} ${scan.description ?? ""}`.toLowerCase(); + if (!haystack.includes(q)) return false; + } + return true; + }); + }, [scans, query, verdictFilters, sourceFilters]); + const handleDelete = (id: number) => { deleteScan.mutate({ id }, { onSuccess: () => { @@ -59,8 +100,87 @@ export default function ScanHistory() { ) : ( -
- {scans.map((scan) => ( + <> +
+
+ + setQuery(e.target.value)} + placeholder="Nach Name oder Beschreibung suchen…" + className="pl-9 pr-9" + aria-label="Scans durchsuchen" + /> + {query && ( + + )} +
+
+
+ Bewertung + + {VERDICT_OPTIONS.map((opt) => ( + + {opt.label} + + ))} + +
+
+ Quelle + + {SOURCE_OPTIONS.map((opt) => ( + + {opt.label} + + ))} + +
+
+
+ + {hasActiveFilters && ( +
+ {filteredScans.length} von {scans.length} Scans + +
+ )} + + {filteredScans.length === 0 ? ( + + +

Keine Treffer

+

Für die aktuellen Filter- und Sucheinstellungen wurden keine Scans gefunden.

+ +
+ ) : ( +
+ {filteredScans.map((scan) => (
@@ -131,7 +251,9 @@ export default function ScanHistory() {
))} -
+
+ )} + )} );