Merge pull request #1506 from Dokploy/feat/add-swarm-to-remote-servers

feat(cluster-nodes): enhance node management by adding serverId prop …
This commit is contained in:
Mauricio Siu 2025-03-16 00:43:35 -06:00 committed by GitHub
commit 9ac68985e0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 141 additions and 52 deletions

View File

@ -13,7 +13,11 @@ import Link from "next/link";
import { AddManager } from "./manager/add-manager"; import { AddManager } from "./manager/add-manager";
import { AddWorker } from "./workers/add-worker"; import { AddWorker } from "./workers/add-worker";
export const AddNode = () => { interface Props {
serverId?: string;
}
export const AddNode = ({ serverId }: Props) => {
return ( return (
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
@ -53,10 +57,10 @@ export const AddNode = () => {
<TabsTrigger value="manager">Manager</TabsTrigger> <TabsTrigger value="manager">Manager</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="worker" className="pt-4"> <TabsContent value="worker" className="pt-4">
<AddWorker /> <AddWorker serverId={serverId} />
</TabsContent> </TabsContent>
<TabsContent value="manager" className="pt-4"> <TabsContent value="manager" className="pt-4">
<AddManager /> <AddManager serverId={serverId} />
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</div> </div>

View File

@ -9,8 +9,12 @@ import copy from "copy-to-clipboard";
import { CopyIcon } from "lucide-react"; import { CopyIcon } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
export const AddManager = () => { interface Props {
const { data } = api.cluster.addManager.useQuery(); serverId?: string;
}
export const AddManager = ({ serverId }: Props) => {
const { data } = api.cluster.addManager.useQuery({ serverId });
return ( return (
<> <>

View File

@ -0,0 +1,30 @@
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { useState } from "react";
import { ShowNodes } from "./show-nodes";
interface Props {
serverId: string;
}
export const ShowNodesModal = ({ serverId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer "
onSelect={(e) => e.preventDefault()}
>
Show Nodes
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="sm:max-w-5xl overflow-y-auto max-h-screen ">
<div className="grid w-full gap-1">
<ShowNodes serverId={serverId} />
</div>
</DialogContent>
</Dialog>
);
};

View File

@ -32,13 +32,25 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Boxes, HelpCircle, LockIcon, MoreHorizontal } from "lucide-react"; import {
Boxes,
HelpCircle,
LockIcon,
MoreHorizontal,
Loader2,
} from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { AddNode } from "./add-node"; import { AddNode } from "./add-node";
import { ShowNodeData } from "./show-node-data"; import { ShowNodeData } from "./show-node-data";
export const ShowNodes = () => { interface Props {
const { data, isLoading, refetch } = api.cluster.getNodes.useQuery(); serverId?: string;
}
export const ShowNodes = ({ serverId }: Props) => {
const { data, isLoading, refetch } = api.cluster.getNodes.useQuery({
serverId,
});
const { data: registry } = api.registry.all.useQuery(); const { data: registry } = api.registry.all.useQuery();
const { mutateAsync: deleteNode } = api.cluster.removeWorker.useMutation(); const { mutateAsync: deleteNode } = api.cluster.removeWorker.useMutation();
@ -58,14 +70,17 @@ export const ShowNodes = () => {
</div> </div>
{haveAtLeastOneRegistry && ( {haveAtLeastOneRegistry && (
<div className="flex flex-row gap-2"> <div className="flex flex-row gap-2">
<AddNode /> <AddNode serverId={serverId} />
</div> </div>
)} )}
</CardHeader> </CardHeader>
<CardContent className="space-y-2 py-8 border-t min-h-[35vh]"> <CardContent className="space-y-2 py-8 border-t min-h-[35vh]">
{haveAtLeastOneRegistry ? ( {isLoading ? (
<div className="flex items-center justify-center w-full h-[40vh]">
<Loader2 className="size-8 animate-spin text-muted-foreground" />
</div>
) : haveAtLeastOneRegistry ? (
<div className="grid md:grid-cols-1 gap-4"> <div className="grid md:grid-cols-1 gap-4">
{isLoading && <div>Loading...</div>}
<Table> <Table>
<TableCaption> <TableCaption>
A list of your managers / workers. A list of your managers / workers.
@ -137,6 +152,7 @@ export const ShowNodes = () => {
onClick={async () => { onClick={async () => {
await deleteNode({ await deleteNode({
nodeId: node.ID, nodeId: node.ID,
serverId,
}) })
.then(() => { .then(() => {
refetch(); refetch();

View File

@ -9,8 +9,12 @@ import copy from "copy-to-clipboard";
import { CopyIcon } from "lucide-react"; import { CopyIcon } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
export const AddWorker = () => { interface Props {
const { data } = api.cluster.addWorker.useQuery(); serverId?: string;
}
export const AddWorker = ({ serverId }: Props) => {
const { data } = api.cluster.addWorker.useQuery({ serverId });
return ( return (
<div> <div>

View File

@ -42,6 +42,7 @@ import { ShowMonitoringModal } from "./show-monitoring-modal";
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal"; import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal"; import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription"; import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
import { ShowNodesModal } from "../cluster/nodes/show-nodes-modal";
export const ShowServers = () => { export const ShowServers = () => {
const { t } = useTranslation("settings"); const { t } = useTranslation("settings");
@ -328,6 +329,9 @@ export const ShowServers = () => {
<ShowSwarmOverviewModal <ShowSwarmOverviewModal
serverId={server.serverId} serverId={server.serverId}
/> />
<ShowNodesModal
serverId={server.serverId}
/>
</> </>
)} )}
</DropdownMenuContent> </DropdownMenuContent>

View File

@ -1,22 +1,35 @@
import { getPublicIpWithFallback } from "@/server/wss/terminal"; import { getPublicIpWithFallback } from "@/server/wss/terminal";
import { type DockerNode, IS_CLOUD, docker, execAsync } from "@dokploy/server"; import {
type DockerNode,
IS_CLOUD,
execAsync,
getRemoteDocker,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";
export const clusterRouter = createTRPCRouter({ export const clusterRouter = createTRPCRouter({
getNodes: protectedProcedure.query(async () => { getNodes: protectedProcedure
if (IS_CLOUD) { .input(
return []; z.object({
} serverId: z.string().optional(),
const workers: DockerNode[] = await docker.listNodes(); }),
)
.query(async ({ input }) => {
if (IS_CLOUD) {
return [];
}
return workers; const docker = await getRemoteDocker(input.serverId);
}), const workers: DockerNode[] = await docker.listNodes();
return workers;
}),
removeWorker: protectedProcedure removeWorker: protectedProcedure
.input( .input(
z.object({ z.object({
nodeId: z.string(), nodeId: z.string(),
serverId: z.string().optional(),
}), }),
) )
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
@ -40,37 +53,51 @@ export const clusterRouter = createTRPCRouter({
}); });
} }
}), }),
addWorker: protectedProcedure.query(async () => { addWorker: protectedProcedure
if (IS_CLOUD) { .input(
return { z.object({
command: "", serverId: z.string().optional(),
version: "", }),
}; )
} .query(async ({ input }) => {
const result = await docker.swarmInspect(); if (IS_CLOUD) {
const docker_version = await docker.version(); return {
command: "",
version: "",
};
}
const docker = await getRemoteDocker(input.serverId);
const result = await docker.swarmInspect();
const docker_version = await docker.version();
return {
command: `docker swarm join --token ${
result.JoinTokens.Worker
} ${await getPublicIpWithFallback()}:2377`,
version: docker_version.Version,
};
}),
addManager: protectedProcedure.query(async () => {
if (IS_CLOUD) {
return { return {
command: "", command: `docker swarm join --token ${
version: "", result.JoinTokens.Worker
} ${await getPublicIpWithFallback()}:2377`,
version: docker_version.Version,
}; };
} }),
const result = await docker.swarmInspect(); addManager: protectedProcedure
const docker_version = await docker.version(); .input(
return { z.object({
command: `docker swarm join --token ${ serverId: z.string().optional(),
result.JoinTokens.Manager }),
} ${await getPublicIpWithFallback()}:2377`, )
version: docker_version.Version, .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();
return {
command: `docker swarm join --token ${
result.JoinTokens.Manager
} ${await getPublicIpWithFallback()}:2377`,
version: docker_version.Version,
};
}),
}); });