Merge pull request #1477 from prymitive/pagination

feat(ui): add a pagination component with keyboard shortcuts
This commit is contained in:
Łukasz Mierzwa
2020-02-29 10:52:40 +00:00
committed by GitHub
11 changed files with 221 additions and 63 deletions

View File

@@ -50,6 +50,10 @@ exports[`<LabelSetList /> matches snapshot with populated list 1`] = `
</li>
</ul>
</div>
<div tabindex=\\"-1\\"
class=\\"components-pagination\\"
>
</div>
</div>
"
`;

View File

@@ -6,16 +6,9 @@ import { observable, action } from "mobx";
import hash from "object-hash";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faAngleLeft } from "@fortawesome/free-solid-svg-icons/faAngleLeft";
import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight";
import { faAngleDoubleLeft } from "@fortawesome/free-solid-svg-icons/faAngleDoubleLeft";
import { faAngleDoubleRight } from "@fortawesome/free-solid-svg-icons/faAngleDoubleRight";
import Pagination from "react-js-pagination";
import { AlertStore } from "Stores/AlertStore";
import { StaticLabel } from "Components/Labels/StaticLabel";
import { PageSelect } from "Components/Pagination";
// take a list of groups and outputs a list of label sets, this ignores
// the receiver, so we'll end up with only unique alerts
@@ -87,27 +80,13 @@ const LabelSetList = observer(
))}
</ul>
</div>
{labelsList.length > this.maxPerPage ? (
<div className="mt-3">
<Pagination
activePage={this.pagination.activePage}
itemsCountPerPage={this.maxPerPage}
totalItemsCount={labelsList.length}
pageRangeDisplayed={5}
onChange={this.pagination.onPageChange}
hideFirstLastPages={labelsList.length / this.maxPerPage < 10}
innerClass="pagination justify-content-center"
itemClass="page-item"
linkClass="page-link"
activeClass="active"
activeLinkClass="font-weight-bold"
prevPageText={<FontAwesomeIcon icon={faAngleLeft} />}
nextPageText={<FontAwesomeIcon icon={faAngleRight} />}
firstPageText={<FontAwesomeIcon icon={faAngleDoubleLeft} />}
lastPageText={<FontAwesomeIcon icon={faAngleDoubleRight} />}
/>
</div>
) : null}
<PageSelect
totalPages={Math.ceil(labelsList.length / this.maxPerPage)}
activePage={this.pagination.activePage}
maxPerPage={this.maxPerPage}
totalItemsCount={labelsList.length}
setPageCallback={this.pagination.onPageChange}
/>
</div>
) : (
<p className="text-muted text-center">No alerts matched</p>

View File

@@ -0,0 +1,90 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { HotKeys } from "react-hotkeys";
import Pagination from "react-js-pagination";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faAngleLeft } from "@fortawesome/free-solid-svg-icons/faAngleLeft";
import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight";
import { faAngleDoubleLeft } from "@fortawesome/free-solid-svg-icons/faAngleDoubleLeft";
import { faAngleDoubleRight } from "@fortawesome/free-solid-svg-icons/faAngleDoubleRight";
class PageSelect extends Component {
static propTypes = {
totalPages: PropTypes.number.isRequired,
activePage: PropTypes.number.isRequired,
maxPerPage: PropTypes.number.isRequired,
totalItemsCount: PropTypes.number.isRequired,
setPageCallback: PropTypes.func.isRequired
};
constructor(props) {
super(props);
this.HotKeysRef = React.createRef();
}
onPageUp = () => {
const { setPageCallback, activePage, totalPages } = this.props;
setPageCallback(Math.min(activePage + 1, totalPages));
};
onPageDown = () => {
const { setPageCallback, activePage } = this.props;
setPageCallback(Math.max(activePage - 1, 1));
};
componentDidMount() {
this.HotKeysRef.current.focus();
}
render() {
const {
totalItemsCount,
totalPages,
maxPerPage,
activePage,
setPageCallback
} = this.props;
return (
<HotKeys
className="components-pagination"
innerRef={this.HotKeysRef}
keyMap={{
onArrowLeft: "ArrowLeft",
onArrowRight: "ArrowRight"
}}
handlers={{
onArrowLeft: this.onPageDown,
onArrowRight: this.onPageUp
}}
>
{totalItemsCount > maxPerPage ? (
<div className="mt-3">
<Pagination
activePage={activePage}
itemsCountPerPage={maxPerPage}
totalItemsCount={totalItemsCount}
pageRangeDisplayed={5}
onChange={setPageCallback}
hideFirstLastPages={totalPages < 10}
innerClass="pagination justify-content-center"
itemClass="page-item"
linkClass="page-link"
activeClass="active"
activeLinkClass="font-weight-bold"
prevPageText={<FontAwesomeIcon icon={faAngleLeft} />}
nextPageText={<FontAwesomeIcon icon={faAngleRight} />}
firstPageText={<FontAwesomeIcon icon={faAngleDoubleLeft} />}
lastPageText={<FontAwesomeIcon icon={faAngleDoubleRight} />}
/>
</div>
) : null}
</HotKeys>
);
}
}
export { PageSelect };

