From 976d1f312f17e1aef4f8e7920e0d099d45a18a28 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Fri, 17 May 2024 02:56:50 -0600 Subject: [PATCH] feat: add table to show nodes and add dropdown to add manager & workers --- .../cluster/nodes/manager/add-manager.tsx | 49 +++++++ .../settings/cluster/nodes/show-node-data.tsx | 43 ++++++ .../settings/cluster/nodes/show-nodes.tsx | 131 ++++++++++++++++++ .../cluster/nodes/workers/add-worker.tsx | 47 +++++++ .../cluster/nodes/workers/delete-worker.tsx | 62 +++++++++ .../settings/cluster/worker/add-worker.tsx | 80 ----------- .../settings/cluster/worker/show-workers.tsx | 62 --------- pages/dashboard/settings/cluster.tsx | 5 +- server/api/routers/cluster.ts | 37 ++++- server/api/services/cluster.ts | 41 ++++++ 10 files changed, 409 insertions(+), 148 deletions(-) create mode 100644 components/dashboard/settings/cluster/nodes/manager/add-manager.tsx create mode 100644 components/dashboard/settings/cluster/nodes/show-node-data.tsx create mode 100644 components/dashboard/settings/cluster/nodes/show-nodes.tsx create mode 100644 components/dashboard/settings/cluster/nodes/workers/add-worker.tsx create mode 100644 components/dashboard/settings/cluster/nodes/workers/delete-worker.tsx delete mode 100644 components/dashboard/settings/cluster/worker/add-worker.tsx delete mode 100644 components/dashboard/settings/cluster/worker/show-workers.tsx create mode 100644 server/api/services/cluster.ts diff --git a/components/dashboard/settings/cluster/nodes/manager/add-manager.tsx b/components/dashboard/settings/cluster/nodes/manager/add-manager.tsx new file mode 100644 index 00000000..8c6b1db8 --- /dev/null +++ b/components/dashboard/settings/cluster/nodes/manager/add-manager.tsx @@ -0,0 +1,49 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { api } from "@/utils/api"; +import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; +export const AddManager = () => { + const { data } = api.cluster.addManager.useQuery(); + + return ( + <> + + + e.preventDefault()} + > + Add Manager + + + + + Add a new manager + Add a new manager + +
+ 1. Go to your new server and run the following command + + curl https://get.docker.com | sh -s -- --version 24.0 + +
+ +
+ + 2. Run the following command to add the node(server) to your + cluster + + {data} +
+
+
+ + ); +}; diff --git a/components/dashboard/settings/cluster/nodes/show-node-data.tsx b/components/dashboard/settings/cluster/nodes/show-node-data.tsx new file mode 100644 index 00000000..c597b948 --- /dev/null +++ b/components/dashboard/settings/cluster/nodes/show-node-data.tsx @@ -0,0 +1,43 @@ +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; + +interface Props { + data: unknown; +} + +export const ShowNodeData = ({ data }: Props) => { + return ( + + + e.preventDefault()} + > + View Config + + + + + Node Config + + See in detail the metadata of this node + + +
+ +
+							{JSON.stringify(data, null, 2)}
+						
+
+
+
+
+ ); +}; diff --git a/components/dashboard/settings/cluster/nodes/show-nodes.tsx b/components/dashboard/settings/cluster/nodes/show-nodes.tsx new file mode 100644 index 00000000..47cbee4c --- /dev/null +++ b/components/dashboard/settings/cluster/nodes/show-nodes.tsx @@ -0,0 +1,131 @@ +import React from "react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { api } from "@/utils/api"; +import { AddWorker } from "./workers/add-worker"; +import { DateTooltip } from "@/components/shared/date-tooltip"; +import { Badge } from "@/components/ui/badge"; +import { DeleteWorker } from "./workers/delete-worker"; +import { + Table, + TableBody, + TableCaption, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { MoreHorizontal, PlusIcon } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { ShowNodeData } from "./show-node-data"; +import { AddManager } from "./manager/add-manager"; + +export const ShowNodes = () => { + const { data, isLoading } = api.cluster.getNodes.useQuery(); + return ( + + +
+ Cluster + Add nodes to your cluster +
+
+ + + + + + Actions + + + + +
+
+ +
+ {isLoading &&
Loading...
} + + A list of your managers / workers. + + + Hostname + Status + Role + Availability + Engine Version + Created + + Actions + + + + {data?.map((node) => { + const isManager = node.Spec.Role === "manager"; + return ( + + + {node.Description.Hostname} + + + {node.Status.State} + + + + {node?.Spec?.Role} + + + + {node.Spec.Availability} + + + + {node?.Description.Engine.EngineVersion} + + + + + Created{" "} + + + + + + + + + Actions + + {!node?.ManagerStatus?.Leader && ( + + )} + + + + + ); + })} + +
+
+
+
+ ); +}; diff --git a/components/dashboard/settings/cluster/nodes/workers/add-worker.tsx b/components/dashboard/settings/cluster/nodes/workers/add-worker.tsx new file mode 100644 index 00000000..f0556158 --- /dev/null +++ b/components/dashboard/settings/cluster/nodes/workers/add-worker.tsx @@ -0,0 +1,47 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; +import { api } from "@/utils/api"; + +export const AddWorker = () => { + const { data } = api.cluster.addWorker.useQuery(); + + return ( + + + e.preventDefault()} + > + Add Worker + + + + + Add a new worker + Add a new worker + +
+ 1. Go to your new server and run the following command + + curl https://get.docker.com | sh -s -- --version 24.0 + +
+ +
+ + 2. Run the following command to add the node(server) to your cluster + + {data} +
+
+
+ ); +}; diff --git a/components/dashboard/settings/cluster/nodes/workers/delete-worker.tsx b/components/dashboard/settings/cluster/nodes/workers/delete-worker.tsx new file mode 100644 index 00000000..2d3810ca --- /dev/null +++ b/components/dashboard/settings/cluster/nodes/workers/delete-worker.tsx @@ -0,0 +1,62 @@ +import React from "react"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; +import { Button } from "@/components/ui/button"; +import { api } from "@/utils/api"; +import { TrashIcon } from "lucide-react"; +import { toast } from "sonner"; +import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; + +interface Props { + nodeId: string; +} +export const DeleteWorker = ({ nodeId }: Props) => { + const { mutateAsync, isLoading } = api.cluster.removeWorker.useMutation(); + const utils = api.useUtils(); + return ( + + + e.preventDefault()}> + Delete + + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete the + worker. + + + + Cancel + { + await mutateAsync({ + nodeId, + }) + .then(async () => { + utils.cluster.getNodes.invalidate(); + toast.success("Worker deleted succesfully"); + }) + .catch(() => { + toast.error("Error to delete the worker"); + }); + }} + > + Confirm + + + + + ); +}; diff --git a/components/dashboard/settings/cluster/worker/add-worker.tsx b/components/dashboard/settings/cluster/worker/add-worker.tsx deleted file mode 100644 index 5a7bccae..00000000 --- a/components/dashboard/settings/cluster/worker/add-worker.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { AlertTriangle, PlusIcon } from "lucide-react"; -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; - -const AddWorkerSchema = z.object({ - name: z.string().min(1, { - message: "Name is required", - }), - description: z.string().optional(), -}); - -type AddWorker = z.infer; - -export const AddWorker = () => { - const utils = api.useUtils(); - - const { data, isLoading } = api.cluster.addWorker.useQuery(); - - return ( - - - - - - - Add a new worker - Add a new worker - - {/* {isError && ( -
- - - {error?.message} - -
- )} */} -
- 1. Go to your new server and run the following command - - curl https://get.docker.com | sh -s -- --version 24.0 - -
- -
- - 2. Run the following command to add the node(server) to your cluster - - {data} -
-
-
- ); -}; diff --git a/components/dashboard/settings/cluster/worker/show-workers.tsx b/components/dashboard/settings/cluster/worker/show-workers.tsx deleted file mode 100644 index 470ab86d..00000000 --- a/components/dashboard/settings/cluster/worker/show-workers.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React from "react"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; - -import { api } from "@/utils/api"; -import { AddWorker } from "./add-worker"; -import { DateTooltip } from "@/components/shared/date-tooltip"; - -export const ShowCluster = () => { - const { data, isLoading } = api.cluster.getWorkers.useQuery(); - // console.log(data) - return ( - - -
- Cluster - Add nodes to your cluster -
- -
- -
- {isLoading &&
Loading...
} - {data?.map((worker, index) => ( -
- - {worker.Description.Hostname} - - - {worker.Status.State} - - - {worker.Spec.Availability} - - - {worker?.ManagerStatus?.Reachability || "-"} - - - {worker?.Spec?.Role} - - - - {worker?.Description.Engine.EngineVersion} - - - {/* Created */} - -
- ))} -
-
-
- ); -}; diff --git a/pages/dashboard/settings/cluster.tsx b/pages/dashboard/settings/cluster.tsx index d433e902..969e97aa 100644 --- a/pages/dashboard/settings/cluster.tsx +++ b/pages/dashboard/settings/cluster.tsx @@ -1,6 +1,5 @@ -import { ShowCertificates } from "@/components/dashboard/settings/certificates/show-certificates"; import { ShowRegistry } from "@/components/dashboard/settings/cluster/registry/show-registry"; -import { ShowCluster } from "@/components/dashboard/settings/cluster/worker/show-workers"; +import { ShowNodes } from "@/components/dashboard/settings/cluster/nodes/show-nodes"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { SettingsLayout } from "@/components/layouts/settings-layout"; import { validateRequest } from "@/server/auth/auth"; @@ -11,7 +10,7 @@ const Page = () => { return (
- +
); }; diff --git a/server/api/routers/cluster.ts b/server/api/routers/cluster.ts index 9a6ff6e7..2e1d9826 100644 --- a/server/api/routers/cluster.ts +++ b/server/api/routers/cluster.ts @@ -1,17 +1,48 @@ import { docker } from "@/server/constants"; import { createTRPCRouter, protectedProcedure } from "../trpc"; import { getPublicIpWithFallback } from "@/server/wss/terminal"; +import type { DockerNode } from "../services/cluster"; +import { z } from "zod"; +import { TRPCError } from "@trpc/server"; +import { execAsync } from "@/server/utils/process/execAsync"; export const clusterRouter = createTRPCRouter({ - getWorkers: protectedProcedure.query(async () => { - const workers = await docker.listNodes(); - // console.log(workers); + getNodes: protectedProcedure.query(async () => { + const workers: DockerNode[] = await docker.listNodes(); + return workers; }), + removeWorker: protectedProcedure + .input( + z.object({ + nodeId: z.string(), + }), + ) + .mutation(async ({ input }) => { + try { + await execAsync( + `docker node update --availability drain ${input.nodeId}`, + ); + await execAsync(`docker node rm ${input.nodeId} --force`); + return true; + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Error to remove the node", + cause: error, + }); + } + }), addWorker: protectedProcedure.query(async ({ input }) => { const result = await docker.swarmInspect(); return `docker swarm join --token ${ result.JoinTokens.Worker } ${await getPublicIpWithFallback()}:2377`; }), + addManager: protectedProcedure.query(async ({ input }) => { + const result = await docker.swarmInspect(); + return `docker swarm join --token ${ + result.JoinTokens.Manager + } ${await getPublicIpWithFallback()}:2377`; + }), }); diff --git a/server/api/services/cluster.ts b/server/api/services/cluster.ts new file mode 100644 index 00000000..ea71d1ae --- /dev/null +++ b/server/api/services/cluster.ts @@ -0,0 +1,41 @@ +export interface DockerNode { + ID: string; + Version: { + Index: number; + }; + CreatedAt: string; + UpdatedAt: string; + Spec: { + Name: string; + Labels: Record; + Role: "worker" | "manager"; + Availability: "active" | "pause" | "drain"; + }; + Description: { + Hostname: string; + Platform: { + Architecture: string; + OS: string; + }; + Resources: { + NanoCPUs: number; + MemoryBytes: number; + }; + Engine: { + EngineVersion: string; + Plugins: Array<{ + Type: string; + Name: string; + }>; + }; + }; + Status: { + State: "unknown" | "down" | "ready" | "disconnected"; + Message: string; + Addr: string; + }; + ManagerStatus?: { + Leader: boolean; + Addr: string; + }; +}