diff --git a/source/console/src/Components/Dashboard/Dashboard.js b/source/console/src/Components/Dashboard/Dashboard.js index 7009fdb..e8696a6 100644 --- a/source/console/src/Components/Dashboard/Dashboard.js +++ b/source/console/src/Components/Dashboard/Dashboard.js @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import React from "react"; -import { Table, Spinner } from "reactstrap"; +import { Table, Spinner, Input, Pagination, PaginationItem, PaginationLink } from "reactstrap"; import { Link } from "react-router-dom"; import { get } from "aws-amplify/api"; @@ -14,13 +14,19 @@ class Dashboard extends React.Component { super(props); this.state = { Items: [], + filteredItems: [], isLoading: true, + searchQuery: '', + currentPage: 1, + itemsPerPage: 10, + totalTests: 0, }; } getItems = async () => { this.setState({ Items: [], + filteredItems: [], isLoading: true, }); @@ -38,19 +44,135 @@ class Dashboard extends React.Component { this.setState({ Items: data.Items, + filteredItems: data.Items, isLoading: false, + totalTests: data.Items.length, }); } catch (err) { - alert(err); + alert(err); } }; + filterItems = (items, query) => { + if (!query) return items; + + const normalizedQuery = query.toLowerCase(); + + return items.filter(item => + Object.values(item).some(value => + value && value.toString().toLowerCase().includes(normalizedQuery) + ) + ); + }; + + handleSearchChange = (event) => { + const searchQuery = event.target.value; + this.setState({ searchQuery, currentPage: 1 }, this.updateFilteredItems); + }; + + updateFilteredItems = () => { + const { Items, searchQuery } = this.state; + const filteredItems = this.filterItems(Items, searchQuery); + this.setState({ filteredItems }); + }; + + + handlePageChange = (pageNumber) => { + this.setState({ currentPage: pageNumber }); + }; + + calculatePaginationIndices = (currentPage, itemsPerPage, totalItems) => { + const indexOfLastItem = currentPage * itemsPerPage; + const indexOfFirstItem = indexOfLastItem - itemsPerPage; + const currentItems = totalItems.slice(indexOfFirstItem, indexOfLastItem); + const currentLoadRangeStart = indexOfFirstItem + 1; + const currentLoadRangeEnd = Math.min(indexOfLastItem, totalItems.length); + + return { currentItems, currentLoadRangeStart, currentLoadRangeEnd }; + }; + + componentDidMount() { this.getItems(); } + renderTableBody = (items) => { + const { currentPage, itemsPerPage } = this.state; + const { currentItems } = this.calculatePaginationIndices(currentPage, itemsPerPage, items); + + return ( + <tbody> + {currentItems.map((item) => { + return ( + <tr key={item.testId}> + <td>{item.testName}</td> + <td>{item.testId}</td> + <td className="desc">{item.testDescription}</td> + <td>{item.startTime ? item.startTime : ""}</td> + <td className={item.status}>{item.status}</td> + <td>{item.nextRun}</td> + <td className="recurrence">{item.scheduleRecurrence}</td> + <td className="td-center"> + <Link + id={`detailLink-${item.testId}`} + to={{ pathname: `/details/${item.testId}`, state: { testId: item.testId } }} + > + <i className="icon-large bi bi-arrow-right-circle-fill" /> + </Link> + </td> + </tr> + ); + })} + </tbody> + ); + }; + + renderPagination = (items) => { + const { currentPage, itemsPerPage } = this.state; + const totalPages = Math.ceil(items.length / itemsPerPage); + + if (totalPages <= 1) return null; + + const pageNumbers = []; + for (let i = 1; i <= totalPages; i++) { + pageNumbers.push(i); + } + + const previousPageItem = ( + <PaginationItem disabled={currentPage <= 1}> + <PaginationLink previous onClick={() => this.handlePageChange(currentPage - 1)}> + Previous page + </PaginationLink> + </PaginationItem> + ); + + const nextPageItem = ( + <PaginationItem disabled={currentPage >= totalPages}> + <PaginationLink next onClick={() => this.handlePageChange(currentPage + 1)}> + Next page + </PaginationLink> + </PaginationItem> + ); + + return ( + <Pagination size="sm" aria-label="Page navigation"> + {previousPageItem} + {pageNumbers.map((number) => ( + <PaginationItem key={number} active={number === currentPage}> + <PaginationLink onClick={() => this.handlePageChange(number)}> + {number} + </PaginationLink> + </PaginationItem> + ))} + {nextPageItem} + </Pagination> + ); + }; + render() { - const { Items } = this.state; + const { filteredItems, isLoading, searchQuery, currentPage, itemsPerPage } = this.state; + const { currentLoadRangeStart, currentLoadRangeEnd } = this.calculatePaginationIndices(currentPage, itemsPerPage, filteredItems); + const loadTests = filteredItems; const welcome = ( <div className="welcome"> @@ -58,30 +180,6 @@ class Dashboard extends React.Component { </div> ); - const tableBody = ( - <tbody> - {Items.map((item) => ( - <tr key={item.testId}> - <td>{item.testName}</td> - <td>{item.testId}</td> - <td className="desc">{item.testDescription}</td> - <td>{item.startTime}</td> - <td className={item.status}>{item.status}</td> - <td>{item.nextRun}</td> - <td className="recurrence">{item.scheduleRecurrence}</td> - <td className="td-center"> - <Link - id={`detailLink-${item.testId}`} - to={{ pathname: `/details/${item.testId}`, state: { testId: item.testId } }} - > - <i className="icon-large bi bi-arrow-right-circle-fill" /> - </Link> - </td> - </tr> - ))} - </tbody> - ); - return ( <div> <PageHeader @@ -89,31 +187,52 @@ class Dashboard extends React.Component { refreshButton={<RefreshButtons key="refresh-buttons" refreshFunction={this.getItems} />} /> <div className="box"> - <Table className="dashboard" borderless responsive> - <thead> - <tr> - <th>Name</th> - <th>Id</th> - <th>Description</th> - <th>Last Run (UTC)</th> - <th>Status</th> - <th>Next Run (UTC)</th> - <th>Recurrence</th> - <th className="td-center">Details</th> - </tr> - </thead> - {tableBody} - </Table> - {this.state.isLoading && ( + <Input + type="text" + placeholder="Filter by any value" + value={searchQuery} + onChange={this.handleSearchChange} + style={{ marginTop: 10, marginBottom: 10, width: 300 }} + /> + <Table className="dashboard" borderless responsive> + <thead> + <tr> + <th>Name</th> + <th>Id</th> + <th>Description</th> + <th>Last Run (UTC)</th> + <th>Status</th> + <th>Next Run (UTC)</th> + <th>Recurrence</th> + <th className="td-center">Details</th> + </tr> + </thead> + {this.renderTableBody(loadTests)} + </Table> + <div className={`page-container ${filteredItems.length === 0 ? 'no-results' : ''}`}> + {filteredItems.length === 0 ? ( + <div className="search-info" style={{padding:50}}> + The search didn't find anything + </div> + ) : ( + <div className="rows-info"> + Showing rows {currentLoadRangeStart} to {currentLoadRangeEnd} of {loadTests.length} + </div> + )} + {this.renderPagination(loadTests)} + </div> + {isLoading && ( <div className="loading"> <Spinner color="secondary" /> </div> )} </div> - {!this.state.isLoading && Items.length === 0 && welcome} + {!isLoading && ( + (this.state.totalTests === 0 && welcome) + )} </div> ); - } +} } export default Dashboard; diff --git a/source/console/src/index.css b/source/console/src/index.css index 96f2705..838e620 100644 --- a/source/console/src/index.css +++ b/source/console/src/index.css @@ -555,3 +555,34 @@ img { font-weight: 600; } } + +.pagination .disabled { + display: none; +} +.page-item .page-link{ + color: black !important; + box-shadow: none; +} +.page-item.active .page-link{ + color: white !important; + background-color: #ffa500; + border-color: #ffa500; +} +.page-container { + display: flex; + justify-content: space-between; + align-items: center; + height: 100%; +} +.page-container.no-results { + justify-content: center; +} +.rows-info, .pagination { + flex: 1; +} +.search-info { + display: flex; + justify-content: center; + align-items: center; + text-align: center; +}