From 7f4df196604fa5764b8c2de98d00262ece34e814 Mon Sep 17 00:00:00 2001 From: Fredrik Strand Oseberg Date: Fri, 10 Nov 2023 14:16:31 +0100 Subject: [PATCH] Feat/pagination bar (#5309) Initial implementation of the sticky pagination bar. --- frontend/src/assets/icons/arrowLeft.svg | 10 ++ frontend/src/assets/icons/arrowRight.svg | 10 ++ .../BatchSelectionActionsBar.tsx | 3 +- .../common/PaginationBar/PaginationBar.tsx | 148 ++++++++++++++++++ .../PaginatedProjectFeatureToggles.tsx | 5 + .../project/Project/ProjectOverview.tsx | 66 ++++++-- src/server-dev.ts | 2 +- 7 files changed, 227 insertions(+), 17 deletions(-) create mode 100644 frontend/src/assets/icons/arrowLeft.svg create mode 100644 frontend/src/assets/icons/arrowRight.svg create mode 100644 frontend/src/component/common/PaginationBar/PaginationBar.tsx diff --git a/frontend/src/assets/icons/arrowLeft.svg b/frontend/src/assets/icons/arrowLeft.svg new file mode 100644 index 000000000000..f57b57ff6ee7 --- /dev/null +++ b/frontend/src/assets/icons/arrowLeft.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/assets/icons/arrowRight.svg b/frontend/src/assets/icons/arrowRight.svg new file mode 100644 index 000000000000..1944b05d3d4c --- /dev/null +++ b/frontend/src/assets/icons/arrowRight.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/component/common/BatchSelectionActionsBar/BatchSelectionActionsBar.tsx b/frontend/src/component/common/BatchSelectionActionsBar/BatchSelectionActionsBar.tsx index 324b68671d3c..7fc34a9dca91 100644 --- a/frontend/src/component/common/BatchSelectionActionsBar/BatchSelectionActionsBar.tsx +++ b/frontend/src/component/common/BatchSelectionActionsBar/BatchSelectionActionsBar.tsx @@ -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', })); diff --git a/frontend/src/component/common/PaginationBar/PaginationBar.tsx b/frontend/src/component/common/PaginationBar/PaginationBar.tsx new file mode 100644 index 000000000000..6954894f92a3 --- /dev/null +++ b/frontend/src/component/common/PaginationBar/PaginationBar.tsx @@ -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 = ({ + 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 ( + + + Showing {calculatePageOffset(currentOffset, total)} out of{' '} + {total} + + + + + + } + /> + + Page {calculateCurrentPage(currentOffset)} of{' '} + {calculateTotalPages(total, pageLimit)} + + + + + } + /> + + + Show rows + + {/* 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. + */} + ) => + setPageLimit(Number(event.target.value)) + } + > + + + + + + + + ); +}; diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/PaginatedProjectFeatureToggles.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/PaginatedProjectFeatureToggles.tsx index ac554290a1e6..5f5390053a8d 100644 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/PaginatedProjectFeatureToggles.tsx +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/PaginatedProjectFeatureToggles.tsx @@ -75,6 +75,7 @@ interface IPaginatedProjectFeatureTogglesProps { total?: number; searchValue: string; setSearchValue: React.Dispatch>; + 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 = ({ {featureToggleModals} + + {paginationBar} diff --git a/frontend/src/component/project/Project/ProjectOverview.tsx b/frontend/src/component/project/Project/ProjectOverview.tsx index 44f2c2ed8f45..00e2de738650 100644 --- a/frontend/src/component/project/Project/ProjectOverview.tsx +++ b/frontend/src/component/project/Project/ProjectOverview.tsx @@ -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 ( @@ -100,14 +101,20 @@ const PaginatedProjectOverview = () => { total={total} searchValue={searchValue} setSearchValue={setSearchValue} - /> - Prev} - /> - Next} + paginationBar={ + + + + } /> @@ -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 ( + + + {children} + + + ); +}; + /** * @deprecated remove when flag `featureSearchFrontend` is removed */ diff --git a/src/server-dev.ts b/src/server-dev.ts index 44ffd4c1e3a6..198800edd61b 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -48,7 +48,7 @@ process.nextTick(async () => { playgroundImprovements: true, featureSwitchRefactor: true, featureSearchAPI: true, - featureSearchFrontend: false, + featureSearchFrontend: true, }, }, authentication: {