mirror of
https://github.com/prymitive/karma
synced 2026-05-19 04:26:41 +00:00
fix(ui): enable computedRequiresReaction
This commit is contained in:
committed by
Łukasz Mierzwa
parent
638531901a
commit
c67d2f6e1d
@@ -18,6 +18,7 @@ import fetchMock from "@fetch-mock/jest";
|
||||
|
||||
import { mockMatchMedia } from "__fixtures__/matchMedia";
|
||||
import { EmptyAPIResponse } from "__fixtures__/Fetch";
|
||||
import { inReactiveContext } from "__fixtures__/MobX";
|
||||
import type { UIDefaults, ThemeT } from "Models/UI";
|
||||
import { SilenceFormStore, NewEmptyMatcher } from "Stores/SilenceFormStore";
|
||||
import { StringToOption } from "Common/Select";
|
||||
@@ -198,7 +199,7 @@ describe("<App />", () => {
|
||||
store.data.setMatchers([m1, m2]);
|
||||
store.data.setComment("base64");
|
||||
});
|
||||
const m = store.data.toBase64;
|
||||
const m = inReactiveContext(() => store.data.toBase64);
|
||||
|
||||
// Use history.pushState instead of setting window.location to avoid jsdom navigation error
|
||||
window.history.pushState({}, "App", `/?q=bar&m=${m}`);
|
||||
@@ -242,7 +243,7 @@ describe("<App />", () => {
|
||||
const store = new SilenceFormStore();
|
||||
store.data.setMatchers([m1, m2]);
|
||||
store.data.setComment("base64");
|
||||
const m = store.data.toBase64;
|
||||
const m = inReactiveContext(() => store.data.toBase64);
|
||||
|
||||
global.window.location = {
|
||||
href: `http://localhost/?q=bar&m=${m}`,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FC, useEffect, useState } from "react";
|
||||
|
||||
import { toJS } from "mobx";
|
||||
import { action, toJS } from "mobx";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
import { addSeconds } from "date-fns/addSeconds";
|
||||
@@ -47,7 +47,7 @@ const AlertAck: FC<{
|
||||
const { response, error, inProgress, reset } =
|
||||
useFetchAny<PostResponseT>(upstreams);
|
||||
|
||||
const onACK = () => {
|
||||
const onACK = action(() => {
|
||||
setIsAcking(true);
|
||||
|
||||
let author =
|
||||
@@ -95,7 +95,7 @@ const AlertAck: FC<{
|
||||
});
|
||||
}
|
||||
setClusters(c);
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (upstreams.length && !inProgress && (error || response)) {
|
||||
|
||||
@@ -69,107 +69,108 @@ interface MenuContentProps {
|
||||
ref?: Ref<HTMLDivElement>;
|
||||
}
|
||||
|
||||
const MenuContent = ({
|
||||
x,
|
||||
y,
|
||||
floating,
|
||||
strategy,
|
||||
group,
|
||||
alert,
|
||||
afterClick,
|
||||
alertStore,
|
||||
silenceFormStore,
|
||||
ref,
|
||||
}: MenuContentProps) => {
|
||||
const actions: APIAnnotationT[] = [
|
||||
...alert.annotations
|
||||
.filter((a) => a.isLink === true)
|
||||
.filter((a) => a.isAction === true),
|
||||
...group.shared.annotations
|
||||
.filter((a) => a.isLink === true)
|
||||
.filter((a) => a.isAction === true),
|
||||
];
|
||||
const MenuContent = observer(
|
||||
({
|
||||
x,
|
||||
y,
|
||||
floating,
|
||||
strategy,
|
||||
group,
|
||||
alert,
|
||||
afterClick,
|
||||
alertStore,
|
||||
silenceFormStore,
|
||||
ref,
|
||||
}: MenuContentProps) => {
|
||||
const actions: APIAnnotationT[] = [
|
||||
...alert.annotations
|
||||
.filter((a) => a.isLink === true)
|
||||
.filter((a) => a.isAction === true),
|
||||
...group.shared.annotations
|
||||
.filter((a) => a.isLink === true)
|
||||
.filter((a) => a.isAction === true),
|
||||
];
|
||||
|
||||
return (
|
||||
<FetchPauser alertStore={alertStore}>
|
||||
<div
|
||||
className="dropdown-menu d-block shadow m-0"
|
||||
ref={(node) => {
|
||||
if (typeof floating === "function") {
|
||||
floating(node);
|
||||
} else if (floating) {
|
||||
// eslint-disable-next-line react-compiler/react-compiler -- assigning to floating ref in a ref callback is an intentional pattern from @floating-ui
|
||||
floating.current = node;
|
||||
}
|
||||
if (typeof ref === "function") {
|
||||
ref(node);
|
||||
} else if (ref) {
|
||||
ref.current = node;
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
position: strategy,
|
||||
top: y,
|
||||
left: x,
|
||||
}}
|
||||
>
|
||||
<h6 className="dropdown-header">Alert source links:</h6>
|
||||
{alert.alertmanager.map((am) => (
|
||||
<MenuLink
|
||||
key={am.name}
|
||||
icon={faExternalLinkAlt}
|
||||
text={am.name}
|
||||
uri={am.source}
|
||||
afterClick={afterClick}
|
||||
/>
|
||||
))}
|
||||
<div className="dropdown-divider" />
|
||||
return (
|
||||
<FetchPauser alertStore={alertStore}>
|
||||
<div
|
||||
className="dropdown-item cursor-pointer"
|
||||
onClick={() => {
|
||||
copy(JSON.stringify(alertToJSON(group, alert)));
|
||||
afterClick();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon className="me-1" icon={faCopy} />
|
||||
Copy to clipboard
|
||||
</div>
|
||||
{actions.length ? (
|
||||
<>
|
||||
<div className="dropdown-divider" />
|
||||
<h6 className="dropdown-header">Actions:</h6>
|
||||
{actions.map((action) => (
|
||||
<MenuLink
|
||||
key={action.name}
|
||||
icon={faWrench}
|
||||
text={action.name}
|
||||
uri={action.value}
|
||||
afterClick={afterClick}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : null}
|
||||
<div className="dropdown-divider" />
|
||||
<div
|
||||
className={`dropdown-item ${
|
||||
Object.keys(alertStore.data.clustersWithoutReadOnly).length === 0
|
||||
? "disabled"
|
||||
: "cursor-pointer"
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (Object.keys(alertStore.data.clustersWithoutReadOnly).length) {
|
||||
onSilenceClick(alertStore, silenceFormStore, group, alert);
|
||||
afterClick();
|
||||
className="dropdown-menu d-block shadow m-0"
|
||||
ref={(node) => {
|
||||
if (typeof floating === "function") {
|
||||
floating(node);
|
||||
} else if (floating) {
|
||||
floating.current = node;
|
||||
}
|
||||
if (typeof ref === "function") {
|
||||
ref(node);
|
||||
} else if (ref) {
|
||||
ref.current = node;
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
position: strategy,
|
||||
top: y,
|
||||
left: x,
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon className="me-1" icon={faBellSlash} />
|
||||
Silence this alert
|
||||
<h6 className="dropdown-header">Alert source links:</h6>
|
||||
{alert.alertmanager.map((am) => (
|
||||
<MenuLink
|
||||
key={am.name}
|
||||
icon={faExternalLinkAlt}
|
||||
text={am.name}
|
||||
uri={am.source}
|
||||
afterClick={afterClick}
|
||||
/>
|
||||
))}
|
||||
<div className="dropdown-divider" />
|
||||
<div
|
||||
className="dropdown-item cursor-pointer"
|
||||
onClick={() => {
|
||||
copy(JSON.stringify(alertToJSON(group, alert)));
|
||||
afterClick();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon className="me-1" icon={faCopy} />
|
||||
Copy to clipboard
|
||||
</div>
|
||||
{actions.length ? (
|
||||
<>
|
||||
<div className="dropdown-divider" />
|
||||
<h6 className="dropdown-header">Actions:</h6>
|
||||
{actions.map((action) => (
|
||||
<MenuLink
|
||||
key={action.name}
|
||||
icon={faWrench}
|
||||
text={action.name}
|
||||
uri={action.value}
|
||||
afterClick={afterClick}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : null}
|
||||
<div className="dropdown-divider" />
|
||||
<div
|
||||
className={`dropdown-item ${
|
||||
Object.keys(alertStore.data.clustersWithoutReadOnly).length === 0
|
||||
? "disabled"
|
||||
: "cursor-pointer"
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (Object.keys(alertStore.data.clustersWithoutReadOnly).length) {
|
||||
onSilenceClick(alertStore, silenceFormStore, group, alert);
|
||||
afterClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon className="me-1" icon={faBellSlash} />
|
||||
Silence this alert
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FetchPauser>
|
||||
);
|
||||
};
|
||||
</FetchPauser>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
interface AlertMenuProps {
|
||||
group: APIAlertGroupT;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { FC, useRef, useState, useCallback, Ref, CSSProperties } from "react";
|
||||
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
import copy from "copy-to-clipboard";
|
||||
|
||||
import { useFloating, shift, offset } from "@floating-ui/react-dom";
|
||||
@@ -58,88 +60,90 @@ const MenuContent: FC<{
|
||||
afterClick: () => void;
|
||||
alertStore: AlertStore;
|
||||
silenceFormStore: SilenceFormStore;
|
||||
}> = ({
|
||||
x,
|
||||
y,
|
||||
floating,
|
||||
strategy,
|
||||
group,
|
||||
afterClick,
|
||||
alertStore,
|
||||
silenceFormStore,
|
||||
}) => {
|
||||
const groupFilters = group.labels.map((label) =>
|
||||
FormatQuery(label.name, QueryOperators.Equal, label.value),
|
||||
);
|
||||
groupFilters.push(
|
||||
FormatQuery(StaticLabels.Receiver, QueryOperators.Equal, group.receiver),
|
||||
);
|
||||
const baseURL = [
|
||||
window.location.protocol,
|
||||
"//",
|
||||
window.location.host,
|
||||
window.location.pathname,
|
||||
].join("");
|
||||
const groupLink = `${baseURL}?${FormatAlertsQ(groupFilters)}`;
|
||||
}> = observer(
|
||||
({
|
||||
x,
|
||||
y,
|
||||
floating,
|
||||
strategy,
|
||||
group,
|
||||
afterClick,
|
||||
alertStore,
|
||||
silenceFormStore,
|
||||
}) => {
|
||||
const groupFilters = group.labels.map((label) =>
|
||||
FormatQuery(label.name, QueryOperators.Equal, label.value),
|
||||
);
|
||||
groupFilters.push(
|
||||
FormatQuery(StaticLabels.Receiver, QueryOperators.Equal, group.receiver),
|
||||
);
|
||||
const baseURL = [
|
||||
window.location.protocol,
|
||||
"//",
|
||||
window.location.host,
|
||||
window.location.pathname,
|
||||
].join("");
|
||||
const groupLink = `${baseURL}?${FormatAlertsQ(groupFilters)}`;
|
||||
|
||||
const actions = group.shared.annotations
|
||||
.filter((a) => a.isLink === true)
|
||||
.filter((a) => a.isAction === true);
|
||||
const actions = group.shared.annotations
|
||||
.filter((a) => a.isLink === true)
|
||||
.filter((a) => a.isAction === true);
|
||||
|
||||
return (
|
||||
<FetchPauser alertStore={alertStore}>
|
||||
<div
|
||||
className="dropdown-menu d-block shadow m-0"
|
||||
ref={floating}
|
||||
style={{
|
||||
position: strategy,
|
||||
top: y,
|
||||
left: x,
|
||||
}}
|
||||
>
|
||||
{actions.length ? (
|
||||
<>
|
||||
<h6 className="dropdown-header">Actions:</h6>
|
||||
{actions.map((action) => (
|
||||
<MenuLink
|
||||
key={action.name}
|
||||
icon={faWrench}
|
||||
text={action.name}
|
||||
uri={action.value}
|
||||
afterClick={afterClick}
|
||||
/>
|
||||
))}
|
||||
<div className="dropdown-divider" />
|
||||
</>
|
||||
) : null}
|
||||
return (
|
||||
<FetchPauser alertStore={alertStore}>
|
||||
<div
|
||||
className="dropdown-item cursor-pointer"
|
||||
onClick={() => {
|
||||
copy(groupLink);
|
||||
afterClick();
|
||||
className="dropdown-menu d-block shadow m-0"
|
||||
ref={floating}
|
||||
style={{
|
||||
position: strategy,
|
||||
top: y,
|
||||
left: x,
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faShareSquare} /> Copy link to this group
|
||||
</div>
|
||||
<div
|
||||
className={`dropdown-item ${
|
||||
Object.keys(alertStore.data.clustersWithoutReadOnly).length === 0
|
||||
? "disabled"
|
||||
: "cursor-pointer"
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (Object.keys(alertStore.data.clustersWithoutReadOnly).length) {
|
||||
onSilenceClick(alertStore, silenceFormStore, group);
|
||||
{actions.length ? (
|
||||
<>
|
||||
<h6 className="dropdown-header">Actions:</h6>
|
||||
{actions.map((action) => (
|
||||
<MenuLink
|
||||
key={action.name}
|
||||
icon={faWrench}
|
||||
text={action.name}
|
||||
uri={action.value}
|
||||
afterClick={afterClick}
|
||||
/>
|
||||
))}
|
||||
<div className="dropdown-divider" />
|
||||
</>
|
||||
) : null}
|
||||
<div
|
||||
className="dropdown-item cursor-pointer"
|
||||
onClick={() => {
|
||||
copy(groupLink);
|
||||
afterClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faBellSlash} /> Silence this group
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faShareSquare} /> Copy link to this group
|
||||
</div>
|
||||
<div
|
||||
className={`dropdown-item ${
|
||||
Object.keys(alertStore.data.clustersWithoutReadOnly).length === 0
|
||||
? "disabled"
|
||||
: "cursor-pointer"
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (Object.keys(alertStore.data.clustersWithoutReadOnly).length) {
|
||||
onSilenceClick(alertStore, silenceFormStore, group);
|
||||
afterClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faBellSlash} /> Silence this group
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FetchPauser>
|
||||
);
|
||||
};
|
||||
</FetchPauser>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const GroupMenu: FC<{
|
||||
group: APIAlertGroupT;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { FC, useEffect, useState, ReactNode } from "react";
|
||||
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faTrash } from "@fortawesome/free-solid-svg-icons/faTrash";
|
||||
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle";
|
||||
@@ -54,7 +56,7 @@ const DeleteResult: FC<{
|
||||
alertStore: AlertStore;
|
||||
cluster: string;
|
||||
id: string;
|
||||
}> = ({ alertStore, cluster, id }) => {
|
||||
}> = observer(({ alertStore, cluster, id }) => {
|
||||
const [currentTime, setCurrentTime] = useState<number>(
|
||||
Math.floor(Date.now()),
|
||||
);
|
||||
@@ -101,7 +103,7 @@ const DeleteResult: FC<{
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
const DeleteSilenceModalContent: FC<{
|
||||
alertStore: AlertStore;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { render, screen, fireEvent } from "@testing-library/react";
|
||||
|
||||
import { MockSilence } from "__fixtures__/Alerts";
|
||||
import { MockThemeContext } from "__fixtures__/Theme";
|
||||
import { inReactiveContext } from "__fixtures__/MobX";
|
||||
import type { APISilenceT } from "Models/APITypes";
|
||||
import { AlertStore } from "Stores/AlertStore";
|
||||
import { SilenceFormStore } from "Stores/SilenceFormStore";
|
||||
@@ -88,18 +89,20 @@ describe("<ManagedSilence />", () => {
|
||||
});
|
||||
|
||||
it("GetAlertmanager() returns alertmanager object from alertStore.data.upstreams.instances", () => {
|
||||
const am = GetAlertmanager(alertStore, cluster);
|
||||
expect(am).toEqual({
|
||||
name: "am1",
|
||||
cluster: "am",
|
||||
clusterMembers: ["am1"],
|
||||
uri: "http://localhost:9093",
|
||||
publicURI: "http://example.com",
|
||||
readonly: false,
|
||||
error: "",
|
||||
version: "0.24.0",
|
||||
headers: {},
|
||||
corsCredentials: "include",
|
||||
inReactiveContext(() => {
|
||||
const am = GetAlertmanager(alertStore, cluster);
|
||||
expect(am).toEqual({
|
||||
name: "am1",
|
||||
cluster: "am",
|
||||
clusterMembers: ["am1"],
|
||||
uri: "http://localhost:9093",
|
||||
publicURI: "http://example.com",
|
||||
readonly: false,
|
||||
error: "",
|
||||
version: "0.24.0",
|
||||
headers: {},
|
||||
corsCredentials: "include",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -135,18 +138,20 @@ describe("<ManagedSilence />", () => {
|
||||
clusters: { am: ["am1", "am2"] },
|
||||
});
|
||||
|
||||
const am = GetAlertmanager(alertStore, cluster);
|
||||
expect(am).toEqual({
|
||||
name: "am1",
|
||||
cluster: "am",
|
||||
clusterMembers: ["am1"],
|
||||
uri: "http://localhost:9093",
|
||||
publicURI: "http://example.com",
|
||||
readonly: false,
|
||||
error: "",
|
||||
version: "0.24.0",
|
||||
headers: {},
|
||||
corsCredentials: "include",
|
||||
inReactiveContext(() => {
|
||||
const am = GetAlertmanager(alertStore, cluster);
|
||||
expect(am).toEqual({
|
||||
name: "am1",
|
||||
cluster: "am",
|
||||
clusterMembers: ["am1"],
|
||||
uri: "http://localhost:9093",
|
||||
publicURI: "http://example.com",
|
||||
readonly: false,
|
||||
error: "",
|
||||
version: "0.24.0",
|
||||
headers: {},
|
||||
corsCredentials: "include",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FC, useEffect, useState } from "react";
|
||||
|
||||
import { action } from "mobx";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
import { parseISO } from "date-fns/parseISO";
|
||||
@@ -55,14 +56,14 @@ const ManagedSilence: FC<{
|
||||
|
||||
const [showDetails, setShowDetails] = useState<boolean>(isOpen);
|
||||
|
||||
const onEditSilence = () => {
|
||||
const onEditSilence = action(() => {
|
||||
const alertmanager = GetAlertmanager(alertStore, cluster);
|
||||
|
||||
silenceFormStore.data.fillFormFromSilence(alertmanager, silence);
|
||||
silenceFormStore.data.resetProgress();
|
||||
silenceFormStore.tab.setTab("editor");
|
||||
silenceFormStore.toggle.show();
|
||||
};
|
||||
});
|
||||
|
||||
const [progress, setProgress] = useState<number>(() =>
|
||||
calculatePercent(silence.startsAt, silence.endsAt),
|
||||
|
||||
@@ -7,6 +7,8 @@ import React, {
|
||||
useEffect,
|
||||
} from "react";
|
||||
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
import { useFloating, shift, flip, offset, size } from "@floating-ui/react-dom";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
@@ -195,10 +197,12 @@ const SilenceDeleteModalContent: FC<{
|
||||
export const MassDeleteProgress: FC<{
|
||||
alertStore: AlertStore;
|
||||
silences: ClusterSilenceT[];
|
||||
}> = ({ alertStore, silences }) => {
|
||||
}> = observer(({ alertStore, silences }) => {
|
||||
const [done, setDone] = useState(0);
|
||||
const [errors, setErrors] = useState<string[]>([]);
|
||||
|
||||
const readWriteAlertmanagers = alertStore.data.readWriteAlertmanagers;
|
||||
|
||||
useEffect(() => {
|
||||
const deleteSilence = async (
|
||||
cluster: string,
|
||||
@@ -237,7 +241,7 @@ export const MassDeleteProgress: FC<{
|
||||
|
||||
const timers: ReturnType<typeof setTimeout>[] = [];
|
||||
silences.forEach((silence, index) => {
|
||||
const ams = alertStore.data.readWriteAlertmanagers.filter(
|
||||
const ams = readWriteAlertmanagers.filter(
|
||||
(u) => u.cluster === silence.cluster,
|
||||
);
|
||||
// eslint-disable-next-line @eslint-react/web-api/no-leaked-timeout -- false positive: timers are collected and cleared in the useEffect cleanup below
|
||||
@@ -296,4 +300,4 @@ export const MassDeleteProgress: FC<{
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FC, useEffect, useState, MouseEvent, SyntheticEvent } from "react";
|
||||
|
||||
import { action } from "mobx";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
import copy from "copy-to-clipboard";
|
||||
@@ -177,7 +178,7 @@ const SilenceForm: FC<{
|
||||
silenceFormStore.data.setComment(comment);
|
||||
};
|
||||
|
||||
const handleSubmit = (event: SyntheticEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = action((event: SyntheticEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
const rbc: { [label: string]: ClusterRequestT } = {};
|
||||
@@ -192,7 +193,7 @@ const SilenceForm: FC<{
|
||||
silenceFormStore.data.setStage("preview");
|
||||
|
||||
silenceFormStore.data.setWasValidated(true);
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} autoComplete="on">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fetchMock from "@fetch-mock/jest";
|
||||
|
||||
import { EmptyAPIResponse } from "__fixtures__/Fetch";
|
||||
import { inReactiveContext } from "__fixtures__/MobX";
|
||||
import { MockGroup } from "__fixtures__/Stories";
|
||||
import {
|
||||
AlertStore,
|
||||
@@ -129,8 +130,10 @@ describe("AlertStore.data", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(store.data.clustersWithoutReadOnly).toEqual({
|
||||
default: ["am2", "am1"],
|
||||
inReactiveContext(() => {
|
||||
expect(store.data.clustersWithoutReadOnly).toEqual({
|
||||
default: ["am2", "am1"],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -178,8 +181,10 @@ describe("AlertStore.data", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(store.data.clustersWithoutReadOnly).toEqual({
|
||||
default: ["am1", "am2", "am3"],
|
||||
inReactiveContext(() => {
|
||||
expect(store.data.clustersWithoutReadOnly).toEqual({
|
||||
default: ["am1", "am2", "am3"],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
MockSilence,
|
||||
MockAlertmanager,
|
||||
} from "__fixtures__/Alerts";
|
||||
import { inReactiveContext } from "__fixtures__/MobX";
|
||||
import { StringToOption, OptionT, MultiValueOptionT } from "Common/Select";
|
||||
import {
|
||||
SilenceFormStore,
|
||||
@@ -826,14 +827,18 @@ describe("SilenceFormStore.data", () => {
|
||||
|
||||
it("toAlertmanagerPayload contains id when store.data.silenceID is set", () => {
|
||||
store.data.setSilenceID("12345");
|
||||
expect(store.data.toAlertmanagerPayload).toMatchObject({
|
||||
id: "12345",
|
||||
inReactiveContext(() => {
|
||||
expect(store.data.toAlertmanagerPayload).toMatchObject({
|
||||
id: "12345",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("toAlertmanagerPayload doesn't contain id when store.data.silenceID is null", () => {
|
||||
store.data.setSilenceID(null);
|
||||
expect(store.data.toAlertmanagerPayload.id).toBeUndefined();
|
||||
inReactiveContext(() => {
|
||||
expect(store.data.toAlertmanagerPayload.id).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("toAlertmanagerPayload creates payload that matches snapshot", () => {
|
||||
@@ -851,7 +856,9 @@ describe("SilenceFormStore.data", () => {
|
||||
store.data.setEnd(new Date(Date.UTC(2000, 1, 1, 1, 0, 0)));
|
||||
store.data.setAuthor("me@example.com");
|
||||
store.data.setComment("toAlertmanagerPayload test");
|
||||
expect(store.data.toAlertmanagerPayload).toMatchSnapshot();
|
||||
inReactiveContext(() => {
|
||||
expect(store.data.toAlertmanagerPayload).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it("toAlertmanagerPayload creates payload that matches snapshot with regex values", () => {
|
||||
@@ -957,7 +964,9 @@ describe("SilenceFormStore.data", () => {
|
||||
store.data.setEnd(new Date(Date.UTC(2000, 1, 1, 1, 0, 0)));
|
||||
store.data.setAuthor("me@example.com");
|
||||
store.data.setComment("toAlertmanagerPayload test");
|
||||
expect(store.data.toAlertmanagerPayload).toMatchSnapshot();
|
||||
inReactiveContext(() => {
|
||||
expect(store.data.toAlertmanagerPayload).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it("dumps to base64 and back", () => {
|
||||
@@ -970,7 +979,7 @@ describe("SilenceFormStore.data", () => {
|
||||
store.data.setStart(new Date());
|
||||
store.data.setEnd(addMinutes(addHours(store.data.startsAt, 7), 45));
|
||||
store.data.setComment("base64");
|
||||
const b64 = store.data.toBase64;
|
||||
const b64 = inReactiveContext(() => store.data.toBase64);
|
||||
|
||||
store.data.setMatchers([]);
|
||||
store.data.setComment("");
|
||||
@@ -1007,7 +1016,7 @@ describe("SilenceFormStore.data", () => {
|
||||
it("base64 restore ignores empty matchers", () => {
|
||||
store.data.setMatchers([]);
|
||||
store.data.setComment("base64");
|
||||
const b64 = store.data.toBase64;
|
||||
const b64 = inReactiveContext(() => store.data.toBase64);
|
||||
|
||||
store.data.setMatchers([]);
|
||||
store.data.setComment("foo");
|
||||
@@ -1048,7 +1057,9 @@ describe("SilenceFormStore.data.isValid", () => {
|
||||
]);
|
||||
store.data.setAuthor("me@example.com");
|
||||
store.data.setComment("fake silence");
|
||||
expect(store.data.isValid).toBe(false);
|
||||
inReactiveContext(() => {
|
||||
expect(store.data.isValid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("isValid returns 'false' if matchers list is empty", () => {
|
||||
@@ -1056,7 +1067,9 @@ describe("SilenceFormStore.data.isValid", () => {
|
||||
store.data.setMatchers([]);
|
||||
store.data.setAuthor("me@example.com");
|
||||
store.data.setComment("fake silence");
|
||||
expect(store.data.isValid).toBe(false);
|
||||
inReactiveContext(() => {
|
||||
expect(store.data.isValid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("isValid returns 'false' if matchers list is populated when a matcher without any name", () => {
|
||||
@@ -1066,7 +1079,9 @@ describe("SilenceFormStore.data.isValid", () => {
|
||||
]);
|
||||
store.data.setAuthor("me@example.com");
|
||||
store.data.setComment("fake silence");
|
||||
expect(store.data.isValid).toBe(false);
|
||||
inReactiveContext(() => {
|
||||
expect(store.data.isValid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("isValid returns 'false' if matchers list is populated when a matcher without any value ([])", () => {
|
||||
@@ -1074,7 +1089,9 @@ describe("SilenceFormStore.data.isValid", () => {
|
||||
store.data.setMatchers([MockMatcher("foo", [])]);
|
||||
store.data.setAuthor("me@example.com");
|
||||
store.data.setComment("fake silence");
|
||||
expect(store.data.isValid).toBe(false);
|
||||
inReactiveContext(() => {
|
||||
expect(store.data.isValid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("isValid returns 'false' if matchers list is populated when a matcher with empty value ([''])", () => {
|
||||
@@ -1082,7 +1099,9 @@ describe("SilenceFormStore.data.isValid", () => {
|
||||
store.data.setMatchers([MockMatcher("foo", [])]);
|
||||
store.data.setAuthor("me@example.com");
|
||||
store.data.setComment("fake silence");
|
||||
expect(store.data.isValid).toBe(false);
|
||||
inReactiveContext(() => {
|
||||
expect(store.data.isValid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("isValid returns 'false' if author is empty", () => {
|
||||
@@ -1092,7 +1111,9 @@ describe("SilenceFormStore.data.isValid", () => {
|
||||
]);
|
||||
store.data.setAuthor("");
|
||||
store.data.setComment("fake silence");
|
||||
expect(store.data.isValid).toBe(false);
|
||||
inReactiveContext(() => {
|
||||
expect(store.data.isValid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("isValid returns 'false' if comment is empty", () => {
|
||||
@@ -1102,7 +1123,9 @@ describe("SilenceFormStore.data.isValid", () => {
|
||||
]);
|
||||
store.data.setAuthor("me@example.com");
|
||||
store.data.setComment("");
|
||||
expect(store.data.isValid).toBe(false);
|
||||
inReactiveContext(() => {
|
||||
expect(store.data.isValid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("isValid returns 'true' if all fields are set", () => {
|
||||
@@ -1112,7 +1135,9 @@ describe("SilenceFormStore.data.isValid", () => {
|
||||
]);
|
||||
store.data.setAuthor("me@example.com");
|
||||
store.data.setComment("fake silence");
|
||||
expect(store.data.isValid).toBe(true);
|
||||
inReactiveContext(() => {
|
||||
expect(store.data.isValid).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1120,30 +1145,36 @@ describe("SilenceFormStore.data startsAt & endsAt validation", () => {
|
||||
it("toDuration returns correct duration for 5d 0h 1m", () => {
|
||||
store.data.setStart(new Date(2000, 1, 1, 0, 0, 0));
|
||||
store.data.setEnd(new Date(2000, 1, 6, 0, 1, 15));
|
||||
expect(store.data.toDuration).toMatchObject({
|
||||
days: 5,
|
||||
hours: 0,
|
||||
minutes: 1,
|
||||
inReactiveContext(() => {
|
||||
expect(store.data.toDuration).toMatchObject({
|
||||
days: 5,
|
||||
hours: 0,
|
||||
minutes: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("toDuration returns correct duration for 2h 15m", () => {
|
||||
store.data.setStart(new Date(2000, 1, 1, 0, 0, 0));
|
||||
store.data.setEnd(new Date(2000, 1, 1, 2, 15, 0));
|
||||
expect(store.data.toDuration).toMatchObject({
|
||||
days: 0,
|
||||
hours: 2,
|
||||
minutes: 15,
|
||||
inReactiveContext(() => {
|
||||
expect(store.data.toDuration).toMatchObject({
|
||||
days: 0,
|
||||
hours: 2,
|
||||
minutes: 15,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("toDuration returns correct duration for 59m", () => {
|
||||
store.data.setStart(new Date(2000, 1, 1, 0, 10, 0));
|
||||
store.data.setEnd(new Date(2000, 1, 1, 1, 9, 0));
|
||||
expect(store.data.toDuration).toMatchObject({
|
||||
days: 0,
|
||||
hours: 0,
|
||||
minutes: 59,
|
||||
inReactiveContext(() => {
|
||||
expect(store.data.toDuration).toMatchObject({
|
||||
days: 0,
|
||||
hours: 0,
|
||||
minutes: 59,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
11
ui/src/__fixtures__/MobX.ts
Normal file
11
ui/src/__fixtures__/MobX.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { autorun } from "mobx";
|
||||
|
||||
function inReactiveContext<T>(fn: () => T): T {
|
||||
let result: T;
|
||||
autorun(() => {
|
||||
result = fn();
|
||||
})();
|
||||
return result!;
|
||||
}
|
||||
|
||||
export { inReactiveContext };
|
||||
@@ -23,7 +23,7 @@ fetchMock.mockGlobal();
|
||||
|
||||
configure({
|
||||
enforceActions: "observed",
|
||||
// computedRequiresReaction: true,
|
||||
computedRequiresReaction: true,
|
||||
// reactionRequiresObservable: true,
|
||||
// observableRequiresReaction: true,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user