From 48902c488fb95322cf3b58d1060a501b73e2ae87 Mon Sep 17 00:00:00 2001 From: quochuydev Date: Wed, 17 Dec 2025 18:48:19 +0700 Subject: [PATCH] feat: add grid/table view toggle for domains page --- .../dashboard/application/domains/columns.tsx | 261 ++++++++++++++++++ .../application/domains/show-domains.tsx | 215 ++++++++++++++- 2 files changed, 471 insertions(+), 5 deletions(-) create mode 100644 apps/dokploy/components/dashboard/application/domains/columns.tsx diff --git a/apps/dokploy/components/dashboard/application/domains/columns.tsx b/apps/dokploy/components/dashboard/application/domains/columns.tsx new file mode 100644 index 000000000..17a1ff1a0 --- /dev/null +++ b/apps/dokploy/components/dashboard/application/domains/columns.tsx @@ -0,0 +1,261 @@ +import type { ColumnDef } from "@tanstack/react-table"; +import { + ArrowUpDown, + CheckCircle2, + ExternalLink, + Loader2, + PenBoxIcon, + RefreshCw, + Trash2, + XCircle, +} from "lucide-react"; +import Link from "next/link"; +import { DialogAction } from "@/components/shared/dialog-action"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import type { RouterOutputs } from "@/utils/api"; +import type { ValidationStates } from "./show-domains"; +import { AddDomain } from "./handle-domain"; +import { DnsHelperModal } from "./dns-helper-modal"; + +export type Domain = + | RouterOutputs["domain"]["byApplicationId"][0] + | RouterOutputs["domain"]["byComposeId"][0]; + +interface ColumnsProps { + id: string; + type: "application" | "compose"; + validationStates: ValidationStates; + handleValidateDomain: (host: string) => Promise; + handleDeleteDomain: (domainId: string) => Promise; + isDeleting: boolean; + serverIp?: string; +} + +export const createColumns = ({ + id, + type, + validationStates, + handleValidateDomain, + handleDeleteDomain, + isDeleting, + serverIp, +}: ColumnsProps): ColumnDef[] => [ + { + accessorKey: "host", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const domain = row.original; + return ( + + + + ); + }, + }, + { + accessorKey: "path", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const path = row.getValue("path") as string; + return
{path || "/"}
; + }, + }, + { + accessorKey: "port", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const port = row.getValue("port") as number; + return {port}; + }, + }, + { + accessorKey: "https", + header: "Protocol", + cell: ({ row }) => { + const https = row.getValue("https") as boolean; + return ( + + + + + {https ? "HTTPS" : "HTTP"} + + + +

+ {https ? "Secure HTTPS connection" : "Standard HTTP connection"} +

+
+
+
+ ); + }, + }, + { + id: "certificate", + header: "Certificate", + cell: ({ row }) => { + const domain = row.original; + const validationState = validationStates[domain.host]; + + return ( +
+ {domain.certificateType && ( + + {domain.certificateType} + + )} + {!domain.host.includes("traefik.me") && ( + + + + handleValidateDomain(domain.host)} + > + {validationState?.isLoading ? ( + <> + + Checking... + + ) : validationState?.isValid ? ( + <> + + {validationState.message && validationState.cdnProvider + ? `${validationState.cdnProvider}` + : "Valid"} + + ) : validationState?.error ? ( + <> + + Invalid + + ) : ( + <> + + Validate + + )} + + + + {validationState?.error ? ( +
+

Error:

+

{validationState.error}

+
+ ) : ( + "Click to validate DNS configuration" + )} +
+
+
+ )} +
+ ); + }, + }, + { + id: "actions", + header: "Actions", + enableHiding: false, + cell: ({ row }) => { + const domain = row.original; + + return ( +
+ {!domain.host.includes("traefik.me") && ( + + )} + + + + { + await handleDeleteDomain(domain.domainId); + }} + > + + +
+ ); + }, + }, +]; diff --git a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx index 1fd3d82e9..411817883 100644 --- a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx +++ b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx @@ -1,8 +1,22 @@ +import { + type ColumnFiltersState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + type SortingState, + useReactTable, + type VisibilityState, +} from "@tanstack/react-table"; import { CheckCircle2, + ChevronDown, ExternalLink, GlobeIcon, InfoIcon, + LayoutGrid, + LayoutList, Loader2, PenBoxIcon, RefreshCw, @@ -23,6 +37,21 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; import { Tooltip, TooltipContent, @@ -30,6 +59,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { api } from "@/utils/api"; +import { createColumns } from "./columns"; import { DnsHelperModal } from "./dns-helper-modal"; import { AddDomain } from "./handle-domain"; @@ -71,6 +101,11 @@ export const ShowDomains = ({ id, type }: Props) => { const [validationStates, setValidationStates] = useState( {}, ); + const [viewMode, setViewMode] = useState<"grid" | "table">("grid"); + const [sorting, setSorting] = useState([]); + const [columnFilters, setColumnFilters] = useState([]); + const [columnVisibility, setColumnVisibility] = useState({}); + const [rowSelection, setRowSelection] = useState({}); const { data: ip } = api.settings.getIp.useQuery(); const { @@ -100,6 +135,16 @@ export const ShowDomains = ({ id, type }: Props) => { const { mutateAsync: deleteDomain, isLoading: isRemoving } = api.domain.delete.useMutation(); + const handleDeleteDomain = async (domainId: string) => { + try { + await deleteDomain({ domainId }); + refetch(); + toast.success("Domain deleted successfully"); + } catch { + toast.error("Error deleting domain"); + } + }; + const handleValidateDomain = async (host: string) => { setValidationStates((prev) => ({ ...prev, @@ -137,6 +182,35 @@ export const ShowDomains = ({ id, type }: Props) => { } }; + const columns = createColumns({ + id, + type, + validationStates, + handleValidateDomain, + handleDeleteDomain, + isDeleting: isRemoving, + serverIp: application?.server?.ipAddress?.toString() || ip?.toString(), + }); + + const table = useReactTable({ + data: data ?? [], + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }); + return (
@@ -148,13 +222,28 @@ export const ShowDomains = ({ id, type }: Props) => {
-
+
{data && data?.length > 0 && ( - - - + + + + )}
@@ -181,6 +270,122 @@ export const ShowDomains = ({ id, type }: Props) => {
+ ) : viewMode === "table" ? ( +
+
+ + table.getColumn("host")?.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. + + + )} + +
+
+ {data && data?.length > 0 && ( +
+
+ + +
+
+ )} +
) : (
{data?.map((item) => {