import { IconArrowsUpDown, IconChevronLeft, IconChevronRight, } from "@tabler/icons-react"; import { useMemo, useState } from "react"; import { Button } from "../ui/button.js"; import { Card, CardContent, CardHeader, CardTitle } from "../ui/card.js"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "../ui/select.js"; import { Skeleton } from "../ui/skeleton.js"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "../ui/table.js"; import { cn } from "../utils.js"; const DEFAULT_PAGE_SIZE_OPTIONS = [10, 25, 50, 100]; const TABLE_MIN_HEIGHT_CLASS = "min-h-[386px]"; export interface DataTableLabels { noData: string; rowsPerPage: string; of: string; previousPage: string; nextPage: string; } export interface DataTableProps { title?: string; data: Record[]; columns?: string[]; loading?: boolean; error?: string; maxRows?: number; pageSizeOptions?: number[]; labels?: Partial; } const DEFAULT_LABELS: DataTableLabels = { noData: "No data available.", rowsPerPage: "Rows per page", of: "of", previousPage: "Previous page", nextPage: "Next page", }; /** A source-agnostic, sortable and paginated dashboard table. */ export function DataTable({ title, data, columns: columnsProp, loading, error, maxRows, pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS, labels: labelOverrides, }: DataTableProps) { const labels = { ...DEFAULT_LABELS, ...labelOverrides }; const [sortColumn, setSortColumn] = useState(null); const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc"); const [page, setPage] = useState(0); const [pageSize, setPageSize] = useState(pageSizeOptions[0] ?? 10); const columns = useMemo(() => { if (columnsProp) return columnsProp; return data.length === 0 ? [] : Object.keys(data[0]); }, [columnsProp, data]); const rows = useMemo( () => maxRows != null && data.length > maxRows ? data.slice(0, maxRows) : data, [data, maxRows], ); const sortedRows = useMemo(() => { if (!sortColumn) return rows; return [...rows].sort((a, b) => { const aValue = a[sortColumn]; const bValue = b[sortColumn]; if (aValue == null && bValue == null) return 0; if (aValue == null) return 1; if (bValue == null) return -1; const comparison = typeof aValue === "number" && typeof bValue === "number" ? aValue - bValue : String(aValue).localeCompare(String(bValue)); return sortDirection === "asc" ? comparison : -comparison; }); }, [rows, sortColumn, sortDirection]); const pageCount = Math.ceil(sortedRows.length / pageSize); const pagedRows = sortedRows.slice(page * pageSize, (page + 1) * pageSize); const handleSort = (column: string) => { if (sortColumn === column) { setSortDirection((direction) => (direction === "asc" ? "desc" : "asc")); } else { setSortColumn(column); setSortDirection("desc"); } setPage(0); }; const content = loading ? ( ) : error ? (

{error}

) : data.length === 0 ? (

{labels.noData}

) : (
{columns.map((column) => ( handleSort(column)} > {column} ))} {pagedRows.map((row, index) => ( {columns.map((column) => ( {formatValue(row[column])} ))} ))}
{sortedRows.length > (pageSizeOptions[0] ?? 10) && (
{labels.rowsPerPage}
{page * pageSize + 1}– {Math.min((page + 1) * pageSize, sortedRows.length)} {labels.of}{" "} {sortedRows.length}
)}
); if (!title) return content; return ( {title} {content} ); } function formatValue(value: unknown): string { if (value == null) return "-"; if (typeof value === "number") return Number.isInteger(value) ? value.toLocaleString() : value.toFixed(4); return typeof value === "object" ? JSON.stringify(value) : String(value); } function DataTableLoadingSkeleton() { const columnWidths = ["w-24", "w-32", "w-20", "w-28"]; return (
{columnWidths.map((width, index) => ( ))}
{Array.from({ length: 10 }).map((_, row) => (
{columnWidths.map((width, column) => ( ))}
))}
); }