diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 718f1b693..bc725c909 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -18,7 +18,7 @@ import { ReactSelectStyles, } from "Components/Theme/ReactSelect"; import { BodyTheme, ThemeContext } from "Components/Theme"; -import { UIDefaults } from "./AppBoot"; +import { UIDefaults } from "Models/UI"; import { ErrorBoundary } from "./ErrorBoundary"; import "Styles/ResetCSS.scss"; diff --git a/ui/src/AppBoot.ts b/ui/src/AppBoot.ts index 18c77883a..fbdca3f2f 100644 --- a/ui/src/AppBoot.ts +++ b/ui/src/AppBoot.ts @@ -2,15 +2,7 @@ import { init } from "@sentry/browser"; -export interface UIDefaults { - Refresh: number; - HideFiltersWhenIdle: boolean; - ColorTitlebar: boolean; - Theme: "light" | "dark" | "auto"; - MinimalGroupWidth: number; - AlertsPerGroup: number; - CollapseGroups: "expanded" | "collapsed" | "collapsedOnMobile"; -} +import { UIDefaults } from "Models/UI"; const SettingsElement = () => document.getElementById("settings"); diff --git a/ui/src/Models/APITypes.ts b/ui/src/Models/APITypes.ts new file mode 100644 index 000000000..7c165ad98 --- /dev/null +++ b/ui/src/Models/APITypes.ts @@ -0,0 +1,98 @@ +export type AlertStateT = "unprocessed" | "active" | "suppressed"; + +export type LabelsT = { [key: string]: string }; + +export interface AlertmanagerSilenceMatcherT { + name: string; + value: string; + isRegex: boolean; +} + +export interface AlertmanagerSilencePayloadT { + id?: string; + matchers: AlertmanagerSilenceMatcherT[]; + startsAt: string; + endsAt: string; + createdBy: string; + comment: string; +} + +export interface APIAnnotationT { + name: string; + value: string; + visible: boolean; + isLink: boolean; +} + +export interface APIAlertmanagerStateT { + fingerprint: string; + name: string; + cluster: string; + state: AlertStateT; + startsAt: string; + source: string; + silencedBy: string[]; + inhibitedBy: string[]; +} + +export interface APIAlertT { + id: string; + annotations: APIAnnotationT[]; + labels: LabelsT; + startsAt: string; + state: AlertStateT; + alertmanager: APIAlertmanagerStateT[]; + receiver: string; +} + +export interface StateCountT { + active: number; + suppressed: number; + unprocessed: number; +} + +export interface APIAlertGroupT { + id: string; + receiver: string; + labels: LabelsT; + alerts: APIAlertT[]; + alertmanagerCount: { [key: string]: number }; + stateCount: StateCountT; + shared: { + annotations: APIAnnotationT[]; + labels: LabelsT; + silences: { [cluster: string]: string[] }; + }; +} + +export interface APISilenceT { + id: string; + matchers: AlertmanagerSilenceMatcherT[]; + startsAt: string; + endsAt: string; + createdAt: string; + createdBy: string; + comment: string; + ticketID: string; + ticketURL: string; +} + +export interface APIGridT { + labelName: string; + labelValue: string; + alertGroups: APIAlertGroupT[]; + stateCount: StateCountT; +} + +export interface APIAlertmanagerUpstreamT { + name: string; + cluster: string; + uri: string; + publicURI: string; + readonly: boolean; + headers: { [key: string]: string }; + corsCredentials: "omit" | "same-origin" | "include"; + error: string; + version: string; + clusterMembers: string[]; +} diff --git a/ui/src/Models/UI.ts b/ui/src/Models/UI.ts new file mode 100644 index 000000000..a5bcbf1ae --- /dev/null +++ b/ui/src/Models/UI.ts @@ -0,0 +1,11 @@ +export interface UIDefaults { + Refresh: number; + HideFiltersWhenIdle: boolean; + ColorTitlebar: boolean; + Theme: "light" | "dark" | "auto"; + MinimalGroupWidth: number; + AlertsPerGroup: number; + CollapseGroups: "expanded" | "collapsed" | "collapsedOnMobile"; + MultiGridLabel: string; + MultiGridSortReverse: boolean; +} diff --git a/ui/src/Stores/Settings.ts b/ui/src/Stores/Settings.ts index c439c9156..b67264932 100644 --- a/ui/src/Stores/Settings.ts +++ b/ui/src/Stores/Settings.ts @@ -1,6 +1,8 @@ import { action } from "mobx"; import { localStored } from "mobx-stored"; +import { UIDefaults } from "Models/UI"; + interface SavedFilter { raw: string; name: string; @@ -205,17 +207,6 @@ class MultiGridConfig { } } -interface Defaults { - Refresh: number; - HideFiltersWhenIdle: boolean; - ColorTitlebar: boolean; - Theme: themeT; - MinimalGroupWidth: number; - AlertsPerGroup: number; - CollapseGroups: collapseStateT; - MultiGridLabel: string; - MultiGridSortReverse: boolean; -} class Settings { savedFilters: SavedFilters; fetchConfig: FetchConfig; @@ -226,8 +217,8 @@ class Settings { themeConfig: ThemeConfig; multiGridConfig: MultiGridConfig; - constructor(defaults: Defaults) { - let defaultSettings: Defaults; + constructor(defaults: UIDefaults | null | undefined) { + let defaultSettings: UIDefaults; if (defaults === undefined || defaults === null) { defaultSettings = { Refresh: 30 * 1000 * 1000 * 1000, diff --git a/ui/src/Stores/SilenceFormStore.js b/ui/src/Stores/SilenceFormStore.ts similarity index 78% rename from ui/src/Stores/SilenceFormStore.js rename to ui/src/Stores/SilenceFormStore.ts index 980234ec6..9756283ac 100644 --- a/ui/src/Stores/SilenceFormStore.js +++ b/ui/src/Stores/SilenceFormStore.ts @@ -10,7 +10,47 @@ import differenceInDays from "date-fns/differenceInDays"; import differenceInHours from "date-fns/differenceInHours"; import differenceInMinutes from "date-fns/differenceInMinutes"; -const NewEmptyMatcher = () => { +import { + APIAlertT, + APIAlertGroupT, + APIAlertmanagerUpstreamT, + AlertmanagerSilencePayloadT, +} from "Models/APITypes"; + +interface OptionT { + label: string; + value: string; +} + +interface MultiValueOptionT { + label: string; + value: string[]; +} + +interface MatcherT { + name: string; + values: OptionT[]; + isRegex: boolean; +} + +interface MatcherWithIDT extends MatcherT { + id: string; +} + +interface SimplifiedMatcherT { + n: string; + v: string[]; + r: boolean; +} + +interface SilenceFormDataFromBase64 { + am: MultiValueOptionT[]; + m: SimplifiedMatcherT[]; + d: number; + c: string; +} + +const NewEmptyMatcher = (): MatcherWithIDT => { return { id: uniqueId(), name: "", @@ -19,28 +59,40 @@ const NewEmptyMatcher = () => { }; }; -const MatcherValueToObject = (value) => ({ label: value, value: value }); +const MatcherValueToObject = (value: string): OptionT => ({ + label: value, + value: value, +}); -const AlertmanagerClustersToOption = (clusterDict) => +const AlertmanagerClustersToOption = (clusterDict: { + [key: string]: string[]; +}): MultiValueOptionT[] => Object.entries(clusterDict).map(([clusterID, clusterMembers]) => ({ label: clusterMembers.length > 1 ? `Cluster: ${clusterID}` : clusterMembers[0], value: clusterMembers, })); +// FIXME delete const SilenceFormStage = Object.freeze({ UserInput: "form", Preview: "preview", Submit: "submit", }); +// FIXME delete const SilenceTabNames = Object.freeze({ Editor: "editor", Browser: "browser", }); -const MatchersFromGroup = (group, stripLabels, alerts, onlyActive) => { - let matchers = []; +const MatchersFromGroup = ( + group: APIAlertGroupT, + stripLabels: string[], + alerts: APIAlertT[], + onlyActive?: boolean +): MatcherWithIDT[] => { + let matchers: MatcherWithIDT[] = []; // add matchers for all shared labels in this group for (const [key, value] of Object.entries( @@ -68,7 +120,7 @@ const MatchersFromGroup = (group, stripLabels, alerts, onlyActive) => { // https://stackoverflow.com/a/34498210/1154047 const sharedLabelKeys = allLabelKeys.length ? allLabelKeys.reduce(function (r, a) { - var last = {}; + var last: { [key: string]: number } = {}; return r.filter(function (b) { var p = a.indexOf(b, last[b] || 0); if (~p) { @@ -81,7 +133,7 @@ const MatchersFromGroup = (group, stripLabels, alerts, onlyActive) => { : []; // add matchers for all unique labels in this group - let labels = {}; + let labels: { [key: string]: Set } = {}; for (const alert of filteredAlerts) { for (const [key, value] of Object.entries(alert.labels)) { if (sharedLabelKeys.includes(key) && !stripLabels.includes(key)) { @@ -96,7 +148,9 @@ const MatchersFromGroup = (group, stripLabels, alerts, onlyActive) => { matchers.push({ id: uniqueId(), name: key, - values: [...values].sort().map((value) => MatcherValueToObject(value)), + values: Array.from(values) + .sort() + .map((value) => MatcherValueToObject(value)), isRegex: values.size > 1, }); } @@ -104,7 +158,7 @@ const MatchersFromGroup = (group, stripLabels, alerts, onlyActive) => { return matchers; }; -const NewClusterRequest = (cluster, members) => ({ +const NewClusterRequest = (cluster: string, members: string[]) => ({ cluster: cluster, members: members, isDone: false, @@ -114,14 +168,14 @@ const NewClusterRequest = (cluster, members) => ({ }); const GenerateAlertmanagerSilenceData = ( - startsAt, - endsAt, - matchers, - author, - comment, - silenceID -) => { - const payload = { + startsAt: Date, + endsAt: Date, + matchers: MatcherT[], + author: string, + comment: string, + silenceID: string | null +): AlertmanagerSilencePayloadT => { + const payload: AlertmanagerSilencePayloadT = { matchers: matchers.map((m) => ({ name: m.name, value: @@ -143,7 +197,7 @@ const GenerateAlertmanagerSilenceData = ( return payload; }; -const UnpackRegexMatcherValues = (isRegex, value) => { +const UnpackRegexMatcherValues = (isRegex: boolean, value: string) => { if (isRegex && value.match(/^\((\w+\|)+\w+\)$/)) { return value .slice(1, -1) @@ -156,8 +210,11 @@ const UnpackRegexMatcherValues = (isRegex, value) => { } }; +interface ClusterRequestT { + foo: boolean; + // FIXME +} class SilenceFormStore { - // this is used to store modal visibility toggle toggle = observable( { visible: false, @@ -171,7 +228,7 @@ class SilenceFormStore { show() { this.visible = true; }, - setBlur(val) { + setBlur(val: boolean) { this.blurred = val; }, }, @@ -186,7 +243,7 @@ class SilenceFormStore { tab = observable( { current: SilenceTabNames.Editor, - setTab(value) { + setTab(value: "editor" | "browser") { this.current = value; }, }, @@ -202,22 +259,22 @@ class SilenceFormStore { data = observable( { currentStage: SilenceFormStage.UserInput, - wasValidated: false, - silenceID: null, - alertmanagers: [], - matchers: [], + 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: {}, - autofillMatchers: true, - resetInputs: true, + 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) => ({ + m: this.matchers.map((m: MatcherWithIDT) => ({ n: m.name, r: m.isRegex, v: m.values.map((v) => v.value), @@ -228,8 +285,8 @@ class SilenceFormStore { return window.btoa(json); }, - fromBase64(s) { - let parsed; + fromBase64(s: string) { + let parsed: SilenceFormDataFromBase64; try { parsed = JSON.parse(window.atob(s)); } catch (error) { @@ -237,8 +294,8 @@ class SilenceFormStore { return false; } - let matchers = []; - parsed.m.forEach((m) => { + let matchers: MatcherWithIDT[] = []; + parsed.m.forEach((m: SimplifiedMatcherT) => { const matcher = NewEmptyMatcher(); matcher.name = m.n; matcher.isRegex = m.r; @@ -271,7 +328,7 @@ class SilenceFormStore { (m) => m.name === "" || m.values.length === 0 || - m.values.filter((v) => v === "").length > 0 + m.values.filter((v) => v.value === "").length > 0 ).length > 0 ) return false; @@ -294,7 +351,7 @@ class SilenceFormStore { this.silenceID = null; }, - setAlertmanagers(val) { + setAlertmanagers(val: MultiValueOptionT[]) { this.alertmanagers = val; }, @@ -304,11 +361,10 @@ class SilenceFormStore { // append a new empty matcher to the list addEmptyMatcher() { - let m = NewEmptyMatcher(); - this.matchers.push(m); + this.matchers.push(NewEmptyMatcher()); }, - deleteMatcher(id) { + 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); @@ -316,7 +372,12 @@ class SilenceFormStore { }, // if alerts argument is not passed all group alerts will be used - fillMatchersFromGroup(group, stripLabels, alertmanagers, alerts) { + fillMatchersFromGroup( + group: APIAlertGroupT, + stripLabels: string[], + alertmanagers: MultiValueOptionT[], + alerts: APIAlertT[] + ) { this.alertmanagers = alertmanagers; this.matchers = MatchersFromGroup(group, stripLabels, alerts); @@ -329,14 +390,17 @@ class SilenceFormStore { this.resetInputs = false; }, - fillFormFromSilence(alertmanager, silence) { + fillFormFromSilence( + alertmanager: APIAlertmanagerUpstreamT, + silence: AlertmanagerSilencePayloadT + ) { this.silenceID = silence.id; this.alertmanagers = AlertmanagerClustersToOption({ [alertmanager.cluster]: alertmanager.clusterMembers, }); - const matchers = []; + const matchers: MatcherWithIDT[] = []; for (const m of silence.matchers) { const matcher = NewEmptyMatcher(); matcher.name = m.name; @@ -366,20 +430,20 @@ class SilenceFormStore { this.endsAt = addMinutes(this.startsAt, 1); } }, - incStart(minutes) { + incStart(minutes: number) { this.startsAt = addMinutes(this.startsAt, minutes); this.verifyStarEnd(); }, - decStart(minutes) { + decStart(minutes: number) { this.startsAt = subMinutes(this.startsAt, minutes); this.verifyStarEnd(); }, - incEnd(minutes) { + incEnd(minutes: number) { this.endsAt = addMinutes(this.endsAt, minutes); this.verifyStarEnd(); }, - decEnd(minutes) { + decEnd(minutes: number) { this.endsAt = subMinutes(this.endsAt, minutes); this.verifyStarEnd(); },