@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.
@@ -0,0 +1,117 @@
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 ADDED
@@ -0,0 +1,6 @@
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';
@@ -0,0 +1,112 @@
1
+ import {Button, Popover, PopoverContent, PopoverTrigger} from '@sqlrooms/ui';
2
+ import {ClipboardIcon} from '@heroicons/react/24/outline';
3
+ import {shorten} from '@sqlrooms/utils';
4
+ import {createColumnHelper} from '@tanstack/react-table';
5
+ import {ColumnDef} from '@tanstack/table-core';
6
+ import * as arrow from 'apache-arrow';
7
+ import {useMemo} from 'react';
8
+
9
+ const columnHelper = createColumnHelper();
10
+
11
+ // TODO: support fetch the result chunks lazily https://github.com/duckdb/duckdb-wasm/tree/master/packages/duckdb-wasm#query-execution
12
+
13
+ type UseArrowDataTableResult = {
14
+ data: ArrayLike<any>;
15
+ columns: ColumnDef<any, any>[];
16
+ };
17
+
18
+ export type ArrowColumnMeta = {
19
+ type: arrow.DataType;
20
+ isNumeric: boolean;
21
+ };
22
+
23
+ const MAX_VALUE_LENGTH = 64;
24
+
25
+ function valueToString(type: arrow.DataType, value: any): string {
26
+ if (value === null || value === undefined) {
27
+ return 'NULL';
28
+ }
29
+ if (arrow.DataType.isTimestamp(type)) {
30
+ switch (typeof value) {
31
+ case 'number':
32
+ case 'bigint':
33
+ return new Date(Number(value)).toISOString();
34
+ case 'string':
35
+ return new Date(value).toISOString();
36
+ }
37
+ }
38
+ if (arrow.DataType.isTime(type)) {
39
+ switch (typeof value) {
40
+ case 'number':
41
+ case 'bigint':
42
+ return new Date(Number(value) / 1000).toISOString().substring(11, 19);
43
+ case 'string':
44
+ return new Date(value).toISOString().substring(11, 19);
45
+ }
46
+ }
47
+ if (arrow.DataType.isDate(type)) {
48
+ if (value instanceof Date) {
49
+ return value.toISOString();
50
+ }
51
+ }
52
+ return String(value);
53
+ }
54
+ // Only use for small tables or in combination with pagination
55
+ export default function useArrowDataTable(
56
+ table: arrow.Table | undefined,
57
+ ): UseArrowDataTableResult | undefined {
58
+ const data = useMemo(() => ({length: table?.numRows ?? 0}), [table]);
59
+ const columns = useMemo(() => {
60
+ if (!table) return undefined;
61
+ const columns: ColumnDef<any, any>[] = [];
62
+ for (const field of table.schema.fields) {
63
+ columns.push(
64
+ columnHelper.accessor((_row, i) => table.getChild(field.name)?.get(i), {
65
+ cell: (info) => {
66
+ const value = info.getValue();
67
+ const valueStr = valueToString(field.type, value);
68
+
69
+ return valueStr.length > MAX_VALUE_LENGTH ? (
70
+ <Popover>
71
+ <PopoverTrigger asChild>
72
+ <span className="cursor-pointer">
73
+ {shorten(`${valueStr}`, MAX_VALUE_LENGTH)}
74
+ </span>
75
+ </PopoverTrigger>
76
+ <PopoverContent className="font-mono text-xs w-auto max-w-[500px]">
77
+ <div className="space-y-2">
78
+ <div className="font-medium">{`"${field.name}" (${field.type})`}</div>
79
+ <div className="relative">
80
+ {valueStr}
81
+ <Button
82
+ variant="ghost"
83
+ size="icon"
84
+ className="absolute top-0 right-0 h-6 w-6"
85
+ onClick={() => navigator.clipboard.writeText(valueStr)}
86
+ >
87
+ <ClipboardIcon className="h-3 w-3" />
88
+ </Button>
89
+ </div>
90
+ </div>
91
+ </PopoverContent>
92
+ </Popover>
93
+ ) : (
94
+ valueStr
95
+ );
96
+ },
97
+ header: field.name,
98
+ meta: {
99
+ type: field.type,
100
+ isNumeric:
101
+ arrow.DataType.isFloat(field.type) ||
102
+ arrow.DataType.isDecimal(field.type) ||
103
+ arrow.DataType.isInt(field.type),
104
+ },
105
+ }),
106
+ );
107
+ }
108
+ return columns;
109
+ }, [table]);
110
+
111
+ return data && columns ? {data, columns} : undefined;
112
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "@sqlrooms/typescript-config/react-library.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist"
5
+ },
6
+ "include": ["src", "turbo"],
7
+ "exclude": ["node_modules", "dist"]
8
+ }