From 2880fb974864c46db65be6a140aefaf6d4a27fb3 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 15 Apr 2026 12:29:07 -0600 Subject: [PATCH] feat: enhance transfer service with auto-deployment and logging Refactor the TransferService component to include automatic deployment after successful transfers for various service types (application, compose, postgres, mysql, mariadb, mongo, redis). Implement logging functionality to capture transfer progress and errors, improving user feedback during the transfer process. Update related API routers to support these enhancements, ensuring a seamless transfer and deployment experience. --- .../dashboard/shared/transfer-service.tsx | 307 ++++++++----- .../dokploy/server/api/routers/application.ts | 44 +- apps/dokploy/server/api/routers/compose.ts | 43 +- apps/dokploy/server/api/routers/mariadb.ts | 7 +- apps/dokploy/server/api/routers/mongo.ts | 7 +- apps/dokploy/server/api/routers/mysql.ts | 7 +- apps/dokploy/server/api/routers/postgres.ts | 7 +- apps/dokploy/server/api/routers/redis.ts | 7 +- packages/server/src/services/transfer.ts | 420 ++++++++++-------- packages/server/src/utils/transfer/scanner.ts | 318 +++++++------ packages/server/src/utils/transfer/sync.ts | 354 ++++++++++++--- 11 files changed, 1042 insertions(+), 479 deletions(-) diff --git a/apps/dokploy/components/dashboard/shared/transfer-service.tsx b/apps/dokploy/components/dashboard/shared/transfer-service.tsx index 575b7dbd2..85726a0c0 100644 --- a/apps/dokploy/components/dashboard/shared/transfer-service.tsx +++ b/apps/dokploy/components/dashboard/shared/transfer-service.tsx @@ -1,6 +1,13 @@ -import { AlertTriangle, ArrowRightLeft, Loader2, Server } from "lucide-react"; +import { + AlertTriangle, + ArrowRightLeft, + Loader2, + Server, +} from "lucide-react"; import { useState } from "react"; import { toast } from "sonner"; +import { DrawerLogs } from "@/components/shared/drawer-logs"; +import type { LogLine } from "@/components/dashboard/docker/logs/utils"; import { AlertDialog, AlertDialogAction, @@ -90,41 +97,16 @@ const formatBytes = (bytes: number): string => { return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`; }; -const useTransferMutations = (serviceType: ServiceType) => { - const appScan = api.application.transferScan.useMutation(); - const appTransfer = api.application.transfer.useMutation(); - const composeScan = api.compose.transferScan.useMutation(); - const composeTransfer = api.compose.transfer.useMutation(); - const postgresScan = api.postgres.transferScan.useMutation(); - const postgresTransfer = api.postgres.transfer.useMutation(); - const mysqlScan = api.mysql.transferScan.useMutation(); - const mysqlTransfer = api.mysql.transfer.useMutation(); - const mariadbScan = api.mariadb.transferScan.useMutation(); - const mariadbTransfer = api.mariadb.transfer.useMutation(); - const mongoScan = api.mongo.transferScan.useMutation(); - const mongoTransfer = api.mongo.transfer.useMutation(); - const redisScan = api.redis.transferScan.useMutation(); - const redisTransfer = api.redis.transfer.useMutation(); - - const mutations: Record< - ServiceType, - { - scan: { mutateAsync: (input: any) => Promise; isPending: boolean }; - transfer: { - mutateAsync: (input: any) => Promise; - isPending: boolean; - }; - } - > = { - application: { scan: appScan, transfer: appTransfer }, - compose: { scan: composeScan, transfer: composeTransfer }, - postgres: { scan: postgresScan, transfer: postgresTransfer }, - mysql: { scan: mysqlScan, transfer: mysqlTransfer }, - mariadb: { scan: mariadbScan, transfer: mariadbTransfer }, - mongo: { scan: mongoScan, transfer: mongoTransfer }, - redis: { scan: redisScan, transfer: redisTransfer }, +const useScanMutation = (serviceType: ServiceType) => { + const mutations = { + application: api.application.transferScan.useMutation(), + compose: api.compose.transferScan.useMutation(), + postgres: api.postgres.transferScan.useMutation(), + mysql: api.mysql.transferScan.useMutation(), + mariadb: api.mariadb.transferScan.useMutation(), + mongo: api.mongo.transferScan.useMutation(), + redis: api.redis.transferScan.useMutation(), }; - return mutations[serviceType]; }; @@ -148,15 +130,17 @@ export const TransferService = ({ }: TransferServiceProps) => { const [targetServerId, setTargetServerId] = useState(""); const [scanResult, setScanResult] = useState(null); - const [step, setStep] = useState<"select" | "scan" | "confirm" | "transfer">( - "select", - ); + const [step, setStep] = useState<"select" | "scan" | "confirm">("select"); const [showConfirm, setShowConfirm] = useState(false); - const [transferLogs, setTransferLogs] = useState([]); + + // Drawer logs state + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + const [filteredLogs, setFilteredLogs] = useState([]); + const [isTransferring, setIsTransferring] = useState(false); const { data: servers } = api.server.all.useQuery(); const utils = api.useUtils(); - const { scan, transfer } = useTransferMutations(serviceType); + const scan = useScanMutation(serviceType); const idKey = getServiceIdKey(serviceType); @@ -166,6 +150,111 @@ export const TransferService = ({ const selectedServer = servers?.find((s) => s.serverId === targetServerId); + // Subscription for transfer with logs + const subscriptionInput = { + [idKey]: serviceId, + targetServerId: targetServerId || "placeholder", + decisions: {}, + }; + + const useTransferSubscription = (sType: ServiceType) => { + api.application.transferWithLogs.useSubscription(subscriptionInput as any, { + enabled: isTransferring && sType === "application", + onData: handleLogData, + onError: handleLogError, + }); + api.compose.transferWithLogs.useSubscription(subscriptionInput as any, { + enabled: isTransferring && sType === "compose", + onData: handleLogData, + onError: handleLogError, + }); + api.postgres.transferWithLogs.useSubscription(subscriptionInput as any, { + enabled: isTransferring && sType === "postgres", + onData: handleLogData, + onError: handleLogError, + }); + api.mysql.transferWithLogs.useSubscription(subscriptionInput as any, { + enabled: isTransferring && sType === "mysql", + onData: handleLogData, + onError: handleLogError, + }); + api.mariadb.transferWithLogs.useSubscription(subscriptionInput as any, { + enabled: isTransferring && sType === "mariadb", + onData: handleLogData, + onError: handleLogError, + }); + api.mongo.transferWithLogs.useSubscription(subscriptionInput as any, { + enabled: isTransferring && sType === "mongo", + onData: handleLogData, + onError: handleLogError, + }); + api.redis.transferWithLogs.useSubscription(subscriptionInput as any, { + enabled: isTransferring && sType === "redis", + onData: handleLogData, + onError: handleLogError, + }); + }; + + const handleLogData = (log: string) => { + if (!isDrawerOpen) { + setIsDrawerOpen(true); + } + + // Try to parse as JSON progress + try { + const progress = JSON.parse(log); + if (progress.message) { + const logLine: LogLine = { + rawTimestamp: new Date().toISOString(), + timestamp: new Date(), + message: `[${progress.phase || "transfer"}] ${progress.message}`, + }; + setFilteredLogs((prev) => [...prev, logLine]); + } + return; + } catch { + // Not JSON, treat as plain text + } + + const logLine: LogLine = { + rawTimestamp: new Date().toISOString(), + timestamp: new Date(), + message: log, + }; + setFilteredLogs((prev) => [...prev, logLine]); + + if ( + log.includes("completed successfully") || + log.includes("Deployment queued") || + log.includes("Deployment started") + ) { + setTimeout(() => { + setIsTransferring(false); + utils.invalidate(); + toast.success("Transfer and deployment completed!"); + }, 2000); + } + + if (log.includes("Transfer failed") || log.includes("Transfer error")) { + setIsTransferring(false); + toast.error("Transfer failed"); + } + }; + + const handleLogError = (error: unknown) => { + console.error("Transfer subscription error:", error); + setIsTransferring(false); + const logLine: LogLine = { + rawTimestamp: new Date().toISOString(), + timestamp: new Date(), + message: `Error: ${error instanceof Error ? error.message : String(error)}`, + }; + setFilteredLogs((prev) => [...prev, logLine]); + }; + + // Register the subscription hooks (must be called unconditionally) + useTransferSubscription(serviceType); + const handleScan = async () => { if (!targetServerId) { toast.error("Please select a target server"); @@ -177,7 +266,7 @@ export const TransferService = ({ const result = await scan.mutateAsync({ [idKey]: serviceId, targetServerId, - }); + } as any); setScanResult(result as ScanResult); setStep("confirm"); } catch (error) { @@ -190,38 +279,27 @@ export const TransferService = ({ const handleTransfer = async () => { setShowConfirm(false); - setStep("transfer"); - setTransferLogs([]); + setFilteredLogs([]); + setIsTransferring(true); + setIsDrawerOpen(true); - try { - await transfer.mutateAsync({ - [idKey]: serviceId, - targetServerId, - decisions: {}, - }); - - toast.success("Transfer completed successfully!"); - setTransferLogs((prev) => [...prev, "Transfer completed successfully!"]); - - await utils.invalidate(); - - setTimeout(() => { - setStep("select"); - setScanResult(null); - setTargetServerId(""); - }, 3000); - } catch (error) { - const message = - error instanceof Error ? error.message : "Unknown error"; - toast.error(`Transfer failed: ${message}`); - setTransferLogs((prev) => [...prev, `Transfer failed: ${message}`]); - setStep("confirm"); - } + // Add initial log + setFilteredLogs([ + { + rawTimestamp: new Date().toISOString(), + timestamp: new Date(), + message: `Starting transfer to ${selectedServer?.name} (${selectedServer?.ipAddress})...`, + }, + ]); }; - const isDbService = ["postgres", "mysql", "mariadb", "mongo", "redis"].includes( - serviceType, - ); + const isDbService = [ + "postgres", + "mysql", + "mariadb", + "mongo", + "redis", + ].includes(serviceType); return ( @@ -247,7 +325,7 @@ export const TransferService = ({ <> {/* Step 1: Select target server */}
- + Target Server