From a4557cfc4999adb1bdaf383c092b06f047177b4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Sat, 8 Sep 2018 20:06:51 +0100 Subject: [PATCH] feat(ui): add alert menu with actions specific to that alert --- .../AlertGrid/AlertGroup/Alert/AlertMenu.js | 134 ++++++++++++++++++ .../AlertGroup/Alert/AlertMenu.test.js | 77 ++++++++++ .../Alert/__snapshots__/index.test.js.snap | 17 ++- .../Grid/AlertGrid/AlertGroup/Alert/index.js | 19 ++- .../Grid/AlertGrid/AlertGroup/index.js | 2 + 5 files changed, 241 insertions(+), 8 deletions(-) create mode 100644 ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.js create mode 100644 ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.test.js diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.js new file mode 100644 index 000000000..54338edc4 --- /dev/null +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.js @@ -0,0 +1,134 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; + +import { action, observable } from "mobx"; +import { observer } from "mobx-react"; + +import { Manager, Reference, Popper } from "react-popper"; +import onClickOutside from "react-onclickoutside"; + +import Moment from "react-moment"; + +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faCaretDown } from "@fortawesome/free-solid-svg-icons/faCaretDown"; +import { faBellSlash } from "@fortawesome/free-solid-svg-icons/faBellSlash"; + +const onSilenceClick = (silenceFormStore, group, alert) => { + silenceFormStore.data.resetProgress(); + silenceFormStore.data.fillMatchersFromGroup(group, [alert]); + silenceFormStore.toggle.show(); +}; + +const MenuContent = onClickOutside( + ({ + popperPlacement, + popperRef, + popperStyle, + group, + alert, + afterClick, + silenceFormStore + }) => { + return ( +
+
onSilenceClick(silenceFormStore, group, alert)} + > + Silence this alert +
+
+ ); + } +); +MenuContent.propTypes = { + popperPlacement: PropTypes.string, + popperRef: PropTypes.func, + popperStyle: PropTypes.object, + group: PropTypes.object.isRequired, + alert: PropTypes.object.isRequired, + afterClick: PropTypes.func.isRequired +}; + +const AlertMenu = observer( + class AlertMenu extends Component { + static propTypes = { + group: PropTypes.object.isRequired, + alert: PropTypes.object.isRequired, + silenceFormStore: PropTypes.object.isRequired + }; + + collapse = observable( + { + value: true, + toggle() { + this.value = !this.value; + }, + hide() { + this.value = true; + } + }, + { toggle: action.bound, hide: action.bound }, + { name: "Alert menu toggle" } + ); + + handleClickOutside = action(event => { + this.collapse.hide(); + }); + + render() { + const { group, alert, silenceFormStore } = this.props; + + return ( + + + {({ ref }) => ( + + + {alert.startsAt} + + )} + + {this.collapse.value ? null : ( + + {({ placement, ref, style }) => ( + + )} + + )} + + ); + } + } +); + +export { AlertMenu, MenuContent }; diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.test.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.test.js new file mode 100644 index 000000000..fe1d72076 --- /dev/null +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.test.js @@ -0,0 +1,77 @@ +import React from "react"; + +import { mount } from "enzyme"; + +import { MockAlertGroup, MockAlert } from "__mocks__/Alerts.js"; +import { SilenceFormStore } from "Stores/SilenceFormStore"; +import { AlertMenu, MenuContent } from "./AlertMenu"; + +let silenceFormStore; +let alert; +let group; + +beforeEach(() => { + silenceFormStore = new SilenceFormStore(); + alert = MockAlert([], { foo: "bar" }, "active"); + group = MockAlertGroup({ alertname: "Fake Alert" }, [alert], [], {}); +}); + +const MockAfterClick = jest.fn(); + +const MountedAlertMenu = group => { + return mount( + + ); +}; + +describe("", () => { + it("is collapsed by default", () => { + const tree = MountedAlertMenu(group); + expect(tree.instance().collapse.value).toBe(true); + }); + + it("clicking toggle sets collapse value to 'false'", () => { + const tree = MountedAlertMenu(group); + const toggle = tree.find(".cursor-pointer"); + toggle.simulate("click"); + expect(tree.instance().collapse.value).toBe(false); + }); + + it("handleClickOutside() call sets collapse value to 'true'", () => { + const tree = MountedAlertMenu(group); + const toggle = tree.find(".cursor-pointer"); + toggle.simulate("click"); + expect(tree.instance().collapse.value).toBe(false); + + tree.instance().handleClickOutside(); + + expect(tree.instance().collapse.value).toBe(true); + }); +}); + +const MountedMenuContent = group => { + return mount( + + ); +}; + +describe("", () => { + it("clicking on 'Silence' icon opens the silence form modal", () => { + const tree = MountedMenuContent(group); + const button = tree.find(".dropdown-item").at(0); + button.simulate("click"); + expect(silenceFormStore.toggle.visible).toBe(true); + }); +}); diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/__snapshots__/index.test.js.snap b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/__snapshots__/index.test.js.snap index 7b6ce47fc..cb879ad25 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/__snapshots__/index.test.js.snap +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/__snapshots__/index.test.js.snap @@ -42,7 +42,22 @@ exports[` matches snapshot with showAlertmanagers=false showReceiver=fa hidden - + + + + + diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.js index dd141e368..196863bc9 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.js @@ -3,31 +3,34 @@ import PropTypes from "prop-types"; import { observer } from "mobx-react"; -import Moment from "react-moment"; - import { GetLabelColorClass } from "Common/Colors"; import { StaticLabels } from "Common/Query"; import { FilteringLabel } from "Components/Labels/FilteringLabel"; import { RenderNonLinkAnnotation, RenderLinkAnnotation } from "../Annotation"; import { Silence } from "../Silence"; +import { AlertMenu } from "./AlertMenu"; import "./index.css"; const Alert = observer( class Alert extends Component { static propTypes = { + group: PropTypes.object.isRequired, alert: PropTypes.object.isRequired, showAlertmanagers: PropTypes.bool.isRequired, showReceiver: PropTypes.bool.isRequired, - afterUpdate: PropTypes.func.isRequired + afterUpdate: PropTypes.func.isRequired, + silenceFormStore: PropTypes.object.isRequired }; render() { const { + group, alert, showAlertmanagers, showReceiver, - afterUpdate + afterUpdate, + silenceFormStore } = this.props; let classNames = [ @@ -55,9 +58,11 @@ const Alert = observer( /> ))} - - {alert.startsAt} - + {Object.entries(alert.labels).map(([name, value]) => ( ))} diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.js index a85aed849..99e334b9f 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.js @@ -181,12 +181,14 @@ const AlertGroup = observer( .map(alert => ( ))} {group.alerts.length > this.defaultRenderCount ? (