mirror of
https://github.com/prymitive/karma
synced 2026-05-05 03:16:51 +00:00
fix(ui): drop useLocalStore from AlertAck
This commit is contained in:
committed by
Łukasz Mierzwa
parent
f063038c30
commit
be882121cd
@@ -1,8 +1,8 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { toJS } from "mobx";
|
||||
import { useObserver, useLocalStore } from "mobx-react";
|
||||
import { useObserver } from "mobx-react";
|
||||
|
||||
import moment from "moment";
|
||||
|
||||
@@ -19,131 +19,19 @@ import {
|
||||
MatchersFromGroup,
|
||||
GenerateAlertmanagerSilenceData,
|
||||
} from "Stores/SilenceFormStore";
|
||||
import { FetchPost } from "Common/Fetch";
|
||||
import { useFetchAny } from "Hooks/useFetchAny";
|
||||
import { TooltipWrapper } from "Components/TooltipWrapper";
|
||||
|
||||
const SubmitState = Object.freeze({
|
||||
Idle: "Idle",
|
||||
InProgress: "InProgress",
|
||||
Done: "Done",
|
||||
Failed: "Failed",
|
||||
});
|
||||
|
||||
const newPendingSilence = (
|
||||
group,
|
||||
members,
|
||||
durationSeconds,
|
||||
author,
|
||||
commentPrefix
|
||||
) => ({
|
||||
payload: GenerateAlertmanagerSilenceData(
|
||||
moment.utc(),
|
||||
moment.utc().add(durationSeconds, "seconds"),
|
||||
MatchersFromGroup(group, [], group.alerts, true),
|
||||
author,
|
||||
`${
|
||||
commentPrefix ? commentPrefix + " " : ""
|
||||
}This alert was acknowledged using karma on ${moment.utc().toString()}`
|
||||
),
|
||||
membersToTry: members,
|
||||
submitState: SubmitState.Idle,
|
||||
submitResult: null,
|
||||
isDone: false,
|
||||
isFailed: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const AlertAck = ({ alertStore, silenceFormStore, group }) => {
|
||||
const submitState = useLocalStore(() => ({
|
||||
silencesByCluster: {},
|
||||
reset() {
|
||||
this.silencesByCluster = {};
|
||||
},
|
||||
pushSilence(cluster, silence) {
|
||||
this.silencesByCluster[cluster] = silence;
|
||||
},
|
||||
markDone(cluster) {
|
||||
this.silencesByCluster[cluster].isDone = true;
|
||||
},
|
||||
markFailed(cluster, err) {
|
||||
this.silencesByCluster[cluster].isDone = true;
|
||||
this.silencesByCluster[cluster].isFailed = true;
|
||||
this.silencesByCluster[cluster].error = err;
|
||||
},
|
||||
get isIdle() {
|
||||
return Object.keys(this.silencesByCluster).length === 0;
|
||||
},
|
||||
get isInprogress() {
|
||||
return (
|
||||
Object.values(this.silencesByCluster).filter(
|
||||
(pendingSilence) => pendingSilence.isDone === false
|
||||
).length > 0
|
||||
);
|
||||
},
|
||||
get isDone() {
|
||||
return (
|
||||
Object.values(this.silencesByCluster).filter(
|
||||
(pendingSilence) => pendingSilence.isDone === true
|
||||
).length > 0
|
||||
);
|
||||
},
|
||||
get isFailed() {
|
||||
return (
|
||||
Object.values(this.silencesByCluster).filter(
|
||||
(pendingSilence) => pendingSilence.isFailed === true
|
||||
).length > 0
|
||||
);
|
||||
},
|
||||
get errorMessages() {
|
||||
return Object.values(this.silencesByCluster)
|
||||
.filter((pendingSilence) => pendingSilence.error !== null)
|
||||
.map((s) => s.error);
|
||||
},
|
||||
}));
|
||||
const [clusters, setClusters] = useState([]);
|
||||
const [upstreams, setUpstreams] = useState([]);
|
||||
const [currentCluster, setCurrentCluster] = useState(0);
|
||||
const [isAcking, setIsAcking] = useState(false);
|
||||
|
||||
const maybeTryAgainAfterError = (cluster, err) => {
|
||||
if (submitState.silencesByCluster[cluster].membersToTry.length) {
|
||||
handleAlertmanagerRequest(cluster);
|
||||
} else {
|
||||
submitState.markFailed(cluster, err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAlertmanagerRequest = (cluster) => {
|
||||
const member = submitState.silencesByCluster[cluster].membersToTry.pop();
|
||||
|
||||
const am = alertStore.data.getAlertmanagerByName(member);
|
||||
if (am === undefined) {
|
||||
const err = `Alertmanager instance "${member} not found`;
|
||||
console.error(err);
|
||||
maybeTryAgainAfterError(cluster, err);
|
||||
return;
|
||||
}
|
||||
|
||||
FetchPost(`${am.uri}/api/v2/silences`, {
|
||||
body: JSON.stringify(submitState.silencesByCluster[cluster].payload),
|
||||
credentials: am.corsCredentials,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...am.headers,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.ok) {
|
||||
return result.json().then((r) => submitState.markDone(cluster));
|
||||
} else {
|
||||
result.text().then((text) => maybeTryAgainAfterError(cluster, text));
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
maybeTryAgainAfterError(cluster, err);
|
||||
});
|
||||
};
|
||||
const { response, error, inProgress, reset } = useFetchAny(upstreams);
|
||||
|
||||
const onACK = () => {
|
||||
if (submitState.isInprogress || submitState.isDone) {
|
||||
return;
|
||||
}
|
||||
setIsAcking(true);
|
||||
|
||||
let author =
|
||||
silenceFormStore.data.author !== ""
|
||||
@@ -166,47 +54,103 @@ const AlertAck = ({ alertStore, silenceFormStore, group }) => {
|
||||
alertmanagers.some((m) => clusterMembers.includes(m))
|
||||
);
|
||||
|
||||
submitState.reset();
|
||||
let c = [];
|
||||
for (const [clusterName, clusterMembers] of clusters) {
|
||||
const pendingSilence = newPendingSilence(
|
||||
toJS(group),
|
||||
toJS(clusterMembers),
|
||||
toJS(alertStore.settings.values.alertAcknowledgement.durationSeconds),
|
||||
author,
|
||||
toJS(alertStore.settings.values.alertAcknowledgement.commentPrefix)
|
||||
const durationSeconds = toJS(
|
||||
alertStore.settings.values.alertAcknowledgement.durationSeconds
|
||||
);
|
||||
submitState.pushSilence(clusterName, pendingSilence);
|
||||
handleAlertmanagerRequest(clusterName);
|
||||
const commentPrefix = toJS(
|
||||
alertStore.settings.values.alertAcknowledgement.commentPrefix
|
||||
);
|
||||
c.push({
|
||||
payload: GenerateAlertmanagerSilenceData(
|
||||
moment.utc(),
|
||||
moment.utc().add(durationSeconds, "seconds"),
|
||||
MatchersFromGroup(group, [], group.alerts, true),
|
||||
author,
|
||||
`${
|
||||
commentPrefix ? commentPrefix + " " : ""
|
||||
}This alert was acknowledged using karma on ${moment
|
||||
.utc()
|
||||
.toString()}`
|
||||
),
|
||||
clusterName: clusterName,
|
||||
members: clusterMembers,
|
||||
});
|
||||
}
|
||||
setClusters(c);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (upstreams.length && !inProgress && (error || response)) {
|
||||
if (clusters.length > currentCluster + 1) {
|
||||
setCurrentCluster(currentCluster + 1);
|
||||
} else {
|
||||
setIsAcking(false);
|
||||
}
|
||||
}
|
||||
}, [clusters, upstreams, currentCluster, inProgress, error, response]);
|
||||
|
||||
useEffect(() => {
|
||||
if (clusters.length) {
|
||||
reset();
|
||||
const cluster = clusters[currentCluster];
|
||||
let u = [];
|
||||
cluster.members.forEach((amName) => {
|
||||
const am = alertStore.data.getAlertmanagerByName(amName);
|
||||
if (am !== undefined) {
|
||||
u.push({
|
||||
uri: `${am.uri}/api/v2/silences`,
|
||||
options: {
|
||||
method: "POST",
|
||||
body: JSON.stringify(cluster.payload),
|
||||
credentials: am.corsCredentials,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...am.headers,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
console.error(`Alertmanager "${amName}" not found`);
|
||||
}
|
||||
});
|
||||
setUpstreams(u);
|
||||
}
|
||||
}, [alertStore.data, clusters, currentCluster, reset]);
|
||||
|
||||
return useObserver(() =>
|
||||
alertStore.settings.values.alertAcknowledgement.enabled === false ? null : (
|
||||
<TooltipWrapper
|
||||
title={
|
||||
submitState.isFailed
|
||||
? submitState.errorMessages[0]
|
||||
!isAcking && error
|
||||
? error
|
||||
: "Acknowledge this alert with a short lived silence"
|
||||
}
|
||||
>
|
||||
<span
|
||||
className={`badge badge-pill components-label components-label-with-hover px-2 ${
|
||||
submitState.isFailed
|
||||
!isAcking && error
|
||||
? "badge-warning"
|
||||
: submitState.isDone
|
||||
: !isAcking && response
|
||||
? "badge-success"
|
||||
: "badge-secondary"
|
||||
}`}
|
||||
onClick={onACK}
|
||||
onClick={() => {
|
||||
if (!isAcking && !(response || error)) {
|
||||
setIsAcking(true);
|
||||
onACK();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{submitState.isIdle ? (
|
||||
<FontAwesomeIcon icon={faCheck} fixedWidth />
|
||||
) : submitState.isInprogress ? (
|
||||
<FontAwesomeIcon icon={faSpinner} fixedWidth spin />
|
||||
) : submitState.isFailed ? (
|
||||
{!isAcking && error ? (
|
||||
<FontAwesomeIcon icon={faExclamationCircle} fixedWidth />
|
||||
) : (
|
||||
) : !isAcking && response ? (
|
||||
<FontAwesomeIcon icon={faCheckCircle} fixedWidth />
|
||||
) : isAcking ? (
|
||||
<FontAwesomeIcon icon={faSpinner} fixedWidth spin />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCheck} fixedWidth />
|
||||
)}
|
||||
</span>
|
||||
</TooltipWrapper>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import { act } from "react-dom/test-utils";
|
||||
|
||||
import { mount } from "enzyme";
|
||||
|
||||
@@ -89,7 +90,9 @@ const MountAndClick = async () => {
|
||||
const tree = MountedAlertAck();
|
||||
const button = tree.find("span.badge");
|
||||
button.simulate("click");
|
||||
await fetchMock.flush(true);
|
||||
await act(async () => {
|
||||
await fetchMock.flush(true);
|
||||
});
|
||||
};
|
||||
|
||||
describe("<AlertAck />", () => {
|
||||
@@ -117,7 +120,9 @@ describe("<AlertAck />", () => {
|
||||
const tree = MountedAlertAck();
|
||||
const button = tree.find("span.badge");
|
||||
button.simulate("click");
|
||||
await fetchMock.flush(true);
|
||||
await act(async () => {
|
||||
await fetchMock.flush(true);
|
||||
});
|
||||
expect(toDiffableHtml(tree.html())).toMatch(/fa-exclamation-circle/);
|
||||
});
|
||||
|
||||
@@ -125,22 +130,95 @@ describe("<AlertAck />", () => {
|
||||
const tree = MountedAlertAck();
|
||||
const button = tree.find("span.badge");
|
||||
button.simulate("click");
|
||||
await fetchMock.flush(true);
|
||||
await act(async () => {
|
||||
await fetchMock.flush(true);
|
||||
});
|
||||
expect(toDiffableHtml(tree.html())).toMatch(/fa-check-circle/);
|
||||
});
|
||||
|
||||
it("sends a request on click", () => {
|
||||
MountAndClick();
|
||||
it("sends a POST request on click", async () => {
|
||||
await MountAndClick();
|
||||
expect(fetchMock.calls()).toHaveLength(1);
|
||||
expect(fetchMock.lastCall()[1]).toMatchObject({
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
});
|
||||
|
||||
it("doesn't send any request on click when already in progress", async () => {
|
||||
const tree = MountedAlertAck();
|
||||
const button = tree.find("span.badge");
|
||||
button.simulate("click");
|
||||
button.simulate("click");
|
||||
await fetchMock.flush(true);
|
||||
expect(fetchMock.calls()).toHaveLength(1);
|
||||
it("sends a POST request to every cluster", async () => {
|
||||
alertStore.data.upstreams = {
|
||||
clusters: { c1: ["m1", "m2"], c2: ["m3", "m4"] },
|
||||
instances: ["m1", "m2", "m3", "m4"].map((a) => ({
|
||||
name: a,
|
||||
uri: `http://${a}.example.com`,
|
||||
publicURI: `http://${a}.example.com`,
|
||||
readonly: false,
|
||||
headers: { "X-Cluster": a === "m1" || a === "2" ? "c1" : "c2" },
|
||||
corsCredentials: a === "m1" || a === "2" ? "same-site" : "include",
|
||||
error: "",
|
||||
version: "0.17.0",
|
||||
cluster: a === "m1" || a === "2" ? "c1" : "c2",
|
||||
clusterMembers: a === "m1" || a === "2" ? ["m1", "m2"] : ["m3", "m4"],
|
||||
})),
|
||||
};
|
||||
group.alertmanagerCount = {
|
||||
m1: 1,
|
||||
m2: 1,
|
||||
m3: 1,
|
||||
m4: 1,
|
||||
};
|
||||
|
||||
await MountAndClick();
|
||||
expect(fetchMock.calls()).toHaveLength(2);
|
||||
expect(fetchMock.calls()[0][0]).toBe(
|
||||
"http://m1.example.com/api/v2/silences"
|
||||
);
|
||||
expect(fetchMock.calls()[0][1]).toMatchObject({
|
||||
method: "POST",
|
||||
credentials: "same-site",
|
||||
headers: { "X-Cluster": "c1" },
|
||||
});
|
||||
expect(fetchMock.calls()[1][0]).toBe(
|
||||
"http://m3.example.com/api/v2/silences"
|
||||
);
|
||||
expect(fetchMock.calls()[1][1]).toMatchObject({
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "X-Cluster": "c2" },
|
||||
});
|
||||
});
|
||||
|
||||
it("skips readonly alertmanagers", async () => {
|
||||
alertStore.data.upstreams = {
|
||||
clusters: { c1: ["m1", "m2"], c2: ["m3", "m4"] },
|
||||
instances: ["m1", "m2", "m3", "m4"].map((a) => ({
|
||||
name: a,
|
||||
uri: `http://${a}.example.com`,
|
||||
publicURI: `http://${a}.example.com`,
|
||||
readonly: a === "m1" || a === "m3" ? true : false,
|
||||
headers: { "X-Cluster": a === "m1" || a === "2" ? "c1" : "c2" },
|
||||
corsCredentials: a === "m1" || a === "m2" ? "same-site" : "include",
|
||||
error: "",
|
||||
version: "0.17.0",
|
||||
cluster: a === "m1" || a === "2" ? "c1" : "c2",
|
||||
clusterMembers: a === "m1" || a === "2" ? ["m1", "m2"] : ["m3", "m4"],
|
||||
})),
|
||||
};
|
||||
group.alertmanagerCount = {
|
||||
m1: 1,
|
||||
m2: 1,
|
||||
m3: 1,
|
||||
m4: 1,
|
||||
};
|
||||
|
||||
await MountAndClick();
|
||||
expect(fetchMock.calls()).toHaveLength(2);
|
||||
expect(fetchMock.calls()[0][0]).toBe(
|
||||
"http://m2.example.com/api/v2/silences"
|
||||
);
|
||||
expect(fetchMock.calls()[1][0]).toBe(
|
||||
"http://m4.example.com/api/v2/silences"
|
||||
);
|
||||
});
|
||||
|
||||
it("doesn't send any request on click when already done", async () => {
|
||||
@@ -148,19 +226,16 @@ describe("<AlertAck />", () => {
|
||||
const button = tree.find("span.badge");
|
||||
|
||||
button.simulate("click");
|
||||
await fetchMock.flush(true);
|
||||
await act(async () => {
|
||||
await fetchMock.flush(true);
|
||||
});
|
||||
expect(fetchMock.calls()).toHaveLength(1);
|
||||
|
||||
button.simulate("click");
|
||||
expect(fetchMock.calls()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("sends POST requests", () => {
|
||||
MountAndClick();
|
||||
expect(fetchMock.calls()[0][1].method).toBe("POST");
|
||||
});
|
||||
|
||||
it("sends correct payload", () => {
|
||||
it("sends correct payload", async () => {
|
||||
fetchMock.any(
|
||||
{
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -172,7 +247,7 @@ describe("<AlertAck />", () => {
|
||||
);
|
||||
|
||||
silenceFormStore.data.author = "karma/ui";
|
||||
MountAndClick();
|
||||
await MountAndClick();
|
||||
expect(JSON.parse(fetchMock.calls()[0][1].body)).toEqual({
|
||||
comment:
|
||||
"PREFIX This alert was acknowledged using karma on Tue Feb 01 2000 00:00:00 GMT+0000",
|
||||
@@ -186,11 +261,11 @@ describe("<AlertAck />", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses settings when generating payload", () => {
|
||||
it("uses settings when generating payload", async () => {
|
||||
alertStore.settings.values.alertAcknowledgement.durationSeconds = 237;
|
||||
alertStore.settings.values.alertAcknowledgement.author = "me";
|
||||
alertStore.settings.values.alertAcknowledgement.commentPrefix = "";
|
||||
MountAndClick();
|
||||
await MountAndClick();
|
||||
expect(JSON.parse(fetchMock.calls()[0][1].body)).toEqual({
|
||||
comment:
|
||||
"This alert was acknowledged using karma on Tue Feb 01 2000 00:00:00 GMT+0000",
|
||||
@@ -204,13 +279,13 @@ describe("<AlertAck />", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses author from authentication info when auth is enabled", () => {
|
||||
it("uses author from authentication info when auth is enabled", async () => {
|
||||
alertStore.info.authentication.enabled = true;
|
||||
alertStore.info.authentication.username = "auth@example.com";
|
||||
alertStore.settings.values.alertAcknowledgement.durationSeconds = 222;
|
||||
alertStore.settings.values.alertAcknowledgement.author = "me";
|
||||
alertStore.settings.values.alertAcknowledgement.commentPrefix = "FOO:";
|
||||
MountAndClick();
|
||||
await MountAndClick();
|
||||
expect(JSON.parse(fetchMock.calls()[0][1].body)).toEqual({
|
||||
comment:
|
||||
"FOO: This alert was acknowledged using karma on Tue Feb 01 2000 00:00:00 GMT+0000",
|
||||
@@ -224,14 +299,14 @@ describe("<AlertAck />", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses author from silenceFormStore if authentication is disabled", () => {
|
||||
it("uses author from silenceFormStore if authentication is disabled", async () => {
|
||||
alertStore.info.authentication.enabled = false;
|
||||
alertStore.info.authentication.username = "wrong";
|
||||
alertStore.settings.values.alertAcknowledgement.durationSeconds = 222;
|
||||
alertStore.settings.values.alertAcknowledgement.author = "me";
|
||||
alertStore.settings.values.alertAcknowledgement.commentPrefix = "FOO:";
|
||||
silenceFormStore.data.author = "bob@example.com";
|
||||
MountAndClick();
|
||||
await MountAndClick();
|
||||
expect(JSON.parse(fetchMock.calls()[0][1].body)).toEqual({
|
||||
comment:
|
||||
"FOO: This alert was acknowledged using karma on Tue Feb 01 2000 00:00:00 GMT+0000",
|
||||
@@ -245,12 +320,12 @@ describe("<AlertAck />", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses default author as fallback", () => {
|
||||
it("uses default author as fallback", async () => {
|
||||
alertStore.settings.values.alertAcknowledgement.durationSeconds = 222;
|
||||
alertStore.settings.values.alertAcknowledgement.author = "me";
|
||||
alertStore.settings.values.alertAcknowledgement.commentPrefix = "FOO:";
|
||||
silenceFormStore.data.author = "";
|
||||
MountAndClick();
|
||||
await MountAndClick();
|
||||
expect(JSON.parse(fetchMock.calls()[0][1].body)).toEqual({
|
||||
comment:
|
||||
"FOO: This alert was acknowledged using karma on Tue Feb 01 2000 00:00:00 GMT+0000",
|
||||
@@ -264,118 +339,136 @@ describe("<AlertAck />", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("sends POST request to /api/v2/silences", () => {
|
||||
MountAndClick();
|
||||
it("sends POST request to /api/v2/silences", async () => {
|
||||
await MountAndClick();
|
||||
const uri = fetchMock.calls()[0][0];
|
||||
expect(uri).toBe("http://localhost/api/v2/silences");
|
||||
});
|
||||
|
||||
it("will retry on another cluster member after 500 response", async () => {
|
||||
fetchMock.reset();
|
||||
fetchMock.mock("http://am2.example.com/api/v2/silences", {
|
||||
fetchMock.mock("http://m1.example.com/api/v2/silences", {
|
||||
status: 500,
|
||||
body: "error message",
|
||||
});
|
||||
fetchMock.mock("http://am1.example.com/api/v2/silences", {
|
||||
fetchMock.mock("http://m2.example.com/api/v2/silences", {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ silenceID: "123" }),
|
||||
});
|
||||
fetchMock.mock("http://m3.example.com/api/v2/silences", {
|
||||
status: 500,
|
||||
body: "error message",
|
||||
});
|
||||
fetchMock.mock("http://m4.example.com/api/v2/silences", {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ silenceID: "456" }),
|
||||
});
|
||||
alertStore.data.upstreams = {
|
||||
clusters: { default: ["default", "fallback"] },
|
||||
instances: [
|
||||
{
|
||||
name: "default",
|
||||
uri: "http://am1.example.com",
|
||||
publicURI: "http://am1.example.com",
|
||||
readonly: false,
|
||||
headers: {},
|
||||
corsCredentials: "include",
|
||||
error: "",
|
||||
version: "0.17.0",
|
||||
cluster: "default",
|
||||
clusterMembers: ["default", "fallback"],
|
||||
},
|
||||
{
|
||||
name: "fallback",
|
||||
uri: "http://am2.example.com",
|
||||
publicURI: "http://am2.example.com",
|
||||
readonly: false,
|
||||
headers: {},
|
||||
corsCredentials: "include",
|
||||
error: "",
|
||||
version: "0.17.0",
|
||||
cluster: "default",
|
||||
clusterMembers: ["default", "fallback"],
|
||||
},
|
||||
],
|
||||
clusters: { c1: ["m1", "m2"], c2: ["m3", "m4"] },
|
||||
instances: ["m1", "m2", "m3", "m4"].map((a) => ({
|
||||
name: a,
|
||||
uri: `http://${a}.example.com`,
|
||||
publicURI: `http://${a}.example.com`,
|
||||
readonly: false,
|
||||
headers: { "X-Cluster": a === "m1" || a === "2" ? "c1" : "c2" },
|
||||
corsCredentials: a === "m1" || a === "m2" ? "same-site" : "include",
|
||||
error: "",
|
||||
version: "0.17.0",
|
||||
cluster: a === "m1" || a === "2" ? "c1" : "c2",
|
||||
clusterMembers: a === "m1" || a === "2" ? ["m1", "m2"] : ["m3", "m4"],
|
||||
})),
|
||||
};
|
||||
group.alertmanagerCount = {
|
||||
m1: 1,
|
||||
m2: 1,
|
||||
m3: 1,
|
||||
m4: 1,
|
||||
};
|
||||
|
||||
const tree = MountedAlertAck();
|
||||
const button = tree.find("span.badge");
|
||||
button.simulate("click");
|
||||
await fetchMock.flush(true);
|
||||
await fetchMock.flush(true);
|
||||
await act(async () => {
|
||||
await fetchMock.flush(true);
|
||||
});
|
||||
expect(fetchMock.calls()).toHaveLength(4);
|
||||
expect(fetchMock.calls()[0][0]).toBe(
|
||||
"http://am2.example.com/api/v2/silences"
|
||||
"http://m1.example.com/api/v2/silences"
|
||||
);
|
||||
expect(fetchMock.calls()[1][0]).toBe(
|
||||
"http://am1.example.com/api/v2/silences"
|
||||
"http://m2.example.com/api/v2/silences"
|
||||
);
|
||||
expect(fetchMock.calls()[2][0]).toBe(
|
||||
"http://m3.example.com/api/v2/silences"
|
||||
);
|
||||
expect(fetchMock.calls()[3][0]).toBe(
|
||||
"http://m4.example.com/api/v2/silences"
|
||||
);
|
||||
});
|
||||
|
||||
it("will retry on another cluster member after fetch failure", async () => {
|
||||
fetchMock.reset();
|
||||
fetchMock.mock("http://am2.example.com/api/v2/silences", {
|
||||
fetchMock.mock("http://m1.example.com/api/v2/silences", {
|
||||
throws: new TypeError("failed to fetch"),
|
||||
});
|
||||
fetchMock.mock("http://am1.example.com/api/v2/silences", {
|
||||
fetchMock.mock("http://m2.example.com/api/v2/silences", {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ silenceID: "123" }),
|
||||
});
|
||||
fetchMock.mock("http://m3.example.com/api/v2/silences", {
|
||||
throws: new TypeError("failed to fetch"),
|
||||
});
|
||||
fetchMock.mock("http://m4.example.com/api/v2/silences", {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ silenceID: "123" }),
|
||||
});
|
||||
alertStore.data.upstreams = {
|
||||
clusters: { default: ["default", "fallback"] },
|
||||
instances: [
|
||||
{
|
||||
name: "default",
|
||||
uri: "http://am1.example.com",
|
||||
publicURI: "http://am1.example.com",
|
||||
readonly: false,
|
||||
headers: {},
|
||||
corsCredentials: "include",
|
||||
error: "",
|
||||
version: "0.17.0",
|
||||
cluster: "default",
|
||||
clusterMembers: ["default", "fallback"],
|
||||
},
|
||||
{
|
||||
name: "fallback",
|
||||
uri: "http://am2.example.com",
|
||||
publicURI: "http://am2.example.com",
|
||||
readonly: false,
|
||||
headers: {},
|
||||
corsCredentials: "include",
|
||||
error: "",
|
||||
version: "0.17.0",
|
||||
cluster: "default",
|
||||
clusterMembers: ["default", "fallback"],
|
||||
},
|
||||
],
|
||||
clusters: { c1: ["m1", "m2"], c2: ["m3", "m4"] },
|
||||
instances: ["m1", "m2", "m3", "m4"].map((a) => ({
|
||||
name: a,
|
||||
uri: `http://${a}.example.com`,
|
||||
publicURI: `http://${a}.example.com`,
|
||||
readonly: false,
|
||||
headers: { "X-Cluster": a === "m1" || a === "2" ? "c1" : "c2" },
|
||||
corsCredentials: a === "m1" || a === "m2" ? "same-site" : "include",
|
||||
error: "",
|
||||
version: "0.17.0",
|
||||
cluster: a === "m1" || a === "2" ? "c1" : "c2",
|
||||
clusterMembers: a === "m1" || a === "2" ? ["m1", "m2"] : ["m3", "m4"],
|
||||
})),
|
||||
};
|
||||
group.alertmanagerCount = {
|
||||
m1: 1,
|
||||
m2: 1,
|
||||
m3: 1,
|
||||
m4: 1,
|
||||
};
|
||||
|
||||
const tree = MountedAlertAck();
|
||||
const button = tree.find("span.badge");
|
||||
button.simulate("click");
|
||||
await fetchMock.flush(true);
|
||||
await fetchMock.flush(true);
|
||||
await act(async () => {
|
||||
await fetchMock.flush(true);
|
||||
});
|
||||
await act(async () => {
|
||||
await fetchMock.flush(true);
|
||||
});
|
||||
expect(fetchMock.calls()).toHaveLength(4);
|
||||
expect(fetchMock.calls()[0][0]).toBe(
|
||||
"http://am2.example.com/api/v2/silences"
|
||||
"http://m1.example.com/api/v2/silences"
|
||||
);
|
||||
expect(fetchMock.calls()[1][0]).toBe(
|
||||
"http://am1.example.com/api/v2/silences"
|
||||
"http://m2.example.com/api/v2/silences"
|
||||
);
|
||||
expect(fetchMock.calls()[2][0]).toBe(
|
||||
"http://m3.example.com/api/v2/silences"
|
||||
);
|
||||
expect(fetchMock.calls()[3][0]).toBe(
|
||||
"http://m4.example.com/api/v2/silences"
|
||||
);
|
||||
});
|
||||
|
||||
it("will log an error if Alertmanager instance is missing from instances and try the next one", () => {
|
||||
it("will log an error if Alertmanager instance is missing from instances and try the next one", async () => {
|
||||
const consoleSpy = jest
|
||||
.spyOn(console, "error")
|
||||
.mockImplementation(() => {});
|
||||
@@ -400,6 +493,10 @@ describe("<AlertAck />", () => {
|
||||
const tree = MountedAlertAck();
|
||||
const button = tree.find("span.badge");
|
||||
button.simulate("click");
|
||||
await act(async () => {
|
||||
await fetchMock.flush(true);
|
||||
});
|
||||
expect(fetchMock.calls()).toHaveLength(1);
|
||||
expect(fetchMock.calls()[0][0]).toBe(
|
||||
"http://am1.example.com/api/v2/silences"
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
import merge from "lodash.merge";
|
||||
|
||||
@@ -6,10 +6,22 @@ import { CommonOptions } from "Common/Fetch";
|
||||
|
||||
const useFetchAny = (upstreams) => {
|
||||
const [index, setIndex] = useState(0);
|
||||
const [response, setResponse] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [inProgress, setInProgress] = useState(true);
|
||||
const [responseURI, setResponseURI] = useState(null);
|
||||
const [response, setResponse] = useState({
|
||||
response: null,
|
||||
error: null,
|
||||
responseURI: null,
|
||||
inProgress: false,
|
||||
});
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setIndex(0);
|
||||
setResponse({
|
||||
response: null,
|
||||
error: null,
|
||||
responseURI: null,
|
||||
inProgress: false,
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// https://dev.to/pallymore/clean-up-async-requests-in-useeffect-hooks-90h
|
||||
@@ -18,8 +30,13 @@ const useFetchAny = (upstreams) => {
|
||||
const fetchData = async () => {
|
||||
const { uri, options } = upstreams[index];
|
||||
|
||||
setResponse({
|
||||
response: null,
|
||||
error: null,
|
||||
responseURI: null,
|
||||
inProgress: true,
|
||||
});
|
||||
try {
|
||||
setInProgress(true);
|
||||
const res = await fetch(
|
||||
uri,
|
||||
merge({}, { method: "GET" }, CommonOptions, options)
|
||||
@@ -35,15 +52,22 @@ const useFetchAny = (upstreams) => {
|
||||
}
|
||||
|
||||
if (res.ok) {
|
||||
setResponse(body);
|
||||
setResponseURI(uri);
|
||||
setInProgress(false);
|
||||
setResponse({
|
||||
response: body,
|
||||
error: null,
|
||||
responseURI: uri,
|
||||
inProgress: false,
|
||||
});
|
||||
} else {
|
||||
if (upstreams.length > index + 1) {
|
||||
setIndex(index + 1);
|
||||
} else {
|
||||
setError(body);
|
||||
setInProgress(false);
|
||||
setResponse({
|
||||
response: null,
|
||||
error: body,
|
||||
responseURI: null,
|
||||
inProgress: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,8 +76,12 @@ const useFetchAny = (upstreams) => {
|
||||
if (upstreams.length > index + 1) {
|
||||
setIndex(index + 1);
|
||||
} else {
|
||||
setError(error.message);
|
||||
setInProgress(false);
|
||||
setResponse({
|
||||
response: null,
|
||||
error: error.message,
|
||||
responseURI: null,
|
||||
inProgress: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,15 +90,15 @@ const useFetchAny = (upstreams) => {
|
||||
if (upstreams.length > 0) {
|
||||
fetchData();
|
||||
} else {
|
||||
setInProgress(false);
|
||||
reset();
|
||||
}
|
||||
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
}, [upstreams, index]);
|
||||
}, [upstreams, index, reset]);
|
||||
|
||||
return { response, error, inProgress, responseURI };
|
||||
return { ...response, reset };
|
||||
};
|
||||
|
||||
export { useFetchAny };
|
||||
|
||||
Reference in New Issue
Block a user