@sqlrooms/data-table 0.0.0 → 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.
package/eslint.config.js DELETED
@@ -1,4 +0,0 @@
1
- import {config} from '@sqlrooms/eslint-config/react-internal';
2
-
3
- /** @type {import("eslint").Linter.Config} */
4
- export default config;
@@ -1,44 +0,0 @@
1
- import {
2
- Button,
3
- Dialog,
4
- DialogContent,
5
- DialogFooter,
6
- DialogHeader,
7
- DialogTitle,
8
- } from '@sqlrooms/ui';
9
- import {FC} from 'react';
10
- import QueryDataTable from './QueryDataTable';
11
-
12
- type Props = {
13
- title: string | undefined;
14
- query: string | undefined;
15
- tableModal: {
16
- isOpen: boolean;
17
- onClose: () => void;
18
- };
19
- };
20
-
21
- const DataTableModal: FC<Props> = ({title, query, tableModal}) => {
22
- return (
23
- <Dialog
24
- open={tableModal.isOpen}
25
- onOpenChange={(isOpen: boolean) => !isOpen && tableModal.onClose()}
26
- >
27
- <DialogContent className="h-[80vh] max-w-[75vw]">
28
- <DialogHeader>
29
- <DialogTitle>{title ? `Table "${title}"` : ''}</DialogTitle>
30
- </DialogHeader>
31
- <div className="flex-1 bg-muted overflow-hidden">
32
- {tableModal.isOpen && query ? <QueryDataTable query={query} /> : null}
33
- </div>
34
- <DialogFooter>
35
- <Button variant="outline" onClick={tableModal.onClose}>
36
- Close
37
- </Button>
38
- </DialogFooter>
39
- </DialogContent>
40
- </Dialog>
41
- );
42
- };
43
-
44
- export default DataTableModal;
@@ -1,296 +0,0 @@
1
- import {
2
- ChevronDownIcon,
3
- ChevronUpIcon,
4
- ArrowDownIcon,
5
- ChevronDoubleLeftIcon,
6
- ChevronDoubleRightIcon,
7
- ChevronLeftIcon,
8
- ChevronRightIcon,
9
- } from '@heroicons/react/24/solid';
10
- import {
11
- Button,
12
- Input,
13
- Select,
14
- SelectContent,
15
- SelectItem,
16
- SelectTrigger,
17
- SelectValue,
18
- Table,
19
- TableBody,
20
- TableCell,
21
- TableHead,
22
- TableHeader,
23
- TableRow,
24
- Badge,
25
- } from '@sqlrooms/ui';
26
- import {formatCount} from '@sqlrooms/utils';
27
- import {
28
- ColumnDef,
29
- PaginationState,
30
- SortingState,
31
- flexRender,
32
- getCoreRowModel,
33
- getSortedRowModel,
34
- useReactTable,
35
- } from '@tanstack/react-table';
36
- import {useEffect, useMemo, useState} from 'react';
37
- import {ArrowColumnMeta} from './useArrowDataTable';
38
-
39
- export type DataTablePaginatedProps<Data extends object> = {
40
- data?: ArrayLike<Data> | undefined;
41
- columns?: ColumnDef<Data, any>[] | undefined;
42
- pageCount?: number | undefined;
43
- numRows?: number | undefined;
44
- isFetching?: boolean;
45
- isExporting?: boolean;
46
- pagination?: PaginationState;
47
- sorting?: SortingState;
48
- onPaginationChange?: (pagination: PaginationState) => void;
49
- onSortingChange?: (sorting: SortingState) => void;
50
- onExport?: () => void;
51
- };
52
-
53
- export default function DataTablePaginated<Data extends object>({
54
- data,
55
- columns,
56
- pageCount,
57
- numRows,
58
- pagination,
59
- sorting,
60
- onPaginationChange,
61
- onSortingChange,
62
- onExport,
63
- isExporting,
64
- isFetching,
65
- }: DataTablePaginatedProps<Data>) {
66
- const defaultData = useMemo(() => [], []);
67
- const table = useReactTable({
68
- data: (data ?? defaultData) as any[],
69
- columns: columns ?? [],
70
- pageCount: pageCount ?? -1,
71
- getSortedRowModel: getSortedRowModel(),
72
- onSortingChange: (update) => {
73
- if (onSortingChange && sorting && typeof update === 'function') {
74
- onSortingChange(update(sorting));
75
- }
76
- },
77
- onPaginationChange: (update) => {
78
- if (onPaginationChange && pagination && typeof update === 'function') {
79
- onPaginationChange(update(pagination));
80
- }
81
- },
82
- getCoreRowModel: getCoreRowModel(),
83
- manualPagination: true,
84
- state: {
85
- pagination,
86
- sorting,
87
- },
88
- });
89
-
90
- const [internalPageIndex, setInternalPageIndex] = useState(
91
- pagination?.pageIndex ?? 0,
92
- );
93
- useEffect(() => {
94
- setInternalPageIndex(pagination?.pageIndex ?? 0);
95
- }, [pagination?.pageIndex]);
96
-
97
- return (
98
- <div className="relative w-full h-full flex flex-col">
99
- <div className="flex-1 overflow-hidden border border-border font-mono">
100
- <div className="overflow-auto h-full">
101
- <Table disableWrapper>
102
- <TableHeader>
103
- {table.getHeaderGroups().map((headerGroup) => (
104
- <TableRow key={headerGroup.id}>
105
- <TableHead
106
- className={`
107
- sticky top-0 left-0 w-auto whitespace-nowrap py-2
108
- bg-background border-r text-center z-20
109
- `}
110
- >
111
- {isFetching ? (
112
- <div className="animate-spin h-4 w-4 border-2 border-primary border-t-transparent rounded-full" />
113
- ) : null}
114
- </TableHead>
115
- {headerGroup.headers.map((header) => {
116
- const meta = header.column.columnDef
117
- .meta as ArrowColumnMeta;
118
- return (
119
- <TableHead
120
- key={header.id}
121
- colSpan={header.colSpan}
122
- className={`
123
- sticky top-0 w-auto whitespace-nowrap cursor-pointer py-2
124
- bg-background border-r hover:bg-muted/80 z-10
125
- ${meta?.isNumeric ? 'text-right' : 'text-left'}
126
- `}
127
- onClick={header.column.getToggleSortingHandler()}
128
- >
129
- <div className="flex gap-2 items-center">
130
- {header.isPlaceholder ? null : (
131
- <div>
132
- {flexRender(
133
- header.column.columnDef.header,
134
- header.getContext(),
135
- )}
136
- </div>
137
- )}
138
- {header.column.getIsSorted() ? (
139
- header.column.getIsSorted() === 'desc' ? (
140
- <ChevronDownIcon className="h-4 w-4" />
141
- ) : (
142
- <ChevronUpIcon className="h-4 w-4" />
143
- )
144
- ) : null}
145
- <div className="flex-1" />
146
- <Badge
147
- variant="outline"
148
- className="opacity-30 text-[9px] max-w-[70px] truncate"
149
- >
150
- {String(meta?.type)}
151
- </Badge>
152
- </div>
153
- </TableHead>
154
- );
155
- })}
156
- <TableHead className="sticky top-0 w-full whitespace-nowrap py-2 bg-background border-r border-t" />
157
- </TableRow>
158
- ))}
159
- </TableHeader>
160
- <TableBody>
161
- {table.getRowModel().rows.map((row, i) => (
162
- <TableRow key={row.id} className="hover:bg-muted/50">
163
- <TableCell className="text-xs border-r bg-muted text-center text-muted-foreground sticky left-0">
164
- {pagination
165
- ? `${pagination.pageIndex * pagination.pageSize + i + 1}`
166
- : ''}
167
- </TableCell>
168
- {row.getVisibleCells().map((cell) => {
169
- const meta = cell.column.columnDef.meta as ArrowColumnMeta;
170
- return (
171
- <TableCell
172
- key={cell.id}
173
- className={`
174
- text-[11px] border-r max-w-[500px] overflow-hidden truncate px-7
175
- ${meta?.isNumeric ? 'text-right' : 'text-left'}
176
- `}
177
- >
178
- {flexRender(
179
- cell.column.columnDef.cell,
180
- cell.getContext(),
181
- )}
182
- </TableCell>
183
- );
184
- })}
185
- <TableCell className="border-r">&nbsp;</TableCell>
186
- </TableRow>
187
- ))}
188
- </TableBody>
189
- </Table>
190
- </div>
191
- </div>
192
- <div className="sticky bottom-0 left-0 bg-background p-2 flex gap-2 items-center flex-wrap border border-t-0">
193
- <Button
194
- variant="outline"
195
- size="icon"
196
- onClick={() => table.setPageIndex(0)}
197
- disabled={!table.getCanPreviousPage()}
198
- >
199
- <ChevronDoubleLeftIcon className="h-4 w-4" />
200
- </Button>
201
- <Button
202
- variant="outline"
203
- size="icon"
204
- onClick={() => table.previousPage()}
205
- disabled={!table.getCanPreviousPage()}
206
- >
207
- <ChevronLeftIcon className="h-4 w-4" />
208
- </Button>
209
- <div className="flex items-center text-sm ml-1 gap-1">
210
- <div>Page</div>
211
- <Input
212
- type="number"
213
- min={1}
214
- max={table.getPageCount()}
215
- className="w-16 h-8"
216
- value={internalPageIndex + 1}
217
- onChange={(e) => {
218
- const value = e.target.value;
219
- if (value) {
220
- const page = Math.max(
221
- 0,
222
- Math.min(table.getPageCount() - 1, Number(value) - 1),
223
- );
224
- setInternalPageIndex(page);
225
- }
226
- }}
227
- onBlur={() => {
228
- if (internalPageIndex !== pagination?.pageIndex) {
229
- table.setPageIndex(internalPageIndex);
230
- }
231
- }}
232
- />
233
- <div>{`of ${formatCount(table.getPageCount())}`}</div>
234
- </div>
235
- <Button
236
- variant="outline"
237
- size="icon"
238
- onClick={() => table.nextPage()}
239
- disabled={!table.getCanNextPage()}
240
- >
241
- <ChevronRightIcon className="h-4 w-4" />
242
- </Button>
243
- <Button
244
- variant="outline"
245
- size="icon"
246
- onClick={() => table.setPageIndex(table.getPageCount() - 1)}
247
- disabled={!table.getCanNextPage()}
248
- >
249
- <ChevronDoubleRightIcon className="h-4 w-4" />
250
- </Button>
251
- <Select
252
- value={String(table.getState().pagination.pageSize)}
253
- onValueChange={(value) => table.setPageSize(Number(value))}
254
- >
255
- <SelectTrigger className="w-[110px] h-8">
256
- <SelectValue />
257
- </SelectTrigger>
258
- <SelectContent>
259
- {[10, 50, 100, 500, 1000].map((pageSize) => (
260
- <SelectItem key={pageSize} value={String(pageSize)}>
261
- {`${pageSize} rows`}
262
- </SelectItem>
263
- ))}
264
- </SelectContent>
265
- </Select>
266
-
267
- <div className="flex-1" />
268
-
269
- {numRows !== undefined && isFinite(numRows) ? (
270
- <div className="text-sm font-normal">
271
- {`${formatCount(numRows)} rows`}
272
- </div>
273
- ) : null}
274
-
275
- {onExport ? (
276
- <Button
277
- variant="outline"
278
- size="sm"
279
- onClick={onExport}
280
- disabled={isExporting}
281
- >
282
- {isExporting ? (
283
- <div className="animate-spin h-4 w-4 border-2 border-primary border-t-transparent rounded-full mr-2" />
284
- ) : (
285
- <ArrowDownIcon className="h-4 w-4 mr-2" />
286
- )}
287
- Export CSV
288
- </Button>
289
- ) : null}
290
- </div>
291
- {isFetching ? (
292
- <div className="absolute inset-0 bg-background/80 animate-pulse" />
293
- ) : null}
294
- </div>
295
- );
296
- }
@@ -1,190 +0,0 @@
1
- import {ChevronDownIcon, ChevronUpIcon} from '@heroicons/react/24/solid';
2
- import {
3
- Table,
4
- TableBody,
5
- TableCell,
6
- TableHead,
7
- TableHeader,
8
- TableRow,
9
- Badge,
10
- } from '@sqlrooms/ui';
11
- import {ErrorPane, SpinnerPane} from '@sqlrooms/ui';
12
- import {formatCount} from '@sqlrooms/utils';
13
- import {
14
- ColumnDef,
15
- SortingState,
16
- flexRender,
17
- getCoreRowModel,
18
- getSortedRowModel,
19
- useReactTable,
20
- } from '@tanstack/react-table';
21
- import * as React from 'react';
22
- import {useVirtual} from 'react-virtual';
23
-
24
- export type Props<Data extends object> = {
25
- data?: ArrayLike<Data>;
26
- columns?: ColumnDef<Data, any>[];
27
- isFetching?: boolean;
28
- error?: any;
29
- isPreview?: boolean;
30
- };
31
-
32
- export type DataTableProps<Data extends object> = {
33
- data: ArrayLike<Data>;
34
- columns: ColumnDef<Data, any>[];
35
- isPreview?: boolean;
36
- };
37
-
38
- const DataTableVirtualized = React.memo(function DataTableVirtualized<
39
- Data extends object,
40
- >({data, columns, isPreview}: DataTableProps<Data>) {
41
- const [sorting, setSorting] = React.useState<SortingState>([]);
42
- const table = useReactTable({
43
- columns,
44
- data: data as Data[],
45
- onSortingChange: setSorting,
46
- getCoreRowModel: getCoreRowModel(),
47
- getSortedRowModel: getSortedRowModel(),
48
- state: {
49
- sorting,
50
- },
51
- });
52
- const tableContainerRef = React.useRef<HTMLDivElement>(null);
53
-
54
- const {rows} = table.getRowModel();
55
- const rowVirtualizer = useVirtual({
56
- parentRef: tableContainerRef,
57
- size: rows.length,
58
- overscan: 20,
59
- });
60
- const {virtualItems: virtualRows, totalSize} = rowVirtualizer;
61
-
62
- const paddingTop = virtualRows.length > 0 ? virtualRows?.[0]?.start || 0 : 0;
63
- const paddingBottom =
64
- virtualRows.length > 0
65
- ? totalSize - (virtualRows?.[virtualRows.length - 1]?.end || 0)
66
- : 0;
67
-
68
- return (
69
- <div className="flex flex-col overflow-hidden">
70
- <div className="overflow-hidden border border-border">
71
- <div ref={tableContainerRef} className="overflow-auto h-full">
72
- <Table disableWrapper>
73
- <TableHeader>
74
- {table.getHeaderGroups().map((headerGroup) => (
75
- <TableRow key={headerGroup.id}>
76
- <TableHead
77
- className={`
78
- sticky top-0 left-0 w-auto whitespace-nowrap py-2
79
- bg-background border-r text-center z-20
80
- `}
81
- />
82
- {headerGroup.headers.map((header) => {
83
- const meta = header.column.columnDef.meta as any;
84
- return (
85
- <TableHead
86
- key={header.id}
87
- className={`
88
- sticky top-0 font-mono whitespace-nowrap cursor-pointer px-7 py-2
89
- bg-background border-r hover:bg-muted/80 z-10
90
- ${meta?.isNumeric ? 'text-right' : 'text-left'}
91
- `}
92
- onClick={header.column.getToggleSortingHandler()}
93
- >
94
- <div
95
- className={`flex items-center gap-1 ${
96
- meta?.isNumeric ? 'justify-end' : 'justify-start'
97
- }`}
98
- >
99
- {flexRender(
100
- header.column.columnDef.header,
101
- header.getContext(),
102
- )}
103
- {header.column.getIsSorted() ? (
104
- header.column.getIsSorted() === 'desc' ? (
105
- <ChevronDownIcon className="h-4 w-4" />
106
- ) : (
107
- <ChevronUpIcon className="h-4 w-4" />
108
- )
109
- ) : null}
110
- <div className="flex-1" />
111
- <Badge
112
- variant="outline"
113
- className="opacity-30 text-[9px]"
114
- >
115
- {String(meta?.type)}
116
- </Badge>
117
- </div>
118
- </TableHead>
119
- );
120
- })}
121
- </TableRow>
122
- ))}
123
- </TableHeader>
124
- <TableBody>
125
- {paddingTop > 0 && (
126
- <TableRow>
127
- <TableCell style={{height: `${paddingTop}px`}} />
128
- </TableRow>
129
- )}
130
- {virtualRows.map((virtualRow) => {
131
- const row = rows[virtualRow.index];
132
- if (!row) return null;
133
- return (
134
- <TableRow key={row.id} className="hover:bg-muted/50">
135
- <TableCell className="text-xs border-r bg-muted text-center text-muted-foreground sticky left-0">
136
- {virtualRow.index + 1}
137
- </TableCell>
138
- {row.getVisibleCells().map((cell) => {
139
- const meta = cell.column.columnDef.meta as any;
140
- return (
141
- <TableCell
142
- key={cell.id}
143
- className={`
144
- text-xs border-r max-w-[500px] overflow-hidden truncate px-7
145
- ${meta?.isNumeric ? 'text-right' : 'text-left'}
146
- `}
147
- >
148
- {flexRender(
149
- cell.column.columnDef.cell,
150
- cell.getContext(),
151
- )}
152
- </TableCell>
153
- );
154
- })}
155
- </TableRow>
156
- );
157
- })}
158
- {paddingBottom > 0 && (
159
- <TableRow>
160
- <TableCell style={{height: `${paddingBottom}px`}} />
161
- </TableRow>
162
- )}
163
- </TableBody>
164
- </Table>
165
- </div>
166
- </div>
167
- <div className="sticky bottom-0 left-0 py-2 px-4 text-xs font-mono bg-background border border-t-0">
168
- {`${isPreview ? 'Preview of the first ' : ''}${formatCount(data.length)} rows`}
169
- </div>
170
- </div>
171
- );
172
- });
173
-
174
- export default function DataTableWithLoader<Data extends object>(
175
- props: Props<Data>,
176
- ) {
177
- const {isPreview, isFetching, error, ...rest} = props;
178
- const {data, columns} = rest;
179
- return error ? (
180
- <ErrorPane error={error} />
181
- ) : isFetching ? (
182
- <SpinnerPane h="100%" />
183
- ) : data && columns ? (
184
- <DataTableVirtualized
185
- data={data}
186
- columns={columns as any}
187
- isPreview={isPreview}
188
- />
189
- ) : null;
190
- }
@@ -1,117 +0,0 @@
1
- import {SpinnerPane} from '@sqlrooms/ui';
2
- import {
3
- escapeId,
4
- exportToCsv,
5
- getColValAsNumber,
6
- useDuckDb,
7
- } from '@sqlrooms/duckdb';
8
- import {genRandomStr} from '@sqlrooms/utils';
9
- import {PaginationState, SortingState} from '@tanstack/table-core';
10
- import {FC, Suspense, useEffect, useState} from 'react';
11
- import DataTablePaginated from './DataTablePaginated';
12
- import useArrowDataTable from './useArrowDataTable';
13
-
14
- type Props = {
15
- query: string;
16
- queryKeyComponents?: any[];
17
- };
18
-
19
- const QueryDataTable: FC<Props> = ({query}) => {
20
- const {conn} = useDuckDb();
21
- const [sorting, setSorting] = useState<SortingState>([]);
22
- const [pagination, setPagination] = useState<PaginationState>({
23
- pageIndex: 0,
24
- pageSize: 100,
25
- });
26
-
27
- const [count, setCount] = useState<number | undefined>(undefined);
28
- const [data, setData] = useState<any>(null);
29
- const [isFetching, setIsFetching] = useState(false);
30
- const [isExporting, setIsExporting] = useState(false);
31
-
32
- // Fetch row count
33
- useEffect(() => {
34
- const fetchCount = async () => {
35
- try {
36
- setIsFetching(true);
37
- const result = await conn.query(`SELECT COUNT(*)::int FROM (${query})`);
38
- setCount(getColValAsNumber(result));
39
- } catch (error) {
40
- console.error('Error fetching count:', error);
41
- } finally {
42
- setIsFetching(false);
43
- }
44
- };
45
-
46
- fetchCount();
47
- }, [query, conn]);
48
-
49
- // Fetch data
50
- useEffect(() => {
51
- const fetchData = async () => {
52
- try {
53
- setIsFetching(true);
54
- const result = await conn.query(
55
- `SELECT * FROM (
56
- ${query}
57
- ) ${
58
- sorting.length > 0
59
- ? `ORDER BY ${sorting
60
- .map((d) => `${escapeId(d.id)}${d.desc ? ' DESC' : ''}`)
61
- .join(', ')}`
62
- : ''
63
- }
64
- OFFSET ${pagination.pageIndex * pagination.pageSize}
65
- LIMIT ${pagination.pageSize}`,
66
- );
67
- setData(result);
68
- } catch (error) {
69
- console.error('Error fetching data:', error);
70
- } finally {
71
- setIsFetching(false);
72
- }
73
- };
74
-
75
- fetchData();
76
- }, [query, pagination, sorting, conn]);
77
-
78
- const arrowTableData = useArrowDataTable(data);
79
-
80
- const handleExport = async () => {
81
- if (!query) return;
82
- try {
83
- setIsExporting(true);
84
- await exportToCsv(query, `export-${genRandomStr(5)}.csv`);
85
- } finally {
86
- setIsExporting(false);
87
- }
88
- };
89
-
90
- return (
91
- <DataTablePaginated
92
- {...arrowTableData}
93
- pageCount={Math.ceil((count ?? 0) / pagination.pageSize)}
94
- numRows={count}
95
- isFetching={isFetching}
96
- pagination={pagination}
97
- onPaginationChange={setPagination}
98
- sorting={sorting}
99
- onSortingChange={setSorting}
100
- onExport={handleExport}
101
- isExporting={isExporting}
102
- />
103
- );
104
- };
105
-
106
- const QueryDataTableWithSuspense: FC<Props> = (props) => {
107
- return (
108
- <Suspense fallback={<SpinnerPane className="w-full h-full" />}>
109
- <QueryDataTable
110
- {...props}
111
- key={props.query} // reset state when query changes
112
- />
113
- </Suspense>
114
- );
115
- };
116
-
117
- export default QueryDataTableWithSuspense;
package/src/index.ts DELETED
@@ -1,6 +0,0 @@
1
- export {default as DataTableModal} from './DataTableModal';
2
- export * from './DataTablePaginated';
3
- export {default as DataTablePaginated} from './DataTablePaginated';
4
- export {default as DataTableVirtualized} from './DataTableVirtualized';
5
- export {default as QueryDataTable} from './QueryDataTable';
6
- export {default as useArrowDataTable} from './useArrowDataTable';