diff --git a/ui/src/Components/MainModal/Configuration/GridLabelName.js b/ui/src/Components/MainModal/Configuration/GridLabelName.js
new file mode 100644
index 000000000..e00c81428
--- /dev/null
+++ b/ui/src/Components/MainModal/Configuration/GridLabelName.js
@@ -0,0 +1,84 @@
+import React from "react";
+import PropTypes from "prop-types";
+
+import { action, observable } from "mobx";
+import { observer } from "mobx-react";
+
+import Creatable from "react-select/creatable";
+
+import { FetchGet } from "Common/Fetch";
+import { FormatBackendURI } from "Stores/AlertStore";
+import { Settings } from "Stores/Settings";
+import { ThemeContext } from "Components/Theme";
+
+const disabledLabel = "Disable multi-grid";
+const emptyValue = { label: disabledLabel, value: "" };
+const valueToOption = (v) => ({ label: v ? v : disabledLabel, value: v });
+
+const GridLabelName = observer(
+ class GridLabelName extends Creatable {
+ static propTypes = {
+ settingsStore: PropTypes.instanceOf(Settings).isRequired,
+ };
+ static contextType = ThemeContext;
+
+ suggestions = observable({
+ names: [],
+ });
+
+ populateNameSuggestions = action(() => {
+ this.nameSuggestionsFetch = FetchGet(
+ FormatBackendURI(`labelNames.json`),
+ {}
+ )
+ .then(
+ (result) => result.json(),
+ (err) => {
+ return [];
+ }
+ )
+ .then((result) => {
+ this.suggestions.names = [
+ ...[emptyValue],
+ ...result.map((value) => ({
+ label: value,
+ value: value,
+ })),
+ ];
+ })
+ .catch((err) => {
+ console.error(err.message);
+ this.suggestions.names = [emptyValue];
+ });
+ });
+
+ onChange = action((newValue, actionMeta) => {
+ const { settingsStore } = this.props;
+
+ settingsStore.multiGridConfig.config.gridLabel = newValue.value;
+ });
+
+ componentDidMount() {
+ this.populateNameSuggestions();
+ }
+
+ render() {
+ const { settingsStore } = this.props;
+
+ return (
+
+ );
+ }
+ }
+);
+
+export { GridLabelName };
diff --git a/ui/src/Components/MainModal/Configuration/MultiGridConfiguration.js b/ui/src/Components/MainModal/Configuration/MultiGridConfiguration.js
new file mode 100644
index 000000000..fe00ab019
--- /dev/null
+++ b/ui/src/Components/MainModal/Configuration/MultiGridConfiguration.js
@@ -0,0 +1,62 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+
+import { action } from "mobx";
+import { observer } from "mobx-react";
+
+import { Settings } from "Stores/Settings";
+import { ThemeContext } from "Components/Theme";
+import { GridLabelName } from "./GridLabelName";
+
+const MultiGridConfiguration = observer(
+ class MultiGridConfiguration extends Component {
+ static propTypes = {
+ settingsStore: PropTypes.instanceOf(Settings).isRequired,
+ };
+ static contextType = ThemeContext;
+
+ onSortReverseChange = action((event) => {
+ const { settingsStore } = this.props;
+
+ settingsStore.multiGridConfig.config.gridSortReverse =
+ event.target.checked;
+ });
+
+ render() {
+ const { settingsStore } = this.props;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+ }
+);
+
+export { MultiGridConfiguration };
diff --git a/ui/src/Components/MainModal/Configuration/MultiGridConfiguration.test.js b/ui/src/Components/MainModal/Configuration/MultiGridConfiguration.test.js
new file mode 100644
index 000000000..71370cbb8
--- /dev/null
+++ b/ui/src/Components/MainModal/Configuration/MultiGridConfiguration.test.js
@@ -0,0 +1,100 @@
+import React from "react";
+
+import { mount } from "enzyme";
+
+import toDiffableHtml from "diffable-html";
+
+import { Settings } from "Stores/Settings";
+import { ThemeContext } from "Components/Theme";
+import {
+ ReactSelectColors,
+ ReactSelectStyles,
+} from "Components/Theme/ReactSelect";
+import { MultiGridConfiguration } from "./MultiGridConfiguration";
+
+let settingsStore;
+beforeEach(() => {
+ fetch.mockResponse(JSON.stringify([]));
+ settingsStore = new Settings();
+});
+
+afterEach(() => {
+ jest.restoreAllMocks();
+});
+
+const FakeConfiguration = () => {
+ return mount(
+
+
+
+ );
+};
+
+const ExpandSortLabelSuggestions = async () => {
+ settingsStore.gridConfig.config.sortOrder =
+ settingsStore.gridConfig.options.label.value;
+ const tree = FakeConfiguration();
+ const labelSelect = tree.find("GridLabelName");
+ await expect(
+ labelSelect.instance().nameSuggestionsFetch
+ ).resolves.toBeUndefined();
+
+ tree
+ .find("input#react-select-configuration-grid-label-input")
+ .simulate("change", { target: { value: "a" } });
+
+ fetch.resetMocks();
+ return tree;
+};
+
+describe("", () => {
+ it("matches snapshot with default values", () => {
+ const tree = FakeConfiguration();
+ expect(toDiffableHtml(tree.html())).toMatchSnapshot();
+ });
+
+ it("label select handles fetch errors", async () => {
+ fetch.mockReject(new Error("Fetch error"));
+ const tree = await ExpandSortLabelSuggestions();
+ const options = tree.find("div.react-select__option");
+ expect(options).toHaveLength(1);
+ expect(options.text()).toBe("Disable multi-grid");
+ });
+
+ it("label select handles invalid JSON", async () => {
+ jest.spyOn(console, "error").mockImplementation(() => {});
+ fetch.mockResponse("invalid JSON");
+ const tree = await ExpandSortLabelSuggestions();
+ const options = tree.find("div.react-select__option");
+ expect(options).toHaveLength(1);
+ expect(options.text()).toBe("Disable multi-grid");
+ });
+
+ 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("div.react-select__option");
+ options.at(2).simulate("click");
+ setTimeout(() => {
+ expect(settingsStore.multiGridConfig.config.gridLabel).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-multigrid-sort-reverse");
+
+ expect(settingsStore.gridConfig.config.reverseSort).toBe(false);
+ checkbox.simulate("change", { target: { checked: true } });
+ setTimeout(() => {
+ expect(settingsStore.multiGridConfig.config.gridSortReverse).toBe(true);
+ done();
+ }, 200);
+ });
+});
diff --git a/ui/src/Components/MainModal/Configuration/__snapshots__/MultiGridConfiguration.test.js.snap b/ui/src/Components/MainModal/Configuration/__snapshots__/MultiGridConfiguration.test.js.snap
new file mode 100644
index 000000000..038600ff6
--- /dev/null
+++ b/ui/src/Components/MainModal/Configuration/__snapshots__/MultiGridConfiguration.test.js.snap
@@ -0,0 +1,72 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` matches snapshot with default values 1`] = `
+"
+
+"
+`;
diff --git a/ui/src/Components/MainModal/Configuration/__snapshots__/index.test.js.snap b/ui/src/Components/MainModal/Configuration/__snapshots__/index.test.js.snap
index 795c6b915..d0d7ad8ca 100644
--- a/ui/src/Components/MainModal/Configuration/__snapshots__/index.test.js.snap
+++ b/ui/src/Components/MainModal/Configuration/__snapshots__/index.test.js.snap
@@ -521,6 +521,103 @@ exports[` matches snapshot 1`] = `
+
"
`;
diff --git a/ui/src/Components/MainModal/Configuration/index.js b/ui/src/Components/MainModal/Configuration/index.js
index 79b1df0bc..ee7233933 100644
--- a/ui/src/Components/MainModal/Configuration/index.js
+++ b/ui/src/Components/MainModal/Configuration/index.js
@@ -11,6 +11,7 @@ import { AlertGroupSortConfiguration } from "./AlertGroupSortConfiguration";
import { AlertGroupCollapseConfiguration } from "./AlertGroupCollapseConfiguration";
import { AlertGroupTitleBarColor } from "./AlertGroupTitleBarColor";
import { ThemeConfiguration } from "./ThemeConfiguration";
+import { MultiGridConfiguration } from "./MultiGridConfiguration";
const Configuration = ({ settingsStore, defaultIsOpen }) => (
);
Configuration.propTypes = {
diff --git a/ui/src/Components/MainModal/Configuration/index.test.js b/ui/src/Components/MainModal/Configuration/index.test.js
index 3c3a4b5d1..16a385b94 100644
--- a/ui/src/Components/MainModal/Configuration/index.test.js
+++ b/ui/src/Components/MainModal/Configuration/index.test.js
@@ -12,6 +12,14 @@ import {
} from "Components/Theme/ReactSelect";
import { Configuration } from ".";
+beforeEach(() => {
+ fetch.mockResponse(JSON.stringify([]));
+});
+
+afterEach(() => {
+ jest.restoreAllMocks();
+});
+
describe("", () => {
it("matches snapshot", () => {
const settingsStore = new Settings();
diff --git a/ui/src/Components/MainModal/MainModalContent.test.js b/ui/src/Components/MainModal/MainModalContent.test.js
index 535e8c9ff..4588f3fbf 100644
--- a/ui/src/Components/MainModal/MainModalContent.test.js
+++ b/ui/src/Components/MainModal/MainModalContent.test.js
@@ -21,6 +21,7 @@ beforeEach(() => {
alertStore = new AlertStore([]);
settingsStore = new Settings();
onHide.mockClear();
+ fetch.mockResponse(JSON.stringify([]));
});
afterEach(() => {
diff --git a/ui/src/Components/MainModal/__snapshots__/MainModalContent.test.js.snap b/ui/src/Components/MainModal/__snapshots__/MainModalContent.test.js.snap
index 6ed928c0e..32624a7e2 100644
--- a/ui/src/Components/MainModal/__snapshots__/MainModalContent.test.js.snap
+++ b/ui/src/Components/MainModal/__snapshots__/MainModalContent.test.js.snap
@@ -540,6 +540,103 @@ exports[` matches snapshot 1`] = `
+