datool 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +218 -0
  2. package/client-dist/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
  3. package/client-dist/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
  4. package/client-dist/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
  5. package/client-dist/assets/index-BeRNeRUq.css +1 -0
  6. package/client-dist/assets/index-uoZ4c_I8.js +164 -0
  7. package/client-dist/index.html +13 -0
  8. package/index.html +12 -0
  9. package/package.json +55 -0
  10. package/src/client/App.tsx +885 -0
  11. package/src/client/components/connection-status.tsx +43 -0
  12. package/src/client/components/data-table-cell.tsx +235 -0
  13. package/src/client/components/data-table-col-icon.tsx +73 -0
  14. package/src/client/components/data-table-header-col.tsx +225 -0
  15. package/src/client/components/data-table-search-input.tsx +729 -0
  16. package/src/client/components/data-table.tsx +2014 -0
  17. package/src/client/components/stream-controls.tsx +157 -0
  18. package/src/client/components/theme-provider.tsx +230 -0
  19. package/src/client/components/ui/button.tsx +68 -0
  20. package/src/client/components/ui/combobox.tsx +308 -0
  21. package/src/client/components/ui/context-menu.tsx +261 -0
  22. package/src/client/components/ui/dropdown-menu.tsx +267 -0
  23. package/src/client/components/ui/input-group.tsx +153 -0
  24. package/src/client/components/ui/input.tsx +19 -0
  25. package/src/client/components/ui/textarea.tsx +18 -0
  26. package/src/client/components/viewer-settings.tsx +185 -0
  27. package/src/client/index.css +192 -0
  28. package/src/client/lib/data-table-search.ts +750 -0
  29. package/src/client/lib/datool-icons.ts +37 -0
  30. package/src/client/lib/datool-url-state.ts +159 -0
  31. package/src/client/lib/filterable-table.ts +146 -0
  32. package/src/client/lib/table-search-persistence.ts +94 -0
  33. package/src/client/lib/utils.ts +6 -0
  34. package/src/client/main.tsx +14 -0
  35. package/src/index.ts +19 -0
  36. package/src/node/cli.ts +54 -0
  37. package/src/node/config.ts +231 -0
  38. package/src/node/lines.ts +82 -0
  39. package/src/node/runtime.ts +102 -0
  40. package/src/node/server.ts +403 -0
  41. package/src/node/sources/command.ts +82 -0
  42. package/src/node/sources/file.ts +116 -0
  43. package/src/node/sources/ssh.ts +59 -0
  44. package/src/shared/columns.ts +41 -0
  45. package/src/shared/types.ts +188 -0
