From 6afd443257d9d94a355b799d9319d51107e9111b Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Mon, 23 Dec 2024 00:03:30 -0600 Subject: [PATCH] feat: add swarm overview for servers --- .../settings/servers/show-servers.tsx | 4 + .../servers/show-swarm-overview-modal.tsx | 51 +++++++++ .../swarm/applications/show-applications.tsx | 25 +++-- .../dashboard/swarm/details/details-card.tsx | 38 ++++--- .../swarm/details/show-node-config.tsx | 8 +- .../dashboard/swarm/monitoring-card.tsx | 82 ++++++-------- .../dashboard/swarm/servers/server-card.tsx | 103 ------------------ .../swarm/servers/servers-overview.tsx | 74 ------------- .../components/layouts/navigation-tabs.tsx | 2 +- apps/dokploy/pages/dashboard/swarm.tsx | 2 - apps/dokploy/server/api/routers/swarm.ts | 33 ++++-- packages/server/src/services/docker.ts | 75 ++++++++++--- 12 files changed, 211 insertions(+), 286 deletions(-) create mode 100644 apps/dokploy/components/dashboard/settings/servers/show-swarm-overview-modal.tsx delete mode 100644 apps/dokploy/components/dashboard/swarm/servers/server-card.tsx delete mode 100644 apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx diff --git a/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx b/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx index a174cd9c..d476fb15 100644 --- a/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx @@ -33,6 +33,7 @@ import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal"; import { UpdateServer } from "./update-server"; import { useRouter } from "next/router"; import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription"; +import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal"; export const ShowServers = () => { const router = useRouter(); @@ -259,6 +260,9 @@ export const ShowServers = () => { + )} diff --git a/apps/dokploy/components/dashboard/settings/servers/show-swarm-overview-modal.tsx b/apps/dokploy/components/dashboard/settings/servers/show-swarm-overview-modal.tsx new file mode 100644 index 00000000..f8acd207 --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/servers/show-swarm-overview-modal.tsx @@ -0,0 +1,51 @@ +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; +import { ContainerIcon } from "lucide-react"; +import { useState } from "react"; +import SwarmMonitorCard from "../../swarm/monitoring-card"; + +interface Props { + serverId: string; +} + +export const ShowSwarmOverviewModal = ({ serverId }: Props) => { + const [isOpen, setIsOpen] = useState(false); + + return ( + + + e.preventDefault()} + > + Show Swarm Overview + + + + +
+ + + Swarm Overview + +

+ See all details of your swarm node +

