feat(ui): add a quick-access label select for multi-grid

This commit is contained in:
Łukasz Mierzwa
2021-02-25 11:00:42 +00:00
committed by Łukasz Mierzwa
parent e5b93eb8b3
commit df82e6a232
10 changed files with 257 additions and 20 deletions
+2
View File
@@ -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
@@ -108,12 +108,16 @@ describe("<AlertMenu />", () => {
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("<AlertMenu />", () => {
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);
@@ -108,12 +108,16 @@ describe("<GroupMenu />", () => {
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("<GroupMenu />", () => {
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);
@@ -127,6 +127,7 @@ const Grid: FC<{
>
<Swimlane
alertStore={alertStore}
settingsStore={settingsStore}
grid={grid}
isExpanded={isExpanded}
onToggle={onCollapseClick}
@@ -0,0 +1,89 @@
import React from "react";
import { act } from "react-dom/test-utils";
import { mount } from "enzyme";
import fetchMock from "fetch-mock";
import { Settings } from "Stores/Settings";
import { GridLabelSelect } from "./GridLabelSelect";
let settingsStore: Settings;
beforeEach(() => {
fetchMock.reset();
fetchMock.mock("*", {
body: JSON.stringify([]),
});
settingsStore = new Settings(null);
jest.useFakeTimers();
});
const MountedGridLabelSelect = () => {
return mount(<GridLabelSelect settingsStore={settingsStore} />);
};
describe("<GridLabelSelect />", () => {
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);
});
});
@@ -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<HTMLDivElement>;
popperStyle?: CSSProperties;
settingsStore: Settings;
}> = ({ popperPlacement, popperRef, popperStyle, settingsStore }) => {
return (
<div
className="dropdown-menu d-block shadow components-grid-label-select-menu border-0 p-0"
ref={popperRef}
style={{
fontSize: "1rem",
fontWeight: "normal",
...popperStyle,
}}
data-placement={popperPlacement}
>
<GridLabelName
settingsStore={settingsStore}
isOpen={true}
selectComponents={{
ClearIndicator: null,
IndicatorSeparator: null,
DropdownIndicator: null,
ValueContainer: NullContainer,
Control: NullContainer,
}}
/>
</div>
);
};
const GridLabelSelect: FC<{
settingsStore: Settings;
}> = observer(({ settingsStore }) => {
const [isVisible, setIsVisible] = useState<boolean>(false);
const hide = useCallback(() => setIsVisible(false), []);
const toggle = useCallback(() => setIsVisible(!isVisible), [isVisible]);
const ref = useRef<HTMLDivElement | null>(null);
useOnClickOutside(ref, hide, isVisible);
return (
<div ref={ref} className="components-label badge pl-1 pr-2">
<Manager>
<Reference>
{({ ref }) => (
<span
ref={ref}
onClick={toggle}
className="border-0 rounded-0 bg-inherit cursor-pointer px-1 py-0 components-grid-label-select-dropdown"
data-toggle="dropdown"
>
<FontAwesomeIcon className="text-muted" icon={faCaretDown} />
</span>
)}
</Reference>
<DropdownSlide in={isVisible} unmountOnExit>
<Popper modifiers={CommonPopperModifiers}>
{({ placement, ref, style }) => (
<Dropdown
popperPlacement={placement}
popperRef={ref}
popperStyle={style}
settingsStore={settingsStore}
/>
)}
</Popper>
</DropdownSlide>
</Manager>
</div>
);
});
export { GridLabelSelect };
+13 -2
View File
@@ -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 (
<h5 className="components-grid-swimlane d-flex flex-row justify-content-between rounded px-2 py-1 mt-2 mb-0 border border-dark">
<span className="flex-shrink-0 flex-grow-0">
@@ -23,7 +26,7 @@ const Swimlane: FC<{
<FontAwesomeIcon icon={faTh} className="text-muted" />
</span>
</span>
<span className="flex-shrink-1 flex-grow-1" style={{ minWidth: "0px" }}>
<span className="flex-shrink-1 flex-grow-0" style={{ minWidth: "0px" }}>
{grid.labelName !== "" && grid.labelValue !== "" && (
<FilteringLabel
key={grid.labelValue}
@@ -33,6 +36,14 @@ const Swimlane: FC<{
/>
)}
</span>
{grid.labelName !== "" && grid.labelValue !== "" && (
<span
className="flex-shrink-0 flex-grow-1 px-0"
style={{ minWidth: "0px" }}
>
<GridLabelSelect settingsStore={settingsStore} />
</span>
)}
<span className="flex-shrink-0 flex-grow-0 ml-2 mr-0">
<FilteringCounterBadge
name="@state"
@@ -248,7 +248,7 @@ describe("<Grid />", () => {
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("<Grid />", () => {
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("<AlertGrid />", () => {
.find("Grid")
.at(0)
.find("span.cursor-pointer")
.at(0)
.at(1)
.simulate("click", { altKey: true });
});
@@ -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<SelectComponentsConfig<OptionT, false, GroupTypeBase<OptionT>>>
| undefined;
}> = ({ settingsStore, isOpen = undefined, selectComponents = undefined }) => {
const { response } = useFetchGet<string[]>(
FormatBackendURI(`labelNames.json`)
);
@@ -51,6 +56,8 @@ const GridLabelName: FC<{
onChange={(option) => {
settingsStore.multiGridConfig.config.gridLabel = (option as OptionT).value;
}}
menuIsOpen={isOpen}
components={selectComponents}
/>
);
};
@@ -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("<History />", () => {
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("<History />", () => {
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("<HistoryMenu />", () => {
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("<HistoryMenu />", () => {
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("<HistoryMenu />", () => {
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("<HistoryMenu />", () => {
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);
});