Skip to content

Commit

Permalink
Merge pull request #1042 from szwabodev/feat/newUpdateModalInNavbar
Browse files Browse the repository at this point in the history
feat: use check updates modal for update available in navbar
  • Loading branch information
Siumauricio authored Jan 2, 2025
2 parents 02b977b + 187f051 commit f042cb7
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -61,9 +70,24 @@ export const UpdateServer = () => {
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="secondary" className="gap-2">
<Sparkles className="h-4 w-4" />
Updates
<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">
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand All @@ -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 && (
<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}
<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>
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>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleConfirm}>Confirm</AlertDialogAction>
</AlertDialogFooter>
{!updating && (
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setOpen(false)}>
Cancel
</AlertDialogCancel>
<AlertDialogAction onClick={handleConfirm}>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
)}
</AlertDialogContent>
</AlertDialog>
);
Expand Down
18 changes: 11 additions & 7 deletions apps/dokploy/components/layouts/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -101,9 +105,9 @@ export const Navbar = () => {
</span>
</Link>
</div>
{isUpdateAvailable && (
{updateData.updateAvailable && (
<div>
<UpdateWebServer isNavbar />
<UpdateServer updateData={updateData} />
</div>
)}
<Link
Expand Down
8 changes: 8 additions & 0 deletions apps/dokploy/pages/api/health.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
4 changes: 3 additions & 1 deletion apps/dokploy/server/api/routers/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit f042cb7

Please sign in to comment.