mirror of
https://github.com/prymitive/karma
synced 2026-05-05 03:16:51 +00:00
fix(ui): drop useLocalStore from SilenceSubmitProgress
This commit is contained in:
committed by
Łukasz Mierzwa
parent
b09dd6f8c9
commit
f063038c30
@@ -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 ? (
|
||||
<FontAwesomeIcon icon={faCheckCircle} className="text-success" />
|
||||
) : stateValue === SubmitState.Failed ? (
|
||||
<FontAwesomeIcon icon={faExclamationCircle} className="text-danger" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCircleNotch} spin />
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const SilenceLink = ({ uri, silenceID }) => (
|
||||
<a
|
||||
href={`${uri}/#/silences/${silenceID}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{silenceID}
|
||||
</a>
|
||||
);
|
||||
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 = <SilenceLink uri={uri} silenceID={response.silenceID} />;
|
||||
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 (
|
||||
<div className="d-flex mb-2">
|
||||
<div className="p-2 flex-fill my-auto flex-grow-0 flex-shrink-0">
|
||||
<SubmitIcon stateValue={submitState.value} />
|
||||
{inProgress ? (
|
||||
<FontAwesomeIcon icon={faCircleNotch} spin />
|
||||
) : error ? (
|
||||
<FontAwesomeIcon icon={faExclamationCircle} className="text-danger" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCheckCircle} className="text-success" />
|
||||
)}
|
||||
</div>
|
||||
<div className="p-2 mr-1 flex-fill my-auto flex-grow-0 flex-shrink-0">
|
||||
{cluster}
|
||||
</div>
|
||||
<div
|
||||
className={`p-2 flex-fill flex-grow-1 flex-shrink-1 rounded text-center ${
|
||||
submitState.value === SubmitState.Failed ? "bg-light" : ""
|
||||
error ? "bg-light" : ""
|
||||
}`}
|
||||
>
|
||||
{submitState.result}
|
||||
{error ? (
|
||||
error
|
||||
) : response && responseURI ? (
|
||||
<a
|
||||
href={`${publicURIs[responseURI]}/#/silences/${response.silenceID}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{response.silenceID}
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
);
|
||||
};
|
||||
SilenceSubmitProgress.propTypes = {
|
||||
cluster: PropTypes.string.isRequired,
|
||||
|
||||
@@ -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("<SilenceSubmitProgress />", () => {
|
||||
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("<SilenceSubmitProgress />", () => {
|
||||
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("<SilenceSubmitProgress />", () => {
|
||||
mount(
|
||||
<SilenceSubmitProgress
|
||||
cluster="ha"
|
||||
members={["am1", "am2"]}
|
||||
members={["am2", "am1"]}
|
||||
payload={{
|
||||
matchers: [],
|
||||
startsAt: "now",
|
||||
@@ -157,7 +166,9 @@ describe("<SilenceSubmitProgress />", () => {
|
||||
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("<SilenceSubmitProgress />", () => {
|
||||
const tree = mount(
|
||||
<SilenceSubmitProgress
|
||||
cluster="ha"
|
||||
members={["am1", "am2"]}
|
||||
members={["am2", "am1"]}
|
||||
payload={{
|
||||
matchers: [],
|
||||
startsAt: "now",
|
||||
@@ -219,7 +230,11 @@ describe("<SilenceSubmitProgress />", () => {
|
||||
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("<SilenceSubmitProgress />", () => {
|
||||
mount(
|
||||
<SilenceSubmitProgress
|
||||
cluster="ha"
|
||||
members={["am1", "am2"]}
|
||||
members={["am2", "am1"]}
|
||||
payload={{
|
||||
matchers: [],
|
||||
startsAt: "now",
|
||||
@@ -260,7 +275,11 @@ describe("<SilenceSubmitProgress />", () => {
|
||||
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("<SilenceSubmitProgress />", () => {
|
||||
mount(
|
||||
<SilenceSubmitProgress
|
||||
cluster="ha"
|
||||
members={["am1", "am2"]}
|
||||
members={["am2", "am1"]}
|
||||
payload={{
|
||||
matchers: [],
|
||||
startsAt: "now",
|
||||
@@ -290,7 +309,11 @@ describe("<SilenceSubmitProgress />", () => {
|
||||
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("<SilenceSubmitProgress />", () => {
|
||||
mount(
|
||||
<SilenceSubmitProgress
|
||||
cluster="ha"
|
||||
members={["am1", "am2"]}
|
||||
members={["am2", "am1"]}
|
||||
payload={{
|
||||
matchers: [],
|
||||
startsAt: "now",
|
||||
@@ -347,7 +370,11 @@ describe("<SilenceSubmitProgress />", () => {
|
||||
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("<SilenceSubmitProgress />", () => {
|
||||
mount(
|
||||
<SilenceSubmitProgress
|
||||
cluster="ha"
|
||||
members={["am1", "am2"]}
|
||||
members={["am2", "am1"]}
|
||||
payload={{
|
||||
matchers: [],
|
||||
startsAt: "now",
|
||||
@@ -418,7 +445,11 @@ describe("<SilenceSubmitProgress />", () => {
|
||||
|
||||
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("<SilenceSubmitProgress />", () => {
|
||||
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("<SilenceSubmitProgress />", () => {
|
||||
|
||||
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("<SilenceSubmitProgress />", () => {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user