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=");
});