Files
dokploy/apps/dokploy/components/dashboard/requests/requests-table.tsx
manalkaff 598fae0e92 fix: filter requests by hostname instead of path
The search filter on the Requests tab was incorrectly filtering by
RequestPath instead of RequestHost, causing "filter by name" to match
URL paths rather than hostnames. Updated the placeholder text to
reflect the correct field being searched.

Fixes #4249
2026-04-19 17:30:42 +08:00

397 lines
10 KiB
TypeScript

import {
type ColumnFiltersState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getSortedRowModel,
type PaginationState,
type SortingState,
useReactTable,
type VisibilityState,
} from "@tanstack/react-table";
import copy from "copy-to-clipboard";
import {
CheckCircle2Icon,
ChevronDown,
Copy,
Download,
Globe,
InfoIcon,
Server,
TrendingUpIcon,
} from "lucide-react";
import { useMemo, useState } from "react";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { api } from "@/utils/api";
import { columns, getStatusColor } from "./columns";
import type { LogEntry } from "./show-requests";
import { DataTableFacetedFilter } from "./status-request-filter";
export const priorities = [
{
label: "100 - 199",
value: "info",
icon: InfoIcon,
},
{
label: "200 - 299",
value: "success",
icon: CheckCircle2Icon,
},
{
label: "300 - 399",
value: "redirect",
icon: TrendingUpIcon,
},
{
label: "400 - 499",
value: "client",
icon: Globe,
},
{
label: "500 - 599",
value: "server",
icon: Server,
},
];
export interface RequestsTableProps {
dateRange?: {
from: Date | undefined;
to: Date | undefined;
};
}
export const RequestsTable = ({ dateRange }: RequestsTableProps) => {
const [statusFilter, setStatusFilter] = useState<string[]>([]);
const [search, setSearch] = useState("");
const [selectedRow, setSelectedRow] = useState<LogEntry>();
const [sorting, setSorting] = useState<SortingState>([]);
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
const [rowSelection, setRowSelection] = useState({});
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const { data: statsLogs } = api.settings.readStatsLogs.useQuery(
{
sort: sorting[0],
page: pagination,
search,
status: statusFilter,
dateRange: dateRange
? {
start: dateRange.from?.toISOString(),
end: dateRange.to?.toISOString(),
}
: undefined,
},
{
refetchInterval: 1333,
},
);
const pageCount = useMemo(() => {
if (statsLogs?.totalCount) {
return Math.ceil(statsLogs.totalCount / pagination.pageSize);
}
return -1;
}, [statsLogs?.totalCount, pagination.pageSize]);
const table = useReactTable({
data: statsLogs?.data ?? [],
columns,
onPaginationChange: setPagination,
onSortingChange: setSorting,
pageCount: pageCount,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
manualPagination: true,
state: {
pagination,
sorting,
columnFilters,
columnVisibility,
rowSelection,
},
});
const formatValue = (key: string, value: any) => {
if (typeof value === "object" && value !== null) {
return JSON.stringify(value, null, 2);
}
if (key === "Duration" || key === "OriginDuration" || key === "Overhead") {
const nanos = Number(value);
const ms = nanos / 1000000;
if (ms < 1) {
return `${(nanos / 1000).toFixed(2)} µs`;
}
if (ms < 1000) {
return `${ms.toFixed(2)} ms`;
}
return `${(ms / 1000).toFixed(2)} s`;
}
if (key === "level") {
return <Badge variant="secondary">{value}</Badge>;
}
if (key === "RequestMethod") {
return <Badge variant="outline">{value}</Badge>;
}
if (key === "DownstreamStatus" || key === "OriginStatus") {
const num = Number(value);
if (num === 0) {
return <Badge variant="secondary">N/A</Badge>;
}
return <Badge variant={getStatusColor(num)}>{value}</Badge>;
}
return value;
};
return (
<>
<div className="flex flex-col gap-6 w-full ">
<div className="mt-6 grid gap-4 pb-20 w-full">
<div className="flex flex-col gap-4 w-full overflow-auto">
<div className="flex items-center gap-2 max-sm:flex-wrap">
<Input
placeholder="Filter by hostname..."
value={search}
onChange={(event) => setSearch(event.target.value)}
className="md:max-w-sm"
/>
<DataTableFacetedFilter
value={statusFilter}
setValue={setStatusFilter}
title="Status"
options={priorities}
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className="sm:ml-auto max-sm:w-full"
>
Columns <ChevronDown className="ml-2 h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="rounded-md border ">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
className="cursor-pointer"
onClick={() => {
setSelectedRow(row.original);
}}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
{statsLogs?.data.length === 0 && (
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
<span className="text-muted-foreground text-lg font-medium">
No results.
</span>
</div>
)}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-end space-x-2 py-4">
{statsLogs?.totalCount && (
<span className="text-muted-foreground text-sm">
Showing{" "}
{Math.min(
pagination.pageIndex * pagination.pageSize + 1,
statsLogs.totalCount,
)}{" "}
to{" "}
{Math.min(
(pagination.pageIndex + 1) * pagination.pageSize,
statsLogs.totalCount,
)}{" "}
of {statsLogs.totalCount} entries
</span>
)}
<div className="space-x-2 flex flex-wrap">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
</div>
</div>
</div>
<Sheet
open={!!selectedRow}
onOpenChange={(_open) => setSelectedRow(undefined)}
>
<SheetContent className="sm:max-w-[740px] flex flex-col">
<SheetHeader>
<SheetTitle>Request log</SheetTitle>
<SheetDescription>
Details of the request log entry.
</SheetDescription>
</SheetHeader>
<ScrollArea className="flex-grow mt-4 pr-4">
<div className="border rounded-md">
<Table>
<TableBody>
{Object.entries(selectedRow || {}).map(([key, value]) => (
<TableRow key={key}>
<TableCell className="font-medium">{key}</TableCell>
<TableCell className="truncate break-words break-before-all whitespace-pre-wrap">
{key === "RequestAddr" ? (
<div className="flex items-center gap-2 bg-muted p-1 rounded">
<span>{value}</span>
<Copy
onClick={() => {
copy(value);
toast.success("Copied to clipboard");
}}
className="h-4 w-4 text-muted-foreground cursor-pointer"
/>
</div>
) : (
formatValue(key, value)
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</ScrollArea>
<div className="mt-4 pt-4 border-t">
<Button
variant="outline"
className="w-full gap-2"
onClick={() => {
const logs = JSON.stringify(selectedRow, null, 2);
const element = document.createElement("a");
element.setAttribute(
"href",
`data:text/plain;charset=utf-8,${encodeURIComponent(logs)}`,
);
element.setAttribute("download", "logs.json");
element.style.display = "none";
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}}
>
<Download className="h-4 w-4" />
Download as JSON
</Button>
</div>
</SheetContent>
</Sheet>
</>
);
};