diff --git a/apps/nextjs/src/components/backup/database-restore-flow.tsx b/apps/nextjs/src/components/backup/database-restore-flow.tsx
index 0b2e478f0..2bc8b113f 100644
--- a/apps/nextjs/src/components/backup/database-restore-flow.tsx
+++ b/apps/nextjs/src/components/backup/database-restore-flow.tsx
@@ -195,6 +195,7 @@ export const DatabaseRestoreFlow = ({ variant = "card", onRestoreComplete }: Dat
return (
);
diff --git a/apps/nextjs/src/components/backup/index.ts b/apps/nextjs/src/components/backup/index.ts
index d7e42a7d6..accd1a935 100644
--- a/apps/nextjs/src/components/backup/index.ts
+++ b/apps/nextjs/src/components/backup/index.ts
@@ -1,3 +1,3 @@
export { DatabaseRestoreFlow } from "./database-restore-flow";
export { DangerousActionConfirmation } from "./dangerous-action-confirmation";
-export type { BackupAnalysis, RestoreStep, RestoreProgress } from "./types";
+export type { BackupAnalysis, RestoreStep } from "./types";
diff --git a/apps/nextjs/src/components/backup/restore-progress-panel.tsx b/apps/nextjs/src/components/backup/restore-progress-panel.tsx
index 46169b351..d32aa300c 100644
--- a/apps/nextjs/src/components/backup/restore-progress-panel.tsx
+++ b/apps/nextjs/src/components/backup/restore-progress-panel.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useEffect, useState } from "react";
+import { useEffect, useMemo, useState } from "react";
import { Box, Group, Loader, Paper, Stack, Text, ThemeIcon, Timeline } from "@mantine/core";
import {
IconCheck,
@@ -11,49 +11,87 @@ import {
IconRefresh,
IconTransform,
} from "@tabler/icons-react";
+import type { Icon } from "@tabler/icons-react";
import { useScopedI18n } from "@homarr/translation/client";
-import { RESTORE_PHASES } from "./types";
+import type { MigrationFile } from "./types";
-const PHASE_ICONS = {
+const PHASE_ICONS: Record = {
extracting: IconFileZip,
migrating: IconDatabaseImport,
encrypting: IconKey,
swapping: IconTransform,
restarting: IconRefresh,
-} as const;
+};
-const PHASE_DURATIONS = {
+const BASE_PHASE_DURATIONS: Record = {
extracting: 1200,
- migrating: 2000,
encrypting: 1500,
swapping: 800,
restarting: 10000,
-} as const;
+};
+
+const MIGRATION_STEP_DURATION = 100;
+
+interface TimelineStep {
+ key: string;
+ label: string;
+ icon: Icon;
+ duration: number;
+}
interface RestoreProgressPanelProps {
active: boolean;
+ migrations: MigrationFile[];
onComplete?: () => void;
}
-export const RestoreProgressPanel = ({ active, onComplete }: RestoreProgressPanelProps) => {
+export const RestoreProgressPanel = ({ active, migrations, onComplete }: RestoreProgressPanelProps) => {
const t = useScopedI18n("management.page.tool.backup.restore.progress");
- const [currentPhaseIdx, setCurrentPhaseIdx] = useState(-1);
+ const [currentStepIdx, setCurrentStepIdx] = useState(-1);
+
+ const steps = useMemo((): TimelineStep[] => {
+ const result: TimelineStep[] = [
+ { key: "extracting", label: t("extracting"), icon: IconFileZip, duration: BASE_PHASE_DURATIONS.extracting! },
+ ];
+
+ if (migrations.length > 0) {
+ for (const migration of migrations) {
+ result.push({
+ key: `migration-${migration.idx}`,
+ label: migration.tag,
+ icon: IconDatabaseImport,
+ duration: MIGRATION_STEP_DURATION,
+ });
+ }
+ } else {
+ result.push({
+ key: "migrating",
+ label: t("migrating"),
+ icon: IconDatabaseImport,
+ duration: 300,
+ });
+ }
+
+ result.push(
+ { key: "encrypting", label: t("encrypting"), icon: IconKey, duration: BASE_PHASE_DURATIONS.encrypting! },
+ { key: "swapping", label: t("swapping"), icon: IconTransform, duration: BASE_PHASE_DURATIONS.swapping! },
+ { key: "restarting", label: t("restarting"), icon: IconRefresh, duration: BASE_PHASE_DURATIONS.restarting! },
+ );
+
+ return result;
+ }, [migrations, t]);
useEffect(() => {
if (!active) return;
let cancelled = false;
const run = async () => {
- for (let i = 0; i < RESTORE_PHASES.length; i++) {
+ for (let i = 0; i < steps.length; i++) {
if (cancelled) return;
- setCurrentPhaseIdx(i);
- const phase = RESTORE_PHASES[i]!;
- const duration = PHASE_DURATIONS[phase];
- if (duration > 0) {
- await new Promise((r) => setTimeout(r, duration + Math.random() * 500));
- }
+ setCurrentStepIdx(i);
+ await new Promise((r) => setTimeout(r, steps[i]!.duration));
}
if (!cancelled) {
onComplete?.();
@@ -62,9 +100,9 @@ export const RestoreProgressPanel = ({ active, onComplete }: RestoreProgressPane
void run();
return () => { cancelled = true; };
- }, [active, onComplete]);
+ }, [active, onComplete, steps]);
- if (!active && currentPhaseIdx === -1) return null;
+ if (!active && currentStepIdx === -1) return null;
return (
@@ -78,19 +116,19 @@ export const RestoreProgressPanel = ({ active, onComplete }: RestoreProgressPane
- {RESTORE_PHASES.map((phase, i) => {
- const Icon = PHASE_ICONS[phase];
- const isDone = i < currentPhaseIdx;
- const isActive = i === currentPhaseIdx;
+ {steps.map((step, i) => {
+ const isDone = i < currentStepIdx;
+ const isActive = i === currentStepIdx;
+ const isMigration = step.key.startsWith("migration-");
return (
@@ -108,13 +146,14 @@ export const RestoreProgressPanel = ({ active, onComplete }: RestoreProgressPane
}
>
-
+
- {t(phase)}
+ {step.label}
diff --git a/apps/nextjs/src/components/backup/types.ts b/apps/nextjs/src/components/backup/types.ts
index c7db98183..c197c6544 100644
--- a/apps/nextjs/src/components/backup/types.ts
+++ b/apps/nextjs/src/components/backup/types.ts
@@ -27,13 +27,4 @@ export interface BackupAnalysis {
export type RestoreStep = "upload" | "preview" | "confirm" | "restoring" | "error";
-export interface RestoreProgress {
- phase: "extracting" | "migrating" | "encrypting" | "swapping" | "restarting";
- migrationIndex?: number;
- migrationTotal?: number;
- migrationTag?: string;
-}
-
export const PREVIEW_TABLE_KEYS = ["board", "user", "app", "integration", "item", "media", "search_engine"] as const;
-
-export const RESTORE_PHASES = ["extracting", "migrating", "encrypting", "swapping", "restarting"] as const;
diff --git a/apps/nextjs/src/components/backup/use-backup-analysis.ts b/apps/nextjs/src/components/backup/use-backup-analysis.ts
index d188e1831..3ebb844a8 100644
--- a/apps/nextjs/src/components/backup/use-backup-analysis.ts
+++ b/apps/nextjs/src/components/backup/use-backup-analysis.ts
@@ -117,7 +117,7 @@ export const useBackupAnalysis = () => {
phase: "applying",
});
- await delay(400 + Math.random() * 300);
+ await delay(100);
try {
const statements = migration.sql
@@ -149,7 +149,7 @@ export const useBackupAnalysis = () => {
phase: "done",
});
- await delay(150);
+ await delay(50);
}
}