go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/common/components/params_pager/params_pager.tsx (about) 1 // Copyright 2024 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 import { Box, Button, ToggleButton, ToggleButtonGroup } from '@mui/material'; 16 import { useState } from 'react'; 17 import { Link } from 'react-router-dom'; 18 19 import { useSyncedSearchParams } from '@/generic_libs/hooks/synced_search_params'; 20 21 import { 22 getPageSize, 23 getPageToken, 24 pageSizeUpdater, 25 pageTokenUpdater, 26 } from './params_pager_utils'; 27 28 export interface ParamsPagerProps { 29 readonly nextPageToken: string; 30 } 31 32 /** 33 * ParamsPager shows a page size and a next/previous page buttons. 34 * This assumes an AIP-158 style pagination API (i.e. pageSize and pageToken fields) 35 * This component does not emit events for updates, but instead stores the current 36 * page and pageSize in the URL. 37 * For the previous page button to work correctly you need to take care to not remount 38 * the component each time you navigate to a page. E.g. loading spinners must not 39 * replace this component, but must be lower in the component hierarchy. 40 */ 41 export function ParamsPager({ nextPageToken }: ParamsPagerProps) { 42 const [searchParams, _] = useSyncedSearchParams(); 43 const pageSize = getPageSize(searchParams); 44 const pageToken = getPageToken(searchParams); 45 46 // There could be a lot of prev pages. Do not keep those tokens in the URL. 47 const [prevPageTokens, setPrevPageTokens] = useState(() => { 48 // If there's a page token when the component is FIRST INITIALIZED, allow 49 // users to go back to the first page by inserting a blank page token. 50 return pageToken ? [''] : []; 51 }); 52 53 const prevPageToken = prevPageTokens.length 54 ? prevPageTokens[prevPageTokens.length - 1] 55 : null; 56 return ( 57 <> 58 <Box sx={{ mt: '5px' }}> 59 Page Size:{' '} 60 <ToggleButtonGroup exclusive value={pageSize} size="small"> 61 {[25, 50, 100, 200].map((s) => ( 62 <ToggleButton 63 key={s} 64 component={Link} 65 to={`?${pageSizeUpdater(s)(searchParams)}`} 66 value={s} 67 > 68 {s} 69 </ToggleButton> 70 ))} 71 </ToggleButtonGroup>{' '} 72 <Button 73 disabled={prevPageToken === null} 74 component={Link} 75 to={`?${pageTokenUpdater(prevPageToken || '')(searchParams)}`} 76 onClick={(e) => { 77 if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) { 78 return; 79 } 80 setPrevPageTokens(prevPageTokens.slice(0, -1)); 81 }} 82 > 83 Previous Page 84 </Button> 85 <Button 86 disabled={nextPageToken === ''} 87 component={Link} 88 to={`?${pageTokenUpdater(nextPageToken)(searchParams)}`} 89 onClick={(e) => { 90 if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) { 91 return; 92 } 93 setPrevPageTokens([...prevPageTokens, pageToken]); 94 }} 95 > 96 Next Page 97 </Button> 98 </Box> 99 </> 100 ); 101 }