@@ -14,6 +14,7 @@ import {
1414 TableBody ,
1515 TableCell ,
1616 TableHead ,
17+ TablePagination ,
1718 TableRow ,
1819 TableSortLabel ,
1920 TextField ,
@@ -23,7 +24,13 @@ import {
2324
2425import { useLocalStorage } from '@uidotdev/usehooks'
2526
26- import { Delete as DeleteIcon } from '@mui/icons-material'
27+ import {
28+ Delete as DeleteIcon ,
29+ FirstPage as FirstPageIcon ,
30+ KeyboardArrowLeft ,
31+ KeyboardArrowRight ,
32+ LastPage as LastPageIcon
33+ } from '@mui/icons-material'
2734import { Autocomplete } from '@mui/material'
2835import type { DownloadSnapshotURL , MetadataModel , ReportModel } from '~/api/types'
2936import { DownloadButton } from '~/components/Actions/DownloadButton'
@@ -96,6 +103,59 @@ type SnapshotActionsWrapperProps = {
96103 snapshotSelection ?: { title : string ; action : ( snapshots : string [ ] ) => void }
97104}
98105
106+ type TablePaginationActionsProps = {
107+ count : number
108+ page : number
109+ rowsPerPage : number
110+ onPageChange : ( event : React . MouseEvent < HTMLButtonElement > , newPage : number ) => void
111+ }
112+
113+ function TablePaginationActions ( {
114+ count,
115+ page,
116+ rowsPerPage,
117+ onPageChange
118+ } : TablePaginationActionsProps ) {
119+ const handleFirstPageButtonClick = ( event : React . MouseEvent < HTMLButtonElement > ) => {
120+ onPageChange ( event , 0 )
121+ }
122+
123+ const handleBackButtonClick = ( event : React . MouseEvent < HTMLButtonElement > ) => {
124+ onPageChange ( event , page - 1 )
125+ }
126+
127+ const handleNextButtonClick = ( event : React . MouseEvent < HTMLButtonElement > ) => {
128+ onPageChange ( event , page + 1 )
129+ }
130+
131+ const handleLastPageButtonClick = ( event : React . MouseEvent < HTMLButtonElement > ) => {
132+ onPageChange ( event , Math . max ( 0 , Math . ceil ( count / rowsPerPage ) - 1 ) )
133+ }
134+
135+ const isLastPage = page >= Math . ceil ( count / rowsPerPage ) - 1
136+
137+ return (
138+ < Box sx = { { flexShrink : 0 , ml : 2.5 } } >
139+ < IconButton
140+ onClick = { handleFirstPageButtonClick }
141+ disabled = { page === 0 }
142+ aria-label = 'first page'
143+ >
144+ < FirstPageIcon />
145+ </ IconButton >
146+ < IconButton onClick = { handleBackButtonClick } disabled = { page === 0 } aria-label = 'previous page' >
147+ < KeyboardArrowLeft />
148+ </ IconButton >
149+ < IconButton onClick = { handleNextButtonClick } disabled = { isLastPage } aria-label = 'next page' >
150+ < KeyboardArrowRight />
151+ </ IconButton >
152+ < IconButton onClick = { handleLastPageButtonClick } disabled = { isLastPage } aria-label = 'last page' >
153+ < LastPageIcon />
154+ </ IconButton >
155+ </ Box >
156+ )
157+ }
158+
99159export const SnapshotsListTemplate = ( props : SnapshotActionsWrapperProps ) => {
100160 const {
101161 query,
@@ -116,6 +176,10 @@ export const SnapshotsListTemplate = (props: SnapshotActionsWrapperProps) => {
116176 const [ selectedTags , setTags ] = useState ( ( ) => query . tags ?. split ( ',' ) || [ ] )
117177 const [ metadataQuery , setMetadataQuery ] = useState ( ( ) => query [ 'metadata-query' ] || '' )
118178
179+ // Pagination state
180+ const [ page , setPage ] = useState ( 0 )
181+ const [ rowsPerPage , setRowsPerPage ] = useState ( 10 )
182+
119183 useUpdateQueryStringValueWithoutNavigation ( 'tags' , selectedTags . join ( ',' ) )
120184 useUpdateQueryStringValueWithoutNavigation ( 'metadata-query' , String ( metadataQuery ) )
121185
@@ -162,6 +226,21 @@ export const SnapshotsListTemplate = (props: SnapshotActionsWrapperProps) => {
162226 [ filteredSnapshotsByMetadata , sortByTimestamp ]
163227 )
164228
229+ // Paginate results
230+ const paginatedSnapshots = useMemo (
231+ ( ) => resultSnapshots . slice ( page * rowsPerPage , page * rowsPerPage + rowsPerPage ) ,
232+ [ resultSnapshots , page , rowsPerPage ]
233+ )
234+
235+ const handleChangePage = ( _event : unknown , newPage : number ) => {
236+ setPage ( newPage )
237+ }
238+
239+ const handleChangeRowsPerPage = ( event : React . ChangeEvent < HTMLInputElement > ) => {
240+ setRowsPerPage ( Number . parseInt ( event . target . value , 10 ) )
241+ setPage ( 0 )
242+ }
243+
165244 const [ selectedSnapshots , setSelectedSnapshots ] = useState < Set < string > > ( new Set ( ) )
166245 const isSnapshotSelected = ( id : string ) => selectedSnapshots . has ( id )
167246
@@ -173,7 +252,10 @@ export const SnapshotsListTemplate = (props: SnapshotActionsWrapperProps) => {
173252 multiple
174253 limitTags = { 2 }
175254 value = { selectedTags }
176- onChange = { ( _ , newSelectedTags ) => setTags ( newSelectedTags ) }
255+ onChange = { ( _ , newSelectedTags ) => {
256+ setTags ( newSelectedTags )
257+ setPage ( 0 )
258+ } }
177259 options = { ALL_TAGS }
178260 renderInput = { ( params ) => (
179261 < TextField { ...params } variant = 'standard' label = 'Filter by Tags' />
@@ -185,7 +267,10 @@ export const SnapshotsListTemplate = (props: SnapshotActionsWrapperProps) => {
185267 < TextField
186268 fullWidth
187269 value = { metadataQuery }
188- onChange = { ( event ) => setMetadataQuery ( event . target . value ) }
270+ onChange = { ( event ) => {
271+ setMetadataQuery ( event . target . value )
272+ setPage ( 0 )
273+ } }
189274 variant = 'standard'
190275 label = 'Search in Metadata'
191276 />
@@ -261,17 +346,16 @@ export const SnapshotsListTemplate = (props: SnapshotActionsWrapperProps) => {
261346 direction = { sortByTimestamp }
262347 onClick = { ( ) => {
263348 setSortByTimestamp ( ( prev ) => {
349+ let next : typeof prev
264350 if ( prev === undefined ) {
265- return 'desc'
266- }
267-
268- if ( prev === 'desc' ) {
269- return 'asc'
270- }
271-
272- if ( prev === 'asc' ) {
273- return undefined
351+ next = 'desc'
352+ } else if ( prev === 'desc' ) {
353+ next = 'asc'
354+ } else {
355+ next = undefined
274356 }
357+ setPage ( 0 )
358+ return next
275359 } )
276360 } }
277361 >
@@ -283,7 +367,7 @@ export const SnapshotsListTemplate = (props: SnapshotActionsWrapperProps) => {
283367 < TableRow />
284368 </ TableHead >
285369 < TableBody >
286- { resultSnapshots . map ( ( snapshot ) => (
370+ { paginatedSnapshots . map ( ( snapshot ) => (
287371 < TableRow key = { `r-${ snapshot . id } ` } >
288372 { snapshotSelection && (
289373 < TableCell padding = 'checkbox' >
@@ -339,8 +423,8 @@ export const SnapshotsListTemplate = (props: SnapshotActionsWrapperProps) => {
339423 variant = { slots ?. donwloadButtonVariant || 'outlined' }
340424 disabled = { disabled ?? false }
341425 downloadLink = {
342- // better type safety
343- downloadLink
426+ // normalize to string before replace (DownloadSnapshotURL isn't guaranteed to be string)
427+ String ( downloadLink )
344428 . replace ( '{project_id}' , projectId )
345429 . replace ( '{snapshot_id}' , snapshot . id )
346430 }
@@ -376,6 +460,16 @@ export const SnapshotsListTemplate = (props: SnapshotActionsWrapperProps) => {
376460 ) ) }
377461 </ TableBody >
378462 </ Table >
463+ < TablePagination
464+ rowsPerPageOptions = { [ 10 , 30 , 50 ] }
465+ component = 'div'
466+ count = { resultSnapshots . length }
467+ rowsPerPage = { rowsPerPage }
468+ page = { page }
469+ onPageChange = { handleChangePage }
470+ onRowsPerPageChange = { handleChangeRowsPerPage }
471+ ActionsComponent = { TablePaginationActions }
472+ />
379473 </ >
380474 )
381475}
0 commit comments