Phase 2: Code-Splitting + Virtual Scrolling (Tasks 2.1-2.7)

This commit is contained in:
Agent Zero
2026-07-23 12:00:34 +02:00
parent 0c14b06b67
commit a8331fbc2b
10 changed files with 1165 additions and 387 deletions
+74 -24
View File
@@ -1,4 +1,4 @@
import React, { useState, useMemo } from 'react';
import React, { useState, useMemo, useRef, useCallback } from 'react';
import {
useReactTable,
getCoreRowModel,
@@ -9,6 +9,7 @@ import {
SortingState,
ColumnFiltersState,
} from '@tanstack/react-table';
import { useVirtualizer } from '@tanstack/react-virtual';
import clsx from 'clsx';
import { Pagination } from '@/components/ui/Pagination';
import { useTranslation } from 'react-i18next';
@@ -30,6 +31,7 @@ export interface DataGridProps<T> {
globalFilter?: string;
onGlobalFilterChange?: (value: string) => void;
testId?: string;
virtualized?: boolean;
}
export function DataGrid<T extends Record<string, any>>({
@@ -44,9 +46,11 @@ export function DataGrid<T extends Record<string, any>>({
loading = false,
enableSorting = true,
testId,
virtualized = true,
}: DataGridProps<T>) {
const { t } = useTranslation();
const [sorting, setSorting] = useState<SortingState>([]);
const scrollRef = useRef<HTMLDivElement>(null);
const table = useReactTable({
data,
@@ -60,11 +64,59 @@ export function DataGrid<T extends Record<string, any>>({
const totalPages = Math.ceil(total / pageSize);
// Auto-skip virtualization for small datasets
const shouldVirtualize = virtualized && data.length >= 50;
const rows = table.getRowModel().rows;
const rowVirtualizer = useVirtualizer({
count: shouldVirtualize ? rows.length : 0,
getScrollElement: () => scrollRef.current,
estimateSize: () => 53, // approx row height
overscan: 10,
enabled: shouldVirtualize,
});
const virtualRows = shouldVirtualize ? rowVirtualizer.getVirtualItems() : [];
const totalHeight = shouldVirtualize ? rowVirtualizer.getTotalSize() : 0;
const paddingTop = virtualRows.length > 0 ? virtualRows[0].start : 0;
const paddingBottom = virtualRows.length > 0 ? totalHeight - virtualRows[virtualRows.length - 1].end : 0;
const renderRow = useCallback(
(row: typeof rows[number]) => (
<tr
key={row.id}
className={clsx(
'hover:bg-secondary-50 motion-safe:transition-colors',
onRowClick && 'cursor-pointer'
)}
onClick={onRowClick ? () => onRowClick(row.original) : undefined}
tabIndex={onRowClick ? 0 : undefined}
onKeyDown={onRowClick ? (e) => {
if (e.key === 'Enter') onRowClick(row.original);
} : undefined}
>
{row.getVisibleCells().map((cell) => (
<td key={cell.id} className="px-6 py-4 text-sm text-secondary-900">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
),
[onRowClick]
);
return (
<div className="bg-white rounded-lg shadow-sm border border-secondary-200 overflow-hidden" data-testid={testId}>
<div className="overflow-x-auto" role="region" aria-label="Data grid">
<div
ref={scrollRef}
className="overflow-x-auto"
role="region"
aria-label="Data grid"
style={shouldVirtualize ? { maxHeight: '70vh', overflowY: 'auto' } : undefined}
>
<table className="min-w-full divide-y divide-secondary-200">
<thead className="bg-secondary-50">
<thead className="bg-secondary-50 sticky top-0 z-10">
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => {
@@ -114,33 +166,31 @@ export function DataGrid<T extends Record<string, any>>({
</span>
</td>
</tr>
) : table.getRowModel().rows.length === 0 ? (
) : rows.length === 0 ? (
<tr>
<td colSpan={columns.length} className="px-6 py-8 text-center text-secondary-500">
{emptyMessage || t('table.empty')}
</td>
</tr>
) : shouldVirtualize ? (
<>
{paddingTop > 0 && (
<tr key="__padding-top" style={{ height: paddingTop }}>
<td colSpan={columns.length} style={{ padding: 0, border: 'none' }} />
</tr>
)}
{virtualRows.map((virtualRow) => {
const row = rows[virtualRow.index];
return renderRow(row);
})}
{paddingBottom > 0 && (
<tr key="__padding-bottom" style={{ height: paddingBottom }}>
<td colSpan={columns.length} style={{ padding: 0, border: 'none' }} />
</tr>
)}
</>
) : (
table.getRowModel().rows.map((row) => (
<tr
key={row.id}
className={clsx(
'hover:bg-secondary-50 motion-safe:transition-colors',
onRowClick && 'cursor-pointer'
)}
onClick={onRowClick ? () => onRowClick(row.original) : undefined}
tabIndex={onRowClick ? 0 : undefined}
onKeyDown={onRowClick ? (e) => {
if (e.key === 'Enter') onRowClick(row.original);
} : undefined}
>
{row.getVisibleCells().map((cell) => (
<td key={cell.id} className="px-6 py-4 text-sm text-secondary-900">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))
rows.map((row) => renderRow(row))
)}
</tbody>
</table>