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