diff --git a/app/client/components/path-selector.tsx b/app/client/components/path-selector.tsx
index bbd6292b..5d8132bc 100644
--- a/app/client/components/path-selector.tsx
+++ b/app/client/components/path-selector.tsx
@@ -28,7 +28,6 @@ export const PathSelector = ({ value, onChange }: Props) => {
);
}
- console.log("Rendering PathSelector with value:", value);
return (
{value}
diff --git a/app/client/components/restore-form.tsx b/app/client/components/restore-form.tsx
new file mode 100644
index 00000000..9055de3d
--- /dev/null
+++ b/app/client/components/restore-form.tsx
@@ -0,0 +1,326 @@
+import { useCallback, useState } from "react";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { useNavigate } from "react-router";
+import { toast } from "sonner";
+import { ChevronDown, FileIcon, FolderOpen, RotateCcw } from "lucide-react";
+import { Button } from "~/client/components/ui/button";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
+import { Checkbox } from "~/client/components/ui/checkbox";
+import { Input } from "~/client/components/ui/input";
+import { Label } from "~/client/components/ui/label";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
+import { PathSelector } from "~/client/components/path-selector";
+import { FileTree } from "~/client/components/file-tree";
+import { listSnapshotFilesOptions, restoreSnapshotMutation } from "~/client/api-client/@tanstack/react-query.gen";
+import { useFileBrowser } from "~/client/hooks/use-file-browser";
+import { OVERWRITE_MODES, type OverwriteMode } from "~/schemas/restic";
+import type { Snapshot } from "~/client/lib/types";
+
+type RestoreLocation = "original" | "custom";
+
+interface RestoreFormProps {
+ snapshot: Snapshot;
+ repositoryName: string;
+ snapshotId: string;
+ /** Path to navigate to after cancel or successful restore */
+ returnPath: string;
+}
+
+export function RestoreForm({ snapshot, repositoryName, snapshotId, returnPath }: RestoreFormProps) {
+ const navigate = useNavigate();
+ const queryClient = useQueryClient();
+
+ const volumeBasePath = snapshot.paths[0]?.match(/^(.*?_data)(\/|$)/)?.[1] || "/";
+
+ const [restoreLocation, setRestoreLocation] = useState
("original");
+ const [customTargetPath, setCustomTargetPath] = useState("");
+ const [overwriteMode, setOverwriteMode] = useState("always");
+ const [showAdvanced, setShowAdvanced] = useState(false);
+ const [excludeXattr, setExcludeXattr] = useState("");
+ const [deleteExtraFiles, setDeleteExtraFiles] = useState(false);
+
+ const [selectedPaths, setSelectedPaths] = useState>(new Set());
+
+ const { data: filesData, isLoading: filesLoading } = useQuery({
+ ...listSnapshotFilesOptions({
+ path: { name: repositoryName, snapshotId },
+ query: { path: volumeBasePath },
+ }),
+ enabled: !!repositoryName && !!snapshotId,
+ });
+
+ const stripBasePath = useCallback(
+ (path: string): string => {
+ if (!volumeBasePath) return path;
+ if (path === volumeBasePath) return "/";
+ if (path.startsWith(`${volumeBasePath}/`)) {
+ const stripped = path.slice(volumeBasePath.length);
+ return stripped;
+ }
+ return path;
+ },
+ [volumeBasePath],
+ );
+
+ const addBasePath = useCallback(
+ (displayPath: string): string => {
+ const vbp = volumeBasePath === "/" ? "" : volumeBasePath;
+
+ if (!vbp) return displayPath;
+ if (displayPath === "/") return vbp;
+ return `${vbp}${displayPath}`;
+ },
+ [volumeBasePath],
+ );
+
+ const fileBrowser = useFileBrowser({
+ initialData: filesData,
+ isLoading: filesLoading,
+ fetchFolder: async (path) => {
+ return await queryClient.ensureQueryData(
+ listSnapshotFilesOptions({
+ path: { name: repositoryName, snapshotId },
+ query: { path },
+ }),
+ );
+ },
+ prefetchFolder: (path) => {
+ queryClient.prefetchQuery(
+ listSnapshotFilesOptions({
+ path: { name: repositoryName, snapshotId },
+ query: { path },
+ }),
+ );
+ },
+ pathTransform: {
+ strip: stripBasePath,
+ add: addBasePath,
+ },
+ });
+
+ const { mutate: restoreSnapshot, isPending: isRestoring } = useMutation({
+ ...restoreSnapshotMutation(),
+ onSuccess: (data) => {
+ toast.success("Restore completed", {
+ description: `Successfully restored ${data.filesRestored} file(s). ${data.filesSkipped} file(s) skipped.`,
+ });
+ navigate(returnPath);
+ },
+ onError: (error) => {
+ toast.error("Restore failed", { description: error.message || "Failed to restore snapshot" });
+ },
+ });
+
+ const handleRestore = useCallback(() => {
+ if (!repositoryName || !snapshotId) return;
+
+ const excludeXattrArray = excludeXattr
+ ?.split(",")
+ .map((s) => s.trim())
+ .filter(Boolean);
+
+ const isCustomLocation = restoreLocation === "custom";
+ const targetPath = isCustomLocation && customTargetPath.trim() ? customTargetPath.trim() : undefined;
+
+ const pathsArray = Array.from(selectedPaths);
+ const includePaths = pathsArray.map((path) => addBasePath(path));
+
+ restoreSnapshot({
+ path: { name: repositoryName },
+ body: {
+ snapshotId,
+ include: includePaths.length > 0 ? includePaths : undefined,
+ delete: deleteExtraFiles,
+ excludeXattr: excludeXattrArray && excludeXattrArray.length > 0 ? excludeXattrArray : undefined,
+ targetPath,
+ overwrite: overwriteMode,
+ },
+ });
+ }, [
+ repositoryName,
+ snapshotId,
+ excludeXattr,
+ restoreLocation,
+ customTargetPath,
+ selectedPaths,
+ addBasePath,
+ deleteExtraFiles,
+ overwriteMode,
+ restoreSnapshot,
+ ]);
+
+ const canRestore = restoreLocation === "original" || customTargetPath.trim();
+
+ return (
+
+
+
+
Restore Snapshot
+
+ {repositoryName} / {snapshotId}
+
+
+
+ navigate(returnPath)}>
+ Cancel
+
+
+ {isRestoring
+ ? "Restoring..."
+ : selectedPaths.size > 0
+ ? `Restore ${selectedPaths.size} ${selectedPaths.size === 1 ? "item" : "items"}`
+ : "Restore All"}
+
+
+
+
+
+
+
+
+ Restore Location
+ Choose where to restore the files
+
+
+
+ setRestoreLocation("original")}
+ >
+
+ Original location
+
+ setRestoreLocation("custom")}
+ >
+
+ Custom location
+
+
+ {restoreLocation === "custom" && (
+
+
+
Files will be restored directly to this path
+
+ )}
+
+
+
+
+
+ Overwrite Mode
+ How to handle existing files
+
+
+ setOverwriteMode(value as OverwriteMode)}>
+
+
+
+
+ Always overwrite
+ Only if content changed
+ Only if snapshot is newer
+ Never overwrite
+
+
+
+ {overwriteMode === OVERWRITE_MODES.always &&
+ "Existing files will always be replaced with the snapshot version."}
+ {overwriteMode === OVERWRITE_MODES.ifChanged &&
+ "Files are only replaced if their content differs from the snapshot."}
+ {overwriteMode === OVERWRITE_MODES.ifNewer &&
+ "Files are only replaced if the snapshot version has a newer modification time."}
+ {overwriteMode === OVERWRITE_MODES.never &&
+ "Existing files will never be replaced, only missing files are restored."}
+
+
+
+
+
+ setShowAdvanced(!showAdvanced)}>
+
+ Advanced options
+
+
+
+ {showAdvanced && (
+
+
+
+ Exclude extended attributes
+
+
setExcludeXattr(e.target.value)}
+ />
+
+ Exclude specific extended attributes during restore (comma-separated)
+
+
+
+
+
+ )}
+
+
+
+
+ Select Files to Restore
+
+ {selectedPaths.size > 0
+ ? `${selectedPaths.size} ${selectedPaths.size === 1 ? "item" : "items"} selected`
+ : "Select specific files or folders, or leave empty to restore everything"}
+
+
+
+ {fileBrowser.isLoading && (
+
+ )}
+
+ {fileBrowser.isEmpty && (
+
+
+
No files in this snapshot
+
+ )}
+
+ {!fileBrowser.isLoading && !fileBrowser.isEmpty && (
+
+
+
+ )}
+
+
+
+
+ );
+}
diff --git a/app/client/components/restore-snapshot-dialog.tsx b/app/client/components/restore-snapshot-dialog.tsx
deleted file mode 100644
index 14ebb88d..00000000
--- a/app/client/components/restore-snapshot-dialog.tsx
+++ /dev/null
@@ -1,198 +0,0 @@
-import { useCallback, useState } from "react";
-import { ChevronDown, FolderOpen, RotateCcw } from "lucide-react";
-import { Button } from "~/client/components/ui/button";
-import { Checkbox } from "~/client/components/ui/checkbox";
-import { Label } from "~/client/components/ui/label";
-import { Input } from "~/client/components/ui/input";
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
-import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
- AlertDialogTrigger,
-} from "~/client/components/ui/alert-dialog";
-import { PathSelector } from "~/client/components/path-selector";
-import { OVERWRITE_MODES, type OverwriteMode } from "~/schemas/restic";
-
-type RestoreLocation = "original" | "custom";
-
-export interface RestoreSnapshotOptions {
- include?: string[];
- delete?: boolean;
- excludeXattr?: string[];
- targetPath?: string;
- overwrite?: OverwriteMode;
-}
-
-interface Props {
- /** Number of selected items to restore (0 means restore everything) */
- selectedCount?: number;
- /** Callback when restore is confirmed */
- onConfirm: (options: RestoreSnapshotOptions) => void;
- /** Trigger element for the dialog */
- trigger: React.ReactNode;
- /** Pre-selected paths to include in restore (already transformed) */
- includePaths?: string[];
-}
-
-export const RestoreSnapshotDialog = (props: Props) => {
- const { selectedCount = 0, onConfirm, trigger, includePaths } = props;
-
- const [open, setOpen] = useState(false);
- const [deleteExtraFiles, setDeleteExtraFiles] = useState(false);
- const [showAdvanced, setShowAdvanced] = useState(false);
- const [excludeXattr, setExcludeXattr] = useState("");
- const [restoreLocation, setRestoreLocation] = useState("original");
- const [customTargetPath, setCustomTargetPath] = useState("");
- const [overwriteMode, setOverwriteMode] = useState("always");
-
- const handleConfirmRestore = useCallback(() => {
- const excludeXattrArray = excludeXattr
- ?.split(",")
- .map((s) => s.trim())
- .filter(Boolean);
-
- const isCustomLocation = restoreLocation === "custom";
- const targetPath = isCustomLocation && customTargetPath.trim() ? customTargetPath.trim() : undefined;
-
- onConfirm({
- include: includePaths && includePaths.length > 0 ? includePaths : undefined,
- delete: deleteExtraFiles,
- excludeXattr: excludeXattrArray && excludeXattrArray.length > 0 ? excludeXattrArray : undefined,
- targetPath,
- overwrite: overwriteMode,
- });
-
- setOpen(false);
- }, [excludeXattr, restoreLocation, customTargetPath, includePaths, deleteExtraFiles, overwriteMode, onConfirm]);
-
- return (
-
- {trigger}
-
-
- Confirm Restore
-
- {selectedCount > 0
- ? `This will restore ${selectedCount} selected ${selectedCount === 1 ? "item" : "items"} from the snapshot.`
- : "This will restore everything from the snapshot."}
-
-
-
-
-
Restore Location
-
- setRestoreLocation("original")}
- >
-
- Original location
-
- setRestoreLocation("custom")}
- >
-
- Custom location
-
-
- {restoreLocation === "custom" && (
-
-
-
Files will be restored directly to this path
-
- )}
-
-
-
-
Overwrite Mode
-
setOverwriteMode(value as OverwriteMode)}>
-
-
-
-
- Always overwrite
- Only if content changed
- Only if snapshot is newer
- Never overwrite
-
-
-
- {overwriteMode === OVERWRITE_MODES.always &&
- "Existing files will always be replaced with the snapshot version."}
- {overwriteMode === OVERWRITE_MODES.ifChanged &&
- "Files are only replaced if their content differs from the snapshot."}
- {overwriteMode === OVERWRITE_MODES.ifNewer &&
- "Files are only replaced if the snapshot version has a newer modification time."}
- {overwriteMode === OVERWRITE_MODES.never &&
- "Existing files will never be replaced, only missing files are restored."}
-
-
-
-
-
setShowAdvanced(!showAdvanced)}
- className="h-auto p-0 text-sm font-normal"
- >
- Advanced Options
-
-
-
- {showAdvanced && (
-
-
-
- Exclude Extended Attributes
-
-
setExcludeXattr(e.target.value)}
- />
-
- Exclude specific extended attributes during restore (comma-separated)
-
-
-
-
-
- )}
-
-
-
- Cancel
-
- Confirm Restore
-
-
-
-
- );
-};
diff --git a/app/client/modules/backups/components/snapshot-file-browser.tsx b/app/client/modules/backups/components/snapshot-file-browser.tsx
index 6025a7b8..752ef71e 100644
--- a/app/client/modules/backups/components/snapshot-file-browser.tsx
+++ b/app/client/modules/backups/components/snapshot-file-browser.tsx
@@ -1,31 +1,27 @@
-import { useCallback, useState } from "react";
-import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { useCallback } from "react";
+import { useQuery, useQueryClient } from "@tanstack/react-query";
import { FileIcon } from "lucide-react";
+import { Link } from "react-router";
import { FileTree } from "~/client/components/file-tree";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
-import { Button } from "~/client/components/ui/button";
-import { Tooltip, TooltipContent, TooltipTrigger } from "~/client/components/ui/tooltip";
-import { RestoreSnapshotDialog, type RestoreSnapshotOptions } from "~/client/components/restore-snapshot-dialog";
-import type { Snapshot, Volume } from "~/client/lib/types";
-import { toast } from "sonner";
-import { listSnapshotFilesOptions, restoreSnapshotMutation } from "~/client/api-client/@tanstack/react-query.gen";
+import { Button, buttonVariants } from "~/client/components/ui/button";
+import type { Snapshot } from "~/client/lib/types";
+import { listSnapshotFilesOptions } from "~/client/api-client/@tanstack/react-query.gen";
import { useFileBrowser } from "~/client/hooks/use-file-browser";
interface Props {
snapshot: Snapshot;
repositoryName: string;
- volume?: Volume;
+ /** If provided, restore link will use /backups/:backupId/:snapshotId/restore route */
+ backupId?: string;
onDeleteSnapshot?: (snapshotId: string) => void;
isDeletingSnapshot?: boolean;
}
export const SnapshotFileBrowser = (props: Props) => {
- const { snapshot, repositoryName, volume, onDeleteSnapshot, isDeletingSnapshot } = props;
-
- const isReadOnly = volume?.config && "readOnly" in volume.config && volume.config.readOnly === true;
+ const { snapshot, repositoryName, backupId, onDeleteSnapshot, isDeletingSnapshot } = props;
const queryClient = useQueryClient();
- const [selectedPaths, setSelectedPaths] = useState>(new Set());
const volumeBasePath = snapshot.paths[0]?.match(/^(.*?_data)(\/|$)/)?.[1] || "/";
@@ -85,39 +81,6 @@ export const SnapshotFileBrowser = (props: Props) => {
},
});
- const { mutate: restoreSnapshot, isPending: isRestoring } = useMutation({
- ...restoreSnapshotMutation(),
- onSuccess: (data) => {
- toast.success("Restore completed", {
- description: `Successfully restored ${data.filesRestored} file(s). ${data.filesSkipped} file(s) skipped.`,
- });
- setSelectedPaths(new Set());
- },
- onError: (error) => {
- toast.error("Restore failed", { description: error.message || "Failed to restore snapshot" });
- },
- });
-
- const handleConfirmRestore = useCallback(
- (options: RestoreSnapshotOptions) => {
- const pathsArray = Array.from(selectedPaths);
- const includePaths = pathsArray.map((path) => addBasePath(path));
-
- restoreSnapshot({
- path: { name: repositoryName },
- body: {
- snapshotId: snapshot.short_id,
- include: includePaths.length > 0 ? includePaths : undefined,
- delete: options.delete,
- excludeXattr: options.excludeXattr,
- targetPath: options.targetPath,
- overwrite: options.overwrite,
- },
- });
- },
- [selectedPaths, addBasePath, repositoryName, snapshot.short_id, restoreSnapshot],
- );
-
return (
@@ -128,31 +91,16 @@ export const SnapshotFileBrowser = (props: Props) => {
{`Viewing snapshot from ${new Date(snapshot?.time ?? 0).toLocaleString()}`}
- {selectedPaths.size > 0 && (
-
-
-
-
- {isRestoring
- ? "Restoring..."
- : `Restore ${selectedPaths.size} selected ${selectedPaths.size === 1 ? "item" : "items"}`}
-
- }
- />
-
-
- {isReadOnly && (
-
- Volume is mounted as read-only.
- Please remount with read-only disabled to restore files.
-
- )}
-
- )}
+
+ Restore
+
{onDeleteSnapshot && (
{
expandedFolders={fileBrowser.expandedFolders}
loadingFolders={fileBrowser.loadingFolders}
className="px-2 py-2"
- withCheckboxes={true}
- selectedPaths={selectedPaths}
- onSelectionChange={setSelectedPaths}
/>
)}
diff --git a/app/client/modules/backups/routes/backup-details.tsx b/app/client/modules/backups/routes/backup-details.tsx
index c7c82889..09433c1b 100644
--- a/app/client/modules/backups/routes/backup-details.tsx
+++ b/app/client/modules/backups/routes/backup-details.tsx
@@ -240,7 +240,7 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
key={selectedSnapshot?.short_id}
snapshot={selectedSnapshot}
repositoryName={schedule.repository.name}
- volume={schedule.volume}
+ backupId={schedule.id.toString()}
onDeleteSnapshot={handleDeleteSnapshot}
isDeletingSnapshot={deleteSnapshot.isPending}
/>
diff --git a/app/client/modules/backups/routes/restore-snapshot.tsx b/app/client/modules/backups/routes/restore-snapshot.tsx
new file mode 100644
index 00000000..0a8ce526
--- /dev/null
+++ b/app/client/modules/backups/routes/restore-snapshot.tsx
@@ -0,0 +1,54 @@
+import { redirect } from "react-router";
+import { getBackupSchedule, getSnapshotDetails } from "~/client/api-client";
+import { RestoreForm } from "~/client/components/restore-form";
+import type { Route } from "./+types/restore-snapshot";
+
+export const handle = {
+ breadcrumb: (match: Route.MetaArgs) => [
+ { label: "Backups", href: "/backups" },
+ { label: `Schedule #${match.params.id}`, href: `/backups/${match.params.id}` },
+ { label: match.params.snapshotId },
+ { label: "Restore" },
+ ],
+};
+
+export function meta({ params }: Route.MetaArgs) {
+ return [
+ { title: `Zerobyte - Restore Snapshot ${params.snapshotId}` },
+ {
+ name: "description",
+ content: "Restore files from a backup snapshot.",
+ },
+ ];
+}
+
+export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => {
+ const schedule = await getBackupSchedule({ path: { scheduleId: params.id } });
+ if (!schedule.data) return redirect("/backups");
+
+ const repositoryName = schedule.data.repository.name;
+ const snapshot = await getSnapshotDetails({
+ path: { name: repositoryName, snapshotId: params.snapshotId },
+ });
+ if (!snapshot.data) return redirect(`/backups/${params.id}`);
+
+ return {
+ snapshot: snapshot.data,
+ repositoryName,
+ snapshotId: params.snapshotId,
+ backupId: params.id,
+ };
+};
+
+export default function RestoreSnapshotFromBackupPage({ loaderData }: Route.ComponentProps) {
+ const { snapshot, repositoryName, snapshotId, backupId } = loaderData;
+
+ return (
+
+ );
+}
diff --git a/app/client/modules/repositories/routes/restore-snapshot.tsx b/app/client/modules/repositories/routes/restore-snapshot.tsx
new file mode 100644
index 00000000..a1d70754
--- /dev/null
+++ b/app/client/modules/repositories/routes/restore-snapshot.tsx
@@ -0,0 +1,45 @@
+import { redirect } from "react-router";
+import { getSnapshotDetails } from "~/client/api-client";
+import { RestoreForm } from "~/client/components/restore-form";
+import type { Route } from "./+types/restore-snapshot";
+
+export const handle = {
+ breadcrumb: (match: Route.MetaArgs) => [
+ { label: "Repositories", href: "/repositories" },
+ { label: match.params.name, href: `/repositories/${match.params.name}` },
+ { label: match.params.snapshotId, href: `/repositories/${match.params.name}/${match.params.snapshotId}` },
+ { label: "Restore" },
+ ],
+};
+
+export function meta({ params }: Route.MetaArgs) {
+ return [
+ { title: `Zerobyte - Restore Snapshot ${params.snapshotId}` },
+ {
+ name: "description",
+ content: "Restore files from a backup snapshot.",
+ },
+ ];
+}
+
+export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => {
+ const snapshot = await getSnapshotDetails({
+ path: { name: params.name, snapshotId: params.snapshotId },
+ });
+ if (snapshot.data) return { snapshot: snapshot.data, name: params.name, snapshotId: params.snapshotId };
+
+ return redirect("/repositories");
+};
+
+export default function RestoreSnapshotPage({ loaderData }: Route.ComponentProps) {
+ const { snapshot, name, snapshotId } = loaderData;
+
+ return (
+
+ );
+}
diff --git a/app/client/modules/repositories/routes/snapshot-details.tsx b/app/client/modules/repositories/routes/snapshot-details.tsx
index 8f2c9f7f..51f4294e 100644
--- a/app/client/modules/repositories/routes/snapshot-details.tsx
+++ b/app/client/modules/repositories/routes/snapshot-details.tsx
@@ -1,13 +1,8 @@
-import { useMutation, useQuery } from "@tanstack/react-query";
-import { redirect, useParams } from "react-router";
-import { toast } from "sonner";
-import { listSnapshotFilesOptions, restoreSnapshotMutation } from "~/client/api-client/@tanstack/react-query.gen";
-import { Button } from "~/client/components/ui/button";
+import { useQuery } from "@tanstack/react-query";
+import { Link, redirect, useParams } from "react-router";
+import { listSnapshotFilesOptions } from "~/client/api-client/@tanstack/react-query.gen";
+import { buttonVariants } from "~/client/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "~/client/components/ui/card";
-import {
- RestoreSnapshotDialog,
- type RestoreSnapshotOptions,
-} from "~/client/components/restore-snapshot-dialog";
import { SnapshotFileBrowser } from "~/client/modules/backups/components/snapshot-file-browser";
import { getSnapshotDetails } from "~/client/api-client";
import type { Route } from "./+types/snapshot-details";
@@ -53,34 +48,6 @@ export default function SnapshotDetailsPage({ loaderData }: Route.ComponentProps
enabled: !!name && !!snapshotId,
});
- const { mutate: restoreSnapshot, isPending: isRestoring } = useMutation({
- ...restoreSnapshotMutation(),
- onSuccess: (data) => {
- toast.success("Restore completed", {
- description: `Successfully restored ${data.filesRestored} file(s). ${data.filesSkipped} file(s) skipped.`,
- });
- },
- onError: (error) => {
- toast.error("Restore failed", { description: error.message || "Failed to restore snapshot" });
- },
- });
-
- const handleRestore = (options: RestoreSnapshotOptions) => {
- if (!name || !snapshotId) return;
-
- restoreSnapshot({
- path: { name },
- body: {
- snapshotId,
- include: options.include,
- delete: options.delete,
- excludeXattr: options.excludeXattr,
- targetPath: options.targetPath,
- overwrite: options.overwrite,
- },
- });
- };
-
if (!name || !snapshotId) {
return (
@@ -96,14 +63,6 @@ export default function SnapshotDetailsPage({ loaderData }: Route.ComponentProps
{name}
Snapshot: {snapshotId}
-
- {isRestoring ? "Restoring..." : "Restore Snapshot"}
-
- }
- />