From b592a025e48fc3a3df48bc1b06675c6a28a4d8a2 Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Tue, 17 Dec 2024 12:04:49 +0100 Subject: [PATCH 01/18] feat: add swarm router and related Docker service functions --- apps/dokploy/server/api/root.ts | 4 +- apps/dokploy/server/api/routers/swarm.ts | 31 +++++++ packages/server/src/services/docker.ts | 103 +++++++++++++++++++++++ 3 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 apps/dokploy/server/api/routers/swarm.ts diff --git a/apps/dokploy/server/api/root.ts b/apps/dokploy/server/api/root.ts index 85eb9763..68f5e4e0 100644 --- a/apps/dokploy/server/api/root.ts +++ b/apps/dokploy/server/api/root.ts @@ -21,6 +21,7 @@ import { mysqlRouter } from "./routers/mysql"; import { notificationRouter } from "./routers/notification"; import { portRouter } from "./routers/port"; import { postgresRouter } from "./routers/postgres"; +import { previewDeploymentRouter } from "./routers/preview-deployment"; import { projectRouter } from "./routers/project"; import { redirectsRouter } from "./routers/redirects"; import { redisRouter } from "./routers/redis"; @@ -30,8 +31,8 @@ import { serverRouter } from "./routers/server"; import { settingsRouter } from "./routers/settings"; import { sshRouter } from "./routers/ssh-key"; import { stripeRouter } from "./routers/stripe"; +import { swarmRouter } from "./routers/swarm"; import { userRouter } from "./routers/user"; -import { previewDeploymentRouter } from "./routers/preview-deployment"; /** * This is the primary router for your server. @@ -73,6 +74,7 @@ export const appRouter = createTRPCRouter({ github: githubRouter, server: serverRouter, stripe: stripeRouter, + swarm: swarmRouter, }); // export type definition of API diff --git a/apps/dokploy/server/api/routers/swarm.ts b/apps/dokploy/server/api/routers/swarm.ts new file mode 100644 index 00000000..fe15d0ef --- /dev/null +++ b/apps/dokploy/server/api/routers/swarm.ts @@ -0,0 +1,31 @@ +import { + getApplicationInfo, + getNodeApplications, + getNodeInfo, + getSwarmNodes, +} from "@dokploy/server"; +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() })) + .query(async ({ input }) => { + return await getNodeInfo(input.nodeId); + }), + getNodeApps: protectedProcedure.query(async () => { + return getNodeApplications(); + }), + getAppInfos: protectedProcedure + .input( + z.object({ + appName: z.string(), + }), + ) + .query(async ({ input }) => { + return await getApplicationInfo(input.appName); + }), +}); diff --git a/packages/server/src/services/docker.ts b/packages/server/src/services/docker.ts index 6ac61354..8681cb22 100644 --- a/packages/server/src/services/docker.ts +++ b/packages/server/src/services/docker.ts @@ -224,3 +224,106 @@ export const containerRestart = async (containerId: string) => { return config; } catch (error) {} }; + +export const getSwarmNodes = async () => { + try { + const { stdout, stderr } = await execAsync( + "docker node ls --format '{{json .}}'", + ); + + if (stderr) { + console.error(`Error: ${stderr}`); + return; + } + + const nodes = JSON.parse(stdout); + + const nodesArray = stdout + .trim() + .split("\n") + .map((line) => JSON.parse(line)); + return nodesArray; + } catch (error) {} +}; + +export const getNodeInfo = async (nodeId: string) => { + try { + const { stdout, stderr } = await execAsync( + `docker node inspect ${nodeId} --format '{{json .}}'`, + ); + + if (stderr) { + console.error(`Error: ${stderr}`); + return; + } + + const nodeInfo = JSON.parse(stdout); + + return nodeInfo; + } catch (error) {} +}; + +export const getNodeApplications = async () => { + try { + // TODO: Implement this + // const { stdout, stderr } = await execAsync( + // `docker service ls --format '{{json .}}'` + // ); + + const stdout = `{"ID":"pxvnj68dxom9","Image":"dokploy/dokploy:latest","Mode":"replicated","Name":"dokploy","Ports":"","Replicas":"1/1"} +{"ID":"1sweo6dr2vrn","Image":"postgres:16","Mode":"replicated","Name":"dokploy-postgres","Ports":"","Replicas":"1/1"} +{"ID":"tnl2fck3rbop","Image":"redis:7","Mode":"replicated","Name":"dokploy-redis","Ports":"","Replicas":"1/1"} +{"ID":"o9ady4y1p96x","Image":"traefik:v3.1.2","Mode":"replicated","Name":"dokploy-traefik","Ports":"","Replicas":"1/1"} +{"ID":"rsxe3l71h9y4","Image":"esports-manager-api-eg8t7w:latest","Mode":"replicated","Name":"esports-manager-api-eg8t7w","Ports":"","Replicas":"1/1"} +{"ID":"fw52vzcw5dc0","Image":"team-synix-admin-dvgspy:latest","Mode":"replicated","Name":"team-synix-admin-dvgspy","Ports":"","Replicas":"1/1"} +{"ID":"551bwmtd6b4t","Image":"team-synix-leaderboard-9vx8ca:latest","Mode":"replicated","Name":"team-synix-leaderboard-9vx8ca","Ports":"","Replicas":"1/1"} +{"ID":"h1eyg3g1tyn3","Image":"postgres:15","Mode":"replicated","Name":"team-synix-webpage-db-fkivnf","Ports":"","Replicas":"1/1"}`; + + // if (stderr) { + // console.error(`Error: ${stderr}`); + // return; + // } + + const appArray = stdout + .trim() + .split("\n") + .map((line) => JSON.parse(line)); + + console.log(appArray); + return appArray; + } catch (error) {} +}; + +export const getApplicationInfo = async (appName: string) => { + try { + // TODO: Implement this + // const { stdout, stderr } = await execAsync( + // `docker service ps ${appName} --format '{{json .}}'` + // ); + + const stdout = `{"CurrentState":"Running 2 weeks ago","DesiredState":"Running","Error":"","ID":"nx8jxlmb8niw","Image":"postgres:16","Name":"dokploy-postgres.1","Node":"v2202411192718297480","Ports":""} +{"CurrentState":"Running 2 weeks ago","DesiredState":"Running","Error":"","ID":"s288g9lwtvi4","Image":"redis:7","Name":"dokploy-redis.1","Node":"v2202411192718297480","Ports":""} +{"CurrentState":"Running 2 weeks ago","DesiredState":"Running","Error":"","ID":"2vcmejz51b23","Image":"traefik:v3.1.2","Name":"dokploy-traefik.1","Node":"v2202411192718297480","Ports":"*:80-\u003e80/tcp,*:80-\u003e80/tcp,*:443-\u003e443/tcp,*:443-\u003e443/tcp"} +{"CurrentState":"Running 26 hours ago","DesiredState":"Running","Error":"","ID":"79iatnbsm2um","Image":"dokploy/dokploy:latest","Name":"dokploy.1","Node":"v2202411192718297480","Ports":"*:3000-\u003e3000/tcp,*:3000-\u003e3000/tcp"} +{"CurrentState":"Shutdown 26 hours ago","DesiredState":"Shutdown","Error":"","ID":"zcwxs501zs7w","Image":"dokploy/dokploy:latest","Name":"dokploy.1","Node":"v2202411192718297480","Ports":""} +{"CurrentState":"Shutdown 7 days ago","DesiredState":"Shutdown","Error":"","ID":"t59qhkoenno4","Image":"dokploy/dokploy:latest","Name":"dokploy.1","Node":"v2202411192718297480","Ports":""} +{"CurrentState":"Shutdown 7 days ago","DesiredState":"Shutdown","Error":"","ID":"o5xtcuj6um7e","Image":"dokploy/dokploy:latest","Name":"dokploy.1","Node":"v2202411192718297480","Ports":""} +{"CurrentState":"Running 2 weeks ago","DesiredState":"Running","Error":"","ID":"q1lr9rmf452g","Image":"esports-manager-api-eg8t7w:latest","Name":"esports-manager-api-eg8t7w.1","Node":"v2202411192718297480","Ports":""} +{"CurrentState":"Shutdown 2 weeks ago","DesiredState":"Shutdown","Error":"","ID":"y9ixpg6b8qdo","Image":"esports-manager-api-eg8t7w:latest","Name":"esports-manager-api-eg8t7w.1","Node":"v2202411192718297480","Ports":""} +{"CurrentState":"Running 24 hours ago","DesiredState":"Running","Error":"","ID":"xgcb919qjg1a","Image":"team-synix-admin-dvgspy:latest","Name":"team-synix-admin-dvgspy.1","Node":"v2202411192718297480","Ports":""} +{"CurrentState":"Running 26 hours ago","DesiredState":"Running","Error":"","ID":"7yi95wh8zhh6","Image":"team-synix-leaderboard-9vx8ca:latest","Name":"team-synix-leaderboard-9vx8ca.1","Node":"v2202411192718297480","Ports":""} +{"CurrentState":"Running 2 weeks ago","DesiredState":"Running","Error":"","ID":"89yzsnghpbq6","Image":"postgres:15","Name":"team-synix-webpage-db-fkivnf.1","Node":"v2202411192718297480","Ports":""}`; + + // if (stderr) { + // console.error(`Error: ${stderr}`); + // return; + // } + + const appArray = stdout + .trim() + .split("\n") + .map((line) => JSON.parse(line)); + + return appArray; + } catch (error) {} +}; From 04d3eb9ec01f246bec10b705dd562d4f64ad1efb Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Tue, 17 Dec 2024 12:05:02 +0100 Subject: [PATCH 02/18] feat: add swarm tab and dashboard page for managing Docker swarm --- .../components/layouts/navigation-tabs.tsx | 298 +++++++++--------- apps/dokploy/pages/dashboard/swarm.tsx | 81 +++++ 2 files changed, 235 insertions(+), 144 deletions(-) create mode 100644 apps/dokploy/pages/dashboard/swarm.tsx diff --git a/apps/dokploy/components/layouts/navigation-tabs.tsx b/apps/dokploy/components/layouts/navigation-tabs.tsx index ab3dafca..c9392b10 100644 --- a/apps/dokploy/components/layouts/navigation-tabs.tsx +++ b/apps/dokploy/components/layouts/navigation-tabs.tsx @@ -7,166 +7,176 @@ import { useEffect, useMemo, useState } from "react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; interface TabInfo { - label: string; - tabLabel?: string; - description: string; - index: string; - type: TabState; - isShow?: ({ rol, user }: { rol?: Auth["rol"]; user?: User }) => boolean; + label: string; + tabLabel?: string; + description: string; + index: string; + type: TabState; + isShow?: ({ rol, user }: { rol?: Auth["rol"]; user?: User }) => boolean; } export type TabState = - | "projects" - | "monitoring" - | "settings" - | "traefik" - | "requests" - | "docker"; + | "projects" + | "monitoring" + | "settings" + | "traefik" + | "requests" + | "docker" + | "swarm"; const getTabMaps = (isCloud: boolean) => { - const elements: TabInfo[] = [ - { - label: "Projects", - description: "Manage your projects", - index: "/dashboard/projects", - type: "projects", - }, - ]; + const elements: TabInfo[] = [ + { + label: "Projects", + description: "Manage your projects", + index: "/dashboard/projects", + type: "projects", + }, + ]; - if (!isCloud) { - elements.push( - { - label: "Monitoring", - description: "Monitor your projects", - index: "/dashboard/monitoring", - type: "monitoring", - }, - { - label: "Traefik", - tabLabel: "Traefik File System", - description: "Manage your traefik", - index: "/dashboard/traefik", - isShow: ({ rol, user }) => { - return Boolean(rol === "admin" || user?.canAccessToTraefikFiles); - }, - type: "traefik", - }, - { - label: "Docker", - description: "Manage your docker", - index: "/dashboard/docker", - isShow: ({ rol, user }) => { - return Boolean(rol === "admin" || user?.canAccessToDocker); - }, - type: "docker", - }, - { - label: "Requests", - description: "Manage your requests", - index: "/dashboard/requests", - isShow: ({ rol, user }) => { - return Boolean(rol === "admin" || user?.canAccessToDocker); - }, - type: "requests", - }, - ); - } + if (!isCloud) { + elements.push( + { + label: "Monitoring", + description: "Monitor your projects", + index: "/dashboard/monitoring", + type: "monitoring", + }, + { + label: "Traefik", + tabLabel: "Traefik File System", + description: "Manage your traefik", + index: "/dashboard/traefik", + isShow: ({ rol, user }) => { + return Boolean(rol === "admin" || user?.canAccessToTraefikFiles); + }, + type: "traefik", + }, + { + label: "Docker", + description: "Manage your docker", + index: "/dashboard/docker", + isShow: ({ rol, user }) => { + return Boolean(rol === "admin" || user?.canAccessToDocker); + }, + type: "docker", + }, + { + label: "Swarm", + description: "Manage your docker swarm", + index: "/dashboard/swarm", + isShow: ({ rol, user }) => { + return Boolean(rol === "admin" || user?.canAccessToDocker); + }, + type: "swarm", + }, + { + label: "Requests", + description: "Manage your requests", + index: "/dashboard/requests", + isShow: ({ rol, user }) => { + return Boolean(rol === "admin" || user?.canAccessToDocker); + }, + type: "requests", + } + ); + } - elements.push({ - label: "Settings", - description: "Manage your settings", - type: "settings", - index: isCloud - ? "/dashboard/settings/profile" - : "/dashboard/settings/server", - }); + elements.push({ + label: "Settings", + description: "Manage your settings", + type: "settings", + index: isCloud + ? "/dashboard/settings/profile" + : "/dashboard/settings/server", + }); - return elements; + return elements; }; interface Props { - tab: TabState; - children: React.ReactNode; + tab: TabState; + children: React.ReactNode; } export const NavigationTabs = ({ tab, children }: Props) => { - const router = useRouter(); - const { data } = api.auth.get.useQuery(); - const [activeTab, setActiveTab] = useState(tab); - const { data: isCloud } = api.settings.isCloud.useQuery(); - const tabMap = useMemo(() => getTabMaps(isCloud ?? false), [isCloud]); - const { data: user } = api.user.byAuthId.useQuery( - { - authId: data?.id || "", - }, - { - enabled: !!data?.id && data?.rol === "user", - }, - ); + const router = useRouter(); + const { data } = api.auth.get.useQuery(); + const [activeTab, setActiveTab] = useState(tab); + const { data: isCloud } = api.settings.isCloud.useQuery(); + const tabMap = useMemo(() => getTabMaps(isCloud ?? false), [isCloud]); + const { data: user } = api.user.byAuthId.useQuery( + { + authId: data?.id || "", + }, + { + enabled: !!data?.id && data?.rol === "user", + } + ); - useEffect(() => { - setActiveTab(tab); - }, [tab]); + useEffect(() => { + setActiveTab(tab); + }, [tab]); - const activeTabInfo = useMemo(() => { - return tabMap.find((tab) => tab.type === activeTab); - }, [activeTab]); + const activeTabInfo = useMemo(() => { + return tabMap.find((tab) => tab.type === activeTab); + }, [activeTab]); - return ( -
-
-
-

- {activeTabInfo?.label} -

-

- {activeTabInfo?.description} -

-
- {tab === "projects" && - (data?.rol === "admin" || user?.canCreateProjects) && } -
-
- { - setActiveTab(e as TabState); - const tab = tabMap.find((tab) => tab.type === e); - router.push(tab?.index || ""); - }} - > -
- - {tabMap.map((tab, index) => { - if (tab?.isShow && !tab?.isShow?.({ rol: data?.rol, user })) { - return null; - } - return ( - - - {tab?.tabLabel || tab?.label} - - {tab.type === activeTab && ( -
-
-
- )} - - ); - })} - -
+ return ( +
+
+
+

+ {activeTabInfo?.label} +

+

+ {activeTabInfo?.description} +

+
+ {tab === "projects" && + (data?.rol === "admin" || user?.canCreateProjects) && } +
+
+ { + setActiveTab(e as TabState); + const tab = tabMap.find((tab) => tab.type === e); + router.push(tab?.index || ""); + }} + > +
+ + {tabMap.map((tab, index) => { + if (tab?.isShow && !tab?.isShow?.({ rol: data?.rol, user })) { + return null; + } + return ( + + + {tab?.tabLabel || tab?.label} + + {tab.type === activeTab && ( +
+
+
+ )} + + ); + })} + +
- - {children} - - -
-
- ); + + {children} + + +
+
+ ); }; diff --git a/apps/dokploy/pages/dashboard/swarm.tsx b/apps/dokploy/pages/dashboard/swarm.tsx new file mode 100644 index 00000000..d353ceed --- /dev/null +++ b/apps/dokploy/pages/dashboard/swarm.tsx @@ -0,0 +1,81 @@ +import { ShowSwarm } from "@/components/dashboard/swarm/show/node-list"; +import ShowSwarmNodes from "@/components/dashboard/swarm/show/show-nodes"; +import { DashboardLayout } from "@/components/layouts/dashboard-layout"; +import { appRouter } from "@/server/api/root"; +import { IS_CLOUD, validateRequest } from "@dokploy/server"; +import { createServerSideHelpers } from "@trpc/react-query/server"; +import type { GetServerSidePropsContext } from "next"; +import React, { type ReactElement } from "react"; +import superjson from "superjson"; + +const Dashboard = () => { + return ; +}; + +export default Dashboard; + +Dashboard.getLayout = (page: ReactElement) => { + return {page}; +}; +export async function getServerSideProps( + ctx: GetServerSidePropsContext<{ serviceId: string }> +) { + if (IS_CLOUD) { + return { + redirect: { + permanent: true, + destination: "/dashboard/projects", + }, + }; + } + const { user, session } = await validateRequest(ctx.req, ctx.res); + if (!user) { + return { + redirect: { + permanent: true, + destination: "/", + }, + }; + } + const { req, res } = ctx; + + const helpers = createServerSideHelpers({ + router: appRouter, + ctx: { + req: req as any, + res: res as any, + db: null as any, + session: session, + user: user, + }, + transformer: superjson, + }); + try { + await helpers.project.all.prefetch(); + const auth = await helpers.auth.get.fetch(); + + if (auth.rol === "user") { + const user = await helpers.user.byAuthId.fetch({ + authId: auth.id, + }); + + if (!user.canAccessToDocker) { + return { + redirect: { + permanent: true, + destination: "/", + }, + }; + } + } + return { + props: { + trpcState: helpers.dehydrate(), + }, + }; + } catch (error) { + return { + props: {}, + }; + } +} From 5716954665cfe59b07ecd684bb50c3f2bb0bef04 Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Tue, 17 Dec 2024 12:05:39 +0100 Subject: [PATCH 03/18] feat: add components for displaying swarm node details and applications --- .../dashboard/swarm/applications/columns.tsx | 218 ++++++++++++++ .../swarm/applications/data-table.tsx | 264 +++++++++++++++++ .../swarm/applications/show-applications.tsx | 122 ++++++++ .../dashboard/swarm/details/show-node.tsx | 54 ++++ .../dashboard/swarm/show/columns.tsx | 201 +++++++++++++ .../dashboard/swarm/show/data-table.tsx | 269 ++++++++++++++++++ .../dashboard/swarm/show/show-nodes.tsx | 16 ++ 7 files changed, 1144 insertions(+) create mode 100644 apps/dokploy/components/dashboard/swarm/applications/columns.tsx create mode 100644 apps/dokploy/components/dashboard/swarm/applications/data-table.tsx create mode 100644 apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx create mode 100644 apps/dokploy/components/dashboard/swarm/details/show-node.tsx create mode 100644 apps/dokploy/components/dashboard/swarm/show/columns.tsx create mode 100644 apps/dokploy/components/dashboard/swarm/show/data-table.tsx create mode 100644 apps/dokploy/components/dashboard/swarm/show/show-nodes.tsx diff --git a/apps/dokploy/components/dashboard/swarm/applications/columns.tsx b/apps/dokploy/components/dashboard/swarm/applications/columns.tsx new file mode 100644 index 00000000..ba2d9e13 --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/applications/columns.tsx @@ -0,0 +1,218 @@ +import type { ColumnDef } from "@tanstack/react-table"; +import { ArrowUpDown, MoreHorizontal } from "lucide-react"; +import * as React from "react"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +import { Badge } from "@/components/ui/badge"; +import { ShowNodeConfig } from "../details/show-node"; +// import { ShowContainerConfig } from "../config/show-container-config"; +// import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs"; +// import { DockerTerminalModal } from "../terminal/docker-terminal-modal"; +// import type { Container } from "./show-containers"; + +export interface ApplicationList { + ID: string; + Image: string; + Mode: string; + Name: string; + Ports: string; + Replicas: string; + CurrentState: string; + DesiredState: string; + Error: string; + Node: string; +} + +export const columns: ColumnDef[] = [ + { + accessorKey: "ID", + accessorFn: (row) => row.ID, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("ID")}
; + }, + }, + { + accessorKey: "Name", + accessorFn: (row) => row.Name, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("Name")}
; + }, + }, + { + accessorKey: "Image", + accessorFn: (row) => row.Image, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("Image")}
; + }, + }, + { + accessorKey: "Mode", + accessorFn: (row) => row.Mode, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("Mode")}
; + }, + }, + { + accessorKey: "CurrentState", + accessorFn: (row) => row.CurrentState, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const value = row.getValue("CurrentState") as string; + const valueStart = value.startsWith("Running") + ? "Running" + : value.startsWith("Shutdown") + ? "Shutdown" + : value; + return ( +
+ + {value} + +
+ ); + }, + }, + { + accessorKey: "DesiredState", + accessorFn: (row) => row.DesiredState, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("DesiredState")}
; + }, + }, + + { + accessorKey: "Replicas", + accessorFn: (row) => row.Replicas, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("Replicas")}
; + }, + }, + + { + accessorKey: "Ports", + accessorFn: (row) => row.Ports, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("Ports")}
; + }, + }, + { + accessorKey: "Errors", + accessorFn: (row) => row.Error, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("Errors")}
; + }, + }, +]; diff --git a/apps/dokploy/components/dashboard/swarm/applications/data-table.tsx b/apps/dokploy/components/dashboard/swarm/applications/data-table.tsx new file mode 100644 index 00000000..1b192f7d --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/applications/data-table.tsx @@ -0,0 +1,264 @@ +"use client"; + +import { + type ColumnFiltersState, + type SortingState, + type VisibilityState, + type ColumnDef, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import React from "react"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, +} from "@/components/ui/dropdown-menu"; +import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; +import { Button } from "@/components/ui/button"; +import { ChevronDown } from "lucide-react"; +import { Input } from "@/components/ui/input"; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; +} + +export function DataTable({ + columns, + data, +}: DataTableProps) { + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState( + [] + ); + const [columnVisibility, setColumnVisibility] = + React.useState({}); + const [rowSelection, setRowSelection] = React.useState({}); + + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }); + + return ( +
+
+
+ + table.getColumn("Name")?.setFilterValue(event.target.value) + } + className="md:max-w-sm" + /> + + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {column.id} + + ); + })} + + +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ); + })} + + ))} + + + {table?.getRowModel()?.rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No results. + {/* {isLoading ? ( +
+ + Loading... + +
+ ) : ( + <>No results. + )} */} +
+
+ )} +
+
+ {/*
+ {isLoading ? ( +
+ + Loading... + +
+ ) : data?.length === 0 ? ( +
+ + No results. + +
+ ) : ( + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ); + })} + + ))} + + + {table?.getRowModel()?.rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + {isLoading ? ( +
+ + Loading... + +
+ ) : ( + <>No results. + )} +
+
+ )} +
+
+ )} +
*/} + {data && data?.length > 0 && ( +
+
+ + +
+
+ )} +
+
+ ); +} diff --git a/apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx b/apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx new file mode 100644 index 00000000..2ef632b9 --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx @@ -0,0 +1,122 @@ +import React from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { api } from "@/utils/api"; +import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; +import { DataTable } from "./data-table"; +import { columns } from "./columns"; +import { LoaderIcon } from "lucide-react"; + +interface Props { + nodeName: string; +} + +interface ApplicationList { + ID: string; + Image: string; + Mode: string; + Name: string; + Ports: string; + Replicas: string; + CurrentState: string; + DesiredState: string; + Error: string; + Node: string; +} + +const ShowNodeApplications = ({ nodeName }: Props) => { + const [loading, setLoading] = React.useState(true); + const { data: NodeApps, isLoading: NodeAppsLoading } = + api.swarm.getNodeApps.useQuery(); + + let applicationList = ""; + + if (NodeApps && NodeApps.length > 0) { + applicationList = NodeApps.map((app) => app.Name).join(" "); + } + + const { data: NodeAppDetails, isLoading: NodeAppDetailsLoading } = + api.swarm.getAppInfos.useQuery({ appName: applicationList }); + + if (NodeAppsLoading || NodeAppDetailsLoading) { + return ( + + + e.preventDefault()} + > + + + + + ); + } + + if (!NodeApps || !NodeAppDetails) { + return
No data found
; + } + + const combinedData: ApplicationList[] = NodeApps.flatMap((app) => { + const appDetails = + NodeAppDetails?.filter((detail) => + detail.Name.startsWith(`${app.Name}.`) + ) || []; + + if (appDetails.length === 0) { + return [ + { + ...app, + CurrentState: "N/A", + DesiredState: "N/A", + Error: "", + Node: "N/A", + Ports: app.Ports, + }, + ]; + } + + return appDetails.map((detail) => ({ + ...app, + CurrentState: detail.CurrentState, + DesiredState: detail.DesiredState, + Error: detail.Error, + Node: detail.Node, + Ports: detail.Ports || app.Ports, + })); + }); + + return ( + + + e.preventDefault()} + > + Show Applications + + + + + Node Applications + + See in detail the applications running on this node + + +
+ +
+ {/*
*/} +
+
+ ); +}; + +export default ShowNodeApplications; diff --git a/apps/dokploy/components/dashboard/swarm/details/show-node.tsx b/apps/dokploy/components/dashboard/swarm/details/show-node.tsx new file mode 100644 index 00000000..9a092152 --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/details/show-node.tsx @@ -0,0 +1,54 @@ +import React from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; +import { CodeEditor } from "@/components/shared/code-editor"; +import { api } from "@/utils/api"; + +interface Props { + nodeId: string; +} + +export const ShowNodeConfig = ({ nodeId }: Props) => { + const { data, isLoading } = api.swarm.getNodeInfo.useQuery({ nodeId }); + return ( + + + e.preventDefault()} + > + View Config + + + + + Node Config + + See in detail the metadata of this node + + +
+ +
+              {/* {JSON.stringify(data, null, 2)} */}
+              
+            
+
+
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/swarm/show/columns.tsx b/apps/dokploy/components/dashboard/swarm/show/columns.tsx new file mode 100644 index 00000000..ba11d749 --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/show/columns.tsx @@ -0,0 +1,201 @@ +import type { ColumnDef } from "@tanstack/react-table"; +import { ArrowUpDown, MoreHorizontal } from "lucide-react"; +import * as React from "react"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +import { Badge } from "@/components/ui/badge"; +import { ShowNodeConfig } from "../details/show-node"; +import ShowNodeApplications from "../applications/show-applications"; +// import { ShowContainerConfig } from "../config/show-container-config"; +// import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs"; +// import { DockerTerminalModal } from "../terminal/docker-terminal-modal"; +// import type { Container } from "./show-containers"; + +export interface SwarmList { + ID: string; + Hostname: string; + Availability: string; + EngineVersion: string; + Status: string; + ManagerStatus: string; + TLSStatus: string; +} + +export const columns: ColumnDef[] = [ + { + accessorKey: "ID", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("ID")}
; + }, + }, + { + accessorKey: "EngineVersion", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("EngineVersion")}
; + }, + }, + { + accessorKey: "Hostname", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("Hostname")}
; + }, + }, + // { + // accessorKey: "Status", + // header: ({ column }) => { + // return ( + // + // ); + // }, + // cell: ({ row }) => { + // const value = row.getValue("status") as string; + // return ( + //
+ // + // {value} + // + //
+ // ); + // }, + // }, + { + accessorKey: "Availability", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const value = row.getValue("Availability") as string; + return ( +
+ + {value} + +
+ ); + }, + }, + { + accessorKey: "ManagerStatus", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => ( +
{row.getValue("ManagerStatus")}
+ ), + }, + { + id: "actions", + enableHiding: false, + cell: ({ row }) => { + return ( + + + + + + Actions + + + {/* + View Logs + + + + Terminal + */} + + + ); + }, + }, +]; diff --git a/apps/dokploy/components/dashboard/swarm/show/data-table.tsx b/apps/dokploy/components/dashboard/swarm/show/data-table.tsx new file mode 100644 index 00000000..d3e99352 --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/show/data-table.tsx @@ -0,0 +1,269 @@ +"use client"; + +import { + type ColumnFiltersState, + type SortingState, + type VisibilityState, + type ColumnDef, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import React from "react"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, +} from "@/components/ui/dropdown-menu"; +import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; +import { Button } from "@/components/ui/button"; +import { ChevronDown } from "lucide-react"; +import { Input } from "@/components/ui/input"; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; + isLoading: boolean; +} + +export function DataTable({ + columns, + data, + isLoading, +}: DataTableProps) { + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState( + [] + ); + const [columnVisibility, setColumnVisibility] = + React.useState({}); + const [rowSelection, setRowSelection] = React.useState({}); + + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }); + + console.log("Data in DataTable", data); + + return ( +
+
+
+ + table.getColumn("Hostname")?.setFilterValue(event.target.value) + } + className="md:max-w-sm" + /> + + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {column.id} + + ); + })} + + +
+ {/* + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ); + })} + + ))} + + + {table?.getRowModel()?.rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + {isLoading ? ( +
+ + Loading... + +
+ ) : ( + <>No results. + )} +
+
+ )} +
+
*/} +
+ {isLoading ? ( +
+ + Loading... + +
+ ) : data?.length === 0 ? ( +
+ + No results. + +
+ ) : ( + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ); + })} + + ))} + + + {table?.getRowModel()?.rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + {isLoading ? ( +
+ + Loading... + +
+ ) : ( + <>No results. + )} +
+
+ )} +
+
+ )} +
+ {data && data?.length > 0 && ( +
+
+ + +
+
+ )} +
+
+ ); +} diff --git a/apps/dokploy/components/dashboard/swarm/show/show-nodes.tsx b/apps/dokploy/components/dashboard/swarm/show/show-nodes.tsx new file mode 100644 index 00000000..6c5bd99d --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/show/show-nodes.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { SwarmList, columns } from "./columns"; +import { DataTable } from "./data-table"; +import { api } from "@/utils/api"; + +function ShowSwarmNodes() { + const { data, isLoading } = api.swarm.getNodes.useQuery(); + + console.log(data); + + return ( + + ); +} + +export default ShowSwarmNodes; From 813da8f8110ecd3040bb5938c8dbdf0a22344a66 Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Tue, 17 Dec 2024 12:06:08 +0100 Subject: [PATCH 04/18] refactor: clean up code formatting and improve readability in swarm dashboard --- apps/dokploy/pages/dashboard/swarm.tsx | 117 ++++++++++++------------- 1 file changed, 58 insertions(+), 59 deletions(-) diff --git a/apps/dokploy/pages/dashboard/swarm.tsx b/apps/dokploy/pages/dashboard/swarm.tsx index d353ceed..b608efc0 100644 --- a/apps/dokploy/pages/dashboard/swarm.tsx +++ b/apps/dokploy/pages/dashboard/swarm.tsx @@ -1,4 +1,3 @@ -import { ShowSwarm } from "@/components/dashboard/swarm/show/node-list"; import ShowSwarmNodes from "@/components/dashboard/swarm/show/show-nodes"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { appRouter } from "@/server/api/root"; @@ -9,73 +8,73 @@ import React, { type ReactElement } from "react"; import superjson from "superjson"; const Dashboard = () => { - return ; + return ; }; export default Dashboard; Dashboard.getLayout = (page: ReactElement) => { - return {page}; + return {page}; }; export async function getServerSideProps( - ctx: GetServerSidePropsContext<{ serviceId: string }> + ctx: GetServerSidePropsContext<{ serviceId: string }>, ) { - if (IS_CLOUD) { - return { - redirect: { - permanent: true, - destination: "/dashboard/projects", - }, - }; - } - const { user, session } = await validateRequest(ctx.req, ctx.res); - if (!user) { - return { - redirect: { - permanent: true, - destination: "/", - }, - }; - } - const { req, res } = ctx; + if (IS_CLOUD) { + return { + redirect: { + permanent: true, + destination: "/dashboard/projects", + }, + }; + } + const { user, session } = await validateRequest(ctx.req, ctx.res); + if (!user) { + return { + redirect: { + permanent: true, + destination: "/", + }, + }; + } + const { req, res } = ctx; - const helpers = createServerSideHelpers({ - router: appRouter, - ctx: { - req: req as any, - res: res as any, - db: null as any, - session: session, - user: user, - }, - transformer: superjson, - }); - try { - await helpers.project.all.prefetch(); - const auth = await helpers.auth.get.fetch(); + const helpers = createServerSideHelpers({ + router: appRouter, + ctx: { + req: req as any, + res: res as any, + db: null as any, + session: session, + user: user, + }, + transformer: superjson, + }); + try { + await helpers.project.all.prefetch(); + const auth = await helpers.auth.get.fetch(); - if (auth.rol === "user") { - const user = await helpers.user.byAuthId.fetch({ - authId: auth.id, - }); + if (auth.rol === "user") { + const user = await helpers.user.byAuthId.fetch({ + authId: auth.id, + }); - if (!user.canAccessToDocker) { - return { - redirect: { - permanent: true, - destination: "/", - }, - }; - } - } - return { - props: { - trpcState: helpers.dehydrate(), - }, - }; - } catch (error) { - return { - props: {}, - }; - } + if (!user.canAccessToDocker) { + return { + redirect: { + permanent: true, + destination: "/", + }, + }; + } + } + return { + props: { + trpcState: helpers.dehydrate(), + }, + }; + } catch (error) { + return { + props: {}, + }; + } } From 3fc5bfc5c51a2d6e295d0ee7ccd029e4a4c94d0e Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Tue, 17 Dec 2024 12:11:43 +0100 Subject: [PATCH 05/18] feat: implement fetching of Docker service applications and their details --- packages/server/src/services/docker.ts | 53 +++++++------------------- 1 file changed, 14 insertions(+), 39 deletions(-) diff --git a/packages/server/src/services/docker.ts b/packages/server/src/services/docker.ts index 8681cb22..c13c71e5 100644 --- a/packages/server/src/services/docker.ts +++ b/packages/server/src/services/docker.ts @@ -265,59 +265,34 @@ export const getNodeInfo = async (nodeId: string) => { export const getNodeApplications = async () => { try { - // TODO: Implement this - // const { stdout, stderr } = await execAsync( - // `docker service ls --format '{{json .}}'` - // ); + const { stdout, stderr } = await execAsync( + `docker service ls --format '{{json .}}'`, + ); - const stdout = `{"ID":"pxvnj68dxom9","Image":"dokploy/dokploy:latest","Mode":"replicated","Name":"dokploy","Ports":"","Replicas":"1/1"} -{"ID":"1sweo6dr2vrn","Image":"postgres:16","Mode":"replicated","Name":"dokploy-postgres","Ports":"","Replicas":"1/1"} -{"ID":"tnl2fck3rbop","Image":"redis:7","Mode":"replicated","Name":"dokploy-redis","Ports":"","Replicas":"1/1"} -{"ID":"o9ady4y1p96x","Image":"traefik:v3.1.2","Mode":"replicated","Name":"dokploy-traefik","Ports":"","Replicas":"1/1"} -{"ID":"rsxe3l71h9y4","Image":"esports-manager-api-eg8t7w:latest","Mode":"replicated","Name":"esports-manager-api-eg8t7w","Ports":"","Replicas":"1/1"} -{"ID":"fw52vzcw5dc0","Image":"team-synix-admin-dvgspy:latest","Mode":"replicated","Name":"team-synix-admin-dvgspy","Ports":"","Replicas":"1/1"} -{"ID":"551bwmtd6b4t","Image":"team-synix-leaderboard-9vx8ca:latest","Mode":"replicated","Name":"team-synix-leaderboard-9vx8ca","Ports":"","Replicas":"1/1"} -{"ID":"h1eyg3g1tyn3","Image":"postgres:15","Mode":"replicated","Name":"team-synix-webpage-db-fkivnf","Ports":"","Replicas":"1/1"}`; - - // if (stderr) { - // console.error(`Error: ${stderr}`); - // return; - // } + if (stderr) { + console.error(`Error: ${stderr}`); + return; + } const appArray = stdout .trim() .split("\n") .map((line) => JSON.parse(line)); - console.log(appArray); return appArray; } catch (error) {} }; export const getApplicationInfo = async (appName: string) => { try { - // TODO: Implement this - // const { stdout, stderr } = await execAsync( - // `docker service ps ${appName} --format '{{json .}}'` - // ); + const { stdout, stderr } = await execAsync( + `docker service ps ${appName} --format '{{json .}}'`, + ); - const stdout = `{"CurrentState":"Running 2 weeks ago","DesiredState":"Running","Error":"","ID":"nx8jxlmb8niw","Image":"postgres:16","Name":"dokploy-postgres.1","Node":"v2202411192718297480","Ports":""} -{"CurrentState":"Running 2 weeks ago","DesiredState":"Running","Error":"","ID":"s288g9lwtvi4","Image":"redis:7","Name":"dokploy-redis.1","Node":"v2202411192718297480","Ports":""} -{"CurrentState":"Running 2 weeks ago","DesiredState":"Running","Error":"","ID":"2vcmejz51b23","Image":"traefik:v3.1.2","Name":"dokploy-traefik.1","Node":"v2202411192718297480","Ports":"*:80-\u003e80/tcp,*:80-\u003e80/tcp,*:443-\u003e443/tcp,*:443-\u003e443/tcp"} -{"CurrentState":"Running 26 hours ago","DesiredState":"Running","Error":"","ID":"79iatnbsm2um","Image":"dokploy/dokploy:latest","Name":"dokploy.1","Node":"v2202411192718297480","Ports":"*:3000-\u003e3000/tcp,*:3000-\u003e3000/tcp"} -{"CurrentState":"Shutdown 26 hours ago","DesiredState":"Shutdown","Error":"","ID":"zcwxs501zs7w","Image":"dokploy/dokploy:latest","Name":"dokploy.1","Node":"v2202411192718297480","Ports":""} -{"CurrentState":"Shutdown 7 days ago","DesiredState":"Shutdown","Error":"","ID":"t59qhkoenno4","Image":"dokploy/dokploy:latest","Name":"dokploy.1","Node":"v2202411192718297480","Ports":""} -{"CurrentState":"Shutdown 7 days ago","DesiredState":"Shutdown","Error":"","ID":"o5xtcuj6um7e","Image":"dokploy/dokploy:latest","Name":"dokploy.1","Node":"v2202411192718297480","Ports":""} -{"CurrentState":"Running 2 weeks ago","DesiredState":"Running","Error":"","ID":"q1lr9rmf452g","Image":"esports-manager-api-eg8t7w:latest","Name":"esports-manager-api-eg8t7w.1","Node":"v2202411192718297480","Ports":""} -{"CurrentState":"Shutdown 2 weeks ago","DesiredState":"Shutdown","Error":"","ID":"y9ixpg6b8qdo","Image":"esports-manager-api-eg8t7w:latest","Name":"esports-manager-api-eg8t7w.1","Node":"v2202411192718297480","Ports":""} -{"CurrentState":"Running 24 hours ago","DesiredState":"Running","Error":"","ID":"xgcb919qjg1a","Image":"team-synix-admin-dvgspy:latest","Name":"team-synix-admin-dvgspy.1","Node":"v2202411192718297480","Ports":""} -{"CurrentState":"Running 26 hours ago","DesiredState":"Running","Error":"","ID":"7yi95wh8zhh6","Image":"team-synix-leaderboard-9vx8ca:latest","Name":"team-synix-leaderboard-9vx8ca.1","Node":"v2202411192718297480","Ports":""} -{"CurrentState":"Running 2 weeks ago","DesiredState":"Running","Error":"","ID":"89yzsnghpbq6","Image":"postgres:15","Name":"team-synix-webpage-db-fkivnf.1","Node":"v2202411192718297480","Ports":""}`; - - // if (stderr) { - // console.error(`Error: ${stderr}`); - // return; - // } + if (stderr) { + console.error(`Error: ${stderr}`); + return; + } const appArray = stdout .trim() From 763219e859dc2112343aedb778e535aacb5960c6 Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Tue, 17 Dec 2024 12:12:48 +0100 Subject: [PATCH 06/18] refactor: streamline imports and improve code formatting in ShowSwarmNodes component --- .../dashboard/swarm/show/show-nodes.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/apps/dokploy/components/dashboard/swarm/show/show-nodes.tsx b/apps/dokploy/components/dashboard/swarm/show/show-nodes.tsx index 6c5bd99d..e629654f 100644 --- a/apps/dokploy/components/dashboard/swarm/show/show-nodes.tsx +++ b/apps/dokploy/components/dashboard/swarm/show/show-nodes.tsx @@ -1,16 +1,14 @@ -import React from "react"; -import { SwarmList, columns } from "./columns"; -import { DataTable } from "./data-table"; import { api } from "@/utils/api"; +import React from "react"; +import { columns } from "./columns"; +import { DataTable } from "./data-table"; function ShowSwarmNodes() { - const { data, isLoading } = api.swarm.getNodes.useQuery(); + const { data, isLoading } = api.swarm.getNodes.useQuery(); - console.log(data); - - return ( - - ); + return ( + + ); } export default ShowSwarmNodes; From f98f18b331cce7409ea0422b45f37ab28aa48c35 Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Tue, 17 Dec 2024 20:48:35 +0100 Subject: [PATCH 07/18] feat: add monitoring card --- .../dashboard/swarm/monitoring-card.tsx | 198 ++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 apps/dokploy/components/dashboard/swarm/monitoring-card.tsx diff --git a/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx new file mode 100644 index 00000000..88c96c24 --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx @@ -0,0 +1,198 @@ +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { api } from "@/utils/api"; +import { + Activity, + AlertCircle, + CheckCircle, + HelpCircle, + Loader2, + Server, +} from "lucide-react"; +import { NodeCard } from "./show/deatils-card"; + +export interface SwarmList { + ID: string; + Hostname: string; + Availability: string; + EngineVersion: string; + Status: string; + ManagerStatus: string; + TLSStatus: string; +} + +interface SwarmMonitorCardProps { + nodes: SwarmList[]; +} + +export default function SwarmMonitorCard() { + const { data: nodes, isLoading } = api.swarm.getNodes.useQuery(); + + if (isLoading) { + return ( +
+ + + + + Docker Swarm Monitor + + + +
+ +
+
+
+
+ ); + } + + if (!nodes) { + return ( + + + + + Docker Swarm Monitor + + + +
+ Failed to load data +
+
+
+ ); + } + + console.log(nodes); + const totalNodes = nodes.length; + const activeNodesCount = nodes.filter( + (node) => node.Status === "Ready", + ).length; + const managerNodesCount = nodes.filter( + (node) => + node.ManagerStatus === "Leader" || node.ManagerStatus === "Reachable", + ).length; + + const activeNodes = nodes.filter((node) => node.Status === "Ready"); + const managerNodes = nodes.filter( + (node) => + node.ManagerStatus === "Leader" || node.ManagerStatus === "Reachable", + ); + + const getStatusIcon = (status: string) => { + switch (status) { + case "Ready": + return ; + case "Down": + return ; + default: + return ; + } + }; + + return ( +
+ + + + + Docker Swarm Monitor + + + +
+
+ Total Nodes: + {totalNodes} +
+
+ Active Nodes: + + + + + {activeNodesCount} / {totalNodes} + + + + {activeNodes.map((node) => ( +
+ {getStatusIcon(node.Status)} + {node.Hostname} +
+ ))} +
+
+
+ {/* + {activeNodesCount} / {totalNodes} + */} +
+
+ Manager Nodes: + + + + + {managerNodesCount} / {totalNodes} + + + + {managerNodes.map((node) => ( +
+ {getStatusIcon(node.Status)} + {node.Hostname} +
+ ))} +
+
+
+ {/* + {managerNodes} / {totalNodes} + */} +
+
+

Node Status:

+
    + {nodes.map((node) => ( +
  • + + {getStatusIcon(node.Status)} + {node.Hostname} + + + {node.ManagerStatus || "Worker"} + +
  • + ))} +
+
+
+
+
+
+ {nodes.map((node) => ( + + ))} +
+
+ ); +} From e3ee89104bdd3b642a6d831316d20efcb77529e1 Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Tue, 17 Dec 2024 20:48:56 +0100 Subject: [PATCH 08/18] chore: remove tables and add new cards --- .../swarm/applications/data-table.tsx | 413 ++++++++---------- .../swarm/applications/show-applications.tsx | 191 ++++---- .../dashboard/swarm/containers/columns.tsx | 139 ++++++ .../dashboard/swarm/containers/data-table.tsx | 210 +++++++++ .../swarm/containers/show-container.tsx | 48 ++ .../dashboard/swarm/details/show-node.tsx | 94 ++-- .../dashboard/swarm/servers/columns.tsx | 168 +++++++ .../dashboard/swarm/servers/data-table.tsx | 210 +++++++++ .../dashboard/swarm/servers/show-server.tsx | 16 + .../dashboard/swarm/show/columns.tsx | 333 +++++++------- .../dashboard/swarm/show/deatils-card.tsx | 124 ++++++ .../components/layouts/navigation-tabs.tsx | 308 ++++++------- apps/dokploy/pages/dashboard/swarm.tsx | 19 +- 13 files changed, 1570 insertions(+), 703 deletions(-) create mode 100644 apps/dokploy/components/dashboard/swarm/containers/columns.tsx create mode 100644 apps/dokploy/components/dashboard/swarm/containers/data-table.tsx create mode 100644 apps/dokploy/components/dashboard/swarm/containers/show-container.tsx create mode 100644 apps/dokploy/components/dashboard/swarm/servers/columns.tsx create mode 100644 apps/dokploy/components/dashboard/swarm/servers/data-table.tsx create mode 100644 apps/dokploy/components/dashboard/swarm/servers/show-server.tsx create mode 100644 apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx diff --git a/apps/dokploy/components/dashboard/swarm/applications/data-table.tsx b/apps/dokploy/components/dashboard/swarm/applications/data-table.tsx index 1b192f7d..03915c19 100644 --- a/apps/dokploy/components/dashboard/swarm/applications/data-table.tsx +++ b/apps/dokploy/components/dashboard/swarm/applications/data-table.tsx @@ -1,156 +1,160 @@ "use client"; import { - type ColumnFiltersState, - type SortingState, - type VisibilityState, - type ColumnDef, - flexRender, - getCoreRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, + type ColumnDef, + type ColumnFiltersState, + type SortingState, + type VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, } from "@tanstack/react-table"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import React from "react"; -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, -} from "@/components/ui/dropdown-menu"; -import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; import { Button } from "@/components/ui/button"; -import { ChevronDown } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, +} from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; +import { ChevronDown } from "lucide-react"; +import React from "react"; interface DataTableProps { - columns: ColumnDef[]; - data: TData[]; + columns: ColumnDef[]; + data: TData[]; } export function DataTable({ - columns, - data, + columns, + data, }: DataTableProps) { - const [sorting, setSorting] = React.useState([]); - const [columnFilters, setColumnFilters] = React.useState( - [] - ); - const [columnVisibility, setColumnVisibility] = - React.useState({}); - const [rowSelection, setRowSelection] = React.useState({}); + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState( + [], + ); + const [columnVisibility, setColumnVisibility] = + React.useState({}); + const [rowSelection, setRowSelection] = React.useState({}); + const [pagination, setPagination] = React.useState({ + pageIndex: 0, //initial page index + pageSize: 8, //default page size + }); - const table = useReactTable({ - data, - columns, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - onColumnVisibilityChange: setColumnVisibility, - onRowSelectionChange: setRowSelection, - state: { - sorting, - columnFilters, - columnVisibility, - rowSelection, - }, - }); + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }); - return ( -
-
-
- - table.getColumn("Name")?.setFilterValue(event.target.value) - } - className="md:max-w-sm" - /> - - - - - - {table - .getAllColumns() - .filter((column) => column.getCanHide()) - .map((column) => { - return ( - - column.toggleVisibility(!!value) - } - > - {column.id} - - ); - })} - - -
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ); - })} - - ))} - - - {table?.getRowModel()?.rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - )) - ) : ( - - - No results. - {/* {isLoading ? ( + return ( +
+
+
+ + table.getColumn("Name")?.setFilterValue(event.target.value) + } + className="md:max-w-sm" + /> + + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {column.id} + + ); + })} + + +
+
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table?.getRowModel()?.rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + No results. + {/* {isLoading ? (
Loading... @@ -159,106 +163,35 @@ export function DataTable({ ) : ( <>No results. )} */} - - - )} - -
- {/*
- {isLoading ? ( -
- - Loading... - -
- ) : data?.length === 0 ? ( -
- - No results. - -
- ) : ( - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ); - })} - - ))} - - - {table?.getRowModel()?.rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - )) - ) : ( - - - {isLoading ? ( -
- - Loading... - -
- ) : ( - <>No results. - )} -
-
- )} -
-
- )} -
*/} - {data && data?.length > 0 && ( -
-
- - -
-
- )} -
-
- ); + + + )} + + + + {data && data?.length > 0 && ( +
+
+ + +
+
+ )} +
+
+ ); } diff --git a/apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx b/apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx index 2ef632b9..4363adc1 100644 --- a/apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx +++ b/apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx @@ -1,122 +1,117 @@ -import React from "react"; +import { Button } from "@/components/ui/button"; import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, } from "@/components/ui/dialog"; -import { api } from "@/utils/api"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; -import { DataTable } from "./data-table"; +import { api } from "@/utils/api"; +import { Layers, LoaderIcon } from "lucide-react"; +import React from "react"; import { columns } from "./columns"; -import { LoaderIcon } from "lucide-react"; +import { DataTable } from "./data-table"; interface Props { - nodeName: string; + nodeName: string; } interface ApplicationList { - ID: string; - Image: string; - Mode: string; - Name: string; - Ports: string; - Replicas: string; - CurrentState: string; - DesiredState: string; - Error: string; - Node: string; + ID: string; + Image: string; + Mode: string; + Name: string; + Ports: string; + Replicas: string; + CurrentState: string; + DesiredState: string; + Error: string; + Node: string; } const ShowNodeApplications = ({ nodeName }: Props) => { - const [loading, setLoading] = React.useState(true); - const { data: NodeApps, isLoading: NodeAppsLoading } = - api.swarm.getNodeApps.useQuery(); + const [loading, setLoading] = React.useState(true); + const { data: NodeApps, isLoading: NodeAppsLoading } = + api.swarm.getNodeApps.useQuery(); - let applicationList = ""; + let applicationList = ""; - if (NodeApps && NodeApps.length > 0) { - applicationList = NodeApps.map((app) => app.Name).join(" "); - } + if (NodeApps && NodeApps.length > 0) { + applicationList = NodeApps.map((app) => app.Name).join(" "); + } - const { data: NodeAppDetails, isLoading: NodeAppDetailsLoading } = - api.swarm.getAppInfos.useQuery({ appName: applicationList }); + const { data: NodeAppDetails, isLoading: NodeAppDetailsLoading } = + api.swarm.getAppInfos.useQuery({ appName: applicationList }); - if (NodeAppsLoading || NodeAppDetailsLoading) { - return ( - - - e.preventDefault()} - > - - - - - ); - } + if (NodeAppsLoading || NodeAppDetailsLoading) { + return ( + + + + + + ); + } - if (!NodeApps || !NodeAppDetails) { - return
No data found
; - } + if (!NodeApps || !NodeAppDetails) { + return
No data found
; + } - const combinedData: ApplicationList[] = NodeApps.flatMap((app) => { - const appDetails = - NodeAppDetails?.filter((detail) => - detail.Name.startsWith(`${app.Name}.`) - ) || []; + const combinedData: ApplicationList[] = NodeApps.flatMap((app) => { + const appDetails = + NodeAppDetails?.filter((detail) => + detail.Name.startsWith(`${app.Name}.`), + ) || []; - if (appDetails.length === 0) { - return [ - { - ...app, - CurrentState: "N/A", - DesiredState: "N/A", - Error: "", - Node: "N/A", - Ports: app.Ports, - }, - ]; - } + if (appDetails.length === 0) { + return [ + { + ...app, + CurrentState: "N/A", + DesiredState: "N/A", + Error: "", + Node: "N/A", + Ports: app.Ports, + }, + ]; + } - return appDetails.map((detail) => ({ - ...app, - CurrentState: detail.CurrentState, - DesiredState: detail.DesiredState, - Error: detail.Error, - Node: detail.Node, - Ports: detail.Ports || app.Ports, - })); - }); + return appDetails.map((detail) => ({ + ...app, + CurrentState: detail.CurrentState, + DesiredState: detail.DesiredState, + Error: detail.Error, + Node: detail.Node, + Ports: detail.Ports || app.Ports, + })); + }); - return ( - - - e.preventDefault()} - > - Show Applications - - - - - Node Applications - - See in detail the applications running on this node - - -
- -
- {/*
*/} -
-
- ); + return ( + + + + + + + Node Applications + + See in detail the applications running on this node + + +
+ +
+ {/*
*/} +
+
+ ); }; export default ShowNodeApplications; diff --git a/apps/dokploy/components/dashboard/swarm/containers/columns.tsx b/apps/dokploy/components/dashboard/swarm/containers/columns.tsx new file mode 100644 index 00000000..0ccf8e37 --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/containers/columns.tsx @@ -0,0 +1,139 @@ +import type { ColumnDef } from "@tanstack/react-table"; +import { ArrowUpDown, MoreHorizontal } from "lucide-react"; +import * as React from "react"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +import type { Badge } from "@/components/ui/badge"; +import { ShowNodeConfig } from "../details/show-node"; +// import { ShowContainerConfig } from "../config/show-container-config"; +// import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs"; +// import { DockerTerminalModal } from "../terminal/docker-terminal-modal"; +// import type { Container } from "./show-containers"; + +export interface ContainerList { + containerId: string; + name: string; + image: string; + ports: string; + state: string; + status: string; + serverId: string | null | undefined; +} + +export const columns: ColumnDef[] = [ + { + accessorKey: "ID", + accessorFn: (row) => row.containerId, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("containerId")}
; + }, + }, + { + accessorKey: "Name", + accessorFn: (row) => row.name, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("name")}
; + }, + }, + { + accessorKey: "Image", + accessorFn: (row) => row.image, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("image")}
; + }, + }, + { + accessorKey: "Ports", + accessorFn: (row) => row.ports, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("ports")}
; + }, + }, + { + accessorKey: "State", + accessorFn: (row) => row.state, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("state")}
; + }, + }, + { + accessorKey: "Status", + accessorFn: (row) => row.status, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("status")}
; + }, + }, +]; diff --git a/apps/dokploy/components/dashboard/swarm/containers/data-table.tsx b/apps/dokploy/components/dashboard/swarm/containers/data-table.tsx new file mode 100644 index 00000000..95b4498e --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/containers/data-table.tsx @@ -0,0 +1,210 @@ +"use client"; + +import { + type ColumnDef, + type ColumnFiltersState, + type SortingState, + type VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, +} from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; +import { ChevronDown } from "lucide-react"; +import React from "react"; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; + isLoading: boolean; +} + +export function DataTable({ + columns, + data, + isLoading, +}: DataTableProps) { + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState( + [], + ); + const [columnVisibility, setColumnVisibility] = + React.useState({}); + const [rowSelection, setRowSelection] = React.useState({}); + + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }); + + return ( +
+
+
+ + table.getColumn("Name")?.setFilterValue(event.target.value) + } + className="md:max-w-sm" + /> + + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {column.id} + + ); + })} + + +
+ +
+ {isLoading ? ( +
+ + Loading... + +
+ ) : data?.length === 0 ? ( +
+ + No results. + +
+ ) : ( + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table?.getRowModel()?.rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + {isLoading ? ( +
+ + Loading... + +
+ ) : ( + <>No results. + )} +
+
+ )} +
+
+ )} +
+ {data && data?.length > 0 && ( +
+
+ + +
+
+ )} +
+
+ ); +} diff --git a/apps/dokploy/components/dashboard/swarm/containers/show-container.tsx b/apps/dokploy/components/dashboard/swarm/containers/show-container.tsx new file mode 100644 index 00000000..dec6c6e7 --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/containers/show-container.tsx @@ -0,0 +1,48 @@ +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; +import { api } from "@/utils/api"; +import React from "react"; +import { ShowContainers } from "../../docker/show/show-containers"; +import { columns } from "./columns"; +import { DataTable } from "./data-table"; +// import { columns } from "./columns"; +// import { DataTable } from "./data-table"; + +interface Props { + serverId: string; +} + +const ShowNodeContainers = ({ serverId }: Props) => { + return ( + + + e.preventDefault()} + > + Show Container + + + + + Node Container + + See all containers running on this node + + +
+ +
+
+
+ ); +}; + +export default ShowNodeContainers; diff --git a/apps/dokploy/components/dashboard/swarm/details/show-node.tsx b/apps/dokploy/components/dashboard/swarm/details/show-node.tsx index 9a092152..4f751805 100644 --- a/apps/dokploy/components/dashboard/swarm/details/show-node.tsx +++ b/apps/dokploy/components/dashboard/swarm/details/show-node.tsx @@ -1,54 +1,60 @@ -import React from "react"; +import { CodeEditor } from "@/components/shared/code-editor"; +import { Button } from "@/components/ui/button"; import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, } from "@/components/ui/dialog"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; -import { CodeEditor } from "@/components/shared/code-editor"; import { api } from "@/utils/api"; +import { Settings } from "lucide-react"; +import React from "react"; interface Props { - nodeId: string; + nodeId: string; } export const ShowNodeConfig = ({ nodeId }: Props) => { - const { data, isLoading } = api.swarm.getNodeInfo.useQuery({ nodeId }); - return ( - - - e.preventDefault()} - > - View Config - - - - - Node Config - - See in detail the metadata of this node - - -
- -
-              {/* {JSON.stringify(data, null, 2)} */}
-              
-            
-
-
-
-
- ); + const { data, isLoading } = api.swarm.getNodeInfo.useQuery({ nodeId }); + return ( + + + {/* e.preventDefault()} + > + Show Config + */} + + + + + Node Config + + See in detail the metadata of this node + + +
+ +
+							{/* {JSON.stringify(data, null, 2)} */}
+							
+						
+
+
+
+
+ ); }; diff --git a/apps/dokploy/components/dashboard/swarm/servers/columns.tsx b/apps/dokploy/components/dashboard/swarm/servers/columns.tsx new file mode 100644 index 00000000..02b013db --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/servers/columns.tsx @@ -0,0 +1,168 @@ +import type { ColumnDef } from "@tanstack/react-table"; +import { ArrowUpDown, MoreHorizontal } from "lucide-react"; +import * as React from "react"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +import type { Badge } from "@/components/ui/badge"; +import { ShowContainers } from "../../docker/show/show-containers"; +import ShowNodeContainers from "../containers/show-container"; +import { ShowNodeConfig } from "../details/show-node"; +// import { ShowContainerConfig } from "../config/show-container-config"; +// import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs"; +// import { DockerTerminalModal } from "../terminal/docker-terminal-modal"; +// import type { Container } from "./show-containers"; + +export interface ServerList { + totalSum: number; + 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; +} + +export const columns: ColumnDef[] = [ + { + accessorKey: "serverId", + accessorFn: (row) => row.serverId, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("serverId")}
; + }, + }, + { + accessorKey: "name", + accessorFn: (row) => row.name, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("name")}
; + }, + }, + { + accessorKey: "ipAddress", + accessorFn: (row) => row.ipAddress, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("ipAddress")}
; + }, + }, + { + accessorKey: "port", + accessorFn: (row) => row.port, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("port")}
; + }, + }, + { + accessorKey: "username", + accessorFn: (row) => row.username, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("username")}
; + }, + }, + { + accessorKey: "createdAt", + accessorFn: (row) => row.createdAt, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("createdAt")}
; + }, + }, + { + id: "actions", + enableHiding: false, + cell: ({ row }) => { + return ( + + + + + + Actions + + + + ); + }, + }, +]; diff --git a/apps/dokploy/components/dashboard/swarm/servers/data-table.tsx b/apps/dokploy/components/dashboard/swarm/servers/data-table.tsx new file mode 100644 index 00000000..95b4498e --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/servers/data-table.tsx @@ -0,0 +1,210 @@ +"use client"; + +import { + type ColumnDef, + type ColumnFiltersState, + type SortingState, + type VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, +} from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; +import { ChevronDown } from "lucide-react"; +import React from "react"; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; + isLoading: boolean; +} + +export function DataTable({ + columns, + data, + isLoading, +}: DataTableProps) { + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState( + [], + ); + const [columnVisibility, setColumnVisibility] = + React.useState({}); + const [rowSelection, setRowSelection] = React.useState({}); + + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }); + + return ( +
+
+
+ + table.getColumn("Name")?.setFilterValue(event.target.value) + } + className="md:max-w-sm" + /> + + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {column.id} + + ); + })} + + +
+ +
+ {isLoading ? ( +
+ + Loading... + +
+ ) : data?.length === 0 ? ( +
+ + No results. + +
+ ) : ( + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table?.getRowModel()?.rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + {isLoading ? ( +
+ + Loading... + +
+ ) : ( + <>No results. + )} +
+
+ )} +
+
+ )} +
+ {data && data?.length > 0 && ( +
+
+ + +
+
+ )} +
+
+ ); +} diff --git a/apps/dokploy/components/dashboard/swarm/servers/show-server.tsx b/apps/dokploy/components/dashboard/swarm/servers/show-server.tsx new file mode 100644 index 00000000..0486b164 --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/servers/show-server.tsx @@ -0,0 +1,16 @@ +import { api } from "@/utils/api"; +import React from "react"; +import { columns } from "./columns"; +import { DataTable } from "./data-table"; + +function ShowApplicationServers() { + const { data, isLoading } = api.server.all.useQuery(); + + console.log(data); + + return ( + + ); +} + +export default ShowApplicationServers; diff --git a/apps/dokploy/components/dashboard/swarm/show/columns.tsx b/apps/dokploy/components/dashboard/swarm/show/columns.tsx index ba11d749..b0774936 100644 --- a/apps/dokploy/components/dashboard/swarm/show/columns.tsx +++ b/apps/dokploy/components/dashboard/swarm/show/columns.tsx @@ -4,180 +4,181 @@ import * as React from "react"; import { Button } from "@/components/ui/button"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuLabel, - DropdownMenuTrigger, + DropdownMenu, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Badge } from "@/components/ui/badge"; -import { ShowNodeConfig } from "../details/show-node"; import ShowNodeApplications from "../applications/show-applications"; +import ShowContainers from "../containers/show-container"; +import { ShowNodeConfig } from "../details/show-node"; // import { ShowContainerConfig } from "../config/show-container-config"; // import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs"; // import { DockerTerminalModal } from "../terminal/docker-terminal-modal"; // import type { Container } from "./show-containers"; export interface SwarmList { - ID: string; - Hostname: string; - Availability: string; - EngineVersion: string; - Status: string; - ManagerStatus: string; - TLSStatus: string; + ID: string; + Hostname: string; + Availability: string; + EngineVersion: string; + Status: string; + ManagerStatus: string; + TLSStatus: string; } export const columns: ColumnDef[] = [ - { - accessorKey: "ID", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("ID")}
; - }, - }, - { - accessorKey: "EngineVersion", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("EngineVersion")}
; - }, - }, - { - accessorKey: "Hostname", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("Hostname")}
; - }, - }, - // { - // accessorKey: "Status", - // header: ({ column }) => { - // return ( - // - // ); - // }, - // cell: ({ row }) => { - // const value = row.getValue("status") as string; - // return ( - //
- // - // {value} - // - //
- // ); - // }, - // }, - { - accessorKey: "Availability", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - const value = row.getValue("Availability") as string; - return ( -
- - {value} - -
- ); - }, - }, - { - accessorKey: "ManagerStatus", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => ( -
{row.getValue("ManagerStatus")}
- ), - }, - { - id: "actions", - enableHiding: false, - cell: ({ row }) => { - return ( - - - - - - Actions - - - {/* { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("ID")}
; + }, + }, + { + accessorKey: "EngineVersion", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("EngineVersion")}
; + }, + }, + { + accessorKey: "Hostname", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("Hostname")}
; + }, + }, + // { + // accessorKey: "Status", + // header: ({ column }) => { + // return ( + // + // ); + // }, + // cell: ({ row }) => { + // const value = row.getValue("status") as string; + // return ( + //
+ // + // {value} + // + //
+ // ); + // }, + // }, + { + accessorKey: "Availability", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const value = row.getValue("Availability") as string; + return ( +
+ + {value} + +
+ ); + }, + }, + { + accessorKey: "ManagerStatus", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => ( +
{row.getValue("ManagerStatus")}
+ ), + }, + { + id: "actions", + enableHiding: false, + cell: ({ row }) => { + return ( + + + + + + Actions + + + {/* @@ -193,9 +194,9 @@ export const columns: ColumnDef[] = [ > Terminal */} - - - ); - }, - }, +
+
+ ); + }, + }, ]; diff --git a/apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx b/apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx new file mode 100644 index 00000000..1d12ab52 --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx @@ -0,0 +1,124 @@ +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + AlertCircle, + CheckCircle, + HelpCircle, + Layers, + Settings, +} from "lucide-react"; +import { useState } from "react"; +import ShowNodeApplications from "../applications/show-applications"; +import { ShowNodeConfig } from "../details/show-node"; + +export interface SwarmList { + ID: string; + Hostname: string; + Availability: string; + EngineVersion: string; + Status: string; + ManagerStatus: string; + TLSStatus: string; +} + +interface NodeCardProps { + node: SwarmList; +} + +export function NodeCard({ node }: NodeCardProps) { + const [showConfig, setShowConfig] = useState(false); + const [showServices, setShowServices] = useState(false); + + const getStatusIcon = (status: string) => { + switch (status) { + case "Ready": + return ; + case "Down": + return ; + default: + return ; + } + }; + + return ( + + + + + {getStatusIcon(node.Status)} + {node.Hostname} + + + {node.ManagerStatus || "Worker"} + + + + +
+
+ Status: + {node.Status} +
+
+ Availability: + {node.Availability} +
+
+ Engine Version: + {node.EngineVersion} +
+
+ TLS Status: + {node.TLSStatus} +
+
+
+ + {/* + + + + + + Node Configuration + +
+
+									{JSON.stringify(node, null, 2)}
+								
+
+
+
*/} + + {/* + + + + + + Node Services + +
+

Service information would be displayed here.

+
+
+
*/} +
+
+
+ ); +} diff --git a/apps/dokploy/components/layouts/navigation-tabs.tsx b/apps/dokploy/components/layouts/navigation-tabs.tsx index c9392b10..46e590a7 100644 --- a/apps/dokploy/components/layouts/navigation-tabs.tsx +++ b/apps/dokploy/components/layouts/navigation-tabs.tsx @@ -7,176 +7,176 @@ import { useEffect, useMemo, useState } from "react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; interface TabInfo { - label: string; - tabLabel?: string; - description: string; - index: string; - type: TabState; - isShow?: ({ rol, user }: { rol?: Auth["rol"]; user?: User }) => boolean; + label: string; + tabLabel?: string; + description: string; + index: string; + type: TabState; + isShow?: ({ rol, user }: { rol?: Auth["rol"]; user?: User }) => boolean; } export type TabState = - | "projects" - | "monitoring" - | "settings" - | "traefik" - | "requests" - | "docker" - | "swarm"; + | "projects" + | "monitoring" + | "settings" + | "traefik" + | "requests" + | "docker" + | "swarm"; const getTabMaps = (isCloud: boolean) => { - const elements: TabInfo[] = [ - { - label: "Projects", - description: "Manage your projects", - index: "/dashboard/projects", - type: "projects", - }, - ]; + const elements: TabInfo[] = [ + { + label: "Projects", + description: "Manage your projects", + index: "/dashboard/projects", + type: "projects", + }, + ]; - if (!isCloud) { - elements.push( - { - label: "Monitoring", - description: "Monitor your projects", - index: "/dashboard/monitoring", - type: "monitoring", - }, - { - label: "Traefik", - tabLabel: "Traefik File System", - description: "Manage your traefik", - index: "/dashboard/traefik", - isShow: ({ rol, user }) => { - return Boolean(rol === "admin" || user?.canAccessToTraefikFiles); - }, - type: "traefik", - }, - { - label: "Docker", - description: "Manage your docker", - index: "/dashboard/docker", - isShow: ({ rol, user }) => { - return Boolean(rol === "admin" || user?.canAccessToDocker); - }, - type: "docker", - }, - { - label: "Swarm", - description: "Manage your docker swarm", - index: "/dashboard/swarm", - isShow: ({ rol, user }) => { - return Boolean(rol === "admin" || user?.canAccessToDocker); - }, - type: "swarm", - }, - { - label: "Requests", - description: "Manage your requests", - index: "/dashboard/requests", - isShow: ({ rol, user }) => { - return Boolean(rol === "admin" || user?.canAccessToDocker); - }, - type: "requests", - } - ); - } + if (!isCloud) { + elements.push( + { + label: "Monitoring", + description: "Monitor your projects", + index: "/dashboard/monitoring", + type: "monitoring", + }, + { + label: "Traefik", + tabLabel: "Traefik File System", + description: "Manage your traefik", + index: "/dashboard/traefik", + isShow: ({ rol, user }) => { + return Boolean(rol === "admin" || user?.canAccessToTraefikFiles); + }, + type: "traefik", + }, + { + label: "Docker", + description: "Manage your docker", + index: "/dashboard/docker", + isShow: ({ rol, user }) => { + return Boolean(rol === "admin" || user?.canAccessToDocker); + }, + type: "docker", + }, + { + label: "Swarm & Server", + description: "Manage your docker swarm and Servers", + index: "/dashboard/swarm", + isShow: ({ rol, user }) => { + return Boolean(rol === "admin" || user?.canAccessToDocker); + }, + type: "swarm", + }, + { + label: "Requests", + description: "Manage your requests", + index: "/dashboard/requests", + isShow: ({ rol, user }) => { + return Boolean(rol === "admin" || user?.canAccessToDocker); + }, + type: "requests", + }, + ); + } - elements.push({ - label: "Settings", - description: "Manage your settings", - type: "settings", - index: isCloud - ? "/dashboard/settings/profile" - : "/dashboard/settings/server", - }); + elements.push({ + label: "Settings", + description: "Manage your settings", + type: "settings", + index: isCloud + ? "/dashboard/settings/profile" + : "/dashboard/settings/server", + }); - return elements; + return elements; }; interface Props { - tab: TabState; - children: React.ReactNode; + tab: TabState; + children: React.ReactNode; } export const NavigationTabs = ({ tab, children }: Props) => { - const router = useRouter(); - const { data } = api.auth.get.useQuery(); - const [activeTab, setActiveTab] = useState(tab); - const { data: isCloud } = api.settings.isCloud.useQuery(); - const tabMap = useMemo(() => getTabMaps(isCloud ?? false), [isCloud]); - const { data: user } = api.user.byAuthId.useQuery( - { - authId: data?.id || "", - }, - { - enabled: !!data?.id && data?.rol === "user", - } - ); + const router = useRouter(); + const { data } = api.auth.get.useQuery(); + const [activeTab, setActiveTab] = useState(tab); + const { data: isCloud } = api.settings.isCloud.useQuery(); + const tabMap = useMemo(() => getTabMaps(isCloud ?? false), [isCloud]); + const { data: user } = api.user.byAuthId.useQuery( + { + authId: data?.id || "", + }, + { + enabled: !!data?.id && data?.rol === "user", + }, + ); - useEffect(() => { - setActiveTab(tab); - }, [tab]); + useEffect(() => { + setActiveTab(tab); + }, [tab]); - const activeTabInfo = useMemo(() => { - return tabMap.find((tab) => tab.type === activeTab); - }, [activeTab]); + const activeTabInfo = useMemo(() => { + return tabMap.find((tab) => tab.type === activeTab); + }, [activeTab]); - return ( -
-
-
-

