feat(ui): silence match preview should use selected Alertmanagers

Counter should use selected Alertmanager instances, rather than match all alerts from all instances
This commit is contained in:
Łukasz Mierzwa
2018-10-06 13:34:37 +01:00
parent 57017fa7b9
commit 8b8a7fb813
8 changed files with 124 additions and 20 deletions

View File

@@ -141,6 +141,7 @@ const SilenceForm = observer(
{silenceFormStore.data.matchers.map(matcher => (
<SilenceMatch
key={matcher.id}
silenceFormStore={silenceFormStore}
matcher={matcher}
onDelete={() => {
silenceFormStore.data.deleteMatcher(matcher.id);

View File

@@ -6,6 +6,7 @@ import { observer } from "mobx-react";
import { components } from "react-select";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { SilenceFormMatcher } from "Models/SilenceForm";
import { MultiSelect } from "Components/MultiSelect";
import { ValidationError } from "Components/MultiSelect/ValidationError";
@@ -21,7 +22,10 @@ const Placeholder = props => {
const ValueContainer = ({ children, ...props }) => (
<components.ValueContainer {...props}>
<MatchCounter matcher={props.selectProps.matcher} />
<MatchCounter
silenceFormStore={props.selectProps.silenceFormStore}
matcher={props.selectProps.matcher}
/>
{children}
</components.ValueContainer>
);
@@ -29,6 +33,7 @@ const ValueContainer = ({ children, ...props }) => (
const LabelValueInput = observer(
class LabelValueInput extends MultiSelect {
static propTypes = {
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
matcher: SilenceFormMatcher.isRequired,
isValid: PropTypes.bool.isRequired
};
@@ -47,7 +52,7 @@ const LabelValueInput = observer(
});
renderProps = () => {
const { matcher, isValid } = this.props;
const { silenceFormStore, matcher, isValid } = this.props;
return {
instanceId: `silence-input-label-value-${matcher.id}`,
@@ -57,6 +62,7 @@ const LabelValueInput = observer(
isMulti: true,
onChange: this.onChange,
components: { ValueContainer, Placeholder },
silenceFormStore: silenceFormStore,
matcher: matcher
};
};

View File

@@ -4,12 +4,18 @@ import { shallow, mount } from "enzyme";
import toDiffableHtml from "diffable-html";
import { NewEmptyMatcher, MatcherValueToObject } from "Stores/SilenceFormStore";
import {
SilenceFormStore,
NewEmptyMatcher,
MatcherValueToObject
} from "Stores/SilenceFormStore";
import { LabelValueInput } from "./LabelValueInput";
let silenceFormStore;
let matcher;
beforeEach(() => {
silenceFormStore = new SilenceFormStore();
matcher = NewEmptyMatcher();
matcher.name = "name";
matcher.suggestions.names = [
@@ -23,11 +29,23 @@ beforeEach(() => {
});
const ShallowLabelValueInput = isValid => {
return shallow(<LabelValueInput matcher={matcher} isValid={isValid} />);
return shallow(
<LabelValueInput
silenceFormStore={silenceFormStore}
matcher={matcher}
isValid={isValid}
/>
);
};
const MountedLabelValueInput = isValid => {
return mount(<LabelValueInput matcher={matcher} isValid={isValid} />);
return mount(
<LabelValueInput
silenceFormStore={silenceFormStore}
matcher={matcher}
isValid={isValid}
/>
);
};
const ValidateSuggestions = () => {

View File

@@ -1,4 +1,5 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { observable, action } from "mobx";
import { observer } from "mobx-react";
@@ -10,13 +11,15 @@ import hash from "object-hash";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle";
import { FormatQuery, QueryOperators } from "Common/Query";
import { FormatQuery, QueryOperators, StaticLabels } from "Common/Query";
import { FormatBackendURI, FormatAlertsQ } from "Stores/AlertStore";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { SilenceFormMatcher } from "Models/SilenceForm";
const MatchCounter = observer(
class MatchCounter extends Component {
static propTypes = {
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
matcher: SilenceFormMatcher.isRequired
};
@@ -39,7 +42,7 @@ const MatchCounter = observer(
);
onFetch = throttle(() => {
const { matcher } = this.props;
const { silenceFormStore, matcher } = this.props;
const filters = [];
@@ -59,6 +62,26 @@ const MatchCounter = observer(
)
);
if (silenceFormStore.data.alertmanagers.length > 1) {
filters.push(
FormatQuery(
StaticLabels.AlertManager,
QueryOperators.Regex,
`^(${silenceFormStore.data.alertmanagers
.map(am => am.label)
.join("|")})$`
)
);
} else if (silenceFormStore.data.alertmanagers.length === 1) {
filters.push(
FormatQuery(
StaticLabels.AlertManager,
QueryOperators.Equal,
silenceFormStore.data.alertmanagers[0].label
)
);
}
const alertsURI =
FormatBackendURI("alerts.json?") + FormatAlertsQ(filters);
@@ -97,13 +120,7 @@ const MatchCounter = observer(
}
render() {
const { matcher } = this.props;
const matcherHash = hash({
name: matcher.name,
values: matcher.values,
isRegex: matcher.isRegex
});
const { silenceFormStore, matcher } = this.props;
if (this.matchedAlerts.error !== null) {
return (
@@ -111,6 +128,15 @@ const MatchCounter = observer(
);
}
const matcherHash = hash({
alertmanagers: silenceFormStore.data.alertmanagers,
matcher: {
name: matcher.name,
values: matcher.values,
isRegex: matcher.isRegex
}
});
return (
<span
className="badge badge-light badge-pill"

View File

@@ -4,14 +4,20 @@ import { mount } from "enzyme";
import toDiffableHtml from "diffable-html";
import { NewEmptyMatcher, MatcherValueToObject } from "Stores/SilenceFormStore";
import {
SilenceFormStore,
NewEmptyMatcher,
MatcherValueToObject
} from "Stores/SilenceFormStore";
import { MatchCounter } from "./MatchCounter";
let matcher;
let silenceFormStore;
beforeEach(() => {
fetch.resetMocks();
silenceFormStore = new SilenceFormStore();
matcher = NewEmptyMatcher();
});
@@ -20,7 +26,9 @@ afterEach(() => {
});
const MountedMatchCounter = () => {
return mount(<MatchCounter matcher={matcher} />);
return mount(
<MatchCounter silenceFormStore={silenceFormStore} matcher={matcher} />
);
};
describe("<MatchCounter />", () => {
@@ -112,4 +120,37 @@ describe("<MatchCounter />", () => {
"./alerts.json?q=foo%3D~%5E%28bar%7Cbaz%29%24"
);
});
it("selecting one Alertmanager instance appends it to the filters", async () => {
fetch.mockResponse(JSON.stringify({ totalAlerts: 0 }));
silenceFormStore.data.alertmanagers = [MatcherValueToObject("am1")];
matcher.name = "foo";
matcher.values = [MatcherValueToObject("bar")];
matcher.isRegex = false;
const tree = MountedMatchCounter();
await expect(tree.instance().matchedAlerts.fetch).resolves.toBeUndefined();
expect(fetch.mock.calls[0][0]).toBe(
"./alerts.json?q=foo%3Dbar&q=%40alertmanager%3Dam1"
);
});
it("selecting two Alertmanager instances appends it correctly to the filters", async () => {
fetch.mockResponse(JSON.stringify({ totalAlerts: 0 }));
silenceFormStore.data.alertmanagers = [
MatcherValueToObject("am1"),
MatcherValueToObject("am1")
];
matcher.name = "foo";
matcher.values = [MatcherValueToObject("bar")];
matcher.isRegex = false;
const tree = MountedMatchCounter();
await expect(tree.instance().matchedAlerts.fetch).resolves.toBeUndefined();
expect(fetch.mock.calls[0][0]).toBe(
"./alerts.json?q=foo%3Dbar&q=%40alertmanager%3D~%5E%28am1%7Cam1%29%24"
);
});
});

View File

@@ -7,7 +7,7 @@ exports[`<LabelValueInput /> matches snapshot 1`] = `
<div class=\\"css-10war8y\\">
<span class=\\"badge badge-light badge-pill\\"
style=\\"font-size:85%\\"
data-hash=\\"a970b397347f75d0af6435a66f0b3aa3e2bc0b99\\"
data-hash=\\"76ea01a7b7d0189a690ed2b409ad07a87dbd039c\\"
>
0
</span>

View File

@@ -4,7 +4,7 @@ exports[`<MatchCounter /> matches snapshot with empty matcher 1`] = `
"
<span class=\\"badge badge-light badge-pill\\"
style=\\"font-size: 85%;\\"
data-hash=\\"a775f465445363786b29ebd6162e99f6ff0d22b7\\"
data-hash=\\"2153b6623363af13fa91f1ad35fa4cfc6462d349\\"
>
0
</span>

View File

@@ -7,6 +7,7 @@ import { observer } from "mobx-react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTrash } from "@fortawesome/free-solid-svg-icons/faTrash";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { SilenceFormMatcher } from "Models/SilenceForm";
import { LabelNameInput } from "./LabelNameInput";
import { LabelValueInput } from "./LabelValueInput";
@@ -14,6 +15,7 @@ import { LabelValueInput } from "./LabelValueInput";
const SilenceMatch = observer(
class SilenceMatch extends Component {
static propTypes = {
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
matcher: SilenceFormMatcher.isRequired,
showDelete: PropTypes.bool.isRequired,
onDelete: PropTypes.func.isRequired,
@@ -30,7 +32,13 @@ const SilenceMatch = observer(
});
render() {
const { matcher, showDelete, onDelete, isValid } = this.props;
const {
silenceFormStore,
matcher,
showDelete,
onDelete,
isValid
} = this.props;
return (
<div className="d-flex flex-fill flex-lg-row flex-column mb-3">
@@ -38,7 +46,11 @@ const SilenceMatch = observer(
<LabelNameInput matcher={matcher} isValid={isValid} />
</div>
<div className="flex-shrink-0 flex-grow-0 flex-basis-50 pr-lg-2 pb-2 pb-lg-0">
<LabelValueInput matcher={matcher} isValid={isValid} />
<LabelValueInput
silenceFormStore={silenceFormStore}
matcher={matcher}
isValid={isValid}
/>
</div>
<div className="flex-shrink-0 flex-grow-1 flex-basis-auto form-check form-check-inline d-flex justify-content-between m-0">
<span>