diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupFooter/index.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupFooter/index.js
index f9e6e82d3..62288f65b 100644
--- a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupFooter/index.js
+++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupFooter/index.js
@@ -5,10 +5,11 @@ import { observer } from "mobx-react";
import { APIGroup } from "Models/API";
import { StaticLabels } from "Common/Query";
+import { AlertStore } from "Stores/AlertStore";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { FilteringLabel } from "Components/Labels/FilteringLabel";
import { RenderNonLinkAnnotation, RenderLinkAnnotation } from "../Annotation";
-import { Silence } from "../Silence";
+import { RenderSilence } from "../Silences";
import "./index.css";
@@ -18,6 +19,7 @@ const GroupFooter = observer(
group: APIGroup.isRequired,
alertmanagers: PropTypes.arrayOf(PropTypes.string).isRequired,
afterUpdate: PropTypes.func.isRequired,
+ alertStore: PropTypes.instanceOf(AlertStore).isRequired,
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired
};
@@ -26,6 +28,7 @@ const GroupFooter = observer(
group,
alertmanagers,
afterUpdate,
+ alertStore,
silenceFormStore
} = this.props;
@@ -65,25 +68,18 @@ const GroupFooter = observer(
/>
))}
{Object.keys(group.shared.silences).length === 0 ? null : (
-
+
{Object.entries(group.shared.silences).map(
([cluster, silences]) =>
- silences.map(silenceID => (
-
- a.alertmanager.filter(
- am => am.cluster === cluster
- )[0]
- )[0]
- }
- silenceID={silenceID}
- afterUpdate={afterUpdate}
- />
- ))
+ silences.map(silenceID =>
+ RenderSilence(
+ alertStore,
+ silenceFormStore,
+ afterUpdate,
+ cluster,
+ silenceID
+ )
+ )
)}
)}
diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupFooter/index.test.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupFooter/index.test.js
index 0849fc0ea..8da6f29e3 100644
--- a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupFooter/index.test.js
+++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupFooter/index.test.js
@@ -64,6 +64,7 @@ const MountedGroupFooter = () => {
group={group}
alertmanagers={["default"]}
afterUpdate={MockAfterUpdate}
+ alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
@@ -81,8 +82,40 @@ describe("
", () => {
group.alerts[id].alertmanager[0].silencedBy = ["123456789"];
}
group.shared.silences = { default: ["123456789"] };
+ alertStore.data.silences = {
+ default: {
+ "123456789": MockSilence()
+ }
+ };
+
const tree = MountedGroupFooter().find("GroupFooter");
- expect(tree.find("Silence")).toHaveLength(1);
+ expect(tree.find("ManagedSilence")).toHaveLength(1);
+ });
+
+ it("render fallback silence if not found in alertStore", () => {
+ for (const id of Object.keys(group.alerts)) {
+ group.alerts[id].alertmanager[0].silencedBy = ["123456789"];
+ }
+ group.shared.silences = { default: ["123456789"] };
+ alertStore.data.silences = {
+ default: {}
+ };
+
+ const tree = MountedGroupFooter().find("GroupFooter");
+ expect(tree.find("FallbackSilenceDesciption")).toHaveLength(1);
+ });
+
+ it("render fallback silence if cluster not found in alertStore", () => {
+ for (const id of Object.keys(group.alerts)) {
+ group.alerts[id].alertmanager[0].silencedBy = ["123456789"];
+ }
+ group.shared.silences = { default: ["123456789"] };
+ alertStore.data.silences = {
+ foo: {}
+ };
+
+ const tree = MountedGroupFooter().find("GroupFooter");
+ expect(tree.find("FallbackSilenceDesciption")).toHaveLength(1);
});
it("mathes snapshot when silence is rendered", () => {
diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/DeleteSilence.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/DeleteSilence.js
deleted file mode 100644
index 42b8eaf29..000000000
--- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/DeleteSilence.js
+++ /dev/null
@@ -1,299 +0,0 @@
-import React, { Component } from "react";
-import PropTypes from "prop-types";
-
-import { observable, action } from "mobx";
-import { observer } from "mobx-react";
-
-import semver from "semver";
-
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { faTrash } from "@fortawesome/free-solid-svg-icons/faTrash";
-import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle";
-import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle";
-import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch";
-
-import { APIAlertmanagerUpstream } from "Models/API";
-import { AlertStore, FormatBackendURI, FormatAlertsQ } from "Stores/AlertStore";
-import { FormatQuery, QueryOperators, StaticLabels } from "Common/Query";
-import { FetchWithCredentials } from "Common/Fetch";
-import { Modal } from "Components/Modal";
-import {
- LabelSetList,
- GroupListToUniqueLabelsList
-} from "Components/LabelSetList";
-
-const ProgressMessage = () => (
-
-
-
-);
-
-const ErrorMessage = ({ message }) => (
-
-);
-ErrorMessage.propTypes = {
- message: PropTypes.node.isRequired
-};
-
-const SuccessMessage = () => (
-
-
-
- Silence deleted, it might take a few minutes for affected alerts to change
- state
-
-
-);
-
-const DeleteSilenceModalContent = observer(
- class DeleteSilenceModalContent extends Component {
- static propTypes = {
- alertStore: PropTypes.instanceOf(AlertStore).isRequired,
- alertmanager: APIAlertmanagerUpstream.isRequired,
- silenceID: PropTypes.string.isRequired,
- onHide: PropTypes.func.isRequired
- };
-
- previewState = observable(
- {
- fetch: null,
- error: null,
- alertLabels: [],
- setError(err) {
- this.error = err;
- },
- groupsToUniqueLabels(groups) {
- this.alertLabels = GroupListToUniqueLabelsList(groups);
- }
- },
- {
- setError: action.bound,
- groupsToUniqueLabels: action.bound
- }
- );
-
- deleteState = observable(
- {
- fetch: null,
- done: false,
- error: null,
- setDone() {
- this.done = true;
- },
- setError(err) {
- this.error = err;
- },
- reset() {
- this.done = false;
- this.error = null;
- }
- },
- {
- setDone: action.bound,
- setError: action.bound,
- reset: action.bound
- }
- );
-
- parseAlertmanagerResponse = response => {
- /*
- {"status": "success"}
- or
- {
- "status": "error",
- "errorType": "bad_data",
- "error": "silence 706959fd-4590-4e21-b983-859ba6ec0e1a already expired"
- }
- */
- if (response.status === "success") {
- this.deleteState.setError(null);
- } else if (response.status === "error" && response.error) {
- this.deleteState.setError(response.error);
- } else {
- this.deleteState.setError(JSON.stringify(response));
- }
- this.deleteState.setDone();
- };
-
- onFetchPreview = () => {
- const { silenceID } = this.props;
-
- const alertsURI =
- FormatBackendURI("alerts.json?") +
- FormatAlertsQ([
- FormatQuery(StaticLabels.SilenceID, QueryOperators.Equal, silenceID)
- ]);
-
- this.previewState.fetch = FetchWithCredentials(alertsURI, {})
- .then(result => result.json())
- .then(result => {
- this.previewState.groupsToUniqueLabels(Object.values(result.groups));
- this.previewState.setError(null);
- })
- .catch(err => {
- console.trace(err);
- return this.previewState.setError(
- `Request fetching affected alerts failed with: ${err.message}`
- );
- });
- };
-
- onDelete = () => {
- const { alertmanager, silenceID } = this.props;
-
- // if it's already deleted then do nothing
- if (this.deleteState.done && this.deleteState.error === null) return;
-
- // reset state so we get a spinner
- this.deleteState.reset();
-
- const isOpenAPI = semver.satisfies(alertmanager.version, ">=0.16.0");
-
- const uri = isOpenAPI
- ? `${alertmanager.uri}/api/v2/silence/${silenceID}`
- : `${alertmanager.uri}/api/v1/silence/${silenceID}`;
-
- this.deleteState.fetch = FetchWithCredentials(uri, {
- method: "DELETE",
- headers: alertmanager.headers
- })
- .then(result => {
- if (isOpenAPI) {
- if (result.ok) {
- this.deleteState.setError(null);
- this.deleteState.setDone();
- } else {
- result.text().then(this.deleteState.setError);
- this.deleteState.setDone();
- }
- } else {
- result.json().then(this.parseAlertmanagerResponse);
- }
- })
- .catch(err => {
- console.trace(err);
- this.deleteState.setDone();
- this.deleteState.setError(
- `Delete request failed with: ${err.message}`
- );
- });
- };
-
- componentDidMount() {
- this.onFetchPreview();
- }
-
- render() {
- const { alertStore, onHide } = this.props;
-
- const isDone = this.deleteState.done && this.deleteState.error === null;
-
- return (
-
-
-
Delete silence
-
-
-
- {this.deleteState.done ? (
- this.deleteState.error !== null ? (
-
- ) : (
-
- )
- ) : this.deleteState.fetch !== null ? (
-
- ) : this.previewState.error === null ? (
-
- ) : (
-
- )}
- {isDone ? null : (
-
-
-
- )}
-
-
- );
- }
- }
-);
-
-const DeleteSilence = observer(
- class DeleteSilence extends Component {
- static propTypes = {
- alertStore: PropTypes.instanceOf(AlertStore).isRequired,
- alertmanager: APIAlertmanagerUpstream.isRequired,
- silenceID: PropTypes.string.isRequired
- };
-
- toggle = observable(
- {
- visible: false,
- toggle() {
- this.visible = !this.visible;
- }
- },
- { toggle: action.bound }
- );
-
- render() {
- const { alertStore, alertmanager, silenceID } = this.props;
-
- return (
-
-
-
- Delete
-
-
-
-
-
- );
- }
- }
-);
-
-export { DeleteSilence, DeleteSilenceModalContent };
diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/DeleteSilence.test.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/DeleteSilence.test.js
deleted file mode 100644
index d5b677009..000000000
--- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/DeleteSilence.test.js
+++ /dev/null
@@ -1,237 +0,0 @@
-import React from "react";
-
-import { mount } from "enzyme";
-
-import { EmptyAPIResponse } from "__mocks__/Fetch";
-import { MockAlertGroup, MockAlert, MockAlertmanager } from "__mocks__/Alerts";
-import { AlertStore } from "Stores/AlertStore";
-import { DeleteSilence, DeleteSilenceModalContent } from "./DeleteSilence";
-
-let alertmanager;
-let alertStore;
-
-beforeEach(() => {
- alertmanager = MockAlertmanager();
- alertStore = new AlertStore([]);
- alertStore.data.upstreams.instances[0] = alertmanager;
- fetch.mockResponseOnce(JSON.stringify(MockAPIResponse()));
-
- jest.restoreAllMocks();
-});
-
-afterEach(() => {
- jest.restoreAllMocks();
- fetch.resetMocks();
-});
-
-const MockOnHide = jest.fn();
-
-const MockAPIResponse = () => {
- const response = EmptyAPIResponse();
- response.groups = {
- "1": MockAlertGroup(
- { alertname: "foo" },
- [MockAlert([], { instance: "foo" }, "suppressed")],
- [],
- { job: "foo" },
- {}
- )
- };
- return response;
-};
-
-const MountedDeleteSilence = () => {
- return mount(
-
- );
-};
-
-const MountedDeleteSilenceModalContent = () => {
- return mount(
-
- );
-};
-
-const VerifyResponse = async response => {
- const tree = MountedDeleteSilenceModalContent();
- await expect(tree.instance().previewState.fetch).resolves.toBeUndefined();
-
- fetch.mockResponseOnce(JSON.stringify(response));
- tree.find(".btn-outline-danger").simulate("click");
- await expect(tree.instance().deleteState.fetch).resolves.toBeUndefined();
-
- return tree;
-};
-
-describe("
", () => {
- it("label is 'Delete' by default", () => {
- const tree = MountedDeleteSilence();
- expect(tree.text()).toBe("Delete");
- });
-
- it("opens modal on click", () => {
- const tree = MountedDeleteSilence();
- tree
- .find(".badge")
- .at(0)
- .simulate("click");
- expect(tree.find(".modal-body")).toHaveLength(1);
- });
-});
-
-describe("
", () => {
- it("renders LabelSetList on mount", () => {
- const tree = MountedDeleteSilenceModalContent();
- expect(tree.find("LabelSetList")).toHaveLength(1);
- });
-
- it("fetches affected alerts on mount", () => {
- MountedDeleteSilenceModalContent();
- expect(fetch).toHaveBeenCalled();
- });
-
- it("renders ErrorMessage on failed fetch", async () => {
- jest.spyOn(console, "trace").mockImplementation(() => {});
- fetch.resetMocks();
- fetch.mockReject("Fetch error");
-
- const tree = MountedDeleteSilenceModalContent();
- await expect(tree.instance().previewState.fetch).resolves.toBeUndefined();
- tree.update();
- expect(tree.find("ErrorMessage")).toHaveLength(1);
- });
-
- it("renders ErrorMessage on fetch with non-JSON response", async () => {
- fetch.mockResponseOnce("not json");
- jest.spyOn(console, "trace").mockImplementation(() => {});
- fetch.resetMocks();
- fetch.mockReject("Fetch error");
-
- const tree = MountedDeleteSilenceModalContent();
- await expect(tree.instance().previewState.fetch).resolves.toBeUndefined();
- tree.update();
- expect(tree.find("ErrorMessage")).toHaveLength(1);
- });
-
- it("[v1] sends a DELETE request after clicking 'Confirm' button", async () => {
- await VerifyResponse({ status: "success" });
- expect(fetch.mock.calls[1][0]).toBe(
- "http://localhost/api/v1/silence/123456789"
- );
- expect(fetch.mock.calls[1][1]).toMatchObject({ method: "DELETE" });
- });
-
- it("[v2] sends a DELETE request after clicking 'Confirm' button", async () => {
- alertmanager.version = "0.16.2";
- await VerifyResponse({ status: "success" });
- expect(fetch.mock.calls[1][0]).toBe(
- "http://localhost/api/v2/silence/123456789"
- );
- expect(fetch.mock.calls[1][1]).toMatchObject({ method: "DELETE" });
- });
-
- it("[v1] sends headers from alertmanager config", async () => {
- alertmanager.headers = { Authorization: "Basic ***" };
- await VerifyResponse({ status: "success" });
- expect(fetch.mock.calls[1][0]).toBe(
- "http://localhost/api/v1/silence/123456789"
- );
- expect(fetch.mock.calls[1][1]).toMatchObject({
- credentials: "include",
- method: "DELETE",
- headers: { Authorization: "Basic ***" }
- });
- });
-
- it("[v1] sends headers from alertmanager config", async () => {
- alertmanager.headers = { Authorization: "Basic ***" };
- alertmanager.version = "0.16.2";
- await VerifyResponse({ status: "success" });
- expect(fetch.mock.calls[1][0]).toBe(
- "http://localhost/api/v2/silence/123456789"
- );
- expect(fetch.mock.calls[1][1]).toMatchObject({
- credentials: "include",
- method: "DELETE",
- headers: { Authorization: "Basic ***" }
- });
- });
-
- it("'Confirm' button is no-op after successful DELETE", async () => {
- const tree = await VerifyResponse({ status: "success" });
- expect(fetch.mock.calls[1][0]).toBe(
- "http://localhost/api/v1/silence/123456789"
- );
- expect(fetch.mock.calls[1][1]).toMatchObject({ method: "DELETE" });
-
- expect(fetch.mock.calls).toHaveLength(2);
- tree.find(".btn-outline-danger").simulate("click");
- expect(fetch.mock.calls).toHaveLength(2);
- tree.instance().onDelete();
- expect(fetch.mock.calls).toHaveLength(2);
- });
-
- it("renders SuccessMessage on 'success' response status", async () => {
- const tree = await VerifyResponse({ status: "success" });
- tree.update();
- expect(tree.find("SuccessMessage")).toHaveLength(1);
- });
-
- it("renders ErrorMessage on 'error' response status", async () => {
- const tree = await VerifyResponse({ status: "error", error: "fake error" });
- tree.update();
- expect(tree.find("ErrorMessage")).toHaveLength(1);
- });
-
- it("renders ErrorMessage on unhandled response status", async () => {
- const tree = await VerifyResponse({ status: "foo bar" });
- tree.update();
- expect(tree.find("ErrorMessage")).toHaveLength(1);
- });
-
- it("renders ErrorMessage on unhandled response body", async () => {
- const tree = await VerifyResponse({ foo: "bar" });
- tree.update();
- expect(tree.find("ErrorMessage")).toHaveLength(1);
- });
-
- it("[v1] renders ErrorMessage on failed fetch request", async () => {
- const tree = MountedDeleteSilenceModalContent();
- await expect(tree.instance().previewState.fetch).resolves.toBeUndefined();
-
- jest.spyOn(console, "trace").mockImplementation(() => {});
- fetch.resetMocks();
- fetch.mockReject("Fetch error");
-
- tree.find(".btn-outline-danger").simulate("click");
- await expect(tree.instance().deleteState.fetch).resolves.toBeUndefined();
-
- tree.update();
- expect(tree.find("ErrorMessage")).toHaveLength(1);
- });
-
- it("[v2] renders ErrorMessage on failed fetch request", async () => {
- alertmanager.version = "0.16.2";
- const tree = MountedDeleteSilenceModalContent();
- await expect(tree.instance().previewState.fetch).resolves.toBeUndefined();
-
- jest.spyOn(console, "trace").mockImplementation(() => {});
- fetch.resetMocks();
- fetch.mockResponseOnce("500 Internal Server Error", { status: 500 });
-
- tree.find(".btn-outline-danger").simulate("click");
- await expect(tree.instance().deleteState.fetch).resolves.toBeUndefined();
-
- tree.update();
- expect(tree.find("ErrorMessage")).toHaveLength(1);
- });
-});
diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/__snapshots__/index.test.js.snap b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/__snapshots__/index.test.js.snap
deleted file mode 100644
index cf84ad9ab..000000000
--- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/__snapshots__/index.test.js.snap
+++ /dev/null
@@ -1,272 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`
matches snapshot when data is not present in alertStore 1`] = `
-"
-
-
- Silenced by default/4cf5fd82-1edd-4169-99d1-ff8415e72179
-
-
-"
-`;
-
-exports[`
matches snapshot when data is present in alertStore 1`] = `
-"
-
-
-
-
-
-
-
- Fake silence
-
-
- …
-
-
-
-
-
-
-"
-`;
-
-exports[`
matches snapshot with expaned details 1`] = `
-"
-
-
-
-
-
-
-
- Fake silence
-
-
- …
-
-
-
-
-
-
-
-
-
-
- Started
-
-
-
-
- Expires
-
-
-
-
- Edit
-
-
-
- Delete
-
-
-
-
-
-
- Matchers:
-
-
-
-
- alertname=MockAlert
-
-
- instance=~foo[0-9]+
-
-
-
-
-
-"
-`;
diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.css b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.css
deleted file mode 100644
index d88dc1c88..000000000
--- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.css
+++ /dev/null
@@ -1,8 +0,0 @@
-.progress.silence-progress {
- height: 2px;
- margin-top: 2px;
-}
-
-.cite.components-grid-alertgroup-silences {
- font-size: 100%;
-}
diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.js
deleted file mode 100644
index 026649b84..000000000
--- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.js
+++ /dev/null
@@ -1,375 +0,0 @@
-import React, { Component } from "react";
-import PropTypes from "prop-types";
-
-import { observable, action } from "mobx";
-import { observer, inject } from "mobx-react";
-
-import hash from "object-hash";
-
-import moment from "moment";
-import Moment from "react-moment";
-
-import Truncate from "react-truncate";
-
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons/faExternalLinkAlt";
-import { faChevronUp } from "@fortawesome/free-solid-svg-icons/faChevronUp";
-import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown";
-import { faEdit } from "@fortawesome/free-solid-svg-icons/faEdit";
-import { faCalendarCheck } from "@fortawesome/free-solid-svg-icons/faCalendarCheck";
-import { faCalendarTimes } from "@fortawesome/free-solid-svg-icons/faCalendarTimes";
-import { faFilter } from "@fortawesome/free-solid-svg-icons/faFilter";
-
-import {
- APIAlertAlertmanagerState,
- APIAlertmanagerUpstream,
- APISilence
-} from "Models/API";
-import { AlertStore } from "Stores/AlertStore";
-import { SilenceFormStore } from "Stores/SilenceFormStore";
-import { StaticLabels, QueryOperators } from "Common/Query";
-import { FilteringLabel } from "Components/Labels/FilteringLabel";
-import { TooltipWrapper } from "Components/TooltipWrapper";
-import { RenderLinkAnnotation } from "../Annotation";
-import { DeleteSilence } from "./DeleteSilence";
-
-import "./index.css";
-
-const SilenceComment = ({ silence, collapsed, afterUpdate }) => {
- const showLines = 2;
- if (silence.jiraURL) {
- return (
-
-
-
- {silence.comment}
-
-
- );
- }
- return (
-
{silence.comment}
- );
-};
-SilenceComment.propTypes = {
- silence: APISilence.isRequired,
- collapsed: PropTypes.bool.isRequired,
- afterUpdate: PropTypes.func.isRequired
-};
-
-const SilenceExpiryBadgeWithProgress = ({ silence, progress }) => {
- // if silence is expired we can skip progress value calculation
- if (moment(silence.endsAt) < moment()) {
- return (
-
- Expired {silence.endsAt}
-
- );
- }
-
- let progressClass;
- if (progress > 90) {
- progressClass = "progress-bar bg-danger";
- } else if (progress > 75) {
- progressClass = "progress-bar bg-warning";
- } else {
- progressClass = "progress-bar bg-success";
- }
-
- return (
-
- Expires {silence.endsAt}
-
-
- );
-};
-SilenceExpiryBadgeWithProgress.propTypes = {
- silence: APISilence.isRequired,
- progress: PropTypes.number.isRequired
-};
-
-const SilenceDetails = ({
- alertStore,
- alertmanager,
- silence,
- onEditSilence
-}) => {
- let expiresClass = "";
- let expiresLabel = "Expires";
- if (moment(silence.endsAt) < moment()) {
- expiresClass = "text-danger";
- expiresLabel = "Expired";
- }
-
- return (
-
-
-
-
-
-
-
-
- Started {silence.startsAt}
-
-
-
- {expiresLabel} {silence.endsAt}
-
-
-
- Edit
-
-
-
-
-
-
-
- Matchers:
-
-
-
- {silence.matchers.map(matcher => (
-
- {matcher.name}
- {matcher.isRegex ? QueryOperators.Regex : QueryOperators.Equal}
- {matcher.value}
-
- ))}
-
-
-
- );
-};
-SilenceDetails.propTypes = {
- alertmanager: APIAlertmanagerUpstream.isRequired,
- silence: APISilence.isRequired,
- onEditSilence: PropTypes.func.isRequired
-};
-
-//
-const FallbackSilenceDesciption = ({ alertmanagerName, silenceID }) => {
- return (
-
-
- Silenced by {alertmanagerName}/{silenceID}
-
-
- );
-};
-FallbackSilenceDesciption.propTypes = {
- alertmanagerName: PropTypes.string.isRequired,
- silenceID: PropTypes.string.isRequired
-};
-
-const Silence = inject("alertStore")(
- observer(
- class Silence extends Component {
- static propTypes = {
- alertStore: PropTypes.instanceOf(AlertStore).isRequired,
- silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
- alertmanagerState: APIAlertAlertmanagerState.isRequired,
- silenceID: PropTypes.string.isRequired,
- afterUpdate: PropTypes.func.isRequired
- };
-
- // store collapse state, by default only silence comment is visible
- // the rest of the silence is hidden until expanded by a click
- collapse = observable(
- {
- value: true,
- toggle() {
- this.value = !this.value;
- }
- },
- { toggle: action.bound },
- { name: "Silence collpase toggle" }
- );
-
- progress = observable(
- {
- value: 0,
- calculate(startsAt, endsAt) {
- const durationDone = moment().unix() - moment(startsAt).unix();
- const durationTotal =
- moment(endsAt).unix() - moment(startsAt).unix();
- const durationPercent = Math.floor(
- (durationDone / durationTotal) * 100
- );
- if (this.value !== durationPercent) {
- this.value = durationPercent;
- }
- }
- },
- {
- calculate: action.bound
- }
- );
-
- constructor(props) {
- super(props);
-
- this.recalculateProgress();
- 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, 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[alertmanagerState.cluster];
- if (!amSilences) return null;
-
- // next check if alertmanager has our silence ID
- const silence = amSilences[silenceID];
- if (!silence) return null;
-
- return silence;
- };
-
- recalculateProgress = () => {
- const silence = this.getSilence();
- if (silence !== null) {
- this.progress.calculate(silence.startsAt, silence.endsAt);
- }
- };
-
- onEditSilence = () => {
- const { silenceFormStore } = this.props;
-
- const silence = this.getSilence();
- const alertmanager = this.getAlertmanager();
-
- silenceFormStore.data.fillFormFromSilence(alertmanager, silence);
- silenceFormStore.data.resetProgress();
- silenceFormStore.toggle.show();
- };
-
- componentDidUpdate() {
- const { afterUpdate } = this.props;
- afterUpdate();
- }
-
- componentWillUnmount() {
- clearInterval(this.progressTimer);
- this.progressTimer = null;
- }
-
- render() {
- const {
- alertStore,
- alertmanagerState,
- silenceID,
- afterUpdate
- } = this.props;
-
- const silence = this.getSilence();
- if (!silence)
- return (
-
- );
-
- const alertmanager = this.getAlertmanager();
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- {silence.createdBy}
-
- {this.collapse.value ? (
-
- ) : null}
-
-
-
- {this.collapse.value ? null : (
-
- )}
-
- );
- }
- }
- )
-);
-
-export {
- Silence,
- SilenceDetails,
- SilenceComment,
- SilenceExpiryBadgeWithProgress
-};
diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.test.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.test.js
deleted file mode 100644
index d877deeb2..000000000
--- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.test.js
+++ /dev/null
@@ -1,306 +0,0 @@
-import React from "react";
-
-import { toJS } from "mobx";
-import { Provider } from "mobx-react";
-
-import { mount } from "enzyme";
-
-import toDiffableHtml from "diffable-html";
-
-import moment from "moment";
-import { advanceTo, clear } from "jest-date-mock";
-
-import { AlertStore } from "Stores/AlertStore";
-import { SilenceFormStore } from "Stores/SilenceFormStore";
-import { Silence, SilenceDetails } from ".";
-
-const mockAfterUpdate = jest.fn();
-
-const alertmanager = {
- name: "default",
- cluster: "default",
- state: "suppressed",
- startsAt: "2000-01-01T10:00:00Z",
- source: "localhost/prometheus",
- silencedBy: ["4cf5fd82-1edd-4169-99d1-ff8415e72179"],
- inhibitedBy: []
-};
-
-const silence = {
- id: "4cf5fd82-1edd-4169-99d1-ff8415e72179",
- matchers: [
- {
- name: "alertname",
- value: "MockAlert",
- isRegex: false
- },
- {
- name: "instance",
- value: "foo[0-9]+",
- isRegex: true
- }
- ],
- startsAt: "2000-01-01T10:00:00Z",
- endsAt: "2000-01-01T20:00:00Z",
- createdAt: "0001-01-01T00:00:00Z",
- createdBy: "me@example.com",
- comment: "Fake silence",
- jiraID: "",
- jiraURL: ""
-};
-
-let alertStore;
-let silenceFormStore;
-
-beforeEach(() => {
- advanceTo(moment.utc([2000, 0, 1, 15, 0, 0]));
- alertStore = new AlertStore([]);
- alertStore.data.upstreams = {
- counters: {
- total: 1,
- healthy: 1,
- failed: 0
- },
- instances: [
- {
- name: "default",
- cluster: "default",
- uri: "file:///mock",
- publicURI: "http://example.com",
- headers: {},
- error: "",
- version: "0.15.0",
- clusterMembers: ["default"]
- }
- ],
- clusters: { default: ["default"] }
- };
- alertStore.data.silences = {
- default: {
- "4cf5fd82-1edd-4169-99d1-ff8415e72179": silence
- }
- };
- silenceFormStore = new SilenceFormStore();
-});
-
-afterEach(() => {
- jest.restoreAllMocks();
- // reset Date() to current time
- clear();
-});
-
-const MountedSilence = alertmanagerState => {
- return mount(
-
-
-
- );
-};
-
-const MountedSilenceDetails = onEditSilence => {
- return mount(
-
-
-
- ).find("SilenceDetails");
-};
-
-describe("
", () => {
- it("matches snapshot when data is present in alertStore", () => {
- const tree = MountedSilence(alertmanager).find("Silence");
- expect(toDiffableHtml(tree.html())).toMatchSnapshot();
- });
-
- it("renders full silence when data is present in alertStore", () => {
- 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(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(alertmanager);
- const fallback = tree.find("FallbackSilenceDesciption");
- expect(fallback).toHaveLength(1);
- expect(tree.text()).toBe(
- "Silenced by default/4cf5fd82-1edd-4169-99d1-ff8415e72179"
- );
- });
-
- it("renders FallbackSilenceDesciption when silence data is not present in alertStore", () => {
- alertStore.data.silences.default = {};
- const tree = MountedSilence(alertmanager);
- const fallback = tree.find("FallbackSilenceDesciption");
- expect(fallback).toHaveLength(1);
- expect(tree.text()).toBe(
- "Silenced by default/4cf5fd82-1edd-4169-99d1-ff8415e72179"
- );
- });
-
- it("clicking on expand toggle shows silence details", () => {
- const tree = MountedSilence(alertmanager);
- const toggle = tree.find(".float-right.cursor-pointer");
- toggle.simulate("click");
- const details = tree.find("SilenceDetails");
- expect(details).toHaveLength(1);
- });
-
- it("matches snapshot with expaned details", () => {
- const tree = MountedSilence(alertmanager).find("Silence");
- tree.instance().collapse.toggle();
- expect(toDiffableHtml(tree.html())).toMatchSnapshot();
- });
-
- it("renders comment as link when jiraURL is set and silence is collapsed", () => {
- alertStore.data.silences.default[silence.id].jiraURL =
- "http://jira.example.com";
- 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("renders comment as link when jiraURL is set and silence is expaned", () => {
- alertStore.data.silences.default[silence.id].jiraURL =
- "http://jira.example.com";
- const tree = MountedSilence(alertmanager).find("Silence");
- tree.instance().collapse.toggle();
- 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(alertmanager).find("Silence");
- const instance = tree.instance();
- expect(instance.progressTimer).toBeTruthy();
- instance.componentWillUnmount();
- expect(instance.progressTimer).toBeNull();
- });
-
- 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",
- cluster: "default",
- uri: "file:///mock",
- publicURI: "http://example.com",
- headers: {},
- error: "",
- version: "0.15.0",
- clusterMembers: ["default"]
- });
- });
-
- 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"
- });
- });
-
- it("clicking on silence edit button calls silenceFormStore.data.fillFormFromSilence", () => {
- const fillSpy = jest.spyOn(silenceFormStore.data, "fillFormFromSilence");
- const tree = MountedSilence(alertmanager);
-
- // expand silence
- tree.find(".float-right.cursor-pointer").simulate("click");
-
- const button = tree.find(".badge-secondary.components-label-with-hover");
- expect(button.text()).toBe("Edit");
- button.simulate("click");
- expect(fillSpy).toHaveBeenCalled();
- });
-
- it("clicking on silence edit button opens the silence form", () => {
- const tree = MountedSilence(alertmanager);
-
- // expand silence
- tree.find(".float-right.cursor-pointer").simulate("click");
-
- const button = tree.find(".badge-secondary.components-label-with-hover");
- expect(button.text()).toBe("Edit");
- button.simulate("click");
- expect(silenceFormStore.toggle.visible).toBe(true);
- });
-});
-
-describe("
", () => {
- it("unexpired silence endsAt label doesn't use 'danger' class", () => {
- const tree = MountedSilenceDetails(jest.fn());
- const endsAt = tree.find("span.badge").at(1);
- expect(endsAt.html()).not.toMatch(/text-danger/);
- });
-
- it("expired silence endsAt label uses 'danger' class", () => {
- advanceTo(moment.utc([2000, 0, 1, 23, 0, 0]));
- const tree = MountedSilenceDetails(jest.fn());
- const endsAt = tree.find("span.badge").at(2);
- expect(endsAt.html()).toMatch(/text-danger/);
- });
-
- it("id links to Alertmanager silence view via alertmanager.publicURI", () => {
- const tree = MountedSilenceDetails(jest.fn());
- const link = tree.find("a");
- expect(link.props().href).toBe(
- "http://example.com/#/silences/4cf5fd82-1edd-4169-99d1-ff8415e72179"
- );
- });
-});
-
-describe("
", () => {
- it("renders with class 'danger' and no progressbar when expired", () => {
- advanceTo(moment.utc([2001, 0, 1, 23, 0, 0]));
- 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(moment.utc([2000, 0, 1, 19, 30, 0]));
- const tree = MountedSilence(alertmanager);
- expect(tree.html()).toMatch(/progress-bar bg-danger/);
- });
-
- it("progressbar uses class 'danger' when > 75%", () => {
- advanceTo(moment.utc([2000, 0, 1, 17, 45, 0]));
- const tree = MountedSilence(alertmanager);
- expect(tree.html()).toMatch(/progress-bar bg-warning/);
- });
-
- it("calling calculate() on progress multiple times in a row doesn't change the value", () => {
- const startsAt = moment.utc([2000, 0, 1, 10, 0, 0]);
- const endsAt = moment.utc([2000, 0, 1, 20, 0, 0]);
-
- const tree = MountedSilence(alertmanager).find("Silence");
- const instance = tree.instance();
-
- const value = toJS(instance.progress.value);
- instance.progress.calculate(startsAt, endsAt);
- instance.progress.calculate(startsAt, endsAt);
- instance.progress.calculate(startsAt, endsAt);
- expect(toJS(instance.progress.value)).toBe(value);
- });
-});
diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silences.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silences.js
new file mode 100644
index 000000000..e14db7cdb
--- /dev/null
+++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silences.js
@@ -0,0 +1,57 @@
+import React from "react";
+import PropTypes from "prop-types";
+
+import { ManagedSilence } from "Components/ManagedSilence";
+const FallbackSilenceDesciption = ({ silenceID }) => {
+ return (
+
+ Silenced by {silenceID}
+
+ );
+};
+FallbackSilenceDesciption.propTypes = {
+ silenceID: PropTypes.string.isRequired
+};
+
+const GetSilenceFromStore = (alertStore, cluster, silenceID) => {
+ const amSilences = alertStore.data.silences[cluster];
+ if (!amSilences) return null;
+
+ // next check if alertmanager has our silence ID
+ const silence = amSilences[silenceID];
+ if (!silence) return null;
+
+ return silence;
+};
+
+const RenderSilence = (
+ alertStore,
+ silenceFormStore,
+ afterUpdate,
+ cluster,
+ silenceID
+) => {
+ const silence = GetSilenceFromStore(alertStore, cluster, silenceID);
+
+ if (silence === null) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+export { RenderSilence };
diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.js
index 3e0cc34a1..5441f759e 100644
--- a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.js
+++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.js
@@ -273,6 +273,7 @@ const AlertGroup = observer(
group={group}
alertmanagers={footerAlertmanagers}
afterUpdate={afterUpdate}
+ alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
) : null}