mirror of
https://github.com/prymitive/karma
synced 2026-08-23 11:56:20 +00:00
feat(ui): disable silence actions for read-only alertmanagers
This commit is contained in:
@@ -192,7 +192,7 @@ const AlertAck = observer(
|
||||
.filter(([amName, alertCount]) => alertCount > 0)
|
||||
.map(([amName, _]) => amName);
|
||||
const clusters = Object.entries(
|
||||
alertStore.data.upstreams.clusters
|
||||
alertStore.data.clustersWithoutReadOnly
|
||||
).filter(([clusterName, clusterMembers]) =>
|
||||
alertmanagers.some(m => clusterMembers.includes(m))
|
||||
);
|
||||
|
||||
@@ -35,6 +35,7 @@ beforeEach(() => {
|
||||
name: "default",
|
||||
uri: "http://localhost",
|
||||
publicURI: "http://example.com",
|
||||
readonly: false,
|
||||
headers: { foo: "bar" },
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
@@ -287,6 +288,7 @@ describe("<AlertAck />", () => {
|
||||
name: "default",
|
||||
uri: "http://am1.example.com",
|
||||
publicURI: "http://am1.example.com",
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
@@ -297,6 +299,7 @@ describe("<AlertAck />", () => {
|
||||
name: "fallback",
|
||||
uri: "http://am2.example.com",
|
||||
publicURI: "http://am2.example.com",
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
@@ -334,6 +337,7 @@ describe("<AlertAck />", () => {
|
||||
name: "default",
|
||||
uri: "http://am1.example.com",
|
||||
publicURI: "http://am1.example.com",
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.16.2",
|
||||
@@ -344,6 +348,7 @@ describe("<AlertAck />", () => {
|
||||
name: "fallback",
|
||||
uri: "http://am2.example.com",
|
||||
publicURI: "http://am2.example.com",
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.16.2",
|
||||
@@ -381,6 +386,7 @@ describe("<AlertAck />", () => {
|
||||
name: "default",
|
||||
uri: "http://am1.example.com",
|
||||
publicURI: "http://am1.example.com",
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
|
||||
@@ -113,6 +113,7 @@ const DeleteSilenceModalContent = observer(
|
||||
getAlertmanager = () =>
|
||||
this.props.alertStore.data.upstreams.instances
|
||||
.filter(u => u.cluster === this.props.cluster)
|
||||
.filter(u => u.readonly === false)
|
||||
.slice(0, 1)[0];
|
||||
|
||||
parseAlertmanagerResponse = response => {
|
||||
@@ -296,11 +297,16 @@ const DeleteSilence = observer(
|
||||
onModalExit
|
||||
} = this.props;
|
||||
|
||||
const members = alertStore.data.getClusterAlertmanagersWithoutReadOnly(
|
||||
cluster
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<button
|
||||
className="btn btn-danger btn-sm"
|
||||
onClick={this.toggle.toggle}
|
||||
disabled={members.length === 0}
|
||||
onClick={members.length && this.toggle.toggle}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className="mr-1 d-none d-sm-inline-block"
|
||||
|
||||
@@ -26,6 +26,7 @@ beforeEach(() => {
|
||||
name: "am1",
|
||||
cluster: "am",
|
||||
uri: "http://localhost:9093",
|
||||
readonly: false,
|
||||
error: "",
|
||||
version: "0.15.3",
|
||||
headers: {}
|
||||
|
||||
@@ -44,6 +44,7 @@ const MockMultipleClusters = () => {
|
||||
name: "default",
|
||||
uri: "http://am1.example.com",
|
||||
publicURI: "http://am1.example.com",
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
@@ -54,6 +55,7 @@ const MockMultipleClusters = () => {
|
||||
name: "fallback",
|
||||
uri: "http://am2.example.com",
|
||||
publicURI: "http://am2.example.com",
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
@@ -64,6 +66,7 @@ const MockMultipleClusters = () => {
|
||||
name: "second",
|
||||
uri: "http://am3.example.com",
|
||||
publicURI: "http://am3.example.com",
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
|
||||
@@ -65,6 +65,10 @@ const SilenceDetails = ({
|
||||
u => u.cluster === cluster
|
||||
);
|
||||
|
||||
const isReadOnly =
|
||||
alertStore.data.getClusterAlertmanagersWithoutReadOnly(cluster).length ===
|
||||
0;
|
||||
|
||||
return (
|
||||
<div className="mt-1">
|
||||
<div className="d-flex flex-fill flex-lg-row flex-column justify-content-between">
|
||||
@@ -154,7 +158,8 @@ const SilenceDetails = ({
|
||||
<div className="d-flex flex-fill flex-lg-column flex-row justify-content-around">
|
||||
<button
|
||||
className="btn btn-primary btn-sm mb-lg-2 mb-0"
|
||||
onClick={onEditSilence}
|
||||
disabled={isReadOnly}
|
||||
onClick={!isReadOnly && onEditSilence}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className="mr-1 d-none d-sm-inline-block"
|
||||
|
||||
@@ -34,6 +34,7 @@ beforeEach(() => {
|
||||
cluster: "am",
|
||||
uri: "http://localhost:9093",
|
||||
publicURI: "http://example.com",
|
||||
readonly: false,
|
||||
error: "",
|
||||
version: "0.15.3",
|
||||
headers: {}
|
||||
|
||||
@@ -47,6 +47,7 @@ const ManagedSilence = observer(
|
||||
getAlertmanager = () =>
|
||||
this.props.alertStore.data.upstreams.instances
|
||||
.filter(u => u.cluster === this.props.cluster)
|
||||
.filter(u => u.readonly === false)
|
||||
.slice(0, 1)[0];
|
||||
|
||||
onEditSilence = () => {
|
||||
|
||||
@@ -30,6 +30,7 @@ storiesOf("ManagedSilence", module)
|
||||
clusterMembers: ["am1"],
|
||||
uri: "http://localhost:9093",
|
||||
publicURI: "http://example.com",
|
||||
readonly: false,
|
||||
error: "",
|
||||
version: "0.15.3",
|
||||
headers: {}
|
||||
@@ -38,6 +39,24 @@ storiesOf("ManagedSilence", module)
|
||||
clusters: { am: ["am1"] }
|
||||
};
|
||||
|
||||
const alertStoreReadOnly = new AlertStore([]);
|
||||
alertStoreReadOnly.data.upstreams = {
|
||||
clusters: { ro: ["readonly"] },
|
||||
instances: [
|
||||
{
|
||||
name: "readonly",
|
||||
uri: "http://localhost:8080",
|
||||
publicURI: "http://example.com",
|
||||
readonly: true,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
cluster: "ro",
|
||||
clusterMembers: ["readonly"]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const silence = MockSilence();
|
||||
silence.startsAt = "2018-08-14T16:00:00Z";
|
||||
silence.endsAt = "2018-08-14T18:00:00Z";
|
||||
@@ -67,6 +86,16 @@ storiesOf("ManagedSilence", module)
|
||||
onDidUpdate={() => {}}
|
||||
isOpen={true}
|
||||
/>
|
||||
<ManagedSilence
|
||||
cluster={"ro"}
|
||||
alertCount={123}
|
||||
alertCountAlwaysVisible={true}
|
||||
silence={silence}
|
||||
alertStore={alertStoreReadOnly}
|
||||
silenceFormStore={silenceFormStore}
|
||||
onDidUpdate={() => {}}
|
||||
isOpen={true}
|
||||
/>
|
||||
<ManagedSilence
|
||||
cluster={cluster}
|
||||
alertCount={123}
|
||||
@@ -86,6 +115,16 @@ storiesOf("ManagedSilence", module)
|
||||
onDidUpdate={() => {}}
|
||||
isOpen={true}
|
||||
/>
|
||||
<ManagedSilence
|
||||
cluster={"ro"}
|
||||
alertCount={123}
|
||||
alertCountAlwaysVisible={true}
|
||||
silence={expiredSilence}
|
||||
alertStore={alertStoreReadOnly}
|
||||
silenceFormStore={silenceFormStore}
|
||||
onDidUpdate={() => {}}
|
||||
isOpen={true}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -33,6 +33,7 @@ beforeEach(() => {
|
||||
clusterMembers: ["am1"],
|
||||
uri: "http://localhost:9093",
|
||||
publicURI: "http://example.com",
|
||||
readonly: false,
|
||||
error: "",
|
||||
version: "0.15.3",
|
||||
headers: {}
|
||||
@@ -95,6 +96,7 @@ describe("<ManagedSilence />", () => {
|
||||
clusterMembers: ["am1"],
|
||||
uri: "http://localhost:9093",
|
||||
publicURI: "http://example.com",
|
||||
readonly: false,
|
||||
error: "",
|
||||
version: "0.15.3",
|
||||
headers: {}
|
||||
|
||||
@@ -30,7 +30,7 @@ const AlertManagerInput = observer(
|
||||
|
||||
if (silenceFormStore.data.alertmanagers.length === 0) {
|
||||
silenceFormStore.data.alertmanagers = AlertmanagerClustersToOption(
|
||||
alertStore.data.upstreams.clusters
|
||||
alertStore.data.clustersWithoutReadOnly
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ const AlertManagerInput = observer(
|
||||
|
||||
// get the list of last known alertmanagers
|
||||
const currentAlertmanagers = AlertmanagerClustersToOption(
|
||||
alertStore.data.upstreams.clusters
|
||||
alertStore.data.clustersWithoutReadOnly
|
||||
);
|
||||
|
||||
// now iterate what's set as silence form values and reset it if any
|
||||
@@ -75,7 +75,7 @@ const AlertManagerInput = observer(
|
||||
instanceId="silence-input-alertmanagers"
|
||||
defaultValue={silenceFormStore.data.alertmanagers}
|
||||
options={AlertmanagerClustersToOption(
|
||||
alertStore.data.upstreams.clusters
|
||||
alertStore.data.clustersWithoutReadOnly
|
||||
)}
|
||||
getOptionValue={JSON.stringify}
|
||||
placeholder={
|
||||
|
||||
@@ -27,6 +27,7 @@ beforeEach(() => {
|
||||
name: "am1",
|
||||
uri: "http://am1.example.com",
|
||||
publicURI: "http://am1.example.com",
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
@@ -37,6 +38,7 @@ beforeEach(() => {
|
||||
name: "am2",
|
||||
uri: "http://am2.example.com",
|
||||
publicURI: "http://am2.example.com",
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
@@ -47,6 +49,7 @@ beforeEach(() => {
|
||||
name: "am3",
|
||||
uri: "http://am3.example.com",
|
||||
publicURI: "http://am3.example.com",
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
@@ -202,4 +205,15 @@ describe("<AlertManagerInput />", () => {
|
||||
expect(silenceFormStore.data.alertmanagers).toHaveLength(0);
|
||||
expect(silenceFormStore.data.alertmanagers).toEqual([]);
|
||||
});
|
||||
|
||||
it("doesn't include readonly instances", () => {
|
||||
alertStore.data.upstreams.instances[0].readonly = true;
|
||||
alertStore.data.upstreams.instances[2].readonly = true;
|
||||
MountedAlertManagerInput();
|
||||
expect(silenceFormStore.data.alertmanagers).toHaveLength(1);
|
||||
expect(silenceFormStore.data.alertmanagers).toContainEqual({
|
||||
label: "am2",
|
||||
value: ["am2"]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,6 +36,7 @@ beforeEach(() => {
|
||||
clusterMembers: ["am1"],
|
||||
uri: "http://localhost:9093",
|
||||
publicURI: "http://example.com",
|
||||
readonly: false,
|
||||
error: "",
|
||||
version: "0.15.3",
|
||||
headers: {}
|
||||
|
||||
@@ -3,6 +3,9 @@ import PropTypes from "prop-types";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faLock } from "@fortawesome/free-solid-svg-icons/faLock";
|
||||
|
||||
import { AlertStore } from "Stores/AlertStore";
|
||||
import {
|
||||
SilenceFormStore,
|
||||
@@ -16,6 +19,15 @@ import { SilencePreview } from "./SilencePreview";
|
||||
import { SilenceSubmitController } from "./SilenceSubmit/SilenceSubmitController";
|
||||
import { Browser } from "./Browser";
|
||||
|
||||
const ReadOnlyPlaceholder = () => (
|
||||
<div className="jumbotron bg-transparent">
|
||||
<h1 className="display-5 text-placeholder text-center">
|
||||
<FontAwesomeIcon icon={faLock} className="mr-3" />
|
||||
Read only mode
|
||||
</h1>
|
||||
</div>
|
||||
);
|
||||
|
||||
const SilenceModalContent = observer(
|
||||
class SilenceModalContent extends Component {
|
||||
static propTypes = {
|
||||
@@ -82,12 +94,17 @@ const SilenceModalContent = observer(
|
||||
{silenceFormStore.tab.current === SilenceTabNames.Editor ? (
|
||||
silenceFormStore.data.currentStage ===
|
||||
SilenceFormStage.UserInput ? (
|
||||
<SilenceForm
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
settingsStore={settingsStore}
|
||||
previewOpen={previewOpen}
|
||||
/>
|
||||
Object.keys(alertStore.data.clustersWithoutReadOnly).length >
|
||||
0 ? (
|
||||
<SilenceForm
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
settingsStore={settingsStore}
|
||||
previewOpen={previewOpen}
|
||||
/>
|
||||
) : (
|
||||
<ReadOnlyPlaceholder />
|
||||
)
|
||||
) : silenceFormStore.data.currentStage ===
|
||||
SilenceFormStage.Preview ? (
|
||||
<SilencePreview
|
||||
|
||||
@@ -20,9 +20,28 @@ beforeEach(() => {
|
||||
settingsStore = new Settings();
|
||||
silenceFormStore = new SilenceFormStore();
|
||||
|
||||
alertStore.data.upstreams = {
|
||||
instances: [
|
||||
{
|
||||
name: "am1",
|
||||
cluster: "am",
|
||||
uri: "http://localhost:9093",
|
||||
readonly: false,
|
||||
error: "",
|
||||
version: "0.15.3",
|
||||
headers: {}
|
||||
}
|
||||
],
|
||||
clusters: { am: ["am1"] }
|
||||
};
|
||||
|
||||
silenceFormStore.tab.current = SilenceTabNames.Editor;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
const MockOnHide = jest.fn();
|
||||
|
||||
const ShallowSilenceModalContent = () => {
|
||||
@@ -38,6 +57,13 @@ const ShallowSilenceModalContent = () => {
|
||||
};
|
||||
|
||||
describe("<SilenceModalContent />", () => {
|
||||
it("Renders ReadOnlyPlaceholder when there are no writable Alertmanager upstreams", () => {
|
||||
alertStore.data.upstreams.instances[0].readonly = true;
|
||||
const tree = ShallowSilenceModalContent();
|
||||
const placeholder = tree.find("ReadOnlyPlaceholder");
|
||||
expect(placeholder).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("Clicking on the Browser tab changes content", () => {
|
||||
const tree = ShallowSilenceModalContent();
|
||||
const tabs = tree.find("Tab");
|
||||
|
||||
@@ -95,9 +95,16 @@ const SilenceSubmitProgress = observer(
|
||||
|
||||
const member = this.submitState.membersToTry.pop();
|
||||
|
||||
if (alertStore.data.isReadOnlyAlertmanager(member)) {
|
||||
const err = `Alertmanager instance "${member}" is read-only`;
|
||||
console.error(err);
|
||||
this.maybeTryAgainAfterError(err);
|
||||
return;
|
||||
}
|
||||
|
||||
const am = alertStore.data.getAlertmanagerByName(member);
|
||||
if (am === undefined) {
|
||||
const err = `Alertmanager instance "${member} not found`;
|
||||
const err = `Alertmanager instance "${member}" not found`;
|
||||
console.error(err);
|
||||
this.maybeTryAgainAfterError(err);
|
||||
return;
|
||||
|
||||
@@ -15,6 +15,7 @@ beforeEach(() => {
|
||||
name: "mockAlertmanager",
|
||||
uri: "http://localhost",
|
||||
publicURI: "http://example.com",
|
||||
readonly: false,
|
||||
headers: { foo: "bar" },
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
@@ -25,6 +26,10 @@ beforeEach(() => {
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
const MountedSilenceSubmitProgress = () => {
|
||||
return mount(
|
||||
<SilenceSubmitProgress
|
||||
@@ -94,6 +99,7 @@ describe("<SilenceSubmitProgress />", () => {
|
||||
name: "am1",
|
||||
uri: "http://am1.example.com",
|
||||
publicURI: "http://am1.example.com",
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
@@ -104,6 +110,7 @@ describe("<SilenceSubmitProgress />", () => {
|
||||
name: "am2",
|
||||
uri: "http://am2.example.com",
|
||||
publicURI: "http://am2.example.com",
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
@@ -150,6 +157,7 @@ describe("<SilenceSubmitProgress />", () => {
|
||||
name: "am1",
|
||||
uri: "http://am1.example.com",
|
||||
publicURI: "http://am1.example.com",
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
@@ -180,6 +188,66 @@ describe("<SilenceSubmitProgress />", () => {
|
||||
expect(consoleSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("will refuse to send requests to an alertmanager instance that is readonly", async () => {
|
||||
fetch.resetMocks();
|
||||
const logs = [];
|
||||
const consoleSpy = jest
|
||||
.spyOn(console, "error")
|
||||
.mockImplementation((message, ...args) => {
|
||||
logs.push(message);
|
||||
});
|
||||
|
||||
alertStore.data.upstreams = {
|
||||
clusters: { ha: ["am1", "am2"] },
|
||||
instances: [
|
||||
{
|
||||
name: "am1",
|
||||
uri: "http://am1.example.com",
|
||||
publicURI: "http://am1.example.com",
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
cluster: "ha",
|
||||
clusterMembers: ["am1", "am2"]
|
||||
},
|
||||
{
|
||||
name: "am2",
|
||||
uri: "http://am2.example.com",
|
||||
publicURI: "http://am2.example.com",
|
||||
readonly: true,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
cluster: "ha",
|
||||
clusterMembers: ["am1", "am2"]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const tree = mount(
|
||||
<SilenceSubmitProgress
|
||||
cluster="ha"
|
||||
members={["am1", "am2"]}
|
||||
payload={{
|
||||
matchers: [],
|
||||
startsAt: "now",
|
||||
endsAt: "later",
|
||||
createdBy: "me@example.com",
|
||||
comment: "fake payload"
|
||||
}}
|
||||
alertStore={alertStore}
|
||||
/>
|
||||
);
|
||||
await expect(tree.instance().submitState.fetch).resolves.toBeUndefined();
|
||||
expect(fetch.mock.calls).toHaveLength(1);
|
||||
expect(fetch.mock.calls[0][0]).toBe(
|
||||
"http://am1.example.com/api/v1/silences"
|
||||
);
|
||||
expect(logs).toEqual(['Alertmanager instance "am2" is read-only']);
|
||||
expect(consoleSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("renders returned silence ID on successful fetch", async () => {
|
||||
fetch.mockResponseOnce(
|
||||
JSON.stringify({ status: "success", data: { silenceId: "123456789" } })
|
||||
|
||||
@@ -40,6 +40,23 @@ storiesOf("SilenceModal", module)
|
||||
const settingsStore = new Settings();
|
||||
const silenceFormStore = new SilenceFormStore();
|
||||
|
||||
alertStore.data.upstreams = {
|
||||
clusters: { default: ["default"] },
|
||||
instances: [
|
||||
{
|
||||
name: "default",
|
||||
uri: "http://localhost:8080",
|
||||
publicURI: "http://example.com",
|
||||
readonly: false,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
cluster: "default",
|
||||
clusterMembers: ["default"]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
silenceFormStore.toggle.visible = true;
|
||||
silenceFormStore.data.matchers = [
|
||||
MockMatcher("cluster", ["prod"], false),
|
||||
@@ -96,8 +113,36 @@ storiesOf("SilenceModal", module)
|
||||
}
|
||||
);
|
||||
|
||||
const alertStoreReadOnly = new AlertStore([]);
|
||||
alertStoreReadOnly.data.upstreams = {
|
||||
clusters: { default: ["readonly"] },
|
||||
instances: [
|
||||
{
|
||||
name: "readonly",
|
||||
uri: "http://localhost:8080",
|
||||
publicURI: "http://example.com",
|
||||
readonly: true,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
cluster: "default",
|
||||
clusterMembers: ["readonly"]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Modal>
|
||||
<SilenceModalContent
|
||||
alertStore={alertStoreReadOnly}
|
||||
silenceFormStore={silenceFormStore}
|
||||
settingsStore={settingsStore}
|
||||
onHide={() => {}}
|
||||
previewOpen={true}
|
||||
onDeleteModalClose={() => {}}
|
||||
/>
|
||||
</Modal>
|
||||
<Modal>
|
||||
<SilenceModalContent
|
||||
alertStore={alertStore}
|
||||
@@ -142,6 +187,7 @@ storiesOf("SilenceModal", module)
|
||||
clusterMembers: ["am1"],
|
||||
uri: "http://localhost:9093",
|
||||
publicURI: "http://example.com",
|
||||
readonly: false,
|
||||
error: "",
|
||||
version: "0.15.3",
|
||||
headers: {}
|
||||
@@ -204,6 +250,7 @@ storiesOf("SilenceModal", module)
|
||||
clusterMembers: ["am1"],
|
||||
uri: "http://localhost:9093",
|
||||
publicURI: "http://example.com",
|
||||
readonly: false,
|
||||
error: "",
|
||||
version: "0.15.3",
|
||||
headers: {}
|
||||
|
||||
@@ -69,6 +69,7 @@ const APIAlertmanagerUpstream = PropTypes.exact({
|
||||
cluster: PropTypes.string.isRequired,
|
||||
uri: PropTypes.string.isRequired,
|
||||
publicURI: PropTypes.string.isRequired,
|
||||
readonly: PropTypes.bool.isRequired,
|
||||
headers: PropTypes.object.isRequired,
|
||||
error: PropTypes.string.isRequired,
|
||||
version: PropTypes.string.isRequired,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { observable, action, toJS } from "mobx";
|
||||
import { observable, action, computed, toJS } from "mobx";
|
||||
|
||||
import throttle from "lodash/throttle";
|
||||
|
||||
@@ -167,13 +167,37 @@ class AlertStore {
|
||||
getAlertmanagerByName(name) {
|
||||
return this.upstreams.instances.find(am => am.name === name);
|
||||
},
|
||||
isReadOnlyAlertmanager(name) {
|
||||
return this.readOnlyAlertmanagers.map(am => am.name).includes(name);
|
||||
},
|
||||
getClusterAlertmanagersWithoutReadOnly(clusterID) {
|
||||
return this.clustersWithoutReadOnly[clusterID] || [];
|
||||
},
|
||||
get readOnlyAlertmanagers() {
|
||||
return this.upstreams.instances.filter(am => am.readonly === true);
|
||||
},
|
||||
get clustersWithoutReadOnly() {
|
||||
const clusters = {};
|
||||
for (const clusterID of Object.keys(this.upstreams.clusters)) {
|
||||
const members = this.upstreams.clusters[clusterID].filter(
|
||||
member => this.isReadOnlyAlertmanager(member) === false
|
||||
);
|
||||
if (members.length > 0) {
|
||||
clusters[clusterID] = members;
|
||||
}
|
||||
}
|
||||
return clusters;
|
||||
},
|
||||
getColorData(name, value) {
|
||||
if (this.colors[name] !== undefined) {
|
||||
return this.colors[name][value];
|
||||
}
|
||||
}
|
||||
},
|
||||
{},
|
||||
{
|
||||
readOnlyAlertmanagers: computed,
|
||||
clustersWithoutReadOnly: computed
|
||||
},
|
||||
{ name: "API Response data" }
|
||||
);
|
||||
|
||||
|
||||
@@ -20,6 +20,78 @@ afterEach(() => {
|
||||
delete process.env.REACT_APP_BACKEND_URI;
|
||||
});
|
||||
|
||||
describe("AlertStore.data", () => {
|
||||
it("getClusterAlertmanagersWithoutReadOnly filters out readonly instances", () => {
|
||||
const store = new AlertStore([]);
|
||||
store.data.upstreams = {
|
||||
clusters: { default: ["default", "readonly"] },
|
||||
instances: [
|
||||
{
|
||||
name: "default",
|
||||
uri: "http://localhost",
|
||||
publicURI: "http://example.com:8080",
|
||||
readonly: false,
|
||||
headers: { foo: "bar" },
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
cluster: "default",
|
||||
clusterMembers: ["default", "readonly"]
|
||||
},
|
||||
{
|
||||
name: "readonly",
|
||||
uri: "http://localhost:8081",
|
||||
publicURI: "http://example.com",
|
||||
readonly: true,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
cluster: "default",
|
||||
clusterMembers: ["default", "readonly"]
|
||||
}
|
||||
]
|
||||
};
|
||||
expect(
|
||||
store.data.getClusterAlertmanagersWithoutReadOnly("default")
|
||||
).toEqual(["default"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("AlertStore.data", () => {
|
||||
it("getClusterAlertmanagersWithoutReadOnly handles clusters with no writable instances", () => {
|
||||
const store = new AlertStore([]);
|
||||
store.data.upstreams = {
|
||||
clusters: { default: ["ro1", "ro2"] },
|
||||
instances: [
|
||||
{
|
||||
name: "ro1",
|
||||
uri: "http://localhost",
|
||||
publicURI: "http://example.com:8080",
|
||||
readonly: true,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
cluster: "default",
|
||||
clusterMembers: ["ro1", "ro2"]
|
||||
},
|
||||
{
|
||||
name: "ro2",
|
||||
uri: "http://localhost:8081",
|
||||
publicURI: "http://example.com",
|
||||
readonly: true,
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
cluster: "default",
|
||||
clusterMembers: ["ro1", "ro2"]
|
||||
}
|
||||
]
|
||||
};
|
||||
expect(
|
||||
store.data.getClusterAlertmanagersWithoutReadOnly("default")
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("AlertStore.status", () => {
|
||||
it("status is initially idle with no error", () => {
|
||||
const store = new AlertStore([]);
|
||||
|
||||
@@ -70,6 +70,7 @@ const MockAlertmanager = () => ({
|
||||
cluster: "default",
|
||||
uri: "http://localhost",
|
||||
publicURI: "http://am.example.com",
|
||||
readonly: false,
|
||||
headers: {
|
||||
Authorization: "Basic foo bar"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user