mirror of
https://github.com/prymitive/karma
synced 2026-08-17 11:27:01 +00:00
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:
committed by
Łukasz Mierzwa
parent
68b0d42a08
commit
7015d250b5
+10
-5
@@ -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,
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -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[]>([]);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user