Merge branch 'canary' into canary

This commit is contained in:
Mohab Gabber
2025-01-03 19:11:33 +02:00
committed by GitHub
21 changed files with 523 additions and 316 deletions

View File

@@ -13,10 +13,12 @@ import { CardTitle } from "@/components/ui/card";
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "@/components/ui/input-otp";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { REGEXP_ONLY_DIGITS } from "input-otp";
import { AlertTriangle } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect } from "react";
@@ -96,16 +98,22 @@ export const Login2FA = ({ authId }: Props) => {
<FormItem className="flex flex-col justify-center max-sm:items-center">
<FormLabel>Pin</FormLabel>
<FormControl>
<InputOTP maxLength={6} {...field}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
<div className="flex justify-center">
<InputOTP
maxLength={6}
{...field}
pattern={REGEXP_ONLY_DIGITS}
>
<InputOTPGroup>
<InputOTPSlot index={0} className="border-border" />
<InputOTPSlot index={1} className="border-border" />
<InputOTPSlot index={2} className="border-border" />
<InputOTPSlot index={3} className="border-border" />
<InputOTPSlot index={4} className="border-border" />
<InputOTPSlot index={5} className="border-border" />
</InputOTPGroup>
</InputOTP>
</div>
</FormControl>
<FormDescription>
Please enter the 6 digits code provided by your authenticator

View File

@@ -81,7 +81,8 @@ export const AddCommand = ({ applicationId }: Props) => {
<div>
<CardTitle className="text-xl">Run Command</CardTitle>
<CardDescription>
Run a custom command in the container
Run a custom command in the container after the application
initialized
</CardDescription>
</div>
</CardHeader>

View File

@@ -55,13 +55,13 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
});
};
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex w-full flex-col gap-5 "
>
<Card className="bg-background p-6">
return (
<Card className="bg-background px-6 pb-6">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex w-full flex-col gap-4"
>
<Secrets
name="env"
title="Environment Settings"
@@ -89,15 +89,13 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
placeholder="NPM_TOKEN=xyz"
/>
)}
<CardContent>
<div className="flex flex-row justify-end">
<Button isLoading={isLoading} className="w-fit" type="submit">
Save
</Button>
</div>
</CardContent>
</Card>
</form>
</Form>
);
<div className="flex flex-row justify-end">
<Button isLoading={isLoading} className="w-fit" type="submit">
Save
</Button>
</div>
</form>
</Form>
</Card>
);
};

View File

@@ -94,10 +94,18 @@ export const ShowRequests = () => {
</div>
</CardHeader>
<CardContent>
<RequestDistributionChart />
{isActive ? (
<RequestDistributionChart />
) : (
<div className="flex items-center justify-center min-h-[25vh]">
<span className="text-muted-foreground py-6">
You need to activate requests
</span>
</div>
)}
</CardContent>
</Card>
<RequestsTable />
{isActive && <RequestsTable />}
</>
);
};

View File

