diff --git a/ui/src/Stores/AlertStore.ts b/ui/src/Stores/AlertStore.ts index 8d8c11eba..406daf174 100644 --- a/ui/src/Stores/AlertStore.ts +++ b/ui/src/Stores/AlertStore.ts @@ -15,6 +15,7 @@ import { APIAlertsResponseSilenceMapT, APIAlertsResponseUpstreamsT, APIAlertsResponseUpstreamsClusterMapT, + APISettingsT, } from "Models/APITypes"; const QueryStringEncodeOptions = { @@ -121,280 +122,358 @@ function NewUnappliedFilter(raw: string): FilterT { }; } +interface AlertStoreFiltersT { + values: FilterT[]; + addFilter: (raw: string) => void; + removeFilter: (raw: string) => void; + replaceFilter: (oldRaw: string, newRaw: string) => void; + setFilters: (raws: string[]) => void; + setFilterValues: (v: FilterT[]) => void; + setWithoutLocation: (raws: string[]) => void; + applyAllFilters: () => void; +} + +interface AlertStoreDataT { + colors: APIAlertsResponseColorsT; + counters: APILabelCounterT[]; + grids: APIGridT[]; + silences: APIAlertsResponseSilenceMapT; + upstreams: APIAlertsResponseUpstreamsT; + receivers: string[]; + readonly gridPadding: number; + getAlertmanagerByName: (name: string) => APIAlertmanagerUpstreamT | undefined; + isReadOnlyAlertmanager: (name: string) => boolean; + getClusterAlertmanagersWithoutReadOnly: (clusterID: string) => string[]; + readonly readOnlyAlertmanagers: APIAlertmanagerUpstreamT[]; + readonly readWriteAlertmanagers: APIAlertmanagerUpstreamT[]; + readonly clustersWithoutReadOnly: APIAlertsResponseUpstreamsClusterMapT; + getColorData: (name: string, value: string) => APILabelColorT | undefined; + setGrids: (g: APIGridT[]) => void; + setUpstreams: (u: APIAlertsResponseUpstreamsT) => void; + setInstances: (i: APIAlertmanagerUpstreamT[]) => void; + setClusters: (c: APIAlertsResponseUpstreamsClusterMapT) => void; +} + +interface AlertStoreInfoT { + authentication: { + enabled: boolean; + username: string; + }; + totalAlerts: number; + version: string; + upgradeReady: boolean; + upgradeNeeded: boolean; + isRetrying: boolean; + reloadNeeded: boolean; + setIsRetrying: () => void; + clearIsRetrying: () => void; + setUpgradeNeeded: () => void; + setReloadNeeded: () => void; + setTotalAlerts: (n: number) => void; +} + +interface AlertStoreSettingsT { + values: APISettingsT; +} + +interface AlertStoreStatusT { + value: symbol; + lastUpdateAt: number | Date; + error: null | string; + stopped: boolean; + paused: boolean; + setIdle: () => void; + setFetching: () => void; + setProcessing: () => void; + setFailure: (err: string) => void; + pause: () => void; + resume: () => void; + togglePause: () => void; + stop: () => void; +} + class AlertStore { - filters = observable( - { - values: [] as FilterT[], - addFilter(raw: string) { - if (this.values.filter((f) => f.raw === raw).length === 0) { - this.values.push(NewUnappliedFilter(raw)); - UpdateLocationSearch({ q: this.values.map((f) => f.raw) }); - } - }, - removeFilter(raw: string) { - if (this.values.filter((f) => f.raw === raw).length > 0) { - this.values = this.values.filter((f) => f.raw !== raw); - UpdateLocationSearch({ q: this.values.map((f) => f.raw) }); - } - }, - replaceFilter(oldRaw: string, newRaw: string) { - const index = this.values.findIndex((e) => e.raw === oldRaw); - if (index >= 0) { - // first check if we would create a duplicated filter - if (this.values.findIndex((e) => e.raw === newRaw) >= 0) { - // we already have newRaw, simply drop oldRaw - this.removeFilter(oldRaw); - } else { - // no dups, continue with a swap - this.values[index] = NewUnappliedFilter(newRaw); - UpdateLocationSearch({ q: this.values.map((f) => f.raw) }); - } - } - }, - setFilters(raws: string[]) { - this.values = raws.map((raw) => NewUnappliedFilter(raw)); - UpdateLocationSearch({ q: this.values.map((f) => f.raw) }); - }, - setFilterValues(v: FilterT[]) { - this.values = v; - }, - setWithoutLocation(raws: string[]) { - const filtersByRaw: { [key: string]: FilterT } = this.values.reduce( - function (map: { [key: string]: FilterT }, obj) { - map[toJS(obj.raw)] = toJS(obj); - return map; - }, - {} - ); - this.values = raws.map((raw) => - filtersByRaw[raw] ? filtersByRaw[raw] : NewUnappliedFilter(raw) - ); - }, - applyAllFilters() { - for (let i = 0; i < this.values.length; i++) { - this.values[i].applied = true; - } - }, - }, - { - addFilter: action.bound, - removeFilter: action.bound, - replaceFilter: action.bound, - setFilters: action.bound, - setFilterValues: action.bound, - setWithoutLocation: action.bound, - applyAllFilters: action.bound, - }, - { name: "API Filters" } - ); - - data = observable( - { - colors: {} as APIAlertsResponseColorsT, - counters: [] as APILabelCounterT[], - grids: [] as APIGridT[], - silences: {} as APIAlertsResponseSilenceMapT, - upstreams: { - counters: { total: 0, healthy: 0, failed: 0 }, - instances: [], - clusters: {}, - } as APIAlertsResponseUpstreamsT, - receivers: [] as string[], - get gridPadding(): number { - return this.grids.filter((g) => g.labelName !== "").length > 0 ? 5 : 0; - }, - getAlertmanagerByName( - name: string - ): APIAlertmanagerUpstreamT | undefined { - return this.upstreams.instances.find((am) => am.name === name); - }, - isReadOnlyAlertmanager(name: string): boolean { - return this.readOnlyAlertmanagers.map((am) => am.name).includes(name); - }, - getClusterAlertmanagersWithoutReadOnly(clusterID: string): string[] { - return this.clustersWithoutReadOnly[clusterID] || []; - }, - get readOnlyAlertmanagers(): APIAlertmanagerUpstreamT[] { - return this.upstreams.instances.filter((am) => am.readonly === true); - }, - get readWriteAlertmanagers(): APIAlertmanagerUpstreamT[] { - return this.upstreams.instances - .filter((am) => am.readonly === false) - .map((am) => - Object.assign({}, am, { - clusterMembers: am.clusterMembers.filter( - (m) => this.isReadOnlyAlertmanager(m) === false - ), - }) - ); - }, - get clustersWithoutReadOnly(): APIAlertsResponseUpstreamsClusterMapT { - const clusters: APIAlertsResponseUpstreamsClusterMapT = {}; - for (const clusterID of Object.keys(this.upstreams.clusters)) { - const members = this.upstreams.clusters[clusterID].filter( - (member) => this.isReadOnlyAlertmanager(member) === false - ); - if (members.length > 0) { - clusters[clusterID] = members; - } - } - return clusters; - }, - getColorData(name: string, value: string): APILabelColorT | undefined { - if (this.colors[name] !== undefined) { - return this.colors[name][value]; - } - }, - setGrids(g: APIGridT[]) { - this.grids = g; - }, - setUpstreams(u: APIAlertsResponseUpstreamsT) { - this.upstreams = u; - }, - setInstances(i: APIAlertmanagerUpstreamT[]) { - this.upstreams.instances = i; - }, - setClusters(c: APIAlertsResponseUpstreamsClusterMapT) { - this.upstreams.clusters = c; - }, - }, - { - gridPadding: computed, - readOnlyAlertmanagers: computed, - readWriteAlertmanagers: computed, - clustersWithoutReadOnly: computed, - setGrids: action.bound, - setUpstreams: action.bound, - setInstances: action.bound, - setClusters: action.bound, - }, - { name: "API Response data" } - ); - - info = observable( - { - authentication: { - enabled: false as boolean, - username: "", - }, - totalAlerts: 0, - version: "unknown", - upgradeReady: false as boolean, - upgradeNeeded: false as boolean, - isRetrying: false as boolean, - reloadNeeded: false as boolean, - setIsRetrying() { - this.isRetrying = true; - }, - clearIsRetrying() { - this.isRetrying = false; - }, - setUpgradeNeeded() { - this.upgradeNeeded = true; - }, - setReloadNeeded() { - this.reloadNeeded = true; - }, - setTotalAlerts(n: number) { - this.totalAlerts = n; - }, - }, - { - setIsRetrying: action.bound, - clearIsRetrying: action.bound, - setReloadNeeded: action.bound, - setUpgradeNeeded: action.bound, - setTotalAlerts: action.bound, - }, - { name: "API response info" } - ); - - settings = observable( - { - values: { - staticColorLabels: [] as string[], - annotationsDefaultHidden: false as boolean, - annotationsHidden: [] as string[], - annotationsVisible: [] as string[], - sorting: { - grid: { - order: "startsAt", - reverse: false as boolean, - label: "alertname", - }, - valueMapping: {}, - }, - silenceForm: { - strip: { - labels: [] as string[], - }, - }, - alertAcknowledgement: { - enabled: false as boolean, - durationSeconds: 900, - author: "karma / author missing", - commentPrefix: "", - }, - }, - }, - {}, - { - name: "Global settings", - } - ); - - status = observable( - { - value: AlertStoreStatuses.Idle, - lastUpdateAt: 0 as number | Date, - error: null as null | string, - stopped: false as boolean, - paused: false as boolean, - setIdle() { - this.value = AlertStoreStatuses.Idle; - this.error = null; - this.lastUpdateAt = new Date(); - }, - setFetching() { - this.value = AlertStoreStatuses.Fetching; - }, - setProcessing() { - this.value = AlertStoreStatuses.Processing; - this.error = null; - }, - setFailure(err: string) { - this.value = AlertStoreStatuses.Failure; - this.error = err; - this.lastUpdateAt = new Date(); - }, - pause() { - this.paused = true; - }, - resume() { - this.paused = this.stopped ? true : false; - }, - togglePause() { - this.paused = this.stopped ? true : !this.paused; - }, - stop() { - this.paused = true; - this.stopped = true; - }, - }, - { - setIdle: action, - setFetching: action, - setProcessing: action, - setFailure: action, - pause: action.bound, - resume: action.bound, - togglePause: action.bound, - stop: action.bound, - }, - { name: "Store status" } - ); + filters: AlertStoreFiltersT; + data: AlertStoreDataT; + info: AlertStoreInfoT; + settings: AlertStoreSettingsT; + status: AlertStoreStatusT; constructor(initialFilters: null | string[]) { + this.filters = observable( + { + values: [] as FilterT[], + addFilter(raw: string) { + if (this.values.filter((f) => f.raw === raw).length === 0) { + this.values.push(NewUnappliedFilter(raw)); + UpdateLocationSearch({ q: this.values.map((f) => f.raw) }); + } + }, + removeFilter(raw: string) { + if (this.values.filter((f) => f.raw === raw).length > 0) { + this.values = this.values.filter((f) => f.raw !== raw); + UpdateLocationSearch({ q: this.values.map((f) => f.raw) }); + } + }, + replaceFilter(oldRaw: string, newRaw: string) { + const index = this.values.findIndex((e) => e.raw === oldRaw); + if (index >= 0) { + // first check if we would create a duplicated filter + if (this.values.findIndex((e) => e.raw === newRaw) >= 0) { + // we already have newRaw, simply drop oldRaw + this.removeFilter(oldRaw); + } else { + // no dups, continue with a swap + this.values[index] = NewUnappliedFilter(newRaw); + UpdateLocationSearch({ q: this.values.map((f) => f.raw) }); + } + } + }, + setFilters(raws: string[]) { + this.values = raws.map((raw) => NewUnappliedFilter(raw)); + UpdateLocationSearch({ q: this.values.map((f) => f.raw) }); + }, + setFilterValues(v: FilterT[]) { + this.values = v; + }, + setWithoutLocation(raws: string[]) { + const filtersByRaw: { [key: string]: FilterT } = this.values.reduce( + function (map: { [key: string]: FilterT }, obj) { + map[toJS(obj.raw)] = toJS(obj); + return map; + }, + {} + ); + this.values = raws.map((raw) => + filtersByRaw[raw] ? filtersByRaw[raw] : NewUnappliedFilter(raw) + ); + }, + applyAllFilters() { + for (let i = 0; i < this.values.length; i++) { + this.values[i].applied = true; + } + }, + }, + { + addFilter: action.bound, + removeFilter: action.bound, + replaceFilter: action.bound, + setFilters: action.bound, + setFilterValues: action.bound, + setWithoutLocation: action.bound, + applyAllFilters: action.bound, + }, + { name: "API Filters" } + ); + + this.data = observable( + { + colors: {} as APIAlertsResponseColorsT, + counters: [] as APILabelCounterT[], + grids: [] as APIGridT[], + silences: {} as APIAlertsResponseSilenceMapT, + upstreams: { + counters: { total: 0, healthy: 0, failed: 0 }, + instances: [], + clusters: {}, + } as APIAlertsResponseUpstreamsT, + receivers: [] as string[], + get gridPadding(): number { + return this.grids.filter((g) => g.labelName !== "").length > 0 + ? 5 + : 0; + }, + getAlertmanagerByName( + name: string + ): APIAlertmanagerUpstreamT | undefined { + return this.upstreams.instances.find((am) => am.name === name); + }, + isReadOnlyAlertmanager(name: string): boolean { + return this.readOnlyAlertmanagers.map((am) => am.name).includes(name); + }, + getClusterAlertmanagersWithoutReadOnly(clusterID: string): string[] { + return this.clustersWithoutReadOnly[clusterID] || []; + }, + get readOnlyAlertmanagers(): APIAlertmanagerUpstreamT[] { + return this.upstreams.instances.filter((am) => am.readonly === true); + }, + get readWriteAlertmanagers(): APIAlertmanagerUpstreamT[] { + return this.upstreams.instances + .filter((am) => am.readonly === false) + .map((am) => + Object.assign({}, am, { + clusterMembers: am.clusterMembers.filter( + (m) => this.isReadOnlyAlertmanager(m) === false + ), + }) + ); + }, + get clustersWithoutReadOnly(): APIAlertsResponseUpstreamsClusterMapT { + const clusters: APIAlertsResponseUpstreamsClusterMapT = {}; + for (const clusterID of Object.keys(this.upstreams.clusters)) { + const members = this.upstreams.clusters[clusterID].filter( + (member) => this.isReadOnlyAlertmanager(member) === false + ); + if (members.length > 0) { + clusters[clusterID] = members; + } + } + return clusters; + }, + getColorData(name: string, value: string): APILabelColorT | undefined { + if (this.colors[name] !== undefined) { + return this.colors[name][value]; + } + }, + setGrids(g: APIGridT[]) { + this.grids = g; + }, + setUpstreams(u: APIAlertsResponseUpstreamsT) { + this.upstreams = u; + }, + setInstances(i: APIAlertmanagerUpstreamT[]) { + this.upstreams.instances = i; + }, + setClusters(c: APIAlertsResponseUpstreamsClusterMapT) { + this.upstreams.clusters = c; + }, + }, + { + gridPadding: computed, + readOnlyAlertmanagers: computed, + readWriteAlertmanagers: computed, + clustersWithoutReadOnly: computed, + setGrids: action.bound, + setUpstreams: action.bound, + setInstances: action.bound, + setClusters: action.bound, + }, + { name: "API Response data" } + ); + + this.info = observable( + { + authentication: { + enabled: false as boolean, + username: "", + }, + totalAlerts: 0, + version: "unknown", + upgradeReady: false as boolean, + upgradeNeeded: false as boolean, + isRetrying: false as boolean, + reloadNeeded: false as boolean, + setIsRetrying() { + this.isRetrying = true; + }, + clearIsRetrying() { + this.isRetrying = false; + }, + setUpgradeNeeded() { + this.upgradeNeeded = true; + }, + setReloadNeeded() { + this.reloadNeeded = true; + }, + setTotalAlerts(n: number) { + this.totalAlerts = n; + }, + }, + { + setIsRetrying: action.bound, + clearIsRetrying: action.bound, + setReloadNeeded: action.bound, + setUpgradeNeeded: action.bound, + setTotalAlerts: action.bound, + }, + { name: "API response info" } + ); + + this.settings = observable( + { + values: { + staticColorLabels: [] as string[], + annotationsDefaultHidden: false as boolean, + annotationsHidden: [] as string[], + annotationsVisible: [] as string[], + sorting: { + grid: { + order: "startsAt", + reverse: false as boolean, + label: "alertname", + }, + valueMapping: {}, + }, + silenceForm: { + strip: { + labels: [] as string[], + }, + }, + alertAcknowledgement: { + enabled: false as boolean, + durationSeconds: 900, + author: "karma / author missing", + commentPrefix: "", + }, + }, + }, + {}, + { + name: "Global settings", + } + ); + + this.status = observable( + { + value: AlertStoreStatuses.Idle, + lastUpdateAt: 0 as number | Date, + error: null as null | string, + stopped: false as boolean, + paused: false as boolean, + setIdle() { + this.value = AlertStoreStatuses.Idle; + this.error = null; + this.lastUpdateAt = new Date(); + }, + setFetching() { + this.value = AlertStoreStatuses.Fetching; + }, + setProcessing() { + this.value = AlertStoreStatuses.Processing; + this.error = null; + }, + setFailure(err: string) { + this.value = AlertStoreStatuses.Failure; + this.error = err; + this.lastUpdateAt = new Date(); + }, + pause() { + this.paused = true; + }, + resume() { + this.paused = this.stopped ? true : false; + }, + togglePause() { + this.paused = this.stopped ? true : !this.paused; + }, + stop() { + this.paused = true; + this.stopped = true; + }, + }, + { + setIdle: action, + setFetching: action, + setProcessing: action, + setFailure: action, + pause: action.bound, + resume: action.bound, + togglePause: action.bound, + stop: action.bound, + }, + { name: "Store status" } + ); + if (initialFilters !== null) this.filters.setFilters(initialFilters); } diff --git a/ui/src/Stores/SilenceFormStore.ts b/ui/src/Stores/SilenceFormStore.ts index f2a26b721..a6dbbe962 100644 --- a/ui/src/Stores/SilenceFormStore.ts +++ b/ui/src/Stores/SilenceFormStore.ts @@ -198,331 +198,408 @@ const UnpackRegexMatcherValues = (isRegex: boolean, value: string) => { 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) => void; + readonly isValid: boolean; + resetStartEnd: () => void; + resetProgress: () => void; + resetSilenceID: () => void; + setSilenceID: (id: string) => void; + setAlertmanagers: (val: MultiValueOptionT[]) => void; + setAutofillMatchers: (v: boolean) => void; + setResetInputs: (v: boolean) => void; + setStageSubmit: () => void; + setMatchers: (m: MatcherWithIDT[]) => void; + addEmptyMatcher: () => 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; + readonly toAlertmanagerPayload: AlertmanagerSilencePayloadT; + readonly toDuration: DurationT; +} + class SilenceFormStore { - toggle = observable( - { - visible: false, - blurred: false, - 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, - } - ); + toggle: SilenceFormStoreToggleT; + tab: SilenceFormStoreTabT; + data: SilenceFormStoreDataT; - tab = observable( - { - current: "editor" as SilenceFormTabT, - setTab(value: SilenceFormTabT) { - this.current = value; + 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; + }, }, - }, - { - setTab: action.bound, - } - ); + { + toggle: action.bound, + hide: action.bound, + show: action.bound, + setBlur: 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 - 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, - v: m.values.map((v) => v.value), - })), - d: differenceInMinutes(this.endsAt, this.startsAt), - c: this.comment, - }); - return window.btoa(json); + 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, + v: m.values.map((v) => v.value), + })), + d: differenceInMinutes(this.endsAt, this.startsAt), + c: this.comment, + }); + return window.btoa(json); + }, + + fromBase64(s: string) { + 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.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; + } - fromBase64(s: string) { - 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.values = m.v.map((v) => StringToOption(v)); - matchers.push(matcher); - }); + 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; + }, - if (matchers.length > 0) { - this.alertmanagers = parsed.am; + 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) { + this.silenceID = id; + }, + + setAlertmanagers(val: MultiValueOptionT[]) { + this.alertmanagers = val; + }, + + setAutofillMatchers(v: boolean) { + this.autofillMatchers = v; + }, + setResetInputs(v: boolean) { + this.resetInputs = v; + }, + + setStageSubmit() { + this.currentStage = "submit"; + }, + + setMatchers(m: MatcherWithIDT[]) { + this.matchers = m; + }, + + // append a new empty matcher to the list + addEmptyMatcher() { + this.matchers.push(NewEmptyMatcher()); + }, + + 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 = MatchersFromGroup(group, stripLabels, alerts); + // 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; + matchers.push(matcher); + } this.matchers = matchers; - this.startsAt = new Date(); - this.endsAt = addMinutes(this.startsAt, parsed.d); - this.comment = parsed.c; + this.startsAt = parseISO(silence.startsAt); + this.endsAt = parseISO(silence.endsAt); + this.comment = silence.comment; + this.author = silence.createdBy; - this.silenceID = null; + // disable matcher autofill this.autofillMatchers = false; - this.resetInputs = false; - return true; - } + }, - return false; - }, + setAuthor(a: string) { + this.author = a; + }, - 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; - }, + setComment(c: string) { + this.comment = c; + }, - resetStartEnd() { - this.startsAt = new Date(); - this.endsAt = addHours(new Date(), 1); - }, + verifyStarEnd() { + const now = new Date(); + now.setSeconds(0); + if (this.startsAt < now) { + this.startsAt = now; + } - resetProgress() { - this.currentStage = "form"; - this.wasValidated = false; - }, + 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(); + }, - resetSilenceID() { - this.silenceID = null; - }, + incEnd(minutes: number) { + this.endsAt = addMinutes(this.endsAt, minutes); + this.verifyStarEnd(); + }, + decEnd(minutes: number) { + this.endsAt = subMinutes(this.endsAt, minutes); + this.verifyStarEnd(); + }, - setSilenceID(id: string) { - this.silenceID = id; - }, + setWasValidated(v: boolean) { + this.wasValidated = v; + }, - setAlertmanagers(val: MultiValueOptionT[]) { - this.alertmanagers = val; - }, + 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 + ); + }, - setAutofillMatchers(v: boolean) { - this.autofillMatchers = v; + 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; + }, }, - setResetInputs(v: boolean) { - this.resetInputs = v; + { + 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, + setStageSubmit: action.bound, + setMatchers: action.bound, + addEmptyMatcher: 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, + toAlertmanagerPayload: computed, + toDuration: computed, }, - - setStageSubmit() { - this.currentStage = "submit"; - }, - - setMatchers(m: MatcherWithIDT[]) { - this.matchers = m; - }, - - // append a new empty matcher to the list - addEmptyMatcher() { - this.matchers.push(NewEmptyMatcher()); - }, - - 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 = MatchersFromGroup(group, stripLabels, alerts); - // 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; - 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; - }, - - 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 = { - 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, - setStageSubmit: action.bound, - setMatchers: action.bound, - addEmptyMatcher: 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, - toAlertmanagerPayload: computed, - toDuration: computed, - }, - { name: "Silence form store" } - ); + { name: "Silence form store" } + ); + } } export {