From 794607919a881390fc2aaa42615f2d8f71c83545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Mon, 28 Oct 2019 17:52:24 +0000 Subject: [PATCH 1/3] feat(ui): experimental dark theme Very ugly, might be buggy, but need to start somewhere --- ui/src/App.test.js | 12 ++ ui/src/App.tsx | 7 + .../Grid/AlertGrid/AlertGroup/index.js | 2 +- .../Configuration/AlertGroupTitleBarColor.js | 2 +- .../Configuration/ThemeConfiguration.js | 57 +++++++ .../Configuration/ThemeConfiguration.test.js | 54 ++++++ .../AlertGroupTitleBarColor.test.js.snap | 2 +- .../ThemeConfiguration.test.js.snap | 25 +++ .../__snapshots__/index.test.js.snap | 23 ++- .../MainModal/Configuration/index.js | 10 +- .../MainModalContent.test.js.snap | 23 ++- ui/src/DarkTheme.scss | 159 ++++++++++++++++++ ui/src/Stores/Settings.js | 16 ++ ui/src/__mocks__/Defaults.js | 3 +- 14 files changed, 385 insertions(+), 10 deletions(-) create mode 100644 ui/src/Components/MainModal/Configuration/ThemeConfiguration.js create mode 100644 ui/src/Components/MainModal/Configuration/ThemeConfiguration.test.js create mode 100644 ui/src/Components/MainModal/Configuration/__snapshots__/ThemeConfiguration.test.js.snap create mode 100644 ui/src/DarkTheme.scss diff --git a/ui/src/App.test.js b/ui/src/App.test.js index 36f95d8d4..03fe95049 100644 --- a/ui/src/App.test.js +++ b/ui/src/App.test.js @@ -154,4 +154,16 @@ describe("", () => { let event = new PopStateEvent("popstate"); window.onpopstate(event); }); + + it("appends 'dark-theme' class to #root if dark mode is enabled", () => { + const tree = shallow( + + ); + tree.instance().componentWillUnmount(); + + expect(document.body.className.split(" ")).toContain("dark-theme"); + }); }); diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 00f962665..f3e7abd5f 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -12,11 +12,13 @@ import { FaviconBadge } from "Components/FaviconBadge"; import { ErrorBoundary } from "./ErrorBoundary"; import "./App.scss"; +import "./DarkTheme.scss"; interface UIDefaults { Refresh: number; HideFiltersWhenIdle: boolean; ColorTitlebar: boolean; + DarkTheme: boolean; MinimalGroupWidth: number; AlertsPerGroup: number; CollapseGroups: "expanded" | "collapsed" | "collapsedOnMobile"; @@ -70,6 +72,11 @@ class App extends Component { componentDidMount() { window.onpopstate = this.onPopState; + + document.body.classList.toggle( + "dark-theme", + this.settingsStore.themeConfig.config.darkTheme + ); } componentWillUnmount() { diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.js index 5441f759e..410edd3e3 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.js @@ -224,7 +224,7 @@ const AlertGroup = observer( setIsMenuOpen={this.renderConfig.setIsMenuOpen} /> {this.collapse.value ? null : ( -
+
    {group.alerts .slice(0, this.renderConfig.alertsToRender) diff --git a/ui/src/Components/MainModal/Configuration/AlertGroupTitleBarColor.js b/ui/src/Components/MainModal/Configuration/AlertGroupTitleBarColor.js index 8bf71aca2..9270cec1a 100644 --- a/ui/src/Components/MainModal/Configuration/AlertGroupTitleBarColor.js +++ b/ui/src/Components/MainModal/Configuration/AlertGroupTitleBarColor.js @@ -22,7 +22,7 @@ const AlertGroupTitleBarColor = observer( const { settingsStore } = this.props; return ( -
    +
    { + const { settingsStore } = this.props; + settingsStore.themeConfig.config.darkTheme = event.target.checked; + + document.body.classList.toggle( + "dark-theme", + settingsStore.themeConfig.config.darkTheme + ); + }); + + render() { + const { settingsStore } = this.props; + + return ( +
    +
    + + + + + Experimental + + +
    +
    + ); + } + } +); + +export { ThemeConfiguration }; diff --git a/ui/src/Components/MainModal/Configuration/ThemeConfiguration.test.js b/ui/src/Components/MainModal/Configuration/ThemeConfiguration.test.js new file mode 100644 index 000000000..6e73f4117 --- /dev/null +++ b/ui/src/Components/MainModal/Configuration/ThemeConfiguration.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 { ThemeConfiguration } from "./ThemeConfiguration"; + +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("darkTheme is 'false' by default", () => { + expect(settingsStore.themeConfig.config.darkTheme).toBe(false); + }); + + it("unchecking the checkbox sets stored darkTheme value to 'false'", done => { + const tree = FakeConfiguration(); + const checkbox = tree.find("#configuration-theme"); + + settingsStore.themeConfig.config.darkTheme = true; + expect(settingsStore.themeConfig.config.darkTheme).toBe(true); + checkbox.simulate("change", { target: { checked: false } }); + setTimeout(() => { + expect(settingsStore.themeConfig.config.darkTheme).toBe(false); + done(); + }, 200); + }); + + it("checking the checkbox sets stored darkTheme value to 'true'", done => { + const tree = FakeConfiguration(); + const checkbox = tree.find("#configuration-theme"); + + settingsStore.themeConfig.config.darkTheme = false; + expect(settingsStore.themeConfig.config.darkTheme).toBe(false); + checkbox.simulate("change", { target: { checked: true } }); + setTimeout(() => { + expect(settingsStore.themeConfig.config.darkTheme).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 index 1dc05774d..db92ecc3f 100644 --- a/ui/src/Components/MainModal/Configuration/__snapshots__/AlertGroupTitleBarColor.test.js.snap +++ b/ui/src/Components/MainModal/Configuration/__snapshots__/AlertGroupTitleBarColor.test.js.snap @@ -2,7 +2,7 @@ exports[` matches snapshot with default values 1`] = ` " -
    +
    matches snapshot with default values 1`] = ` +" +
    +
    + + + + + Experimental + + +
    +
    +" +`; 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 aa5924138..d6542ef7d 100644 --- a/ui/src/Components/MainModal/Configuration/__snapshots__/index.test.js.snap +++ b/ui/src/Components/MainModal/Configuration/__snapshots__/index.test.js.snap @@ -126,7 +126,7 @@ exports[` matches snapshot 1`] = `
    - Alert group titlebar configuration + Theme
    matches snapshot 1`] = ` style=\\"height:0;-webkit-transition:height 50ms linear;-ms-transition:height 50ms linear;transition:height 50ms linear;overflow:hidden\\" >
    -
    +
    matches snapshot 1`] = `
    +
    +
    + + + + + Experimental + + +
    +
    diff --git a/ui/src/Components/MainModal/Configuration/index.js b/ui/src/Components/MainModal/Configuration/index.js index 75c2014aa..9f7065dcb 100644 --- a/ui/src/Components/MainModal/Configuration/index.js +++ b/ui/src/Components/MainModal/Configuration/index.js @@ -10,6 +10,7 @@ import { AlertGroupWidthConfiguration } from "./AlertGroupWidthConfiguration"; import { AlertGroupSortConfiguration } from "./AlertGroupSortConfiguration"; import { AlertGroupCollapseConfiguration } from "./AlertGroupCollapseConfiguration"; import { AlertGroupTitleBarColor } from "./AlertGroupTitleBarColor"; +import { ThemeConfiguration } from "./ThemeConfiguration"; const Configuration = ({ settingsStore }) => (
    @@ -23,8 +24,13 @@ const Configuration = ({ settingsStore }) => ( content={} /> } + text="Theme" + content={ + + + + + } /> matches snapshot 1`] = `
    - Alert group titlebar configuration + Theme
    matches snapshot 1`] = ` style=\\"height: 0px; transition: height 50ms linear; overflow: hidden;\\" >
    -
    +
    matches snapshot 1`] = `
    +
    +
    + + + + + Experimental + + +
    +
    diff --git a/ui/src/DarkTheme.scss b/ui/src/DarkTheme.scss new file mode 100644 index 000000000..69abd4616 --- /dev/null +++ b/ui/src/DarkTheme.scss @@ -0,0 +1,159 @@ +@import "src/App.scss"; + +$alertgroup-body-bg: #5a6e7b; +$alertgroup-header-bg: #515658; +$alertgroup-footer-bg: #728998; +$alertgroup-border-inside: #485862; + +$silence-bg: #c3cdd5; +$silence-progress-bg: #eaeef0; + +$badge-light-bg: #d2dae0; + +.dark-theme .card.bg-light { + border-color: #3c3e3e !important; +} +.dark-theme .components-grid-alertgrid-card { + &.card-body, + & > .list-group, + & > .list-group > .list-group-item { + background-color: $alertgroup-body-bg !important; + } +} +.dark-theme .card.bg-light > .card-header { + background-color: $alertgroup-header-bg !important; +} +.dark-theme .card > .card-header { + border-bottom-color: $alertgroup-border-inside !important; +} +.dark-theme .bg-card-footer-default { + background-color: $alertgroup-footer-bg !important; + border-top-color: $alertgroup-border-inside !important; +} + +.dark-theme .btn.bg-white { + background-color: $alertgroup-body-bg !important; +} + +.dark-theme { + & .components-grid-annotation .text-muted, + & .components-managed-silence-cite .text-muted, + & .components-managed-silence svg.text-muted, + & .text-muted.fa-bell-slash { + color: $dark !important; + } + & .text-muted { + color: $white !important; + } +} + +.dark-theme .components-managed-silence { + border-left-color: $dark; + + & > .card-header, + & > .card-body { + background-color: $silence-bg !important; + } +} + +.dark-theme { + & .modal-content { + background-color: $alertgroup-body-bg; + + & .list-group-item { + background-color: $alertgroup-body-bg; + } + } + & .modal-header { + border-bottom-color: $alertgroup-border-inside; + } + & .modal-footer { + border-top-color: $alertgroup-border-inside; + } + + & .components-grid-annotation.bg-light { + background-color: $silence-bg !important; + } + + & .badge.badge-light { + background-color: $badge-light-bg !important; + } + & .components-filteredinputlabel > .badge-pill.badge-light { + background-color: $badge-light-bg !important; + filter: none; + } + + & .dropdown-menu { + background-color: $silence-bg !important; + } + & .dropdown-header { + color: $dark !important; + } + + & .progress.bg-white { + background-color: $silence-progress-bg !important; + } +} + +.dark-theme { + & .react-select__control, + & .react-select__indicators, + & .react-select__value-container { + background-color: $alertgroup-body-bg !important; + } + & .react-select__menu { + background-color: $silence-bg !important; + } + + & .tab-content, + & .tab-content .text-muted, + & .nav-item:not(.components-tab-inactive), + & .custom-control-label, + & .react-select__placeholder { + color: $white !important; + } + + & .nav-link.active { + color: $black !important; + background-color: $silence-bg !important; + } + + & .form-control { + color: $gray-200 !important; + background-color: $alertgroup-body-bg !important; + } + & .input-group-text { + background-color: $silence-bg !important; + } + + & .react-datepicker, + & .react-datepicker__header, + & .react-datepicker__today-button { + background-color: $silence-bg !important; + } +} + +.dark-theme { + & .jumbotron.bg-white { + background-color: $alertgroup-body-bg !important; + } +} + +.dark-theme { + & .collapse, + & .collapse > .card-body { + color: $white; + background-color: $alertgroup-body-bg !important; + } + & .Collapsible > .card-header { + background-color: $alertgroup-footer-bg !important; + border-bottom-color: $alertgroup-border-inside !important; + } + & .input-range__label, + & .Collapsible__trigger.bg-light { + color: $white; + } + & .Collapsible__trigger.is-closed { + color: $black; + } +} diff --git a/ui/src/Stores/Settings.js b/ui/src/Stores/Settings.js index b9ac01396..4a714617c 100644 --- a/ui/src/Stores/Settings.js +++ b/ui/src/Stores/Settings.js @@ -110,6 +110,20 @@ class FilterBarConfig { } } +class ThemeConfig { + constructor(darkTheme) { + this.config = localStored( + "themeConfig", + { + darkTheme: darkTheme + }, + { + delay: 100 + } + ); + } +} + class Settings { constructor(defaults) { let defaultSettings; @@ -118,6 +132,7 @@ class Settings { Refresh: 30 * 1000 * 1000 * 1000, HideFiltersWhenIdle: true, ColorTitlebar: false, + DarkTheme: false, MinimalGroupWidth: 420, AlertsPerGroup: 5, CollapseGroups: "collapsedOnMobile" @@ -140,6 +155,7 @@ class Settings { this.filterBarConfig = new FilterBarConfig( defaultSettings.HideFiltersWhenIdle ); + this.themeConfig = new ThemeConfig(defaultSettings.DarkTheme); } } diff --git a/ui/src/__mocks__/Defaults.js b/ui/src/__mocks__/Defaults.js index d647135c3..381458357 100644 --- a/ui/src/__mocks__/Defaults.js +++ b/ui/src/__mocks__/Defaults.js @@ -1,9 +1,10 @@ const DefaultsBase64 = - "eyJSZWZyZXNoIjo0NTAwMDAwMDAwMCwiSGlkZUZpbHRlcnNXaGVuSWRsZSI6ZmFsc2UsIkNvbG9yVGl0bGViYXIiOmZhbHNlLCJNaW5pbWFsR3JvdXBXaWR0aCI6NTU1LCJBbGVydHNQZXJHcm91cCI6MTUsIkNvbGxhcHNlR3JvdXBzIjoiZXhwYW5kZWQifQo="; + "eyJSZWZyZXNoIjo0NTAwMDAwMDAwMCwiSGlkZUZpbHRlcnNXaGVuSWRsZSI6ZmFsc2UsIkNvbG9yVGl0bGViYXIiOmZhbHNlLCJEYXJrTW9kZSI6ZmFsc2UsIk1pbmltYWxHcm91cFdpZHRoIjo1NTUsIkFsZXJ0c1Blckdyb3VwIjoxNSwiQ29sbGFwc2VHcm91cHMiOiJleHBhbmRlZCJ9Cg=="; const DefaultsObject = { Refresh: 45000000000, HideFiltersWhenIdle: false, ColorTitlebar: false, + DarkMode: false, MinimalGroupWidth: 555, AlertsPerGroup: 15, CollapseGroups: "expanded" From 03caf18a4fe4dc0d788d26db6d57b8c8ed8f8f23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Mon, 28 Oct 2019 17:52:46 +0000 Subject: [PATCH 2/3] feat(ui): allow setting default value for UI darkTheme option --- docs/CONFIGURATION.md | 4 ++++ internal/config/config.go | 2 ++ internal/config/config_test.go | 1 + internal/config/models.go | 1 + 4 files changed, 8 insertions(+) diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index f2155049b..d044653c7 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -770,6 +770,7 @@ ui: refresh: duration hideFiltersWhenIdle: bool colorTitlebar: bool + darkTheme: bool minimalGroupWidth: integer alertsPerGroup: integer collapseGroups: string @@ -781,6 +782,8 @@ ui: user inactivity - `colorTitlebar` - if enabled alert group title bar color will be set to follow alerts in that group +- `darkTheme` - if enabled dark mode will be enabled. + Note: dark mode is *experimental* and might be buggy. - `minimalGroupWidth` - minimal width (in pixels) for each alert group rendered on the grid. This value is used to calculate the number of columns rendered on the grid. @@ -799,6 +802,7 @@ ui: refresh: 30s hideFiltersWhenIdle: true colorTitlebar: false + darkTheme: false minimalGroupWidth: 420 alertsPerGroup: 5 collapseGroups: collapsedOnMobile diff --git a/internal/config/config.go b/internal/config/config.go index 9e2a6b1a1..a3e13b663 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -96,6 +96,7 @@ func init() { pflag.Duration("ui.refresh", time.Second*30, "UI refresh interval") pflag.Bool("ui.hideFiltersWhenIdle", true, "Hide the filters bar when idle") pflag.Bool("ui.colorTitlebar", false, "Color alert group titlebar based on alert state") + pflag.Bool("ui.darkTheme", false, "Enable dark theme") pflag.Int("ui.minimalGroupWidth", 420, "Minimal width for each alert group on the grid") pflag.Int("ui.alertsPerGroup", 5, "Default number of alerts to show for each alert group") pflag.String("ui.collapseGroups", "collapsedOnMobile", "Default state for alert groups") @@ -186,6 +187,7 @@ func (config *configSchema) Read() { config.UI.Refresh = v.GetDuration("ui.refresh") config.UI.HideFiltersWhenIdle = v.GetBool("ui.hideFiltersWhenIdle") config.UI.ColorTitlebar = v.GetBool("ui.colorTitlebar") + config.UI.DarkTheme = v.GetBool("ui.darkTheme") config.UI.MinimalGroupWidth = v.GetInt("ui.minimalGroupWidth") config.UI.AlertsPerGroup = v.GetInt("ui.alertsPerGroup") config.UI.CollapseGroups = v.GetString("ui.collapseGroups") diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 0cb2a78ea..5b0ea0ab6 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -136,6 +136,7 @@ ui: refresh: 30s hideFiltersWhenIdle: true colorTitlebar: false + darkTheme: false minimalGroupWidth: 420 alertsPerGroup: 5 collapseGroups: collapsedOnMobile diff --git a/internal/config/models.go b/internal/config/models.go index c3620dfaa..df00f43cb 100644 --- a/internal/config/models.go +++ b/internal/config/models.go @@ -112,6 +112,7 @@ type configSchema struct { Refresh time.Duration HideFiltersWhenIdle bool `yaml:"hideFiltersWhenIdle" mapstructure:"hideFiltersWhenIdle"` ColorTitlebar bool `yaml:"colorTitlebar" mapstructure:"colorTitlebar"` + DarkTheme bool `yaml:"darkTheme" mapstructure:"darkTheme"` MinimalGroupWidth int `yaml:"minimalGroupWidth" mapstructure:"minimalGroupWidth"` AlertsPerGroup int `yaml:"alertsPerGroup" mapstructure:"alertsPerGroup"` CollapseGroups string `yaml:"collapseGroups" mapstructure:"collapseGroups"` From 1de6f8f251636d961de9c23056c12b9671f12773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Mon, 28 Oct 2019 17:56:04 +0000 Subject: [PATCH 3/3] chore(project): remove dark.css - dark mode is now built in --- Dockerfile | 1 - docs/CONFIGURATION.md | 8 -------- docs/dark.css | 43 ------------------------------------------- 3 files changed, 52 deletions(-) delete mode 100644 docs/dark.css diff --git a/Dockerfile b/Dockerfile index f8bf469e3..1b13066fe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,6 @@ ARG VERSION RUN CGO_ENABLED=0 make -C /src VERSION="${VERSION:-dev}" karma FROM gcr.io/distroless/base -COPY ./docs/dark.css /themes/dark.css COPY --from=go-builder /src/karma /karma EXPOSE 8080 ENTRYPOINT ["/karma"] diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index d044653c7..6d529915a 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -836,14 +836,6 @@ custom: Use at your own risk and be aware that used CSS class names might change without warning. This feature is provided as is without any guarantees. -There is an example `dark.css` file providing a dark theme. It's included in the -docker image as `/themes/dark.css` and can be enabled by passing environment -variable via docker: - -```shell --e CUSTOM_CSS=/themes/dark.css -``` - ## Command line flags Config file options are mapped to command line flags, so `alertmanager:interval` diff --git a/docs/dark.css b/docs/dark.css deleted file mode 100644 index 572954638..000000000 --- a/docs/dark.css +++ /dev/null @@ -1,43 +0,0 @@ -/* Example dark theme */ - -body, -.bg-primary-transparent { - background-color: #2e2727 !important; -} - -.card, -.list-group-item, -.dropdown-menu, -.modal-content { - background-color: #455a64 !important; -} - -.input-group-text, -.components-filterinput { - background-color: #455a64 !important; -} - -.components-grid-annotation { - background-color: #95a5a6 !important; -} - -.badge:not(.components-label-bright), -.form-group > label, -.nav-link { - color: #ccc !important; -} - -.nav > .nav-link { - color: #ccc !important; -} - -.nav > .nav-link.active, -.nav-item > .nav-link.active { - color: #fff !important; - background-color: #455a64 !important; -} - -.badge.badge-light { - color: #666; - background-color: #444; -}