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 ? (