feat(ui): allow disabling animations

This commit is contained in:
Łukasz Mierzwa
2020-10-11 16:27:33 +01:00
committed by Łukasz Mierzwa
parent d6029506d5
commit 45f30f6ce9
19 changed files with 318 additions and 65 deletions

View File

@@ -2,20 +2,26 @@ import React, { FC, ReactNode } from "react";
import { CSSTransition } from "react-transition-group";
import { ThemeContext } from "Components/Theme";
const DropdownSlide: FC<{
children: ReactNode;
in?: boolean;
unmountOnExit?: boolean;
}> = ({ children, ...props }) => (
<CSSTransition
classNames="components-animation-slide"
timeout={150}
appear={true}
exit={true}
{...props}
>
{children}
</CSSTransition>
);
}> = ({ children, ...props }) => {
const context = React.useContext(ThemeContext);
return (
<CSSTransition
classNames="components-animation-slide"
timeout={context.animations.duration ? 150 : 0}
appear={true}
exit={true}
{...props}
>
{children}
</CSSTransition>
);
};
export { DropdownSlide };

View File

@@ -4,12 +4,15 @@ import { act } from "react-dom/test-utils";
import { mount } from "enzyme";
import { MockAlert, MockAlertGroup } from "__mocks__/Alerts";
import { MockThemeContext } from "__mocks__/Theme";
import {
MockThemeContext,
MockThemeContextWithoutAnimations,
} from "__mocks__/Theme";
import { APIAlertGroupT } from "Models/APITypes";
import { AlertStore } from "Stores/AlertStore";
import { Settings, CollapseStateT } from "Stores/Settings";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { ThemeContext } from "Components/Theme";
import { ThemeContext, ThemeCtx } from "Components/Theme";
import AlertGroup from ".";
let alertStore: AlertStore;
@@ -60,7 +63,8 @@ const MockAlerts = (alertCount: number) => {
const MountedAlertGroup = (
afterUpdate: () => void,
showAlertmanagers: boolean,
initialAlertsToRender?: number
initialAlertsToRender?: number,
theme?: ThemeCtx
) => {
return mount(
<AlertGroup
@@ -76,7 +80,7 @@ const MountedAlertGroup = (
/>,
{
wrappingComponent: ThemeContext.Provider,
wrappingComponentProps: { value: MockThemeContext },
wrappingComponentProps: { value: theme || MockThemeContext },
}
);
};
@@ -102,6 +106,27 @@ describe("<AlertGroup />", () => {
tree.unmount();
});
it("uses 'animate' class when settingsStore.themeConfig.config.animations is true", () => {
MockAlerts(5);
const tree = MountedAlertGroup(jest.fn(), true, 5, MockThemeContext);
expect(
tree.find("div.components-grid-alertgrid-alertgroup").hasClass("animate")
).toBe(true);
});
it("doesn't use 'animate' class when settingsStore.themeConfig.config.animations is false", () => {
MockAlerts(5);
const tree = MountedAlertGroup(
jest.fn(),
true,
5,
MockThemeContextWithoutAnimations
);
expect(
tree.find("div.components-grid-alertgrid-alertgroup").hasClass("animate")
).toBe(false);
});
it("renders Alertmanager cluster labels in footer if showAlertmanagersInFooter=true", () => {
MockAlerts(2);
const tree = MountedAlertGroup(jest.fn(), true).find("AlertGroup");

View File

@@ -13,6 +13,7 @@ import { AlertStore } from "Stores/AlertStore";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { BackgroundClassMap } from "Common/Colors";
import { TooltipWrapper } from "Components/TooltipWrapper";
import { ThemeContext } from "Components/Theme";
import GroupHeader from "./GroupHeader";
import Alert from "./Alert";
import GroupFooter from "./GroupFooter";
@@ -168,9 +169,13 @@ const AlertGroup: FC<{
}
}
const context = React.useContext(ThemeContext);
return (
<div
className="components-grid-alertgrid-alertgroup"
className={`components-grid-alertgrid-alertgroup ${
context.animations.duration ? "animate" : ""
}`}
style={{
width: groupWidth,
zIndex: isMenuOpen ? 100 : undefined,

View File

@@ -139,7 +139,11 @@ const Grid: FC<{
? grid.alertGroups.slice(0, groupsToRender).map((group) => (
<CSSTransition
key={group.id}
classNames="components-animation-fade"
classNames={
context.animations.duration
? "components-animation-fade"
: ""
}
timeout={context.animations.duration}
onEntering={repack}
onExited={debouncedRepack}

View File

@@ -6,12 +6,15 @@ import { shallow, mount } from "enzyme";
import { advanceBy, clear } from "jest-date-mock";
import { MockAlert, MockAlertGroup } from "__mocks__/Alerts";
import { MockThemeContext } from "__mocks__/Theme";
import {
MockThemeContext,
MockThemeContextWithoutAnimations,
} from "__mocks__/Theme";
import { mockMatchMedia } from "__mocks__/matchMedia";
import { AlertStore } from "Stores/AlertStore";
import { Settings } from "Stores/Settings";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { ThemeContext } from "Components/Theme";
import { ThemeContext, ThemeCtx } from "Components/Theme";
import { GetGridElementWidth, GridSizesConfig } from "./GridSize";
import Grid from "./Grid";
import AlertGrid from ".";
@@ -109,7 +112,7 @@ const ShallowGrid = () => {
);
};
const MountedGrid = () => {
const MountedGrid = (theme?: ThemeCtx) => {
return mount(
<Grid
alertStore={alertStore}
@@ -122,7 +125,7 @@ const MountedGrid = () => {
/>,
{
wrappingComponent: ThemeContext.Provider,
wrappingComponentProps: { value: MockThemeContext },
wrappingComponentProps: { value: theme || MockThemeContext },
}
);
};
@@ -183,6 +186,25 @@ const MockGroupList = (count: number, alertPerGroup: number) => {
};
describe("<Grid />", () => {
it("uses animations when settingsStore.themeConfig.config.animations is true", () => {
MockGroupList(1, 1);
const tree = MountedGrid(MockThemeContext);
expect(
tree.find("div.components-grid-alertgrid-alertgroup").html()
).toMatch(/animate components-animation-fade-appear/);
});
it("doesn't use animations when settingsStore.themeConfig.config.animations is false", () => {
jest
.spyOn(React, "useContext")
.mockImplementation(() => MockThemeContextWithoutAnimations);
MockGroupList(1, 1);
const tree = MountedGrid(MockThemeContextWithoutAnimations);
expect(
tree.find("div.components-grid-alertgrid-alertgroup").html()
).not.toMatch(/animate components-animation-fade-appear/);
});
it("renders only first 50 alert groups", () => {
MockGroupList(55, 5);
const tree = MountedGrid();

View File

@@ -0,0 +1,54 @@
import React from "react";
import { mount } from "enzyme";
import toDiffableHtml from "diffable-html";
import { Settings } from "Stores/Settings";
import { AnimationsConfiguration } from "./AnimationsConfiguration";
let settingsStore: Settings;
beforeEach(() => {
settingsStore = new Settings(null);
});
const FakeConfiguration = () => {
return mount(<AnimationsConfiguration settingsStore={settingsStore} />);
};
describe("<AnimationsConfiguration />", () => {
it("matches snapshot with default values", () => {
const tree = FakeConfiguration();
expect(toDiffableHtml(tree.html())).toMatchSnapshot();
});
it("animations is 'true' by default", () => {
expect(settingsStore.themeConfig.config.animations).toBe(true);
});
it("unchecking the checkbox sets stored animations value to 'false'", (done) => {
const tree = FakeConfiguration();
const checkbox = tree.find("#configuration-animations");
settingsStore.themeConfig.setAnimations(true);
expect(settingsStore.themeConfig.config.animations).toBe(true);
checkbox.simulate("change", { target: { checked: false } });
setTimeout(() => {
expect(settingsStore.themeConfig.config.animations).toBe(false);
done();
}, 200);
});
it("checking the checkbox sets stored animations value to 'true'", (done) => {
const tree = FakeConfiguration();
const checkbox = tree.find("#configuration-animations");
settingsStore.themeConfig.setAnimations(false);
expect(settingsStore.themeConfig.config.animations).toBe(false);
checkbox.simulate("change", { target: { checked: true } });
setTimeout(() => {
expect(settingsStore.themeConfig.config.animations).toBe(true);
done();
}, 200);
});
});

View File

@@ -0,0 +1,38 @@
import React, { FC } from "react";
import { observer } from "mobx-react-lite";
import { Settings } from "Stores/Settings";
const AnimationsConfiguration: FC<{
settingsStore: Settings;
}> = observer(({ settingsStore }) => {
const onChange = (value: boolean) => {
settingsStore.themeConfig.setAnimations(value);
};
return (
<div className="form-group mb-0">
<div className="form-check form-check-inline">
<span className="custom-control custom-switch">
<input
id="configuration-animations"
className="custom-control-input"
type="checkbox"
value=""
checked={settingsStore.themeConfig.config.animations || false}
onChange={(event) => onChange(event.target.checked)}
/>
<label
className="custom-control-label cursor-pointer mr-3"
htmlFor="configuration-animations"
>
Enable animations
</label>
</span>
</div>
</div>
);
});
export { AnimationsConfiguration };

View File

@@ -0,0 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<AnimationsConfiguration /> matches snapshot with default values 1`] = `
"
<div class=\\"form-group mb-0\\">
<div class=\\"form-check form-check-inline\\">
<span class=\\"custom-control custom-switch\\">
<input id=\\"configuration-animations\\"
class=\\"custom-control-input\\"
type=\\"checkbox\\"
value
checked
>
<label class=\\"custom-control-label cursor-pointer mr-3\\"
for=\\"configuration-animations\\"
>
Enable animations
</label>
</span>
</div>
</div>
"
`;

View File

@@ -206,6 +206,23 @@ exports[`<Configuration /> matches snapshot 1`] = `
</span>
</div>
</div>
<div class=\\"form-group mb-0\\">
<div class=\\"form-check form-check-inline\\">
<span class=\\"custom-control custom-switch\\">
<input id=\\"configuration-animations\\"
class=\\"custom-control-input\\"
type=\\"checkbox\\"
value
checked
>
<label class=\\"custom-control-label cursor-pointer mr-3\\"
for=\\"configuration-animations\\"
>
Enable animations
</label>
</span>
</div>
</div>
</div>
</div>
<div class=\\"accordion card\\">

View File

@@ -11,6 +11,7 @@ import { AlertGroupCollapseConfiguration } from "./AlertGroupCollapseConfigurati
import { AlertGroupTitleBarColor } from "./AlertGroupTitleBarColor";
import { ThemeConfiguration } from "./ThemeConfiguration";
import { MultiGridConfiguration } from "./MultiGridConfiguration";
import { AnimationsConfiguration } from "./AnimationsConfiguration";
const Configuration: FC<{
settingsStore: Settings;
@@ -33,6 +34,7 @@ const Configuration: FC<{
<React.Fragment>
<ThemeConfiguration settingsStore={settingsStore} />
<AlertGroupTitleBarColor settingsStore={settingsStore} />
<AnimationsConfiguration settingsStore={settingsStore} />
</React.Fragment>
}
defaultIsOpen={defaultIsOpen}

View File

@@ -225,6 +225,23 @@ exports[`<MainModalContent /> matches snapshot 1`] = `
</span>
</div>
</div>
<div class=\\"form-group mb-0\\">
<div class=\\"form-check form-check-inline\\">
<span class=\\"custom-control custom-switch\\">
<input id=\\"configuration-animations\\"
class=\\"custom-control-input\\"
type=\\"checkbox\\"
value
checked
>
<label class=\\"custom-control-label cursor-pointer mr-3\\"
for=\\"configuration-animations\\"
>
Enable animations
</label>
</span>
</div>
</div>
</div>
</div>
<div class=\\"accordion card\\">

View File

@@ -7,6 +7,8 @@ import { disableBodyScroll, enableBodyScroll } from "body-scroll-lock";
import { useHotkeys } from "react-hotkeys-hook";
import { ThemeContext } from "Components/Theme";
const ModalInner: FC<{
size: "lg" | "xl";
isUpper: boolean;
@@ -60,12 +62,15 @@ const Modal: FC<{
onExited,
children,
}) => {
const context = React.useContext(ThemeContext);
return ReactDOM.createPortal(
<React.Fragment>
<CSSTransition
in={isOpen}
classNames="components-animation-modal"
timeout={300}
classNames={
context.animations.duration ? "components-animation-modal" : ""
}
timeout={context.animations.duration ? 300 : 0}
onExited={onExited}
enter
exit
@@ -78,7 +83,7 @@ const Modal: FC<{
<CSSTransition
in={isOpen && !isUpper}
classNames="components-animation-backdrop"
timeout={300}
timeout={context.animations.duration ? 300 : 0}
enter
exit
unmountOnExit

View File

@@ -40,7 +40,7 @@ const Placeholder = () => {
);
};
interface ThemeCtx {
export interface ThemeCtx {
isDark: boolean;
reactSelectStyles: Styles;
animations: {