mirror of
https://github.com/prymitive/karma
synced 2026-05-09 03:36:44 +00:00
feat(ui): use toasts for error messages
This commit is contained in:
committed by
Łukasz Mierzwa
parent
46ac4840e5
commit
da5ecf182f
@@ -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>
|
||||
));
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
"
|
||||
`;
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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 };
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
77
ui/src/Components/Toast/AppToasts.test.tsx
Normal file
77
ui/src/Components/Toast/AppToasts.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
46
ui/src/Components/Toast/AppToasts.tsx
Normal file
46
ui/src/Components/Toast/AppToasts.tsx
Normal 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 };
|
||||
51
ui/src/Components/Toast/ToastMessages.test.tsx
Normal file
51
ui/src/Components/Toast/ToastMessages.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
||||
68
ui/src/Components/Toast/ToastMessages.tsx
Normal file
68
ui/src/Components/Toast/ToastMessages.tsx
Normal 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 };
|
||||
183
ui/src/Components/Toast/__snapshots__/AppToasts.test.tsx.snap
Normal file
183
ui/src/Components/Toast/__snapshots__/AppToasts.test.tsx.snap
Normal 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>
|
||||
"
|
||||
`;
|
||||
@@ -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>
|
||||
"
|
||||
`;
|
||||
38
ui/src/Components/Toast/index.stories.tsx
Normal file
38
ui/src/Components/Toast/index.stories.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
60
ui/src/Components/Toast/index.tsx
Normal file
60
ui/src/Components/Toast/index.tsx
Normal 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 };
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
40
ui/src/Styles/Components/Toast.scss
Normal file
40
ui/src/Styles/Components/Toast.scss
Normal 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%;
|
||||
}
|
||||
}
|
||||
@@ -24,3 +24,4 @@
|
||||
@import "Pagination";
|
||||
@import "Tooltip";
|
||||
@import "Fetcher";
|
||||
@import "Toast";
|
||||
|
||||
Reference in New Issue
Block a user