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} /> );