@@ -0,0 +1,2014 @@
1
+ /* eslint-disable react-hooks/incompatible-library, react-refresh/only-export-components */
2
+ import {
3
+ functionalUpdate,
4
+ getCoreRowModel,
5
+ getFilteredRowModel,
6
+ getSortedRowModel,
7
+ useReactTable,
8
+ type ColumnDef,
9
+ type ColumnFiltersState,
10
+ type FilterFn,
11
+ type OnChangeFn,
12
+ type Row,
13
+ type RowData,
14
+ type RowSelectionState,
15
+ type SortingState,
16
+ type ColumnSizingState,
17
+ type VisibilityState,
18
+ } from "@tanstack/react-table"
19
+ import { useVirtualizer } from "@tanstack/react-virtual"
20
+ import {
21
+ Check,
22
+ CircleAlert,
23
+ EyeOff,
24
+ LayoutGrid,
25
+ LoaderCircle,
26
+ Search,
27
+ SlidersHorizontal,
28
+ Sparkles,
29
+ X,
30
+ } from "lucide-react"
31
+ import * as React from "react"
32
+ import { useDeferredValue } from "react"
33
+
34
+ import {
35
+ DataTableBodyCell,
36
+ DataTableCheckbox,
37
+ fallbackCellValue,
38
+ } from "./data-table-cell"
39
+ import {
40
+ DataTableHeaderCol,
41
+ type DataTableAlign,
42
+ type DataTableColumnMeta,
43
+ } from "./data-table-header-col"
44
+ import {
45
+ DataTableColIcon,
46
+ inferDataTableColumnKind,
47
+ type DataTableColumnKind,
48
+ } from "./data-table-col-icon"
49
+ import { Button } from "@/components/ui/button"
50
+ import {
51
+ ContextMenu,
52
+ ContextMenuContent,
53
+ ContextMenuItem,
54
+ ContextMenuLabel,
55
+ ContextMenuSeparator,
56
+ ContextMenuSub,
57
+ ContextMenuSubContent,
58
+ ContextMenuSubTrigger,
59
+ ContextMenuShortcut,
60
+ ContextMenuTrigger,
61
+ } from "@/components/ui/context-menu"
62
+ import {
63
+ getColumnHighlightTerms,
64
+ parseSearchQuery,
65
+ type DataTableSearchField,
66
+ } from "@/lib/data-table-search"
67
+ import {
68
+ buildTableSearchFields,
69
+ withColumnSearchFilters,
70
+ } from "@/lib/filterable-table"
71
+ import {
72
+ readPersistedSearch,
73
+ type SearchStatePersistence,
74
+ writePersistedSearch,
75
+ } from "@/lib/table-search-persistence"
76
+ import { cn } from "@/lib/utils"
77
+
78
+ type DataTableRow = Record<string, unknown>
79
+
80
+ export type DataTableRowActionScope = "row" | "selection"
81
+
82
+ export type DataTableRowActionButtonVariant =
83
+ | "default"
84
+ | "outline"
85
+ | "secondary"
86
+ | "ghost"
87
+ | "destructive"
88
+ | "link"
89
+
90
+ export type DataTableRowActionButtonSize =
91
+ | "default"
92
+ | "xs"
93
+ | "sm"
94
+ | "lg"
95
+ | "xl"
96
+ | "icon"
97
+ | "icon-xs"
98
+ | "icon-sm"
99
+ | "icon-lg"
100
+ | "icon-xl"
101
+
102
+ export type DataTableRowActionButtonConfig =
103
+ | false
104
+ | DataTableRowActionButtonVariant
105
+ | {
106
+ className?: string
107
+ label?: string
108
+ size?: DataTableRowActionButtonSize
109
+ variant?: DataTableRowActionButtonVariant
110
+ }
111
+
112
+ export type DataTableRowActionContext<TData extends DataTableRow> = {
113
+ actionRowIds: string[]
114
+ actionRows: TData[]
115
+ anchorRow: TData
116
+ anchorRowId: string
117
+ selectedRowIds: string[]
118
+ selectedRows: TData[]
119
+ }
120
+
121
+ type DataTableRowActionChildren<TData extends DataTableRow> =
122
+ | DataTableRowAction<TData>[]
123
+ | ((context: DataTableRowActionContext<TData>) => DataTableRowAction<TData>[])
124
+
125
+ export type DataTableRowAction<TData extends DataTableRow> = {
126
+ button?: DataTableRowActionButtonConfig
127
+ disabled?: boolean | ((context: DataTableRowActionContext<TData>) => boolean)
128
+ hidden?: boolean | ((context: DataTableRowActionContext<TData>) => boolean)
129
+ icon?: React.ComponentType<{ className?: string }>
130
+ id: string
131
+ items?: DataTableRowActionChildren<TData>
132
+ label: string | ((context: DataTableRowActionContext<TData>) => string)
133
+ onSelect?: (
134
+ context: DataTableRowActionContext<TData>
135
+ ) => Promise<unknown> | unknown
136
+ scope?: DataTableRowActionScope
137
+ shortcut?: string
138
+ variant?: "default" | "destructive"
139
+ }
140
+
141
+ export type DataTableColumnConfig<TData extends DataTableRow> = {
142
+ accessorFn?: (row: TData) => unknown
143
+ accessorKey?: Extract<keyof TData, string>
144
+ align?: DataTableAlign
145
+ cell?: (args: { row: TData; value: unknown }) => React.ReactNode
146
+ cellClassName?: string
147
+ enableFiltering?: boolean
148
+ enableHiding?: boolean
149
+ enableResizing?: boolean
150
+ enableSorting?: boolean
151
+ filterFn?: FilterFn<TData>
152
+ header?: string
153
+ headerClassName?: string
154
+ highlightMatches?: boolean
155
+ id?: string
156
+ kind?: DataTableColumnKind
157
+ maxWidth?: number
158
+ minWidth?: number
159
+ truncate?: boolean
160
+ width?: number
161
+ }
162
+
163
+ export type DataTableProps<TData extends DataTableRow> = {
164
+ autoScrollToBottom?: boolean
165
+ autoScrollToBottomThreshold?: number
166
+ columnFilters?: ColumnFiltersState
167
+ columnVisibility?: VisibilityState
168
+ columns?: DataTableColumnConfig<TData>[]
169
+ data: TData[]
170
+ edgeHorizontalPadding?: React.CSSProperties["paddingLeft"]
171
+ enableRowSelection?: boolean
172
+ filterPlaceholder?: string
173
+ globalFilter?: string
174
+ getRowId?: (row: TData, index: number) => string
175
+ height?: React.CSSProperties["height"]
176
+ highlightQuery?: string
177
+ id: string
178
+ onColumnFiltersChange?: (value: ColumnFiltersState) => void
179
+ onColumnVisibilityChange?: (value: VisibilityState) => void
180
+ onGlobalFilterChange?: (value: string) => void
181
+ resolveColumnHighlightTerms?: (columnId: string, query: string) => string[]
182
+ rowActions?: DataTableRowAction<TData>[]
183
+ rowClassName?: (row: TData) => string | undefined
184
+ rowHeight?: number
185
+ statePersistence?: "localStorage" | "none" | "url"
186
+ }
187
+
188
+ export type DataTableProviderProps<TData extends DataTableRow> = Omit<
189
+ DataTableProps<TData>,
190
+ | "columnFilters"
191
+ | "columns"
192
+ | "data"
193
+ | "globalFilter"
194
+ | "highlightQuery"
195
+ | "id"
196
+ | "onColumnFiltersChange"
197
+ | "onGlobalFilterChange"
198
+ | "resolveColumnHighlightTerms"
199
+ | "statePersistence"
200
+ > & {
201
+ children: React.ReactNode
202
+ columns: DataTableColumnConfig<TData>[]
203
+ data: TData[]
204
+ fieldOptions?: Partial<Record<string, string[]>>
205
+ id: string
206
+ onSearchChange?: (value: string) => void
207
+ persistSearch?: boolean
208
+ search?: string
209
+ searchPersistence?: SearchStatePersistence | "none"
210
+ statePersistence?: DataTableProps<TData>["statePersistence"]
211
+ }
212
+
213
+ type DataTableContextValue<TData extends DataTableRow> = {
214
+ search: string
215
+ searchFields: DataTableSearchField<TData>[]
216
+ setSearch: (value: string) => void
217
+ tableProps: DataTableProps<TData>
218
+ }
219
+
220
+ type PersistedTableState = {
221
+ columnFilters?: ColumnFiltersState
222
+ columnSizing?: ColumnSizingState
223
+ highlightedColumns?: Record<string, boolean>
224
+ columnVisibility?: VisibilityState
225
+ globalFilter?: string
226
+ sorting?: SortingState
227
+ }
228
+
229
+ const LOCAL_STORAGE_PREFIX = "datatable:"
230
+ const URL_PARAM_PREFIX = "datatable-"
231
+ const DataTableContext =
232
+ React.createContext<DataTableContextValue<DataTableRow> | null>(null)
233
+
234
+ export function useOptionalDataTableContext<TData extends DataTableRow>() {
235
+ return React.useContext(
236
+ DataTableContext
237
+ ) as DataTableContextValue<TData> | null
238
+ }
239
+
240
+ export function useDataTableContext<TData extends DataTableRow>() {
241
+ const context = useOptionalDataTableContext<TData>()
242
+
243
+ if (!context) {
244
+ throw new Error(
245
+ "useDataTableContext must be used inside DataTableProvider."
246
+ )
247
+ }
248
+
249
+ return context
250
+ }
251
+
252
+ export function DataTableProvider<TData extends DataTableRow>({
253
+ children,
254
+ columns,
255
+ data,
256
+ fieldOptions,
257
+ id,
258
+ onSearchChange,
259
+ persistSearch = false,
260
+ search: controlledSearch,
261
+ searchPersistence,
262
+ statePersistence,
263
+ ...tableProps
264
+ }: DataTableProviderProps<TData>) {
265
+ const resolvedSearchPersistence =
266
+ searchPersistence ?? (persistSearch ? "localStorage" : "none")
267
+ const isSearchControlled = controlledSearch !== undefined
268
+ const [search, setSearch] = React.useState(() =>
269
+ resolvedSearchPersistence === "none"
270
+ ? ""
271
+ : readPersistedSearch(id, resolvedSearchPersistence)
272
+ )
273
+
274
+ const resolvedSearch = controlledSearch ?? search
275
+ const handleSearchChange = React.useCallback(
276
+ (value: string) => {
277
+ if (!isSearchControlled) {
278
+ setSearch(value)
279
+ }
280
+
281
+ onSearchChange?.(value)
282
+ },
283
+ [isSearchControlled, onSearchChange]
284
+ )
285
+
286
+ React.useEffect(() => {
287
+ if (isSearchControlled || resolvedSearchPersistence === "none") {
288
+ setSearch("")
289
+ return
290
+ }
291
+
292
+ setSearch(readPersistedSearch(id, resolvedSearchPersistence))
293
+ }, [id, isSearchControlled, resolvedSearchPersistence])
294
+
295
+ React.useEffect(() => {
296
+ if (isSearchControlled || resolvedSearchPersistence === "none") {
297
+ return
298
+ }
299
+
300
+ writePersistedSearch(id, resolvedSearchPersistence, search)
301
+ }, [id, isSearchControlled, resolvedSearchPersistence, search])
302
+
303
+ const searchFields = React.useMemo(
304
+ () =>
305
+ buildTableSearchFields(columns, data, {
306
+ fieldOptions,
307
+ }),
308
+ [columns, data, fieldOptions]
309
+ )
310
+ const parsedSearch = React.useMemo(
311
+ () => parseSearchQuery(resolvedSearch, searchFields),
312
+ [resolvedSearch, searchFields]
313
+ )
314
+ const resolvedColumns = React.useMemo(
315
+ () => withColumnSearchFilters(columns, searchFields),
316
+ [columns, searchFields]
317
+ )
318
+ const resolvedTableProps = React.useMemo(
319
+ () =>
320
+ ({
321
+ ...tableProps,
322
+ columnFilters: parsedSearch.columnFilters,
323
+ columns: resolvedColumns,
324
+ data,
325
+ globalFilter: parsedSearch.globalFilter,
326
+ highlightQuery: resolvedSearch,
327
+ id,
328
+ resolveColumnHighlightTerms: (columnId: string, query: string) =>
329
+ getColumnHighlightTerms(query, columnId, searchFields),
330
+ statePersistence:
331
+ statePersistence ??
332
+ (resolvedSearchPersistence === "none"
333
+ ? "none"
334
+ : resolvedSearchPersistence),
335
+ }) satisfies DataTableProps<TData>,
336
+ [
337
+ data,
338
+ id,
339
+ parsedSearch.columnFilters,
340
+ parsedSearch.globalFilter,
341
+ resolvedColumns,
342
+ resolvedSearchPersistence,
343
+ resolvedSearch,
344
+ searchFields,
345
+ statePersistence,
346
+ tableProps,
347
+ ]
348
+ )
349
+ const contextValue = React.useMemo(
350
+ () =>
351
+ ({
352
+ search: resolvedSearch,
353
+ searchFields,
354
+ setSearch: handleSearchChange,
355
+ tableProps: resolvedTableProps,
356
+ }) satisfies DataTableContextValue<TData>,
357
+ [handleSearchChange, resolvedSearch, resolvedTableProps, searchFields]
358
+ )
359
+
360
+ return (
361
+ <DataTableContext.Provider
362
+ value={contextValue as DataTableContextValue<DataTableRow>}
363
+ >
364
+ {children}
365
+ </DataTableContext.Provider>
366
+ )
367
+ }
368
+
369
+ function formatHeaderLabel(key: string) {
370
+ return key
371
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
372
+ .replace(/[_-]+/g, " ")
373
+ .replace(/\s+/g, " ")
374
+ .trim()
375
+ .replace(/\b\w/g, (match) => match.toUpperCase())
376
+ }
377
+
378
+ function stringifyFilterValue(value: unknown) {
379
+ if (value === null || value === undefined) {
380
+ return ""
381
+ }
382
+
383
+ if (value instanceof Date) {
384
+ return value.toISOString()
385
+ }
386
+
387
+ if (typeof value === "object") {
388
+ return JSON.stringify(value)
389
+ }
390
+
391
+ return String(value)
392
+ }
393
+
394
+ function getPersistedUrlParam(id: string) {
395
+ return `${URL_PARAM_PREFIX}${id}`
396
+ }
397
+
398
+ function isPersistedStateEmpty(state: PersistedTableState) {
399
+ return (
400
+ (state.sorting?.length ?? 0) === 0 &&
401
+ (state.columnFilters?.length ?? 0) === 0 &&
402
+ Object.keys(state.highlightedColumns ?? {}).length === 0 &&
403
+ Object.keys(state.columnVisibility ?? {}).length === 0 &&
404
+ Object.keys(state.columnSizing ?? {}).length === 0 &&
405
+ !state.globalFilter
406
+ )
407
+ }
408
+
409
+ function readPersistedState(
410
+ id: string,
411
+ statePersistence: DataTableProps<DataTableRow>["statePersistence"]
412
+ ): PersistedTableState {
413
+ if (typeof window === "undefined") {
414
+ return {}
415
+ }
416
+
417
+ try {
418
+ if (statePersistence === "none") {
419
+ return {}
420
+ }
421
+
422
+ const rawValue =
423
+ statePersistence === "url"
424
+ ? new URL(window.location.href).searchParams.get(
425
+ getPersistedUrlParam(id)
426
+ )
427
+ : window.localStorage.getItem(`${LOCAL_STORAGE_PREFIX}${id}`)
428
+
429
+ return rawValue ? (JSON.parse(rawValue) as PersistedTableState) : {}
430
+ } catch {
431
+ return {}
432
+ }
433
+ }
434
+
435
+ function writePersistedState(
436
+ id: string,
437
+ statePersistence: DataTableProps<DataTableRow>["statePersistence"],
438
+ state: PersistedTableState
439
+ ) {
440
+ if (typeof window === "undefined") {
441
+ return
442
+ }
443
+
444
+ if (statePersistence === "none") {
445
+ return
446
+ }
447
+
448
+ if (statePersistence === "url") {
449
+ const url = new URL(window.location.href)
450
+
451
+ if (isPersistedStateEmpty(state)) {
452
+ url.searchParams.delete(getPersistedUrlParam(id))
453
+ } else {
454
+ url.searchParams.set(getPersistedUrlParam(id), JSON.stringify(state))
455
+ }
456
+
457
+ window.history.replaceState(window.history.state, "", url)
458
+ return
459
+ }
460
+
461
+ window.localStorage.setItem(
462
+ `${LOCAL_STORAGE_PREFIX}${id}`,
463
+ JSON.stringify(state)
464
+ )
465
+ }
466
+
467
+ function inferAlignment(kind: DataTableColumnKind): DataTableAlign {
468
+ if (kind === "number") {
469
+ return "right"
470
+ }
471
+
472
+ if (kind === "boolean" || kind === "selection") {
473
+ return "center"
474
+ }
475
+
476
+ return "left"
477
+ }
478
+
479
+ function inferWidth(kind: DataTableColumnKind) {
480
+ switch (kind) {
481
+ case "boolean":
482
+ case "selection":
483
+ return 76
484
+ case "number":
485
+ return 132
486
+ case "date":
487
+ return 168
488
+ case "json":
489
+ return 240
490
+ default:
491
+ return 220
492
+ }
493
+ }
494
+
495
+ function resolveColumnId<TData extends DataTableRow>(
496
+ column: DataTableColumnConfig<TData>,
497
+ index: number
498
+ ) {
499
+ return (
500
+ column.id ??
501
+ column.accessorKey ??
502
+ (column.header
503
+ ? column.header.toLowerCase().replace(/\s+/g, "-")
504
+ : `column-${index}`)
505
+ )
506
+ }
507
+
508
+ function buildColumns<TData extends DataTableRow>(
509
+ data: TData[],
510
+ columns?: DataTableColumnConfig<TData>[],
511
+ showRowSelectionColumn?: boolean,
512
+ showRowActionButtonsColumn?: boolean,
513
+ rowActionsColumnSize?: number
514
+ ) {
515
+ const inferredColumns: DataTableColumnConfig<TData>[] = (
516
+ Object.keys(data[0] ?? {}) as Array<Extract<keyof TData, string>>
517
+ ).map((accessorKey) => ({
518
+ accessorKey,
519
+ header: formatHeaderLabel(accessorKey),
520
+ }))
521
+
522
+ const sourceColumns =
523
+ columns && columns.length > 0 ? columns : inferredColumns
524
+
525
+ const builtColumns = sourceColumns.map<ColumnDef<TData>>((column, index) => {
526
+ const id = resolveColumnId(column, index)
527
+ const samples = data
528
+ .slice(0, 25)
529
+ .map((row) =>
530
+ column.accessorFn
531
+ ? column.accessorFn(row)
532
+ : column.accessorKey
533
+ ? row[column.accessorKey]
534
+ : undefined
535
+ )
536
+ const kind = column.kind ?? inferDataTableColumnKind(samples)
537
+ const meta: DataTableColumnMeta = {
538
+ align: column.align ?? inferAlignment(kind),
539
+ cellClassName: column.cellClassName,
540
+ headerClassName: column.headerClassName,
541
+ highlightMatches:
542
+ column.highlightMatches ?? (kind === "text" ? true : false),
543
+ kind,
544
+ truncate: column.truncate ?? true,
545
+ }
546
+
547
+ return {
548
+ accessorFn: column.accessorFn,
549
+ accessorKey: column.accessorKey,
550
+ cell: ({ getValue, row }) =>
551
+ column.cell
552
+ ? column.cell({ row: row.original, value: getValue() })
553
+ : fallbackCellValue(getValue(), kind),
554
+ enableGlobalFilter: column.enableFiltering ?? true,
555
+ filterFn: column.filterFn,
556
+ enableHiding: column.enableHiding ?? true,
557
+ enableResizing: column.enableResizing ?? true,
558
+ enableSorting: column.enableSorting ?? true,
559
+ header: column.header ?? formatHeaderLabel(id),
560
+ id,
561
+ maxSize: column.maxWidth ?? 420,
562
+ meta,
563
+ minSize: column.minWidth ?? Math.min(inferWidth(kind), 120),
564
+ size: column.width ?? inferWidth(kind),
565
+ }
566
+ })
567
+
568
+ if (!showRowSelectionColumn) {
569
+ if (!showRowActionButtonsColumn) {
570
+ return builtColumns
571
+ }
572
+
573
+ return [
574
+ ...builtColumns,
575
+ {
576
+ cell: () => null,
577
+ enableGlobalFilter: false,
578
+ enableHiding: false,
579
+ enableResizing: false,
580
+ enableSorting: false,
581
+ header: "Actions",
582
+ id: "__actions",
583
+ maxSize: rowActionsColumnSize ?? 280,
584
+ meta: {
585
+ align: "right",
586
+ truncate: false,
587
+ } satisfies DataTableColumnMeta,
588
+ minSize: Math.min(rowActionsColumnSize ?? 180, 180),
589
+ size: rowActionsColumnSize ?? 220,
590
+ },
591
+ ] satisfies ColumnDef<TData>[]
592
+ }
593
+
594
+ const withSelectionColumn = [
595
+ {
596
+ cell: ({ row }) => (
597
+ <div className="flex items-center justify-center">
598
+ <DataTableCheckbox
599
+ ariaLabel={`Select row ${row.index + 1}`}
600
+ checked={row.getIsSelected()}
601
+ onCheckedChange={(checked) => row.toggleSelected(checked)}
602
+ />
603
+ </div>
604
+ ),
605
+ enableGlobalFilter: false,
606
+ enableHiding: false,
607
+ enableResizing: false,
608
+ enableSorting: false,
609
+ header: ({ table }) => (
610
+ <div className="flex items-center justify-center">
611
+ <DataTableCheckbox
612
+ ariaLabel="Select all visible rows"
613
+ checked={table.getIsAllPageRowsSelected()}
614
+ indeterminate={table.getIsSomePageRowsSelected()}
615
+ onCheckedChange={(checked) =>
616
+ table.toggleAllPageRowsSelected(checked)
617
+ }
618
+ />
619
+ </div>
620
+ ),
621
+ id: "__select",
622
+ maxSize: 56,
623
+ meta: {
624
+ align: "center",
625
+ kind: "selection",
626
+ sticky: "left",
627
+ truncate: false,
628
+ } satisfies DataTableColumnMeta,
629
+ minSize: 56,
630
+ size: 56,
631
+ },
632
+ ...builtColumns,
633
+ ] satisfies ColumnDef<TData>[]
634
+
635
+ if (!showRowActionButtonsColumn) {
636
+ return withSelectionColumn
637
+ }
638
+
639
+ return [
640
+ ...withSelectionColumn,
641
+ {
642
+ cell: () => null,
643
+ enableGlobalFilter: false,
644
+ enableHiding: false,
645
+ enableResizing: false,
646
+ enableSorting: false,
647
+ header: "Actions",
648
+ id: "__actions",
649
+ maxSize: rowActionsColumnSize ?? 280,
650
+ meta: {
651
+ align: "right",
652
+ truncate: false,
653
+ } satisfies DataTableColumnMeta,
654
+ minSize: Math.min(rowActionsColumnSize ?? 180, 180),
655
+ size: rowActionsColumnSize ?? 220,
656
+ },
657
+ ] satisfies ColumnDef<TData>[]
658
+ }
659
+
660
+ const globalFilterFn: FilterFn<RowData> = (row, columnId, filterValue) => {
661
+ const query = String(filterValue ?? "")
662
+ .trim()
663
+ .toLowerCase()
664
+
665
+ if (!query) {
666
+ return true
667
+ }
668
+
669
+ return stringifyFilterValue(row.getValue(columnId))
670
+ .toLowerCase()
671
+ .includes(query)
672
+ }
673
+
674
+ function shouldIgnoreRowSelectionTarget(target: EventTarget | null) {
675
+ return (
676
+ target instanceof Element &&
677
+ Boolean(
678
+ target.closest(
679
+ [
680
+ "a",
681
+ "button",
682
+ "input",
683
+ "label",
684
+ "select",
685
+ "summary",
686
+ "textarea",
687
+ "[contenteditable=true]",
688
+ "[data-no-row-select=true]",
689
+ '[role="button"]',
690
+ '[role="link"]',
691
+ '[role="menuitem"]',
692
+ ].join(",")
693
+ )
694
+ )
695
+ )
696
+ }
697
+
698
+ function resolveRowActionRows<TData extends DataTableRow>(
699
+ action: DataTableRowAction<TData>,
700
+ row: Row<TData>,
701
+ selectedRows: Row<TData>[]
702
+ ) {
703
+ if (action.scope === "selection" && row.getIsSelected()) {
704
+ return selectedRows.length > 0
705
+ ? selectedRows.map((selectedRow) => selectedRow.original)
706
+ : [row.original]
707
+ }
708
+
709
+ return [row.original]
710
+ }
711
+
712
+ function resolveRowActionRowIds<TData extends DataTableRow>(
713
+ action: DataTableRowAction<TData>,
714
+ row: Row<TData>,
715
+ selectedRows: Row<TData>[]
716
+ ) {
717
+ if (action.scope === "selection" && row.getIsSelected()) {
718
+ return selectedRows.length > 0
719
+ ? selectedRows.map((selectedRow) => selectedRow.id)
720
+ : [row.id]
721
+ }
722
+
723
+ return [row.id]
724
+ }
725
+
726
+ function buildRowActionContext<TData extends DataTableRow>(
727
+ action: DataTableRowAction<TData>,
728
+ row: Row<TData>,
729
+ selectedRows: Row<TData>[]
730
+ ) {
731
+ return {
732
+ actionRowIds: resolveRowActionRowIds(action, row, selectedRows),
733
+ actionRows: resolveRowActionRows(action, row, selectedRows),
734
+ anchorRow: row.original,
735
+ anchorRowId: row.id,
736
+ selectedRowIds: selectedRows.map((selectedRow) => selectedRow.id),
737
+ selectedRows: selectedRows.map((selectedRow) => selectedRow.original),
738
+ } satisfies DataTableRowActionContext<TData>
739
+ }
740
+
741
+ function resolveRowActionState<TData extends DataTableRow>(
742
+ value:
743
+ | boolean
744
+ | ((context: DataTableRowActionContext<TData>) => boolean)
745
+ | undefined,
746
+ context: DataTableRowActionContext<TData>
747
+ ) {
748
+ if (typeof value === "function") {
749
+ return value(context)
750
+ }
751
+
752
+ return Boolean(value)
753
+ }
754
+
755
+ function resolveRowActionLabel<TData extends DataTableRow>(
756
+ action: DataTableRowAction<TData>,
757
+ context: DataTableRowActionContext<TData>
758
+ ) {
759
+ return typeof action.label === "function"
760
+ ? action.label(context)
761
+ : action.label
762
+ }
763
+
764
+ function resolveRowActionButton(
765
+ action: DataTableRowAction<DataTableRow>
766
+ ): Exclude<DataTableRowActionButtonConfig, false> | null {
767
+ if (action.button === undefined || action.button === false) {
768
+ return null
769
+ }
770
+
771
+ return action.button
772
+ }
773
+
774
+ function getRowActionStatusKey<TData extends DataTableRow>(
775
+ action: DataTableRowAction<TData>,
776
+ context: DataTableRowActionContext<TData>
777
+ ) {
778
+ return `${action.id}:${context.actionRowIds.join(",")}`
779
+ }
780
+
781
+ function getRowActionResultMessage(result: unknown) {
782
+ if (typeof result === "string") {
783
+ return result
784
+ }
785
+
786
+ if (
787
+ result &&
788
+ typeof result === "object" &&
789
+ "message" in result &&
790
+ (typeof result.message === "string" || result.message === undefined)
791
+ ) {
792
+ return result.message
793
+ }
794
+
795
+ return undefined
796
+ }
797
+
798
+ function hasSelectionScopedAction<TData extends DataTableRow>(
799
+ actions: DataTableRowAction<TData>[]
800
+ ): boolean {
801
+ return actions.some((action) => {
802
+ if (action.scope === "selection") {
803
+ return true
804
+ }
805
+
806
+ const items = Array.isArray(action.items) ? action.items : undefined
807
+
808
+ return items ? hasSelectionScopedAction(items) : false
809
+ })
810
+ }
811
+
812
+ function countStaticButtonActions<TData extends DataTableRow>(
813
+ actions: DataTableRowAction<TData>[]
814
+ ): number {
815
+ return actions.reduce((count, action) => {
816
+ const nextCount =
817
+ resolveRowActionButton(action as DataTableRowAction<DataTableRow>) !== null
818
+ ? count + 1
819
+ : count
820
+
821
+ const items = Array.isArray(action.items) ? action.items : undefined
822
+
823
+ return items ? nextCount + countStaticButtonActions(items) : nextCount
824
+ }, 0)
825
+ }
826
+
827
+ type ResolvedRowAction<TData extends DataTableRow> = {
828
+ action: DataTableRowAction<TData>
829
+ context: DataTableRowActionContext<TData>
830
+ items: ResolvedRowAction<TData>[]
831
+ }
832
+
833
+ type RowActionState = "idle" | "loading" | "success" | "error"
834
+
835
+ type RowActionStatus = {
836
+ message?: string
837
+ state: RowActionState
838
+ }
839
+
840
+ function resolveRowActionItems<TData extends DataTableRow>(
841
+ action: DataTableRowAction<TData>,
842
+ context: DataTableRowActionContext<TData>,
843
+ row: Row<TData>,
844
+ selectedRows: Row<TData>[]
845
+ ) {
846
+ const items =
847
+ typeof action.items === "function" ? action.items(context) : action.items
848
+
849
+ return resolveVisibleRowActions(items ?? [], row, selectedRows)
850
+ }
851
+
852
+ function resolveVisibleRowActions<TData extends DataTableRow>(
853
+ actions: DataTableRowAction<TData>[],
854
+ row: Row<TData>,
855
+ selectedRows: Row<TData>[]
856
+ ): ResolvedRowAction<TData>[] {
857
+ return actions.flatMap((action) => {
858
+ const context = buildRowActionContext(action, row, selectedRows)
859
+
860
+ if (resolveRowActionState(action.hidden, context)) {
861
+ return []
862
+ }
863
+
864
+ const items = resolveRowActionItems(action, context, row, selectedRows)
865
+
866
+ if (action.items && items.length === 0) {
867
+ return []
868
+ }
869
+
870
+ return [
871
+ {
872
+ action,
873
+ context,
874
+ items,
875
+ },
876
+ ]
877
+ })
878
+ }
879
+
880
+ function RowActionMenuItem<TData extends DataTableRow>({
881
+ resolvedAction,
882
+ setStatus,
883
+ statuses,
884
+ }: {
885
+ resolvedAction: ResolvedRowAction<TData>
886
+ setStatus: (key: string, status: RowActionStatus, resetAfterMs?: number) => void
887
+ statuses: Record<string, RowActionStatus>
888
+ }) {
889
+ const { action, context, items } = resolvedAction
890
+ const label = resolveRowActionLabel(action, context)
891
+ const isDisabled = resolveRowActionState(action.disabled, context)
892
+ const statusKey = getRowActionStatusKey(action, context)
893
+ const status = statuses[statusKey]
894
+ const button = resolveRowActionButton(
895
+ action as DataTableRowAction<DataTableRow>
896
+ )
897
+
898
+ const handleRun = React.useCallback(async () => {
899
+ if (!action.onSelect) {
900
+ return
901
+ }
902
+
903
+ setStatus(statusKey, {
904
+ state: "loading",
905
+ })
906
+
907
+ try {
908
+ const result = await action.onSelect(context)
909
+
910
+ setStatus(
911
+ statusKey,
912
+ {
913
+ message: getRowActionResultMessage(result),
914
+ state: "success",
915
+ },
916
+ 2_500
917
+ )
918
+ } catch (error) {
919
+ setStatus(
920
+ statusKey,
921
+ {
922
+ message: error instanceof Error ? error.message : String(error),
923
+ state: "error",
924
+ },
925
+ 4_000
926
+ )
927
+ }
928
+ }, [action, context, setStatus, statusKey])
929
+
930
+ const Icon =
931
+ status?.state === "loading"
932
+ ? LoaderCircle
933
+ : status?.state === "success"
934
+ ? Check
935
+ : status?.state === "error"
936
+ ? CircleAlert
937
+ : action.icon
938
+ const isBusy = status?.state === "loading"
939
+
940
+ if (items.length > 0) {
941
+ return (
942
+ <ContextMenuSub>
943
+ <ContextMenuSubTrigger disabled={isDisabled || isBusy}>
944
+ {Icon ? <Icon className="size-3.5" /> : null}
945
+ <span className="truncate">{label}</span>
946
+ </ContextMenuSubTrigger>
947
+ <ContextMenuSubContent className="w-72">
948
+ {items.map((item) => (
949
+ <RowActionMenuItem
950
+ key={item.action.id}
951
+ resolvedAction={item}
952
+ setStatus={setStatus}
953
+ statuses={statuses}
954
+ />
955
+ ))}
956
+ </ContextMenuSubContent>
957
+ </ContextMenuSub>
958
+ )
959
+ }
960
+
961
+ return (
962
+ <ContextMenuItem
963
+ disabled={isDisabled || isBusy}
964
+ onSelect={() => {
965
+ void handleRun()
966
+ }}
967
+ title={status?.message}
968
+ variant={
969
+ action.variant ??
970
+ ((button === "destructive" ||
971
+ (typeof button === "object" && button?.variant === "destructive"))
972
+ ? "destructive"
973
+ : "default")
974
+ }
975
+ >
976
+ {Icon ? (
977
+ <Icon
978
+ className={cn("size-3.5", isBusy && "animate-spin")}
979
+ />
980
+ ) : null}
981
+ <span className="truncate">{label}</span>
982
+ {action.shortcut ? (
983
+ <ContextMenuShortcut>{action.shortcut}</ContextMenuShortcut>
984
+ ) : null}
985
+ </ContextMenuItem>
986
+ )
987
+ }
988
+
989
+ function RowActionButtonGroup<TData extends DataTableRow>({
990
+ resolvedActions,
991
+ setStatus,
992
+ statuses,
993
+ }: {
994
+ resolvedActions: ResolvedRowAction<TData>[]
995
+ setStatus: (key: string, status: RowActionStatus, resetAfterMs?: number) => void
996
+ statuses: Record<string, RowActionStatus>
997
+ }) {
998
+ const buttonActions = resolvedActions.filter(
999
+ (resolvedAction) =>
1000
+ resolvedAction.items.length === 0 &&
1001
+ resolveRowActionButton(
1002
+ resolvedAction.action as DataTableRowAction<DataTableRow>
1003
+ ) !== null
1004
+ )
1005
+
1006
+ if (buttonActions.length === 0) {
1007
+ return null
1008
+ }
1009
+
1010
+ return (
1011
+ <div
1012
+ className="flex w-full justify-end gap-1.5"
1013
+ data-no-row-select="true"
1014
+ >
1015
+ {buttonActions.map((resolvedAction) => {
1016
+ const { action, context } = resolvedAction
1017
+ const button = resolveRowActionButton(
1018
+ action as DataTableRowAction<DataTableRow>
1019
+ )
1020
+ const label = resolveRowActionLabel(action, context)
1021
+ const statusKey = getRowActionStatusKey(action, context)
1022
+ const status = statuses[statusKey]
1023
+ const isDisabled =
1024
+ resolveRowActionState(action.disabled, context) ||
1025
+ status?.state === "loading"
1026
+ const Icon =
1027
+ status?.state === "loading"
1028
+ ? LoaderCircle
1029
+ : status?.state === "success"
1030
+ ? Check
1031
+ : status?.state === "error"
1032
+ ? CircleAlert
1033
+ : action.icon
1034
+ const buttonLabel =
1035
+ button && typeof button === "object" && button.label
1036
+ ? button.label
1037
+ : label
1038
+ const buttonVariant =
1039
+ status?.state === "error"
1040
+ ? "destructive"
1041
+ : typeof button === "string"
1042
+ ? button
1043
+ : button && typeof button === "object"
1044
+ ? button.variant ?? "outline"
1045
+ : "outline"
1046
+ const buttonSize =
1047
+ button && typeof button === "object" && button.size
1048
+ ? button.size
1049
+ : "sm"
1050
+ const buttonClassName =
1051
+ button && typeof button === "object" ? button.className : undefined
1052
+
1053
+ return (
1054
+ <Button
1055
+ aria-busy={status?.state === "loading"}
1056
+ className={buttonClassName}
1057
+ disabled={isDisabled}
1058
+ key={statusKey}
1059
+ onClick={() => {
1060
+ const runAction = action.onSelect
1061
+
1062
+ if (!runAction) {
1063
+ return
1064
+ }
1065
+
1066
+ void (async () => {
1067
+ setStatus(statusKey, {
1068
+ state: "loading",
1069
+ })
1070
+
1071
+ try {
1072
+ const result = await runAction(context)
1073
+
1074
+ setStatus(
1075
+ statusKey,
1076
+ {
1077
+ message: getRowActionResultMessage(result),
1078
+ state: "success",
1079
+ },
1080
+ 2_500
1081
+ )
1082
+ } catch (error) {
1083
+ setStatus(
1084
+ statusKey,
1085
+ {
1086
+ message:
1087
+ error instanceof Error ? error.message : String(error),
1088
+ state: "error",
1089
+ },
1090
+ 4_000
1091
+ )
1092
+ }
1093
+ })()
1094
+ }}
1095
+ size={buttonSize}
1096
+ title={status?.message}
1097
+ type="button"
1098
+ variant={buttonVariant}
1099
+ >
1100
+ {Icon ? (
1101
+ <Icon
1102
+ className={cn("size-3.5", status?.state === "loading" && "animate-spin")}
1103
+ />
1104
+ ) : null}
1105
+ {buttonLabel}
1106
+ </Button>
1107
+ )
1108
+ })}
1109
+ </div>
1110
+ )
1111
+ }
1112
+
1113
+ function DataTableView<TData extends DataTableRow>({
1114
+ autoScrollToBottom = false,
1115
+ autoScrollToBottomThreshold = 96,
1116
+ columnFilters: controlledColumnFilters,
1117
+ columnVisibility: controlledColumnVisibility,
1118
+ columns,
1119
+ data,
1120
+ edgeHorizontalPadding = "16px",
1121
+ enableRowSelection = false,
1122
+ filterPlaceholder = "Search across visible columns",
1123
+ globalFilter,
1124
+ getRowId,
1125
+ height = 620,
1126
+ highlightQuery = "",
1127
+ id,
1128
+ onColumnFiltersChange,
1129
+ onColumnVisibilityChange,
1130
+ onGlobalFilterChange,
1131
+ resolveColumnHighlightTerms,
1132
+ rowActions,
1133
+ rowClassName,
1134
+ rowHeight = 48,
1135
+ statePersistence = "localStorage",
1136
+ }: DataTableProps<TData>) {
1137
+ const persistedState = React.useMemo(
1138
+ () => readPersistedState(id, statePersistence),
1139
+ [id, statePersistence]
1140
+ )
1141
+ const [sorting, setSorting] = React.useState<SortingState>(
1142
+ () => persistedState.sorting ?? []
1143
+ )
1144
+ const [columnSizing, setColumnSizing] = React.useState<ColumnSizingState>(
1145
+ () => persistedState.columnSizing ?? {}
1146
+ )
1147
+ const [columnVisibility, setColumnVisibility] =
1148
+ React.useState<VisibilityState>(() => persistedState.columnVisibility ?? {})
1149
+ const [highlightedColumns, setHighlightedColumns] = React.useState<
1150
+ Record<string, boolean>
1151
+ >(() => persistedState.highlightedColumns ?? {})
1152
+ const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
1153
+ () => persistedState.columnFilters ?? []
1154
+ )
1155
+ const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({})
1156
+ const [rowActionStatuses, setRowActionStatuses] = React.useState<
1157
+ Record<string, RowActionStatus>
1158
+ >({})
1159
+ const rowActionStatusTimersRef = React.useRef<Record<string, number>>({})
1160
+ const selectionAnchorIdRef = React.useRef<string | null>(null)
1161
+ const dragSelectionRef = React.useRef<{
1162
+ anchorId: string
1163
+ baseSelection: RowSelectionState
1164
+ } | null>(null)
1165
+ const dragPointerRef = React.useRef<{
1166
+ clientX: number
1167
+ clientY: number
1168
+ } | null>(null)
1169
+ const [isDragSelecting, setIsDragSelecting] = React.useState(false)
1170
+ const [searchDraft, setSearchDraft] = React.useState(
1171
+ () => persistedState.globalFilter ?? ""
1172
+ )
1173
+ const deferredSearch = useDeferredValue(searchDraft)
1174
+ const isColumnFiltersControlled = controlledColumnFilters !== undefined
1175
+ const isColumnVisibilityControlled = controlledColumnVisibility !== undefined
1176
+ const isGlobalFilterControlled = globalFilter !== undefined
1177
+ const resolvedColumnFilters = controlledColumnFilters ?? columnFilters
1178
+ const resolvedColumnVisibility =
1179
+ controlledColumnVisibility ?? columnVisibility
1180
+ const hasSelectionActions = rowActions
1181
+ ? hasSelectionScopedAction(rowActions)
1182
+ : false
1183
+ const rowActionButtonCount = rowActions
1184
+ ? countStaticButtonActions(rowActions)
1185
+ : 0
1186
+ const canSelectRows = enableRowSelection || hasSelectionActions
1187
+ const showRowSelectionColumn = enableRowSelection
1188
+ const showRowActionButtonsColumn = rowActionButtonCount > 0
1189
+ const rowActionsColumnSize = React.useMemo(
1190
+ () => Math.max(160, Math.min(320, rowActionButtonCount * 96)),
1191
+ [rowActionButtonCount]
1192
+ )
1193
+ const tableColumns = React.useMemo(
1194
+ () =>
1195
+ buildColumns(
1196
+ data,
1197
+ columns,
1198
+ showRowSelectionColumn,
1199
+ showRowActionButtonsColumn,
1200
+ rowActionsColumnSize
1201
+ ),
1202
+ [
1203
+ columns,
1204
+ data,
1205
+ rowActionsColumnSize,
1206
+ showRowActionButtonsColumn,
1207
+ showRowSelectionColumn,
1208
+ ]
1209
+ )
1210
+ const setRowActionStatus = React.useCallback(
1211
+ (key: string, status: RowActionStatus, resetAfterMs?: number) => {
1212
+ const existingTimer = rowActionStatusTimersRef.current[key]
1213
+
1214
+ if (existingTimer) {
1215
+ window.clearTimeout(existingTimer)
1216
+ delete rowActionStatusTimersRef.current[key]
1217
+ }
1218
+
1219
+ setRowActionStatuses((current) => ({
1220
+ ...current,
1221
+ [key]: status,
1222
+ }))
1223
+
1224
+ if (!resetAfterMs) {
1225
+ return
1226
+ }
1227
+
1228
+ rowActionStatusTimersRef.current[key] = window.setTimeout(() => {
1229
+ setRowActionStatuses((current) => {
1230
+ const next = { ...current }
1231
+
1232
+ delete next[key]
1233
+
1234
+ return next
1235
+ })
1236
+ delete rowActionStatusTimersRef.current[key]
1237
+ }, resetAfterMs)
1238
+ },
1239
+ []
1240
+ )
1241
+ const handleColumnSizingChange = React.useCallback<
1242
+ OnChangeFn<ColumnSizingState>
1243
+ >((updater) => {
1244
+ setColumnSizing((current) => functionalUpdate(updater, current))
1245
+ }, [])
1246
+ const handleColumnFiltersChange = React.useCallback<
1247
+ OnChangeFn<ColumnFiltersState>
1248
+ >(
1249
+ (updater) => {
1250
+ const nextValue = functionalUpdate(updater, resolvedColumnFilters)
1251
+
1252
+ if (!isColumnFiltersControlled) {
1253
+ setColumnFilters(nextValue)
1254
+ }
1255
+
1256
+ onColumnFiltersChange?.(nextValue)
1257
+ },
1258
+ [isColumnFiltersControlled, onColumnFiltersChange, resolvedColumnFilters]
1259
+ )
1260
+ const handleColumnVisibilityChange = React.useCallback<
1261
+ OnChangeFn<VisibilityState>
1262
+ >(
1263
+ (updater) => {
1264
+ const nextValue = functionalUpdate(updater, resolvedColumnVisibility)
1265
+
1266
+ if (!isColumnVisibilityControlled) {
1267
+ setColumnVisibility(nextValue)
1268
+ }
1269
+
1270
+ onColumnVisibilityChange?.(nextValue)
1271
+ },
1272
+ [
1273
+ isColumnVisibilityControlled,
1274
+ onColumnVisibilityChange,
1275
+ resolvedColumnVisibility,
1276
+ ]
1277
+ )
1278
+
1279
+ const table = useReactTable({
1280
+ columnResizeMode: "onChange",
1281
+ columns: tableColumns,
1282
+ data,
1283
+ enableColumnResizing: true,
1284
+ enableRowSelection: canSelectRows,
1285
+ getCoreRowModel: getCoreRowModel(),
1286
+ getFilteredRowModel: getFilteredRowModel(),
1287
+ getRowId,
1288
+ getSortedRowModel: getSortedRowModel(),
1289
+ globalFilterFn: globalFilterFn as FilterFn<TData>,
1290
+ onColumnFiltersChange: handleColumnFiltersChange,
1291
+ onColumnSizingChange: handleColumnSizingChange,
1292
+ onColumnVisibilityChange: handleColumnVisibilityChange,
1293
+ onRowSelectionChange: setRowSelection,
1294
+ onSortingChange: setSorting,
1295
+ state: {
1296
+ columnFilters: resolvedColumnFilters,
1297
+ columnSizing,
1298
+ columnVisibility: resolvedColumnVisibility,
1299
+ globalFilter: deferredSearch.trim(),
1300
+ rowSelection,
1301
+ sorting,
1302
+ },
1303
+ })
1304
+
1305
+ const containerRef = React.useRef<HTMLDivElement>(null)
1306
+ const shouldAutoScrollRef = React.useRef(true)
1307
+ const hasAutoScrolledOnMountRef = React.useRef(false)
1308
+ const previousDataLengthRef = React.useRef(0)
1309
+ const rows = table.getRowModel().rows
1310
+ const selectedTableRows = table.getSelectedRowModel().rows
1311
+ const resolvedHeight = typeof height === "number" ? height : 0
1312
+ const initialOffsetRef = React.useRef(
1313
+ autoScrollToBottom
1314
+ ? Math.max(rows.length * rowHeight - resolvedHeight, 0)
1315
+ : 0
1316
+ )
1317
+ const rowVirtualizer = useVirtualizer({
1318
+ count: rows.length,
1319
+ estimateSize: () => rowHeight,
1320
+ getScrollElement: () => containerRef.current,
1321
+ initialOffset: initialOffsetRef.current,
1322
+ overscan: 12,
1323
+ })
1324
+ const virtualRows = rowVirtualizer.getVirtualItems()
1325
+ const totalRows = data.length
1326
+ const filteredRows = rows.length
1327
+ const selectedRows = selectedTableRows.length
1328
+
1329
+ React.useEffect(() => {
1330
+ return () => {
1331
+ for (const timerId of Object.values(rowActionStatusTimersRef.current)) {
1332
+ window.clearTimeout(timerId)
1333
+ }
1334
+ }
1335
+ }, [])
1336
+
1337
+ React.useEffect(() => {
1338
+ if (isColumnFiltersControlled) {
1339
+ return
1340
+ }
1341
+
1342
+ const nextState = readPersistedState(id, statePersistence)
1343
+
1344
+ setSorting(nextState.sorting ?? [])
1345
+ setColumnSizing(nextState.columnSizing ?? {})
1346
+ if (!isColumnVisibilityControlled) {
1347
+ setColumnVisibility(nextState.columnVisibility ?? {})
1348
+ }
1349
+ setHighlightedColumns(nextState.highlightedColumns ?? {})
1350
+ setColumnFilters(nextState.columnFilters ?? [])
1351
+ setSearchDraft(nextState.globalFilter ?? "")
1352
+ setRowSelection({})
1353
+ dragSelectionRef.current = null
1354
+ dragPointerRef.current = null
1355
+ setIsDragSelecting(false)
1356
+ selectionAnchorIdRef.current = null
1357
+ for (const timerId of Object.values(rowActionStatusTimersRef.current)) {
1358
+ window.clearTimeout(timerId)
1359
+ }
1360
+ rowActionStatusTimersRef.current = {}
1361
+ setRowActionStatuses({})
1362
+ }, [id, isColumnFiltersControlled, isColumnVisibilityControlled, statePersistence])
1363
+
1364
+ React.useEffect(() => {
1365
+ if (!isColumnFiltersControlled) {
1366
+ return
1367
+ }
1368
+
1369
+ setColumnFilters(controlledColumnFilters)
1370
+ }, [controlledColumnFilters, isColumnFiltersControlled])
1371
+
1372
+ React.useEffect(() => {
1373
+ if (!isColumnVisibilityControlled) {
1374
+ return
1375
+ }
1376
+
1377
+ setColumnVisibility(controlledColumnVisibility)
1378
+ }, [controlledColumnVisibility, isColumnVisibilityControlled])
1379
+
1380
+ React.useEffect(() => {
1381
+ if (!isGlobalFilterControlled) {
1382
+ return
1383
+ }
1384
+
1385
+ setSearchDraft(globalFilter)
1386
+ }, [globalFilter, isGlobalFilterControlled])
1387
+
1388
+ React.useEffect(() => {
1389
+ if (typeof window === "undefined" || statePersistence !== "url") {
1390
+ return
1391
+ }
1392
+
1393
+ const syncFromUrl = () => {
1394
+ const nextState = readPersistedState(id, "url")
1395
+
1396
+ setSorting(nextState.sorting ?? [])
1397
+ setColumnSizing(nextState.columnSizing ?? {})
1398
+ if (!isColumnVisibilityControlled) {
1399
+ setColumnVisibility(nextState.columnVisibility ?? {})
1400
+ }
1401
+ setHighlightedColumns(nextState.highlightedColumns ?? {})
1402
+ setColumnFilters(nextState.columnFilters ?? [])
1403
+ setSearchDraft(nextState.globalFilter ?? "")
1404
+ setRowSelection({})
1405
+ dragSelectionRef.current = null
1406
+ dragPointerRef.current = null
1407
+ setIsDragSelecting(false)
1408
+ selectionAnchorIdRef.current = null
1409
+ }
1410
+
1411
+ window.addEventListener("popstate", syncFromUrl)
1412
+
1413
+ return () => window.removeEventListener("popstate", syncFromUrl)
1414
+ }, [id, isColumnVisibilityControlled, statePersistence])
1415
+
1416
+ React.useEffect(() => {
1417
+ if (isGlobalFilterControlled || isColumnFiltersControlled) {
1418
+ return
1419
+ }
1420
+
1421
+ writePersistedState(id, statePersistence, {
1422
+ columnFilters,
1423
+ columnSizing,
1424
+ highlightedColumns,
1425
+ columnVisibility: resolvedColumnVisibility,
1426
+ globalFilter: deferredSearch.trim(),
1427
+ sorting,
1428
+ })
1429
+ }, [
1430
+ columnFilters,
1431
+ columnSizing,
1432
+ deferredSearch,
1433
+ highlightedColumns,
1434
+ id,
1435
+ isColumnFiltersControlled,
1436
+ isGlobalFilterControlled,
1437
+ resolvedColumnVisibility,
1438
+ sorting,
1439
+ statePersistence,
1440
+ ])
1441
+
1442
+ React.useEffect(() => {
1443
+ const container = containerRef.current
1444
+
1445
+ if (!autoScrollToBottom || !container) {
1446
+ return
1447
+ }
1448
+
1449
+ const updateShouldAutoScroll = () => {
1450
+ const distanceFromBottom =
1451
+ container.scrollHeight - container.scrollTop - container.clientHeight
1452
+
1453
+ shouldAutoScrollRef.current =
1454
+ distanceFromBottom <= autoScrollToBottomThreshold
1455
+ }
1456
+
1457
+ updateShouldAutoScroll()
1458
+ container.addEventListener("scroll", updateShouldAutoScroll, {
1459
+ passive: true,
1460
+ })
1461
+
1462
+ return () => {
1463
+ container.removeEventListener("scroll", updateShouldAutoScroll)
1464
+ }
1465
+ }, [autoScrollToBottom, autoScrollToBottomThreshold])
1466
+
1467
+ React.useLayoutEffect(() => {
1468
+ const container = containerRef.current
1469
+ const previousDataLength = previousDataLengthRef.current
1470
+
1471
+ previousDataLengthRef.current = data.length
1472
+
1473
+ const scrollToBottom = () => {
1474
+ if (rows.length === 0) {
1475
+ return
1476
+ }
1477
+
1478
+ rowVirtualizer.scrollToIndex(rows.length - 1, {
1479
+ align: "end",
1480
+ })
1481
+ container?.scrollTo({
1482
+ top: container.scrollHeight,
1483
+ })
1484
+ }
1485
+
1486
+ const scheduleBottomScroll = () => {
1487
+ const frameIds: number[] = []
1488
+
1489
+ const run = (remainingFrames: number) => {
1490
+ scrollToBottom()
1491
+
1492
+ if (remainingFrames <= 0) {
1493
+ return
1494
+ }
1495
+
1496
+ const frameId = window.requestAnimationFrame(() =>
1497
+ run(remainingFrames - 1)
1498
+ )
1499
+
1500
+ frameIds.push(frameId)
1501
+ }
1502
+
1503
+ run(4)
1504
+
1505
+ return () => {
1506
+ for (const frameId of frameIds) {
1507
+ window.cancelAnimationFrame(frameId)
1508
+ }
1509
+ }
1510
+ }
1511
+
1512
+ if (
1513
+ autoScrollToBottom &&
1514
+ container &&
1515
+ !hasAutoScrolledOnMountRef.current &&
1516
+ rows.length > 0
1517
+ ) {
1518
+ hasAutoScrolledOnMountRef.current = true
1519
+ shouldAutoScrollRef.current = true
1520
+
1521
+ return scheduleBottomScroll()
1522
+ }
1523
+
1524
+ if (
1525
+ !autoScrollToBottom ||
1526
+ !container ||
1527
+ data.length <= previousDataLength ||
1528
+ !shouldAutoScrollRef.current
1529
+ ) {
1530
+ return
1531
+ }
1532
+
1533
+ return scheduleBottomScroll()
1534
+ }, [autoScrollToBottom, data.length, rowVirtualizer, rows.length])
1535
+
1536
+ const handleSearchDraftChange = (value: string) => {
1537
+ if (!isGlobalFilterControlled) {
1538
+ setSearchDraft(value)
1539
+ }
1540
+
1541
+ onGlobalFilterChange?.(value)
1542
+ }
1543
+
1544
+ const isColumnHighlightEnabled = React.useCallback(
1545
+ (columnId: string, meta: DataTableColumnMeta) =>
1546
+ highlightedColumns[columnId] ?? Boolean(meta.highlightMatches),
1547
+ [highlightedColumns]
1548
+ )
1549
+ const selectRange = React.useCallback(
1550
+ (
1551
+ anchorId: string,
1552
+ rowId: string,
1553
+ baseSelection: RowSelectionState = {}
1554
+ ) => {
1555
+ const anchorIndex = rows.findIndex(
1556
+ (candidateRow) => candidateRow.id === anchorId
1557
+ )
1558
+ const rowIndex = rows.findIndex(
1559
+ (candidateRow) => candidateRow.id === rowId
1560
+ )
1561
+
1562
+ if (anchorIndex === -1 || rowIndex === -1) {
1563
+ return
1564
+ }
1565
+
1566
+ const [start, end] =
1567
+ anchorIndex <= rowIndex
1568
+ ? [anchorIndex, rowIndex]
1569
+ : [rowIndex, anchorIndex]
1570
+
1571
+ setRowSelection({
1572
+ ...baseSelection,
1573
+ ...Object.fromEntries(
1574
+ rows
1575
+ .slice(start, end + 1)
1576
+ .map((candidateRow) => [candidateRow.id, true])
1577
+ ),
1578
+ })
1579
+ },
1580
+ [rows]
1581
+ )
1582
+ const selectSingleRow = React.useCallback((rowId: string) => {
1583
+ selectionAnchorIdRef.current = rowId
1584
+ setRowSelection({ [rowId]: true })
1585
+ }, [])
1586
+ const updateDragSelectionFromPointer = React.useCallback(
1587
+ (clientX: number, clientY: number) => {
1588
+ if (!dragSelectionRef.current) {
1589
+ return
1590
+ }
1591
+
1592
+ const container = containerRef.current
1593
+
1594
+ if (!container) {
1595
+ return
1596
+ }
1597
+
1598
+ const rowElement = document
1599
+ .elementFromPoint(clientX, clientY)
1600
+ ?.closest<HTMLTableRowElement>("tr[data-row-id]")
1601
+ const renderedRows = Array.from(
1602
+ container.querySelectorAll<HTMLTableRowElement>("tr[data-row-id]")
1603
+ )
1604
+ const targetRowId =
1605
+ rowElement?.dataset.rowId ??
1606
+ (clientY < container.getBoundingClientRect().top
1607
+ ? renderedRows[0]?.dataset.rowId
1608
+ : clientY > container.getBoundingClientRect().bottom
1609
+ ? renderedRows.at(-1)?.dataset.rowId
1610
+ : undefined)
1611
+
1612
+ if (!targetRowId) {
1613
+ return
1614
+ }
1615
+
1616
+ const { anchorId, baseSelection } = dragSelectionRef.current
1617
+
1618
+ selectRange(anchorId, targetRowId, baseSelection)
1619
+ },
1620
+ [selectRange]
1621
+ )
1622
+ const handleRowMouseDown = React.useCallback(
1623
+ (event: React.MouseEvent<HTMLTableRowElement>, row: Row<TData>) => {
1624
+ if (
1625
+ event.button !== 0 ||
1626
+ !canSelectRows ||
1627
+ shouldIgnoreRowSelectionTarget(event.target)
1628
+ ) {
1629
+ return
1630
+ }
1631
+
1632
+ event.preventDefault()
1633
+ dragPointerRef.current = {
1634
+ clientX: event.clientX,
1635
+ clientY: event.clientY,
1636
+ }
1637
+
1638
+ const rowId = row.id
1639
+ const isAdditiveSelection = event.metaKey || event.ctrlKey
1640
+ const currentSelection = table.getState().rowSelection
1641
+
1642
+ if (event.shiftKey) {
1643
+ const anchorId = selectionAnchorIdRef.current ?? rowId
1644
+
1645
+ selectionAnchorIdRef.current = anchorId
1646
+ dragSelectionRef.current = {
1647
+ anchorId,
1648
+ baseSelection: {},
1649
+ }
1650
+ setIsDragSelecting(true)
1651
+ selectRange(anchorId, rowId)
1652
+ return
1653
+ }
1654
+
1655
+ if (isAdditiveSelection) {
1656
+ const baseSelection = { ...currentSelection }
1657
+
1658
+ if (baseSelection[rowId]) {
1659
+ delete baseSelection[rowId]
1660
+ } else {
1661
+ baseSelection[rowId] = true
1662
+ }
1663
+
1664
+ selectionAnchorIdRef.current = rowId
1665
+ dragSelectionRef.current = {
1666
+ anchorId: rowId,
1667
+ baseSelection,
1668
+ }
1669
+ setIsDragSelecting(true)
1670
+ setRowSelection(baseSelection)
1671
+ return
1672
+ }
1673
+
1674
+ selectionAnchorIdRef.current = rowId
1675
+ dragSelectionRef.current = {
1676
+ anchorId: rowId,
1677
+ baseSelection: {},
1678
+ }
1679
+ setIsDragSelecting(true)
1680
+ setRowSelection({ [rowId]: true })
1681
+ },
1682
+ [canSelectRows, selectRange, table]
1683
+ )
1684
+ const handleRowMouseEnter = React.useCallback(
1685
+ (event: React.MouseEvent<HTMLTableRowElement>, row: Row<TData>) => {
1686
+ if (
1687
+ !canSelectRows ||
1688
+ event.buttons !== 1 ||
1689
+ shouldIgnoreRowSelectionTarget(event.target) ||
1690
+ !dragSelectionRef.current
1691
+ ) {
1692
+ return
1693
+ }
1694
+
1695
+ const { anchorId, baseSelection } = dragSelectionRef.current
1696
+
1697
+ selectRange(anchorId, row.id, baseSelection)
1698
+ },
1699
+ [canSelectRows, selectRange]
1700
+ )
1701
+ const handleRowContextMenu = React.useCallback(
1702
+ (row: Row<TData>) => {
1703
+ if (!canSelectRows) {
1704
+ return
1705
+ }
1706
+
1707
+ if (row.getIsSelected()) {
1708
+ selectionAnchorIdRef.current = row.id
1709
+ return
1710
+ }
1711
+
1712
+ selectSingleRow(row.id)
1713
+ },
1714
+ [canSelectRows, selectSingleRow]
1715
+ )
1716
+ React.useEffect(() => {
1717
+ if (!isDragSelecting) {
1718
+ return
1719
+ }
1720
+
1721
+ const handleWindowMouseMove = (event: MouseEvent) => {
1722
+ dragPointerRef.current = {
1723
+ clientX: event.clientX,
1724
+ clientY: event.clientY,
1725
+ }
1726
+ updateDragSelectionFromPointer(event.clientX, event.clientY)
1727
+ }
1728
+
1729
+ const stopDragSelection = () => {
1730
+ dragSelectionRef.current = null
1731
+ dragPointerRef.current = null
1732
+ setIsDragSelecting(false)
1733
+ }
1734
+
1735
+ window.addEventListener("mousemove", handleWindowMouseMove)
1736
+ window.addEventListener("mouseup", stopDragSelection)
1737
+
1738
+ return () => {
1739
+ window.removeEventListener("mousemove", handleWindowMouseMove)
1740
+ window.removeEventListener("mouseup", stopDragSelection)
1741
+ }
1742
+ }, [isDragSelecting, updateDragSelectionFromPointer])
1743
+ React.useEffect(() => {
1744
+ if (!isDragSelecting) {
1745
+ return
1746
+ }
1747
+
1748
+ let frameId = 0
1749
+
1750
+ const tick = () => {
1751
+ const container = containerRef.current
1752
+ const pointer = dragPointerRef.current
1753
+
1754
+ if (container && pointer) {
1755
+ const { top, bottom } = container.getBoundingClientRect()
1756
+ const edgeThreshold = 48
1757
+ let delta = 0
1758
+
1759
+ if (pointer.clientY < top + edgeThreshold) {
1760
+ delta = -Math.ceil((top + edgeThreshold - pointer.clientY) / 6)
1761
+ } else if (pointer.clientY > bottom - edgeThreshold) {
1762
+ delta = Math.ceil((pointer.clientY - (bottom - edgeThreshold)) / 6)
1763
+ }
1764
+
1765
+ if (delta !== 0) {
1766
+ const maxDelta = 28
1767
+ const nextDelta = Math.max(-maxDelta, Math.min(maxDelta, delta))
1768
+
1769
+ container.scrollTop += nextDelta
1770
+ updateDragSelectionFromPointer(pointer.clientX, pointer.clientY)
1771
+ }
1772
+ }
1773
+
1774
+ frameId = window.requestAnimationFrame(tick)
1775
+ }
1776
+
1777
+ frameId = window.requestAnimationFrame(tick)
1778
+
1779
+ return () => {
1780
+ window.cancelAnimationFrame(frameId)
1781
+ }
1782
+ }, [isDragSelecting, updateDragSelectionFromPointer])
1783
+
1784
+ return (
1785
+ <section
1786
+ className="flex min-h-0 flex-col overflow-hidden border-t border-border bg-card text-card-foreground shadow-sm"
1787
+ style={{ height }}
1788
+ >
1789
+
1790
+ <div
1791
+ className={cn(
1792
+ "min-h-0 flex-1 overflow-auto pb-4",
1793
+ isDragSelecting && "select-none"
1794
+ )}
1795
+ ref={containerRef}
1796
+ style={{
1797
+ scrollbarGutter: "stable",
1798
+ }}
1799
+ >
1800
+ <table
1801
+ className="grid w-full border-separate border-spacing-0"
1802
+ role="grid"
1803
+ >
1804
+ <thead className="sticky top-0 z-20 grid">
1805
+ {table.getHeaderGroups().map((headerGroup) => (
1806
+ <tr
1807
+ className="flex w-full"
1808
+ key={headerGroup.id}
1809
+ style={{
1810
+ width: table.getTotalSize(),
1811
+ }}
1812
+ >
1813
+ {headerGroup.headers.map((header, index) => (
1814
+ <DataTableHeaderCol
1815
+ header={header}
1816
+ highlightEnabled={isColumnHighlightEnabled(
1817
+ header.column.id,
1818
+ (header.column.columnDef.meta ??
1819
+ {}) as DataTableColumnMeta
1820
+ )}
1821
+ key={header.id}
1822
+ onToggleHighlight={() =>
1823
+ setHighlightedColumns((current) => ({
1824
+ ...current,
1825
+ [header.column.id]: !isColumnHighlightEnabled(
1826
+ header.column.id,
1827
+ (header.column.columnDef.meta ??
1828
+ {}) as DataTableColumnMeta
1829
+ ),
1830
+ }))
1831
+ }
1832
+ paddingLeft={index === 0 ? edgeHorizontalPadding : undefined}
1833
+ paddingRight={
1834
+ index === headerGroup.headers.length - 1
1835
+ ? edgeHorizontalPadding
1836
+ : undefined
1837
+ }
1838
+ scrollContainerRef={containerRef}
1839
+ />
1840
+ ))}
1841
+ </tr>
1842
+ ))}
1843
+ </thead>
1844
+
1845
+ <tbody
1846
+ className="relative grid"
1847
+ style={{
1848
+ height: rowVirtualizer.getTotalSize(),
1849
+ }}
1850
+ >
1851
+ {virtualRows.length === 0 ? (
1852
+ <tr className="absolute inset-x-0 top-0 flex h-full items-center justify-center">
1853
+ <td className="px-4 py-10 text-center text-sm text-muted-foreground">
1854
+ No rows match the current filters.
1855
+ </td>
1856
+ </tr>
1857
+ ) : (
1858
+ virtualRows.map((virtualRow) => {
1859
+ const row = rows[virtualRow.index]
1860
+ const visibleRowActions = resolveVisibleRowActions(
1861
+ rowActions ?? [],
1862
+ row,
1863
+ selectedTableRows
1864
+ )
1865
+ const isSelected = row.getIsSelected()
1866
+ const rowContent = (
1867
+ <tr
1868
+ aria-selected={isSelected}
1869
+ className={cn(
1870
+ "absolute left-0 flex w-full bg-card transition-colors",
1871
+ canSelectRows && "cursor-pointer",
1872
+ rowClassName?.(row.original),
1873
+ isSelected &&
1874
+ "bg-primary/10 before:absolute before:-top-px before:left-0 before:h-px before:w-full before:bg-primary before:content-[''] after:absolute after:bottom-0 after:left-0 after:h-px after:w-full after:bg-primary after:content-['']"
1875
+ )}
1876
+ data-index={virtualRow.index}
1877
+ data-row-id={row.id}
1878
+ data-state={isSelected ? "selected" : undefined}
1879
+ key={row.id}
1880
+ onContextMenu={() => handleRowContextMenu(row)}
1881
+ onMouseDown={(event) => handleRowMouseDown(event, row)}
1882
+ onMouseEnter={(event) => handleRowMouseEnter(event, row)}
1883
+ ref={(node) => {
1884
+ if (node) {
1885
+ rowVirtualizer.measureElement(node)
1886
+ }
1887
+ }}
1888
+ style={{
1889
+ minHeight: rowHeight,
1890
+ transform: `translateY(${virtualRow.start}px)`,
1891
+ width: table.getTotalSize(),
1892
+ }}
1893
+ >
1894
+ {row.getVisibleCells().map((cell, index, visibleCells) => {
1895
+ const meta = (cell.column.columnDef.meta ??
1896
+ {}) as DataTableColumnMeta
1897
+ const isActionsCell = cell.column.id === "__actions"
1898
+ const highlightTerms =
1899
+ meta.kind === "text" &&
1900
+ resolveColumnHighlightTerms &&
1901
+ isColumnHighlightEnabled(cell.column.id, meta)
1902
+ ? resolveColumnHighlightTerms(
1903
+ cell.column.id,
1904
+ highlightQuery
1905
+ )
1906
+ : []
1907
+
1908
+ if (isActionsCell) {
1909
+ return (
1910
+ <td
1911
+ className={cn(
1912
+ "flex shrink-0 border-b border-border px-2 py-1.5 align-middle text-sm text-foreground justify-end text-right",
1913
+ meta.cellClassName
1914
+ )}
1915
+ key={cell.id}
1916
+ style={{
1917
+ paddingLeft:
1918
+ index === 0 ? edgeHorizontalPadding : undefined,
1919
+ paddingRight:
1920
+ index === visibleCells.length - 1
1921
+ ? edgeHorizontalPadding
1922
+ : undefined,
1923
+ width: cell.column.getSize(),
1924
+ }}
1925
+ >
1926
+ <div className="w-full min-w-0">
1927
+ <RowActionButtonGroup
1928
+ resolvedActions={visibleRowActions}
1929
+ setStatus={setRowActionStatus}
1930
+ statuses={rowActionStatuses}
1931
+ />
1932
+ </div>
1933
+ </td>
1934
+ )
1935
+ }
1936
+
1937
+ return (
1938
+ <DataTableBodyCell
1939
+ cell={cell}
1940
+ highlightTerms={highlightTerms}
1941
+ key={cell.id}
1942
+ paddingLeft={
1943
+ index === 0 ? edgeHorizontalPadding : undefined
1944
+ }
1945
+ paddingRight={
1946
+ index === visibleCells.length - 1
1947
+ ? edgeHorizontalPadding
1948
+ : undefined
1949
+ }
1950
+ />
1951
+ )
1952
+ })}
1953
+ </tr>
1954
+ )
1955
+
1956
+ if (visibleRowActions.length === 0) {
1957
+ return rowContent
1958
+ }
1959
+
1960
+ return (
1961
+ <ContextMenu key={row.id}>
1962
+ <ContextMenuTrigger asChild>
1963
+ {rowContent}
1964
+ </ContextMenuTrigger>
1965
+ <ContextMenuContent className="w-64">
1966
+ {visibleRowActions.map((resolvedAction) => (
1967
+ <RowActionMenuItem
1968
+ key={resolvedAction.action.id}
1969
+ resolvedAction={resolvedAction}
1970
+ setStatus={setRowActionStatus}
1971
+ statuses={rowActionStatuses}
1972
+ />
1973
+ ))}
1974
+ <ContextMenuSeparator />
1975
+
1976
+ <ContextMenuLabel>
1977
+ {isSelected && selectedTableRows.length > 1
1978
+ ? `${selectedTableRows.length} selected rows`
1979
+ : "Row actions"}
1980
+ </ContextMenuLabel>
1981
+ </ContextMenuContent>
1982
+ </ContextMenu>
1983
+ )
1984
+ })
1985
+ )}
1986
+ </tbody>
1987
+ </table>
1988
+ </div>
1989
+ </section>
1990
+ )
1991
+ }
1992
+
1993
+ export function DataTable<TData extends DataTableRow>(
1994
+ props: DataTableProps<TData>
1995
+ ): React.JSX.Element
1996
+ export function DataTable<TData extends DataTableRow>(
1997
+ props?: Partial<DataTableProps<TData>>
1998
+ ): React.JSX.Element
1999
+ export function DataTable<TData extends DataTableRow>(
2000
+ props: Partial<DataTableProps<TData>> = {}
2001
+ ) {
2002
+ const context = useOptionalDataTableContext<TData>()
2003
+ const resolvedProps = context
2004
+ ? ({ ...context.tableProps, ...props } as Partial<DataTableProps<TData>>)
2005
+ : props
2006
+
2007
+ if (resolvedProps.data === undefined || resolvedProps.id === undefined) {
2008
+ throw new Error(
2009
+ "DataTable must be used inside DataTableProvider or receive data and id props."
2010
+ )
2011
+ }
2012
+
2013
+ return <DataTableView {...(resolvedProps as DataTableProps<TData>)} />
2014
+ }