refactor: improve file explorer performance by pre-fetching on hover

This commit is contained in:
Nicolas Meienberger
2025-10-17 21:19:58 +02:00
parent c8fc5a1273
commit 269116c25e
3 changed files with 35 additions and 4 deletions

View File

@@ -27,6 +27,7 @@ interface Props {
selectedFile?: string;
onFileSelect?: (filePath: string) => void;
onFolderExpand?: (folderPath: string) => void;
onFolderHover?: (folderPath: string) => void;
expandedFolders?: Set<string>;
loadingFolders?: Set<string>;
className?: string;
@@ -38,6 +39,7 @@ export const FileTree = memo((props: Props) => {
onFileSelect,
selectedFile,
onFolderExpand,
onFolderHover,
expandedFolders = new Set(),
loadingFolders = new Set(),
className,
@@ -137,6 +139,7 @@ export const FileTree = memo((props: Props) => {
collapsed={collapsedFolders.has(fileOrFolder.fullPath)}
loading={loadingFolders.has(fileOrFolder.fullPath)}
onToggle={toggleCollapseState}
onHover={onFolderHover}
/>
);
}
@@ -154,9 +157,10 @@ interface FolderProps {
collapsed: boolean;
loading?: boolean;
onToggle: (fullPath: string) => void;
onHover?: (fullPath: string) => void;
}
const Folder = memo(({ folder, collapsed, loading, onToggle }: FolderProps) => {
const Folder = memo(({ folder, collapsed, loading, onToggle, onHover }: FolderProps) => {
const { depth, name, fullPath } = folder;
const FolderIconComponent = collapsed ? FolderIcon : FolderOpen;
@@ -164,6 +168,12 @@ const Folder = memo(({ folder, collapsed, loading, onToggle }: FolderProps) => {
onToggle(fullPath);
}, [onToggle, fullPath]);
const handleMouseEnter = useCallback(() => {
if (collapsed) {
onHover?.(fullPath);
}
}, [onHover, fullPath, collapsed]);
return (
<NodeButton
className={cn("group hover:bg-accent/50 text-foreground")}
@@ -178,6 +188,7 @@ const Folder = memo(({ folder, collapsed, loading, onToggle }: FolderProps) => {
)
}
onClick={handleClick}
onMouseEnter={handleMouseEnter}
>
<FolderIconComponent className="w-4 h-4 shrink-0 text-strong-accent" />
<span className="truncate">{name}</span>
@@ -219,9 +230,10 @@ interface ButtonProps {
children: ReactNode;
className?: string;
onClick?: () => void;
onMouseEnter?: () => void;
}
const NodeButton = memo(({ depth, icon, onClick, className, children }: ButtonProps) => {
const NodeButton = memo(({ depth, icon, onClick, onMouseEnter, className, children }: ButtonProps) => {
const paddingLeft = useMemo(() => `${8 + depth * NODE_PADDING_LEFT}px`, [depth]);
return (
@@ -230,6 +242,7 @@ const NodeButton = memo(({ depth, icon, onClick, className, children }: ButtonPr
className={cn("flex items-center gap-2 w-full pr-2 text-sm py-1.5 text-left", className)}
style={{ paddingLeft }}
onClick={onClick}
onMouseEnter={onMouseEnter}
>
{icon}
<div className="truncate w-full flex items-center gap-2">{children}</div>

View File

@@ -1,11 +1,10 @@
import { useQuery } from "@tanstack/react-query";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { FolderOpen } from "lucide-react";
import { useCallback, useMemo, useState } from "react";
import { listFilesOptions } from "~/api-client/@tanstack/react-query.gen";
import { listFiles } from "~/api-client/sdk.gen";
import { FileTree } from "~/components/file-tree";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
import { parseError } from "~/lib/errors";
import type { Volume } from "~/lib/types";
type Props = {
@@ -21,6 +20,7 @@ interface FileEntry {
}
export const FilesTabContent = ({ volume }: Props) => {
const queryClient = useQueryClient();
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
const [fetchedFolders, setFetchedFolders] = useState<Set<string>>(new Set(["/"]));
const [loadingFolders, setLoadingFolders] = useState<Set<string>>(new Set());
@@ -88,6 +88,21 @@ export const FilesTabContent = ({ volume }: Props) => {
[volume.name, fetchedFolders],
);
// Prefetch folder contents on hover
const handleFolderHover = useCallback(
(folderPath: string) => {
if (!fetchedFolders.has(folderPath) && !loadingFolders.has(folderPath)) {
queryClient.prefetchQuery(
listFilesOptions({
path: { name: volume.name },
query: { path: folderPath },
}),
);
}
},
[volume.name, fetchedFolders, loadingFolders, queryClient],
);
const fileArray = useMemo(() => Array.from(allFiles.values()), [allFiles]);
if (volume.status !== "mounted") {
@@ -133,6 +148,7 @@ export const FilesTabContent = ({ volume }: Props) => {
<FileTree
files={fileArray}
onFolderExpand={handleFolderExpand}
onFolderHover={handleFolderHover}
expandedFolders={expandedFolders}
loadingFolders={loadingFolders}
className="p-2"

View File

@@ -131,5 +131,7 @@ export const volumeController = new Hono()
path: result.path,
} satisfies ListFilesResponseDto;
c.header("Cache-Control", "public, max-age=10, stale-while-revalidate=60");
return c.json(response, 200);
});