feat(ui): use toasts for error messages

This commit is contained in:
Łukasz Mierzwa
2020-09-07 14:49:37 +01:00
committed by Łukasz Mierzwa
parent 46ac4840e5
commit da5ecf182f
18 changed files with 688 additions and 126 deletions

View File

@@ -41,6 +41,11 @@ const FaviconBadge = React.lazy(() =>
default: module.FaviconBadge,
}))
);
const AppToasts = React.lazy(() =>
import("Components/Toast/AppToasts").then((module) => ({
default: module.AppToasts,
}))
);
interface AppProps {
defaultFilters: Array<string>;
@@ -138,11 +143,10 @@ const App: FunctionComponent<AppProps> = ({ defaultFilters, uiDefaults }) => {
settingsStore={settingsStore}
silenceFormStore={silenceFormStore}
/>
<AppToasts alertStore={alertStore} />
<FaviconBadge alertStore={alertStore} />
</React.Suspense>
</ThemeContext.Provider>
<React.Suspense fallback={null}>
<FaviconBadge alertStore={alertStore} />
</React.Suspense>
</ErrorBoundary>
));
};

View File

@@ -1,17 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<UpstreamError /> matches snapshot 1`] = `
"
<div class=\\"alert alert-danger text-center m-1\\"
role=\\"alert\\"
>
<h4 class=\\"alert-heading mb-0 text-wrap text-break\\">
Alertmanager
<span class=\\"font-weight-bold\\">
foo
</span>
raised an error: bar
</h4>
</div>
"
`;

View File

@@ -1,19 +0,0 @@
import React from "react";
import { shallow } from "enzyme";
import toDiffableHtml from "diffable-html";
import { MockThemeContext } from "__mocks__/Theme";
import { UpstreamError } from ".";
beforeAll(() => {
jest.spyOn(React, "useContext").mockImplementation(() => MockThemeContext);
});
describe("<UpstreamError />", () => {
it("matches snapshot", () => {
const tree = shallow(<UpstreamError name="foo" message="bar" />);
expect(toDiffableHtml(tree.html())).toMatchSnapshot();
});
});

View File

@@ -1,17 +0,0 @@
import React, { FunctionComponent } from "react";
const UpstreamError: FunctionComponent<{
name: string;
message: string;
}> = ({ name, message }) => {
return (
<div className="alert alert-danger text-center m-1" role="alert">
<h4 className="alert-heading mb-0 text-wrap text-break">
Alertmanager <span className="font-weight-bold">{name}</span> raised an
error: {message}
</h4>
</div>
);
};
export { UpstreamError };

View File

@@ -89,53 +89,6 @@ describe("<Grid />", () => {
expect(tree.text()).toBe("<FatalError />");
});
it("renders UpstreamError for each unhealthy upstream", () => {
alertStore.data.upstreams = {
counters: { total: 3, healthy: 1, failed: 2 },
instances: [
{
name: "am1",
cluster: "am",
clusterMembers: ["am1"],
uri: "http://am1",
publicURI: "http://am1",
error: "error 1",
version: "0.21.0",
readonly: false,
corsCredentials: "include",
headers: {},
},
{
name: "am2",
cluster: "am",
clusterMembers: ["am2"],
uri: "file:///mock",
publicURI: "file:///mock",
error: "",
version: "0.21.0",
readonly: false,
corsCredentials: "include",
headers: {},
},
{
name: "am3",
cluster: "am",
clusterMembers: ["am3"],
uri: "http://am3",
publicURI: "http://am3",
error: "error 2",
version: "0.21.0",
readonly: false,
corsCredentials: "include",
headers: {},
},
],
clusters: { am1: ["am1"], am2: ["am2"], am3: ["am3"] },
};
const tree = ShallowGrid();
expect(tree.text()).toBe("<UpstreamError /><UpstreamError /><AlertGrid />");
});
it("renders only FatalError on failed fetch", () => {
alertStore.status.error = "error";
alertStore.data.upstreams = {

View File

@@ -7,7 +7,6 @@ import { Settings } from "Stores/Settings";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { AlertGrid } from "./AlertGrid";
import { FatalError } from "./FatalError";
import { UpstreamError } from "./UpstreamError";
import { UpgradeNeeded } from "./UpgradeNeeded";
import { ReloadNeeded } from "./ReloadNeeded";
import { EmptyGrid } from "./EmptyGrid";
@@ -34,22 +33,11 @@ const Grid: FC<{
alertStore.info.totalAlerts === 0 ? (
<EmptyGrid />
) : (
<React.Fragment>
{alertStore.data.upstreams.instances
.filter((upstream) => upstream.error !== "")
.map((upstream) => (
<UpstreamError
key={upstream.name}
name={upstream.name}
message={upstream.error}
/>
))}
<AlertGrid
alertStore={alertStore}
settingsStore={settingsStore}
silenceFormStore={silenceFormStore}
/>
</React.Fragment>
<AlertGrid
alertStore={alertStore}
settingsStore={settingsStore}
silenceFormStore={silenceFormStore}
/>
)
);
};

View File

@@ -0,0 +1,77 @@
import React from "react";
import { mount } from "enzyme";
import toDiffableHtml from "diffable-html";
import { AlertStore } from "Stores/AlertStore";
import { AppToasts } from "./AppToasts";
let alertStore: AlertStore;
beforeEach(() => {
alertStore = new AlertStore([]);
});
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();
});
it("renders upstream error toasts for each unhealthy upstream", () => {
alertStore.data.upstreams = {
counters: { total: 3, healthy: 1, failed: 2 },
instances: [
{
name: "am1",
cluster: "am",
clusterMembers: ["am1"],
uri: "http://am1",
publicURI: "http://am1",
error: "error 1",
version: "0.21.0",
readonly: false,
corsCredentials: "include",
headers: {},
},
{
name: "am2",
cluster: "am",
clusterMembers: ["am2"],
uri: "file:///mock",
publicURI: "file:///mock",
error: "",
version: "0.21.0",
readonly: false,
corsCredentials: "include",
headers: {},
},
{
name: "am3",
cluster: "am",
clusterMembers: ["am3"],
uri: "http://am3",
publicURI: "http://am3",
error: "error 2",
version: "0.21.0",
readonly: false,
corsCredentials: "include",
headers: {},
},
],
clusters: { am1: ["am1"], am2: ["am2"], am3: ["am3"] },
};
const tree = mount(<AppToasts alertStore={alertStore} />);
expect(tree.find("Toast")).toHaveLength(2);
expect(toDiffableHtml(tree.html())).toMatchSnapshot();
});
it("renders UpgradeToastMessage when alertStore.info.upgradeReady=true", () => {
alertStore.info.upgradeReady = true;
const tree = mount(<AppToasts alertStore={alertStore} />);
expect(tree.find("UpgradeToastMessage")).toHaveLength(1);
expect(toDiffableHtml(tree.html())).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,46 @@
import React, { FC } from "react";
import { useObserver } from "mobx-react-lite";
import { faArrowUp } from "@fortawesome/free-solid-svg-icons/faArrowUp";
import { faExclamation } from "@fortawesome/free-solid-svg-icons/faExclamation";
import { AlertStore } from "Stores/AlertStore";
import { ToastContainer, Toast } from ".";
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 ? (
<Toast
key="upgrade"
icon={faArrowUp}
iconClass="text-success"
message={<UpgradeToastMessage alertStore={alertStore} />}
/>
) : null}
</ToastContainer>
)
);
};
export { AppToasts };

View File

@@ -0,0 +1,51 @@
import React from "react";
import { mount } from "enzyme";
import toDiffableHtml from "diffable-html";
import { AlertStore } from "Stores/AlertStore";
import { ToastMessage, UpgradeToastMessage } from "./ToastMessages";
let alertStore: AlertStore;
beforeEach(() => {
alertStore = new AlertStore([]);
alertStore.info.version = "1.2.3";
});
describe("<ToastMessage />", () => {
it("matches snapshot", () => {
const tree = mount(
<ToastMessage title="title string" message={<div>Div Message</div>} />
);
expect(toDiffableHtml(tree.html())).toMatchSnapshot();
});
});
describe("<UpgradeToastMessage />", () => {
it("matches snapshot", () => {
const tree = mount(<UpgradeToastMessage alertStore={alertStore} />);
expect(toDiffableHtml(tree.html())).toMatchSnapshot();
});
it("clicking on the stop button pauses page reload", () => {
const tree = mount(<UpgradeToastMessage alertStore={alertStore} />);
expect(tree.find("button").html()).toMatch(/fa-stop/);
expect(tree.find("button").text()).toBe("Stop auto-reload");
tree.find("button").simulate("click");
expect(tree.find("button").html()).toMatch(/fa-sync/);
expect(tree.find("button").text()).toBe("Reload now");
});
it("clicking on the reload buton triggers a reload", () => {
const tree = mount(<UpgradeToastMessage alertStore={alertStore} />);
tree.find("button").simulate("click");
expect(tree.find("button").text()).toBe("Reload now");
tree.find("button").simulate("click");
expect(alertStore.info.upgradeNeeded).toBe(true);
});
});

View File

@@ -0,0 +1,68 @@
import React, { FC, ReactNode, useState, useCallback } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faStop } from "@fortawesome/free-solid-svg-icons/faStop";
import { faSync } from "@fortawesome/free-solid-svg-icons/faSync";
import { AlertStore } from "Stores/AlertStore";
const ToastMessage: FC<{
title: ReactNode;
message: ReactNode;
}> = ({ title, message }) => {
return (
<div>
<div>{title}</div>
<div>
<code>{message}</code>
</div>
</div>
);
};
const UpgradeToastMessage: FC<{
alertStore: AlertStore;
}> = ({ alertStore }) => {
const [isPaused, setIsPaused] = useState<boolean>(false);
const setPause = useCallback(() => {
if (isPaused) {
alertStore.info.setUpgradeNeeded();
} else {
setIsPaused(true);
}
}, [alertStore.info, isPaused]);
return (
<div>
<div>
New version available, updates are paused until this page auto reloads
</div>
<div>
<code>{alertStore.info.version}</code>
</div>
<div className="d-flex flex-row-reverse">
<button
type="button"
className="btn btn-sm btn-light"
onClick={setPause}
>
<FontAwesomeIcon icon={isPaused ? faSync : faStop} className="mr-2" />
{isPaused ? "Reload now" : "Stop auto-reload"}
</button>
</div>
<div className="mt-2 progress bg-dark" style={{ height: 2 }}>
<div
className={`progress-bar bg-white ${
isPaused ? "" : "toast-upgrade-progressbar"
}`}
onAnimationEnd={alertStore.info.setUpgradeNeeded}
role="progressbar"
style={{ width: 0 }}
></div>
</div>
</div>
);
};
export { ToastMessage, UpgradeToastMessage };

View File

@@ -0,0 +1,183 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<AppToasts /> renders UpgradeToastMessage when alertStore.info.upgradeReady=true 1`] = `
"
<div class=\\"toast-container d-flex flex-column\\">
<div class=\\"m-1 bg-toast text-white rounded shadow components-animation-fade-appear components-animation-fade-appear-active\\">
<div class=\\"d-flex flex-row p-1\\">
<div class=\\"flex-shrink-0 flex-grow-0 align-self-center fa-stack fa-2x\\">
<svg aria-hidden=\\"true\\"
focusable=\\"false\\"
data-prefix=\\"fas\\"
data-icon=\\"circle\\"
class=\\"svg-inline--fa fa-circle fa-w-16 fa-stack-2x text-success\\"
role=\\"img\\"
xmlns=\\"http://www.w3.org/2000/svg\\"
viewbox=\\"0 0 512 512\\"
>
<path fill=\\"currentColor\\"
d=\\"M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8z\\"
>
</path>
</svg>
<svg aria-hidden=\\"true\\"
focusable=\\"false\\"
data-prefix=\\"fas\\"
data-icon=\\"arrow-up\\"
class=\\"svg-inline--fa fa-arrow-up fa-w-14 fa-stack-1x text-white\\"
role=\\"img\\"
xmlns=\\"http://www.w3.org/2000/svg\\"
viewbox=\\"0 0 448 512\\"
>
<path fill=\\"currentColor\\"
d=\\"M34.9 289.5l-22.2-22.2c-9.4-9.4-9.4-24.6 0-33.9L207 39c9.4-9.4 24.6-9.4 33.9 0l194.3 194.3c9.4 9.4 9.4 24.6 0 33.9L413 289.4c-9.5 9.5-25 9.3-34.3-.4L264 168.6V456c0 13.3-10.7 24-24 24h-32c-13.3 0-24-10.7-24-24V168.6L69.2 289.1c-9.3 9.8-24.8 10-34.3.4z\\"
>
</path>
</svg>
</div>
<div class=\\"flex-shrink-1 flex-grow-1 align-self-center ml-1 mr-3 text-break text-wrap\\">
<div>
<div>
New version available, updates are paused until this page auto reloads
</div>
<div>
<code>
unknown
</code>
</div>
<div class=\\"d-flex flex-row-reverse\\">
<button type=\\"button\\"
class=\\"btn btn-sm btn-light\\"
>
<svg aria-hidden=\\"true\\"
focusable=\\"false\\"
data-prefix=\\"fas\\"
data-icon=\\"stop\\"
class=\\"svg-inline--fa fa-stop fa-w-14 mr-2\\"
role=\\"img\\"
xmlns=\\"http://www.w3.org/2000/svg\\"
viewbox=\\"0 0 448 512\\"
>
<path fill=\\"currentColor\\"
d=\\"M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48z\\"
>
</path>
</svg>
Stop auto-reload
</button>
</div>
<div class=\\"mt-2 progress bg-dark\\"
style=\\"height: 2px;\\"
>
<div class=\\"progress-bar bg-white toast-upgrade-progressbar\\"
role=\\"progressbar\\"
style=\\"width: 0px;\\"
>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
"
`;
exports[`<AppToasts /> renders upstream error toasts for each unhealthy upstream 1`] = `
"
<div class=\\"toast-container d-flex flex-column\\">
<div class=\\"m-1 bg-toast text-white rounded shadow components-animation-fade-appear components-animation-fade-appear-active\\">
<div class=\\"d-flex flex-row p-1\\">
<div class=\\"flex-shrink-0 flex-grow-0 align-self-center fa-stack fa-2x\\">
<svg aria-hidden=\\"true\\"
focusable=\\"false\\"
data-prefix=\\"fas\\"
data-icon=\\"circle\\"
class=\\"svg-inline--fa fa-circle fa-w-16 fa-stack-2x text-danger\\"
role=\\"img\\"
xmlns=\\"http://www.w3.org/2000/svg\\"
viewbox=\\"0 0 512 512\\"
>
<path fill=\\"currentColor\\"
d=\\"M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8z\\"
>
</path>
</svg>
<svg aria-hidden=\\"true\\"
focusable=\\"false\\"
data-prefix=\\"fas\\"
data-icon=\\"exclamation\\"
class=\\"svg-inline--fa fa-exclamation fa-w-6 fa-stack-1x text-white\\"
role=\\"img\\"
xmlns=\\"http://www.w3.org/2000/svg\\"
viewbox=\\"0 0 192 512\\"
>
<path fill=\\"currentColor\\"
d=\\"M176 432c0 44.112-35.888 80-80 80s-80-35.888-80-80 35.888-80 80-80 80 35.888 80 80zM25.26 25.199l13.6 272C39.499 309.972 50.041 320 62.83 320h66.34c12.789 0 23.331-10.028 23.97-22.801l13.6-272C167.425 11.49 156.496 0 142.77 0H49.23C35.504 0 24.575 11.49 25.26 25.199z\\"
>
</path>
</svg>
</div>
<div class=\\"flex-shrink-1 flex-grow-1 align-self-center ml-1 mr-3 text-break text-wrap\\">
<div>
<div>
Alertmanager am1 raised an error
</div>
<div>
<code>
error 1
</code>
</div>
</div>
</div>
</div>
</div>
<div class=\\"m-1 bg-toast text-white rounded shadow components-animation-fade-appear components-animation-fade-appear-active\\">
<div class=\\"d-flex flex-row p-1\\">
<div class=\\"flex-shrink-0 flex-grow-0 align-self-center fa-stack fa-2x\\">
<svg aria-hidden=\\"true\\"
focusable=\\"false\\"
data-prefix=\\"fas\\"
data-icon=\\"circle\\"
class=\\"svg-inline--fa fa-circle fa-w-16 fa-stack-2x text-danger\\"
role=\\"img\\"
xmlns=\\"http://www.w3.org/2000/svg\\"
viewbox=\\"0 0 512 512\\"
>
<path fill=\\"currentColor\\"
d=\\"M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8z\\"
>
</path>
</svg>
<svg aria-hidden=\\"true\\"
focusable=\\"false\\"
data-prefix=\\"fas\\"
data-icon=\\"exclamation\\"
class=\\"svg-inline--fa fa-exclamation fa-w-6 fa-stack-1x text-white\\"
role=\\"img\\"
xmlns=\\"http://www.w3.org/2000/svg\\"
viewbox=\\"0 0 192 512\\"
>
<path fill=\\"currentColor\\"
d=\\"M176 432c0 44.112-35.888 80-80 80s-80-35.888-80-80 35.888-80 80-80 80 35.888 80 80zM25.26 25.199l13.6 272C39.499 309.972 50.041 320 62.83 320h66.34c12.789 0 23.331-10.028 23.97-22.801l13.6-272C167.425 11.49 156.496 0 142.77 0H49.23C35.504 0 24.575 11.49 25.26 25.199z\\"
>
</path>
</svg>
</div>
<div class=\\"flex-shrink-1 flex-grow-1 align-self-center ml-1 mr-3 text-break text-wrap\\">
<div>
<div>
Alertmanager am3 raised an error
</div>
<div>
<code>
error 2
</code>
</div>
</div>
</div>
</div>
</div>
</div>
"
`;

