Pagination for Peerlist (#21)

Previously all peers used to be displayed on a single page.
This change aims to improve the usability of the Peers page
by paginating the peer list.
By default 25 peers will be shown per page,
and there will be 5 pages shown in the pagination bar per default.
The current page shown will be centered in the bar shown below.
This commit is contained in:
shatoboar
2022-03-25 14:48:21 +01:00
committed by GitHub
parent 856a2e1264
commit 0206212282
3 changed files with 371 additions and 191 deletions

View File

@@ -1,7 +1,7 @@
import React, {useEffect, useState} from 'react';
import Navbar from './components/Navbar';
import {Redirect, Route, Switch} from 'react-router-dom';
import Peers from './views/Peers';
import {Peers} from './views/Peers';
import Footer from './components/Footer';
import {useAuth0} from "@auth0/auth0-react";
import Loading from "./components/Loading";

View File

@@ -0,0 +1,219 @@
import React, { useState, useEffect } from "react";
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/solid";
import { withAuthenticationRequired } from "@auth0/auth0-react";
import Loading from "../components/Loading";
// @data the data that will be paginated
// @RenderComponent the component that needs to be rendered
// @pageLimit number of Elements shown in Pagination bar
// @dataLimit maximum Elements rendered per page
const PaginatedPeersList = (props) => {
const [pageCount] = useState(Math.ceil(props.data.length / props.dataLimit)); // actual pageCount we have
const [currentPage, setCurrentPage] = useState(1);
// sliding window of size pageLimit for shown elements of bar
useEffect(() => {
window.scrollTo({ behavior: "smooth", top: "0px" });
}, [currentPage]);
function goToNextPage() {
if (currentPage === pageCount) return;
setCurrentPage((page) => page + 1);
}
function goToPreviousPage() {
if (currentPage === 1) return;
setCurrentPage((page) => page - 1);
}
function changePage(event) {
const pageNumber = Number(event.target.textContent);
setCurrentPage(pageNumber);
}
function goToFirst() {
setCurrentPage(1);
}
function goToLast() {
setCurrentPage(pageCount);
}
const getPaginatedData = () => {
const startIndex = currentPage * props.dataLimit - props.dataLimit;
const endIndex = startIndex + props.dataLimit;
return props.data.slice(startIndex, endIndex);
};
const compressPagination = () => {
// if the pageLimit is greater than the actual number of pages we have, just render all pages
if (props.pageLimit > pageCount) {
return [...Array(pageCount).keys()].map((index) => index + 1);
}
// if the currentPage is already presented in the paginationBar we can just leave the bar alone
// center the currentPage
let bar = [];
let offset = Math.floor(props.pageLimit / 2);
if (currentPage - offset <= 1) {
return [...Array(props.pageLimit).keys()].map((index) => index + 1);
}
if (currentPage + offset > pageCount) {
for (let i = pageCount - props.pageLimit + 1; i <= pageCount; i++)
bar.push(i);
return bar;
}
for (let i = offset; i > 0; i--) {
bar.push(currentPage - i);
}
bar.push(currentPage);
for (let i = 1; i <= offset; i++) {
bar.push(currentPage + i);
}
return bar;
};
function PaginationBarElem(props) {
let default_btn =
"z-10 bg-white squared-md border-gray-300 text-gray-700 relative inline-flex items-center px-4 py-2 border text-sm font-medium hover:bg-gray-50";
let clicked_btn =
"z-10 bg-gray-50 border-gray-500 text-gray-600 relative inline-flex items-center px-4 py-2 border text-sm font-medium hover:bg-gray-50";
return (
<a
aria-current="page"
className={
props.pageNo === props.clicked ? clicked_btn : default_btn
}
onClick={changePage}
>
{props.pageNo}
</a>
);
}
return (
<>
<div className="flex flex-col">
<div className="-my-2 sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
<div className="shadow border-b border-gray-200 sm:rounded-lg">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-100">
<tr>
{[
"Name",
"IP",
"Status",
"Last Seen",
"OS",
"Version",
].map((col) => {
return (
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
key={col}
>
{col}
</th>
);
})}
<th
scope="col"
className="relative px-6 py-3"
>
<span className="sr-only">
Edit
</span>
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{getPaginatedData().map((elem) =>
props.RenderComponent(elem)
)}
</tbody>
</table>
</div>
</div>
</div>
</div>
<div className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p className="text-sm text-gray-700">
Showing{" "}
<span className="font-medium">{currentPage}</span>{" "}
to <span className="font-medium">{pageCount}</span>{" "}
of <span className="font-medium">{pageCount}</span>
</p>
</div>
{pageCount == 1 ? (
<div />
) : (
<div>
<nav
className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px"
aria-label="Pagination"
>
<a
className="relative inline-flex items-center px-2 py-2 squared-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
onClick={goToFirst}
>
first
</a>
<a
className="relative inline-flex items-center px-2 py-2 squared-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
onClick={goToPreviousPage}
>
<span className="sr-only">Previous</span>
<ChevronLeftIcon
className="h-5 w-5"
aria-hidden="true"
/>
</a>
<div>
{compressPagination().map((elem) => {
return (
<PaginationBarElem
clicked={currentPage}
pageNo={elem}
key={elem}
/>
);
})}
</div>
<a
className="relative inline-flex items-center px-2 py-2 squared-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
onClick={goToNextPage}
>
<span className="sr-only">Next</span>
<ChevronRightIcon
className="h-5 w-5"
aria-hidden="true"
/>
</a>
<a
className="relative inline-flex items-center px-2 py-2 squared-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
onClick={goToLast}
>
last
</a>
</nav>
</div>
)}
</div>
</div>
</>
);
};
export default withAuthenticationRequired(PaginatedPeersList, {
onRedirecting: () => <Loading />,
});

View File

@@ -1,191 +1,156 @@
import React, {useEffect, useState} from "react";
import {useAuth0, withAuthenticationRequired} from "@auth0/auth0-react";
import React, { useEffect, useState } from "react";
import { useAuth0, withAuthenticationRequired } from "@auth0/auth0-react";
import Loading from "../components/Loading";
import {deletePeer, getPeers} from "../api/ManagementAPI";
import {timeAgo} from "../utils/common";
import { deletePeer, getPeers } from "../api/ManagementAPI";
import { timeAgo } from "../utils/common";
import EditButton from "../components/EditButton";
import CopyText from "../components/CopyText";
import DeleteModal from "../components/DeleteDialog";
import EmptyPeersPanel from "../components/EmptyPeers";
import PaginatedPeersList from "../components/PaginatedPeersList"
export const Peers = () => {
const [peers, setPeers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [peerToDelete, setPeerToDelete] = useState(null);
const [deleteDialogText, setDeleteDialogText] = useState("");
const [deleteDialogTitle, setDeleteDialogTitle] = useState("");
const [peers, setPeers] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const [deleteDialogText, setDeleteDialogText] = useState("")
const [deleteDialogTitle, setDeleteDialogTitle] = useState("")
const [peerToDelete, setPeerToDelete] = useState(null)
const { getAccessTokenSilently } = useAuth0();
const {
getAccessTokenSilently,
} = useAuth0();
const handleError = error => {
console.error('Error to fetch data:', error);
setLoading(false)
const handleError = (error) => {
console.error("Error to fetch data:", error);
setLoading(false);
setError(error);
};
//called when user clicks on table row menu item
const handleRowMenuClick = (action, peer) => {
if (action === 'Delete') {
setPeerToDelete(peer)
setDeleteDialogText("Are you sure you want to delete peer from your account?")
setDeleteDialogTitle("Delete peer \"" + peer.Name + "\"")
setShowDeleteDialog(true)
if (action === "Delete") {
setPeerToDelete(peer);
setDeleteDialogText(
"Are you sure you want to delete peer from your account?"
);
setDeleteDialogTitle('Delete peer "' + peer.Name + '"');
setShowDeleteDialog(true);
}
};
const refresh = () => {
getPeers(getAccessTokenSilently)
.then(responseData => responseData.sort((a, b) => (a.Name > b.Name) ? 1 : -1))
.then(sorted => setPeers(sorted))
.then((responseData) =>
responseData.sort((a, b) => (a.Name > b.Name ? 1 : -1))
)
.then((sorted) => setPeers(sorted))
.then(() => setLoading(false))
.catch(error => handleError(error))
}
.catch((error) => handleError(error));
};
// after user confirms (or not) deletion of the peer
const handleDeleteConfirmation = (confirmed) => {
setShowDeleteDialog(false)
setShowDeleteDialog(false);
if (confirmed) {
deletePeer(getAccessTokenSilently, peerToDelete.IP)
.then(() => setPeerToDelete(null))
.then(() => refresh())
.catch(error => {
setPeerToDelete(null)
console.log(error)
})
.catch((error) => {
setPeerToDelete(null);
console.log(error);
});
} else {
setPeerToDelete(null)
setPeerToDelete(null);
}
}
};
useEffect(() => {
refresh()
}, [getAccessTokenSilently])
refresh();
}, [getAccessTokenSilently]);
const PeerRow = (peer) => {
return (
<tr key={peer.IP}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium font-semibold font-mono text-gray-900">
{peer.Name}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium font-mono text-gray-900">
<CopyText
text={peer.IP.toUpperCase()}
idPrefix={"peers-ip-" + peer.IP}
/>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{peer.Connected && (
<span className="px-2 inline-flex text-sm leading-5 font-mono squared-full bg-green-100 text-green-800">
Online
</span>
)}
{!peer.Connected && (
<span className="px-2 inline-flex text-sm leading-5 font-mono squared-full bg-red-100 text-red-800">
Offline
</span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-700">
{peer.ConnectedP ? "just now" : timeAgo(peer.LastSeen)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-700">
{peer.OS}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-700">
{peer.Version}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-m font-medium">
<EditButton
items={[{ name: "Delete" }]}
handler={(action) => handleRowMenuClick(action, peer)}
/>
</td>
</tr>
);
};
return (
<>
<div className="py-10">
<header>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 className="text-2xl font-mono leading-tight text-gray-900 font-bold">Peers</h1>
<h1 className="text-2xl font-mono leading-tight text-gray-900 font-bold">
Peers
</h1>
</div>
</header>
<main>
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="px-4 py-8 sm:px-0">
{loading && (<Loading/>)}
{error != null && (
<span>{error.toString()}</span>
)}
{loading && <Loading />}
{error != null && <span>{error.toString()}</span>}
<main>
{loading && (<Loading/>)}
{loading && <Loading />}
{error != null && (
<span>{error.toString()}</span>
)}
{peers.length === 0 ?
(<EmptyPeersPanel/>) : (
{peers.length === 0 ? (
<EmptyPeersPanel />
) : (
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="px-4 py-8 sm:px-0">
<DeleteModal show={showDeleteDialog}
confirmCallback={handleDeleteConfirmation}
text={deleteDialogText} title={deleteDialogTitle}/>
<div className="flex flex-col">
<div className="-my-2 sm:-mx-6 lg:-mx-8">
<div
className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
<div
className="shadow border-b border-gray-200 sm:rounded-lg">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-100">
<tr>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Name
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
IP
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Status
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Last Seen
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
OS
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Version
</th>
<th scope="col" className="relative px-6 py-3">
<span className="sr-only">Edit</span>
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{peers.map((peer, idx) => (
<tr key={peer.IP}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium font-semibold font-mono text-gray-900">{peer.Name}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium font-mono text-gray-900">
<CopyText text={peer.IP.toUpperCase()}
idPrefix={"peers-ip-" + peer.IP}/>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{peer.Connected && (
<span
className="px-2 inline-flex text-sm leading-5 font-mono squared-full bg-green-100 text-green-800">
Online
</span>
)}
{!peer.Connected && (
<span
className="px-2 inline-flex text-sm leading-5 font-mono squared-full bg-red-100 text-red-800">
Offline
</span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-700">
{peer.Connected ? ("just now") : timeAgo(peer.LastSeen)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-700">{peer.OS}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-700">{peer.Version}</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-m font-medium">
<EditButton items={[{name: "Delete"}]}
handler={action => handleRowMenuClick(action, peer)}/>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
<DeleteModal
show={showDeleteDialog}
confirmCallback={
handleDeleteConfirmation
}
text={deleteDialogText}
title={deleteDialogTitle}
/>
<PaginatedPeersList
data={peers}
RenderComponent={PeerRow}
dataLimit={25}
pageLimit={5}
/>
</div>
</div>
)}
@@ -196,11 +161,7 @@ export const Peers = () => {
</div>
</>
);
}
;
export default withAuthenticationRequired(Peers,
{
onRedirecting: () => <Loading/>,
}
);
};
export default withAuthenticationRequired(Peers, {
onRedirecting: () => <Loading />,
});