diff --git a/ui/src/Components/LabelSetList/__snapshots__/index.test.js.snap b/ui/src/Components/LabelSetList/__snapshots__/index.test.js.snap
index e3ab2dbc5..5d4b9e057 100644
--- a/ui/src/Components/LabelSetList/__snapshots__/index.test.js.snap
+++ b/ui/src/Components/LabelSetList/__snapshots__/index.test.js.snap
@@ -50,6 +50,10 @@ exports[` matches snapshot with populated list 1`] = `
+
"
`;
diff --git a/ui/src/Components/LabelSetList/index.js b/ui/src/Components/LabelSetList/index.js
index 65805e717..33dcc8ff5 100644
--- a/ui/src/Components/LabelSetList/index.js
+++ b/ui/src/Components/LabelSetList/index.js
@@ -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(
))}
- {labelsList.length > this.maxPerPage ? (
-
-
}
- nextPageText={
}
- firstPageText={
}
- lastPageText={
}
- />
-
- ) : null}
+
) : (
No alerts matched
diff --git a/ui/src/Components/Pagination/index.js b/ui/src/Components/Pagination/index.js
new file mode 100644
index 000000000..d2e322504
--- /dev/null
+++ b/ui/src/Components/Pagination/index.js
@@ -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 (
+
+ {totalItemsCount > maxPerPage ? (
+
+
}
+ nextPageText={
}
+ firstPageText={
}
+ lastPageText={
}
+ />
+
+ ) : null}
+
+ );
+ }
+}
+
+export { PageSelect };
diff --git a/ui/src/Components/Pagination/index.test.js b/ui/src/Components/Pagination/index.test.js
new file mode 100644
index 000000000..3c42f9313
--- /dev/null
+++ b/ui/src/Components/Pagination/index.test.js
@@ -0,0 +1,51 @@
+import React from "react";
+
+import { mount } from "enzyme";
+
+import { PressKey } from "__mocks__/KeyPress";
+import { PageSelect } from ".";
+
+describe("", () => {
+ it("calls setPageCallback on arrow key press", () => {
+ const setPageCallback = jest.fn();
+
+ const tree = mount(
+
+ );
+ 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);
+ });
+});
diff --git a/ui/src/Components/SilenceModal/Browser/index.js b/ui/src/Components/SilenceModal/Browser/index.js
index eb0e1f333..c59f0fd49 100644
--- a/ui/src/Components/SilenceModal/Browser/index.js
+++ b/ui/src/Components/SilenceModal/Browser/index.js
@@ -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 }) => (
@@ -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 ? (
-
-
}
- nextPageText={
}
- firstPageText={
-
- }
- lastPageText={
-
- }
- />
-
- ) : null}
)
) : (
@@ -283,6 +254,15 @@ const Browser = observer(
content={
}
/>
)}
+
);
}
diff --git a/ui/src/Components/SilenceModal/Browser/index.test.js b/ui/src/Components/SilenceModal/Browser/index.test.js
index b730efe59..906194b7c 100644
--- a/ui/src/Components/SilenceModal/Browser/index.test.js
+++ b/ui/src/Components/SilenceModal/Browser/index.test.js
@@ -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("
", () => {
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();
diff --git a/ui/src/Components/SilenceModal/SilencePreview/__snapshots__/index.test.js.snap b/ui/src/Components/SilenceModal/SilencePreview/__snapshots__/index.test.js.snap
index 61199c21b..500492a15 100644
--- a/ui/src/Components/SilenceModal/SilencePreview/__snapshots__/index.test.js.snap
+++ b/ui/src/Components/SilenceModal/SilencePreview/__snapshots__/index.test.js.snap
@@ -89,6 +89,10 @@ exports[`
matches snapshot 1`] = `
+
diff --git a/ui/src/Styles/Components/Pagination.scss b/ui/src/Styles/Components/Pagination.scss
new file mode 100644
index 000000000..550e68f80
--- /dev/null
+++ b/ui/src/Styles/Components/Pagination.scss
@@ -0,0 +1,3 @@
+.components-pagination:focus {
+ outline: none;
+}
diff --git a/ui/src/Styles/DarkTheme.scss b/ui/src/Styles/DarkTheme.scss
index 0f0b67961..066614883 100644
--- a/ui/src/Styles/DarkTheme.scss
+++ b/ui/src/Styles/DarkTheme.scss
@@ -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 {
diff --git a/ui/src/Styles/LightTheme.scss b/ui/src/Styles/LightTheme.scss
index c276a293e..17db130f2 100644
--- a/ui/src/Styles/LightTheme.scss
+++ b/ui/src/Styles/LightTheme.scss
@@ -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;
diff --git a/ui/src/__mocks__/KeyPress.js b/ui/src/__mocks__/KeyPress.js
new file mode 100644
index 000000000..c4652b0b1
--- /dev/null
+++ b/ui/src/__mocks__/KeyPress.js
@@ -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 };