mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-14 07:55:18 +00:00
Merge pull request #3616 from Dokploy/fix/add-loader-and-toast-success-when-traefik-loss-connection
feat(health-check): implement health check hook for post-mutation val…
This commit is contained in:
+36
-19
@@ -12,6 +12,7 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { useHealthCheckAfterMutation } from "@/hooks/use-health-check-after-mutation";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { EditTraefikEnv } from "../../web-server/edit-traefik-env";
|
import { EditTraefikEnv } from "../../web-server/edit-traefik-env";
|
||||||
import { ManageTraefikPorts } from "../../web-server/manage-traefik-ports";
|
import { ManageTraefikPorts } from "../../web-server/manage-traefik-ports";
|
||||||
@@ -33,14 +34,33 @@ export const ShowTraefikActions = ({ serverId }: Props) => {
|
|||||||
serverId,
|
serverId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
execute: executeWithHealthCheck,
|
||||||
|
isExecuting: isHealthCheckExecuting,
|
||||||
|
} = useHealthCheckAfterMutation({
|
||||||
|
initialDelay: 5000,
|
||||||
|
successMessage: "Traefik dashboard updated successfully",
|
||||||
|
onSuccess: () => {
|
||||||
|
refetchDashboard();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger
|
||||||
asChild
|
asChild
|
||||||
disabled={reloadTraefikIsLoading || toggleDashboardIsLoading}
|
disabled={
|
||||||
|
reloadTraefikIsLoading ||
|
||||||
|
toggleDashboardIsLoading ||
|
||||||
|
isHealthCheckExecuting
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
isLoading={reloadTraefikIsLoading || toggleDashboardIsLoading}
|
isLoading={
|
||||||
|
reloadTraefikIsLoading ||
|
||||||
|
toggleDashboardIsLoading ||
|
||||||
|
isHealthCheckExecuting
|
||||||
|
}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
{t("settings.server.webServer.traefik.label")}
|
{t("settings.server.webServer.traefik.label")}
|
||||||
@@ -108,24 +128,21 @@ export const ShowTraefikActions = ({ serverId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await toggleDashboard({
|
try {
|
||||||
enableDashboard: !haveTraefikDashboardPortEnabled,
|
await executeWithHealthCheck(() =>
|
||||||
serverId: serverId,
|
toggleDashboard({
|
||||||
})
|
enableDashboard: !haveTraefikDashboardPortEnabled,
|
||||||
.then(async () => {
|
serverId: serverId,
|
||||||
toast.success(
|
}),
|
||||||
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
|
);
|
||||||
);
|
} catch (error) {
|
||||||
refetchDashboard();
|
const errorMessage =
|
||||||
})
|
(error as Error)?.message ||
|
||||||
.catch((error) => {
|
"Failed to toggle dashboard. Please check if port 8080 is available.";
|
||||||
const errorMessage =
|
toast.error(errorMessage);
|
||||||
error?.message ||
|
}
|
||||||
"Failed to toggle dashboard. Please check if port 8080 is available.";
|
|
||||||
toast.error(errorMessage);
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
disabled={toggleDashboardIsLoading}
|
disabled={toggleDashboardIsLoading || isHealthCheckExecuting}
|
||||||
type="default"
|
type="default"
|
||||||
>
|
>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
import { useHealthCheckAfterMutation } from "@/hooks/use-health-check-after-mutation";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
@@ -46,6 +47,14 @@ export const EditTraefikEnv = ({ children, serverId }: Props) => {
|
|||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
api.settings.writeTraefikEnv.useMutation();
|
api.settings.writeTraefikEnv.useMutation();
|
||||||
|
|
||||||
|
const {
|
||||||
|
execute: executeWithHealthCheck,
|
||||||
|
isExecuting: isHealthCheckExecuting,
|
||||||
|
} = useHealthCheckAfterMutation({
|
||||||
|
initialDelay: 5000,
|
||||||
|
successMessage: "Traefik Env Updated",
|
||||||
|
});
|
||||||
|
|
||||||
const form = useForm<Schema>({
|
const form = useForm<Schema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
env: data || "",
|
env: data || "",
|
||||||
@@ -63,16 +72,16 @@ export const EditTraefikEnv = ({ children, serverId }: Props) => {
|
|||||||
}, [form, form.reset, data]);
|
}, [form, form.reset, data]);
|
||||||
|
|
||||||
const onSubmit = async (data: Schema) => {
|
const onSubmit = async (data: Schema) => {
|
||||||
await mutateAsync({
|
try {
|
||||||
env: data.env,
|
await executeWithHealthCheck(() =>
|
||||||
serverId,
|
mutateAsync({
|
||||||
})
|
env: data.env,
|
||||||
.then(async () => {
|
serverId,
|
||||||
toast.success("Traefik Env Updated");
|
}),
|
||||||
})
|
);
|
||||||
.catch(() => {
|
} catch {
|
||||||
toast.error("Error updating the Traefik env");
|
toast.error("Error updating the Traefik env");
|
||||||
});
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add keyboard shortcut for Ctrl+S/Cmd+S
|
// Add keyboard shortcut for Ctrl+S/Cmd+S
|
||||||
@@ -154,8 +163,8 @@ TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_HTTP_CHALLENGE_DNS_PROVIDER=cloudflare
|
|||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
isLoading={isLoading}
|
isLoading={isLoading || isHealthCheckExecuting}
|
||||||
disabled={canEdit || isLoading}
|
disabled={canEdit || isLoading || isHealthCheckExecuting}
|
||||||
form="hook-form-update-server-traefik-config"
|
form="hook-form-update-server-traefik-config"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { ArrowRightLeft, Plus, Trash2 } from "lucide-react";
|
import { ArrowRightLeft, Plus, Trash2 } from "lucide-react";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
|
import { useHealthCheckAfterMutation } from "@/hooks/use-health-check-after-mutation";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useFieldArray, useForm } from "react-hook-form";
|
import { useFieldArray, useForm } from "react-hook-form";
|
||||||
@@ -76,11 +77,19 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: updatePorts, isLoading } =
|
const { mutateAsync: updatePorts, isLoading } =
|
||||||
api.settings.updateTraefikPorts.useMutation({
|
api.settings.updateTraefikPorts.useMutation();
|
||||||
onSuccess: () => {
|
|
||||||
refetchPorts();
|
const {
|
||||||
},
|
execute: executeWithHealthCheck,
|
||||||
});
|
isExecuting: isHealthCheckExecuting,
|
||||||
|
} = useHealthCheckAfterMutation({
|
||||||
|
initialDelay: 5000,
|
||||||
|
successMessage: t("settings.server.webServer.traefik.portsUpdated"),
|
||||||
|
onSuccess: () => {
|
||||||
|
refetchPorts();
|
||||||
|
setOpen(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentPorts) {
|
if (currentPorts) {
|
||||||
@@ -99,11 +108,12 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
|||||||
|
|
||||||
const onSubmit = async (data: TraefikPortsForm) => {
|
const onSubmit = async (data: TraefikPortsForm) => {
|
||||||
try {
|
try {
|
||||||
await updatePorts({
|
await executeWithHealthCheck(() =>
|
||||||
serverId,
|
updatePorts({
|
||||||
additionalPorts: data.ports,
|
serverId,
|
||||||
});
|
additionalPorts: data.ports,
|
||||||
toast.success(t("settings.server.webServer.traefik.portsUpdated"));
|
}),
|
||||||
|
);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error((error as Error).message || "Error updating Traefik ports");
|
toast.error((error as Error).message || "Error updating Traefik ports");
|
||||||
@@ -317,7 +327,7 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
variant="default"
|
variant="default"
|
||||||
className="text-sm"
|
className="text-sm"
|
||||||
isLoading={isLoading}
|
isLoading={isLoading || isHealthCheckExecuting}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -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<void>;
|
||||||
|
/**
|
||||||
|
* 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<boolean> => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(HEALTH_CHECK_URL);
|
||||||
|
return response.ok;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const pollUntilHealthy = useCallback(async (): Promise<void> => {
|
||||||
|
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 <T>(mutationFn: () => Promise<T>): Promise<T> => {
|
||||||
|
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 };
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user