mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge pull request #1042 from szwabodev/feat/newUpdateModalInNavbar
feat: use check updates modal for update available in navbar
This commit is contained in:
commit
f042cb720f
@ -22,16 +22,25 @@ import { useState } from "react";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { ToggleAutoCheckUpdates } from "./toggle-auto-check-updates";
|
import { ToggleAutoCheckUpdates } from "./toggle-auto-check-updates";
|
||||||
import { UpdateWebServer } from "./update-webserver";
|
import { UpdateWebServer } from "./update-webserver";
|
||||||
|
import type { IUpdateData } from "@dokploy/server/index";
|
||||||
|
|
||||||
export const UpdateServer = () => {
|
interface Props {
|
||||||
const [hasCheckedUpdate, setHasCheckedUpdate] = useState(false);
|
updateData?: IUpdateData;
|
||||||
const [isUpdateAvailable, setIsUpdateAvailable] = useState(false);
|
}
|
||||||
|
|
||||||
|
export const UpdateServer = ({ updateData }: Props) => {
|
||||||
|
const [hasCheckedUpdate, setHasCheckedUpdate] = useState(!!updateData);
|
||||||
|
const [isUpdateAvailable, setIsUpdateAvailable] = useState(
|
||||||
|
!!updateData?.updateAvailable,
|
||||||
|
);
|
||||||
const { mutateAsync: getUpdateData, isLoading } =
|
const { mutateAsync: getUpdateData, isLoading } =
|
||||||
api.settings.getUpdateData.useMutation();
|
api.settings.getUpdateData.useMutation();
|
||||||
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
||||||
const { data: releaseTag } = api.settings.getReleaseTag.useQuery();
|
const { data: releaseTag } = api.settings.getReleaseTag.useQuery();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [latestVersion, setLatestVersion] = useState("");
|
const [latestVersion, setLatestVersion] = useState(
|
||||||
|
updateData?.latestVersion ?? "",
|
||||||
|
);
|
||||||
|
|
||||||
const handleCheckUpdates = async () => {
|
const handleCheckUpdates = async () => {
|
||||||
try {
|
try {
|
||||||
@ -61,9 +70,24 @@ export const UpdateServer = () => {
|
|||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="secondary" className="gap-2">
|
<Button
|
||||||
<Sparkles className="h-4 w-4" />
|
variant={updateData ? "outline" : "secondary"}
|
||||||
Updates
|
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>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-lg p-6">
|
<DialogContent className="max-w-lg p-6">
|
||||||
@ -99,10 +123,6 @@ export const UpdateServer = () => {
|
|||||||
<div className="mb-8">
|
<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="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">
|
<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" />
|
<Download className="h-4 w-4 text-emerald-400" />
|
||||||
<span className="text font-medium text-emerald-400 ">
|
<span className="text font-medium text-emerald-400 ">
|
||||||
New version available:
|
New version available:
|
||||||
|
@ -11,30 +11,50 @@ import {
|
|||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { HardDriveDownload } from "lucide-react";
|
import { HardDriveDownload, Loader2 } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
export const UpdateWebServer = () => {
|
||||||
isNavbar?: boolean;
|
const [updating, setUpdating] = useState(false);
|
||||||
}
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
export const UpdateWebServer = ({ isNavbar }: Props) => {
|
const { mutateAsync: updateServer } = api.settings.updateServer.useMutation();
|
||||||
const { mutateAsync: updateServer, isLoading } =
|
|
||||||
api.settings.updateServer.useMutation();
|
|
||||||
|
|
||||||
const buttonLabel = isNavbar ? "Update available" : "Update Server";
|
const checkIsUpdateFinished = async () => {
|
||||||
|
|
||||||
const handleConfirm = async () => {
|
|
||||||
try {
|
try {
|
||||||
await updateServer();
|
const response = await fetch("/api/health");
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Health check failed");
|
||||||
|
}
|
||||||
|
|
||||||
toast.success(
|
toast.success(
|
||||||
"The server has been updated. The page will be reloaded to reflect the changes...",
|
"The server has been updated. The page will be reloaded to reflect the changes...",
|
||||||
);
|
);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Allow seeing the toast before reloading
|
// Allow seeing the toast before reloading
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}, 2000);
|
}, 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) {
|
} catch (error) {
|
||||||
|
setUpdating(false);
|
||||||
console.error("Error updating server:", error);
|
console.error("Error updating server:", error);
|
||||||
toast.error(
|
toast.error(
|
||||||
"An error occurred while updating the server, please try again.",
|
"An error occurred while updating the server, please try again.",
|
||||||
@ -43,35 +63,54 @@ export const UpdateWebServer = ({ isNavbar }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<AlertDialog open={open}>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
className="relative w-full"
|
className="relative w-full"
|
||||||
variant={isNavbar ? "outline" : "secondary"}
|
variant="secondary"
|
||||||
isLoading={isLoading}
|
onClick={() => setOpen(true)}
|
||||||
>
|
>
|
||||||
{!isLoading && <HardDriveDownload className="h-4 w-4" />}
|
<HardDriveDownload className="h-4 w-4" />
|
||||||
{!isLoading && (
|
<span className="absolute -right-1 -top-2 flex h-3 w-3">
|
||||||
<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="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 className="relative inline-flex rounded-full h-3 w-3 bg-green-500" />
|
</span>
|
||||||
</span>
|
Update Server
|
||||||
)}
|
|
||||||
{isLoading ? "Updating..." : buttonLabel}
|
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
<AlertDialogTitle>
|
||||||
|
{updating
|
||||||
|
? "Server update in progress"
|
||||||
|
: "Are you absolutely sure?"}
|
||||||
|
</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
This action cannot be undone. This will update the web server to the
|
{updating ? (
|
||||||
new version. The page will be reloaded once the update is finished.
|
<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>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
{!updating && (
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogFooter>
|
||||||
<AlertDialogAction onClick={handleConfirm}>Confirm</AlertDialogAction>
|
<AlertDialogCancel onClick={() => setOpen(false)}>
|
||||||
</AlertDialogFooter>
|
Cancel
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={handleConfirm}>
|
||||||
|
Confirm
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
)}
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
);
|
);
|
||||||
|
@ -13,15 +13,19 @@ import { HeartIcon } from "lucide-react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { UpdateWebServer } from "../dashboard/settings/web-server/update-webserver";
|
|
||||||
import { Logo } from "../shared/logo";
|
import { Logo } from "../shared/logo";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
||||||
import { buttonVariants } from "../ui/button";
|
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;
|
const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7;
|
||||||
|
|
||||||
export const Navbar = () => {
|
export const Navbar = () => {
|
||||||
const [isUpdateAvailable, setIsUpdateAvailable] = useState<boolean>(false);
|
const [updateData, setUpdateData] = useState<IUpdateData>({
|
||||||
|
latestVersion: null,
|
||||||
|
updateAvailable: false,
|
||||||
|
});
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data } = api.auth.get.useQuery();
|
const { data } = api.auth.get.useQuery();
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
@ -62,12 +66,12 @@ export const Navbar = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { updateAvailable } = await getUpdateData();
|
const fetchedUpdateData = await getUpdateData();
|
||||||
|
|
||||||
if (updateAvailable) {
|
if (fetchedUpdateData?.updateAvailable) {
|
||||||
// Stop interval when update is available
|
// Stop interval when update is available
|
||||||
clearUpdatesInterval();
|
clearUpdatesInterval();
|
||||||
setIsUpdateAvailable(true);
|
setUpdateData(fetchedUpdateData);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error auto-checking for updates:", error);
|
console.error("Error auto-checking for updates:", error);
|
||||||
@ -101,9 +105,9 @@ export const Navbar = () => {
|
|||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
{isUpdateAvailable && (
|
{updateData.updateAvailable && (
|
||||||
<div>
|
<div>
|
||||||
<UpdateWebServer isNavbar />
|
<UpdateServer updateData={updateData} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Link
|
<Link
|
||||||
|
8
apps/dokploy/pages/api/health.ts
Normal file
8
apps/dokploy/pages/api/health.ts
Normal 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 });
|
||||||
|
}
|
@ -359,7 +359,9 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
|
|
||||||
await pullLatestRelease();
|
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",
|
"service",
|
||||||
"update",
|
"update",
|
||||||
"--force",
|
"--force",
|
||||||
|
Loading…
Reference in New Issue
Block a user