Skip to content

fix: create centralized PaginationContainer component #10967

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Dec 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5076aaf
chore: add Pagination component, add new test, and update other pagin…
Parkreiner Nov 30, 2023
b3274e1
fix: add back temp spacing for WorkspacesPageView
Parkreiner Nov 30, 2023
b053d8a
chore: update AuditPage to use Pagination
Parkreiner Nov 30, 2023
22cb113
chore: update UsersPage to use Pagination
Parkreiner Nov 30, 2023
6615442
refactor: move parts of Pagination into WorkspacesPageView
Parkreiner Nov 30, 2023
0db7164
fix: handle empty states for pagination labels better
Parkreiner Dec 1, 2023
b9f07c3
docs: rewrite comment for clarity
Parkreiner Dec 1, 2023
58c6973
refactor: rename components/properties for clarity
Parkreiner Dec 1, 2023
90ef163
fix: rename component files for clarity
Parkreiner Dec 1, 2023
a02babe
chore: add story for PaginationContainer
Parkreiner Dec 1, 2023
a9f57f3
chore: rename story for clarity
Parkreiner Dec 1, 2023
fffc236
fix: handle undefined case better
Parkreiner Dec 1, 2023
5e94899
fix: update imports for PaginationContainer mocks
Parkreiner Dec 1, 2023
7242ba2
fix: update story values for clarity
Parkreiner Dec 1, 2023
f4fd2a3
fix: update scroll logic to go to the bottom instead of the top
Parkreiner Dec 2, 2023
cac222f
fix: update mock setup for test
Parkreiner Dec 2, 2023
e47d55a
fix: update stories
Parkreiner Dec 2, 2023
80635a4
fix: remove scrolling functionality
Parkreiner Dec 2, 2023
77f15d0
fix: remove deprecated property
Parkreiner Dec 2, 2023
a9e0965
refactor: rename prop
Parkreiner Dec 2, 2023
21e2d73
fix: remove debounce flake
Parkreiner Dec 2, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions site/src/components/PaginationWidget/PaginationContainer.mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* @file Mock input props for use with PaginationContainer's tests and stories.
*
* Had to split this off into a separate file because housing these in the test
* file and then importing them from the stories file was causing Chromatic's
* Vite test environment to break
*/
import type { PaginationResult } from "./PaginationContainer";

type ResultBase = Omit<
PaginationResult,
"isPreviousData" | "currentOffsetStart" | "totalRecords" | "totalPages"
>;

export const mockPaginationResultBase: ResultBase = {
isSuccess: false,
currentPage: 1,
limit: 25,
hasNextPage: false,
hasPreviousPage: false,
goToPreviousPage: () => {},
goToNextPage: () => {},
goToFirstPage: () => {},
onPageChange: () => {},
};

export const mockInitialRenderResult: PaginationResult = {
...mockPaginationResultBase,
isSuccess: false,
isPreviousData: false,
currentOffsetStart: undefined,
hasNextPage: false,
hasPreviousPage: false,
totalRecords: undefined,
totalPages: undefined,
};

export const mockSuccessResult: PaginationResult = {
...mockPaginationResultBase,
isSuccess: true,
isPreviousData: false,
currentOffsetStart: 1,
totalPages: 1,
totalRecords: 4,
};
122 changes: 122 additions & 0 deletions site/src/components/PaginationWidget/PaginationContainer.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import type {
ComponentProps,
FC,
HTMLAttributes,
PropsWithChildren,
} from "react";
import { PaginationContainer } from "./PaginationContainer";
import type { Meta, StoryObj } from "@storybook/react";

import {
mockPaginationResultBase,
mockInitialRenderResult,
} from "./PaginationContainer.mocks";

// Filtering out optional <div> props to give better auto-complete experience
type EssentialComponent = FC<
Omit<
ComponentProps<typeof PaginationContainer>,
keyof HTMLAttributes<HTMLDivElement>
> &
PropsWithChildren
>;

const meta: Meta<EssentialComponent> = {
title: "components/PaginationContainer",
component: PaginationContainer,
args: {
paginationUnitLabel: "puppies",
children: <div>Put any content here</div>,
},
};

export default meta;
type Story = StoryObj<EssentialComponent>;

export const FirstPageBeforeFetch: Story = {
args: {
query: mockInitialRenderResult,
},
};

export const FirstPageWithData: Story = {
args: {
query: {
...mockPaginationResultBase,
isSuccess: true,
currentPage: 1,
currentOffsetStart: 1,
totalRecords: 100,
totalPages: 4,
hasPreviousPage: false,
hasNextPage: true,
isPreviousData: false,
},
},
};

export const FirstPageWithLittleData: Story = {
args: {
query: {
...mockPaginationResultBase,
isSuccess: true,
currentPage: 1,
currentOffsetStart: 1,
totalRecords: 7,
totalPages: 1,
hasPreviousPage: false,
hasNextPage: false,
isPreviousData: false,
},
},
};

export const FirstPageWithNoData: Story = {
args: {
query: {
...mockPaginationResultBase,
isSuccess: true,
currentPage: 1,
currentOffsetStart: 1,
totalRecords: 0,
totalPages: 0,
hasPreviousPage: false,
hasNextPage: false,
isPreviousData: false,
},
},
};

export const TransitionFromFirstToSecondPage: Story = {
args: {
query: {
...mockPaginationResultBase,
isSuccess: true,
currentPage: 2,
currentOffsetStart: 26,
totalRecords: 100,
totalPages: 4,
hasPreviousPage: false,
hasNextPage: false,
isPreviousData: true,
},
children: <div>Previous data from page 1</div>,
},
};

