/** * MigrationModal Component * * Displays detailed migration instructions in a modal dialog. * Shows step-by-step guide for users to run the migration command. */ import { useMemo, useState, useEffect, useRef } from "react"; import { AlertTriangle, ExternalLink, Info, Loader2, Terminal, } from "lucide-react"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ds/ui/dialog"; import { Button } from "@/components/ds/ui/button"; import { Alert, AlertDescription } from "@/components/ds/ui/alert"; import { Input } from "@/components/ds/ui/input"; import { Label } from "@/components/ds/ui/label"; import { toast } from "sonner"; import { useTranslate } from "ra-core"; import { getSupabaseConfig } from "@/lib/supabase-config"; import type { MigrationStatus } from "@/lib/migration-check"; interface MigrationModalProps { /** Whether the modal is open */ open: boolean; /** Callback when modal is closed */ onOpenChange: (open: boolean) => void; /** Migration status */ status: MigrationStatus; } export function MigrationModal({ open, onOpenChange, status, }: MigrationModalProps) { const config = getSupabaseConfig(); const translate = useTranslate(); // Auto-migration state const [isMigrating, setIsMigrating] = useState(false); const [migrationLogs, setMigrationLogs] = useState([]); const [accessToken, setAccessToken] = useState(""); const logsEndRef = useRef(null); const abortControllerRef = useRef(null); const projectId = useMemo(() => { const url = config?.url; if (!url) return ""; try { const host = new URL(url).hostname; return host.split(".")[0] || ""; } catch { return ""; } }, [config?.url]); // Scroll logs to bottom useEffect(() => { if (logsEndRef.current) { logsEndRef.current.scrollIntoView({ behavior: "smooth" }); } }, [migrationLogs]); // Cleanup: abort migration if component unmounts or modal closes useEffect(() => { return () => { if (abortControllerRef.current) { abortControllerRef.current.abort(); abortControllerRef.current = null; } }; }, []); const handleAutoMigrate = async () => { if (!projectId) { toast.error(translate("crm.migration.modal.auto.missing_project_id")); return; } setIsMigrating(true); setMigrationLogs([translate("crm.migration.modal.auto.init_log")]); // Create AbortController for proper cleanup const abortController = new AbortController(); abortControllerRef.current = abortController; try { const response = await fetch("/api/migrate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ projectRef: projectId, accessToken, }), signal: abortController.signal, }); if (!response.ok) { throw new Error( `Server returned ${response.status}: ${response.statusText}`, ); } const reader = response.body?.getReader(); if (!reader) throw new Error("No response stream received."); const decoder = new TextDecoder(); let migrationSucceeded = false; try { while (true) { const { done, value } = await reader.read(); if (done) break; // Use stream: true for proper multi-byte character handling const text = decoder.decode(value, { stream: true }); const lines = text.split("\n").filter(Boolean); setMigrationLogs((prev) => [...prev, ...lines]); // Check if migration succeeded if ( text.includes("Migration completed successfully") || text.includes("✅") ) { migrationSucceeded = true; } } } finally { reader.releaseLock(); } // Auto-reload on success if (migrationSucceeded) { setMigrationLogs((prev) => [ ...prev, translate("crm.migration.modal.auto.reload_message"), ]); setTimeout(() => { window.location.reload(); }, 2000); } } catch (err) { // Don't show error if request was aborted (user closed modal) if (err instanceof Error && err.name === "AbortError") { console.log("Migration request aborted"); return; } console.error(err); setMigrationLogs((prev) => [ ...prev, `${translate("crm.migration.modal.auto.error_prefix")}${err instanceof Error ? err.message : String(err)}`, ]); toast.error(translate("crm.migration.modal.auto.failure_toast")); } finally { setIsMigrating(false); abortControllerRef.current = null; } }; return ( !isMigrating && onOpenChange(val)} > {translate("crm.migration.modal.title")} {translate("crm.migration.modal.description", { version: status.appVersion, })}
{/* Overview Alert */} {translate("crm.migration.modal.overview.title")}
  • {translate("crm.migration.modal.overview.update_schema", { version: status.appVersion, })}
  • {translate("crm.migration.modal.overview.enable_features")}
  • {translate("crm.migration.modal.overview.data_safe")}
{/* Auto-Migration Interface */}

{translate("crm.migration.modal.auto.title")}

{translate("crm.migration.modal.auto.description")}

{translate("crm.migration.modal.auto.generate_token")}{" "}
setAccessToken(e.target.value)} disabled={isMigrating} />

{translate("crm.migration.modal.auto.access_token_hint")}

{/* Logs Terminal */}
{migrationLogs.length === 0 ? (
{translate("crm.migration.modal.auto.logs_placeholder")}
) : ( migrationLogs.map((log, i) => (
{log}
)) )}
{/* Troubleshooting */} {translate("crm.migration.modal.troubleshooting.title")}
); }