mirror of
https://github.com/prymitive/karma
synced 2026-05-19 04:26:41 +00:00
feat(ui): add alert menu with actions specific to that alert
This commit is contained in:
134
ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.js
Normal file
134
ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.js
Normal file
@@ -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 (
|
||||
<div
|
||||
className="dropdown-menu d-block"
|
||||
ref={popperRef}
|
||||
style={popperStyle}
|
||||
data-placement={popperPlacement}
|
||||
>
|
||||
<div
|
||||
className="dropdown-item cursor-pointer"
|
||||
onClick={() => onSilenceClick(silenceFormStore, group, alert)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faBellSlash} /> Silence this alert
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
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 (
|
||||
<Manager>
|
||||
<Reference>
|
||||
{({ ref }) => (
|
||||
<span
|
||||
className="components-label-with-hover text-nowrap text-truncate px-1 mr-1 badge badge-secondary cursor-pointer"
|
||||
ref={ref}
|
||||
onClick={this.collapse.toggle}
|
||||
data-toggle="dropdown"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className="pr-1"
|
||||
style={{ width: "0.8rem" }}
|
||||
icon={faCaretDown}
|
||||
/>
|
||||
<Moment fromNow>{alert.startsAt}</Moment>
|
||||
</span>
|
||||
)}
|
||||
</Reference>
|
||||
{this.collapse.value ? null : (
|
||||
<Popper
|
||||
placement="bottom-start"
|
||||
modifiers={{
|
||||
arrow: { enabled: false },
|
||||
offset: { offset: "-5px, 0px" }
|
||||
}}
|
||||
>
|
||||
{({ placement, ref, style }) => (
|
||||
<MenuContent
|
||||
popperPlacement={placement}
|
||||
popperRef={ref}
|
||||
popperStyle={style}
|
||||
group={group}
|
||||
alert={alert}
|
||||
silenceFormStore={silenceFormStore}
|
||||
afterClick={this.collapse.hide}
|
||||
handleClickOutside={this.collapse.hide}
|
||||
/>
|
||||
)}
|
||||
</Popper>
|
||||
)}
|
||||
</Manager>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export { AlertMenu, MenuContent };
|
||||
@@ -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(
|
||||
<AlertMenu
|
||||
group={group}
|
||||
alert={alert}
|
||||
silenceFormStore={silenceFormStore}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
describe("<AlertMenu />", () => {
|
||||
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(
|
||||
<MenuContent
|
||||
popperPlacement="top"
|
||||
popperRef={null}
|
||||
popperStyle={{}}
|
||||
group={group}
|
||||
alert={alert}
|
||||
afterClick={MockAfterClick}
|
||||
silenceFormStore={silenceFormStore}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
describe("<MenuContent />", () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -42,7 +42,22 @@ exports[`<Alert /> matches snapshot with showAlertmanagers=false showReceiver=fa
|
||||
hidden
|
||||
</div>
|
||||
</div>
|
||||
<span class=\\"text-nowrap text-truncate px-1 mr-1 badge badge-secondary\\">
|
||||
<span class=\\"components-label-with-hover text-nowrap text-truncate px-1 mr-1 badge badge-secondary cursor-pointer\\"
|
||||
data-toggle=\\"dropdown\\"
|
||||
>
|
||||
<svg aria-hidden=\\"true\\"
|
||||
data-prefix=\\"fas\\"
|
||||
data-icon=\\"caret-down\\"
|
||||
class=\\"svg-inline--fa fa-caret-down fa-w-10 pr-1\\"
|
||||
role=\\"img\\"
|
||||
xmlns=\\"http://www.w3.org/2000/svg\\"
|
||||
viewbox=\\"0 0 320 512\\"
|
||||
>
|
||||
<path fill=\\"currentColor\\"
|
||||
d=\\"M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z\\"
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
<time datetime=\\"1534268200017\\">
|
||||
a day ago
|
||||
</time>
|
||||
|
||||
@@ -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(
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-nowrap text-truncate px-1 mr-1 badge badge-secondary">
|
||||
<Moment fromNow>{alert.startsAt}</Moment>
|
||||
</span>
|
||||
<AlertMenu
|
||||
group={group}
|
||||
alert={alert}
|
||||
silenceFormStore={silenceFormStore}
|
||||
/>
|
||||
{Object.entries(alert.labels).map(([name, value]) => (
|
||||
<FilteringLabel key={name} name={name} value={value} />
|
||||
))}
|
||||
|
||||
@@ -181,12 +181,14 @@ const AlertGroup = observer(
|
||||
.map(alert => (
|
||||
<Alert
|
||||
key={hash(alert.labels)}
|
||||
group={group}
|
||||
alert={alert}
|
||||
showAlertmanagers={
|
||||
showAlertmanagers && !showAlertmanagersInFooter
|
||||
}
|
||||
showReceiver={group.alerts.length === 1}
|
||||
afterUpdate={afterUpdate}
|
||||
silenceFormStore={silenceFormStore}
|
||||
/>
|
||||
))}
|
||||
{group.alerts.length > this.defaultRenderCount ? (
|
||||
|
||||
Reference in New Issue
Block a user