diff --git a/ui/src/Components/AlertAck/index.js b/ui/src/Components/AlertAck/index.js
index 74df2fafc..a9125b214 100644
--- a/ui/src/Components/AlertAck/index.js
+++ b/ui/src/Components/AlertAck/index.js
@@ -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 : (
{
+ if (!isAcking && !(response || error)) {
+ setIsAcking(true);
+ onACK();
+ }
+ }}
>
- {submitState.isIdle ? (
-
- ) : submitState.isInprogress ? (
-
- ) : submitState.isFailed ? (
+ {!isAcking && error ? (
- ) : (
+ ) : !isAcking && response ? (
+ ) : isAcking ? (
+
+ ) : (
+
)}
diff --git a/ui/src/Components/AlertAck/index.test.js b/ui/src/Components/AlertAck/index.test.js
index ff67e95da..008392c12 100644
--- a/ui/src/Components/AlertAck/index.test.js
+++ b/ui/src/Components/AlertAck/index.test.js
@@ -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("", () => {
@@ -117,7 +120,9 @@ describe("", () => {
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("", () => {
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("", () => {
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("", () => {
);
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("", () => {
});
});
- 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("", () => {
});
});
- 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("", () => {
});
});
- 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("", () => {
});
});
- 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("", () => {
});
});
- 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("", () => {
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"
);
diff --git a/ui/src/Hooks/useFetchAny.js b/ui/src/Hooks/useFetchAny.js
index 26e3df806..841be7ecc 100644
--- a/ui/src/Hooks/useFetchAny.js
+++ b/ui/src/Hooks/useFetchAny.js
@@ -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 };