feat(ui): allow configuring default collapse state for alert groups

This commit is contained in:
Łukasz Mierzwa
2019-04-03 14:13:57 -07:00
parent 5bffa0af71
commit 2509704190
10 changed files with 611 additions and 37 deletions

View File

@@ -61,30 +61,45 @@ const AlertGroup = observer(
constructor(props) {
super(props);
const { alertGroupConfig } = props.settingsStore;
this.defaultRenderCount = toJS(
props.settingsStore.alertGroupConfig.config.defaultRenderCount
alertGroupConfig.config.defaultRenderCount
);
this.renderConfig = observable({
alertsToRender: this.defaultRenderCount
});
}
// store collapse state, alert groups can be collapsed to only show
// the header, this is controlled by UI element on the header itself, so
// this observable needs to be passed down to it
collapse = observable(
{
value: IsMobile(),
toggle() {
this.value = !this.value;
}
},
{
toggle: action.bound
},
{ name: "Collpase toggle" }
);
let defaultCollapseState;
switch (alertGroupConfig.config.defaultCollapseState) {
case alertGroupConfig.options.collapsed.value:
defaultCollapseState = true;
break;
case alertGroupConfig.options.collapsedOnMobile.value:
defaultCollapseState = IsMobile();
break;
default:
defaultCollapseState = false;
break;
}
// store collapse state, alert groups can be collapsed to only show
// the header, this is controlled by UI element on the header itself, so
// this observable needs to be passed down to it
this.collapse = observable(
{
value: defaultCollapseState,
toggle() {
this.value = !this.value;
}
},
{
toggle: action.bound
},
{ name: "Collpase toggle" }
);
}
loadMore = action(() => {
const { group } = this.props;

View File

@@ -70,6 +70,22 @@ const MountedAlertGroup = (afterUpdate, showAlertmanagers) => {
);
};
const ValidateCollapse = (
innerWidth,
defaultCollapseState,
shouldBeCollapsed
) => {
global.innerWidth = innerWidth;
settingsStore.alertGroupConfig.config.defaultCollapseState = defaultCollapseState;
MockAlerts(3);
const tree = MountedAlertGroup(jest.fn(), false);
const alertGroup = tree.find("AlertGroup");
expect(alertGroup.instance().collapse.value).toBe(shouldBeCollapsed);
expect(tree.find("Alert")).toHaveLength(shouldBeCollapsed ? 0 : 3);
};
describe("<AlertGroup />", () => {
it("renders Alertmanager labels in footer if showAlertmanagersInFooter=true", () => {
MockAlerts(2);
@@ -109,26 +125,58 @@ describe("<AlertGroup />", () => {
expect(tree.find("ul.list-group")).toHaveLength(0);
});
it("is expanded by default on desktop", () => {
it("is collapsed by default on desktop when defaultCollapseState=collapsed", () => {
// set window.innerWidth to 2k to mock a desktop window
global.innerWidth = 2048;
MockAlerts(3);
const tree = MountedAlertGroup(jest.fn(), false);
const alertGroup = tree.find("AlertGroup");
expect(alertGroup.instance().collapse.value).toBe(false);
expect(tree.find("Alert")).toHaveLength(3);
ValidateCollapse(
2048,
settingsStore.alertGroupConfig.options.collapsed.value,
true
);
});
it("is collapsed by default on mobile", () => {
it("is collapsed by default on mobile when defaultCollapseState=collapsed", () => {
// set window.innerWidth to <768 to mock a mobile window
global.innerWidth = 700;
ValidateCollapse(
767,
settingsStore.alertGroupConfig.options.collapsed.value,
true
);
});
MockAlerts(3);
const tree = MountedAlertGroup(jest.fn(), false);
const alertGroup = tree.find("AlertGroup");
expect(alertGroup.instance().collapse.value).toBe(true);
expect(tree.find("Alert")).toHaveLength(0);
it("is expanded by default on desktop when defaultCollapseState=collapsedOnMobile", () => {
// set window.innerWidth to 2k to mock a desktop window
ValidateCollapse(
2048,
settingsStore.alertGroupConfig.options.collapsedOnMobile.value,
false
);
});
it("is collapsed by default on mobile when defaultCollapseState=collapsedOnMobile", () => {
// set window.innerWidth to <768 to mock a mobile window
ValidateCollapse(
767,
settingsStore.alertGroupConfig.options.collapsedOnMobile.value,
true
);
});
it("is expanded by default on desktop when defaultCollapseState=expanded", () => {
// set window.innerWidth to 2k to mock a desktop window
ValidateCollapse(
2048,
settingsStore.alertGroupConfig.options.expanded.value,
false
);
});
it("is expanded by default on mobile when defaultCollapseState=expanded", () => {
// set window.innerWidth to <768 to mock a mobile window
ValidateCollapse(
767,
settingsStore.alertGroupConfig.options.expanded.value,
false
);
});
});

View File

@@ -0,0 +1,80 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { action } from "mobx";
import { observer } from "mobx-react";
import ReactSelect from "react-select";
import { Settings } from "Stores/Settings";
import { ReactSelectStyles } from "Components/MultiSelect";
const AlertGroupCollapseConfiguration = observer(
class AlertGroupCollapseConfiguration extends Component {
static propTypes = {
settingsStore: PropTypes.instanceOf(Settings).isRequired
};
constructor(props) {
super(props);
this.validateConfig();
}
valueToOption = val => {
const { settingsStore } = this.props;
return {
label: settingsStore.alertGroupConfig.options[val].label,
value: val
};
};
validateConfig = action(() => {
const { settingsStore } = this.props;
if (
!Object.values(settingsStore.alertGroupConfig.options)
.map(o => o.value)
.includes(settingsStore.alertGroupConfig.config.defaultCollapseState)
) {
settingsStore.alertGroupConfig.config.defaultCollapseState =
settingsStore.alertGroupConfig.options.collapsedOnMobile.value;
}
});
onCollapseChange = action((newValue, actionMeta) => {
const { settingsStore } = this.props;
settingsStore.alertGroupConfig.config.defaultCollapseState =
newValue.value;
});
render() {
const { settingsStore } = this.props;
return (
<div className="form-group">
<div className="text-center">
<label className="mb-4 font-weight-bold">
Default alert group display
</label>
</div>
<ReactSelect
styles={ReactSelectStyles}
classNamePrefix="react-select"
instanceId="configuration-collapse"
defaultValue={this.valueToOption(
settingsStore.alertGroupConfig.config.defaultCollapseState
)}
options={Object.values(settingsStore.alertGroupConfig.options)}
onChange={this.onCollapseChange}
hideSelectedOptions
/>
</div>
);
}
}
);
export { AlertGroupCollapseConfiguration };

View File

@@ -0,0 +1,69 @@
import React from "react";
import { mount } from "enzyme";
import toDiffableHtml from "diffable-html";
import { Settings } from "Stores/Settings";
import { AlertGroupCollapseConfiguration } from "./AlertGroupCollapseConfiguration";
let settingsStore;
beforeEach(() => {
settingsStore = new Settings();
});
const FakeConfiguration = () => {
return mount(
<AlertGroupCollapseConfiguration settingsStore={settingsStore} />
);
};
describe("<AlertGroupCollapseConfiguration />", () => {
it("matches snapshot with default values", () => {
const tree = FakeConfiguration();
expect(toDiffableHtml(tree.html())).toMatchSnapshot();
});
it("resets stored config to defaults if it is invalid", done => {
settingsStore.alertGroupConfig.config.defaultCollapseState = "foo";
const tree = FakeConfiguration();
const select = tree.find(".react-select__value-container");
expect(select.text()).toBe(
settingsStore.alertGroupConfig.options.collapsedOnMobile.label
);
setTimeout(() => {
expect(settingsStore.alertGroupConfig.config.defaultCollapseState).toBe(
settingsStore.alertGroupConfig.options.collapsedOnMobile.value
);
done();
}, 200);
});
it("rendered correct default value", done => {
settingsStore.alertGroupConfig.config.defaultCollapseState =
settingsStore.alertGroupConfig.options.expanded.value;
const tree = FakeConfiguration();
const select = tree.find(".react-select__value-container");
setTimeout(() => {
expect(select.text()).toBe(
settingsStore.alertGroupConfig.options.expanded.label
);
done();
}, 200);
});
it("clicking on a label option updates settingsStore", done => {
const tree = FakeConfiguration();
tree
.find("input#react-select-configuration-collapse-input")
.simulate("change", { target: { value: " " } });
const options = tree.find(".react-select__option");
options.at(1).simulate("click");
setTimeout(() => {
expect(settingsStore.alertGroupConfig.config.defaultCollapseState).toBe(
settingsStore.alertGroupConfig.options.collapsed.value
);
done();
}, 200);
});
});

View File

@@ -0,0 +1,59 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<AlertGroupCollapseConfiguration /> matches snapshot with default values 1`] = `
"
<div class=\\"form-group\\">
<div class=\\"text-center\\">
<label class=\\"mb-4 font-weight-bold\\">
Default alert group display
</label>
</div>
<div class=\\"css-10nd86i\\">
<div class=\\"css-7jxtyj react-select__control\\">
<div class=\\"css-pb81dw react-select__value-container react-select__value-container--has-value\\">
<div class=\\"css-xp4uvy react-select__single-value\\">
Collapse on mobile
</div>
<div class=\\"css-1g6gooi\\">
<div class=\\"react-select__input\\"
style=\\"display: inline-block;\\"
>
<input autocapitalize=\\"none\\"
autocomplete=\\"off\\"
autocorrect=\\"off\\"
id=\\"react-select-configuration-collapse-input\\"
spellcheck=\\"false\\"
tabindex=\\"0\\"
type=\\"text\\"
aria-autocomplete=\\"list\\"
style=\\"box-sizing: content-box; width: 2px; border: 0px; font-size: inherit; opacity: 1; outline: 0; padding: 0px;\\"
value
>
<div style=\\"position: absolute; top: 0px; left: 0px; visibility: hidden; height: 0px; overflow: scroll; white-space: pre; font-size: inherit; font-family: -webkit-small-control; letter-spacing: normal; text-transform: none;\\">
</div>
</div>
</div>
</div>
<div class=\\"css-mik995 react-select__indicators\\">
<span class=\\"css-d8oujb react-select__indicator-separator\\">
</span>
<div aria-hidden=\\"true\\"
class=\\"css-1ep9fjw react-select__indicator react-select__dropdown-indicator\\"
>
<svg height=\\"20\\"
width=\\"20\\"
viewbox=\\"0 0 20 20\\"
aria-hidden=\\"true\\"
focusable=\\"false\\"
class=\\"css-19bqh2r\\"
>
<path d=\\"M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z\\">
</path>
</svg>
</div>
</div>
</div>
</div>
</div>
"
`;

View File

@@ -0,0 +1,234 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Configuration /> matches snapshot 1`] = `
"
<form class=\\"px-3\\">
<div class=\\"form-group text-center\\">
<label class=\\"mb-4 font-weight-bold\\">
Refresh interval
</label>
<div aria-disabled=\\"false\\"
class=\\"input-range\\"
>
<span class=\\"input-range__label input-range__label--min\\">
<span class=\\"input-range__label-container\\">
10s
</span>
</span>
<div class=\\"input-range__track input-range__track--background\\">
<div style=\\"left:0%;width:18.181818181818183%\\"
class=\\"input-range__track input-range__track--active\\"
>
</div>
<span class=\\"input-range__slider-container\\"
style=\\"position:absolute;left:18.181818181818183%\\"
>
<span class=\\"input-range__label input-range__label--value\\">
<span class=\\"input-range__label-container\\">
30s
</span>
</span>
<div aria-valuemax=\\"120\\"
aria-valuemin=\\"10\\"
aria-valuenow=\\"30\\"
class=\\"input-range__slider\\"
draggable=\\"false\\"
role=\\"slider\\"
tabindex=\\"0\\"
>
</div>
</span>
</div>
<span class=\\"input-range__label input-range__label--max\\">
<span class=\\"input-range__label-container\\">
120s
</span>
</span>
</div>
</div>
<div class=\\"mt-5\\">
</div>
<div class=\\"form-group\\">
<div class=\\"text-center\\">
<label class=\\"mb-2 font-weight-bold\\">
Filter bar configuration
</label>
</div>
<div class=\\"form-check form-check-inline\\">
<span class=\\"custom-control custom-switch\\">
<input type=\\"checkbox\\"
id=\\"configuration-autohide\\"
class=\\"custom-control-input\\"
value
checked
>
<label class=\\"custom-control-label cursor-pointer mr-3\\"
for=\\"configuration-autohide\\"
>
Hide filter bar when idle
</label>
</span>
</div>
</div>
<div class=\\"mt-5\\">
</div>
<div class=\\"form-group text-center\\">
<label class=\\"mb-4 font-weight-bold\\">
Default number of alerts to show per group
</label>
<div aria-disabled=\\"false\\"
class=\\"input-range\\"
>
<span class=\\"input-range__label input-range__label--min\\">
<span class=\\"input-range__label-container\\">
1
</span>
</span>
<div class=\\"input-range__track input-range__track--background\\">
<div style=\\"left:0%;width:44.44444444444444%\\"
class=\\"input-range__track input-range__track--active\\"
>
</div>
<span class=\\"input-range__slider-container\\"
style=\\"position:absolute;left:44.44444444444444%\\"
>
<span class=\\"input-range__label input-range__label--value\\">
<span class=\\"input-range__label-container\\">
5
</span>
</span>
<div aria-valuemax=\\"10\\"
aria-valuemin=\\"1\\"
aria-valuenow=\\"5\\"
class=\\"input-range__slider\\"
draggable=\\"false\\"
role=\\"slider\\"
tabindex=\\"0\\"
>
</div>
</span>
</div>
<span class=\\"input-range__label input-range__label--max\\">
<span class=\\"input-range__label-container\\">
10
</span>
</span>
</div>
</div>
<div class=\\"mt-5\\">
</div>
<div class=\\"form-group\\">
<div class=\\"text-center\\">
<label class=\\"mb-4 font-weight-bold\\">
Default alert group display
</label>
</div>
<div class=\\"css-10nd86i\\">
<div class=\\"css-7jxtyj react-select__control\\">
<div class=\\"css-pb81dw react-select__value-container react-select__value-container--has-value\\">
<div class=\\"css-xp4uvy react-select__single-value\\">
Collapse on mobile
</div>
<div class=\\"css-1g6gooi\\">
<div class=\\"react-select__input\\"
style=\\"display:inline-block\\"
>
<input type=\\"text\\"
autocapitalize=\\"none\\"
autocomplete=\\"off\\"
autocorrect=\\"off\\"
id=\\"react-select-configuration-collapse-input\\"
spellcheck=\\"false\\"
tabindex=\\"0\\"
value
aria-autocomplete=\\"list\\"
style=\\"box-sizing:content-box;width:1px;background:0;border:0;font-size:inherit;opacity:1;outline:0;padding:0;color:inherit\\"
>
<div style=\\"position:absolute;top:0;left:0;visibility:hidden;height:0;overflow:scroll;white-space:pre\\">
</div>
</div>
</div>
</div>
<div class=\\"css-mik995 react-select__indicators\\">
<span class=\\"css-d8oujb react-select__indicator-separator\\">
</span>
<div aria-hidden=\\"true\\"
class=\\"css-1ep9fjw react-select__indicator react-select__dropdown-indicator\\"
>
<svg height=\\"20\\"
width=\\"20\\"
viewbox=\\"0 0 20 20\\"
aria-hidden=\\"true\\"
focusable=\\"false\\"
class=\\"css-19bqh2r\\"
>
<path d=\\"M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z\\">
</path>
</svg>
</div>
</div>
</div>
</div>
</div>
<div class=\\"mt-5\\">
</div>
<div class=\\"form-group\\">
<div class=\\"text-center\\">
<label class=\\"mb-2 font-weight-bold\\">
Grid sort order
</label>
</div>
<div class=\\"d-flex flex-fill flex-lg-row flex-column justify-content-between\\">
<div class=\\"flex-shrink-0 flex-grow-1 flex-basis-auto\\">
<div class=\\"css-10nd86i\\">
<div class=\\"css-7jxtyj react-select__control\\">
<div class=\\"css-pb81dw react-select__value-container react-select__value-container--has-value\\">
<div class=\\"css-xp4uvy react-select__single-value\\">
Use defaults from karma config file
</div>
<div class=\\"css-1g6gooi\\">
<div class=\\"react-select__input\\"
style=\\"display:inline-block\\"
>
<input type=\\"text\\"
autocapitalize=\\"none\\"
autocomplete=\\"off\\"
autocorrect=\\"off\\"
id=\\"react-select-configuration-sort-order-input\\"
spellcheck=\\"false\\"
tabindex=\\"0\\"
value
aria-autocomplete=\\"list\\"
style=\\"box-sizing:content-box;width:1px;background:0;border:0;font-size:inherit;opacity:1;outline:0;padding:0;color:inherit\\"
>
<div style=\\"position:absolute;top:0;left:0;visibility:hidden;height:0;overflow:scroll;white-space:pre\\">
</div>
</div>
</div>
</div>
<div class=\\"css-mik995 react-select__indicators\\">
<span class=\\"css-d8oujb react-select__indicator-separator\\">
</span>
<div aria-hidden=\\"true\\"
class=\\"css-1ep9fjw react-select__indicator react-select__dropdown-indicator\\"
>
<svg height=\\"20\\"
width=\\"20\\"
viewbox=\\"0 0 20 20\\"
aria-hidden=\\"true\\"
focusable=\\"false\\"
class=\\"css-19bqh2r\\"
>
<path d=\\"M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z\\">
</path>
</svg>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
"
`;

View File

@@ -6,6 +6,7 @@ import { FetchConfiguration } from "./FetchConfiguration";
import { FilterBarConfiguration } from "./FilterBarConfiguration";
import { AlertGroupConfiguration } from "./AlertGroupConfiguration";
import { AlertGroupSortConfiguration } from "./AlertGroupSortConfiguration";
import { AlertGroupCollapseConfiguration } from "./AlertGroupCollapseConfiguration";
const Configuration = ({ settingsStore }) => (
<form className="px-3">
@@ -15,6 +16,8 @@ const Configuration = ({ settingsStore }) => (
<div className="mt-5" />
<AlertGroupConfiguration settingsStore={settingsStore} />
<div className="mt-5" />
<AlertGroupCollapseConfiguration settingsStore={settingsStore} />
<div className="mt-5" />
<AlertGroupSortConfiguration settingsStore={settingsStore} />
</form>
);

View File

@@ -2,15 +2,15 @@ import React from "react";
import { shallow } from "enzyme";
import toDiffableHtml from "diffable-html";
import { Settings } from "Stores/Settings";
import { Configuration } from ".";
describe("<Configuration />", () => {
it("renders correctly", () => {
it("matches snapshot", () => {
const settingsStore = new Settings();
const tree = shallow(<Configuration settingsStore={settingsStore} />);
expect(tree.text()).toBe(
"<FetchConfiguration /><FilterBarConfiguration /><AlertGroupConfiguration /><AlertGroupSortConfiguration />"
);
expect(toDiffableHtml(tree.html())).toMatchSnapshot();
});
});

View File

@@ -136,6 +136,61 @@ exports[`<MainModalContent /> matches snapshot 1`] = `
</div>
<div class=\\"mt-5\\">
</div>
<div class=\\"form-group\\">
<div class=\\"text-center\\">
<label class=\\"mb-4 font-weight-bold\\">
Default alert group display
</label>
</div>
<div class=\\"css-10nd86i\\">
<div class=\\"css-7jxtyj react-select__control\\">
<div class=\\"css-pb81dw react-select__value-container react-select__value-container--has-value\\">
<div class=\\"css-xp4uvy react-select__single-value\\">
Collapse on mobile
</div>
<div class=\\"css-1g6gooi\\">
<div class=\\"react-select__input\\"
style=\\"display: inline-block;\\"
>
<input autocapitalize=\\"none\\"
autocomplete=\\"off\\"
autocorrect=\\"off\\"
id=\\"react-select-configuration-collapse-input\\"
spellcheck=\\"false\\"
tabindex=\\"0\\"
type=\\"text\\"
aria-autocomplete=\\"list\\"
style=\\"box-sizing: content-box; width: 2px; border: 0px; font-size: inherit; opacity: 1; outline: 0; padding: 0px;\\"
value
>
<div style=\\"position: absolute; top: 0px; left: 0px; visibility: hidden; height: 0px; overflow: scroll; white-space: pre; font-size: inherit; font-family: -webkit-small-control; letter-spacing: normal; text-transform: none;\\">
</div>
</div>
</div>
</div>
<div class=\\"css-mik995 react-select__indicators\\">
<span class=\\"css-d8oujb react-select__indicator-separator\\">
</span>
<div aria-hidden=\\"true\\"
class=\\"css-1ep9fjw react-select__indicator react-select__dropdown-indicator\\"
>
<svg height=\\"20\\"
width=\\"20\\"
viewbox=\\"0 0 20 20\\"
aria-hidden=\\"true\\"
focusable=\\"false\\"
class=\\"css-19bqh2r\\"
>
<path d=\\"M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z\\">
</path>
</svg>
</div>
</div>
</div>
</div>
</div>
<div class=\\"mt-5\\">
</div>
<div class=\\"form-group\\">
<div class=\\"text-center\\">
<label class=\\"mb-2 font-weight-bold\\">

View File

@@ -33,9 +33,20 @@ class FetchConfig {
}
class AlertGroupConfig {
options = Object.freeze({
expanded: { label: "Always expanded", value: "expanded" },
collapsedOnMobile: {
label: "Collapse on mobile",
value: "collapsedOnMobile"
},
collapsed: { label: "Always collapsed", value: "collapsed" }
});
config = localStored(
"alertGroupConfig",
{ defaultRenderCount: 5 },
{
defaultRenderCount: 5,
defaultCollapseState: this.options.collapsedOnMobile.value
},
{ delay: 100 }
);