From ac1637eaf8686926a2497c0022e04a083632371b Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Tue, 20 Aug 2024 00:15:08 -0600 Subject: [PATCH 01/23] feat: add requests --- .../components/dashboard/requests/columns.tsx | 105 +++++ .../dashboard/requests/show-requests.tsx | 300 +++++++++++++++ .../components/layouts/navigation-tabs.tsx | 9 + apps/dokploy/components/ui/chart.tsx | 363 ++++++++++++++++++ apps/dokploy/package.json | 6 +- apps/dokploy/pages/dashboard/requests.tsx | 153 ++++++++ apps/dokploy/server/api/routers/settings.ts | 108 ++++++ .../server/utils/traefik/application.ts | 9 + apps/dokploy/styles/globals.css | 20 +- pnpm-lock.yaml | 8 +- 10 files changed, 1070 insertions(+), 11 deletions(-) create mode 100644 apps/dokploy/components/dashboard/requests/columns.tsx create mode 100644 apps/dokploy/components/dashboard/requests/show-requests.tsx create mode 100644 apps/dokploy/components/ui/chart.tsx create mode 100644 apps/dokploy/pages/dashboard/requests.tsx diff --git a/apps/dokploy/components/dashboard/requests/columns.tsx b/apps/dokploy/components/dashboard/requests/columns.tsx new file mode 100644 index 00000000..7e5765d5 --- /dev/null +++ b/apps/dokploy/components/dashboard/requests/columns.tsx @@ -0,0 +1,105 @@ +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 type { LogEntry } from "./show-requests"; + +export const columns: ColumnDef[] = [ + { + accessorKey: "level", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.original.level}
; + }, + }, + { + accessorKey: "RequestPath", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const log = row.original; + return ( +
+
+ {log.RequestMethod} {log.RequestPath} +
+
+ + Status: {log.OriginStatus} + + + Exec Time: {convertMicroseconds(log.Duration)} + + IP: {log.ClientAddr} +
+
+ ); + }, + }, + { + id: "actions", + enableHiding: false, + cell: ({ row }) => { + const container = row.original; + + return ( + + + + + + Actions + {/* + View Logs + + + + Terminal + */} + + + ); + }, + }, +]; +function convertMicroseconds(microseconds: number): string { + if (microseconds < 1000) { + return `${microseconds} µs`; + } + if (microseconds < 1000000) { + return `${(microseconds / 1000).toFixed(2)} ms`; + } + return `${(microseconds / 1000000).toFixed(2)} s`; +} diff --git a/apps/dokploy/components/dashboard/requests/show-requests.tsx b/apps/dokploy/components/dashboard/requests/show-requests.tsx new file mode 100644 index 00000000..0f8587f7 --- /dev/null +++ b/apps/dokploy/components/dashboard/requests/show-requests.tsx @@ -0,0 +1,300 @@ +import { api, type RouterOutputs } from "@/utils/api"; +import { + type ColumnFiltersState, + type SortingState, + type VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; +import * as React from "react"; +import { + Area, + AreaChart, + Bar, + BarChart, + CartesianGrid, + ResponsiveContainer, + XAxis, + YAxis, +} from "recharts"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + type ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from "@/components/ui/chart"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { ChevronDown, MoreHorizontal, TrendingUp } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Table, + TableBody, + TableCaption, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { columns } from "./columns"; +import { Input } from "@/components/ui/input"; + +export type LogEntry = NonNullable< + RouterOutputs["settings"]["readMonitoringConfig"]["data"] +>[0]; + +const chartConfig = { + views: { + label: "Page Views", + }, + count: { + label: "Count", + color: "hsl(var(--chart-1))", + }, +} satisfies ChartConfig; +export const ShowRequests = () => { + const { data } = api.settings.readMonitoringConfig.useQuery(); + + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState( + [], + ); + const [columnVisibility, setColumnVisibility] = + React.useState({}); + const [rowSelection, setRowSelection] = React.useState({}); + + const table = useReactTable({ + data: 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 ( + <> + + + Request Distribution + + Showing web and API requests over time + + + + + + + + + new Date(value).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }) + } + /> + + } + labelFormatter={(value) => + new Date(value).toLocaleString([], { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }) + } + /> + + + + + + + +
+
+
+
+ + table + .getColumn("RequestPath") + ?.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. + )} */} +
+
+ )} +
+
+
+
+
+ + +
+
+
+
+
+ + ); +}; diff --git a/apps/dokploy/components/layouts/navigation-tabs.tsx b/apps/dokploy/components/layouts/navigation-tabs.tsx index b251a279..2a397fe9 100644 --- a/apps/dokploy/components/layouts/navigation-tabs.tsx +++ b/apps/dokploy/components/layouts/navigation-tabs.tsx @@ -19,6 +19,7 @@ export type TabState = | "monitoring" | "settings" | "traefik" + | "requests" | "docker"; const tabMap: Record = { @@ -49,6 +50,14 @@ const tabMap: Record = { return Boolean(rol === "admin" || user?.canAccessToDocker); }, }, + requests: { + label: "Requests", + description: "Manage your requests", + index: "/dashboard/requests", + // isShow: ({ rol, user }) => { + // return Boolean(rol === "admin" || user?.canAccessToDocker); + // }, + }, settings: { label: "Settings", description: "Manage your settings", diff --git a/apps/dokploy/components/ui/chart.tsx b/apps/dokploy/components/ui/chart.tsx new file mode 100644 index 00000000..a21d77ee --- /dev/null +++ b/apps/dokploy/components/ui/chart.tsx @@ -0,0 +1,363 @@ +import * as React from "react" +import * as RechartsPrimitive from "recharts" + +import { cn } from "@/lib/utils" + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { light: "", dark: ".dark" } as const + +export type ChartConfig = { + [k in string]: { + label?: React.ReactNode + icon?: React.ComponentType + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ) +} + +type ChartContextProps = { + config: ChartConfig +} + +const ChartContext = React.createContext(null) + +function useChart() { + const context = React.useContext(ChartContext) + + if (!context) { + throw new Error("useChart must be used within a ") + } + + return context +} + +const ChartContainer = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + config: ChartConfig + children: React.ComponentProps< + typeof RechartsPrimitive.ResponsiveContainer + >["children"] + } +>(({ id, className, children, config, ...props }, ref) => { + const uniqueId = React.useId() + const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` + + return ( + +
+ + + {children} + +
+
+ ) +}) +ChartContainer.displayName = "Chart" + +const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { + const colorConfig = Object.entries(config).filter( + ([_, config]) => config.theme || config.color + ) + + if (!colorConfig.length) { + return null + } + + return ( +