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