6
6
getFilteredRowModel ,
7
7
getPaginationRowModel ,
8
8
getSortedRowModel ,
9
+ PaginationState ,
10
+ Table as ReactTable ,
9
11
useReactTable ,
10
12
} from '@tanstack/react-table' ;
11
13
@@ -25,12 +27,19 @@ import DebouncedInput from './DebouncedInput';
25
27
import loadData from '../lib/loadData' ;
26
28
import LoadingSpinner from './LoadingSpinner' ;
27
29
30
+ export type TableData = { cols : { key : string , name : string } [ ] ; data : any [ ] ; total : number } ;
31
+
28
32
export type TableProps = {
29
33
data ?: Array < { [ key : string ] : number | string } > ;
30
34
cols ?: Array < { [ key : string ] : string } > ;
31
35
csv ?: string ;
32
36
url ?: string ;
33
37
fullWidth ?: boolean ;
38
+ datastoreConfig ?: {
39
+ dataStoreURI : string ;
40
+ rowsPerPage ?: number ;
41
+ dataMapperFn : ( data ) => Promise < TableData > | TableData ;
42
+ } ;
34
43
} ;
35
44
36
45
export const Table = ( {
@@ -39,8 +48,28 @@ export const Table = ({
39
48
csv = '' ,
40
49
url = '' ,
41
50
fullWidth = false ,
51
+ datastoreConfig,
42
52
} : TableProps ) => {
43
53
const [ isLoading , setIsLoading ] = useState < boolean > ( false ) ;
54
+ const [ pageMap , setPageMap ] = useState ( new Map < number , boolean > ( ) ) ;
55
+ const {
56
+ dataMapperFn,
57
+ dataStoreURI,
58
+ rowsPerPage = 10 ,
59
+ } = datastoreConfig ?? { } ;
60
+
61
+ const [ globalFilter , setGlobalFilter ] = useState ( '' ) ;
62
+ const [ isLoadingPage , setIsLoadingPage ] = useState < boolean > ( false ) ;
63
+ const [ totalOfRows , setTotalOfRows ] = useState < number > ( 0 ) ;
64
+
65
+ const [ { pageIndex, pageSize } , setPagination ] = useState < PaginationState > ( {
66
+ pageIndex : 0 ,
67
+ pageSize : rowsPerPage ,
68
+ } ) ;
69
+
70
+ const [ lastIndex , setLastIndex ] = useState ( pageSize ) ;
71
+ const [ startIndex , setStartIndex ] = useState ( 0 ) ;
72
+ const [ hasSorted , setHasSorted ] = useState ( false ) ;
44
73
45
74
if ( csv ) {
46
75
const out = parseCsv ( csv ) ;
@@ -62,21 +91,56 @@ export const Table = ({
62
91
) ;
63
92
} , [ data , cols ] ) ;
64
93
65
- const [ globalFilter , setGlobalFilter ] = useState ( '' ) ;
94
+ let table : ReactTable < unknown > ;
66
95
67
- const table = useReactTable ( {
68
- data,
69
- columns : tableCols ,
70
- getCoreRowModel : getCoreRowModel ( ) ,
71
- state : {
72
- globalFilter,
73
- } ,
74
- globalFilterFn : globalFilterFn ,
75
- onGlobalFilterChange : setGlobalFilter ,
76
- getFilteredRowModel : getFilteredRowModel ( ) ,
77
- getPaginationRowModel : getPaginationRowModel ( ) ,
78
- getSortedRowModel : getSortedRowModel ( ) ,
79
- } ) ;
96
+ if ( datastoreConfig ) {
97
+ useEffect ( ( ) => {
98
+ setIsLoading ( true ) ;
99
+ fetch ( `${ dataStoreURI } &limit=${ rowsPerPage } &offset=0` )
100
+ . then ( ( res ) => res . json ( ) )
101
+ . then ( async ( res ) => {
102
+ const { data, cols, total } = await dataMapperFn ( res ) ;
103
+ setData ( data ) ;
104
+ setCols ( cols ) ;
105
+ setTotalOfRows ( Math . ceil ( total / rowsPerPage ) ) ;
106
+ pageMap . set ( 0 , true ) ;
107
+ } )
108
+ . finally ( ( ) => setIsLoading ( false ) ) ;
109
+ } , [ dataStoreURI ] ) ;
110
+
111
+ table = useReactTable ( {
112
+ data,
113
+ pageCount : totalOfRows ,
114
+ columns : tableCols ,
115
+ getCoreRowModel : getCoreRowModel ( ) ,
116
+ state : {
117
+ pagination : { pageIndex, pageSize } ,
118
+ } ,
119
+ getFilteredRowModel : getFilteredRowModel ( ) ,
120
+ manualPagination : true ,
121
+ onPaginationChange : setPagination ,
122
+ getSortedRowModel : getSortedRowModel ( ) ,
123
+ } ) ;
124
+
125
+ useEffect ( ( ) => {
126
+ if ( ! hasSorted ) return ;
127
+ queryDataByText ( globalFilter ) ;
128
+ } , [ table . getState ( ) . sorting ] ) ;
129
+ } else {
130
+ table = useReactTable ( {
131
+ data,
132
+ columns : tableCols ,
133
+ getCoreRowModel : getCoreRowModel ( ) ,
134
+ state : {
135
+ globalFilter,
136
+ } ,
137
+ globalFilterFn : globalFilterFn ,
138
+ onGlobalFilterChange : setGlobalFilter ,
139
+ getFilteredRowModel : getFilteredRowModel ( ) ,
140
+ getPaginationRowModel : getPaginationRowModel ( ) ,
141
+ getSortedRowModel : getSortedRowModel ( ) ,
142
+ } ) ;
143
+ }
80
144
81
145
useEffect ( ( ) => {
82
146
if ( url ) {
@@ -91,6 +155,70 @@ export const Table = ({
91
155
}
92
156
} , [ url ] ) ;
93
157
158
+ const queryDataByText = ( filter ) => {
159
+ setIsLoadingPage ( true ) ;
160
+ const sortedParam = getSortParam ( ) ;
161
+ fetch (
162
+ `${ dataStoreURI } &limit=${ rowsPerPage } &offset=0&q=${ filter } ${ sortedParam } `
163
+ )
164
+ . then ( ( res ) => res . json ( ) )
165
+ . then ( async ( res ) => {
166
+ const { data, total = 0 } = await dataMapperFn ( res ) ;
167
+ setTotalOfRows ( Math . ceil ( total / rowsPerPage ) ) ;
168
+ setData ( data ) ;
169
+ const newMap = new Map ( ) ;
170
+ newMap . set ( 0 , true ) ;
171
+ setPageMap ( newMap ) ;
172
+ table . setPageIndex ( 0 ) ;
173
+ setStartIndex ( 0 ) ;
174
+ setLastIndex ( pageSize ) ;
175
+ } )
176
+ . finally ( ( ) => setIsLoadingPage ( false ) ) ;
177
+ } ;
178
+
179
+ const getSortParam = ( ) => {
180
+ const sort = table . getState ( ) . sorting ;
181
+ return sort . length == 0
182
+ ? ``
183
+ : '&sort=' +
184
+ sort
185
+ . map (
186
+ ( x , i ) =>
187
+ `${ x . id } ${
188
+ i === sort . length - 1 ? ( x . desc ? ` desc` : ` asc` ) : `,`
189
+ } `
190
+ )
191
+ . reduce ( ( x1 , x2 ) => x1 + x2 ) ;
192
+ } ;
193
+
194
+ const queryPaginatedData = ( newPageIndex ) => {
195
+ let newStartIndex = newPageIndex * pageSize ;
196
+ setStartIndex ( newStartIndex ) ;
197
+ setLastIndex ( newStartIndex + pageSize ) ;
198
+
199
+ if ( ! pageMap . get ( newPageIndex ) ) pageMap . set ( newPageIndex , true ) ;
200
+ else return ;
201
+
202
+ const sortedParam = getSortParam ( ) ;
203
+
204
+ setIsLoadingPage ( true ) ;
205
+ fetch (
206
+ `${ dataStoreURI } &limit=${ rowsPerPage } &offset=${
207
+ newStartIndex + pageSize
208
+ } &q=${ globalFilter } ${ sortedParam } `
209
+ )
210
+ . then ( ( res ) => res . json ( ) )
211
+ . then ( async ( res ) => {
212
+ const { data : responseData } = await dataMapperFn ( res ) ;
213
+ responseData . forEach ( ( e ) => {
214
+ data [ newStartIndex ] = e ;
215
+ newStartIndex ++ ;
216
+ } ) ;
217
+ setData ( [ ...data ] ) ;
218
+ } )
219
+ . finally ( ( ) => setIsLoadingPage ( false ) ) ;
220
+ } ;
221
+
94
222
return isLoading ? (
95
223
< div className = "w-full h-full min-h-[500px] flex items-center justify-center" >
96
224
< LoadingSpinner />
@@ -99,7 +227,10 @@ export const Table = ({
99
227
< div className = { `${ fullWidth ? 'w-[90vw] ml-[calc(50%-45vw)]' : 'w-full' } ` } >
100
228
< DebouncedInput
101
229
value = { globalFilter ?? '' }
102
- onChange = { ( value : any ) => setGlobalFilter ( String ( value ) ) }
230
+ onChange = { ( value : any ) => {
231
+ if ( datastoreConfig ) queryDataByText ( String ( value ) ) ;
232
+ setGlobalFilter ( String ( value ) ) ;
233
+ } }
103
234
className = "p-2 text-sm shadow border border-block"
104
235
placeholder = "Search all columns..."
105
236
/>
@@ -114,7 +245,10 @@ export const Table = ({
114
245
className : h . column . getCanSort ( )
115
246
? 'cursor-pointer select-none'
116
247
: '' ,
117
- onClick : h . column . getToggleSortingHandler ( ) ,
248
+ onClick : ( v ) => {
249
+ setHasSorted ( true ) ;
250
+ h . column . getToggleSortingHandler ( ) ( v ) ;
251
+ } ,
118
252
} }
119
253
>
120
254
{ flexRender ( h . column . columnDef . header , h . getContext ( ) ) }
@@ -135,23 +269,39 @@ export const Table = ({
135
269
) ) }
136
270
</ thead >
137
271
< tbody >
138
- { table . getRowModel ( ) . rows . map ( ( r ) => (
139
- < tr key = { r . id } className = "border-b border-b-slate-200" >
140
- { r . getVisibleCells ( ) . map ( ( c ) => (
141
- < td key = { c . id } className = "py-2 " >
142
- { flexRender ( c . column . columnDef . cell , c . getContext ( ) ) }
143
- </ td >
144
- ) ) }
272
+ { datastoreConfig && isLoadingPage ? (
273
+ < tr >
274
+ < td colSpan = { cols . length } rowSpan = { cols . length } >
275
+ < div className = "w-full h-full flex items-center justify-center pt-6 " >
276
+ < LoadingSpinner />
277
+ </ div >
278
+ </ td >
145
279
</ tr >
146
- ) ) }
280
+ ) : (
281
+ ( datastoreConfig
282
+ ? table . getRowModel ( ) . rows . slice ( startIndex , lastIndex )
283
+ : table . getRowModel ( ) . rows
284
+ ) . map ( ( r ) => (
285
+ < tr key = { r . id } className = "border-b border-b-slate-200" >
286
+ { r . getVisibleCells ( ) . map ( ( c ) => (
287
+ < td key = { c . id } className = "py-2" >
288
+ { flexRender ( c . column . columnDef . cell , c . getContext ( ) ) }
289
+ </ td >
290
+ ) ) }
291
+ </ tr >
292
+ ) )
293
+ ) }
147
294
</ tbody >
148
295
</ table >
149
296
< div className = "flex gap-2 items-center justify-center mt-10" >
150
297
< button
151
298
className = { `w-6 h-6 ${
152
299
! table . getCanPreviousPage ( ) ? 'opacity-25' : 'opacity-100'
153
300
} `}
154
- onClick = { ( ) => table . setPageIndex ( 0 ) }
301
+ onClick = { ( ) => {
302
+ if ( datastoreConfig ) queryPaginatedData ( 0 ) ;
303
+ table . setPageIndex ( 0 ) ;
304
+ } }
155
305
disabled = { ! table . getCanPreviousPage ( ) }
156
306
>
157
307
< ChevronDoubleLeftIcon />
@@ -160,7 +310,12 @@ export const Table = ({
160
310
className = { `w-6 h-6 ${
161
311
! table . getCanPreviousPage ( ) ? 'opacity-25' : 'opacity-100'
162
312
} `}
163
- onClick = { ( ) => table . previousPage ( ) }
313
+ onClick = { ( ) => {
314
+ if ( datastoreConfig ) {
315
+ queryPaginatedData ( table . getState ( ) . pagination . pageIndex - 1 ) ;
316
+ }
317
+ table . previousPage ( ) ;
318
+ } }
164
319
disabled = { ! table . getCanPreviousPage ( ) }
165
320
>
166
321
< ChevronLeftIcon />
@@ -176,7 +331,11 @@ export const Table = ({
176
331
className = { `w-6 h-6 ${
177
332
! table . getCanNextPage ( ) ? 'opacity-25' : 'opacity-100'
178
333
} `}
179
- onClick = { ( ) => table . nextPage ( ) }
334
+ onClick = { ( ) => {
335
+ if ( datastoreConfig )
336
+ queryPaginatedData ( table . getState ( ) . pagination . pageIndex + 1 ) ;
337
+ table . nextPage ( ) ;
338
+ } }
180
339
disabled = { ! table . getCanNextPage ( ) }
181
340
>
182
341
< ChevronRightIcon />
@@ -185,7 +344,11 @@ export const Table = ({
185
344
className = { `w-6 h-6 ${
186
345
! table . getCanNextPage ( ) ? 'opacity-25' : 'opacity-100'
187
346
} `}
188
- onClick = { ( ) => table . setPageIndex ( table . getPageCount ( ) - 1 ) }
347
+ onClick = { ( ) => {
348
+ const pageIndexToNavigate = table . getPageCount ( ) - 1 ;
349
+ if ( datastoreConfig ) queryPaginatedData ( pageIndexToNavigate ) ;
350
+ table . setPageIndex ( pageIndexToNavigate ) ;
351
+ } }
189
352
disabled = { ! table . getCanNextPage ( ) }
190
353
>
191
354
< ChevronDoubleRightIcon />
0 commit comments