Kaynağa Gözat

Add `pagination number` with `page counts`

Signed-off-by: Mihaela Balutoiu <mbalutoiu@cloudbasesolutions.com>
Mihaela Balutoiu 10 ay önce
ebeveyn
işleme
cb40323b31

+ 2 - 2
config.ts

@@ -198,9 +198,9 @@ const conf: Config = {
     "client_secret",
   ],
 
-  // The number of items per page applicable to main lists:
+  // The number of items per page applicable to default lists:
   // transfers, deployments, endpoints, users etc.
-  mainListItemsPerPage: 20,
+  defaultListItemsPerPage: 25,
 
   maxMinionPoolEventsPerPage: 50,
 

+ 1 - 1
src/@types/Config.ts

@@ -39,7 +39,7 @@ export type Config = {
   providersDisabledExecuteOptions: [ProviderTypes];
   hiddenUsers: string[];
   passwordFields: string[];
-  mainListItemsPerPage: number;
+  defaultListItemsPerPage?: number;
   servicesUrls: Services;
   maxMinionPoolEventsPerPage: number;
   bareMetalEndpointName: string;

+ 2 - 0
src/components/smart/EndpointsPage/EndpointsPage.tsx

@@ -355,6 +355,8 @@ class EndpointsPage extends React.Component<Props, State> {
               onEmptyListButtonClick={() => {
                 this.handleEmptyListButtonClick();
               }}
+              itemsPerPageOptions={[10, 25, 50]}
+              initialItemsPerPage={10}
             />
           }
           headerComponent={

+ 2 - 0
src/components/smart/ProjectsPage/ProjectsPage.tsx

@@ -159,6 +159,8 @@ class ProjectsPage extends React.Component<Props, State> {
                   }
                 />
               )}
+              itemsPerPageOptions={[10, 25, 50]}
+              initialItemsPerPage={10}
             />
           }
           headerComponent={

+ 2 - 0
src/components/smart/UsersPage/UsersPage.tsx

@@ -145,6 +145,8 @@ class UsersPage extends React.Component<Props, State> {
                   getProjectName={projectId => this.getProjectName(projectId)}
                 />
               )}
