feat(ui): allow sorting alert grid

This adds the ability for user to sort the grid of alerts via selected attribute.
UI configuration is provided to setup if timestamps or labels should be used to sort alerts.
This commit is contained in:
Łukasz Mierzwa
2019-02-15 15:55:37 +00:00
parent 0498c27896
commit da6368288a
14 changed files with 680 additions and 15 deletions

View File

@@ -4,6 +4,8 @@ import PropTypes from "prop-types";
import { observable, action } from "mobx";
import { observer } from "mobx-react";
import moment from "moment";
import MasonryInfiniteScroller from "react-masonry-infinite";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -67,6 +69,42 @@ const AlertGrid = observer(
);
});
compare = (a, b) => {
const { settingsStore } = this.props;
// don't sort if sorting is disabled
if (
settingsStore.gridConfig.config.sortOrder ===
settingsStore.gridConfig.options.disabled.value
)
return 0;
const getLabelValue = g => {
// if timestamp sort is enabled use latest alert for sorting
if (
settingsStore.gridConfig.config.sortOrder ===
settingsStore.gridConfig.options.startsAt.value
) {
return moment.max(g.alerts.map(a => moment(a.startsAt)));
}
const label = settingsStore.gridConfig.config.sortLabel;
return g.labels[label] || g.alerts[0].labels[label] || "";
};
const av = getLabelValue(a);
const bv = getLabelValue(b);
const val = settingsStore.gridConfig.config.reverseSort ? -1 : 1;
if (av > bv) {
return val;
} else if (av < bv) {
return val * -1;
} else {
return 0;
}
};
componentDidUpdate() {
// whenever grid component re-renders we need to ensure that grid elements
// are packed correctly
@@ -94,13 +132,13 @@ const AlertGrid = observer(
</div>
}
>
{Object.keys(alertStore.data.groups)
.sort()
{Object.values(alertStore.data.groups)
.sort(this.compare)
.slice(0, this.groupsToRender.value)
.map(id => (
.map(group => (
<AlertGroup
key={id}
group={alertStore.data.groups[id]}
key={group.id}
group={group}
showAlertmanagers={
Object.keys(alertStore.data.upstreams.clusters).length > 1
}

View File

@@ -46,12 +46,12 @@ const MockGroup = (groupName, alertCount) => {
return group;
};
const MockGroupList = count => {
const MockGroupList = (count, alertPerGroup) => {
let groups = {};
for (let i = 1; i <= count; i++) {
let id = `id${i}`;
let hash = `hash${i}`;
let group = MockGroup(`group${i}`, count);
let group = MockGroup(`group${i}`, alertPerGroup);
group.id = id;
group.hash = hash;
groups[id] = group;
@@ -66,14 +66,14 @@ const MockGroupList = count => {
describe("<AlertGrid />", () => {
it("renders only first 50 alert groups", () => {
MockGroupList(60);
MockGroupList(60, 5);
const tree = ShallowAlertGrid();
const alertGroups = tree.find("AlertGroup");
expect(alertGroups).toHaveLength(50);
});
it("appends 30 groups after loadMore() call", () => {
MockGroupList(100);
MockGroupList(100, 5);
const tree = ShallowAlertGrid();
// call it directly, it should happen on scroll to the bottom of the page
tree.instance().loadMore();
@@ -107,6 +107,127 @@ describe("<AlertGrid />", () => {
const tree = ShallowAlertGrid();
const instance = tree.instance();
instance.storeMasonryRef("foo");
expect(instance.masonryComponentReference.ref).toBe("foo");
expect(instance.masonryComponentReference.ref).toEqual("foo");
});
it("doesn't sort groups when sorting is set to 'disabled'", () => {
settingsStore.gridConfig.config.sortOrder =
settingsStore.gridConfig.options.disabled.value;
settingsStore.gridConfig.config.reverseSort = false;
MockGroupList(3, 1);
const tree = ShallowAlertGrid();
const alertGroups = tree.find("AlertGroup");
expect(alertGroups.map(g => g.props().group.id)).toEqual([
"id1",
"id2",
"id3"
]);
});
it("doesn't sort groups when sorting is set to 'disabled' and 'reverse' is on", () => {
settingsStore.gridConfig.config.sortOrder =
settingsStore.gridConfig.options.disabled.value;
settingsStore.gridConfig.config.reverseSort = true;
MockGroupList(3, 1);
const tree = ShallowAlertGrid();
const alertGroups = tree.find("AlertGroup");
expect(alertGroups.map(g => g.props().group.id)).toEqual([
"id1",
"id2",
"id3"
]);
});
it("groups are sorted by timestamp when sorting is set to 'startsAt'", () => {
settingsStore.gridConfig.config.sortOrder =
settingsStore.gridConfig.options.startsAt.value;
settingsStore.gridConfig.config.reverseSort = false;
MockGroupList(3, 1);
alertStore.data.groups.id1.alerts[0].startsAt = "2001-01-01T00:00:00Z";
alertStore.data.groups.id2.alerts[0].startsAt = "2002-01-01T00:00:00Z";
alertStore.data.groups.id3.alerts[0].startsAt = "2000-01-01T00:00:00Z";
const tree = ShallowAlertGrid();
const alertGroups = tree.find("AlertGroup");
expect(alertGroups.map(g => g.props().group.id)).toEqual([
"id3",
"id1",
"id2"
]);
});
it("groups are sorted by reversed timestamp when sorting is set to 'startsAt' and 'reverse' is on", () => {
settingsStore.gridConfig.config.sortOrder =
settingsStore.gridConfig.options.startsAt.value;
settingsStore.gridConfig.config.reverseSort = true;
MockGroupList(3, 1);
alertStore.data.groups.id1.alerts[0].startsAt = "2001-01-01T00:00:00Z";
alertStore.data.groups.id2.alerts[0].startsAt = "2002-01-01T00:00:00Z";
alertStore.data.groups.id3.alerts[0].startsAt = "2000-01-01T00:00:00Z";
const tree = ShallowAlertGrid();
const alertGroups = tree.find("AlertGroup");
expect(alertGroups.map(g => g.props().group.id)).toEqual([
"id2",
"id1",
"id3"
]);
});
it("groups are sorted by label when sorting is set to 'label'", () => {
settingsStore.gridConfig.config.sortOrder =
settingsStore.gridConfig.options.label.value;
settingsStore.gridConfig.config.sortLabel = "instance";
settingsStore.gridConfig.config.reverseSort = false;
MockGroupList(3, 1);
alertStore.data.groups.id1.alerts[0].labels.instance = "abc1";
alertStore.data.groups.id2.alerts[0].labels.instance = "abc3";
alertStore.data.groups.id3.alerts[0].labels.instance = "abc2";
const tree = ShallowAlertGrid();
const alertGroups = tree.find("AlertGroup");
expect(alertGroups.map(g => g.props().group.id)).toEqual([
"id1",
"id3",
"id2"
]);
});
it("groups are sorted by reverse label when sorting is set to 'label' and 'reverse' is on", () => {
settingsStore.gridConfig.config.sortOrder =
settingsStore.gridConfig.options.label.value;
settingsStore.gridConfig.config.sortLabel = "instance";
settingsStore.gridConfig.config.reverseSort = true;
MockGroupList(3, 1);
alertStore.data.groups.id1.alerts[0].labels.instance = "abc1";
alertStore.data.groups.id2.alerts[0].labels.instance = "abc3";
alertStore.data.groups.id3.alerts[0].labels.instance = "abc2";
const tree = ShallowAlertGrid();
const alertGroups = tree.find("AlertGroup");
expect(alertGroups.map(g => g.props().group.id)).toEqual([
"id2",
"id3",
"id1"
]);
});
it("sorting is no-op when when sorting is set to 'label' and alerts lack that label", () => {
settingsStore.gridConfig.config.sortOrder =
settingsStore.gridConfig.options.label.value;
settingsStore.gridConfig.config.sortLabel = "foo";
settingsStore.gridConfig.config.reverseSort = false;
MockGroupList(3, 1);
const tree = ShallowAlertGrid();
const alertGroups = tree.find("AlertGroup");
expect(alertGroups.map(g => g.props().group.id)).toEqual([
"id1",
"id2",
"id3"
]);
});
});

View File

@@ -16,7 +16,7 @@ const FakeConfiguration = () => {
return mount(<AlertGroupConfiguration settingsStore={settingsStore} />);
};
describe("<AlertGroupConfiguration /> className", () => {
describe("<AlertGroupConfiguration />", () => {
it("matches snapshot with default values", () => {
const tree = FakeConfiguration();
expect(toDiffableHtml(tree.html())).toMatchSnapshot();

View File

@@ -0,0 +1,109 @@
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";
import { SortLabelName } from "./SortLabelName";
const AlertGroupSortConfiguration = observer(
class AlertGroupSortConfiguration extends Component {
static propTypes = {
settingsStore: PropTypes.instanceOf(Settings).isRequired
};
constructor(props) {
super(props);
this.validateConfig();
}
onSortOrderChange = action((newValue, actionMeta) => {
const { settingsStore } = this.props;
settingsStore.gridConfig.config.sortOrder = newValue.value;
});
onSortReverseChange = action(event => {
const { settingsStore } = this.props;
settingsStore.gridConfig.config.reverseSort = event.target.checked;
});
valueToOption = val => {
const { settingsStore } = this.props;
return { label: settingsStore.gridConfig.options[val].label, value: val };
};
validateConfig = action(() => {
const { settingsStore } = this.props;
if (
!Object.values(settingsStore.gridConfig.options)
.map(o => o.value)
.includes(settingsStore.gridConfig.config.sortOrder)
) {
settingsStore.gridConfig.config.sortOrder =
settingsStore.gridConfig.defaults.sortOrder;
}
});
render() {
const { settingsStore } = this.props;
return (
<div className="form-group">
<div className="text-center">
<label className="mb-4">Grid sort order</label>
</div>
<div className="d-flex flex-fill flex-lg-row flex-column justify-content-between">
<div className="flex-shrink-0 flex-grow-1 flex-basis-auto">
<ReactSelect
styles={ReactSelectStyles}
classNamePrefix="react-select"
instanceId="configuration-sort-order"
defaultValue={this.valueToOption(
settingsStore.gridConfig.config.sortOrder
)}
options={Object.values(settingsStore.gridConfig.options)}
onChange={this.onSortOrderChange}
hideSelectedOptions
/>
</div>
{settingsStore.gridConfig.config.sortOrder ===
settingsStore.gridConfig.options.label.value ? (
<div className="flex-shrink-0 flex-grow-1 flex-basis-auto mx-0 mx-lg-1 mt-1 mt-lg-0">
<SortLabelName settingsStore={settingsStore} />
</div>
) : null}
<div className="flex-shrink-1 flex-grow-0 form-check form-check-inline flex-basis-auto mt-1 mt-lg-0 ml-0 ml-lg-1 mr-0">
<span className="custom-control custom-switch">
<input
id="configuration-sort-reverse"
className="custom-control-input"
type="checkbox"
value=""
checked={settingsStore.gridConfig.config.reverseSort}
onChange={this.onSortReverseChange}
/>
<label
className="custom-control-label cursor-pointer mr-3"
htmlFor="configuration-sort-reverse"
>
Reverse
</label>
</span>
</div>
</div>
</div>
);
}
}
);
export { AlertGroupSortConfiguration };

View File

@@ -0,0 +1,141 @@
import React from "react";
import { mount } from "enzyme";
import toDiffableHtml from "diffable-html";
import { Settings } from "Stores/Settings";
import { AlertGroupSortConfiguration } from "./AlertGroupSortConfiguration";
let settingsStore;
beforeEach(() => {
fetch.mockResponse(JSON.stringify([]));
settingsStore = new Settings();
});
afterEach(() => {
jest.restoreAllMocks();
});
const FakeConfiguration = () => {
return mount(<AlertGroupSortConfiguration settingsStore={settingsStore} />);
};
const ExpandSortLabelSuggestions = async () => {
settingsStore.gridConfig.config.sortOrder =
settingsStore.gridConfig.options.label.value;
const tree = FakeConfiguration();
const labelSelect = tree.find("SortLabelName");
await expect(
labelSelect.instance().nameSuggestionsFetch
).resolves.toBeUndefined();
tree
.find("input#react-select-configuration-sort-label-input")
.simulate("change", { target: { value: "a" } });
fetch.resetMocks();
return tree;
};
describe("<AlertGroupSortConfiguration />", () => {
it("matches snapshot with default values", () => {
const tree = FakeConfiguration();
expect(toDiffableHtml(tree.html())).toMatchSnapshot();
});
it("invalid sortOrder value is reset on mount", done => {
settingsStore.gridConfig.config.sortOrder = "badValue";
FakeConfiguration();
setTimeout(() => {
expect(settingsStore.gridConfig.config.sortOrder).toBe(
settingsStore.gridConfig.defaults.sortOrder
);
done();
}, 200);
});
it("changing sort order value update settingsStore", async done => {
settingsStore.gridConfig.config.sortOrder =
settingsStore.gridConfig.options.label.value;
expect(settingsStore.gridConfig.config.sortOrder).toBe(
settingsStore.gridConfig.options.label.value
);
const tree = FakeConfiguration();
tree.instance().onSortOrderChange({
label: settingsStore.gridConfig.options.startsAt.label,
value: settingsStore.gridConfig.options.startsAt.value
});
setTimeout(() => {
expect(settingsStore.gridConfig.config.sortOrder).toBe(
settingsStore.gridConfig.options.startsAt.value
);
done();
}, 200);
});
it("label select is not rendered when sort order is != 'label'", () => {
settingsStore.gridConfig.config.sortOrder =
settingsStore.gridConfig.options.disabled.value;
const tree = FakeConfiguration();
const labelSelect = tree.find("SortLabelName");
expect(labelSelect).toHaveLength(0);
});
it("label select is rendered when sort order is == 'label'", () => {
settingsStore.gridConfig.config.sortOrder =
settingsStore.gridConfig.options.label.value;
const tree = FakeConfiguration();
const labelSelect = tree.find("SortLabelName");
expect(labelSelect).toHaveLength(1);
});
it("label select renders suggestions on click", async () => {
fetch.mockResponse(JSON.stringify(["alertname", "cluster", "fakeLabel"]));
const tree = await ExpandSortLabelSuggestions();
const options = tree.find(".react-select__option");
expect(options).toHaveLength(3);
expect(options.at(0).text()).toBe("alertname");
expect(options.at(1).text()).toBe("cluster");
expect(options.at(2).text()).toBe("fakeLabel");
});
it("label select handles fetch errors", async () => {
fetch.mockReject("error");
const tree = await ExpandSortLabelSuggestions();
const options = tree.find(".react-select__option");
expect(options).toHaveLength(0);
});
it("label select handles invalid JSON", async () => {
jest.spyOn(console, "error").mockImplementation(() => {});
fetch.mockResponse("invalid JSON");
const tree = await ExpandSortLabelSuggestions();
const options = tree.find(".react-select__option");
expect(options).toHaveLength(0);
});
it("clicking on a label option updates settingsStore", async done => {
fetch.mockResponse(JSON.stringify(["alertname", "cluster", "fakeLabel"]));
const tree = await ExpandSortLabelSuggestions();
const options = tree.find(".react-select__option");
options.at(1).simulate("click");
setTimeout(() => {
expect(settingsStore.gridConfig.config.sortLabel).toBe("cluster");
done();
}, 200);
});
it("clicking on the 'reverse' checkbox updates settingsStore", done => {
settingsStore.gridConfig.config.reverseSort = false;
const tree = FakeConfiguration();
const checkbox = tree.find("#configuration-sort-reverse");
expect(settingsStore.gridConfig.config.reverseSort).toBe(false);
checkbox.simulate("change", { target: { checked: true } });
setTimeout(() => {
expect(settingsStore.gridConfig.config.reverseSort).toBe(true);
done();
}, 200);
});
});

View File

@@ -16,7 +16,7 @@ const FakeConfiguration = () => {
return mount(<FetchConfiguration settingsStore={settingsStore} />);
};
describe("<FetchConfiguration /> className", () => {
describe("<FetchConfiguration />", () => {
it("matches snapshot with default values", () => {
const tree = FakeConfiguration();
expect(toDiffableHtml(tree.html())).toMatchSnapshot();

View File

@@ -0,0 +1,76 @@
import React from "react";
import PropTypes from "prop-types";
import { action, observable } from "mobx";
import { observer } from "mobx-react";
import CreatableSelect from "react-select/lib/Creatable";
import { FormatBackendURI } from "Stores/AlertStore";
import { Settings } from "Stores/Settings";
import { ReactSelectStyles } from "Components/MultiSelect";
const valueToOption = v => ({ label: v, value: v });
const SortLabelName = observer(
class SortLabelName extends CreatableSelect {
static propTypes = {
settingsStore: PropTypes.instanceOf(Settings).isRequired
};
suggestions = observable({
names: []
});
populateNameSuggestions = action(() => {
this.nameSuggestionsFetch = fetch(FormatBackendURI(`labelNames.json`), {
credentials: "include"
})
.then(
result => result.json(),
err => {
return [];
}
)
.then(result => {
this.suggestions.names = result.map(value => ({
label: value,
value: value
}));
})
.catch(err => {
console.error(err.message);
this.suggestions.names = [];
});
});
onChange = action((newValue, actionMeta) => {
const { settingsStore } = this.props;
settingsStore.gridConfig.config.sortLabel = newValue.value;
});
componentDidMount() {
this.populateNameSuggestions();
}
render() {
const { settingsStore } = this.props;
return (
<CreatableSelect
styles={ReactSelectStyles}
classNamePrefix="react-select"
instanceId="configuration-sort-label"
defaultValue={valueToOption(
settingsStore.gridConfig.config.sortLabel
)}
options={this.suggestions.names}
onChange={this.onChange}
/>
);
}
}
);
export { SortLabelName };

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<AlertGroupConfiguration /> className matches snapshot with default values 1`] = `
exports[`<AlertGroupConfiguration /> matches snapshot with default values 1`] = `
"
<div class=\\"form-group text-center\\">
<label class=\\"mb-4\\">

View File

@@ -0,0 +1,78 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<AlertGroupSortConfiguration /> matches snapshot with default values 1`] = `
"
<div class=\\"form-group\\">
<div class=\\"text-center\\">
<label class=\\"mb-4\\">
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\\">
Sort by alert timestamp
</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-sort-order-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=\\"flex-shrink-1 flex-grow-0 form-check form-check-inline flex-basis-auto mt-1 mt-lg-0 ml-0 ml-lg-1 mr-0\\">
<span class=\\"custom-control custom-switch\\">
<input id=\\"configuration-sort-reverse\\"
class=\\"custom-control-input\\"
type=\\"checkbox\\"
value
checked
>
<label class=\\"custom-control-label cursor-pointer mr-3\\"
for=\\"configuration-sort-reverse\\"
>
Reverse
</label>
</span>
</div>
</div>
</div>
"
`;

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<FetchConfiguration /> className matches snapshot with default values 1`] = `
exports[`<FetchConfiguration /> matches snapshot with default values 1`] = `
"
<div class=\\"form-group text-center\\">
<label class=\\"mb-4\\">

View File

@@ -4,12 +4,15 @@ import PropTypes from "prop-types";
import { Settings } from "Stores/Settings";
import { FetchConfiguration } from "./FetchConfiguration";
import { AlertGroupConfiguration } from "./AlertGroupConfiguration";
import { AlertGroupSortConfiguration } from "./AlertGroupSortConfiguration";
const Configuration = ({ settingsStore }) => (
<form className="px-3">
<FetchConfiguration settingsStore={settingsStore} />
<div className="mt-5" />
<AlertGroupConfiguration settingsStore={settingsStore} />
<div className="mt-5" />
<AlertGroupSortConfiguration settingsStore={settingsStore} />
</form>
);
Configuration.propTypes = {

View File

@@ -10,7 +10,7 @@ describe("<Configuration />", () => {
const settingsStore = new Settings();
const tree = shallow(<Configuration settingsStore={settingsStore} />);
expect(tree.text()).toBe(
"<FetchConfiguration /><AlertGroupConfiguration />"
"<FetchConfiguration /><AlertGroupConfiguration /><AlertGroupSortConfiguration />"
);
});
});

View File

@@ -110,6 +110,80 @@ exports[`<MainModalContent /> matches snapshot 1`] = `
</span>
</div>
</div>
<div class=\\"mt-5\\">
</div>
<div class=\\"form-group\\">
<div class=\\"text-center\\">
<label class=\\"mb-4\\">
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\\">
Sort by alert timestamp
</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-sort-order-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=\\"flex-shrink-1 flex-grow-0 form-check form-check-inline flex-basis-auto mt-1 mt-lg-0 ml-0 ml-lg-1 mr-0\\">
<span class=\\"custom-control custom-switch\\">
<input id=\\"configuration-sort-reverse\\"
class=\\"custom-control-input\\"
type=\\"checkbox\\"
value
checked
>
<label class=\\"custom-control-label cursor-pointer mr-3\\"
for=\\"configuration-sort-reverse\\"
>
Reverse
</label>
</span>
</div>
</div>
</div>
</form>
</div>
<div class=\\"modal-footer\\">

View File

@@ -1,6 +1,8 @@
import { action } from "mobx";
import { localStored } from "mobx-stored";
import { StaticLabels } from "Common/Query";
class SavedFilters {
config = localStored(
"savedFilters",
@@ -54,11 +56,34 @@ class SilenceFormConfig {
});
}
class GridConfig {
options = Object.freeze({
disabled: { label: "No sorting", value: "disabled" },
startsAt: { label: "Sort by alert timestamp", value: "startsAt" },
label: { label: "Sort by alert label", value: "label" }
});
defaults = {
sortOrder: this.options.startsAt.value,
reverseSort: true,
sortLabel: StaticLabels.AlertName
};
config = localStored(
"gridConfig",
{
sortOrder: this.defaults.sortOrder,
reverseSort: this.defaults.reverseSort,
sortLabel: this.defaults.sortLabel
},
{ delay: 100 }
);
}
class Settings {
constructor() {
this.savedFilters = new SavedFilters();
this.fetchConfig = new FetchConfig();
this.alertGroupConfig = new AlertGroupConfig();
this.gridConfig = new GridConfig();
this.silenceFormConfig = new SilenceFormConfig();
}
}