From df82e6a232f0768de52ad4d5cbf97eb541f6e4c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Thu, 25 Feb 2021 10:47:38 +0000 Subject: [PATCH] feat(ui): add a quick-access label select for multi-grid --- CHANGELOG.md | 2 + .../AlertGroup/Alert/AlertMenu.test.tsx | 12 ++- .../AlertGroup/GroupHeader/GroupMenu.test.tsx | 12 ++- ui/src/Components/Grid/AlertGrid/Grid.tsx | 1 + .../Grid/AlertGrid/GridLabelSelect.test.tsx | 89 +++++++++++++++++ .../Grid/AlertGrid/GridLabelSelect.tsx | 99 +++++++++++++++++++ ui/src/Components/Grid/AlertGrid/Swimlane.tsx | 15 ++- .../Components/Grid/AlertGrid/index.test.tsx | 6 +- .../MainModal/Configuration/GridLabelName.tsx | 9 +- .../NavBar/FilterInput/History.test.tsx | 32 ++++-- 10 files changed, 257 insertions(+), 20 deletions(-) create mode 100644 ui/src/Components/Grid/AlertGrid/GridLabelSelect.test.tsx create mode 100644 ui/src/Components/Grid/AlertGrid/GridLabelSelect.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index ce6252c59..600fd554d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ### Added - Add `/robots.txt` to block search engine crawlers. +- Easily change multi-grid source label via quick access dropdown on the grid + header. ## v0.79 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 5749484ea..c629624a1 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.test.tsx +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.test.tsx @@ -108,12 +108,16 @@ describe("", () => { const toggle = tree.find("span.cursor-pointer"); toggle.simulate("click"); - jest.runOnlyPendingTimers(); + act(() => { + jest.runOnlyPendingTimers(); + }); expect(MockSetIsMenuOpen).toHaveBeenCalledTimes(1); expect(tree.find("div.dropdown-menu")).toHaveLength(1); toggle.simulate("click"); - jest.runOnlyPendingTimers(); + act(() => { + jest.runOnlyPendingTimers(); + }); tree.update(); expect(MockSetIsMenuOpen).toHaveBeenCalledTimes(2); expect(tree.find("div.dropdown-menu")).toHaveLength(0); @@ -130,7 +134,9 @@ describe("", () => { expect(tree.find("div.dropdown-menu")).toHaveLength(1); tree.find("a.dropdown-item").at(0).simulate("click"); - jest.runOnlyPendingTimers(); + act(() => { + jest.runOnlyPendingTimers(); + }); tree.update(); expect(MockSetIsMenuOpen).toHaveBeenCalledTimes(2); expect(tree.find("div.dropdown-menu")).toHaveLength(0); diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.test.tsx b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.test.tsx index 63353cee7..0b24e02f2 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.test.tsx +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.test.tsx @@ -108,12 +108,16 @@ describe("", () => { const toggle = tree.find("span.cursor-pointer"); toggle.simulate("click"); - jest.runOnlyPendingTimers(); + act(() => { + jest.runOnlyPendingTimers(); + }); expect(MockSetIsMenuOpen).toHaveBeenCalledTimes(1); expect(tree.find("div.dropdown-menu")).toHaveLength(1); toggle.simulate("click"); - jest.runOnlyPendingTimers(); + act(() => { + jest.runOnlyPendingTimers(); + }); tree.update(); expect(MockSetIsMenuOpen).toHaveBeenCalledTimes(2); expect(tree.find("div.dropdown-menu")).toHaveLength(0); @@ -131,7 +135,9 @@ describe("", () => { expect(tree.find("div.dropdown-menu")).toHaveLength(1); tree.find("div.dropdown-item").at(0).simulate("click"); - jest.runOnlyPendingTimers(); + act(() => { + jest.runOnlyPendingTimers(); + }); tree.update(); expect(MockSetIsMenuOpen).toHaveBeenCalledTimes(2); expect(tree.find("div.dropdown-menu")).toHaveLength(0); diff --git a/ui/src/Components/Grid/AlertGrid/Grid.tsx b/ui/src/Components/Grid/AlertGrid/Grid.tsx index f5f0588b7..2df038011 100644 --- a/ui/src/Components/Grid/AlertGrid/Grid.tsx +++ b/ui/src/Components/Grid/AlertGrid/Grid.tsx @@ -127,6 +127,7 @@ const Grid: FC<{ > { + fetchMock.reset(); + fetchMock.mock("*", { + body: JSON.stringify([]), + }); + + settingsStore = new Settings(null); + + jest.useFakeTimers(); +}); + +const MountedGridLabelSelect = () => { + return mount(); +}; + +describe("", () => { + it("select dropdown is hidden by default", async () => { + const promise = Promise.resolve(); + const tree = MountedGridLabelSelect(); + expect(tree.find("div.components-grid-label-select-menu")).toHaveLength(0); + await act(() => promise); + }); + + it("clicking toggle renders select dropdown", async () => { + const promise = Promise.resolve(); + const tree = MountedGridLabelSelect(); + const toggle = tree.find("span.components-grid-label-select-dropdown"); + toggle.simulate("click"); + expect(tree.find("div.components-grid-label-select-menu")).toHaveLength(1); + await act(() => promise); + }); + + it("clicking toggle twice hides select dropdown", async () => { + const promise = Promise.resolve(); + const tree = MountedGridLabelSelect(); + const toggle = tree.find("span.components-grid-label-select-dropdown"); + + toggle.simulate("click"); + act(() => { + jest.runOnlyPendingTimers(); + }); + expect(tree.find("div.components-grid-label-select-menu")).toHaveLength(1); + + toggle.simulate("click"); + act(() => { + jest.runOnlyPendingTimers(); + }); + tree.update(); + expect(tree.find("div.components-grid-label-select-menu")).toHaveLength(0); + await act(() => promise); + }); + + it("clicking outside hides select dropdown", async () => { + const promise = Promise.resolve(); + const tree = MountedGridLabelSelect(); + const toggle = tree.find("span.components-grid-label-select-dropdown"); + + toggle.simulate("click"); + act(() => { + jest.runOnlyPendingTimers(); + }); + expect(tree.find("div.components-grid-label-select-menu")).toHaveLength(1); + + const clickEvent = document.createEvent("MouseEvents"); + clickEvent.initEvent("mousedown", true, true); + act(() => { + document.dispatchEvent(clickEvent); + }); + + act(() => { + jest.runOnlyPendingTimers(); + }); + tree.update(); + expect(tree.find("div.components-grid-label-select-menu")).toHaveLength(0); + await act(() => promise); + }); +}); diff --git a/ui/src/Components/Grid/AlertGrid/GridLabelSelect.tsx b/ui/src/Components/Grid/AlertGrid/GridLabelSelect.tsx new file mode 100644 index 000000000..6be20491e --- /dev/null +++ b/ui/src/Components/Grid/AlertGrid/GridLabelSelect.tsx @@ -0,0 +1,99 @@ +import React, { + FC, + Ref, + CSSProperties, + useRef, + useState, + useCallback, +} from "react"; + +import { observer } from "mobx-react-lite"; + +import { Manager, Reference, Popper } from "react-popper"; + +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faCaretDown } from "@fortawesome/free-solid-svg-icons/faCaretDown"; + +import { Settings } from "Stores/Settings"; +import { CommonPopperModifiers } from "Common/Popper"; +import { DropdownSlide } from "Components/Animations/DropdownSlide"; +import { useOnClickOutside } from "Hooks/useOnClickOutside"; +import { GridLabelName } from "Components/MainModal/Configuration/GridLabelName"; + +const NullContainer: FC = () => null; + +const Dropdown: FC<{ + popperPlacement?: string; + popperRef?: Ref; + popperStyle?: CSSProperties; + settingsStore: Settings; +}> = ({ popperPlacement, popperRef, popperStyle, settingsStore }) => { + return ( +
+ +
+ ); +}; + +const GridLabelSelect: FC<{ + settingsStore: Settings; +}> = observer(({ settingsStore }) => { + const [isVisible, setIsVisible] = useState(false); + const hide = useCallback(() => setIsVisible(false), []); + const toggle = useCallback(() => setIsVisible(!isVisible), [isVisible]); + + const ref = useRef(null); + useOnClickOutside(ref, hide, isVisible); + + return ( +
+ + + {({ ref }) => ( + + + + )} + + + + {({ placement, ref, style }) => ( + + )} + + + +
+ ); +}); + +export { GridLabelSelect }; diff --git a/ui/src/Components/Grid/AlertGrid/Swimlane.tsx b/ui/src/Components/Grid/AlertGrid/Swimlane.tsx index cd1990198..22a0d686b 100644 --- a/ui/src/Components/Grid/AlertGrid/Swimlane.tsx +++ b/ui/src/Components/Grid/AlertGrid/Swimlane.tsx @@ -4,18 +4,21 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faTh } from "@fortawesome/free-solid-svg-icons/faTh"; import { AlertStore } from "Stores/AlertStore"; +import { Settings } from "Stores/Settings"; import { APIGridT } from "Models/APITypes"; import FilteringLabel from "Components/Labels/FilteringLabel"; import FilteringCounterBadge from "Components/Labels/FilteringCounterBadge"; import { TooltipWrapper } from "Components/TooltipWrapper"; import { ToggleIcon } from "Components/ToggleIcon"; +import { GridLabelSelect } from "./GridLabelSelect"; const Swimlane: FC<{ alertStore: AlertStore; + settingsStore: Settings; grid: APIGridT; isExpanded: boolean; onToggle: (event: MouseEvent) => void; -}> = ({ alertStore, grid, isExpanded, onToggle }) => { +}> = ({ alertStore, settingsStore, grid, isExpanded, onToggle }) => { return (
@@ -23,7 +26,7 @@ const Swimlane: FC<{ - + {grid.labelName !== "" && grid.labelValue !== "" && ( )} + {grid.labelName !== "" && grid.labelValue !== "" && ( + + + + )} ", () => { tree.setProps({ grid: grid }); expect(tree.find("AlertGroup")).toHaveLength(10); - tree.find("span.cursor-pointer").at(0).simulate("click"); + tree.find("span.cursor-pointer").at(1).simulate("click"); act(() => { jest.runOnlyPendingTimers(); }); @@ -257,7 +257,7 @@ describe("", () => { 0 ); - tree.find("span.cursor-pointer").at(0).simulate("click"); + tree.find("span.cursor-pointer").at(1).simulate("click"); act(() => { jest.runOnlyPendingTimers(); }); @@ -625,7 +625,7 @@ describe("", () => { .find("Grid") .at(0) .find("span.cursor-pointer") - .at(0) + .at(1) .simulate("click", { altKey: true }); }); diff --git a/ui/src/Components/MainModal/Configuration/GridLabelName.tsx b/ui/src/Components/MainModal/Configuration/GridLabelName.tsx index 13068d55d..9939b09dc 100644 --- a/ui/src/Components/MainModal/Configuration/GridLabelName.tsx +++ b/ui/src/Components/MainModal/Configuration/GridLabelName.tsx @@ -1,6 +1,7 @@ import React, { FC } from "react"; import Creatable from "react-select/creatable"; +import { GroupTypeBase, SelectComponentsConfig } from "react-select"; import { useFetchGet } from "Hooks/useFetchGet"; import { FormatBackendURI } from "Stores/AlertStore"; @@ -24,7 +25,11 @@ const staticValues = [ const GridLabelName: FC<{ settingsStore: Settings; -}> = ({ settingsStore }) => { + isOpen?: boolean | undefined; + selectComponents?: + | Partial>> + | undefined; +}> = ({ settingsStore, isOpen = undefined, selectComponents = undefined }) => { const { response } = useFetchGet( FormatBackendURI(`labelNames.json`) ); @@ -51,6 +56,8 @@ const GridLabelName: FC<{ onChange={(option) => { settingsStore.multiGridConfig.config.gridLabel = (option as OptionT).value; }} + menuIsOpen={isOpen} + components={selectComponents} /> ); }; diff --git a/ui/src/Components/NavBar/FilterInput/History.test.tsx b/ui/src/Components/NavBar/FilterInput/History.test.tsx index beb031faa..93ddb6ac1 100644 --- a/ui/src/Components/NavBar/FilterInput/History.test.tsx +++ b/ui/src/Components/NavBar/FilterInput/History.test.tsx @@ -47,7 +47,9 @@ const PopulateHistory = (tree: ReactWrapper, count: number) => { AppliedFilter("baz", "=~", `bar${i}`), ]); tree.update(); - jest.runOnlyPendingTimers(); + act(() => { + jest.runOnlyPendingTimers(); + }); } }; @@ -74,11 +76,15 @@ describe("", () => { const toggle = tree.find("button.cursor-pointer"); toggle.simulate("click"); - jest.runOnlyPendingTimers(); + act(() => { + jest.runOnlyPendingTimers(); + }); expect(tree.find("div.dropdown-menu")).toHaveLength(1); toggle.simulate("click"); - jest.runOnlyPendingTimers(); + act(() => { + jest.runOnlyPendingTimers(); + }); tree.update(); expect(tree.find("div.dropdown-menu")).toHaveLength(0); await act(() => promise); @@ -93,7 +99,9 @@ describe("", () => { expect(tree.find("div.dropdown-menu")).toHaveLength(1); tree.find(".component-history-button").at(0).simulate("click"); - jest.runOnlyPendingTimers(); + act(() => { + jest.runOnlyPendingTimers(); + }); tree.update(); expect(tree.find("div.dropdown-menu")).toHaveLength(0); await act(() => promise); @@ -158,7 +166,9 @@ describe("", () => { expect(alertStore.filters.values).toHaveLength(1); button.simulate("click"); - jest.runOnlyPendingTimers(); + act(() => { + jest.runOnlyPendingTimers(); + }); expect(alertStore.filters.values).toHaveLength(2); expect(alertStore.filters.values[0]).toMatchObject({ raw: "foo=bar1" }); expect(alertStore.filters.values[1]).toMatchObject({ raw: "baz=~bar1" }); @@ -229,7 +239,9 @@ describe("", () => { expect(button.text()).toBe("Save filters"); button.simulate("click"); - jest.runOnlyPendingTimers(); + act(() => { + jest.runOnlyPendingTimers(); + }); expect(settingsStore.savedFilters.config.filters).toHaveLength(2); expect(settingsStore.savedFilters.config.filters).toContain("foo=bar"); expect(settingsStore.savedFilters.config.filters).toContain("bar=~baz"); @@ -243,7 +255,9 @@ describe("", () => { const button = tree.find(".component-history-button").at(1); expect(button.text()).toBe("Reset filters"); button.simulate("click"); - jest.runOnlyPendingTimers(); + act(() => { + jest.runOnlyPendingTimers(); + }); expect(settingsStore.savedFilters.config.filters).toHaveLength(0); }); @@ -256,7 +270,9 @@ describe("", () => { const button = tree.find(".component-history-button").at(2); expect(button.text()).toBe("Clear history"); button.simulate("click"); - jest.runOnlyPendingTimers(); + act(() => { + jest.runOnlyPendingTimers(); + }); tree.update(); expect(tree.find("button.dropdown-item")).toHaveLength(0); });