feat(ui): deduplicate silences

If all alerts in a group are silenced and the same silence ID is used for all of them then there's no point in rendering silence object for each of the alerts, since they are all identical. Move the silence rendering to the footer if that happens so we save screen space
This commit is contained in:
Łukasz Mierzwa
2019-03-08 17:41:22 +00:00
parent 6ebc88b0ba
commit 41cca9e501
16 changed files with 319 additions and 32 deletions

View File

@@ -18,7 +18,7 @@ beforeEach(() => {
alertStore = new AlertStore([]);
silenceFormStore = new SilenceFormStore();
alert = MockAlert([], { foo: "bar" }, "active");
group = MockAlertGroup({ alertname: "Fake Alert" }, [alert], [], {});
group = MockAlertGroup({ alertname: "Fake Alert" }, [alert], [], {}, {});
});
const MockAfterClick = jest.fn();

View File

@@ -58,7 +58,10 @@ const Alert = observer(
};
}
for (let silenceID of am.silencedBy) {
if (!silences[am.cluster].silences.includes(silenceID)) {
if (
!silences[am.cluster].silences.includes(silenceID) &&
!(group.shared.silences[am.cluster] === silenceID)
) {
silences[am.cluster].silences.push(silenceID);
}
}

View File

@@ -61,7 +61,7 @@ const MountedAlert = (alert, group, showAlertmanagers, showReceiver) => {
describe("<Alert />", () => {
it("matches snapshot with showAlertmanagers=false showReceiver=false", () => {
const alert = MockedAlert();
const group = MockAlertGroup({}, [alert], [], {});
const group = MockAlertGroup({}, [alert], [], {}, {});
const tree = MountedAlert(alert, group, false, false);
expect(toDiffableHtml(tree.html())).toMatchSnapshot();
});
@@ -69,7 +69,7 @@ describe("<Alert />", () => {
it("matches snapshot when inhibited", () => {
const alert = MockedAlert();
alert.alertmanager[0].inhibitedBy = ["123456"];
const group = MockAlertGroup({}, [alert], [], {});
const group = MockAlertGroup({}, [alert], [], {}, {});
const tree = MountedAlert(alert, group, false, false);
expect(toDiffableHtml(tree.html())).toMatchSnapshot();
});
@@ -77,14 +77,14 @@ describe("<Alert />", () => {
it("renders inhibition icon when inhibited", () => {
const alert = MockedAlert();
alert.alertmanager[0].inhibitedBy = ["123456"];
const group = MockAlertGroup({}, [alert], [], {});
const group = MockAlertGroup({}, [alert], [], {}, {});
const tree = MountedAlert(alert, group, false, false);
expect(tree.find(".fa-volume-mute")).toHaveLength(1);
});
it("renders @alertmanager label with showAlertmanagers=true", () => {
const alert = MockedAlert();
const group = MockAlertGroup({}, [alert], [], {});
const group = MockAlertGroup({}, [alert], [], {}, {});
const tree = MountedAlert(alert, group, true, false);
const label = tree
.find("FilteringLabel")
@@ -94,7 +94,7 @@ describe("<Alert />", () => {
it("renders @receiver label with showReceiver=true", () => {
const alert = MockedAlert();
const group = MockAlertGroup({}, [alert], [], {});
const group = MockAlertGroup({}, [alert], [], {}, {});
const tree = MountedAlert(alert, group, false, true);
const label = tree
.find("FilteringLabel")
@@ -105,7 +105,7 @@ describe("<Alert />", () => {
it("renders a silence if alert is silenced", () => {
const alert = MockedAlert();
alert.alertmanager[0].silencedBy = ["silence123456789"];
const group = MockAlertGroup({}, [alert], [], {});
const group = MockAlertGroup({}, [alert], [], {}, {});
const tree = MountedAlert(alert, group, false, false);
const silence = tree.find("Silence");
expect(silence).toHaveLength(1);
@@ -136,7 +136,7 @@ describe("<Alert />", () => {
inhibitedBy: []
}
];
const group = MockAlertGroup({}, [alert], [], {});
const group = MockAlertGroup({}, [alert], [], {}, {});
const tree = MountedAlert(alert, group, false, false);
const silence = tree.find("Silence");
expect(silence).toHaveLength(1);
@@ -146,7 +146,7 @@ describe("<Alert />", () => {
it("uses BorderClassMap.active when @state=active", () => {
const alert = MockedAlert();
alert.state = "active";
const group = MockAlertGroup({}, [alert], [], {});
const group = MockAlertGroup({}, [alert], [], {}, {});
const tree = MountedAlert(alert, group, false, false);
expect(
tree
@@ -158,7 +158,7 @@ describe("<Alert />", () => {
it("uses BorderClassMap.suppressed when @state=suppressed", () => {
const alert = MockedAlert();
alert.state = "suppressed";
const group = MockAlertGroup({}, [alert], [], {});
const group = MockAlertGroup({}, [alert], [], {}, {});
const tree = MountedAlert(alert, group, false, false);
expect(
tree
@@ -170,7 +170,7 @@ describe("<Alert />", () => {
it("uses BorderClassMap.unprocessed when @state=unprocessed", () => {
const alert = MockedAlert();
alert.state = "unprocessed";
const group = MockAlertGroup({}, [alert], [], {});
const group = MockAlertGroup({}, [alert], [], {}, {});
const tree = MountedAlert(alert, group, false, false);
expect(
tree
@@ -184,7 +184,7 @@ describe("<Alert />", () => {
const alert = MockedAlert();
alert.state = "foobar";
const group = MockAlertGroup({}, [alert], [], {});
const group = MockAlertGroup({}, [alert], [], {}, {});
const tree = MountedAlert(alert, group, false, false);
expect(
tree

View File

@@ -128,3 +128,184 @@ exports[`<GroupFooter /> matches snapshot 1`] = `
</div>
"
`;
exports[`<GroupFooter /> mathes snapshot when silence is rendered 1`] = `
"
<div class=\\"card-footer px-2 py-1\\">
<div class=\\"mb-1\\">
<div class=\\"mr-1 mb-1 p-1 bg-light cursor-pointer d-inline-block rounded components-grid-annotation text-break\\">
<svg aria-hidden=\\"true\\"
focusable=\\"false\\"
data-prefix=\\"fas\\"
data-icon=\\"search-minus\\"
class=\\"svg-inline--fa fa-search-minus fa-w-16 mr-1\\"
role=\\"img\\"
xmlns=\\"http://www.w3.org/2000/svg\\"
viewbox=\\"0 0 512 512\\"
>
<path fill=\\"currentColor\\"
d=\\"M304 192v32c0 6.6-5.4 12-12 12H124c-6.6 0-12-5.4-12-12v-32c0-6.6 5.4-12 12-12h168c6.6 0 12 5.4 12 12zm201 284.7L476.7 505c-9.4 9.4-24.6 9.4-33.9 0L343 405.3c-4.5-4.5-7-10.6-7-17V372c-35.3 27.6-79.7 44-128 44C93.1 416 0 322.9 0 208S93.1 0 208 0s208 93.1 208 208c0 48.3-16.4 92.7-44 128h16.3c6.4 0 12.5 2.5 17 7l99.7 99.7c9.3 9.4 9.3 24.6 0 34zM344 208c0-75.2-60.8-136-136-136S72 132.8 72 208s60.8 136 136 136 136-60.8 136-136z\\"
>
</path>
</svg>
<span class=\\"text-muted\\">
summary:
</span>
<span class=\\"Linkify\\">
This is summary
</span>
</div>
<div class=\\"mr-1 mb-1 p-1 bg-light cursor-pointer d-inline-block rounded components-grid-annotation text-break\\">
<svg aria-hidden=\\"true\\"
focusable=\\"false\\"
data-prefix=\\"fas\\"
data-icon=\\"search-plus\\"
class=\\"svg-inline--fa fa-search-plus fa-w-16 mr-1\\"
role=\\"img\\"
xmlns=\\"http://www.w3.org/2000/svg\\"
viewbox=\\"0 0 512 512\\"
>
<path fill=\\"currentColor\\"
d=\\"M304 192v32c0 6.6-5.4 12-12 12h-56v56c0 6.6-5.4 12-12 12h-32c-6.6 0-12-5.4-12-12v-56h-56c-6.6 0-12-5.4-12-12v-32c0-6.6 5.4-12 12-12h56v-56c0-6.6 5.4-12 12-12h32c6.6 0 12 5.4 12 12v56h56c6.6 0 12 5.4 12 12zm201 284.7L476.7 505c-9.4 9.4-24.6 9.4-33.9 0L343 405.3c-4.5-4.5-7-10.6-7-17V372c-35.3 27.6-79.7 44-128 44C93.1 416 0 322.9 0 208S93.1 0 208 0s208 93.1 208 208c0 48.3-16.4 92.7-44 128h16.3c6.4 0 12.5 2.5 17 7l99.7 99.7c9.3 9.4 9.3 24.6 0 34zM344 208c0-75.2-60.8-136-136-136S72 132.8 72 208s60.8 136 136 136 136-60.8 136-136z\\"
>
</path>
</svg>
hidden
</div>
</div>
<div class
style=\\"display: inline;\\"
data-tooltipped
aria-describedby=\\"tippy-tooltip-9\\"
data-original-title=\\"Click to only show alerts with this label or Alt+Click to hide them\\"
>
<span class=\\"components-label badge text-nowrap text-truncate mw-100 badge-warning components-label-dark components-label-with-hover\\">
<span class=\\"components-label-name\\">
label1:
</span>
<span class=\\"components-label-value\\">
foo
</span>
</span>
</div>
<div class
style=\\"display: inline;\\"
data-tooltipped
aria-describedby=\\"tippy-tooltip-10\\"
data-original-title=\\"Click to only show alerts with this label or Alt+Click to hide them\\"
>
<span class=\\"components-label badge text-nowrap text-truncate mw-100 badge-warning components-label-dark components-label-with-hover\\">
<span class=\\"components-label-name\\">
label2:
</span>
<span class=\\"components-label-value\\">
bar
</span>
</span>
</div>
<div class
style=\\"display: inline;\\"
data-tooltipped
aria-describedby=\\"tippy-tooltip-11\\"
data-original-title=\\"Click to only show alerts with this label or Alt+Click to hide them\\"
>
<span class=\\"components-label badge text-nowrap text-truncate mw-100 badge-warning components-label-dark components-label-with-hover\\">
<span class=\\"components-label-name\\">
@alertmanager:
</span>
<span class=\\"components-label-value\\">
default
</span>
</span>
</div>
<div class
style=\\"display: inline;\\"
data-tooltipped
aria-describedby=\\"tippy-tooltip-12\\"
data-original-title=\\"Click to only show alerts with this label or Alt+Click to hide them\\"
>
<span class=\\"components-label badge text-nowrap text-truncate mw-100 badge-warning components-label-dark components-label-with-hover\\">
<span class=\\"components-label-name\\">
@receiver:
</span>
<span class=\\"components-label-value\\">
by-name
</span>
</span>
</div>
<a href=\\"http://link.example.com\\"
target=\\"_blank\\"
rel=\\"noopener noreferrer\\"
class=\\"components-label-with-hover text-nowrap text-truncate badge mr-1 components-grid-annotation-link\\"
>
<svg aria-hidden=\\"true\\"
focusable=\\"false\\"
data-prefix=\\"fas\\"
data-icon=\\"external-link-alt\\"
class=\\"svg-inline--fa fa-external-link-alt fa-w-18 \\"
role=\\"img\\"
xmlns=\\"http://www.w3.org/2000/svg\\"
viewbox=\\"0 0 576 512\\"
>
<path fill=\\"currentColor\\"
d=\\"M576 24v127.984c0 21.461-25.96 31.98-40.971 16.971l-35.707-35.709-243.523 243.523c-9.373 9.373-24.568 9.373-33.941 0l-22.627-22.627c-9.373-9.373-9.373-24.569 0-33.941L442.756 76.676l-35.703-35.705C391.982 25.9 402.656 0 424.024 0H552c13.255 0 24 10.745 24 24zM407.029 270.794l-16 16A23.999 23.999 0 0 0 384 303.765V448H64V128h264a24.003 24.003 0 0 0 16.97-7.029l16-16C376.089 89.851 365.381 64 344 64H48C21.49 64 0 85.49 0 112v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V287.764c0-21.382-25.852-32.09-40.971-16.97z\\"
>
</path>
</svg>
link
</a>
<div class=\\"components-grid-alertgrid-alertgroup-shared-silence rounded-0 border-left-1 border-right-0 border-top-0 border-bottom-0 border-success \\">
<div class=\\"card mt-1 border-0 p-1\\">
<div class=\\"card-text mb-0\\">
<span class=\\"text-muted my-1\\">
<span width=\\"0\\">
<span>
</span>
<span>
Mocked Silence
</span>
<span style=\\"position: fixed; visibility: hidden; top: 0px; left: 0px;\\">
</span>
</span>
<span class=\\"blockquote-footer pt-1\\">
<span class=\\"float-right cursor-pointer\\">
<div class
style=\\"display: inline;\\"
data-tooltipped
aria-describedby=\\"tippy-tooltip-13\\"
data-original-title=\\"Toggle silence details\\"
>
<svg aria-hidden=\\"true\\"
focusable=\\"false\\"
data-prefix=\\"fas\\"
data-icon=\\"chevron-up\\"
class=\\"svg-inline--fa fa-chevron-up fa-w-14 \\"
role=\\"img\\"
xmlns=\\"http://www.w3.org/2000/svg\\"
viewbox=\\"0 0 448 512\\"
>
<path fill=\\"currentColor\\"
d=\\"M240.971 130.524l194.343 194.343c9.373 9.373 9.373 24.569 0 33.941l-22.667 22.667c-9.357 9.357-24.522 9.375-33.901.04L224 227.495 69.255 381.516c-9.379 9.335-24.544 9.317-33.901-.04l-22.667-22.667c-9.373-9.373-9.373-24.569 0-33.941L207.03 130.525c9.372-9.373 24.568-9.373 33.941-.001z\\"
>
</path>
</svg>
</div>
</span>
<cite class=\\"components-grid-alertgroup-silences mr-2\\">
me@example.com
</cite>
<span class=\\"badge badge-danger text-nowrap text-truncate mw-100 align-bottom\\">
Expired
<time datetime=\\"946688400000\\">
14 hours ago
</time>
</span>
</span>
</span>
</div>
</div>
</div>
</div>
"
`;

View File

@@ -0,0 +1,8 @@
.components-grid-alertgrid-alertgroup-shared-silence {
border-width: 3px;
border-style: solid;
}
.components-grid-alertgrid-alertgroup-shared-silence > .card {
background-color: inherit;
}

View File

@@ -5,19 +5,29 @@ import { observer } from "mobx-react";
import { APIGroup } from "Models/API";
import { StaticLabels } from "Common/Query";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { FilteringLabel } from "Components/Labels/FilteringLabel";
import { RenderNonLinkAnnotation, RenderLinkAnnotation } from "../Annotation";
import { Silence } from "../Silence";
import "./index.css";
const GroupFooter = observer(
class GroupFooter extends Component {
static propTypes = {
group: APIGroup.isRequired,
alertmanagers: PropTypes.arrayOf(PropTypes.string).isRequired,
afterUpdate: PropTypes.func.isRequired
afterUpdate: PropTypes.func.isRequired,
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired
};
render() {
const { group, alertmanagers, afterUpdate } = this.props;
const {
group,
alertmanagers,
afterUpdate,
silenceFormStore
} = this.props;
return (
<div className="card-footer px-2 py-1">
@@ -54,6 +64,26 @@ const GroupFooter = observer(
value={a.value}
/>
))}
{Object.keys(group.shared.silences).length === 0 ? null : (
<div className="components-grid-alertgrid-alertgroup-shared-silence rounded-0 border-left-1 border-right-0 border-top-0 border-bottom-0 border-success ">
{Object.entries(group.shared.silences).map(
([cluster, silenceID]) => (
<Silence
key={silenceID}
silenceFormStore={silenceFormStore}
alertmanagerState={
group.alerts.map(
a =>
a.alertmanager.filter(am => am.cluster === cluster)[0]
)[0]
}
silenceID={silenceID}
afterUpdate={afterUpdate}
/>
)
)}
</div>
)}
</div>
);
}

View File

@@ -6,23 +6,38 @@ import { mount } from "enzyme";
import toDiffableHtml from "diffable-html";
import { MockAlertGroup, MockAnnotation } from "__mocks__/Alerts.js";
import moment from "moment";
import { advanceTo, clear } from "jest-date-mock";
import {
MockAlertGroup,
MockAnnotation,
MockAlert,
MockSilence
} from "__mocks__/Alerts.js";
import { AlertStore } from "Stores/AlertStore";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { GroupFooter } from ".";
let group;
let alertStore;
let silenceFormStore;
const MockGroup = () => {
const group = MockAlertGroup(
{ alertname: "Fake Alert" },
[],
[
MockAlert([], {}, "suppressed"),
MockAlert([], {}, "suppressed"),
MockAlert([], {}, "suppressed")
],
[
MockAnnotation("summary", "This is summary", true, false),
MockAnnotation("hidden", "This is hidden annotation", false, false),
MockAnnotation("link", "http://link.example.com", true, true)
],
{ label1: "foo", label2: "bar" }
{ label1: "foo", label2: "bar" },
{}
);
return group;
};
@@ -31,7 +46,15 @@ const MockAfterUpdate = jest.fn();
beforeEach(() => {
alertStore = new AlertStore([]);
silenceFormStore = new SilenceFormStore();
group = MockGroup();
advanceTo(moment.utc([2000, 0, 1, 15, 0, 0]));
});
afterEach(() => {
jest.restoreAllMocks();
// reset Date() to current time
clear();
});
const MountedGroupFooter = () => {
@@ -41,6 +64,7 @@ const MountedGroupFooter = () => {
group={group}
alertmanagers={["default"]}
afterUpdate={MockAfterUpdate}
silenceFormStore={silenceFormStore}
/>
</Provider>
);
@@ -51,4 +75,30 @@ describe("<GroupFooter />", () => {
const tree = MountedGroupFooter().find("GroupFooter");
expect(toDiffableHtml(tree.html())).toMatchSnapshot();
});
it("render deduplicated silence if present", () => {
for (const id of Object.keys(group.alerts)) {
group.alerts[id].alertmanager[0].silencedBy = ["123456789"];
}
group.shared.silences = { default: "123456789" };
const tree = MountedGroupFooter().find("GroupFooter");
expect(tree.find("Silence")).toHaveLength(1);
});
it("mathes snapshot when silence is rendered", () => {
for (const id of Object.keys(group.alerts)) {
group.alerts[id].alertmanager[0].silencedBy = ["123456789"];
}
group.shared.silences = { default: "123456789" };
alertStore.data.silences = {
default: {
"123456789": MockSilence()
}
};
alertStore.data.silences["default"]["123456789"].id = "123456789";
const tree = MountedGroupFooter().find("GroupFooter");
expect(toDiffableHtml(tree.html())).toMatchSnapshot();
});
});

View File

@@ -31,13 +31,13 @@ const MountedGroupMenu = group => {
describe("<GroupMenu />", () => {
it("is collapsed by default", () => {
const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {});
const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {}, {});
const tree = MountedGroupMenu(group);
expect(tree.instance().collapse.value).toBe(true);
});
it("clicking toggle sets collapse value to 'false'", () => {
const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {});
const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {}, {});
const tree = MountedGroupMenu(group);
const toggle = tree.find(".cursor-pointer");
toggle.simulate("click");
@@ -45,7 +45,7 @@ describe("<GroupMenu />", () => {
});
it("handleClickOutside() call sets collapse value to 'true'", () => {
const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {});
const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {}, {});
const tree = MountedGroupMenu(group);
const toggle = tree.find(".cursor-pointer");
@@ -75,7 +75,7 @@ const MountedMenuContent = group => {
describe("<MenuContent />", () => {
it("clicking on 'Copy' icon copies the link to clickboard", () => {
const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {});
const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {}, {});
const tree = MountedMenuContent(group);
const button = tree.find(".dropdown-item").at(0);
button.simulate("click");
@@ -83,7 +83,7 @@ describe("<MenuContent />", () => {
});
it("clicking on 'Silence' icon opens the silence form modal", () => {
const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {});
const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {}, {});
const tree = MountedMenuContent(group);
const button = tree.find(".dropdown-item").at(1);
button.simulate("click");

View File

@@ -32,7 +32,8 @@ const MockAPIResponse = () => {
{ alertname: "foo" },
[MockAlert([], { instance: "foo" }, "suppressed")],
[],
{ job: "foo" }
{ job: "foo" },
{}
)
};
return response;

View File

@@ -219,6 +219,7 @@ const AlertGroup = observer(
group={group}
alertmanagers={footerAlertmanagers}
afterUpdate={afterUpdate}
silenceFormStore={silenceFormStore}
/>
) : null}
</div>

View File

@@ -22,6 +22,7 @@ const MockGroup = groupName => {
{ alertname: "Fake Alert", groupName: groupName },
[],
[],
{},
{}
);
return group;

View File

@@ -41,6 +41,7 @@ const MockGroup = (groupName, alertCount) => {
{ alertname: "Fake Alert", group: groupName },
alerts,
[],
{},
{}
);
return group;

View File

@@ -42,7 +42,8 @@ const MockAPIResponse = () => {
{ alertname: "foo" },
[MockAlert([], { instance: "foo1" }, "active")],
[],
{ job: "foo" }
{ job: "foo" },
{}
),
"2": MockAlertGroup(
{ alertname: "bar" },
@@ -51,7 +52,8 @@ const MockAPIResponse = () => {
MockAlert([], { instance: "bar2" }, "active")
],
[],
{ job: "bar" }
{ job: "bar" },
{}
)
};
return response;

View File

@@ -44,7 +44,8 @@ const APIGroup = PropTypes.exact({
}),
shared: PropTypes.exact({
annotations: PropTypes.arrayOf(Annotation).isRequired,
labels: PropTypes.object.isRequired
labels: PropTypes.object.isRequired,
silences: PropTypes.object.isRequired
}).isRequired
});

View File

@@ -23,9 +23,15 @@ const MockGroup = () => {
MockAlert([], { instance: "prod2", cluster: "prod" }),
MockAlert([], { instance: "dev1", cluster: "dev" })
];
const group = MockAlertGroup({ alertname: "FakeAlert" }, alerts, [], {
job: "mock"
});
const group = MockAlertGroup(
{ alertname: "FakeAlert" },
alerts,
[],
{
job: "mock"
},
{}
);
return group;
};

View File

@@ -30,7 +30,8 @@ const MockAlertGroup = (
rootLabels,
alerts,
sharedAnnotations,
sharedLabels
sharedLabels,
sharedSilences
) => ({
receiver: "by-name",
labels: rootLabels,
@@ -47,7 +48,8 @@ const MockAlertGroup = (
},
shared: {
annotations: sharedAnnotations,
labels: sharedLabels
labels: sharedLabels,
silences: sharedSilences
}
});