feat(cluster-nodes): enhance node management by adding serverId prop to components and implementing ShowNodesModal

This commit is contained in:
Mauricio Siu 2025-03-16 00:42:19 -06:00
parent 60c03e1ca7
commit 35ff8dcfe6
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 { AddWorker } from "./workers/add-worker";
export const AddNode = () => {
interface Props {
serverId?: string;
}
export const AddNode = ({ serverId }: Props) => {
return (
<Dialog>
<DialogTrigger asChild>
@ -53,10 +57,10 @@ export const AddNode = () => {
<TabsTrigger value="manager">Manager</TabsTrigger>
</TabsList>
<TabsContent value="worker" className="pt-4">
<AddWorker />
<AddWorker serverId={serverId} />
</TabsContent>
<TabsContent value="manager" className="pt-4">
<AddManager />
<AddManager serverId={serverId} />
</TabsContent>
</Tabs>
</div>

View File

@ -9,8 +9,12 @@ import copy from "copy-to-clipboard";
import { CopyIcon } from "lucide-react";
import { toast } from "sonner";
export const AddManager = () => {
const { data } = api.cluster.addManager.useQuery();
interface Props {
serverId?: string;
}
export const AddManager = ({ serverId }: Props) => {
const { data } = api.cluster.addManager.useQuery({ serverId });
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,
} from "@/components/ui/tooltip";
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 { AddNode } from "./add-node";
import { ShowNodeData } from "./show-node-data";
export const ShowNodes = () => {
const { data, isLoading, refetch } = api.cluster.getNodes.useQuery();
interface Props {
serverId?: string;
}
export const ShowNodes = ({ serverId }: Props) => {
const { data, isLoading, refetch } = api.cluster.getNodes.useQuery({
serverId,
});
const { data: registry } = api.registry.all.useQuery();
const { mutateAsync: deleteNode } = api.cluster.removeWorker.useMutation();
@ -58,14 +70,17 @@ export const ShowNodes = () => {
</div>
{haveAtLeastOneRegistry && (
<div className="flex flex-row gap-2">
<AddNode />
<AddNode serverId={serverId} />
</div>
)}
</CardHeader>
<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">
{isLoading && <div>Loading...</div>}
<Table>
<TableCaption>
A list of your managers / workers.
@ -137,6 +152,7 @@ export const ShowNodes = () => {
onClick={async () => {
await deleteNode({
nodeId: node.ID,
serverId,
})
.then(() => {
refetch();

View File

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

View File

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

View File

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