+              itemsPerPageOptions={[10, 25, 50]}
+              initialItemsPerPage={10}
             />
           }
           headerComponent={

+ 1 - 1
src/components/ui/Lists/FilterList/FilterList.spec.tsx

@@ -21,7 +21,7 @@ import userEvent from "@testing-library/user-event";
 import { ItemComponentProps } from "@src/components/ui/Lists/MainList";
 
 jest.mock("@src/utils/Config", () => ({
-  config: { mainListItemsPerPage: 2 },
+  config: { defaultListItemsPerPage: 2 },
 }));
 
 const ITEMS = [

+ 81 - 39
src/components/ui/Lists/FilterList/FilterList.tsx

@@ -17,11 +17,10 @@ import { observer } from "mobx-react";
 import styled from "styled-components";
 
 import MainListFilter from "@src/components/ui/Lists/MainListFilter";
-import Pagination from "@src/components/ui/Pagination";
+import NumberedPagination from "@src/components/ui/Pagination/NumberedPagination";
 import type { DropdownAction } from "@src/components/ui/Dropdowns/ActionDropdown";
 import type { ItemComponentProps } from "@src/components/ui/Lists/MainList";
 import MainList from "@src/components/ui/Lists/MainList";
-
 import configLoader from "@src/utils/Config";
 
 const Wrapper = styled.div<any>`
@@ -31,6 +30,15 @@ const Wrapper = styled.div<any>`
   min-height: 0;
 `;
 
+const Footer = styled.div<any>`
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16px 32px;
+  color: #616770;
+  border-top: 1px solid #e8e8e8;
+`;
+
 type DictItem = { value: string; label: string };
 type Props = {
   items: any[];
@@ -57,6 +65,8 @@ type Props = {
   customFilterComponent?: React.ReactNode;
   largeDropdownActionItems?: boolean;
   listHeaderComponent?: React.ReactNode;
+  itemsPerPageOptions?: number[];
+  initialItemsPerPage?: number;
 };
 type State = {
   items: any[];
@@ -65,6 +75,8 @@ type State = {
   selectedItems: any[];
   selectAllSelected: boolean;
   currentPage: number;
+  itemsPerPage: any;
+  itemsPerPageOptions?: number[];
 };
 @observer
 class FilterList extends React.Component<Props, State> {
@@ -77,6 +89,9 @@ class FilterList extends React.Component<Props, State> {
     selectedItems: [],
     selectAllSelected: false,
     currentPage: 1,
+    itemsPerPage:
+      this.props.initialItemsPerPage ||
+      configLoader.config.defaultListItemsPerPage,
   };
 
   UNSAFE_componentWillMount() {
@@ -96,6 +111,7 @@ class FilterList extends React.Component<Props, State> {
           filterText: "",
           selectedItems: [],
           currentPage: 1,
+          itemsPerPage: props.initialItemsPerPage || this.state.itemsPerPage,
         },
         () => {
           if (this.props.onPaginatedItemsChange) {
@@ -122,14 +138,11 @@ class FilterList extends React.Component<Props, State> {
 
   get paginatedItems() {
     let paginatedItems = this.state.items;
-    if (paginatedItems.length > configLoader.config.mainListItemsPerPage) {
+    if (paginatedItems.length > this.state.itemsPerPage) {
       paginatedItems = this.state.items.filter(
         (_, i) =>
-          i <
-            configLoader.config.mainListItemsPerPage * this.state.currentPage &&
-          i >=
-            configLoader.config.mainListItemsPerPage *
-              (this.state.currentPage - 1),
+          i < this.state.itemsPerPage * this.state.currentPage &&
+          i >= this.state.itemsPerPage * (this.state.currentPage - 1),
       );
     }
     return paginatedItems;
@@ -224,41 +237,62 @@ class FilterList extends React.Component<Props, State> {
     return filteredItems;
   }
 
-  handlePageClick(page: number) {
-    this.setState({ currentPage: page }, () => {
-      if (this.props.onPaginatedItemsChange) {
-        this.props.onPaginatedItemsChange(this.paginatedItems);
-      }
-      this.mainListWrapperRef.current?.scrollTo(0, 0);
-    });
+  setPageAndItemsPerPage(page: number, itemsPerPage?: number) {
+    this.setState(
+      prevState => ({
+        currentPage: page,
+        itemsPerPage:
+          itemsPerPage !== undefined ? itemsPerPage : prevState.itemsPerPage,
+        selectedItems: [],
+        selectAllSelected: false,
+      }),
+      () => {
+        if (this.props.onPaginatedItemsChange) {
+          this.props.onPaginatedItemsChange(this.paginatedItems);
+        }
+        this.mainListWrapperRef.current?.scrollTo(0, 0);
+        if (this.props.onSelectedItemsChange) {
+          this.props.onSelectedItemsChange([]);
+        }
+      },
+    );
+  }
+
+  handlePageClick = (page: number) => {
+    this.setPageAndItemsPerPage(page);
+  };
+
+  handleItemsPerPageChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
+    const itemsPerPage = parseInt(event.target.value, 10);
+    this.setPageAndItemsPerPage(1, itemsPerPage);
+  };
+
+  getFooterText() {
+    if (!this.paginatedItems.length) return "";
+
+    const label =
+      this.props.selectionLabel.charAt(0).toUpperCase() +
+      this.props.selectionLabel.slice(1);
+    const plural = this.paginatedItems.length !== 1 ? "s" : "";
+    return `${this.paginatedItems.length} ${label}${plural}`;
   }
 
   renderPagination() {
-    const itemsCount = this.state.items.length;
-    const totalPages = Math.ceil(
-      itemsCount / configLoader.config.mainListItemsPerPage,
-    );
-    const hasNextPage =
-      this.state.currentPage * configLoader.config.mainListItemsPerPage <
-      itemsCount;
-    const isPreviousDisabled = this.state.currentPage === 1;
-    const isNextDisabled = !hasNextPage;
+    if (this.state.items.length === 0) {
+      return null;
+    }
 
-    return itemsCount > configLoader.config.mainListItemsPerPage ? (
-      <Pagination
+    return (
+      <NumberedPagination
+        itemsCount={this.state.items.length}
         currentPage={this.state.currentPage}
-        totalPages={totalPages}
-        nextDisabled={isNextDisabled}
-        previousDisabled={isPreviousDisabled}
-        onNextClick={() => {
-          this.handlePageClick(this.state.currentPage + 1);
-        }}
-        onPreviousClick={() => {
-          this.handlePageClick(this.state.currentPage - 1);
-        }}
-        style={{ margin: "32px 0 16px 0" }}
+        itemsPerPage={this.state.itemsPerPage}
+        style={{ margin: "0 5px" }}
+        onPageChange={this.handlePageClick}
+        onItemsPerPageChange={this.handleItemsPerPageChange}
+        itemsPerPageOptions={this.props.itemsPerPageOptions || [25, 50, 100]}
       />
-    ) : null;
+    );
   }
 
   render() {
@@ -281,7 +315,7 @@ class FilterList extends React.Component<Props, State> {
           customFilterComponent={this.props.customFilterComponent}
           selectionInfo={{
             selected: this.state.selectedItems.length,
-            total: this.state.items.length,
+            total: this.paginatedItems.length,
             label: this.props.selectionLabel,
           }}
           items={this.props.filterItems}
@@ -311,7 +345,15 @@ class FilterList extends React.Component<Props, State> {
           emptyListComponent={this.props.emptyListComponent}
           onEmptyListButtonClick={this.props.onEmptyListButtonClick}
         />
-        {this.renderPagination()}
+        {this.state.items.length > 0 && (
+          <Footer>
+            <div>{this.getFooterText()}</div>
+            <div style={{ flex: 1, display: "flex", justifyContent: "center" }}>
+              {this.renderPagination()}
+            </div>
+            <div></div>
+          </Footer>
+        )}
       </Wrapper>
     );
   }

+ 116 - 0
src/components/ui/Pagination/NumberedPagination/NumberedPagination.tsx

@@ -0,0 +1,116 @@
+/*
+Copyright (C) 2025  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import { observer } from "mobx-react";
+import React from "react";
+import styled from "styled-components";
+
+const PaginationWrapper = styled.div<any>`
+  display: flex;
+  justify-content: center;
+  align-items: center;
+`;
+
+const PaginationButton = styled.button`
+  background-color: #007bff;
+  color: white;
+  border: 1px solid #dee2e6;
+  padding: 5px 20px;
+  margin: 0 5px;
+  cursor: pointer;
+  border-radius: 4px;
+
+  &:disabled {
+    background-color: #e9ecef;
+    color: #6c757d;
+    border-color: #dee2e6;
+    cursor: default;
+  }
+`;
+
+const PageNumber = styled.span``;
+
+const PageNext = styled(PaginationButton)``;
+
+const PagePrevious = styled(PaginationButton)``;
+
+const ItemsPerPageSelect = styled.select`
+  margin-left: 10px;
+  padding: 5px;
+  border-radius: 4px;
+  border: 1px solid #cccccc;
+`;
+
+type Props = {
+  className?: string;
+  style?: any;
+  itemsCount: number;
+  currentPage: number;
+  itemsPerPage: number;
+  itemsPerPageOptions: number[];
+  onPageChange: (newPage: number) => void;
+  onItemsPerPageChange?: (event: React.ChangeEvent<HTMLSelectElement>) => void;
+};
+
+@observer
+class NumberedPagination extends React.Component<Props> {
+  render() {
+    const { itemsCount, currentPage, itemsPerPage } = this.props;
+    const totalPages = Math.max(1, Math.ceil(itemsCount / itemsPerPage));
+    const hasNextPage = currentPage * itemsPerPage < itemsCount;
+
+    return (
+      <PaginationWrapper
+        style={this.props.style}
+        className={this.props.className}
+      >
+        <PagePrevious
+          onClick={() => {
+            if (currentPage > 1) {
+              this.props.onPageChange(currentPage - 1);
+            }
+          }}
+          disabled={currentPage === 1}
+        >
+          Previous
+        </PagePrevious>
+        <PageNumber>
+          Page {currentPage} of {totalPages}
+        </PageNumber>
+        <PageNext
+          onClick={() => {
+            if (currentPage < totalPages) {
+              this.props.onPageChange(currentPage + 1);
+            }
+          }}
+          disabled={!hasNextPage}
+        >
+          Next
+        </PageNext>
+        <ItemsPerPageSelect
+          value={itemsPerPage}
+          onChange={this.props.onItemsPerPageChange}
+        >
+          {this.props.itemsPerPageOptions.map(opt => (
+            <option key={opt} value={opt}>
+              {opt}
+            </option>
+          ))}
+        </ItemsPerPageSelect>
+      </PaginationWrapper>
+    );
+  }
+}
+
+export default NumberedPagination;

+ 6 - 0
src/components/ui/Pagination/NumberedPagination/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "NumberedPagination",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./NumberedPagination.tsx"
+}