diff --git a/ui/src/Components/FetchPauser/index.js b/ui/src/Components/FetchPauser/index.js new file mode 100644 index 000000000..78be3973a --- /dev/null +++ b/ui/src/Components/FetchPauser/index.js @@ -0,0 +1,29 @@ +import { Component } from "react"; +import PropTypes from "prop-types"; + +import { inject } from "mobx-react"; + +const FetchPauser = inject("alertStore")( + class FetchPauser extends Component { + static propTypes = { + children: PropTypes.any, + alertStore: PropTypes.object.isRequired + }; + + componentDidMount() { + const { alertStore } = this.props; + alertStore.status.pause(); + } + + componentWillUnmount() { + const { alertStore } = this.props; + alertStore.status.resume(); + } + + render() { + return this.props.children; + } + } +); + +export { FetchPauser }; diff --git a/ui/src/Components/FetchPauser/index.test.js b/ui/src/Components/FetchPauser/index.test.js new file mode 100644 index 000000000..bc9dfa634 --- /dev/null +++ b/ui/src/Components/FetchPauser/index.test.js @@ -0,0 +1,37 @@ +import React from "react"; + +import { Provider } from "mobx-react"; + +import { mount } from "enzyme"; + +import { AlertStore } from "Stores/AlertStore"; +import { FetchPauser } from "."; + +let alertStore; + +beforeEach(() => { + alertStore = new AlertStore([]); +}); + +const MountedFetchPauser = () => { + return mount( + + +
+ + + ); +}; + +describe("", () => { + it("mounting FetchPauser pauses alertStore", () => { + MountedFetchPauser(); + expect(alertStore.status.paused).toBe(true); + }); + + it("unmounting FetchPauser resumes alertStore", () => { + const tree = MountedFetchPauser(); + tree.unmount(); + expect(alertStore.status.paused).toBe(false); + }); +}); diff --git a/ui/src/Components/Fetcher/index.js b/ui/src/Components/Fetcher/index.js index 1361825a5..53e0e1896 100644 --- a/ui/src/Components/Fetcher/index.js +++ b/ui/src/Components/Fetcher/index.js @@ -43,7 +43,7 @@ const Fetcher = observer( status === AlertStoreStatuses.Fetching.toString() || status === AlertStoreStatuses.Processing.toString(); - if (pastDeadline && !updateInProgress) { + if (pastDeadline && !updateInProgress && !alertStore.status.paused) { this.lastTick.update(); alertStore.fetchWithThrottle(); } @@ -61,8 +61,10 @@ const Fetcher = observer( componentDidUpdate() { const { alertStore } = this.props; - this.lastTick.update(); - alertStore.fetchWithThrottle(); + if (!alertStore.status.paused) { + this.lastTick.update(); + alertStore.fetchWithThrottle(); + } } componentWillUnmount() { diff --git a/ui/src/Components/Fetcher/index.test.js b/ui/src/Components/Fetcher/index.test.js index 2f4679602..1f3610e5d 100644 --- a/ui/src/Components/Fetcher/index.test.js +++ b/ui/src/Components/Fetcher/index.test.js @@ -147,4 +147,34 @@ describe("", () => { instance.componentWillUnmount(); expect(instance.timer).toBeNull(); }); + + it("doesn't fetch on mount when paused", () => { + alertStore.status.pause(); + MountedFetcher(); + expect(fetchSpy).toHaveBeenCalledTimes(0); + }); + + it("doesn't fetch on update when paused", () => { + alertStore.status.pause(); + const tree = MountedFetcher(); + tree.instance().componentDidUpdate(); + expect(fetchSpy).toHaveBeenCalledTimes(0); + }); + + it("fetches on update when resumed", () => { + alertStore.status.pause(); + const tree = MountedFetcher(); + alertStore.status.resume(); + tree.instance().componentDidUpdate(); + expect(fetchSpy).toHaveBeenCalledTimes(1); + }); + + it("fetches on resume", () => { + alertStore.status.pause(); + MountedFetcher(); + alertStore.status.resume(); + advanceBy(2 * 1000); + jest.runOnlyPendingTimers(); + expect(fetchSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.js index 7ac4e5446..4e521bc85 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.js @@ -15,6 +15,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCaretDown } from "@fortawesome/free-solid-svg-icons/faCaretDown"; import { faBellSlash } from "@fortawesome/free-solid-svg-icons/faBellSlash"; +import { FetchPauser } from "Components/FetchPauser"; + const onSilenceClick = (silenceFormStore, group, alert) => { silenceFormStore.data.resetProgress(); silenceFormStore.data.fillMatchersFromGroup(group, [alert]); @@ -32,33 +34,35 @@ const MenuContent = onClickOutside( silenceFormStore }) => { return ( -
-
Alert source links:
- {alert.alertmanager.map(am => ( - - {am.name} - - ))} -
+
onSilenceClick(silenceFormStore, group, alert)} + className="dropdown-menu d-block" + ref={popperRef} + style={popperStyle} + data-placement={popperPlacement} > - Silence this alert +
Alert source links:
+ {alert.alertmanager.map(am => ( + + {am.name} + + ))} +
+
onSilenceClick(silenceFormStore, group, alert)} + > + Silence this alert +
-
+
); } ); diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.test.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.test.js index d8bca5f3d..ecc328666 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.test.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.test.js @@ -1,16 +1,21 @@ import React from "react"; +import { Provider } from "mobx-react"; + import { mount } from "enzyme"; import { MockAlertGroup, MockAlert } from "__mocks__/Alerts.js"; +import { AlertStore } from "Stores/AlertStore"; import { SilenceFormStore } from "Stores/SilenceFormStore"; import { AlertMenu, MenuContent } from "./AlertMenu"; +let alertStore; let silenceFormStore; let alert; let group; beforeEach(() => { + alertStore = new AlertStore([]); silenceFormStore = new SilenceFormStore(); alert = MockAlert([], { foo: "bar" }, "active"); group = MockAlertGroup({ alertname: "Fake Alert" }, [alert], [], {}); @@ -20,12 +25,14 @@ const MockAfterClick = jest.fn(); const MountedAlertMenu = group => { return mount( - - ); + + + + ).find("AlertMenu"); }; describe("", () => { @@ -55,15 +62,17 @@ describe("", () => { const MountedMenuContent = group => { return mount( - + + + ); }; diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.js index b337576f8..81244998c 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.js @@ -16,6 +16,7 @@ import { faBellSlash } from "@fortawesome/free-solid-svg-icons/faBellSlash"; import { FormatAPIFilterQuery } from "Stores/AlertStore"; import { QueryOperators, StaticLabels, FormatQuery } from "Common/Query"; +import { FetchPauser } from "Components/FetchPauser"; const onSilenceClick = (silenceFormStore, group) => { silenceFormStore.data.resetProgress(); @@ -43,28 +44,30 @@ const MenuContent = onClickOutside( )}`; return ( -
+
{ - copy(groupLink); - afterClick(); - }} + className="dropdown-menu d-block" + ref={popperRef} + style={popperStyle} + data-placement={popperPlacement} > - Copy link to this group +
{ + copy(groupLink); + afterClick(); + }} + > + Copy link to this group +
+
onSilenceClick(silenceFormStore, group)} + > + Silence this group +
-
onSilenceClick(silenceFormStore, group)} - > - Silence this group -
-
+ ); } ); diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.test.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.test.js index eb8c3a7ff..80b20234c 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.test.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.test.js @@ -1,23 +1,32 @@ import React from "react"; +import { Provider } from "mobx-react"; + import { mount } from "enzyme"; import copy from "copy-to-clipboard"; import { MockAlertGroup } from "__mocks__/Alerts.js"; +import { AlertStore } from "Stores/AlertStore"; import { SilenceFormStore } from "Stores/SilenceFormStore"; import { GroupMenu, MenuContent } from "./GroupMenu"; +let alertStore; let silenceFormStore; beforeEach(() => { + alertStore = new AlertStore([]); silenceFormStore = new SilenceFormStore(); }); const MockAfterClick = jest.fn(); const MountedGroupMenu = group => { - return mount(); + return mount( + + + + ).find("GroupMenu"); }; describe("", () => { @@ -51,14 +60,16 @@ describe("", () => { const MountedMenuContent = group => { return mount( - + + + ); }; diff --git a/ui/src/Components/NavBar/FetchIndicator/__snapshots__/index.test.js.snap b/ui/src/Components/NavBar/FetchIndicator/__snapshots__/index.test.js.snap index 19ea854d1..6c380c8aa 100644 --- a/ui/src/Components/NavBar/FetchIndicator/__snapshots__/index.test.js.snap +++ b/ui/src/Components/NavBar/FetchIndicator/__snapshots__/index.test.js.snap @@ -24,7 +24,7 @@ exports[` matches snapshot when idle 1`] = ` matches snapshot when idle 1`] = ` " `; +exports[` matches snapshot when paused 1`] = ` +" + + + + +" +`; + exports[` matches snapshot when response is processed 1`] = ` " ( + +); +FetchIcon.propTypes = { + icon: PropTypes.object.isRequired, + color: PropTypes.string, + visible: PropTypes.bool, + spin: PropTypes.bool +}; +FetchIcon.defaultProps = { + color: "muted", + visible: true, + spin: false +}; - render() { - const { status } = this.props; +const FetchIndicator = observer( + class FetchIndicator extends Component { + static propTypes = { + alertStore: PropTypes.object.isRequired + }; - const visible = - status === AlertStoreStatuses.Fetching.toString() || - status === AlertStoreStatuses.Processing.toString(); - const textClass = - status === AlertStoreStatuses.Fetching.toString() - ? "text-muted" - : "text-success"; + render() { + const { alertStore } = this.props; - return ( - - ); + if (alertStore.status.paused) return ; + + const status = alertStore.status.value.toString(); + + if (status === AlertStoreStatuses.Fetching.toString()) + return ; + + if (status === AlertStoreStatuses.Processing.toString()) + return ; + + return ; + } } -} +); export { FetchIndicator }; diff --git a/ui/src/Components/NavBar/FetchIndicator/index.test.js b/ui/src/Components/NavBar/FetchIndicator/index.test.js index 42af72286..f58156fa0 100644 --- a/ui/src/Components/NavBar/FetchIndicator/index.test.js +++ b/ui/src/Components/NavBar/FetchIndicator/index.test.js @@ -5,69 +5,88 @@ import { mount } from "enzyme"; import toDiffableHtml from "diffable-html"; import { FetchIndicator } from "."; -import { AlertStoreStatuses } from "Stores/AlertStore"; +import { AlertStore } from "Stores/AlertStore"; + +let alertStore; + +beforeEach(() => { + alertStore = new AlertStore([]); +}); + +const MountedFetchIndicator = () => { + return mount(); +}; describe("", () => { + it("shows a pause icon when fetching is paused", () => { + alertStore.status.pause(); + const tree = MountedFetchIndicator(); + expect(tree.html()).toMatch(/fa-pause-circle/); + }); + + it("shows a cirle notch icon when fetching is resumed", () => { + alertStore.status.resume(); + const tree = MountedFetchIndicator(); + expect(tree.html()).toMatch(/fa-circle-notch/); + }); + it("opacity is 1 when fetch is in progress", () => { - const tree = mount( - - ); + alertStore.status.setFetching(); + const tree = MountedFetchIndicator(); expect(tree.find("FontAwesomeIcon").props().style.opacity).toEqual(1); }); it("uses text-muted when fetch is in progress", () => { - const tree = mount( - - ); + alertStore.status.setFetching(); + const tree = MountedFetchIndicator(); expect(tree.find("FontAwesomeIcon").hasClass("text-muted")).toBe(true); }); it("opacity is 1 when response is processed", () => { - const tree = mount( - - ); + alertStore.status.setProcessing(); + const tree = MountedFetchIndicator(); expect(tree.find("FontAwesomeIcon").props().style.opacity).toEqual(1); }); it("uses text-success when response is processed", () => { - const tree = mount( - - ); + alertStore.status.setProcessing(); + const tree = MountedFetchIndicator(); expect(tree.find("FontAwesomeIcon").hasClass("text-success")).toBe(true); }); it("opacity is 0 when idle", () => { - const tree = mount( - - ); + alertStore.status.setIdle(); + const tree = MountedFetchIndicator(); expect(tree.find("FontAwesomeIcon").props().style.opacity).toEqual(0); }); it("opacity is 0 when fetch failed", () => { - const tree = mount( - - ); + alertStore.status.setFailure(); + const tree = MountedFetchIndicator(); expect(tree.find("FontAwesomeIcon").props().style.opacity).toEqual(0); }); + it("matches snapshot when paused", () => { + alertStore.status.pause(); + const tree = MountedFetchIndicator(); + expect(toDiffableHtml(tree.html())).toMatchSnapshot(); + }); + it("matches snapshot when fetch is in progress", () => { - const tree = mount( - - ); + alertStore.status.setFetching(); + const tree = MountedFetchIndicator(); expect(toDiffableHtml(tree.html())).toMatchSnapshot(); }); it("matches snapshot when response is processed", () => { - const tree = mount( - - ); + alertStore.status.setProcessing(); + const tree = MountedFetchIndicator(); expect(toDiffableHtml(tree.html())).toMatchSnapshot(); }); it("matches snapshot when idle", () => { - const tree = mount( - - ); + alertStore.status.setIdle(); + const tree = MountedFetchIndicator(); expect(toDiffableHtml(tree.html())).toMatchSnapshot(); }); }); diff --git a/ui/src/Components/NavBar/index.js b/ui/src/Components/NavBar/index.js index f9be2e86a..4fccf366a 100644 --- a/ui/src/Components/NavBar/index.js +++ b/ui/src/Components/NavBar/index.js @@ -72,7 +72,7 @@ const NavBar = observer( {alertStore.info.totalAlerts} - +