From 4cc15f10d1facf1f2e2f45a1750aa2b96afbac1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Fri, 29 Nov 2019 17:47:34 +0000 Subject: [PATCH 1/5] chore(backend): use new settings for UI theme configuration --- docs/CONFIGURATION.md | 14 ++++++++++---- internal/config/config.go | 8 ++++++-- internal/config/config_test.go | 18 +++++++++++++++++- internal/config/models.go | 2 +- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 673a11ee1..a7e51592a 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -842,7 +842,7 @@ ui: refresh: duration hideFiltersWhenIdle: bool colorTitlebar: bool - darkTheme: bool + theme: string minimalGroupWidth: integer alertsPerGroup: integer collapseGroups: string @@ -854,8 +854,14 @@ 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. +- `theme` - default theme, possible values: + - `light` - bright theme + - `dark` - dark theme + - `auto` - follows browser preferences using + [prefers-color-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) + media queries + + Default value is `auto`. - `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. @@ -874,7 +880,7 @@ ui: refresh: 30s hideFiltersWhenIdle: true colorTitlebar: false - darkTheme: false + theme: "auto" minimalGroupWidth: 420 alertsPerGroup: 5 collapseGroups: collapsedOnMobile diff --git a/internal/config/config.go b/internal/config/config.go index e7d2abc9d..03339c8ef 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -101,7 +101,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.String("ui.theme", "auto", "Default theme, 'light', 'dark' or 'auto' (follow browser preference)") 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") @@ -196,7 +196,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.Theme = v.GetString("ui.theme") config.UI.MinimalGroupWidth = v.GetInt("ui.minimalGroupWidth") config.UI.AlertsPerGroup = v.GetInt("ui.alertsPerGroup") config.UI.CollapseGroups = v.GetString("ui.collapseGroups") @@ -254,6 +254,10 @@ func (config *configSchema) Read() { log.Fatalf("Invalid ui.collapseGroups value '%s', allowed options: expanded, collapsed, collapsedOnMobile", config.UI.CollapseGroups) } + if !slices.StringInSlice([]string{"light", "dark", "auto"}, config.UI.Theme) { + log.Fatalf("Invalid ui.theme value '%s', allowed options: light, dark, auto", config.UI.Theme) + } + // FIXME workaround for https://github.com/prymitive/karma/issues/507 // until https://github.com/spf13/viper/pull/635 is merged // read in raw config file if it's used and override maps where keys are label diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 365874bda..a5f2fe2de 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -144,7 +144,7 @@ ui: refresh: 30s hideFiltersWhenIdle: true colorTitlebar: false - darkTheme: false + theme: auto minimalGroupWidth: 420 alertsPerGroup: 5 collapseGroups: collapsedOnMobile @@ -311,3 +311,19 @@ func TestInvalidUICollapseGroups(t *testing.T) { t.Error("Invalid ui.collapseGroups value didn't cause log.Fatal()") } } + +func TestInvalidUITheme(t *testing.T) { + resetEnv() + os.Setenv("UI_THEME", "foo") + + log.SetLevel(log.PanicLevel) + defer func() { log.StandardLogger().ExitFunc = nil }() + var wasFatal bool + log.StandardLogger().ExitFunc = func(int) { wasFatal = true } + + Config.Read() + + if !wasFatal { + t.Error("Invalid ui.theme value didn't cause log.Fatal()") + } +} diff --git a/internal/config/models.go b/internal/config/models.go index 10bb58db6..caab853ab 100644 --- a/internal/config/models.go +++ b/internal/config/models.go @@ -124,7 +124,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"` + Theme string `yaml:"theme" mapstructure:"theme"` MinimalGroupWidth int `yaml:"minimalGroupWidth" mapstructure:"minimalGroupWidth"` AlertsPerGroup int `yaml:"alertsPerGroup" mapstructure:"alertsPerGroup"` CollapseGroups string `yaml:"collapseGroups" mapstructure:"collapseGroups"` From cc4da6d16b77174bc18426bafec78d2cb45af310 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Fri, 29 Nov 2019 17:47:54 +0000 Subject: [PATCH 2/5] feat(ui): follow browser preferences when setting theme --- ui/package-lock.json | 24 +++ ui/package.json | 1 + ui/src/App.test.js | 151 ++++++++++++++---- ui/src/App.tsx | 83 ++++++---- .../Configuration/AlertGroupTitleBarColor.js | 2 +- .../Configuration/ThemeConfiguration.js | 78 +++++---- .../Configuration/ThemeConfiguration.test.js | 60 ++++--- .../AlertGroupTitleBarColor.test.js.snap | 2 +- .../ThemeConfiguration.test.js.snap | 63 ++++++-- .../__snapshots__/index.test.js.snap | 67 +++++--- .../MainModal/Configuration/index.js | 2 +- .../MainModalContent.test.js.snap | 67 +++++--- ui/src/Components/Theme/index.js | 40 +++-- ui/src/Components/Theme/index.test.js | 48 ++++-- ui/src/Stores/Settings.js | 16 +- ui/src/__mocks__/Defaults.js | 4 +- ui/src/__mocks__/matchMedia.js | 16 ++ ui/src/index.test.js | 9 ++ 18 files changed, 529 insertions(+), 204 deletions(-) create mode 100644 ui/src/__mocks__/matchMedia.js diff --git a/ui/package-lock.json b/ui/package-lock.json index dd343b584..a0897e8ca 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -11390,6 +11390,14 @@ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, + "json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha1-tje9O6nqvhIsg+lyBIOusQ0skEo=", + "requires": { + "string-convert": "^0.2.0" + } + }, "json3": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz", @@ -15348,6 +15356,17 @@ "react-infinite-scroller": "^1.0.12" } }, + "react-media": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/react-media/-/react-media-1.10.0.tgz", + "integrity": "sha512-FjgYmFoaPTImST06jqotuu0Mk8LOXiGYS/fIyiXuLnf20l3DPniBwtrxi604/HxxjqvmHS3oz5rAwnqdvosV4A==", + "requires": { + "@babel/runtime": "^7.2.0", + "invariant": "^2.2.2", + "json2mq": "^0.2.0", + "prop-types": "^15.5.10" + } + }, "react-moment": { "version": "0.9.6", "resolved": "https://registry.npmjs.org/react-moment/-/react-moment-0.9.6.tgz", @@ -17527,6 +17546,11 @@ "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=" }, + "string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha1-aYLMMEn7tM2F+LJFaLnZvznu/5c=" + }, "string-length": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz", diff --git a/ui/package.json b/ui/package.json index 331faa928..98bf176e9 100644 --- a/ui/package.json +++ b/ui/package.json @@ -44,6 +44,7 @@ "react-json-pretty": "2.2.0", "react-linkify": "0.2.2", "react-masonry-infinite": "1.2.2", + "react-media": "1.10.0", "react-moment": "0.9.6", "react-onclickoutside": "6.9.0", "react-popper": "1.3.6", diff --git a/ui/src/App.test.js b/ui/src/App.test.js index 4e6d4467c..89c6dc648 100644 --- a/ui/src/App.test.js +++ b/ui/src/App.test.js @@ -2,6 +2,7 @@ import React from "react"; import { shallow, mount } from "enzyme"; +import { mockMatchMedia } from "__mocks__/matchMedia"; import { NewUnappliedFilter } from "Stores/AlertStore"; import { App } from "./App"; @@ -19,14 +20,14 @@ beforeEach(() => { // ensure it's wiped after each test window.history.pushState({}, "App", "/"); - document.body.className = ""; + // matchMedia needs mocking + window.matchMedia = mockMatchMedia({}); }); afterEach(() => { localStorage.setItem("savedFilters", ""); jest.restoreAllMocks(); window.history.pushState({}, "App", "/"); - document.body.className = ""; }); describe("", () => { @@ -157,44 +158,140 @@ describe("", () => { let event = new PopStateEvent("popstate"); window.onpopstate(event); }); +}); - it("appends correct theme class to #root if dark mode is disabled", () => { - const tree = shallow( +describe(" theme", () => { + const getApp = theme => + mount( ); + + it("configures light theme when uiDefaults passes it", () => { + const tree = getApp("light"); + expect(tree.instance().settingsStore.themeConfig.config.theme).toBe( + "light" + ); tree.instance().componentWillUnmount(); + }); - expect(document.body.className.split(" ")).toContain("theme-light"); + it("configures dark theme when uiDefaults passes it", () => { + const tree = getApp("dark"); + expect(tree.instance().settingsStore.themeConfig.config.theme).toBe("dark"); + tree.instance().componentWillUnmount(); }); - it("appends 'theme-dark' class to #root if dark mode is enabled", () => { - const tree = shallow( - - ); + it("configures automatic theme when uiDefaults passes it", () => { + const tree = getApp("auto"); + expect(tree.instance().settingsStore.themeConfig.config.theme).toBe("auto"); + tree.instance().componentWillUnmount(); + }); + + it("configures automatic theme when uiDefaults doesn't pass any value", () => { + const tree = mount(); + expect(tree.instance().settingsStore.themeConfig.config.theme).toBe("auto"); tree.instance().componentWillUnmount(); + }); - expect(document.body.className.split(" ")).toContain("theme-dark"); + it("applies light theme when theme=auto and browser doesn't support prefers-color-scheme", () => { + window.matchMedia = mockMatchMedia({}); + const tree = getApp("auto"); + expect(tree.find("LightTheme")).toHaveLength(1); + tree.instance().componentWillUnmount(); }); - it("toggling settingsStore.themeConfig.config.darkTheme modifies the theme", () => { - const tree = mount( - - ); - tree.update(); - expect(document.body.className.split(" ")).toContain("theme-light"); + const lightMatch = () => ({ + "(prefers-color-scheme)": { + media: "(prefers-color-scheme)", + matches: true + }, + "(prefers-color-scheme: light)": { + media: "(prefers-color-scheme: light)", + matches: true + }, + "(prefers-color-scheme: dark)": { + media: "(prefers-color-scheme: dark)", + matches: false + } + }); - tree.instance().settingsStore.themeConfig.config.darkTheme = true; - tree.update(); - expect(document.body.className.split(" ")).toContain("theme-dark"); - tree.instance().componentWillUnmount(); + const darkMatch = () => ({ + "(prefers-color-scheme)": { + media: "(prefers-color-scheme)", + matches: true + }, + "(prefers-color-scheme: light)": { + media: "(prefers-color-scheme: light)", + matches: false + }, + "(prefers-color-scheme: dark)": { + media: "(prefers-color-scheme: dark)", + matches: true + } }); + + const testCases = [ + { + name: + "applies LightTheme when config=auto and browser doesn't support prefers-color-scheme", + settings: "auto", + matchMedia: {}, + theme: "LightTheme" + }, + { + name: + "applies LightTheme when config=auto and browser prefers-color-scheme:light matches", + settings: "auto", + matchMedia: lightMatch(), + theme: "LightTheme" + }, + { + name: + "applies DarkTheme when config=auto and browser prefers-color-scheme:dark matches", + settings: "auto", + matchMedia: darkMatch(), + theme: "DarkTheme" + }, + + { + name: + "applies LightTheme when config=light and browser doesn't support prefers-color-scheme", + settings: "light", + matchMedia: {}, + theme: "LightTheme" + }, + { + name: + "applies LightTheme when config=light and browser prefers-color-scheme:light matches", + settings: "light", + matchMedia: lightMatch(), + theme: "LightTheme" + }, + + { + name: + "applies DarkTheme when config=dark and browser doesn't support prefers-color-scheme", + settings: "dark", + matchMedia: {}, + theme: "DarkTheme" + }, + { + name: + "applies DarkTheme when config=dark and browser prefers-color-scheme:dark matches", + settings: "dark", + matchMedia: darkMatch(), + theme: "DarkTheme" + } + ]; + for (const testCase of testCases) { + it(testCase.name, () => { + window.matchMedia = mockMatchMedia(testCase.matchMedia); + const tree = getApp(testCase.settings); + expect(tree.find(testCase.theme)).toHaveLength(1); + tree.instance().componentWillUnmount(); + window.matchMedia.mockRestore(); + }); + } }); diff --git a/ui/src/App.tsx b/ui/src/App.tsx index a560c3cd5..c6ae949d3 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -2,6 +2,8 @@ import React, { Component } from "react"; import { observer } from "mobx-react"; +import Media from "react-media"; + import { AlertStore, DecodeLocationSearch } from "Stores/AlertStore"; import { Settings } from "Stores/Settings"; import { SilenceFormStore } from "Stores/SilenceFormStore"; @@ -11,7 +13,7 @@ import { ReactSelectColors, ReactSelectStyles } from "Components/Theme/ReactSelect"; -import { Theme, ThemeContext } from "Components/Theme"; +import { BodyTheme, ThemeContext } from "Components/Theme"; import { ErrorBoundary } from "./ErrorBoundary"; import "Styles/ResetCSS.scss"; @@ -59,8 +61,6 @@ const App = observer( this.silenceFormStore = new SilenceFormStore(); this.settingsStore = new Settings(uiDefaults); - this.state = { darkTheme: false }; - let filters; // parse and decode request query args @@ -90,15 +90,6 @@ const App = observer( componentDidMount() { window.onpopstate = this.onPopState; - - document.body.classList.toggle( - "theme-dark", - this.settingsStore.themeConfig.config.darkTheme - ); - document.body.classList.toggle( - "theme-light", - !this.settingsStore.themeConfig.config.darkTheme - ); } componentWillUnmount() { @@ -108,29 +99,55 @@ const App = observer( render() { return ( - - + - - - - - - - + {matches => ( + + + + + + + + + + )} + +
{ + constructor(props) { + super(props); + + this.validateConfig(); + } + + valueToOption = val => { const { settingsStore } = this.props; - settingsStore.themeConfig.config.darkTheme = event.target.checked; - document.body.classList.toggle( - "theme-dark", - settingsStore.themeConfig.config.darkTheme - ); - document.body.classList.toggle( - "theme-light", - !settingsStore.themeConfig.config.darkTheme - ); + return { + label: settingsStore.themeConfig.options[val].label, + value: val + }; + }; + + validateConfig = action(() => { + const { settingsStore } = this.props; + + if ( + !Object.values(settingsStore.themeConfig.options) + .map(o => o.value) + .includes(settingsStore.themeConfig.config.theme) + ) { + settingsStore.themeConfig.config.theme = + settingsStore.themeConfig.options.auto.value; + } + }); + + onCollapseChange = action((newValue, actionMeta) => { + const { settingsStore } = this.props; + + settingsStore.themeConfig.config.theme = newValue.value; }); render() { const { settingsStore } = this.props; return ( -
-
- - - - - Experimental - - -
+
+ matches snapshot with default values 1`] = ` " -
-
- - - - - Experimental - - +
+
+
+
+
+ Automatic theme, follow browser preferences +
+
+
+ +
+
+
+
+
+
+ + +
+ + + + +
+
+
" 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 d69874e53..d49f218b2 100644 --- a/ui/src/Components/MainModal/Configuration/__snapshots__/index.test.js.snap +++ b/ui/src/Components/MainModal/Configuration/__snapshots__/index.test.js.snap @@ -151,6 +151,54 @@ exports[` matches snapshot 1`] = ` >
+
+
+
+
+ Automatic theme, follow browser preferences +
+
+
+ +
+
+
+
+
+
+ + +
+ + + + +
+
+
+
+
+
matches snapshot 1`] = `
-
-
- - - - - Experimental - - -
-
diff --git a/ui/src/Components/MainModal/Configuration/index.js b/ui/src/Components/MainModal/Configuration/index.js index c322b9f8a..20777dc26 100644 --- a/ui/src/Components/MainModal/Configuration/index.js +++ b/ui/src/Components/MainModal/Configuration/index.js @@ -28,8 +28,8 @@ const Configuration = ({ settingsStore, defaultIsOpen }) => ( text="Theme" content={ - + } extraProps={{ open: defaultIsOpen }} diff --git a/ui/src/Components/MainModal/__snapshots__/MainModalContent.test.js.snap b/ui/src/Components/MainModal/__snapshots__/MainModalContent.test.js.snap index 5457dcbf3..e8c486edd 100644 --- a/ui/src/Components/MainModal/__snapshots__/MainModalContent.test.js.snap +++ b/ui/src/Components/MainModal/__snapshots__/MainModalContent.test.js.snap @@ -170,6 +170,54 @@ exports[` matches snapshot 1`] = ` >
+
+
+
+
+ Automatic theme, follow browser preferences +
+
+
+ +
+
+
+
+
+
+ + +
+ + + + +
+
+
+
+
+
matches snapshot 1`] = `
-
-
- - - - - Experimental - - -
-
diff --git a/ui/src/Components/Theme/index.js b/ui/src/Components/Theme/index.js index 769f543e1..2bde87d9d 100644 --- a/ui/src/Components/Theme/index.js +++ b/ui/src/Components/Theme/index.js @@ -1,8 +1,6 @@ -import React from "react"; +import React, { Component } from "react"; import ReactDOM from "react-dom"; -import { observer } from "mobx-react"; - import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faSun } from "@fortawesome/free-solid-svg-icons/faSun"; @@ -41,16 +39,30 @@ const Placeholder = () => { ); }; -const Theme = observer(({ settingsStore }) => ( - }> - {settingsStore.themeConfig.config.darkTheme ? ( - - ) : ( - - )} - -)); - const ThemeContext = React.createContext(); -export { Theme, ThemeContext }; +class BodyTheme extends Component { + onToggleBodyClass = isDark => { + document.body.classList.toggle("theme-dark", isDark); + document.body.classList.toggle("theme-light", !isDark); + }; + + componentDidMount() { + this.onToggleBodyClass(this.context.isDark); + } + + componentDidUpdate() { + this.onToggleBodyClass(this.context.isDark); + } + + render() { + return ( + }> + {this.context.isDark ? : } + + ); + } +} +BodyTheme.contextType = ThemeContext; + +export { BodyTheme, ThemeContext }; diff --git a/ui/src/Components/Theme/index.test.js b/ui/src/Components/Theme/index.test.js index 00157e7bf..7a83ed401 100644 --- a/ui/src/Components/Theme/index.test.js +++ b/ui/src/Components/Theme/index.test.js @@ -1,26 +1,44 @@ -import React from "react"; +import * as React from "react"; import { mount } from "enzyme"; -import { Settings } from "Stores/Settings"; -import { Theme } from "."; - -let settingsStore; +import { BodyTheme, ThemeContext } from "."; beforeEach(() => { - settingsStore = new Settings(); + document.body.classList.remove("theme-light"); + document.body.classList.remove("theme-dark"); }); -describe("", () => { - it("renders DarkTheme when settingsStore.themeConfig.config.darkTheme is true", () => { - settingsStore.themeConfig.config.darkTheme = true; - const tree = mount(); - expect(tree.text()).toBe(""); +describe("", () => { + it("uses light theme when ThemeContext->isDark is false", () => { + mount(, { + wrappingComponent: ThemeContext.Provider, + wrappingComponentProps: { value: { isDark: false } } + }); + expect(document.body.classList.contains("theme-light")).toEqual(true); }); - it("renders LightTheme when settingsStore.themeConfig.config.darkTheme is false", () => { - settingsStore.themeConfig.config.darkTheme = false; - const tree = mount(); - expect(tree.text()).toBe(""); + it("uses dark theme when ThemeContext->isDark is true", () => { + mount(, { + wrappingComponent: ThemeContext.Provider, + wrappingComponentProps: { value: { isDark: true } } + }); + expect(document.body.classList.contains("theme-dark")).toEqual(true); + }); + + it("updates theme when ThemeContext->isDark is updated", () => { + const tree = mount(, { + wrappingComponent: ThemeContext.Provider, + wrappingComponentProps: { value: { isDark: true } } + }); + expect(document.body.classList.contains("theme-dark")).toEqual(true); + + document.body.classList.remove("theme-light"); + document.body.classList.remove("theme-dark"); + + const provider = tree.getWrappingComponent(); + provider.setProps({ value: { isDark: false } }); + + expect(document.body.classList.contains("theme-light")).toEqual(true); }); }); diff --git a/ui/src/Stores/Settings.js b/ui/src/Stores/Settings.js index 4a714617c..520bd841e 100644 --- a/ui/src/Stores/Settings.js +++ b/ui/src/Stores/Settings.js @@ -111,11 +111,19 @@ class FilterBarConfig { } class ThemeConfig { - constructor(darkTheme) { + options = Object.freeze({ + auto: { + label: "Automatic theme, follow browser preferences", + value: "auto" + }, + light: { label: "Light theme", value: "light" }, + dark: { label: "Dark theme", value: "dark" } + }); + constructor(defaultTheme) { this.config = localStored( "themeConfig", { - darkTheme: darkTheme + theme: defaultTheme }, { delay: 100 @@ -132,7 +140,7 @@ class Settings { Refresh: 30 * 1000 * 1000 * 1000, HideFiltersWhenIdle: true, ColorTitlebar: false, - DarkTheme: false, + Theme: "auto", MinimalGroupWidth: 420, AlertsPerGroup: 5, CollapseGroups: "collapsedOnMobile" @@ -155,7 +163,7 @@ class Settings { this.filterBarConfig = new FilterBarConfig( defaultSettings.HideFiltersWhenIdle ); - this.themeConfig = new ThemeConfig(defaultSettings.DarkTheme); + this.themeConfig = new ThemeConfig(defaultSettings.Theme); } } diff --git a/ui/src/__mocks__/Defaults.js b/ui/src/__mocks__/Defaults.js index 381458357..333074b16 100644 --- a/ui/src/__mocks__/Defaults.js +++ b/ui/src/__mocks__/Defaults.js @@ -1,10 +1,10 @@ const DefaultsBase64 = - "eyJSZWZyZXNoIjo0NTAwMDAwMDAwMCwiSGlkZUZpbHRlcnNXaGVuSWRsZSI6ZmFsc2UsIkNvbG9yVGl0bGViYXIiOmZhbHNlLCJEYXJrTW9kZSI6ZmFsc2UsIk1pbmltYWxHcm91cFdpZHRoIjo1NTUsIkFsZXJ0c1Blckdyb3VwIjoxNSwiQ29sbGFwc2VHcm91cHMiOiJleHBhbmRlZCJ9Cg=="; + "eyJSZWZyZXNoIjo0NTAwMDAwMDAwMCwiSGlkZUZpbHRlcnNXaGVuSWRsZSI6ZmFsc2UsIkNvbG9yVGl0bGViYXIiOmZhbHNlLCJUaGVtZSI6ImxpZ2h0IiwiTWluaW1hbEdyb3VwV2lkdGgiOjU1NSwiQWxlcnRzUGVyR3JvdXAiOjE1LCJDb2xsYXBzZUdyb3VwcyI6ImV4cGFuZGVkIn0K=="; const DefaultsObject = { Refresh: 45000000000, HideFiltersWhenIdle: false, ColorTitlebar: false, - DarkMode: false, + Theme: "light", MinimalGroupWidth: 555, AlertsPerGroup: 15, CollapseGroups: "expanded" diff --git a/ui/src/__mocks__/matchMedia.js b/ui/src/__mocks__/matchMedia.js new file mode 100644 index 000000000..f2a0ddab4 --- /dev/null +++ b/ui/src/__mocks__/matchMedia.js @@ -0,0 +1,16 @@ +const mockMatchMedia = mapOfMedia => { + return jest.fn().mockImplementation(query => { + return { + matches: mapOfMedia[query] ? mapOfMedia[query].matches : false, + media: mapOfMedia[query] ? mapOfMedia[query].media : "not all", + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn() + }; + }); +}; + +export { mockMatchMedia }; diff --git a/ui/src/index.test.js b/ui/src/index.test.js index a836de339..fd56ccb24 100644 --- a/ui/src/index.test.js +++ b/ui/src/index.test.js @@ -1,5 +1,6 @@ import { EmptyAPIResponse } from "__mocks__/Fetch"; import { DefaultsBase64 } from "__mocks__/Defaults"; +import { mockMatchMedia } from "__mocks__/matchMedia"; const settingsElement = { dataset: { @@ -9,6 +10,14 @@ const settingsElement = { } }; +beforeEach(() => { + window.matchMedia = mockMatchMedia({}); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + it("renders without crashing with missing defaults div", () => { const root = document.createElement("div"); jest.spyOn(global.document, "getElementById").mockImplementation(name => { From abe9bbf49c41ee06ec5250ea183d3593938e80a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Fri, 29 Nov 2019 21:13:48 +0000 Subject: [PATCH 3/5] chore(demo): set auto theme --- demo/karma.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/demo/karma.yaml b/demo/karma.yaml index 7b4dab137..e2cd5ccb9 100644 --- a/demo/karma.yaml +++ b/demo/karma.yaml @@ -84,6 +84,7 @@ ui: refresh: 10s hideFiltersWhenIdle: true colorTitlebar: false + theme: auto minimalGroupWidth: 420 alertsPerGroup: 5 collapseGroups: collapsedOnMobile From b02cfa8ddf9d00661c60692902f0f966916dec7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Fri, 29 Nov 2019 21:43:24 +0000 Subject: [PATCH 4/5] fix(docs): link to releases --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 303845c73..f01540291 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,11 @@ Alert dashboard for --- +Release notes can be found on +[GitHub Release Page](https://github.com/prymitive/karma/releases). + +--- + Alertmanager `0.16.x` is **NOT** supported by karma due to changes in the API, see [this issue](https://github.com/prymitive/karma/issues/115) for details. From 10ec1a84fc2a96559317b6260abae3ca74734c48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Fri, 29 Nov 2019 22:04:58 +0000 Subject: [PATCH 5/5] fix(ui): left side of a select needs radius --- .../AlertGroupCollapseConfiguration.test.js.snap | 2 +- .../AlertGroupSortConfiguration.test.js.snap | 2 +- .../__snapshots__/ThemeConfiguration.test.js.snap | 2 +- .../Configuration/__snapshots__/index.test.js.snap | 6 +++--- .../__snapshots__/MainModalContent.test.js.snap | 6 +++--- .../MultiSelect/__snapshots__/index.test.js.snap | 14 +++++++------- .../__snapshots__/index.test.js.snap | 2 +- .../__snapshots__/LabelNameInput.test.js.snap | 2 +- .../__snapshots__/LabelValueInput.test.js.snap | 2 +- ui/src/Components/Theme/ReactSelect.js | 6 ++++-- 10 files changed, 23 insertions(+), 21 deletions(-) diff --git a/ui/src/Components/MainModal/Configuration/__snapshots__/AlertGroupCollapseConfiguration.test.js.snap b/ui/src/Components/MainModal/Configuration/__snapshots__/AlertGroupCollapseConfiguration.test.js.snap index 5d7aae1ce..e22c3e65e 100644 --- a/ui/src/Components/MainModal/Configuration/__snapshots__/AlertGroupCollapseConfiguration.test.js.snap +++ b/ui/src/Components/MainModal/Configuration/__snapshots__/AlertGroupCollapseConfiguration.test.js.snap @@ -5,7 +5,7 @@ exports[` matches snapshot with default value
-
+
Collapse on mobile
diff --git a/ui/src/Components/MainModal/Configuration/__snapshots__/AlertGroupSortConfiguration.test.js.snap b/ui/src/Components/MainModal/Configuration/__snapshots__/AlertGroupSortConfiguration.test.js.snap index 9d431d1cd..b3d920f18 100644 --- a/ui/src/Components/MainModal/Configuration/__snapshots__/AlertGroupSortConfiguration.test.js.snap +++ b/ui/src/Components/MainModal/Configuration/__snapshots__/AlertGroupSortConfiguration.test.js.snap @@ -7,7 +7,7 @@ exports[` matches snapshot with default values 1`
-
+
Use defaults from karma config file
diff --git a/ui/src/Components/MainModal/Configuration/__snapshots__/ThemeConfiguration.test.js.snap b/ui/src/Components/MainModal/Configuration/__snapshots__/ThemeConfiguration.test.js.snap index c5cd767dd..e9fc0d99d 100644 --- a/ui/src/Components/MainModal/Configuration/__snapshots__/ThemeConfiguration.test.js.snap +++ b/ui/src/Components/MainModal/Configuration/__snapshots__/ThemeConfiguration.test.js.snap @@ -5,7 +5,7 @@ exports[` matches snapshot with default values 1`] = `
-
+
Automatic theme, follow browser preferences
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 d49f218b2..93116a562 100644 --- a/ui/src/Components/MainModal/Configuration/__snapshots__/index.test.js.snap +++ b/ui/src/Components/MainModal/Configuration/__snapshots__/index.test.js.snap @@ -153,7 +153,7 @@ exports[` matches snapshot 1`] = `
-
+
Automatic theme, follow browser preferences
@@ -390,7 +390,7 @@ exports[` matches snapshot 1`] = `
-
+
Collapse on mobile
@@ -471,7 +471,7 @@ exports[` matches snapshot 1`] = `
-
+
Use defaults from karma config file
diff --git a/ui/src/Components/MainModal/__snapshots__/MainModalContent.test.js.snap b/ui/src/Components/MainModal/__snapshots__/MainModalContent.test.js.snap index e8c486edd..bcb5ee98b 100644 --- a/ui/src/Components/MainModal/__snapshots__/MainModalContent.test.js.snap +++ b/ui/src/Components/MainModal/__snapshots__/MainModalContent.test.js.snap @@ -172,7 +172,7 @@ exports[` matches snapshot 1`] = `
-
+
Automatic theme, follow browser preferences
@@ -409,7 +409,7 @@ exports[` matches snapshot 1`] = `
-
+
Collapse on mobile
@@ -490,7 +490,7 @@ exports[` matches snapshot 1`] = `
-
+
Use defaults from karma config file
diff --git a/ui/src/Components/MultiSelect/__snapshots__/index.test.js.snap b/ui/src/Components/MultiSelect/__snapshots__/index.test.js.snap index 809931633..1bea08bdb 100644 --- a/ui/src/Components/MultiSelect/__snapshots__/index.test.js.snap +++ b/ui/src/Components/MultiSelect/__snapshots__/index.test.js.snap @@ -4,7 +4,7 @@ exports[` matches snapshot without any extra props 1`] = ` "
-
+
Select...
@@ -65,7 +65,7 @@ exports[` matches snapshot when focused 1`] = `

-
+
Select...
@@ -116,7 +116,7 @@ exports[` matches snapshot with a value 1`] = ` "
-
+
foo
@@ -167,7 +167,7 @@ exports[` matches snapshot with defaults 1`] = ` "
-
+
Select...
@@ -218,7 +218,7 @@ exports[` matches snapshot with isDisabled=true 1`] "
-
+
foo
@@ -270,7 +270,7 @@ exports[` matches snapshot with isMulti=true 1`] = ` "
-
+
Select...
@@ -321,7 +321,7 @@ exports[` matches snapshot with isMulti=true and a v "
-
+
foo diff --git a/ui/src/Components/SilenceModal/AlertManagerInput/__snapshots__/index.test.js.snap b/ui/src/Components/SilenceModal/AlertManagerInput/__snapshots__/index.test.js.snap index ff50d3602..3fb7ef3f3 100644 --- a/ui/src/Components/SilenceModal/AlertManagerInput/__snapshots__/index.test.js.snap +++ b/ui/src/Components/SilenceModal/AlertManagerInput/__snapshots__/index.test.js.snap @@ -4,7 +4,7 @@ exports[` matches snapshot 1`] = ` "
-
+
am1 | am2 diff --git a/ui/src/Components/SilenceModal/SilenceMatch/__snapshots__/LabelNameInput.test.js.snap b/ui/src/Components/SilenceModal/SilenceMatch/__snapshots__/LabelNameInput.test.js.snap index 1cfb9465a..b5b9cbef1 100644 --- a/ui/src/Components/SilenceModal/SilenceMatch/__snapshots__/LabelNameInput.test.js.snap +++ b/ui/src/Components/SilenceModal/SilenceMatch/__snapshots__/LabelNameInput.test.js.snap @@ -4,7 +4,7 @@ exports[` matches snapshot 1`] = ` "
-
+
name
diff --git a/ui/src/Components/SilenceModal/SilenceMatch/__snapshots__/LabelValueInput.test.js.snap b/ui/src/Components/SilenceModal/SilenceMatch/__snapshots__/LabelValueInput.test.js.snap index 7e5bb822a..840cb94b5 100644 --- a/ui/src/Components/SilenceModal/SilenceMatch/__snapshots__/LabelValueInput.test.js.snap +++ b/ui/src/Components/SilenceModal/SilenceMatch/__snapshots__/LabelValueInput.test.js.snap @@ -4,7 +4,7 @@ exports[` matches snapshot 1`] = ` "
-
+
({ state.isMulti ? { ...base, - borderRadius: 0, + borderTopLeftRadius: "0.25rem", + borderBottomLeftRadius: "0.25rem", backgroundColor: state.isDisabled ? theme.disabledValueContainerBackground : theme.valueContainerBackground, @@ -65,7 +66,8 @@ const ReactSelectStyles = theme => ({ } : { ...base, - borderRadius: 0, + borderTopLeftRadius: "0.25rem", + borderBottomLeftRadius: "0.25rem", backgroundColor: state.isDisabled ? theme.disabledValueContainerBackground : theme.valueContainerBackground