added react-virtuloso and tenstack-table

This commit is contained in:
Boki 2025-06-15 21:08:58 -04:00
parent 3a9d45c543
commit 8e01d523d0
8 changed files with 161 additions and 227 deletions

View file

@ -9,15 +9,13 @@ import {
SortingState,
useReactTable,
} from '@tanstack/react-table';
import React, { useCallback, useMemo, useState } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { VariableSizeGrid as Grid } from 'react-window';
import { useState } from 'react';
import { TableVirtuoso } from 'react-virtuoso';
interface DataTableProps<T> {
data: T[];
columns: ColumnDef<T>[];
rowHeight?: number;
headerHeight?: number;
height?: number;
loading?: boolean;
onRowClick?: (row: T) => void;
className?: string;
@ -26,8 +24,7 @@ interface DataTableProps<T> {
export function DataTable<T>({
data,
columns,
rowHeight = 35,
headerHeight = 40,
height = 500,
loading = false,
onRowClick,
className = '',
@ -52,91 +49,6 @@ export function DataTable<T>({
});
const { rows } = table.getRowModel();
const visibleColumns = table.getVisibleFlatColumns();
// Calculate column widths as numbers for react-window
const columnWidths = useMemo(() => {
return visibleColumns.map(column => column.getSize());
}, [visibleColumns]);
// Unified cell renderer that handles both header and data rows
const Cell = useCallback(
({
columnIndex,
rowIndex,
style,
}: {
columnIndex: number;
rowIndex: number;
style: React.CSSProperties;
}) => {
const column = visibleColumns[columnIndex];
// Header row (rowIndex 0) - make it sticky
if (rowIndex === 0) {
return (
<div
style={{
...style,
width: columnWidths[columnIndex],
position: 'sticky',
top: 0,
zIndex: 10,
}}
className={cn(
'flex items-center justify-between px-3 py-2 text-xs font-medium text-text-secondary uppercase tracking-wider',
'border-r border-b border-border bg-surface hover:bg-surface-secondary',
column.getCanSort() && 'cursor-pointer select-none'
)}
onClick={column.getToggleSortingHandler()}
>
<span className="truncate">
{typeof column.columnDef.header === 'string' ? column.columnDef.header : column.id}
</span>
{column.getCanSort() && (
<div className="ml-2 flex-shrink-0">
{column.getIsSorted() === 'asc' ? (
<ChevronUpIcon className="h-4 w-4 text-primary-400" />
) : column.getIsSorted() === 'desc' ? (
<ChevronDownIcon className="h-4 w-4 text-primary-400" />
) : (
<div className="h-4 w-4" />
)}
</div>
)}
</div>
);
}
// Data rows (rowIndex > 0)
const row = rows[rowIndex - 1]; // Subtract 1 because row 0 is header
const cell = row?.getVisibleCells()[columnIndex];
if (!cell || !column) {
return <div style={style} className="border-r border-border bg-background" />;
}
return (
<div
style={{
...style,
width: columnWidths[columnIndex],
}}
className={cn(
'flex items-center px-3 py-2 text-sm text-text-primary border-r border-border bg-background',
'hover:bg-surface cursor-pointer'
)}
onClick={() => onRowClick?.(row.original)}
>
<div className="truncate w-full">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</div>
</div>
);
},
[rows, visibleColumns, onRowClick, columnWidths]
);
if (loading) {
return (
@ -147,40 +59,103 @@ export function DataTable<T>({
}
return (
<div className={cn('bg-background text-text-primary w-full h-full flex flex-col', className)}>
<div className={cn('bg-background text-text-primary', className)}>
{/* Search */}
<div className="p-4 border-b border-border flex-shrink-0">
<div className="p-4 border-b border-border">
<input
type="text"
value={globalFilter}
onChange={e => setGlobalFilter(e.target.value)}
onChange={(e) => setGlobalFilter(e.target.value)}
placeholder="Search..."
className="w-full max-w-md bg-surface border border-border rounded px-3 py-2 text-sm text-text-primary placeholder-text-muted focus:ring-1 focus:ring-primary-500 focus:border-primary-500"
/>
</div>
{/* Virtual Grid with AutoSizer - includes sticky header */}
<div className="flex-1 border border-border rounded-lg overflow-hidden">
<AutoSizer>
{({ height: autoHeight, width: autoWidth }) => (
<Grid
height={autoHeight}
width={autoWidth}
rowCount={rows.length + 1} // +1 for header row
columnCount={visibleColumns.length}
rowHeight={index => (index === 0 ? headerHeight : rowHeight)} // Header height for row 0
columnWidth={index => columnWidths[index] || 150}
overscanRowCount={5}
overscanColumnCount={2}
>
{Cell}
</Grid>
)}
</AutoSizer>
</div>
{/* Virtualized Table */}
<TableVirtuoso
style={{ height: `${height}px` }}
className="border border-border rounded-lg"
totalCount={rows.length}
components={{
Table: ({ style, ...props }) => (
<table
{...props}
style={{
...style,
minWidth: '100%',
tableLayout: 'auto',
borderCollapse: 'collapse',
borderSpacing: 0,
}}
className="bg-background"
/>
),
TableRow: (props) => {
const index = props['data-index'] as number;
const row = rows[index];
return (
<tr
{...props}
className="hover:bg-surface cursor-pointer border-b border-border"
onClick={() => onRowClick?.(row.original)}
>
{row.getVisibleCells().map((cell) => (
<td
key={cell.id}
className="px-3 py-2 text-sm text-text-primary border-r border-border"
style={{ width: cell.column.getSize() }}
>
<div className="truncate">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</div>
</td>
))}
</tr>
);
},
}}
fixedHeaderContent={() =>
table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id} className="bg-surface border-b border-border">
{headerGroup.headers.map((header) => (
<th
key={header.id}
colSpan={header.colSpan}
className={cn(
'px-3 py-2 text-xs font-medium text-text-secondary uppercase tracking-wider text-left border-r border-border',
header.column.getCanSort() && 'cursor-pointer select-none hover:bg-surface-secondary'
)}
style={{ width: header.getSize() }}
onClick={header.column.getToggleSortingHandler()}
>
{header.isPlaceholder ? null : (
<div className="flex items-center justify-between">
<span className="truncate">
{flexRender(header.column.columnDef.header, header.getContext())}
</span>
{header.column.getCanSort() && (
<div className="ml-2 flex-shrink-0">
{header.column.getIsSorted() === 'asc' ? (
<ChevronUpIcon className="h-4 w-4 text-primary-400" />
) : header.column.getIsSorted() === 'desc' ? (
<ChevronDownIcon className="h-4 w-4 text-primary-400" />
) : (
<div className="h-4 w-4" />
)}
</div>
)}
</div>
)}
</th>
))}
</tr>
))
}
/>
{/* Footer */}
<div className="flex items-center justify-between mt-2 px-2 text-sm text-text-secondary flex-shrink-0">
<div className="flex items-center justify-between mt-2 px-2 text-sm text-text-secondary">
<span>
Showing {rows.length} of {data.length} rows
</span>