removed angular app and switched to react
This commit is contained in:
parent
56e3938561
commit
3a9d45c543
101 changed files with 2697 additions and 4075 deletions
191
apps/web/src/components/ui/DataTable/DataTable.tsx
Normal file
191
apps/web/src/components/ui/DataTable/DataTable.tsx
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
import { cn } from '@/lib/utils';
|
||||
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/outline';
|
||||
import {
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getSortedRowModel,
|
||||
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';
|
||||
|
||||
interface DataTableProps<T> {
|
||||
data: T[];
|
||||
columns: ColumnDef<T>[];
|
||||
rowHeight?: number;
|
||||
headerHeight?: number;
|
||||
loading?: boolean;
|
||||
onRowClick?: (row: T) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function DataTable<T>({
|
||||
data,
|
||||
columns,
|
||||
rowHeight = 35,
|
||||
headerHeight = 40,
|
||||
loading = false,
|
||||
onRowClick,
|
||||
className = '',
|
||||
}: DataTableProps<T>) {
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [globalFilter, setGlobalFilter] = useState('');
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
state: {
|
||||
sorting,
|
||||
globalFilter,
|
||||
},
|
||||
onSortingChange: setSorting,
|
||||
onGlobalFilterChange: setGlobalFilter,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
enableSorting: true,
|
||||
enableGlobalFilter: true,
|
||||
});
|
||||
|
||||
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 (
|
||||
<div className="flex items-center justify-center h-64 bg-background border border-border rounded-lg">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('bg-background text-text-primary w-full h-full flex flex-col', className)}>
|
||||
{/* Search */}
|
||||
<div className="p-4 border-b border-border flex-shrink-0">
|
||||
<input
|
||||
type="text"
|
||||
value={globalFilter}
|
||||
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>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between mt-2 px-2 text-sm text-text-secondary flex-shrink-0">
|
||||
<span>
|
||||
Showing {rows.length} of {data.length} rows
|
||||
</span>
|
||||
{globalFilter && <span>Filtered by: "{globalFilter}"</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue