mirror of
https://github.com/prymitive/karma
synced 2026-05-07 03:26:52 +00:00
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:
@@ -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);
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user