mirror of
https://github.com/prymitive/karma
synced 2026-05-21 04:33:07 +00:00
717 lines
20 KiB
TypeScript
717 lines
20 KiB
TypeScript
import { observable, action, computed } from "mobx";
|
|
|
|
import uniqueId from "lodash.uniqueid";
|
|
|
|
import { parseISO } from "date-fns/parseISO";
|
|
import { addHours } from "date-fns/addHours";
|
|
import { addMinutes } from "date-fns/addMinutes";
|
|
import { subMinutes } from "date-fns/subMinutes";
|
|
import { differenceInDays } from "date-fns/differenceInDays";
|
|
import { differenceInHours } from "date-fns/differenceInHours";
|
|
import { differenceInMinutes } from "date-fns/differenceInMinutes";
|
|
|
|
import type {
|
|
APIAlertT,
|
|
APIAlertGroupT,
|
|
APIAlertmanagerUpstreamT,
|
|
AlertmanagerSilencePayloadT,
|
|
AlertmanagerSilenceMatcherT,
|
|
} from "Models/APITypes";
|
|
import { StringToOption, OptionT, MultiValueOptionT } from "Common/Select";
|
|
import { QueryOperators } from "Common/Query";
|
|
|
|
export interface MatcherT {
|
|
name: string;
|
|
values: OptionT[];
|
|
isEqual: boolean;
|
|
isRegex: boolean;
|
|
}
|
|
|
|
export interface MatcherWithIDT extends MatcherT {
|
|
id: string;
|
|
}
|
|
|
|
interface SimplifiedMatcherT {
|
|
n: string;
|
|
v: string[];
|
|
r: boolean;
|
|
e: boolean;
|
|
}
|
|
|
|
interface SilenceFormDataFromBase64 {
|
|
am: MultiValueOptionT[];
|
|
m: SimplifiedMatcherT[];
|
|
d: number;
|
|
c: string;
|
|
}
|
|
|
|
const NewEmptyMatcher = (): MatcherWithIDT => {
|
|
return {
|
|
id: uniqueId(),
|
|
name: "",
|
|
values: [],
|
|
isRegex: false,
|
|
isEqual: true,
|
|
};
|
|
};
|
|
|
|
const MatcherToOperator = (
|
|
matcher: MatcherT | MatcherWithIDT | AlertmanagerSilenceMatcherT,
|
|
): string => {
|
|
if (matcher.isRegex) {
|
|
return matcher.isEqual === false
|
|
? QueryOperators.NegativeRegex
|
|
: QueryOperators.Regex;
|
|
}
|
|
return matcher.isEqual === false
|
|
? QueryOperators.NotEqual
|
|
: QueryOperators.Equal;
|
|
};
|
|
|
|
const AlertmanagerClustersToOption = (clusterDict: {
|
|
[key: string]: string[];
|
|
}): MultiValueOptionT[] =>
|
|
Object.entries(clusterDict).map(([clusterID, clusterMembers]) => ({
|
|
label:
|
|
clusterMembers.length > 1 ? `Cluster: ${clusterID}` : clusterMembers[0],
|
|
value: clusterMembers,
|
|
}));
|
|
|
|
export const EscapeRegex = (v: string): string => {
|
|
return v.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
};
|
|
|
|
export const UnescapeRegex = (v: string): string => {
|
|
return v.replaceAll("\\", "");
|
|
};
|
|
|
|
const MatchersFromGroup = (
|
|
group: APIAlertGroupT,
|
|
stripLabels: string[],
|
|
onlyActive?: boolean,
|
|
): MatcherWithIDT[] => {
|
|
const matchers: MatcherWithIDT[] = [];
|
|
|
|
const allLabels: { [key: string]: string[] } = {};
|
|
for (const [state, labels] of Object.entries(group.allLabels)) {
|
|
if (onlyActive === true && state !== "active") {
|
|
continue;
|
|
}
|
|
for (const [key, values] of Object.entries(labels).filter(
|
|
([key, _]) => !stripLabels.includes(key),
|
|
)) {
|
|
allLabels[key] = Array.from(
|
|
new Set([...(allLabels[key] || []), ...values]),
|
|
);
|
|
}
|
|
}
|
|
for (const [key, values] of Object.entries(allLabels)) {
|
|
matchers.push({
|
|
id: uniqueId(),
|
|
name: key,
|
|
values: values.map((value) => StringToOption(value)),
|
|
isRegex: values.length > 1,
|
|
isEqual: true,
|
|
});
|
|
}
|
|
|
|
return matchers;
|
|
};
|
|
|
|
const MatchersFromAlerts = (
|
|
group: APIAlertGroupT,
|
|
stripLabels: string[],
|
|
alerts: APIAlertT[],
|
|
): MatcherWithIDT[] => {
|
|
const matchers: MatcherWithIDT[] = [];
|
|
|
|
// add matchers for all shared labels in this group
|
|
for (const [key, value] of Object.entries(
|
|
Object.assign(
|
|
{},
|
|
Object.fromEntries(group.labels.map((l) => [l.name, l.value])),
|
|
Object.fromEntries(group.shared.labels.map((l) => [l.name, l.value])),
|
|
),
|
|
)) {
|
|
if (!stripLabels.includes(key)) {
|
|
const matcher = NewEmptyMatcher();
|
|
matcher.name = key;
|
|
matcher.values = [StringToOption(value)];
|
|
matchers.push(matcher);
|
|
}
|
|
}
|
|
|
|
// array of arrays with label keys for each alert
|
|
const allLabelKeys = alerts
|
|
.map((alert) => alert.labels.map((l) => l.name))
|
|
.filter((a) => a.length > 0);
|
|
|
|
// this is the list of label key that are shared across all alerts in the group
|
|
// https://stackoverflow.com/a/34498210/1154047
|
|
const sharedLabelKeys = allLabelKeys.length
|
|
? allLabelKeys.reduce(function (r, a) {
|
|
const last: { [key: string]: number } = {};
|
|
return r.filter(function (b) {
|
|
const p = a.indexOf(b, last[b] || 0);
|
|
if (~p) {
|
|
last[b] = p + 1;
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
})
|
|
: [];
|
|
|
|
// add matchers for all unique labels in this group
|
|
const labels: { [key: string]: Set<string> } = {};
|
|
for (const alert of alerts) {
|
|
for (const label of alert.labels) {
|
|
if (
|
|
sharedLabelKeys.includes(label.name) &&
|
|
!stripLabels.includes(label.name)
|
|
) {
|
|
if (!labels[label.name]) {
|
|
labels[label.name] = new Set();
|
|
}
|
|
labels[label.name].add(label.value);
|
|
}
|
|
}
|
|
}
|
|
for (const [key, values] of Object.entries(labels)) {
|
|
matchers.push({
|
|
id: uniqueId(),
|
|
name: key,
|
|
values: Array.from(values)
|
|
.sort()
|
|
.map((value) => StringToOption(value)),
|
|
isRegex: values.size > 1,
|
|
isEqual: true,
|
|
});
|
|
}
|
|
|
|
return matchers;
|
|
};
|
|
|
|
export interface ClusterRequestT {
|
|
cluster: string;
|
|
members: string[];
|
|
isDone: boolean;
|
|
silenceID: undefined | string;
|
|
silenceLink: undefined | string;
|
|
error: null | string;
|
|
}
|
|
|
|
const NewClusterRequest = (
|
|
cluster: string,
|
|
members: string[],
|
|
): ClusterRequestT => ({
|
|
cluster: cluster,
|
|
members: members,
|
|
isDone: false,
|
|
silenceID: undefined,
|
|
silenceLink: undefined,
|
|
error: null,
|
|
});
|
|
|
|
const GenerateAlertmanagerSilenceData = (
|
|
startsAt: Date,
|
|
endsAt: Date,
|
|
matchers: MatcherT[],
|
|
author: string,
|
|
comment: string,
|
|
silenceID: string | null = null,
|
|
): AlertmanagerSilencePayloadT => {
|
|
const payload: AlertmanagerSilencePayloadT = {
|
|
matchers: matchers.map((m) => ({
|
|
name: m.name,
|
|
value:
|
|
m.values.length > 1
|
|
? `(${m.values
|
|
.map((v) =>
|
|
v.wasCreated || !m.isRegex ? v.value : EscapeRegex(v.value),
|
|
)
|
|
.join("|")})`
|
|
: m.values.length === 1
|
|
? m.values[0].wasCreated || !m.isRegex
|
|
? m.values[0].value
|
|
: EscapeRegex(m.values[0].value)
|
|
: "",
|
|
isRegex: m.isRegex,
|
|
isEqual: m.isEqual,
|
|
})),
|
|
startsAt: startsAt.toISOString(),
|
|
endsAt: endsAt.toISOString(),
|
|
createdBy: author,
|
|
comment: comment,
|
|
};
|
|
if (silenceID !== null) {
|
|
payload.id = silenceID;
|
|
}
|
|
return payload;
|
|
};
|
|
|
|
const UnpackRegexMatcherValues = (isRegex: boolean, value: string) => {
|
|
let val: string = value;
|
|
if (isRegex) {
|
|
val = UnescapeRegex(val);
|
|
}
|
|
if (isRegex && val.match(/^\(([a-zA-Z0-9_\-. ]+\|)+[a-zA-Z0-9_\-. ]+\)$/)) {
|
|
return val
|
|
.slice(1, -1)
|
|
.split("|")
|
|
.map((v) => StringToOption(v));
|
|
} else if (
|
|
isRegex &&
|
|
val.match(/^([a-zA-Z0-9_\-. ]+\|)+[a-zA-Z0-9_\-. ]+$/)
|
|
) {
|
|
return val.split("|").map((v) => StringToOption(v));
|
|
} else {
|
|
return [{ ...StringToOption(val), wasCreated: true }];
|
|
}
|
|
};
|
|
|
|
export type SilenceFormTabT = "editor" | "browser";
|
|
export type SilenceFormStageT = "form" | "preview" | "submit";
|
|
|
|
interface SilenceFormStoreToggleT {
|
|
visible: boolean;
|
|
blurred: boolean;
|
|
toggle: () => void;
|
|
hide: () => void;
|
|
show: () => void;
|
|
setBlur: (val: boolean) => void;
|
|
}
|
|
|
|
interface SilenceFormStoreTabT {
|
|
current: SilenceFormTabT;
|
|
setTab: (value: SilenceFormTabT) => void;
|
|
}
|
|
|
|
interface DurationT {
|
|
days: number;
|
|
hours: number;
|
|
minutes: number;
|
|
}
|
|
|
|
interface SilenceFormStoreDataT {
|
|
currentStage: SilenceFormStageT;
|
|
wasValidated: boolean;
|
|
silenceID: null | undefined | string;
|
|
alertmanagers: MultiValueOptionT[];
|
|
matchers: MatcherWithIDT[];
|
|
startsAt: Date;
|
|
endsAt: Date;
|
|
comment: string;
|
|
author: string;
|
|
requestsByCluster: { [key: string]: ClusterRequestT };
|
|
autofillMatchers: boolean;
|
|
resetInputs: boolean;
|
|
readonly toBase64: string;
|
|
fromBase64: (s: string) => boolean;
|
|
readonly isValid: boolean;
|
|
resetStartEnd: () => void;
|
|
resetProgress: () => void;
|
|
resetSilenceID: () => void;
|
|
setSilenceID: (id: string | null) => void;
|
|
setAlertmanagers: (val: MultiValueOptionT[]) => void;
|
|
setAutofillMatchers: (v: boolean) => void;
|
|
setResetInputs: (v: boolean) => void;
|
|
setStage: (val: SilenceFormStageT) => void;
|
|
setMatchers: (m: MatcherWithIDT[]) => void;
|
|
addEmptyMatcher: () => void;
|
|
addMatcherWithID: (m: MatcherWithIDT) => void;
|
|
deleteMatcher: (id: string) => void;
|
|
fillMatchersFromGroup: (
|
|
group: APIAlertGroupT,
|
|
stripLabels: string[],
|
|
alertmanagers: MultiValueOptionT[],
|
|
alerts?: APIAlertT[],
|
|
) => void;
|
|
fillFormFromSilence: (
|
|
alertmanager: APIAlertmanagerUpstreamT,
|
|
silence: AlertmanagerSilencePayloadT,
|
|
) => void;
|
|
setAuthor: (a: string) => void;
|
|
setComment: (c: string) => void;
|
|
verifyStarEnd: () => void;
|
|
setStart: (startsAt: Date) => void;
|
|
setEnd: (endsAt: Date) => void;
|
|
incStart: (minutes: number) => void;
|
|
decStart: (minutes: number) => void;
|
|
incEnd: (minutes: number) => void;
|
|
decEnd: (minutes: number) => void;
|
|
setWasValidated: (v: boolean) => void;
|
|
setRequestsByCluster: (val: { [key: string]: ClusterRequestT }) => void;
|
|
setRequestsByClusterUpdate: (
|
|
key: string,
|
|
v: Partial<ClusterRequestT>,
|
|
) => void;
|
|
readonly toAlertmanagerPayload: AlertmanagerSilencePayloadT;
|
|
readonly toDuration: DurationT;
|
|
}
|
|
|
|
class SilenceFormStore {
|
|
toggle: SilenceFormStoreToggleT;
|
|
tab: SilenceFormStoreTabT;
|
|
data: SilenceFormStoreDataT;
|
|
|
|
constructor() {
|
|
this.toggle = observable(
|
|
{
|
|
visible: false as boolean,
|
|
blurred: false as boolean,
|
|
toggle() {
|
|
this.visible = !this.visible;
|
|
},
|
|
hide() {
|
|
this.visible = false;
|
|
},
|
|
show() {
|
|
this.visible = true;
|
|
},
|
|
setBlur(val: boolean) {
|
|
this.blurred = val;
|
|
},
|
|
},
|
|
{
|
|
toggle: action.bound,
|
|
hide: action.bound,
|
|
show: action.bound,
|
|
setBlur: action.bound,
|
|
},
|
|
);
|
|
|
|
this.tab = observable(
|
|
{
|
|
current: "editor" as SilenceFormTabT,
|
|
setTab(value: SilenceFormTabT) {
|
|
this.current = value;
|
|
},
|
|
},
|
|
{
|
|
setTab: action.bound,
|
|
},
|
|
);
|
|
|
|
// form data is stored here, it's global (rather than attached to the form)
|
|
// so it can be manipulated from other parts of the code
|
|
// example: when user clicks a silence button on alert we should populate
|
|
// this form from that alert so user can easily silence that alert
|
|
this.data = observable(
|
|
{
|
|
currentStage: "form" as SilenceFormStageT,
|
|
wasValidated: false as boolean,
|
|
silenceID: null as null | undefined | string,
|
|
alertmanagers: [] as MultiValueOptionT[],
|
|
matchers: [] as MatcherWithIDT[],
|
|
startsAt: new Date(),
|
|
endsAt: addHours(new Date(), 1),
|
|
comment: "",
|
|
author: "",
|
|
requestsByCluster: {} as { [key: string]: ClusterRequestT },
|
|
autofillMatchers: true as boolean,
|
|
resetInputs: true as boolean,
|
|
|
|
get toBase64() {
|
|
const json = JSON.stringify({
|
|
am: this.alertmanagers,
|
|
m: this.matchers.map((m: MatcherWithIDT) => ({
|
|
n: m.name,
|
|
r: m.isRegex,
|
|
e: m.isEqual,
|
|
v: m.values.map((v) => v.value),
|
|
})),
|
|
d: differenceInMinutes(this.endsAt, this.startsAt),
|
|
c: this.comment,
|
|
});
|
|
return window.btoa(json);
|
|
},
|
|
|
|
fromBase64(s: string): boolean {
|
|
let parsed: SilenceFormDataFromBase64;
|
|
try {
|
|
parsed = JSON.parse(window.atob(s));
|
|
} catch (error) {
|
|
console.error(`Failed to parse JSON: ${error}`);
|
|
return false;
|
|
}
|
|
|
|
const matchers: MatcherWithIDT[] = [];
|
|
parsed.m.forEach((m: SimplifiedMatcherT) => {
|
|
const matcher = NewEmptyMatcher();
|
|
matcher.name = m.n;
|
|
matcher.isRegex = m.r;
|
|
matcher.isEqual = m.e;
|
|
matcher.values = m.v.map((v) => StringToOption(v));
|
|
matchers.push(matcher);
|
|
});
|
|
|
|
if (matchers.length > 0) {
|
|
this.alertmanagers = parsed.am;
|
|
this.matchers = matchers;
|
|
|
|
this.startsAt = new Date();
|
|
this.endsAt = addMinutes(this.startsAt, parsed.d);
|
|
this.comment = parsed.c;
|
|
|
|
this.silenceID = null;
|
|
this.autofillMatchers = false;
|
|
this.resetInputs = false;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
get isValid() {
|
|
if (this.alertmanagers.length === 0) return false;
|
|
if (this.matchers.length === 0) return false;
|
|
if (
|
|
this.matchers.filter(
|
|
(m) =>
|
|
m.name === "" ||
|
|
m.values.length === 0 ||
|
|
m.values.filter((v) => v.value === "").length > 0,
|
|
).length > 0
|
|
)
|
|
return false;
|
|
if (this.comment === "") return false;
|
|
if (this.author === "") return false;
|
|
return true;
|
|
},
|
|
|
|
resetStartEnd() {
|
|
this.startsAt = new Date();
|
|
this.endsAt = addHours(new Date(), 1);
|
|
},
|
|
|
|
resetProgress() {
|
|
this.currentStage = "form";
|
|
this.wasValidated = false;
|
|
},
|
|
|
|
resetSilenceID() {
|
|
this.silenceID = null;
|
|
},
|
|
|
|
setSilenceID(id: string | null) {
|
|
this.silenceID = id;
|
|
},
|
|
|
|
setAlertmanagers(val: MultiValueOptionT[]) {
|
|
this.alertmanagers = val;
|
|
},
|
|
|
|
setAutofillMatchers(v: boolean) {
|
|
this.autofillMatchers = v;
|
|
},
|
|
setResetInputs(v: boolean) {
|
|
this.resetInputs = v;
|
|
},
|
|
|
|
setStage(val: SilenceFormStageT) {
|
|
this.currentStage = val;
|
|
},
|
|
|
|
setMatchers(m: MatcherWithIDT[]) {
|
|
this.matchers = m;
|
|
},
|
|
|
|
// append a new empty matcher to the list
|
|
addEmptyMatcher() {
|
|
this.matchers.push(NewEmptyMatcher());
|
|
},
|
|
addMatcherWithID(m: MatcherWithIDT) {
|
|
this.matchers.push(m);
|
|
},
|
|
|
|
deleteMatcher(id: string) {
|
|
// only delete matchers if we have more than 1
|
|
if (this.matchers.length > 1) {
|
|
this.matchers = this.matchers.filter((m) => m.id !== id);
|
|
}
|
|
},
|
|
|
|
// if alerts argument is not passed all group alerts will be used
|
|
fillMatchersFromGroup(
|
|
group: APIAlertGroupT,
|
|
stripLabels: string[],
|
|
alertmanagers: MultiValueOptionT[],
|
|
alerts?: APIAlertT[],
|
|
) {
|
|
this.alertmanagers = alertmanagers;
|
|
|
|
this.matchers = alerts
|
|
? MatchersFromAlerts(group, stripLabels, alerts)
|
|
: MatchersFromGroup(group, stripLabels);
|
|
// ensure that silenceID is nulled, since it's used to edit silences
|
|
// and this is used to silence groups
|
|
this.silenceID = null;
|
|
// disable matcher autofill
|
|
this.autofillMatchers = false;
|
|
// disable alertmanager input reset
|
|
this.resetInputs = false;
|
|
},
|
|
|
|
fillFormFromSilence(
|
|
alertmanager: APIAlertmanagerUpstreamT,
|
|
silence: AlertmanagerSilencePayloadT,
|
|
) {
|
|
this.silenceID = silence.id;
|
|
|
|
this.alertmanagers = AlertmanagerClustersToOption({
|
|
[alertmanager.cluster]: alertmanager.clusterMembers,
|
|
});
|
|
|
|
const matchers: MatcherWithIDT[] = [];
|
|
for (const m of silence.matchers) {
|
|
const matcher = NewEmptyMatcher();
|
|
matcher.name = m.name;
|
|
matcher.values = UnpackRegexMatcherValues(m.isRegex, m.value);
|
|
matcher.isRegex = m.isRegex;
|
|
matcher.isEqual = m.isEqual === false ? false : true;
|
|
matchers.push(matcher);
|
|
}
|
|
this.matchers = matchers;
|
|
|
|
this.startsAt = parseISO(silence.startsAt);
|
|
this.endsAt = parseISO(silence.endsAt);
|
|
this.comment = silence.comment;
|
|
this.author = silence.createdBy;
|
|
|
|
// disable matcher autofill
|
|
this.autofillMatchers = false;
|
|
},
|
|
|
|
setAuthor(a: string) {
|
|
this.author = a;
|
|
},
|
|
|
|
setComment(c: string) {
|
|
this.comment = c;
|
|
},
|
|
|
|
verifyStarEnd() {
|
|
const now = new Date();
|
|
now.setSeconds(0);
|
|
if (this.startsAt < now) {
|
|
this.startsAt = now;
|
|
}
|
|
|
|
if (this.endsAt <= this.startsAt) {
|
|
this.endsAt = addMinutes(this.startsAt, 1);
|
|
}
|
|
},
|
|
setStart(startsAt: Date) {
|
|
this.startsAt = startsAt;
|
|
},
|
|
setEnd(endsAt: Date) {
|
|
this.endsAt = endsAt;
|
|
},
|
|
incStart(minutes: number) {
|
|
this.startsAt = addMinutes(this.startsAt, minutes);
|
|
this.verifyStarEnd();
|
|
},
|
|
decStart(minutes: number) {
|
|
this.startsAt = subMinutes(this.startsAt, minutes);
|
|
this.verifyStarEnd();
|
|
},
|
|
|
|
incEnd(minutes: number) {
|
|
this.endsAt = addMinutes(this.endsAt, minutes);
|
|
this.verifyStarEnd();
|
|
},
|
|
decEnd(minutes: number) {
|
|
this.endsAt = subMinutes(this.endsAt, minutes);
|
|
this.verifyStarEnd();
|
|
},
|
|
|
|
setWasValidated(v: boolean) {
|
|
this.wasValidated = v;
|
|
},
|
|
|
|
setRequestsByCluster(val: { [key: string]: ClusterRequestT }) {
|
|
this.requestsByCluster = val;
|
|
},
|
|
setRequestsByClusterUpdate(key: string, v: Partial<ClusterRequestT>) {
|
|
this.requestsByCluster[key] = {
|
|
...this.requestsByCluster[key],
|
|
...v,
|
|
};
|
|
},
|
|
|
|
get toAlertmanagerPayload() {
|
|
const startsAt = new Date(this.startsAt);
|
|
startsAt.setSeconds(0);
|
|
startsAt.setMilliseconds(0);
|
|
const endsAt = new Date(this.endsAt);
|
|
endsAt.setSeconds(0);
|
|
endsAt.setMilliseconds(0);
|
|
return GenerateAlertmanagerSilenceData(
|
|
startsAt,
|
|
endsAt,
|
|
this.matchers,
|
|
this.author,
|
|
this.comment,
|
|
this.silenceID,
|
|
);
|
|
},
|
|
|
|
get toDuration() {
|
|
const data: DurationT = {
|
|
days: differenceInDays(this.endsAt, this.startsAt),
|
|
hours: differenceInHours(this.endsAt, this.startsAt) % 24,
|
|
minutes: differenceInMinutes(this.endsAt, this.startsAt) % 60,
|
|
};
|
|
return data;
|
|
},
|
|
},
|
|
{
|
|
toBase64: computed,
|
|
fromBase64: action.bound,
|
|
resetStartEnd: action.bound,
|
|
resetProgress: action.bound,
|
|
resetSilenceID: action.bound,
|
|
setSilenceID: action.bound,
|
|
setAlertmanagers: action.bound,
|
|
setAutofillMatchers: action.bound,
|
|
setResetInputs: action.bound,
|
|
setStage: action.bound,
|
|
setMatchers: action.bound,
|
|
addEmptyMatcher: action.bound,
|
|
addMatcherWithID: action.bound,
|
|
deleteMatcher: action.bound,
|
|
fillMatchersFromGroup: action.bound,
|
|
fillFormFromSilence: action.bound,
|
|
setAuthor: action.bound,
|
|
setComment: action.bound,
|
|
verifyStarEnd: action.bound,
|
|
setStart: action.bound,
|
|
setEnd: action.bound,
|
|
incStart: action.bound,
|
|
decStart: action.bound,
|
|
incEnd: action.bound,
|
|
decEnd: action.bound,
|
|
isValid: computed,
|
|
setWasValidated: action.bound,
|
|
setRequestsByCluster: action.bound,
|
|
setRequestsByClusterUpdate: action.bound,
|
|
toAlertmanagerPayload: computed,
|
|
toDuration: computed,
|
|
},
|
|
{ name: "Silence form store" },
|
|
);
|
|
}
|
|
}
|
|
|
|
export {
|
|
SilenceFormStore,
|
|
NewEmptyMatcher,
|
|
AlertmanagerClustersToOption,
|
|
MatchersFromGroup,
|
|
MatchersFromAlerts,
|
|
GenerateAlertmanagerSilenceData,
|
|
NewClusterRequest,
|
|
MatcherToOperator,
|
|
};
|