diff --git a/artifacts/skillguard/public/opengraph.jpg b/artifacts/skillguard/public/opengraph.jpg index e738dc5..1715a0b 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-report.tsx b/artifacts/skillguard/src/pages/scan-report.tsx index fe0c757..71e2414 100644 --- a/artifacts/skillguard/src/pages/scan-report.tsx +++ b/artifacts/skillguard/src/pages/scan-report.tsx @@ -19,7 +19,7 @@ import { Button } from "@/components/ui/button"; import { Progress } from "@/components/ui/progress"; import { VerdictBadge, SeverityBadge, AxisBadge, CheckpointStatusBadge, CHECKPOINT_STATUS_LABELS, RelationBadge } from "@/components/ui-helpers"; import { formatDate } from "@/lib/format"; -import { ShieldQuestion, ShieldAlert, AlertTriangle, Download, FileCode, CheckCircle2, Code, Shield, FileDown, ListChecks, Fingerprint, GitCompare, History, GitCommitVertical, Sparkles, Loader2, Folder, File as FileIcon, Copy, Check } from "lucide-react"; +import { ShieldQuestion, ShieldAlert, AlertTriangle, Download, FileCode, CheckCircle2, Code, Shield, FileDown, ListChecks, Fingerprint, GitCompare, History, GitCommitVertical, Sparkles, Loader2, Folder, File as FileIcon, Copy, Check, ChevronRight, ChevronDown } from "lucide-react"; import type { ScanDetail } from "@workspace/api-client-react"; type ScanReportFile = ScanDetail["files"][number]; @@ -60,20 +60,50 @@ function sortFileTree(nodes: FileTreeNode[]) { } } -type FlatFileRow = { depth: number; node: FileTreeNode }; +function countFiles(node: FileTreeNode): number { + if (node.type === "file") return 1; + return node.children.reduce((sum, c) => sum + countFiles(c), 0); +} -function flattenFileTree(nodes: FileTreeNode[], depth = 0, out: FlatFileRow[] = []): FlatFileRow[] { +type FlatFileRow = + | { type: "file"; depth: number; path: string; node: Extract } + | { type: "dir"; depth: number; path: string; node: Extract; fileCount: number; collapsed: boolean }; + +function flattenFileTree( + nodes: FileTreeNode[], + collapsed: Set, + depth = 0, + parentPath = "", + out: FlatFileRow[] = [], +): FlatFileRow[] { for (const node of nodes) { - out.push({ depth, node }); - if (node.type === "dir") flattenFileTree(node.children, depth + 1, out); + const path = parentPath ? `${parentPath}/${node.name}` : node.name; + if (node.type === "dir") { + const isCollapsed = collapsed.has(path); + out.push({ type: "dir", depth, path, node, fileCount: countFiles(node), collapsed: isCollapsed }); + if (!isCollapsed) flattenFileTree(node.children, collapsed, depth + 1, path, out); + } else { + out.push({ type: "file", depth, path, node }); + } } return out; } function FilesTree({ files }: { files: ScanReportFile[] }) { - const rows = useMemo(() => flattenFileTree(buildFileTree(files)), [files]); + const tree = useMemo(() => buildFileTree(files), [files]); + const [collapsed, setCollapsed] = useState>(() => new Set()); + const rows = useMemo(() => flattenFileTree(tree, collapsed), [tree, collapsed]); const [copiedHash, setCopiedHash] = useState(null); + const toggleFolder = (path: string) => { + setCollapsed((prev) => { + const next = new Set(prev); + if (next.has(path)) next.delete(path); + else next.add(path); + return next; + }); + }; + const copyHash = async (hash: string) => { try { await navigator.clipboard.writeText(hash); @@ -102,50 +132,64 @@ function FilesTree({ files }: { files: ScanReportFile[] }) { Keine Dateien verfügbar. ) : ( - rows.map(({ depth, node }, i) => - node.type === "dir" ? ( + rows.map((row, i) => + row.type === "dir" ? ( - - + + ) : ( - + - {node.name} + {row.node.name} - {node.file.kind === "instruction" ? "Anweisung" : node.file.kind === "script" ? "Skript" : "Ressource"} + {row.node.file.kind === "instruction" ? "Anweisung" : row.node.file.kind === "script" ? "Skript" : "Ressource"} - {node.file.language || "-"} + {row.node.file.language || "-"} - {node.file.hash ? node.file.hash.slice(0, 12) : "-"} - {node.file.hash && ( + {row.node.file.hash ? row.node.file.hash.slice(0, 12) : "-"} + {row.node.file.hash && ( )} - {!node.file.hasContent && ( + {!row.node.file.hasContent && ( binär )} - {node.file.size} B + {row.node.file.size} B ), )