fix(ui): migrate to mobx-react-lite v3

This commit is contained in:
Łukasz Mierzwa
2020-10-02 17:28:34 +01:00
committed by Łukasz Mierzwa
parent 84fc3e2fe4
commit fd155db464
71 changed files with 928 additions and 915 deletions
+100 -111
View File
@@ -5,7 +5,7 @@ import React, {
useCallback,
} from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
// no types, see react-app-env.d.ts
import { useMediaPredicate } from "react-media-hook";
@@ -25,130 +25,119 @@ import "Styles/ResetCSS.scss";
import "Styles/FontBundle.scss";
import "Styles/App.scss";
// https://github.com/facebook/react/issues/14603
const Grid = React.lazy(() =>
import("Components/Grid").then((module) => ({
default: module.Grid,
}))
);
const NavBar = React.lazy(() =>
import("Components/NavBar").then((module) => ({
default: module.NavBar,
}))
);
const FaviconBadge = React.lazy(() =>
import("Components/FaviconBadge").then((module) => ({
default: module.FaviconBadge,
}))
);
const AppToasts = React.lazy(() =>
import("Components/Toast/AppToasts").then((module) => ({
default: module.AppToasts,
}))
);
const Grid = React.lazy(() => import("Components/Grid"));
const NavBar = React.lazy(() => import("Components/NavBar"));
const FaviconBadge = React.lazy(() => import("Components/FaviconBadge"));
const AppToasts = React.lazy(() => import("Components/Toast/AppToasts"));
interface AppProps {
defaultFilters: Array<string>;
uiDefaults: UIDefaults | null;
}
const App: FunctionComponent<AppProps> = ({ defaultFilters, uiDefaults }) => {
const [alertStore] = useState<AlertStore>(new AlertStore(null));
const [silenceFormStore] = useState<SilenceFormStore>(new SilenceFormStore());
const [settingsStore] = useState<Settings>(new Settings(uiDefaults));
const App: FunctionComponent<AppProps> = observer(
({ defaultFilters, uiDefaults }) => {
const [alertStore] = useState<AlertStore>(new AlertStore(null));
const [silenceFormStore] = useState<SilenceFormStore>(
new SilenceFormStore()
);
const [settingsStore] = useState<Settings>(new Settings(uiDefaults));
useEffect(() => {
let filters;
// parse and decode request query args
const p: {
params: {
q: string[];
m?: string;
};
defaultsUsed: boolean;
} = DecodeLocationSearch(window.location.search);
// p.defaultsUsed means that karma URI didn't have ?q=foo query args
if (p.defaultsUsed) {
// no ?q=foo set, use defaults saved by the user or from backend config
if (settingsStore.savedFilters.config.present) {
filters = settingsStore.savedFilters.config.filters;
useEffect(() => {
let filters;
// parse and decode request query args
const p: {
params: {
q: string[];
m?: string;
};
defaultsUsed: boolean;
} = DecodeLocationSearch(window.location.search);
// p.defaultsUsed means that karma URI didn't have ?q=foo query args
if (p.defaultsUsed) {
// no ?q=foo set, use defaults saved by the user or from backend config
if (settingsStore.savedFilters.config.present) {
filters = settingsStore.savedFilters.config.filters;
} else {
filters = defaultFilters;
}
} else {
filters = defaultFilters;
// user passed ?q=foo, use it as initial filters
filters = p.params.q;
}
} else {
// user passed ?q=foo, use it as initial filters
filters = p.params.q;
}
alertStore.filters.setFilters(filters);
alertStore.filters.setFilters(filters);
if (p.params.m && silenceFormStore.data.fromBase64(p.params.m)) {
silenceFormStore.toggle.show();
}
}, [alertStore, defaultFilters, settingsStore]); // eslint-disable-line react-hooks/exhaustive-deps
if (p.params.m && silenceFormStore.data.fromBase64(p.params.m)) {
silenceFormStore.toggle.show();
}
}, [alertStore, defaultFilters, settingsStore]); // eslint-disable-line react-hooks/exhaustive-deps
const onPopState = useCallback(
(event: PopStateEvent) => {
event.preventDefault();
const p = DecodeLocationSearch(window.location.search);
alertStore.filters.setWithoutLocation(p.params.q);
},
[alertStore]
);
const onPopState = useCallback(
(event: PopStateEvent) => {
event.preventDefault();
const p = DecodeLocationSearch(window.location.search);
alertStore.filters.setWithoutLocation(p.params.q);
},
[alertStore]
);
useEffect(() => {
window.onpopstate = onPopState;
return () => {
window.onpopstate = () => {};
};
}, [onPopState]);
useEffect(() => {
window.onpopstate = onPopState;
return () => {
window.onpopstate = () => {};
};
}, [onPopState]);
const prefersColorScheme = useMediaPredicate("(prefers-color-scheme)");
const prefersDark = useMediaPredicate("(prefers-color-scheme: dark)");
const prefersColorScheme = useMediaPredicate("(prefers-color-scheme)");
const prefersDark = useMediaPredicate("(prefers-color-scheme: dark)");
return useObserver(() => (
<ErrorBoundary>
<span data-theme={`${settingsStore.themeConfig.config.theme}`} />
<ThemeContext.Provider
value={{
isDark:
settingsStore.themeConfig.config.theme ===
settingsStore.themeConfig.options.auto.value && prefersColorScheme
? prefersDark
: settingsStore.themeConfig.config.theme ===
settingsStore.themeConfig.options.dark.value,
reactSelectStyles:
settingsStore.themeConfig.config.theme ===
settingsStore.themeConfig.options.auto.value && prefersColorScheme
? prefersDark
return (
<ErrorBoundary>
<span data-theme={`${settingsStore.themeConfig.config.theme}`} />
<ThemeContext.Provider
value={{
isDark:
settingsStore.themeConfig.config.theme ===
settingsStore.themeConfig.options.auto.value &&
prefersColorScheme
? prefersDark
: settingsStore.themeConfig.config.theme ===
settingsStore.themeConfig.options.dark.value,
reactSelectStyles:
settingsStore.themeConfig.config.theme ===
settingsStore.themeConfig.options.auto.value &&
prefersColorScheme
? prefersDark
? ReactSelectStyles(ReactSelectColors.Dark)
: ReactSelectStyles(ReactSelectColors.Light)
: settingsStore.themeConfig.config.theme ===
settingsStore.themeConfig.options.dark.value
? ReactSelectStyles(ReactSelectColors.Dark)
: ReactSelectStyles(ReactSelectColors.Light)
: settingsStore.themeConfig.config.theme ===
settingsStore.themeConfig.options.dark.value
? ReactSelectStyles(ReactSelectColors.Dark)
: ReactSelectStyles(ReactSelectColors.Light),
animations: {
duration: 500,
},
}}
>
<BodyTheme />
<React.Suspense fallback={null}>
<NavBar
alertStore={alertStore}
settingsStore={settingsStore}
silenceFormStore={silenceFormStore}
/>
<Grid
alertStore={alertStore}
settingsStore={settingsStore}
silenceFormStore={silenceFormStore}
/>
<AppToasts alertStore={alertStore} />
<FaviconBadge alertStore={alertStore} />
</React.Suspense>
</ThemeContext.Provider>
</ErrorBoundary>
));
};
: ReactSelectStyles(ReactSelectColors.Light),
animations: {
duration: 500,
},
}}
>
<BodyTheme />
<React.Suspense fallback={null}>
<NavBar
alertStore={alertStore}
settingsStore={settingsStore}
silenceFormStore={silenceFormStore}
/>
<Grid
alertStore={alertStore}
settingsStore={settingsStore}
silenceFormStore={silenceFormStore}
/>
<AppToasts alertStore={alertStore} />
<FaviconBadge alertStore={alertStore} />
</React.Suspense>
</ThemeContext.Provider>
</ErrorBoundary>
);
}
);
export { App };
+1 -1
View File
@@ -103,7 +103,7 @@ describe("<AlertAck />", () => {
it("is null when acks are disabled", () => {
alertStore.settings.values.alertAcknowledgement.enabled = false;
const tree = MountedAlertAck();
expect(tree.html()).toBeNull();
expect(tree.html()).toBe("");
});
it("uses faCheck icon when idle", () => {
+36 -37
View File
@@ -1,7 +1,7 @@
import React, { FC, useEffect, useState } from "react";
import { toJS } from "mobx";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import addSeconds from "date-fns/addSeconds";
@@ -35,7 +35,7 @@ const AlertAck: FC<{
alertStore: AlertStore;
silenceFormStore: SilenceFormStore;
group: APIAlertGroupT;
}> = ({ alertStore, silenceFormStore, group }) => {
}> = observer(({ alertStore, silenceFormStore, group }) => {
const [clusters, setClusters] = useState<ClusterT[]>([]);
const [upstreams, setUpstreams] = useState<UpstreamT[]>([]);
const [currentCluster, setCurrentCluster] = useState<number>(0);
@@ -145,43 +145,42 @@ const AlertAck: FC<{
return () => clearTimeout(timer);
}, [isAcking, error, reset]);
return useObserver(() =>
alertStore.settings.values.alertAcknowledgement.enabled === false ? null : (
<TooltipWrapper
title={
return alertStore.settings.values.alertAcknowledgement.enabled ===
false ? null : (
<TooltipWrapper
title={
!isAcking && error
? error
: "Acknowledge this alert with a short lived silence"
}
>
<span
className={`badge badge-pill components-label components-label-with-hover px-2 ${
!isAcking && error
? error
: "Acknowledge this alert with a short lived silence"
}
? "badge-warning"
: !isAcking && response
? "badge-success"
: "badge-secondary"
}`}
onClick={() => {
if (!isAcking && !(response || error)) {
setIsAcking(true);
onACK();
}
}}
>
<span
className={`badge badge-pill components-label components-label-with-hover px-2 ${
!isAcking && error
? "badge-warning"
: !isAcking && response
? "badge-success"
: "badge-secondary"
}`}
onClick={() => {
if (!isAcking && !(response || error)) {
setIsAcking(true);
onACK();
}
}}
>
{!isAcking && error ? (
<FontAwesomeIcon icon={faExclamationCircle} fixedWidth />
) : !isAcking && response ? (
<FontAwesomeIcon icon={faCheckCircle} fixedWidth />
) : isAcking ? (
<FontAwesomeIcon icon={faSpinner} fixedWidth spin />
) : (
<FontAwesomeIcon icon={faCheck} fixedWidth />
)}
</span>
</TooltipWrapper>
)
{!isAcking && error ? (
<FontAwesomeIcon icon={faExclamationCircle} fixedWidth />
) : !isAcking && response ? (
<FontAwesomeIcon icon={faCheckCircle} fixedWidth />
) : isAcking ? (
<FontAwesomeIcon icon={faSpinner} fixedWidth spin />
) : (
<FontAwesomeIcon icon={faCheck} fixedWidth />
)}
</span>
</TooltipWrapper>
);
};
});
export { AlertAck };
@@ -5,7 +5,7 @@ import { mount } from "enzyme";
import Favico from "favico.js";
import { AlertStore } from "Stores/AlertStore";
import { FaviconBadge } from ".";
import FaviconBadge from ".";
let alertStore: AlertStore;
+1 -1
View File
@@ -31,4 +31,4 @@ const FaviconBadge: FC<{
return null;
};
export { FaviconBadge };
export default FaviconBadge;
+36 -37
View File
@@ -1,7 +1,7 @@
import React, { useEffect, useRef, useState, FC } from "react";
import { reaction } from "mobx";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import addSeconds from "date-fns/addSeconds";
import differenceInSeconds from "date-fns/differenceInSeconds";
@@ -59,43 +59,42 @@ const PlayButton: FC<{ alertStore: AlertStore }> = ({ alertStore }) => {
);
};
const Dots: FC<{ alertStore: AlertStore; dots: number }> = ({
alertStore,
dots,
}) => {
return useObserver(() => (
<div
className={`cursor-pointer components-fetcher ${
alertStore.info.isRetrying ? "retrying" : ""
} ${
alertStore.status.value.toString() ===
AlertStoreStatuses.Processing.toString()
? "processing"
: ""
} ${
dots === 0 ||
alertStore.status.value.toString() ===
AlertStoreStatuses.Fetching.toString()
? "fetching"
: ""
}`}
>
{Array.from(Array(9).keys()).map((i) => (
<div
key={i}
className={`dot ${i === 4 ? "dot-middle" : ""} ${
i < dots ? "visible" : "hidden"
}`}
></div>
))}
</div>
));
};
const Dots: FC<{ alertStore: AlertStore; dots: number }> = observer(
({ alertStore, dots }) => {
return (
<div
className={`cursor-pointer components-fetcher ${
alertStore.info.isRetrying ? "retrying" : ""
} ${
alertStore.status.value.toString() ===
AlertStoreStatuses.Processing.toString()
? "processing"
: ""
} ${
dots === 0 ||
alertStore.status.value.toString() ===
AlertStoreStatuses.Fetching.toString()
? "fetching"
: ""
}`}
>
{Array.from(Array(9).keys()).map((i) => (
<div
key={i}
className={`dot ${i === 4 ? "dot-middle" : ""} ${
i < dots ? "visible" : "hidden"
}`}
></div>
))}
</div>
);
}
);
const Fetcher: FC<{
alertStore: AlertStore;
settingsStore: Settings;
}> = ({ alertStore, settingsStore }) => {
}> = observer(({ alertStore, settingsStore }) => {
const timer = useRef<number | undefined>(undefined);
const [percentLeft, setPercentLeft] = useState<number>(100);
const [isHover, setIsHover] = useState(false);
@@ -233,7 +232,7 @@ const Fetcher: FC<{
const dots = Math.max(0, Math.min(9, percentLeft / 10));
return useObserver(() => (
return (
<div
className="navbar-brand py-0 mr-2 d-none d-sm-block"
onMouseEnter={() => setIsHover(true)}
@@ -247,7 +246,7 @@ const Fetcher: FC<{
<Dots alertStore={alertStore} dots={dots} />
)}
</div>
));
};
);
});
export { Fetcher, Dots, PlayButton, PauseButton };
@@ -7,7 +7,7 @@ import React, {
useCallback,
} from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import { Manager, Reference, Popper } from "react-popper";
@@ -128,61 +128,63 @@ const AlertMenu: FC<{
alertStore: AlertStore;
silenceFormStore: SilenceFormStore;
setIsMenuOpen: (isOpen: boolean) => void;
}> = ({ group, alert, alertStore, silenceFormStore, setIsMenuOpen }) => {
const [isHidden, setIsHidden] = useState<boolean>(true);
}> = observer(
({ group, alert, alertStore, silenceFormStore, setIsMenuOpen }) => {
const [isHidden, setIsHidden] = useState<boolean>(true);
const toggle = useCallback(() => {
setIsMenuOpen(isHidden);
setIsHidden(!isHidden);
}, [isHidden, setIsMenuOpen]);
const toggle = useCallback(() => {
setIsMenuOpen(isHidden);
setIsHidden(!isHidden);
}, [isHidden, setIsMenuOpen]);
const hide = useCallback(() => {
setIsHidden(true);
setIsMenuOpen(false);
}, [setIsMenuOpen]);
const hide = useCallback(() => {
setIsHidden(true);
setIsMenuOpen(false);
}, [setIsMenuOpen]);
const rootRef = useRef<HTMLSpanElement | null>(null);
useOnClickOutside(rootRef, hide, !isHidden);
const rootRef = useRef<HTMLSpanElement | null>(null);
useOnClickOutside(rootRef, hide, !isHidden);
return useObserver(() => (
<span ref={rootRef}>
<Manager>
<Reference>
{({ ref }) => (
<span
className="components-label components-label-with-hover px-1 mr-1 badge badge-secondary cursor-pointer"
ref={ref}
onClick={toggle}
data-toggle="dropdown"
>
<FontAwesomeIcon
className="pr-1"
style={{ width: "0.8rem" }}
icon={faCaretDown}
/>
<DateFromNow timestamp={alert.startsAt} />
</span>
)}
</Reference>
<DropdownSlide in={!isHidden} unmountOnExit>
<Popper placement="bottom-start" modifiers={PopperModifiers}>
{({ placement, ref, style }) => (
<MenuContent
popperPlacement={placement}
popperRef={ref}
popperStyle={style}
group={group}
alert={alert}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
afterClick={hide}
/>
return (
<span ref={rootRef}>
<Manager>
<Reference>
{({ ref }) => (
<span
className="components-label components-label-with-hover px-1 mr-1 badge badge-secondary cursor-pointer"
ref={ref}
onClick={toggle}
data-toggle="dropdown"
>
<FontAwesomeIcon
className="pr-1"
style={{ width: "0.8rem" }}
icon={faCaretDown}
/>
<DateFromNow timestamp={alert.startsAt} />
</span>
)}
</Popper>
</DropdownSlide>
</Manager>
</span>
));
};
</Reference>
<DropdownSlide in={!isHidden} unmountOnExit>
<Popper placement="bottom-start" modifiers={PopperModifiers}>
{({ placement, ref, style }) => (
<MenuContent
popperPlacement={placement}
popperRef={ref}
popperStyle={style}
group={group}
alert={alert}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
afterClick={hide}
/>
)}
</Popper>
</DropdownSlide>
</Manager>
</span>
);
}
);
export { AlertMenu, MenuContent };
@@ -19,7 +19,7 @@ import { AlertStore } from "Stores/AlertStore";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { BorderClassMap } from "Common/Colors";
import { ThemeContext } from "Components/Theme";
import { Alert } from ".";
import Alert from ".";
let alertStore: AlertStore;
let silenceFormStore: SilenceFormStore;
@@ -1,6 +1,6 @@
import React, { FC } from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import {
APIAlertT,
@@ -11,7 +11,7 @@ import { AlertStore } from "Stores/AlertStore";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { BorderClassMap } from "Common/Colors";
import { StaticLabels } from "Common/Query";
import { FilteringLabel } from "Components/Labels/FilteringLabel";
import FilteringLabel from "Components/Labels/FilteringLabel";
import { InhibitedByModal } from "Components/InhibitedByModal";
import { RenderNonLinkAnnotation, RenderLinkAnnotation } from "../Annotation";
import { AlertMenu } from "./AlertMenu";
@@ -81,7 +81,7 @@ const Alert: FC<{
}
}
return useObserver(() => (
return (
<li className={classNames.join(" ")}>
<div>
{alert.annotations
@@ -149,7 +149,7 @@ const Alert: FC<{
))
)}
</li>
));
);
};
export { Alert };
export default observer(Alert);
@@ -17,7 +17,7 @@ import { MockThemeContext } from "__mocks__/Theme";
import { AlertStore } from "Stores/AlertStore";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { ThemeContext } from "Components/Theme";
import { GroupFooter } from ".";
import GroupFooter from ".";
let group: APIAlertGroupT;
let alertStore: AlertStore;
@@ -1,12 +1,12 @@
import React, { FC } from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import { APIAlertGroupT } from "Models/APITypes";
import { StaticLabels } from "Common/Query";
import { AlertStore } from "Stores/AlertStore";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { FilteringLabel } from "Components/Labels/FilteringLabel";
import FilteringLabel from "Components/Labels/FilteringLabel";
import { RenderNonLinkAnnotation, RenderLinkAnnotation } from "../Annotation";
import { RenderSilence } from "../Silences";
@@ -17,7 +17,7 @@ const GroupFooter: FC<{
alertStore: AlertStore;
silenceFormStore: SilenceFormStore;
}> = ({ group, alertmanagers, afterUpdate, alertStore, silenceFormStore }) => {
return useObserver(() => (
return (
<div className="card-footer components-grid-alertgrid-alertgroup-footer px-2 py-1">
<div className="mb-1">
{group.shared.annotations
@@ -77,7 +77,7 @@ const GroupFooter: FC<{
</div>
)}
</div>
));
);
};
export { GroupFooter };
export default observer(GroupFooter);
@@ -1,12 +1,12 @@
import React, { FC, MouseEvent } from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import { APIAlertGroupT } from "Models/APITypes";
import { AlertStore } from "Stores/AlertStore";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { FilteringLabel } from "Components/Labels/FilteringLabel";
import { FilteringCounterBadge } from "Components/Labels/FilteringCounterBadge";
import FilteringLabel from "Components/Labels/FilteringLabel";
import FilteringCounterBadge from "Components/Labels/FilteringCounterBadge";
import { TooltipWrapper } from "Components/TooltipWrapper";
import { AlertAck } from "Components/AlertAck";
import { ToggleIcon } from "Components/ToggleIcon";
@@ -47,7 +47,7 @@ const GroupHeader: FC<{
}
};
return useObserver(() => (
return (
<h5
className={`card-header mb-0 d-flex flex-row p-1 ${
isCollapsed ? "border-bottom-0" : ""
@@ -113,7 +113,7 @@ const GroupHeader: FC<{
</span>
</span>
</h5>
));
);
};
export { GroupHeader };
export default observer(GroupHeader);
@@ -10,7 +10,7 @@ import { AlertStore } from "Stores/AlertStore";
import { Settings, CollapseStateT } from "Stores/Settings";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { ThemeContext } from "Components/Theme";
import { AlertGroup } from ".";
import AlertGroup from ".";
let alertStore: AlertStore;
let settingsStore: Settings;
@@ -1,6 +1,6 @@
import React, { FC, useEffect, useCallback, useState, ReactNode } from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -13,9 +13,9 @@ import { AlertStore } from "Stores/AlertStore";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { BackgroundClassMap } from "Common/Colors";
import { TooltipWrapper } from "Components/TooltipWrapper";
import { GroupHeader } from "./GroupHeader";
import { Alert } from "./Alert";
import { GroupFooter } from "./GroupFooter";
import GroupHeader from "./GroupHeader";
import Alert from "./Alert";
import GroupFooter from "./GroupFooter";
import { DefaultDetailsCollapseValue } from "./DetailsToggle";
const LoadButton: FC<{
@@ -168,7 +168,7 @@ const AlertGroup: FC<{
}
}
return useObserver(() => (
return (
<div
className="components-grid-alertgrid-alertgroup"
style={{
@@ -247,7 +247,7 @@ const AlertGroup: FC<{
) : null}
</div>
</div>
));
);
};
export { AlertGroup };
export default observer(AlertGroup);
+5 -5
View File
@@ -1,6 +1,6 @@
import React, { FC, useEffect, useState, useCallback, MouseEvent } from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import debounce from "lodash.debounce";
@@ -21,7 +21,7 @@ import { APIGridT } from "Models/APITypes";
import { useGrid } from "Hooks/useGrid";
import { ThemeContext } from "Components/Theme";
import { DefaultDetailsCollapseValue } from "./AlertGroup/DetailsToggle";
import { AlertGroup } from "./AlertGroup";
import AlertGroup from "./AlertGroup";
import { Swimlane } from "./Swimlane";
const Grid: FC<{
@@ -108,7 +108,7 @@ const Grid: FC<{
repack();
});
return useObserver(() => (
return (
<React.Fragment>
<CSSTransition
key={grid.labelValue}
@@ -189,7 +189,7 @@ const Grid: FC<{
)}
</TransitionGroup>
</React.Fragment>
));
);
};
export { Grid };
export default observer(Grid);
@@ -5,8 +5,8 @@ import { faTh } from "@fortawesome/free-solid-svg-icons/faTh";
import { AlertStore } from "Stores/AlertStore";
import { APIGridT } from "Models/APITypes";
import { FilteringLabel } from "Components/Labels/FilteringLabel";
import { FilteringCounterBadge } from "Components/Labels/FilteringCounterBadge";
import FilteringLabel from "Components/Labels/FilteringLabel";
import FilteringCounterBadge from "Components/Labels/FilteringCounterBadge";
import { TooltipWrapper } from "Components/TooltipWrapper";
import { ToggleIcon } from "Components/ToggleIcon";
@@ -13,8 +13,8 @@ import { Settings } from "Stores/Settings";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { ThemeContext } from "Components/Theme";
import { GetGridElementWidth, GridSizesConfig } from "./GridSize";
import { Grid } from "./Grid";
import { AlertGrid } from ".";
import Grid from "./Grid";
import AlertGrid from ".";
let alertStore: AlertStore;
let settingsStore: Settings;
+5 -5
View File
@@ -1,7 +1,7 @@
import React, { FC, Ref, useEffect, useState } from "react";
import { autorun } from "mobx";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import useDimensions from "react-cool-dimensions";
@@ -13,7 +13,7 @@ import { AlertStore } from "Stores/AlertStore";
import { Settings } from "Stores/Settings";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { useWindowSize } from "Hooks/useWindowSize";
import { Grid } from "./Grid";
import Grid from "./Grid";
import { GridSizesConfig, GetGridElementWidth } from "./GridSize";
const AlertGrid: FC<{
@@ -56,7 +56,7 @@ const AlertGrid: FC<{
useHotkeys("alt+space", alertStore.status.togglePause);
return useObserver(() => (
return (
<React.Fragment>
<div ref={ref as Ref<HTMLDivElement>} />
{alertStore.data.grids.map((grid) => (
@@ -72,7 +72,7 @@ const AlertGrid: FC<{
/>
))}
</React.Fragment>
));
);
};
export { AlertGrid };
export default observer(AlertGrid);
+1 -1
View File
@@ -10,7 +10,7 @@ import { FatalError } from "./FatalError";
import { UpgradeNeeded } from "./UpgradeNeeded";
import { ReloadNeeded } from "./ReloadNeeded";
import { EmptyGrid } from "./EmptyGrid";
import { Grid } from ".";
import Grid from ".";
import { InternalError } from "../../ErrorBoundary";
import "Styles/Percy.scss";
+4 -4
View File
@@ -5,7 +5,7 @@ import { shallow } from "enzyme";
import { AlertStore } from "Stores/AlertStore";
import { Settings } from "Stores/Settings";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { Grid } from ".";
import Grid from ".";
let alertStore: AlertStore;
let settingsStore: Settings;
@@ -40,7 +40,7 @@ const ShallowGrid = () => {
describe("<Grid />", () => {
it("renders only AlertGrid when all upstreams are healthy", () => {
const tree = ShallowGrid();
expect(tree.text()).toBe("<AlertGrid />");
expect(tree.find("AlertGrid")).toHaveLength(1);
});
it("renders FatalError if there's only one upstream and it's unhealthy", () => {
@@ -129,7 +129,7 @@ describe("<Grid />", () => {
alertStore.info.version = "unknown";
alertStore.info.setTotalAlerts(0);
const tree = ShallowGrid();
expect(tree.text()).toBe("<AlertGrid />");
expect(tree.find("AlertGrid")).toHaveLength(1);
});
it("renders EmptyGrid after first fetch when totalAlerts is 0", () => {
@@ -143,7 +143,7 @@ describe("<Grid />", () => {
alertStore.info.version = "unknown";
alertStore.info.setTotalAlerts(1);
const tree = ShallowGrid();
expect(tree.text()).toBe("<AlertGrid />");
expect(tree.find("AlertGrid")).toHaveLength(1);
});
it("unmounts without crashes", () => {
+24 -26
View File
@@ -1,11 +1,11 @@
import React, { FC } from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import { AlertStore } from "Stores/AlertStore";
import { Settings } from "Stores/Settings";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { AlertGrid } from "./AlertGrid";
import AlertGrid from "./AlertGrid";
import { FatalError } from "./FatalError";
import { UpgradeNeeded } from "./UpgradeNeeded";
import { ReloadNeeded } from "./ReloadNeeded";
@@ -16,30 +16,28 @@ const Grid: FC<{
silenceFormStore: SilenceFormStore;
settingsStore: Settings;
}> = ({ alertStore, settingsStore, silenceFormStore }) => {
return useObserver(() =>
alertStore.info.upgradeNeeded ? (
<UpgradeNeeded newVersion={alertStore.info.version} reloadAfter={3000} />
) : alertStore.info.reloadNeeded ? (
<ReloadNeeded reloadAfter={4000} />
) : alertStore.status.error ? (
<FatalError message={alertStore.status.error} />
) : alertStore.data.upstreams.counters &&
alertStore.data.upstreams.counters.total === 1 &&
alertStore.data.upstreams.counters.healthy === 0 &&
alertStore.data.upstreams.instances[0] &&
alertStore.data.upstreams.instances[0].error !== "" ? (
<FatalError message={alertStore.data.upstreams.instances[0].error} />
) : alertStore.info.version !== "unknown" &&
alertStore.info.totalAlerts === 0 ? (
<EmptyGrid />
) : (
<AlertGrid
alertStore={alertStore}
settingsStore={settingsStore}
silenceFormStore={silenceFormStore}
/>
)
return alertStore.info.upgradeNeeded ? (
<UpgradeNeeded newVersion={alertStore.info.version} reloadAfter={3000} />
) : alertStore.info.reloadNeeded ? (
<ReloadNeeded reloadAfter={4000} />
) : alertStore.status.error ? (
<FatalError message={alertStore.status.error} />
) : alertStore.data.upstreams.counters &&
alertStore.data.upstreams.counters.total === 1 &&
alertStore.data.upstreams.counters.healthy === 0 &&
alertStore.data.upstreams.instances[0] &&
alertStore.data.upstreams.instances[0].error !== "" ? (
<FatalError message={alertStore.data.upstreams.instances[0].error} />
) : alertStore.info.version !== "unknown" &&
alertStore.info.totalAlerts === 0 ? (
<EmptyGrid />
) : (
<AlertGrid
alertStore={alertStore}
settingsStore={settingsStore}
silenceFormStore={silenceFormStore}
/>
);
};
export { Grid };
export default observer(Grid);
+1 -1
View File
@@ -4,7 +4,7 @@ import { AlertStore } from "Stores/AlertStore";
import { APIAlertGroupT } from "Models/APITypes";
import { IsMobile } from "Common/Device";
import { hashObject } from "Common/Hash";
import { StaticLabel } from "Components/Labels/StaticLabel";
import StaticLabel from "Components/Labels/StaticLabel";
import { PageSelect } from "Components/Pagination";
// take a list of groups and outputs a list of label sets, this ignores
@@ -1,6 +1,6 @@
import React, { FC } from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle";
@@ -16,7 +16,7 @@ import { InlineEdit } from "Components/InlineEdit";
const FilterInputLabel: FC<{
alertStore: AlertStore;
filter: FilterT;
}> = ({ alertStore, filter }) => {
}> = observer(({ alertStore, filter }) => {
const onChange = (val: string) => {
alertStore.status.resume();
// if filter is empty string then remove it
@@ -42,7 +42,7 @@ const FilterInputLabel: FC<{
...cs.baseClassNames,
].join(" ");
return useObserver(() => (
return (
<button
type="button"
className={`${rootClasses} d-inline-flex flex-row align-items-center`}
@@ -80,7 +80,7 @@ const FilterInputLabel: FC<{
onClick={() => alertStore.filters.removeFilter(filter.raw)}
/>
</button>
));
};
);
});
export { FilterInputLabel };
@@ -4,7 +4,7 @@ import { mount, render } from "enzyme";
import { AlertStore, NewUnappliedFilter } from "Stores/AlertStore";
import { QueryOperators } from "Common/Query";
import { FilteringCounterBadge } from ".";
import FilteringCounterBadge from ".";
let alertStore: AlertStore;
@@ -22,73 +22,69 @@ const FilteringCounterBadge: FC<{
alwaysVisible?: boolean;
defaultColor?: "light" | "primary";
isAppend?: boolean;
}> = observer(
({
}> = ({
alertStore,
name,
value,
counter,
themed,
alwaysVisible = false,
defaultColor = "light",
isAppend = true,
}) => {
const { ref, props } = useFlashTransition(counter);
const handleClick = useCallback(
(event: MouseEvent) => {
// left click => apply foo=bar filter
// left click + alt => apply foo!=bar filter
const operator =
event.altKey === true ? QueryOperators.NotEqual : QueryOperators.Equal;
event.preventDefault();
if (isAppend) {
alertStore.filters.addFilter(FormatQuery(name, operator, value));
} else {
alertStore.filters.setFilters([FormatQuery(name, operator, value)]);
}
},
[alertStore.filters, name, value, isAppend]
);
if (!alwaysVisible && counter === 0) return null;
const cs = GetClassAndStyle(
alertStore,
name,
value,
counter,
themed,
alwaysVisible = false,
defaultColor = "light",
isAppend = true,
}) => {
const { ref, props } = useFlashTransition(counter);
"badge-pill components-label-with-hover"
);
const handleClick = useCallback(
(event: MouseEvent) => {
// left click => apply foo=bar filter
// left click + alt => apply foo!=bar filter
const operator =
event.altKey === true
? QueryOperators.NotEqual
: QueryOperators.Equal;
return (
<TooltipWrapper
title={`Click to only show ${name}=${value} alerts or Alt+Click to hide them`}
>
<CSSTransition {...props}>
<span
ref={ref}
className={
themed
? cs.className
: [
`badge-${defaultColor}`,
"badge-pill components-label-with-hover",
...cs.baseClassNames,
].join(" ")
}
style={themed ? {} : cs.style}
onClick={handleClick}
>
{counter}
</span>
</CSSTransition>
</TooltipWrapper>
);
};
event.preventDefault();
if (isAppend) {
alertStore.filters.addFilter(FormatQuery(name, operator, value));
} else {
alertStore.filters.setFilters([FormatQuery(name, operator, value)]);
}
},
[alertStore.filters, name, value, isAppend]
);
if (!alwaysVisible && counter === 0) return null;
const cs = GetClassAndStyle(
alertStore,
name,
value,
"badge-pill components-label-with-hover"
);
return (
<TooltipWrapper
title={`Click to only show ${name}=${value} alerts or Alt+Click to hide them`}
>
<CSSTransition {...props}>
<span
ref={ref}
className={
themed
? cs.className
: [
`badge-${defaultColor}`,
"badge-pill components-label-with-hover",
...cs.baseClassNames,
].join(" ")
}
style={themed ? {} : cs.style}
onClick={handleClick}
>
{counter}
</span>
</CSSTransition>
</TooltipWrapper>
);
}
);
export { FilteringCounterBadge };
export default observer(FilteringCounterBadge);
@@ -6,7 +6,7 @@ import toDiffableHtml from "diffable-html";
import { AlertStore, NewUnappliedFilter } from "Stores/AlertStore";
import { FilteringLabel } from ".";
import FilteringLabel from ".";
let alertStore: AlertStore;
@@ -1,6 +1,6 @@
import React, { FC, useCallback, MouseEvent } from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import { AlertStore } from "Stores/AlertStore";
import { QueryOperators, FormatQuery } from "Common/Query";
@@ -33,14 +33,14 @@ const FilteringLabel: FC<{
"components-label-with-hover"
);
return useObserver(() => (
return (
<TooltipWrapper title="Click to only show alerts with this label or Alt+Click to hide them">
<span className={cs.className} style={cs.style} onClick={handleClick}>
<span className="components-label-name">{name}:</span>{" "}
<span className="components-label-value">{value}</span>
</span>
</TooltipWrapper>
));
);
};
export { FilteringLabel };
export default observer(FilteringLabel);
@@ -4,7 +4,7 @@ import { shallow } from "enzyme";
import { AlertStore } from "Stores/AlertStore";
import { HistoryLabel } from ".";
import HistoryLabel from ".";
let alertStore: AlertStore;
@@ -1,6 +1,6 @@
import React, { FC } from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import { QueryOperators } from "Common/Query";
import { AlertStore } from "Stores/AlertStore";
@@ -19,12 +19,12 @@ const HistoryLabel: FC<{
"components-label-history components-label-value"
);
return useObserver(() => (
return (
<span className={cs.className} style={cs.style}>
{name ? `${name}${matcher}` : null}
{value}
</span>
));
);
};
export { HistoryLabel };
export default observer(HistoryLabel);
@@ -6,7 +6,7 @@ import toDiffableHtml from "diffable-html";
import { AlertStore, NewUnappliedFilter } from "Stores/AlertStore";
import { LabelWithPercent } from ".";
import LabelWithPercent from ".";
let alertStore: AlertStore;
@@ -17,82 +17,78 @@ const LabelWithPercent: FC<{
percent: number;
offset: number;
isActive: boolean;
}> = observer(
({ alertStore, name, value, hits, percent, offset, isActive }) => {
const handleClick = useCallback(
(event: MouseEvent) => {
// left click => apply foo=bar filter
// left click + alt => apply foo!=bar filter
const operator =
event.altKey === true
? QueryOperators.NotEqual
: QueryOperators.Equal;
}> = ({ alertStore, name, value, hits, percent, offset, isActive }) => {
const handleClick = useCallback(
(event: MouseEvent) => {
// left click => apply foo=bar filter
// left click + alt => apply foo!=bar filter
const operator =
event.altKey === true ? QueryOperators.NotEqual : QueryOperators.Equal;
event.preventDefault();
event.preventDefault();
alertStore.filters.addFilter(FormatQuery(name, operator, value));
},
[alertStore.filters, name, value]
alertStore.filters.addFilter(FormatQuery(name, operator, value));
},
[alertStore.filters, name, value]
);
const removeFromFilters = () => {
alertStore.filters.removeFilter(
FormatQuery(name, QueryOperators.Equal, value)
);
};
const removeFromFilters = () => {
alertStore.filters.removeFilter(
FormatQuery(name, QueryOperators.Equal, value)
);
};
const cs = GetClassAndStyle(
alertStore,
name,
value,
"components-label-with-hover mb-0 pl-0 text-left"
);
const cs = GetClassAndStyle(
alertStore,
name,
value,
"components-label-with-hover mb-0 pl-0 text-left"
);
const progressBarBg =
percent > 66 ? "bg-danger" : percent > 33 ? "bg-warning" : "bg-success";
const progressBarBg =
percent > 66 ? "bg-danger" : percent > 33 ? "bg-warning" : "bg-success";
return (
<div className="d-inline-block mw-100">
<span className={cs.className} style={cs.style}>
<span className="mr-1 px-1 bg-primary text-white components-labelWithPercent-percent">
{hits}
</span>
<span onClick={handleClick}>
<span className="components-label-name">{name}:</span>{" "}
<span className="components-label-value">{value}</span>
</span>
{isActive ? (
<FontAwesomeIcon
className="cursor-pointer text-reset ml-1 close"
style={{ fontSize: "100%" }}
icon={faTimes}
onClick={removeFromFilters}
/>
) : null}
return (
<div className="d-inline-block mw-100">
<span className={cs.className} style={cs.style}>
<span className="mr-1 px-1 bg-primary text-white components-labelWithPercent-percent">
{hits}
</span>
<div className="progress components-labelWithPercent-progress mr-1">
{offset === 0 ? null : (
<div
className="progress-bar bg-transparent"
role="progressbar"
style={{ width: offset + "%" }}
aria-valuenow={offset}
aria-valuemin={0}
aria-valuemax={100}
/>
)}
<span onClick={handleClick}>
<span className="components-label-name">{name}:</span>{" "}
<span className="components-label-value">{value}</span>
</span>
{isActive ? (
<FontAwesomeIcon
className="cursor-pointer text-reset ml-1 close"
style={{ fontSize: "100%" }}
icon={faTimes}
onClick={removeFromFilters}
/>
) : null}
</span>
<div className="progress components-labelWithPercent-progress mr-1">
{offset === 0 ? null : (
<div
className={`progress-bar ${progressBarBg}`}
className="progress-bar bg-transparent"
role="progressbar"
style={{ width: percent + "%" }}
aria-valuenow={percent}
style={{ width: offset + "%" }}
aria-valuenow={offset}
aria-valuemin={0}
aria-valuemax={100}
/>
</div>
)}
<div
className={`progress-bar ${progressBarBg}`}
role="progressbar"
style={{ width: percent + "%" }}
aria-valuenow={percent}
aria-valuemin={0}
aria-valuemax={100}
/>
</div>
);
}
);
</div>
);
};
export { LabelWithPercent };
export default observer(LabelWithPercent);
@@ -6,7 +6,7 @@ import toDiffableHtml from "diffable-html";
import { AlertStore } from "Stores/AlertStore";
import { StaticLabel } from ".";
import StaticLabel from ".";
let alertStore: AlertStore;
@@ -1,6 +1,6 @@
import React, { FC } from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import { AlertStore } from "Stores/AlertStore";
import { GetClassAndStyle } from "Components/Labels/Utils";
@@ -13,12 +13,12 @@ const StaticLabel: FC<{
}> = ({ alertStore, name, value }) => {
const cs = GetClassAndStyle(alertStore, name, value);
return useObserver(() => (
return (
<span className={cs.className} style={cs.style}>
<span className="components-label-name">{name}:</span>{" "}
<span className="components-label-value">{value}</span>
</span>
));
);
};
export { StaticLabel };
export default observer(StaticLabel);
@@ -1,6 +1,6 @@
import React, { FC } from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import Select from "react-select";
@@ -10,7 +10,7 @@ import { ThemeContext } from "Components/Theme";
const AlertGroupCollapseConfiguration: FC<{
settingsStore: Settings;
}> = ({ settingsStore }) => {
}> = observer(({ settingsStore }) => {
if (
!Object.values(settingsStore.alertGroupConfig.options)
.map((o) => o.value)
@@ -32,7 +32,7 @@ const AlertGroupCollapseConfiguration: FC<{
const context = React.useContext(ThemeContext);
return useObserver(() => (
return (
<div className="form-group mb-0">
<Select
styles={context.reactSelectStyles}
@@ -48,7 +48,7 @@ const AlertGroupCollapseConfiguration: FC<{
hideSelectedOptions
/>
</div>
));
};
);
});
export { AlertGroupCollapseConfiguration };
@@ -1,6 +1,6 @@
import React, { FC } from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import Select from "react-select";
@@ -11,7 +11,7 @@ import { SortLabelName } from "./SortLabelName";
const AlertGroupSortConfiguration: FC<{
settingsStore: Settings;
}> = ({ settingsStore }) => {
}> = observer(({ settingsStore }) => {
if (
!Object.values(settingsStore.gridConfig.options)
.map((o) => o.value)
@@ -40,7 +40,7 @@ const AlertGroupSortConfiguration: FC<{
const context = React.useContext(ThemeContext);
return useObserver(() => (
return (
<div className="form-group mb-0">
<div className="d-flex flex-fill flex-lg-row flex-column justify-content-between">
<div className="flex-shrink-0 flex-grow-1 flex-basis-auto">
@@ -86,7 +86,7 @@ const AlertGroupSortConfiguration: FC<{
)}
</div>
</div>
));
};
);
});
export { AlertGroupSortConfiguration };
@@ -1,17 +1,17 @@
import React, { FC } from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import { Settings } from "Stores/Settings";
const AlertGroupTitleBarColor: FC<{
settingsStore: Settings;
}> = ({ settingsStore }) => {
}> = observer(({ settingsStore }) => {
const onChange = (value: boolean) => {
settingsStore.alertGroupConfig.setColorTitleBar(value);
};
return useObserver(() => (
return (
<div className="form-group mb-0">
<div className="form-check form-check-inline">
<span className="custom-control custom-switch">
@@ -34,7 +34,7 @@ const AlertGroupTitleBarColor: FC<{
</span>
</div>
</div>
));
};
);
});
export { AlertGroupTitleBarColor };
@@ -1,16 +1,16 @@
import React, { FC } from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import { Settings } from "Stores/Settings";
const FilterBarConfiguration: FC<{
settingsStore: Settings;
}> = ({ settingsStore }) => {
}> = observer(({ settingsStore }) => {
const onAutohideChange = (value: boolean) => {
settingsStore.filterBarConfig.setAutohide(value);
};
return useObserver(() => (
return (
<div className="form-group mb-0">
<div className="form-check form-check-inline">
<span className="custom-control custom-switch">
@@ -31,7 +31,7 @@ const FilterBarConfiguration: FC<{
</span>
</div>
</div>
));
};
);
});
export { FilterBarConfiguration };
@@ -1,18 +1,18 @@
import React, { FC } from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import { Settings } from "Stores/Settings";
import { GridLabelName } from "./GridLabelName";
const MultiGridConfiguration: FC<{
settingsStore: Settings;
}> = ({ settingsStore }) => {
}> = observer(({ settingsStore }) => {
const onSortReverseChange = (value: boolean) => {
settingsStore.multiGridConfig.setGridSortReverse(value);
};
return useObserver(() => (
return (
<div className="form-group mb-0">
<div className="d-flex flex-fill flex-lg-row flex-column justify-content-between">
<div className="flex-shrink-0 flex-grow-1 flex-basis-auto mx-0 mx-lg-1 mt-1 mt-lg-0">
@@ -40,7 +40,7 @@ const MultiGridConfiguration: FC<{
</div>
</div>
</div>
));
};
);
});
export { MultiGridConfiguration };
@@ -1,6 +1,6 @@
import React, { FC } from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import Select from "react-select";
@@ -10,7 +10,7 @@ import { ThemeContext } from "Components/Theme";
const ThemeConfiguration: FC<{
settingsStore: Settings;
}> = ({ settingsStore }) => {
}> = observer(({ settingsStore }) => {
if (
!Object.values(settingsStore.themeConfig.options)
.map((o) => o.value)
@@ -32,7 +32,7 @@ const ThemeConfiguration: FC<{
const context = React.useContext(ThemeContext);
return useObserver(() => (
return (
<div className="form-group mb-2">
<Select
styles={context.reactSelectStyles}
@@ -46,7 +46,7 @@ const ThemeConfiguration: FC<{
hideSelectedOptions
/>
</div>
));
};
);
});
export { ThemeConfiguration };
@@ -1,6 +1,6 @@
import React, { FC, useState } from "react";
import { useObserver } from "mobx-react-lite";
import { Observer } from "mobx-react-lite";
import { AlertStore } from "Stores/AlertStore";
import { Settings } from "Stores/Settings";
@@ -25,7 +25,7 @@ const MainModalContent: FC<{
}) => {
const [tab, setTab] = useState<OpenTabT>(openTab);
return useObserver(() => (
return (
<React.Fragment>
<div className="modal-header py-2">
<nav className="nav nav-pills nav-justified w-100">
@@ -54,15 +54,25 @@ const MainModalContent: FC<{
) : null}
</div>
<div className="modal-footer">
{alertStore.info.authentication.enabled && (
<span className="text-muted mr-2">
Username: {alertStore.info.authentication.username}
</span>
)}
<span className="text-muted">Version: {alertStore.info.version}</span>
<Observer>
{() =>
alertStore.info.authentication.enabled ? (
<span className="text-muted mr-2">
Username: {alertStore.info.authentication.username}
</span>
) : null
}
</Observer>
<Observer>
{() => (
<span className="text-muted">
Version: {alertStore.info.version}
</span>
)}
</Observer>
</div>
</React.Fragment>
));
);
};
export { MainModalContent };
+2 -2
View File
@@ -54,8 +54,8 @@ describe("<MainModal />", () => {
const toggle = tree.find(".nav-link");
toggle.simulate("click");
expect(tree.find("FontAwesomeIcon")).not.toHaveLength(0);
expect(tree.find("MainModalContent")).toHaveLength(0);
expect(tree.find(".modal-content").find("svg.fa-spinner")).toHaveLength(1);
expect(tree.find("MainModalContent")).toHaveLength(0);
});
it("renders modal content if fallback is not used", () => {
@@ -63,8 +63,8 @@ describe("<MainModal />", () => {
const toggle = tree.find(".nav-link");
toggle.simulate("click");
expect(tree.find("FontAwesomeIcon")).not.toHaveLength(0);
expect(tree.find("MainModalContent")).toHaveLength(1);
expect(tree.find(".modal-content").find("svg.fa-spinner")).toHaveLength(0);
expect(tree.find("MainModalContent")).toHaveLength(1);
});
it("hides the modal when toggle() is called twice", () => {
@@ -6,7 +6,7 @@ import { faBellSlash } from "@fortawesome/free-solid-svg-icons/faBellSlash";
import { APISilenceT } from "Models/APITypes";
import { AlertStore } from "Stores/AlertStore";
import { FilteringCounterBadge } from "Components/Labels/FilteringCounterBadge";
import FilteringCounterBadge from "Components/Labels/FilteringCounterBadge";
import { ToggleIcon } from "Components/ToggleIcon";
import { SilenceProgress } from "./SilenceProgress";
@@ -1,6 +1,6 @@
import React, { FC, useEffect, useState } from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import parseISO from "date-fns/parseISO";
import getUnixTime from "date-fns/getUnixTime";
@@ -18,7 +18,7 @@ const calculatePercent = (startsAt: string, endsAt: string) => {
const SilenceProgress: FC<{
silence: APISilenceT;
}> = ({ silence }) => {
}> = observer(({ silence }) => {
const [progress, setProgress] = useState<number>(
calculatePercent(silence.startsAt, silence.endsAt)
);
@@ -30,33 +30,31 @@ const SilenceProgress: FC<{
return () => clearInterval(timer);
}, [silence.startsAt, silence.endsAt]);
return useObserver(() =>
parseISO(silence.endsAt) < new Date() ? (
<span className="badge badge-danger align-text-bottom p-1">
Expired <DateFromNow timestamp={silence.endsAt} />
</span>
) : (
<span className="badge badge-light nmb-05 align-text-bottom p-1">
Expires <DateFromNow timestamp={silence.endsAt} />
<div className="progress silence-progress">
<div
className={
progress > 90
? "progress-bar bg-danger"
: progress > 75
? "progress-bar bg-warning"
: "progress-bar bg-success"
}
role="progressbar"
style={{ width: progress + "%" }}
aria-valuenow={progress}
aria-valuemin={0}
aria-valuemax={100}
/>
</div>
</span>
)
return parseISO(silence.endsAt) < new Date() ? (
<span className="badge badge-danger align-text-bottom p-1">
Expired <DateFromNow timestamp={silence.endsAt} />
</span>
) : (
<span className="badge badge-light nmb-05 align-text-bottom p-1">
Expires <DateFromNow timestamp={silence.endsAt} />
<div className="progress silence-progress">
<div
className={
progress > 90
? "progress-bar bg-danger"
: progress > 75
? "progress-bar bg-warning"
: "progress-bar bg-success"
}
role="progressbar"
style={{ width: progress + "%" }}
aria-valuenow={progress}
aria-valuemin={0}
aria-valuemax={100}
/>
</div>
</span>
);
};
});
export { SilenceProgress };
@@ -9,7 +9,7 @@ import React, {
ReactNode,
} from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import { localStored } from "mobx-stored";
import { Manager, Reference, Popper } from "react-popper";
@@ -27,7 +27,7 @@ import { Settings } from "Stores/Settings";
import { IsMobile } from "Common/Device";
import { CommonPopperModifiers } from "Common/Popper";
import { DropdownSlide } from "Components/Animations/DropdownSlide";
import { HistoryLabel } from "Components/Labels/HistoryLabel";
import HistoryLabel from "Components/Labels/HistoryLabel";
import { useOnClickOutside } from "Hooks/useOnClickOutside";
interface ReduceFilterT {
@@ -164,7 +164,7 @@ interface HistoryStorageT {
const History: FC<{
alertStore: AlertStore;
settingsStore: Settings;
}> = ({ alertStore, settingsStore }) => {
}> = observer(({ alertStore, settingsStore }) => {
// this will be dumped to local storage via mobx-stored
const history: HistoryStorageT = localStored(
"history.filters",
@@ -214,7 +214,7 @@ const History: FC<{
const ref = useRef<HTMLSpanElement | null>(null);
useOnClickOutside(ref, hide, isVisible);
return useObserver(() => (
return (
// data-filters is there to register filters for observation in mobx
// it needs to be using full filter object to notice changes to
// name & value but ignore hits
@@ -264,7 +264,7 @@ const History: FC<{
</DropdownSlide>
</Manager>
</span>
));
};
);
});
export { History, HistoryMenu, ReduceFilter };
@@ -1,6 +1,6 @@
import React, { FC, useEffect, useState, useRef, useCallback } from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import Autosuggest from "react-autosuggest";
import Highlight from "react-highlighter";
@@ -21,7 +21,7 @@ import { History } from "./History";
const FilterInput: FC<{
alertStore: AlertStore;
settingsStore: Settings;
}> = ({ alertStore, settingsStore }) => {
}> = observer(({ alertStore, settingsStore }) => {
const autosuggestRef = useRef<Autosuggest | null>(null);
const inputRef = useRef<HTMLElement | null>(null);
const formRef = useRef<HTMLDivElement | null>(null);
@@ -135,7 +135,7 @@ const FilterInput: FC<{
);
};
return useObserver(() => (
return (
// data-filters is there to register filters for observation in mobx
// in order to re-render input component
<form className="form-inline mw-100" onSubmit={onSubmit}>
@@ -184,7 +184,7 @@ const FilterInput: FC<{
</div>
</div>
</form>
));
};
);
});
export { FilterInput };
+1 -1
View File
@@ -6,7 +6,7 @@ import { AlertStore, NewUnappliedFilter, FilterT } from "Stores/AlertStore";
import { Settings } from "Stores/Settings";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { HistoryMenu } from "./FilterInput/History";
import { NavBar } from ".";
import NavBar from ".";
import "Styles/Percy.scss";
+2 -1
View File
@@ -7,7 +7,8 @@ import { MockThemeContext } from "__mocks__/Theme";
import { AlertStore } from "Stores/AlertStore";
import { Settings } from "Stores/Settings";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { NavBar, MobileIdleTimeout, DesktopIdleTimeout } from ".";
import NavBar from ".";
import { MobileIdleTimeout, DesktopIdleTimeout } from "./timeouts";
let alertStore: AlertStore;
let settingsStore: Settings;
+6 -8
View File
@@ -1,7 +1,7 @@
import React, { FC, useState, useEffect, useCallback } from "react";
import { reaction } from "mobx";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import useDimensions from "react-cool-dimensions";
@@ -15,13 +15,11 @@ import { SilenceFormStore } from "Stores/SilenceFormStore";
import { IsMobile } from "Common/Device";
import { OverviewModal } from "Components/OverviewModal";
import { MainModal } from "Components/MainModal";
import { SilenceModal } from "Components/SilenceModal";
import SilenceModal from "Components/SilenceModal";
import { ThemeContext } from "Components/Theme";
import { Fetcher } from "Components/Fetcher";
import { FilterInput } from "./FilterInput";
const DesktopIdleTimeout = 1000 * 60 * 3;
const MobileIdleTimeout = 1000 * 12;
import { MobileIdleTimeout, DesktopIdleTimeout } from "./timeouts";
const NavBar: FC<{
alertStore: AlertStore;
@@ -87,7 +85,7 @@ const NavBar: FC<{
[] // eslint-disable-line react-hooks/exhaustive-deps
);
return useObserver(() => (
return (
<div className={`container p-0 m-0 mw-100 ${containerClass}`}>
<CSSTransition
classNames="components-animation-navbar"
@@ -128,7 +126,7 @@ const NavBar: FC<{
</nav>
</CSSTransition>
</div>
));
);
};
export { NavBar, MobileIdleTimeout, DesktopIdleTimeout };
export default observer(NavBar);
+4
View File
@@ -0,0 +1,4 @@
const DesktopIdleTimeout = 1000 * 60 * 3;
const MobileIdleTimeout = 1000 * 12;
export { MobileIdleTimeout, DesktopIdleTimeout };
@@ -5,7 +5,7 @@ import { observer } from "mobx-react-lite";
import { APILabelCounterT } from "Models/APITypes";
import { AlertStore } from "Stores/AlertStore";
import { TooltipWrapper } from "Components/TooltipWrapper";
import { LabelWithPercent } from "Components/Labels/LabelWithPercent";
import LabelWithPercent from "Components/Labels/LabelWithPercent";
import { ToggleIcon } from "Components/ToggleIcon";
const TableRows: FC<{
+5 -5
View File
@@ -1,6 +1,6 @@
import React, { FC, useState, useCallback } from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import { CSSTransition } from "react-transition-group";
@@ -21,14 +21,14 @@ const OverviewModalContent = React.lazy(() =>
const OverviewModal: FC<{
alertStore: AlertStore;
}> = ({ alertStore }) => {
}> = observer(({ alertStore }) => {
const [isVisible, setIsVisible] = useState<boolean>(false);
const toggle = useCallback(() => setIsVisible(!isVisible), [isVisible]);
const { ref, props } = useFlashTransition(alertStore.info.totalAlerts);
return useObserver(() => (
return (
<React.Fragment>
<TooltipWrapper title="Show alert overview">
<CSSTransition {...props}>
@@ -58,7 +58,7 @@ const OverviewModal: FC<{
</React.Suspense>
</Modal>
</React.Fragment>
));
};
);
});
export { OverviewModal };
@@ -1,7 +1,7 @@
import React, { FC, useEffect } from "react";
import { autorun } from "mobx";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import Select from "react-select";
@@ -17,7 +17,7 @@ import { ValidationError } from "Components/ValidationError";
const AlertManagerInput: FC<{
alertStore: AlertStore;
silenceFormStore: SilenceFormStore;
}> = ({ alertStore, silenceFormStore }) => {
}> = observer(({ alertStore, silenceFormStore }) => {
useEffect(() => {
if (silenceFormStore.data.alertmanagers.length === 0) {
silenceFormStore.data.setAlertmanagers(
@@ -52,7 +52,7 @@ const AlertManagerInput: FC<{
const context = React.useContext(ThemeContext);
return useObserver(() => (
return (
<Select
styles={context.reactSelectStyles}
classNamePrefix="react-select"
@@ -77,7 +77,7 @@ const AlertManagerInput: FC<{
}}
isDisabled={silenceFormStore.data.silenceID !== null}
/>
));
};
);
});
export { AlertManagerInput };
@@ -16,7 +16,7 @@ import { Settings } from "Stores/Settings";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { ThemeContext } from "Components/Theme";
import { useFetchGet } from "__mocks__/Hooks/useFetchGet";
import { Browser } from ".";
import Browser from ".";
let alertStore: AlertStore;
let silenceFormStore: SilenceFormStore;
@@ -1,6 +1,6 @@
import React, { FC, useState, useEffect, ReactNode } from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import TransitionGroup from "react-transition-group/TransitionGroup";
import { CSSTransition } from "react-transition-group";
@@ -88,7 +88,7 @@ const Browser: FC<{
const context = React.useContext(ThemeContext);
return useObserver(() => (
return (
<React.Fragment>
<div
className="d-flex flex-fill flex-lg-row flex-column justify-content-between mb-3"
@@ -178,7 +178,7 @@ const Browser: FC<{
</React.Fragment>
)}
</React.Fragment>
));
);
};
export { Browser };
export default observer(Browser);
@@ -1,6 +1,6 @@
import React, { FC, useEffect, useState, useCallback, ReactNode } from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import differenceInMinutes from "date-fns/differenceInMinutes";
import differenceInHours from "date-fns/differenceInHours";
@@ -56,8 +56,8 @@ const Tab: FC<{
const TabContentStart: FC<{
silenceFormStore: SilenceFormStore;
}> = ({ silenceFormStore }) => {
return useObserver(() => (
}> = observer(({ silenceFormStore }) => {
return (
<div className="d-flex flex-sm-row flex-column justify-content-around mx-3 mt-2">
<div className="d-flex justify-content-center align-items-center">
<DayPicker
@@ -93,50 +93,50 @@ const TabContentStart: FC<{
onMinuteDec={() => silenceFormStore.data.decStart(1)}
/>
</div>
));
};
);
});
const TabContentEnd: FC<{ silenceFormStore: SilenceFormStore }> = ({
silenceFormStore,
}) => {
return useObserver(() => (
<div className="d-flex flex-sm-row flex-column justify-content-around mx-3 mt-2">
<div className="d-flex justify-content-center align-items-center">
<DayPicker
className="components-date-range"
month={silenceFormStore.data.endsAt}
disabledDays={{
before: setSeconds(silenceFormStore.data.startsAt, 0),
}}
todayButton="Today"
onDayClick={(val) => {
const endsAt = new Date(val);
endsAt.setHours(silenceFormStore.data.endsAt.getHours());
endsAt.setMinutes(silenceFormStore.data.endsAt.getMinutes());
endsAt.setSeconds(0);
silenceFormStore.data.setEnd(endsAt);
silenceFormStore.data.verifyStarEnd();
}}
selectedDays={{
from: silenceFormStore.data.startsAt,
to: silenceFormStore.data.endsAt,
}}
modifiers={{
start: silenceFormStore.data.startsAt,
end: silenceFormStore.data.endsAt,
}}
const TabContentEnd: FC<{ silenceFormStore: SilenceFormStore }> = observer(
({ silenceFormStore }) => {
return (
<div className="d-flex flex-sm-row flex-column justify-content-around mx-3 mt-2">
<div className="d-flex justify-content-center align-items-center">
<DayPicker
className="components-date-range"
month={silenceFormStore.data.endsAt}
disabledDays={{
before: setSeconds(silenceFormStore.data.startsAt, 0),
}}
todayButton="Today"
onDayClick={(val) => {
const endsAt = new Date(val);
endsAt.setHours(silenceFormStore.data.endsAt.getHours());
endsAt.setMinutes(silenceFormStore.data.endsAt.getMinutes());
endsAt.setSeconds(0);
silenceFormStore.data.setEnd(endsAt);
silenceFormStore.data.verifyStarEnd();
}}
selectedDays={{
from: silenceFormStore.data.startsAt,
to: silenceFormStore.data.endsAt,
}}
modifiers={{
start: silenceFormStore.data.startsAt,
end: silenceFormStore.data.endsAt,
}}
/>
</div>
<HourMinute
dateValue={silenceFormStore.data.endsAt}
onHourInc={() => silenceFormStore.data.incEnd(60)}
onHourDec={() => silenceFormStore.data.decEnd(60)}
onMinuteInc={() => silenceFormStore.data.incEnd(1)}
onMinuteDec={() => silenceFormStore.data.decEnd(1)}
/>
</div>
<HourMinute
dateValue={silenceFormStore.data.endsAt}
onHourInc={() => silenceFormStore.data.incEnd(60)}
onHourDec={() => silenceFormStore.data.decEnd(60)}
onMinuteInc={() => silenceFormStore.data.incEnd(1)}
onMinuteDec={() => silenceFormStore.data.decEnd(1)}
/>
</div>
));
};
);
}
);
// calculate value for duration increase button using a goal step
const CalculateChangeValueUp = (currentValue: number, step: number): number => {
@@ -163,8 +163,8 @@ const CalculateChangeValueDown = (
const TabContentDuration: FC<{
silenceFormStore: SilenceFormStore;
}> = ({ silenceFormStore }) => {
return useObserver(() => (
}> = observer(({ silenceFormStore }) => {
return (
<div className="d-flex flex-sm-row flex-column justify-content-around mt-2 mx-3">
<Duration
label="days"
@@ -196,15 +196,15 @@ const TabContentDuration: FC<{
}
/>
</div>
));
};
);
});
type tabT = "start" | "end" | "duration";
const DateTimeSelect: FC<{
silenceFormStore: SilenceFormStore;
openTab?: tabT;
}> = ({ silenceFormStore, openTab = "duration" }) => {
}> = observer(({ silenceFormStore, openTab = "duration" }) => {
const [currentTab, setCurrentTab] = useState<tabT>(openTab);
const [timeNow, setTimeNow] = useState<Date>(nowZeroSeconds());
@@ -219,7 +219,7 @@ const DateTimeSelect: FC<{
};
}, [updateTimeNow]);
return useObserver(() => (
return (
<React.Fragment>
<ul className="nav nav-tabs nav-fill">
<Tab
@@ -277,7 +277,7 @@ const DateTimeSelect: FC<{
) : null}
</div>
</React.Fragment>
));
};
);
});
export { DateTimeSelect, TabContentStart, TabContentEnd, TabContentDuration };
@@ -5,7 +5,7 @@ import { render } from "enzyme";
import toDiffableHtml from "diffable-html";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { PayloadPreview } from ".";
import PayloadPreview from ".";
describe("<PayloadPreview />", () => {
it("matches snapshot", () => {
@@ -1,6 +1,6 @@
import React, { FC } from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import JSONPretty from "react-json-pretty";
import * as theme from "react-json-pretty/dist/monikai";
@@ -10,13 +10,13 @@ import { SilenceFormStore } from "Stores/SilenceFormStore";
const PayloadPreview: FC<{
silenceFormStore: SilenceFormStore;
}> = ({ silenceFormStore }) => {
return useObserver(() => (
return (
<JSONPretty
json={silenceFormStore.data.toAlertmanagerPayload}
theme={theme}
themeClassName="rounded p-1"
/>
));
);
};
export { PayloadPreview };
export default observer(PayloadPreview);
@@ -9,7 +9,7 @@ import { AlertStore, NewUnappliedFilter } from "Stores/AlertStore";
import { Settings } from "Stores/Settings";
import { SilenceFormStore, NewEmptyMatcher } from "Stores/SilenceFormStore";
import { QueryOperators, StaticLabels } from "Common/Query";
import { SilenceForm } from "./SilenceForm";
import SilenceForm from "./SilenceForm";
let alertStore: AlertStore;
let settingsStore: Settings;
+10 -10
View File
@@ -1,6 +1,6 @@
import React, { FC, useEffect, useState, MouseEvent, FormEvent } from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import copy from "copy-to-clipboard";
@@ -29,14 +29,14 @@ import { useFlashTransition } from "Hooks/useFlashTransition";
import { TooltipWrapper } from "Components/TooltipWrapper";
import { ToggleIcon } from "Components/ToggleIcon";
import { AlertManagerInput } from "./AlertManagerInput";
import { SilenceMatch } from "./SilenceMatch";
import SilenceMatch from "./SilenceMatch";
import { DateTimeSelect } from "./DateTimeSelect";
import { PayloadPreview } from "./PayloadPreview";
import PayloadPreview from "./PayloadPreview";
import { IconInput, AuthenticatedAuthorInput } from "./AuthorInput";
const ShareButton: FC<{
silenceFormStore: SilenceFormStore;
}> = ({ silenceFormStore }) => {
}> = observer(({ silenceFormStore }) => {
const [clickCount, setClickCount] = useState<number>(0);
const baseURL = [
@@ -48,7 +48,7 @@ const ShareButton: FC<{
const { ref, props } = useFlashTransition(clickCount);
return useObserver(() => (
return (
<div className="input-group mb-3">
<div className="input-group-prepend">
<span className="input-group-text">
@@ -79,8 +79,8 @@ const ShareButton: FC<{
</span>
</div>
</div>
));
};
);
});
const SilenceForm: FC<{
alertStore: AlertStore;
@@ -178,7 +178,7 @@ const SilenceForm: FC<{
silenceFormStore.data.setWasValidated(true);
};
return useObserver(() => (
return (
<form onSubmit={handleSubmit} autoComplete="on">
<div className="mb-3">
<AlertManagerInput
@@ -262,7 +262,7 @@ const SilenceForm: FC<{
</div>
) : null}
</form>
));
);
};
export { SilenceForm };
export default observer(SilenceForm);
@@ -1,6 +1,6 @@
import React, { FC } from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle";
@@ -16,7 +16,7 @@ import { MatcherToFilter, AlertManagersToFilter } from "../Matchers";
const MatchCounter: FC<{
silenceFormStore: SilenceFormStore;
matcher: MatcherWithIDT;
}> = ({ silenceFormStore, matcher }) => {
}> = observer(({ silenceFormStore, matcher }) => {
const filters = [MatcherToFilter(matcher)];
if (silenceFormStore.data.alertmanagers.length) {
filters.push(AlertManagersToFilter(silenceFormStore.data.alertmanagers));
@@ -26,33 +26,31 @@ const MatchCounter: FC<{
APIAlertsResponseT
>(FormatBackendURI("alerts.json?") + FormatAlertsQ(filters));
return useObserver(() =>
error ? (
<TooltipWrapper
title={`Failed to fetch alerts matching this label: ${error}`}
return error ? (
<TooltipWrapper
title={`Failed to fetch alerts matching this label: ${error}`}
>
<FontAwesomeIcon className="text-danger" icon={faExclamationCircle} />
</TooltipWrapper>
) : (
<TooltipWrapper title="Number of alerts matching this label">
<span
className="badge badge-light badge-pill d-block"
style={{ fontSize: "85%", lineHeight: "1rem" }}
data-am={silenceFormStore.data.alertmanagers.length}
>
<FontAwesomeIcon className="text-danger" icon={faExclamationCircle} />
</TooltipWrapper>
) : (
<TooltipWrapper title="Number of alerts matching this label">
<span
className="badge badge-light badge-pill d-block"
style={{ fontSize: "85%", lineHeight: "1rem" }}
data-am={silenceFormStore.data.alertmanagers.length}
>
{isLoading || response === null ? (
<FontAwesomeIcon
icon={faSpinner}
spin
className={isRetrying ? "text-danger" : ""}
/>
) : (
Math.max(response.totalAlerts, 0)
)}
</span>
</TooltipWrapper>
)
{isLoading || response === null ? (
<FontAwesomeIcon
icon={faSpinner}
spin
className={isRetrying ? "text-danger" : ""}
/>
) : (
Math.max(response.totalAlerts, 0)
)}
</span>
</TooltipWrapper>
);
};
});
export { MatchCounter };
@@ -8,7 +8,7 @@ import {
MatcherWithIDT,
} from "Stores/SilenceFormStore";
import { StringToOption } from "Common/Select";
import { SilenceMatch } from ".";
import SilenceMatch from ".";
let silenceFormStore: SilenceFormStore;
let matcher: MatcherWithIDT;
@@ -1,6 +1,6 @@
import React, { FC } from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTrash } from "@fortawesome/free-solid-svg-icons/faTrash";
@@ -17,7 +17,7 @@ const SilenceMatch: FC<{
onDelete: () => void;
isValid: boolean;
}> = ({ silenceFormStore, matcher, showDelete, onDelete, isValid }) => {
return useObserver(() => (
return (
<div className="d-flex flex-fill flex-lg-row flex-column mb-3">
<div className="flex-shrink-0 flex-grow-0 flex-basis-25 pr-lg-2 pb-2 pb-lg-0">
<LabelNameInput matcher={matcher} isValid={isValid} />
@@ -64,7 +64,7 @@ const SilenceMatch: FC<{
) : null}
</div>
</div>
));
);
};
export { SilenceMatch };
export default observer(SilenceMatch);
@@ -1,6 +1,6 @@
import React, { FC } from "react";
import { useObserver } from "mobx-react-lite";
import { Observer } from "mobx-react-lite";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSpinner } from "@fortawesome/free-solid-svg-icons/faSpinner";
@@ -10,10 +10,10 @@ import { AlertStore } from "Stores/AlertStore";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { Settings } from "Stores/Settings";
import { Tab } from "Components/Modal/Tab";
import { SilenceForm } from "./SilenceForm";
import SilenceForm from "./SilenceForm";
import { SilencePreview } from "./SilencePreview";
import { SilenceSubmitController } from "./SilenceSubmit/SilenceSubmitController";
import { Browser } from "./Browser";
import SilenceSubmitController from "./SilenceSubmit/SilenceSubmitController";
import Browser from "./Browser";
const ReadOnlyPlaceholder: FC = () => (
<div className="jumbotron bg-transparent">
@@ -37,79 +37,83 @@ const SilenceModalContent: FC<{
onHide,
previewOpen = false,
}) => {
return useObserver(() => (
<React.Fragment>
<div className="modal-header py-2">
<nav className="nav nav-pills nav-justified w-100">
<Tab
title={
silenceFormStore.data.currentStage === "form"
? silenceFormStore.data.silenceID === null
? "New silence"
: "Editing silence"
: silenceFormStore.data.currentStage === "preview"
? "Preview silenced alerts"
: "Silence submitted"
}
active={silenceFormStore.tab.current === "editor"}
onClick={() => silenceFormStore.tab.setTab("editor")}
/>
<Tab
title="Browse"
active={silenceFormStore.tab.current === "browser"}
onClick={() => silenceFormStore.tab.setTab("browser")}
/>
<button type="button" className="close" onClick={onHide}>
<span>&times;</span>
</button>
</nav>
</div>
<div
className={`modal-body ${
silenceFormStore.toggle.blurred ? "modal-content-blur" : ""
}`}
>
{silenceFormStore.tab.current === "editor" ? (
silenceFormStore.data.currentStage === "form" ? (
alertStore.data.upstreams.instances.length > 0 ? (
Object.keys(alertStore.data.clustersWithoutReadOnly).length >
0 ? (
<SilenceForm
return (
<Observer>
{() => (
<React.Fragment>
<div className="modal-header py-2">
<nav className="nav nav-pills nav-justified w-100">
<Tab
title={
silenceFormStore.data.currentStage === "form"
? silenceFormStore.data.silenceID === null
? "New silence"
: "Editing silence"
: silenceFormStore.data.currentStage === "preview"
? "Preview silenced alerts"
: "Silence submitted"
}
active={silenceFormStore.tab.current === "editor"}
onClick={() => silenceFormStore.tab.setTab("editor")}
/>
<Tab
title="Browse"
active={silenceFormStore.tab.current === "browser"}
onClick={() => silenceFormStore.tab.setTab("browser")}
/>
<button type="button" className="close" onClick={onHide}>
<span>&times;</span>
</button>
</nav>
</div>
<div
className={`modal-body ${
silenceFormStore.toggle.blurred ? "modal-content-blur" : ""
}`}
>
{silenceFormStore.tab.current === "editor" ? (
silenceFormStore.data.currentStage === "form" ? (
alertStore.data.upstreams.instances.length > 0 ? (
Object.keys(alertStore.data.clustersWithoutReadOnly).length >
0 ? (
<SilenceForm
alertStore={alertStore}
silenceFormStore={silenceFormStore}
settingsStore={settingsStore}
previewOpen={previewOpen}
/>
) : (
<ReadOnlyPlaceholder />
)
) : (
<h1 className="text-center display-1 text-placeholder p-5 m-auto">
<FontAwesomeIcon icon={faSpinner} size="lg" spin />
</h1>
)
) : silenceFormStore.data.currentStage === "preview" ? (
<SilencePreview
alertStore={alertStore}
silenceFormStore={silenceFormStore}
settingsStore={settingsStore}
previewOpen={previewOpen}
/>
) : (
<ReadOnlyPlaceholder />
<SilenceSubmitController
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
)
) : (
<h1 className="text-center display-1 text-placeholder p-5 m-auto">
<FontAwesomeIcon icon={faSpinner} size="lg" spin />
</h1>
)
) : silenceFormStore.data.currentStage === "preview" ? (
<SilencePreview
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
) : (
<SilenceSubmitController
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
)
) : null}
{silenceFormStore.tab.current === "browser" ? (
<Browser
alertStore={alertStore}
silenceFormStore={silenceFormStore}
settingsStore={settingsStore}
/>
) : null}
</div>
</React.Fragment>
));
) : null}
{silenceFormStore.tab.current === "browser" ? (
<Browser
alertStore={alertStore}
silenceFormStore={silenceFormStore}
settingsStore={settingsStore}
/>
) : null}
</div>
</React.Fragment>
)}
</Observer>
);
};
export { SilenceModalContent };
@@ -0,0 +1,81 @@
import React, { FC } from "react";
import { observer } from "mobx-react-lite";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle";
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle";
import { AlertStore } from "Stores/AlertStore";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { SilenceSubmitProgress } from "./SilenceSubmitProgress";
const MultiClusterStatus: FC<{
alertStore: AlertStore;
silenceFormStore: SilenceFormStore;
}> = ({ silenceFormStore, alertStore }) => {
return (
<div className="table-responsive">
<table className="table table-borderless">
<tbody>
{Object.values(silenceFormStore.data.requestsByCluster).map(
(clusterRequest) => (
<tr key={clusterRequest.cluster}>
<td className="align-middle" style={{ width: "1%" }}>
{clusterRequest.isDone ? (
clusterRequest.error ? (
<FontAwesomeIcon
icon={faExclamationCircle}
className="text-danger"
/>
) : (
<FontAwesomeIcon
icon={faCheckCircle}
className="text-success"
/>
)
) : (
<SilenceSubmitProgress
key={clusterRequest.cluster}
cluster={clusterRequest.cluster}
members={clusterRequest.members}
payload={silenceFormStore.data.toAlertmanagerPayload}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
)}
</td>
<td className="align-middle">{clusterRequest.cluster}</td>
<td>
<div
className={`rounded text-center ${
clusterRequest.isDone && clusterRequest.error
? "bg-light"
: ""
}`}
>
{clusterRequest.isDone ? (
clusterRequest.error ? (
clusterRequest.error
) : (
<a
href={clusterRequest.silenceLink}
target="_blank"
rel="noopener noreferrer"
>
{clusterRequest.silenceID}
</a>
)
) : null}
</div>
</td>
</tr>
)
)}
</tbody>
</table>
</div>
);
};
export default observer(MultiClusterStatus);
@@ -4,11 +4,9 @@ import { shallow } from "enzyme";
import { AlertStore } from "Stores/AlertStore";
import { SilenceFormStore, NewClusterRequest } from "Stores/SilenceFormStore";
import {
SilenceSubmitController,
MultiClusterStatus,
SingleClusterStatus,
} from "./SilenceSubmitController";
import SilenceSubmitController from "./SilenceSubmitController";
import SingleClusterStatus from "./SingleClusterStatus";
import MultiClusterStatus from "./MultiClusterStatus";
let alertStore: AlertStore;
let silenceFormStore: SilenceFormStore;
@@ -1,144 +1,20 @@
import React, { FC } from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowLeft } from "@fortawesome/free-solid-svg-icons/faArrowLeft";
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle";
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle";
import { AlertStore } from "Stores/AlertStore";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { SilenceSubmitProgress } from "./SilenceSubmitProgress";
const SingleClusterStatus: FC<{
alertStore: AlertStore;
silenceFormStore: SilenceFormStore;
}> = ({ silenceFormStore, alertStore }) => {
const clusterRequest = Object.values(
silenceFormStore.data.requestsByCluster
)[0];
return useObserver(() => (
<div className="text-center">
<div className="display-1 mb-3">
{clusterRequest.isDone ? (
clusterRequest.error ? (
<FontAwesomeIcon
icon={faExclamationCircle}
className="text-danger"
/>
) : (
<FontAwesomeIcon icon={faCheckCircle} className="text-success" />
)
) : (
<SilenceSubmitProgress
key={clusterRequest.cluster}
cluster={clusterRequest.cluster}
members={clusterRequest.members}
payload={silenceFormStore.data.toAlertmanagerPayload}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
)}
</div>
<div className="badge badge-primary">{clusterRequest.cluster}</div>
{clusterRequest.isDone ? (
<p
className={`mt-2 rounded text-center ${
clusterRequest.isDone && clusterRequest.error ? "bg-light" : ""
}`}
>
{clusterRequest.error ? (
clusterRequest.error
) : (
<a
href={clusterRequest.silenceLink}
target="_blank"
rel="noopener noreferrer"
>
{clusterRequest.silenceID}
</a>
)}
</p>
) : null}
</div>
));
};
const MultiClusterStatus: FC<{
alertStore: AlertStore;
silenceFormStore: SilenceFormStore;
}> = ({ silenceFormStore, alertStore }) => {
return useObserver(() => (
<div className="table-responsive">
<table className="table table-borderless">
<tbody>
{Object.values(silenceFormStore.data.requestsByCluster).map(
(clusterRequest) => (
<tr key={clusterRequest.cluster}>
<td className="align-middle" style={{ width: "1%" }}>
{clusterRequest.isDone ? (
clusterRequest.error ? (
<FontAwesomeIcon
icon={faExclamationCircle}
className="text-danger"
/>
) : (
<FontAwesomeIcon
icon={faCheckCircle}
className="text-success"
/>
)
) : (
<SilenceSubmitProgress
key={clusterRequest.cluster}
cluster={clusterRequest.cluster}
members={clusterRequest.members}
payload={silenceFormStore.data.toAlertmanagerPayload}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
)}
</td>
<td className="align-middle">{clusterRequest.cluster}</td>
<td>
<div
className={`rounded text-center ${
clusterRequest.isDone && clusterRequest.error
? "bg-light"
: ""
}`}
>
{clusterRequest.isDone ? (
clusterRequest.error ? (
clusterRequest.error
) : (
<a
href={clusterRequest.silenceLink}
target="_blank"
rel="noopener noreferrer"
>
{clusterRequest.silenceID}
</a>
)
) : null}
</div>
</td>
</tr>
)
)}
</tbody>
</table>
</div>
));
};
import SingleClusterStatus from "./SingleClusterStatus";
import MultiClusterStatus from "./MultiClusterStatus";
const SilenceSubmitController: FC<{
alertStore: AlertStore;
silenceFormStore: SilenceFormStore;
}> = ({ silenceFormStore, alertStore }) => {
return useObserver(() => (
return (
<React.Fragment>
{Object.keys(silenceFormStore.data.requestsByCluster).length === 1 ? (
<SingleClusterStatus
@@ -162,7 +38,7 @@ const SilenceSubmitController: FC<{
</button>
</div>
</React.Fragment>
));
);
};
export { SilenceSubmitController, MultiClusterStatus, SingleClusterStatus };
export default observer(SilenceSubmitController);
@@ -0,0 +1,68 @@
import React, { FC } from "react";
import { observer } from "mobx-react-lite";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle";
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle";
import { AlertStore } from "Stores/AlertStore";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { SilenceSubmitProgress } from "./SilenceSubmitProgress";
const SingleClusterStatus: FC<{
alertStore: AlertStore;
silenceFormStore: SilenceFormStore;
}> = ({ silenceFormStore, alertStore }) => {
const clusterRequest = Object.values(
silenceFormStore.data.requestsByCluster
)[0];
return (
<div className="text-center">
<div className="display-1 mb-3">
{clusterRequest.isDone ? (
clusterRequest.error ? (
<FontAwesomeIcon
icon={faExclamationCircle}
className="text-danger"
/>
) : (
<FontAwesomeIcon icon={faCheckCircle} className="text-success" />
)
) : (
<SilenceSubmitProgress
key={clusterRequest.cluster}
cluster={clusterRequest.cluster}
members={clusterRequest.members}
payload={silenceFormStore.data.toAlertmanagerPayload}
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
)}
</div>
<div className="badge badge-primary">{clusterRequest.cluster}</div>
{clusterRequest.isDone ? (
<p
className={`mt-2 rounded text-center ${
clusterRequest.isDone && clusterRequest.error ? "bg-light" : ""
}`}
>
{clusterRequest.error ? (
clusterRequest.error
) : (
<a
href={clusterRequest.silenceLink}
target="_blank"
rel="noopener noreferrer"
>
{clusterRequest.silenceID}
</a>
)}
</p>
) : null}
</div>
);
};
export default observer(SingleClusterStatus);
@@ -10,7 +10,7 @@ import { ThemeContext } from "Components/Theme";
import { AlertStore } from "Stores/AlertStore";
import { Settings } from "Stores/Settings";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { SilenceModal } from ".";
import SilenceModal from ".";
let alertStore: AlertStore;
let settingsStore: Settings;
+4 -4
View File
@@ -1,6 +1,6 @@
import React, { FC } from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBellSlash } from "@fortawesome/free-solid-svg-icons/faBellSlash";
@@ -24,7 +24,7 @@ const SilenceModal: FC<{
silenceFormStore: SilenceFormStore;
settingsStore: Settings;
}> = ({ alertStore, silenceFormStore, settingsStore }) => {
return useObserver(() => (
return (
<React.Fragment>
<li
className={`nav-item components-navbar-button ${
@@ -65,7 +65,7 @@ const SilenceModal: FC<{
</React.Suspense>
</Modal>
</React.Fragment>
));
);
};
export { SilenceModal };
export default observer(SilenceModal);
+2 -2
View File
@@ -5,7 +5,7 @@ import { mount } from "enzyme";
import toDiffableHtml from "diffable-html";
import { AlertStore } from "Stores/AlertStore";
import { AppToasts } from "./AppToasts";
import AppToasts from "./AppToasts";
let alertStore: AlertStore;
@@ -17,7 +17,7 @@ describe("<AppToasts />", () => {
it("doesn't render anything when alertStore.info.upgradeNeeded=true", () => {
alertStore.info.upgradeNeeded = true;
const tree = mount(<AppToasts alertStore={alertStore} />);
expect(tree.html()).toBeNull();
expect(tree.html()).toBe("");
});
it("renders upstream error toasts for each unhealthy upstream", () => {
+26 -28
View File
@@ -1,6 +1,6 @@
import React, { FC } from "react";
import { useObserver } from "mobx-react-lite";
import { observer } from "mobx-react-lite";
import { faArrowUp } from "@fortawesome/free-solid-svg-icons/faArrowUp";
import { faExclamation } from "@fortawesome/free-solid-svg-icons/faExclamation";
@@ -12,35 +12,33 @@ import { ToastMessage, UpgradeToastMessage } from "./ToastMessages";
const AppToasts: FC<{
alertStore: AlertStore;
}> = ({ alertStore }) => {
return useObserver(() =>
alertStore.info.upgradeNeeded ? null : (
<ToastContainer>
{alertStore.data.upstreams.instances
.filter((upstream) => upstream.error !== "")
.map((upstream) => (
<Toast
key={upstream.name}
icon={faExclamation}
iconClass="text-danger"
message={
<ToastMessage
title={`Alertmanager ${upstream.name} raised an error`}
message={upstream.error}
/>
}
/>
))}
{alertStore.info.upgradeReady ? (
return alertStore.info.upgradeNeeded ? null : (
<ToastContainer>
{alertStore.data.upstreams.instances
.filter((upstream) => upstream.error !== "")
.map((upstream) => (
<Toast
key="upgrade"
icon={faArrowUp}
iconClass="text-success"
message={<UpgradeToastMessage alertStore={alertStore} />}
key={upstream.name}
icon={faExclamation}
iconClass="text-danger"
message={
<ToastMessage
title={`Alertmanager ${upstream.name} raised an error`}
message={upstream.error}
/>
}
/>
) : null}
</ToastContainer>
)
))}
{alertStore.info.upgradeReady ? (
<Toast
key="upgrade"
icon={faArrowUp}
iconClass="text-success"
message={<UpgradeToastMessage alertStore={alertStore} />}
/>
) : null}
</ToastContainer>
);
};
export { AppToasts };
export default observer(AppToasts);