diff --git a/src/app/base/components/DhcpFormFields/MachineSelect/MachineSelectBox/MachineSelectBox.tsx b/src/app/base/components/DhcpFormFields/MachineSelect/MachineSelectBox/MachineSelectBox.tsx
index 780723310b..a86a4dd057 100644
--- a/src/app/base/components/DhcpFormFields/MachineSelect/MachineSelectBox/MachineSelectBox.tsx
+++ b/src/app/base/components/DhcpFormFields/MachineSelect/MachineSelectBox/MachineSelectBox.tsx
@@ -19,7 +19,7 @@ const MachineSelectBox = ({
const [searchText, setSearchText] = useState("");
const [debouncedText, setDebouncedText] = useState("");
const [currentPage, setCurrentPage] = useState(1);
- const { machines, machineCount, loading } = useFetchMachines({
+ const { machines, machineCount, loading, totalPages } = useFetchMachines({
filters: {
[FilterGroupKey.FreeText]: debouncedText,
...(filters ? filters : {}),
@@ -58,6 +58,7 @@ const MachineSelectBox = ({
machineCount={machineCount}
machinesLoading={loading}
paginate={setCurrentPage}
+ totalPages={totalPages}
/>
diff --git a/src/app/base/components/Pagination/Pagination.test.tsx b/src/app/base/components/Pagination/Pagination.test.tsx
new file mode 100644
index 0000000000..918d8ae042
--- /dev/null
+++ b/src/app/base/components/Pagination/Pagination.test.tsx
@@ -0,0 +1,147 @@
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+
+import Pagination from "./Pagination";
+import { Label as PaginationButtonLabel } from "./PaginationButton/PaginationButton";
+
+describe("", () => {
+ // snapshot tests
+ it("renders and matches the snapshot", () => {
+ render(
+
+ );
+
+ expect(screen.getByRole("navigation")).toMatchSnapshot();
+ });
+
+ // unit tests
+ it("renders no pagination with only a single page", () => {
+ render(
+
+ );
+
+ expect(screen.queryByRole("navigation")).not.toBeInTheDocument();
+ });
+
+ it("renders a simple paginator with back and forward arrows if only five pages or less", () => {
+ render(
+
+ );
+
+ expect(
+ screen.queryByRole("listitem", { name: "…" })
+ ).not.toBeInTheDocument();
+ expect(
+ screen.getByRole("button", { name: PaginationButtonLabel.Next })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("button", { name: PaginationButtonLabel.Previous })
+ ).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "1" })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "2" })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "3" })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "4" })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "5" })).toBeInTheDocument();
+ });
+
+ it("renders a complex paginator with truncation if more than five pages", () => {
+ render(
+
+ );
+
+ expect(screen.getAllByText("…")).toHaveLength(2);
+ expect(
+ screen.getByRole("button", { name: PaginationButtonLabel.Next })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("button", { name: PaginationButtonLabel.Previous })
+ ).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "1" })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "4" })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "5" })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "6" })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "100" })).toBeInTheDocument();
+ });
+
+ it("does not render a truncation separator if currentPage is contiguous at start", () => {
+ render(
+
+ );
+
+ // There should only be one ellipsis.
+ expect(screen.getAllByText("…")).toHaveLength(1);
+ });
+
+ it("does not render a truncation separator if currentPage is contiguous at end", () => {
+ render(
+
+ );
+
+ // There should only be one ellipsis.
+ expect(screen.getAllByText("…")).toHaveLength(1);
+ });
+
+ it("does not trigger form submission on pagination button click by default", async () => {
+ const handleOnSubmit = jest.fn();
+ render(
+
+ );
+
+ await userEvent.click(screen.getByRole("button", { name: "Next page" }));
+ await userEvent.click(screen.getByRole("button", { name: "99" }));
+ expect(handleOnSubmit).not.toHaveBeenCalled();
+ });
+
+ it("can be centered", () => {
+ render(
+
+ );
+ // eslint-disable-next-line testing-library/no-node-access
+ expect(document.querySelector(".p-pagination__items")).toHaveClass(
+ "u-align--center"
+ );
+ });
+});
diff --git a/src/app/base/components/Pagination/Pagination.tsx b/src/app/base/components/Pagination/Pagination.tsx
new file mode 100644
index 0000000000..53e47f454f
--- /dev/null
+++ b/src/app/base/components/Pagination/Pagination.tsx
@@ -0,0 +1,199 @@
+/* eslint-disable react/no-multi-comp */
+import type { HTMLProps } from "react";
+
+import type { PropsWithSpread } from "@canonical/react-components";
+import classNames from "classnames";
+
+import PaginationButton from "./PaginationButton";
+import PaginationItem from "./PaginationItem";
+
+const scrollTop = () => window.scrollTo(0, 0);
+
+const generatePaginationItems = (
+ pageNumbers: number[],
+ currentPage: number,
+ truncateThreshold: number,
+ changePage: (page: number) => void
+) => {
+ const lastPage = pageNumbers.length;
+ const truncated = lastPage > truncateThreshold;
+
+ let visiblePages;
+ if (truncated) {
+ // the default range for pages outside the start and end threshold
+ let start = currentPage - 2;
+ let end = currentPage + 1;
+ // on page 1, also show pages 2, 3 and 4
+ if (currentPage === 1) {
+ start = 1;
+ end = currentPage + 3;
+ }
+ // on page 2, show page 1, and also pages 3, and 4
+ if (currentPage === 2) {
+ start = 1;
+ end = currentPage + 2;
+ }
+ // on the last page and page before last, also show the 3 previous pages
+ if (currentPage === lastPage || currentPage === lastPage - 1) {
+ start = lastPage - 4;
+ end = lastPage - 1;
+ }
+ visiblePages = pageNumbers.slice(start, end);
+ } else {
+ visiblePages = pageNumbers;
+ }
+
+ const items = [];
+ if (truncated) {
+ // render first in sequence
+ items.push(
+ changePage(1)}
+ />
+ );
+ if (![1, 2, 3].includes(currentPage)) {
+ items.push();
+ }
+ }
+
+ items.push(
+ visiblePages.map((number) => (
+ changePage(number)}
+ />
+ ))
+ );
+
+ if (truncated) {
+ // render last in sequence
+ if (![lastPage, lastPage - 1, lastPage - 2].includes(currentPage)) {
+ items.push();
+ }
+ items.push(
+ changePage(lastPage)}
+ />
+ );
+ }
+ return items;
+};
+
+const PaginationItemSeparator = (): JSX.Element => (
+
+ …
+
+);
+
+export type Props = PropsWithSpread<
+ {
+ /**
+ * The current page being viewed.
+ */
+ currentPage: number;
+ /**
+ * The number of items to show per page.
+ */
+ itemsPerPage: number;
+ /**
+ * The total number of pages.
+ */
+ totalPages?: number;
+ /**
+ * Function to handle paginating the items.
+ */
+ paginate: (page: number) => void;
+ /**
+ * The total number of items.
+ */
+ totalItems: number;
+ /**
+ * Whether to scroll to the top of the list on page change.
+ */
+ scrollToTop?: boolean;
+ /**
+ * The number of pages at which to truncate the pagination items.
+ */
+ truncateThreshold?: number;
+ /**
+ * Whether or not the pagination is ceneterd on the page.
+ */
+ centered?: boolean;
+ },
+ HTMLProps
+>;
+
+const Pagination = ({
+ itemsPerPage,
+ totalItems,
+ paginate,
+ currentPage,
+ scrollToTop,
+ truncateThreshold = 10,
+ centered,
+ totalPages,
+ ...navProps
+}: Props): JSX.Element | null => {
+ // return early if no pagination is required
+ if (totalItems <= itemsPerPage) {
+ return null;
+ }
+
+ const pageNumbers = [];
+
+ if (totalPages) {
+ for (let i = 1; i <= totalPages; i++) {
+ pageNumbers.push(i);
+ }
+ } else {
+ for (let i = 1; i <= Math.ceil(totalItems / itemsPerPage); i++) {
+ pageNumbers.push(i);
+ }
+ }
+
+ const changePage = (page: number) => {
+ paginate(page);
+ scrollToTop && scrollTop();
+ };
+
+ return (
+
+ );
+};
+
+export default Pagination;
diff --git a/src/app/base/components/Pagination/PaginationButton/PaginationButton.tsx b/src/app/base/components/Pagination/PaginationButton/PaginationButton.tsx
new file mode 100644
index 0000000000..eff404c29d
--- /dev/null
+++ b/src/app/base/components/Pagination/PaginationButton/PaginationButton.tsx
@@ -0,0 +1,49 @@
+import type { MouseEventHandler } from "react";
+
+import classNames from "classnames";
+
+export enum Label {
+ Next = "Next page",
+ Previous = "Previous page",
+}
+
+export type PaginationDirection = "forward" | "back";
+export type Props = {
+ /**
+ * The direction of the pagination.
+ */
+ direction: PaginationDirection;
+ /**
+ * Whether the pagination button should be disabled.
+ */
+ disabled?: boolean;
+ /**
+ * Function to handle clicking the pagination button.
+ */
+ onClick: MouseEventHandler;
+};
+
+const PaginationButton = ({
+ direction,
+ onClick,
+ disabled = false,
+}: Props): JSX.Element => {
+ const label = direction === "back" ? Label.Previous : Label.Next;
+ return (
+
+
+
+ );
+};
+
+export default PaginationButton;
diff --git a/src/app/base/components/Pagination/PaginationButton/index.ts b/src/app/base/components/Pagination/PaginationButton/index.ts
new file mode 100644
index 0000000000..43f366f9fc
--- /dev/null
+++ b/src/app/base/components/Pagination/PaginationButton/index.ts
@@ -0,0 +1,2 @@
+export { default } from "./PaginationButton";
+export type { Props as PaginationButtonProps } from "./PaginationButton";
diff --git a/src/app/base/components/Pagination/PaginationItem/PaginationItem.test.tsx b/src/app/base/components/Pagination/PaginationItem/PaginationItem.test.tsx
new file mode 100644
index 0000000000..95c81b0469
--- /dev/null
+++ b/src/app/base/components/Pagination/PaginationItem/PaginationItem.test.tsx
@@ -0,0 +1,33 @@
+import { render, screen } from "@testing-library/react";
+
+import PaginationItem from "./PaginationItem";
+
+describe("", () => {
+ // snapshot tests
+ it("renders and matches the snapshot", () => {
+ const { container } = render(
+
+ );
+
+ expect(container).toMatchSnapshot();
+ });
+
+ // unit tests
+ it("displays the page number", () => {
+ render();
+
+ expect(screen.getByRole("button", { name: "1" })).toBeInTheDocument();
+ });
+
+ it("sets isActive", () => {
+ render();
+
+ expect(screen.getByRole("button")).toHaveClass("is-active");
+ });
+
+ it("sets aria-current when isActive is true", () => {
+ render();
+
+ expect(screen.getByRole("button", { current: "page" })).toBeInTheDocument();
+ });
+});
diff --git a/src/app/base/components/Pagination/PaginationItem/PaginationItem.tsx b/src/app/base/components/Pagination/PaginationItem/PaginationItem.tsx
new file mode 100644
index 0000000000..9ab02d1358
--- /dev/null
+++ b/src/app/base/components/Pagination/PaginationItem/PaginationItem.tsx
@@ -0,0 +1,39 @@
+import type { MouseEventHandler } from "react";
+
+import classNames from "classnames";
+
+export type Props = {
+ /**
+ * Whether the pagination item is active, i.e. the current page is this page.
+ */
+ isActive?: boolean;
+ /**
+ * The page number.
+ */
+ number: number;
+ /**
+ * Function to handle clicking the pagination item.
+ */
+ onClick: MouseEventHandler;
+};
+
+const PaginationItem = ({
+ number,
+ onClick,
+ isActive = false,
+}: Props): JSX.Element => (
+
+
+
+);
+
+export default PaginationItem;
diff --git a/src/app/base/components/Pagination/PaginationItem/__snapshots__/PaginationItem.test.tsx.snap b/src/app/base/components/Pagination/PaginationItem/__snapshots__/PaginationItem.test.tsx.snap
new file mode 100644
index 0000000000..9c9887936e
--- /dev/null
+++ b/src/app/base/components/Pagination/PaginationItem/__snapshots__/PaginationItem.test.tsx.snap
@@ -0,0 +1,16 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` renders and matches the snapshot 1`] = `
+
+
+
+`;
diff --git a/src/app/base/components/Pagination/PaginationItem/index.ts b/src/app/base/components/Pagination/PaginationItem/index.ts
new file mode 100644
index 0000000000..a5a671f299
--- /dev/null
+++ b/src/app/base/components/Pagination/PaginationItem/index.ts
@@ -0,0 +1,2 @@
+export { default } from "./PaginationItem";
+export type { Props as PaginationItemProps } from "./PaginationItem";
diff --git a/src/app/base/components/Pagination/__snapshots__/Pagination.test.tsx.snap b/src/app/base/components/Pagination/__snapshots__/Pagination.test.tsx.snap
new file mode 100644
index 0000000000..28cc587f5c
--- /dev/null
+++ b/src/app/base/components/Pagination/__snapshots__/Pagination.test.tsx.snap
@@ -0,0 +1,93 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` renders and matches the snapshot 1`] = `
+
+`;
diff --git a/src/app/base/components/Pagination/index.ts b/src/app/base/components/Pagination/index.ts
new file mode 100644
index 0000000000..575f63a62a
--- /dev/null
+++ b/src/app/base/components/Pagination/index.ts
@@ -0,0 +1,2 @@
+export { default } from "./Pagination";
+export type { Props as PaginationProps } from "./Pagination";
diff --git a/src/app/kvm/components/VmResources/VmResources.tsx b/src/app/kvm/components/VmResources/VmResources.tsx
index 4cf661b127..fbb4220fac 100644
--- a/src/app/kvm/components/VmResources/VmResources.tsx
+++ b/src/app/kvm/components/VmResources/VmResources.tsx
@@ -43,6 +43,7 @@ const VmResources = ({ filters, podId }: Props): JSX.Element => {
machineCount,
machines: vms,
groups,
+ totalPages,
} = useFetchMachines({
filters: {
...filters,
@@ -57,6 +58,7 @@ const VmResources = ({ filters, podId }: Props): JSX.Element => {
},
});
const count = useFetchedCount(machineCount, loading);
+ const pages = useFetchedCount(totalPages, loading);
return (
@@ -91,6 +93,7 @@ const VmResources = ({ filters, podId }: Props): JSX.Element => {
showActions={false}
sortDirection={sortDirection}
sortKey={sortKey}
+ totalPages={pages}
/>
diff --git a/src/app/machines/components/MachineHeaderForms/MachineActionFormWrapper/CloneForm/CloneFormFields/SourceMachineSelect/SourceMachineSelect.tsx b/src/app/machines/components/MachineHeaderForms/MachineActionFormWrapper/CloneForm/CloneFormFields/SourceMachineSelect/SourceMachineSelect.tsx
index ceadebea02..55321bc6c1 100644
--- a/src/app/machines/components/MachineHeaderForms/MachineActionFormWrapper/CloneForm/CloneFormFields/SourceMachineSelect/SourceMachineSelect.tsx
+++ b/src/app/machines/components/MachineHeaderForms/MachineActionFormWrapper/CloneForm/CloneFormFields/SourceMachineSelect/SourceMachineSelect.tsx
@@ -44,7 +44,7 @@ export const SourceMachineSelect = ({
// We filter by a subset of machine parameters rather than using the search
// selector, because the search selector will match parameters that aren't
// included in the clone source table.
- const { machines, machineCount, loading } = useFetchMachines({
+ const { machines, machineCount, loading, totalPages } = useFetchMachines({
filters: FilterMachines.parseFetchFilters(debouncedText),
pagination: {
currentPage,
@@ -93,6 +93,7 @@ export const SourceMachineSelect = ({
machineCount={machineCount}
machinesLoading={loading}
paginate={setCurrentPage}
+ totalPages={totalPages}
/>
);
diff --git a/src/app/machines/views/MachineList/MachineList.tsx b/src/app/machines/views/MachineList/MachineList.tsx
index 97dfafe97e..86b0b3c6b3 100644
--- a/src/app/machines/views/MachineList/MachineList.tsx
+++ b/src/app/machines/views/MachineList/MachineList.tsx
@@ -76,15 +76,22 @@ const MachineList = ({
[]
);
- const { callId, loading, machineCount, machines, machinesErrors, groups } =
- useFetchMachinesWithGroupingUpdates({
- collapsedGroups: hiddenGroups,
- filters: FilterMachines.parseFetchFilters(searchFilter),
- grouping,
- sortDirection,
- sortKey,
- pagination: { currentPage, setCurrentPage, pageSize: PAGE_SIZE },
- });
+ const {
+ callId,
+ loading,
+ machineCount,
+ machines,
+ machinesErrors,
+ groups,
+ totalPages,
+ } = useFetchMachinesWithGroupingUpdates({
+ collapsedGroups: hiddenGroups,
+ filters: FilterMachines.parseFetchFilters(searchFilter),
+ grouping,
+ sortDirection,
+ sortKey,
+ pagination: { currentPage, setCurrentPage, pageSize: PAGE_SIZE },
+ });
useEffect(
() => () => {
@@ -136,6 +143,7 @@ const MachineList = ({
setSortKey={setSortKey}
sortDirection={sortDirection}
sortKey={sortKey}
+ totalPages={totalPages}
/>
>
);
diff --git a/src/app/machines/views/MachineList/MachineListTable/MachineListPagination/MachineListPagination.test.tsx b/src/app/machines/views/MachineList/MachineListTable/MachineListPagination/MachineListPagination.test.tsx
index 7767338e17..1e5e00c998 100644
--- a/src/app/machines/views/MachineList/MachineListTable/MachineListPagination/MachineListPagination.test.tsx
+++ b/src/app/machines/views/MachineList/MachineListTable/MachineListPagination/MachineListPagination.test.tsx
@@ -10,6 +10,7 @@ it("displays pagination if there are machines", () => {
machineCount={100}
machinesLoading={false}
paginate={jest.fn()}
+ totalPages={4}
/>
);
expect(
@@ -25,6 +26,7 @@ it("does not display pagination if there are no machines", () => {
machineCount={0}
machinesLoading={false}
paginate={jest.fn()}
+ totalPages={4}
/>
);
expect(
@@ -37,6 +39,7 @@ it("displays pagination while refetching machines", () => {
const props = {
currentPage: 1,
itemsPerPage: 20,
+ totalPages: 4,
machineCount: 100,
machinesLoading: false,
paginate: jest.fn(),
@@ -56,6 +59,7 @@ it("hides pagination if there are no refetched machines", () => {
const props = {
currentPage: 1,
itemsPerPage: 20,
+ totalPages: 4,
machineCount: 100,
machinesLoading: false,
paginate: jest.fn(),
diff --git a/src/app/machines/views/MachineList/MachineListTable/MachineListPagination/MachineListPagination.tsx b/src/app/machines/views/MachineList/MachineListTable/MachineListPagination/MachineListPagination.tsx
index 5e6f4f6d58..7fcc3fec63 100644
--- a/src/app/machines/views/MachineList/MachineListTable/MachineListPagination/MachineListPagination.tsx
+++ b/src/app/machines/views/MachineList/MachineListTable/MachineListPagination/MachineListPagination.tsx
@@ -2,8 +2,8 @@ import type {
PaginationProps,
PropsWithSpread,
} from "@canonical/react-components";
-import { Pagination } from "@canonical/react-components";
+import Pagination from "app/base/components/Pagination";
import { useFetchedCount } from "app/store/machine/utils";
export enum Label {
@@ -15,6 +15,7 @@ type Props = PropsWithSpread<
currentPage: PaginationProps["currentPage"];
itemsPerPage: PaginationProps["itemsPerPage"];
machineCount: number | null;
+ totalPages: number | null;
machinesLoading?: boolean | null;
paginate: PaginationProps["paginate"];
},
@@ -24,15 +25,18 @@ type Props = PropsWithSpread<
const MachineListPagination = ({
machineCount,
machinesLoading,
+ totalPages,
...props
}: Props): JSX.Element | null => {
const count = useFetchedCount(machineCount, machinesLoading);
+ const pages = useFetchedCount(totalPages, machinesLoading);
return count > 0 ? (
) : null;
diff --git a/src/app/machines/views/MachineList/MachineListTable/MachineListTable.test.tsx b/src/app/machines/views/MachineList/MachineListTable/MachineListTable.test.tsx
index 0da64ae1c3..dbff2de6d4 100644
--- a/src/app/machines/views/MachineList/MachineListTable/MachineListTable.test.tsx
+++ b/src/app/machines/views/MachineList/MachineListTable/MachineListTable.test.tsx
@@ -257,6 +257,7 @@ describe("MachineListTable", () => {
setSortKey={jest.fn()}
sortDirection="none"
sortKey={null}
+ totalPages={4}
/>,
{ state }
);
@@ -303,6 +304,7 @@ describe("MachineListTable", () => {
setSortKey={jest.fn()}
sortDirection="none"
sortKey={null}
+ totalPages={4}
/>,
{ state }
);
@@ -333,6 +335,7 @@ describe("MachineListTable", () => {
setSortKey={jest.fn()}
sortDirection="none"
sortKey={null}
+ totalPages={4}
/>
@@ -371,6 +374,7 @@ describe("MachineListTable", () => {
setSortKey={jest.fn()}
sortDirection="none"
sortKey={null}
+ totalPages={4}
/>
@@ -403,6 +407,7 @@ describe("MachineListTable", () => {
setSortKey={jest.fn()}
sortDirection="none"
sortKey={null}
+ totalPages={4}
/>
@@ -454,6 +459,7 @@ describe("MachineListTable", () => {
setSortKey={setSortKey}
sortDirection="none"
sortKey={null}
+ totalPages={4}
/>
@@ -493,6 +499,7 @@ describe("MachineListTable", () => {
setSortKey={setSortKey}
sortDirection={SortDirection.ASCENDING}
sortKey={FetchGroupKey.CpuCount}
+ totalPages={4}
/>
@@ -532,6 +539,7 @@ describe("MachineListTable", () => {
setSortKey={setSortKey}
sortDirection={SortDirection.DESCENDING}
sortKey={FetchGroupKey.CpuCount}
+ totalPages={4}
/>
@@ -571,6 +579,7 @@ describe("MachineListTable", () => {
setSortKey={setSortKey}
sortDirection={SortDirection.NONE}
sortKey={FetchGroupKey.CpuCount}
+ totalPages={4}
/>
@@ -610,6 +619,7 @@ describe("MachineListTable", () => {
setSortKey={setSortKey}
sortDirection={SortDirection.NONE}
sortKey={FetchGroupKey.CpuCount}
+ totalPages={4}
/>
@@ -649,6 +659,7 @@ describe("MachineListTable", () => {
setSortKey={jest.fn()}
sortDirection="none"
sortKey={null}
+ totalPages={4}
/>
@@ -683,6 +694,7 @@ describe("MachineListTable", () => {
showActions={false}
sortDirection="none"
sortKey={null}
+ totalPages={4}
/>
@@ -721,6 +733,7 @@ describe("MachineListTable", () => {
setSortKey={jest.fn()}
sortDirection="none"
sortKey={null}
+ totalPages={4}
/>
@@ -757,6 +770,7 @@ describe("MachineListTable", () => {
showActions
sortDirection="none"
sortKey={null}
+ totalPages={4}
/>
@@ -789,6 +803,7 @@ describe("MachineListTable", () => {
showActions={false}
sortDirection="none"
sortKey={null}
+ totalPages={4}
/>
diff --git a/src/app/machines/views/MachineList/MachineListTable/MachineListTable.tsx b/src/app/machines/views/MachineList/MachineListTable/MachineListTable.tsx
index d11b68c193..97e140772d 100644
--- a/src/app/machines/views/MachineList/MachineListTable/MachineListTable.tsx
+++ b/src/app/machines/views/MachineList/MachineListTable/MachineListTable.tsx
@@ -62,6 +62,7 @@ type Props = {
hiddenColumns?: string[];
hiddenGroups?: (string | null)[];
machineCount: number | null;
+ totalPages: number | null;
machines: Machine[];
machinesLoading?: boolean | null;
pageSize: number;
@@ -505,6 +506,7 @@ const generateGroupRows = ({
export const MachineListTable = ({
callId,
currentPage,
+ totalPages,
filter = "",
groups,
grouping,
@@ -843,6 +845,7 @@ export const MachineListTable = ({
machineCount={machineCount}
machinesLoading={machinesLoading}
paginate={setCurrentPage}
+ totalPages={totalPages}
/>
>
);
diff --git a/src/app/store/machine/selectors.ts b/src/app/store/machine/selectors.ts
index 1c00ae38dc..9c6a63346a 100644
--- a/src/app/store/machine/selectors.ts
+++ b/src/app/store/machine/selectors.ts
@@ -433,6 +433,17 @@ const listCount = createSelector(
(machineState, callId) => getList(machineState, callId)?.count ?? null
);
+/**
+ * Get the total page count for a machine list request with a given callId.
+ */
+const listTotalPages = createSelector(
+ [
+ machineState,
+ (_state: RootState, callId: string | null | undefined) => callId,
+ ],
+ (machineState, callId) => getList(machineState, callId)?.num_pages ?? null
+);
+
/**
* Get the stale value for a machine count request with a given callId
*/
@@ -675,6 +686,7 @@ const selectors = {
linkingSubnet: statusSelectors["linkingSubnet"],
list,
listCount,
+ listTotalPages,
listErrors,
listGroup,
listGroups,
diff --git a/src/app/store/machine/utils/hooks.ts b/src/app/store/machine/utils/hooks.ts
index 2879f209ac..7277bd7e22 100644
--- a/src/app/store/machine/utils/hooks.ts
+++ b/src/app/store/machine/utils/hooks.ts
@@ -375,6 +375,7 @@ export const useFetchSelectedMachines = (
: true,
machineCount: groupData.machineCount || 0 + (itemsData?.machineCount || 0),
machinesErrors: groupData.machinesErrors || itemsData.machinesErrors,
+ totalPages: null,
};
};
@@ -534,6 +535,7 @@ type UseFetchMachinesData = {
machineCount: number | null;
machines: Machine[];
machinesErrors: APIError;
+ totalPages: number | null;
};
/**
@@ -569,6 +571,9 @@ export const useFetchMachines = (
const machineCount = useSelector((state: RootState) =>
machineSelectors.listCount(state, callId)
);
+ const totalPages = useSelector((state: RootState) =>
+ machineSelectors.listTotalPages(state, callId)
+ );
const machinesErrors = useSelector((state: RootState) =>
machineSelectors.listErrors(state, callId)
);
@@ -654,6 +659,7 @@ export const useFetchMachines = (
loading,
groups,
machineCount,
+ totalPages,
machines,
machinesErrors,
};
@@ -677,6 +683,7 @@ export const useFetchMachinesWithGroupingUpdates = (
loaded,
machinesErrors,
machineCount,
+ totalPages,
} = useFetchMachines(options, queryOptions);
const needsUpdate = useSelector((state: RootState) =>
machineSelectors.listNeedsUpdate(state, initialCallId)
@@ -766,6 +773,7 @@ export const useFetchMachinesWithGroupingUpdates = (
machines,
machineCount,
machinesErrors,
+ totalPages,
};
};
diff --git a/src/app/tags/views/TagMachines/TagMachines.tsx b/src/app/tags/views/TagMachines/TagMachines.tsx
index 40ec75d39f..fd642e3aab 100644
--- a/src/app/tags/views/TagMachines/TagMachines.tsx
+++ b/src/app/tags/views/TagMachines/TagMachines.tsx
@@ -45,17 +45,24 @@ const TagMachines = (): JSX.Element => {
if (tag) {
filters.tags = [tag.name];
}
- const { callId, loading, machineCount, machines, groups, machinesErrors } =
- useFetchMachines({
- filters,
- sortDirection,
- sortKey,
- pagination: {
- currentPage,
- setCurrentPage,
- pageSize: PAGE_SIZE,
- },
- });
+ const {
+ callId,
+ loading,
+ machineCount,
+ machines,
+ groups,
+ machinesErrors,
+ totalPages,
+ } = useFetchMachines({
+ filters,
+ sortDirection,
+ sortKey,
+ pagination: {
+ currentPage,
+ setCurrentPage,
+ pageSize: PAGE_SIZE,
+ },
+ });
useWindowTitle(tag ? `Deployed machines for: ${tag.name}` : "Tag");
@@ -89,6 +96,7 @@ const TagMachines = (): JSX.Element => {
showActions={false}
sortDirection={sortDirection}
sortKey={sortKey}
+ totalPages={totalPages}
/>
>
);