Skip to content

Commit

Permalink
Merge pull request #951 from szwabodev/checkUpdatesTweaks
Browse files Browse the repository at this point in the history
feat: automatic check for updates
  • Loading branch information
Siumauricio authored Dec 21, 2024
2 parents 73782ff + 18e89df commit 392be2c
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 56 deletions.
Original file line number Diff line number Diff line change
@@ -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>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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" />
Expand All @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,42 +13,62 @@ 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>
<AlertDialogHeader>
<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>
Expand Down
63 changes: 63 additions & 0 deletions apps/dokploy/components/layouts/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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">
Expand All @@ -43,6 +101,11 @@ export const Navbar = () => {
</span>
</Link>
</div>
{isUpdateAvailable && (
<div>
<UpdateWebServer isNavbar />
</div>
)}
<Link
className={buttonVariants({
variant: "outline",
Expand Down
14 changes: 10 additions & 4 deletions apps/dokploy/server/api/routers/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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",
Expand All @@ -361,6 +366,7 @@ export const settingsRouter = createTRPCRouter({
getDokployImage(),
"dokploy",
]);

return true;
}),

Expand Down
Loading

0 comments on commit 392be2c

Please sign in to comment.