diff --git a/ui/src/Components/Grid/AlertGrid/Grid.js b/ui/src/Components/Grid/AlertGrid/Grid.js
new file mode 100644
index 000000000..06803eab6
--- /dev/null
+++ b/ui/src/Components/Grid/AlertGrid/Grid.js
@@ -0,0 +1,202 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+
+import { observable, action } from "mobx";
+import { observer } from "mobx-react";
+
+import debounce from "lodash/debounce";
+
+import MasonryInfiniteScroller from "react-masonry-infinite";
+
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faChevronUp } from "@fortawesome/free-solid-svg-icons/faChevronUp";
+import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown";
+import { faAngleDoubleDown } from "@fortawesome/free-solid-svg-icons/faAngleDoubleDown";
+
+import { AlertStore } from "Stores/AlertStore";
+import { Settings } from "Stores/Settings";
+import { SilenceFormStore } from "Stores/SilenceFormStore";
+import { APIGroup } from "Models/API";
+import { FilteringLabel } from "Components/Labels/FilteringLabel";
+import { TooltipWrapper } from "Components/TooltipWrapper";
+import { AlertGroup } from "./AlertGroup";
+
+const Grid = observer(
+ class Grid extends Component {
+ static propTypes = {
+ alertStore: PropTypes.instanceOf(AlertStore).isRequired,
+ settingsStore: PropTypes.instanceOf(Settings).isRequired,
+ silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
+ gridSizesConfig: PropTypes.array.isRequired,
+ groupWidth: PropTypes.number.isRequired,
+ gridLabelName: PropTypes.string.isRequired,
+ gridLabelValue: PropTypes.string.isRequired,
+ gridAlertGroups: PropTypes.arrayOf(APIGroup).isRequired,
+ };
+
+ // store reference to generated masonry component so we can call it
+ // to repack the grid after any component was re-rendered, which could
+ // alter its size breaking grid layout
+ masonryComponentReference = observable(
+ { ref: false },
+ {},
+ { name: "Masonry reference" }
+ );
+ // store it for later
+ storeMasonryRef = action((ref) => {
+ this.masonryComponentReference.ref = ref;
+ });
+ // used to call forcePack() which will repack all grid elements
+ // (alert groups), this needs to be called if any group size changes
+ masonryRepack = debounce(
+ action(() => {
+ if (this.masonryComponentReference.ref) {
+ this.masonryComponentReference.ref.forcePack();
+ }
+ }),
+ 10
+ );
+
+ initial = 50;
+ groupsToRender = observable(
+ {
+ value: this.initial,
+ setValue(value) {
+ this.value = value;
+ },
+ },
+ {
+ setValue: action.bound,
+ },
+ { name: "Groups to render" }
+ );
+ // how many groups add to render count when user scrolls to the bottom
+ loadMoreStep = 30;
+
+ loadMore = action(() => {
+ const { gridAlertGroups } = this.props;
+
+ this.groupsToRender.value = Math.min(
+ this.groupsToRender.value + this.loadMoreStep,
+ gridAlertGroups.length
+ );
+ });
+
+ gridToggle = observable(
+ {
+ show: true,
+ toggle() {
+ this.show = !this.show;
+ },
+ },
+ {
+ toggle: action.bound,
+ }
+ );
+
+ componentDidUpdate() {
+ const { gridAlertGroups } = this.props;
+
+ this.masonryRepack();
+
+ if (this.groupsToRender.value > gridAlertGroups.length) {
+ this.groupsToRender.setValue(
+ Math.max(this.initial, gridAlertGroups.length)
+ );
+ }
+ }
+
+ render() {
+ const {
+ alertStore,
+ settingsStore,
+ silenceFormStore,
+ gridSizesConfig,
+ groupWidth,
+ gridLabelName,
+ gridLabelValue,
+ gridAlertGroups,
+ } = this.props;
+
+ return (
+
+ {gridLabelName !== "" && gridLabelValue !== "" && (
+
+
+ {gridAlertGroups.length}
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {this.gridToggle.show
+ ? gridAlertGroups
+ .slice(0, this.groupsToRender.value)
+ .map((group) => (
+
+ 1
+ }
+ afterUpdate={this.masonryRepack}
+ alertStore={alertStore}
+ settingsStore={settingsStore}
+ silenceFormStore={silenceFormStore}
+ style={{
+ width: groupWidth,
+ }}
+ />
+ ))
+ : []}
+
+ {gridAlertGroups.length > this.groupsToRender.value && (
+
+
+
+
+
+ )}
+
+ );
+ }
+ }
+);
+
+export { Grid };
diff --git a/ui/src/Components/Grid/AlertGrid/index.js b/ui/src/Components/Grid/AlertGrid/index.js
index 7ad215873..2bfba1616 100644
--- a/ui/src/Components/Grid/AlertGrid/index.js
+++ b/ui/src/Components/Grid/AlertGrid/index.js
@@ -10,15 +10,10 @@ import debounce from "lodash/debounce";
import ReactResizeDetector from "react-resize-detector";
-import MasonryInfiniteScroller from "react-masonry-infinite";
-
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch";
-
import { AlertStore } from "Stores/AlertStore";
import { Settings } from "Stores/Settings";
import { SilenceFormStore } from "Stores/SilenceFormStore";
-import { AlertGroup } from "./AlertGroup";
+import { Grid } from "./Grid";
import { GridSizesConfig, GetGridElementWidth } from "./GridSize";
const AlertGrid = observer(
@@ -69,54 +64,6 @@ const AlertGrid = observer(
this.viewport.updateWidths(document.body.clientWidth, window.innerWidth);
}, 100);
- // store reference to generated masonry component so we can call it
- // to repack the grid after any component was re-rendered, which could
- // alter its size breaking grid layout
- masonryComponentReference = observable(
- { ref: false },
- {},
- { name: "Masonry reference" }
- );
- // store it for later
- storeMasonryRef = action((ref) => {
- this.masonryComponentReference.ref = ref;
- });
- // used to call forcePack() which will repack all grid elements
- // (alert groups), this needs to be called if any group size changes
- masonryRepack = debounce(
- action(() => {
- if (this.masonryComponentReference.ref) {
- this.masonryComponentReference.ref.forcePack();
- }
- }),
- 10
- );
-
- initial = 50;
- groupsToRender = observable(
- {
- value: this.initial,
- setValue(value) {
- this.value = value;
- },
- },
- {
- setValue: action.bound,
- },
- { name: "Groups to render" }
- );
- // how many groups add to render count when user scrolls to the bottom
- loadMoreStep = 30;
-
- loadMore = action(() => {
- const { alertStore } = this.props;
-
- this.groupsToRender.value = Math.min(
- this.groupsToRender.value + this.loadMoreStep,
- alertStore.data.groups.length
- );
- });
-
componentDidMount() {
// We have font-display:swap set for font assets, this means that on initial
// render a fallback font might be used and later swapped for the final one
@@ -135,16 +82,6 @@ const AlertGrid = observer(
window.addEventListener("resize", this.handleResize);
}
- componentDidUpdate() {
- const { alertStore } = this.props;
-
- if (this.groupsToRender.value > alertStore.data.groups.length) {
- this.groupsToRender.setValue(
- Math.max(this.initial, alertStore.data.groups.length)
- );
- }
- }
-
componentWillUnmount() {
window.removeEventListener("resize", this.handleResize);
}
@@ -159,40 +96,19 @@ const AlertGrid = observer(
handleHeight
onResize={debounce(this.handleResize, 100)}
/>
-
-
-
- }
- >
- {alertStore.data.groups
- .slice(0, this.groupsToRender.value)
- .map((group) => (
- 1
- }
- afterUpdate={this.masonryRepack}
- alertStore={alertStore}
- settingsStore={settingsStore}
- silenceFormStore={silenceFormStore}
- style={{
- width: this.viewport.groupWidth,
- }}
- />
- ))}
-
+ {alertStore.data.grids.map((grid) => (
+
+ ))}
);
}
diff --git a/ui/src/Components/Grid/AlertGrid/index.test.js b/ui/src/Components/Grid/AlertGrid/index.test.js
index dd2628f6f..3639f9dd5 100644
--- a/ui/src/Components/Grid/AlertGrid/index.test.js
+++ b/ui/src/Components/Grid/AlertGrid/index.test.js
@@ -9,7 +9,8 @@ import { mockMatchMedia } from "__mocks__/matchMedia";
import { AlertStore } from "Stores/AlertStore";
import { Settings } from "Stores/Settings";
import { SilenceFormStore } from "Stores/SilenceFormStore";
-import { GetGridElementWidth } from "./GridSize";
+import { GetGridElementWidth, GridSizesConfig } from "./GridSize";
+import { Grid } from "./Grid";
import { AlertGrid } from ".";
let alertStore;
@@ -45,12 +46,36 @@ const ShallowAlertGrid = () => {
);
};
-const MountedAlertGroup = () => {
- return mount(
- {
+ return shallow(
+
+ );
+};
+
+const MountedGrid = () => {
+ return mount(
+
);
};
@@ -83,58 +108,54 @@ const MockGroupList = (count, alertPerGroup) => {
instances: [{ name: "am", uri: "http://am", error: "" }],
clusters: { am: ["am"] },
};
- alertStore.data.groups = groups;
+ alertStore.data.grids = [
+ {
+ labelName: "",
+ labelValue: "",
+ alertGroups: groups,
+ },
+ ];
};
-const VerifyColumnCount = (innerWidth, outerWidth, columns) => {
- MockGroupList(60, 5);
- const tree = ShallowAlertGrid();
- tree.instance().viewport.updateWidths(innerWidth, outerWidth);
- expect(tree.find("AlertGroup").at(0).props().style.width).toBe(
- Math.floor(innerWidth / columns)
- );
-};
-
-describe("", () => {
+describe("", () => {
it("renders only first 50 alert groups", () => {
MockGroupList(60, 5);
- const tree = ShallowAlertGrid();
+ const tree = ShallowGrid();
const alertGroups = tree.find("AlertGroup");
expect(alertGroups).toHaveLength(50);
});
- it("appends 30 groups after loadMore() call", () => {
+ it("appends 30 groups after clicking 'Load More' button", () => {
MockGroupList(100, 5);
- const tree = ShallowAlertGrid();
- // call it directly, it should happen on scroll to the bottom of the page
- tree.instance().loadMore();
+ const tree = ShallowGrid();
+ tree.find("button").simulate("click");
const alertGroups = tree.find("AlertGroup");
expect(alertGroups).toHaveLength(80);
});
- it("resets groupsToRender.value back to 50 if current value is > alertStore.data.groups.length", () => {
+ it("resets groupsToRender.value back to 50 if current value is more than group alerts", () => {
MockGroupList(100, 5);
- const tree = ShallowAlertGrid();
+ const tree = ShallowGrid();
expect(tree.find("AlertGroup")).toHaveLength(50);
expect(tree.instance().groupsToRender.value).toBe(50);
- tree.instance().loadMore();
+ tree.find("button").simulate("click");
expect(tree.find("AlertGroup")).toHaveLength(80);
expect(tree.instance().groupsToRender.value).toBe(80);
MockGroupList(10, 5);
- tree.instance().componentDidUpdate();
+ tree.setProps({ gridAlertGroups: alertStore.data.grids[0].alertGroups });
expect(tree.find("AlertGroup")).toHaveLength(10);
expect(tree.instance().groupsToRender.value).toBe(50);
MockGroupList(100, 5);
- tree.instance().componentDidUpdate();
+ tree.setProps({ gridAlertGroups: alertStore.data.grids[0].alertGroups });
expect(tree.find("AlertGroup")).toHaveLength(50);
expect(tree.instance().groupsToRender.value).toBe(50);
});
it("calling masonryRepack() calls forcePack() on Masonry instance`", () => {
- const tree = ShallowAlertGrid();
+ const tree = ShallowGrid();
const instance = tree.instance();
// it's a shallow render so we don't really have masonry mounted, fake it
instance.masonryComponentReference.ref = {
@@ -145,28 +166,28 @@ describe("", () => {
});
it("masonryRepack() doesn't crash when masonryComponentReference.ref=false`", () => {
- const tree = ShallowAlertGrid();
+ const tree = ShallowGrid();
const instance = tree.instance();
instance.masonryComponentReference.ref = false;
instance.masonryRepack();
});
it("masonryRepack() doesn't crash when masonryComponentReference.ref=null`", () => {
- const tree = ShallowAlertGrid();
+ const tree = ShallowGrid();
const instance = tree.instance();
instance.masonryComponentReference.ref = null;
instance.masonryRepack();
});
it("masonryRepack() doesn't crash when masonryComponentReference.ref=undefined`", () => {
- const tree = ShallowAlertGrid();
+ const tree = ShallowGrid();
const instance = tree.instance();
instance.masonryComponentReference.ref = undefined;
instance.masonryRepack();
});
it("calling storeMasonryRef() saves the ref in local store", () => {
- const tree = ShallowAlertGrid();
+ const tree = ShallowGrid();
const instance = tree.instance();
instance.storeMasonryRef("foo");
expect(instance.masonryComponentReference.ref).toEqual("foo");
@@ -177,7 +198,7 @@ describe("", () => {
settingsStore.gridConfig.options.disabled.value;
settingsStore.gridConfig.config.reverseSort = false;
MockGroupList(3, 1);
- const tree = ShallowAlertGrid();
+ const tree = ShallowGrid();
const alertGroups = tree.find("AlertGroup");
expect(alertGroups.map((g) => g.props().group.id)).toEqual([
"id1",
@@ -186,6 +207,85 @@ describe("", () => {
]);
});
+ it("click on the grid toggle toggles all groups", () => {
+ MockGroupList(10, 3);
+ const tree = MountedGrid();
+ tree.setProps({
+ gridLabelName: "foo",
+ gridLabelValue: "bar",
+ });
+ expect(tree.find("AlertGroup")).toHaveLength(10);
+
+ tree.find("span.cursor-pointer").at(0).simulate("click");
+ expect(tree.find("AlertGroup")).toHaveLength(0);
+
+ tree.find("span.cursor-pointer").at(0).simulate("click");
+ expect(tree.find("AlertGroup")).toHaveLength(10);
+ });
+
+ it("left click on a group collapse toggle only toggles clicked group", () => {
+ MockGroupList(10, 3);
+ const tree = MountedGrid();
+
+ for (let i = 0; i <= 9; i++) {
+ expect(tree.find("AlertGroup").at(i).find("Alert")).toHaveLength(3);
+ }
+
+ tree
+ .find("AlertGroup")
+ .at(2)
+ .find("GroupHeader")
+ .find("span.cursor-pointer")
+ .at(1)
+ .simulate("click");
+
+ for (let i = 0; i <= 9; i++) {
+ expect(tree.find("AlertGroup").at(i).find("Alert")).toHaveLength(
+ i === 2 ? 0 : 3
+ );
+ }
+ });
+
+ it("left click + alt on a group collapse toggle toggles all groups", () => {
+ MockGroupList(10, 3);
+ const tree = MountedGrid();
+
+ for (let i = 0; i <= 9; i++) {
+ expect(tree.find("AlertGroup").at(i).find("Alert")).toHaveLength(3);
+ }
+
+ tree
+ .find("AlertGroup")
+ .at(2)
+ .find("GroupHeader")
+ .find("span.cursor-pointer")
+ .at(1)
+ .simulate("click", { altKey: true });
+
+ for (let i = 0; i <= 9; i++) {
+ expect(tree.find("AlertGroup").at(i).find("Alert")).toHaveLength(0);
+ }
+ });
+});
+
+describe("", () => {
+ const VerifyColumnCount = (innerWidth, outerWidth, columns) => {
+ MockGroupList(60, 5);
+
+ const wrapper = ShallowAlertGrid();
+ wrapper.instance().viewport.updateWidths(innerWidth, outerWidth);
+
+ const tree = ShallowGrid();
+ tree.setProps({
+ gridSizesConfig: wrapper.instance().viewport.gridSizesConfig,
+ groupWidth: wrapper.instance().viewport.groupWidth,
+ });
+
+ expect(tree.find("AlertGroup").at(0).props().style.width).toBe(
+ Math.floor(innerWidth / columns)
+ );
+ };
+
it("doesn't throw errors after FontFaceObserver timeout", () => {
MockGroupList(60, 5);
ShallowAlertGrid();
@@ -261,13 +361,24 @@ describe("", () => {
it("viewport resize also resizes alert groups", () => {
MockGroupList(60, 5);
- const tree = ShallowAlertGrid();
+
+ const wrapper = ShallowAlertGrid();
+ const tree = ShallowGrid();
+
// set initial width
- tree.instance().viewport.updateWidths(1980, 1980);
+ wrapper.instance().viewport.updateWidths(1980, 1980);
+ tree.setProps({
+ gridSizesConfig: wrapper.instance().viewport.gridSizesConfig,
+ groupWidth: wrapper.instance().viewport.groupWidth,
+ });
expect(tree.find("AlertGroup").at(0).props().style.width).toBe(1980 / 4);
// then resize and verify if column count was changed
- tree.instance().viewport.updateWidths(1000, 1000);
+ wrapper.instance().viewport.updateWidths(1000, 1000);
+ tree.setProps({
+ gridSizesConfig: wrapper.instance().viewport.gridSizesConfig,
+ groupWidth: wrapper.instance().viewport.groupWidth,
+ });
expect(tree.find("AlertGroup").at(0).props().style.width).toBe(1000 / 2);
});
@@ -275,13 +386,23 @@ describe("", () => {
settingsStore.gridConfig.config.groupWidth = 400;
MockGroupList(60, 5);
- const tree = ShallowAlertGrid();
+ const wrapper = ShallowAlertGrid();
+ const tree = ShallowGrid();
+
// set initial width
- tree.instance().viewport.updateWidths(1600, 1600);
+ wrapper.instance().viewport.updateWidths(1600, 1600);
+ tree.setProps({
+ gridSizesConfig: wrapper.instance().viewport.gridSizesConfig,
+ groupWidth: wrapper.instance().viewport.groupWidth,
+ });
expect(tree.find("AlertGroup").at(0).props().style.width).toBe(400);
// then resize and verify if column count was changed
- tree.instance().viewport.updateWidths(1584, 1600);
+ wrapper.instance().viewport.updateWidths(1584, 1600);
+ tree.setProps({
+ gridSizesConfig: wrapper.instance().viewport.gridSizesConfig,
+ groupWidth: wrapper.instance().viewport.groupWidth,
+ });
expect(tree.find("AlertGroup").at(0).props().style.width).toBe(396);
});
@@ -297,14 +418,20 @@ describe("", () => {
it("viewport resize doesn't allow loops", () => {
settingsStore.gridConfig.config.groupWidth = 400;
- const tree = ShallowAlertGrid();
+
+ MockGroupList(60, 5);
+ const wrapper = ShallowAlertGrid();
+ const tree = ShallowGrid();
let results = [];
for (var index = 0; index < 14; index++) {
- MockGroupList(60, 5);
- tree
+ wrapper
.instance()
.viewport.updateWidths(index % 2 === 0 ? 1600 : 1584, 1600);
+ tree.setProps({
+ gridSizesConfig: wrapper.instance().viewport.gridSizesConfig,
+ groupWidth: wrapper.instance().viewport.groupWidth,
+ });
results.push(tree.find("AlertGroup").at(0).props().style.width);
}
@@ -326,50 +453,6 @@ describe("", () => {
]);
});
- it("left click on a group collapse toggle only toggles clicked group", () => {
- MockGroupList(10, 3);
- const tree = MountedAlertGroup();
-
- for (let i = 0; i <= 9; i++) {
- expect(tree.find("AlertGroup").at(i).find("Alert")).toHaveLength(3);
- }
-
- tree
- .find("AlertGroup")
- .at(2)
- .find("GroupHeader")
- .find("span.cursor-pointer")
- .at(1)
- .simulate("click");
-
- for (let i = 0; i <= 9; i++) {
- expect(tree.find("AlertGroup").at(i).find("Alert")).toHaveLength(
- i === 2 ? 0 : 3
- );
- }
- });
-
- it("left click + alt on a group collapse toggle toggles all groups", () => {
- MockGroupList(10, 3);
- const tree = MountedAlertGroup();
-
- for (let i = 0; i <= 9; i++) {
- expect(tree.find("AlertGroup").at(i).find("Alert")).toHaveLength(3);
- }
-
- tree
- .find("AlertGroup")
- .at(2)
- .find("GroupHeader")
- .find("span.cursor-pointer")
- .at(1)
- .simulate("click", { altKey: true });
-
- for (let i = 0; i <= 9; i++) {
- expect(tree.find("AlertGroup").at(i).find("Alert")).toHaveLength(0);
- }
- });
-
it("doesn't crash on unmount", () => {
MockGroupList(60, 5);
const tree = ShallowAlertGrid();
diff --git a/ui/src/__mocks__/Stories.js b/ui/src/__mocks__/Stories.js
index b459229ea..4b9dffa4a 100644
--- a/ui/src/__mocks__/Stories.js
+++ b/ui/src/__mocks__/Stories.js
@@ -243,7 +243,13 @@ const MockGrid = (alertStore) => {
],
clusters: { am: ["am1", "am2"], failed: ["failed"] },
};
- alertStore.data.groups = groups;
+ alertStore.data.grids = [
+ {
+ labelName: "",
+ labelValue: "",
+ alertGroups: groups,
+ },
+ ];
};
export { MockGrid };