import { useMemo, useRef, useState, useCallback } from "react"; import { useReactTable, getCoreRowModel, getSortedRowModel, flexRender, type ColumnDef, type SortingState, type ColumnResizeMode, } from "@tanstack/react-table"; import { useVirtualizer } from "@tanstack/react-virtual"; import { ArrowUp, ArrowDown } from "lucide-react"; interface Props { columns: string[]; types: string[]; rows: unknown[][]; onCellDoubleClick?: ( rowIndex: number, colIndex: number, value: unknown ) => void; highlightedCells?: Set; } export function ResultsTable({ columns: colNames, rows, onCellDoubleClick, highlightedCells, }: Props) { const [sorting, setSorting] = useState([]); const [columnResizeMode] = useState("onChange"); const parentRef = useRef(null); const columns = useMemo[]>( () => colNames.map((name, i) => ({ id: name, accessorFn: (row: unknown[]) => row[i], header: name, cell: ({ getValue, row: tableRow }) => { const value = getValue(); const rowIndex = tableRow.index; const key = `${rowIndex}:${i}`; const isHighlighted = highlightedCells?.has(key); return (
onCellDoubleClick?.(rowIndex, i, value) } > {value === null ? "NULL" : String(value)}
); }, size: 150, minSize: 50, maxSize: 800, })), [colNames, onCellDoubleClick, highlightedCells] ); const table = useReactTable({ data: rows, columns, state: { sorting }, onSortingChange: setSorting, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), columnResizeMode, enableColumnResizing: true, }); const { rows: tableRows } = table.getRowModel(); const virtualizer = useVirtualizer({ count: tableRows.length, getScrollElement: () => parentRef.current, estimateSize: () => 28, overscan: 20, }); const columnSizing = table.getState().columnSizing; const getColWidth = useCallback( (colId: string, fallback: number) => columnSizing[colId] ?? fallback, [columnSizing] ); if (colNames.length === 0) return null; return (
{/* Header */}
{table.getHeaderGroups().map((headerGroup) => headerGroup.headers.map((header) => (
{flexRender( header.column.columnDef.header, header.getContext() )} {header.column.getIsSorted() === "asc" && ( )} {header.column.getIsSorted() === "desc" && ( )}
{/* Resize handle */}
header.column.resetSize()} className={`absolute right-0 top-0 h-full w-1 cursor-col-resize select-none touch-none hover:bg-primary ${ header.column.getIsResizing() ? "bg-primary" : "" }`} />
)) )}
{/* Virtual rows */}
{virtualizer.getVirtualItems().map((virtualRow) => { const row = tableRows[virtualRow.index]; return (
{row.getVisibleCells().map((cell) => { const w = getColWidth(cell.column.id, cell.column.getSize()); return (
{flexRender( cell.column.columnDef.cell, cell.getContext() )}
); })}
); })}
); }