diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.js index 213e96b5a..e5478689b 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.js @@ -1,8 +1,7 @@ -import React, { Component } from "react"; +import React from "react"; import PropTypes from "prop-types"; -import { action, observable } from "mobx"; -import { observer } from "mobx-react"; +import { useObserver, useLocalStore } from "mobx-react"; import hash from "object-hash"; @@ -96,94 +95,79 @@ MenuContent.propTypes = { afterClick: PropTypes.func.isRequired, }; -const AlertMenu = observer( - class AlertMenu extends Component { - static propTypes = { - group: APIGroup.isRequired, - alert: APIAlert.isRequired, - alertStore: PropTypes.instanceOf(AlertStore).isRequired, - silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired, - setIsMenuOpen: PropTypes.func.isRequired, - }; +const AlertMenu = ({ + group, + alert, + alertStore, + silenceFormStore, + setIsMenuOpen, +}) => { + const collapse = useLocalStore(() => ({ + value: true, + toggle() { + this.value = !this.value; + setIsMenuOpen(!this.value); + }, + hide() { + this.value = true; + setIsMenuOpen(!this.value); + }, + })); - constructor(props) { - super(props); + const uniqueClass = `components-grid-alert-${group.id}-${hash(alert.labels)}`; - this.collapse = observable( - { - value: true, - toggle() { - this.value = !this.value; - props.setIsMenuOpen(!this.value); - }, - hide() { - this.value = true; - props.setIsMenuOpen(!this.value); - }, - }, - { toggle: action.bound, hide: action.bound }, - { name: "Alert menu toggle" } - ); - } - - handleClickOutside = action((event) => { - this.collapse.hide(); - }); - - render() { - const { group, alert, alertStore, silenceFormStore } = this.props; - - const uniqueClass = `components-grid-alert-${group.id}-${hash( - alert.labels - )}`; - - return ( - - - {({ ref }) => ( - - - {alert.startsAt} - - )} - - - - {({ placement, ref, style }) => ( - - )} - - - - ); - } - } -); + return useObserver(() => ( + + + {({ ref }) => ( + + + {alert.startsAt} + + )} + + + + {({ placement, ref, style }) => ( + + )} + + + + )); +}; +AlertMenu.propTypes = { + group: APIGroup.isRequired, + alert: APIAlert.isRequired, + alertStore: PropTypes.instanceOf(AlertStore).isRequired, + silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired, + setIsMenuOpen: PropTypes.func.isRequired, +}; export { AlertMenu, MenuContent }; diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.test.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.test.js index 87126bdee..d35a8e9f5 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.test.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.test.js @@ -13,9 +13,20 @@ let silenceFormStore; let alert; let group; +let MockAfterClick; +let MockSetIsMenuOpen; + +beforeAll(() => { + jest.useFakeTimers(); +}); + beforeEach(() => { alertStore = new AlertStore([]); silenceFormStore = new SilenceFormStore(); + + MockAfterClick = jest.fn(); + MockSetIsMenuOpen = jest.fn(); + alert = MockAlert([], { foo: "bar" }, "active"); group = MockAlertGroup({ alertname: "Fake Alert" }, [alert], [], {}, {}); @@ -38,9 +49,6 @@ beforeEach(() => { }; }); -const MockAfterClick = jest.fn(); -const MockSetIsMenuOpen = jest.fn(); - const MountedAlertMenu = (group) => { return mount( { }; describe("", () => { - it("is collapsed by default", () => { + it("menu content is hidden by default", () => { const tree = MountedAlertMenu(group); - expect(tree.instance().collapse.value).toBe(true); + expect(tree.find("div.dropdown-menu")).toHaveLength(0); + expect(MockSetIsMenuOpen).not.toHaveBeenCalled(); }); - it("clicking toggle sets collapse value to 'false'", async () => { + it("clicking toggle renders menu content", async () => { const promise = Promise.resolve(); const tree = MountedAlertMenu(group); - const toggle = tree.find(".cursor-pointer"); + const toggle = tree.find("span.cursor-pointer"); toggle.simulate("click"); - expect(tree.instance().collapse.value).toBe(false); + expect(MockSetIsMenuOpen).toHaveBeenCalledTimes(1); + expect(tree.find("div.dropdown-menu")).toHaveLength(1); await act(() => promise); }); - it("handleClickOutside() call sets collapse value to 'true'", async () => { + it("clicking toggle twice hides menu content", async () => { const promise = Promise.resolve(); const tree = MountedAlertMenu(group); - const toggle = tree.find(".cursor-pointer"); + const toggle = tree.find("span.cursor-pointer"); + toggle.simulate("click"); - expect(tree.instance().collapse.value).toBe(false); + jest.runOnlyPendingTimers(); + expect(MockSetIsMenuOpen).toHaveBeenCalledTimes(1); + expect(tree.find("div.dropdown-menu")).toHaveLength(1); - tree.instance().handleClickOutside(); + toggle.simulate("click"); + jest.runOnlyPendingTimers(); + tree.update(); + expect(MockSetIsMenuOpen).toHaveBeenCalledTimes(2); + expect(tree.find("div.dropdown-menu")).toHaveLength(0); + await act(() => promise); + }); - expect(tree.instance().collapse.value).toBe(true); + it("clicking menu item hides menu content", async () => { + const promise = Promise.resolve(); + const tree = MountedAlertMenu(group); + const toggle = tree.find("span.cursor-pointer"); + + toggle.simulate("click"); + expect(MockSetIsMenuOpen).toHaveBeenCalledTimes(1); + expect(tree.find("div.dropdown-menu")).toHaveLength(1); + + tree.find("a.dropdown-item").at(0).simulate("click"); + jest.runOnlyPendingTimers(); + tree.update(); + expect(MockSetIsMenuOpen).toHaveBeenCalledTimes(2); + expect(tree.find("div.dropdown-menu")).toHaveLength(0); await act(() => promise); }); }); diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.js index e85237a11..88c1bfc4d 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.js @@ -1,8 +1,7 @@ -import React, { Component } from "react"; +import React from "react"; import PropTypes from "prop-types"; -import { action, observable } from "mobx"; -import { observer } from "mobx-react"; +import { useObserver, useLocalStore } from "mobx-react"; import copy from "copy-to-clipboard"; @@ -99,88 +98,75 @@ MenuContent.propTypes = { afterClick: PropTypes.func.isRequired, }; -const GroupMenu = observer( - class GroupMenu extends Component { - static propTypes = { - group: APIGroup.isRequired, - alertStore: PropTypes.instanceOf(AlertStore).isRequired, - silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired, - themed: PropTypes.bool.isRequired, - setIsMenuOpen: PropTypes.func.isRequired, - }; +const GroupMenu = ({ + group, + alertStore, + silenceFormStore, + themed, + setIsMenuOpen, +}) => { + const collapse = useLocalStore(() => ({ + isHidden: true, + toggle() { + this.isHidden = !this.isHidden; + setIsMenuOpen(!this.isHidden); + }, + hide() { + this.isHidden = true; + setIsMenuOpen(!this.isHidden); + }, + })); - constructor(props) { - super(props); - - this.collapse = observable( - { - value: true, - toggle() { - this.value = !this.value; - props.setIsMenuOpen(!this.value); - }, - hide() { - this.value = true; - props.setIsMenuOpen(!this.value); - }, - }, - { toggle: action.bound, hide: action.bound }, - { name: "Alert group menu toggle" } - ); - } - - handleClickOutside = action((event) => { - this.collapse.hide(); - }); - - render() { - const { group, alertStore, silenceFormStore, themed } = this.props; - - return ( - - - {({ ref }) => ( - - - - )} - - - - {({ placement, ref, style }) => ( - - )} - - - - ); - } - } -); + return useObserver(() => ( + + + {({ ref }) => ( + + + + )} + + + + {({ placement, ref, style }) => ( + + )} + + + + )); +}; +GroupMenu.propTypes = { + group: APIGroup.isRequired, + alertStore: PropTypes.instanceOf(AlertStore).isRequired, + silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired, + themed: PropTypes.bool.isRequired, + setIsMenuOpen: PropTypes.func.isRequired, +}; export { GroupMenu, MenuContent }; diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.test.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.test.js index abd9bef3e..05d5450da 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.test.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.test.js @@ -13,10 +13,21 @@ import { GroupMenu, MenuContent } from "./GroupMenu"; let alertStore; let silenceFormStore; +let MockAfterClick; +let MockSetIsMenuOpen; + +beforeAll(() => { + jest.useFakeTimers(); +}); + beforeEach(() => { alertStore = new AlertStore([]); silenceFormStore = new SilenceFormStore(); + jest.clearAllMocks(); + MockAfterClick = jest.fn(); + MockSetIsMenuOpen = jest.fn(); + alertStore.data.upstreams = { clusters: { default: ["am1"] }, instances: [ @@ -36,9 +47,6 @@ beforeEach(() => { }; }); -const MockAfterClick = jest.fn(); -const MockSetIsMenuOpen = jest.fn(); - const MountedGroupMenu = (group, themed) => { return mount( { themed={themed} setIsMenuOpen={MockSetIsMenuOpen} /> - ).find("GroupMenu"); + ); }; describe("", () => { - it("is collapsed by default", () => { + it("menu content is hidden by default", () => { const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {}, {}); const tree = MountedGroupMenu(group, true); - expect(tree.instance().collapse.value).toBe(true); + expect(tree.find("div.dropdown-menu")).toHaveLength(0); + expect(MockSetIsMenuOpen).not.toHaveBeenCalled(); }); - it("clicking toggle sets collapse value to 'false'", async () => { + it("clicking toggle renders menu content", async () => { const promise = Promise.resolve(); const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {}, {}); const tree = MountedGroupMenu(group, true); - const toggle = tree.find(".cursor-pointer"); + const toggle = tree.find("span.cursor-pointer"); toggle.simulate("click"); - expect(tree.instance().collapse.value).toBe(false); + expect(MockSetIsMenuOpen).toHaveBeenCalledTimes(1); + expect(tree.find("div.dropdown-menu")).toHaveLength(1); await act(() => promise); }); - it("handleClickOutside() call sets collapse value to 'true'", async () => { + it("clicking toggle twice hides menu content", async () => { const promise = Promise.resolve(); const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {}, {}); const tree = MountedGroupMenu(group, true); - const toggle = tree.find(".cursor-pointer"); + const toggle = tree.find("span.cursor-pointer"); toggle.simulate("click"); - expect(tree.instance().collapse.value).toBe(false); + jest.runOnlyPendingTimers(); + expect(MockSetIsMenuOpen).toHaveBeenCalledTimes(1); + expect(tree.find("div.dropdown-menu")).toHaveLength(1); - tree.instance().handleClickOutside(); + toggle.simulate("click"); + jest.runOnlyPendingTimers(); + tree.update(); + expect(MockSetIsMenuOpen).toHaveBeenCalledTimes(2); + expect(tree.find("div.dropdown-menu")).toHaveLength(0); + await act(() => promise); + }); - expect(tree.instance().collapse.value).toBe(true); + it("clicking menu item hides menu content", async () => { + const promise = Promise.resolve(); + const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {}, {}); + const tree = MountedGroupMenu(group, true); + const toggle = tree.find("span.cursor-pointer"); + + toggle.simulate("click"); + expect(MockSetIsMenuOpen).toHaveBeenCalledTimes(1); + expect(tree.find("div.dropdown-menu")).toHaveLength(1); + + tree.find("div.dropdown-item").at(0).simulate("click"); + jest.runOnlyPendingTimers(); + tree.update(); + expect(MockSetIsMenuOpen).toHaveBeenCalledTimes(2); + expect(tree.find("div.dropdown-menu")).toHaveLength(0); await act(() => promise); }); });