diff --git a/.gitignore b/.gitignore index 9a9c9f0..30e6ed1 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts +.playwright-mcp/ diff --git a/src/app/os/files/page.tsx b/src/app/os/files/page.tsx index 743b91e..54c0a1f 100644 --- a/src/app/os/files/page.tsx +++ b/src/app/os/files/page.tsx @@ -18,6 +18,7 @@ export default function Files() { const [loading, setLoading] = useState(true); const [files, setFiles] = useState([]); const [keyword, setKeyword] = useState(""); + const [expandAll, setExpandAll] = useState(null); const [debouncedKeyword] = useDebounce(keyword, 200); @@ -39,6 +40,11 @@ export default function Files() { const isFiltering = debouncedKeyword.length > 0; + // Reset expandAll when filter changes + useEffect(() => { + setExpandAll(isFiltering ? true : null); + }, [isFiltering]); + return (
@@ -62,17 +68,39 @@ export default function Files() { )}
- {!loading && ( -
- {isFiltering ? ( - <> - {filtered.length} of {files.length} paths - - ) : ( - <>{files.length} paths - )} -
- )} +
+ {!loading && files.length > 0 && ( +
+ + +
+ )} + {!loading && ( +
+ {isFiltering ? ( + <> + {filtered.length} of {files.length} paths + + ) : ( + <>{files.length} paths + )} +
+ )} +
{loading ? ( @@ -96,7 +124,7 @@ export default function Files() { )} ) : ( - + )} ); diff --git a/src/app/os/find/page.tsx b/src/app/os/find/page.tsx index 9588f53..7f615ab 100644 --- a/src/app/os/find/page.tsx +++ b/src/app/os/find/page.tsx @@ -1,11 +1,14 @@ "use client"; -import FileSystem from "@/components/filesystem"; - -import { createEngine } from "@/lib/engine"; - +import { useState, useEffect, useMemo } from "react"; import { redirect, useSearchParams } from "next/navigation"; -import { useEffect, useState } from "react"; +import { useDebounce } from "use-debounce"; +import { Search, X } from "lucide-react"; + +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import FileSystem from "@/components/filesystem"; +import { createEngine } from "@/lib/engine"; export default function FindByKey() { const params = useSearchParams(); @@ -24,7 +27,12 @@ export default function FindByKey() { redirect("/404"); } + const [loading, setLoading] = useState(true); const [paths, setPaths] = useState([]); + const [keyword, setKeyword] = useState(""); + const [expandAll, setExpandAll] = useState(null); + + const [debouncedKeyword] = useDebounce(keyword, 200); useEffect(() => { async function fetchPaths() { @@ -34,20 +42,115 @@ export default function FindByKey() { const result = await engine.getPathsForKey(build, key); setPaths(result); } - fetchPaths(); + setLoading(true); + fetchPaths().finally(() => setLoading(false)); }, [group, build, key]); + const filtered = useMemo( + () => + paths.filter((path) => + path.toLowerCase().includes(debouncedKeyword.toLowerCase()) + ), + [debouncedKeyword, paths] + ); + + const isFiltering = debouncedKeyword.length > 0; + + // Reset expandAll when filter changes + useEffect(() => { + setExpandAll(isFiltering ? true : null); + }, [isFiltering]); + return (
-
-

+
+

Binaries that have the following entitlement:

- {key} + {key}

- + +
+
+ + setKeyword(e.target.value)} + className="pl-9 pr-9" + /> + {keyword && ( + + )} +
+
+ {!loading && paths.length > 0 && ( +
+ + +
+ )} + {!loading && ( +
+ {isFiltering ? ( + <> + {filtered.length} of {paths.length} paths + + ) : ( + <>{paths.length} paths + )} +
+ )} +
+
+ + {loading ? ( +
+ {Array.from({ length: 6 }).map((_, i) => ( +
+
+
+
+ ))} +
+ ) : filtered.length === 0 ? ( +
+ {paths.length === 0 ? ( +

No binaries found with this entitlement.

+ ) : ( +

No paths match "{keyword}"

+ )} +
+ ) : ( + + )}
); } diff --git a/src/app/os/keys/page.tsx b/src/app/os/keys/page.tsx index f3f31c3..2985548 100644 --- a/src/app/os/keys/page.tsx +++ b/src/app/os/keys/page.tsx @@ -4,13 +4,7 @@ import { useState, useEffect, useMemo, useCallback } from "react"; import { useSearchParams } from "next/navigation"; import { useDebounce } from "use-debounce"; import Link from "next/link"; -import { - Search, - X, - ChevronRight, - ChevronDown, - ChevronsUpDown, -} from "lucide-react"; +import { Search, X, ChevronRight, ChevronDown } from "lucide-react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; @@ -104,8 +98,8 @@ function KeyGroup({ } }, [forceOpen]); - // Single standalone key - just show it inline - if (keys.length === 1 && keys[0] === prefix) { + // Single key in group - just show it inline without collapsible wrapper + if (keys.length === 1) { return (
diff --git a/src/components/filesystem.tsx b/src/components/filesystem.tsx index 75427ed..ff32de0 100644 --- a/src/components/filesystem.tsx +++ b/src/components/filesystem.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Collapsible, CollapsibleContent, @@ -63,7 +63,7 @@ function Tree({ item: TreeWithFullPath; os: string; depth: number; - expandAll: boolean; + expandAll: boolean | null; }) { const entries = Object.entries(item); const files = entries.filter(([, v]) => typeof v === "string"); @@ -110,13 +110,19 @@ function TreeFolder({ item: TreeWithFullPath; os: string; depth: number; - expandAll: boolean; + expandAll: boolean | null; }) { - const [open, setOpen] = useState(expandAll || depth < 1); + const [open, setOpen] = useState(expandAll === true || (expandAll === null && depth < 1)); const itemCount = countItems(item); const maxDepth = getMaxDepth(item); const isShallow = maxDepth === 0; + useEffect(() => { + if (expandAll !== null) { + setOpen(expandAll); + } + }, [expandAll]); + return (
  • @@ -158,11 +164,11 @@ function TreeFolder({ export default function FileSystem({ list, os, - expandAll = false, + expandAll = null, }: { list: string[]; os: string; - expandAll?: boolean; + expandAll?: boolean | null; }) { const tree = filesToTree(list); return (