diff --git a/app/client/components/restore-form.tsx b/app/client/components/restore-form.tsx index 77025816..4fcc334d 100644 --- a/app/client/components/restore-form.tsx +++ b/app/client/components/restore-form.tsx @@ -1,16 +1,26 @@ -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -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 { 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 { + AlertDialog, + AlertDialogAction, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "~/client/components/ui/alert-dialog"; import { PathSelector } from "~/client/components/path-selector"; import { FileTree } from "~/client/components/file-tree"; +import { RestoreProgress } from "~/client/components/restore-progress"; import { listSnapshotFilesOptions, restoreSnapshotMutation } from "~/client/api-client/@tanstack/react-query.gen"; import { useFileBrowser } from "~/client/hooks/use-file-browser"; +import { type RestoreCompletedEvent, useServerEvents } from "~/client/hooks/use-server-events"; import { OVERWRITE_MODES, type OverwriteMode } from "~/schemas/restic"; import type { Repository, Snapshot } from "~/client/lib/types"; import { handleRepositoryError } from "~/client/lib/errors"; @@ -27,6 +37,7 @@ interface RestoreFormProps { export function RestoreForm({ snapshot, repository, snapshotId, returnPath }: RestoreFormProps) { const navigate = useNavigate(); + const { addEventListener } = useServerEvents(); const queryClient = useQueryClient(); const volumeBasePath = snapshot.paths[0]?.match(/^(.*?_data)(\/|$)/)?.[1] || "/"; @@ -36,9 +47,48 @@ export function RestoreForm({ snapshot, repository, snapshotId, returnPath }: Re const [overwriteMode, setOverwriteMode] = useState("always"); const [showAdvanced, setShowAdvanced] = useState(false); const [excludeXattr, setExcludeXattr] = useState(""); + const [isRestoreActive, setIsRestoreActive] = useState(false); + const [restoreResult, setRestoreResult] = useState(null); + const [showRestoreResultAlert, setShowRestoreResultAlert] = useState(false); + const restoreCompletedRef = useRef(false); const [selectedPaths, setSelectedPaths] = useState>(new Set()); + useEffect(() => { + const unsubscribeStarted = addEventListener("restore:started", (startedData) => { + if (startedData.repositoryId === repository.id && startedData.snapshotId === snapshotId) { + restoreCompletedRef.current = false; + setIsRestoreActive(true); + setRestoreResult(null); + setShowRestoreResultAlert(false); + } + }); + + const unsubscribeProgress = addEventListener("restore:progress", (progressData) => { + if (progressData.repositoryId === repository.id && progressData.snapshotId === snapshotId) { + if (restoreCompletedRef.current) { + return; + } + setIsRestoreActive(true); + } + }); + + const unsubscribeCompleted = addEventListener("restore:completed", (completedData) => { + if (completedData.repositoryId === repository.id && completedData.snapshotId === snapshotId) { + restoreCompletedRef.current = true; + setIsRestoreActive(false); + setRestoreResult(completedData); + setShowRestoreResultAlert(true); + } + }); + + return () => { + unsubscribeStarted(); + unsubscribeProgress(); + unsubscribeCompleted(); + }; + }, [addEventListener, repository.id, snapshotId]); + const { data: filesData, isLoading: filesLoading } = useQuery({ ...listSnapshotFilesOptions({ path: { id: repository.id, snapshotId }, @@ -97,11 +147,9 @@ export function RestoreForm({ snapshot, repository, snapshotId, returnPath }: Re const { mutate: restoreSnapshot, isPending: isRestoring } = useMutation({ ...restoreSnapshotMutation(), - onSuccess: () => { - toast.success("Restore completed"); - void navigate({ to: returnPath }); - }, onError: (error) => { + restoreCompletedRef.current = true; + setIsRestoreActive(false); handleRepositoryError("Restore failed", error, repository.id); }, }); @@ -118,6 +166,11 @@ export function RestoreForm({ snapshot, repository, snapshotId, returnPath }: Re const pathsArray = Array.from(selectedPaths); const includePaths = pathsArray.map((path) => addBasePath(path)); + restoreCompletedRef.current = false; + setIsRestoreActive(true); + setRestoreResult(null); + setShowRestoreResultAlert(false); + restoreSnapshot({ path: { id: repository.id }, body: { @@ -140,7 +193,19 @@ export function RestoreForm({ snapshot, repository, snapshotId, returnPath }: Re restoreSnapshot, ]); + const acknowledgeRestoreResult = useCallback(() => { + setShowRestoreResultAlert(false); + setRestoreResult(null); + }, []); + + const handleResultAlertOpenChange = useCallback((open: boolean) => { + if (open) { + setShowRestoreResultAlert(true); + } + }, []); + const canRestore = restoreLocation === "original" || customTargetPath.trim(); + const isRestoreRunning = isRestoring || isRestoreActive; return (
@@ -155,9 +220,9 @@ export function RestoreForm({ snapshot, repository, snapshotId, returnPath }: Re -