diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.test.tsx b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.test.tsx index f1d874e41..f37a3b6d5 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.test.tsx +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.test.tsx @@ -7,6 +7,7 @@ import type { APIAlertGroupT, APIAlertT, APIAlertsResponseUpstreamsT, + APIGridT, } from "Models/APITypes"; import { AlertStore } from "Stores/AlertStore"; import { SilenceFormStore } from "Stores/SilenceFormStore"; @@ -16,6 +17,7 @@ let alertStore: AlertStore; let silenceFormStore: SilenceFormStore; let alert: APIAlertT; let group: APIAlertGroupT; +let grid: APIGridT; let MockAfterClick: () => void; let MockSetIsMenuOpen: () => void; @@ -74,6 +76,17 @@ beforeEach(() => { alert = MockAlert([], { foo: "bar" }, "active"); group = MockAlertGroup({ alertname: "Fake Alert" }, [alert], [], {}, {}); + grid = { + labelName: "foo", + labelValue: "bar", + alertGroups: [], + totalGroups: 0, + stateCount: { + active: 0, + suppressed: 0, + unprocessed: 0, + }, + }; alertStore.data.setUpstreams(generateUpstreams()); }); @@ -81,6 +94,7 @@ beforeEach(() => { const MountedAlertMenu = (group: APIAlertGroupT) => { return mount( void; }> = observer( - ({ group, alert, alertStore, silenceFormStore, setIsMenuOpen }) => { + ({ grid, group, alert, alertStore, silenceFormStore, setIsMenuOpen }) => { const [isHidden, setIsHidden] = useState(true); const toggle = useCallback(() => { + window.dispatchEvent( + new CustomEvent("gridMenuOpen", { + detail: { isOpen: isHidden, labelValue: grid.labelValue }, + }) + ); setIsMenuOpen(isHidden); setIsHidden(!isHidden); - }, [isHidden, setIsMenuOpen]); + }, [grid.labelValue, isHidden, setIsMenuOpen]); const hide = useCallback(() => { setIsHidden(true); diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.test.tsx b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.test.tsx index cf5332501..7308abba7 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.test.tsx +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/index.test.tsx @@ -13,7 +13,7 @@ import { MockSilence, } from "__fixtures__/Alerts"; import { MockThemeContext } from "__fixtures__/Theme"; -import type { APIAlertGroupT, APIAlertT } from "Models/APITypes"; +import type { APIAlertGroupT, APIAlertT, APIGridT } from "Models/APITypes"; import { AlertStore } from "Stores/AlertStore"; import { SilenceFormStore } from "Stores/SilenceFormStore"; import { BorderClassMap } from "Common/Colors"; @@ -22,11 +22,24 @@ import Alert from "."; let alertStore: AlertStore; let silenceFormStore: SilenceFormStore; +let grid: APIGridT; beforeEach(() => { advanceTo(new Date(Date.UTC(2018, 7, 15, 20, 40, 0))); alertStore = new AlertStore([]); silenceFormStore = new SilenceFormStore(); + + grid = { + labelName: "foo", + labelValue: "bar", + alertGroups: [], + totalGroups: 0, + stateCount: { + active: 0, + suppressed: 0, + unprocessed: 0, + }, + }; }); afterEach(() => { @@ -58,6 +71,7 @@ const MountedAlert = ( ) => { return mount( void; }> = ({ + grid, group, alert, showReceiver, @@ -99,6 +102,7 @@ const Alert: FC<{ ))} void; let MockSetIsMenuOpen: () => void; @@ -72,11 +74,24 @@ beforeEach(() => { MockSetIsMenuOpen = jest.fn(); alertStore.data.setUpstreams(generateUpstreams()); + + grid = { + labelName: "foo", + labelValue: "bar", + alertGroups: [], + totalGroups: 0, + stateCount: { + active: 0, + suppressed: 0, + unprocessed: 0, + }, + }; }); const MountedGroupMenu = (group: APIAlertGroupT, themed: boolean) => { return mount( void; -}> = ({ group, alertStore, silenceFormStore, themed, setIsMenuOpen }) => { +}> = ({ grid, group, alertStore, silenceFormStore, themed, setIsMenuOpen }) => { const [isHidden, setIsHidden] = useState(true); const toggle = useCallback(() => { + window.dispatchEvent( + new CustomEvent("gridMenuOpen", { + detail: { isOpen: isHidden, labelValue: grid.labelValue }, + }) + ); setIsMenuOpen(isHidden); setIsHidden(!isHidden); - }, [isHidden, setIsMenuOpen]); + }, [setIsMenuOpen, isHidden, grid.labelValue]); const hide = useCallback(() => { setIsHidden(true); diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/index.tsx b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/index.tsx index ae0d3b703..f11ff3a76 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/index.tsx +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/index.tsx @@ -2,7 +2,7 @@ import type { FC, MouseEvent } from "react"; import { observer } from "mobx-react-lite"; -import type { APIAlertGroupT } from "Models/APITypes"; +import type { APIAlertGroupT, APIGridT } from "Models/APITypes"; import type { AlertStore } from "Stores/AlertStore"; import type { SilenceFormStore } from "Stores/SilenceFormStore"; import FilteringLabel from "Components/Labels/FilteringLabel"; @@ -15,6 +15,7 @@ import { GroupMenu } from "./GroupMenu"; const GroupHeader: FC<{ isCollapsed: boolean; setIsCollapsed: (isCollapsed: boolean) => void; + grid: APIGridT; group: APIAlertGroupT; alertStore: AlertStore; silenceFormStore: SilenceFormStore; @@ -24,6 +25,7 @@ const GroupHeader: FC<{ }> = ({ isCollapsed, setIsCollapsed, + grid, group, alertStore, silenceFormStore, @@ -55,6 +57,7 @@ const GroupHeader: FC<{ > { @@ -50,6 +51,18 @@ beforeEach(() => { ...alertStore.settings.values, ...{ historyEnabled: false }, }); + + grid = { + labelName: "foo", + labelValue: "bar", + alertGroups: [], + totalGroups: 0, + stateCount: { + active: 0, + suppressed: 0, + unprocessed: 0, + }, + }; }); afterEach(() => { @@ -67,16 +80,12 @@ const MockAlerts = (alertCount: number, totalAlerts: number) => { group.totalAlerts = totalAlerts; }; -const MountedAlertGroup = ( - afterUpdate: () => void, - showAlertmanagers: boolean, - theme?: ThemeCtx -) => { +const MountedAlertGroup = (afterUpdate: () => void, theme?: ThemeCtx) => { return mount( ", () => { it("doesn't crash on unmount", () => { MockAlerts(5, 5); - const tree = MountedAlertGroup(jest.fn(), true); + const tree = MountedAlertGroup(jest.fn()); tree.unmount(); }); it("uses 'animate' class when settingsStore.themeConfig.config.animations is true", () => { MockAlerts(5, 5); - const tree = MountedAlertGroup(jest.fn(), true, MockThemeContext); + const tree = MountedAlertGroup(jest.fn(), MockThemeContext); expect( tree.find("div.components-grid-alertgrid-alertgroup").hasClass("animate") ).toBe(true); @@ -123,7 +132,6 @@ describe("", () => { MockAlerts(5, 5); const tree = MountedAlertGroup( jest.fn(), - true, MockThemeContextWithoutAnimations ); expect( @@ -134,7 +142,7 @@ describe("", () => { it("renders Alertmanager cluster labels in footer if shared", () => { MockAlerts(2, 2); group.shared.clusters = ["default"]; - const tree = MountedAlertGroup(jest.fn(), true).find("AlertGroup"); + const tree = MountedAlertGroup(jest.fn()).find("AlertGroup"); expect(tree.find("GroupFooter").html()).toMatch(/@cluster/); }); @@ -163,7 +171,7 @@ describe("", () => { }); } group.shared.clusters = ["default", "HA"]; - const tree = MountedAlertGroup(jest.fn(), true).find("AlertGroup"); + const tree = MountedAlertGroup(jest.fn()).find("AlertGroup"); const labels = tree.find("GroupFooter").find("FilteringLabel"); expect(labels).toHaveLength(3); expect(labels.at(0).text()).toBe("@cluster: default"); @@ -176,7 +184,7 @@ describe("", () => { for (let i = 0; i < group.alerts.length; i++) { group.alerts[i].alertmanager = []; } - const tree = MountedAlertGroup(jest.fn(), true).find("AlertGroup"); + const tree = MountedAlertGroup(jest.fn()).find("AlertGroup"); const labels = tree.find("GroupFooter").find("FilteringLabel"); expect(labels).toHaveLength(1); expect(labels.at(0).text()).toBe("@receiver: by-name"); @@ -194,7 +202,7 @@ describe("", () => { fakeAlertmanager3: 1, fakeAlertmanager4: 1, }; - const tree = MountedAlertGroup(jest.fn(), true); + const tree = MountedAlertGroup(jest.fn()); const alerts = tree.find("ul.list-group"); expect(alerts.html()).toMatch(/@cluster/); @@ -205,7 +213,7 @@ describe("", () => { it("only renders titlebar when collapsed", () => { MockAlerts(5, 10); - const tree = MountedAlertGroup(jest.fn(), false); + const tree = MountedAlertGroup(jest.fn()); tree.find("span.badge.cursor-pointer").at(1).simulate("click"); expect(tree.find("Alert")).toHaveLength(0); expect(tree.find("ul.list-group")).toHaveLength(0); @@ -214,7 +222,7 @@ describe("", () => { it("renders reduced details when idle", () => { MockAlerts(5, 10); alertStore.ui.setIsIdle(true); - const tree = MountedAlertGroup(jest.fn(), true, MockThemeContext); + const tree = MountedAlertGroup(jest.fn(), MockThemeContext); expect(tree.find("Alert")).toHaveLength(1); }); @@ -251,14 +259,14 @@ describe("", () => { it("renders @receiver label when alertStore.data.receivers.length > 1", () => { alertStore.data.setReceivers(["foo", "bar"]); MockAlerts(5, 10); - const tree = MountedAlertGroup(jest.fn(), false); + const tree = MountedAlertGroup(jest.fn()); expect(tree.html()).toMatch(/@receiver:/); }); it("doesn't render @receiver label when alertStore.data.receivers.length == 0", () => { alertStore.data.setReceivers([]); MockAlerts(5, 10); - const tree = MountedAlertGroup(jest.fn(), false); + const tree = MountedAlertGroup(jest.fn()); expect(tree.html()).not.toMatch(/@receiver:/); }); }); @@ -269,7 +277,7 @@ const ValidateLoadButtonPresent = ( isPresent: boolean ) => { MockAlerts(alertCount, totalAlerts); - const tree = MountedAlertGroup(jest.fn(), false); + const tree = MountedAlertGroup(jest.fn()); const buttons = tree.find("button"); expect(buttons).toHaveLength(isPresent ? 2 : 0); }; @@ -282,7 +290,7 @@ const ValidateLoadButtonAction = ( loadedAlerts: number ) => { MockAlerts(alertCount, totalAlerts); - const tree = MountedAlertGroup(jest.fn(), false); + const tree = MountedAlertGroup(jest.fn()); const loadMore = tree.find("button").at(buttonIndex); expect(loadMore.html()).toMatch(iconMatch); loadMore.simulate("click"); @@ -348,7 +356,7 @@ describe(" renderConfig", () => { const promise = Promise.resolve(); MockAlerts(5, 5); - const tree = MountedAlertGroup(jest.fn(), false); + const tree = MountedAlertGroup(jest.fn()); tree .find("Alert") @@ -366,7 +374,7 @@ describe(" renderConfig", () => { it("uses 'z-index: 100' style after setIsMenuOpen() is called on AlertGroup header menu", async () => { const promise = Promise.resolve(); MockAlerts(5, 5); - const tree = MountedAlertGroup(jest.fn(), false); + const tree = MountedAlertGroup(jest.fn()); tree.find("span.cursor-pointer").at(0).simulate("click"); await act(() => promise); @@ -381,7 +389,7 @@ describe(" card theme", () => { it("renders bg-light background when colorTitleBar=false", () => { settingsStore.alertGroupConfig.setColorTitleBar(false); group.stateCount = { active: 5, suppressed: 0, unprocessed: 0 }; - const tree = MountedAlertGroup(jest.fn(), false); + const tree = MountedAlertGroup(jest.fn()); expect(tree.find(".card").hasClass("bg-light")).toBe(true); expect(tree.find(".card").hasClass("bg-danger")).toBe(false); expect(tree.find(".card").hasClass("bg-success")).toBe(false); @@ -391,14 +399,14 @@ describe(" card theme", () => { it("renders themed titlebar when colorTitleBar=false", () => { settingsStore.alertGroupConfig.setColorTitleBar(false); group.stateCount = { active: 5, suppressed: 0, unprocessed: 0 }; - const tree = MountedAlertGroup(jest.fn(), false); + const tree = MountedAlertGroup(jest.fn()); expect(tree.find("GroupHeader").prop("themedCounters")).toBe(true); }); it("renders bg-light border when colorTitleBar=true and there are multiple alert states", () => { settingsStore.alertGroupConfig.setColorTitleBar(false); group.stateCount = { active: 5, suppressed: 0, unprocessed: 0 }; - const tree = MountedAlertGroup(jest.fn(), false); + const tree = MountedAlertGroup(jest.fn()); expect(tree.find(".card").hasClass("bg-light")).toBe(true); expect(tree.find(".card").hasClass("bg-danger")).toBe(false); expect(tree.find(".card").hasClass("bg-success")).toBe(false); @@ -408,14 +416,14 @@ describe(" card theme", () => { it("renders themed titlebar when colorTitleBar=true and there are multiple alert states", () => { settingsStore.alertGroupConfig.setColorTitleBar(true); group.stateCount = { active: 5, suppressed: 6, unprocessed: 7 }; - const tree = MountedAlertGroup(jest.fn(), false); + const tree = MountedAlertGroup(jest.fn()); expect(tree.find("GroupHeader").prop("themedCounters")).toBe(true); }); it("renders state based background when colorTitleBar=true and there's only one alert state", () => { settingsStore.alertGroupConfig.setColorTitleBar(true); group.stateCount = { active: 0, suppressed: 5, unprocessed: 0 }; - const tree = MountedAlertGroup(jest.fn(), false); + const tree = MountedAlertGroup(jest.fn()); expect(tree.find(".card").hasClass("bg-light")).toBe(false); expect(tree.find(".card").hasClass("bg-danger")).toBe(false); expect(tree.find(".card").hasClass("bg-success")).toBe(true); @@ -425,7 +433,7 @@ describe(" card theme", () => { it("renders unthemed titlebar when colorTitleBar=true and there's only one alert state", () => { settingsStore.alertGroupConfig.setColorTitleBar(true); group.stateCount = { active: 5, suppressed: 0, unprocessed: 0 }; - const tree = MountedAlertGroup(jest.fn(), false); + const tree = MountedAlertGroup(jest.fn()); expect(tree.find("GroupHeader").prop("themedCounters")).toBe(false); }); @@ -447,7 +455,7 @@ describe(" card theme", () => { ...{ historyEnabled: true }, }); group.stateCount = { active: 5, suppressed: 0, unprocessed: 0 }; - const tree = MountedAlertGroup(jest.fn(), false); + const tree = MountedAlertGroup(jest.fn()); await act(async () => { await fetchMock.flush(true); }); @@ -462,7 +470,7 @@ describe(" card theme", () => { ...{ historyEnabled: false }, }); group.stateCount = { active: 5, suppressed: 0, unprocessed: 0 }; - const tree = MountedAlertGroup(jest.fn(), false); + const tree = MountedAlertGroup(jest.fn()); expect(tree.find("AlertHistory")).toHaveLength(0); }); }); diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.tsx b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.tsx index b8b7983b8..c48d8f334 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.tsx +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.tsx @@ -8,7 +8,7 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons/faPlus"; import { faMinus } from "@fortawesome/free-solid-svg-icons/faMinus"; import { faEllipsisH } from "@fortawesome/free-solid-svg-icons/faEllipsisH"; -import type { APIAlertGroupT, AlertStateT } from "Models/APITypes"; +import type { APIGridT, APIAlertGroupT, AlertStateT } from "Models/APITypes"; import type { Settings } from "Stores/Settings"; import type { AlertStore } from "Stores/AlertStore"; import type { SilenceFormStore } from "Stores/SilenceFormStore"; @@ -40,8 +40,8 @@ const LoadButton: FC<{ }; const AlertGroup: FC<{ + grid: APIGridT; group: APIAlertGroupT; - showAlertmanagers: boolean; afterUpdate: () => void; alertStore: AlertStore; settingsStore: Settings; @@ -49,8 +49,8 @@ const AlertGroup: FC<{ groupWidth: number; gridLabelValue: string; }> = ({ + grid, group, - showAlertmanagers, afterUpdate, silenceFormStore, alertStore, @@ -159,6 +159,7 @@ const AlertGroup: FC<{ ( (false); + const onMenuOpen = useCallback( + (event) => { + if (event.detail.labelValue === grid.labelValue) { + setIsMenuOpen(event.detail.isOpen); + } + }, + [grid.labelValue] + ); + useEffect(() => { + window.addEventListener("gridMenuOpen", onMenuOpen); + return () => { + window.removeEventListener("gridMenuOpen", onMenuOpen); + }; + }, [onMenuOpen]); return (
setIsMenuOpen(true)} - onMenuClose={() => setIsMenuOpen(false)} />
1 - } afterUpdate={debouncedRepack} alertStore={alertStore} settingsStore={settingsStore} diff --git a/ui/src/Components/Grid/AlertGrid/GridLabelSelect.test.tsx b/ui/src/Components/Grid/AlertGrid/GridLabelSelect.test.tsx index 7b487c650..8e57b269f 100644 --- a/ui/src/Components/Grid/AlertGrid/GridLabelSelect.test.tsx +++ b/ui/src/Components/Grid/AlertGrid/GridLabelSelect.test.tsx @@ -64,8 +64,6 @@ const MountedGridLabelSelect = () => { alertStore={alertStore} settingsStore={settingsStore} grid={grid} - onMenuOpen={jest.fn()} - onMenuClose={jest.fn()} /> ); }; @@ -190,4 +188,78 @@ describe("", () => { await act(() => promise); }); + + it("sending event from current grid sets z-index", () => { + alertStore.data.setGrids([ + { + labelName: "foo", + labelValue: "baz", + alertGroups: [], + totalGroups: 0, + stateCount: { + unprocessed: 1, + suppressed: 2, + active: 3, + }, + }, + ]); + const tree = mount( + , + { + wrappingComponent: ThemeContext.Provider, + wrappingComponentProps: { value: MockThemeContextWithoutAnimations }, + } + ); + + act(() => { + window.dispatchEvent( + new CustomEvent("gridMenuOpen", { + detail: { isOpen: true, labelValue: "baz" }, + }) + ); + }); + tree.update(); + expect(tree.find("div").at(1).props().style?.zIndex).toBe(102); + }); + + it("sending event from a different grid is ignored", () => { + alertStore.data.setGrids([ + { + labelName: "foo", + labelValue: "baz", + alertGroups: [], + totalGroups: 0, + stateCount: { + unprocessed: 1, + suppressed: 2, + active: 3, + }, + }, + ]); + const tree = mount( + , + { + wrappingComponent: ThemeContext.Provider, + wrappingComponentProps: { value: MockThemeContextWithoutAnimations }, + } + ); + + act(() => { + window.dispatchEvent( + new CustomEvent("gridMenuOpen", { + detail: { isOpen: true, labelValue: "fake" }, + }) + ); + }); + tree.update(); + expect(tree.find("div").at(1).props().style?.zIndex).toBeUndefined(); + }); }); diff --git a/ui/src/Components/Grid/AlertGrid/GridLabelSelect.tsx b/ui/src/Components/Grid/AlertGrid/GridLabelSelect.tsx index 8d4f74ac0..07862cc57 100644 --- a/ui/src/Components/Grid/AlertGrid/GridLabelSelect.tsx +++ b/ui/src/Components/Grid/AlertGrid/GridLabelSelect.tsx @@ -131,57 +131,61 @@ const GridLabelSelect: FC<{ alertStore: AlertStore; settingsStore: Settings; grid: APIGridT; - onMenuOpen: () => void; - onMenuClose: () => void; -}> = observer( - ({ alertStore, settingsStore, grid, onMenuOpen, onMenuClose }) => { - const [isVisible, setIsVisible] = useState(false); - const hide = useCallback(() => setIsVisible(false), []); - const toggle = useCallback(() => { - if (isVisible) { - onMenuClose(); - } else { - onMenuOpen(); - } - setIsVisible(!isVisible); - }, [isVisible, onMenuOpen, onMenuClose]); - const ref = useRef(null); - useOnClickOutside(ref, hide, isVisible); +}> = observer(({ alertStore, settingsStore, grid }) => { + const [isVisible, setIsVisible] = useState(false); + const hide = useCallback(() => setIsVisible(false), []); + const toggle = useCallback(() => { + if (isVisible) { + window.dispatchEvent( + new CustomEvent("gridMenuOpen", { + detail: { isOpen: false, labelValue: grid.labelValue }, + }) + ); + } else { + window.dispatchEvent( + new CustomEvent("gridMenuOpen", { + detail: { isOpen: true, labelValue: grid.labelValue }, + }) + ); + } + setIsVisible(!isVisible); + }, [isVisible, grid.labelValue]); + const ref = useRef(null); + useOnClickOutside(ref, hide, isVisible); - return ( -
- - - {({ ref }) => ( - - - + return ( +
+ + + {({ ref }) => ( + + + + )} + + + + {({ placement, ref, style }) => ( + )} - - - - {({ placement, ref, style }) => ( - - )} - - - -
- ); - } -); + + +
+
+ ); +}); export { GridLabelSelect }; diff --git a/ui/src/Components/Grid/AlertGrid/Swimlane.tsx b/ui/src/Components/Grid/AlertGrid/Swimlane.tsx index 48eb58667..a53681cf1 100644 --- a/ui/src/Components/Grid/AlertGrid/Swimlane.tsx +++ b/ui/src/Components/Grid/AlertGrid/Swimlane.tsx @@ -19,8 +19,6 @@ const Swimlane: FC<{ isExpanded: boolean; onToggle: (event: MouseEvent) => void; paddingTop: number; - onMenuOpen: () => void; - onMenuClose: () => void; }> = ({ alertStore, settingsStore, @@ -28,8 +26,6 @@ const Swimlane: FC<{ isExpanded, onToggle, paddingTop, - onMenuOpen, - onMenuClose, }) => { return (
)}