@@ -18,6 +18,7 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
@@ -114,7 +115,15 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
</DialogTitle>
<DialogDescription className="text-base w-full">
<div className="flex items-center justify-between">
{t("settings.server.webServer.traefik.managePortsDescription")}
<div className="flex flex-col gap-1">
{t(
"settings.server.webServer.traefik.managePortsDescription",
)}
<span className="text-sm text-muted-foreground">
{fields.length} port mapping{fields.length !== 1 ? "s" : ""}{" "}
configured
</span>
</div>
<Button
onClick={handleAddPort}
variant="default"
@@ -141,109 +150,111 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
</p>
</div>
) : (
<div className="grid gap-4">
{fields.map((field, index) => (
<Card key={field.id}>
<CardContent className="grid grid-cols-[1fr_1fr_1.5fr_auto] gap-4 p-4 transparent">
<FormField
control={form.control}
name={`ports.${index}.targetPort`}
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium text-muted-foreground">
{t(
"settings.server.webServer.traefik.targetPort",
)}
</FormLabel>
<FormControl>
<Input
type="number"
{...field}
onChange={(e) =>
field.onChange(Number(e.target.value))
}
className="w-full dark:bg-black"
placeholder="e.g. 8080"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`ports.${index}.publishedPort`}
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium text-muted-foreground">
{t(
"settings.server.webServer.traefik.publishedPort",
)}
</FormLabel>
<FormControl>
<Input
type="number"
{...field}
onChange={(e) =>
field.onChange(Number(e.target.value))
}
className="w-full dark:bg-black"
placeholder="e.g. 80"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`ports.${index}.publishMode`}
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium text-muted-foreground">
{t(
"settings.server.webServer.traefik.publishMode",
)}
</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
>
<ScrollArea className="h-[400px] pr-4">
<div className="grid gap-4">
{fields.map((field, index) => (
<Card key={field.id}>
<CardContent className="grid grid-cols-[1fr_1fr_1.5fr_auto] gap-4 p-4 transparent">
<FormField
control={form.control}
name={`ports.${index}.targetPort`}
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium text-muted-foreground">
{t(
"settings.server.webServer.traefik.targetPort",
)}
</FormLabel>
<FormControl>
<SelectTrigger className="dark:bg-black">
<SelectValue />
</SelectTrigger>
<Input
type="number"
{...field}
onChange={(e) =>
field.onChange(Number(e.target.value))
}
className="w-full dark:bg-black"
placeholder="e.g. 8080"
/>
</FormControl>
<SelectContent>
<SelectItem value="host">
Host Mode
</SelectItem>
<SelectItem value="ingress">
Ingress Mode
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
</FormItem>
)}
/>
<div className="flex items-end">
<Button
onClick={() => remove(index)}
variant="ghost"
size="icon"
className="text-muted-foreground hover:text-destructive"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
))}
</div>
<FormField
control={form.control}
name={`ports.${index}.publishedPort`}
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium text-muted-foreground">
{t(
"settings.server.webServer.traefik.publishedPort",
)}
</FormLabel>
<FormControl>
<Input
type="number"
{...field}
onChange={(e) =>
field.onChange(Number(e.target.value))
}
className="w-full dark:bg-black"
placeholder="e.g. 80"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`ports.${index}.publishMode`}
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium text-muted-foreground">
{t(
"settings.server.webServer.traefik.publishMode",
)}
</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
>
<FormControl>
<SelectTrigger className="dark:bg-black">
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="host">
Host Mode
</SelectItem>
<SelectItem value="ingress">
Ingress Mode
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<div className="flex items-end">
<Button
onClick={() => remove(index)}
variant="ghost"
size="icon"
className="text-muted-foreground hover:text-destructive"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</ScrollArea>
)}
{fields.length > 0 && (
@@ -281,7 +292,6 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
</AlertBlock>
)}
</div>
<DialogFooter>
<Button
type="submit"

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">
<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">
@@ -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 && (
<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>
);

View File

