diff --git a/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.js b/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.js
index 9648910ba..bbf3e3f95 100644
--- a/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.js
+++ b/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.js
@@ -1,8 +1,6 @@
-import React, { useEffect, useCallback } from "react";
+import React, { useEffect, useState } from "react";
import PropTypes from "prop-types";
-import { useObserver, useLocalStore } from "mobx-react";
-
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons/faCheckCircle";
@@ -10,139 +8,80 @@ import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclama
import { APISilenceMatcher } from "Models/API";
import { AlertStore } from "Stores/AlertStore";
-import { FetchPost } from "Common/Fetch";
-
-const SubmitState = Object.freeze({
- InProgress: "InProgress",
- Done: "Done",
- Failed: "Failed",
-});
-
-const SubmitIcon = ({ stateValue }) => {
- return useObserver(() =>
- stateValue === SubmitState.Done ? (
-
- ) : stateValue === SubmitState.Failed ? (
-
- ) : (
-
- )
- );
-};
-
-const SilenceLink = ({ uri, silenceID }) => (
-
- {silenceID}
-
-);
-SilenceLink.propTypes = {
- uri: PropTypes.string.isRequired,
- silenceID: PropTypes.string.isRequired,
-};
+import { useFetchAny } from "Hooks/useFetchAny";
const SilenceSubmitProgress = ({ alertStore, cluster, members, payload }) => {
- const submitState = useLocalStore(() => ({
- membersToTry: [],
- value: SubmitState.InProgress,
- result: null,
- markDone(result) {
- this.result = result;
- this.value = SubmitState.Done;
- },
- markFailed(result) {
- this.result = result;
- this.value = SubmitState.Failed;
- },
- }));
-
- const handleAlertmanagerRequest = useCallback(() => {
- const member = submitState.membersToTry.pop();
-
- if (alertStore.data.isReadOnlyAlertmanager(member)) {
- const err = `Alertmanager instance "${member}" is read-only`;
- console.error(err);
- if (submitState.membersToTry.length) {
- return handleAlertmanagerRequest();
- } else {
- submitState.markFailed(err.message);
- }
- return;
- }
-
- const am = alertStore.data.getAlertmanagerByName(member);
- if (am === undefined) {
- const err = `Alertmanager instance "${member}" not found`;
- console.error(err);
- if (submitState.membersToTry.length) {
- return handleAlertmanagerRequest();
- } else {
- submitState.markFailed(err.message);
- }
- return;
- }
-
- const parseOpenAPIResponse = (uri, response) => {
- const link = ;
- submitState.markDone(link);
- // return silenceID so we can assert it in tests
- return response.silenceID;
- };
-
- FetchPost(`${am.uri}/api/v2/silences`, {
- body: JSON.stringify(payload),
- credentials: am.corsCredentials,
- headers: {
- "Content-Type": "application/json",
- ...am.headers,
- },
- })
- .then((result) => {
- if (result.ok) {
- return result
- .json()
- .then((r) => parseOpenAPIResponse(am.publicURI, r));
- } else {
- return result.text().then((text) => {
- submitState.markFailed(text);
- return text;
- });
- }
- })
- .catch((err) => {
- if (submitState.membersToTry.length) {
- return handleAlertmanagerRequest();
- } else {
- submitState.markFailed(err.message);
- }
- });
- }, [alertStore.data, payload, submitState]);
+ const [upstreams, setUpstreams] = useState([]);
+ const { response, error, inProgress, responseURI } = useFetchAny(upstreams);
+ const [publicURIs, setPublicURIs] = useState({});
useEffect(() => {
- submitState.membersToTry = [...members];
- handleAlertmanagerRequest();
- }, []); // eslint-disable-line react-hooks/exhaustive-deps
+ let uris = {};
+ let membersToTry = [];
+ for (const member of members) {
+ if (alertStore.data.isReadOnlyAlertmanager(member)) {
+ console.error(`Alertmanager instance "${member}" is read-only`);
+ } else {
+ const am = alertStore.data.getAlertmanagerByName(member);
+ if (am === undefined) {
+ console.error(`Alertmanager instance "${member}" not found`);
+ } else {
+ const uri = `${am.uri}/api/v2/silences`;
+ membersToTry.push({
+ uri: uri,
+ options: {
+ method: "POST",
+ body: JSON.stringify(payload),
+ credentials: am.corsCredentials,
+ headers: {
+ "Content-Type": "application/json",
+ ...am.headers,
+ },
+ },
+ });
+ uris[uri] = am.publicURI;
+ }
+ }
+ }
+ if (membersToTry.length) {
+ setPublicURIs(uris);
+ setUpstreams(membersToTry);
+ }
+ }, [alertStore.data, members, payload]);
- return useObserver(() => (
+ return (
-
+ {inProgress ? (
+
+ ) : error ? (
+
+ ) : (
+
+ )}
{cluster}
- ));
+ );
};
SilenceSubmitProgress.propTypes = {
cluster: PropTypes.string.isRequired,
diff --git a/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.test.js b/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.test.js
index 7d74fe4a5..ec9b8ade6 100644
--- a/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.test.js
+++ b/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.test.js
@@ -1,4 +1,5 @@
import React from "react";
+import { act } from "react-dom/test-utils";
import { mount } from "enzyme";
@@ -65,20 +66,26 @@ const MountedSilenceSubmitProgress = () => {
describe("", () => {
it("sends a request on mount", async () => {
MountedSilenceSubmitProgress();
- await fetchMock.flush(true);
+ await act(async () => {
+ await fetchMock.flush(true);
+ });
expect(fetchMock.calls()).toHaveLength(1);
});
it("appends /api/v2/silences to the passed URI", async () => {
MountedSilenceSubmitProgress();
- await fetchMock.flush(true);
+ await act(async () => {
+ await fetchMock.flush(true);
+ });
const uri = fetchMock.calls()[0][0];
expect(uri).toBe("http://localhost/api/v2/silences");
});
it("sends correct JSON payload", async () => {
MountedSilenceSubmitProgress();
- await fetchMock.flush(true);
+ await act(async () => {
+ await fetchMock.flush(true);
+ });
const payload = fetchMock.calls()[0][1];
expect(payload).toMatchObject({
method: "POST",
@@ -96,7 +103,9 @@ describe("", () => {
it("uses CORS credentials from alertmanager config", async () => {
alertStore.data.upstreams.instances[0].corsCredentials = "same-origin";
MountedSilenceSubmitProgress();
- await fetchMock.flush(true);
+ await act(async () => {
+ await fetchMock.flush(true);
+ });
expect(fetchMock.calls()[0][0]).toBe("http://localhost/api/v2/silences");
expect(fetchMock.calls()[0][1]).toMatchObject({
credentials: "same-origin",
@@ -146,7 +155,7 @@ describe("", () => {
mount(
", () => {
alertStore={alertStore}
/>
);
- await fetchMock.flush(true);
+ await act(async () => {
+ await fetchMock.flush(true);
+ });
expect(fetchMock.calls()[0][0]).toBe(
"http://am2.example.com/api/v2/silences"
);
@@ -208,7 +219,7 @@ describe("", () => {
const tree = mount(
", () => {
alertStore={alertStore}
/>
);
- await fetchMock.flush(true);
+ await act(async () => {
+ await act(async () => {
+ await fetchMock.flush(true);
+ });
+ });
expect(fetchMock.calls()).toHaveLength(2);
expect(tree.text()).toBe("hafailed to fetch from am1");
});
@@ -249,7 +264,7 @@ describe("", () => {
mount(
", () => {
alertStore={alertStore}
/>
);
- await fetchMock.flush(true);
+ await act(async () => {
+ await act(async () => {
+ await fetchMock.flush(true);
+ });
+ });
expect(fetchMock.calls()[0][0]).toBe(
"http://am1.example.com/api/v2/silences"
);
@@ -279,7 +298,7 @@ describe("", () => {
mount(
", () => {
alertStore={alertStore}
/>
);
- await fetchMock.flush(true);
+ await act(async () => {
+ await act(async () => {
+ await fetchMock.flush(true);
+ });
+ });
expect(fetchMock.calls()).toHaveLength(0);
expect(consoleSpy).toHaveBeenCalledTimes(2);
});
@@ -336,7 +359,7 @@ describe("", () => {
mount(
", () => {
alertStore={alertStore}
/>
);
- await fetchMock.flush(true);
+ await act(async () => {
+ 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"
@@ -397,7 +424,7 @@ describe("", () => {
mount(
", () => {
it("renders returned silence ID on successful fetch", async () => {
const tree = MountedSilenceSubmitProgress();
- await fetchMock.flush(true);
+ await act(async () => {
+ await act(async () => {
+ await fetchMock.flush(true);
+ });
+ });
// force re-render
tree.update();
const silenceLink = tree.find("a");
@@ -433,13 +464,21 @@ describe("", () => {
body: "mock error message",
});
const tree = MountedSilenceSubmitProgress();
- await fetchMock.flush(true);
+ await act(async () => {
+ await act(async () => {
+ await fetchMock.flush(true);
+ });
+ });
expect(tree.text()).toBe("mockAlertmanagermock error message");
});
it("renders success icon on successful fetch", async () => {
const tree = MountedSilenceSubmitProgress();
- await fetchMock.flush(true);
+ await act(async () => {
+ await act(async () => {
+ await fetchMock.flush(true);
+ });
+ });
tree.update();
expect(tree.find("FontAwesomeIcon.text-success")).toHaveLength(1);
expect(tree.find("FontAwesomeIcon.text-danger")).toHaveLength(0);
@@ -447,7 +486,11 @@ describe("", () => {
it("renders silence link on successful fetch", async () => {
const tree = MountedSilenceSubmitProgress();
- await fetchMock.flush(true);
+ await act(async () => {
+ await act(async () => {
+ await fetchMock.flush(true);
+ });
+ });
tree.update();
expect(tree.find("a").getDOMNode().getAttribute("href")).toBe(
"http://example.com/#/silences/123456789"
@@ -461,7 +504,11 @@ describe("", () => {
body: "error message",
});
const tree = MountedSilenceSubmitProgress();
- await fetchMock.flush(true);
+ await act(async () => {
+ await act(async () => {
+ await fetchMock.flush(true);
+ });
+ });
tree.update();
expect(tree.find("FontAwesomeIcon.text-success")).toHaveLength(0);
expect(tree.find("FontAwesomeIcon.text-danger")).toHaveLength(1);