mirror of
https://github.com/prymitive/karma
synced 2026-05-07 03:26:52 +00:00
fix(ui): rewrite alert group menu components with hooks
This commit is contained in:
committed by
Łukasz Mierzwa
parent
d15a0f1ff1
commit
84a32a07cf
@@ -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 (
|
||||
<Manager>
|
||||
<Reference>
|
||||
{({ ref }) => (
|
||||
<span
|
||||
className={`components-label components-label-with-hover px-1 mr-1 badge badge-secondary cursor-pointer ${uniqueClass}`}
|
||||
ref={ref}
|
||||
onClick={this.collapse.toggle}
|
||||
data-toggle="dropdown"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className="pr-1"
|
||||
style={{ width: "0.8rem" }}
|
||||
icon={faCaretDown}
|
||||
/>
|
||||
<Moment fromNow>{alert.startsAt}</Moment>
|
||||
</span>
|
||||
)}
|
||||
</Reference>
|
||||
<DropdownSlide in={!this.collapse.value} unmountOnExit>
|
||||
<Popper
|
||||
placement="bottom-start"
|
||||
modifiers={[
|
||||
{ name: "arrow", enabled: false },
|
||||
{ name: "offset", options: { offset: "-5px, 0px" } },
|
||||
]}
|
||||
>
|
||||
{({ placement, ref, style }) => (
|
||||
<MenuContent
|
||||
popperPlacement={placement}
|
||||
popperRef={ref}
|
||||
popperStyle={style}
|
||||
group={group}
|
||||
alert={alert}
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
afterClick={this.collapse.hide}
|
||||
handleClickOutside={this.collapse.hide}
|
||||
outsideClickIgnoreClass={uniqueClass}
|
||||
/>
|
||||
)}
|
||||
</Popper>
|
||||
</DropdownSlide>
|
||||
</Manager>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
return useObserver(() => (
|
||||
<Manager>
|
||||
<Reference>
|
||||
{({ ref }) => (
|
||||
<span
|
||||
className={`components-label components-label-with-hover px-1 mr-1 badge badge-secondary cursor-pointer ${uniqueClass}`}
|
||||
ref={ref}
|
||||
onClick={collapse.toggle}
|
||||
data-toggle="dropdown"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className="pr-1"
|
||||
style={{ width: "0.8rem" }}
|
||||
icon={faCaretDown}
|
||||
/>
|
||||
<Moment fromNow>{alert.startsAt}</Moment>
|
||||
</span>
|
||||
)}
|
||||
</Reference>
|
||||
<DropdownSlide in={!collapse.value} unmountOnExit>
|
||||
<Popper
|
||||
placement="bottom-start"
|
||||
modifiers={[
|
||||
{ name: "arrow", enabled: false },
|
||||
{ name: "offset", options: { offset: "-5px, 0px" } },
|
||||
]}
|
||||
>
|
||||
{({ placement, ref, style }) => (
|
||||
<MenuContent
|
||||
popperPlacement={placement}
|
||||
popperRef={ref}
|
||||
popperStyle={style}
|
||||
group={group}
|
||||
alert={alert}
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
afterClick={collapse.hide}
|
||||
handleClickOutside={collapse.hide}
|
||||
outsideClickIgnoreClass={uniqueClass}
|
||||
/>
|
||||
)}
|
||||
</Popper>
|
||||
</DropdownSlide>
|
||||
</Manager>
|
||||
));
|
||||
};
|
||||
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 };
|
||||
|
||||
@@ -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(
|
||||
<AlertMenu
|
||||
@@ -54,30 +62,54 @@ const MountedAlertMenu = (group) => {
|
||||
};
|
||||
|
||||
describe("<AlertMenu />", () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
<Manager>
|
||||
<Reference>
|
||||
{({ ref }) => (
|
||||
<span
|
||||
ref={ref}
|
||||
onClick={this.collapse.toggle}
|
||||
className={`${
|
||||
themed ? "text-white" : "text-muted"
|
||||
} cursor-pointer badge pl-0 pr-3 pr-sm-2 components-label mr-0 components-grid-alertgroup-${
|
||||
group.id
|
||||
}`}
|
||||
data-toggle="dropdown"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</span>
|
||||
)}
|
||||
</Reference>
|
||||
<DropdownSlide in={!this.collapse.value} unmountOnExit>
|
||||
<Popper
|
||||
placement="bottom-start"
|
||||
modifiers={[
|
||||
{ name: "arrow", enabled: false },
|
||||
{ name: "offset", options: { offset: "-5px, 0px" } },
|
||||
]}
|
||||
>
|
||||
{({ placement, ref, style }) => (
|
||||
<MenuContent
|
||||
popperPlacement={placement}
|
||||
popperRef={ref}
|
||||
popperStyle={style}
|
||||
group={group}
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
afterClick={this.collapse.hide}
|
||||
handleClickOutside={this.collapse.hide}
|
||||
outsideClickIgnoreClass={`components-grid-alertgroup-${group.id}`}
|
||||
/>
|
||||
)}
|
||||
</Popper>
|
||||
</DropdownSlide>
|
||||
</Manager>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
return useObserver(() => (
|
||||
<Manager>
|
||||
<Reference>
|
||||
{({ ref }) => (
|
||||
<span
|
||||
ref={ref}
|
||||
onClick={collapse.toggle}
|
||||
className={`${
|
||||
themed ? "text-white" : "text-muted"
|
||||
} cursor-pointer badge pl-0 pr-3 pr-sm-2 components-label mr-0 components-grid-alertgroup-${
|
||||
group.id
|
||||
}`}
|
||||
data-toggle="dropdown"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</span>
|
||||
)}
|
||||
</Reference>
|
||||
<DropdownSlide in={!collapse.isHidden} unmountOnExit>
|
||||
<Popper
|
||||
placement="bottom-start"
|
||||
modifiers={[
|
||||
{ name: "arrow", enabled: false },
|
||||
{ name: "offset", options: { offset: "-5px, 0px" } },
|
||||
]}
|
||||
>
|
||||
{({ placement, ref, style }) => (
|
||||
<MenuContent
|
||||
popperPlacement={placement}
|
||||
popperRef={ref}
|
||||
popperStyle={style}
|
||||
group={group}
|
||||
alertStore={alertStore}
|
||||
silenceFormStore={silenceFormStore}
|
||||
afterClick={collapse.hide}
|
||||
handleClickOutside={collapse.hide}
|
||||
outsideClickIgnoreClass={`components-grid-alertgroup-${group.id}`}
|
||||
/>
|
||||
)}
|
||||
</Popper>
|
||||
</DropdownSlide>
|
||||
</Manager>
|
||||
));
|
||||
};
|
||||
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 };
|
||||
|
||||
@@ -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(
|
||||
<GroupMenu
|
||||
@@ -48,38 +56,62 @@ const MountedGroupMenu = (group, themed) => {
|
||||
themed={themed}
|
||||
setIsMenuOpen={MockSetIsMenuOpen}
|
||||
/>
|
||||
).find("GroupMenu");
|
||||
);
|
||||
};
|
||||
|
||||
describe("<GroupMenu />", () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user