mirror of
https://github.com/prymitive/karma
synced 2026-05-07 03:26:52 +00:00
feat(ui): use api/v2 silence endpoints for alertmanager 0.16+
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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("<DeleteSilenceModalContent />", () => {
|
||||
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("<DeleteSilenceModalContent />", () => {
|
||||
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("<DeleteSilenceModalContent />", () => {
|
||||
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("<DeleteSilenceModalContent />", () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 = <SilenceLink uri={uri} silenceId={response.silenceID} />;
|
||||
this.submitState.markDone(link);
|
||||
// return silenceId so we can assert it in tests
|
||||
return response.silenceID;
|
||||
};
|
||||
|
||||
parseAlertmanagerResponse = (uri, response) => {
|
||||
|
||||
@@ -48,7 +48,15 @@ describe("<SilenceSubmitProgress />", () => {
|
||||
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("<SilenceSubmitProgress />", () => {
|
||||
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("<SilenceSubmitProgress />", () => {
|
||||
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("<SilenceSubmitProgress />", () => {
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user