data-table.tsx 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. 'use client';
  2. import { DataTablePagination } from '@/components/data-table/data-table-pagination.js';
  3. import { DataTableViewOptions } from '@/components/data-table/data-table-view-options.js';
  4. import { Badge } from '@/components/ui/badge.js';
  5. import { Input } from '@/components/ui/input.js';
  6. import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table.js';
  7. import {
  8. ColumnDef,
  9. ColumnFilter,
  10. ColumnFiltersState,
  11. flexRender,
  12. getCoreRowModel,
  13. getPaginationRowModel,
  14. PaginationState,
  15. SortingState,
  16. Table as TableType,
  17. useReactTable,
  18. VisibilityState,
  19. } from '@tanstack/react-table';
  20. import { TableOptions } from '@tanstack/table-core';
  21. import { CircleX, Filter } from 'lucide-react';
  22. import React, { Suspense, useEffect } from 'react';
  23. import { DataTableFacetedFilter, DataTableFacetedFilterOption } from './data-table-faceted-filter.js';
  24. export interface FacetedFilter {
  25. title: string;
  26. icon?: React.ComponentType<{ className?: string }>;
  27. optionsFn?: () => Promise<DataTableFacetedFilterOption[]>;
  28. options?: DataTableFacetedFilterOption[];
  29. }
  30. interface DataTableProps<TData> {
  31. columns: ColumnDef<TData, any>[];
  32. data: TData[];
  33. totalItems: number;
  34. page?: number;
  35. itemsPerPage?: number;
  36. sorting?: SortingState;
  37. columnFilters?: ColumnFiltersState;
  38. onPageChange?: (table: TableType<TData>, page: number, itemsPerPage: number) => void;
  39. onSortChange?: (table: TableType<TData>, sorting: SortingState) => void;
  40. onFilterChange?: (table: TableType<TData>, columnFilters: ColumnFilter[]) => void;
  41. onSearchTermChange?: (searchTerm: string) => void;
  42. defaultColumnVisibility?: VisibilityState;
  43. facetedFilters?: { [key: string]: FacetedFilter | undefined };
  44. disableViewOptions?: boolean;
  45. /**
  46. * This property allows full control over _all_ features of TanStack Table
  47. * when needed.
  48. */
  49. setTableOptions?: (table: TableOptions<TData>) => TableOptions<TData>;
  50. }
  51. export function DataTable<TData>({
  52. columns,
  53. data,
  54. totalItems,
  55. page,
  56. itemsPerPage,
  57. sorting: sortingInitialState,
  58. columnFilters: filtersInitialState,
  59. onPageChange,
  60. onSortChange,
  61. onFilterChange,
  62. onSearchTermChange,
  63. defaultColumnVisibility,
  64. facetedFilters,
  65. disableViewOptions,
  66. setTableOptions,
  67. }: DataTableProps<TData>) {
  68. const [sorting, setSorting] = React.useState<SortingState>(sortingInitialState || []);
  69. const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(filtersInitialState || []);
  70. const [pagination, setPagination] = React.useState<PaginationState>({
  71. pageIndex: (page ?? 1) - 1,
  72. pageSize: itemsPerPage ?? 10,
  73. });
  74. const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>(
  75. defaultColumnVisibility ?? {},
  76. );
  77. let tableOptions: TableOptions<TData> = {
  78. data,
  79. columns,
  80. getCoreRowModel: getCoreRowModel(),
  81. getPaginationRowModel: getPaginationRowModel(),
  82. manualPagination: true,
  83. manualSorting: true,
  84. manualFiltering: true,
  85. rowCount: totalItems,
  86. onPaginationChange: setPagination,
  87. onSortingChange: setSorting,
  88. onColumnVisibilityChange: setColumnVisibility,
  89. onColumnFiltersChange: setColumnFilters,
  90. state: {
  91. pagination,
  92. sorting,
  93. columnVisibility,
  94. columnFilters,
  95. },
  96. };
  97. if (typeof setTableOptions === 'function') {
  98. tableOptions = setTableOptions(tableOptions);
  99. }
  100. const table = useReactTable(tableOptions);
  101. useEffect(() => {
  102. onPageChange?.(table, pagination.pageIndex + 1, pagination.pageSize);
  103. }, [pagination]);
  104. useEffect(() => {
  105. onSortChange?.(table, sorting);
  106. }, [sorting]);
  107. useEffect(() => {
  108. onFilterChange?.(table, columnFilters);
  109. }, [columnFilters]);
  110. return (
  111. <>
  112. <div className="flex justify-between items-start">
  113. <div className="flex flex-col space-y-2">
  114. <div className="flex items-center justify-start gap-2">
  115. {onSearchTermChange && (
  116. <div className="flex items-center">
  117. <Input
  118. placeholder="Filter..."
  119. onChange={event => onSearchTermChange(event.target.value)}
  120. className="max-w-sm w-md"
  121. />
  122. </div>
  123. )}
  124. <Suspense>
  125. {Object.entries(facetedFilters ?? {}).map(([key, filter]) => (
  126. <DataTableFacetedFilter
  127. key={key}
  128. column={table.getColumn(key)}
  129. title={filter?.title}
  130. options={filter?.options}
  131. optionsFn={filter?.optionsFn}
  132. />
  133. ))}
  134. </Suspense>
  135. </div>
  136. <div className="flex gap-1">
  137. {columnFilters
  138. .filter(f => !facetedFilters?.[f.id])
  139. .map(f => {
  140. const [operator, value] = Object.entries(
  141. f.value as Record<string, string>,
  142. )[0];
  143. return (
  144. <Badge key={f.id} className="flex gap-1 items-center" variant="secondary">
  145. <Filter size="12" className="opacity-50" />
  146. <div>{f.id}</div>
  147. <div>{operator}</div>
  148. <div>{value}</div>
  149. <button
  150. className="cursor-pointer"
  151. onClick={() =>
  152. setColumnFilters(old => old.filter(x => x.id !== f.id))
  153. }
  154. >
  155. <CircleX size="14" />
  156. </button>
  157. </Badge>
  158. );
  159. })}
  160. </div>
  161. </div>
  162. {!disableViewOptions && <DataTableViewOptions table={table} />}
  163. </div>
  164. <div className="rounded-md border my-2">
  165. <Table>
  166. <TableHeader>
  167. {table.getHeaderGroups().map(headerGroup => (
  168. <TableRow key={headerGroup.id}>
  169. {headerGroup.headers.map(header => {
  170. return (
  171. <TableHead key={header.id}>
  172. {header.isPlaceholder
  173. ? null
  174. : flexRender(
  175. header.column.columnDef.header,
  176. header.getContext(),
  177. )}
  178. </TableHead>
  179. );
  180. })}
  181. </TableRow>
  182. ))}
  183. </TableHeader>
  184. <TableBody>
  185. {table.getRowModel().rows?.length ? (
  186. table.getRowModel().rows.map(row => (
  187. <TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
  188. {row.getVisibleCells().map(cell => (
  189. <TableCell key={cell.id}>
  190. {flexRender(cell.column.columnDef.cell, cell.getContext())}
  191. </TableCell>
  192. ))}
  193. </TableRow>
  194. ))
  195. ) : (
  196. <TableRow>
  197. <TableCell colSpan={columns.length} className="h-24 text-center">
  198. No results.
  199. </TableCell>
  200. </TableRow>
  201. )}
  202. </TableBody>
  203. </Table>
  204. </div>
  205. <DataTablePagination table={table} />
  206. </>
  207. );
  208. }