Merge pull request #957 from drudge/check-for-updates

New Updates UI
This commit is contained in:
Mauricio Siu
2024-12-21 12:47:24 -06:00
committed by GitHub
5 changed files with 184 additions and 57 deletions

View File

@@ -2,7 +2,7 @@ import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { useState } from "react"; import { useState } from "react";
export const ToggleAutoCheckUpdates = () => { export const ToggleAutoCheckUpdates = ({ disabled }: { disabled: boolean }) => {
const [enabled, setEnabled] = useState<boolean>( const [enabled, setEnabled] = useState<boolean>(
localStorage.getItem("enableAutoCheckUpdates") === "true", localStorage.getItem("enableAutoCheckUpdates") === "true",
); );
@@ -18,6 +18,7 @@ export const ToggleAutoCheckUpdates = () => {
checked={enabled} checked={enabled}
onCheckedChange={handleToggle} onCheckedChange={handleToggle}
id="autoCheckUpdatesToggle" id="autoCheckUpdatesToggle"
disabled={disabled}
/> />
<Label className="text-primary" htmlFor="autoCheckUpdatesToggle"> <Label className="text-primary" htmlFor="autoCheckUpdatesToggle">
Automatically check for new updates Automatically check for new updates

View File

@@ -3,38 +3,53 @@ import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription,
DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { RefreshCcw } from "lucide-react"; import {
Bug,
Download,
Info,
RefreshCcw,
Server,
ServerCrash,
Sparkles,
Stars,
} from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { UpdateWebServer } from "./update-webserver";
import { ToggleAutoCheckUpdates } from "./toggle-auto-check-updates"; import { ToggleAutoCheckUpdates } from "./toggle-auto-check-updates";
import { UpdateWebServer } from "./update-webserver";
export const UpdateServer = () => { export const UpdateServer = () => {
const [isUpdateAvailable, setIsUpdateAvailable] = useState<null | boolean>( const [hasCheckedUpdate, setHasCheckedUpdate] = useState(false);
null, const [isUpdateAvailable, setIsUpdateAvailable] = useState(false);
);
const { mutateAsync: getUpdateData, isLoading } = const { mutateAsync: getUpdateData, isLoading } =
api.settings.getUpdateData.useMutation(); api.settings.getUpdateData.useMutation();
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [latestVersion, setLatestVersion] = useState("");
const handleCheckUpdates = async () => { const handleCheckUpdates = async () => {
try { try {
const { updateAvailable, latestVersion } = await getUpdateData(); const updateData = await getUpdateData();
setIsUpdateAvailable(updateAvailable); const versionToUpdate = updateData.latestVersion || "";
if (updateAvailable) { setHasCheckedUpdate(true);
toast.success(`${latestVersion} update is available!`); setIsUpdateAvailable(updateData.updateAvailable);
setLatestVersion(versionToUpdate);
if (updateData.updateAvailable) {
toast.success(versionToUpdate, {
description: "New version available!",
});
} else { } else {
toast.info("No updates available"); toast.info("No updates available");
} }
} catch (error) { } catch (error) {
console.error("Error checking for updates:", error); console.error("Error checking for updates:", error);
setHasCheckedUpdate(true);
setIsUpdateAvailable(false); setIsUpdateAvailable(false);
toast.error( toast.error(
"An error occurred while checking for updates, please try again.", "An error occurred while checking for updates, please try again.",
@@ -45,59 +60,166 @@ export const UpdateServer = () => {
return ( return (
<Dialog open={isOpen} onOpenChange={setIsOpen}> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="secondary"> <Button variant="secondary" className="gap-2">
<RefreshCcw className="h-4 w-4" /> <Sparkles className="h-4 w-4" />
Updates Updates
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:m:max-w-lg "> <DialogContent className="max-w-lg p-6">
<DialogHeader> <div className="flex items-center justify-between mb-8">
<DialogTitle>Web Server Update</DialogTitle> <DialogTitle className="text-2xl font-semibold">
<DialogDescription> Web Server Update
Check new releases and update your dokploy </DialogTitle>
</DialogDescription> {dokployVersion && (
</DialogHeader> <div className="flex items-center gap-1.5 rounded-full px-3 py-1 mr-2 bg-muted">
<Server className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">
{dokployVersion}
</span>
</div>
)}
</div>
<div className="flex flex-col gap-4"> {/* Initial state */}
<span className="text-sm text-muted-foreground"> {!hasCheckedUpdate && (
We suggest to update your dokploy to the latest version only if you: <div className="mb-8">
</span> <p className="text text-muted-foreground">
<ul className="list-disc list-inside text-sm text-muted-foreground"> Check for new releases and update Dokploy.
<li>Want to try the latest features</li> <br />
<li>Some bug that is blocking to use some features</li> <br />
</ul> We recommend checking for updates regularly to ensure you have the
<AlertBlock type="info"> latest features and security improvements.
We recommend checking the latest version for any breaking changes </p>
before updating. Go to{" "} </div>
<Link )}
href="https://github.com/Dokploy/dokploy/releases"
target="_blank"
className="text-foreground"
>
Dokploy Releases
</Link>{" "}
to check the latest version.
</AlertBlock>
<div className="w-full flex flex-col gap-4"> {/* Update available state */}
<ToggleAutoCheckUpdates /> {isUpdateAvailable && latestVersion && (
{isUpdateAvailable === false && ( <div className="mb-8">
<div className="flex flex-col items-center gap-3"> <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">
<RefreshCcw className="size-6 self-center text-muted-foreground" /> <div className="flex items-center gap-1.5">
<span className="text-sm text-muted-foreground"> <span className="flex h-2 w-2">
You are using the latest version <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:
</span> </span>
</div> </div>
)} <span className="text font-semibold text-emerald-300">
{latestVersion}
</span>
</div>
<div className="space-y-4 text-muted-foreground">
<p className="text">
A new version of the server software is available. Consider
updating if you:
</p>
<ul className="space-y-3">
<li className="flex items-start gap-2">
<Stars className="h-5 w-5 mt-0.5 text-[#5B9DFF]" />
<span className="text">
Want to access the latest features and improvements
</span>
</li>
<li className="flex items-start gap-2">
<Bug className="h-5 w-5 mt-0.5 text-[#5B9DFF]" />
<span className="text">
Are experiencing issues that may be resolved in the new
version
</span>
</li>
</ul>
</div>
</div>
)}
{/* Up to date state */}
{hasCheckedUpdate && !isUpdateAvailable && !isLoading && (
<div className="mb-8">
<div className="flex flex-col items-center gap-6 mb-6">
<div className="rounded-full p-4 bg-emerald-400/40">
<Sparkles className="h-8 w-8 text-emerald-400" />
</div>
<div className="text-center space-y-2">
<h3 className="text-lg font-medium">
You are using the latest version
</h3>
<p className="text text-muted-foreground">
Your server is up to date with all the latest features and
security improvements.
</p>
</div>
</div>
</div>
)}
{hasCheckedUpdate && isLoading && (
<div className="mb-8">
<div className="flex flex-col items-center gap-6 mb-6">
<div className="rounded-full p-4 bg-[#5B9DFF]/40 text-foreground">
<RefreshCcw className="h-8 w-8 animate-spin" />
</div>
<div className="text-center space-y-2">
<h3 className="text-lg font-medium">Checking for updates...</h3>
<p className="text text-muted-foreground">
Please wait while we pull the latest version information from
Docker Hub.
</p>
</div>
</div>
</div>
)}
{isUpdateAvailable && (
<div className="rounded-lg bg-[#16254D] p-4 mb-8">
<div className="flex gap-2">
<Info className="h-5 w-5 flex-shrink-0 text-[#5B9DFF]" />
<div className="text-[#5B9DFF]">
We recommend reviewing the{" "}
<Link
href="https://github.com/Dokploy/dokploy/releases"
target="_blank"
className="text-white underline hover:text-zinc-200"
>
release notes
</Link>{" "}
for any breaking changes before updating.
</div>
</div>
</div>
)}
<div className="flex items-center justify-between pt-2">
<ToggleAutoCheckUpdates disabled={isLoading} />
</div>
<div className="space-y-4 flex items-center justify-end">
<div className="flex items-center gap-2">
<Button variant="outline" onClick={() => setIsOpen(false)}>
Cancel
</Button>
{isUpdateAvailable ? ( {isUpdateAvailable ? (
<UpdateWebServer /> <UpdateWebServer />
) : ( ) : (
<Button <Button
className="w-full" variant="secondary"
onClick={handleCheckUpdates} onClick={handleCheckUpdates}
isLoading={isLoading} disabled={isLoading}
> >
{isLoading ? "Checking for updates..." : "Check for updates"} {isLoading ? (
<>
<RefreshCcw className="h-4 w-4 animate-spin" />
Checking for updates
</>
) : (
<>
<RefreshCcw className="h-4 w-4" />
Check for updates
</>
)}
</Button> </Button>
)} )}
</div> </div>
@@ -106,3 +228,5 @@ export const UpdateServer = () => {
</Dialog> </Dialog>
); );
}; };
export default UpdateServer;

View File

@@ -11,6 +11,7 @@ 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 { toast } from "sonner"; import { toast } from "sonner";
interface Props { interface Props {
@@ -21,7 +22,7 @@ export const UpdateWebServer = ({ isNavbar }: Props) => {
const { mutateAsync: updateServer, isLoading } = const { mutateAsync: updateServer, isLoading } =
api.settings.updateServer.useMutation(); api.settings.updateServer.useMutation();
const buttonLabel = isNavbar ? "Update available" : "Update server"; const buttonLabel = isNavbar ? "Update available" : "Update Server";
const handleConfirm = async () => { const handleConfirm = async () => {
try { try {
@@ -49,6 +50,7 @@ export const UpdateWebServer = ({ isNavbar }: Props) => {
variant={isNavbar ? "outline" : "secondary"} variant={isNavbar ? "outline" : "secondary"}
isLoading={isLoading} isLoading={isLoading}
> >
{!isLoading && <HardDriveDownload className="h-4 w-4" />}
{!isLoading && ( {!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" />

View File

@@ -12,11 +12,11 @@ import { api } from "@/utils/api";
import { HeartIcon } from "lucide-react"; 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 { 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 { useEffect, useRef, useState } from "react";
import { UpdateWebServer } from "../dashboard/settings/web-server/update-webserver";
const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 5; const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 5;

View File

@@ -12,6 +12,7 @@ import {
} from "@/server/db/schema"; } from "@/server/db/schema";
import { removeJob, schedule } from "@/server/utils/backup"; import { removeJob, schedule } from "@/server/utils/backup";
import { import {
DEFAULT_UPDATE_DATA,
IS_CLOUD, IS_CLOUD,
canAccessToTraefikFiles, canAccessToTraefikFiles,
cleanStoppedContainers, cleanStoppedContainers,
@@ -25,6 +26,7 @@ import {
findAdminById, findAdminById,
findServerById, findServerById,
getDokployImage, getDokployImage,
getUpdateData,
initializeTraefik, initializeTraefik,
logRotationManager, logRotationManager,
parseRawConfig, parseRawConfig,
@@ -45,14 +47,12 @@ import {
stopService, stopService,
stopServiceRemote, stopServiceRemote,
updateAdmin, updateAdmin,
getUpdateData,
updateLetsEncryptEmail, updateLetsEncryptEmail,
updateServerById, updateServerById,
updateServerTraefik, updateServerTraefik,
writeConfig, writeConfig,
writeMainConfig, writeMainConfig,
writeTraefikConfigInPath, writeTraefikConfigInPath,
DEFAULT_UPDATE_DATA,
} from "@dokploy/server"; } from "@dokploy/server";
import { checkGPUStatus, setupGPUSupport } from "@dokploy/server"; import { checkGPUStatus, setupGPUSupport } from "@dokploy/server";
import { generateOpenApiDocument } from "@dokploy/trpc-openapi"; import { generateOpenApiDocument } from "@dokploy/trpc-openapi";