@sqlrooms/data-table 0.0.0
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/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-dev.log +7 -0
- package/.turbo/turbo-lint.log +31 -0
- package/CHANGELOG.md +8 -0
- package/LICENSE.md +9 -0
- package/dist/DataTableModal.d.ts +12 -0
- package/dist/DataTableModal.d.ts.map +1 -0
- package/dist/DataTableModal.js +7 -0
- package/dist/DataTablePaginated.d.ts +16 -0
- package/dist/DataTablePaginated.d.ts.map +1 -0
- package/dist/DataTablePaginated.js +65 -0
- package/dist/DataTableVirtualized.d.ts +15 -0
- package/dist/DataTableVirtualized.d.ts.map +1 -0
- package/dist/DataTableVirtualized.js +60 -0
- package/dist/QueryDataTable.d.ts +8 -0
- package/dist/QueryDataTable.d.ts.map +1 -0
- package/dist/QueryDataTable.js +79 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/useArrowDataTable.d.ts +13 -0
- package/dist/useArrowDataTable.d.ts.map +1 -0
- package/dist/useArrowDataTable.js +65 -0
- package/docs/architecture.md +18 -0
- package/eslint.config.js +4 -0
- package/package.json +32 -0
- package/src/DataTableModal.tsx +44 -0
- package/src/DataTablePaginated.tsx +296 -0
- package/src/DataTableVirtualized.tsx +190 -0
- package/src/QueryDataTable.tsx +117 -0
- package/src/index.ts +6 -0
- package/src/useArrowDataTable.tsx +112 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ColumnDef } from '@tanstack/table-core';
|
|
2
|
+
import * as arrow from 'apache-arrow';
|
|
3
|
+
type UseArrowDataTableResult = {
|
|
4
|
+
data: ArrayLike<any>;
|
|
5
|
+
columns: ColumnDef<any, any>[];
|
|
6
|
+
};
|
|
7
|
+
export type ArrowColumnMeta = {
|
|
8
|
+
type: arrow.DataType;
|
|
9
|
+
isNumeric: boolean;
|
|
10
|
+
};
|
|
11
|
+
export default function useArrowDataTable(table: arrow.Table | undefined): UseArrowDataTableResult | undefined;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=useArrowDataTable.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useArrowDataTable.d.ts","sourceRoot":"","sources":["../src/useArrowDataTable.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAC,SAAS,EAAC,MAAM,sBAAsB,CAAC;AAC/C,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AAOtC,KAAK,uBAAuB,GAAG;IAC7B,IAAI,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC;IACrB,OAAO,EAAE,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,KAAK,CAAC,QAAQ,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;CACpB,CAAC;AAkCF,MAAM,CAAC,OAAO,UAAU,iBAAiB,CACvC,KAAK,EAAE,KAAK,CAAC,KAAK,GAAG,SAAS,GAC7B,uBAAuB,GAAG,SAAS,CAuDrC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button, Popover, PopoverContent, PopoverTrigger } from '@sqlrooms/ui';
|
|
3
|
+
import { ClipboardIcon } from '@heroicons/react/24/outline';
|
|
4
|
+
import { shorten } from '@sqlrooms/utils';
|
|
5
|
+
import { createColumnHelper } from '@tanstack/react-table';
|
|
6
|
+
import * as arrow from 'apache-arrow';
|
|
7
|
+
import { useMemo } from 'react';
|
|
8
|
+
const columnHelper = createColumnHelper();
|
|
9
|
+
const MAX_VALUE_LENGTH = 64;
|
|
10
|
+
function valueToString(type, value) {
|
|
11
|
+
if (value === null || value === undefined) {
|
|
12
|
+
return 'NULL';
|
|
13
|
+
}
|
|
14
|
+
if (arrow.DataType.isTimestamp(type)) {
|
|
15
|
+
switch (typeof value) {
|
|
16
|
+
case 'number':
|
|
17
|
+
case 'bigint':
|
|
18
|
+
return new Date(Number(value)).toISOString();
|
|
19
|
+
case 'string':
|
|
20
|
+
return new Date(value).toISOString();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (arrow.DataType.isTime(type)) {
|
|
24
|
+
switch (typeof value) {
|
|
25
|
+
case 'number':
|
|
26
|
+
case 'bigint':
|
|
27
|
+
return new Date(Number(value) / 1000).toISOString().substring(11, 19);
|
|
28
|
+
case 'string':
|
|
29
|
+
return new Date(value).toISOString().substring(11, 19);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (arrow.DataType.isDate(type)) {
|
|
33
|
+
if (value instanceof Date) {
|
|
34
|
+
return value.toISOString();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return String(value);
|
|
38
|
+
}
|
|
39
|
+
// Only use for small tables or in combination with pagination
|
|
40
|
+
export default function useArrowDataTable(table) {
|
|
41
|
+
const data = useMemo(() => ({ length: table?.numRows ?? 0 }), [table]);
|
|
42
|
+
const columns = useMemo(() => {
|
|
43
|
+
if (!table)
|
|
44
|
+
return undefined;
|
|
45
|
+
const columns = [];
|
|
46
|
+
for (const field of table.schema.fields) {
|
|
47
|
+
columns.push(columnHelper.accessor((_row, i) => table.getChild(field.name)?.get(i), {
|
|
48
|
+
cell: (info) => {
|
|
49
|
+
const value = info.getValue();
|
|
50
|
+
const valueStr = valueToString(field.type, value);
|
|
51
|
+
return valueStr.length > MAX_VALUE_LENGTH ? (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("span", { className: "cursor-pointer", children: shorten(`${valueStr}`, MAX_VALUE_LENGTH) }) }), _jsx(PopoverContent, { className: "font-mono text-xs w-auto max-w-[500px]", children: _jsxs("div", { className: "space-y-2", children: [_jsx("div", { className: "font-medium", children: `"${field.name}" (${field.type})` }), _jsxs("div", { className: "relative", children: [valueStr, _jsx(Button, { variant: "ghost", size: "icon", className: "absolute top-0 right-0 h-6 w-6", onClick: () => navigator.clipboard.writeText(valueStr), children: _jsx(ClipboardIcon, { className: "h-3 w-3" }) })] })] }) })] })) : (valueStr);
|
|
52
|
+
},
|
|
53
|
+
header: field.name,
|
|
54
|
+
meta: {
|
|
55
|
+
type: field.type,
|
|
56
|
+
isNumeric: arrow.DataType.isFloat(field.type) ||
|
|
57
|
+
arrow.DataType.isDecimal(field.type) ||
|
|
58
|
+
arrow.DataType.isInt(field.type),
|
|
59
|
+
},
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
return columns;
|
|
63
|
+
}, [table]);
|
|
64
|
+
return data && columns ? { data, columns } : undefined;
|
|
65
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
## Package Structure
|
|
2
|
+
|
|
3
|
+
SQLRooms is organized into several key packages:
|
|
4
|
+
|
|
5
|
+
### Core Packages
|
|
6
|
+
|
|
7
|
+
- `@sqlrooms/project-builder`: Core framework and UI components
|
|
8
|
+
- `@sqlrooms/project-config`: Configuration and type definitions
|
|
9
|
+
- `@sqlrooms/layout`: Layout system and panel management
|
|
10
|
+
- `@sqlrooms/ui`: Shared UI components and styling
|
|
11
|
+
|
|
12
|
+
### Feature Packages
|
|
13
|
+
|
|
14
|
+
- `@sqlrooms/data-table`: Interactive data grid component for query results
|
|
15
|
+
- `@sqlrooms/duckdb`: DuckDB-WASM integration and utilities
|
|
16
|
+
- `@sqlrooms/dropzone`: File upload and drag-and-drop functionality
|
|
17
|
+
- `@sqlrooms/s3-browser`: S3-compatible storage browser and file manager
|
|
18
|
+
- `@sqlrooms/sql-editor`: SQL query editor with syntax highlighting and autocompletion
|
package/eslint.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sqlrooms/data-table",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "src/index.ts",
|
|
6
|
+
"module": "dist/index.js",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"private": false,
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@heroicons/react": "*",
|
|
14
|
+
"@sqlrooms/duckdb": "0.0.0",
|
|
15
|
+
"@sqlrooms/ui": "0.0.0",
|
|
16
|
+
"@sqlrooms/utils": "0.0.0",
|
|
17
|
+
"@tanstack/react-table": "^8.10.7",
|
|
18
|
+
"@tanstack/table-core": "^8.10.7",
|
|
19
|
+
"react-virtual": "2.10.4"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"apache-arrow": "*",
|
|
23
|
+
"react": "*",
|
|
24
|
+
"react-dom": "*"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"dev": "tsc -w",
|
|
28
|
+
"build": "tsc",
|
|
29
|
+
"lint": "eslint ."
|
|
30
|
+
},
|
|
31
|
+
"gitHead": "4b0c709542475e4f95db0b2a8405ecadcf2ec186"
|
|
32
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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;
|
|
@@ -0,0 +1,296 @@
|
|
|
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"> </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
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
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
|
+
}
|