Skip to content

Commit

Permalink
Feat/pagination bar (#5309)
Browse files Browse the repository at this point in the history
Initial implementation of the sticky pagination bar.
  • Loading branch information
FredrikOseberg authored Nov 10, 2023
1 parent 15f77f5 commit 7f4df19
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 17 deletions.
10 changes: 10 additions & 0 deletions frontend/src/assets/icons/arrowLeft.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions frontend/src/assets/icons/arrowRight.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}));
Expand Down
148 changes: 148 additions & 0 deletions frontend/src/component/common/PaginationBar/PaginationBar.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand All @@ -91,6 +92,7 @@ export const PaginatedProjectFeatureToggles = ({
total,
searchValue,
setSearchValue,
paginationBar,
}: IPaginatedProjectFeatureTogglesProps) => {
const { classes: styles } = useStyles();
const theme = useTheme();
Expand Down Expand Up @@ -491,6 +493,7 @@ export const PaginatedProjectFeatureToggles = ({
<PageContent
isLoading={loading}
className={styles.container}
sx={{ borderBottomLeftRadius: 0, borderBottomRightRadius: 0 }}
header={
<PageHeader
titleElement={
Expand Down Expand Up @@ -651,6 +654,8 @@ export const PaginatedProjectFeatureToggles = ({
/>
{featureToggleModals}
</PageContent>

{paginationBar}
<BatchSelectionActionsBar
count={Object.keys(selectedRowIds).length}
>
Expand Down
66 changes: 52 additions & 14 deletions frontend/src/component/project/Project/ProjectOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => ({
Expand All @@ -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(
Expand All @@ -56,23 +57,23 @@ const PaginatedProjectOverview = () => {
total,
refetch,
loading,
} = useFeatureSearch(currentOffset, PAGE_LIMIT, projectId, searchValue, {
} = useFeatureSearch(currentOffset, pageLimit, projectId, searchValue, {
refreshInterval,
});

const { members, features, health, description, environments, stats } =
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>
Expand Down Expand Up @@ -100,21 +101,58 @@ 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>
</StyledContainer>
);
};

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
*/
Expand Down
2 changes: 1 addition & 1 deletion src/server-dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ process.nextTick(async () => {
playgroundImprovements: true,
featureSwitchRefactor: true,
featureSearchAPI: true,
featureSearchFrontend: false,
featureSearchFrontend: true,
},
},
authentication: {
Expand Down

0 comments on commit 7f4df19

Please sign in to comment.