mirror of
https://github.com/prymitive/karma
synced 2026-08-28 11:17:28 +00:00
fix(ui): remove alertmanager API V1 support
This commit is contained in:
Generated
+2
-1
@@ -20942,7 +20942,8 @@
|
||||
"semver": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.1.3.tgz",
|
||||
"integrity": "sha512-ekM0zfiA9SCBlsKa2X1hxyxiI4L3B6EbVJkkdgQXnSEEaHlGdvyodMruTiulSRWMMB4NeIuYNMC9rTKTz97GxA=="
|
||||
"integrity": "sha512-ekM0zfiA9SCBlsKa2X1hxyxiI4L3B6EbVJkkdgQXnSEEaHlGdvyodMruTiulSRWMMB4NeIuYNMC9rTKTz97GxA==",
|
||||
"dev": true
|
||||
},
|
||||
"send": {
|
||||
"version": "0.17.1",
|
||||
|
||||
@@ -56,7 +56,6 @@
|
||||
"react-select": "3.0.8",
|
||||
"react-tippy": "1.3.4",
|
||||
"react-transition-group": "4.3.0",
|
||||
"semver": "7.1.3",
|
||||
"typeface-open-sans": "0.0.75",
|
||||
"typescript": "3.7.5"
|
||||
},
|
||||
|
||||
@@ -6,8 +6,6 @@ import { observer } from "mobx-react";
|
||||
|
||||
import moment from "moment";
|
||||
|
||||
import satisfies from "semver/functions/satisfies";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faCheck } from "@fortawesome/free-solid-svg-icons/faCheck";
|
||||
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle";
|
||||
@@ -142,38 +140,23 @@ const AlertAck = observer(
|
||||
return;
|
||||
}
|
||||
|
||||
const isOpenAPI = satisfies(am.version, ">=0.16.0");
|
||||
|
||||
const uri = isOpenAPI
|
||||
? `${am.uri}/api/v2/silences`
|
||||
: `${am.uri}/api/v1/silences`;
|
||||
|
||||
this.submitState.silencesByCluster[cluster].fetch = FetchPost(uri, {
|
||||
body: JSON.stringify(
|
||||
this.submitState.silencesByCluster[cluster].payload
|
||||
),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...am.headers
|
||||
this.submitState.silencesByCluster[cluster].fetch = FetchPost(
|
||||
`${am.uri}/api/v2/silences`,
|
||||
{
|
||||
body: JSON.stringify(
|
||||
this.submitState.silencesByCluster[cluster].payload
|
||||
),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...am.headers
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
.then(result => {
|
||||
if (isOpenAPI) {
|
||||
if (result.ok) {
|
||||
return result
|
||||
.json()
|
||||
.then(r => this.submitState.markDone(cluster));
|
||||
} else {
|
||||
this.maybeTryAgainAfterError(cluster);
|
||||
}
|
||||
if (result.ok) {
|
||||
return result.json().then(r => this.submitState.markDone(cluster));
|
||||
} else {
|
||||
return result
|
||||
.json()
|
||||
.then(r =>
|
||||
r.status === "success"
|
||||
? this.submitState.markDone(cluster)
|
||||
: this.maybeTryAgainAfterError(cluster)
|
||||
);
|
||||
this.maybeTryAgainAfterError(cluster);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
@@ -38,7 +38,7 @@ beforeEach(() => {
|
||||
readonly: false,
|
||||
headers: { foo: "bar" },
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
version: "0.17.0",
|
||||
cluster: "default",
|
||||
clusterMembers: ["default"]
|
||||
}
|
||||
@@ -102,22 +102,8 @@ describe("<AlertAck />", () => {
|
||||
expect(toDiffableHtml(tree.html())).toMatch(/fa-exclamation-circle/);
|
||||
});
|
||||
|
||||
it("[v1] uses faCheckCircle after successful fetch", async () => {
|
||||
fetch.mockResponse(
|
||||
JSON.stringify({ status: "success", data: { silenceId: "123456789" } })
|
||||
);
|
||||
const tree = MountedAlertAck();
|
||||
const button = tree.find("span.badge");
|
||||
button.simulate("click");
|
||||
await expect(
|
||||
tree.instance().submitState.silencesByCluster["default"].fetch
|
||||
).resolves.toBeUndefined();
|
||||
expect(toDiffableHtml(tree.html())).toMatch(/fa-check-circle/);
|
||||
});
|
||||
|
||||
it("[v2] uses faCheckCircle after successful fetch", async () => {
|
||||
it("uses faCheckCircle after successful fetch", async () => {
|
||||
fetch.mockResponse(JSON.stringify({ silenceID: "123" }));
|
||||
alertStore.data.upstreams.instances[0].version = "0.16.2";
|
||||
const tree = MountedAlertAck();
|
||||
const button = tree.find("span.badge");
|
||||
button.simulate("click");
|
||||
@@ -166,9 +152,7 @@ describe("<AlertAck />", () => {
|
||||
});
|
||||
|
||||
it("sends correct payload", () => {
|
||||
fetch.mockResponse(
|
||||
JSON.stringify({ status: "success", data: { silenceId: "123456789" } })
|
||||
);
|
||||
fetch.mockResponse(JSON.stringify({ silenceID: "123456789" }));
|
||||
|
||||
silenceFormStore.data.author = "karma/ui";
|
||||
MountAndClick();
|
||||
@@ -262,71 +246,13 @@ describe("<AlertAck />", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("[v1] sends POST request to /api/v1/silences", () => {
|
||||
MountAndClick();
|
||||
const uri = fetch.mock.calls[0][0];
|
||||
expect(uri).toBe("http://localhost/api/v1/silences");
|
||||
});
|
||||
|
||||
it("[v2] sends POST request to /api/v2/silences", () => {
|
||||
alertStore.data.upstreams.instances[0].version = "0.16.2";
|
||||
it("sends POST request to /api/v2/silences", () => {
|
||||
MountAndClick();
|
||||
const uri = fetch.mock.calls[0][0];
|
||||
expect(uri).toBe("http://localhost/api/v2/silences");
|
||||
});
|
||||
|
||||
it("[v1] will retry on another cluster member after fetch failure", async () => {
|
||||
fetch
|
||||
.mockResponseOnce(JSON.stringify({ status: "error" }))
|
||||
.mockResponseOnce(
|
||||
JSON.stringify({ status: "success", data: { silenceId: "123456789" } })
|
||||
);
|
||||
alertStore.data.upstreams = {
|
||||
clusters: { default: ["default", "fallback"] },
|
||||
instances: [
|
||||
{
|
||||
name: "default",
|
||||
uri: "http://am1.example.com",
|
||||
publicURI: "http://am1.example.com",
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
cluster: "default",
|
||||
clusterMembers: ["default", "fallback"]
|
||||
},
|
||||
{
|
||||
name: "fallback",
|
||||
uri: "http://am2.example.com",
|
||||
publicURI: "http://am2.example.com",
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
cluster: "default",
|
||||
clusterMembers: ["default", "fallback"]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const tree = MountedAlertAck();
|
||||
const button = tree.find("span.badge");
|
||||
button.simulate("click");
|
||||
await expect(
|
||||
tree.instance().submitState.silencesByCluster["default"].fetch
|
||||
).resolves.toBeUndefined();
|
||||
expect(fetch.mock.calls[0][0]).toBe(
|
||||
"http://am2.example.com/api/v1/silences"
|
||||
);
|
||||
await expect(
|
||||
tree.instance().submitState.silencesByCluster["default"].fetch
|
||||
).resolves.toBeUndefined();
|
||||
expect(fetch.mock.calls[1][0]).toBe(
|
||||
"http://am1.example.com/api/v1/silences"
|
||||
);
|
||||
});
|
||||
|
||||
it("[v2] will retry on another cluster member after fetch failure", async () => {
|
||||
it("will retry on another cluster member after fetch failure", async () => {
|
||||
fetch
|
||||
.mockResponseOnce("error message", { status: 500 })
|
||||
.mockResponseOnce(JSON.stringify({ silenceID: "123" }));
|
||||
@@ -340,7 +266,7 @@ describe("<AlertAck />", () => {
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.16.2",
|
||||
version: "0.17.0",
|
||||
cluster: "default",
|
||||
clusterMembers: ["default", "fallback"]
|
||||
},
|
||||
@@ -351,7 +277,7 @@ describe("<AlertAck />", () => {
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.16.2",
|
||||
version: "0.17.0",
|
||||
cluster: "default",
|
||||
clusterMembers: ["default", "fallback"]
|
||||
}
|
||||
@@ -389,7 +315,7 @@ describe("<AlertAck />", () => {
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
version: "0.17.0",
|
||||
cluster: "default",
|
||||
clusterMembers: ["default", "fallback"]
|
||||
}
|
||||
@@ -403,7 +329,7 @@ describe("<AlertAck />", () => {
|
||||
tree.instance().submitState.silencesByCluster["default"].fetch
|
||||
).resolves.toBeUndefined();
|
||||
expect(fetch.mock.calls[0][0]).toBe(
|
||||
"http://am1.example.com/api/v1/silences"
|
||||
"http://am1.example.com/api/v2/silences"
|
||||
);
|
||||
expect(consoleSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ beforeEach(() => {
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
version: "0.17.0",
|
||||
cluster: "default",
|
||||
clusterMembers: ["am1"]
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ beforeEach(() => {
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
version: "0.17.0",
|
||||
cluster: "default",
|
||||
clusterMembers: ["am1"]
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import PropTypes from "prop-types";
|
||||
import { observable, action } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
import satisfies from "semver/functions/satisfies";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faTrash } from "@fortawesome/free-solid-svg-icons/faTrash";
|
||||
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle";
|
||||
@@ -115,26 +113,6 @@ const DeleteSilenceModalContent = observer(
|
||||
.filter(u => u.cluster === this.props.cluster)
|
||||
.slice(0, 1)[0];
|
||||
|
||||
parseAlertmanagerResponse = response => {
|
||||
/*
|
||||
{"status": "success"}
|
||||
or
|
||||
{
|
||||
"status": "error",
|
||||
"errorType": "bad_data",
|
||||
"error": "silence 706959fd-4590-4e21-b983-859ba6ec0e1a already expired"
|
||||
}
|
||||
*/
|
||||
if (response.status === "success") {
|
||||
this.deleteState.setError(null);
|
||||
} else if (response.status === "error" && response.error) {
|
||||
this.deleteState.setError(response.error);
|
||||
} else {
|
||||
this.deleteState.setError(JSON.stringify(response));
|
||||
}
|
||||
this.deleteState.setDone();
|
||||
};
|
||||
|
||||
onFetchPreview = () => {
|
||||
const { silence } = this.props;
|
||||
|
||||
@@ -169,32 +147,25 @@ const DeleteSilenceModalContent = observer(
|
||||
|
||||
const alertmanager = this.getAlertmanager();
|
||||
|
||||
const isOpenAPI = satisfies(alertmanager.version, ">=0.16.0");
|
||||
|
||||
const uri = isOpenAPI
|
||||
? `${alertmanager.uri}/api/v2/silence/${silence.id}`
|
||||
: `${alertmanager.uri}/api/v1/silence/${silence.id}`;
|
||||
|
||||
this.deleteState.fetch = FetchDelete(uri, {
|
||||
headers: alertmanager.headers
|
||||
})
|
||||
this.deleteState.fetch = FetchDelete(
|
||||
`${alertmanager.uri}/api/v2/silence/${silence.id}`,
|
||||
{
|
||||
headers: alertmanager.headers
|
||||
}
|
||||
)
|
||||
.then(result => {
|
||||
if (isOpenAPI) {
|
||||
if (result.ok) {
|
||||
this.deleteState.setError(null);
|
||||
this.deleteState.setDone();
|
||||
} else {
|
||||
result.text().then(this.deleteState.setError);
|
||||
this.deleteState.setDone();
|
||||
}
|
||||
if (result.ok) {
|
||||
this.deleteState.setError(null);
|
||||
this.deleteState.setDone();
|
||||
} else {
|
||||
result.json().then(this.parseAlertmanagerResponse);
|
||||
result.text().then(this.deleteState.setError);
|
||||
this.deleteState.setDone();
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.trace(err);
|
||||
this.deleteState.setDone();
|
||||
this.deleteState.setError(
|
||||
return this.deleteState.setError(
|
||||
`Delete request failed with: ${err.message}`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ beforeEach(() => {
|
||||
silenceFormStore = new SilenceFormStore();
|
||||
cluster = "am";
|
||||
silence = MockSilence();
|
||||
fetch.mockResponse(JSON.stringify(MockAPIResponse()));
|
||||
fetch.mockResponseOnce(JSON.stringify(MockAPIResponse()));
|
||||
|
||||
alertStore.data.upstreams = {
|
||||
instances: [
|
||||
@@ -28,7 +28,7 @@ beforeEach(() => {
|
||||
uri: "http://localhost:9093",
|
||||
readonly: false,
|
||||
error: "",
|
||||
version: "0.15.3",
|
||||
version: "0.17.0",
|
||||
headers: {},
|
||||
clusterMembers: ["am1"]
|
||||
}
|
||||
@@ -94,6 +94,17 @@ const VerifyResponse = async response => {
|
||||
return tree;
|
||||
};
|
||||
|
||||
const VerifyError = async error => {
|
||||
const tree = MountedDeleteSilenceModalContent();
|
||||
await expect(tree.instance().previewState.fetch).resolves.toBeUndefined();
|
||||
|
||||
fetch.mockResponseOnce(error, { status: 500 });
|
||||
tree.find(".btn-danger").simulate("click");
|
||||
await expect(tree.instance().deleteState.fetch).resolves.toBeUndefined();
|
||||
|
||||
return tree;
|
||||
};
|
||||
|
||||
describe("<DeleteSilence />", () => {
|
||||
it("label is 'Delete' by default", () => {
|
||||
const tree = MountedDeleteSilence();
|
||||
@@ -147,8 +158,10 @@ describe("<DeleteSilenceModalContent />", () => {
|
||||
expect(fetch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders ErrorMessage on failed fetch", async () => {
|
||||
jest.spyOn(console, "trace").mockImplementation(() => {});
|
||||
it("renders ErrorMessage on failed preview fetch", async () => {
|
||||
const consoleSpy = jest
|
||||
.spyOn(console, "trace")
|
||||
.mockImplementation(() => {});
|
||||
fetch.resetMocks();
|
||||
fetch.mockReject(new Error("Fetch error"));
|
||||
|
||||
@@ -156,9 +169,10 @@ describe("<DeleteSilenceModalContent />", () => {
|
||||
await expect(tree.instance().previewState.fetch).resolves.toBeUndefined();
|
||||
tree.update();
|
||||
expect(tree.find("ErrorMessage")).toHaveLength(1);
|
||||
expect(consoleSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("renders ErrorMessage on fetch with non-JSON response", async () => {
|
||||
it("renders ErrorMessage on preview fetch with non-JSON response", async () => {
|
||||
fetch.mockResponseOnce("not json");
|
||||
jest.spyOn(console, "trace").mockImplementation(() => {});
|
||||
fetch.resetMocks();
|
||||
@@ -170,16 +184,7 @@ describe("<DeleteSilenceModalContent />", () => {
|
||||
expect(tree.find("ErrorMessage")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("[v1] sends a DELETE request after clicking 'Confirm' button", async () => {
|
||||
await VerifyResponse({ status: "success" });
|
||||
expect(fetch.mock.calls[1][0]).toBe(
|
||||
"http://localhost:9093/api/v1/silence/04d37636-2350-4878-b382-e0b50353230f"
|
||||
);
|
||||
expect(fetch.mock.calls[1][1]).toMatchObject({ method: "DELETE" });
|
||||
});
|
||||
|
||||
it("[v2] sends a DELETE request after clicking 'Confirm' button", async () => {
|
||||
alertStore.data.upstreams.instances[0].version = "0.16.2";
|
||||
it("sends a DELETE request after clicking 'Confirm' button", async () => {
|
||||
await VerifyResponse({ status: "success" });
|
||||
expect(fetch.mock.calls[1][0]).toBe(
|
||||
"http://localhost:9093/api/v2/silence/04d37636-2350-4878-b382-e0b50353230f"
|
||||
@@ -187,27 +192,11 @@ describe("<DeleteSilenceModalContent />", () => {
|
||||
expect(fetch.mock.calls[1][1]).toMatchObject({ method: "DELETE" });
|
||||
});
|
||||
|
||||
it("[v1] sends headers from alertmanager config", async () => {
|
||||
it("sends headers from alertmanager config", async () => {
|
||||
alertStore.data.upstreams.instances[0].headers = {
|
||||
Authorization: "Basic ***"
|
||||
};
|
||||
await VerifyResponse({ status: "success" });
|
||||
expect(fetch.mock.calls[1][0]).toBe(
|
||||
"http://localhost:9093/api/v1/silence/04d37636-2350-4878-b382-e0b50353230f"
|
||||
);
|
||||
expect(fetch.mock.calls[1][1]).toMatchObject({
|
||||
credentials: "include",
|
||||
method: "DELETE",
|
||||
headers: { Authorization: "Basic ***" }
|
||||
});
|
||||
});
|
||||
|
||||
it("[v1] sends headers from alertmanager config", async () => {
|
||||
alertStore.data.upstreams.instances[0].headers = {
|
||||
Authorization: "Basic ***"
|
||||
};
|
||||
alertStore.data.upstreams.instances[0].version = "0.16.2";
|
||||
await VerifyResponse({ status: "success" });
|
||||
expect(fetch.mock.calls[1][0]).toBe(
|
||||
"http://localhost:9093/api/v2/silence/04d37636-2350-4878-b382-e0b50353230f"
|
||||
);
|
||||
@@ -221,7 +210,7 @@ describe("<DeleteSilenceModalContent />", () => {
|
||||
it("'Confirm' button is no-op after successful DELETE", async () => {
|
||||
const tree = await VerifyResponse({ status: "success" });
|
||||
expect(fetch.mock.calls[1][0]).toBe(
|
||||
"http://localhost:9093/api/v1/silence/04d37636-2350-4878-b382-e0b50353230f"
|
||||
"http://localhost:9093/api/v2/silence/04d37636-2350-4878-b382-e0b50353230f"
|
||||
);
|
||||
expect(fetch.mock.calls[1][1]).toMatchObject({ method: "DELETE" });
|
||||
|
||||
@@ -232,58 +221,37 @@ describe("<DeleteSilenceModalContent />", () => {
|
||||
expect(fetch.mock.calls).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("renders SuccessMessage on 'success' response status", async () => {
|
||||
it("renders SuccessMessage on successful response status", async () => {
|
||||
const tree = await VerifyResponse({ status: "success" });
|
||||
tree.update();
|
||||
expect(tree.find("SuccessMessage")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("renders ErrorMessage on 'error' response status", async () => {
|
||||
const tree = await VerifyResponse({ status: "error", error: "fake error" });
|
||||
it("renders ErrorMessage on failed delete fetch request", async () => {
|
||||
const tree = await VerifyError("mock error");
|
||||
|
||||
tree.find(".btn-danger").simulate("click");
|
||||
await expect(tree.instance().deleteState.fetch).resolves.toBeUndefined();
|
||||
|
||||
tree.update();
|
||||
expect(tree.find("ErrorMessage")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("renders ErrorMessage on unhandled response status", async () => {
|
||||
const tree = await VerifyResponse({ status: "foo bar" });
|
||||
tree.update();
|
||||
expect(tree.find("ErrorMessage")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("renders ErrorMessage on unhandled response body", async () => {
|
||||
const tree = await VerifyResponse({ foo: "bar" });
|
||||
tree.update();
|
||||
expect(tree.find("ErrorMessage")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("[v1] renders ErrorMessage on failed fetch request", async () => {
|
||||
const tree = MountedDeleteSilenceModalContent();
|
||||
await expect(tree.instance().previewState.fetch).resolves.toBeUndefined();
|
||||
|
||||
jest.spyOn(console, "trace").mockImplementation(() => {});
|
||||
it("renders ErrorMessage on failed delete fetch", async () => {
|
||||
const consoleSpy = jest
|
||||
.spyOn(console, "trace")
|
||||
.mockImplementation(() => {});
|
||||
fetch.resetMocks();
|
||||
fetch.mockResponseOnce(JSON.stringify(MockAPIResponse()));
|
||||
fetch.mockReject(new Error("Fetch error"));
|
||||
|
||||
tree.find(".btn-danger").simulate("click");
|
||||
await expect(tree.instance().deleteState.fetch).resolves.toBeUndefined();
|
||||
|
||||
tree.update();
|
||||
expect(tree.find("ErrorMessage")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("[v2] renders ErrorMessage on failed fetch request", async () => {
|
||||
alertStore.data.upstreams.instances[0].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-danger").simulate("click");
|
||||
await expect(tree.instance().deleteState.fetch).resolves.toBeUndefined();
|
||||
|
||||
tree.update();
|
||||
expect(tree.find("ErrorMessage")).toHaveLength(1);
|
||||
expect(consoleSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,7 +47,7 @@ const MockMultipleClusters = () => {
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
version: "0.17.0",
|
||||
cluster: "default",
|
||||
clusterMembers: ["default", "fallback"]
|
||||
},
|
||||
@@ -58,7 +58,7 @@ const MockMultipleClusters = () => {
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
version: "0.17.0",
|
||||
cluster: "default",
|
||||
clusterMembers: ["default", "fallback"]
|
||||
},
|
||||
@@ -69,7 +69,7 @@ const MockMultipleClusters = () => {
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
version: "0.17.0",
|
||||
cluster: "second",
|
||||
clusterMembers: ["second"]
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ beforeEach(() => {
|
||||
publicURI: "http://example.com",
|
||||
readonly: false,
|
||||
error: "",
|
||||
version: "0.15.3",
|
||||
version: "0.17.0",
|
||||
headers: {}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -32,7 +32,7 @@ storiesOf("ManagedSilence", module)
|
||||
publicURI: "http://example.com",
|
||||
readonly: false,
|
||||
error: "",
|
||||
version: "0.15.3",
|
||||
version: "0.17.0",
|
||||
headers: {}
|
||||
}
|
||||
],
|
||||
@@ -50,7 +50,7 @@ storiesOf("ManagedSilence", module)
|
||||
readonly: true,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
version: "0.17.0",
|
||||
cluster: "ro",
|
||||
clusterMembers: ["readonly"]
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ beforeEach(() => {
|
||||
publicURI: "http://example.com",
|
||||
readonly: false,
|
||||
error: "",
|
||||
version: "0.15.3",
|
||||
version: "0.17.0",
|
||||
headers: {}
|
||||
}
|
||||
],
|
||||
@@ -98,7 +98,7 @@ describe("<ManagedSilence />", () => {
|
||||
publicURI: "http://example.com",
|
||||
readonly: false,
|
||||
error: "",
|
||||
version: "0.15.3",
|
||||
version: "0.17.0",
|
||||
headers: {}
|
||||
});
|
||||
});
|
||||
@@ -114,7 +114,7 @@ describe("<ManagedSilence />", () => {
|
||||
publicURI: "http://example.com",
|
||||
readonly: false,
|
||||
error: "",
|
||||
version: "0.15.3",
|
||||
version: "0.17.0",
|
||||
headers: {}
|
||||
},
|
||||
{
|
||||
@@ -125,7 +125,7 @@ describe("<ManagedSilence />", () => {
|
||||
publicURI: "http://example.com",
|
||||
readonly: true,
|
||||
error: "",
|
||||
version: "0.15.3",
|
||||
version: "0.17.0",
|
||||
headers: {}
|
||||
}
|
||||
],
|
||||
@@ -143,7 +143,7 @@ describe("<ManagedSilence />", () => {
|
||||
publicURI: "http://example.com",
|
||||
readonly: false,
|
||||
error: "",
|
||||
version: "0.15.3",
|
||||
version: "0.17.0",
|
||||
headers: {}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@ beforeEach(() => {
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
version: "0.17.0",
|
||||
cluster: "ha",
|
||||
clusterMembers: ["am1", "am2"]
|
||||
},
|
||||
@@ -41,7 +41,7 @@ beforeEach(() => {
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
version: "0.17.0",
|
||||
cluster: "ha",
|
||||
clusterMembers: ["am1", "am2"]
|
||||
},
|
||||
@@ -52,7 +52,7 @@ beforeEach(() => {
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
version: "0.17.0",
|
||||
cluster: "am3",
|
||||
clusterMembers: ["am3"]
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ beforeEach(() => {
|
||||
publicURI: "http://example.com",
|
||||
readonly: false,
|
||||
error: "",
|
||||
version: "0.15.3",
|
||||
version: "0.17.0",
|
||||
headers: {}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -28,7 +28,7 @@ beforeEach(() => {
|
||||
uri: "http://localhost:9093",
|
||||
readonly: false,
|
||||
error: "",
|
||||
version: "0.15.3",
|
||||
version: "0.17.0",
|
||||
headers: {}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -4,8 +4,6 @@ import PropTypes from "prop-types";
|
||||
import { action, observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
import satisfies from "semver/functions/satisfies";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch";
|
||||
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons/faCheckCircle";
|
||||
@@ -33,18 +31,18 @@ const SubmitIcon = observer(({ stateValue }) => {
|
||||
return <FontAwesomeIcon icon={faCircleNotch} spin />;
|
||||
});
|
||||
|
||||
const SilenceLink = ({ uri, silenceId }) => (
|
||||
const SilenceLink = ({ uri, silenceID }) => (
|
||||
<a
|
||||
href={`${uri}/#/silences/${silenceId}`}
|
||||
href={`${uri}/#/silences/${silenceID}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{silenceId}
|
||||
{silenceID}
|
||||
</a>
|
||||
);
|
||||
SilenceLink.propTypes = {
|
||||
uri: PropTypes.string.isRequired,
|
||||
silenceId: PropTypes.string.isRequired
|
||||
silenceID: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
const SilenceSubmitProgress = observer(
|
||||
@@ -84,7 +82,7 @@ const SilenceSubmitProgress = observer(
|
||||
|
||||
maybeTryAgainAfterError = err => {
|
||||
if (this.submitState.membersToTry.length) {
|
||||
this.handleAlertmanagerRequest();
|
||||
return this.handleAlertmanagerRequest();
|
||||
} else {
|
||||
this.submitState.markFailed(err.message);
|
||||
}
|
||||
@@ -106,17 +104,10 @@ const SilenceSubmitProgress = observer(
|
||||
if (am === undefined) {
|
||||
const err = `Alertmanager instance "${member}" not found`;
|
||||
console.error(err);
|
||||
this.maybeTryAgainAfterError(err);
|
||||
return;
|
||||
return this.maybeTryAgainAfterError(err);
|
||||
}
|
||||
|
||||
const isOpenAPI = satisfies(am.version, ">=0.16.0");
|
||||
|
||||
const uri = isOpenAPI
|
||||
? `${am.uri}/api/v2/silences`
|
||||
: `${am.uri}/api/v1/silences`;
|
||||
|
||||
this.submitState.fetch = FetchPost(uri, {
|
||||
this.submitState.fetch = FetchPost(`${am.uri}/api/v2/silences`, {
|
||||
body: JSON.stringify(payload),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -124,51 +115,27 @@ const SilenceSubmitProgress = observer(
|
||||
}
|
||||
})
|
||||
.then(result => {
|
||||
if (isOpenAPI) {
|
||||
if (result.ok) {
|
||||
return result
|
||||
.json()
|
||||
.then(r => this.parseOpenAPIResponse(am.publicURI, r));
|
||||
} else {
|
||||
return result.text().then(text => {
|
||||
this.submitState.markFailed(text);
|
||||
return text;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (result.ok) {
|
||||
return result
|
||||
.json()
|
||||
.then(r => this.parseAlertmanagerResponse(am.publicURI, r));
|
||||
.then(r => this.parseOpenAPIResponse(am.publicURI, r));
|
||||
} else {
|
||||
return result.text().then(text => {
|
||||
this.submitState.markFailed(text);
|
||||
return text;
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.maybeTryAgainAfterError(err);
|
||||
});
|
||||
.catch(this.maybeTryAgainAfterError);
|
||||
};
|
||||
|
||||
parseOpenAPIResponse = (uri, response) => {
|
||||
const link = <SilenceLink uri={uri} silenceId={response.silenceID} />;
|
||||
const link = <SilenceLink uri={uri} silenceID={response.silenceID} />;
|
||||
this.submitState.markDone(link);
|
||||
// return silenceId so we can assert it in tests
|
||||
// return silenceID so we can assert it in tests
|
||||
return response.silenceID;
|
||||
};
|
||||
|
||||
parseAlertmanagerResponse = (uri, response) => {
|
||||
if (response.status === "success") {
|
||||
const link = (
|
||||
<SilenceLink uri={uri} silenceId={response.data.silenceId} />
|
||||
);
|
||||
this.submitState.markDone(link);
|
||||
} else if (response.status === "error") {
|
||||
this.submitState.markFailed(response.error);
|
||||
} else {
|
||||
this.submitState.markFailed(JSON.stringify(response));
|
||||
}
|
||||
|
||||
// return status so we can assert it in tests
|
||||
return response.status;
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { members } = this.props;
|
||||
this.submitState.membersToTry = [...members];
|
||||
|
||||
@@ -18,16 +18,19 @@ beforeEach(() => {
|
||||
readonly: false,
|
||||
headers: { foo: "bar" },
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
version: "0.17.0",
|
||||
cluster: "mockAlertmanager",
|
||||
clusterMembers: ["mockAlertmanager"]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
fetch.resetMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
fetch.resetMocks();
|
||||
});
|
||||
|
||||
const MountedSilenceSubmitProgress = () => {
|
||||
@@ -54,19 +57,11 @@ describe("<SilenceSubmitProgress />", () => {
|
||||
expect(fetch.mock.calls).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("[v1] appends /api/v1/silences to the passed URI", async () => {
|
||||
it("appends /api/v2/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://localhost/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];
|
||||
expect(uri).toBe("http://localhost/api/v1/silences");
|
||||
expect(uri).toBe("http://localhost/api/v2/silences");
|
||||
});
|
||||
|
||||
it("sends correct JSON payload", () => {
|
||||
@@ -86,12 +81,9 @@ describe("<SilenceSubmitProgress />", () => {
|
||||
});
|
||||
|
||||
it("will retry on another cluster member after fetch failure", async () => {
|
||||
fetch.resetMocks();
|
||||
fetch
|
||||
.mockRejectOnce(new Error("mock error message"))
|
||||
.mockResponseOnce(
|
||||
JSON.stringify({ status: "success", data: { silenceId: "123456789" } })
|
||||
);
|
||||
.mockResponseOnce(JSON.stringify({ silenceID: "123456789" }));
|
||||
alertStore.data.upstreams = {
|
||||
clusters: { ha: ["am1", "am2"] },
|
||||
instances: [
|
||||
@@ -102,7 +94,7 @@ describe("<SilenceSubmitProgress />", () => {
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
version: "0.17.0",
|
||||
cluster: "ha",
|
||||
clusterMembers: ["am1", "am2"]
|
||||
},
|
||||
@@ -113,7 +105,7 @@ describe("<SilenceSubmitProgress />", () => {
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
version: "0.17.0",
|
||||
cluster: "ha",
|
||||
clusterMembers: ["am1", "am2"]
|
||||
}
|
||||
@@ -136,11 +128,11 @@ describe("<SilenceSubmitProgress />", () => {
|
||||
);
|
||||
await expect(tree.instance().submitState.fetch).resolves.toBeUndefined();
|
||||
expect(fetch.mock.calls[0][0]).toBe(
|
||||
"http://am2.example.com/api/v1/silences"
|
||||
"http://am2.example.com/api/v2/silences"
|
||||
);
|
||||
await expect(tree.instance().submitState.fetch).resolves.toBe("success");
|
||||
await expect(tree.instance().submitState.fetch).resolves.toBe("123456789");
|
||||
expect(fetch.mock.calls[1][0]).toBe(
|
||||
"http://am1.example.com/api/v1/silences"
|
||||
"http://am1.example.com/api/v2/silences"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -160,7 +152,7 @@ describe("<SilenceSubmitProgress />", () => {
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
version: "0.17.0",
|
||||
cluster: "ha",
|
||||
clusterMembers: ["am1", "am2"]
|
||||
}
|
||||
@@ -183,7 +175,7 @@ describe("<SilenceSubmitProgress />", () => {
|
||||
);
|
||||
await expect(tree.instance().submitState.fetch).resolves.toBeUndefined();
|
||||
expect(fetch.mock.calls[0][0]).toBe(
|
||||
"http://am1.example.com/api/v1/silences"
|
||||
"http://am1.example.com/api/v2/silences"
|
||||
);
|
||||
expect(consoleSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -207,7 +199,7 @@ describe("<SilenceSubmitProgress />", () => {
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
version: "0.17.0",
|
||||
cluster: "ha",
|
||||
clusterMembers: ["am1", "am2"]
|
||||
},
|
||||
@@ -218,7 +210,7 @@ describe("<SilenceSubmitProgress />", () => {
|
||||
readonly: true,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
version: "0.17.0",
|
||||
cluster: "ha",
|
||||
clusterMembers: ["am1", "am2"]
|
||||
}
|
||||
@@ -242,18 +234,16 @@ describe("<SilenceSubmitProgress />", () => {
|
||||
await expect(tree.instance().submitState.fetch).resolves.toBeUndefined();
|
||||
expect(fetch.mock.calls).toHaveLength(1);
|
||||
expect(fetch.mock.calls[0][0]).toBe(
|
||||
"http://am1.example.com/api/v1/silences"
|
||||
"http://am1.example.com/api/v2/silences"
|
||||
);
|
||||
expect(logs).toEqual(['Alertmanager instance "am2" is read-only']);
|
||||
expect(consoleSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("renders returned silence ID on successful fetch", async () => {
|
||||
fetch.mockResponseOnce(
|
||||
JSON.stringify({ status: "success", data: { silenceId: "123456789" } })
|
||||
);
|
||||
fetch.mockResponseOnce(JSON.stringify({ silenceID: "123456789" }));
|
||||
const tree = MountedSilenceSubmitProgress();
|
||||
await expect(tree.instance().submitState.fetch).resolves.toBe("success");
|
||||
await expect(tree.instance().submitState.fetch).resolves.toBe("123456789");
|
||||
// force re-render
|
||||
tree.update();
|
||||
const silenceLink = tree.find("a");
|
||||
@@ -262,40 +252,15 @@ describe("<SilenceSubmitProgress />", () => {
|
||||
});
|
||||
|
||||
it("renders returned error message on failed fetch", async () => {
|
||||
fetch.mockRejectOnce(new Error("mock error message"));
|
||||
fetch.mockResponseOnce("mock error message", { status: 500 });
|
||||
const tree = MountedSilenceSubmitProgress();
|
||||
await expect(tree.instance().submitState.fetch).resolves.toBeUndefined();
|
||||
await expect(tree.instance().submitState.fetch).resolves.toBe(
|
||||
"mock error message"
|
||||
);
|
||||
expect(tree.text()).toBe("mockAlertmanagermock error message");
|
||||
});
|
||||
|
||||
it("[v1] renders success icon 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("FontAwesomeIcon.text-success")).toHaveLength(1);
|
||||
expect(tree.find("FontAwesomeIcon.text-danger")).toHaveLength(0);
|
||||
});
|
||||
|
||||
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("http://example.com/#/silences/123");
|
||||
});
|
||||
|
||||
it("[v2] renders success icon on successful fetch", async () => {
|
||||
alertStore.data.upstreams.instances[0].version = "0.16.2";
|
||||
it("renders success icon on successful fetch", async () => {
|
||||
fetch.mockResponseOnce(JSON.stringify({ silenceID: "123" }));
|
||||
const tree = MountedSilenceSubmitProgress();
|
||||
await expect(tree.instance().submitState.fetch).resolves.toBe("123");
|
||||
@@ -304,8 +269,7 @@ describe("<SilenceSubmitProgress />", () => {
|
||||
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";
|
||||
it("renders silence link on successful fetch", async () => {
|
||||
fetch.mockResponseOnce(JSON.stringify({ silenceID: "123" }));
|
||||
const tree = MountedSilenceSubmitProgress();
|
||||
await expect(tree.instance().submitState.fetch).resolves.toBe("123");
|
||||
@@ -318,17 +282,7 @@ describe("<SilenceSubmitProgress />", () => {
|
||||
).toBe("http://example.com/#/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");
|
||||
tree.update();
|
||||
expect(tree.find("FontAwesomeIcon.text-success")).toHaveLength(0);
|
||||
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";
|
||||
it("renders error icon on failed fetch", async () => {
|
||||
fetch.mockResponseOnce("error message", { status: 500 });
|
||||
const tree = MountedSilenceSubmitProgress();
|
||||
await expect(tree.instance().submitState.fetch).resolves.toBe(
|
||||
@@ -338,14 +292,4 @@ describe("<SilenceSubmitProgress />", () => {
|
||||
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();
|
||||
await expect(tree.instance().submitState.fetch).resolves.toBe("unhandled");
|
||||
tree.update();
|
||||
expect(tree.find("FontAwesomeIcon.text-success")).toHaveLength(0);
|
||||
expect(tree.find("FontAwesomeIcon.text-danger")).toHaveLength(1);
|
||||
expect(tree.text()).toBe('mockAlertmanager{"status":"unhandled"}');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,7 +50,7 @@ storiesOf("SilenceModal", module)
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
version: "0.17.0",
|
||||
cluster: "default",
|
||||
clusterMembers: ["default"]
|
||||
}
|
||||
@@ -124,7 +124,7 @@ storiesOf("SilenceModal", module)
|
||||
readonly: true,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
version: "0.17.0",
|
||||
cluster: "default",
|
||||
clusterMembers: ["readonly"]
|
||||
}
|
||||
@@ -189,7 +189,7 @@ storiesOf("SilenceModal", module)
|
||||
publicURI: "http://example.com",
|
||||
readonly: false,
|
||||
error: "",
|
||||
version: "0.15.3",
|
||||
version: "0.17.0",
|
||||
headers: {}
|
||||
}
|
||||
],
|
||||
@@ -252,7 +252,7 @@ storiesOf("SilenceModal", module)
|
||||
publicURI: "http://example.com",
|
||||
readonly: false,
|
||||
error: "",
|
||||
version: "0.15.3",
|
||||
version: "0.17.0",
|
||||
headers: {}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -33,7 +33,7 @@ describe("AlertStore.data", () => {
|
||||
readonly: false,
|
||||
headers: { foo: "bar" },
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
version: "0.17.0",
|
||||
cluster: "default",
|
||||
clusterMembers: ["default", "readonly"]
|
||||
},
|
||||
@@ -44,7 +44,7 @@ describe("AlertStore.data", () => {
|
||||
readonly: true,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
version: "0.17.0",
|
||||
cluster: "default",
|
||||
clusterMembers: ["default", "readonly"]
|
||||
}
|
||||
@@ -69,7 +69,7 @@ describe("AlertStore.data", () => {
|
||||
readonly: true,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
version: "0.17.0",
|
||||
cluster: "default",
|
||||
clusterMembers: ["ro1", "ro2"]
|
||||
},
|
||||
@@ -80,7 +80,7 @@ describe("AlertStore.data", () => {
|
||||
readonly: true,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
version: "0.17.0",
|
||||
cluster: "default",
|
||||
clusterMembers: ["ro1", "ro2"]
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ const MockAlertmanager = () => ({
|
||||
Authorization: "Basic foo bar"
|
||||
},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
version: "0.17.0",
|
||||
clusterMembers: ["default"]
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user