mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge pull request #951 from szwabodev/checkUpdatesTweaks
feat: automatic check for updates
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { useState } from "react";
|
||||
|
||||
export const ToggleAutoCheckUpdates = () => {
|
||||
const [enabled, setEnabled] = useState<boolean>(
|
||||
localStorage.getItem("enableAutoCheckUpdates") === "true",
|
||||
);
|
||||
|
||||
const handleToggle = (checked: boolean) => {
|
||||
setEnabled(checked);
|
||||
localStorage.setItem("enableAutoCheckUpdates", String(checked));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-4">
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onCheckedChange={handleToggle}
|
||||
id="autoCheckUpdatesToggle"
|
||||
/>
|
||||
<Label className="text-primary" htmlFor="autoCheckUpdatesToggle">
|
||||
Automatically check for new updates
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -14,15 +14,34 @@ import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { UpdateWebServer } from "./update-webserver";
|
||||
import { ToggleAutoCheckUpdates } from "./toggle-auto-check-updates";
|
||||
|
||||
export const UpdateServer = () => {
|
||||
const [isUpdateAvailable, setIsUpdateAvailable] = useState<null | boolean>(
|
||||
null,
|
||||
);
|
||||
const { mutateAsync: checkAndUpdateImage, isLoading } =
|
||||
api.settings.checkAndUpdateImage.useMutation();
|
||||
const { mutateAsync: getUpdateData, isLoading } =
|
||||
api.settings.getUpdateData.useMutation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const handleCheckUpdates = async () => {
|
||||
try {
|
||||
const { updateAvailable, latestVersion } = await getUpdateData();
|
||||
setIsUpdateAvailable(updateAvailable);
|
||||
if (updateAvailable) {
|
||||
toast.success(`${latestVersion} update is available!`);
|
||||
} else {
|
||||
toast.info("No updates available");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error checking for updates:", error);
|
||||
setIsUpdateAvailable(false);
|
||||
toast.error(
|
||||
"An error occurred while checking for updates, please try again.",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
@@ -61,6 +80,7 @@ export const UpdateServer = () => {
|
||||
</AlertBlock>
|
||||
|
||||
<div className="w-full flex flex-col gap-4">
|
||||
<ToggleAutoCheckUpdates />
|
||||
{isUpdateAvailable === false && (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<RefreshCcw className="size-6 self-center text-muted-foreground" />
|
||||
@@ -74,20 +94,10 @@ export const UpdateServer = () => {
|
||||
) : (
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={async () => {
|
||||
await checkAndUpdateImage()
|
||||
.then(async (e) => {
|
||||
setIsUpdateAvailable(e);
|
||||
})
|
||||
.catch(() => {
|
||||
setIsUpdateAvailable(false);
|
||||
toast.error("Error to check updates");
|
||||
});
|
||||
toast.success("Check updates");
|
||||
}}
|
||||
onClick={handleCheckUpdates}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Check Updates
|
||||
{isLoading ? "Checking for updates..." : "Check for updates"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -13,22 +13,49 @@ import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const UpdateWebServer = () => {
|
||||
interface Props {
|
||||
isNavbar?: boolean;
|
||||
}
|
||||
|
||||
export const UpdateWebServer = ({ isNavbar }: Props) => {
|
||||
const { mutateAsync: updateServer, isLoading } =
|
||||
api.settings.updateServer.useMutation();
|
||||
|
||||
const buttonLabel = isNavbar ? "Update available" : "Update server";
|
||||
|
||||
const handleConfirm = async () => {
|
||||
try {
|
||||
await updateServer();
|
||||
toast.success(
|
||||
"The server has been updated. The page will be reloaded to reflect the changes...",
|
||||
);
|
||||
setTimeout(() => {
|
||||
// Allow seeing the toast before reloading
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
console.error("Error updating server:", error);
|
||||
toast.error(
|
||||
"An error occurred while updating the server, please try again.",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
className="relative w-full"
|
||||
variant="secondary"
|
||||
variant={isNavbar ? "outline" : "secondary"}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
<span className="absolute -right-1 -top-2 flex h-3 w-3">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-3 w-3 bg-green-500" />
|
||||
</span>
|
||||
Update Server
|
||||
{!isLoading && (
|
||||
<span className="absolute -right-1 -top-2 flex h-3 w-3">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-3 w-3 bg-green-500" />
|
||||
</span>
|
||||
)}
|
||||
{isLoading ? "Updating..." : buttonLabel}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
@@ -36,19 +63,12 @@ export const UpdateWebServer = () => {
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will update the web server to the
|
||||
new version.
|
||||
new version. The page will be reloaded once the update is finished.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await updateServer();
|
||||
toast.success("Please reload the browser to see the changes");
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
<AlertDialogAction onClick={handleConfirm}>Confirm</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
@@ -15,8 +15,13 @@ import { useRouter } from "next/router";
|
||||
import { Logo } from "../shared/logo";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
||||
import { buttonVariants } from "../ui/button";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { UpdateWebServer } from "../dashboard/settings/web-server/update-webserver";
|
||||
|
||||
const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 5;
|
||||
|
||||
export const Navbar = () => {
|
||||
const [isUpdateAvailable, setIsUpdateAvailable] = useState<boolean>(false);
|
||||
const router = useRouter();
|
||||
const { data } = api.auth.get.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
@@ -29,6 +34,59 @@ export const Navbar = () => {
|
||||
},
|
||||
);
|
||||
const { mutateAsync } = api.auth.logout.useMutation();
|
||||
const { mutateAsync: getUpdateData } =
|
||||
api.settings.getUpdateData.useMutation();
|
||||
|
||||
const checkUpdatesIntervalRef = useRef<null | NodeJS.Timeout>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Handling of automatic check for server updates
|
||||
if (isCloud) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!localStorage.getItem("enableAutoCheckUpdates")) {
|
||||
// Enable auto update checking by default if user didn't change it
|
||||
localStorage.setItem("enableAutoCheckUpdates", "true");
|
||||
}
|
||||
|
||||
const clearUpdatesInterval = () => {
|
||||
if (checkUpdatesIntervalRef.current) {
|
||||
clearInterval(checkUpdatesIntervalRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
const checkUpdates = async () => {
|
||||
try {
|
||||
if (localStorage.getItem("enableAutoCheckUpdates") !== "true") {
|
||||
return;
|
||||
}
|
||||
|
||||
const { updateAvailable } = await getUpdateData();
|
||||
|
||||
if (updateAvailable) {
|
||||
// Stop interval when update is available
|
||||
clearUpdatesInterval();
|
||||
setIsUpdateAvailable(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error auto-checking for updates:", error);
|
||||
}
|
||||
};
|
||||
|
||||
checkUpdatesIntervalRef.current = setInterval(
|
||||
checkUpdates,
|
||||
AUTO_CHECK_UPDATES_INTERVAL_MINUTES * 60000,
|
||||
);
|
||||
|
||||
// Also check for updates on initial page load
|
||||
checkUpdates();
|
||||
|
||||
return () => {
|
||||
clearUpdatesInterval();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<nav className="border-divider sticky inset-x-0 top-0 z-40 flex h-auto w-full items-center justify-center border-b bg-background/70 backdrop-blur-lg backdrop-saturate-150 data-[menu-open=true]:border-none data-[menu-open=true]:backdrop-blur-xl">
|
||||
<header className="relative z-40 flex w-full max-w-8xl flex-row flex-nowrap items-center justify-between gap-4 px-4 sm:px-6 h-16">
|
||||
@@ -43,6 +101,11 @@ export const Navbar = () => {
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
{isUpdateAvailable && (
|
||||
<div>
|
||||
<UpdateWebServer isNavbar />
|
||||
</div>
|
||||
)}
|
||||
<Link
|
||||
className={buttonVariants({
|
||||
variant: "outline",
|
||||
|
||||
@@ -45,12 +45,14 @@ import {
|
||||
stopService,
|
||||
stopServiceRemote,
|
||||
updateAdmin,
|
||||
getUpdateData,
|
||||
updateLetsEncryptEmail,
|
||||
updateServerById,
|
||||
updateServerTraefik,
|
||||
writeConfig,
|
||||
writeMainConfig,
|
||||
writeTraefikConfigInPath,
|
||||
DEFAULT_UPDATE_DATA,
|
||||
} from "@dokploy/server";
|
||||
import { checkGPUStatus, setupGPUSupport } from "@dokploy/server";
|
||||
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
|
||||
@@ -342,17 +344,20 @@ export const settingsRouter = createTRPCRouter({
|
||||
writeConfig("middlewares", input.traefikConfig);
|
||||
return true;
|
||||
}),
|
||||
|
||||
checkAndUpdateImage: adminProcedure.mutation(async () => {
|
||||
getUpdateData: adminProcedure.mutation(async () => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
return await pullLatestRelease();
|
||||
|
||||
return await getUpdateData();
|
||||
}),
|
||||
updateServer: adminProcedure.mutation(async () => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
|
||||
await pullLatestRelease();
|
||||
|
||||
await spawnAsync("docker", [
|
||||
"service",
|
||||
"update",
|
||||
@@ -361,6 +366,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
getDokployImage(),
|
||||
"dokploy",
|
||||
]);
|
||||
|
||||
return true;
|
||||
}),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user