From 5716954665cfe59b07ecd684bb50c3f2bb0bef04 Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Tue, 17 Dec 2024 12:05:39 +0100 Subject: [PATCH] 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 000000000..ba2d9e13f --- /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 000000000..1b192f7d0 --- /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 000000000..2ef632b9b --- /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 000000000..9a0921526 --- /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 000000000..ba11d749a --- /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 000000000..d3e993529 --- /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 000000000..6c5bd99d6 --- /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;