import { buildPinStyle, type ColumnConfigEntry, DraggableColumnHeader, PIN_SHADOW_LEFT, PIN_SHADOW_RIGHT, useHeaderCellRefs, usePinOffsets, } from "@/components/table"; import { Button } from "@/components/ui/button"; import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table"; import { useTablePageSize } from "@/hooks/useTablePageSize"; import type { LogEntry, Pagination } from "@/lib/types/logs"; import { cn } from "@/lib/utils"; import type { ColumnOrderState, ColumnPinningState, VisibilityState } from "@tanstack/react-table"; import { ColumnDef, flexRender, getCoreRowModel, SortingState, useReactTable } from "@tanstack/react-table"; import { ChevronLeft, ChevronRight, Loader2, RefreshCw } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; interface DataTableProps { columns: ColumnDef[]; data: LogEntry[]; totalItems: number; pagination: Pagination; onPaginationChange: (pagination: Pagination) => void; onRowClick?: (log: LogEntry, columnId: string) => void; polling: boolean; loading?: boolean; onRefresh: () => void; /** Column config — computed by the parent via useColumnConfig */ columnEntries: ColumnConfigEntry[]; columnOrder: ColumnOrderState; columnVisibility: VisibilityState; columnPinning: ColumnPinningState; onToggleColumnVisibility: (id: string) => void; onTogglePin: (id: string, side: "left" | "right") => void; onReorderColumns: (entries: ColumnConfigEntry[]) => void; } export function LogsDataTable({ columns, data, totalItems, pagination, onPaginationChange, onRowClick, polling, loading, onRefresh, columnEntries, columnOrder, columnVisibility, columnPinning, onToggleColumnVisibility, onTogglePin, onReorderColumns, }: DataTableProps) { const [sorting, setSorting] = useState([{ id: pagination.sort_by, desc: pagination.order === "desc" }]); const tableContainerRef = useRef(null); const calculatedPageSize = useTablePageSize(tableContainerRef); const fixedColumnIds = useMemo(() => new Set([]), []); // Measure actual header cell widths for pixel-perfect pin offsets const { headerCellRefs, setHeaderCellRef } = useHeaderCellRefs(); const pinOffsets = usePinOffsets(headerCellRefs, columnPinning); // Shadow on the edge of pinned groups const lastLeftPinId = columnPinning.left?.at(-1); const firstRightPinId = columnPinning.right?.at(0); // Handle native drag-and-drop reorder const handleColumnDrop = useCallback( (draggedId: string, targetId: string) => { const newEntries = [...columnEntries]; const draggedIdx = newEntries.findIndex((e) => e.id === draggedId); const targetIdx = newEntries.findIndex((e) => e.id === targetId); if (draggedIdx === -1 || targetIdx === -1) return; const [moved] = newEntries.splice(draggedIdx, 1); newEntries.splice(targetIdx, 0, moved); onReorderColumns(newEntries); }, [columnEntries, onReorderColumns], ); // Refs to avoid stale closures in the page size effect const paginationRef = useRef(pagination); const onPaginationChangeRef = useRef(onPaginationChange); paginationRef.current = pagination; onPaginationChangeRef.current = onPaginationChange; useEffect(() => { if (calculatedPageSize && calculatedPageSize > paginationRef.current.limit) { onPaginationChangeRef.current({ ...paginationRef.current, limit: calculatedPageSize, offset: 0, }); } }, [calculatedPageSize]); const handleSortingChange = (updaterOrValue: SortingState | ((old: SortingState) => SortingState)) => { const newSorting = typeof updaterOrValue === "function" ? updaterOrValue(sorting) : updaterOrValue; setSorting(newSorting); if (newSorting.length > 0) { const { id, desc } = newSorting[0]; onPaginationChange({ ...pagination, sort_by: id as "timestamp" | "latency" | "tokens" | "cost", order: desc ? "desc" : "asc", }); } }; const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), manualPagination: true, manualSorting: true, manualFiltering: true, pageCount: Math.ceil(totalItems / pagination.limit), state: { sorting, columnOrder, columnVisibility, columnPinning, }, onSortingChange: handleSortingChange, }); const hasItems = totalItems > 0; const currentPage = hasItems ? Math.floor(pagination.offset / pagination.limit) + 1 : 0; const totalPages = hasItems ? Math.ceil(totalItems / pagination.limit) : 0; const startItem = hasItems ? pagination.offset + 1 : 0; const endItem = hasItems ? Math.min(pagination.offset + pagination.limit, totalItems) : 0; const goToPage = (page: number) => { const newOffset = (page - 1) * pagination.limit; onPaginationChange({ ...pagination, offset: newOffset, }); }; return (
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( ))} ))}
{polling ? ( <> Waiting for new logs... ) : ( )}
{table.getRowModel().rows.length ? ( table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => { const pinned = cell.column.getIsPinned(); const size = cell.column.getSize(); return ( onRowClick?.(row.original, cell.column.id)} key={cell.id} style={{ width: size, minWidth: size, maxWidth: size, ...buildPinStyle(cell.column, pinOffsets), }} className={cn( "py-1.5 align-middle", pinned && "bg-card", cell.column.id === lastLeftPinId && PIN_SHADOW_LEFT, cell.column.id === firstRightPinId && PIN_SHADOW_RIGHT, "group-hover/table-row:bg-[#f7f7f7] dark:group-hover/table-row:bg-[#232327]", )} > {flexRender(cell.column.columnDef.cell, cell.getContext())} ); })} )) ) : ( No results found. Try adjusting your filters and/or time range. )}
{/* Pagination Footer */}
{startItem.toLocaleString()}-{endItem.toLocaleString()} of {totalItems.toLocaleString()} entries
Page {currentPage} of {totalPages}
); }