fix(ui): avoid updates on silence components re-rerenders

Fixes #1878
This commit is contained in:
Łukasz Mierzwa
2020-06-23 14:45:12 +01:00
committed by Łukasz Mierzwa
parent 6a4cbe80f9
commit d913232869
6 changed files with 126 additions and 122 deletions

View File

@@ -145,8 +145,7 @@ describe("<SilenceModalContent /> Editor", () => {
it("renders SilenceSubmitController when silenceFormStore.data.currentStage is 'Submit'", () => {
silenceFormStore.data.currentStage = SilenceFormStage.Submit;
const tree = MountedSilenceModalContent();
const ctrl = tree.find("SilenceSubmitController");
expect(ctrl).toHaveLength(1);
expect(tree.html()).toMatchSnapshot();
});
});

View File

@@ -1,4 +1,4 @@
import React from "react";
import React, { memo } from "react";
import PropTypes from "prop-types";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -8,7 +8,7 @@ import { AlertStore } from "Stores/AlertStore";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { SilenceSubmitProgress } from "./SilenceSubmitProgress";
const SilenceSubmitController = ({ silenceFormStore, alertStore }) => {
const SilenceSubmitController = memo(({ silenceFormStore, alertStore }) => {
return (
<React.Fragment>
<div>
@@ -34,7 +34,7 @@ const SilenceSubmitController = ({ silenceFormStore, alertStore }) => {
</div>
</React.Fragment>
);
};
});
SilenceSubmitController.propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,

View File

@@ -31,8 +31,7 @@ describe("<SilenceSubmitController />", () => {
value: ["am2", "am3"],
});
const tree = ShallowSilenceSubmitController();
const alertmanagers = tree.find("SilenceSubmitProgress");
expect(alertmanagers).toHaveLength(2);
expect(tree.find("div").at(0).children()).toHaveLength(2);
});
it("resets the form on 'Back' button click", () => {

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useState, memo } from "react";
import PropTypes from "prop-types";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -10,79 +10,84 @@ import { APISilenceMatcher } from "Models/API";
import { AlertStore } from "Stores/AlertStore";
import { useFetchAny } from "Hooks/useFetchAny";
const SilenceSubmitProgress = ({ alertStore, cluster, members, payload }) => {
const [upstreams, setUpstreams] = useState([]);
const { response, error, inProgress, responseURI } = useFetchAny(upstreams);
const [publicURIs, setPublicURIs] = useState({});
const SilenceSubmitProgress = memo(
({ alertStore, cluster, members, payload }) => {
const [upstreams, setUpstreams] = useState([]);
const { response, error, inProgress, responseURI } = useFetchAny(upstreams);
const [publicURIs, setPublicURIs] = useState({});
useEffect(() => {
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`);
useEffect(() => {
let uris = {};
let membersToTry = [];
for (const member of members) {
if (alertStore.data.isReadOnlyAlertmanager(member)) {
console.error(`Alertmanager instance "${member}" is read-only`);
} 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,
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;
});
uris[uri] = am.publicURI;
}
}
}
}
if (membersToTry.length) {
setPublicURIs(uris);
setUpstreams(membersToTry);
}
}, [alertStore.data, members, payload]);
if (membersToTry.length) {
setPublicURIs(uris);
setUpstreams(membersToTry);
}
}, [alertStore.data, members, payload]);
return (
<div className="d-flex mb-2">
<div className="p-2 flex-fill my-auto flex-grow-0 flex-shrink-0">
{inProgress ? (
<FontAwesomeIcon icon={faCircleNotch} spin />
) : error ? (
<FontAwesomeIcon icon={faExclamationCircle} className="text-danger" />
) : (
<FontAwesomeIcon icon={faCheckCircle} className="text-success" />
)}
return (
<div className="d-flex mb-2">
<div className="p-2 flex-fill my-auto flex-grow-0 flex-shrink-0">
{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 ${
error ? "bg-light" : ""
}`}
>
{error ? (
error
) : response && responseURI ? (
<a
href={`${publicURIs[responseURI]}/#/silences/${response.silenceID}`}
target="_blank"
rel="noopener noreferrer"
>
{response.silenceID}
</a>
) : null}
</div>
</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 ${
error ? "bg-light" : ""
}`}
>
{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,
members: PropTypes.arrayOf(PropTypes.string).isRequired,

View File

@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<SilenceModalContent /> Editor renders SilenceSubmitController when silenceFormStore.data.currentStage is 'Submit' 1`] = `"<div class=\\"modal-header py-2\\"><nav class=\\"nav nav-pills nav-justified w-100\\"><span class=\\"nav-item nav-link cursor-pointer mx-1 px-2 active\\">Silence submitted</span><span class=\\"nav-item nav-link cursor-pointer mx-1 px-2 components-tab-inactive\\">Browse</span><button type=\\"button\\" class=\\"close\\"><span>×</span></button></nav></div><div class=\\"modal-body \\"><div></div><div class=\\"d-flex flex-row-reverse\\"><button type=\\"button\\" class=\\"btn btn-primary\\"><svg aria-hidden=\\"true\\" focusable=\\"false\\" data-prefix=\\"fas\\" data-icon=\\"arrow-left\\" class=\\"svg-inline--fa fa-arrow-left fa-w-14 pr-1\\" role=\\"img\\" xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 448 512\\"><path fill=\\"currentColor\\" d=\\"M257.5 445.1l-22.2 22.2c-9.4 9.4-24.6 9.4-33.9 0L7 273c-9.4-9.4-9.4-24.6 0-33.9L201.4 44.7c9.4-9.4 24.6-9.4 33.9 0l22.2 22.2c9.5 9.5 9.3 25-.4 34.3L136.6 216H424c13.3 0 24 10.7 24 24v32c0 13.3-10.7 24-24 24H136.6l120.5 114.8c9.8 9.3 10 24.8.4 34.3z\\"></path></svg>Back</button></div></div>"`;

View File

@@ -1,7 +1,7 @@
import React from "react";
import PropTypes from "prop-types";
import { observer } from "mobx-react-lite";
import { useObserver } from "mobx-react-lite";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBellSlash } from "@fortawesome/free-solid-svg-icons/faBellSlash";
@@ -20,55 +20,53 @@ const SilenceModalContent = React.lazy(() =>
}))
);
const SilenceModal = observer(
({ alertStore, silenceFormStore, settingsStore }) => {
const onDeleteModalClose = React.useCallback(() => {
const event = new CustomEvent("remountModal");
window.dispatchEvent(event);
}, []);
const SilenceModal = ({ alertStore, silenceFormStore, settingsStore }) => {
const onDeleteModalClose = React.useCallback(() => {
const event = new CustomEvent("remountModal");
window.dispatchEvent(event);
}, []);
return (
<React.Fragment>
<li
className={`nav-item components-navbar-button ${
silenceFormStore.toggle.visible ? "border-info" : ""
}`}
>
<TooltipWrapper title="New silence">
<span
id="components-new-silence"
className="nav-link cursor-pointer"
onClick={silenceFormStore.toggle.toggle}
>
<FontAwesomeIcon icon={faBellSlash} />
</span>
</TooltipWrapper>
</li>
<Modal
isOpen={silenceFormStore.toggle.visible}
toggleOpen={silenceFormStore.toggle.toggle}
onExited={silenceFormStore.data.resetProgress}
>
<React.Suspense
fallback={
<h1 className="display-1 text-placeholder p-5 m-auto">
<FontAwesomeIcon icon={faSpinner} size="lg" spin />
</h1>
}
return useObserver(() => (
<React.Fragment>
<li
className={`nav-item components-navbar-button ${
silenceFormStore.toggle.visible ? "border-info" : ""
}`}
>
<TooltipWrapper title="New silence">
<span
id="components-new-silence"
className="nav-link cursor-pointer"
onClick={silenceFormStore.toggle.toggle}
>
<SilenceModalContent
alertStore={alertStore}
silenceFormStore={silenceFormStore}
settingsStore={settingsStore}
onHide={silenceFormStore.toggle.hide}
onDeleteModalClose={onDeleteModalClose}
/>
</React.Suspense>
</Modal>
</React.Fragment>
);
}
);
<FontAwesomeIcon icon={faBellSlash} />
</span>
</TooltipWrapper>
</li>
<Modal
isOpen={silenceFormStore.toggle.visible}
toggleOpen={silenceFormStore.toggle.toggle}
onExited={silenceFormStore.data.resetProgress}
>
<React.Suspense
fallback={
<h1 className="display-1 text-placeholder p-5 m-auto">
<FontAwesomeIcon icon={faSpinner} size="lg" spin />
</h1>
}
>
<SilenceModalContent
alertStore={alertStore}
silenceFormStore={silenceFormStore}
settingsStore={settingsStore}
onHide={silenceFormStore.toggle.hide}
onDeleteModalClose={onDeleteModalClose}
/>
</React.Suspense>
</Modal>
</React.Fragment>
));
};
SilenceModal.propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,