mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	Feat/pagination bar (#5309)
Initial implementation of the sticky pagination bar.
This commit is contained in:
		
							parent
							
								
									15f77f5b8b
								
							
						
					
					
						commit
						7f4df19660
					
				
							
								
								
									
										10
									
								
								frontend/src/assets/icons/arrowLeft.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/assets/icons/arrowLeft.svg
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"> | ||||
| <g clip-path="url(#clip0_18717_114578)" transform="rotate(180, 12, 12)"> | ||||
| <path d="M9.00539 8.41445L12.8854 12.2945L9.00539 16.1745C8.61539 16.5645 8.61539 17.1945 9.00539 17.5845C9.39539 17.9745 10.0254 17.9745 10.4154 17.5845L15.0054 12.9945C15.3954 12.6045 15.3954 11.9745 15.0054 11.5845L10.4154 6.99445C10.0254 6.60445 9.39539 6.60445 9.00539 6.99445C8.62539 7.38445 8.61539 8.02445 9.00539 8.41445Z" fill="#6C65E5"/> | ||||
| </g> | ||||
| <defs> | ||||
| <clipPath id="clip0_18717_114578"> | ||||
| <rect width="24" height="24" fill="white"/> | ||||
| </clipPath> | ||||
| </defs> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 635 B | 
							
								
								
									
										10
									
								
								frontend/src/assets/icons/arrowRight.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/assets/icons/arrowRight.svg
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"> | ||||
| <g clip-path="url(#clip0_18717_114578)"> | ||||
| <path d="M9.00539 8.41445L12.8854 12.2945L9.00539 16.1745C8.61539 16.5645 8.61539 17.1945 9.00539 17.5845C9.39539 17.9745 10.0254 17.9745 10.4154 17.5845L15.0054 12.9945C15.3954 12.6045 15.3954 11.9745 15.0054 11.5845L10.4154 6.99445C10.0254 6.60445 9.39539 6.60445 9.00539 6.99445C8.62539 7.38445 8.61539 8.02445 9.00539 8.41445Z" fill="#6C65E5"/> | ||||
| </g> | ||||
| <defs> | ||||
| <clipPath id="clip0_18717_114578"> | ||||
| <rect width="24" height="24" fill="white"/> | ||||
| </clipPath> | ||||
| </defs> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 603 B | 
| @ -8,8 +8,7 @@ interface IBatchSelectionActionsBarProps { | ||||
| 
 | ||||
| const StyledStickyContainer = styled('div')(({ theme }) => ({ | ||||
|     position: 'sticky', | ||||
|     marginTop: 'auto', | ||||
|     bottom: 0, | ||||
|     bottom: 50, | ||||
|     zIndex: theme.zIndex.mobileStepper, | ||||
|     pointerEvents: 'none', | ||||
| })); | ||||
|  | ||||
							
								
								
									
										148
									
								
								frontend/src/component/common/PaginationBar/PaginationBar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								frontend/src/component/common/PaginationBar/PaginationBar.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,148 @@ | ||||
| import React from 'react'; | ||||
| import { Box, Typography, Button, styled } from '@mui/material'; | ||||
| import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender'; | ||||
| import { ReactComponent as ArrowRight } from 'assets/icons/arrowRight.svg'; | ||||
| import { ReactComponent as ArrowLeft } from 'assets/icons/arrowLeft.svg'; | ||||
| 
 | ||||
| const StyledPaginationButton = styled(Button)(({ theme }) => ({ | ||||
|     padding: `0 ${theme.spacing(0.8)}`, | ||||
|     minWidth: 'auto', | ||||
| })); | ||||
| 
 | ||||
| const StyledTypography = styled(Typography)(({ theme }) => ({ | ||||
|     color: theme.palette.text.secondary, | ||||
|     fontSize: theme.fontSizes.smallerBody, | ||||
| })); | ||||
| 
 | ||||
| const StyledTypographyPageText = styled(Typography)(({ theme }) => ({ | ||||
|     marginLeft: theme.spacing(2), | ||||
|     marginRight: theme.spacing(2), | ||||
|     color: theme.palette.text.primary, | ||||
|     fontSize: theme.fontSizes.smallerBody, | ||||
| })); | ||||
| 
 | ||||
| const StyledBoxContainer = styled(Box)({ | ||||
|     display: 'flex', | ||||
|     justifyContent: 'space-between', | ||||
|     alignItems: 'center', | ||||
|     width: '100%', | ||||
| }); | ||||
| 
 | ||||
| const StyledCenterBox = styled(Box)({ | ||||
|     display: 'flex', | ||||
|     alignItems: 'center', | ||||
| }); | ||||
| 
 | ||||
| const StyledSelect = styled('select')(({ theme }) => ({ | ||||
|     backgroundColor: theme.palette.background.paper, | ||||
|     border: `1px solid ${theme.palette.divider}`, | ||||
|     borderRadius: theme.shape.borderRadius, | ||||
|     padding: theme.spacing(0.5), | ||||
|     fontSize: theme.fontSizes.smallBody, | ||||
|     marginLeft: theme.spacing(1), | ||||
|     color: theme.palette.text.primary, | ||||
| })); | ||||
| 
 | ||||
| interface PaginationBarProps { | ||||
|     total: number; | ||||
|     currentOffset: number; | ||||
|     fetchPrevPage: () => void; | ||||
|     fetchNextPage: () => void; | ||||
|     hasPreviousPage: boolean; | ||||
|     hasNextPage: boolean; | ||||
|     pageLimit: number; | ||||
|     setPageLimit: (limit: number) => void; | ||||
| } | ||||
| 
 | ||||
| export const PaginationBar: React.FC<PaginationBarProps> = ({ | ||||
|     total, | ||||
|     currentOffset, | ||||
|     fetchPrevPage, | ||||
|     fetchNextPage, | ||||
|     hasPreviousPage, | ||||
|     hasNextPage, | ||||
|     pageLimit, | ||||
|     setPageLimit, | ||||
| }) => { | ||||
|     const calculatePageOffset = ( | ||||
|         currentOffset: number, | ||||
|         total: number, | ||||
|     ): string => { | ||||
|         if (total === 0) return '0-0'; | ||||
| 
 | ||||
|         const start = currentOffset + 1; | ||||
|         const end = Math.min(total, currentOffset + pageLimit); | ||||
| 
 | ||||
|         return `${start}-${end}`; | ||||
|     }; | ||||
| 
 | ||||
|     const calculateTotalPages = (total: number, offset: number): number => { | ||||
|         return Math.ceil(total / pageLimit); | ||||
|     }; | ||||
| 
 | ||||
|     const calculateCurrentPage = (offset: number): number => { | ||||
|         return Math.floor(offset / pageLimit) + 1; | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <StyledBoxContainer> | ||||
|             <StyledTypography> | ||||
|                 Showing {calculatePageOffset(currentOffset, total)} out of{' '} | ||||
|                 {total} | ||||
|             </StyledTypography> | ||||
|             <StyledCenterBox> | ||||
|                 <ConditionallyRender | ||||
|                     condition={hasPreviousPage} | ||||
|                     show={ | ||||
|                         <StyledPaginationButton | ||||
|                             variant='outlined' | ||||
|                             color='primary' | ||||
|                             onClick={fetchPrevPage} | ||||
|                         > | ||||
|                             <ArrowLeft /> | ||||
|                         </StyledPaginationButton> | ||||
|                     } | ||||
|                 /> | ||||
|                 <StyledTypographyPageText> | ||||
|                     Page {calculateCurrentPage(currentOffset)} of{' '} | ||||
|                     {calculateTotalPages(total, pageLimit)} | ||||
|                 </StyledTypographyPageText> | ||||
|                 <ConditionallyRender | ||||
|                     condition={hasNextPage} | ||||
|                     show={ | ||||
|                         <StyledPaginationButton | ||||
|                             onClick={fetchNextPage} | ||||
|                             variant='outlined' | ||||
|                             color='primary' | ||||
|                         > | ||||
|                             <ArrowRight /> | ||||
|                         </StyledPaginationButton> | ||||
|                     } | ||||
|                 /> | ||||
|             </StyledCenterBox> | ||||
|             <StyledCenterBox> | ||||
|                 <StyledTypography>Show rows</StyledTypography> | ||||
| 
 | ||||
|                 {/* We are using the native select element instead of the Material-UI Select  | ||||
|                 component due to an issue with Material-UI's Select. When the Material-UI  | ||||
|                 Select dropdown is opened, it temporarily removes the scrollbar,  | ||||
|                 causing the page to jump. This can be disorienting for users.  | ||||
|                 The native select does not have this issue,  | ||||
|                 as it does not affect the scrollbar when opened.  | ||||
|                 Therefore, we use the native select to provide a better user experience.  | ||||
|                 */} | ||||
|                 <StyledSelect | ||||
|                     value={pageLimit} | ||||
|                     onChange={(event: React.ChangeEvent<HTMLSelectElement>) => | ||||
|                         setPageLimit(Number(event.target.value)) | ||||
|                     } | ||||
|                 > | ||||
|                     <option value={25}>25</option> | ||||
|                     <option value={50}>50</option> | ||||
|                     <option value={75}>75</option> | ||||
|                     <option value={100}>100</option> | ||||
|                 </StyledSelect> | ||||
|             </StyledCenterBox> | ||||
|         </StyledBoxContainer> | ||||
|     ); | ||||
| }; | ||||
| @ -75,6 +75,7 @@ interface IPaginatedProjectFeatureTogglesProps { | ||||
|     total?: number; | ||||
|     searchValue: string; | ||||
|     setSearchValue: React.Dispatch<React.SetStateAction<string>>; | ||||
|     paginationBar: JSX.Element; | ||||
| } | ||||
| 
 | ||||
| const staticColumns = ['Select', 'Actions', 'name', 'favorite']; | ||||
| @ -91,6 +92,7 @@ export const PaginatedProjectFeatureToggles = ({ | ||||
|     total, | ||||
|     searchValue, | ||||
|     setSearchValue, | ||||
|     paginationBar, | ||||
| }: IPaginatedProjectFeatureTogglesProps) => { | ||||
|     const { classes: styles } = useStyles(); | ||||
|     const theme = useTheme(); | ||||
| @ -491,6 +493,7 @@ export const PaginatedProjectFeatureToggles = ({ | ||||
|             <PageContent | ||||
|                 isLoading={loading} | ||||
|                 className={styles.container} | ||||
|                 sx={{ borderBottomLeftRadius: 0, borderBottomRightRadius: 0 }} | ||||
|                 header={ | ||||
|                     <PageHeader | ||||
|                         titleElement={ | ||||
| @ -651,6 +654,8 @@ export const PaginatedProjectFeatureToggles = ({ | ||||
|                 /> | ||||
|                 {featureToggleModals} | ||||
|             </PageContent> | ||||
| 
 | ||||
|             {paginationBar} | ||||
|             <BatchSelectionActionsBar | ||||
|                 count={Object.keys(selectedRowIds).length} | ||||
|             > | ||||
|  | ||||
| @ -16,6 +16,8 @@ import { useFeatureSearch } from 'hooks/api/getters/useFeatureSearch/useFeatureS | ||||
| import { PaginatedProjectFeatureToggles } from './ProjectFeatureToggles/PaginatedProjectFeatureToggles'; | ||||
| import { useSearchParams } from 'react-router-dom'; | ||||
| 
 | ||||
| import { PaginationBar } from 'component/common/PaginationBar/PaginationBar'; | ||||
| 
 | ||||
| const refreshInterval = 15 * 1000; | ||||
| 
 | ||||
| const StyledContainer = styled('div')(({ theme }) => ({ | ||||
| @ -37,14 +39,13 @@ const StyledContentContainer = styled(Box)(() => ({ | ||||
|     minWidth: 0, | ||||
| })); | ||||
| 
 | ||||
| const PAGE_LIMIT = 25; | ||||
| 
 | ||||
| const PaginatedProjectOverview = () => { | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
|     const [searchParams, setSearchParams] = useSearchParams(); | ||||
|     const { project, loading: projectLoading } = useProject(projectId, { | ||||
|         refreshInterval, | ||||
|     }); | ||||
|     const [pageLimit, setPageLimit] = useState(10); | ||||
|     const [currentOffset, setCurrentOffset] = useState(0); | ||||
| 
 | ||||
|     const [searchValue, setSearchValue] = useState( | ||||
| @ -56,7 +57,7 @@ const PaginatedProjectOverview = () => { | ||||
|         total, | ||||
|         refetch, | ||||
|         loading, | ||||
|     } = useFeatureSearch(currentOffset, PAGE_LIMIT, projectId, searchValue, { | ||||
|     } = useFeatureSearch(currentOffset, pageLimit, projectId, searchValue, { | ||||
|         refreshInterval, | ||||
|     }); | ||||
| 
 | ||||
| @ -64,15 +65,15 @@ const PaginatedProjectOverview = () => { | ||||
|         project; | ||||
|     const fetchNextPage = () => { | ||||
|         if (!loading) { | ||||
|             setCurrentOffset(Math.min(total, currentOffset + PAGE_LIMIT)); | ||||
|             setCurrentOffset(Math.min(total, currentOffset + pageLimit)); | ||||
|         } | ||||
|     }; | ||||
|     const fetchPrevPage = () => { | ||||
|         setCurrentOffset(Math.max(0, currentOffset - PAGE_LIMIT)); | ||||
|         setCurrentOffset(Math.max(0, currentOffset - pageLimit)); | ||||
|     }; | ||||
| 
 | ||||
|     const hasPreviousPage = currentOffset > 0; | ||||
|     const hasNextPage = currentOffset + PAGE_LIMIT < total; | ||||
|     const hasNextPage = currentOffset + pageLimit < total; | ||||
| 
 | ||||
|     return ( | ||||
|         <StyledContainer> | ||||
| @ -100,14 +101,20 @@ const PaginatedProjectOverview = () => { | ||||
|                         total={total} | ||||
|                         searchValue={searchValue} | ||||
|                         setSearchValue={setSearchValue} | ||||
|                     /> | ||||
|                     <ConditionallyRender | ||||
|                         condition={hasPreviousPage} | ||||
|                         show={<Box onClick={fetchPrevPage}>Prev</Box>} | ||||
|                     /> | ||||
|                     <ConditionallyRender | ||||
|                         condition={hasNextPage} | ||||
|                         show={<Box onClick={fetchNextPage}>Next</Box>} | ||||
|                         paginationBar={ | ||||
|                             <StickyPaginationBar> | ||||
|                                 <PaginationBar | ||||
|                                     total={total} | ||||
|                                     hasNextPage={hasNextPage} | ||||
|                                     hasPreviousPage={hasPreviousPage} | ||||
|                                     fetchNextPage={fetchNextPage} | ||||
|                                     fetchPrevPage={fetchPrevPage} | ||||
|                                     currentOffset={currentOffset} | ||||
|                                     pageLimit={pageLimit} | ||||
|                                     setPageLimit={setPageLimit} | ||||
|                                 /> | ||||
|                             </StickyPaginationBar> | ||||
|                         } | ||||
|                     /> | ||||
|                 </StyledProjectToggles> | ||||
|             </StyledContentContainer> | ||||
| @ -115,6 +122,37 @@ const PaginatedProjectOverview = () => { | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| const StyledStickyBar = styled('div')(({ theme }) => ({ | ||||
|     position: 'sticky', | ||||
|     bottom: 0, | ||||
|     backgroundColor: theme.palette.background.paper, | ||||
|     padding: theme.spacing(2), | ||||
|     marginLeft: theme.spacing(2), | ||||
|     zIndex: theme.zIndex.mobileStepper, | ||||
|     borderBottomLeftRadius: theme.shape.borderRadiusMedium, | ||||
|     borderBottomRightRadius: theme.shape.borderRadiusMedium, | ||||
|     borderTop: `1px solid ${theme.palette.divider}`, | ||||
|     boxShadow: `0px -2px 8px 0px rgba(32, 32, 33, 0.06)`, | ||||
|     height: '52px', | ||||
| })); | ||||
| 
 | ||||
| const StyledStickyBarContentContainer = styled(Box)(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
|     justifyContent: 'space-between', | ||||
|     width: '100%', | ||||
|     minWidth: 0, | ||||
| })); | ||||
| 
 | ||||
| const StickyPaginationBar: React.FC = ({ children }) => { | ||||
|     return ( | ||||
|         <StyledStickyBar> | ||||
|             <StyledStickyBarContentContainer> | ||||
|                 {children} | ||||
|             </StyledStickyBarContentContainer> | ||||
|         </StyledStickyBar> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * @deprecated remove when flag `featureSearchFrontend` is removed | ||||
|  */ | ||||
|  | ||||
| @ -48,7 +48,7 @@ process.nextTick(async () => { | ||||
|                         playgroundImprovements: true, | ||||
|                         featureSwitchRefactor: true, | ||||
|                         featureSearchAPI: true, | ||||
|                         featureSearchFrontend: false, | ||||
|                         featureSearchFrontend: true, | ||||
|                     }, | ||||
|                 }, | ||||
|                 authentication: { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user