/ dashboard-v2 / frontend / src / components / DataTable.jsx
DataTable.jsx
 1  import {
 2    useReactTable,
 3    getCoreRowModel,
 4    getSortedRowModel,
 5    getFilteredRowModel,
 6    flexRender,
 7  } from '@tanstack/react-table';
 8  import { useState } from 'react';
 9  
10  /**
11   * Sortable, filterable data table.
12   * columns: TanStack Table column def array
13   * data: array of row objects
14   * filterPlaceholder: optional search box placeholder
15   * maxRows: optional truncation (shows "and N more" footer)
16   */
17  export default function DataTable({ columns, data = [], filterPlaceholder, maxRows }) {
18    const [sorting, setSorting] = useState([]);
19    const [globalFilter, setGlobalFilter] = useState('');
20  
21    const displayData = maxRows ? data.slice(0, maxRows) : data;
22    const hidden = maxRows ? Math.max(0, data.length - maxRows) : 0;
23  
24    const table = useReactTable({
25      data: displayData,
26      columns,
27      state: { sorting, globalFilter },
28      onSortingChange: setSorting,
29      onGlobalFilterChange: setGlobalFilter,
30      getCoreRowModel: getCoreRowModel(),
31      getSortedRowModel: getSortedRowModel(),
32      getFilteredRowModel: getFilteredRowModel(),
33    });
34  
35    return (
36      <div>
37        {filterPlaceholder && (
38          <input
39            value={globalFilter}
40            onChange={e => setGlobalFilter(e.target.value)}
41            placeholder={filterPlaceholder}
42            className="mb-3 w-full rounded-md border border-slate-700 bg-slate-800 px-3 py-1.5
43                       text-sm text-slate-200 placeholder-slate-500 focus:outline-none focus:border-sky-500"
44          />
45        )}
46        <div className="overflow-x-auto rounded-lg border border-slate-700">
47          <table className="w-full text-sm">
48            <thead className="bg-slate-800 text-slate-400 uppercase text-xs tracking-wider">
49              {table.getHeaderGroups().map(hg => (
50                <tr key={hg.id}>
51                  {hg.headers.map(h => (
52                    <th
53                      key={h.id}
54                      onClick={h.column.getToggleSortingHandler()}
55                      className={`px-4 py-2.5 text-left select-none ${
56                        h.column.getCanSort() ? 'cursor-pointer hover:text-slate-200' : ''
57                      }`}
58                    >
59                      {flexRender(h.column.columnDef.header, h.getContext())}
60                      {{ asc: ' ↑', desc: ' ↓' }[h.column.getIsSorted()] ?? ''}
61                    </th>
62                  ))}
63                </tr>
64              ))}
65            </thead>
66            <tbody className="divide-y divide-slate-700/50">
67              {table.getRowModel().rows.map((row, i) => (
68                <tr key={row.id} className={i % 2 === 0 ? 'bg-slate-900' : 'bg-slate-800/40'}>
69                  {row.getVisibleCells().map(cell => (
70                    <td key={cell.id} className="px-4 py-2.5 text-slate-300">
71                      {flexRender(cell.column.columnDef.cell, cell.getContext())}
72                    </td>
73                  ))}
74                </tr>
75              ))}
76              {table.getRowModel().rows.length === 0 && (
77                <tr>
78                  <td colSpan={columns.length} className="px-4 py-6 text-center text-slate-500">
79                    No data
80                  </td>
81                </tr>
82              )}
83            </tbody>
84          </table>
85        </div>
86        {hidden > 0 && (
87          <p className="mt-2 text-xs text-slate-500 text-right">… and {hidden} more rows</p>
88        )}
89      </div>
90    );
91  }