mirror of
https://github.com/prymitive/karma
synced 2026-05-07 03:26:52 +00:00
Merge pull request #1477 from prymitive/pagination
feat(ui): add a pagination component with keyboard shortcuts
This commit is contained in:
@@ -50,6 +50,10 @@ exports[`<LabelSetList /> matches snapshot with populated list 1`] = `
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div tabindex=\\"-1\\"
|
||||
class=\\"components-pagination\\"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -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>
|
||||
|
||||
90
ui/src/Components/Pagination/index.js
Normal file
90
ui/src/Components/Pagination/index.js
Normal 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 };
|
||||
51
ui/src/Components/Pagination/index.test.js
Normal file
51
ui/src/Components/Pagination/index.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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\\">
|
||||
|
||||
3
ui/src/Styles/Components/Pagination.scss
Normal file
3
ui/src/Styles/Components/Pagination.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
.components-pagination:focus {
|
||||
outline: none;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
6
ui/src/__mocks__/KeyPress.js
Normal file
6
ui/src/__mocks__/KeyPress.js
Normal 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 };
|
||||
Reference in New Issue
Block a user