Compare commits

...

14 Commits

Author SHA1 Message Date
Mauricio Siu
91183056f0 Merge pull request #1534 from Dokploy/feat/enable-swarm-overview
Feat/enable swarm overview
2025-03-19 00:52:22 -06:00
Mauricio Siu
03bd4398d0 chore(package): bump version to v0.20.8 2025-03-19 00:51:49 -06:00
Mauricio Siu
8c260eff72 feat(cluster): enhance AddNode and ShowNodes components for better user guidance and functionality
- Added an AlertBlock in AddNode to inform users about architecture compatibility when adding nodes.
- Updated ShowNodes to correctly handle node deletion actions based on ManagerStatus.
- Refactored cluster API to remove cloud-specific checks and improve command execution for remote servers.
2025-03-19 00:51:27 -06:00
Mauricio Siu
6e28196b0e chore(package): bump version to v0.20.7 2025-03-18 21:36:39 -06:00
Mauricio Siu
18bacae175 Merge pull request #1507 from nb5p/fix-alpine-linux-compatibility
fix(server-setup): resolve Alpine Linux compatibility issues
2025-03-18 21:35:43 -06:00
Mauricio Siu
f2be5a378e Merge pull request #1522 from ensarkurrt/canary
fix(ui): Improve Numeric Input Handling in Swarm Cluster Settings, Traefik Port Mappings, and Email Notifications
2025-03-18 21:27:20 -06:00
Mauricio Siu
aef24296b9 Merge pull request #1531 from Dokploy/fix/loader-swarm
Fix/loader swarm
2025-03-18 21:18:17 -06:00
Mauricio Siu
7123b9b109 feat(cluster): add error handling in AddManager and AddWorker components
- Integrated error handling in AddManager and AddWorker components to display error messages using AlertBlock when data fetching fails.
- Updated API query hooks to include error and isError states for improved user feedback during data operations.
2025-03-18 21:17:11 -06:00
Mauricio Siu
891dc840f5 feat(cluster): enhance node management UI with loading indicators and improved tab content
- Added loading indicators in AddManager and AddWorker components to enhance user experience during data fetching.
- Updated AddNode component to include overflow handling for tab content.
- Renamed "Show Nodes" to "Show Swarm Nodes" in ShowNodesModal for clarity.
2025-03-18 21:11:50 -06:00
Ensar Kurt
3cdf4c426c revert commit from #1513 2025-03-18 00:05:59 +03:00
Ensar Kurt
7cb184dc97 email notification port, last digit staying error fix 2025-03-17 23:48:17 +03:00
Ensar Kurt
fe57333f84 manage port inputs, default zero fix 2025-03-17 23:47:54 +03:00
Ensar Kurt
04fd77c3a9 replicas input cannot be zero and empty 2025-03-17 23:42:09 +03:00
nb5p
2974a8183e fix(server-setup): resolve Alpine Linux compatibility issues with setup scripts
Resolves #1482
2025-03-16 15:37:28 +08:00
12 changed files with 169 additions and 153 deletions

View File

@@ -40,7 +40,7 @@ interface Props {
}
const AddRedirectchema = z.object({
replicas: z.number(),
replicas: z.number().min(1, "Replicas must be at least 1"),
registryId: z.string(),
});
@@ -130,9 +130,11 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
placeholder="1"
{...field}
onChange={(e) => {
field.onChange(Number(e.target.value));
const value = e.target.value;
field.onChange(value === "" ? 0 : Number(value));
}}
type="number"
value={field.value || ""}
/>
</FormControl>

View File

