import { useState, useEffect, useCallback, useRef } from "react"; import { ArrowLeftRight, Database } from "lucide-react"; import { Button } from "../ui/Button"; import { BackupProgress } from "./BackupProgress"; import { useBackupStore } from "../../stores/backupStore"; import { useConnectionStore } from "../../stores/connectionStore"; import { useNotificationStore } from "../../stores/notificationStore"; import { detectPgTools, dbSync, getSchemas, detectMysqlTools, mysqlSync, sqliteSync, } from "../../lib/commands"; import type { PgToolStatus, MySqlToolStatus, BackupJob, DbType } from "../../lib/types"; export function SyncPage() { const [sourceConnectionId, setSourceConnectionId] = useState(""); const [targetConnectionId, setTargetConnectionId] = useState(""); const [schema, setSchema] = useState(""); const [confirmed, setConfirmed] = useState(false); const [pgToolStatus, setPgToolStatus] = useState(null); const [mysqlToolStatus, setMysqlToolStatus] = useState(null); const [checkingTools, setCheckingTools] = useState(false); const [availableSchemas, setAvailableSchemas] = useState([]); const connections = useConnectionStore((s) => s.connections); const activeJobId = useBackupStore((s) => s.activeJobId); const jobs = useBackupStore((s) => s.jobs); const startJob = useBackupStore((s) => s.startJob); const notify = useNotificationStore((s) => s.notify); const activeJob = jobs.find((j) => j.id === activeJobId); const isRunning = activeJob?.status === "running"; const pendingJobRef = useRef(null); const sourceConnection = connections.find( (c) => c.id === sourceConnectionId, ); const dbType: DbType | null = sourceConnection?.db_type ?? null; const isPg = dbType === "postgresql"; const isMysql = dbType === "mysql"; useEffect(() => { if (!pendingJobRef.current || !activeJob) return; if (activeJob.id !== pendingJobRef.current) return; if (activeJob.status === "completed") { notify("Sync completed successfully", "success"); pendingJobRef.current = null; } else if (activeJob.status === "failed") { notify( `Sync failed: ${activeJob.error_message || "Unknown error"}`, "error", ); pendingJobRef.current = null; } }, [activeJob, notify]); // Tool detection: depends on the selected source connection's DB type useEffect(() => { setPgToolStatus(null); setMysqlToolStatus(null); setConfirmed(false); if (isPg) { setCheckingTools(true); detectPgTools() .then((status) => setPgToolStatus(status)) .catch(() => setPgToolStatus({ pg_dump_found: false, pg_restore_found: false, pg_dump_version: null, pg_restore_version: null, pg_dump_source: null, pg_restore_source: null, }), ) .finally(() => setCheckingTools(false)); } else if (isMysql) { setCheckingTools(true); detectMysqlTools() .then((status) => setMysqlToolStatus(status)) .catch(() => setMysqlToolStatus({ mysqldumpFound: false, mysqlFound: false, mysqldumpVersion: null, mysqlVersion: null, mysqldumpSource: null, mysqlSource: null, }), ) .finally(() => setCheckingTools(false)); } }, [isPg, isMysql]); // Fetch schemas from the source connection when it changes useEffect(() => { if (!sourceConnectionId) { setAvailableSchemas([]); setSchema(""); return; } if (!isPg && !isMysql) { setAvailableSchemas([]); setSchema(""); return; } getSchemas(sourceConnectionId) .then((schemas) => setAvailableSchemas(schemas)) .catch(() => setAvailableSchemas([])); }, [sourceConnectionId, isPg, isMysql]); const runWithProgress = useCallback( async (type: BackupJob["type"], action: () => Promise) => { const jobId = `${type}-${Date.now()}`; startJob(jobId, type); pendingJobRef.current = jobId; try { await action(); } catch (e) { const msg = e instanceof Error ? e.message : String(e); useBackupStore.getState().failJob(jobId, msg); } }, [startJob], ); const handleStartSync = useCallback(async () => { if (!sourceConnectionId || !targetConnectionId) { notify("Please select both source and target connections", "error"); return; } if (sourceConnectionId === targetConnectionId) { notify("Source and target must be different", "error"); return; } if (!dbType) { notify("Unable to determine database type for sync", "error"); return; } const options = { sourceConnectionId, targetConnectionId, schema: schema || undefined, tables: undefined, dbType, }; await runWithProgress("sync", () => { if (isPg) return dbSync(options); if (isMysql) return mysqlSync(options); return sqliteSync(options); }); }, [ sourceConnectionId, targetConnectionId, dbType, isPg, isMysql, schema, notify, runWithProgress, ]); const toolsMissing = isPg ? pgToolStatus && (!pgToolStatus.pg_dump_found || !pgToolStatus.pg_restore_found) : isMysql ? mysqlToolStatus && (!mysqlToolStatus.mysqldumpFound || !mysqlToolStatus.mysqlFound) : false; const toolsBundled = isPg ? pgToolStatus?.pg_dump_source === "bundled" && pgToolStatus?.pg_restore_source === "bundled" : isMysql ? mysqlToolStatus?.mysqldumpSource === "bundled" && mysqlToolStatus?.mysqlSource === "bundled" : false; const canStart = sourceConnectionId && targetConnectionId && confirmed && !isRunning; const checkingMessage = isPg ? "Checking for pg_dump / pg_restore..." : isMysql ? "Checking for mysqldump / mysql..." : null; const targetConnections = dbType ? connections.filter((c) => c.db_type === dbType) : []; return (
{/* Toolbar header */}
DB Sync Transfer data between databases via pipe
{/* Content */}
{/* Tool check */} {checkingTools && checkingMessage && (

{checkingMessage}

)} {toolsMissing && !toolsBundled && (

{isPg ? "PostgreSQL tools not found" : "MySQL tools not found"}

Both {isPg ? "pg_dump and pg_restore" : "mysqldump and mysql"} are required for database sync.

    {isPg && !pgToolStatus?.pg_dump_found && (
  • pg_dump is missing.
  • )} {isPg && !pgToolStatus?.pg_restore_found && (
  • pg_restore is missing.
  • )} {isMysql && !mysqlToolStatus?.mysqldumpFound && (
  • mysqldump is missing.
  • )} {isMysql && !mysqlToolStatus?.mysqlFound && (
  • mysql is missing.
  • )}
)} {!checkingTools && !toolsMissing && ( <> {/* Configuration card */}
{/* Source & Target connection pickers */}
{/* Schema (optional) */} {(isPg || isMysql) && (
)} {/* Flow indicator */} {sourceConnectionId && targetConnectionId && (
{connections.find( (c) => c.id === sourceConnectionId, )?.name ?? sourceConnectionId} {connections.find( (c) => c.id === targetConnectionId, )?.name ?? targetConnectionId}
)}
{/* Destructive confirmation */}
{/* Progress */} {activeJob && (
)} {/* Actions */}
)}
); }