diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx index aebba8877..36761973c 100644 --- a/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx @@ -12,6 +12,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { useHealthCheckAfterMutation } from "@/hooks/use-health-check-after-mutation"; import { api } from "@/utils/api"; import { EditTraefikEnv } from "../../web-server/edit-traefik-env"; import { ManageTraefikPorts } from "../../web-server/manage-traefik-ports"; @@ -33,14 +34,33 @@ export const ShowTraefikActions = ({ serverId }: Props) => { serverId, }); + const { + execute: executeWithHealthCheck, + isExecuting: isHealthCheckExecuting, + } = useHealthCheckAfterMutation({ + initialDelay: 5000, + successMessage: "Traefik dashboard updated successfully", + onSuccess: () => { + refetchDashboard(); + }, + }); + return ( diff --git a/apps/dokploy/hooks/use-health-check-after-mutation.ts b/apps/dokploy/hooks/use-health-check-after-mutation.ts new file mode 100644 index 000000000..e2df72720 --- /dev/null +++ b/apps/dokploy/hooks/use-health-check-after-mutation.ts @@ -0,0 +1,92 @@ +import { useCallback, useState } from "react"; +import { toast } from "sonner"; + +const HEALTH_CHECK_URL = "/api/health"; + +export interface UseHealthCheckAfterMutationOptions { + /** + * Delay in ms before starting to poll the health endpoint. + * Gives time for the service (e.g. Traefik) to restart. + * @default 5000 + */ + initialDelay?: number; + /** + * Delay in ms between each health check poll. + * @default 2000 + */ + pollInterval?: number; + /** + * Message shown in toast when the operation completes successfully. + */ + successMessage: string; + /** + * Callback when health check passes. Use for refetching data. + */ + onSuccess?: () => void | Promise; + /** + * If true, reloads the page when health check passes (e.g. for server update). + * @default false + */ + reloadOnSuccess?: boolean; +} + +export const useHealthCheckAfterMutation = ({ + initialDelay = 5000, + pollInterval = 2000, + successMessage, + onSuccess, + reloadOnSuccess = false, +}: UseHealthCheckAfterMutationOptions) => { + const [isExecuting, setIsExecuting] = useState(false); + + const checkHealth = useCallback(async (): Promise => { + try { + const response = await fetch(HEALTH_CHECK_URL); + return response.ok; + } catch { + return false; + } + }, []); + + const pollUntilHealthy = useCallback(async (): Promise => { + const isHealthy = await checkHealth(); + + if (isHealthy) { + toast.success(successMessage); + + if (reloadOnSuccess) { + setTimeout(() => { + window.location.reload(); + }, 2000); + } else { + await onSuccess?.(); + } + return; + } + + await new Promise((resolve) => setTimeout(resolve, pollInterval)); + await pollUntilHealthy(); + }, [checkHealth, successMessage, reloadOnSuccess, onSuccess, pollInterval]); + + const execute = useCallback( + async (mutationFn: () => Promise): Promise => { + setIsExecuting(true); + + try { + const result = await mutationFn(); + + // Give time for the service to restart before polling + await new Promise((resolve) => setTimeout(resolve, initialDelay)); + + await pollUntilHealthy(); + + return result; + } finally { + setIsExecuting(false); + } + }, + [initialDelay, pollUntilHealthy], + ); + + return { execute, isExecuting }; +};