@@ -12,6 +12,7 @@ import { ExternalLink, PlusIcon } from "lucide-react";
import Link from "next/link";
import { AddManager } from "./manager/add-manager";
import { AddWorker } from "./workers/add-worker";
import { AlertBlock } from "@/components/shared/alert-block";
interface Props {
serverId?: string;
@@ -48,6 +49,10 @@ export const AddNode = ({ serverId }: Props) => {
Architecture
<ExternalLink className="h-4 w-4" />
</Link>
<AlertBlock type="warning">
Make sure you use the same architecture as the node you are
adding.
</AlertBlock>
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-2">
@@ -56,10 +61,10 @@ export const AddNode = ({ serverId }: Props) => {
<TabsTrigger value="worker">Worker</TabsTrigger>
<TabsTrigger value="manager">Manager</TabsTrigger>
</TabsList>
<TabsContent value="worker" className="pt-4">
<TabsContent value="worker" className="pt-4 overflow-hidden">
<AddWorker serverId={serverId} />
</TabsContent>
<TabsContent value="manager" className="pt-4">
<TabsContent value="manager" className="pt-4 overflow-hidden">
<AddManager serverId={serverId} />
</TabsContent>
</Tabs>

View File

@@ -1,3 +1,4 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { CardContent } from "@/components/ui/card";
import {
DialogDescription,
@@ -6,7 +7,7 @@ import {
} from "@/components/ui/dialog";
import { api } from "@/utils/api";
import copy from "copy-to-clipboard";
import { CopyIcon } from "lucide-react";
import { CopyIcon, Loader2 } from "lucide-react";
import { toast } from "sonner";
interface Props {
@@ -14,56 +15,66 @@ interface Props {
}
export const AddManager = ({ serverId }: Props) => {
const { data } = api.cluster.addManager.useQuery({ serverId });
const { data, isLoading, error, isError } = api.cluster.addManager.useQuery({
serverId,
});
return (
<>
<div>
<CardContent className="sm:max-w-4xl max-h-screen overflow-y-auto flex flex-col gap-4 px-0">
<DialogHeader>
<DialogTitle>Add a new manager</DialogTitle>
<DialogDescription>Add a new manager</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-2.5 text-sm">
<span>1. Go to your new server and run the following command</span>
<span className="bg-muted rounded-lg p-2 flex justify-between">
curl https://get.docker.com | sh -s -- --version {data?.version}
<button
type="button"
className="self-center"
onClick={() => {
copy(
`curl https://get.docker.com | sh -s -- --version ${data?.version}`,
);
toast.success("Copied to clipboard");
}}
>
<CopyIcon className="h-4 w-4 cursor-pointer" />
</button>
</span>
</div>
<CardContent className="sm:max-w-4xl flex flex-col gap-4 px-0">
<DialogHeader>
<DialogTitle>Add a new manager</DialogTitle>
<DialogDescription>Add a new manager</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
{isLoading ? (
<Loader2 className="w-full animate-spin text-muted-foreground" />
) : (
<>
<div className="flex flex-col gap-2.5 text-sm">
<span>
1. Go to your new server and run the following command
</span>
<span className="bg-muted rounded-lg p-2 flex justify-between">
curl https://get.docker.com | sh -s -- --version {data?.version}
<button
type="button"
className="self-center"
onClick={() => {
copy(
`curl https://get.docker.com | sh -s -- --version ${data?.version}`,
);
toast.success("Copied to clipboard");
}}
>
<CopyIcon className="h-4 w-4 cursor-pointer" />
</button>
</span>
</div>
<div className="flex flex-col gap-2.5 text-sm">
<span>
2. Run the following command to add the node(manager) to your
cluster
</span>
<span className="bg-muted rounded-lg p-2 flex">
{data?.command}
<button
type="button"
className="self-start"
onClick={() => {
copy(data?.command || "");
toast.success("Copied to clipboard");
}}
>
<CopyIcon className="h-4 w-4 cursor-pointer" />
</button>
</span>
</div>
</CardContent>
</div>
<div className="flex flex-col gap-2.5 text-sm">
<span>
2. Run the following command to add the node(manager) to your
cluster
</span>
<span className="bg-muted rounded-lg p-2 flex">
{data?.command}
<button
type="button"
className="self-start"
onClick={() => {
copy(data?.command || "");
toast.success("Copied to clipboard");
}}
>
<CopyIcon className="h-4 w-4 cursor-pointer" />
</button>
</span>
</div>
</>
)}
</CardContent>
</>
);
};

View File

@@ -17,7 +17,7 @@ export const ShowNodesModal = ({ serverId }: Props) => {
className="w-full cursor-pointer "
onSelect={(e) => e.preventDefault()}
>
Show Nodes
Show Swarm Nodes
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="sm:max-w-5xl overflow-y-auto max-h-screen ">

View File

@@ -144,7 +144,7 @@ export const ShowNodes = ({ serverId }: Props) => {
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<ShowNodeData data={node} />
{node?.ManagerStatus?.Leader && (
{!node?.ManagerStatus?.Leader && (
<DialogAction
title="Delete Node"
description="Are you sure you want to delete this node from the cluster?"

View File

@@ -1,3 +1,4 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { CardContent } from "@/components/ui/card";
import {
DialogDescription,
@@ -6,7 +7,7 @@ import {
} from "@/components/ui/dialog";
import { api } from "@/utils/api";
import copy from "copy-to-clipboard";
import { CopyIcon } from "lucide-react";
import { CopyIcon, Loader2 } from "lucide-react";
import { toast } from "sonner";
interface Props {
@@ -14,54 +15,62 @@ interface Props {
}
export const AddWorker = ({ serverId }: Props) => {
const { data } = api.cluster.addWorker.useQuery({ serverId });
const { data, isLoading, error, isError } = api.cluster.addWorker.useQuery({
serverId,
});
return (
<div>
<CardContent className="sm:max-w-4xl max-h-screen overflow-y-auto flex flex-col gap-4 px-0">
<DialogHeader>
<DialogTitle>Add a new worker</DialogTitle>
<DialogDescription>Add a new worker</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-2.5 text-sm">
<span>1. Go to your new server and run the following command</span>
<span className="bg-muted rounded-lg p-2 flex justify-between">
curl https://get.docker.com | sh -s -- --version {data?.version}
<button
type="button"
className="self-center"
onClick={() => {
copy(
`curl https://get.docker.com | sh -s -- --version ${data?.version}`,
);
toast.success("Copied to clipboard");
}}
>
<CopyIcon className="h-4 w-4 cursor-pointer" />
</button>
</span>
</div>
<CardContent className="sm:max-w-4xl flex flex-col gap-4 px-0">
<DialogHeader>
<DialogTitle>Add a new worker</DialogTitle>
<DialogDescription>Add a new worker</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
{isLoading ? (
<Loader2 className="w-full animate-spin text-muted-foreground" />
) : (
<>
<div className="flex flex-col gap-2.5 text-sm">
<span>1. Go to your new server and run the following command</span>
<span className="bg-muted rounded-lg p-2 flex justify-between">
curl https://get.docker.com | sh -s -- --version {data?.version}
<button
type="button"
className="self-center"
onClick={() => {
copy(
`curl https://get.docker.com | sh -s -- --version ${data?.version}`,
);
toast.success("Copied to clipboard");
}}
>
<CopyIcon className="h-4 w-4 cursor-pointer" />
</button>
</span>
</div>
<div className="flex flex-col gap-2.5 text-sm">
<span>
2. Run the following command to add the node(worker) to your cluster
</span>
<div className="flex flex-col gap-2.5 text-sm">
<span>
2. Run the following command to add the node(worker) to your
cluster
</span>
<span className="bg-muted rounded-lg p-2 flex">
{data?.command}
<button
type="button"
className="self-start"
onClick={() => {
copy(data?.command || "");
toast.success("Copied to clipboard");
}}
>
<CopyIcon className="h-4 w-4 cursor-pointer" />
</button>
</span>
</div>
</CardContent>
</div>
<span className="bg-muted rounded-lg p-2 flex">
{data?.command}
<button
type="button"
className="self-start"
onClick={() => {
copy(data?.command || "");
toast.success("Copied to clipboard");
}}
>
<CopyIcon className="h-4 w-4 cursor-pointer" />
</button>
</span>
</div>
</>
)}
</CardContent>
);
};

View File

@@ -663,13 +663,16 @@ export const HandleNotifications = ({ notificationId }: Props) => {
{...field}
onChange={(e) => {
const value = e.target.value;
if (value) {
if (value === "") {
field.onChange(undefined);
} else {
const port = Number.parseInt(value);
if (port > 0 && port < 65536) {
field.onChange(port);
}
}
}}
value={field.value || ""}
type="number"
/>
</FormControl>

View File

@@ -159,9 +159,11 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
<Input
type="number"
{...field}
onChange={(e) =>
field.onChange(Number(e.target.value))
}
onChange={(e) => {
const value = e.target.value;
field.onChange(value === "" ? undefined : Number(value));
}}
value={field.value || ""}
className="w-full dark:bg-black"
placeholder="e.g. 8080"
/>
@@ -185,9 +187,11 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
<Input
type="number"
{...field}
onChange={(e) =>
field.onChange(Number(e.target.value))
}
onChange={(e) => {
const value = e.target.value;
field.onChange(value === "" ? undefined : Number(value));
}}
value={field.value || ""}
className="w-full dark:bg-black"
placeholder="e.g. 80"
/>

View File

@@ -39,7 +39,7 @@ const NumberInput = React.forwardRef<HTMLInputElement, InputProps>(
className={cn("text-left", className)}
ref={ref}
{...props}
value={props.value === undefined || props.value === "" ? "" : String(props.value)}
value={props.value === undefined ? undefined : String(props.value)}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
@@ -60,21 +60,6 @@ const NumberInput = React.forwardRef<HTMLInputElement, InputProps>(
}
}
}}
onBlur={(e) => {
// If input is empty, make 0 when focus is lost
if (e.target.value === "") {
const syntheticEvent = {
...e,
target: {
...e.target,
value: "0",
},
};
props.onChange?.(
syntheticEvent as unknown as React.ChangeEvent<HTMLInputElement>,
);
}
}}
/>
);
},

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.20.6",
"version": "v0.20.8",
"private": true,
"license": "Apache-2.0",
"type": "module",

View File

@@ -1,8 +1,9 @@
import { getPublicIpWithFallback } from "@/server/wss/terminal";
import {
type DockerNode,
IS_CLOUD,
execAsync,
execAsyncRemote,
findServerById,
getRemoteDocker,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
@@ -16,10 +17,6 @@ export const clusterRouter = createTRPCRouter({
}),
)
.query(async ({ input }) => {
if (IS_CLOUD) {
return [];
}
const docker = await getRemoteDocker(input.serverId);
const workers: DockerNode[] = await docker.listNodes();
@@ -33,17 +30,17 @@ export const clusterRouter = createTRPCRouter({
}),
)
.mutation(async ({ input }) => {
if (IS_CLOUD) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Functionality not available in cloud version",
});
}
try {
await execAsync(
`docker node update --availability drain ${input.nodeId}`,
);
await execAsync(`docker node rm ${input.nodeId} --force`);
const drainCommand = `docker node update --availability drain ${input.nodeId}`;
const removeCommand = `docker node rm ${input.nodeId} --force`;
if (input.serverId) {
await execAsyncRemote(input.serverId, drainCommand);
await execAsyncRemote(input.serverId, removeCommand);
} else {
await execAsync(drainCommand);
await execAsync(removeCommand);
}
return true;
} catch (error) {
throw new TRPCError({
@@ -60,20 +57,20 @@ export const clusterRouter = createTRPCRouter({
}),
)
.query(async ({ input }) => {
if (IS_CLOUD) {
return {
command: "",
version: "",
};
}
const docker = await getRemoteDocker(input.serverId);
const result = await docker.swarmInspect();
const docker_version = await docker.version();
let ip = await getPublicIpWithFallback();
if (input.serverId) {
const server = await findServerById(input.serverId);
ip = server?.ipAddress;
}
return {
command: `docker swarm join --token ${
result.JoinTokens.Worker
} ${await getPublicIpWithFallback()}:2377`,
} ${ip}:2377`,
version: docker_version.Version,
};
}),
@@ -84,19 +81,19 @@ export const clusterRouter = createTRPCRouter({
}),
)
.query(async ({ input }) => {
if (IS_CLOUD) {
return {
command: "",
version: "",
};
}
const docker = await getRemoteDocker(input.serverId);
const result = await docker.swarmInspect();
const docker_version = await docker.version();
let ip = await getPublicIpWithFallback();
if (input.serverId) {
const server = await findServerById(input.serverId);
ip = server?.ipAddress;
}
return {
command: `docker swarm join --token ${
result.JoinTokens.Manager
} ${await getPublicIpWithFallback()}:2377`,
} ${ip}:2377`,
version: docker_version.Version,
};
}),

View File

@@ -361,7 +361,7 @@ const installUtilities = () => `
alpine)
sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
apk update >/dev/null
apk add curl wget git jq openssl >/dev/null
apk add curl wget git jq openssl sudo unzip tar >/dev/null
;;
ubuntu | debian | raspbian)
DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null