sts-sponsors team mailing list archive
-
sts-sponsors team
-
Mailing list archive
-
Message #06980
[Merge] ~jonesogolo/maas-site-manager:1525-add-enrollment-pagination into maas-site-manager:main
Jones Ogolo has proposed merging ~jonesogolo/maas-site-manager:1525-add-enrollment-pagination into maas-site-manager:main.
Commit message:
added pagination bar to requests page
Requested reviews:
MAAS Committers (maas-committers)
For more details, see:
https://code.launchpad.net/~jonesogolo/maas-site-manager/+git/maas-site-manager/+merge/440958
This ticket adds a pagination bar to the requests and tokens pages and abstracts the pagination methods to a separate hook.
Steps to QA:
1. goto /requests
2. ensure pagination buttons work as expected
3. click the next button a couple of times and then change the items per page
4. Ensure that you're returned to the first page after step 3.
--
Your team MAAS Committers is requested to review the proposed merge of ~jonesogolo/maas-site-manager:1525-add-enrollment-pagination into maas-site-manager:main.
diff --git a/frontend/src/components/RequestsList/RequestsList.tsx b/frontend/src/components/RequestsList/RequestsList.tsx
index da895ed..5554850 100644
--- a/frontend/src/components/RequestsList/RequestsList.tsx
+++ b/frontend/src/components/RequestsList/RequestsList.tsx
@@ -1,22 +1,48 @@
-import { useState } from "react";
+import { useState, useEffect } from "react";
import { Col, Row } from "@canonical/react-components";
import EnrollmentActions from "@/components/EnrollmentActions";
import RequestsTable from "@/components/RequestsTable";
+import PaginationBar from "@/components/base/PaginationBar";
import { useRequestsQuery } from "@/hooks/api";
+import usePagination from "@/hooks/usePagination";
+const DEFAULT_PAGE_SIZE = 50;
const Requests: React.FC = () => {
- // TODO: update page and size when pagination is implemented
- // https://warthogs.atlassian.net/browse/MAASENG-1525
- const [page] = useState<number>(0);
- const [size] = useState<number>(50);
- const { data, isLoading, isFetchedAfterMount } = useRequestsQuery({ page: `${page}`, size: `${size}` });
+ const [totalDataCount, setTotalDataCount] = useState(0);
+ const { page, debouncedPage, size, handleNextClick, handlePreviousClick, handlePageSizeChange, setPage } =
+ usePagination(DEFAULT_PAGE_SIZE, totalDataCount);
+ const { data, isLoading, isFetchedAfterMount } = useRequestsQuery({
+ page: `${debouncedPage}`,
+ size: `${size}`,
+ });
+
+ useEffect(() => {
+ if (data && "total" in data) {
+ setTotalDataCount(data.total);
+ }
+ }, [data]);
+
return (
<section>
<EnrollmentActions />
<Row>
<Col size={12}>
+ <PaginationBar
+ currentPage={page + 1}
+ dataContext="open enrolment requests"
+ debouncedPageNumber={debouncedPage + 1}
+ handlePageSizeChange={handlePageSizeChange}
+ isLoading={isLoading}
+ itemsPerPage={size}
+ onNextClick={handleNextClick}
+ onPreviousClick={handlePreviousClick}
+ setCurrentPage={setPage}
+ totalItems={data?.total || 0}
+ />
+ </Col>
+ <Col size={12}>
<RequestsTable data={data} isFetchedAfterMount={isFetchedAfterMount} isLoading={isLoading} />
</Col>
</Row>
diff --git a/frontend/src/components/TokensList/TokensList.tsx b/frontend/src/components/TokensList/TokensList.tsx
index ea75f63..a6b1912 100644
--- a/frontend/src/components/TokensList/TokensList.tsx
+++ b/frontend/src/components/TokensList/TokensList.tsx
@@ -1,4 +1,4 @@
-import { useState } from "react";
+import { useState, useEffect } from "react";
import { Button, Col, Row } from "@canonical/react-components";
@@ -8,26 +8,23 @@ import TokensTable from "./components/TokensTable/TokensTable";
import { useAppContext } from "@/context";
import { useTokensQuery } from "@/hooks/api";
-import useDebouncedValue from "@/hooks/useDebouncedValue";
+import usePagination from "@/hooks/usePagination";
const DEFAULT_PAGE_SIZE = 50;
const TokensList = () => {
const { setSidebar } = useAppContext();
- const [page, setPage] = useState(0);
- const [size, setSize] = useState(DEFAULT_PAGE_SIZE);
- const debouncedPageNumber = useDebouncedValue(page);
+ const [totalDataCount, setTotalDataCount] = useState(0);
+ const { page, debouncedPage, size, handleNextClick, handlePreviousClick, handlePageSizeChange, setPage } =
+ usePagination(DEFAULT_PAGE_SIZE, totalDataCount);
- const { data, isLoading, isFetchedAfterMount } = useTokensQuery({ page: `${debouncedPageNumber}`, size: `${size}` });
+ const { data, isLoading, isFetchedAfterMount } = useTokensQuery({ page: `${debouncedPage}`, size: `${size}` });
- const handleNextClick = () => {
- const maxPage = data?.total ? data?.total / size : 1;
- setPage((prev) => (prev >= maxPage ? maxPage : prev + 1));
- };
-
- const handlePreviousClick = () => {
- setPage((prev) => (prev === 0 ? 0 : prev - 1));
- };
+ useEffect(() => {
+ if (data && "total" in data) {
+ setTotalDataCount(data.total);
+ }
+ }, [data]);
return (
<section>
@@ -50,13 +47,13 @@ const TokensList = () => {
<PaginationBar
currentPage={page + 1}
dataContext="tokens"
+ debouncedPageNumber={debouncedPage + 1}
+ handlePageSizeChange={handlePageSizeChange}
isLoading={isLoading}
itemsPerPage={size}
onNextClick={handleNextClick}
onPreviousClick={handlePreviousClick}
- resetPageCount={() => setPage(0)}
setCurrentPage={setPage}
- setPageSize={setSize}
totalItems={data?.total || 0}
/>
<TokensTable data={data} isFetchedAfterMount={isFetchedAfterMount} isLoading={isLoading} />
diff --git a/frontend/src/components/base/PaginationBar/PaginationBar.test.tsx b/frontend/src/components/base/PaginationBar/PaginationBar.test.tsx
index 062460d..a282680 100644
--- a/frontend/src/components/base/PaginationBar/PaginationBar.test.tsx
+++ b/frontend/src/components/base/PaginationBar/PaginationBar.test.tsx
@@ -7,13 +7,13 @@ it("should render the PaginationBar component correctly", () => {
<PaginationBar
currentPage={1}
dataContext="tokens"
+ debouncedPageNumber={1}
+ handlePageSizeChange={() => {}}
isLoading={false}
itemsPerPage={10}
onNextClick={() => {}}
onPreviousClick={() => {}}
- resetPageCount={() => {}}
setCurrentPage={() => {}}
- setPageSize={() => {}}
totalItems={50}
/>,
);
diff --git a/frontend/src/components/base/PaginationBar/PaginationBar.tsx b/frontend/src/components/base/PaginationBar/PaginationBar.tsx
index c146da8..42d3a51 100644
--- a/frontend/src/components/base/PaginationBar/PaginationBar.tsx
+++ b/frontend/src/components/base/PaginationBar/PaginationBar.tsx
@@ -7,11 +7,11 @@ import type { AppPaginationProps } from "@/components/base/TablePagination/Table
import TablePagination from "@/components/base/TablePagination/TablePagination";
type TokensTableControlProps = AppPaginationProps & {
- setPageSize: (size: number) => void;
- resetPageCount: () => void;
+ handlePageSizeChange: (size: number) => void;
dataContext: string;
setCurrentPage: (page: number) => void;
isLoading: boolean;
+ debouncedPageNumber: number;
};
const PaginationBar = ({
@@ -20,11 +20,11 @@ const PaginationBar = ({
totalItems,
onNextClick,
onPreviousClick,
- setPageSize,
- resetPageCount,
+ handlePageSizeChange,
dataContext,
setCurrentPage,
isLoading,
+ debouncedPageNumber,
}: TokensTableControlProps) => {
const pageCounts = useMemo(() => [20, 30, 50], []);
const pageOptions = useMemo(
@@ -33,9 +33,8 @@ const PaginationBar = ({
);
const handleSizeChange = (e: ChangeEvent<HTMLSelectElement>) => {
- resetPageCount();
const { value } = e.target;
- setPageSize(Number(value));
+ handlePageSizeChange(Number(value));
};
const getDisplayedDataCount = () => {
@@ -60,6 +59,7 @@ const PaginationBar = ({
<Col medium={3} size={3} small={4}>
<TablePagination
currentPage={currentPage}
+ debouncedPageNumber={debouncedPageNumber}
isLoading={isLoading}
itemsPerPage={itemsPerPage}
onNextClick={onNextClick}
diff --git a/frontend/src/components/base/PaginationBar/_PaginationBar.scss.scss b/frontend/src/components/base/PaginationBar/_PaginationBar.scss.scss
index 3936387..fc38e04 100644
--- a/frontend/src/components/base/PaginationBar/_PaginationBar.scss.scss
+++ b/frontend/src/components/base/PaginationBar/_PaginationBar.scss.scss
@@ -3,6 +3,7 @@
border-bottom: 1px solid $color-mid-light;
align-items: center;
padding: $spv--small 0;
+ margin-bottom: $spv--large;
.pagination-bar__description {
margin: 0;
diff --git a/frontend/src/components/base/TablePagination/TablePagination.test.tsx b/frontend/src/components/base/TablePagination/TablePagination.test.tsx
index 4befc42..2b915fe 100644
--- a/frontend/src/components/base/TablePagination/TablePagination.test.tsx
+++ b/frontend/src/components/base/TablePagination/TablePagination.test.tsx
@@ -8,6 +8,7 @@ it("should render pagination component correctly", () => {
render(
<TablePagination
currentPage={1}
+ debouncedPageNumber={1}
isLoading={false}
itemsPerPage={10}
onNextClick={() => {}}
@@ -23,6 +24,7 @@ it("should render previous button as disabled on first page", () => {
render(
<TablePagination
currentPage={1}
+ debouncedPageNumber={1}
isLoading={false}
itemsPerPage={10}
onNextClick={() => {}}
@@ -38,6 +40,7 @@ it("should render next button as disabled on last page", () => {
render(
<TablePagination
currentPage={1}
+ debouncedPageNumber={1}
isLoading={false}
itemsPerPage={10}
onNextClick={() => {}}
@@ -55,6 +58,7 @@ it("next and previous buttons work as expected", async () => {
render(
<TablePagination
currentPage={2}
+ debouncedPageNumber={1}
isLoading={false}
itemsPerPage={10}
onNextClick={onNextClick}
@@ -77,6 +81,7 @@ it("should have a numeric input showing the current page", () => {
render(
<TablePagination
currentPage={currentPage}
+ debouncedPageNumber={1}
isLoading={false}
itemsPerPage={10}
onNextClick={() => {}}
@@ -93,6 +98,7 @@ it("disables numeric input and buttons when data is loading", () => {
render(
<TablePagination
currentPage={1}
+ debouncedPageNumber={1}
isLoading={true}
itemsPerPage={10}
onNextClick={() => {}}
diff --git a/frontend/src/components/base/TablePagination/TablePagination.tsx b/frontend/src/components/base/TablePagination/TablePagination.tsx
index 2dbd86f..60f2455 100644
--- a/frontend/src/components/base/TablePagination/TablePagination.tsx
+++ b/frontend/src/components/base/TablePagination/TablePagination.tsx
@@ -11,6 +11,7 @@ export type AppPaginationProps = {
onPreviousClick: () => void;
setCurrentPage: (x: number) => void;
isLoading: boolean;
+ debouncedPageNumber: number;
};
const TablePagination = ({
@@ -21,10 +22,11 @@ const TablePagination = ({
onPreviousClick,
setCurrentPage,
isLoading,
+ debouncedPageNumber,
}: AppPaginationProps) => {
const totalPages = useMemo(() => Math.ceil(totalItems / itemsPerPage), [itemsPerPage, totalItems]);
- const isInputError = currentPage <= 0 || currentPage > totalPages;
+ const isInputError = debouncedPageNumber <= 0 || debouncedPageNumber > totalPages;
const handlePageInput = (e: ChangeEvent<HTMLInputElement>) => {
const { valueAsNumber } = e.target;
diff --git a/frontend/src/hooks/usePagination.test.ts b/frontend/src/hooks/usePagination.test.ts
new file mode 100644
index 0000000..2c4f6b8
--- /dev/null
+++ b/frontend/src/hooks/usePagination.test.ts
@@ -0,0 +1,41 @@
+import usePagination from "./usePagination";
+
+import { hookAct, renderHook } from "@/test-utils";
+
+describe("usePagination", () => {
+ const samplePageSize = 50;
+ const sampleTotalCount = 200;
+
+ it("initializes the page to 0 when called", () => {
+ const { result } = renderHook(() => usePagination(samplePageSize, sampleTotalCount));
+
+ expect(result.current.page).toBe(0);
+ });
+
+ it("next and previous page functions work correctly", () => {
+ const { result } = renderHook(() => usePagination(samplePageSize, sampleTotalCount));
+
+ hookAct(() => {
+ result.current.handleNextClick();
+ });
+
+ expect(result.current.page).toBe(1);
+
+ hookAct(() => {
+ result.current.handlePreviousClick();
+ });
+
+ expect(result.current.page).toBe(0);
+ });
+
+ it("should reset page count after page size is changed", () => {
+ const { result } = renderHook(() => usePagination(samplePageSize, sampleTotalCount));
+
+ hookAct(() => {
+ result.current.handleNextClick();
+ result.current.handlePageSizeChange(10);
+ });
+
+ expect(result.current.page).toBe(0);
+ });
+});
diff --git a/frontend/src/hooks/usePagination.ts b/frontend/src/hooks/usePagination.ts
new file mode 100644
index 0000000..5608eb7
--- /dev/null
+++ b/frontend/src/hooks/usePagination.ts
@@ -0,0 +1,38 @@
+import { useState } from "react";
+
+import useDebouncedValue from "./useDebouncedValue";
+
+function usePagination(pageSize: number, totalItems: number) {
+ const [page, setPage] = useState(0);
+ const [size, setSize] = useState(pageSize);
+ const debouncedPage = useDebouncedValue(page);
+
+ const handleNextClick = () => {
+ const maxPage = totalItems > 0 ? totalItems / size : 1;
+ setPage((prev) => (prev >= maxPage ? maxPage : prev + 1));
+ };
+
+ const handlePreviousClick = () => {
+ setPage((prev) => (prev === 0 ? 0 : prev - 1));
+ };
+
+ const resetPageCount = () => setPage(0);
+
+ const handlePageSizeChange = (size: number) => {
+ setSize(size);
+ resetPageCount();
+ };
+
+ return {
+ page: page,
+ size,
+ setPage,
+ debouncedPage: debouncedPage,
+ handleNextClick,
+ handlePreviousClick,
+ handlePageSizeChange,
+ totalItems,
+ };
+}
+
+export default usePagination;
diff --git a/frontend/src/test-utils.tsx b/frontend/src/test-utils.tsx
index 2d72a39..cabf08d 100644
--- a/frontend/src/test-utils.tsx
+++ b/frontend/src/test-utils.tsx
@@ -71,7 +71,7 @@ const getByTextContent = (text: string | RegExp) => {
export { screen, within, waitFor, act } from "@testing-library/react";
export { customRender as render };
-export { renderHook } from "@testing-library/react-hooks";
+export { renderHook, act as hookAct } from "@testing-library/react-hooks";
export { default as userEvent } from "@testing-library/user-event";
export { renderWithMemoryRouter };
export { Providers };
Follow ups