+
+
+ +
+
+ +
+
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx b/apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx index e3b38a71..132cb008 100644 --- a/apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx +++ b/apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx @@ -8,13 +8,13 @@ import { DialogTrigger, } from "@/components/ui/dialog"; import { api } from "@/utils/api"; -import { Layers, LoaderIcon } from "lucide-react"; +import { Layers, Loader2 } from "lucide-react"; import React from "react"; import { columns } from "./columns"; import { DataTable } from "./data-table"; interface Props { - nodeName: string; + serverId?: string; } interface ApplicationList { @@ -30,10 +30,9 @@ interface ApplicationList { Node: string; } -const ShowNodeApplications = ({ nodeName }: Props) => { - const [loading, setLoading] = React.useState(true); +export const ShowNodeApplications = ({ serverId }: Props) => { const { data: NodeApps, isLoading: NodeAppsLoading } = - api.swarm.getNodeApps.useQuery(); + api.swarm.getNodeApps.useQuery({ serverId }); let applicationList = ""; @@ -42,14 +41,14 @@ const ShowNodeApplications = ({ nodeName }: Props) => { } const { data: NodeAppDetails, isLoading: NodeAppDetailsLoading } = - api.swarm.getAppInfos.useQuery({ appName: applicationList }); + api.swarm.getAppInfos.useQuery({ appName: applicationList, serverId }); if (NodeAppsLoading || NodeAppDetailsLoading) { return ( @@ -57,7 +56,11 @@ const ShowNodeApplications = ({ nodeName }: Props) => { } if (!NodeApps || !NodeAppDetails) { - return
No data found
; + return ( + + No data found + + ); } const combinedData: ApplicationList[] = NodeApps.flatMap((app) => { @@ -97,19 +100,17 @@ const ShowNodeApplications = ({ nodeName }: Props) => { Services - + Node Applications See in detail the applications running on this node -
+
); }; - -export default ShowNodeApplications; diff --git a/apps/dokploy/components/dashboard/swarm/details/details-card.tsx b/apps/dokploy/components/dashboard/swarm/details/details-card.tsx index b8eb9f81..a499f898 100644 --- a/apps/dokploy/components/dashboard/swarm/details/details-card.tsx +++ b/apps/dokploy/components/dashboard/swarm/details/details-card.tsx @@ -1,9 +1,14 @@ import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { api } from "@/utils/api"; -import { AlertCircle, CheckCircle, HelpCircle, LoaderIcon } from "lucide-react"; -import { useState } from "react"; -import ShowNodeApplications from "../applications/show-applications"; +import { + AlertCircle, + CheckCircle, + HelpCircle, + Loader2, + LoaderIcon, +} from "lucide-react"; +import { ShowNodeApplications } from "../applications/show-applications"; import { ShowNodeConfig } from "./show-node-config"; export interface SwarmList { @@ -16,13 +21,15 @@ export interface SwarmList { TLSStatus: string; } -interface NodeCardProps { +interface Props { node: SwarmList; + serverId?: string; } -export function NodeCard({ node }: NodeCardProps) { +export function NodeCard({ node, serverId }: Props) { const { data, isLoading } = api.swarm.getNodeInfo.useQuery({ nodeId: node.ID, + serverId, }); const getStatusIcon = (status: string) => { @@ -40,7 +47,7 @@ export function NodeCard({ node }: NodeCardProps) { return ( - + {getStatusIcon(node.Status)} {node.Hostname} @@ -52,7 +59,7 @@ export function NodeCard({ node }: NodeCardProps) {
- +
@@ -63,7 +70,7 @@ export function NodeCard({ node }: NodeCardProps) { - + {getStatusIcon(node.Status)} {node.Hostname} @@ -83,7 +90,7 @@ export function NodeCard({ node }: NodeCardProps) { {isLoading ? ( ) : ( - {data.Status.Addr} + {data?.Status?.Addr} )}
@@ -100,7 +107,7 @@ export function NodeCard({ node }: NodeCardProps) { ) : ( - {(data.Description.Resources.NanoCPUs / 1e9).toFixed(2)} GHz + {(data?.Description?.Resources?.NanoCPUs / 1e9).toFixed(2)} GHz )}
@@ -110,9 +117,10 @@ export function NodeCard({ node }: NodeCardProps) { ) : ( - {(data.Description.Resources.MemoryBytes / 1024 ** 3).toFixed( - 2, - )}{" "} + {( + data?.Description?.Resources?.MemoryBytes / + 1024 ** 3 + ).toFixed(2)}{" "} GB )} @@ -123,8 +131,8 @@ export function NodeCard({ node }: NodeCardProps) {
- - + +
diff --git a/apps/dokploy/components/dashboard/swarm/details/show-node-config.tsx b/apps/dokploy/components/dashboard/swarm/details/show-node-config.tsx index 2d8a3e3e..a41c5a49 100644 --- a/apps/dokploy/components/dashboard/swarm/details/show-node-config.tsx +++ b/apps/dokploy/components/dashboard/swarm/details/show-node-config.tsx @@ -13,10 +13,14 @@ import { Settings } from "lucide-react"; interface Props { nodeId: string; + serverId?: string; } -export const ShowNodeConfig = ({ nodeId }: Props) => { - const { data, isLoading } = api.swarm.getNodeInfo.useQuery({ nodeId }); +export const ShowNodeConfig = ({ nodeId, serverId }: Props) => { + const { data, isLoading } = api.swarm.getNodeInfo.useQuery({ + nodeId, + serverId, + }); return ( diff --git a/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx index 81a68172..e2453dd9 100644 --- a/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx +++ b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx @@ -9,68 +9,44 @@ import { } from "@/components/ui/tooltip"; import { api } from "@/utils/api"; import { - Activity, AlertCircle, CheckCircle, HelpCircle, Loader2, Server, } from "lucide-react"; -import Link from "next/link"; import { NodeCard } from "./details/details-card"; -export interface SwarmList { - ID: string; - Hostname: string; - Availability: string; - EngineVersion: string; - Status: string; - ManagerStatus: string; - TLSStatus: string; +interface Props { + serverId?: string; } -interface SwarmMonitorCardProps { - nodes: SwarmList[]; -} - -export default function SwarmMonitorCard() { - const { data: nodes, isLoading } = api.swarm.getNodes.useQuery(); +export default function SwarmMonitorCard({ serverId }: Props) { + const { data: nodes, isLoading } = api.swarm.getNodes.useQuery({ + serverId, + }); if (isLoading) { return (
- - - - - Docker Swarm Monitor - - - -
- -
-
-
+
+
+ +
+
); } if (!nodes) { return ( - - - - - Docker Swarm Monitor - - - -
+
+
+
Failed to load data
- - +
+
); } @@ -105,19 +81,23 @@ export default function SwarmMonitorCard() { return (
-

Docker Swarm Overview

- +

Docker Swarm Overview

+ {!serverId && ( + + )}
- - - Docker Swarm Monitor + + + Monitor @@ -200,7 +180,7 @@ export default function SwarmMonitorCard() {
{nodes.map((node) => ( - + ))}
diff --git a/apps/dokploy/components/dashboard/swarm/servers/server-card.tsx b/apps/dokploy/components/dashboard/swarm/servers/server-card.tsx deleted file mode 100644 index 4b732df4..00000000 --- a/apps/dokploy/components/dashboard/swarm/servers/server-card.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; -import { AlertCircle, CheckCircle, HelpCircle, ServerIcon } from "lucide-react"; -import { ShowContainers } from "../../docker/show/show-containers"; - -export interface Server { - serverId: string; - name: string; - description: string | null; - ipAddress: string; - port: number; - username: string; - appName: string; - enableDockerCleanup: boolean; - createdAt: string; - adminId: string; - serverStatus: "active" | "inactive"; - command: string; - sshKeyId: string | null; -} - -interface ServerOverviewCardProps { - server: Server; -} - -export function ServerOverviewCard({ server }: ServerOverviewCardProps) { - const getStatusIcon = (status: string) => { - switch (status) { - case "active": - return ; - case "inactive": - return ; - default: - return ; - } - }; - - return ( - - - - - {getStatusIcon(server.serverStatus)} - {server.name} - - - {server.serverStatus} - - - - -
-
- IP Address: - {server.ipAddress} -
-
- Port: - {server.port} -
-
- Username: - {server.username} -
-
- App Name: - {server.appName} -
-
- Docker Cleanup: - {server.enableDockerCleanup ? "Enabled" : "Disabled"} -
-
- Created At: - {new Date(server.createdAt).toLocaleString()} -
-
-
- - - - - - - - -
-
-
- ); -} diff --git a/apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx b/apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx deleted file mode 100644 index bd54f43e..00000000 --- a/apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; -import { api } from "@/utils/api"; -import { LoaderIcon } from "lucide-react"; -import { ServerOverviewCard } from "./server-card"; - -export default function ServersOverview() { - const { data: servers, isLoading } = api.server.all.useQuery(); - - if (isLoading) { - return ( - <> - - - - - - - - - - - - -
-
- IP Address: -
-
- Port: -
-
- Username: -
-
- App Name: -
-
- Docker Cleanup: -
-
- Created At: -
-
-
-
- - ); - } - - if (!servers) { - return
No servers found
; - } - return ( -
-
-

Server Overview

- -
-
- {servers.map((server) => ( - - ))} -
-
- ); -} diff --git a/apps/dokploy/components/layouts/navigation-tabs.tsx b/apps/dokploy/components/layouts/navigation-tabs.tsx index 46e590a7..782b9d46 100644 --- a/apps/dokploy/components/layouts/navigation-tabs.tsx +++ b/apps/dokploy/components/layouts/navigation-tabs.tsx @@ -62,7 +62,7 @@ const getTabMaps = (isCloud: boolean) => { type: "docker", }, { - label: "Swarm & Server", + label: "Swarm", description: "Manage your docker swarm and Servers", index: "/dashboard/swarm", isShow: ({ rol, user }) => { diff --git a/apps/dokploy/pages/dashboard/swarm.tsx b/apps/dokploy/pages/dashboard/swarm.tsx index 2722e0e4..15a7d793 100644 --- a/apps/dokploy/pages/dashboard/swarm.tsx +++ b/apps/dokploy/pages/dashboard/swarm.tsx @@ -1,7 +1,5 @@ import SwarmMonitorCard from "@/components/dashboard/swarm/monitoring-card"; -import ServersOverview from "@/components/dashboard/swarm/servers/servers-overview"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; -import { Separator } from "@/components/ui/separator"; import { appRouter } from "@/server/api/root"; import { IS_CLOUD, validateRequest } from "@dokploy/server"; import { createServerSideHelpers } from "@trpc/react-query/server"; diff --git a/apps/dokploy/server/api/routers/swarm.ts b/apps/dokploy/server/api/routers/swarm.ts index fe15d0ef..c5a2d4c8 100644 --- a/apps/dokploy/server/api/routers/swarm.ts +++ b/apps/dokploy/server/api/routers/swarm.ts @@ -8,24 +8,37 @@ import { z } from "zod"; import { createTRPCRouter, protectedProcedure } from "../trpc"; export const swarmRouter = createTRPCRouter({ - getNodes: protectedProcedure.query(async () => { - return await getSwarmNodes(); - }), - getNodeInfo: protectedProcedure - .input(z.object({ nodeId: z.string() })) + getNodes: protectedProcedure + .input( + z.object({ + serverId: z.string().optional(), + }), + ) .query(async ({ input }) => { - return await getNodeInfo(input.nodeId); + return await getSwarmNodes(input.serverId); + }), + getNodeInfo: protectedProcedure + .input(z.object({ nodeId: z.string(), serverId: z.string().optional() })) + .query(async ({ input }) => { + return await getNodeInfo(input.nodeId, input.serverId); + }), + getNodeApps: protectedProcedure + .input( + z.object({ + serverId: z.string().optional(), + }), + ) + .query(async ({ input }) => { + return getNodeApplications(input.serverId); }), - getNodeApps: protectedProcedure.query(async () => { - return getNodeApplications(); - }), getAppInfos: protectedProcedure .input( z.object({ appName: z.string(), + serverId: z.string().optional(), }), ) .query(async ({ input }) => { - return await getApplicationInfo(input.appName); + return await getApplicationInfo(input.appName, input.serverId); }), }); diff --git a/packages/server/src/services/docker.ts b/packages/server/src/services/docker.ts index c13c71e5..60262ba1 100644 --- a/packages/server/src/services/docker.ts +++ b/packages/server/src/services/docker.ts @@ -225,11 +225,21 @@ export const containerRestart = async (containerId: string) => { } catch (error) {} }; -export const getSwarmNodes = async () => { +export const getSwarmNodes = async (serverId?: string) => { try { - const { stdout, stderr } = await execAsync( - "docker node ls --format '{{json .}}'", - ); + let stdout = ""; + let stderr = ""; + const command = "docker node ls --format '{{json .}}'"; + + if (serverId) { + const result = await execAsyncRemote(serverId, command); + stdout = result.stdout; + stderr = result.stderr; + } else { + const result = await execAsync(command); + stdout = result.stdout; + stderr = result.stderr; + } if (stderr) { console.error(`Error: ${stderr}`); @@ -246,11 +256,20 @@ export const getSwarmNodes = async () => { } catch (error) {} }; -export const getNodeInfo = async (nodeId: string) => { +export const getNodeInfo = async (nodeId: string, serverId?: string) => { try { - const { stdout, stderr } = await execAsync( - `docker node inspect ${nodeId} --format '{{json .}}'`, - ); + const command = `docker node inspect ${nodeId} --format '{{json .}}'`; + let stdout = ""; + let stderr = ""; + if (serverId) { + const result = await execAsyncRemote(serverId, command); + stdout = result.stdout; + stderr = result.stderr; + } else { + const result = await execAsync(command); + stdout = result.stdout; + stderr = result.stderr; + } if (stderr) { console.error(`Error: ${stderr}`); @@ -263,11 +282,22 @@ export const getNodeInfo = async (nodeId: string) => { } catch (error) {} }; -export const getNodeApplications = async () => { +export const getNodeApplications = async (serverId?: string) => { try { - const { stdout, stderr } = await execAsync( - `docker service ls --format '{{json .}}'`, - ); + let stdout = ""; + let stderr = ""; + const command = `docker service ls --format '{{json .}}'`; + + if (serverId) { + const result = await execAsyncRemote(serverId, command); + stdout = result.stdout; + stderr = result.stderr; + } else { + const result = await execAsync(command); + + stdout = result.stdout; + stderr = result.stderr; + } if (stderr) { console.error(`Error: ${stderr}`); @@ -283,11 +313,24 @@ export const getNodeApplications = async () => { } catch (error) {} }; -export const getApplicationInfo = async (appName: string) => { +export const getApplicationInfo = async ( + appName: string, + serverId?: string, +) => { try { - const { stdout, stderr } = await execAsync( - `docker service ps ${appName} --format '{{json .}}'`, - ); + let stdout = ""; + let stderr = ""; + const command = `docker service ps ${appName} --format '{{json .}}'`; + + if (serverId) { + const result = await execAsyncRemote(serverId, command); + stdout = result.stdout; + stderr = result.stderr; + } else { + const result = await execAsync(command); + stdout = result.stdout; + stderr = result.stderr; + } if (stderr) { console.error(`Error: ${stderr}`);