mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat: add table to show nodes and add dropdown to add manager & workers
This commit is contained in:
@@ -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 (
|
||||||
|
<>
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="cursor-pointer flex flex-row gap-2 items-center"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Add Manager
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-4xl max-h-screen overflow-y-auto ">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Add a new manager</DialogTitle>
|
||||||
|
<DialogDescription>Add a new manager</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex flex-col gap-4 text-sm">
|
||||||
|
<span>1. Go to your new server and run the following command</span>
|
||||||
|
<span className="bg-muted rounded-lg p-2">
|
||||||
|
curl https://get.docker.com | sh -s -- --version 24.0
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-4 text-sm">
|
||||||
|
<span>
|
||||||
|
2. Run the following command to add the node(server) to your
|
||||||
|
cluster
|
||||||
|
</span>
|
||||||
|
<span className="bg-muted rounded-lg p-2 ">{data}</span>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
View Config
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Node Config</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
See in detail the metadata of this node
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="text-wrap rounded-lg border p-4 text-sm sm:max-w-[59rem] bg-card">
|
||||||
|
<code>
|
||||||
|
<pre className="whitespace-pre-wrap break-words">
|
||||||
|
{JSON.stringify(data, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
131
components/dashboard/settings/cluster/nodes/show-nodes.tsx
Normal file
131
components/dashboard/settings/cluster/nodes/show-nodes.tsx
Normal file
@@ -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 (
|
||||||
|
<Card className="bg-transparent h-full">
|
||||||
|
<CardHeader className="flex flex-row gap-2 justify-between w-full items-center flex-wrap">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<CardTitle className="text-xl">Cluster</CardTitle>
|
||||||
|
<CardDescription>Add nodes to your cluster</CardDescription>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost">
|
||||||
|
<PlusIcon className="h-4 w-4" />
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
|
<AddWorker />
|
||||||
|
<AddManager />
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
<div className="grid md:grid-cols-1 gap-4">
|
||||||
|
{isLoading && <div>Loading...</div>}
|
||||||
|
<Table>
|
||||||
|
<TableCaption>A list of your managers / workers.</TableCaption>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[100px]">Hostname</TableHead>
|
||||||
|
<TableHead className="text-right">Status</TableHead>
|
||||||
|
<TableHead className="text-right">Role</TableHead>
|
||||||
|
<TableHead className="text-right">Availability</TableHead>
|
||||||
|
<TableHead className="text-right">Engine Version</TableHead>
|
||||||
|
<TableHead className="text-right">Created</TableHead>
|
||||||
|
|
||||||
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{data?.map((node) => {
|
||||||
|
const isManager = node.Spec.Role === "manager";
|
||||||
|
return (
|
||||||
|
<TableRow key={node.ID}>
|
||||||
|
<TableCell className="w-[100px]">
|
||||||
|
{node.Description.Hostname}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
{node.Status.State}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<Badge variant={isManager ? "default" : "secondary"}>
|
||||||
|
{node?.Spec?.Role}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
{node.Spec.Availability}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell className="text-right">
|
||||||
|
{node?.Description.Engine.EngineVersion}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<DateTooltip date={node.CreatedAt} className="text-sm">
|
||||||
|
Created{" "}
|
||||||
|
</DateTooltip>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right flex justify-end">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
|
<span className="sr-only">Open menu</span>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
|
<ShowNodeData data={node} />
|
||||||
|
{!node?.ManagerStatus?.Leader && (
|
||||||
|
<DeleteWorker nodeId={node.ID} />
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="cursor-pointer flex flex-row gap-2 items-center"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Add Worker
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-4xl max-h-screen overflow-y-auto ">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Add a new worker</DialogTitle>
|
||||||
|
<DialogDescription>Add a new worker</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex flex-col gap-4 text-sm">
|
||||||
|
<span>1. Go to your new server and run the following command</span>
|
||||||
|
<span className="bg-muted rounded-lg p-2">
|
||||||
|
curl https://get.docker.com | sh -s -- --version 24.0
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-4 text-sm">
|
||||||
|
<span>
|
||||||
|
2. Run the following command to add the node(server) to your cluster
|
||||||
|
</span>
|
||||||
|
<span className="bg-muted rounded-lg p-2 ">{data}</span>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This action cannot be undone. This will permanently delete the
|
||||||
|
worker.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={async () => {
|
||||||
|
await mutateAsync({
|
||||||
|
nodeId,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
utils.cluster.getNodes.invalidate();
|
||||||
|
toast.success("Worker deleted succesfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to delete the worker");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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<typeof AddWorkerSchema>;
|
|
||||||
|
|
||||||
export const AddWorker = () => {
|
|
||||||
const utils = api.useUtils();
|
|
||||||
|
|
||||||
const { data, isLoading } = api.cluster.addWorker.useQuery();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button>
|
|
||||||
<PlusIcon className="h-4 w-4" />
|
|
||||||
Add Worker
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="sm:max-w-4xl max-h-screen overflow-y-auto ">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Add a new worker</DialogTitle>
|
|
||||||
<DialogDescription>Add a new worker</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
{/* {isError && (
|
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)} */}
|
|
||||||
<div className="flex flex-col gap-4 text-sm">
|
|
||||||
<span>1. Go to your new server and run the following command</span>
|
|
||||||
<span className="bg-muted rounded-lg p-2">
|
|
||||||
curl https://get.docker.com | sh -s -- --version 24.0
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 text-sm">
|
|
||||||
<span>
|
|
||||||
2. Run the following command to add the node(server) to your cluster
|
|
||||||
</span>
|
|
||||||
<span className="bg-muted rounded-lg p-2 ">{data}</span>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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 (
|
|
||||||
<Card className="bg-transparent h-full">
|
|
||||||
<CardHeader className="flex flex-row gap-2 justify-between w-full items-center flex-wrap">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<CardTitle className="text-xl">Cluster</CardTitle>
|
|
||||||
<CardDescription>Add nodes to your cluster</CardDescription>
|
|
||||||
</div>
|
|
||||||
<AddWorker />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4">
|
|
||||||
<div className="grid md:grid-cols-1 gap-4">
|
|
||||||
{isLoading && <div>Loading...</div>}
|
|
||||||
{data?.map((worker, index) => (
|
|
||||||
<div
|
|
||||||
key={`key-${index}`}
|
|
||||||
className="flex flex-row gap-4 w-full flex-wrap"
|
|
||||||
>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{worker.Description.Hostname}
|
|
||||||
</span>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{worker.Status.State}
|
|
||||||
</span>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{worker.Spec.Availability}
|
|
||||||
</span>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{worker?.ManagerStatus?.Reachability || "-"}
|
|
||||||
</span>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{worker?.Spec?.Role}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{worker?.Description.Engine.EngineVersion}
|
|
||||||
</span>
|
|
||||||
<DateTooltip date={worker.CreatedAt} className="text-sm">
|
|
||||||
{/* <span className="text-sm text-muted-foreground">Created</span> */}
|
|
||||||
</DateTooltip>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { ShowCertificates } from "@/components/dashboard/settings/certificates/show-certificates";
|
|
||||||
import { ShowRegistry } from "@/components/dashboard/settings/cluster/registry/show-registry";
|
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 { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
import { validateRequest } from "@/server/auth/auth";
|
import { validateRequest } from "@/server/auth/auth";
|
||||||
@@ -11,7 +10,7 @@ const Page = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<div className="flex flex-col gap-4 w-full">
|
||||||
<ShowRegistry />
|
<ShowRegistry />
|
||||||
<ShowCluster />
|
<ShowNodes />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,17 +1,48 @@
|
|||||||
import { docker } from "@/server/constants";
|
import { docker } from "@/server/constants";
|
||||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
import { getPublicIpWithFallback } from "@/server/wss/terminal";
|
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({
|
export const clusterRouter = createTRPCRouter({
|
||||||
getWorkers: protectedProcedure.query(async () => {
|
getNodes: protectedProcedure.query(async () => {
|
||||||
const workers = await docker.listNodes();
|
const workers: DockerNode[] = await docker.listNodes();
|
||||||
// console.log(workers);
|
|
||||||
return workers;
|
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 }) => {
|
addWorker: protectedProcedure.query(async ({ input }) => {
|
||||||
const result = await docker.swarmInspect();
|
const result = await docker.swarmInspect();
|
||||||
return `docker swarm join --token ${
|
return `docker swarm join --token ${
|
||||||
result.JoinTokens.Worker
|
result.JoinTokens.Worker
|
||||||
} ${await getPublicIpWithFallback()}:2377`;
|
} ${await getPublicIpWithFallback()}:2377`;
|
||||||
}),
|
}),
|
||||||
|
addManager: protectedProcedure.query(async ({ input }) => {
|
||||||
|
const result = await docker.swarmInspect();
|
||||||
|
return `docker swarm join --token ${
|
||||||
|
result.JoinTokens.Manager
|
||||||
|
} ${await getPublicIpWithFallback()}:2377`;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
41
server/api/services/cluster.ts
Normal file
41
server/api/services/cluster.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
export interface DockerNode {
|
||||||
|
ID: string;
|
||||||
|
Version: {
|
||||||
|
Index: number;
|
||||||
|
};
|
||||||
|
CreatedAt: string;
|
||||||
|
UpdatedAt: string;
|
||||||
|
Spec: {
|
||||||
|
Name: string;
|
||||||
|
Labels: Record<string, string>;
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user