From 26963b4d3baaf4ce576bb607c69502ef576ffd3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Fri, 28 Feb 2020 21:34:10 +0000 Subject: [PATCH 1/3] feat(ui): add a pagination component with keyboard shortcuts --- ui/src/Components/Pagination/index.js | 89 ++++++++++++++++++++++ ui/src/Components/Pagination/index.test.js | 51 +++++++++++++ ui/src/Styles/Components/Pagination.scss | 3 + ui/src/Styles/DarkTheme.scss | 1 + ui/src/Styles/LightTheme.scss | 1 + ui/src/__mocks__/KeyPress.js | 6 ++ 6 files changed, 151 insertions(+) create mode 100644 ui/src/Components/Pagination/index.js create mode 100644 ui/src/Components/Pagination/index.test.js create mode 100644 ui/src/Styles/Components/Pagination.scss create mode 100644 ui/src/__mocks__/KeyPress.js diff --git a/ui/src/Components/Pagination/index.js b/ui/src/Components/Pagination/index.js new file mode 100644 index 000000000..c654eedfd --- /dev/null +++ b/ui/src/Components/Pagination/index.js @@ -0,0 +1,89 @@ +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, + 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/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 }; From b6e8e18d6144923a3b5b3d1ac86d1792ec0f1d54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Fri, 28 Feb 2020 21:35:30 +0000 Subject: [PATCH 2/3] chore(ui): switch silence browser to use pagination component --- .../Components/SilenceModal/Browser/index.js | 48 ++++++------------- .../SilenceModal/Browser/index.test.js | 39 +++++++++++++++ 2 files changed, 53 insertions(+), 34 deletions(-) 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(); From 9c725655cb6a703d8e1dc3b46f9657ca234347a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Fri, 28 Feb 2020 21:44:46 +0000 Subject: [PATCH 3/3] chore(ui): switch matched alerts preview to use pagination component --- .../__snapshots__/index.test.js.snap | 4 ++ ui/src/Components/LabelSetList/index.js | 37 ++++--------------- ui/src/Components/Pagination/index.js | 3 +- .../__snapshots__/index.test.js.snap | 4 ++ 4 files changed, 18 insertions(+), 30 deletions(-) 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 index c654eedfd..d2e322504 100644 --- a/ui/src/Components/Pagination/index.js +++ b/ui/src/Components/Pagination/index.js @@ -42,6 +42,7 @@ class PageSelect extends Component { render() { const { totalItemsCount, + totalPages, maxPerPage, activePage, setPageCallback @@ -68,7 +69,7 @@ class PageSelect extends Component { totalItemsCount={totalItemsCount} pageRangeDisplayed={5} onChange={setPageCallback} - hideFirstLastPages={totalItemsCount / maxPerPage < 20} + hideFirstLastPages={totalPages < 10} innerClass="pagination justify-content-center" itemClass="page-item" linkClass="page-link" 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`] = ` +
+