mirror of
https://github.com/ChiChou/entdb.git
synced 2026-06-10 23:07:47 +02:00
Add search filter and expand/collapse to find page
- Find page now has same controls as files page - Both pages support expand all / collapse all buttons - FileSystem component accepts controlled expandAll state - Fixed single-key groups to not show as collapsible
This commit is contained in:
@@ -34,3 +34,4 @@ yarn-error.log*
|
||||
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
.playwright-mcp/
|
||||
|
||||
+40
-12
@@ -18,6 +18,7 @@ export default function Files() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [files, setFiles] = useState<string[]>([]);
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [expandAll, setExpandAll] = useState<boolean | null>(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 (
|
||||
<div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-3 mb-6">
|
||||
@@ -62,17 +68,39 @@ export default function Files() {
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{!loading && (
|
||||
<div className="text-sm text-muted-foreground whitespace-nowrap">
|
||||
{isFiltering ? (
|
||||
<>
|
||||
{filtered.length} of {files.length} paths
|
||||
</>
|
||||
) : (
|
||||
<>{files.length} paths</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-3">
|
||||
{!loading && files.length > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setExpandAll(true)}
|
||||
className="h-8 px-2 text-xs"
|
||||
>
|
||||
Expand All
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setExpandAll(false)}
|
||||
className="h-8 px-2 text-xs"
|
||||
>
|
||||
Collapse All
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{!loading && (
|
||||
<div className="text-sm text-muted-foreground whitespace-nowrap">
|
||||
{isFiltering ? (
|
||||
<>
|
||||
{filtered.length} of {files.length} paths
|
||||
</>
|
||||
) : (
|
||||
<>{files.length} paths</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
@@ -96,7 +124,7 @@ export default function Files() {
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<FileSystem os={os} list={filtered} expandAll={isFiltering} />
|
||||
<FileSystem os={os} list={filtered} expandAll={expandAll} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
+113
-10
@@ -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<string[]>([]);
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [expandAll, setExpandAll] = useState<boolean | null>(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 (
|
||||
<div>
|
||||
<header>
|
||||
<h1 className="text-gray-800">
|
||||
<header className="mb-4">
|
||||
<h1 className="text-foreground">
|
||||
Binaries that have the following entitlement:
|
||||
</h1>
|
||||
<p>
|
||||
<code className="text-sm break-all text-red-700">{key}</code>
|
||||
<code className="text-sm break-all text-red-700 dark:text-red-400">{key}</code>
|
||||
</p>
|
||||
</header>
|
||||
<FileSystem os={os} list={paths} />
|
||||
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-3 mb-6">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Filter paths..."
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
className="pl-9 pr-9"
|
||||
/>
|
||||
{keyword && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setKeyword("")}
|
||||
className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 p-0"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{!loading && paths.length > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setExpandAll(true)}
|
||||
className="h-8 px-2 text-xs"
|
||||
>
|
||||
Expand All
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setExpandAll(false)}
|
||||
className="h-8 px-2 text-xs"
|
||||
>
|
||||
Collapse All
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{!loading && (
|
||||
<div className="text-sm text-muted-foreground whitespace-nowrap">
|
||||
{isFiltering ? (
|
||||
<>
|
||||
{filtered.length} of {paths.length} paths
|
||||
</>
|
||||
) : (
|
||||
<>{paths.length} paths</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="space-y-2">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="flex items-center gap-2 py-1">
|
||||
<div className="h-4 w-4 bg-muted rounded animate-pulse" />
|
||||
<div
|
||||
className="h-5 bg-muted rounded animate-pulse"
|
||||
style={{ width: `${120 + Math.random() * 200}px` }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : filtered.length === 0 ? (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
{paths.length === 0 ? (
|
||||
<p>No binaries found with this entitlement.</p>
|
||||
) : (
|
||||
<p>No paths match "{keyword}"</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<FileSystem os={os} list={filtered} expandAll={expandAll} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className="py-1">
|
||||
<KeyBadge keyName={keys[0]} prefix="" os={os} />
|
||||
|
||||
@@ -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 (
|
||||
<li>
|
||||
<Collapsible open={open} onOpenChange={setOpen}>
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user