diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.js
index 0933af936..955121834 100644
--- a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.js
+++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.js
@@ -130,9 +130,7 @@ const GroupMenu = ({
onClick={collapse.toggle}
className={`${
themed ? "text-white" : "text-muted"
- } cursor-pointer badge pl-0 pr-3 pr-sm-2 components-label mr-0 components-grid-alertgroup-${
- group.id
- }`}
+ } cursor-pointer badge pl-0 pr-3 pr-sm-2 components-label mr-0`}
data-toggle="dropdown"
>
diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.js
index cbb591c5a..031682ca9 100644
--- a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.js
+++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.js
@@ -1,8 +1,8 @@
-import React, { Component } from "react";
+import React, { useEffect, useCallback, useRef } from "react";
import PropTypes from "prop-types";
-import { observer } from "mobx-react";
-import { observable, action, toJS } from "mobx";
+import { toJS } from "mobx";
+import { useObserver, useLocalStore } from "mobx-react";
import { Fade } from "react-reveal";
@@ -46,273 +46,232 @@ const AllAlertsAreUsingSameAlertmanagers = (alerts) => {
);
};
-const AlertGroup = observer(
- class AlertGroup extends Component {
- static propTypes = {
- afterUpdate: PropTypes.func.isRequired,
- group: APIGroup.isRequired,
- showAlertmanagers: PropTypes.bool.isRequired,
- alertStore: PropTypes.instanceOf(AlertStore).isRequired,
- settingsStore: PropTypes.instanceOf(Settings).isRequired,
- silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
- groupWidth: PropTypes.number.isRequired,
- gridLabelValue: PropTypes.string.isRequired,
- };
+const AlertGroup = ({
+ group,
+ showAlertmanagers,
+ afterUpdate,
+ silenceFormStore,
+ alertStore,
+ settingsStore,
+ groupWidth,
+ gridLabelValue,
+ initialAlertsToRender,
+}) => {
+ const defaultRenderCount = toJS(
+ settingsStore.alertGroupConfig.config.defaultRenderCount
+ );
- constructor(props) {
- super(props);
+ const renderConfig = useLocalStore(() => ({
+ alertsToRender: initialAlertsToRender || defaultRenderCount,
+ isMenuOpen: false,
+ setIsMenuOpen(val) {
+ this.isMenuOpen = val;
+ },
+ }));
- const { settingsStore } = props;
+ const collapse = useLocalStore(() => ({
+ value: DefaultDetailsCollapseValue(settingsStore),
+ toggle() {
+ this.value = !this.value;
+ },
+ set(value) {
+ this.value = value;
+ },
+ }));
- this.defaultRenderCount = toJS(
- settingsStore.alertGroupConfig.config.defaultRenderCount
- );
+ // Used to calculate step size when loading more alerts.
+ // Step is calculated from the excesive alert count
+ // (what's > defaultRenderCount) by dividing it into 5 clicks.
+ // Don't use step lower than 5, too much clicking if we have a group of 9:
+ // * we'll show initially 5
+ // * step would be 1
+ // * 4 extra clicks to see the entire group
+ // but ensure that step wouldn't push us above totalSize
+ // With 9 alerts and rendering 5 initially we want to show extra 9 after one
+ // click, and when user clicks showLess we want to go back to 5.
+ const getStepSize = (totalSize) => {
+ const val = Math.min(
+ Math.max(Math.round((totalSize - defaultRenderCount) / 5), 5),
+ totalSize - defaultRenderCount
+ );
+ return val;
+ };
- this.renderConfig = observable(
- {
- alertsToRender: this.defaultRenderCount,
- isMenuOpen: false,
- animationDone: false,
- setIsMenuOpen(val) {
- this.isMenuOpen = val;
- },
- setAnimationDone() {
- this.animationDone = true;
- },
- },
- {
- setIsMenuOpen: action.bound,
- setAnimationDone: action.bound,
- }
- );
+ const loadMore = () => {
+ const step = getStepSize(group.alerts.length);
- // 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: DefaultDetailsCollapseValue(settingsStore),
- toggle() {
- this.value = !this.value;
- },
- set(value) {
- this.value = value;
- },
- },
- {
- toggle: action.bound,
- set: action,
- },
- { name: "Collpase toggle" }
- );
- }
+ // show cur+step, but not more that total alert count
+ renderConfig.alertsToRender = Math.min(
+ renderConfig.alertsToRender + step,
+ group.alerts.length
+ );
+ };
- loadMore = action(() => {
- const { group } = this.props;
+ const loadLess = () => {
+ const step = getStepSize(group.alerts.length);
- const step = this.getStepSize(group.alerts.length);
-
- // show cur+step, but not more that total alert count
- this.renderConfig.alertsToRender = Math.min(
- this.renderConfig.alertsToRender + step,
- group.alerts.length
- );
- });
-
- loadLess = action(() => {
- const { group } = this.props;
-
- const step = this.getStepSize(group.alerts.length);
-
- // show cur-step, but not less than 1
- this.renderConfig.alertsToRender = Math.max(
- this.renderConfig.alertsToRender - step,
- 1
- );
- });
-
- // Used to calculate step size when loading more alerts.
- // Step is calculated from the excesive alert count
- // (what's > defaultRenderCount) by dividing it into 5 clicks.
- // Don't use step lower than 5, too much clicking if we have a group of 9:
- // * we'll show initially 5
- // * step would be 1
- // * 4 extra clicks to see the entire group
- // but ensure that step wouldn't push us above totalSize
- // With 9 alerts and rendering 5 initially we want to show extra 9 after one
- // click, and when user clicks showLess we want to go back to 5.
- getStepSize(totalSize) {
- const val = Math.min(
- Math.max(Math.round((totalSize - this.defaultRenderCount) / 5), 5),
- totalSize - this.defaultRenderCount
- );
- return val;
- }
-
- onAlertGroupCollapseEvent = (event) => {
- const { gridLabelValue } = this.props;
+ // show cur-step, but not less than 1
+ renderConfig.alertsToRender = Math.max(
+ renderConfig.alertsToRender - step,
+ 1
+ );
+ };
+ const onAlertGroupCollapseEvent = useCallback(
+ (event) => {
if (event.detail.gridLabelValue === gridLabelValue) {
- this.collapse.set(event.detail.value);
+ collapse.set(event.detail.value);
}
- };
+ },
+ [collapse, gridLabelValue]
+ );
- componentDidMount() {
- window.addEventListener(
- "alertGroupCollapse",
- this.onAlertGroupCollapseEvent
- );
- }
-
- componentDidUpdate() {
- // whenever grid component re-renders we need to ensure that grid elements
- // are packed correctly
- this.props.afterUpdate();
- }
-
- componentWillUnmount() {
+ useEffect(() => {
+ window.addEventListener("alertGroupCollapse", onAlertGroupCollapseEvent);
+ return () => {
window.removeEventListener(
"alertGroupCollapse",
- this.onAlertGroupCollapseEvent
+ onAlertGroupCollapseEvent
);
- }
+ };
+ }, [onAlertGroupCollapseEvent]);
- render() {
- const {
- group,
- showAlertmanagers,
- afterUpdate,
- silenceFormStore,
- alertStore,
- settingsStore,
- groupWidth,
- gridLabelValue,
- } = this.props;
+ useEffect(() => {
+ afterUpdate();
+ });
- let footerAlertmanagers = [];
- let showAlertmanagersInFooter = false;
+ let footerAlertmanagers = [];
+ let showAlertmanagersInFooter = false;
- // There's no need to render @alertmanager labels if there's only 1
- // alertmanager upstream
- if (showAlertmanagers) {
- // Decide if we show @alertmanager label in footer or for every alert
- // we show it in the footer only if every alert has the same set of
- // alertmanagers (and there's > 1 alert to show, there's no footer for 1)
- showAlertmanagersInFooter =
- group.alerts.length > 1 &&
- AllAlertsAreUsingSameAlertmanagers(group.alerts);
- if (showAlertmanagersInFooter) {
- footerAlertmanagers = group.alerts[0].alertmanager.map(
- (am) => am.name
- );
- }
- }
-
- let themedCounters = true;
- let cardBackgroundClass = "bg-light";
- if (settingsStore.alertGroupConfig.config.colorTitleBar) {
- const stateList = Object.entries(group.stateCount)
- .filter(([k, v]) => v !== 0)
- .map(([k, _]) => k);
- if (stateList.length === 1) {
- const state = stateList.pop();
- cardBackgroundClass = BackgroundClassMap[state];
- themedCounters = false;
- }
- }
-
- return (
-
-
-
-
- {this.collapse.value ? null : (
-
-
- {group.alerts
- .slice(0, this.renderConfig.alertsToRender)
- .map((alert) => (
- 1 &&
- group.alerts.length === 1
- }
- afterUpdate={afterUpdate}
- alertStore={alertStore}
- silenceFormStore={silenceFormStore}
- setIsMenuOpen={this.renderConfig.setIsMenuOpen}
- />
- ))}
- {group.alerts.length > this.defaultRenderCount ? (
- -
-
-
- {Math.min(
- this.renderConfig.alertsToRender,
- group.alerts.length
- )}
- {" of "}
- {group.alerts.length}
-
-
-
- ) : null}
-
-
- )}
- {this.collapse.value === false && group.alerts.length > 1 ? (
-
- ) : null}
-
-
-
- );
+ // There's no need to render @alertmanager labels if there's only 1
+ // alertmanager upstream
+ if (showAlertmanagers) {
+ // Decide if we show @alertmanager label in footer or for every alert
+ // we show it in the footer only if every alert has the same set of
+ // alertmanagers (and there's > 1 alert to show, there's no footer for 1)
+ showAlertmanagersInFooter =
+ group.alerts.length > 1 &&
+ AllAlertsAreUsingSameAlertmanagers(group.alerts);
+ if (showAlertmanagersInFooter) {
+ footerAlertmanagers = group.alerts[0].alertmanager.map((am) => am.name);
}
}
-);
-AlertGroup.contextType = ThemeContext;
+
+ let themedCounters = true;
+ let cardBackgroundClass = "bg-light";
+ if (settingsStore.alertGroupConfig.config.colorTitleBar) {
+ const stateList = Object.entries(group.stateCount)
+ .filter(([k, v]) => v !== 0)
+ .map(([k, _]) => k);
+ if (stateList.length === 1) {
+ const state = stateList.pop();
+ cardBackgroundClass = BackgroundClassMap[state];
+ themedCounters = false;
+ }
+ }
+
+ const context = React.useContext(ThemeContext);
+
+ const mountRef = useRef(null);
+
+ return useObserver(() => (
+
+
+
+
+ {collapse.value ? null : (
+
+
+ {group.alerts
+ .slice(0, renderConfig.alertsToRender)
+ .map((alert) => (
+ 1 &&
+ group.alerts.length === 1
+ }
+ afterUpdate={afterUpdate}
+ alertStore={alertStore}
+ silenceFormStore={silenceFormStore}
+ setIsMenuOpen={renderConfig.setIsMenuOpen}
+ />
+ ))}
+ {group.alerts.length > defaultRenderCount ? (
+ -
+
+
+ {Math.min(
+ renderConfig.alertsToRender,
+ group.alerts.length
+ )}
+ {" of "}
+ {group.alerts.length}
+
+
+
+ ) : null}
+
+
+ )}
+ {collapse.value === false && group.alerts.length > 1 ? (
+
+ ) : null}
+
+
+
+ ));
+};
+AlertGroup.propTypes = {
+ afterUpdate: PropTypes.func.isRequired,
+ group: APIGroup.isRequired,
+ showAlertmanagers: PropTypes.bool.isRequired,
+ alertStore: PropTypes.instanceOf(AlertStore).isRequired,
+ settingsStore: PropTypes.instanceOf(Settings).isRequired,
+ silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
+ groupWidth: PropTypes.number.isRequired,
+ gridLabelValue: PropTypes.string.isRequired,
+ initialAlertsToRender: PropTypes.number,
+};
export { AlertGroup };
diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.js
index 7ef61d4de..b49b58ebb 100644
--- a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.js
+++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.js
@@ -1,4 +1,5 @@
import React from "react";
+import { act } from "react-dom/test-utils";
import { mount } from "enzyme";
@@ -31,8 +32,6 @@ const MockGroup = (groupName) => {
let originalInnerWidth;
beforeAll(() => {
- jest.useFakeTimers();
-
originalInnerWidth = global.innerWidth;
});
@@ -60,12 +59,17 @@ const MockAlerts = (alertCount) => {
}
};
-const MountedAlertGroup = (afterUpdate, showAlertmanagers) => {
+const MountedAlertGroup = (
+ afterUpdate,
+ showAlertmanagers,
+ initialAlertsToRender
+) => {
return mount(
", () => {
tree.unmount();
});
- it("appends components-animation-fade-appear-done class after 1s", () => {
+ it("appends components-animation-fade-appear-done class after first mount", () => {
MockAlerts(5);
const tree = MountedAlertGroup(jest.fn(), true);
expect(
@@ -110,9 +112,7 @@ describe("", () => {
.find("div.components-grid-alertgrid-alertgroup")
.hasClass("components-animation-fade-appear-done")
).toBe(false);
-
- tree.instance().renderConfig.setAnimationDone();
- tree.update();
+ tree.setProps({});
expect(
tree
.find("div.components-grid-alertgrid-alertgroup")
@@ -150,10 +150,7 @@ describe("", () => {
it("only renders titlebar when collapsed", () => {
MockAlerts(10);
const tree = MountedAlertGroup(jest.fn(), false);
- const alertGroup = tree.find("AlertGroup");
- alertGroup.instance().collapse.toggle();
- expect(alertGroup.instance().collapse.value).toBe(true);
- tree.update();
+ tree.find("span.badge.cursor-pointer").at(1).simulate("click");
expect(tree.find("Alert")).toHaveLength(0);
expect(tree.find("ul.list-group")).toHaveLength(0);
});
@@ -242,13 +239,7 @@ const ValidateLoadButtonAction = (
alertsToRenderBeforeClick
) => {
MockAlerts(totalAlerts);
- const tree = MountedAlertGroup(jest.fn(), false);
- if (alertsToRenderBeforeClick !== undefined) {
- tree
- .find("AlertGroup")
- .instance().renderConfig.alertsToRender = alertsToRenderBeforeClick;
- tree.update();
- }
+ const tree = MountedAlertGroup(jest.fn(), false, alertsToRenderBeforeClick);
const loadMore = tree.find("button").at(buttonIndex);
expect(loadMore.html()).toMatch(iconMatch);
loadMore.simulate("click");
@@ -261,16 +252,13 @@ describe(" renderConfig", () => {
expect(settingsStore.alertGroupConfig.config.defaultRenderCount).toBe(5);
});
- it("renderConfig.alertsToRender should be 5 by default", () => {
- const tree = MountedAlertGroup(jest.fn(), false).find("AlertGroup");
- expect(tree.instance().renderConfig.alertsToRender).toBe(5);
- });
-
- it("renders only up to renderConfig.alertsToRender alerts", () => {
+ it("renders only up to settingsStore.alertGroupConfig.config.defaultRenderCount alerts", () => {
MockAlerts(50);
const tree = MountedAlertGroup(jest.fn(), false).find("AlertGroup");
const alerts = tree.find("Alert");
- expect(alerts).toHaveLength(tree.instance().renderConfig.alertsToRender);
+ expect(alerts).toHaveLength(
+ settingsStore.alertGroupConfig.config.defaultRenderCount
+ );
});
it("load buttons are not rendered for 1 alert", () => {
@@ -321,15 +309,18 @@ describe(" renderConfig", () => {
ValidateLoadButtonAction(25, 1, /fa-plus/, 22, 17);
});
- it("uses 'z-index: 100' style after setIsMenuOpen() is called on any Alert", () => {
+ it("uses 'z-index: 100' style after setIsMenuOpen() is called on any Alert", async () => {
+ const promise = Promise.resolve();
MockAlerts(5);
const tree = MountedAlertGroup(jest.fn(), false);
- const instance = tree.find("AlertGroup").instance();
- expect(instance.renderConfig.isMenuOpen).toBe(false);
-
- tree.find("Alert").at(0).props().setIsMenuOpen(true);
- expect(instance.renderConfig.isMenuOpen).toBe(true);
+ tree
+ .find("Alert")
+ .at(0)
+ .find("span.badge-secondary.cursor-pointer")
+ .at(0)
+ .simulate("click");
+ await act(() => promise);
tree.update();
expect(
tree.find(".components-grid-alertgrid-alertgroup").at(0).props().style
@@ -337,15 +328,13 @@ describe(" renderConfig", () => {
).toEqual(100);
});
- it("uses 'z-index: 100' style after setIsMenuOpen() is called on AlertGroup header menu", () => {
+ it("uses 'z-index: 100' style after setIsMenuOpen() is called on AlertGroup header menu", async () => {
+ const promise = Promise.resolve();
MockAlerts(5);
const tree = MountedAlertGroup(jest.fn(), false);
- const instance = tree.find("AlertGroup").instance();
- expect(instance.renderConfig.isMenuOpen).toBe(false);
-
- tree.find("GroupHeader").at(0).props().setIsMenuOpen(true);
- expect(instance.renderConfig.isMenuOpen).toBe(true);
+ tree.find("span.cursor-pointer").at(0).simulate("click");
+ await act(() => promise);
tree.update();
expect(
tree.find(".components-grid-alertgrid-alertgroup").at(0).props().style