From 45f30f6ce99ea2a68daecf2d9490969c41774b96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Sun, 11 Oct 2020 16:27:33 +0100 Subject: [PATCH] feat(ui): allow disabling animations --- ui/src/App.tsx | 2 +- .../Animations/DropdownSlide/index.tsx | 28 +++--- .../Grid/AlertGrid/AlertGroup/index.test.tsx | 33 ++++++- .../Grid/AlertGrid/AlertGroup/index.tsx | 7 +- ui/src/Components/Grid/AlertGrid/Grid.tsx | 6 +- .../Components/Grid/AlertGrid/index.test.tsx | 30 +++++- .../AnimationsConfiguration.test.tsx | 54 +++++++++++ .../Configuration/AnimationsConfiguration.tsx | 38 ++++++++ .../AnimationsConfiguration.test.tsx.snap | 23 +++++ .../__snapshots__/index.test.tsx.snap | 17 ++++ .../MainModal/Configuration/index.tsx | 2 + .../MainModalContent.test.tsx.snap | 17 ++++ ui/src/Components/Modal/index.tsx | 11 ++- ui/src/Components/Theme/index.tsx | 2 +- ui/src/Models/UI.ts | 1 + ui/src/Stores/Settings.ts | 95 ++++++++++++------- ui/src/Styles/Components/AlertGroup.scss | 4 +- ui/src/__mocks__/Defaults.ts | 3 +- ui/src/__mocks__/Theme.ts | 10 +- 19 files changed, 318 insertions(+), 65 deletions(-) create mode 100644 ui/src/Components/MainModal/Configuration/AnimationsConfiguration.test.tsx create mode 100644 ui/src/Components/MainModal/Configuration/AnimationsConfiguration.tsx create mode 100644 ui/src/Components/MainModal/Configuration/__snapshots__/AnimationsConfiguration.test.tsx.snap diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 568c8d5c8..f308d7b0b 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -115,7 +115,7 @@ const App: FunctionComponent = observer( ? ReactSelectStyles(ReactSelectColors.Dark) : ReactSelectStyles(ReactSelectColors.Light), animations: { - duration: 500, + duration: settingsStore.themeConfig.config.animations ? 500 : 0, }, }} > diff --git a/ui/src/Components/Animations/DropdownSlide/index.tsx b/ui/src/Components/Animations/DropdownSlide/index.tsx index f59afc451..7b8d40595 100644 --- a/ui/src/Components/Animations/DropdownSlide/index.tsx +++ b/ui/src/Components/Animations/DropdownSlide/index.tsx @@ -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 }) => ( - - {children} - -); +}> = ({ children, ...props }) => { + const context = React.useContext(ThemeContext); + + return ( + + {children} + + ); +}; export { DropdownSlide }; diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.tsx b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.tsx index 87887c1ad..45806d01f 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.tsx +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.tsx @@ -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( , { wrappingComponent: ThemeContext.Provider, - wrappingComponentProps: { value: MockThemeContext }, + wrappingComponentProps: { value: theme || MockThemeContext }, } ); }; @@ -102,6 +106,27 @@ describe("", () => { 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"); diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.tsx b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.tsx index eecddf402..059e5aaaf 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.tsx +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.tsx @@ -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 (
( { ); }; -const MountedGrid = () => { +const MountedGrid = (theme?: ThemeCtx) => { return mount( { />, { wrappingComponent: ThemeContext.Provider, - wrappingComponentProps: { value: MockThemeContext }, + wrappingComponentProps: { value: theme || MockThemeContext }, } ); }; @@ -183,6 +186,25 @@ const MockGroupList = (count: number, alertPerGroup: number) => { }; describe("", () => { + 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(); diff --git a/ui/src/Components/MainModal/Configuration/AnimationsConfiguration.test.tsx b/ui/src/Components/MainModal/Configuration/AnimationsConfiguration.test.tsx new file mode 100644 index 000000000..9bcb34297 --- /dev/null +++ b/ui/src/Components/MainModal/Configuration/AnimationsConfiguration.test.tsx @@ -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(); +}; + +describe("", () => { + 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); + }); +}); diff --git a/ui/src/Components/MainModal/Configuration/AnimationsConfiguration.tsx b/ui/src/Components/MainModal/Configuration/AnimationsConfiguration.tsx new file mode 100644 index 000000000..137e6b14f --- /dev/null +++ b/ui/src/Components/MainModal/Configuration/AnimationsConfiguration.tsx @@ -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 ( +
+
+ + onChange(event.target.checked)} + /> + + +
+
+ ); +}); + +export { AnimationsConfiguration }; diff --git a/ui/src/Components/MainModal/Configuration/__snapshots__/AnimationsConfiguration.test.tsx.snap b/ui/src/Components/MainModal/Configuration/__snapshots__/AnimationsConfiguration.test.tsx.snap new file mode 100644 index 000000000..51ecc1c51 --- /dev/null +++ b/ui/src/Components/MainModal/Configuration/__snapshots__/AnimationsConfiguration.test.tsx.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` matches snapshot with default values 1`] = ` +" +
+
+ + + + +
+
+" +`; diff --git a/ui/src/Components/MainModal/Configuration/__snapshots__/index.test.tsx.snap b/ui/src/Components/MainModal/Configuration/__snapshots__/index.test.tsx.snap index e828cbb84..f519420ca 100644 --- a/ui/src/Components/MainModal/Configuration/__snapshots__/index.test.tsx.snap +++ b/ui/src/Components/MainModal/Configuration/__snapshots__/index.test.tsx.snap @@ -206,6 +206,23 @@ exports[` matches snapshot 1`] = `
+
+
+ + + + +
+
diff --git a/ui/src/Components/MainModal/Configuration/index.tsx b/ui/src/Components/MainModal/Configuration/index.tsx index f9a6c2440..3b7c097d2 100644 --- a/ui/src/Components/MainModal/Configuration/index.tsx +++ b/ui/src/Components/MainModal/Configuration/index.tsx @@ -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<{ + } defaultIsOpen={defaultIsOpen} diff --git a/ui/src/Components/MainModal/__snapshots__/MainModalContent.test.tsx.snap b/ui/src/Components/MainModal/__snapshots__/MainModalContent.test.tsx.snap index c45c09139..97a0f629d 100644 --- a/ui/src/Components/MainModal/__snapshots__/MainModalContent.test.tsx.snap +++ b/ui/src/Components/MainModal/__snapshots__/MainModalContent.test.tsx.snap @@ -225,6 +225,23 @@ exports[` matches snapshot 1`] = `
+
+
+ + + + +
+
diff --git a/ui/src/Components/Modal/index.tsx b/ui/src/Components/Modal/index.tsx index 6919f5dac..236f2dcb8 100644 --- a/ui/src/Components/Modal/index.tsx +++ b/ui/src/Components/Modal/index.tsx @@ -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( { ); }; -interface ThemeCtx { +export interface ThemeCtx { isDark: boolean; reactSelectStyles: Styles; animations: { diff --git a/ui/src/Models/UI.ts b/ui/src/Models/UI.ts index 5cf8d95a3..bbc4620f2 100644 --- a/ui/src/Models/UI.ts +++ b/ui/src/Models/UI.ts @@ -7,6 +7,7 @@ export interface UIDefaults { HideFiltersWhenIdle: boolean; ColorTitlebar: boolean; Theme: ThemeT; + Animations: boolean; MinimalGroupWidth: number; AlertsPerGroup: number; CollapseGroups: CollapseGroupsT; diff --git a/ui/src/Stores/Settings.ts b/ui/src/Stores/Settings.ts index d1be33cbf..8aee43d04 100644 --- a/ui/src/Stores/Settings.ts +++ b/ui/src/Stores/Settings.ts @@ -102,15 +102,20 @@ interface SilenceFormConfigStorage { author: string; } class SilenceFormConfig { - config: SilenceFormConfigStorage = localStored( - "silenceFormConfig", - { author: "" }, - { delay: 100 } - ); + config: SilenceFormConfigStorage; + saveAuthor: (newAuthor: string) => void; - saveAuthor = action((newAuthor: string) => { - this.config.author = newAuthor; - }); + constructor() { + this.config = localStored( + "silenceFormConfig", + { author: "" }, + { delay: 100 } + ); + + this.saveAuthor = action((newAuthor: string) => { + this.config.author = newAuthor; + }); + } } export type SortOrderT = "default" | "disabled" | "startsAt" | "label"; @@ -129,6 +134,10 @@ class GridConfig { }); config: GridConfigStorage; + setSortOrder: (o: SortOrderT) => void; + setSortLabel: (l: string) => void; + setSortReverse: (v: boolean | null) => void; + constructor(groupWidth: number) { this.config = localStored( "alertGridConfig", @@ -140,17 +149,17 @@ class GridConfig { }, { delay: 100 } ); - } - setSortOrder = action((o: SortOrderT) => { - this.config.sortOrder = o; - }); - setSortLabel = action((l: string) => { - this.config.sortLabel = l; - }); - setSortReverse = action((v: boolean | null) => { - this.config.reverseSort = v; - }); + this.setSortOrder = action((o: SortOrderT) => { + this.config.sortOrder = o; + }); + this.setSortLabel = action((l: string) => { + this.config.sortLabel = l; + }); + this.setSortReverse = action((v: boolean | null) => { + this.config.reverseSort = v; + }); + } } interface FilterBarConfigStorage { @@ -158,6 +167,8 @@ interface FilterBarConfigStorage { } class FilterBarConfig { config: FilterBarConfigStorage; + setAutohide: (v: boolean) => void; + constructor(autohide: boolean) { this.config = localStored( "filterBarConfig", @@ -168,16 +179,16 @@ class FilterBarConfig { delay: 100, } ); + this.setAutohide = action((v: boolean) => { + this.config.autohide = v; + }); } - - setAutohide = action((v: boolean) => { - this.config.autohide = v; - }); } export type ThemeT = "auto" | "light" | "dark"; interface ThemeConfigStorage { theme: ThemeT; + animations: boolean; } class ThemeConfig { options = Object.freeze({ @@ -188,22 +199,29 @@ class ThemeConfig { light: { label: "Light theme", value: "light" }, dark: { label: "Dark theme", value: "dark" }, }); + config: ThemeConfigStorage; - constructor(defaultTheme: ThemeT) { + setTheme: (v: ThemeT) => void; + setAnimations: (v: boolean) => void; + + constructor(defaultTheme: ThemeT, animations: boolean) { this.config = localStored( "themeConfig", { theme: defaultTheme, + animations: animations, }, { delay: 100, } ); + this.setTheme = action((v: ThemeT) => { + this.config.theme = v; + }); + this.setAnimations = action((v: boolean) => { + this.config.animations = v; + }); } - - setTheme = action((v: ThemeT) => { - this.config.theme = v; - }); } interface MultiGridConfigStorage { @@ -212,6 +230,9 @@ interface MultiGridConfigStorage { } class MultiGridConfig { config: MultiGridConfigStorage; + setGridLabel: (l: string) => void; + setGridSortReverse: (v: boolean) => void; + constructor(gridLabel: string, gridSortReverse: boolean) { this.config = localStored( "multiGridConfig", @@ -223,14 +244,14 @@ class MultiGridConfig { delay: 100, } ); - } - setGridLabel = action((l: string) => { - this.config.gridLabel = l; - }); - setGridSortReverse = action((v: boolean) => { - this.config.gridSortReverse = v; - }); + this.setGridLabel = action((l: string) => { + this.config.gridLabel = l; + }); + this.setGridSortReverse = action((v: boolean) => { + this.config.gridSortReverse = v; + }); + } } class Settings { @@ -251,6 +272,7 @@ class Settings { HideFiltersWhenIdle: true, ColorTitlebar: false, Theme: "auto", + Animations: true, MinimalGroupWidth: 420, AlertsPerGroup: 5, CollapseGroups: "collapsedOnMobile", @@ -275,7 +297,10 @@ class Settings { this.filterBarConfig = new FilterBarConfig( defaultSettings.HideFiltersWhenIdle ); - this.themeConfig = new ThemeConfig(defaultSettings.Theme); + this.themeConfig = new ThemeConfig( + defaultSettings.Theme, + defaultSettings.Animations + ); this.multiGridConfig = new MultiGridConfig( defaultSettings.MultiGridLabel, defaultSettings.MultiGridSortReverse diff --git a/ui/src/Styles/Components/AlertGroup.scss b/ui/src/Styles/Components/AlertGroup.scss index 387fbc94b..87f449fc6 100644 --- a/ui/src/Styles/Components/AlertGroup.scss +++ b/ui/src/Styles/Components/AlertGroup.scss @@ -1,8 +1,8 @@ .components-grid-alertgrid-alertgroup { padding: 0.3rem; - &.components-animation-fade-appear-done, - &.components-animation-fade-enter-done { + &.components-animation-fade-appear-done.animate, + &.components-animation-fade-enter-done.animate { z-index: 1; transition-property: transform; transition-duration: 0.4s; diff --git a/ui/src/__mocks__/Defaults.ts b/ui/src/__mocks__/Defaults.ts index 8fa596551..cab47d51e 100644 --- a/ui/src/__mocks__/Defaults.ts +++ b/ui/src/__mocks__/Defaults.ts @@ -1,12 +1,13 @@ import { UIDefaults } from "Models/UI"; const DefaultsBase64 = - "eyJSZWZyZXNoIjo0NTAwMDAwMDAwMCwiSGlkZUZpbHRlcnNXaGVuSWRsZSI6ZmFsc2UsIkNvbG9yVGl0bGViYXIiOmZhbHNlLCJUaGVtZSI6ImxpZ2h0IiwiTWluaW1hbEdyb3VwV2lkdGgiOjU1NSwiQWxlcnRzUGVyR3JvdXAiOjE1LCJDb2xsYXBzZUdyb3VwcyI6ImV4cGFuZGVkIiwiTXVsdGlHcmlkTGFiZWwiOiIiLCJNdWx0aUdyaWRTb3J0UmV2ZXJzZSI6ZmFsc2V9Cg=="; + "eyJSZWZyZXNoIjo0NTAwMDAwMDAwMCwiSGlkZUZpbHRlcnNXaGVuSWRsZSI6ZmFsc2UsIkNvbG9yVGl0bGViYXIiOmZhbHNlLCJUaGVtZSI6ImxpZ2h0IiwiQW5pbWF0aW9ucyI6dHJ1ZSwiTWluaW1hbEdyb3VwV2lkdGgiOjU1NSwiQWxlcnRzUGVyR3JvdXAiOjE1LCJDb2xsYXBzZUdyb3VwcyI6ImV4cGFuZGVkIiwiTXVsdGlHcmlkTGFiZWwiOiIiLCJNdWx0aUdyaWRTb3J0UmV2ZXJzZSI6ZmFsc2V9Cg=="; const DefaultsObject: UIDefaults = { Refresh: 45000000000, HideFiltersWhenIdle: false, ColorTitlebar: false, Theme: "light", + Animations: true, MinimalGroupWidth: 555, AlertsPerGroup: 15, CollapseGroups: "expanded", diff --git a/ui/src/__mocks__/Theme.ts b/ui/src/__mocks__/Theme.ts index b83dec5ec..d0a44ef9d 100644 --- a/ui/src/__mocks__/Theme.ts +++ b/ui/src/__mocks__/Theme.ts @@ -11,4 +11,12 @@ const MockThemeContext = { reactSelectStyles: ReactSelectStyles(ReactSelectColors.Light), }; -export { MockThemeContext }; +const MockThemeContextWithoutAnimations = { + animations: { + duration: 0, + }, + isDark: false, + reactSelectStyles: ReactSelectStyles(ReactSelectColors.Light), +}; + +export { MockThemeContext, MockThemeContextWithoutAnimations };