View File

@@ -0,0 +1,51 @@
import React from "react";
import { mount } from "enzyme";
import { PressKey } from "__mocks__/KeyPress";
import { PageSelect } from ".";
describe("<PageSelect />", () => {
it("calls setPageCallback on arrow key press", () => {
const setPageCallback = jest.fn();
const tree = mount(
<PageSelect
totalPages={4}
activePage={1}
maxPerPage={5}
totalItemsCount={17}
setPageCallback={setPageCallback}
/>
);
tree.simulate("focus");
setPageCallback.mockImplementation(val =>
tree.setProps({ activePage: val })
);
PressKey(tree, "ArrowRight", 39);
expect(setPageCallback).toHaveBeenLastCalledWith(2);
PressKey(tree, "ArrowRight", 39);
expect(setPageCallback).toHaveBeenLastCalledWith(3);
PressKey(tree, "ArrowRight", 39);
expect(setPageCallback).toHaveBeenLastCalledWith(4);
PressKey(tree, "ArrowRight", 39);
expect(setPageCallback).toHaveBeenLastCalledWith(4);
PressKey(tree, "ArrowLeft", 37);
expect(setPageCallback).toHaveBeenLastCalledWith(3);
PressKey(tree, "ArrowLeft", 37);
expect(setPageCallback).toHaveBeenLastCalledWith(2);
PressKey(tree, "ArrowLeft", 37);
expect(setPageCallback).toHaveBeenLastCalledWith(1);
PressKey(tree, "ArrowLeft", 37);
expect(setPageCallback).toHaveBeenLastCalledWith(1);
});
});

View File

@@ -6,17 +6,11 @@ import { observer } from "mobx-react";
import debounce from "lodash/debounce";
import Pagination from "react-js-pagination";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSpinner } from "@fortawesome/free-solid-svg-icons/faSpinner";
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle";
import { faSortAmountDownAlt } from "@fortawesome/free-solid-svg-icons/faSortAmountDownAlt";
import { faSortAmountUp } from "@fortawesome/free-solid-svg-icons/faSortAmountUp";
import { faAngleLeft } from "@fortawesome/free-solid-svg-icons/faAngleLeft";
import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight";
import { faAngleDoubleLeft } from "@fortawesome/free-solid-svg-icons/faAngleDoubleLeft";
import { faAngleDoubleRight } from "@fortawesome/free-solid-svg-icons/faAngleDoubleRight";
import { AlertStore, FormatBackendURI } from "Stores/AlertStore";
import { SilenceFormStore } from "Stores/SilenceFormStore";
@@ -24,6 +18,7 @@ import { Settings } from "Stores/Settings";
import { FetchGet } from "Common/Fetch";
import { MountFade } from "Components/Animations/MountFade";
import { ManagedSilence } from "Components/ManagedSilence";
import { PageSelect } from "Components/Pagination";
const FetchError = ({ message }) => (
<div className="text-center">
@@ -57,7 +52,10 @@ const Browser = observer(
onDeleteModalClose: PropTypes.func.isRequired
};
fetchTimer = null;
constructor(props) {
super(props);
this.fetchTimer = null;
}
dataSource = observable(
{
@@ -249,33 +247,6 @@ const Browser = observer(
onDeleteModalClose={onDeleteModalClose}
/>
))}
{this.dataSource.silences.length > this.maxPerPage ? (
<div className="mt-3">
<Pagination
activePage={this.pagination.activePage}
itemsCountPerPage={this.maxPerPage}
totalItemsCount={this.dataSource.silences.length}
pageRangeDisplayed={5}
onChange={this.pagination.onPageChange}
hideFirstLastPages={
this.dataSource.silences.length / this.maxPerPage < 20
}
innerClass="pagination justify-content-center"
itemClass="page-item"
linkClass="page-link"
activeClass="active"
activeLinkClass="font-weight-bold"
prevPageText={<FontAwesomeIcon icon={faAngleLeft} />}
nextPageText={<FontAwesomeIcon icon={faAngleRight} />}
firstPageText={
<FontAwesomeIcon icon={faAngleDoubleLeft} />
}
lastPageText={
<FontAwesomeIcon icon={faAngleDoubleRight} />
}
/>
</div>
) : null}
</React.Fragment>
)
) : (
@@ -283,6 +254,15 @@ const Browser = observer(
content={<FontAwesomeIcon icon={faSpinner} size="lg" spin />}
/>
)}
<PageSelect
totalPages={Math.ceil(
this.dataSource.silences.length / this.maxPerPage
)}
activePage={this.pagination.activePage}
maxPerPage={this.maxPerPage}
totalItemsCount={this.dataSource.silences.length}
setPageCallback={this.pagination.onPageChange}
/>
</React.Fragment>
);
}

