feat(ui): ui default settings are populated from the config file

This commit is contained in:
Łukasz Mierzwa
2019-10-04 23:04:53 +01:00
parent d7c15240f1
commit f43d9ece7d
9 changed files with 192 additions and 61 deletions

View File

@@ -1,16 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta name="theme-color" content="#000000" />
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<link rel="stylesheet" type="text/css" href="%PUBLIC_URL%/custom.css" media="screen" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<link
rel="stylesheet"
type="text/css"
href="%PUBLIC_URL%/custom.css"
media="screen"
/>
<title>karma</title>
</head>
<body>
@@ -21,10 +29,12 @@
Settings span is used to pass config keys that needs to be accessible
early, before the UI app is started.
-->
<span id="settings"
data-sentry-dsn="{{ .SentryDSN }}"
data-version="{{ .Version }}"
data-default-filters-base64="{{ .DefaultFilter }}">
<span
id="settings"
data-sentry-dsn="{{ .SentryDSN }}"
data-version="{{ .Version }}"
data-default-filters-base64="{{ .DefaultFilter }}"
>
</span>
<div id="root"></div>
<!--
@@ -38,5 +48,8 @@
To create a production bundle, use `npm run build` or `yarn build`.
-->
<script src="%PUBLIC_URL%/custom.js"></script>
<script type="text/plain" id="defaults">
{{ .Defaults }}
</script>
</body>
</html>

View File

@@ -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;

View File

@@ -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("<App />", () => {
it("uses passed default filters if there's no query args or saved filters", () => {
expect(window.location.search).toBe("");
const tree = shallow(<App defaultFilters={["foo=bar"]} />);
const tree = shallow(
<App defaultFilters={["foo=bar"]} uiDefaults={uiDefaults} />
);
const instance = tree.instance();
expect(instance.alertStore.filters.values).toHaveLength(1);
expect(instance.alertStore.filters.values[0]).toMatchObject(
@@ -40,7 +51,9 @@ describe("<App />", () => {
// https://github.com/facebook/jest/issues/6798#issuecomment-412871616
const getItemSpy = jest.spyOn(Storage.prototype, "getItem");
const tree = shallow(<App defaultFilters={["ignore=defaults"]} />);
const tree = shallow(
<App defaultFilters={["ignore=defaults"]} uiDefaults={uiDefaults} />
);
const instance = tree.instance();
expect(getItemSpy).toHaveBeenCalledWith("savedFilters");
@@ -69,7 +82,9 @@ describe("<App />", () => {
// https://github.com/facebook/jest/issues/6798#issuecomment-412871616
const getItemSpy = jest.spyOn(Storage.prototype, "getItem");
const tree = shallow(<App defaultFilters={["use=defaults"]} />);
const tree = shallow(
<App defaultFilters={["use=defaults"]} uiDefaults={uiDefaults} />
);
const instance = tree.instance();
expect(getItemSpy).toHaveBeenCalledWith("savedFilters");
@@ -94,7 +109,9 @@ describe("<App />", () => {
window.history.pushState({}, "App", "/?q=use%3Dquery");
const tree = shallow(<App defaultFilters={["ignore=defaults"]} />);
const tree = shallow(
<App defaultFilters={["ignore=defaults"]} uiDefaults={uiDefaults} />
);
const instance = tree.instance();
expect(instance.alertStore.filters.values).toHaveLength(1);

View File

@@ -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 };

View File

@@ -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();
});
});

View File

@@ -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
);
}
}

View File

@@ -0,0 +1,12 @@
const DefaultsBase64 =
"eyJSZWZyZXNoIjo0NTAwMDAwMDAwMCwiSGlkZUZpbHRlcnNXaGVuSWRsZSI6ZmFsc2UsIkNvbG9yVGl0bGViYXIiOmZhbHNlLCJNaW5pbWFsR3JvdXBXaWR0aCI6NTU1LCJBbGVydHNQZXJHcm91cCI6MTUsIkNvbGxhcHNlR3JvdXBzIjoiZXhwYW5kZWQifQo=";
const DefaultsObject = {
Refresh: 45000000000,
HideFiltersWhenIdle: false,
ColorTitlebar: false,
MinimalGroupWidth: 555,
AlertsPerGroup: 15,
CollapseGroups: "expanded"
};
export { DefaultsBase64, DefaultsObject };

View File

@@ -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(
<App defaultFilters={defaultFilters} />,
<App defaultFilters={defaultFilters} uiDefaults={uiDefaults} />,
document.getElementById("root") || document.createElement("div")
);

View File

@@ -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: `<div id="defaults">${DefaultsBase64}</div>`
};
});
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=");
});