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);
});