"use client"; import { useCallback, useRef, useState } from "react"; import { Alert, Button, Group, Loader, rem, Stack, Text } from "@mantine/core"; import type { FileWithPath } from "@mantine/dropzone"; import { Dropzone, MIME_TYPES } from "@mantine/dropzone"; import { IconAlertTriangle, IconArrowRight, IconFileZip, IconUpload, IconX } from "@tabler/icons-react"; import "@mantine/dropzone/styles.css"; import { useScopedI18n } from "@homarr/translation/client"; import type { RestoreStep } from "./types"; import { BackupPreviewPanel } from "./backup-preview-panel"; import { MigrationProgressPanel } from "./migration-progress-panel"; import { RestoreConfirmation } from "./restore-confirmation"; import { RestoreProgressPanel } from "./restore-progress-panel"; import { useBackupAnalysis } from "./use-backup-analysis"; interface DatabaseRestoreFlowProps { variant?: "card" | "standalone"; onRestoreComplete?: () => void; } export const DatabaseRestoreFlow = ({ variant = "card", onRestoreComplete }: DatabaseRestoreFlowProps) => { const t = useScopedI18n("management.page.tool.backup.restore"); const [file, setFile] = useState(null); const [step, setStep] = useState("upload"); const [importError, setImportError] = useState(null); const apiDoneRef = useRef(false); const animDoneRef = useRef(false); const apiErrorRef = useRef(null); const { analysis, loading, error: analysisError, migrationProgress, analyzeFile, reset } = useBackupAnalysis(); const handleFileDrop = useCallback( (files: FileWithPath[]) => { const droppedFile = files[0]; if (!droppedFile) return; setFile(droppedFile); setStep("preview"); setImportError(null); void analyzeFile(droppedFile); }, [analyzeFile], ); const handleClear = useCallback(() => { setFile(null); setStep("upload"); setImportError(null); apiDoneRef.current = false; animDoneRef.current = false; apiErrorRef.current = null; reset(); }, [reset]); const tryFinalize = useCallback(() => { if (!apiDoneRef.current) return; if (apiErrorRef.current) { setImportError(apiErrorRef.current); setStep("error"); return; } if (!animDoneRef.current) return; onRestoreComplete?.(); window.location.reload(); }, [onRestoreComplete]); const handleConfirm = useCallback(async () => { if (!file) return; setStep("restoring"); setImportError(null); apiDoneRef.current = false; animDoneRef.current = false; apiErrorRef.current = null; try { const formData = new FormData(); formData.append("file", file); const response = await fetch("/api/backup/import", { method: "POST", body: formData }); if (!response.ok) { try { const data = await response.json(); apiErrorRef.current = data.error ?? t("failed.title"); } catch { apiErrorRef.current = `Server returned ${response.status}`; } } } catch { apiErrorRef.current = t("failed.title"); } finally { apiDoneRef.current = true; tryFinalize(); } }, [file, t, tryFinalize]); const handleAnimationComplete = useCallback(() => { animDoneRef.current = true; tryFinalize(); }, [tryFinalize]); if (step === "upload") { return (
{t("dropzone.title")} {t("dropzone.description")}
); } if (step === "preview") { return (
{file?.name} {file ? `${(file.size / 1024 / 1024).toFixed(2)} MB` : ""}
{loading && migrationProgress && } {loading && !migrationProgress && ( {t("analyzing")} )} {analysisError && ( } title={t("analysisError")}> {analysisError} )} {analysis && ( <> )}
); } if (step === "confirm") { return ( {analysis && } setStep("preview")} /> ); } if (step === "restoring") { return ( ); } if (step === "error") { return ( } title={t("failed.title")} radius="md"> {importError} ); } return null; };