export const SecondPageWithData: Story = {
args: {
query: {
...mockPaginationResultBase,
isSuccess: true,
currentPage: 2,
currentOffsetStart: 26,
totalRecords: 100,
totalPages: 4,
hasPreviousPage: true,
hasNextPage: true,
isPreviousData: false,
},
children: <div>New data for page 2</div>,
},
};
53 changes: 53 additions & 0 deletions site/src/components/PaginationWidget/PaginationContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { type FC, type HTMLAttributes } from "react";
import { type PaginationResultInfo } from "hooks/usePaginatedQuery";
import { PaginationWidgetBase } from "./PaginationWidgetBase";
import { PaginationHeader } from "./PaginationHeader";

export type PaginationResult = PaginationResultInfo & {
isPreviousData: boolean;
};

type PaginationProps = HTMLAttributes<HTMLDivElement> & {
query: PaginationResult;
paginationUnitLabel: string;
};

export const PaginationContainer: FC<PaginationProps> = ({
children,
query,
paginationUnitLabel,
...delegatedProps
}) => {
return (
<>
<PaginationHeader
limit={query.limit}
totalRecords={query.totalRecords}
currentOffsetStart={query.currentOffsetStart}
paginationUnitLabel={paginationUnitLabel}
/>

<div
css={{
display: "flex",
flexFlow: "column nowrap",
rowGap: "16px",
}}
{...delegatedProps}
>
{children}

{query.isSuccess && (
<PaginationWidgetBase
totalRecords={query.totalRecords}
currentPage={query.currentPage}
pageSize={query.limit}
onPageChange={query.onPageChange}
hasPreviousPage={query.hasPreviousPage}
hasNextPage={query.hasNextPage}
/>
)}
</div>
</>
);
};
62 changes: 62 additions & 0 deletions site/src/components/PaginationWidget/PaginationHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { type FC } from "react";
import { useTheme } from "@emotion/react";
import Skeleton from "@mui/material/Skeleton";

type PaginationHeaderProps = {
paginationUnitLabel: string;
limit: number;
totalRecords: number | undefined;
currentOffsetStart: number | undefined;
};

export const PaginationHeader: FC<PaginationHeaderProps> = ({
paginationUnitLabel,
limit,
totalRecords,
currentOffsetStart,
}) => {
const theme = useTheme();

return (
<div
css={{
display: "flex",
flexFlow: "row nowrap",
alignItems: "center",
margin: 0,
fontSize: "13px",
paddingBottom: "8px",
color: theme.palette.text.secondary,
height: "36px", // The size of a small button
"& strong": {
color: theme.palette.text.primary,
},
}}
>
{totalRecords !== undefined ? (
<>
{/**
* Have to put text content in divs so that flexbox doesn't scramble
* the inner text nodes up
*/}
{totalRecords === 0 && <div>No records available</div>}

{totalRecords !== 0 && currentOffsetStart !== undefined && (
<div>
Showing {paginationUnitLabel}{" "}
<strong>
{currentOffsetStart}&ndash;
{currentOffsetStart +
Math.min(limit - 1, totalRecords - currentOffsetStart)}
</strong>{" "}
(<strong>{totalRecords.toLocaleString()}</strong>{" "}
{paginationUnitLabel} total)
</div>
)}
</>
) : (
<Skeleton variant="text" width={160} height={16} />
)}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,15 @@ describe(PaginationWidgetBase.name, () => {
expect(prevButton).not.toBeDisabled();
expect(prevButton).toHaveAttribute("aria-disabled", "false");

await userEvent.click(prevButton);
expect(onPageChange).toHaveBeenCalledTimes(1);

expect(nextButton).not.toBeDisabled();
expect(nextButton).toHaveAttribute("aria-disabled", "false");

await userEvent.click(prevButton);
await userEvent.click(nextButton);
expect(onPageChange).toHaveBeenCalledTimes(2);

unmount();
}
});
Expand Down
24 changes: 16 additions & 8 deletions site/src/components/PaginationWidget/PaginationWidgetBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,18 @@ export type PaginationWidgetBaseProps = {
pageSize: number;
totalRecords: number;
onPageChange: (newPage: number) => void;

hasPreviousPage?: boolean;
hasNextPage?: boolean;
};

export const PaginationWidgetBase = ({
currentPage,
pageSize,
totalRecords,
onPageChange,
hasPreviousPage,
hasNextPage,
}: PaginationWidgetBaseProps) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("md"));
Expand All @@ -28,8 +33,11 @@ export const PaginationWidgetBase = ({
return null;
}

const onFirstPage = currentPage <= 1;
const onLastPage = currentPage >= totalPages;
const currentPageOffset = (currentPage - 1) * pageSize;
const isPrevDisabled = !(hasPreviousPage ?? currentPage > 1);
const isNextDisabled = !(
hasNextPage ?? pageSize + currentPageOffset < totalRecords
);

return (
<div
Expand All @@ -38,16 +46,16 @@ export const PaginationWidgetBase = ({
alignItems: "center",
display: "flex",
flexDirection: "row",
padding: "20px",
padding: "0 20px",
columnGap: "6px",
}}
>
<PaginationNavButton
disabledMessage="You are already on the first page"
disabled={onFirstPage}
disabled={isPrevDisabled}
aria-label="Previous page"
onClick={() => {
if (!onFirstPage) {
if (!isPrevDisabled) {
onPageChange(currentPage - 1);
}
}}
Expand All @@ -70,11 +78,11 @@ export const PaginationWidgetBase = ({
)}

<PaginationNavButton
disabledMessage="You're already on the last page"
disabled={onLastPage}
disabledMessage="You are already on the last page"
disabled={isNextDisabled}
aria-label="Next page"
onClick={() => {
if (!onLastPage) {
if (!isNextDisabled) {
onPageChange(currentPage + 1);
}
}}
Expand Down
Loading