fix(ui): migrate mobx code to new version

Latest mobx removes support for old decorators and other things, migrate code to the new way of mobxing.
This commit is contained in:
Lukasz Mierzwa
2026-08-03 16:14:24 +01:00
committed by Łukasz Mierzwa
parent 68b0d42a08
commit 7015d250b5
22 changed files with 808 additions and 936 deletions
+10 -5
View File
@@ -1,4 +1,9 @@
import type { AlertStateT, APIAlertT, APIAlertGroupT } from "Models/APITypes";
import type {
AlertStateT,
APIAlertT,
APIAlertGroupT,
ReadOnly,
} from "Models/APITypes";
export interface VanillaAlertT {
labels: { [key: string]: string };
@@ -15,8 +20,8 @@ export interface VanillaAlertT {
}
export const alertToJSON = (
group: APIAlertGroupT,
alert: APIAlertT,
group: ReadOnly<APIAlertGroupT>,
alert: ReadOnly<APIAlertT>,
): VanillaAlertT[] => {
const alerts: VanillaAlertT[] = [];
@@ -36,8 +41,8 @@ export const alertToJSON = (
startsAt: am.startsAt,
generatorURL: am.source,
status: {
inhibitedBy: am.inhibitedBy,
silencedBy: am.silencedBy,
inhibitedBy: [...am.inhibitedBy],
silencedBy: [...am.silencedBy],
state: am.state,
},
}))
+2 -1
View File
@@ -14,6 +14,7 @@ import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclama
import type {
APIAlertGroupT,
AlertmanagerSilencePayloadT,
ReadOnly,
} from "Models/APITypes";
import type { AlertStore } from "Stores/AlertStore";
import {
@@ -37,7 +38,7 @@ interface PostResponseT {
const AlertAck: FC<{
alertStore: AlertStore;
silenceFormStore: SilenceFormStore;
group: APIAlertGroupT;
group: ReadOnly<APIAlertGroupT>;
}> = observer(({ alertStore, silenceFormStore, group }) => {
const [clusters, setClusters] = useState<ClusterT[]>([]);
const [upstreams, setUpstreams] = useState<UpstreamT[]>([]);
+5 -4
View File
@@ -7,6 +7,7 @@ import type {
APIAlertGroupT,
APIGridT,
HistoryResponseT,
ReadOnly,
} from "Models/APITypes";
import { useFetchAny, UpstreamT } from "Hooks/useFetchAny";
import { TooltipWrapper } from "Components/TooltipWrapper";
@@ -26,10 +27,10 @@ const GetUTCSeconds = (): number => {
return (now.getTime() + now.getTimezoneOffset()) / 1000;
};
export const AlertHistory: FC<{ group: APIAlertGroupT; grid: APIGridT }> = ({
group,
grid,
}) => {
export const AlertHistory: FC<{
group: ReadOnly<APIAlertGroupT>;
grid: ReadOnly<APIGridT>;
}> = ({ group, grid }) => {
const [ref, inView] = useInView({ triggerOnce: true });
const [lastUpdate, setLastUpdate] = useState<number>(() => GetUTCSeconds());
@@ -17,6 +17,7 @@ import type {
APIAlertT,
APIAlertGroupT,
APIAnnotationT,
ReadOnly,
} from "Models/APITypes";
import type { AlertStore } from "Stores/AlertStore";
import {
@@ -33,8 +34,8 @@ import { alertToJSON } from "Common/Alert";
const onSilenceClick = (
alertStore: AlertStore,
silenceFormStore: SilenceFormStore,
group: APIAlertGroupT,
alert: APIAlertT,
group: ReadOnly<APIAlertGroupT>,
alert: ReadOnly<APIAlertT>,
) => {
const clusters: { [cluster: string]: string[] } = {};
Object.entries(alertStore.data.clustersWithoutReadOnly).forEach(
@@ -61,8 +62,8 @@ interface MenuContentProps {
y: number;
floating: Ref<HTMLDivElement> | null;
strategy: CSSProperties["position"];
group: APIAlertGroupT;
alert: APIAlertT;
group: ReadOnly<APIAlertGroupT>;
alert: ReadOnly<APIAlertT>;
afterClick: () => void;
alertStore: AlertStore;
silenceFormStore: SilenceFormStore;
@@ -173,8 +174,8 @@ const MenuContent = observer(
);
interface AlertMenuProps {
group: APIAlertGroupT;
alert: APIAlertT;
group: ReadOnly<APIAlertGroupT>;
alert: ReadOnly<APIAlertT>;
alertStore: AlertStore;
silenceFormStore: SilenceFormStore;
setIsMenuOpen: (isOpen: boolean) => void;
@@ -6,6 +6,7 @@ import type {
APIAlertT,
APIAlertGroupT,
APIAlertmanagerStateT,
ReadOnly,
} from "Models/APITypes";
import type { AlertStore } from "Stores/AlertStore";
import type { SilenceFormStore } from "Stores/SilenceFormStore";
@@ -18,8 +19,8 @@ import { AlertMenu } from "./AlertMenu";
import { RenderSilence } from "../Silences";
const Alert: FC<{
group: APIAlertGroupT;
alert: APIAlertT;
group: ReadOnly<APIAlertGroupT>;
alert: ReadOnly<APIAlertT>;
showReceiver: boolean;
showOnlyExpandedAnnotations: boolean;
afterUpdate: () => void;
@@ -48,7 +49,7 @@ const Alert: FC<{
const silences: {
[cluster: string]: {
alertmanager: APIAlertmanagerStateT;
alertmanager: ReadOnly<APIAlertmanagerStateT>;
silences: string[];
};
} = {};
@@ -2,7 +2,7 @@ import type { FC } from "react";
import { observer } from "mobx-react-lite";
import type { APIAlertGroupT } from "Models/APITypes";
import type { APIAlertGroupT, ReadOnly } from "Models/APITypes";
import { StaticLabels } from "Common/Query";
import type { AlertStore } from "Stores/AlertStore";
import type { SilenceFormStore } from "Stores/SilenceFormStore";
@@ -11,7 +11,7 @@ import { RenderNonLinkAnnotation, RenderLinkAnnotation } from "../Annotation";
import { RenderSilence } from "../Silences";
const GroupFooter: FC<{
group: APIAlertGroupT;
group: ReadOnly<APIAlertGroupT>;
afterUpdate: () => void;
alertStore: AlertStore;
silenceFormStore: SilenceFormStore;
@@ -12,7 +12,7 @@ import { faShareSquare } from "@fortawesome/free-solid-svg-icons/faShareSquare";
import { faBellSlash } from "@fortawesome/free-solid-svg-icons/faBellSlash";
import { faWrench } from "@fortawesome/free-solid-svg-icons/faWrench";
import type { APIAlertGroupT } from "Models/APITypes";
import type { APIAlertGroupT, ReadOnly } from "Models/APITypes";
import { FormatAlertsQ } from "Stores/AlertStore";
import type { AlertStore } from "Stores/AlertStore";
import {
@@ -28,7 +28,7 @@ import { MenuLink } from "Components/Grid/AlertGrid/AlertGroup/MenuLink";
const onSilenceClick = (
alertStore: AlertStore,
silenceFormStore: SilenceFormStore,
group: APIAlertGroupT,
group: ReadOnly<APIAlertGroupT>,
) => {
const clusters: { [cluster: string]: string[] } = {};
Object.entries(alertStore.data.clustersWithoutReadOnly).forEach(
@@ -56,7 +56,7 @@ const MenuContent: FC<{
y: number;
floating: Ref<HTMLDivElement> | null;
strategy: CSSProperties["position"];
group: APIAlertGroupT;
group: ReadOnly<APIAlertGroupT>;
afterClick: () => void;
alertStore: AlertStore;
silenceFormStore: SilenceFormStore;
@@ -146,7 +146,7 @@ const MenuContent: FC<{
);
const GroupMenu: FC<{
group: APIAlertGroupT;
group: ReadOnly<APIAlertGroupT>;
alertStore: AlertStore;
silenceFormStore: SilenceFormStore;
themed: boolean;
@@ -1,6 +1,6 @@
import type { FC, MouseEvent } from "react";
import type { APIAlertGroupT } from "Models/APITypes";
import type { APIAlertGroupT, ReadOnly } from "Models/APITypes";
import type { AlertStore } from "Stores/AlertStore";
import type { SilenceFormStore } from "Stores/SilenceFormStore";
import FilteringLabel from "Components/Labels/FilteringLabel";
@@ -13,7 +13,7 @@ import { GroupMenu } from "./GroupMenu";
const GroupHeader: FC<{
isCollapsed: boolean;
setIsCollapsed: (isCollapsed: boolean) => void;
group: APIAlertGroupT;
group: ReadOnly<APIAlertGroupT>;
alertStore: AlertStore;
silenceFormStore: SilenceFormStore;
themedCounters: boolean;
@@ -1,6 +1,6 @@
import { FC, memo } from "react";
import type { APISilenceT } from "Models/APITypes";
import type { APISilenceT, ReadOnly } from "Models/APITypes";
import type { AlertStore } from "Stores/AlertStore";
import type { SilenceFormStore } from "Stores/SilenceFormStore";
import { ManagedSilence } from "Components/ManagedSilence";
@@ -19,7 +19,7 @@ const GetSilenceFromStore = (
alertStore: AlertStore,
cluster: string,
silenceID: string,
): APISilenceT | null => {
): ReadOnly<APISilenceT> | null => {
const amSilences = alertStore.data.silences[cluster];
if (!amSilences) return null;
@@ -16,7 +16,12 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons/faPlus";
import { faMinus } from "@fortawesome/free-solid-svg-icons/faMinus";
import { faEllipsisH } from "@fortawesome/free-solid-svg-icons/faEllipsisH";
import type { APIGridT, APIAlertGroupT, AlertStateT } from "Models/APITypes";
import type {
APIGridT,
APIAlertGroupT,
AlertStateT,
ReadOnly,
} from "Models/APITypes";
import type { Settings } from "Stores/Settings";
import type { AlertStore } from "Stores/AlertStore";
import type { SilenceFormStore } from "Stores/SilenceFormStore";
@@ -48,8 +53,8 @@ const LoadButton: FC<{
};
interface AlertGroupProps {
grid: APIGridT;
group: APIAlertGroupT;
grid: ReadOnly<APIGridT>;
group: ReadOnly<APIAlertGroupT>;
afterUpdate: () => void;
alertStore: AlertStore;
settingsStore: Settings;
+2 -2
View File
@@ -27,7 +27,7 @@ import { faAngleDoubleDown } from "@fortawesome/free-solid-svg-icons/faAngleDoub
import type { AlertStore } from "Stores/AlertStore";
import type { Settings } from "Stores/Settings";
import type { SilenceFormStore } from "Stores/SilenceFormStore";
import type { APIGridT } from "Models/APITypes";
import type { APIGridT, ReadOnly } from "Models/APITypes";
import { useGrid } from "Hooks/useGrid";
import { ThemeContext } from "Components/Theme";
import { DefaultDetailsCollapseValue } from "./AlertGroup/DetailsToggle";
@@ -118,7 +118,7 @@ const Grid: FC<{
settingsStore: Settings;
gridSizesConfig: SizeDetail[];
groupWidth: number;
grid: APIGridT;
grid: ReadOnly<APIGridT>;
outerPadding: number;
paddingTop: number;
zIndex: number;
@@ -18,7 +18,7 @@ import { faCaretDown } from "@fortawesome/free-solid-svg-icons/faCaretDown";
import type { AlertStore } from "Stores/AlertStore";
import type { Settings } from "Stores/Settings";
import type { APIGridT } from "Models/APITypes";
import type { APIGridT, ReadOnly } from "Models/APITypes";
import { StringToOption, OptionT } from "Common/Select";
import { DropdownSlide } from "Components/Animations/DropdownSlide";
import { ThemeContext } from "Components/Theme";
@@ -36,7 +36,7 @@ const NullContainer: FC = () => null;
const GridLabelNameSelect: FC<{
alertStore: AlertStore;
settingsStore: Settings;
grid: APIGridT;
grid: ReadOnly<APIGridT>;
onClose: () => void;
}> = ({ alertStore, settingsStore, grid, onClose }) => {
const loadOptions = (
@@ -94,7 +94,7 @@ const Dropdown: FC<{
strategy: CSSProperties["position"];
alertStore: AlertStore;
settingsStore: Settings;
grid: APIGridT;
grid: ReadOnly<APIGridT>;
onClose: () => void;
}> = ({
x,
@@ -131,7 +131,7 @@ const Dropdown: FC<{
const GridLabelSelect: FC<{
alertStore: AlertStore;
settingsStore: Settings;
grid: APIGridT;
grid: ReadOnly<APIGridT>;
}> = ({ alertStore, settingsStore, grid }) => {
const [isVisible, setIsVisible] = useState<boolean>(false);
const hide = useCallback(() => setIsVisible(false), []);
@@ -5,7 +5,7 @@ import { faGrip } from "@fortawesome/free-solid-svg-icons/faGrip";
import type { AlertStore } from "Stores/AlertStore";
import type { Settings } from "Stores/Settings";
import type { APIGridT } from "Models/APITypes";
import type { APIGridT, ReadOnly } from "Models/APITypes";
import FilteringLabel from "Components/Labels/FilteringLabel";
import FilteringCounterBadge from "Components/Labels/FilteringCounterBadge";
import { TooltipWrapper } from "Components/TooltipWrapper";
@@ -15,7 +15,7 @@ import { GridLabelSelect } from "./GridLabelSelect";
interface SwimlaneProps {
alertStore: AlertStore;
settingsStore: Settings;
grid: APIGridT;
grid: ReadOnly<APIGridT>;
isExpanded: boolean;
onToggle: (event: MouseEvent) => void;
paddingTop: number;
@@ -9,7 +9,7 @@ import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle";
import { faRedo } from "@fortawesome/free-solid-svg-icons/faRedo";
import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch";
import type { APISilenceT } from "Models/APITypes";
import type { APISilenceT, ReadOnly } from "Models/APITypes";
import type { AlertStore } from "Stores/AlertStore";
import type { SilenceFormStore } from "Stores/SilenceFormStore";
import { FormatQuery, QueryOperators, StaticLabels } from "Common/Query";
@@ -109,7 +109,7 @@ const DeleteSilenceModalContent: FC<{
alertStore: AlertStore;
silenceFormStore: SilenceFormStore;
cluster: string;
silence: APISilenceT;
silence: ReadOnly<APISilenceT>;
onHide: () => void;
}> = ({ alertStore, silenceFormStore, cluster, silence, onHide }) => {
const [confirm, setConfirm] = useState<boolean>(false);
@@ -167,7 +167,7 @@ const DeleteSilence: FC<{
alertStore: AlertStore;
silenceFormStore: SilenceFormStore;
cluster: string;
silence: APISilenceT;
silence: ReadOnly<APISilenceT>;
isUpper?: boolean;
}> = ({ alertStore, silenceFormStore, cluster, silence, isUpper = false }) => {
const [visible, setVisible] = useState<boolean>(false);
@@ -8,7 +8,7 @@ import { differenceInSeconds } from "date-fns";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBellSlash } from "@fortawesome/free-solid-svg-icons/faBellSlash";
import type { APISilenceT } from "Models/APITypes";
import type { APISilenceT, ReadOnly } from "Models/APITypes";
import type { AlertStore } from "Stores/AlertStore";
import FilteringCounterBadge from "Components/Labels/FilteringCounterBadge";
import { ToggleIcon } from "Components/ToggleIcon";
@@ -16,7 +16,7 @@ import { DateFromNow } from "Components/DateFromNow";
import { StaticLabels } from "Common/Query";
const SilenceProgress: FC<{
silence: APISilenceT;
silence: ReadOnly<APISilenceT>;
}> = ({ silence }) => {
const [now] = useState(() => new Date());
const diff = differenceInSeconds(parseISO(silence.endsAt), now);
@@ -40,7 +40,7 @@ const SilenceProgress: FC<{
const SilenceComment: FC<{
cluster: string;
silence: APISilenceT;
silence: ReadOnly<APISilenceT>;
alertCount: number;
collapsed: boolean;
collapseToggle: () => void;
@@ -17,7 +17,7 @@ import { faHome } from "@fortawesome/free-solid-svg-icons/faHome";
import { faFingerprint } from "@fortawesome/free-solid-svg-icons/faFingerprint";
import { faCopy } from "@fortawesome/free-solid-svg-icons/faCopy";
import type { APISilenceT } from "Models/APITypes";
import type { APISilenceT, ReadOnly } from "Models/APITypes";
import type { AlertStore } from "Stores/AlertStore";
import { SilenceFormStore, MatcherToOperator } from "Stores/SilenceFormStore";
import { TooltipWrapper } from "Components/TooltipWrapper";
@@ -53,7 +53,7 @@ const SilenceIDCopyButton: FC<{
const SilenceDetails: FC<{
alertStore: AlertStore;
silenceFormStore: SilenceFormStore;
silence: APISilenceT;
silence: ReadOnly<APISilenceT>;
cluster: string;
onEditSilence: () => void;
isUpper?: boolean;
+7 -3
View File
@@ -5,7 +5,11 @@ import { action } from "mobx";
import { parseISO } from "date-fns/parseISO";
import { getUnixTime } from "date-fns/getUnixTime";
import type { APISilenceT, APIAlertmanagerUpstreamT } from "Models/APITypes";
import type {
APISilenceT,
APIAlertmanagerUpstreamT,
ReadOnly,
} from "Models/APITypes";
import type { AlertStore } from "Stores/AlertStore";
import type { SilenceFormStore } from "Stores/SilenceFormStore";
import { SilenceComment } from "./SilenceComment";
@@ -14,7 +18,7 @@ import { SilenceDetails } from "./SilenceDetails";
const GetAlertmanager = (
alertStore: AlertStore,
cluster: string,
): APIAlertmanagerUpstreamT =>
): ReadOnly<APIAlertmanagerUpstreamT> =>
alertStore.data.readWriteAlertmanagers
.filter((u) => u.cluster === cluster)
.slice(0, 1)[0];
@@ -31,7 +35,7 @@ const ManagedSilence: FC<{
cluster: string;
alertCount: number;
alertCountAlwaysVisible: boolean;
silence: APISilenceT;
silence: ReadOnly<APISilenceT>;
alertStore: AlertStore;
silenceFormStore: SilenceFormStore;
isOpen?: boolean;
@@ -17,6 +17,7 @@ import { faTrash } from "@fortawesome/free-solid-svg-icons/faTrash";
import type {
APIAlertmanagerUpstreamT,
APIManagedSilenceT,
ReadOnly,
} from "Models/APITypes";
import type { AlertStore } from "Stores/AlertStore";
import type { SilenceFormStore } from "Stores/SilenceFormStore";
@@ -207,7 +208,7 @@ const MassDeleteProgress: FC<{
const deleteSilence = async (
cluster: string,
id: string,
ams: APIAlertmanagerUpstreamT[],
ams: ReadOnly<APIAlertmanagerUpstreamT>[],
) => {
let err = "";
for (const am of ams) {
+6
View File
@@ -1,5 +1,11 @@
export type AlertStateT = "unprocessed" | "active" | "suppressed";
export type ReadOnly<T> = T extends (infer R)[]
? readonly ReadOnly<R>[]
: T extends object
? { readonly [K in keyof T]: ReadOnly<T[K]> }
: T;
interface LabelT {
name: string;
value: string;
+387 -459
View File
@@ -1,4 +1,4 @@
import { observable, action, computed, toJS } from "mobx";
import { makeAutoObservable, observableRef, action, toJS } from "mobx";
import { throttle } from "es-toolkit";
@@ -9,6 +9,7 @@ import type {
APIAlertsResponseT,
APIAlertsResponseColorsT,
APIGridT,
ReadOnly,
APIAlertsResponseSilenceMapT,
APIAlertsResponseUpstreamsT,
APIAlertsResponseUpstreamsClusterMapT,
@@ -118,478 +119,405 @@ 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;
class AlertStoreFilters {
values: FilterT[] = [];
constructor() {
makeAutoObservable(this, {}, { autoBind: true, name: "API Filters" });
}
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) });
}
} else {
this.addFilter(newRaw);
}
}
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;
}
}
}
interface AlertStoreDataT {
colors: APIAlertsResponseColorsT;
grids: APIGridT[];
labelNames: string[];
setLabelNames: (v: string[]) => void;
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;
setClusters: (c: APIAlertsResponseUpstreamsClusterMapT) => void;
setSilences: (s: APIAlertsResponseSilenceMapT) => void;
setReceivers: (r: string[]) => void;
setColors: (c: APIAlertsResponseColorsT) => void;
readonly upstreamsWithErrors: APIAlertmanagerUpstreamT[];
}
interface AlertStoreInfoT {
authentication: {
enabled: boolean;
username: string;
class AlertStoreData {
colors: ReadOnly<APIAlertsResponseColorsT> = {};
grids: ReadOnly<APIGridT[]> = [];
labelNames: ReadOnly<string[]> = [];
silences: ReadOnly<APIAlertsResponseSilenceMapT> = {};
upstreams: ReadOnly<APIAlertsResponseUpstreamsT> = {
counters: { total: 0, healthy: 0, failed: 0 },
instances: [],
clusters: {},
};
totalAlerts: number;
timestamp: string;
version: string;
upgradeReady: boolean;
upgradeNeeded: boolean;
isRetrying: boolean;
reloadNeeded: boolean;
setIsRetrying: () => void;
clearIsRetrying: () => void;
setUpgradeNeeded: (v: boolean) => void;
setUpgradeReady: (v: boolean) => void;
setReloadNeeded: (v: boolean) => void;
setTotalAlerts: (n: number) => void;
setAuthentication: (enabled: boolean, username: string) => void;
setVersion: (v: string) => void;
setTimestamp: (v: string) => void;
receivers: ReadOnly<string[]> = [];
constructor() {
makeAutoObservable(
this,
{
// all of these are replaced wholesale on every API response,
// so there's no need to deep convert them into observables
colors: observableRef,
grids: observableRef,
labelNames: observableRef,
silences: observableRef,
upstreams: observableRef,
receivers: observableRef,
},
{ autoBind: true, name: "API Response data" },
);
}
get gridPadding(): number {
return this.grids.filter((g) => g.labelName !== "").length > 0 ? 5 : 0;
}
getAlertmanagerByName(
name: string,
): ReadOnly<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(): ReadOnly<APIAlertmanagerUpstreamT>[] {
return this.upstreams.instances.filter((am) => am.readonly === true);
}
get readWriteAlertmanagers(): ReadOnly<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 unhealthy = this.upstreams.instances
.filter((upstream) => upstream.error !== "")
.map((upstream) => upstream.name);
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.filter((member) => !unhealthy.includes(member)),
...members.filter((member) => unhealthy.includes(member)),
];
}
}
return clusters;
}
getColorData(
name: string,
value: string,
): ReadOnly<APILabelColorT> | undefined {
if (this.colors[name] !== undefined) {
return this.colors[name][value];
}
}
setGrids(g: ReadOnly<APIGridT[]>) {
this.grids = g;
}
setUpstreams(u: ReadOnly<APIAlertsResponseUpstreamsT>) {
this.upstreams = u;
}
setClusters(c: ReadOnly<APIAlertsResponseUpstreamsClusterMapT>) {
this.upstreams = { ...this.upstreams, clusters: c };
}
setSilences(s: ReadOnly<APIAlertsResponseSilenceMapT>) {
this.silences = s;
}
setReceivers(r: ReadOnly<string[]>) {
this.receivers = r;
}
setColors(c: ReadOnly<APIAlertsResponseColorsT>) {
this.colors = c;
}
setLabelNames(v: ReadOnly<string[]>) {
this.labelNames = v;
}
get upstreamsWithErrors(): ReadOnly<APIAlertmanagerUpstreamT>[] {
const unhealthy: ReadOnly<APIAlertmanagerUpstreamT>[] = [];
for (const clusterID of Object.keys(this.upstreams.clusters)) {
const members = this.upstreams.instances.filter(
(upstream) => upstream.cluster === clusterID,
);
if (
members.length > 0 &&
members.filter((upstream) => upstream.error === "").length === 0
) {
unhealthy.push(...members);
}
}
return unhealthy;
}
}
interface AlertStoreSettingsT {
values: APISettingsT;
setValues: (v: APISettingsT) => void;
class AlertStoreInfo {
authentication = {
enabled: false as boolean,
username: "",
};
totalAlerts = 0;
version = "unknown";
timestamp = "";
upgradeReady = false;
upgradeNeeded = false;
isRetrying = false;
reloadNeeded = false;
constructor() {
makeAutoObservable(this, {}, { autoBind: true, name: "API response info" });
}
setIsRetrying() {
this.isRetrying = true;
}
clearIsRetrying() {
this.isRetrying = false;
}
setUpgradeNeeded(v: boolean) {
this.upgradeNeeded = v;
}
setUpgradeReady(v: boolean) {
this.upgradeReady = v;
}
setReloadNeeded(v: boolean) {
this.reloadNeeded = v;
}
setTotalAlerts(n: number) {
this.totalAlerts = n;
}
setAuthentication(enabled: boolean, username: string) {
this.authentication.enabled = enabled;
this.authentication.username = username;
}
setVersion(v: string) {
this.version = v;
}
setTimestamp(v: string) {
this.timestamp = v;
}
}
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;
setError: (e: null | string) => void;
class AlertStoreSettings {
values: ReadOnly<APISettingsT> = {
annotationsDefaultHidden: false as boolean,
annotationsHidden: [] as string[],
annotationsVisible: [] as string[],
annotationsEnableHTML: false as boolean,
sorting: {
grid: {
order: "startsAt",
reverse: false as boolean,
label: "alertname",
},
valueMapping: {},
},
silenceForm: {
strip: {
labels: [] as string[],
},
defaultAlertmanagers: [] as string[],
},
alertAcknowledgement: {
enabled: false as boolean,
durationSeconds: 900,
author: "karma / author missing",
comment: "ACK! This alert was acknowledged using karma",
},
historyEnabled: true,
gridGroupLimit: 40,
labels: {},
};
constructor() {
makeAutoObservable(
this,
{ values: observableRef },
{ autoBind: true, name: "Global settings" },
);
}
setValues(v: ReadOnly<APISettingsT>) {
this.values = v;
}
}
interface AlertStoreUIT {
isIdle: boolean;
setIsIdle: (val: boolean) => void;
gridGroupLimits: { [key: string]: { [val: string]: number } };
setGridGroupLimit: (key: string, val: string, limit: number) => void;
groupAlertLimits: { [gid: string]: number };
setGroupAlertLimit: (gid: string, limit: number) => void;
purgeGroupAlertLimits: (knownGids: string[]) => void;
class AlertStoreStatus {
value: symbol = AlertStoreStatuses.Idle;
lastUpdateAt: number | Date = 0;
error: null | string = null;
stopped = false;
paused = false;
constructor() {
makeAutoObservable(this, {}, { autoBind: true, name: "Store status" });
}
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;
}
setError(e: null | string) {
this.error = e;
}
}
class AlertStoreUI {
isIdle = false;
gridGroupLimits: { [key: string]: { [val: string]: number } } = {};
groupAlertLimits: { [gid: string]: number } = {};
constructor() {
makeAutoObservable(this, {}, { autoBind: true });
}
setIsIdle(val: boolean) {
this.isIdle = val;
}
setGridGroupLimit(key: string, val: string, limit: number) {
this.gridGroupLimits = {
[key]: { ...this.gridGroupLimits[key], [val]: limit },
};
}
setGroupAlertLimit(gid: string, limit: number) {
this.groupAlertLimits[gid] = limit;
}
purgeGroupAlertLimits(knownGids: string[]) {
const newLimits: { [gid: string]: number } = {};
Object.entries(this.groupAlertLimits)
.filter(([gid, _]) => knownGids.includes(gid))
.forEach(([gid, limit]) => {
newLimits[gid] = limit;
});
this.groupAlertLimits = newLimits;
}
}
class AlertStore {
filters: AlertStoreFiltersT;
data: AlertStoreDataT;
info: AlertStoreInfoT;
settings: AlertStoreSettingsT;
status: AlertStoreStatusT;
ui: AlertStoreUIT;
filters = new AlertStoreFilters();
data = new AlertStoreData();
info = new AlertStoreInfo();
settings = new AlertStoreSettings();
status = new AlertStoreStatus();
ui = new AlertStoreUI();
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) });
}
} else {
this.addFilter(newRaw);
}
},
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,
grids: [] as APIGridT[],
labelNames: [] as string[],
setLabelNames(v: string[]) {
this.labelNames = v;
},
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 unhealthy = this.upstreams.instances
.filter((upstream) => upstream.error !== "")
.map((upstream) => upstream.name);
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.filter((member) => !unhealthy.includes(member)),
...members.filter((member) => unhealthy.includes(member)),
];
}
}
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;
},
setClusters(c: APIAlertsResponseUpstreamsClusterMapT) {
this.upstreams.clusters = c;
},
setSilences(s: APIAlertsResponseSilenceMapT) {
this.silences = s;
},
setReceivers(r: string[]) {
this.receivers = r;
},
setColors(c: APIAlertsResponseColorsT) {
this.colors = c;
},
get upstreamsWithErrors(): APIAlertmanagerUpstreamT[] {
const unhealthy: APIAlertmanagerUpstreamT[] = [];
for (const clusterID of Object.keys(this.upstreams.clusters)) {
const members = this.upstreams.instances.filter(
(upstream) => upstream.cluster === clusterID,
);
if (
members.length > 0 &&
members.filter((upstream) => upstream.error === "").length === 0
) {
unhealthy.push(...members);
}
}
return unhealthy;
},
},
{
gridPadding: computed,
readOnlyAlertmanagers: computed,
readWriteAlertmanagers: computed,
clustersWithoutReadOnly: computed,
setGrids: action.bound,
setUpstreams: action.bound,
setClusters: action.bound,
setSilences: action.bound,
setReceivers: action.bound,
setColors: action.bound,
setLabelNames: action.bound,
},
{ name: "API Response data" },
);
this.info = observable(
{
authentication: {
enabled: false as boolean,
username: "",
},
totalAlerts: 0,
version: "unknown",
timestamp: "",
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(v: boolean) {
this.upgradeNeeded = v;
},
setUpgradeReady(v: boolean) {
this.upgradeReady = v;
},
setReloadNeeded(v: boolean) {
this.reloadNeeded = v;
},
setTotalAlerts(n: number) {
this.totalAlerts = n;
},
setAuthentication(enabled: boolean, username: string) {
this.authentication.enabled = enabled;
this.authentication.username = username;
},
setVersion(v: string) {
this.version = v;
},
setTimestamp(v: string) {
this.timestamp = v;
},
},
{
setIsRetrying: action.bound,
clearIsRetrying: action.bound,
setReloadNeeded: action.bound,
setUpgradeNeeded: action.bound,
setTotalAlerts: action.bound,
setAuthentication: action.bound,
setVersion: action.bound,
setTimestamp: action.bound,
},
{ name: "API response info" },
);
this.settings = observable(
{
values: {
annotationsDefaultHidden: false as boolean,
annotationsHidden: [] as string[],
annotationsVisible: [] as string[],
annotationsEnableHTML: false as boolean,
sorting: {
grid: {
order: "startsAt",
reverse: false as boolean,
label: "alertname",
},
valueMapping: {},
},
silenceForm: {
strip: {
labels: [] as string[],
},
defaultAlertmanagers: [] as string[],
},
alertAcknowledgement: {
enabled: false as boolean,
durationSeconds: 900,
author: "karma / author missing",
comment: "ACK! This alert was acknowledged using karma",
},
historyEnabled: true,
gridGroupLimit: 40,
labels: {},
} as APISettingsT,
setValues(v: APISettingsT) {
this.values = v;
},
},
{
setValues: action.bound,
},
{
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;
},
setError(e: null | string) {
this.error = e;
},
},
{
setIdle: action,
setFetching: action,
setProcessing: action,
setFailure: action,
pause: action.bound,
resume: action.bound,
togglePause: action.bound,
stop: action.bound,
setError: action.bound,
},
{ name: "Store status" },
);
this.ui = observable(
{
isIdle: false as boolean,
setIsIdle(val: boolean) {
this.isIdle = val;
},
gridGroupLimits: {} as { [key: string]: { [val: string]: number } },
setGridGroupLimit(key: string, val: string, limit: number) {
this.gridGroupLimits = {
[key]: { ...this.gridGroupLimits[key], [val]: limit },
};
},
groupAlertLimits: {} as { [gid: string]: number },
setGroupAlertLimit(gid: string, limit: number) {
this.groupAlertLimits[gid] = limit;
},
purgeGroupAlertLimits(knownGids: string[]) {
const newLimits: { [gid: string]: number } = {};
Object.entries(this.groupAlertLimits)
.filter(([gid, _]) => knownGids.includes(gid))
.forEach(([gid, limit]) => {
newLimits[gid] = limit;
});
this.groupAlertLimits = newLimits;
},
},
{
setIsIdle: action.bound,
setGridGroupLimit: action.bound,
setGroupAlertLimit: action.bound,
},
);
if (initialFilters !== null) this.filters.setFilters(initialFilters);
}
+335 -422
View File
@@ -1,4 +1,4 @@
import { observable, action, computed } from "mobx";
import { makeAutoObservable, actionBound } from "mobx";
import { parseISO } from "date-fns/parseISO";
import { addHours } from "date-fns/addHours";
@@ -14,6 +14,7 @@ import type {
APIAlertmanagerUpstreamT,
AlertmanagerSilencePayloadT,
AlertmanagerSilenceMatcherT,
ReadOnly,
} from "Models/APITypes";
import { StringToOption, OptionT, MultiValueOptionT } from "Common/Select";
import { QueryOperators } from "Common/Query";
@@ -70,12 +71,12 @@ const MatcherToOperator = (
};
const AlertmanagerClustersToOption = (clusterDict: {
[key: string]: string[];
[key: string]: ReadOnly<string[]>;
}): MultiValueOptionT[] =>
Object.entries(clusterDict).map(([clusterID, clusterMembers]) => ({
label:
clusterMembers.length > 1 ? `Cluster: ${clusterID}` : clusterMembers[0],
value: clusterMembers,
value: [...clusterMembers],
}));
export const EscapeRegex = (v: string): string => {
@@ -87,8 +88,8 @@ const UnescapeRegex = (v: string): string => {
};
const MatchersFromGroup = (
group: APIAlertGroupT,
stripLabels: string[],
group: ReadOnly<APIAlertGroupT>,
stripLabels: ReadOnly<string[]>,
onlyActive?: boolean,
): MatcherWithIDT[] => {
const matchers: MatcherWithIDT[] = [];
@@ -120,9 +121,9 @@ const MatchersFromGroup = (
};
const MatchersFromAlerts = (
group: APIAlertGroupT,
stripLabels: string[],
alerts: APIAlertT[],
group: ReadOnly<APIAlertGroupT>,
stripLabels: ReadOnly<string[]>,
alerts: ReadOnly<APIAlertT[]>,
): MatcherWithIDT[] => {
const matchers: MatcherWithIDT[] = [];
@@ -275,18 +276,41 @@ const UnpackRegexMatcherValues = (isRegex: boolean, value: string) => {
type SilenceFormTabT = "editor" | "browser";
type SilenceFormStageT = "form" | "preview" | "submit";
interface SilenceFormStoreToggleT {
visible: boolean;
blurred: boolean;
toggle: () => void;
hide: () => void;
show: () => void;
setBlur: (val: boolean) => void;
class SilenceFormStoreToggle {
visible = false;
blurred = false;
constructor() {
makeAutoObservable(this, {}, { autoBind: true });
}
toggle() {
this.visible = !this.visible;
}
hide() {
this.visible = false;
}
show() {
this.visible = true;
}
setBlur(val: boolean) {
this.blurred = val;
}
}
interface SilenceFormStoreTabT {
current: SilenceFormTabT;
setTab: (value: SilenceFormTabT) => void;
class SilenceFormStoreTab {
current: SilenceFormTabT = "editor";
constructor() {
makeAutoObservable(this, {}, { autoBind: true });
}
setTab(value: SilenceFormTabT) {
this.current = value;
}
}
interface DurationT {
@@ -295,415 +319,304 @@ interface DurationT {
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[],
class SilenceFormStoreData {
currentStage: SilenceFormStageT = "form";
wasValidated = false;
silenceID: null | undefined | string = null;
alertmanagers: MultiValueOptionT[] = [];
matchers: MatcherWithIDT[] = [];
startsAt = new Date();
endsAt = addHours(new Date(), 1);
comment = "";
author = "";
requestsByCluster: { [key: string]: ClusterRequestT } = {};
autofillMatchers = true;
resetInputs = true;
constructor() {
makeAutoObservable(
this,
{
// called from an autorun in AlertManagerInput, autoAction wouldn't
// create an action context there and trip enforceActions
setAlertmanagers: actionBound,
},
{ autoBind: true, name: "Silence form store" },
);
}
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: ReadOnly<APIAlertGroupT>,
stripLabels: ReadOnly<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;
alerts?: ReadOnly<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: ReadOnly<APIAlertmanagerUpstreamT>,
silence: ReadOnly<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;
}
}
class SilenceFormStore {
toggle: SilenceFormStoreToggleT;
tab: SilenceFormStoreTabT;
data: SilenceFormStoreDataT;
toggle = new SilenceFormStoreToggle();
tab = new SilenceFormStoreTab();
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" },
);
}
// 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 = new SilenceFormStoreData();
}
export {
+9 -3
View File
@@ -267,13 +267,19 @@ const makeGridAlertStore = (): AlertStore => {
unprocessedGroup.totalAlerts = 3;
const grids = alertStore.data.grids;
grids[0].alertGroups = [
const alertGroups = [
suppressedGroup,
unprocessedGroup,
...grids[0].alertGroups,
];
grids[0].totalGroups = grids[0].alertGroups.length;
alertStore.data.setGrids(grids);
alertStore.data.setGrids([
{
...grids[0],
alertGroups: alertGroups,
totalGroups: alertGroups.length,
},
...grids.slice(1),
]);
return alertStore;
};