Merge pull request #39 from prymitive/fix-silence-link

fix(ui): silence ID link doesn't point to the right URI
This commit is contained in:
Łukasz Mierzwa
2018-09-18 22:17:54 +01:00
committed by GitHub
10 changed files with 156 additions and 41 deletions

View File

@@ -94,7 +94,7 @@ const Alert = observer(
am.silencedBy.map(silenceID => (
<Silence
key={silenceID}
alertmanager={am}
alertmanagerState={am}
silenceID={silenceID}
afterUpdate={afterUpdate}
/>

View File

@@ -123,17 +123,17 @@ SilenceDetails.propTypes = {
};
//
const FallbackSilenceDesciption = ({ alertmanager, silenceID }) => {
const FallbackSilenceDesciption = ({ alertmanagerName, silenceID }) => {
return (
<div>
<small className="text-muted">
Silenced by {alertmanager.name}/{silenceID}
Silenced by {alertmanagerName}/{silenceID}
</small>
</div>
);
};
FallbackSilenceDesciption.propTypes = {
alertmanager: PropTypes.object.isRequired,
alertmanagerName: PropTypes.string.isRequired,
silenceID: PropTypes.string.isRequired
};
@@ -142,7 +142,7 @@ const Silence = inject("alertStore")(
class Silence extends Component {
static propTypes = {
alertStore: PropTypes.object.isRequired,
alertmanager: PropTypes.object.isRequired,
alertmanagerState: PropTypes.object.isRequired,
silenceID: PropTypes.string.isRequired,
afterUpdate: PropTypes.func.isRequired
};
@@ -187,14 +187,28 @@ const Silence = inject("alertStore")(
this.progressTimer = setInterval(this.recalculateProgress, 30 * 1000);
}
getAlertmanager = () => {
const { alertStore, alertmanagerState } = this.props;
const alertmanager = alertStore.data.getAlertmanagerByName(
alertmanagerState.name
);
if (alertmanager) return alertmanager;
return {
name: alertmanagerState.name
};
};
getSilence = () => {
const { alertStore, alertmanager, silenceID } = this.props;
const { alertStore, alertmanagerState, silenceID } = this.props;
// We pass alertmanager name and silence ID to Silence component
// and we need to lookup the actual silence data in the store.
// Data might be missing from the store so first check if we have
// anything for this alertmanager instance
const amSilences = alertStore.data.silences[alertmanager.name];
const amSilences = alertStore.data.silences[alertmanagerState.name];
if (!amSilences) return null;
// next check if alertmanager has our silence ID
@@ -222,17 +236,19 @@ const Silence = inject("alertStore")(
}
render() {
const { alertmanager, silenceID } = this.props;
const { alertmanagerState, silenceID } = this.props;
const silence = this.getSilence();
if (!silence)
return (
<FallbackSilenceDesciption
alertmanager={alertmanager}
alertmanagerName={alertmanagerState.name}
silenceID={silenceID}
/>
);
const alertmanager = this.getAlertmanager();
return (
<div className="card mt-1 border-0 p-1">
<div className="card-text mb-0">

View File

@@ -16,7 +16,6 @@ const mockAfterUpdate = jest.fn();
const alertmanager = {
name: "default",
uri: "file:///mock",
state: "suppressed",
startsAt: "2000-01-01T10:00:00Z",
endsAt: "0001-01-01T00:00:00Z",
@@ -62,6 +61,7 @@ beforeEach(() => {
{
name: "default",
uri: "file:///mock",
publicURI: "http://example.com",
error: ""
}
]
@@ -78,12 +78,12 @@ afterEach(() => {
clear();
});
const MountedSilence = () => {
const MountedSilence = alertmanagerState => {
return mount(
<Provider alertStore={alertStore}>
<Silence
alertStore={alertStore}
alertmanager={alertmanager}
alertmanagerState={alertmanagerState}
silenceID="4cf5fd82-1edd-4169-99d1-ff8415e72179"
afterUpdate={mockAfterUpdate}
/>
@@ -91,27 +91,36 @@ const MountedSilence = () => {
);
};
const ShallowSilenceDetails = () => {
return shallow(
<SilenceDetails
alertmanager={alertStore.data.upstreams.instances[0]}
silence={silence}
/>
);
};
describe("<Silence />", () => {
it("matches snapshot when data is present in alertStore", () => {
const tree = MountedSilence().find("Silence");
const tree = MountedSilence(alertmanager).find("Silence");
expect(toDiffableHtml(tree.html())).toMatchSnapshot();
});
it("renders full silence when data is present in alertStore", () => {
const tree = MountedSilence().find("Silence");
const tree = MountedSilence(alertmanager).find("Silence");
const fallback = tree.find("FallbackSilenceDesciption");
expect(fallback).toHaveLength(0);
});
it("matches snapshot when data is not present in alertStore", () => {
alertStore.data.silences = {};
const tree = MountedSilence().find("Silence");
const tree = MountedSilence(alertmanager).find("Silence");
expect(toDiffableHtml(tree.html())).toMatchSnapshot();
});
it("renders FallbackSilenceDesciption when Alertmanager data is not present in alertStore", () => {
alertStore.data.silences = {};
const tree = MountedSilence();
const tree = MountedSilence(alertmanager);
const fallback = tree.find("FallbackSilenceDesciption");
expect(fallback).toHaveLength(1);
expect(tree.text()).toBe(
@@ -121,7 +130,7 @@ describe("<Silence />", () => {
it("renders FallbackSilenceDesciption when silence data is not present in alertStore", () => {
alertStore.data.silences.default = {};
const tree = MountedSilence();
const tree = MountedSilence(alertmanager);
const fallback = tree.find("FallbackSilenceDesciption");
expect(fallback).toHaveLength(1);
expect(tree.text()).toBe(
@@ -130,7 +139,7 @@ describe("<Silence />", () => {
});
it("clicking on expand toggle shows silence details", () => {
const tree = MountedSilence();
const tree = MountedSilence(alertmanager);
const toggle = tree.find("a.float-right.cursor-pointer");
toggle.simulate("click");
const details = tree.find("SilenceDetails");
@@ -138,7 +147,7 @@ describe("<Silence />", () => {
});
it("matches snapshot with expaned details", () => {
const tree = MountedSilence().find("Silence");
const tree = MountedSilence(alertmanager).find("Silence");
tree.instance().collapse.toggle();
expect(toDiffableHtml(tree.html())).toMatchSnapshot();
});
@@ -146,26 +155,42 @@ describe("<Silence />", () => {
it("renders comment as link when jiraURL is set", () => {
alertStore.data.silences.default[silence.id].jiraURL =
"http://jira.example.com";
const tree = MountedSilence().find("Silence");
const tree = MountedSilence(alertmanager).find("Silence");
const link = tree.find("a[href='http://jira.example.com']");
expect(link).toHaveLength(1);
expect(link.text()).toBe("Fake silence");
});
it("clears progress timer on unmount", () => {
const tree = MountedSilence().find("Silence");
const tree = MountedSilence(alertmanager).find("Silence");
const instance = tree.instance();
expect(instance.progressTimer).toBeTruthy();
instance.componentWillUnmount();
expect(instance.progressTimer).toBeNull();
});
});
const ShallowSilenceDetails = () => {
return shallow(
<SilenceDetails alertmanager={alertmanager} silence={silence} />
);
};
it("getAlertmanager() returns alertmanager object from alertStore.data.upstreams.instances", () => {
const tree = MountedSilence(alertmanager).find("Silence");
const instance = tree.instance();
const am = instance.getAlertmanager();
expect(am).toEqual({
name: "default",
uri: "file:///mock",
publicURI: "http://example.com",
error: ""
});
});
it("getAlertmanager() return object with only name if given name is not in alertStore", () => {
const missingAlertmanager = { ...alertmanager, name: "notDefault" };
const tree = MountedSilence(missingAlertmanager).find("Silence");
const instance = tree.instance();
const am = instance.getAlertmanager();
expect(am).toEqual({
name: "notDefault"
});
});
});
describe("<SilenceDetails />", () => {
it("unexpired silence endsAt label uses 'secondary' class", () => {
@@ -180,25 +205,33 @@ describe("<SilenceDetails />", () => {
const endsAt = tree.find("span.badge").at(1);
expect(endsAt.html()).toMatch(/badge-danger/);
});
it("id links to Alertmanager silence view via alertmanager.uri", () => {
const tree = ShallowSilenceDetails();
const link = tree.find("a");
expect(link.props().href).toBe(
"file:///mock/#/silences/4cf5fd82-1edd-4169-99d1-ff8415e72179"
);
});
});
describe("<SilenceExpiryBadgeWithProgress />", () => {
it("renders with class 'danger' and no progressbar when expired", () => {
advanceTo(new Date(2001, 0, 1, 23, 0, 0));
const tree = MountedSilence();
const tree = MountedSilence(alertmanager);
expect(tree.html()).toMatch(/badge-danger/);
expect(tree.text()).toMatch(/Expired a year ago/);
});
it("progressbar uses class 'danger' when > 90%", () => {
advanceTo(new Date(2000, 0, 1, 19, 30, 0));
const tree = MountedSilence();
const tree = MountedSilence(alertmanager);
expect(tree.html()).toMatch(/progress-bar bg-danger/);
});
it("progressbar uses class 'danger' when > 75%", () => {
advanceTo(new Date(2000, 0, 1, 17, 45, 0));
const tree = MountedSilence();
const tree = MountedSilence(alertmanager);
expect(tree.html()).toMatch(/progress-bar bg-warning/);
});
@@ -206,7 +239,7 @@ describe("<SilenceExpiryBadgeWithProgress />", () => {
const startsAt = new Date(2000, 0, 1, 10, 0, 0);
const endsAt = new Date(2000, 0, 1, 20, 0, 0);
const tree = MountedSilence().find("Silence");
const tree = MountedSilence(alertmanager).find("Silence");
const instance = tree.instance();
const value = toJS(instance.progress.value);

View File

@@ -47,6 +47,7 @@ const SilenceModalContent = observer(
<div className="modal-body">
{silenceFormStore.data.inProgress ? (
<SilenceSubmitController
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
) : (

View File

@@ -8,11 +8,12 @@ import { SilenceSubmitProgress } from "./SilenceSubmitProgress";
class SilenceSubmitController extends Component {
static propTypes = {
alertStore: PropTypes.object.isRequired,
silenceFormStore: PropTypes.object.isRequired
};
render() {
const { silenceFormStore } = this.props;
const { silenceFormStore, alertStore } = this.props;
return (
<React.Fragment>
@@ -23,6 +24,7 @@ class SilenceSubmitController extends Component {
name={am.label}
uri={am.value}
payload={silenceFormStore.data.toAlertmanagerPayload}
alertStore={alertStore}
/>
))}
</div>

View File

@@ -2,21 +2,27 @@ import React from "react";
import { shallow } from "enzyme";
import { AlertStore } from "Stores/AlertStore";
import {
SilenceFormStore,
MatcherValueToObject
} from "Stores/SilenceFormStore";
import { SilenceSubmitController } from "./SilenceSubmitController";
let alertStore;
let silenceFormStore;
beforeEach(() => {
alertStore = new AlertStore([]);
silenceFormStore = new SilenceFormStore();
});
const ShallowSilenceSubmitController = () => {
return shallow(
<SilenceSubmitController silenceFormStore={silenceFormStore} />
<SilenceSubmitController
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
);
};

View File

@@ -46,7 +46,8 @@ const SilenceSubmitProgress = observer(
static propTypes = {
name: PropTypes.string.isRequired,
uri: PropTypes.string.isRequired,
payload: PropTypes.object.isRequired
payload: PropTypes.object.isRequired,
alertStore: PropTypes.object.isRequired
};
submitState = observable(
@@ -83,13 +84,22 @@ const SilenceSubmitProgress = observer(
};
parseAlertmanagerResponse = response => {
const { uri } = this.props;
const { name, alertStore } = this.props;
const alertmanager = alertStore.data.getAlertmanagerByName(name);
if (response.status === "success") {
const link = (
<SilenceLink uri={uri} silenceId={response.data.silenceId} />
);
this.submitState.markDone(link);
if (alertmanager) {
const link = (
<SilenceLink
uri={alertmanager.uri}
silenceId={response.data.silenceId}
/>
);
this.submitState.markDone(link);
} else {
this.submitState.markDone(response.data.silenceId);
}
} else if (response.status === "error") {
this.submitState.markFailed(response.error);
} else {

View File

@@ -2,14 +2,34 @@ import React from "react";
import { mount } from "enzyme";
import toDiffableHtml from "diffable-html";
import { AlertStore } from "Stores/AlertStore";
import { SilenceSubmitProgress } from "./SilenceSubmitProgress";
let alertStore;
beforeEach(() => {
alertStore = new AlertStore([]);
alertStore.data.upstreams = {
instances: [
{
name: "mockAlertmanager",
uri: "file:///mock",
publicURI: "http://example.com",
error: ""
}
]
};
});
const MountedSilenceSubmitProgress = () => {
return mount(
<SilenceSubmitProgress
name="mockAlertmanager"
uri="http://localhost/mock"
payload={{ foo: "bar" }}
alertStore={alertStore}
/>
);
};
@@ -49,6 +69,21 @@ describe("<SilenceSubmitProgress />", () => {
expect(silenceLink.text()).toBe("123456789");
});
it("renders returned silence ID as text if alertmanager is not found in AlertStore", async () => {
fetch.mockResponseOnce(
JSON.stringify({ status: "success", data: { silenceId: "123456789" } })
);
alertStore.data.upstreams.instances = [];
const tree = MountedSilenceSubmitProgress();
await expect(tree.instance().submitState.fetch).resolves.toBe("success");
// force re-render
tree.update();
const silenceLink = tree.find("a");
expect(silenceLink).toHaveLength(0);
const idDiv = tree.find("div.flex-fill").at(2);
expect(toDiffableHtml(idDiv.html())).toMatchSnapshot();
});
it("renders returned error message on failed fetch", async () => {
fetch.mockRejectOnce(new Error("mock error message"));
const tree = MountedSilenceSubmitProgress();

View File

@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<SilenceSubmitProgress /> renders returned silence ID as text if alertmanager is not found in AlertStore 1`] = `
"
<div class=\\"p-2 flex-fill\\">
123456789
</div>
"
`;

View File

@@ -134,9 +134,12 @@ class AlertStore {
counters: {},
groups: {},
silences: {},
upstreams: { instances: [] }
upstreams: { instances: [] },
getAlertmanagerByName(name) {
return this.upstreams.instances.find(am => am.name === name);
}
},
{},
{ getAlertmanagerByName: action },
{ name: "API Response data" }
);