fix(ui): move silence request state off component

Fixes #1878
This commit is contained in:
Łukasz Mierzwa
2020-06-25 10:44:50 +01:00
committed by Łukasz Mierzwa
parent 0c65bc7821
commit d8a5b8dff1
8 changed files with 323 additions and 159 deletions
+2 -1
View File
@@ -67,7 +67,8 @@
"jest": {
"collectCoverageFrom": [
"src/**/*.{js,ts,tsx}",
"!src/**/*.stories.{js,ts,tsx}"
"!src/**/*.stories.{js,ts,tsx}",
"!src/react-app-env.d.ts"
]
},
"devDependencies": {
@@ -16,6 +16,7 @@ import {
SilenceFormStage,
NewEmptyMatcher,
MatcherValueToObject,
NewClusterRequest,
} from "Stores/SilenceFormStore";
import { Settings } from "Stores/Settings";
import { QueryOperators } from "Common/Query";
@@ -43,6 +44,9 @@ const SilenceForm = ({
silenceFormStore.data.verifyStarEnd();
}
// reset cluster request state
silenceFormStore.data.requestsByCluster = {};
if (
silenceFormStore.data.matchers.filter(
(m) => m.name !== "" || m.values.length
@@ -100,6 +104,12 @@ const SilenceForm = ({
const handleSubmit = (event) => {
event.preventDefault();
let rbc = {};
silenceFormStore.data.alertmanagers.forEach((am) => {
rbc[am.label] = NewClusterRequest(am.label, am.value);
});
silenceFormStore.data.requestsByCluster = rbc;
settingsStore.silenceFormConfig.saveAuthor(silenceFormStore.data.author);
if (silenceFormStore.data.isValid)
@@ -1,26 +1,79 @@
import React, { memo } from "react";
import React from "react";
import PropTypes from "prop-types";
import { useObserver } from "mobx-react-lite";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowLeft } from "@fortawesome/free-solid-svg-icons/faArrowLeft";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons/faCheckCircle";
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle";
import { AlertStore } from "Stores/AlertStore";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { SilenceSubmitProgress } from "./SilenceSubmitProgress";
const SilenceSubmitController = memo(({ silenceFormStore, alertStore }) => {
return (
const SilenceSubmitController = ({ silenceFormStore, alertStore }) => {
return useObserver(() => (
<React.Fragment>
<div>
{silenceFormStore.data.alertmanagers.map((am) => (
<SilenceSubmitProgress
key={am.label}
cluster={am.label}
members={am.value}
payload={silenceFormStore.data.toAlertmanagerPayload}
alertStore={alertStore}
/>
))}
<div className="table-responsive">
<table className="table table-borderless">
<tbody>
{Object.values(silenceFormStore.data.requestsByCluster).map(
(clusterRequest) => (
<tr key={clusterRequest.cluster}>
<td className="align-middle" style={{ width: "1%" }}>
{clusterRequest.isDone ? (
clusterRequest.error ? (
<FontAwesomeIcon
icon={faExclamationCircle}
className="text-danger"
/>
) : (
<FontAwesomeIcon
icon={faCheckCircle}
className="text-success"
/>
)
) : (
<SilenceSubmitProgress
key={clusterRequest.cluster}
cluster={clusterRequest.cluster}
members={clusterRequest.members}
payload={silenceFormStore.data.toAlertmanagerPayload}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
)}
</td>
<td className="align-middle">{clusterRequest.cluster}</td>
<td>
<div
className={`rounded text-center ${
clusterRequest.isDone && clusterRequest.error
? "bg-light"
: ""
}`}
>
{clusterRequest.isDone ? (
clusterRequest.error ? (
clusterRequest.error
) : (
<a
href={clusterRequest.silenceLink}
target="_blank"
rel="noopener noreferrer"
>
{clusterRequest.silenceID}
</a>
)
) : null}
</div>
</td>
</tr>
)
)}
</tbody>
</table>
</div>
<div className="d-flex flex-row-reverse">
<button
@@ -33,8 +86,8 @@ const SilenceSubmitController = memo(({ silenceFormStore, alertStore }) => {
</button>
</div>
</React.Fragment>
);
});
));
};
SilenceSubmitController.propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
@@ -3,7 +3,11 @@ import React from "react";
import { shallow } from "enzyme";
import { AlertStore } from "Stores/AlertStore";
import { SilenceFormStore, SilenceFormStage } from "Stores/SilenceFormStore";
import {
SilenceFormStore,
SilenceFormStage,
NewClusterRequest,
} from "Stores/SilenceFormStore";
import { SilenceSubmitController } from "./SilenceSubmitController";
let alertStore;
@@ -12,31 +16,126 @@ let silenceFormStore;
beforeEach(() => {
alertStore = new AlertStore([]);
silenceFormStore = new SilenceFormStore();
});
const ShallowSilenceSubmitController = () => {
return shallow(
<SilenceSubmitController
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
);
};
alertStore.data.upstreams = {
clusters: { ha: ["am1", "am2"], single: "single" },
instances: [
{
name: "am1",
uri: "http://am1.example.com",
publicURI: "http://am1.example.com",
readonly: false,
headers: {},
corsCredentials: "include",
error: "",
version: "0.17.0",
cluster: "ha",
clusterMembers: ["am1", "am2"],
},
{
name: "am2",
uri: "http://am2.example.com",
publicURI: "http://am2.example.com",
readonly: false,
headers: {},
corsCredentials: "include",
error: "",
version: "0.17.0",
cluster: "ha",
clusterMembers: ["am1", "am2"],
},
{
name: "single",
uri: "http://single.example.com",
publicURI: "http://single.example.com",
readonly: false,
headers: {},
corsCredentials: "include",
error: "",
version: "0.17.0",
cluster: "ha",
clusterMembers: ["single"],
},
],
};
});
describe("<SilenceSubmitController />", () => {
it("renders all passed SilenceSubmitProgress", () => {
silenceFormStore.data.alertmanagers.push({ label: "am1", value: ["am1"] });
silenceFormStore.data.alertmanagers.push({
label: "ha",
value: ["am2", "am3"],
});
const tree = ShallowSilenceSubmitController();
expect(tree.find("div").at(0).children()).toHaveLength(2);
silenceFormStore.data.requestsByCluster = {
ha: NewClusterRequest("ha", ["am1", "am2"]),
single: NewClusterRequest("single", ["single"]),
};
const tree = shallow(
<SilenceSubmitController
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
);
expect(tree.find("tr")).toHaveLength(2);
});
it("renders spinner for pending requests", () => {
const single = NewClusterRequest("single", ["single"]);
silenceFormStore.data.requestsByCluster = { single: single };
const tree = shallow(
<SilenceSubmitController
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
);
expect(tree.find("tr")).toHaveLength(1);
expect(tree.find("td").at(0).html()).toMatch(/fa-circle-notch/);
expect(tree.find("td").at(1).text()).toBe("single");
expect(tree.find("td").at(2).text()).toBe("");
});
it("renders error for failed requests", () => {
const single = NewClusterRequest("single", ["single"]);
single.isDone = true;
single.error = "fake error";
silenceFormStore.data.requestsByCluster = { single: single };
const tree = shallow(
<SilenceSubmitController
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
);
expect(tree.find("tr")).toHaveLength(1);
expect(tree.find("td").at(0).html()).toMatch(/fa-exclamation-circle/);
expect(tree.find("td").at(1).text()).toBe("single");
expect(tree.find("td").at(2).text()).toBe("fake error");
});
it("renders silence link for completed requests", () => {
const single = NewClusterRequest("single", ["single"]);
single.isDone = true;
single.silenceID = "123456789";
single.silenceLink = "http://localhost";
silenceFormStore.data.requestsByCluster = { single: single };
const tree = shallow(
<SilenceSubmitController
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
);
expect(tree.find("tr")).toHaveLength(1);
expect(tree.find("td").at(0).html()).toMatch(/fa-check-circle/);
expect(tree.find("td").at(1).text()).toBe("single");
expect(tree.find("td").at(2).text()).toBe("123456789");
expect(
tree.find("td").at(2).find('a[href="http://localhost"]')
).toHaveLength(1);
});
it("resets the form on 'Back' button click", () => {
silenceFormStore.data.currentStage = SilenceFormStage.Submit;
const tree = ShallowSilenceSubmitController();
const tree = shallow(
<SilenceSubmitController
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
);
const button = tree.find("button");
button.simulate("click");
expect(silenceFormStore.data.currentStage).toBe(SilenceFormStage.UserInput);
@@ -1,93 +1,75 @@
import React, { useEffect, useState, memo } from "react";
import React, { useEffect, useState } from "react";
import PropTypes from "prop-types";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons/faCheckCircle";
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle";
import { APISilenceMatcher } from "Models/API";
import { AlertStore } from "Stores/AlertStore";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { useFetchAny } from "Hooks/useFetchAny";
const SilenceSubmitProgress = memo(
({ alertStore, cluster, members, payload }) => {
const [upstreams, setUpstreams] = useState([]);
const { response, error, inProgress, responseURI } = useFetchAny(upstreams);
const [publicURIs, setPublicURIs] = useState({});
const SilenceSubmitProgress = ({
alertStore,
silenceFormStore,
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`);
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`);
} 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,
},
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" />
)}
</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>
);
}
);
useEffect(() => {
if (!inProgress && error !== null) {
silenceFormStore.data.requestsByCluster[cluster].isDone = true;
silenceFormStore.data.requestsByCluster[cluster].error = error;
} else if (!inProgress && response !== null) {
silenceFormStore.data.requestsByCluster[cluster].isDone = true;
silenceFormStore.data.requestsByCluster[cluster].silenceID =
response.silenceID;
silenceFormStore.data.requestsByCluster[
cluster
].silenceLink = `${publicURIs[responseURI]}/#/silences/${response.silenceID}`;
}
}, [cluster, error, inProgress, publicURIs, response, responseURI]); // eslint-disable-line react-hooks/exhaustive-deps
return <FontAwesomeIcon icon={faCircleNotch} spin />;
};
SilenceSubmitProgress.propTypes = {
cluster: PropTypes.string.isRequired,
members: PropTypes.arrayOf(PropTypes.string).isRequired,
@@ -100,6 +82,7 @@ SilenceSubmitProgress.propTypes = {
id: PropTypes.string,
}).isRequired,
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
};
export { SilenceSubmitProgress };
@@ -6,12 +6,16 @@ import { mount } from "enzyme";
import fetchMock from "fetch-mock";
import { AlertStore } from "Stores/AlertStore";
import { SilenceFormStore, NewClusterRequest } from "Stores/SilenceFormStore";
import { SilenceSubmitProgress } from "./SilenceSubmitProgress";
let alertStore;
let silenceFormStore;
beforeEach(() => {
alertStore = new AlertStore([]);
silenceFormStore = new SilenceFormStore();
alertStore.data.upstreams = {
instances: [
{
@@ -29,6 +33,12 @@ beforeEach(() => {
],
};
silenceFormStore.data.requestsByCluster = {
mockAlertmanager: NewClusterRequest("mockAlertmanager", [
"mockAlertmanager",
]),
};
fetchMock.resetHistory();
fetchMock.any(
{
@@ -59,6 +69,7 @@ const MountedSilenceSubmitProgress = () => {
comment: "fake payload",
}}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
);
};
@@ -151,6 +162,9 @@ describe("<SilenceSubmitProgress />", () => {
},
],
};
silenceFormStore.data.requestsByCluster = {
ha: NewClusterRequest("ha", ["am1", "am2"]),
};
mount(
<SilenceSubmitProgress
@@ -164,6 +178,7 @@ describe("<SilenceSubmitProgress />", () => {
comment: "fake payload",
}}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
);
await act(async () => {
@@ -178,7 +193,7 @@ describe("<SilenceSubmitProgress />", () => {
);
});
it("will render error message from last failed cluster member", async () => {
it("will use error message from last failed cluster member", async () => {
fetchMock.reset();
fetchMock.mock("http://am2.example.com/api/v2/silences", {
throws: new TypeError("failed to fetch from am2"),
@@ -215,6 +230,9 @@ describe("<SilenceSubmitProgress />", () => {
},
],
};
silenceFormStore.data.requestsByCluster = {
ha: NewClusterRequest("ha", ["am1", "am2"]),
};
const tree = mount(
<SilenceSubmitProgress
@@ -228,6 +246,7 @@ describe("<SilenceSubmitProgress />", () => {
comment: "fake payload",
}}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
);
await act(async () => {
@@ -235,8 +254,12 @@ describe("<SilenceSubmitProgress />", () => {
await fetchMock.flush(true);
});
});
tree.update();
expect(fetchMock.calls()).toHaveLength(2);
expect(tree.text()).toBe("hafailed to fetch from am1");
expect(silenceFormStore.data.requestsByCluster.ha).toMatchObject({
isDone: true,
error: "failed to fetch from am1",
});
});
it("will log an error if Alertmanager instance is missing from instances and try the next one", async () => {
@@ -260,6 +283,9 @@ describe("<SilenceSubmitProgress />", () => {
},
],
};
silenceFormStore.data.requestsByCluster = {
ha: NewClusterRequest("ha", ["am1", "am2"]),
};
mount(
<SilenceSubmitProgress
@@ -273,6 +299,7 @@ describe("<SilenceSubmitProgress />", () => {
comment: "fake payload",
}}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
);
await act(async () => {
@@ -294,6 +321,9 @@ describe("<SilenceSubmitProgress />", () => {
clusters: { ha: ["am1", "am2"] },
instances: [],
};
silenceFormStore.data.requestsByCluster = {
ha: NewClusterRequest("ha", ["am1", "am2"]),
};
mount(
<SilenceSubmitProgress
@@ -307,6 +337,7 @@ describe("<SilenceSubmitProgress />", () => {
comment: "fake payload",
}}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
);
await act(async () => {
@@ -355,6 +386,9 @@ describe("<SilenceSubmitProgress />", () => {
},
],
};
silenceFormStore.data.requestsByCluster = {
ha: NewClusterRequest("ha", ["am1", "am2"]),
};
mount(
<SilenceSubmitProgress
@@ -368,6 +402,7 @@ describe("<SilenceSubmitProgress />", () => {
comment: "fake payload",
}}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
);
await act(async () => {
@@ -420,6 +455,9 @@ describe("<SilenceSubmitProgress />", () => {
},
],
};
silenceFormStore.data.requestsByCluster = {
ha: NewClusterRequest("ha", ["am1", "am2"]),
};
mount(
<SilenceSubmitProgress
@@ -433,6 +471,7 @@ describe("<SilenceSubmitProgress />", () => {
comment: "fake payload",
}}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
);
expect(fetchMock.calls()).toHaveLength(0);
@@ -443,47 +482,6 @@ describe("<SilenceSubmitProgress />", () => {
expect(consoleSpy).toHaveBeenCalledTimes(2);
});
it("renders returned silence ID on successful fetch", async () => {
const tree = MountedSilenceSubmitProgress();
await act(async () => {
await act(async () => {
await fetchMock.flush(true);
});
});
// force re-render
tree.update();
const silenceLink = tree.find("a");
expect(silenceLink).toHaveLength(1);
expect(silenceLink.text()).toBe("123456789");
});
it("renders returned error message on failed fetch", async () => {
fetchMock.reset();
fetchMock.any({
status: 500,
body: "mock error message",
});
const tree = MountedSilenceSubmitProgress();
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 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);
});
it("renders silence link on successful fetch", async () => {
const tree = MountedSilenceSubmitProgress();
await act(async () => {
@@ -492,12 +490,17 @@ describe("<SilenceSubmitProgress />", () => {
});
});
tree.update();
expect(tree.find("a").getDOMNode().getAttribute("href")).toBe(
"http://example.com/#/silences/123456789"
);
expect(
silenceFormStore.data.requestsByCluster.mockAlertmanager
).toMatchObject({
isDone: true,
error: null,
silenceID: "123456789",
silenceLink: "http://example.com/#/silences/123456789",
});
});
it("renders error icon on failed fetch", async () => {
it("sets error icon on failed fetch", async () => {
fetchMock.reset();
fetchMock.any({
status: 500,
@@ -510,7 +513,11 @@ describe("<SilenceSubmitProgress />", () => {
});
});
tree.update();
expect(tree.find("FontAwesomeIcon.text-success")).toHaveLength(0);
expect(tree.find("FontAwesomeIcon.text-danger")).toHaveLength(1);
expect(
silenceFormStore.data.requestsByCluster.mockAlertmanager
).toMatchObject({
isDone: true,
error: "error message",
});
});
});
@@ -1,3 +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>"`;
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 class=\\"table-responsive\\"><table class=\\"table table-borderless\\"><tbody></tbody></table></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>"`;
+11
View File
@@ -104,6 +104,15 @@ const MatchersFromGroup = (group, stripLabels, alerts, onlyActive) => {
return matchers;
};
const NewClusterRequest = (cluster, members) => ({
cluster: cluster,
members: members,
isDone: false,
silenceID: null,
silenceLink: null,
error: null,
});
const GenerateAlertmanagerSilenceData = (
startsAt,
endsAt,
@@ -188,6 +197,7 @@ class SilenceFormStore {
endsAt: addHours(new Date(), 1),
comment: "",
author: "",
requestsByCluster: {},
get isValid() {
if (this.alertmanagers.length === 0) return false;
@@ -371,4 +381,5 @@ export {
SilenceTabNames,
MatchersFromGroup,
GenerateAlertmanagerSilenceData,
NewClusterRequest,
};