- {activeTabInfo?.label} -

-

- {activeTabInfo?.description} -

-
- {tab === "projects" && - (data?.rol === "admin" || user?.canCreateProjects) && } -
-
- { - setActiveTab(e as TabState); - const tab = tabMap.find((tab) => tab.type === e); - router.push(tab?.index || ""); - }} - > -
- - {tabMap.map((tab, index) => { - if (tab?.isShow && !tab?.isShow?.({ rol: data?.rol, user })) { - return null; - } - return ( - - - {tab?.tabLabel || tab?.label} - - {tab.type === activeTab && ( -
-
-
- )} - - ); - })} - -
+ return ( +
+
+
+

+ {activeTabInfo?.label} +

+

+ {activeTabInfo?.description} +

+
+ {tab === "projects" && + (data?.rol === "admin" || user?.canCreateProjects) && } +
+
+ { + setActiveTab(e as TabState); + const tab = tabMap.find((tab) => tab.type === e); + router.push(tab?.index || ""); + }} + > +
+ + {tabMap.map((tab, index) => { + if (tab?.isShow && !tab?.isShow?.({ rol: data?.rol, user })) { + return null; + } + return ( + + + {tab?.tabLabel || tab?.label} + + {tab.type === activeTab && ( +
+
+
+ )} + + ); + })} + +
- - {children} - - -
-
- ); + + {children} + + +
+
+ ); }; diff --git a/apps/dokploy/pages/dashboard/swarm.tsx b/apps/dokploy/pages/dashboard/swarm.tsx index b608efc0..11035da7 100644 --- a/apps/dokploy/pages/dashboard/swarm.tsx +++ b/apps/dokploy/pages/dashboard/swarm.tsx @@ -1,6 +1,11 @@ +import { ShowServers } from "@/components/dashboard/settings/servers/show-servers"; +import SwarmMonitorCard from "@/components/dashboard/swarm/monitoring-card"; +import ShowApplicationServers from "@/components/dashboard/swarm/servers/show-server"; import ShowSwarmNodes from "@/components/dashboard/swarm/show/show-nodes"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; +import { Separator } from "@/components/ui/separator"; import { appRouter } from "@/server/api/root"; +import { api } from "@/utils/api"; import { IS_CLOUD, validateRequest } from "@dokploy/server"; import { createServerSideHelpers } from "@trpc/react-query/server"; import type { GetServerSidePropsContext } from "next"; @@ -8,7 +13,19 @@ import React, { type ReactElement } from "react"; import superjson from "superjson"; const Dashboard = () => { - return ; + return ( + <> +
+ +
+ + {/*

Swarm Nodes

+ + +

Server Nodes

+ */} + + ); }; export default Dashboard; From 3080926a505e8fbf9d54e950b447de29ed850725 Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Tue, 17 Dec 2024 21:07:30 +0100 Subject: [PATCH 09/18] feat: add new items --- .../dashboard/swarm/show/deatils-card.tsx | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx b/apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx index 1d12ab52..b622ba82 100644 --- a/apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx +++ b/apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx @@ -8,11 +8,13 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; +import { api } from "@/utils/api"; import { AlertCircle, CheckCircle, HelpCircle, Layers, + LoaderIcon, Settings, } from "lucide-react"; import { useState } from "react"; @@ -37,6 +39,10 @@ export function NodeCard({ node }: NodeCardProps) { const [showConfig, setShowConfig] = useState(false); const [showServices, setShowServices] = useState(false); + const { data, isLoading } = api.swarm.getNodeInfo.useQuery({ + nodeId: node.ID, + }); + const getStatusIcon = (status: string) => { switch (status) { case "Ready": @@ -48,6 +54,30 @@ export function NodeCard({ node }: NodeCardProps) { } }; + if (isLoading) { + return ( + + + + + {getStatusIcon(node.Status)} + {node.Hostname} + + + {node.ManagerStatus || "Worker"} + + + + +
+ +
+
+
+ ); + } + + console.log(data); return ( @@ -67,6 +97,14 @@ export function NodeCard({ node }: NodeCardProps) { Status: {node.Status}
+
+ IP Address: + {isLoading ? ( + + ) : ( + {data.Status.Addr} + )} +
Availability: {node.Availability} @@ -75,6 +113,29 @@ export function NodeCard({ node }: NodeCardProps) { Engine Version: {node.EngineVersion}
+
+ CPU: + {isLoading ? ( + + ) : ( + + {(data.Description.Resources.NanoCPUs / 1e9).toFixed(2)} GHz + + )} +
+
+ Memory: + {isLoading ? ( + + ) : ( + + {(data.Description.Resources.MemoryBytes / 1024 ** 3).toFixed( + 2, + )}{" "} + GB + + )} +
TLS Status: {node.TLSStatus} From be237ae4cf0ff10f4fb1eb53b62d180b8ea6db3a Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Tue, 17 Dec 2024 21:07:47 +0100 Subject: [PATCH 10/18] chore: comment out refresh for now --- .../dashboard/swarm/monitoring-card.tsx | 89 ++++++++++--------- 1 file changed, 49 insertions(+), 40 deletions(-) diff --git a/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx index 88c96c24..f55ef07b 100644 --- a/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx +++ b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx @@ -102,14 +102,25 @@ export default function SwarmMonitorCard() { return (
- + Docker Swarm Monitor + {/* */} -
+
Total Nodes: {totalNodes} @@ -121,24 +132,23 @@ export default function SwarmMonitorCard() { {activeNodesCount} / {totalNodes} - {activeNodes.map((node) => ( -
- {getStatusIcon(node.Status)} - {node.Hostname} -
- ))} +
+ {activeNodes.map((node) => ( +
+ {getStatusIcon(node.Status)} + {node.Hostname} +
+ ))} +
- {/* - {activeNodesCount} / {totalNodes} - */}
Manager Nodes: @@ -153,39 +163,38 @@ export default function SwarmMonitorCard() { - {managerNodes.map((node) => ( -
- {getStatusIcon(node.Status)} - {node.Hostname} -
- ))} +
+ {managerNodes.map((node) => ( +
+ {getStatusIcon(node.Status)} + {node.Hostname} +
+ ))} +
- {/* - {managerNodes} / {totalNodes} - */} -
-
-

Node Status:

-
    - {nodes.map((node) => ( -
  • - - {getStatusIcon(node.Status)} - {node.Hostname} - - - {node.ManagerStatus || "Worker"} - -
  • - ))} -
+
+

Node Status:

+
    + {nodes.map((node) => ( +
  • + + {getStatusIcon(node.Status)} + {node.Hostname} + + + {node.ManagerStatus || "Worker"} + +
  • + ))} +
+
From 577b126e66046deaace6a8899fe9eff38c90c1cb Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Tue, 17 Dec 2024 21:19:38 +0100 Subject: [PATCH 11/18] feat: make bg transparent --- apps/dokploy/components/dashboard/swarm/monitoring-card.tsx | 4 +++- apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx index f55ef07b..16b6102a 100644 --- a/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx +++ b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx @@ -94,6 +94,8 @@ export default function SwarmMonitorCard() { return ; case "Down": return ; + case "Disconnected": + return ; default: return ; } @@ -101,7 +103,7 @@ export default function SwarmMonitorCard() { return (
- + diff --git a/apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx b/apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx index b622ba82..83d226a5 100644 --- a/apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx +++ b/apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx @@ -56,7 +56,7 @@ export function NodeCard({ node }: NodeCardProps) { if (isLoading) { return ( - + @@ -79,7 +79,7 @@ export function NodeCard({ node }: NodeCardProps) { console.log(data); return ( - + From 752c9f28185e6c39bb9612184b26f8ad7e291aaf Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Tue, 17 Dec 2024 21:35:32 +0100 Subject: [PATCH 12/18] style: remove bg and border --- .../components/dashboard/swarm/containers/show-container.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/swarm/containers/show-container.tsx b/apps/dokploy/components/dashboard/swarm/containers/show-container.tsx index dec6c6e7..4d6582aa 100644 --- a/apps/dokploy/components/dashboard/swarm/containers/show-container.tsx +++ b/apps/dokploy/components/dashboard/swarm/containers/show-container.tsx @@ -37,7 +37,7 @@ const ShowNodeContainers = ({ serverId }: Props) => { See all containers running on this node -
+
From 9d497142db89d2571ef81a778eb5cd866d2a005d Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Wed, 18 Dec 2024 08:43:07 +0100 Subject: [PATCH 13/18] feat: add latest cards --- .../dashboard/swarm/server-card.tsx | 122 ++++++++++++++++++ .../swarm/servers/servers-overview.tsx | 24 ++++ apps/dokploy/pages/dashboard/swarm.tsx | 5 +- 3 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 apps/dokploy/components/dashboard/swarm/server-card.tsx create mode 100644 apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx diff --git a/apps/dokploy/components/dashboard/swarm/server-card.tsx b/apps/dokploy/components/dashboard/swarm/server-card.tsx new file mode 100644 index 00000000..10029114 --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/server-card.tsx @@ -0,0 +1,122 @@ +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 { useState } from "react"; +import { ShowContainers } from "../docker/show/show-containers"; +// import type { Server } from "../types/server"; +// import { ShowServerContainers } from "./ShowServerContainers"; + +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 [showContainers, setShowContainers] = useState(false); + + 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()} +
+
+
+ + + + + + + + + {/* */} +
+ {/* {showContainers && ( +
+ +
+ )} */} +
+
+ ); +} diff --git a/apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx b/apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx new file mode 100644 index 00000000..8768a88c --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx @@ -0,0 +1,24 @@ +import { api } from "@/utils/api"; +import { ServerOverviewCard } from "../server-card"; + +export default function ServersOverview() { + const { data: servers, isLoading } = api.server.all.useQuery(); + + if (isLoading) { + return
Loading...
; + } + + if (!servers) { + return
No servers found
; + } + return ( +
+

Server Overview

+
+ {servers.map((server) => ( + + ))} +
+
+ ); +} diff --git a/apps/dokploy/pages/dashboard/swarm.tsx b/apps/dokploy/pages/dashboard/swarm.tsx index 11035da7..24fa4326 100644 --- a/apps/dokploy/pages/dashboard/swarm.tsx +++ b/apps/dokploy/pages/dashboard/swarm.tsx @@ -1,5 +1,7 @@ import { ShowServers } from "@/components/dashboard/settings/servers/show-servers"; import SwarmMonitorCard from "@/components/dashboard/swarm/monitoring-card"; +import { ServerOverviewCard } from "@/components/dashboard/swarm/server-card"; +import ServersOverview from "@/components/dashboard/swarm/servers/servers-overview"; import ShowApplicationServers from "@/components/dashboard/swarm/servers/show-server"; import ShowSwarmNodes from "@/components/dashboard/swarm/show/show-nodes"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; @@ -18,7 +20,8 @@ const Dashboard = () => {
- + + {/* */} {/*

Swarm Nodes

From d4d74d38316defd53021ce60ecde5a60b34971bf Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Wed, 18 Dec 2024 08:49:02 +0100 Subject: [PATCH 14/18] refactor: remove not needed import, move to better folder structure --- .../dashboard/swarm/applications/columns.tsx | 2 +- .../swarm/applications/show-applications.tsx | 2 - .../dashboard/swarm/containers/columns.tsx | 139 --------- .../dashboard/swarm/containers/data-table.tsx | 210 -------------- .../swarm/containers/show-container.tsx | 48 ---- .../swarm/{show => details}/deatils-card.tsx | 57 +--- .../{show-node.tsx => show-node-config.tsx} | 8 - .../dashboard/swarm/monitoring-card.tsx | 2 +- .../dashboard/swarm/servers/columns.tsx | 168 ----------- .../dashboard/swarm/servers/data-table.tsx | 210 -------------- .../swarm/{ => servers}/server-card.tsx | 21 +- .../swarm/servers/servers-overview.tsx | 2 +- .../dashboard/swarm/servers/show-server.tsx | 16 -- .../dashboard/swarm/show/columns.tsx | 202 ------------- .../dashboard/swarm/show/data-table.tsx | 269 ------------------ .../dashboard/swarm/show/show-nodes.tsx | 14 - apps/dokploy/pages/dashboard/swarm.tsx | 2 +- 17 files changed, 7 insertions(+), 1365 deletions(-) delete mode 100644 apps/dokploy/components/dashboard/swarm/containers/columns.tsx delete mode 100644 apps/dokploy/components/dashboard/swarm/containers/data-table.tsx delete mode 100644 apps/dokploy/components/dashboard/swarm/containers/show-container.tsx rename apps/dokploy/components/dashboard/swarm/{show => details}/deatils-card.tsx (69%) rename apps/dokploy/components/dashboard/swarm/details/{show-node.tsx => show-node-config.tsx} (85%) delete mode 100644 apps/dokploy/components/dashboard/swarm/servers/columns.tsx delete mode 100644 apps/dokploy/components/dashboard/swarm/servers/data-table.tsx rename apps/dokploy/components/dashboard/swarm/{ => servers}/server-card.tsx (81%) delete mode 100644 apps/dokploy/components/dashboard/swarm/servers/show-server.tsx delete mode 100644 apps/dokploy/components/dashboard/swarm/show/columns.tsx delete mode 100644 apps/dokploy/components/dashboard/swarm/show/data-table.tsx delete mode 100644 apps/dokploy/components/dashboard/swarm/show/show-nodes.tsx diff --git a/apps/dokploy/components/dashboard/swarm/applications/columns.tsx b/apps/dokploy/components/dashboard/swarm/applications/columns.tsx index ba2d9e13..1961cd99 100644 --- a/apps/dokploy/components/dashboard/swarm/applications/columns.tsx +++ b/apps/dokploy/components/dashboard/swarm/applications/columns.tsx @@ -11,7 +11,7 @@ import { } from "@/components/ui/dropdown-menu"; import { Badge } from "@/components/ui/badge"; -import { ShowNodeConfig } from "../details/show-node"; +import { ShowNodeConfig } from "../details/show-node-config"; // import { ShowContainerConfig } from "../config/show-container-config"; // import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs"; // import { DockerTerminalModal } from "../terminal/docker-terminal-modal"; diff --git a/apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx b/apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx index 4363adc1..e3b38a71 100644 --- a/apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx +++ b/apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx @@ -7,7 +7,6 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; -import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; import { api } from "@/utils/api"; import { Layers, LoaderIcon } from "lucide-react"; import React from "react"; @@ -108,7 +107,6 @@ const ShowNodeApplications = ({ nodeName }: Props) => {
- {/*
*/} ); diff --git a/apps/dokploy/components/dashboard/swarm/containers/columns.tsx b/apps/dokploy/components/dashboard/swarm/containers/columns.tsx deleted file mode 100644 index 0ccf8e37..00000000 --- a/apps/dokploy/components/dashboard/swarm/containers/columns.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import type { ColumnDef } from "@tanstack/react-table"; -import { ArrowUpDown, MoreHorizontal } from "lucide-react"; -import * as React from "react"; - -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuLabel, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; - -import type { Badge } from "@/components/ui/badge"; -import { ShowNodeConfig } from "../details/show-node"; -// import { ShowContainerConfig } from "../config/show-container-config"; -// import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs"; -// import { DockerTerminalModal } from "../terminal/docker-terminal-modal"; -// import type { Container } from "./show-containers"; - -export interface ContainerList { - containerId: string; - name: string; - image: string; - ports: string; - state: string; - status: string; - serverId: string | null | undefined; -} - -export const columns: ColumnDef[] = [ - { - accessorKey: "ID", - accessorFn: (row) => row.containerId, - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("containerId")}
; - }, - }, - { - accessorKey: "Name", - accessorFn: (row) => row.name, - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("name")}
; - }, - }, - { - accessorKey: "Image", - accessorFn: (row) => row.image, - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("image")}
; - }, - }, - { - accessorKey: "Ports", - accessorFn: (row) => row.ports, - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("ports")}
; - }, - }, - { - accessorKey: "State", - accessorFn: (row) => row.state, - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("state")}
; - }, - }, - { - accessorKey: "Status", - accessorFn: (row) => row.status, - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("status")}
; - }, - }, -]; diff --git a/apps/dokploy/components/dashboard/swarm/containers/data-table.tsx b/apps/dokploy/components/dashboard/swarm/containers/data-table.tsx deleted file mode 100644 index 95b4498e..00000000 --- a/apps/dokploy/components/dashboard/swarm/containers/data-table.tsx +++ /dev/null @@ -1,210 +0,0 @@ -"use client"; - -import { - type ColumnDef, - type ColumnFiltersState, - type SortingState, - type VisibilityState, - flexRender, - getCoreRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table"; - -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, -} from "@/components/ui/dropdown-menu"; -import { Input } from "@/components/ui/input"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; -import { ChevronDown } from "lucide-react"; -import React from "react"; - -interface DataTableProps { - columns: ColumnDef[]; - data: TData[]; - isLoading: boolean; -} - -export function DataTable({ - columns, - data, - isLoading, -}: DataTableProps) { - const [sorting, setSorting] = React.useState([]); - const [columnFilters, setColumnFilters] = React.useState( - [], - ); - const [columnVisibility, setColumnVisibility] = - React.useState({}); - const [rowSelection, setRowSelection] = React.useState({}); - - const table = useReactTable({ - data, - columns, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - onColumnVisibilityChange: setColumnVisibility, - onRowSelectionChange: setRowSelection, - state: { - sorting, - columnFilters, - columnVisibility, - rowSelection, - }, - }); - - return ( -
-
-
- - table.getColumn("Name")?.setFilterValue(event.target.value) - } - className="md:max-w-sm" - /> - - - - - - {table - .getAllColumns() - .filter((column) => column.getCanHide()) - .map((column) => { - return ( - - column.toggleVisibility(!!value) - } - > - {column.id} - - ); - })} - - -
- -
- {isLoading ? ( -
- - Loading... - -
- ) : data?.length === 0 ? ( -
- - No results. - -
- ) : ( - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - ); - })} - - ))} - - - {table?.getRowModel()?.rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} - - ))} - - )) - ) : ( - - - {isLoading ? ( -
- - Loading... - -
- ) : ( - <>No results. - )} -
-
- )} -
-
- )} -
- {data && data?.length > 0 && ( -
-
- - -
-
- )} -
-
- ); -} diff --git a/apps/dokploy/components/dashboard/swarm/containers/show-container.tsx b/apps/dokploy/components/dashboard/swarm/containers/show-container.tsx deleted file mode 100644 index 4d6582aa..00000000 --- a/apps/dokploy/components/dashboard/swarm/containers/show-container.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; -import { api } from "@/utils/api"; -import React from "react"; -import { ShowContainers } from "../../docker/show/show-containers"; -import { columns } from "./columns"; -import { DataTable } from "./data-table"; -// import { columns } from "./columns"; -// import { DataTable } from "./data-table"; - -interface Props { - serverId: string; -} - -const ShowNodeContainers = ({ serverId }: Props) => { - return ( - - - e.preventDefault()} - > - Show Container - - - - - Node Container - - See all containers running on this node - - -
- -
-
-
- ); -}; - -export default ShowNodeContainers; diff --git a/apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx b/apps/dokploy/components/dashboard/swarm/details/deatils-card.tsx similarity index 69% rename from apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx rename to apps/dokploy/components/dashboard/swarm/details/deatils-card.tsx index 83d226a5..b8eb9f81 100644 --- a/apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx +++ b/apps/dokploy/components/dashboard/swarm/details/deatils-card.tsx @@ -1,25 +1,10 @@ import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; import { api } from "@/utils/api"; -import { - AlertCircle, - CheckCircle, - HelpCircle, - Layers, - LoaderIcon, - Settings, -} from "lucide-react"; +import { AlertCircle, CheckCircle, HelpCircle, LoaderIcon } from "lucide-react"; import { useState } from "react"; import ShowNodeApplications from "../applications/show-applications"; -import { ShowNodeConfig } from "../details/show-node"; +import { ShowNodeConfig } from "./show-node-config"; export interface SwarmList { ID: string; @@ -36,9 +21,6 @@ interface NodeCardProps { } export function NodeCard({ node }: NodeCardProps) { - const [showConfig, setShowConfig] = useState(false); - const [showServices, setShowServices] = useState(false); - const { data, isLoading } = api.swarm.getNodeInfo.useQuery({ nodeId: node.ID, }); @@ -77,7 +59,6 @@ export function NodeCard({ node }: NodeCardProps) { ); } - console.log(data); return ( @@ -143,41 +124,7 @@ export function NodeCard({ node }: NodeCardProps) {
- {/* - - - - - - Node Configuration - -
-
-									{JSON.stringify(node, null, 2)}
-								
-
-
-
*/} - {/* - - - - - - Node Services - -
-

Service information would be displayed here.

-
-
-
*/}
diff --git a/apps/dokploy/components/dashboard/swarm/details/show-node.tsx b/apps/dokploy/components/dashboard/swarm/details/show-node-config.tsx similarity index 85% rename from apps/dokploy/components/dashboard/swarm/details/show-node.tsx rename to apps/dokploy/components/dashboard/swarm/details/show-node-config.tsx index 4f751805..2d8a3e3e 100644 --- a/apps/dokploy/components/dashboard/swarm/details/show-node.tsx +++ b/apps/dokploy/components/dashboard/swarm/details/show-node-config.tsx @@ -8,10 +8,8 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; -import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; import { api } from "@/utils/api"; import { Settings } from "lucide-react"; -import React from "react"; interface Props { nodeId: string; @@ -22,12 +20,6 @@ export const ShowNodeConfig = ({ nodeId }: Props) => { return ( - {/* e.preventDefault()} - > - Show Config - */} - ); - }, - cell: ({ row }) => { - return
{row.getValue("serverId")}
; - }, - }, - { - accessorKey: "name", - accessorFn: (row) => row.name, - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("name")}
; - }, - }, - { - accessorKey: "ipAddress", - accessorFn: (row) => row.ipAddress, - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("ipAddress")}
; - }, - }, - { - accessorKey: "port", - accessorFn: (row) => row.port, - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("port")}
; - }, - }, - { - accessorKey: "username", - accessorFn: (row) => row.username, - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("username")}
; - }, - }, - { - accessorKey: "createdAt", - accessorFn: (row) => row.createdAt, - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("createdAt")}
; - }, - }, - { - id: "actions", - enableHiding: false, - cell: ({ row }) => { - return ( - - - - - - Actions - - - - ); - }, - }, -]; diff --git a/apps/dokploy/components/dashboard/swarm/servers/data-table.tsx b/apps/dokploy/components/dashboard/swarm/servers/data-table.tsx deleted file mode 100644 index 95b4498e..00000000 --- a/apps/dokploy/components/dashboard/swarm/servers/data-table.tsx +++ /dev/null @@ -1,210 +0,0 @@ -"use client"; - -import { - type ColumnDef, - type ColumnFiltersState, - type SortingState, - type VisibilityState, - flexRender, - getCoreRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table"; - -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, -} from "@/components/ui/dropdown-menu"; -import { Input } from "@/components/ui/input"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; -import { ChevronDown } from "lucide-react"; -import React from "react"; - -interface DataTableProps { - columns: ColumnDef[]; - data: TData[]; - isLoading: boolean; -} - -export function DataTable({ - columns, - data, - isLoading, -}: DataTableProps) { - const [sorting, setSorting] = React.useState([]); - const [columnFilters, setColumnFilters] = React.useState( - [], - ); - const [columnVisibility, setColumnVisibility] = - React.useState({}); - const [rowSelection, setRowSelection] = React.useState({}); - - const table = useReactTable({ - data, - columns, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - onColumnVisibilityChange: setColumnVisibility, - onRowSelectionChange: setRowSelection, - state: { - sorting, - columnFilters, - columnVisibility, - rowSelection, - }, - }); - - return ( -
-
-
- - table.getColumn("Name")?.setFilterValue(event.target.value) - } - className="md:max-w-sm" - /> - - - - - - {table - .getAllColumns() - .filter((column) => column.getCanHide()) - .map((column) => { - return ( - - column.toggleVisibility(!!value) - } - > - {column.id} - - ); - })} - - -
- -
- {isLoading ? ( -
- - Loading... - -
- ) : data?.length === 0 ? ( -
- - No results. - -
- ) : ( - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - ); - })} - - ))} - - - {table?.getRowModel()?.rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} - - ))} - - )) - ) : ( - - - {isLoading ? ( -
- - Loading... - -
- ) : ( - <>No results. - )} -
-
- )} -
-
- )} -
- {data && data?.length > 0 && ( -
-
- - -
-
- )} -
-
- ); -} diff --git a/apps/dokploy/components/dashboard/swarm/server-card.tsx b/apps/dokploy/components/dashboard/swarm/servers/server-card.tsx similarity index 81% rename from apps/dokploy/components/dashboard/swarm/server-card.tsx rename to apps/dokploy/components/dashboard/swarm/servers/server-card.tsx index 10029114..4b732df4 100644 --- a/apps/dokploy/components/dashboard/swarm/server-card.tsx +++ b/apps/dokploy/components/dashboard/swarm/servers/server-card.tsx @@ -3,10 +3,7 @@ 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 { useState } from "react"; -import { ShowContainers } from "../docker/show/show-containers"; -// import type { Server } from "../types/server"; -// import { ShowServerContainers } from "./ShowServerContainers"; +import { ShowContainers } from "../../docker/show/show-containers"; export interface Server { serverId: string; @@ -29,8 +26,6 @@ interface ServerOverviewCardProps { } export function ServerOverviewCard({ server }: ServerOverviewCardProps) { - const [showContainers, setShowContainers] = useState(false); - const getStatusIcon = (status: string) => { switch (status) { case "active": @@ -101,21 +96,7 @@ export function ServerOverviewCard({ server }: ServerOverviewCardProps) {
- {/* */}
- {/* {showContainers && ( -
- -
- )} */} ); diff --git a/apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx b/apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx index 8768a88c..a90546c9 100644 --- a/apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx +++ b/apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx @@ -1,5 +1,5 @@ import { api } from "@/utils/api"; -import { ServerOverviewCard } from "../server-card"; +import { ServerOverviewCard } from "./server-card"; export default function ServersOverview() { const { data: servers, isLoading } = api.server.all.useQuery(); diff --git a/apps/dokploy/components/dashboard/swarm/servers/show-server.tsx b/apps/dokploy/components/dashboard/swarm/servers/show-server.tsx deleted file mode 100644 index 0486b164..00000000 --- a/apps/dokploy/components/dashboard/swarm/servers/show-server.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { api } from "@/utils/api"; -import React from "react"; -import { columns } from "./columns"; -import { DataTable } from "./data-table"; - -function ShowApplicationServers() { - const { data, isLoading } = api.server.all.useQuery(); - - console.log(data); - - return ( - - ); -} - -export default ShowApplicationServers; diff --git a/apps/dokploy/components/dashboard/swarm/show/columns.tsx b/apps/dokploy/components/dashboard/swarm/show/columns.tsx deleted file mode 100644 index b0774936..00000000 --- a/apps/dokploy/components/dashboard/swarm/show/columns.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import type { ColumnDef } from "@tanstack/react-table"; -import { ArrowUpDown, MoreHorizontal } from "lucide-react"; -import * as React from "react"; - -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuLabel, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; - -import { Badge } from "@/components/ui/badge"; -import ShowNodeApplications from "../applications/show-applications"; -import ShowContainers from "../containers/show-container"; -import { ShowNodeConfig } from "../details/show-node"; -// import { ShowContainerConfig } from "../config/show-container-config"; -// import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs"; -// import { DockerTerminalModal } from "../terminal/docker-terminal-modal"; -// import type { Container } from "./show-containers"; - -export interface SwarmList { - ID: string; - Hostname: string; - Availability: string; - EngineVersion: string; - Status: string; - ManagerStatus: string; - TLSStatus: string; -} - -export const columns: ColumnDef[] = [ - { - accessorKey: "ID", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("ID")}
; - }, - }, - { - accessorKey: "EngineVersion", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("EngineVersion")}
; - }, - }, - { - accessorKey: "Hostname", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("Hostname")}
; - }, - }, - // { - // accessorKey: "Status", - // header: ({ column }) => { - // return ( - // - // ); - // }, - // cell: ({ row }) => { - // const value = row.getValue("status") as string; - // return ( - //
- // - // {value} - // - //
- // ); - // }, - // }, - { - accessorKey: "Availability", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - const value = row.getValue("Availability") as string; - return ( -
- - {value} - -
- ); - }, - }, - { - accessorKey: "ManagerStatus", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => ( -
{row.getValue("ManagerStatus")}
- ), - }, - { - id: "actions", - enableHiding: false, - cell: ({ row }) => { - return ( - - - - - - Actions - - - {/* - View Logs - - - - Terminal - */} - - - ); - }, - }, -]; diff --git a/apps/dokploy/components/dashboard/swarm/show/data-table.tsx b/apps/dokploy/components/dashboard/swarm/show/data-table.tsx deleted file mode 100644 index d3e99352..00000000 --- a/apps/dokploy/components/dashboard/swarm/show/data-table.tsx +++ /dev/null @@ -1,269 +0,0 @@ -"use client"; - -import { - type ColumnFiltersState, - type SortingState, - type VisibilityState, - type ColumnDef, - flexRender, - getCoreRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table"; - -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import React from "react"; -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, -} from "@/components/ui/dropdown-menu"; -import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; -import { Button } from "@/components/ui/button"; -import { ChevronDown } from "lucide-react"; -import { Input } from "@/components/ui/input"; - -interface DataTableProps { - columns: ColumnDef[]; - data: TData[]; - isLoading: boolean; -} - -export function DataTable({ - columns, - data, - isLoading, -}: DataTableProps) { - const [sorting, setSorting] = React.useState([]); - const [columnFilters, setColumnFilters] = React.useState( - [] - ); - const [columnVisibility, setColumnVisibility] = - React.useState({}); - const [rowSelection, setRowSelection] = React.useState({}); - - const table = useReactTable({ - data, - columns, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - onColumnVisibilityChange: setColumnVisibility, - onRowSelectionChange: setRowSelection, - state: { - sorting, - columnFilters, - columnVisibility, - rowSelection, - }, - }); - - console.log("Data in DataTable", data); - - return ( -
-
-
- - table.getColumn("Hostname")?.setFilterValue(event.target.value) - } - className="md:max-w-sm" - /> - - - - - - {table - .getAllColumns() - .filter((column) => column.getCanHide()) - .map((column) => { - return ( - - column.toggleVisibility(!!value) - } - > - {column.id} - - ); - })} - - -
- {/* - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ); - })} - - ))} - - - {table?.getRowModel()?.rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - )) - ) : ( - - - {isLoading ? ( -
- - Loading... - -
- ) : ( - <>No results. - )} -
-
- )} -
-
*/} -
- {isLoading ? ( -
- - Loading... - -
- ) : data?.length === 0 ? ( -
- - No results. - -
- ) : ( - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ); - })} - - ))} - - - {table?.getRowModel()?.rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - )) - ) : ( - - - {isLoading ? ( -
- - Loading... - -
- ) : ( - <>No results. - )} -
-
- )} -
-
- )} -
- {data && data?.length > 0 && ( -
-
- - -
-
- )} -
-
- ); -} diff --git a/apps/dokploy/components/dashboard/swarm/show/show-nodes.tsx b/apps/dokploy/components/dashboard/swarm/show/show-nodes.tsx deleted file mode 100644 index e629654f..00000000 --- a/apps/dokploy/components/dashboard/swarm/show/show-nodes.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { api } from "@/utils/api"; -import React from "react"; -import { columns } from "./columns"; -import { DataTable } from "./data-table"; - -function ShowSwarmNodes() { - const { data, isLoading } = api.swarm.getNodes.useQuery(); - - return ( - - ); -} - -export default ShowSwarmNodes; diff --git a/apps/dokploy/pages/dashboard/swarm.tsx b/apps/dokploy/pages/dashboard/swarm.tsx index 24fa4326..b294c09b 100644 --- a/apps/dokploy/pages/dashboard/swarm.tsx +++ b/apps/dokploy/pages/dashboard/swarm.tsx @@ -1,6 +1,6 @@ import { ShowServers } from "@/components/dashboard/settings/servers/show-servers"; import SwarmMonitorCard from "@/components/dashboard/swarm/monitoring-card"; -import { ServerOverviewCard } from "@/components/dashboard/swarm/server-card"; +import { ServerOverviewCard } from "@/components/dashboard/swarm/servers/server-card"; import ServersOverview from "@/components/dashboard/swarm/servers/servers-overview"; import ShowApplicationServers from "@/components/dashboard/swarm/servers/show-server"; import ShowSwarmNodes from "@/components/dashboard/swarm/show/show-nodes"; From b52f57cb0ddf09d37ff18efc8e5614374e7f1966 Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Wed, 18 Dec 2024 08:51:49 +0100 Subject: [PATCH 15/18] chore: remove imports --- .../swarm/details/{deatils-card.tsx => details-card.tsx} | 0 .../components/dashboard/swarm/monitoring-card.tsx | 2 +- apps/dokploy/pages/dashboard/swarm.tsx | 8 +------- 3 files changed, 2 insertions(+), 8 deletions(-) rename apps/dokploy/components/dashboard/swarm/details/{deatils-card.tsx => details-card.tsx} (100%) diff --git a/apps/dokploy/components/dashboard/swarm/details/deatils-card.tsx b/apps/dokploy/components/dashboard/swarm/details/details-card.tsx similarity index 100% rename from apps/dokploy/components/dashboard/swarm/details/deatils-card.tsx rename to apps/dokploy/components/dashboard/swarm/details/details-card.tsx diff --git a/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx index 7dae38de..63984dec 100644 --- a/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx +++ b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx @@ -15,7 +15,7 @@ import { Loader2, Server, } from "lucide-react"; -import { NodeCard } from "./details/deatils-card"; +import { NodeCard } from "./details/details-card"; export interface SwarmList { ID: string; diff --git a/apps/dokploy/pages/dashboard/swarm.tsx b/apps/dokploy/pages/dashboard/swarm.tsx index b294c09b..ce4cfce2 100644 --- a/apps/dokploy/pages/dashboard/swarm.tsx +++ b/apps/dokploy/pages/dashboard/swarm.tsx @@ -1,17 +1,11 @@ -import { ShowServers } from "@/components/dashboard/settings/servers/show-servers"; import SwarmMonitorCard from "@/components/dashboard/swarm/monitoring-card"; -import { ServerOverviewCard } from "@/components/dashboard/swarm/servers/server-card"; import ServersOverview from "@/components/dashboard/swarm/servers/servers-overview"; -import ShowApplicationServers from "@/components/dashboard/swarm/servers/show-server"; -import ShowSwarmNodes from "@/components/dashboard/swarm/show/show-nodes"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; -import { Separator } from "@/components/ui/separator"; import { appRouter } from "@/server/api/root"; -import { api } from "@/utils/api"; import { IS_CLOUD, validateRequest } from "@dokploy/server"; import { createServerSideHelpers } from "@trpc/react-query/server"; import type { GetServerSidePropsContext } from "next"; -import React, { type ReactElement } from "react"; +import type { ReactElement } from "react"; import superjson from "superjson"; const Dashboard = () => { From 8642d8235eb58abf530623cc130142f24c233317 Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Wed, 18 Dec 2024 09:14:10 +0100 Subject: [PATCH 16/18] chore: add seperator and make tittles big --- .../dashboard/swarm/monitoring-card.tsx | 23 ++++---- .../swarm/servers/servers-overview.tsx | 56 ++++++++++++++++++- apps/dokploy/pages/dashboard/swarm.tsx | 12 ++-- 3 files changed, 69 insertions(+), 22 deletions(-) diff --git a/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx index 63984dec..81a68172 100644 --- a/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx +++ b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx @@ -1,4 +1,5 @@ import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Tooltip, @@ -15,6 +16,7 @@ import { Loader2, Server, } from "lucide-react"; +import Link from "next/link"; import { NodeCard } from "./details/details-card"; export interface SwarmList { @@ -72,7 +74,6 @@ export default function SwarmMonitorCard() { ); } - console.log(nodes); const totalNodes = nodes.length; const activeNodesCount = nodes.filter( (node) => node.Status === "Ready", @@ -103,23 +104,21 @@ export default function SwarmMonitorCard() { return (
+
+

Docker Swarm Overview

+ +
Docker Swarm Monitor - {/* */}
diff --git a/apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx b/apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx index a90546c9..bd54f43e 100644 --- a/apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx +++ b/apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx @@ -1,19 +1,69 @@ +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
Loading...
; + return ( + <> + + + + + + + + + + + + +
+
+ IP Address: +
+
+ Port: +
+
+ Username: +
+
+ App Name: +
+
+ Docker Cleanup: +
+
+ Created At: +
+
+
+
+ + ); } if (!servers) { return
No servers found
; } return ( -
-

Server Overview

+
+
+

Server Overview

+ +
{servers.map((server) => ( diff --git a/apps/dokploy/pages/dashboard/swarm.tsx b/apps/dokploy/pages/dashboard/swarm.tsx index ce4cfce2..d8b6b061 100644 --- a/apps/dokploy/pages/dashboard/swarm.tsx +++ b/apps/dokploy/pages/dashboard/swarm.tsx @@ -1,6 +1,7 @@ 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"; @@ -14,13 +15,10 @@ const Dashboard = () => {
- - {/* */} - {/*

Swarm Nodes

- - -

Server Nodes

- */} + +
+ +
); }; From 5c5066bc721982725c59aa6b6ac8c9d6a2859540 Mon Sep 17 00:00:00 2001 From: DJKnaeckebrot Date: Sun, 22 Dec 2024 20:07:48 +0100 Subject: [PATCH 17/18] chore: remove server from swarms page --- apps/dokploy/pages/dashboard/swarm.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/dokploy/pages/dashboard/swarm.tsx b/apps/dokploy/pages/dashboard/swarm.tsx index d8b6b061..2722e0e4 100644 --- a/apps/dokploy/pages/dashboard/swarm.tsx +++ b/apps/dokploy/pages/dashboard/swarm.tsx @@ -15,10 +15,6 @@ const Dashboard = () => {
- -
- -
); }; 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 18/18] 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}`);