mirror of
https://github.com/prymitive/karma
synced 2026-05-07 03:26:52 +00:00
Merge pull request #447 from prymitive/grid-sort
feat(ui): allow sorting alert grid
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 };
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
76
ui/src/Components/MainModal/Configuration/SortLabelName.js
Normal file
76
ui/src/Components/MainModal/Configuration/SortLabelName.js
Normal 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 };
|
||||
@@ -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\\">
|
||||
|
||||
@@ -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>
|
||||
"
|
||||
`;
|
||||
@@ -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\\">
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 />"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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\\">
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user