mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-15 01:16:22 +02: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