View File

@@ -8,6 +8,7 @@ import moment from "moment";
import { advanceTo, clear } from "jest-date-mock";
import { MockSilence } from "__mocks__/Alerts";
import { PressKey } from "__mocks__/KeyPress";
import { AlertStore } from "Stores/AlertStore";
import { Settings } from "Stores/Settings";
import { SilenceFormStore } from "Stores/SilenceFormStore";
@@ -202,14 +203,52 @@ describe("<Browser />", () => {
const tree = MountedBrowser();
await expect(tree.instance().dataSource.fetch).resolves.toBeUndefined();
tree.update();
expect(tree.instance().pagination.activePage).toBe(1);
expect(tree.find("ManagedSilence")).toHaveLength(5);
const pageLink = tree.find(".page-link").at(3);
pageLink.simulate("click");
tree.update();
expect(tree.instance().pagination.activePage).toBe(2);
expect(tree.find("ManagedSilence")).toHaveLength(1);
});
it("renders next/previous page after arrow key press", async () => {
fetch.mockResponse(JSON.stringify(MockSilenceList(11)));
const tree = MountedBrowser();
await expect(tree.instance().dataSource.fetch).resolves.toBeUndefined();
tree.update();
expect(tree.instance().pagination.activePage).toBe(1);
expect(tree.find("ManagedSilence")).toHaveLength(5);
const paginator = tree.find(".components-pagination").at(0);
paginator.simulate("focus");
PressKey(paginator, "ArrowRight", 39);
expect(tree.instance().pagination.activePage).toBe(2);
expect(tree.find("ManagedSilence")).toHaveLength(5);
PressKey(paginator, "ArrowRight", 39);
expect(tree.instance().pagination.activePage).toBe(3);
expect(tree.find("ManagedSilence")).toHaveLength(1);
PressKey(paginator, "ArrowRight", 39);
expect(tree.instance().pagination.activePage).toBe(3);
expect(tree.find("ManagedSilence")).toHaveLength(1);
PressKey(paginator, "ArrowLeft", 37);
expect(tree.instance().pagination.activePage).toBe(2);
expect(tree.find("ManagedSilence")).toHaveLength(5);
PressKey(paginator, "ArrowLeft", 37);
expect(tree.instance().pagination.activePage).toBe(1);
expect(tree.find("ManagedSilence")).toHaveLength(5);
PressKey(paginator, "ArrowLeft", 37);
expect(tree.instance().pagination.activePage).toBe(1);
expect(tree.find("ManagedSilence")).toHaveLength(5);
});
it("resets pagination to last page on truncation", async () => {
fetch.mockResponseOnce(JSON.stringify(MockSilenceList(11)));
const tree = MountedBrowser();

View File

@@ -89,6 +89,10 @@ exports[`<SilencePreview /> matches snapshot 1`] = `
</li>
</ul>
</div>
<div tabindex=\\"-1\\"
class=\\"components-pagination\\"
>
</div>
</div>
</div>
<div class=\\"d-flex flex-row-reverse\\">

View File

@@ -0,0 +1,3 @@
.components-pagination:focus {
outline: none;
}

View File

@@ -122,6 +122,7 @@ $color-default: #708090;
@import "Styles/Components/MountFade";
@import "Styles/Components/NavBarSlide";
@import "Styles/Components/SilenceModal";
@import "Styles/Components/Pagination";
.badge,
.modal-content {

View File

@@ -105,6 +105,7 @@ $color-default: #708090;
@import "Styles/Components/MountFade";
@import "Styles/Components/NavBarSlide";
@import "Styles/Components/SilenceModal";
@import "Styles/Components/Pagination";
a {
color: $link-color;

View File

@@ -0,0 +1,6 @@
const PressKey = (tree, key, code) => {
tree.simulate("keyDown", { key: key, keyCode: code, which: code });
tree.simulate("keyUp", { key: key, keyCode: code, which: code });
};
export { PressKey };