diff --git a/CHANGELOG.md b/CHANGELOG.md index 600fd554d..36fb840ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ - Easily change multi-grid source label via quick access dropdown on the grid header. +### Changed + +- Reworked how notifications are displayed. + ## v0.79 ### Fixed diff --git a/ui/src/App.tsx b/ui/src/App.tsx index f308d7b0b..afcbadca0 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -28,7 +28,6 @@ import "Styles/App.scss"; 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; @@ -131,7 +130,6 @@ const App: FunctionComponent = observer( settingsStore={settingsStore} silenceFormStore={silenceFormStore} /> - diff --git a/ui/src/Components/MainModal/index.tsx b/ui/src/Components/MainModal/index.tsx index 690885be7..646129f7a 100644 --- a/ui/src/Components/MainModal/index.tsx +++ b/ui/src/Components/MainModal/index.tsx @@ -29,7 +29,7 @@ const MainModal: FC<{
  • - +
  • diff --git a/ui/src/Components/NavBar/FilterInput/index.tsx b/ui/src/Components/NavBar/FilterInput/index.tsx index a08a9b498..1e131f441 100644 --- a/ui/src/Components/NavBar/FilterInput/index.tsx +++ b/ui/src/Components/NavBar/FilterInput/index.tsx @@ -147,7 +147,11 @@ const FilterInput: FC<{ return ( // data-filters is there to register filters for observation in mobx // in order to re-render input component -
    +
    { ); }; -const ValidateNavClass = (totalFilters: number, expectedClass: string) => { - for (let i = 0; i < totalFilters; i++) { - alertStore.filters.addFilter(`foo=${i}`); - } - const tree = MountedNavbar(); - const nav = tree.find("ul.navbar-nav"); - expect((nav.props().className as string).split(" ")).toContain(expectedClass); -}; - describe("", () => { it("navbar-brand shows 15 alerts with totalAlerts=15", () => { alertStore.info.setTotalAlerts(15); @@ -76,22 +67,6 @@ describe("", () => { expect(brand.text()).toBe("15"); }); - it("navbar-nav includes 'flex-row' class with 0 filters", () => { - ValidateNavClass(0, "flex-row"); - }); - - it("navbar-nav includes 'flex-row' class with 1 filter", () => { - ValidateNavClass(1, "flex-column"); - }); - - it("navbar-nav includes 'flex-column' class with 2 filters", () => { - ValidateNavClass(2, "flex-column"); - }); - - it("navbar-nav includes 'flex-column' class with 3 filters", () => { - ValidateNavClass(3, "flex-column"); - }); - it("navbar includes 'fixed-top' class by default", () => { const tree = MountedNavbar(); const nav = tree.find(".navbar"); diff --git a/ui/src/Components/NavBar/index.tsx b/ui/src/Components/NavBar/index.tsx index f0707f4c4..85ee44f2c 100644 --- a/ui/src/Components/NavBar/index.tsx +++ b/ui/src/Components/NavBar/index.tsx @@ -16,6 +16,7 @@ import { IsMobile } from "Common/Device"; import { OverviewModal } from "Components/OverviewModal"; import { MainModal } from "Components/MainModal"; import SilenceModal from "Components/SilenceModal"; +import AppToasts from "Components/Toast/AppToasts"; import { ThemeContext } from "Components/Theme"; import { Fetcher } from "Components/Fetcher"; import { FilterInput } from "./FilterInput"; @@ -102,23 +103,19 @@ const NavBar: FC<{ >
    diff --git a/ui/src/Components/SilenceModal/index.tsx b/ui/src/Components/SilenceModal/index.tsx index 71b3892c8..cdf06c529 100644 --- a/ui/src/Components/SilenceModal/index.tsx +++ b/ui/src/Components/SilenceModal/index.tsx @@ -29,7 +29,7 @@ const SilenceModal: FC<{
  • - +
  • diff --git a/ui/src/Components/Toast/AppToasts.test.tsx b/ui/src/Components/Toast/AppToasts.test.tsx index f66931911..c9ff616ed 100644 --- a/ui/src/Components/Toast/AppToasts.test.tsx +++ b/ui/src/Components/Toast/AppToasts.test.tsx @@ -1,5 +1,7 @@ import React from "react"; +import { act } from "react-dom/test-utils"; + import { mount } from "enzyme"; import toDiffableHtml from "diffable-html"; @@ -13,6 +15,51 @@ beforeEach(() => { alertStore = new AlertStore([]); }); +const makeErrors = () => { + alertStore.data.setUpstreams({ + 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"] }, + }); +}; + describe("", () => { it("doesn't render anything when alertStore.info.upgradeNeeded=true", () => { alertStore.info.upgradeNeeded = true; @@ -20,9 +67,25 @@ describe("", () => { expect(tree.html()).toBe(""); }); + it("doesn't render anything when there are no notifications to show", () => { + const tree = mount(); + expect(tree.html()).toBe(""); + }); + it("renders upstream error toasts for each unhealthy upstream", () => { + makeErrors(); + const tree = mount(); + expect(tree.find("Toast")).toHaveLength(2); + expect(toDiffableHtml(tree.html())).toMatchSnapshot(); + }); + + it("removes notifications when upstream recovers", () => { + makeErrors(); + const tree = mount(); + expect(tree.find("Toast")).toHaveLength(2); + alertStore.data.setUpstreams({ - counters: { total: 3, healthy: 1, failed: 2 }, + counters: { total: 3, healthy: 3, failed: 0 }, instances: [ { name: "am1", @@ -30,7 +93,7 @@ describe("", () => { clusterMembers: ["am1"], uri: "http://am1", publicURI: "http://am1", - error: "error 1", + error: "", version: "0.21.0", readonly: false, corsCredentials: "include", @@ -54,7 +117,7 @@ describe("", () => { clusterMembers: ["am3"], uri: "http://am3", publicURI: "http://am3", - error: "error 2", + error: "", version: "0.21.0", readonly: false, corsCredentials: "include", @@ -63,9 +126,24 @@ describe("", () => { ], clusters: { am1: ["am1"], am2: ["am2"], am3: ["am3"] }, }); + tree.update(); + expect(tree.find("Toast")).toHaveLength(0); + }); + + it("clicking navbar icon toggles all notifications", () => { + makeErrors(); + alertStore.info.upgradeNeeded = false; + alertStore.info.upgradeReady = false; const tree = mount(); - expect(tree.find("Toast")).toHaveLength(2); - expect(toDiffableHtml(tree.html())).toMatchSnapshot(); + expect(tree.find("div.bg-toast")).toHaveLength(2); + expect(tree.find("span.badge.cursor-pointer.with-click")).toHaveLength(2); + + tree.find("span.badge.cursor-pointer.with-click").at(1).simulate("click"); + expect(tree.find("div.bg-toast")).toHaveLength(1); + + tree.find("span#components-notifications").simulate("click"); + tree.update(); + expect(tree.find("div.bg-toast")).toHaveLength(2); }); it("renders UpgradeToastMessage when alertStore.info.upgradeReady=true", () => { diff --git a/ui/src/Components/Toast/AppToasts.tsx b/ui/src/Components/Toast/AppToasts.tsx index 9bafa44e1..c69debffe 100644 --- a/ui/src/Components/Toast/AppToasts.tsx +++ b/ui/src/Components/Toast/AppToasts.tsx @@ -1,22 +1,51 @@ -import React, { FC } from "react"; +import React, { FC, Fragment, useCallback } from "react"; import { observer } from "mobx-react-lite"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faArrowUp } from "@fortawesome/free-solid-svg-icons/faArrowUp"; import { faExclamation } from "@fortawesome/free-solid-svg-icons/faExclamation"; +import { faInfoCircle } from "@fortawesome/free-solid-svg-icons/faInfoCircle"; import { AlertStore } from "Stores/AlertStore"; +import { TooltipWrapper } from "Components/TooltipWrapper"; import { ToastContainer, Toast } from "."; import { ToastMessage, UpgradeToastMessage } from "./ToastMessages"; const AppToasts: FC<{ alertStore: AlertStore; }> = ({ alertStore }) => { - return alertStore.info.upgradeNeeded ? null : ( - - {alertStore.data.upstreams.instances - .filter((upstream) => upstream.error !== "") - .map((upstream) => ( + const show = useCallback(() => { + const e = new CustomEvent("showNotifications"); + window.dispatchEvent(e); + }, []); + + if (alertStore.info.upgradeNeeded) { + return null; + } + + if ( + alertStore.data.upstreamsWithErrors.length === 0 && + alertStore.info.upgradeReady === false + ) { + return null; + } + + return ( + +
  • + + + + + +
  • + + {alertStore.data.upstreamsWithErrors.map((upstream) => ( ))} - {alertStore.info.upgradeReady ? ( - } - /> - ) : null} - + {alertStore.info.upgradeReady ? ( + } + /> + ) : null} +
    + ); }; diff --git a/ui/src/Components/Toast/__snapshots__/AppToasts.test.tsx.snap b/ui/src/Components/Toast/__snapshots__/AppToasts.test.tsx.snap index 24e7be143..16a2f59dc 100644 --- a/ui/src/Components/Toast/__snapshots__/AppToasts.test.tsx.snap +++ b/ui/src/Components/Toast/__snapshots__/AppToasts.test.tsx.snap @@ -2,6 +2,28 @@ exports[` renders UpgradeToastMessage when alertStore.info.upgradeReady=true 1`] = ` " +
  • +
    + + + + + + +
    +
  • @@ -78,19 +100,18 @@ exports[` renders UpgradeToastMessage when alertStore.info.upgradeR
    - + @@ -104,6 +125,28 @@ exports[` renders UpgradeToastMessage when alertStore.info.upgradeR exports[` renders upstream error toasts for each unhealthy upstream 1`] = ` " +
  • +
    + + + + + + +
    +
  • @@ -150,19 +193,18 @@ exports[` renders upstream error toasts for each unhealthy upstream
    - + @@ -215,19 +257,18 @@ exports[` renders upstream error toasts for each unhealthy upstream
    - + diff --git a/ui/src/Components/Toast/index.test.tsx b/ui/src/Components/Toast/index.test.tsx index 72f9b0535..d9060914c 100644 --- a/ui/src/Components/Toast/index.test.tsx +++ b/ui/src/Components/Toast/index.test.tsx @@ -1,5 +1,7 @@ import React from "react"; +import { act } from "react-dom/test-utils"; + import { mount } from "enzyme"; import toDiffableHtml from "diffable-html"; @@ -20,7 +22,7 @@ describe("", () => { expect(toDiffableHtml(tree.html())).toMatch(/fake error/); }); - it("toggles body on toggle icon click", () => { + it("hides body on close icon click", () => { const tree = mount( ", () => { ); expect(toDiffableHtml(tree.html())).toMatch(/fake error/); - tree.find("svg.cursor-pointer").simulate("click"); + tree.find("span.badge.cursor-pointer").simulate("click"); + expect(toDiffableHtml(tree.html())).not.toMatch(/fake error/); + }); + + it("shows hidden body on showNotifications event", () => { + const tree = mount( + + ); + expect(toDiffableHtml(tree.html())).toMatch(/fake error/); + + tree.find("span.badge.cursor-pointer").simulate("click"); expect(toDiffableHtml(tree.html())).not.toMatch(/fake error/); - tree.find("svg.cursor-pointer").simulate("click"); + const e = new CustomEvent("showNotifications"); + act(() => { + window.dispatchEvent(e); + }); + tree.update(); expect(toDiffableHtml(tree.html())).toMatch(/fake error/); }); + + it("unmounts cleanly", () => { + const tree = mount( + + ); + tree.unmount(); + }); }); diff --git a/ui/src/Components/Toast/index.tsx b/ui/src/Components/Toast/index.tsx index ee8b5e004..36a6baba4 100644 --- a/ui/src/Components/Toast/index.tsx +++ b/ui/src/Components/Toast/index.tsx @@ -1,4 +1,4 @@ -import React, { FC, ReactNode, useState } from "react"; +import React, { FC, ReactNode, useState, useEffect } from "react"; import ReactDOM from "react-dom"; import TransitionGroup from "react-transition-group/TransitionGroup"; @@ -7,9 +7,9 @@ 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 { faTimes } from "@fortawesome/free-solid-svg-icons/faTimes"; import { ThemeContext } from "Components/Theme"; -import { ToggleIcon } from "Components/ToggleIcon"; const Toast: FC<{ icon: IconDefinition; @@ -18,6 +18,16 @@ const Toast: FC<{ }> = ({ icon, iconClass, message }) => { const [isOpen, setIsOpen] = useState(true); + useEffect(() => { + const show = () => setIsOpen(true); + window.addEventListener("showNotifications", show); + return () => { + window.removeEventListener("showNotifications", show); + }; + }, []); + + if (!isOpen) return null; + return (
    @@ -29,15 +39,14 @@ const Toast: FC<{
    - {isOpen ? message : null} + {message}
    - - setIsOpen((v) => !v)} - /> + setIsOpen(false)} + > +
    diff --git a/ui/src/Stores/AlertStore.ts b/ui/src/Stores/AlertStore.ts index 4e3b7a98e..00e53a5c1 100644 --- a/ui/src/Stores/AlertStore.ts +++ b/ui/src/Stores/AlertStore.ts @@ -152,6 +152,7 @@ interface AlertStoreDataT { setUpstreams: (u: APIAlertsResponseUpstreamsT) => void; setInstances: (i: APIAlertmanagerUpstreamT[]) => void; setClusters: (c: APIAlertsResponseUpstreamsClusterMapT) => void; + readonly upstreamsWithErrors: APIAlertmanagerUpstreamT[]; } interface AlertStoreInfoT { @@ -343,6 +344,11 @@ class AlertStore { setClusters(c: APIAlertsResponseUpstreamsClusterMapT) { this.upstreams.clusters = c; }, + get upstreamsWithErrors(): APIAlertmanagerUpstreamT[] { + return this.upstreams.instances.filter( + (upstream) => upstream.error !== "" + ); + }, }, { gridPadding: computed,