@@ -63,7 +63,7 @@ export function NodeCard({ node, serverId }: Props) {
</CardHeader>
<CardContent>
<div className="space-y-6">
<div className="flex flex-wrap items-center justify-between">
<div className="flex flex-wrap gap-y-2 items-center justify-between">
<div className="flex items-center space-x-4 p-2 rounded-xl border">
<div className={`h-2.5 w-2.5 rounded-full ${node.Status === "Ready" ? "bg-green-500" : "bg-red-500"}`} />
<div className="font-medium">{node.Hostname}</div>

View File

@@ -2,29 +2,23 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import {
Activity,
Loader2,
Monitor,
Settings,
Server,
} from "lucide-react";
import { Activity, Loader2, Monitor, Settings, Server } from "lucide-react";
import { NodeCard } from "./details/details-card";
interface Props {
serverId?: string;
serverId?: string;
}
export default function SwarmMonitorCard({ serverId }: Props) {
const { data: nodes, isLoading } = api.swarm.getNodes.useQuery({
serverId,
});
const { data: nodes, isLoading } = api.swarm.getNodes.useQuery({
serverId,
});
if (isLoading) {
return (
@@ -50,116 +44,132 @@ export default function SwarmMonitorCard({ serverId }: Props) {
);
}
const totalNodes = nodes.length;
const activeNodesCount = nodes.filter((node) => node.Status === "Ready").length;
const managerNodesCount = nodes.filter((node) =>node.ManagerStatus === "Leader" || node.ManagerStatus === "Reachable").length;
const activeNodes = nodes.filter((node) => node.Status === "Ready");
const managerNodes = nodes.filter((node) => node.ManagerStatus === "Leader" || node.ManagerStatus === "Reachable");
const totalNodes = nodes.length;
const activeNodesCount = nodes.filter(
(node) => node.Status === "Ready",
).length;
const managerNodesCount = nodes.filter(
(node) =>
node.ManagerStatus === "Leader" || node.ManagerStatus === "Reachable",
).length;
const activeNodes = nodes.filter((node) => node.Status === "Ready");
const managerNodes = nodes.filter(
(node) =>
node.ManagerStatus === "Leader" || node.ManagerStatus === "Reachable",
);
return (
<div className="min-h-screen">
<div className="w-full max-w-7xl mx-auto space-y-6 py-4">
<header className="flex items-center justify-between">
<div className="space-y-1">
<h1 className="text-2xl font-semibold tracking-tight">Docker Swarm Overview</h1>
<p className="text-sm text-muted-foreground">Monitor and manage your Docker Swarm cluster</p>
</div>
{!serverId && (
<Button onClick={() => window.location.replace("/dashboard/settings/cluster")}>
<Settings className="mr-2 h-4 w-4" />
Manage Cluster
</Button>
)}
</header>
return (
<div>
<div className="w-full max-w-7xl mx-auto space-y-6 py-4">
<header className="flex items-center justify-between">
<div className="space-y-1">
<h1 className="text-2xl font-semibold tracking-tight">
Docker Swarm Overview
</h1>
<p className="text-sm text-muted-foreground">
Monitor and manage your Docker Swarm cluster
</p>
</div>
{!serverId && (
<Button
onClick={() =>
window.location.replace("/dashboard/settings/cluster")
}
>
<Settings className="mr-2 h-4 w-4" />
Manage Cluster
</Button>
)}
</header>
<div className="grid gap-6 md:grid-cols-3">
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Nodes</CardTitle>
<div className="p-2 bg-emerald-600/20 text-emerald-600 rounded-md">
<Server className="h-4 w-4 text-muted-foreground dark:text-emerald-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{totalNodes}</div>
</CardContent>
</Card>
<div className="grid gap-6 md:grid-cols-3">
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Nodes</CardTitle>
<div className="p-2 bg-emerald-600/20 text-emerald-600 rounded-md">
<Server className="h-4 w-4 text-muted-foreground dark:text-emerald-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{totalNodes}</div>
</CardContent>
</Card>
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Active Nodes
<Badge variant="green">
Online
</Badge>
</CardTitle>
<div className="p-2 bg-emerald-600/20 text-emerald-600 rounded-md">
<Activity className="h-4 w-4 text-muted-foreground dark:text-emerald-600" />
</div>
</CardHeader>
<CardContent>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<div className="text-2xl font-bold">
{activeNodesCount} / {totalNodes}
</div>
</TooltipTrigger>
<TooltipContent>
<div className="max-h-48 overflow-y-auto">
{activeNodes.map((node) => (
<div key={node.ID} className="flex items-center gap-2">
{node.Hostname}
</div>
))}
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</CardContent>
</Card>
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<div className="flex items-center gap-2">
<CardTitle className="text-sm font-medium">
Active Nodes
</CardTitle>
<Badge variant="green">Online</Badge>
</div>
<div className="p-2 bg-emerald-600/20 text-emerald-600 rounded-md">
<Activity className="h-4 w-4 text-muted-foreground dark:text-emerald-600" />
</div>
</CardHeader>
<CardContent>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<div className="text-2xl font-bold">
{activeNodesCount} / {totalNodes}
</div>
</TooltipTrigger>
<TooltipContent>
<div className="max-h-48 overflow-y-auto">
{activeNodes.map((node) => (
<div key={node.ID} className="flex items-center gap-2">
{node.Hostname}
</div>
))}
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</CardContent>
</Card>
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Manager Nodes
<Badge variant="green">
Online
</Badge>
</CardTitle>
<div className="p-2 bg-emerald-600/20 text-emerald-600 rounded-md">
<Monitor className="h-4 w-4 text-muted-foreground dark:text-emerald-600" />
</div>
</CardHeader>
<CardContent>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<div className="text-2xl font-bold">
{managerNodesCount} / {totalNodes}
</div>
</TooltipTrigger>
<TooltipContent>
<div className="max-h-48 overflow-y-auto">
{managerNodes.map((node) => (
<div key={node.ID} className="flex items-center gap-2">
{node.Hostname}
</div>
))}
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</CardContent>
</Card>
</div>
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<div className="flex items-center gap-2">
<CardTitle className="text-sm font-medium">
Manager Nodes
</CardTitle>
<Badge variant="green">Online</Badge>
</div>
<div className="p-2 bg-emerald-600/20 text-emerald-600 rounded-md">
<Monitor className="h-4 w-4 text-muted-foreground dark:text-emerald-600" />
</div>
</CardHeader>
<CardContent>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<div className="text-2xl font-bold">
{managerNodesCount} / {totalNodes}
</div>
</TooltipTrigger>
<TooltipContent>
<div className="max-h-48 overflow-y-auto">
{managerNodes.map((node) => (
<div key={node.ID} className="flex items-center gap-2">
{node.Hostname}
</div>
))}
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</CardContent>
</Card>
</div>
<div className="flex flex-row gap-4">
{nodes.map((node) => (
<NodeCard key={node.ID} node={node} serverId={serverId} />
))}
</div>
</div>
</div>
);
}
<div className="flex flex-row gap-4">
{nodes.map((node) => (
<NodeCard key={node.ID} node={node} serverId={serverId} />
))}
</div>
</div>
</div>
);
}

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

@@ -1,5 +1,12 @@
<svg width="6323" height="5778" viewBox="0 0 6323 5778" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4638.51 44.5295C4616.52 81.8286 4611.45 115.575 4619.9 213.263C4636.82 433.505 4772.12 710.584 4924.33 842.019C5002.12 909.512 5196.61 1012.53 5245.66 1012.53C5284.56 1012.53 5282.87 1019.63 5213.53 1129.75C5140.8 1243.43 5024.11 1339.34 4890.5 1389.07C4743.36 1445.91 4455.85 1453.01 4234.3 1405.06C4016.13 1357.1 3931.57 1323.35 3211.11 977.006C2265.71 522.312 2253.87 516.984 2125.34 481.461C2017.1 451.267 1917.32 445.938 1316.93 435.281C853.533 428.177 601.539 429.953 538.964 444.162C334.325 485.013 156.745 632.434 70.4925 829.586C12.9907 961.021 -7.30411 1191.92 2.84328 1589.78C7.91697 1841.99 16.3731 1911.26 46.8153 2005.39C114.465 2213.2 226.086 2342.86 422.269 2445.88C1594.29 3055.1 1969.74 3206.07 2529.54 3294.88C2732.49 3326.85 3258.46 3330.4 3459.72 3303.76C3755.69 3261.13 4107.46 3161.66 4403.43 3033.78C4540.42 2975.17 4904.03 2776.24 5220.29 2587.97C5910.31 2177.68 6006.71 2111.96 6037.16 2030.26C6070.98 1934.35 5988.11 1811.79 5888.33 1811.79C5851.12 1811.79 5862.96 1806.47 5426.62 2069.34C4352.69 2715.85 4026.28 2865.05 3485.09 2957.41C3162.06 3014.24 2587.04 2987.6 2274.17 2902.35C1924.08 2806.44 1839.52 2770.91 1051.41 2383.71C552.493 2140.38 444.255 2079.99 395.209 2023.16C363.076 1984.08 336.016 1945.01 336.016 1934.35C336.016 1920.14 467.932 1916.59 787.575 1921.92L1240.82 1929.02L1435.32 2001.84C1541.86 2040.92 1744.81 2126.17 1883.49 2190.11C2296.15 2381.94 2610.72 2451.21 3058.9 2451.21C3490.16 2451.21 3872.38 2374.83 4305.33 2198.99C4910.8 1955.66 5342.06 1596.88 5545.01 1172.38C5565.3 1127.98 5585.6 1090.68 5587.29 1087.13C5590.67 1083.57 5660.01 1074.69 5742.88 1065.81C5940.76 1046.28 6084.51 978.782 6221.5 842.019L6322.97 740.779V520.536V302.071L6253.63 353.579C6177.53 412.192 6062.52 444.162 5920.46 444.162C5795.31 444.162 5661.7 508.104 5568.69 614.672L5497.65 692.823L5487.51 646.643C5451.99 500.999 5304.85 364.236 5115.44 300.294C4956.46 248.786 4893.88 206.159 4831.31 108.471C4800.87 64.0671 4770.42 21.4395 4760.28 14.335C4721.38 -14.0833 4665.57 1.90186 4638.51 44.5295ZM2057.69 806.496C2162.55 834.914 2250.49 873.99 2517.7 1007.2C2605.65 1051.6 2796.76 1142.19 2940.51 1211.46C3084.27 1280.73 3332.88 1397.95 3490.16 1472.55C3948.49 1691.02 4049.96 1726.54 4301.95 1754.96L4437.25 1770.94L4310.41 1833.11C4153.12 1911.26 4016.13 1960.99 3804.73 2016.05C3512.15 2090.65 3402.22 2104.86 3050.44 2104.86C2590.43 2103.08 2370.57 2056.9 1974.82 1872.18C1413.33 1611.09 1386.27 1603.99 801.104 1589.78C457.784 1580.9 356.311 1572.01 336.016 1552.48C278.514 1492.09 303.882 1019.63 373.223 914.841C412.121 854.452 474.697 806.496 552.493 779.854C577.862 770.973 904.27 767.421 1278.03 772.749C1814.15 778.078 1978.2 785.182 2057.69 806.496Z" fill="white"/>
<path d="M1266.2 1060.49C1173.18 1097.79 1129.21 1207.91 1171.49 1294.94C1222.22 1394.4 1332.15 1417.49 1413.33 1342.89C1477.6 1286.06 1479.29 1174.16 1418.41 1112C1374.44 1065.82 1308.48 1042.73 1266.2 1060.49Z" fill="white"/>
<path d="M87.4063 2513.37C7.91846 2548.89 -8.99385 2616.39 4.536 2836.63C19.7571 3072.86 46.8168 3222.05 124.613 3488.48C427.344 4532.85 1129.2 5287.71 2106.74 5623.4C2641.17 5806.35 3236.48 5827.66 3752.3 5682.01C4596.23 5445.79 5315 4836.57 5692.15 4040.86C5886.64 3630.57 6018.55 3111.93 6018.55 2753.15C6018.55 2582.64 5991.49 2518.7 5910.31 2497.39C5820.68 2474.3 5575.45 2609.28 5164.48 2911.23C4484.61 3410.32 4229.23 3563.07 3890.98 3676.75C3635.61 3763.78 3466.49 3797.52 3194.2 3818.84C2651.31 3863.24 2057.69 3731.81 1570.62 3458.28C1394.73 3358.82 846.769 2980.5 581.246 2772.69C285.28 2540.01 270.059 2529.36 199.028 2508.04C155.056 2495.61 124.613 2497.39 87.4063 2513.37ZM5678.62 3076.41C5661.7 3138.57 5646.48 3202.52 5646.48 3218.5C5646.48 3236.26 5626.19 3262.9 5600.82 3280.67C5573.76 3296.65 5482.43 3371.25 5396.18 3445.85C5308.24 3518.67 5198.31 3611.03 5150.95 3650.1C5101.91 3689.18 4990.28 3781.54 4902.34 3856.14C4699.39 4026.65 4406.81 4236.23 4242.76 4330.37C4085.48 4420.95 3767.52 4532.85 3532.44 4582.58C2847.5 4724.67 2054.31 4570.15 1516.5 4190.05C1173.18 3946.72 412.123 3314.41 388.445 3254.02C363.077 3182.98 330.944 3042.66 337.708 3021.35C341.091 3012.47 417.196 3060.42 505.14 3129.69C1056.48 3559.52 1563.85 3863.24 1942.69 3992.9C2328.29 4124.34 2565.06 4163.41 2991.25 4163.41C3380.23 4163.41 3628.84 4126.11 3963.71 4012.44C4345.93 3884.56 4531.96 3781.54 5052.86 3405C5391.11 3161.66 5676.92 2968.06 5700.6 2966.29C5705.68 2966.29 5697.22 3016.02 5678.62 3076.41ZM5426.62 3881C5426.62 3886.33 5409.71 3925.41 5391.11 3966.26C5318.38 4115.45 5144.19 4364.11 5003.81 4518.64C4587.77 4973.33 4090.55 5271.73 3540.9 5392.5C3309.2 5444.01 2708.81 5440.46 2483.88 5387.17C1716.06 5204.23 1105.53 4754.87 696.249 4071.05C647.204 3987.57 609.997 3916.53 613.379 3912.97C616.762 3909.42 774.046 4028.42 965.155 4177.62C1154.57 4326.82 1371.05 4486.67 1443.77 4532.85C1974.82 4863.21 2463.59 4991.09 3118.09 4968C3461.41 4955.57 3691.42 4912.94 3997.53 4806.38C4357.76 4680.27 4623.29 4513.31 5130.66 4095.92C5382.65 3888.11 5426.62 3856.14 5426.62 3881Z" fill="white"/>
<style>
:root { color-scheme: light dark; }
path { fill: black; }
@media (prefers-color-scheme: dark) {
path { fill: white; }
}
</style>
<path d="M4638.51 44.5295C4616.52 81.8286 4611.45 115.575 4619.9 213.263C4636.82 433.505 4772.12 710.584 4924.33 842.019C5002.12 909.512 5196.61 1012.53 5245.66 1012.53C5284.56 1012.53 5282.87 1019.63 5213.53 1129.75C5140.8 1243.43 5024.11 1339.34 4890.5 1389.07C4743.36 1445.91 4455.85 1453.01 4234.3 1405.06C4016.13 1357.1 3931.57 1323.35 3211.11 977.006C2265.71 522.312 2253.87 516.984 2125.34 481.461C2017.1 451.267 1917.32 445.938 1316.93 435.281C853.533 428.177 601.539 429.953 538.964 444.162C334.325 485.013 156.745 632.434 70.4925 829.586C12.9907 961.021 -7.30411 1191.92 2.84328 1589.78C7.91697 1841.99 16.3731 1911.26 46.8153 2005.39C114.465 2213.2 226.086 2342.86 422.269 2445.88C1594.29 3055.1 1969.74 3206.07 2529.54 3294.88C2732.49 3326.85 3258.46 3330.4 3459.72 3303.76C3755.69 3261.13 4107.46 3161.66 4403.43 3033.78C4540.42 2975.17 4904.03 2776.24 5220.29 2587.97C5910.31 2177.68 6006.71 2111.96 6037.16 2030.26C6070.98 1934.35 5988.11 1811.79 5888.33 1811.79C5851.12 1811.79 5862.96 1806.47 5426.62 2069.34C4352.69 2715.85 4026.28 2865.05 3485.09 2957.41C3162.06 3014.24 2587.04 2987.6 2274.17 2902.35C1924.08 2806.44 1839.52 2770.91 1051.41 2383.71C552.493 2140.38 444.255 2079.99 395.209 2023.16C363.076 1984.08 336.016 1945.01 336.016 1934.35C336.016 1920.14 467.932 1916.59 787.575 1921.92L1240.82 1929.02L1435.32 2001.84C1541.86 2040.92 1744.81 2126.17 1883.49 2190.11C2296.15 2381.94 2610.72 2451.21 3058.9 2451.21C3490.16 2451.21 3872.38 2374.83 4305.33 2198.99C4910.8 1955.66 5342.06 1596.88 5545.01 1172.38C5565.3 1127.98 5585.6 1090.68 5587.29 1087.13C5590.67 1083.57 5660.01 1074.69 5742.88 1065.81C5940.76 1046.28 6084.51 978.782 6221.5 842.019L6322.97 740.779V520.536V302.071L6253.63 353.579C6177.53 412.192 6062.52 444.162 5920.46 444.162C5795.31 444.162 5661.7 508.104 5568.69 614.672L5497.65 692.823L5487.51 646.643C5451.99 500.999 5304.85 364.236 5115.44 300.294C4956.46 248.786 4893.88 206.159 4831.31 108.471C4800.87 64.0671 4770.42 21.4395 4760.28 14.335C4721.38 -14.0833 4665.57 1.90186 4638.51 44.5295ZM2057.69 806.496C2162.55 834.914 2250.49 873.99 2517.7 1007.2C2605.65 1051.6 2796.76 1142.19 2940.51 1211.46C3084.27 1280.73 3332.88 1397.95 3490.16 1472.55C3948.49 1691.02 4049.96 1726.54 4301.95 1754.96L4437.25 1770.94L4310.41 1833.11C4153.12 1911.26 4016.13 1960.99 3804.73 2016.05C3512.15 2090.65 3402.22 2104.86 3050.44 2104.86C2590.43 2103.08 2370.57 2056.9 1974.82 1872.18C1413.33 1611.09 1386.27 1603.99 801.104 1589.78C457.784 1580.9 356.311 1572.01 336.016 1552.48C278.514 1492.09 303.882 1019.63 373.223 914.841C412.121 854.452 474.697 806.496 552.493 779.854C577.862 770.973 904.27 767.421 1278.03 772.749C1814.15 778.078 1978.2 785.182 2057.69 806.496Z"/>
<path d="M1266.2 1060.49C1173.18 1097.79 1129.21 1207.91 1171.49 1294.94C1222.22 1394.4 1332.15 1417.49 1413.33 1342.89C1477.6 1286.06 1479.29 1174.16 1418.41 1112C1374.44 1065.82 1308.48 1042.73 1266.2 1060.49Z"/>
<path d="M87.4063 2513.37C7.91846 2548.89 -8.99385 2616.39 4.536 2836.63C19.7571 3072.86 46.8168 3222.05 124.613 3488.48C427.344 4532.85 1129.2 5287.71 2106.74 5623.4C2641.17 5806.35 3236.48 5827.66 3752.3 5682.01C4596.23 5445.79 5315 4836.57 5692.15 4040.86C5886.64 3630.57 6018.55 3111.93 6018.55 2753.15C6018.55 2582.64 5991.49 2518.7 5910.31 2497.39C5820.68 2474.3 5575.45 2609.28 5164.48 2911.23C4484.61 3410.32 4229.23 3563.07 3890.98 3676.75C3635.61 3763.78 3466.49 3797.52 3194.2 3818.84C2651.31 3863.24 2057.69 3731.81 1570.62 3458.28C1394.73 3358.82 846.769 2980.5 581.246 2772.69C285.28 2540.01 270.059 2529.36 199.028 2508.04C155.056 2495.61 124.613 2497.39 87.4063 2513.37ZM5678.62 3076.41C5661.7 3138.57 5646.48 3202.52 5646.48 3218.5C5646.48 3236.26 5626.19 3262.9 5600.82 3280.67C5573.76 3296.65 5482.43 3371.25 5396.18 3445.85C5308.24 3518.67 5198.31 3611.03 5150.95 3650.1C5101.91 3689.18 4990.28 3781.54 4902.34 3856.14C4699.39 4026.65 4406.81 4236.23 4242.76 4330.37C4085.48 4420.95 3767.52 4532.85 3532.44 4582.58C2847.5 4724.67 2054.31 4570.15 1516.5 4190.05C1173.18 3946.72 412.123 3314.41 388.445 3254.02C363.077 3182.98 330.944 3042.66 337.708 3021.35C341.091 3012.47 417.196 3060.42 505.14 3129.69C1056.48 3559.52 1563.85 3863.24 1942.69 3992.9C2328.29 4124.34 2565.06 4163.41 2991.25 4163.41C3380.23 4163.41 3628.84 4126.11 3963.71 4012.44C4345.93 3884.56 4531.96 3781.54 5052.86 3405C5391.11 3161.66 5676.92 2968.06 5700.6 2966.29C5705.68 2966.29 5697.22 3016.02 5678.62 3076.41ZM5426.62 3881C5426.62 3886.33 5409.71 3925.41 5391.11 3966.26C5318.38 4115.45 5144.19 4364.11 5003.81 4518.64C4587.77 4973.33 4090.55 5271.73 3540.9 5392.5C3309.2 5444.01 2708.81 5440.46 2483.88 5387.17C1716.06 5204.23 1105.53 4754.87 696.249 4071.05C647.204 3987.57 609.997 3916.53 613.379 3912.97C616.762 3909.42 774.046 4028.42 965.155 4177.62C1154.57 4326.82 1371.05 4486.67 1443.77 4532.85C1974.82 4863.21 2463.59 4991.09 3118.09 4968C3461.41 4955.57 3691.42 4912.94 3997.53 4806.38C4357.76 4680.27 4623.29 4513.31 5130.66 4095.92C5382.65 3888.11 5426.62 3856.14 5426.62 3881Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -19,6 +19,14 @@
"settings.server.webServer.server.label": "Serwer",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Zmodyfikuj środowisko",
"settings.server.webServer.traefik.managePorts": "Dodatkowe mapowania portów",
"settings.server.webServer.traefik.managePortsDescription": "Dodaj lub usuń dodatkowe porty dla Traefik",
"settings.server.webServer.traefik.targetPort": "Port docelowy",
"settings.server.webServer.traefik.publishedPort": "Port opublikowany",
"settings.server.webServer.traefik.addPort": "Dodaj port",
"settings.server.webServer.traefik.portsUpdated": "Porty zaktualizowane pomyślnie",
"settings.server.webServer.traefik.portsUpdateError": "Nie udało się zaktualizować portów",
"settings.server.webServer.traefik.publishMode": "Tryb publikacji",
"settings.server.webServer.storage.label": "Przestrzeń",
"settings.server.webServer.storage.cleanUnusedImages": "Wyczyść nieużywane obrazy",
"settings.server.webServer.storage.cleanUnusedVolumes": "Wyczyść nieużywane wolumeny",

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

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",

View File

@@ -0,0 +1,47 @@
services:
client:
image: bluewaveuptime/uptime_client:latest
restart: always
environment:
UPTIME_APP_API_BASE_URL: "http://${DOMAIN}/api/v1"
ports:
- 80
- 443
depends_on:
- server
networks:
- dokploy-network
server:
image: bluewaveuptime/uptime_server:latest
restart: always
ports:
- 5000
depends_on:
- redis
- mongodb
environment:
- DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db
- REDIS_HOST=redis
networks:
- dokploy-network
# volumes:
# - /var/run/docker.sock:/var/run/docker.sock:ro
redis:
image: bluewaveuptime/uptime_redis:latest
restart: always
ports:
- 6379
volumes:
- ../files/redis/data:/data
networks:
- dokploy-network
mongodb:
image: bluewaveuptime/uptime_database_mongo:latest
restart: always
volumes:
- ../files/mongo/data:/data/db
command: ["mongod", "--quiet"]
ports:
- 27017
networks:
- dokploy-network

View File

@@ -0,0 +1,25 @@
import {
type DomainSchema,
type Schema,
type Template,
generateRandomDomain,
} from "../utils";
export function generate(schema: Schema): Template {
const mainDomain = generateRandomDomain(schema);
const envs = [`DOMAIN=${mainDomain}`];
const domains: DomainSchema[] = [
{
host: mainDomain,
port: 80,
serviceName: "client",
},
];
return {
domains,
envs,
};
}

View File

@@ -11,7 +11,7 @@ export function generate(schema: Schema): Template {
const hulySecret = generateBase64(64);
const domains: DomainSchema[] = [
{
host: generateRandomDomain(schema),
host: mainDomain,
port: 80,
serviceName: "nginx",
},

View File

@@ -1180,5 +1180,20 @@ export const templates: TemplateData[] = [
},
tags: ["self-hosted", "project-management", "management"],
load: () => import("./glpi/index").then((m) => m.generate),
},
{
id: "checkmate",
name: "Checkmate",
version: "2.0.1",
description:
"Checkmate is an open-source, self-hosted tool designed to track and monitor server hardware, uptime, response times, and incidents in real-time with beautiful visualizations.",
logo: "checkmate.png",
links: {
github: "https://github.com/bluewave-labs/checkmate",
website: "https://bluewavelabs.ca",
docs: "https://bluewavelabs.gitbook.io/checkmate",
},
tags: ["self-hosted", "monitoring", "uptime"],
load: () => import("./checkmate/index").then((m) => m.generate),
},
];

View File

@@ -360,7 +360,7 @@ const installUtilities = () => `
;;
ubuntu | debian | raspbian)
DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null
DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git jq openssl >/dev/null
DEBIAN_FRONTEND=noninteractive apt-get install -y unzip curl wget git jq openssl >/dev/null
;;
centos | fedora | rhel | ol | rocky | almalinux | amzn)
if [ "$OS_TYPE" = "amzn" ]; then

View File

@@ -35,7 +35,6 @@ export async function checkGPUStatus(serverId?: string): Promise<GPUInfo> {
...cudaInfo,
};
} catch (error) {
console.error("Error in checkGPUStatus:", error);
return {
driverInstalled: false,
driverVersion: undefined,
@@ -317,7 +316,6 @@ const setupLocalServer = async (daemonConfig: any) => {
try {
await execAsync(setupCommands);
} catch (error) {
console.error("Setup failed:", error);
throw new Error(
"Failed to configure GPU support. Please ensure you have sudo privileges and try again.",
);
@@ -344,11 +342,10 @@ const verifySetup = async (nodeId: string, serverId?: string) => {
"cat /etc/nvidia-container-runtime/config.toml",
].join(" && ");
const { stdout: diagnostics } = serverId
? await execAsyncRemote(serverId, diagnosticCommands)
: await execAsync(diagnosticCommands);
await (serverId
? execAsyncRemote(serverId, diagnosticCommands)
: execAsync(diagnosticCommands));
console.error("Diagnostic Information:", diagnostics);
throw new Error("GPU support not detected in swarm after setup");
}