From f43d9ece7d70da3fd7ed752671e27494b917455f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Fri, 4 Oct 2019 23:04:53 +0100 Subject: [PATCH] feat(ui): ui default settings are populated from the config file --- ui/public/index.html | 33 +++++++---- ui/src/App.js | 18 +++++- ui/src/App.test.js | 25 ++++++-- ui/src/AppBoot.js | 11 +++- ui/src/AppBoot.test.js | 20 ++++++- ui/src/Stores/Settings.js | 109 +++++++++++++++++++++++------------ ui/src/__mocks__/Defaults.js | 12 ++++ ui/src/index.js | 13 ++++- ui/src/index.test.js | 12 +++- 9 files changed, 192 insertions(+), 61 deletions(-) create mode 100644 ui/src/__mocks__/Defaults.js diff --git a/ui/public/index.html b/ui/public/index.html index df4b5c695..14fa72e38 100644 --- a/ui/public/index.html +++ b/ui/public/index.html @@ -1,16 +1,24 @@ - - - + + + - - - + + + karma @@ -21,10 +29,12 @@ Settings span is used to pass config keys that needs to be accessible early, before the UI app is started. --> - +
+ diff --git a/ui/src/App.js b/ui/src/App.js index b25ded833..c2adfeb6d 100644 --- a/ui/src/App.js +++ b/ui/src/App.js @@ -16,16 +16,28 @@ import "./App.scss"; class App extends Component { static propTypes = { - defaultFilters: PropTypes.arrayOf(PropTypes.string).isRequired + defaultFilters: PropTypes.arrayOf(PropTypes.string).isRequired, + uiDefaults: PropTypes.exact({ + Refresh: PropTypes.number.isRequired, + HideFiltersWhenIdle: PropTypes.bool.isRequired, + ColorTitlebar: PropTypes.bool.isRequired, + MinimalGroupWidth: PropTypes.number.isRequired, + AlertsPerGroup: PropTypes.number.isRequired, + CollapseGroups: PropTypes.oneOf([ + "expanded", + "collapsed", + "collapsedOnMobile" + ]).isRequired + }) }; constructor(props) { super(props); - const { defaultFilters } = this.props; + const { defaultFilters, uiDefaults } = this.props; this.silenceFormStore = new SilenceFormStore(); - this.settingsStore = new Settings(); + this.settingsStore = new Settings(uiDefaults); let filters; diff --git a/ui/src/App.test.js b/ui/src/App.test.js index 4ebd36029..9862a8e57 100644 --- a/ui/src/App.test.js +++ b/ui/src/App.test.js @@ -5,6 +5,15 @@ import { shallow } from "enzyme"; import { NewUnappliedFilter } from "Stores/AlertStore"; import { App } from "./App"; +const uiDefaults = { + Refresh: 30 * 1000 * 1000 * 1000, + HideFiltersWhenIdle: true, + ColorTitlebar: false, + MinimalGroupWidth: 420, + AlertsPerGroup: 5, + CollapseGroups: "collapsedOnMobile" +}; + beforeEach(() => { // createing App instance will push current filters into window.location // ensure it's wiped after each test @@ -19,7 +28,9 @@ afterEach(() => { describe("", () => { it("uses passed default filters if there's no query args or saved filters", () => { expect(window.location.search).toBe(""); - const tree = shallow(); + const tree = shallow( + + ); const instance = tree.instance(); expect(instance.alertStore.filters.values).toHaveLength(1); expect(instance.alertStore.filters.values[0]).toMatchObject( @@ -40,7 +51,9 @@ describe("", () => { // https://github.com/facebook/jest/issues/6798#issuecomment-412871616 const getItemSpy = jest.spyOn(Storage.prototype, "getItem"); - const tree = shallow(); + const tree = shallow( + + ); const instance = tree.instance(); expect(getItemSpy).toHaveBeenCalledWith("savedFilters"); @@ -69,7 +82,9 @@ describe("", () => { // https://github.com/facebook/jest/issues/6798#issuecomment-412871616 const getItemSpy = jest.spyOn(Storage.prototype, "getItem"); - const tree = shallow(); + const tree = shallow( + + ); const instance = tree.instance(); expect(getItemSpy).toHaveBeenCalledWith("savedFilters"); @@ -94,7 +109,9 @@ describe("", () => { window.history.pushState({}, "App", "/?q=use%3Dquery"); - const tree = shallow(); + const tree = shallow( + + ); const instance = tree.instance(); expect(instance.alertStore.filters.values).toHaveLength(1); diff --git a/ui/src/AppBoot.js b/ui/src/AppBoot.js index 78771134c..bf60eb3ab 100644 --- a/ui/src/AppBoot.js +++ b/ui/src/AppBoot.js @@ -51,4 +51,13 @@ const ParseDefaultFilters = settingsElement => { return defaultFilters; }; -export { SettingsElement, SetupSentry, ParseDefaultFilters }; +const ParseUIDefaults = b64data => { + const decoded = Buffer.from(b64data, "base64").toString("ascii"); + try { + return JSON.parse(decoded); + } catch { + return undefined; + } +}; + +export { SettingsElement, SetupSentry, ParseDefaultFilters, ParseUIDefaults }; diff --git a/ui/src/AppBoot.test.js b/ui/src/AppBoot.test.js index ae7052af7..e42a84cc3 100644 --- a/ui/src/AppBoot.test.js +++ b/ui/src/AppBoot.test.js @@ -1,6 +1,12 @@ import * as Sentry from "@sentry/browser"; -import { SettingsElement, SetupSentry, ParseDefaultFilters } from "./AppBoot"; +import { DefaultsBase64, DefaultsObject } from "__mocks__/Defaults"; +import { + SettingsElement, + SetupSentry, + ParseDefaultFilters, + ParseUIDefaults +} from "./AppBoot"; beforeAll(() => { // https://github.com/jamesmfriedman/rmwc/issues/103#issuecomment-360007979 @@ -129,3 +135,15 @@ describe("ParseDefaultFilters()", () => { expect(filters).toHaveLength(0); }); }); + +describe("ParseUIDefaults()", () => { + it("parses base64 encoded JSON with defaults", () => { + const uiDefaults = ParseUIDefaults(DefaultsBase64); + expect(uiDefaults).toStrictEqual(DefaultsObject); + }); + + it("returns undefined on invalid JSON", () => { + const uiDefaults = ParseUIDefaults("e3h4eC9mZgo="); + expect(uiDefaults).toBeUndefined(); + }); +}); diff --git a/ui/src/Stores/Settings.js b/ui/src/Stores/Settings.js index 8a4eed34f..114588e53 100644 --- a/ui/src/Stores/Settings.js +++ b/ui/src/Stores/Settings.js @@ -25,11 +25,17 @@ class SavedFilters { } class FetchConfig { - config = localStored("fetchConfig", { interval: 30 }, { delay: 100 }); + constructor(refresh) { + this.config = localStored( + "fetchConfig", + { interval: refresh }, + { delay: 100 } + ); - setInterval = action(newInterval => { - this.config.interval = newInterval; - }); + this.setInterval = action(newInterval => { + this.config.interval = newInterval; + }); + } } class AlertGroupConfig { @@ -41,15 +47,18 @@ class AlertGroupConfig { }, collapsed: { label: "Always collapsed", value: "collapsed" } }); - config = localStored( - "alertGroupConfig", - { - defaultRenderCount: 5, - defaultCollapseState: this.options.collapsedOnMobile.value, - colorTitleBar: false - }, - { delay: 100 } - ); + + constructor(renderCount, collapseState, colorTitleBar) { + this.config = localStored( + "alertGroupConfig", + { + defaultRenderCount: renderCount, + defaultCollapseState: collapseState, + colorTitleBar: colorTitleBar + }, + { delay: 100 } + ); + } update = action(data => { for (const [key, val] of Object.entries(data)) { @@ -73,38 +82,64 @@ class GridConfig { startsAt: { label: "Sort by alert timestamp", value: "startsAt" }, label: { label: "Sort by alert label", value: "label" } }); - config = localStored( - "alertGridConfig", - { - sortOrder: this.options.default.value, - sortLabel: null, - reverseSort: null, - groupWidth: 420 - }, - { delay: 100 } - ); + constructor(groupWidth) { + this.config = localStored( + "alertGridConfig", + { + sortOrder: this.options.default.value, + sortLabel: null, + reverseSort: null, + groupWidth: groupWidth + }, + { delay: 100 } + ); + } } class FilterBarConfig { - config = localStored( - "filterBarConfig", - { - autohide: true - }, - { - delay: 100 - } - ); + constructor(autohide) { + this.config = localStored( + "filterBarConfig", + { + autohide: autohide + }, + { + delay: 100 + } + ); + } } class Settings { - constructor() { + constructor(defaults) { + let defaultSettings; + if (defaults === undefined) { + defaultSettings = { + Refresh: 30 * 1000 * 1000 * 1000, + HideFiltersWhenIdle: true, + ColorTitlebar: false, + MinimalGroupWidth: 420, + AlertsPerGroup: 5, + CollapseGroups: "collapsedOnMobile" + }; + } else { + defaultSettings = defaults; + } + this.savedFilters = new SavedFilters(); - this.fetchConfig = new FetchConfig(); - this.alertGroupConfig = new AlertGroupConfig(); - this.gridConfig = new GridConfig(); + this.fetchConfig = new FetchConfig( + defaultSettings.Refresh / 1000 / 1000 / 1000 + ); + this.alertGroupConfig = new AlertGroupConfig( + defaultSettings.AlertsPerGroup, + defaultSettings.CollapseGroups, + defaultSettings.ColorTitlebar + ); + this.gridConfig = new GridConfig(defaultSettings.MinimalGroupWidth); this.silenceFormConfig = new SilenceFormConfig(); - this.filterBarConfig = new FilterBarConfig(); + this.filterBarConfig = new FilterBarConfig( + defaultSettings.HideFiltersWhenIdle + ); } } diff --git a/ui/src/__mocks__/Defaults.js b/ui/src/__mocks__/Defaults.js new file mode 100644 index 000000000..d647135c3 --- /dev/null +++ b/ui/src/__mocks__/Defaults.js @@ -0,0 +1,12 @@ +const DefaultsBase64 = + "eyJSZWZyZXNoIjo0NTAwMDAwMDAwMCwiSGlkZUZpbHRlcnNXaGVuSWRsZSI6ZmFsc2UsIkNvbG9yVGl0bGViYXIiOmZhbHNlLCJNaW5pbWFsR3JvdXBXaWR0aCI6NTU1LCJBbGVydHNQZXJHcm91cCI6MTUsIkNvbGxhcHNlR3JvdXBzIjoiZXhwYW5kZWQifQo="; +const DefaultsObject = { + Refresh: 45000000000, + HideFiltersWhenIdle: false, + ColorTitlebar: false, + MinimalGroupWidth: 555, + AlertsPerGroup: 15, + CollapseGroups: "expanded" +}; + +export { DefaultsBase64, DefaultsObject }; diff --git a/ui/src/index.js b/ui/src/index.js index c5362d696..fd45b7331 100644 --- a/ui/src/index.js +++ b/ui/src/index.js @@ -9,9 +9,18 @@ import ReactDOM from "react-dom"; import Moment from "react-moment"; -import { SettingsElement, SetupSentry, ParseDefaultFilters } from "./AppBoot"; +import { + SettingsElement, + SetupSentry, + ParseDefaultFilters, + ParseUIDefaults +} from "./AppBoot"; import { App } from "./App"; +const uiDefaults = ParseUIDefaults( + document.getElementById("defaults").innerHTML +); + const settingsElement = SettingsElement(); SetupSentry(settingsElement); @@ -25,6 +34,6 @@ const defaultFilters = ParseDefaultFilters(settingsElement); // https://wetainment.com/testing-indexjs/ export default ReactDOM.render( - , + , document.getElementById("root") || document.createElement("div") ); diff --git a/ui/src/index.test.js b/ui/src/index.test.js index acb9fe90a..70e6e7d83 100644 --- a/ui/src/index.test.js +++ b/ui/src/index.test.js @@ -1,6 +1,12 @@ import { EmptyAPIResponse } from "__mocks__/Fetch"; +import { DefaultsBase64 } from "__mocks__/Defaults"; it("renders without crashing", () => { + jest.spyOn(document, "getElementById").mockImplementationOnce(() => { + return { + innerHTML: `
${DefaultsBase64}
` + }; + }); const response = EmptyAPIResponse(); response.filters = []; fetch.mockResponse(JSON.stringify(response)); @@ -23,19 +29,19 @@ describe("console", () => { it("console.info() throws an error", () => { expect(() => { - console.warn("foo", "bar", "abc"); + console.info("foo", "bar", "abc"); }).toThrowError("message=foo args=bar,abc"); }); it("console.log() throws an error", () => { expect(() => { - console.warn("foo bar"); + console.log("foo bar"); }).toThrowError("message=foo bar args="); }); it("console.trace() throws an error", () => { expect(() => { - console.warn(); + console.trace(); }).toThrowError("message=undefined args="); });