refactor(ui): enhance update server button and sidebar layout

- Improve UpdateServer component with flexible rendering and tooltip support
- Modify sidebar layout to integrate update server button more cleanly
- Add conditional rendering and styling for update availability
- Introduce more consistent button and tooltip interactions
This commit is contained in:
Mauricio Siu 2025-03-08 15:31:08 -06:00
parent 8ba3a42c1e
commit c89f957133
3 changed files with 119 additions and 41 deletions

View File

@ -5,6 +5,12 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import type { IUpdateData } from "@dokploy/server/index"; import type { IUpdateData } from "@dokploy/server/index";
import { import {
@ -24,9 +30,17 @@ import { UpdateWebServer } from "./update-webserver";
interface Props { interface Props {
updateData?: IUpdateData; updateData?: IUpdateData;
children?: React.ReactNode;
isOpen?: boolean;
onOpenChange?: (open: boolean) => void;
} }
export const UpdateServer = ({ updateData }: Props) => { export const UpdateServer = ({
updateData,
children,
isOpen: isOpenProp,
onOpenChange: onOpenChangeProp,
}: Props) => {
const [hasCheckedUpdate, setHasCheckedUpdate] = useState(!!updateData); const [hasCheckedUpdate, setHasCheckedUpdate] = useState(!!updateData);
const [isUpdateAvailable, setIsUpdateAvailable] = useState( const [isUpdateAvailable, setIsUpdateAvailable] = useState(
!!updateData?.updateAvailable, !!updateData?.updateAvailable,
@ -35,10 +49,10 @@ export const UpdateServer = ({ updateData }: Props) => {
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 [latestVersion, setLatestVersion] = useState( const [latestVersion, setLatestVersion] = useState(
updateData?.latestVersion ?? "", updateData?.latestVersion ?? "",
); );
const [isOpenInternal, setIsOpenInternal] = useState(false);
const handleCheckUpdates = async () => { const handleCheckUpdates = async () => {
try { try {
@ -65,28 +79,52 @@ export const UpdateServer = ({ updateData }: Props) => {
} }
}; };
const isOpen = isOpenInternal || isOpenProp;
const onOpenChange = (open: boolean) => {
setIsOpenInternal(open);
onOpenChangeProp?.(open);
};
return ( return (
<Dialog open={isOpen} onOpenChange={setIsOpen}> <Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button {children ? (
variant={updateData ? "outline" : "secondary"} children
className="gap-2" ) : (
> <TooltipProvider delayDuration={0}>
{updateData ? ( <Tooltip>
<> <TooltipTrigger asChild>
<span className="flex h-2 w-2"> <Button
<span className="animate-ping absolute inline-flex h-2 w-2 rounded-full bg-emerald-400 opacity-75" /> variant={updateData ? "outline" : "secondary"}
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500" /> size="sm"
</span> onClick={() => onOpenChange?.(true)}
Update available >
</> <Download className="h-4 w-4 flex-shrink-0" />
) : ( {updateData ? (
<> <span className="font-medium truncate group-data-[collapsible=icon]:hidden">
<Sparkles className="h-4 w-4" /> Update Available
Updates </span>
</> ) : (
)} <span className="font-medium truncate group-data-[collapsible=icon]:hidden">
</Button> Check for updates
</span>
)}
{updateData && (
<span className="absolute right-2 flex h-2 w-2 group-data-[collapsible=icon]:hidden">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500" />
</span>
)}
</Button>
</TooltipTrigger>
{updateData && (
<TooltipContent side="right" sideOffset={10}>
<p>Update Available</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
)}
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-w-lg p-6"> <DialogContent className="max-w-lg p-6">
<div className="flex items-center justify-between mb-8"> <div className="flex items-center justify-between mb-8">
@ -217,7 +255,7 @@ export const UpdateServer = ({ updateData }: Props) => {
<div className="space-y-4 flex items-center justify-end"> <div className="space-y-4 flex items-center justify-end">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button variant="outline" onClick={() => setIsOpen(false)}> <Button variant="outline" onClick={() => onOpenChange?.(false)}>
Cancel Cancel
</Button> </Button>
{isUpdateAvailable ? ( {isUpdateAvailable ? (

View File

@ -37,8 +37,6 @@ import {
BreadcrumbItem, BreadcrumbItem,
BreadcrumbLink, BreadcrumbLink,
BreadcrumbList, BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"; } from "@/components/ui/breadcrumb";
import { import {
Collapsible, Collapsible,
@ -1017,18 +1015,16 @@ export default function Page({ children }: Props) {
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
))} ))}
{!isCloud && auth?.role === "owner" && (
<SidebarMenuItem>
<SidebarMenuButton asChild>
<UpdateServerButton />
</SidebarMenuButton>
</SidebarMenuItem>
)}
</SidebarMenu> </SidebarMenu>
</SidebarGroup> </SidebarGroup>
</SidebarContent> </SidebarContent>
<SidebarFooter> <SidebarFooter>
<SidebarMenu> <SidebarMenu className="flex flex-col gap-2">
{!isCloud && auth?.role === "owner" && (
<SidebarMenuItem>
<UpdateServerButton />
</SidebarMenuItem>
)}
<SidebarMenuItem> <SidebarMenuItem>
<UserNav /> <UserNav />
</SidebarMenuItem> </SidebarMenuItem>

View File

@ -3,7 +3,14 @@ import type { IUpdateData } from "@dokploy/server/index";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import UpdateServer from "../dashboard/settings/web-server/update-server"; import UpdateServer from "../dashboard/settings/web-server/update-server";
import { Button } from "../ui/button";
import { Download } from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "../ui/tooltip";
const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7; const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7;
export const UpdateServerButton = () => { export const UpdateServerButton = () => {
@ -15,6 +22,7 @@ export const UpdateServerButton = () => {
const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery();
const { mutateAsync: getUpdateData } = const { mutateAsync: getUpdateData } =
api.settings.getUpdateData.useMutation(); api.settings.getUpdateData.useMutation();
const [isOpen, setIsOpen] = useState(false);
const checkUpdatesIntervalRef = useRef<null | NodeJS.Timeout>(null); const checkUpdatesIntervalRef = useRef<null | NodeJS.Timeout>(null);
@ -69,11 +77,47 @@ export const UpdateServerButton = () => {
}; };
}, []); }, []);
return ( return updateData.updateAvailable ? (
updateData.updateAvailable && ( <div className="border-t pt-4">
<div> <UpdateServer
<UpdateServer updateData={updateData} /> updateData={updateData}
</div> isOpen={isOpen}
) onOpenChange={setIsOpen}
); >
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={updateData ? "outline" : "secondary"}
className="w-full"
onClick={() => setIsOpen(true)}
>
<Download className="h-4 w-4 flex-shrink-0" />
{updateData ? (
<span className="font-medium truncate group-data-[collapsible=icon]:hidden">
Update Available
</span>
) : (
<span className="font-medium truncate group-data-[collapsible=icon]:hidden">
Check for updates
</span>
)}
{updateData && (
<span className="absolute right-2 flex h-2 w-2 group-data-[collapsible=icon]:hidden">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500" />
</span>
)}
</Button>
</TooltipTrigger>
{updateData && (
<TooltipContent side="right" sideOffset={10}>
<p>Update Available</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
</UpdateServer>
</div>
) : null;
}; };