From ebde58a281057a3e6d78fc9202b0c22784fd267b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Fri, 19 Apr 2019 21:09:37 +0100 Subject: [PATCH] feat(ui): add colored second display style for titlebar This enables second mode for titlebar styling - enabling it will cause the background to be set to the same color as the alerts, but only if all alerts in a group are in the same state. --- ui/src/Common/Colors.js | 10 ++- .../AlertGroup/GroupHeader/GroupMenu.js | 9 ++- .../AlertGroup/GroupHeader/GroupMenu.test.js | 14 ++-- .../AlertGrid/AlertGroup/GroupHeader/index.js | 27 ++++++-- .../Grid/AlertGrid/AlertGroup/index.js | 24 ++++++- .../Grid/AlertGrid/AlertGroup/index.test.js | 65 +++++++++++++++++++ .../Labels/FilteringCounterBadge/index.js | 18 +++-- .../FilteringCounterBadge/index.test.js | 37 ++++++----- .../Configuration/AlertGroupTitleBarColor.js | 57 ++++++++++++++++ .../AlertGroupTitleBarColor.test.js | 54 +++++++++++++++ .../AlertGroupTitleBarColor.test.js.snap | 27 ++++++++ .../__snapshots__/index.test.js.snap | 23 +++++++ .../MainModal/Configuration/index.js | 3 + .../MainModalContent.test.js.snap | 23 +++++++ ui/src/Stores/Settings.js | 3 +- 15 files changed, 358 insertions(+), 36 deletions(-) create mode 100644 ui/src/Components/MainModal/Configuration/AlertGroupTitleBarColor.js create mode 100644 ui/src/Components/MainModal/Configuration/AlertGroupTitleBarColor.test.js create mode 100644 ui/src/Components/MainModal/Configuration/__snapshots__/AlertGroupTitleBarColor.test.js.snap diff --git a/ui/src/Common/Colors.js b/ui/src/Common/Colors.js index f1429a46f..3ca90caf1 100644 --- a/ui/src/Common/Colors.js +++ b/ui/src/Common/Colors.js @@ -13,6 +13,7 @@ const StateLabelClassMap = Object.freeze({ suppressed: "badge-success components-label-dark", unprocessed: "badge-secondary components-label-bright" }); + // same but for borders const BorderClassMap = Object.freeze({ active: "border-danger", @@ -20,10 +21,17 @@ const BorderClassMap = Object.freeze({ unprocessed: "border-secondary" }); +const BackgroundClassMap = Object.freeze({ + active: "bg-danger", + suppressed: "bg-success", + unprocessed: "bg-secondary" +}); + export { DefaultLabelClass, StaticColorLabelClass, AlertNameLabelClass, StateLabelClassMap, - BorderClassMap + BorderClassMap, + BackgroundClassMap }; diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.js index 0908b0533..cd27dd9cc 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.js @@ -86,7 +86,8 @@ const GroupMenu = observer( class GroupMenu extends Component { static propTypes = { group: APIGroup.isRequired, - silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired + silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired, + themed: PropTypes.bool.isRequired }; collapse = observable( @@ -108,7 +109,7 @@ const GroupMenu = observer( }); render() { - const { group, silenceFormStore } = this.props; + const { group, silenceFormStore, themed } = this.props; return ( @@ -117,7 +118,9 @@ const GroupMenu = observer( { const MockAfterClick = jest.fn(); -const MountedGroupMenu = group => { +const MountedGroupMenu = (group, themed) => { return mount( - + ).find("GroupMenu"); }; @@ -32,13 +36,13 @@ const MountedGroupMenu = group => { describe("", () => { it("is collapsed by default", () => { const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {}, {}); - const tree = MountedGroupMenu(group); + const tree = MountedGroupMenu(group, true); expect(tree.instance().collapse.value).toBe(true); }); it("clicking toggle sets collapse value to 'false'", () => { const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {}, {}); - const tree = MountedGroupMenu(group); + const tree = MountedGroupMenu(group, true); const toggle = tree.find(".cursor-pointer"); toggle.simulate("click"); expect(tree.instance().collapse.value).toBe(false); @@ -46,7 +50,7 @@ describe("", () => { it("handleClickOutside() call sets collapse value to 'true'", () => { const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {}, {}); - const tree = MountedGroupMenu(group); + const tree = MountedGroupMenu(group, true); const toggle = tree.find(".cursor-pointer"); toggle.simulate("click"); diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/index.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/index.js index 1765f8978..578f647a7 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/index.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/index.js @@ -22,20 +22,32 @@ const GroupHeader = observer( toggle: PropTypes.func.isRequired }).isRequired, group: APIGroup.isRequired, - silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired + silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired, + headerBackgroundClass: PropTypes.string.isRequired, + themedCounters: PropTypes.bool.isRequired }; render() { - const { collapseStore, group, silenceFormStore } = this.props; + const { + collapseStore, + group, + silenceFormStore, + headerBackgroundClass, + themedCounters + } = this.props; return (
- + {Object.keys(group.labels).map(name => ( @@ -51,19 +63,24 @@ const GroupHeader = observer( name="@state" value="unprocessed" counter={group.stateCount.unprocessed} + themed={themedCounters} /> diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.js index 235eefa34..dd27f11d7 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.js @@ -14,6 +14,7 @@ import { APIGroup } from "Models/API"; import { Settings } from "Stores/Settings"; import { SilenceFormStore } from "Stores/SilenceFormStore"; import { IsMobile } from "Common/Device"; +import { BackgroundClassMap, BorderClassMap } from "Common/Colors"; import { MountFade } from "Components/Animations/MountFade"; import { TooltipWrapper } from "Components/TooltipWrapper"; import { GroupHeader } from "./GroupHeader"; @@ -156,6 +157,7 @@ const AlertGroup = observer( showAlertmanagers, afterUpdate, silenceFormStore, + settingsStore, style } = this.props; @@ -176,14 +178,34 @@ const AlertGroup = observer( } } + let themedCounters = true; + const groupClassesMap = { + background: "bg-light", + border: "border-light" + }; + + if (settingsStore.alertGroupConfig.config.colorTitleBar) { + const stateList = Object.entries(group.stateCount) + .filter(([k, v]) => v !== 0) + .map(([k, _]) => k); + if (stateList.length === 1) { + const state = stateList.pop(); + groupClassesMap.background = BackgroundClassMap[state]; + groupClassesMap.border = BorderClassMap[state]; + themedCounters = false; + } + } + return (
-
+
{this.collapse.value ? null : (
diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.js index 4bebce2c3..b4cc83977 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.js @@ -274,3 +274,68 @@ describe(" renderConfig", () => { ValidateLoadButtonAction(25, 1, /fa-plus/, 22, 17); }); }); + +describe(" theme", () => { + it("renders bg-light border when colorTitleBar=false", () => { + settingsStore.alertGroupConfig.config.colorTitleBar = false; + group.stateCount = { active: 5, suppressed: 0, unprocessed: 0 }; + const tree = MountedAlertGroup(jest.fn(), false); + expect(tree.find(".card").hasClass("border-light")).toBe(true); + expect(tree.find(".card").hasClass("border-danger")).toBe(false); + expect(tree.find(".card").hasClass("border-success")).toBe(false); + expect(tree.find(".card").hasClass("border-secondary")).toBe(false); + }); + + it("renders themed titlebar when colorTitleBar=false", () => { + settingsStore.alertGroupConfig.config.colorTitleBar = false; + group.stateCount = { active: 5, suppressed: 0, unprocessed: 0 }; + const tree = MountedAlertGroup(jest.fn(), false); + expect(tree.find("GroupHeader").props().themedCounters).toBe(true); + expect(tree.find(".card-header").hasClass("bg-light")).toBe(true); + expect(tree.find(".card-header").hasClass("bg-danger")).toBe(false); + expect(tree.find(".card-header").hasClass("bg-success")).toBe(false); + expect(tree.find(".card-header").hasClass("bg-secondary")).toBe(false); + }); + + it("renders bg-light border when colorTitleBar=true and there are multiple alert states", () => { + settingsStore.alertGroupConfig.config.colorTitleBar = false; + group.stateCount = { active: 5, suppressed: 0, unprocessed: 0 }; + const tree = MountedAlertGroup(jest.fn(), false); + expect(tree.find(".card").hasClass("border-light")).toBe(true); + expect(tree.find(".card").hasClass("border-danger")).toBe(false); + expect(tree.find(".card").hasClass("border-success")).toBe(false); + expect(tree.find(".card").hasClass("border-secondary")).toBe(false); + }); + + it("renders themed titlebar when colorTitleBar=true and there are multiple alert states", () => { + settingsStore.alertGroupConfig.config.colorTitleBar = true; + group.stateCount = { active: 5, suppressed: 6, unprocessed: 7 }; + const tree = MountedAlertGroup(jest.fn(), false); + expect(tree.find("GroupHeader").props().themedCounters).toBe(true); + expect(tree.find(".card-header").hasClass("bg-light")).toBe(true); + expect(tree.find(".card-header").hasClass("bg-danger")).toBe(false); + expect(tree.find(".card-header").hasClass("bg-success")).toBe(false); + expect(tree.find(".card-header").hasClass("bg-secondary")).toBe(false); + }); + + it("renders state based border when colorTitleBar=true and there's only one alert state", () => { + settingsStore.alertGroupConfig.config.colorTitleBar = true; + group.stateCount = { active: 0, suppressed: 5, unprocessed: 0 }; + const tree = MountedAlertGroup(jest.fn(), false); + expect(tree.find(".card").hasClass("border-light")).toBe(false); + expect(tree.find(".card").hasClass("border-danger")).toBe(false); + expect(tree.find(".card").hasClass("border-success")).toBe(true); + expect(tree.find(".card").hasClass("border-secondary")).toBe(false); + }); + + it("renders unthemed titlebar when colorTitleBar=true and there's only one alert state", () => { + settingsStore.alertGroupConfig.config.colorTitleBar = true; + group.stateCount = { active: 5, suppressed: 0, unprocessed: 0 }; + const tree = MountedAlertGroup(jest.fn(), false); + expect(tree.find("GroupHeader").props().themedCounters).toBe(false); + expect(tree.find(".card-header").hasClass("bg-light")).toBe(false); + expect(tree.find(".card-header").hasClass("bg-danger")).toBe(true); + expect(tree.find(".card-header").hasClass("bg-success")).toBe(false); + expect(tree.find(".card-header").hasClass("bg-secondary")).toBe(false); + }); +}); diff --git a/ui/src/Components/Labels/FilteringCounterBadge/index.js b/ui/src/Components/Labels/FilteringCounterBadge/index.js index 34dde451b..92e4bc433 100644 --- a/ui/src/Components/Labels/FilteringCounterBadge/index.js +++ b/ui/src/Components/Labels/FilteringCounterBadge/index.js @@ -17,15 +17,16 @@ const FilteringCounterBadge = inject("alertStore")( alertStore: PropTypes.instanceOf(AlertStore).isRequired, name: PropTypes.string.isRequired, value: PropTypes.string.isRequired, - counter: PropTypes.number.isRequired + counter: PropTypes.number.isRequired, + themed: PropTypes.bool.isRequired }; render() { - const { name, value, counter } = this.props; + const { name, value, counter, themed } = this.props; if (counter === 0) return null; - let cs = this.getClassAndStyle( + const cs = this.getClassAndStyle( name, value, "badge-pill components-label-with-hover" @@ -36,8 +37,15 @@ const FilteringCounterBadge = inject("alertStore")( title={`Click to only show ${value} alerts or Alt+Click to hide them`} > this.handleClick(e)} > {counter} diff --git a/ui/src/Components/Labels/FilteringCounterBadge/index.test.js b/ui/src/Components/Labels/FilteringCounterBadge/index.test.js index a92f35964..caf639d8c 100644 --- a/ui/src/Components/Labels/FilteringCounterBadge/index.test.js +++ b/ui/src/Components/Labels/FilteringCounterBadge/index.test.js @@ -12,37 +12,40 @@ beforeEach(() => { alertStore = new AlertStore([]); }); -const validateClassName = (value, className) => { +const validateClassName = (value, className, themed) => { const tree = mount( ); expect(tree.find("span").hasClass(className)).toBe(true); }; -const validateStyle = value => { +const validateStyle = (value, themed) => { const tree = mount( ); expect(tree.find("span").prop("style")).toEqual({}); }; -const validateOnClick = value => { +const validateOnClick = (value, themed) => { const tree = mount( ); tree.find(".components-label").simulate("click"); @@ -53,24 +56,27 @@ const validateOnClick = value => { }; describe("", () => { - it("@state=unprocessed counter badge should have className 'badge-secondary'", () => { - validateClassName("unprocessed", "badge-secondary"); + it("themed @state=unprocessed counter badge should have className 'badge-secondary'", () => { + validateClassName("unprocessed", "badge-secondary", true); }); - it("@state=active counter badge should have className 'badge-secondary'", () => { - validateClassName("active", "badge-danger"); + it("themed @state=active counter badge should have className 'badge-secondary'", () => { + validateClassName("active", "badge-danger", true); }); - it("@state=suppressed counter badge should have className 'badge-secondary'", () => { - validateClassName("suppressed", "badge-success"); + it("themed @state=suppressed counter badge should have className 'badge-secondary'", () => { + validateClassName("suppressed", "badge-success", true); + }); + it("unthemed @state=suppressed counter badge should have className 'badge-light'", () => { + validateClassName("suppressed", "badge-light", false); }); it("@state=unprocessed counter badge should have empty style", () => { - validateStyle("unprocessed"); + validateStyle("unprocessed", true); }); it("@state=active counter badge should have empty style", () => { - validateStyle("active"); + validateStyle("active", true); }); it("@state=suppressed counter badge should have empty style", () => { - validateStyle("suppressed"); + validateStyle("suppressed", true); }); it("counter badge should have correct children based on the counter prop value", () => { @@ -80,18 +86,19 @@ describe("", () => { name="@state" value="active" counter={123} + themed={true} /> ); expect(tree.text()).toBe("123"); }); it("onClick method on @state=unprocessed counter badge should add a new filter", () => { - validateOnClick("unprocessed"); + validateOnClick("unprocessed", true); }); it("onClick method on @state=active counter badge should add a new filter", () => { - validateOnClick("active"); + validateOnClick("active", true); }); it("onClick method on @state=suppressed counter badge should add a new filter", () => { - validateOnClick("suppressed"); + validateOnClick("suppressed", true); }); }); diff --git a/ui/src/Components/MainModal/Configuration/AlertGroupTitleBarColor.js b/ui/src/Components/MainModal/Configuration/AlertGroupTitleBarColor.js new file mode 100644 index 000000000..9868772cd --- /dev/null +++ b/ui/src/Components/MainModal/Configuration/AlertGroupTitleBarColor.js @@ -0,0 +1,57 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; + +import { action } from "mobx"; +import { observer } from "mobx-react"; + +import { Settings } from "Stores/Settings"; + +const AlertGroupTitleBarColor = observer( + class AlertGroupTitleBarColor extends Component { + static propTypes = { + settingsStore: PropTypes.instanceOf(Settings).isRequired + }; + + onChange = action(event => { + const { settingsStore } = this.props; + settingsStore.alertGroupConfig.config.colorTitleBar = + event.target.checked; + }); + + render() { + const { settingsStore } = this.props; + + return ( +
+
+ +
+
+ + + + +
+
+ ); + } + } +); + +export { AlertGroupTitleBarColor }; diff --git a/ui/src/Components/MainModal/Configuration/AlertGroupTitleBarColor.test.js b/ui/src/Components/MainModal/Configuration/AlertGroupTitleBarColor.test.js new file mode 100644 index 000000000..6dbf4ed60 --- /dev/null +++ b/ui/src/Components/MainModal/Configuration/AlertGroupTitleBarColor.test.js @@ -0,0 +1,54 @@ +import React from "react"; + +import { mount } from "enzyme"; + +import toDiffableHtml from "diffable-html"; + +import { Settings } from "Stores/Settings"; +import { AlertGroupTitleBarColor } from "./AlertGroupTitleBarColor"; + +let settingsStore; +beforeEach(() => { + settingsStore = new Settings(); +}); + +const FakeConfiguration = () => { + return mount(); +}; + +describe("", () => { + it("matches snapshot with default values", () => { + const tree = FakeConfiguration(); + expect(toDiffableHtml(tree.html())).toMatchSnapshot(); + }); + + it("colorTitleBar is 'false' by default", () => { + expect(settingsStore.alertGroupConfig.config.colorTitleBar).toBe(false); + }); + + it("unchecking the checkbox sets stored colorTitleBar value to 'false'", done => { + const tree = FakeConfiguration(); + const checkbox = tree.find("#configuration-colortitlebar"); + + settingsStore.alertGroupConfig.config.colorTitleBar = true; + expect(settingsStore.alertGroupConfig.config.colorTitleBar).toBe(true); + checkbox.simulate("change", { target: { checked: false } }); + setTimeout(() => { + expect(settingsStore.alertGroupConfig.config.colorTitleBar).toBe(false); + done(); + }, 200); + }); + + it("checking the checkbox sets stored colorTitleBar value to 'true'", done => { + const tree = FakeConfiguration(); + const checkbox = tree.find("#configuration-colortitlebar"); + + settingsStore.alertGroupConfig.config.colorTitleBar = false; + expect(settingsStore.alertGroupConfig.config.colorTitleBar).toBe(false); + checkbox.simulate("change", { target: { checked: true } }); + setTimeout(() => { + expect(settingsStore.alertGroupConfig.config.colorTitleBar).toBe(true); + done(); + }, 200); + }); +}); diff --git a/ui/src/Components/MainModal/Configuration/__snapshots__/AlertGroupTitleBarColor.test.js.snap b/ui/src/Components/MainModal/Configuration/__snapshots__/AlertGroupTitleBarColor.test.js.snap new file mode 100644 index 000000000..69dc194cf --- /dev/null +++ b/ui/src/Components/MainModal/Configuration/__snapshots__/AlertGroupTitleBarColor.test.js.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` matches snapshot with default values 1`] = ` +" +
+
+ +
+
+ + + + +
+
+" +`; diff --git a/ui/src/Components/MainModal/Configuration/__snapshots__/index.test.js.snap b/ui/src/Components/MainModal/Configuration/__snapshots__/index.test.js.snap index c230bc7d7..754f12cb2 100644 --- a/ui/src/Components/MainModal/Configuration/__snapshots__/index.test.js.snap +++ b/ui/src/Components/MainModal/Configuration/__snapshots__/index.test.js.snap @@ -72,6 +72,29 @@ exports[` matches snapshot 1`] = `
+
+
+ +
+
+ + + + +
+
+
+