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}
- {submitState.result} + {error ? ( + error + ) : response && responseURI ? ( + + {response.silenceID} + + ) : null}
- )); + ); }; 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);