mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge branch 'canary' into canary
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
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 });
|
||||
}
|
||||
@@ -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 |
@@ -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",
|
||||
|
||||
BIN
apps/dokploy/public/templates/checkmate.png
Normal file
BIN
apps/dokploy/public/templates/checkmate.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
@@ -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",
|
||||
|
||||
47
apps/dokploy/templates/checkmate/docker-compose.yml
Normal file
47
apps/dokploy/templates/checkmate/docker-compose.yml
Normal 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
|
||||
25
apps/dokploy/templates/checkmate/index.ts
Normal file
25
apps/dokploy/templates/checkmate/index.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user