View File

@@ -0,0 +1,63 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ToastMessage /> matches snapshot 1`] = `
"
<div>
<div>
title string
</div>
<div>
<code>
<div>
Div Message
</div>
</code>
</div>
</div>
"
`;
exports[`<UpgradeToastMessage /> matches snapshot 1`] = `
"
<div>
<div>
New version available, updates are paused until this page auto reloads
</div>
<div>
<code>
1.2.3
</code>
</div>
<div class=\\"d-flex flex-row-reverse\\">
<button type=\\"button\\"
class=\\"btn btn-sm btn-light\\"
>
<svg aria-hidden=\\"true\\"
focusable=\\"false\\"
data-prefix=\\"fas\\"
data-icon=\\"stop\\"
class=\\"svg-inline--fa fa-stop fa-w-14 mr-2\\"
role=\\"img\\"
xmlns=\\"http://www.w3.org/2000/svg\\"
viewbox=\\"0 0 448 512\\"
>
<path fill=\\"currentColor\\"
d=\\"M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48z\\"
>
</path>
</svg>
Stop auto-reload
</button>
</div>
<div class=\\"mt-2 progress bg-dark\\"
style=\\"height: 2px;\\"
>
<div class=\\"progress-bar bg-white toast-upgrade-progressbar\\"
role=\\"progressbar\\"
style=\\"width: 0px;\\"
>
</div>
</div>
</div>
"
`;

View File

@@ -0,0 +1,38 @@
import React from "react";
import { storiesOf } from "@storybook/react";
import { faArrowUp } from "@fortawesome/free-solid-svg-icons/faArrowUp";
import { faExclamation } from "@fortawesome/free-solid-svg-icons/faExclamation";
import { AlertStore } from "Stores/AlertStore";
import { Toast } from ".";
import { ToastMessage, UpgradeToastMessage } from "./ToastMessages";
import "Styles/Percy.scss";
storiesOf("AppToasts", module).add("AppToasts", () => {
const alertStore = new AlertStore([]);
alertStore.info.version = "999.99.0";
return (
<div className="d-flex flex-column">
<Toast
icon={faExclamation}
iconClass="text-danger"
message={
<ToastMessage
title={`Alertmanager am1 raised an error`}
message="connection refused"
/>
}
/>
<Toast
key="upgrade"
icon={faArrowUp}
iconClass="text-success"
message={<UpgradeToastMessage alertStore={alertStore} />}
/>
</div>
);
});

View File

@@ -0,0 +1,60 @@
import React, { FC, ReactNode } from "react";
import ReactDOM from "react-dom";
import TransitionGroup from "react-transition-group/TransitionGroup";
import { CSSTransition } from "react-transition-group";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCircle } from "@fortawesome/free-solid-svg-icons/faCircle";
import { ThemeContext } from "Components/Theme";
const Toast: FC<{
icon: IconDefinition;
iconClass: string;
message: ReactNode;
}> = ({ icon, iconClass, message }) => {
return (
<div className="m-1 bg-toast text-white rounded shadow">
<div className="d-flex flex-row p-1">
<div className="flex-shrink-0 flex-grow-0 align-self-center fa-stack fa-2x">
<FontAwesomeIcon
icon={faCircle}
className={`fa-stack-2x ${iconClass}`}
/>
<FontAwesomeIcon icon={icon} className="fa-stack-1x text-white" />
</div>
<div className="flex-shrink-1 flex-grow-1 align-self-center ml-1 mr-3 text-break text-wrap">
{message}
</div>
</div>
</div>
);
};
const ToastContainer: FC = ({ children }) => {
const context = React.useContext(ThemeContext);
return ReactDOM.createPortal(
<div className="toast-container d-flex flex-column">
<TransitionGroup component={null} appear enter exit>
{React.Children.map(children, (toast, i) =>
toast ? (
<CSSTransition
key={i}
classNames="components-animation-fade"
timeout={context.animations.duration}
unmountOnExit
>
{toast}
</CSSTransition>
) : null
)}
</TransitionGroup>
</div>,
document.body
);
};
export { ToastContainer, Toast };

View File

@@ -153,6 +153,37 @@ describe("AlertStore.status", () => {
store.status.togglePause();
expect(store.status.paused).toBe(false);
});
it("togglePause() always leaves store paused if it's stopped", () => {
const store = new AlertStore([]);
expect(store.status.paused).toBe(false);
store.status.stop();
expect(store.status.paused).toBe(true);
store.status.togglePause();
expect(store.status.paused).toBe(true);
store.status.togglePause();
expect(store.status.paused).toBe(true);
});
it("stop() enforces a pause", () => {
const store = new AlertStore([]);
expect(store.status.paused).toBe(false);
store.status.stop();
expect(store.status.paused).toBe(true);
store.status.resume();
expect(store.status.paused).toBe(true);
});
});
describe("AlertStore.info", () => {
it("setUpgradeNeeded() sets upgradeNeeded to true", () => {
const store = new AlertStore([]);
expect(store.info.upgradeNeeded).toBe(false);
store.info.setUpgradeNeeded();
expect(store.info.upgradeNeeded).toBe(true);
store.info.setUpgradeNeeded();
expect(store.info.upgradeNeeded).toBe(true);
});
});
describe("AlertStore.filters", () => {
@@ -609,7 +640,7 @@ describe("AlertStore.fetch", () => {
});
const store = new AlertStore(["label=value"]);
await expect(store.fetch("", false, "", "", "")).resolves.toBeUndefined();
expect(store.info.upgradeNeeded).toBe(false);
expect(store.info.upgradeReady).toBe(false);
response.version = "newFakeVersion";
fetchMock.reset();
@@ -617,7 +648,7 @@ describe("AlertStore.fetch", () => {
body: JSON.stringify(response),
});
await expect(store.fetch("", false, "", "", "")).resolves.toBeUndefined();
expect(store.info.upgradeNeeded).toBe(true);
expect(store.info.upgradeReady).toBe(true);
});
it("adds new groups to the store after fetch", () => {

View File

@@ -259,6 +259,7 @@ class AlertStore {
},
totalAlerts: 0,
version: "unknown",
upgradeReady: false as boolean,
upgradeNeeded: false as boolean,
isRetrying: false as boolean,
reloadNeeded: false as boolean,
@@ -268,6 +269,9 @@ class AlertStore {
clearIsRetrying() {
this.isRetrying = false;
},
setUpgradeNeeded() {
this.upgradeNeeded = true;
},
setReloadNeeded() {
this.reloadNeeded = true;
},
@@ -275,7 +279,8 @@ class AlertStore {
{
setIsRetrying: action.bound,
clearIsRetrying: action.bound,
setReloadNeeded: action,
setReloadNeeded: action.bound,
setUpgradeNeeded: action.bound,
},
{ name: "API response info" }
);
@@ -319,6 +324,7 @@ class AlertStore {
value: AlertStoreStatuses.Idle,
lastUpdateAt: 0 as number | Date,
error: null as null | string,
stopped: false as boolean,
paused: false as boolean,
setIdle() {
this.value = AlertStoreStatuses.Idle;
@@ -341,10 +347,14 @@ class AlertStore {
this.paused = true;
},
resume() {
this.paused = false;
this.paused = this.stopped ? true : false;
},
togglePause() {
this.paused = !this.paused;
this.paused = this.stopped ? true : !this.paused;
},
stop() {
this.paused = true;
this.stopped = true;
},
},
{
@@ -355,6 +365,7 @@ class AlertStore {
pause: action.bound,
resume: action.bound,
togglePause: action.bound,
stop: action.bound,
},
{ name: "Store status" }
);
@@ -467,7 +478,8 @@ class AlertStore {
this.info.version !== "unknown" &&
this.info.version !== result.version
) {
this.info.upgradeNeeded = true;
this.info.upgradeReady = true;
this.status.stop();
}
// update extra root level keys that are stored under 'info'
this.info.totalAlerts = result.totalAlerts;

View File

@@ -0,0 +1,40 @@
.toast-container {
position: fixed;
bottom: 0.5rem;
right: 0.6rem;
z-index: 500;
max-width: 500px;
code {
color: $warning;
}
}
@media screen and (max-width: 600px) {
.toast-container {
max-width: 100%;
bottom: 0;
right: 0;
}
}
.bg-toast {
background-color: darken($dark, 5%);
}
.toast-upgrade-progressbar {
animation-duration: 20s;
animation-name: upgradeProgress;
animation-timing-function: linear;
}
@keyframes upgradeProgress {
0% {
width: 0%;
}
100% {
width: 100%;
}
}

View File

@@ -24,3 +24,4 @@
@import "Pagination";
@import "Tooltip";
@import "Fetcher";
@import "Toast";