mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-13 23:45:17 +00:00
feat: add sorting and pagination to Docker events table
This commit is contained in:
@@ -1,4 +1,14 @@
|
|||||||
import { Activity, Loader2, RefreshCw } from "lucide-react";
|
import {
|
||||||
|
type ColumnDef,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
type PaginationState,
|
||||||
|
type SortingState,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
import { Activity, ArrowUpDown, Loader2, RefreshCw } from "lucide-react";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -32,6 +42,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DockerEvent = RouterOutputs["docker"]["getEvents"]["events"][number];
|
type DockerEvent = RouterOutputs["docker"]["getEvents"]["events"][number];
|
||||||
|
type EventRow = DockerEvent & { key: string };
|
||||||
|
|
||||||
const RANGE_OPTIONS = [
|
const RANGE_OPTIONS = [
|
||||||
{ label: "Last 5 minutes", value: 5 },
|
{ label: "Last 5 minutes", value: 5 },
|
||||||
@@ -73,9 +84,46 @@ const getTypeVariant = (type?: string): BadgeVariant =>
|
|||||||
const getActionVariant = (action?: string): BadgeVariant =>
|
const getActionVariant = (action?: string): BadgeVariant =>
|
||||||
(action && ACTION_VARIANTS[action]) || "blank";
|
(action && ACTION_VARIANTS[action]) || "blank";
|
||||||
|
|
||||||
|
const getResource = (event: DockerEvent) =>
|
||||||
|
event.Actor?.Attributes?.name ?? event.Actor?.ID ?? "-";
|
||||||
|
|
||||||
|
const getAttributesText = (event: DockerEvent) => {
|
||||||
|
const attributes = Object.entries(event.Actor?.Attributes ?? {}).filter(
|
||||||
|
([key]) => key !== "name",
|
||||||
|
);
|
||||||
|
return attributes.map(([key, value]) => `${key}=${value}`).join(" ") || "-";
|
||||||
|
};
|
||||||
|
|
||||||
|
const SortableHeader = ({
|
||||||
|
column,
|
||||||
|
title,
|
||||||
|
}: {
|
||||||
|
column: {
|
||||||
|
getIsSorted: () => false | "asc" | "desc";
|
||||||
|
toggleSorting: (asc: boolean) => void;
|
||||||
|
};
|
||||||
|
title: string;
|
||||||
|
}) => (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="-ml-3 h-8"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
<ArrowUpDown className="ml-2 size-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
export const ShowDockerEvents = ({ serverId }: Props) => {
|
export const ShowDockerEvents = ({ serverId }: Props) => {
|
||||||
const [minutes, setMinutes] = useState(15);
|
const [minutes, setMinutes] = useState(15);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
const [sorting, setSorting] = useState<SortingState>([
|
||||||
|
{ id: "time", desc: true },
|
||||||
|
]);
|
||||||
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 15,
|
||||||
|
});
|
||||||
|
|
||||||
const { data, isLoading, isRefetching, refetch, error } =
|
const { data, isLoading, isRefetching, refetch, error } =
|
||||||
api.docker.getEvents.useQuery({
|
api.docker.getEvents.useQuery({
|
||||||
@@ -83,7 +131,14 @@ export const ShowDockerEvents = ({ serverId }: Props) => {
|
|||||||
minutes,
|
minutes,
|
||||||
});
|
});
|
||||||
|
|
||||||
const events = data?.events ?? [];
|
const events = useMemo<EventRow[]>(
|
||||||
|
() =>
|
||||||
|
(data?.events ?? []).map((event, index) => ({
|
||||||
|
...event,
|
||||||
|
key: `${event.time}-${event.Action}-${index}`,
|
||||||
|
})),
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
|
||||||
const filteredEvents = useMemo(() => {
|
const filteredEvents = useMemo(() => {
|
||||||
if (!search.trim()) return events;
|
if (!search.trim()) return events;
|
||||||
@@ -98,6 +153,96 @@ export const ShowDockerEvents = ({ serverId }: Props) => {
|
|||||||
});
|
});
|
||||||
}, [events, search]);
|
}, [events, search]);
|
||||||
|
|
||||||
|
const columns = useMemo<ColumnDef<EventRow>[]>(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
id: "time",
|
||||||
|
accessorFn: (event) => event.time ?? 0,
|
||||||
|
header: ({ column }) => <SortableHeader column={column} title="Time" />,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="text-xs text-muted-foreground whitespace-nowrap">
|
||||||
|
{row.original.time
|
||||||
|
? new Date(row.original.time * 1000).toLocaleTimeString()
|
||||||
|
: "-"}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "type",
|
||||||
|
accessorFn: (event) => event.Type ?? "",
|
||||||
|
header: ({ column }) => <SortableHeader column={column} title="Type" />,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Badge variant={getTypeVariant(row.original.Type)}>
|
||||||
|
{row.original.Type ?? "unknown"}
|
||||||
|
</Badge>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "action",
|
||||||
|
accessorFn: (event) => event.Action ?? "",
|
||||||
|
header: ({ column }) => (
|
||||||
|
<SortableHeader column={column} title="Action" />
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Badge variant={getActionVariant(row.original.Action)}>
|
||||||
|
{row.original.Action ?? "-"}
|
||||||
|
</Badge>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "resource",
|
||||||
|
accessorFn: (event) => getResource(event),
|
||||||
|
header: ({ column }) => (
|
||||||
|
<SortableHeader column={column} title="Resource" />
|
||||||
|
),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const resource = getResource(row.original);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="max-w-[220px] truncate font-mono text-xs"
|
||||||
|
title={resource}
|
||||||
|
>
|
||||||
|
{resource}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "attributes",
|
||||||
|
accessorFn: (event) => getAttributesText(event),
|
||||||
|
header: "Attributes",
|
||||||
|
enableSorting: false,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const attributesText = getAttributesText(row.original);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="max-w-[360px] truncate text-xs text-muted-foreground"
|
||||||
|
title={attributesText}
|
||||||
|
>
|
||||||
|
{attributesText}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: filteredEvents,
|
||||||
|
columns,
|
||||||
|
getRowId: (row) => row.key,
|
||||||
|
state: {
|
||||||
|
sorting,
|
||||||
|
pagination,
|
||||||
|
},
|
||||||
|
onSortingChange: setSorting,
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl">
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl">
|
||||||
@@ -135,7 +280,10 @@ export const ShowDockerEvents = ({ serverId }: Props) => {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Filter by type, action or name..."
|
placeholder="Filter by type, action or name..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => {
|
||||||
|
setSearch(e.target.value);
|
||||||
|
setPagination((prev) => ({ ...prev, pageIndex: 0 }));
|
||||||
|
}}
|
||||||
className="max-w-xs"
|
className="max-w-xs"
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
@@ -157,19 +305,26 @@ export const ShowDockerEvents = ({ serverId }: Props) => {
|
|||||||
<div className="rounded-md border overflow-x-auto">
|
<div className="rounded-md border overflow-x-auto">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
<TableHead className="w-[110px]">Time</TableHead>
|
<TableRow key={headerGroup.id}>
|
||||||
<TableHead className="w-[100px]">Type</TableHead>
|
{headerGroup.headers.map((header) => (
|
||||||
<TableHead className="w-[110px]">Action</TableHead>
|
<TableHead key={header.id}>
|
||||||
<TableHead>Resource</TableHead>
|
{header.isPlaceholder
|
||||||
<TableHead>Attributes</TableHead>
|
? null
|
||||||
</TableRow>
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext(),
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell
|
<TableCell
|
||||||
colSpan={5}
|
colSpan={columns.length}
|
||||||
className="h-24 text-center text-muted-foreground"
|
className="h-24 text-center text-muted-foreground"
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-center gap-2">
|
<div className="flex flex-row items-center justify-center gap-2">
|
||||||
@@ -178,56 +333,23 @@ export const ShowDockerEvents = ({ serverId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : filteredEvents.length ? (
|
) : table.getRowModel().rows?.length ? (
|
||||||
filteredEvents.map((event: DockerEvent, index: number) => {
|
table.getRowModel().rows.map((row) => (
|
||||||
const attributes = Object.entries(
|
<TableRow key={row.id}>
|
||||||
event.Actor?.Attributes ?? {},
|
{row.getVisibleCells().map((cell) => (
|
||||||
).filter(([key]) => key !== "name");
|
<TableCell key={cell.id}>
|
||||||
const attributesText =
|
{flexRender(
|
||||||
attributes
|
cell.column.columnDef.cell,
|
||||||
.map(([key, value]) => `${key}=${value}`)
|
cell.getContext(),
|
||||||
.join(" ") || "-";
|
)}
|
||||||
const resource =
|
|
||||||
event.Actor?.Attributes?.name ?? event.Actor?.ID ?? "-";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow
|
|
||||||
key={`${event.time}-${event.Action}-${index}`}
|
|
||||||
>
|
|
||||||
<TableCell className="text-xs text-muted-foreground whitespace-nowrap">
|
|
||||||
{event.time
|
|
||||||
? new Date(event.time * 1000).toLocaleTimeString()
|
|
||||||
: "-"}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
))}
|
||||||
<Badge variant={getTypeVariant(event.Type)}>
|
</TableRow>
|
||||||
{event.Type ?? "unknown"}
|
))
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge variant={getActionVariant(event.Action)}>
|
|
||||||
{event.Action ?? "-"}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell
|
|
||||||
className="max-w-[220px] truncate font-mono text-xs"
|
|
||||||
title={resource}
|
|
||||||
>
|
|
||||||
{resource}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell
|
|
||||||
className="max-w-[360px] truncate text-xs text-muted-foreground"
|
|
||||||
title={attributesText}
|
|
||||||
>
|
|
||||||
{attributesText}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) : (
|
) : (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell
|
<TableCell
|
||||||
colSpan={5}
|
colSpan={columns.length}
|
||||||
className="h-24 text-center text-muted-foreground"
|
className="h-24 text-center text-muted-foreground"
|
||||||
>
|
>
|
||||||
No events found in the selected time range.
|
No events found in the selected time range.
|
||||||
@@ -237,6 +359,32 @@ export const ShowDockerEvents = ({ serverId }: Props) => {
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
{table.getPageCount() > 1 && (
|
||||||
|
<div className="flex items-center justify-end gap-4">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Page {table.getState().pagination.pageIndex + 1} of{" "}
|
||||||
|
{table.getPageCount()}
|
||||||
|
</span>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user