mirror of
https://github.com/prymitive/karma
synced 2026-05-07 03:26:52 +00:00
feat(ui): ui default settings are populated from the config file
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
12
ui/src/__mocks__/Defaults.js
Normal file
12
ui/src/__mocks__/Defaults.js
Normal 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 };
|
||||
@@ -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")
|
||||
);
|
||||
|
||||
@@ -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=");
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user