diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.js index 196863bc9..d1ce58b8e 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.js @@ -94,7 +94,7 @@ const Alert = observer( am.silencedBy.map(silenceID => ( diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.js index 40742cb7f..8cb510938 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.js @@ -123,17 +123,17 @@ SilenceDetails.propTypes = { }; // -const FallbackSilenceDesciption = ({ alertmanager, silenceID }) => { +const FallbackSilenceDesciption = ({ alertmanagerName, silenceID }) => { return (
- Silenced by {alertmanager.name}/{silenceID} + Silenced by {alertmanagerName}/{silenceID}
); }; FallbackSilenceDesciption.propTypes = { - alertmanager: PropTypes.object.isRequired, + alertmanagerName: PropTypes.string.isRequired, silenceID: PropTypes.string.isRequired }; @@ -142,7 +142,7 @@ const Silence = inject("alertStore")( class Silence extends Component { static propTypes = { alertStore: PropTypes.object.isRequired, - alertmanager: PropTypes.object.isRequired, + alertmanagerState: PropTypes.object.isRequired, silenceID: PropTypes.string.isRequired, afterUpdate: PropTypes.func.isRequired }; @@ -187,14 +187,28 @@ const Silence = inject("alertStore")( this.progressTimer = setInterval(this.recalculateProgress, 30 * 1000); } + getAlertmanager = () => { + const { alertStore, alertmanagerState } = this.props; + + const alertmanager = alertStore.data.getAlertmanagerByName( + alertmanagerState.name + ); + + if (alertmanager) return alertmanager; + + return { + name: alertmanagerState.name + }; + }; + getSilence = () => { - const { alertStore, alertmanager, silenceID } = this.props; + const { alertStore, alertmanagerState, silenceID } = this.props; // We pass alertmanager name and silence ID to Silence component // and we need to lookup the actual silence data in the store. // Data might be missing from the store so first check if we have // anything for this alertmanager instance - const amSilences = alertStore.data.silences[alertmanager.name]; + const amSilences = alertStore.data.silences[alertmanagerState.name]; if (!amSilences) return null; // next check if alertmanager has our silence ID @@ -222,17 +236,19 @@ const Silence = inject("alertStore")( } render() { - const { alertmanager, silenceID } = this.props; + const { alertmanagerState, silenceID } = this.props; const silence = this.getSilence(); if (!silence) return ( ); + const alertmanager = this.getAlertmanager(); + return (
diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.test.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.test.js index dd23a3930..04a742b91 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.test.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.test.js @@ -16,7 +16,6 @@ const mockAfterUpdate = jest.fn(); const alertmanager = { name: "default", - uri: "file:///mock", state: "suppressed", startsAt: "2000-01-01T10:00:00Z", endsAt: "0001-01-01T00:00:00Z", @@ -62,6 +61,7 @@ beforeEach(() => { { name: "default", uri: "file:///mock", + publicURI: "http://example.com", error: "" } ] @@ -78,12 +78,12 @@ afterEach(() => { clear(); }); -const MountedSilence = () => { +const MountedSilence = alertmanagerState => { return mount( @@ -91,27 +91,36 @@ const MountedSilence = () => { ); }; +const ShallowSilenceDetails = () => { + return shallow( + + ); +}; + describe("", () => { it("matches snapshot when data is present in alertStore", () => { - const tree = MountedSilence().find("Silence"); + const tree = MountedSilence(alertmanager).find("Silence"); expect(toDiffableHtml(tree.html())).toMatchSnapshot(); }); it("renders full silence when data is present in alertStore", () => { - const tree = MountedSilence().find("Silence"); + const tree = MountedSilence(alertmanager).find("Silence"); const fallback = tree.find("FallbackSilenceDesciption"); expect(fallback).toHaveLength(0); }); it("matches snapshot when data is not present in alertStore", () => { alertStore.data.silences = {}; - const tree = MountedSilence().find("Silence"); + const tree = MountedSilence(alertmanager).find("Silence"); expect(toDiffableHtml(tree.html())).toMatchSnapshot(); }); it("renders FallbackSilenceDesciption when Alertmanager data is not present in alertStore", () => { alertStore.data.silences = {}; - const tree = MountedSilence(); + const tree = MountedSilence(alertmanager); const fallback = tree.find("FallbackSilenceDesciption"); expect(fallback).toHaveLength(1); expect(tree.text()).toBe( @@ -121,7 +130,7 @@ describe("", () => { it("renders FallbackSilenceDesciption when silence data is not present in alertStore", () => { alertStore.data.silences.default = {}; - const tree = MountedSilence(); + const tree = MountedSilence(alertmanager); const fallback = tree.find("FallbackSilenceDesciption"); expect(fallback).toHaveLength(1); expect(tree.text()).toBe( @@ -130,7 +139,7 @@ describe("", () => { }); it("clicking on expand toggle shows silence details", () => { - const tree = MountedSilence(); + const tree = MountedSilence(alertmanager); const toggle = tree.find("a.float-right.cursor-pointer"); toggle.simulate("click"); const details = tree.find("SilenceDetails"); @@ -138,7 +147,7 @@ describe("", () => { }); it("matches snapshot with expaned details", () => { - const tree = MountedSilence().find("Silence"); + const tree = MountedSilence(alertmanager).find("Silence"); tree.instance().collapse.toggle(); expect(toDiffableHtml(tree.html())).toMatchSnapshot(); }); @@ -146,26 +155,42 @@ describe("", () => { it("renders comment as link when jiraURL is set", () => { alertStore.data.silences.default[silence.id].jiraURL = "http://jira.example.com"; - const tree = MountedSilence().find("Silence"); + const tree = MountedSilence(alertmanager).find("Silence"); const link = tree.find("a[href='http://jira.example.com']"); expect(link).toHaveLength(1); expect(link.text()).toBe("Fake silence"); }); it("clears progress timer on unmount", () => { - const tree = MountedSilence().find("Silence"); + const tree = MountedSilence(alertmanager).find("Silence"); const instance = tree.instance(); expect(instance.progressTimer).toBeTruthy(); instance.componentWillUnmount(); expect(instance.progressTimer).toBeNull(); }); -}); -const ShallowSilenceDetails = () => { - return shallow( - - ); -}; + it("getAlertmanager() returns alertmanager object from alertStore.data.upstreams.instances", () => { + const tree = MountedSilence(alertmanager).find("Silence"); + const instance = tree.instance(); + const am = instance.getAlertmanager(); + expect(am).toEqual({ + name: "default", + uri: "file:///mock", + publicURI: "http://example.com", + error: "" + }); + }); + + it("getAlertmanager() return object with only name if given name is not in alertStore", () => { + const missingAlertmanager = { ...alertmanager, name: "notDefault" }; + const tree = MountedSilence(missingAlertmanager).find("Silence"); + const instance = tree.instance(); + const am = instance.getAlertmanager(); + expect(am).toEqual({ + name: "notDefault" + }); + }); +}); describe("", () => { it("unexpired silence endsAt label uses 'secondary' class", () => { @@ -180,25 +205,33 @@ describe("", () => { const endsAt = tree.find("span.badge").at(1); expect(endsAt.html()).toMatch(/badge-danger/); }); + + it("id links to Alertmanager silence view via alertmanager.uri", () => { + const tree = ShallowSilenceDetails(); + const link = tree.find("a"); + expect(link.props().href).toBe( + "file:///mock/#/silences/4cf5fd82-1edd-4169-99d1-ff8415e72179" + ); + }); }); describe("", () => { it("renders with class 'danger' and no progressbar when expired", () => { advanceTo(new Date(2001, 0, 1, 23, 0, 0)); - const tree = MountedSilence(); + const tree = MountedSilence(alertmanager); expect(tree.html()).toMatch(/badge-danger/); expect(tree.text()).toMatch(/Expired a year ago/); }); it("progressbar uses class 'danger' when > 90%", () => { advanceTo(new Date(2000, 0, 1, 19, 30, 0)); - const tree = MountedSilence(); + const tree = MountedSilence(alertmanager); expect(tree.html()).toMatch(/progress-bar bg-danger/); }); it("progressbar uses class 'danger' when > 75%", () => { advanceTo(new Date(2000, 0, 1, 17, 45, 0)); - const tree = MountedSilence(); + const tree = MountedSilence(alertmanager); expect(tree.html()).toMatch(/progress-bar bg-warning/); }); @@ -206,7 +239,7 @@ describe("", () => { const startsAt = new Date(2000, 0, 1, 10, 0, 0); const endsAt = new Date(2000, 0, 1, 20, 0, 0); - const tree = MountedSilence().find("Silence"); + const tree = MountedSilence(alertmanager).find("Silence"); const instance = tree.instance(); const value = toJS(instance.progress.value); diff --git a/ui/src/Components/SilenceModal/SilenceModalContent.js b/ui/src/Components/SilenceModal/SilenceModalContent.js index a975870ff..ff83d4493 100644 --- a/ui/src/Components/SilenceModal/SilenceModalContent.js +++ b/ui/src/Components/SilenceModal/SilenceModalContent.js @@ -47,6 +47,7 @@ const SilenceModalContent = observer(
{silenceFormStore.data.inProgress ? ( ) : ( diff --git a/ui/src/Components/SilenceModal/SilenceSubmitController.js b/ui/src/Components/SilenceModal/SilenceSubmitController.js index 93149add9..7a2de3f5c 100644 --- a/ui/src/Components/SilenceModal/SilenceSubmitController.js +++ b/ui/src/Components/SilenceModal/SilenceSubmitController.js @@ -8,11 +8,12 @@ import { SilenceSubmitProgress } from "./SilenceSubmitProgress"; class SilenceSubmitController extends Component { static propTypes = { + alertStore: PropTypes.object.isRequired, silenceFormStore: PropTypes.object.isRequired }; render() { - const { silenceFormStore } = this.props; + const { silenceFormStore, alertStore } = this.props; return ( @@ -23,6 +24,7 @@ class SilenceSubmitController extends Component { name={am.label} uri={am.value} payload={silenceFormStore.data.toAlertmanagerPayload} + alertStore={alertStore} /> ))}
diff --git a/ui/src/Components/SilenceModal/SilenceSubmitController.test.js b/ui/src/Components/SilenceModal/SilenceSubmitController.test.js index c8be7db3f..facc04ffc 100644 --- a/ui/src/Components/SilenceModal/SilenceSubmitController.test.js +++ b/ui/src/Components/SilenceModal/SilenceSubmitController.test.js @@ -2,21 +2,27 @@ import React from "react"; import { shallow } from "enzyme"; +import { AlertStore } from "Stores/AlertStore"; import { SilenceFormStore, MatcherValueToObject } from "Stores/SilenceFormStore"; import { SilenceSubmitController } from "./SilenceSubmitController"; +let alertStore; let silenceFormStore; beforeEach(() => { + alertStore = new AlertStore([]); silenceFormStore = new SilenceFormStore(); }); const ShallowSilenceSubmitController = () => { return shallow( - + ); }; diff --git a/ui/src/Components/SilenceModal/SilenceSubmitProgress.js b/ui/src/Components/SilenceModal/SilenceSubmitProgress.js index a19fd2fa3..103f3a32e 100644 --- a/ui/src/Components/SilenceModal/SilenceSubmitProgress.js +++ b/ui/src/Components/SilenceModal/SilenceSubmitProgress.js @@ -46,7 +46,8 @@ const SilenceSubmitProgress = observer( static propTypes = { name: PropTypes.string.isRequired, uri: PropTypes.string.isRequired, - payload: PropTypes.object.isRequired + payload: PropTypes.object.isRequired, + alertStore: PropTypes.object.isRequired }; submitState = observable( @@ -83,13 +84,22 @@ const SilenceSubmitProgress = observer( }; parseAlertmanagerResponse = response => { - const { uri } = this.props; + const { name, alertStore } = this.props; + + const alertmanager = alertStore.data.getAlertmanagerByName(name); if (response.status === "success") { - const link = ( - - ); - this.submitState.markDone(link); + if (alertmanager) { + const link = ( + + ); + this.submitState.markDone(link); + } else { + this.submitState.markDone(response.data.silenceId); + } } else if (response.status === "error") { this.submitState.markFailed(response.error); } else { diff --git a/ui/src/Components/SilenceModal/SilenceSubmitProgress.test.js b/ui/src/Components/SilenceModal/SilenceSubmitProgress.test.js index eb64be239..355e1e159 100644 --- a/ui/src/Components/SilenceModal/SilenceSubmitProgress.test.js +++ b/ui/src/Components/SilenceModal/SilenceSubmitProgress.test.js @@ -2,14 +2,34 @@ import React from "react"; import { mount } from "enzyme"; +import toDiffableHtml from "diffable-html"; + +import { AlertStore } from "Stores/AlertStore"; import { SilenceSubmitProgress } from "./SilenceSubmitProgress"; +let alertStore; + +beforeEach(() => { + alertStore = new AlertStore([]); + alertStore.data.upstreams = { + instances: [ + { + name: "mockAlertmanager", + uri: "file:///mock", + publicURI: "http://example.com", + error: "" + } + ] + }; +}); + const MountedSilenceSubmitProgress = () => { return mount( ); }; @@ -49,6 +69,21 @@ describe("", () => { expect(silenceLink.text()).toBe("123456789"); }); + it("renders returned silence ID as text if alertmanager is not found in AlertStore", async () => { + fetch.mockResponseOnce( + JSON.stringify({ status: "success", data: { silenceId: "123456789" } }) + ); + alertStore.data.upstreams.instances = []; + const tree = MountedSilenceSubmitProgress(); + await expect(tree.instance().submitState.fetch).resolves.toBe("success"); + // force re-render + tree.update(); + const silenceLink = tree.find("a"); + expect(silenceLink).toHaveLength(0); + const idDiv = tree.find("div.flex-fill").at(2); + expect(toDiffableHtml(idDiv.html())).toMatchSnapshot(); + }); + it("renders returned error message on failed fetch", async () => { fetch.mockRejectOnce(new Error("mock error message")); const tree = MountedSilenceSubmitProgress(); diff --git a/ui/src/Components/SilenceModal/__snapshots__/SilenceSubmitProgress.test.js.snap b/ui/src/Components/SilenceModal/__snapshots__/SilenceSubmitProgress.test.js.snap new file mode 100644 index 000000000..23a21fae0 --- /dev/null +++ b/ui/src/Components/SilenceModal/__snapshots__/SilenceSubmitProgress.test.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders returned silence ID as text if alertmanager is not found in AlertStore 1`] = ` +" +
+ 123456789 +
+" +`; diff --git a/ui/src/Stores/AlertStore.js b/ui/src/Stores/AlertStore.js index 1380ff992..3bb9362e8 100644 --- a/ui/src/Stores/AlertStore.js +++ b/ui/src/Stores/AlertStore.js @@ -134,9 +134,12 @@ class AlertStore { counters: {}, groups: {}, silences: {}, - upstreams: { instances: [] } + upstreams: { instances: [] }, + getAlertmanagerByName(name) { + return this.upstreams.instances.find(am => am.name === name); + } }, - {}, + { getAlertmanagerByName: action }, { name: "API Response data" } );