66 getFilteredRowModel ,
77 getPaginationRowModel ,
88 getSortedRowModel ,
9+ PaginationState ,
10+ Table as ReactTable ,
911 useReactTable ,
1012} from '@tanstack/react-table' ;
1113
@@ -25,12 +27,19 @@ import DebouncedInput from './DebouncedInput';
2527import loadData from '../lib/loadData' ;
2628import LoadingSpinner from './LoadingSpinner' ;
2729
30+ export type TableData = { cols : { key : string , name : string } [ ] ; data : any [ ] ; total : number } ;
31+
2832export type TableProps = {
2933 data ?: Array < { [ key : string ] : number | string } > ;
3034 cols ?: Array < { [ key : string ] : string } > ;
3135 csv ?: string ;
3236 url ?: string ;
3337 fullWidth ?: boolean ;
38+ datastoreConfig ?: {
39+ dataStoreURI : string ;
40+ rowsPerPage ?: number ;
41+ dataMapperFn : ( data ) => Promise < TableData > | TableData ;
42+ } ;
3443} ;
3544
3645export const Table = ( {
@@ -39,8 +48,28 @@ export const Table = ({
3948 csv = '' ,
4049 url = '' ,
4150 fullWidth = false ,
51+ datastoreConfig,
4252} : TableProps ) => {
4353 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 ) ;
4473
4574 if ( csv ) {
4675 const out = parseCsv ( csv ) ;
@@ -62,21 +91,56 @@ export const Table = ({
6291 ) ;
6392 } , [ data , cols ] ) ;
6493
65- const [ globalFilter , setGlobalFilter ] = useState ( '' ) ;
94+ let table : ReactTable < unknown > ;
6695
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+ }
80144
81145 useEffect ( ( ) => {
82146 if ( url ) {
@@ -91,6 +155,70 @@ export const Table = ({
91155 }
92156 } , [ url ] ) ;
93157
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+
94222 return isLoading ? (
95223 < div className = "w-full h-full min-h-[500px] flex items-center justify-center" >
96224 < LoadingSpinner />
@@ -99,7 +227,10 @@ export const Table = ({
99227 < div className = { `${ fullWidth ? 'w-[90vw] ml-[calc(50%-45vw)]' : 'w-full' } ` } >
100228 < DebouncedInput
101229 value = { globalFilter ?? '' }
102- onChange = { ( value : any ) => setGlobalFilter ( String ( value ) ) }
230+ onChange = { ( value : any ) => {
231+ if ( datastoreConfig ) queryDataByText ( String ( value ) ) ;
232+ setGlobalFilter ( String ( value ) ) ;
233+ } }
103234 className = "p-2 text-sm shadow border border-block"
104235 placeholder = "Search all columns..."
105236 />
@@ -114,7 +245,10 @@ export const Table = ({
114245 className : h . column . getCanSort ( )
115246 ? 'cursor-pointer select-none'
116247 : '' ,
117- onClick : h . column . getToggleSortingHandler ( ) ,
248+ onClick : ( v ) => {
249+ setHasSorted ( true ) ;
250+ h . column . getToggleSortingHandler ( ) ( v ) ;
251+ } ,
118252 } }
119253 >
120254 { flexRender ( h . column . columnDef . header , h . getContext ( ) ) }
@@ -135,23 +269,39 @@ export const Table = ({
135269 ) ) }
136270 </ thead >
137271 < 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 >
145279 </ 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+ ) }
147294 </ tbody >
148295 </ table >
149296 < div className = "flex gap-2 items-center justify-center mt-10" >
150297 < button
151298 className = { `w-6 h-6 ${
152299 ! table . getCanPreviousPage ( ) ? 'opacity-25' : 'opacity-100'
153300 } `}
154- onClick = { ( ) => table . setPageIndex ( 0 ) }
301+ onClick = { ( ) => {
302+ if ( datastoreConfig ) queryPaginatedData ( 0 ) ;
303+ table . setPageIndex ( 0 ) ;
304+ } }
155305 disabled = { ! table . getCanPreviousPage ( ) }
156306 >
157307 < ChevronDoubleLeftIcon />
@@ -160,7 +310,12 @@ export const Table = ({
160310 className = { `w-6 h-6 ${
161311 ! table . getCanPreviousPage ( ) ? 'opacity-25' : 'opacity-100'
162312 } `}
163- onClick = { ( ) => table . previousPage ( ) }
313+ onClick = { ( ) => {
314+ if ( datastoreConfig ) {
315+ queryPaginatedData ( table . getState ( ) . pagination . pageIndex - 1 ) ;
316+ }
317+ table . previousPage ( ) ;
318+ } }
164319 disabled = { ! table . getCanPreviousPage ( ) }
165320 >
166321 < ChevronLeftIcon />
@@ -176,7 +331,11 @@ export const Table = ({
176331 className = { `w-6 h-6 ${
177332 ! table . getCanNextPage ( ) ? 'opacity-25' : 'opacity-100'
178333 } `}
179- onClick = { ( ) => table . nextPage ( ) }
334+ onClick = { ( ) => {
335+ if ( datastoreConfig )
336+ queryPaginatedData ( table . getState ( ) . pagination . pageIndex + 1 ) ;
337+ table . nextPage ( ) ;
338+ } }
180339 disabled = { ! table . getCanNextPage ( ) }
181340 >
182341 < ChevronRightIcon />
@@ -185,7 +344,11 @@ export const Table = ({
185344 className = { `w-6 h-6 ${
186345 ! table . getCanNextPage ( ) ? 'opacity-25' : 'opacity-100'
187346 } `}
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+ } }
189352 disabled = { ! table . getCanNextPage ( ) }
190353 >
191354 < ChevronDoubleRightIcon />
0 commit comments