Merge pull request #1042 from szwabodev/feat/newUpdateModalInNavbar

feat: use check updates modal for update available in navbar
This commit is contained in:
Mauricio Siu 2025-01-01 19:15:12 -06:00 committed by GitHub
commit f042cb720f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 121 additions and 48 deletions

View File

@ -22,16 +22,25 @@ import { useState } from "react";
import { toast } from "sonner";
import { ToggleAutoCheckUpdates } from "./toggle-auto-check-updates";
import { UpdateWebServer } from "./update-webserver";
import type { IUpdateData } from "@dokploy/server/index";
export const UpdateServer = () => {
const [hasCheckedUpdate, setHasCheckedUpdate] = useState(false);
const [isUpdateAvailable, setIsUpdateAvailable] = useState(false);
interface Props {
updateData?: IUpdateData;
}
export const UpdateServer = ({ updateData }: Props) => {
const [hasCheckedUpdate, setHasCheckedUpdate] = useState(!!updateData);
const [isUpdateAvailable, setIsUpdateAvailable] = useState(
!!updateData?.updateAvailable,
);
const { mutateAsync: getUpdateData, isLoading } =
api.settings.getUpdateData.useMutation();
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
const { data: releaseTag } = api.settings.getReleaseTag.useQuery();
const [isOpen, setIsOpen] = useState(false);
const [latestVersion, setLatestVersion] = useState("");
const [latestVersion, setLatestVersion] = useState(
updateData?.latestVersion ?? "",
);
const handleCheckUpdates = async () => {
try {
@ -61,9 +70,24 @@ export const UpdateServer = () => {
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="secondary" className="gap-2">
<Button
variant={updateData ? "outline" : "secondary"}
className="gap-2"
>
{updateData ? (
<>
<span className="flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-2 w-2 rounded-full bg-emerald-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500" />
</span>
Update available
</>
) : (
<>
<Sparkles className="h-4 w-4" />
Updates
</>
)}
</Button>
</DialogTrigger>
<DialogContent className="max-w-lg p-6">
@ -99,10 +123,6 @@ export const UpdateServer = () => {
<div className="mb-8">
<div className="inline-flex items-center gap-2 rounded-lg px-3 py-2 border border-emerald-900 bg-emerald-900 dark:bg-emerald-900/40 mb-4 w-full">
<div className="flex items-center gap-1.5">
<span className="flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-2 w-2 rounded-full bg-emerald-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500" />
</span>
<Download className="h-4 w-4 text-emerald-400" />
<span className="text font-medium text-emerald-400 ">
New version available:

View File

@ -11,30 +11,50 @@ import {
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { HardDriveDownload } from "lucide-react";
import { HardDriveDownload, Loader2 } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
interface Props {
isNavbar?: boolean;
}
export const UpdateWebServer = () => {
const [updating, setUpdating] = useState(false);
const [open, setOpen] = useState(false);
export const UpdateWebServer = ({ isNavbar }: Props) => {
const { mutateAsync: updateServer, isLoading } =
api.settings.updateServer.useMutation();
const { mutateAsync: updateServer } = api.settings.updateServer.useMutation();
const buttonLabel = isNavbar ? "Update available" : "Update Server";
const handleConfirm = async () => {
const checkIsUpdateFinished = async () => {
try {
await updateServer();
const response = await fetch("/api/health");
if (!response.ok) {
throw new Error("Health check failed");
}
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 {
// Delay each request
await new Promise((resolve) => setTimeout(resolve, 2000));
// Keep running until it returns 200
void checkIsUpdateFinished();
}
};
const handleConfirm = async () => {
try {
setUpdating(true);
await updateServer();
// Give some time for docker service restart before starting to check status
await new Promise((resolve) => setTimeout(resolve, 8000));
await checkIsUpdateFinished();
} catch (error) {
setUpdating(false);
console.error("Error updating server:", error);
toast.error(
"An error occurred while updating the server, please try again.",
@ -43,35 +63,54 @@ export const UpdateWebServer = ({ isNavbar }: Props) => {
};
return (
<AlertDialog>
<AlertDialog open={open}>
<AlertDialogTrigger asChild>
<Button
className="relative w-full"
variant={isNavbar ? "outline" : "secondary"}
isLoading={isLoading}
variant="secondary"
onClick={() => setOpen(true)}
>
{!isLoading && <HardDriveDownload className="h-4 w-4" />}
{!isLoading && (
<HardDriveDownload className="h-4 w-4" />
<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}
Update Server
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogTitle>
{updating
? "Server update in progress"
: "Are you absolutely sure?"}
</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will update the web server to the
new version. The page will be reloaded once the update is finished.
{updating ? (
<span className="flex items-center gap-1">
<Loader2 className="animate-spin" />
The server is being updated, please wait...
</span>
) : (
<>
This action cannot be undone. This will update the web server to
the new version. You will not be able to use the panel during
the update process. The page will be reloaded once the update is
finished.
</>
)}
</AlertDialogDescription>
</AlertDialogHeader>
{!updating && (
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleConfirm}>Confirm</AlertDialogAction>
<AlertDialogCancel onClick={() => setOpen(false)}>
Cancel
</AlertDialogCancel>
<AlertDialogAction onClick={handleConfirm}>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
)}
</AlertDialogContent>
</AlertDialog>
);

View File

@ -13,15 +13,19 @@ import { HeartIcon } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useRef, useState } from "react";
import { UpdateWebServer } from "../dashboard/settings/web-server/update-webserver";
import { Logo } from "../shared/logo";
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
import { buttonVariants } from "../ui/button";
import UpdateServer from "../dashboard/settings/web-server/update-server";
import type { IUpdateData } from "@dokploy/server/index";
const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7;
export const Navbar = () => {
const [isUpdateAvailable, setIsUpdateAvailable] = useState<boolean>(false);
const [updateData, setUpdateData] = useState<IUpdateData>({
latestVersion: null,
updateAvailable: false,
});
const router = useRouter();
const { data } = api.auth.get.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
@ -62,12 +66,12 @@ export const Navbar = () => {
return;
}
const { updateAvailable } = await getUpdateData();
const fetchedUpdateData = await getUpdateData();
if (updateAvailable) {
if (fetchedUpdateData?.updateAvailable) {
// Stop interval when update is available
clearUpdatesInterval();
setIsUpdateAvailable(true);
setUpdateData(fetchedUpdateData);
}
} catch (error) {
console.error("Error auto-checking for updates:", error);
@ -101,9 +105,9 @@ export const Navbar = () => {
</span>
</Link>
</div>
{isUpdateAvailable && (
{updateData.updateAvailable && (
<div>
<UpdateWebServer isNavbar />
<UpdateServer updateData={updateData} />
</div>
)}
<Link

View File

@ -0,0 +1,8 @@
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
return res.status(200).json({ ok: true });
}

View File

@ -359,7 +359,9 @@ export const settingsRouter = createTRPCRouter({
await pullLatestRelease();
await spawnAsync("docker", [
// This causes restart of dokploy, thus it will not finish executing properly, so don't await it
// Status after restart is checked via frontend /api/health endpoint
void spawnAsync("docker", [
"service",
"update",
"--force",