mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
refactor(browsers): create hook for common operations
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { FileTree } from "./file-tree";
|
||||||
import { FileTree, type FileEntry } from "./file-tree";
|
|
||||||
import { ScrollArea } from "./ui/scroll-area";
|
import { ScrollArea } from "./ui/scroll-area";
|
||||||
import { browseFilesystemOptions } from "../api-client/@tanstack/react-query.gen";
|
import { browseFilesystemOptions } from "../api-client/@tanstack/react-query.gen";
|
||||||
|
import { useFileBrowser } from "../hooks/use-file-browser";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onSelectPath: (path: string) => void;
|
onSelectPath: (path: string) => void;
|
||||||
@@ -11,82 +11,23 @@ type Props = {
|
|||||||
|
|
||||||
export const DirectoryBrowser = ({ onSelectPath, selectedPath }: Props) => {
|
export const DirectoryBrowser = ({ onSelectPath, selectedPath }: Props) => {
|
||||||
const queryClient = useQueryClient();
|
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());
|
|
||||||
const [allFiles, setAllFiles] = useState<Map<string, FileEntry>>(new Map());
|
|
||||||
|
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useQuery({
|
||||||
...browseFilesystemOptions({ query: { path: "/" } }),
|
...browseFilesystemOptions({ query: { path: "/" } }),
|
||||||
});
|
});
|
||||||
|
|
||||||
useMemo(() => {
|
const fileBrowser = useFileBrowser({
|
||||||
if (data?.directories) {
|
initialData: data,
|
||||||
setAllFiles((prev) => {
|
isLoading,
|
||||||
const next = new Map(prev);
|
fetchFolder: async (path) => {
|
||||||
for (const dir of data.directories) {
|
return await queryClient.ensureQueryData(browseFilesystemOptions({ query: { path } }));
|
||||||
next.set(dir.path, { name: dir.name, path: dir.path, type: "folder" });
|
|
||||||
}
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
const fileArray = useMemo(() => Array.from(allFiles.values()), [allFiles]);
|
|
||||||
|
|
||||||
const handleFolderExpand = useCallback(
|
|
||||||
async (folderPath: string) => {
|
|
||||||
setExpandedFolders((prev) => {
|
|
||||||
const next = new Set(prev);
|
|
||||||
next.add(folderPath);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!fetchedFolders.has(folderPath)) {
|
|
||||||
setLoadingFolders((prev) => new Set(prev).add(folderPath));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await queryClient.ensureQueryData(
|
|
||||||
browseFilesystemOptions({
|
|
||||||
query: { path: folderPath },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.directories) {
|
|
||||||
setAllFiles((prev) => {
|
|
||||||
const next = new Map(prev);
|
|
||||||
for (const dir of result.directories) {
|
|
||||||
next.set(dir.path, { name: dir.name, path: dir.path, type: "folder" });
|
|
||||||
}
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
|
|
||||||
setFetchedFolders((prev) => new Set(prev).add(folderPath));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to fetch folder contents:", error);
|
|
||||||
} finally {
|
|
||||||
setLoadingFolders((prev) => {
|
|
||||||
const next = new Set(prev);
|
|
||||||
next.delete(folderPath);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[fetchedFolders, queryClient],
|
prefetchFolder: (path) => {
|
||||||
);
|
queryClient.prefetchQuery(browseFilesystemOptions({ query: { path } }));
|
||||||
|
|
||||||
const handleFolderHover = useCallback(
|
|
||||||
(folderPath: string) => {
|
|
||||||
if (!fetchedFolders.has(folderPath) && !loadingFolders.has(folderPath)) {
|
|
||||||
queryClient.prefetchQuery(browseFilesystemOptions({ query: { path: folderPath } }));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[fetchedFolders, loadingFolders, queryClient],
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (isLoading && fileArray.length === 0) {
|
if (fileBrowser.isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="border rounded-lg overflow-hidden">
|
<div className="border rounded-lg overflow-hidden">
|
||||||
<ScrollArea className="h-64">
|
<ScrollArea className="h-64">
|
||||||
@@ -96,7 +37,7 @@ export const DirectoryBrowser = ({ onSelectPath, selectedPath }: Props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileArray.length === 0) {
|
if (fileBrowser.isEmpty) {
|
||||||
return (
|
return (
|
||||||
<div className="border rounded-lg overflow-hidden">
|
<div className="border rounded-lg overflow-hidden">
|
||||||
<ScrollArea className="h-64">
|
<ScrollArea className="h-64">
|
||||||
@@ -110,11 +51,11 @@ export const DirectoryBrowser = ({ onSelectPath, selectedPath }: Props) => {
|
|||||||
<div className="border rounded-lg overflow-hidden">
|
<div className="border rounded-lg overflow-hidden">
|
||||||
<ScrollArea className="h-64">
|
<ScrollArea className="h-64">
|
||||||
<FileTree
|
<FileTree
|
||||||
files={fileArray}
|
files={fileBrowser.fileArray}
|
||||||
onFolderExpand={handleFolderExpand}
|
onFolderExpand={fileBrowser.handleFolderExpand}
|
||||||
onFolderHover={handleFolderHover}
|
onFolderHover={fileBrowser.handleFolderHover}
|
||||||
expandedFolders={expandedFolders}
|
expandedFolders={fileBrowser.expandedFolders}
|
||||||
loadingFolders={loadingFolders}
|
loadingFolders={fileBrowser.loadingFolders}
|
||||||
foldersOnly
|
foldersOnly
|
||||||
selectableFolders
|
selectableFolders
|
||||||
selectedFolder={selectedPath}
|
selectedFolder={selectedPath}
|
||||||
|
|||||||
@@ -1,16 +1,8 @@
|
|||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { FolderOpen } from "lucide-react";
|
import { FolderOpen } from "lucide-react";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
|
||||||
import { FileTree } from "~/client/components/file-tree";
|
import { FileTree } from "~/client/components/file-tree";
|
||||||
import { listFilesOptions } from "../api-client/@tanstack/react-query.gen";
|
import { listFilesOptions } from "../api-client/@tanstack/react-query.gen";
|
||||||
|
import { useFileBrowser } from "../hooks/use-file-browser";
|
||||||
interface FileEntry {
|
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
type: "file" | "directory";
|
|
||||||
size?: number;
|
|
||||||
modifiedAt?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
type VolumeFileBrowserProps = {
|
type VolumeFileBrowserProps = {
|
||||||
volumeName: string;
|
volumeName: string;
|
||||||
@@ -36,89 +28,34 @@ export const VolumeFileBrowser = ({
|
|||||||
emptyDescription,
|
emptyDescription,
|
||||||
}: VolumeFileBrowserProps) => {
|
}: VolumeFileBrowserProps) => {
|
||||||
const queryClient = useQueryClient();
|
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());
|
|
||||||
const [allFiles, setAllFiles] = useState<Map<string, FileEntry>>(new Map());
|
|
||||||
|
|
||||||
const { data, isLoading, error } = useQuery({
|
const { data, isLoading, error } = useQuery({
|
||||||
...listFilesOptions({ path: { name: volumeName } }),
|
...listFilesOptions({ path: { name: volumeName } }),
|
||||||
enabled,
|
enabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
useMemo(() => {
|
const fileBrowser = useFileBrowser({
|
||||||
if (data?.files) {
|
initialData: data,
|
||||||
setAllFiles((prev) => {
|
isLoading,
|
||||||
const next = new Map(prev);
|
fetchFolder: async (path) => {
|
||||||
for (const file of data.files) {
|
return await queryClient.ensureQueryData(
|
||||||
next.set(file.path, file);
|
listFilesOptions({
|
||||||
}
|
path: { name: volumeName },
|
||||||
return next;
|
query: { path },
|
||||||
});
|
}),
|
||||||
}
|
);
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
const fileArray = useMemo(() => Array.from(allFiles.values()), [allFiles]);
|
|
||||||
|
|
||||||
const handleFolderExpand = useCallback(
|
|
||||||
async (folderPath: string) => {
|
|
||||||
setExpandedFolders((prev) => {
|
|
||||||
const next = new Set(prev);
|
|
||||||
next.add(folderPath);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!fetchedFolders.has(folderPath)) {
|
|
||||||
setLoadingFolders((prev) => new Set(prev).add(folderPath));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await queryClient.ensureQueryData(
|
|
||||||
listFilesOptions({
|
|
||||||
path: { name: volumeName },
|
|
||||||
query: { path: folderPath },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.files) {
|
|
||||||
setAllFiles((prev) => {
|
|
||||||
const next = new Map(prev);
|
|
||||||
for (const file of result.files) {
|
|
||||||
next.set(file.path, file);
|
|
||||||
}
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
|
|
||||||
setFetchedFolders((prev) => new Set(prev).add(folderPath));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to fetch folder contents:", error);
|
|
||||||
} finally {
|
|
||||||
setLoadingFolders((prev) => {
|
|
||||||
const next = new Set(prev);
|
|
||||||
next.delete(folderPath);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[volumeName, fetchedFolders, queryClient.ensureQueryData],
|
prefetchFolder: (path) => {
|
||||||
);
|
queryClient.prefetchQuery(
|
||||||
|
listFilesOptions({
|
||||||
const handleFolderHover = useCallback(
|
path: { name: volumeName },
|
||||||
(folderPath: string) => {
|
query: { path },
|
||||||
if (!fetchedFolders.has(folderPath) && !loadingFolders.has(folderPath)) {
|
}),
|
||||||
queryClient.prefetchQuery(
|
);
|
||||||
listFilesOptions({
|
|
||||||
path: { name: volumeName },
|
|
||||||
query: { path: folderPath },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[volumeName, fetchedFolders, loadingFolders, queryClient],
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (isLoading && fileArray.length === 0) {
|
if (fileBrowser.isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-full min-h-[200px]">
|
<div className="flex items-center justify-center h-full min-h-[200px]">
|
||||||
<p className="text-muted-foreground">Loading files...</p>
|
<p className="text-muted-foreground">Loading files...</p>
|
||||||
@@ -134,7 +71,7 @@ export const VolumeFileBrowser = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileArray.length === 0) {
|
if (fileBrowser.isEmpty) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center h-full text-center p-8 min-h-[200px]">
|
<div className="flex flex-col items-center justify-center h-full text-center p-8 min-h-[200px]">
|
||||||
<FolderOpen className="mb-4 h-12 w-12 text-muted-foreground" />
|
<FolderOpen className="mb-4 h-12 w-12 text-muted-foreground" />
|
||||||
@@ -147,11 +84,11 @@ export const VolumeFileBrowser = ({
|
|||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<FileTree
|
<FileTree
|
||||||
files={fileArray}
|
files={fileBrowser.fileArray}
|
||||||
onFolderExpand={handleFolderExpand}
|
onFolderExpand={fileBrowser.handleFolderExpand}
|
||||||
onFolderHover={handleFolderHover}
|
onFolderHover={fileBrowser.handleFolderHover}
|
||||||
expandedFolders={expandedFolders}
|
expandedFolders={fileBrowser.expandedFolders}
|
||||||
loadingFolders={loadingFolders}
|
loadingFolders={fileBrowser.loadingFolders}
|
||||||
withCheckboxes={withCheckboxes}
|
withCheckboxes={withCheckboxes}
|
||||||
selectedPaths={selectedPaths}
|
selectedPaths={selectedPaths}
|
||||||
onSelectionChange={onSelectionChange}
|
onSelectionChange={onSelectionChange}
|
||||||
|
|||||||
141
app/client/hooks/use-file-browser.ts
Normal file
141
app/client/hooks/use-file-browser.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import type { FileEntry } from "../components/file-tree";
|
||||||
|
|
||||||
|
type FetchFolderFn = (
|
||||||
|
path: string,
|
||||||
|
) => Promise<{ files?: FileEntry[]; directories?: Array<{ name: string; path: string }> }>;
|
||||||
|
|
||||||
|
type PathTransformFns = {
|
||||||
|
strip?: (path: string) => string;
|
||||||
|
add?: (path: string) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UseFileBrowserOptions = {
|
||||||
|
initialData?: { files?: FileEntry[]; directories?: Array<{ name: string; path: string }> };
|
||||||
|
isLoading?: boolean;
|
||||||
|
fetchFolder: FetchFolderFn;
|
||||||
|
prefetchFolder?: (path: string) => void;
|
||||||
|
pathTransform?: PathTransformFns;
|
||||||
|
rootPath?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFileBrowser = ({
|
||||||
|
initialData,
|
||||||
|
isLoading = false,
|
||||||
|
fetchFolder,
|
||||||
|
prefetchFolder,
|
||||||
|
pathTransform,
|
||||||
|
rootPath = "/",
|
||||||
|
}: UseFileBrowserOptions) => {
|
||||||
|
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
|
||||||
|
const [fetchedFolders, setFetchedFolders] = useState<Set<string>>(new Set([rootPath]));
|
||||||
|
const [loadingFolders, setLoadingFolders] = useState<Set<string>>(new Set());
|
||||||
|
const [allFiles, setAllFiles] = useState<Map<string, FileEntry>>(new Map());
|
||||||
|
|
||||||
|
const stripPath = pathTransform?.strip;
|
||||||
|
const addPath = pathTransform?.add;
|
||||||
|
|
||||||
|
useMemo(() => {
|
||||||
|
if (initialData?.files) {
|
||||||
|
const files = initialData.files;
|
||||||
|
setAllFiles((prev) => {
|
||||||
|
const next = new Map(prev);
|
||||||
|
for (const file of files) {
|
||||||
|
const path = stripPath ? stripPath(file.path) : file.path;
|
||||||
|
if (path !== rootPath) {
|
||||||
|
next.set(path, { ...file, path });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
if (rootPath) {
|
||||||
|
setFetchedFolders((prev) => new Set(prev).add(rootPath));
|
||||||
|
}
|
||||||
|
} else if (initialData?.directories) {
|
||||||
|
const directories = initialData.directories;
|
||||||
|
setAllFiles((prev) => {
|
||||||
|
const next = new Map(prev);
|
||||||
|
for (const dir of directories) {
|
||||||
|
next.set(dir.path, { name: dir.name, path: dir.path, type: "folder" });
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [initialData, stripPath, rootPath]);
|
||||||
|
|
||||||
|
const fileArray = useMemo(() => Array.from(allFiles.values()), [allFiles]);
|
||||||
|
|
||||||
|
const handleFolderExpand = useCallback(
|
||||||
|
async (folderPath: string) => {
|
||||||
|
setExpandedFolders((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.add(folderPath);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!fetchedFolders.has(folderPath)) {
|
||||||
|
setLoadingFolders((prev) => new Set(prev).add(folderPath));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pathToFetch = addPath ? addPath(folderPath) : folderPath;
|
||||||
|
const result = await fetchFolder(pathToFetch);
|
||||||
|
|
||||||
|
if (result.files) {
|
||||||
|
const files = result.files;
|
||||||
|
setAllFiles((prev) => {
|
||||||
|
const next = new Map(prev);
|
||||||
|
for (const file of files) {
|
||||||
|
const strippedPath = stripPath ? stripPath(file.path) : file.path;
|
||||||
|
// Skip the directory itself
|
||||||
|
if (strippedPath !== folderPath) {
|
||||||
|
next.set(strippedPath, { ...file, path: strippedPath });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
} else if (result.directories) {
|
||||||
|
const directories = result.directories;
|
||||||
|
setAllFiles((prev) => {
|
||||||
|
const next = new Map(prev);
|
||||||
|
for (const dir of directories) {
|
||||||
|
next.set(dir.path, { name: dir.name, path: dir.path, type: "folder" });
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setFetchedFolders((prev) => new Set(prev).add(folderPath));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch folder contents:", error);
|
||||||
|
} finally {
|
||||||
|
setLoadingFolders((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.delete(folderPath);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[fetchedFolders, fetchFolder, stripPath, addPath],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFolderHover = useCallback(
|
||||||
|
(folderPath: string) => {
|
||||||
|
if (!fetchedFolders.has(folderPath) && !loadingFolders.has(folderPath) && prefetchFolder) {
|
||||||
|
const pathToPrefetch = addPath ? addPath(folderPath) : folderPath;
|
||||||
|
prefetchFolder(pathToPrefetch);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[fetchedFolders, loadingFolders, prefetchFolder, addPath],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fileArray,
|
||||||
|
expandedFolders,
|
||||||
|
loadingFolders,
|
||||||
|
handleFolderExpand,
|
||||||
|
handleFolderHover,
|
||||||
|
isLoading: isLoading && fileArray.length === 0,
|
||||||
|
isEmpty: fileArray.length === 0 && !isLoading,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { FileIcon } from "lucide-react";
|
import { FileIcon } from "lucide-react";
|
||||||
import { FileTree, type FileEntry } from "~/client/components/file-tree";
|
import { FileTree } from "~/client/components/file-tree";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
|
||||||
import { Button } from "~/client/components/ui/button";
|
import { Button } from "~/client/components/ui/button";
|
||||||
import { Checkbox } from "~/client/components/ui/checkbox";
|
import { Checkbox } from "~/client/components/ui/checkbox";
|
||||||
@@ -20,6 +20,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "~/client/components/ui/
|
|||||||
import type { Snapshot, Volume } from "~/client/lib/types";
|
import type { Snapshot, Volume } from "~/client/lib/types";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { listSnapshotFilesOptions, restoreSnapshotMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
import { listSnapshotFilesOptions, restoreSnapshotMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
|
import { useFileBrowser } from "~/client/hooks/use-file-browser";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
snapshot: Snapshot;
|
snapshot: Snapshot;
|
||||||
@@ -33,10 +34,6 @@ export const SnapshotFileBrowser = (props: Props) => {
|
|||||||
const isReadOnly = volume?.config && "readOnly" in volume.config && volume.config.readOnly === true;
|
const isReadOnly = volume?.config && "readOnly" in volume.config && volume.config.readOnly === true;
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
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());
|
|
||||||
const [allFiles, setAllFiles] = useState<Map<string, FileEntry>>(new Map());
|
|
||||||
const [selectedPaths, setSelectedPaths] = useState<Set<string>>(new Set());
|
const [selectedPaths, setSelectedPaths] = useState<Set<string>>(new Set());
|
||||||
const [showRestoreDialog, setShowRestoreDialog] = useState(false);
|
const [showRestoreDialog, setShowRestoreDialog] = useState(false);
|
||||||
const [deleteExtraFiles, setDeleteExtraFiles] = useState(false);
|
const [deleteExtraFiles, setDeleteExtraFiles] = useState(false);
|
||||||
@@ -72,89 +69,30 @@ export const SnapshotFileBrowser = (props: Props) => {
|
|||||||
[volumeBasePath],
|
[volumeBasePath],
|
||||||
);
|
);
|
||||||
|
|
||||||
useMemo(() => {
|
const fileBrowser = useFileBrowser({
|
||||||
if (filesData?.files) {
|
initialData: filesData,
|
||||||
setAllFiles((prev) => {
|
isLoading: filesLoading,
|
||||||
const next = new Map(prev);
|
fetchFolder: async (path) => {
|
||||||
for (const file of filesData.files) {
|
return await queryClient.ensureQueryData(
|
||||||
const strippedPath = stripBasePath(file.path);
|
listSnapshotFilesOptions({
|
||||||
if (strippedPath !== "/") {
|
path: { name: repositoryName, snapshotId: snapshot.short_id },
|
||||||
next.set(strippedPath, { ...file, path: strippedPath });
|
query: { path },
|
||||||
}
|
}),
|
||||||
}
|
);
|
||||||
return next;
|
|
||||||
});
|
|
||||||
setFetchedFolders((prev) => new Set(prev).add("/"));
|
|
||||||
}
|
|
||||||
}, [filesData, stripBasePath]);
|
|
||||||
|
|
||||||
const fileArray = useMemo(() => Array.from(allFiles.values()), [allFiles]);
|
|
||||||
|
|
||||||
const handleFolderExpand = useCallback(
|
|
||||||
async (folderPath: string) => {
|
|
||||||
setExpandedFolders((prev) => {
|
|
||||||
const next = new Set(prev);
|
|
||||||
next.add(folderPath);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!fetchedFolders.has(folderPath)) {
|
|
||||||
setLoadingFolders((prev) => new Set(prev).add(folderPath));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const fullPath = addBasePath(folderPath);
|
|
||||||
|
|
||||||
const result = await queryClient.ensureQueryData(
|
|
||||||
listSnapshotFilesOptions({
|
|
||||||
path: { name: repositoryName, snapshotId: snapshot.short_id },
|
|
||||||
query: { path: fullPath },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.files) {
|
|
||||||
setAllFiles((prev) => {
|
|
||||||
const next = new Map(prev);
|
|
||||||
for (const file of result.files) {
|
|
||||||
const strippedPath = stripBasePath(file.path);
|
|
||||||
// Skip the directory itself
|
|
||||||
if (strippedPath !== folderPath) {
|
|
||||||
next.set(strippedPath, { ...file, path: strippedPath });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
|
|
||||||
setFetchedFolders((prev) => new Set(prev).add(folderPath));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to fetch folder contents:", error);
|
|
||||||
} finally {
|
|
||||||
setLoadingFolders((prev) => {
|
|
||||||
const next = new Set(prev);
|
|
||||||
next.delete(folderPath);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[repositoryName, snapshot, fetchedFolders, queryClient, stripBasePath, addBasePath],
|
prefetchFolder: (path) => {
|
||||||
);
|
queryClient.prefetchQuery(
|
||||||
|
listSnapshotFilesOptions({
|
||||||
const handleFolderHover = useCallback(
|
path: { name: repositoryName, snapshotId: snapshot.short_id },
|
||||||
(folderPath: string) => {
|
query: { path },
|
||||||
if (!fetchedFolders.has(folderPath) && !loadingFolders.has(folderPath)) {
|
}),
|
||||||
const fullPath = addBasePath(folderPath);
|
);
|
||||||
|
|
||||||
queryClient.prefetchQuery({
|
|
||||||
...listSnapshotFilesOptions({
|
|
||||||
path: { name: repositoryName, snapshotId: snapshot.short_id },
|
|
||||||
query: { path: fullPath },
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[repositoryName, snapshot, fetchedFolders, loadingFolders, queryClient, addBasePath],
|
pathTransform: {
|
||||||
);
|
strip: stripBasePath,
|
||||||
|
add: addBasePath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const { mutate: restoreSnapshot, isPending: isRestoring } = useMutation({
|
const { mutate: restoreSnapshot, isPending: isRestoring } = useMutation({
|
||||||
...restoreSnapshotMutation(),
|
...restoreSnapshotMutation(),
|
||||||
@@ -225,27 +163,27 @@ export const SnapshotFileBrowser = (props: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex-1 overflow-hidden flex flex-col p-0">
|
<CardContent className="flex-1 overflow-hidden flex flex-col p-0">
|
||||||
{filesLoading && fileArray.length === 0 && (
|
{fileBrowser.isLoading && (
|
||||||
<div className="flex items-center justify-center flex-1">
|
<div className="flex items-center justify-center flex-1">
|
||||||
<p className="text-muted-foreground">Loading files...</p>
|
<p className="text-muted-foreground">Loading files...</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{fileArray.length === 0 && !filesLoading && (
|
{fileBrowser.isEmpty && (
|
||||||
<div className="flex flex-col items-center justify-center flex-1 text-center p-8">
|
<div className="flex flex-col items-center justify-center flex-1 text-center p-8">
|
||||||
<FileIcon className="w-12 h-12 text-muted-foreground/50 mb-4" />
|
<FileIcon className="w-12 h-12 text-muted-foreground/50 mb-4" />
|
||||||
<p className="text-muted-foreground">No files in this snapshot</p>
|
<p className="text-muted-foreground">No files in this snapshot</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{fileArray.length > 0 && (
|
{!fileBrowser.isLoading && !fileBrowser.isEmpty && (
|
||||||
<div className="overflow-auto flex-1 border border-border rounded-md bg-card m-4">
|
<div className="overflow-auto flex-1 border border-border rounded-md bg-card m-4">
|
||||||
<FileTree
|
<FileTree
|
||||||
files={fileArray}
|
files={fileBrowser.fileArray}
|
||||||
onFolderExpand={handleFolderExpand}
|
onFolderExpand={fileBrowser.handleFolderExpand}
|
||||||
onFolderHover={handleFolderHover}
|
onFolderHover={fileBrowser.handleFolderHover}
|
||||||
expandedFolders={expandedFolders}
|
expandedFolders={fileBrowser.expandedFolders}
|
||||||
loadingFolders={loadingFolders}
|
loadingFolders={fileBrowser.loadingFolders}
|
||||||
className="px-2 py-2"
|
className="px-2 py-2"
|
||||||
withCheckboxes={true}
|
withCheckboxes={true}
|
||||||
selectedPaths={selectedPaths}
|
selectedPaths={selectedPaths}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ services:
|
|||||||
- /var/lib/ironmount:/var/lib/ironmount
|
- /var/lib/ironmount:/var/lib/ironmount
|
||||||
|
|
||||||
- ./app:/app/app
|
- ./app:/app/app
|
||||||
# - ~/.config/rclone:/root/.config/rclone
|
- ~/.config/rclone:/root/.config/rclone
|
||||||
|
|
||||||
ironmount-prod:
|
ironmount-prod:
|
||||||
build:
|
build:
|
||||||
|
|||||||
Reference in New Issue
Block a user