chore(ui): migrate SilenceFormStore to typescript

This commit is contained in:
Łukasz Mierzwa
2020-07-10 18:03:02 +01:00
committed by Łukasz Mierzwa
parent ab96f0213c
commit a2b5fbb11b
6 changed files with 224 additions and 68 deletions

View File

@@ -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";

View File

@@ -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");

98
ui/src/Models/APITypes.ts Normal file
View File

@@ -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[];
}

11
ui/src/Models/UI.ts Normal file
View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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<string> } = {};
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();
},