diff --git a/ui/package.json b/ui/package.json
index 90ed8f0b3..d8afad5e4 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -50,6 +50,7 @@
"react-tippy": "1.2.3",
"react-transition-group": "4.0.0",
"react-truncate": "2.4.0",
+ "semver": "6.0.0",
"whatwg-fetch": "3.0.0"
},
"scripts": {
diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/DeleteSilence.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/DeleteSilence.js
index 586933a09..256297fac 100644
--- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/DeleteSilence.js
+++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/DeleteSilence.js
@@ -4,6 +4,8 @@ import PropTypes from "prop-types";
import { observable, action } from "mobx";
import { observer } from "mobx-react";
+import semver from "semver";
+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTrash } from "@fortawesome/free-solid-svg-icons/faTrash";
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle";
@@ -119,9 +121,7 @@ const DeleteSilenceModalContent = observer(
]);
this.previewState.fetch = fetch(alertsURI, { credentials: "include" })
- .then(result => {
- return result.json();
- })
+ .then(result => result.json())
.then(result => {
this.previewState.groupsToUniqueLabels(Object.values(result.groups));
this.previewState.setError(null);
@@ -140,13 +140,29 @@ const DeleteSilenceModalContent = observer(
// if it's already deleted then do nothing
if (this.deleteState.done && this.deleteState.error === null) return;
- const uri = `${alertmanager.publicURI}/api/v1/silence/${silenceID}`;
+ const isOpenAPI = semver.satisfies(alertmanager.version, ">=0.16.0");
+
+ const uri = isOpenAPI
+ ? `${alertmanager.publicURI}/api/v2/silence/${silenceID}`
+ : `${alertmanager.publicURI}/api/v1/silence/${silenceID}`;
+
this.deleteState.fetch = fetch(uri, {
method: "DELETE",
credentials: "include"
})
- .then(result => result.json())
- .then(result => this.parseAlertmanagerResponse(result))
+ .then(result => {
+ if (isOpenAPI) {
+ if (result.ok) {
+ this.deleteState.setError(null);
+ this.deleteState.setDone();
+ } else {
+ result.text().then(this.deleteState.setError);
+ this.deleteState.setDone();
+ }
+ } else {
+ result.json().then(this.parseAlertmanagerResponse);
+ }
+ })
.catch(err => {
console.trace(err);
this.deleteState.setDone();
diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/DeleteSilence.test.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/DeleteSilence.test.js
index 9da08c88d..a119c9309 100644
--- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/DeleteSilence.test.js
+++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/DeleteSilence.test.js
@@ -13,6 +13,7 @@ let alertStore;
beforeEach(() => {
alertmanager = MockAlertmanager();
alertStore = new AlertStore([]);
+ alertStore.data.upstreams.instances[0] = alertmanager;
fetch.mockResponseOnce(JSON.stringify(MockAPIResponse()));
jest.restoreAllMocks();
@@ -121,7 +122,7 @@ describe("", () => {
expect(tree.find("ErrorMessage")).toHaveLength(1);
});
- it("sends a DELETE request after clicking 'Confirm' button", async () => {
+ it("[v1] sends a DELETE request after clicking 'Confirm' button ", async () => {
await VerifyResponse({ status: "success" });
expect(fetch.mock.calls[1][0]).toBe(
"http://am.example.com/api/v1/silence/123456789"
@@ -129,6 +130,15 @@ describe("", () => {
expect(fetch.mock.calls[1][1]).toMatchObject({ method: "DELETE" });
});
+ it("[v2] sends a DELETE request after clicking 'Confirm' button ", async () => {
+ alertmanager.version = "0.16.2";
+ await VerifyResponse({ status: "success" });
+ expect(fetch.mock.calls[1][0]).toBe(
+ "http://am.example.com/api/v2/silence/123456789"
+ );
+ expect(fetch.mock.calls[1][1]).toMatchObject({ method: "DELETE" });
+ });
+
it("'Confirm' button is no-op after successful DELETE", async () => {
const tree = await VerifyResponse({ status: "success" });
expect(fetch.mock.calls[1][0]).toBe(
@@ -165,7 +175,7 @@ describe("", () => {
expect(tree.find("ErrorMessage")).toHaveLength(1);
});
- it("renders ErrorMessage on failed fetch request", async () => {
+ it("[v1] renders ErrorMessage on failed fetch request", async () => {
const tree = MountedDeleteSilenceModalContent();
await expect(tree.instance().previewState.fetch).resolves.toBeUndefined();
@@ -179,4 +189,20 @@ describe("", () => {
tree.update();
expect(tree.find("ErrorMessage")).toHaveLength(1);
});
+
+ it("[v2] renders ErrorMessage on failed fetch request", async () => {
+ alertmanager.version = "0.16.2";
+ const tree = MountedDeleteSilenceModalContent();
+ await expect(tree.instance().previewState.fetch).resolves.toBeUndefined();
+
+ jest.spyOn(console, "trace").mockImplementation(() => {});
+ fetch.resetMocks();
+ fetch.mockResponseOnce("500 Internal Server Error", { status: 500 });
+
+ tree.find(".btn-outline-danger").simulate("click");
+ await expect(tree.instance().deleteState.fetch).resolves.toBeUndefined();
+
+ tree.update();
+ expect(tree.find("ErrorMessage")).toHaveLength(1);
+ });
});
diff --git a/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.js b/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.js
index bbfec10d4..45c322ae7 100644
--- a/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.js
+++ b/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.js
@@ -4,6 +4,8 @@ import PropTypes from "prop-types";
import { action, observable } from "mobx";
import { observer } from "mobx-react";
+import semver from "semver";
+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons/faCheckCircle";
@@ -54,7 +56,8 @@ const SilenceSubmitProgress = observer(
startsAt: PropTypes.string.isRequired,
endsAt: PropTypes.string.isRequired,
createdBy: PropTypes.string.isRequired,
- comment: PropTypes.string.isRequired
+ comment: PropTypes.string.isRequired,
+ id: PropTypes.string
}).isRequired,
alertStore: PropTypes.instanceOf(AlertStore).isRequired
};
@@ -99,7 +102,13 @@ const SilenceSubmitProgress = observer(
return;
}
- this.submitState.fetch = fetch(`${am.publicURI}/api/v1/silences`, {
+ const isOpenAPI = semver.satisfies(am.version, ">=0.16.0");
+
+ const uri = isOpenAPI
+ ? `${am.publicURI}/api/v2/silences`
+ : `${am.publicURI}/api/v1/silences`;
+
+ this.submitState.fetch = fetch(uri, {
method: "POST",
body: JSON.stringify(payload),
headers: {
@@ -107,9 +116,34 @@ const SilenceSubmitProgress = observer(
},
credentials: "include"
})
- .then(result => result.json())
- .then(result => this.parseAlertmanagerResponse(am.uri, result))
- .catch(err => this.maybeTryAgainAfterError(err));
+ .then(result => {
+ if (isOpenAPI) {
+ if (result.ok) {
+ return result
+ .json()
+ .then(r => this.parseOpenAPIResponse(am.uri, r));
+ } else {
+ return result.text().then(text => {
+ this.submitState.markFailed(text);
+ return text;
+ });
+ }
+ } else {
+ return result
+ .json()
+ .then(r => this.parseAlertmanagerResponse(am.uri, r));
+ }
+ })
+ .catch(err => {
+ this.maybeTryAgainAfterError(err);
+ });
+ };
+
+ parseOpenAPIResponse = (uri, response) => {
+ const link = ;
+ this.submitState.markDone(link);
+ // return silenceId so we can assert it in tests
+ return response.silenceID;
};
parseAlertmanagerResponse = (uri, response) => {
diff --git a/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.test.js b/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.test.js
index 6e18f0de2..446cd2d2d 100644
--- a/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.test.js
+++ b/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.test.js
@@ -48,7 +48,15 @@ describe("", () => {
expect(fetch.mock.calls).toHaveLength(1);
});
- it("appends /api/v1/silences to the passed URI", async () => {
+ it("[v1] appends /api/v1/silences to the passed URI", async () => {
+ const tree = MountedSilenceSubmitProgress();
+ await expect(tree.instance().submitState.fetch).resolves.toBeUndefined();
+ const uri = fetch.mock.calls[0][0];
+ expect(uri).toBe("http://example.com/api/v1/silences");
+ });
+
+ it("[v2] appends /api/v2/silences to the passed URI", async () => {
+ alertStore.data.upstreams.instances[0].version = "0.16.2";
const tree = MountedSilenceSubmitProgress();
await expect(tree.instance().submitState.fetch).resolves.toBeUndefined();
const uri = fetch.mock.calls[0][0];
@@ -188,7 +196,7 @@ describe("", () => {
expect(tree.text()).toBe("mockAlertmanagermock error message");
});
- it("renders success icon on successful fetch", async () => {
+ it("[v1] renders success icon on successful fetch", async () => {
fetch.mockResponseOnce(
JSON.stringify({ status: "success", data: { silenceId: "123" } })
);
@@ -199,7 +207,36 @@ describe("", () => {
expect(tree.find("FontAwesomeIcon.text-danger")).toHaveLength(0);
});
- it("renders error icon on failed fetch", async () => {
+ it("[v1] renders silence link on successful fetch", async () => {
+ fetch.mockResponseOnce(
+ JSON.stringify({ status: "success", data: { silenceId: "123" } })
+ );
+ const tree = MountedSilenceSubmitProgress();
+ await expect(tree.instance().submitState.fetch).resolves.toBe("success");
+ tree.update();
+ expect(tree.find("a").getDOMNode().getAttribute("href")).toBe("file:///mock/#/silences/123");
+ });
+
+ it("[v2] renders success icon on successful fetch", async () => {
+ alertStore.data.upstreams.instances[0].version = "0.16.2";
+ fetch.mockResponseOnce(JSON.stringify({ silenceID: "123" }));
+ const tree = MountedSilenceSubmitProgress();
+ await expect(tree.instance().submitState.fetch).resolves.toBe("123");
+ tree.update();
+ expect(tree.find("FontAwesomeIcon.text-success")).toHaveLength(1);
+ expect(tree.find("FontAwesomeIcon.text-danger")).toHaveLength(0);
+ });
+
+ it("[v2] renders silence link on successful fetch", async () => {
+ alertStore.data.upstreams.instances[0].version = "0.16.2";
+ fetch.mockResponseOnce(JSON.stringify({ silenceID: "123" }));
+ const tree = MountedSilenceSubmitProgress();
+ await expect(tree.instance().submitState.fetch).resolves.toBe("123");
+ tree.update();
+ expect(tree.find("a").getDOMNode().getAttribute("href")).toBe("file:///mock/#/silences/123");
+ });
+
+ it("[v1] renders error icon on failed fetch", async () => {
fetch.mockResponseOnce(JSON.stringify({ status: "error" }));
const tree = MountedSilenceSubmitProgress();
await expect(tree.instance().submitState.fetch).resolves.toBe("error");
@@ -208,6 +245,18 @@ describe("", () => {
expect(tree.find("FontAwesomeIcon.text-danger")).toHaveLength(1);
});
+ it("[v2] renders error icon on failed fetch", async () => {
+ alertStore.data.upstreams.instances[0].version = "0.16.2";
+ fetch.mockResponseOnce("error message", { status: 500 });
+ const tree = MountedSilenceSubmitProgress();
+ await expect(tree.instance().submitState.fetch).resolves.toBe(
+ "error message"
+ );
+ tree.update();
+ expect(tree.find("FontAwesomeIcon.text-success")).toHaveLength(0);
+ expect(tree.find("FontAwesomeIcon.text-danger")).toHaveLength(1);
+ });
+
it("renders unhandled 'status' values in the response as error", async () => {
fetch.mockResponseOnce(JSON.stringify({ status: "unhandled" }));
const tree = MountedSilenceSubmitProgress();