From 2f74561809e2faafd4f648daa08eea87adc5ac36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Thu, 18 Jun 2020 21:45:10 +0100 Subject: [PATCH] chore(ui): drop react-reveal animations This library is not maintained anymore and it causes small glitches when rendering a lot of alerts - scrolling groups into view doesn't always trigger animation and they stay hidden. --- ui/.storybook/config.js | 6 +- ui/package-lock.json | 26 ++- ui/package.json | 3 +- ui/src/App.tsx | 3 +- .../__snapshots__/index.test.js.snap | 6 +- ui/src/Components/CenteredMessage/index.js | 23 +-- .../Alert/__snapshots__/index.test.js.snap | 4 +- .../AlertGrid/AlertGroup/Annotation/index.js | 66 ++++---- .../__snapshots__/index.test.js.snap | 8 +- .../Grid/AlertGrid/AlertGroup/index.js | 151 ++++++++---------- .../Grid/AlertGrid/AlertGroup/index.test.js | 18 --- ui/src/Components/Grid/AlertGrid/Grid.js | 107 +++++++------ ui/src/Components/Grid/AlertGrid/Swimlane.js | 95 +++++------ .../Components/Grid/AlertGrid/index.test.js | 16 +- .../__snapshots__/index.test.js.snap | 28 ++-- .../__snapshots__/index.test.js.snap | 2 +- .../__snapshots__/index.test.js.snap | 2 +- .../__snapshots__/index.test.js.snap | 2 +- .../Labels/FilteringCounterBadge/index.js | 10 +- .../FilteringCounterBadge/index.test.js | 3 +- .../ManagedSilence/SilenceDetails.js | 10 +- .../__snapshots__/SilenceComment.test.js.snap | 16 +- .../__snapshots__/index.test.js.snap | 20 +-- ui/src/Components/ManagedSilence/index.js | 50 +++--- ui/src/Components/NavBar/index.js | 14 +- ui/src/Components/OverviewModal/index.js | 10 +- .../Components/SilenceModal/Browser/index.js | 13 +- ui/src/Components/SilenceModal/SilenceForm.js | 13 +- ui/src/Hooks/useFlashTransition.js | 40 +++++ ui/src/Hooks/useFlashTransition.test.js | 97 +++++++++++ ui/src/Styles/Components/AlertGroup.scss | 16 +- ui/src/Styles/Components/Fade.scss | 19 +++ ui/src/Styles/Components/Flash.scss | 26 +++ ui/src/Styles/Components/NavBarSlide.scss | 23 +++ ui/src/Styles/DarkTheme.scss | 3 + ui/src/Styles/LightTheme.scss | 3 + ui/src/__mocks__/Theme.js | 1 - .../__mocks__/react-intersection-observer.js | 15 ++ ui/src/index.tsx | 3 + ui/src/setupTests.js | 5 +- 40 files changed, 596 insertions(+), 380 deletions(-) create mode 100644 ui/src/Hooks/useFlashTransition.js create mode 100644 ui/src/Hooks/useFlashTransition.test.js create mode 100644 ui/src/Styles/Components/Fade.scss create mode 100644 ui/src/Styles/Components/Flash.scss create mode 100644 ui/src/Styles/Components/NavBarSlide.scss create mode 100644 ui/src/__mocks__/react-intersection-observer.js diff --git a/ui/.storybook/config.js b/ui/.storybook/config.js index 41c402b9d..0993ce46e 100644 --- a/ui/.storybook/config.js +++ b/ui/.storybook/config.js @@ -18,6 +18,10 @@ import { ReactSelectStyles, } from "Components/Theme/ReactSelect"; +import { config } from "react-transition-group"; + +config.disabled = true; + const { percyAddon, serializeStories } = createPercyAddon(); setAddon(percyAddon); @@ -36,7 +40,6 @@ addDecorator((story) => { value={{ reactSelectStyles: ReactSelectStyles(ReactSelectColors.Light), animations: { - in: true, duration: 0, }, }} @@ -58,7 +61,6 @@ addDecorator((story) => { value={{ reactSelectStyles: ReactSelectStyles(ReactSelectColors.Dark), animations: { - in: true, duration: 0, }, }} diff --git a/ui/package-lock.json b/ui/package-lock.json index 170aca580..525cea41f 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -12422,6 +12422,11 @@ "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", "dev": true }, + "intersection-observer": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.11.0.tgz", + "integrity": "sha512-KZArj2QVnmdud9zTpKf279m2bbGfG+4/kn16UU0NL3pTVl52ZHiJ9IRNSsnn6jaHrL9EGLFM5eWjTx2fz/+zoQ==" + }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -18966,6 +18971,14 @@ "prop-types": "^15.5.8" } }, + "react-intersection-observer": { + "version": "8.26.2", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-8.26.2.tgz", + "integrity": "sha512-GmSjLNK+oV7kS+BHfrJSaA4wF61ELA33gizKHmN+tk59UT6/aW8kkqvlrFGPwxGoaIzLKS2evfG5fgkw5MIIsg==", + "requires": { + "tiny-invariant": "^1.1.0" + } + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -19061,14 +19074,6 @@ "resize-observer-polyfill": "^1.5.1" } }, - "react-reveal": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/react-reveal/-/react-reveal-1.2.2.tgz", - "integrity": "sha512-JCv3fAoU6Z+Lcd8U48bwzm4pMZ79qsedSXYwpwt6lJNtj/v5nKJYZZbw3yhaQPPgYePo3Y0NOCoYOq/jcsisuw==", - "requires": { - "prop-types": "^15.5.10" - } - }, "react-scripts": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.4.1.tgz", @@ -21682,6 +21687,11 @@ "dev": true, "optional": true }, + "tiny-invariant": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz", + "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==" + }, "tlds": { "version": "1.207.0", "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.207.0.tgz", diff --git a/ui/package.json b/ui/package.json index fedf8962e..9b64fa3d8 100644 --- a/ui/package.json +++ b/ui/package.json @@ -24,6 +24,7 @@ "date-fns": "2.14.0", "favico.js": "0.3.10", "fontfaceobserver": "2.1.0", + "intersection-observer": "0.11.0", "lodash.debounce": "4.0.8", "lodash.merge": "4.6.2", "lodash.throttle": "4.1.1", @@ -44,13 +45,13 @@ "react-hotkeys-hook": "2.1.4", "react-idle-timer": "4.2.12", "react-input-range": "1.3.0", + "react-intersection-observer": "8.26.2", "react-js-pagination": "3.0.3", "react-json-pretty": "2.2.0", "react-linkify": "0.2.2", "react-media-hook": "0.4.7", "react-popper": "2.2.3", "react-resize-detector": "5.0.6", - "react-reveal": "1.2.2", "react-scripts": "3.4.1", "react-select": "3.1.0", "react-transition-group": "4.4.1", diff --git a/ui/src/App.tsx b/ui/src/App.tsx index e62402280..b57891722 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -136,8 +136,7 @@ const App: FunctionComponent = ({ defaultFilters, uiDefaults }) => { ? ReactSelectStyles(ReactSelectColors.Dark) : ReactSelectStyles(ReactSelectColors.Light), animations: { - enabled: true, - duration: 1000, + duration: 500, }, }} > diff --git a/ui/src/Components/CenteredMessage/__snapshots__/index.test.js.snap b/ui/src/Components/CenteredMessage/__snapshots__/index.test.js.snap index 94c9af1b2..b6ed545f0 100644 --- a/ui/src/Components/CenteredMessage/__snapshots__/index.test.js.snap +++ b/ui/src/Components/CenteredMessage/__snapshots__/index.test.js.snap @@ -3,10 +3,8 @@ exports[` matches snapshot 1`] = ` "

-
-
- Foo -
+
+ Foo

" diff --git a/ui/src/Components/CenteredMessage/index.js b/ui/src/Components/CenteredMessage/index.js index e7c6446f7..5d8aa6ffc 100644 --- a/ui/src/Components/CenteredMessage/index.js +++ b/ui/src/Components/CenteredMessage/index.js @@ -1,21 +1,26 @@ import React from "react"; -import { Fade } from "react-reveal"; +import { CSSTransition } from "react-transition-group"; import { ThemeContext } from "Components/Theme"; const CenteredMessage = ({ children, className }) => { - const theme = React.useContext(ThemeContext); + const context = React.useContext(ThemeContext); return ( -

- +

{children} - -

+

+ ); }; diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/__snapshots__/index.test.js.snap b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/__snapshots__/index.test.js.snap index 7bf9ce9d5..c32d0ad5c 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/__snapshots__/index.test.js.snap +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/__snapshots__/index.test.js.snap @@ -28,7 +28,7 @@ exports[` matches snapshot when inhibited 1`] = ` - + some long text @@ -175,7 +175,7 @@ exports[` matches snapshot with showAlertmanagers=false showReceiver=fa - + some long text diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Annotation/index.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Annotation/index.js index 6e74ee8f1..545dc039f 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Annotation/index.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Annotation/index.js @@ -3,7 +3,7 @@ import PropTypes from "prop-types"; import Linkify from "react-linkify"; -import Flash from "react-reveal/Flash"; +import { CSSTransition } from "react-transition-group"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons/faExternalLinkAlt"; @@ -11,6 +11,7 @@ import { faSearchPlus } from "@fortawesome/free-solid-svg-icons/faSearchPlus"; import { faSearchMinus } from "@fortawesome/free-solid-svg-icons/faSearchMinus"; import { TooltipWrapper } from "Components/TooltipWrapper"; +import { useFlashTransition } from "Hooks/useFlashTransition"; const RenderNonLinkAnnotation = memo( ({ name, value, visible, afterUpdate }) => { @@ -26,40 +27,43 @@ const RenderNonLinkAnnotation = memo( } }); + const { ref, props } = useFlashTransition(value); + const className = "mb-1 p-1 bg-light d-inline-block rounded components-grid-annotation text-break mw-100"; - if (!isVisible) { - return ( - -
setIsVisible(!isVisible)} - > - - {name} -
-
- ); - } - return ( - -
- setIsVisible(false)} className="cursor-pointer"> - - {name}: - - - - {value} - - + +
setIsVisible(!isVisible)} + > + {isVisible ? ( + + setIsVisible(false)} + className="cursor-pointer" + > + + {name}: + + + + {value} + + + + ) : ( + + + {name} + + )}
); diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupFooter/__snapshots__/index.test.js.snap b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupFooter/__snapshots__/index.test.js.snap index f17d4404e..99e01d608 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupFooter/__snapshots__/index.test.js.snap +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupFooter/__snapshots__/index.test.js.snap @@ -28,7 +28,7 @@ exports[` matches snapshot 1`] = ` - + This is summary @@ -157,7 +157,7 @@ exports[` mathes snapshot when silence is rendered 1`] = ` - + This is summary @@ -255,9 +255,7 @@ exports[` mathes snapshot when silence is rendered 1`] = ` link
-
+
diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.js index 4f87a42c8..7b81eeda9 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.js @@ -1,10 +1,8 @@ -import React, { useEffect, useCallback, useRef, useState } from "react"; +import React, { useEffect, useCallback, useState } from "react"; import PropTypes from "prop-types"; import { useObserver } from "mobx-react-lite"; -import { Fade } from "react-reveal"; - import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faPlus } from "@fortawesome/free-solid-svg-icons/faPlus"; import { faMinus } from "@fortawesome/free-solid-svg-icons/faMinus"; @@ -15,7 +13,6 @@ 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"; @@ -158,20 +155,9 @@ const AlertGroup = ({ } } - const context = React.useContext(ThemeContext); - - const mountRef = useRef(null); - - const [fadeDone, setFadeDone] = useState(false); - return useObserver(() => (
- setFadeDone(true)} +
-
- + {isCollapsed ? null : ( +
+
    + {group.alerts.slice(0, alertsToRender).map((alert) => ( + 1 && + group.alerts.length === 1 + } + afterUpdate={afterUpdate} + alertStore={alertStore} + silenceFormStore={silenceFormStore} + setIsMenuOpen={setIsMenuOpen} + /> + ))} + {group.alerts.length > defaultRenderCount ? ( +
  • + + + {Math.min(alertsToRender, group.alerts.length)} + {" of "} + {group.alerts.length} + + +
  • + ) : null} +
+
+ )} + {isCollapsed === false && group.alerts.length > 1 ? ( + - {isCollapsed ? null : ( -
-
    - {group.alerts.slice(0, alertsToRender).map((alert) => ( - 1 && - group.alerts.length === 1 - } - afterUpdate={afterUpdate} - alertStore={alertStore} - silenceFormStore={silenceFormStore} - setIsMenuOpen={setIsMenuOpen} - /> - ))} - {group.alerts.length > defaultRenderCount ? ( -
  • - - - {Math.min(alertsToRender, group.alerts.length)} - {" of "} - {group.alerts.length} - - -
  • - ) : null} -
-
- )} - {isCollapsed === false && group.alerts.length > 1 ? ( - - ) : null} -
- + ) : null} +
)); }; diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.js index f02a7dd29..340eacb84 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/index.test.js @@ -102,24 +102,6 @@ describe("", () => { tree.unmount(); }); - it("appends components-animation-fade-appear-done class after first mount", () => { - MockAlerts(5); - const tree = MountedAlertGroup(jest.fn(), true); - expect( - tree - .find("div.components-grid-alertgrid-alertgroup") - .hasClass("components-animation-fade-appear-done") - ).toBe(false); - tree.setProps({}); - act(() => tree.find("RevealBase").at(0).props().onReveal()); - tree.update(); - expect( - tree - .find("div.components-grid-alertgrid-alertgroup") - .hasClass("components-animation-fade-appear-done") - ).toBe(true); - }); - 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/Grid.js b/ui/src/Components/Grid/AlertGrid/Grid.js index 0e8579a5b..fb69f389b 100644 --- a/ui/src/Components/Grid/AlertGrid/Grid.js +++ b/ui/src/Components/Grid/AlertGrid/Grid.js @@ -5,7 +5,8 @@ import { useObserver } from "mobx-react-lite"; import debounce from "lodash.debounce"; -import { Fade } from "react-reveal"; +import TransitionGroup from "react-transition-group/TransitionGroup"; +import { CSSTransition } from "react-transition-group"; import FontFaceObserver from "fontfaceobserver"; @@ -31,6 +32,7 @@ const Grid = ({ grid, outerPadding, }) => { + const context = React.useContext(ThemeContext); const { ref, repack } = useGrid(gridSizesConfig); const debouncedRepack = useCallback(debounce(repack, 10), [repack]); @@ -93,18 +95,22 @@ const Grid = ({ } }, [grid.alertGroups.length, groupsToRender]); - const context = React.useContext(ThemeContext); - return useObserver(() => ( - {grid.labelName !== "" && ( + - )} +
- {isExpanded || grid.labelName === "" - ? grid.alertGroups - .slice(0, groupsToRender) - .map((group) => ( - + {isExpanded || grid.labelName === "" + ? grid.alertGroups.slice(0, groupsToRender).map((group) => ( + 1 - } - afterUpdate={debouncedRepack} - alertStore={alertStore} - settingsStore={settingsStore} - silenceFormStore={silenceFormStore} - groupWidth={groupWidth} - gridLabelValue={grid.labelValue} - /> + classNames="components-animation-fade" + timeout={context.animations.duration} + onEntering={repack} + onExited={debouncedRepack} + unmountOnExit + > + 1 + } + afterUpdate={debouncedRepack} + alertStore={alertStore} + settingsStore={settingsStore} + silenceFormStore={silenceFormStore} + groupWidth={groupWidth} + gridLabelValue={grid.labelValue} + /> + )) - : []} + : []} +
- {isExpanded && grid.alertGroups.length > groupsToRender && ( -
-
- - - -
-
- )} + + {isExpanded && grid.alertGroups.length > groupsToRender && ( + +
+
+ +
+
+
+ )} +
)); }; diff --git a/ui/src/Components/Grid/AlertGrid/Swimlane.js b/ui/src/Components/Grid/AlertGrid/Swimlane.js index ede59bafa..22225807f 100644 --- a/ui/src/Components/Grid/AlertGrid/Swimlane.js +++ b/ui/src/Components/Grid/AlertGrid/Swimlane.js @@ -1,8 +1,6 @@ import React from "react"; import PropTypes from "prop-types"; -import { Fade } from "react-reveal"; - import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faTh } from "@fortawesome/free-solid-svg-icons/faTh"; @@ -12,62 +10,57 @@ import { FilteringLabel } from "Components/Labels/FilteringLabel"; import { FilteringCounterBadge } from "Components/Labels/FilteringCounterBadge"; import { TooltipWrapper } from "Components/TooltipWrapper"; import { ToggleIcon } from "Components/ToggleIcon"; -import { ThemeContext } from "Components/Theme"; const Swimlane = ({ alertStore, grid, isExpanded, onToggle }) => { - const context = React.useContext(ThemeContext); - return ( - -
- - - - +
+ + + - - {grid.labelName !== "" && grid.labelValue !== "" && ( - - )} - - - + + {grid.labelName !== "" && grid.labelValue !== "" && ( + - - - - - - - + )} + + + + + + + + + -
- +
+
); }; Swimlane.propTypes = { diff --git a/ui/src/Components/Grid/AlertGrid/index.test.js b/ui/src/Components/Grid/AlertGrid/index.test.js index f35467121..49a933085 100644 --- a/ui/src/Components/Grid/AlertGrid/index.test.js +++ b/ui/src/Components/Grid/AlertGrid/index.test.js @@ -157,14 +157,14 @@ const MockGroupList = (count, alertPerGroup) => { describe("", () => { it("renders only first 50 alert groups", () => { - MockGroupList(60, 5); + MockGroupList(55, 5); const tree = MountedGrid(); const alertGroups = tree.find("AlertGroup"); expect(alertGroups).toHaveLength(50); }); it("appends 30 groups after clicking 'Load More' button", () => { - MockGroupList(100, 5); + MockGroupList(85, 5); const tree = MountedGrid(); tree.find("button").simulate("click"); const alertGroups = tree.find("AlertGroup"); @@ -186,6 +186,8 @@ describe("", () => { }); it("click on the grid toggle toggles all groups", () => { + jest.useFakeTimers(); + MockGroupList(10, 3); const tree = MountedGrid(); const grid = MockGrid(); @@ -201,12 +203,18 @@ describe("", () => { expect(tree.find("AlertGroup")).toHaveLength(10); tree.find("span.cursor-pointer").at(0).simulate("click"); + act(() => jest.runOnlyPendingTimers()); tree.update(); - expect(tree.find("AlertGroup")).toHaveLength(0); + expect(tree.find("div.components-grid-alertgrid-alertgroup")).toHaveLength( + 0 + ); tree.find("span.cursor-pointer").at(0).simulate("click"); + act(() => jest.runOnlyPendingTimers()); tree.update(); - expect(tree.find("AlertGroup")).toHaveLength(10); + expect(tree.find("div.components-grid-alertgrid-alertgroup")).toHaveLength( + 10 + ); }); it("renders filter badge for grids with a value", () => { diff --git a/ui/src/Components/Grid/EmptyGrid/__snapshots__/index.test.js.snap b/ui/src/Components/Grid/EmptyGrid/__snapshots__/index.test.js.snap index 8587190f4..83c5063f8 100644 --- a/ui/src/Components/Grid/EmptyGrid/__snapshots__/index.test.js.snap +++ b/ui/src/Components/Grid/EmptyGrid/__snapshots__/index.test.js.snap @@ -3,22 +3,20 @@ exports[` matches snapshot 1`] = ` "

-
- + - - - -
+ +

" `; diff --git a/ui/src/Components/Grid/FatalError/__snapshots__/index.test.js.snap b/ui/src/Components/Grid/FatalError/__snapshots__/index.test.js.snap index 75f66c2c5..ff7b85610 100644 --- a/ui/src/Components/Grid/FatalError/__snapshots__/index.test.js.snap +++ b/ui/src/Components/Grid/FatalError/__snapshots__/index.test.js.snap @@ -3,7 +3,7 @@ exports[` matches snapshot 1`] = ` "

-
+
matches snapshot 1`] = ` "

-
+
matches snapshot 1`] = ` "

-
+
{ + const { ref, props } = useFlashTransition(counter); + const handleClick = useCallback( (event) => { // left click => apply foo=bar filter @@ -52,8 +55,9 @@ const FilteringCounterBadge = observer( - + {counter} - + ); } diff --git a/ui/src/Components/Labels/FilteringCounterBadge/index.test.js b/ui/src/Components/Labels/FilteringCounterBadge/index.test.js index 6194f5641..1c1bd477c 100644 --- a/ui/src/Components/Labels/FilteringCounterBadge/index.test.js +++ b/ui/src/Components/Labels/FilteringCounterBadge/index.test.js @@ -35,8 +35,7 @@ const validateStyle = (value, themed) => { themed={themed} /> ); - // opacity=>1 is set by react-reveal/Flash - expect(tree.find("span").prop("style")).toEqual({ opacity: 1 }); + expect(tree.find("span").prop("style")).toEqual({}); }; const validateOnClick = (value, themed, isNegative) => { diff --git a/ui/src/Components/ManagedSilence/SilenceDetails.js b/ui/src/Components/ManagedSilence/SilenceDetails.js index 1f89c72a6..e98aa90fa 100644 --- a/ui/src/Components/ManagedSilence/SilenceDetails.js +++ b/ui/src/Components/ManagedSilence/SilenceDetails.js @@ -5,7 +5,7 @@ import parseISO from "date-fns/parseISO"; import copy from "copy-to-clipboard"; -import Flash from "react-reveal/Flash"; +import { CSSTransition } from "react-transition-group"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faEdit } from "@fortawesome/free-solid-svg-icons/faEdit"; @@ -22,14 +22,18 @@ import { QueryOperators } from "Common/Query"; import { TooltipWrapper } from "Components/TooltipWrapper"; import { RenderLinkAnnotation } from "Components/Grid/AlertGrid/AlertGroup/Annotation"; import { DateFromNow } from "Components/DateFromNow"; +import { useFlashTransition } from "Hooks/useFlashTransition"; import { DeleteSilence } from "./DeleteSilence"; const SilenceIDCopyButton = ({ id }) => { const [clickCount, setClickCount] = useState(0); + const { ref, props } = useFlashTransition(clickCount); + return ( - + { copy(id); @@ -38,7 +42,7 @@ const SilenceIDCopyButton = ({ id }) => { > - + ); }; diff --git a/ui/src/Components/ManagedSilence/__snapshots__/SilenceComment.test.js.snap b/ui/src/Components/ManagedSilence/__snapshots__/SilenceComment.test.js.snap index 4fab3181f..c8c94c3f8 100644 --- a/ui/src/Components/ManagedSilence/__snapshots__/SilenceComment.test.js.snap +++ b/ui/src/Components/ManagedSilence/__snapshots__/SilenceComment.test.js.snap @@ -47,9 +47,7 @@ exports[` Matches snapshot when collapsed 1`] = `
- + 123
@@ -124,9 +122,7 @@ exports[` Matches snapshot when collapsed and multiple cluster
- + 123
@@ -185,9 +181,7 @@ exports[` Matches snapshot when collapsed and multiple cluster
- + 123
@@ -246,9 +240,7 @@ exports[` Matches snapshot when expanded 1`] = `
- + 123
diff --git a/ui/src/Components/ManagedSilence/__snapshots__/index.test.js.snap b/ui/src/Components/ManagedSilence/__snapshots__/index.test.js.snap index c5a7e39c7..fb481953d 100644 --- a/ui/src/Components/ManagedSilence/__snapshots__/index.test.js.snap +++ b/ui/src/Components/ManagedSilence/__snapshots__/index.test.js.snap @@ -2,9 +2,7 @@ exports[` matches snapshot when collapsed 1`] = ` " -
+
@@ -51,9 +49,7 @@ exports[` matches snapshot when collapsed 1`] = `
- + 123
@@ -82,9 +78,7 @@ exports[` matches snapshot when collapsed 1`] = ` exports[` matches snapshot with expaned details 1`] = ` " -
+
@@ -118,9 +112,7 @@ exports[` matches snapshot with expaned details 1`] = `
- + 123
@@ -207,9 +199,7 @@ exports[` matches snapshot with expaned details 1`] = `
- + -
-
- +
+ setShowDetails(!showDetails)} + /> +
+ {showDetails ? ( +
+ setShowDetails(!showDetails)} + alertStore={alertStore} + silenceFormStore={silenceFormStore} + onEditSilence={onEditSilence} + isUpper={isNested} />
- - {showDetails ? ( -
- -
- ) : null} -
- + ) : null} +
); }; ManagedSilence.propTypes = { diff --git a/ui/src/Components/NavBar/index.js b/ui/src/Components/NavBar/index.js index c299f95cf..4d033ef42 100644 --- a/ui/src/Components/NavBar/index.js +++ b/ui/src/Components/NavBar/index.js @@ -8,7 +8,7 @@ import ReactResizeDetector from "react-resize-detector"; import IdleTimer from "react-idle-timer"; -import { Fade } from "react-reveal"; +import { CSSTransition } from "react-transition-group"; import { AlertStore } from "Stores/AlertStore"; import { Settings } from "Stores/Settings"; @@ -96,7 +96,15 @@ const NavBar = ({ alertStore, settingsStore, silenceFormStore, fixedTop }) => { timeout={IsMobile() ? MobileIdleTimeout : DesktopIdleTimeout} >
- + {}} + onExited={() => {}} + enter + exit + > - +
)); diff --git a/ui/src/Components/OverviewModal/index.js b/ui/src/Components/OverviewModal/index.js index 82833dc94..86f031d6a 100644 --- a/ui/src/Components/OverviewModal/index.js +++ b/ui/src/Components/OverviewModal/index.js @@ -3,12 +3,13 @@ import PropTypes from "prop-types"; import { useObserver } from "mobx-react-lite"; -import Flash from "react-reveal/Flash"; +import { CSSTransition } from "react-transition-group"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faSpinner } from "@fortawesome/free-solid-svg-icons/faSpinner"; import { AlertStore } from "Stores/AlertStore"; +import { useFlashTransition } from "Hooks/useFlashTransition"; import { TooltipWrapper } from "Components/TooltipWrapper"; import { Modal } from "Components/Modal"; @@ -24,11 +25,14 @@ const OverviewModal = ({ alertStore }) => { const toggle = useCallback(() => setIsVisible(!isVisible), [isVisible]); + const { ref, props } = useFlashTransition(alertStore.info.totalAlerts); + return useObserver(() => ( - +
{ > {alertStore.info.totalAlerts}
-
+
{ - const theme = React.useContext(ThemeContext); + const context = React.useContext(ThemeContext); return ( - +

{content}

-
+ ); }; Placeholder.propTypes = { diff --git a/ui/src/Components/SilenceModal/SilenceForm.js b/ui/src/Components/SilenceModal/SilenceForm.js index 926dcb40b..52ad976c9 100644 --- a/ui/src/Components/SilenceModal/SilenceForm.js +++ b/ui/src/Components/SilenceModal/SilenceForm.js @@ -3,10 +3,10 @@ import PropTypes from "prop-types"; import { useObserver } from "mobx-react-lite"; -import Flash from "react-reveal/Flash"; - import copy from "copy-to-clipboard"; +import { CSSTransition } from "react-transition-group"; + import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faPlus } from "@fortawesome/free-solid-svg-icons/faPlus"; import { faUser } from "@fortawesome/free-solid-svg-icons/faUser"; @@ -26,6 +26,7 @@ import { } from "Stores/SilenceFormStore"; import { Settings } from "Stores/Settings"; import { QueryOperators } from "Common/Query"; +import { useFlashTransition } from "Hooks/useFlashTransition"; import { TooltipWrapper } from "Components/TooltipWrapper"; import { ToggleIcon } from "Components/ToggleIcon"; import { AlertManagerInput } from "./AlertManagerInput"; @@ -44,6 +45,8 @@ const ShareButton = ({ silenceFormStore }) => { window.location.pathname, ].join(""); + const { ref, props } = useFlashTransition(clickCount); + return useObserver(() => (
@@ -59,7 +62,7 @@ const ShareButton = ({ silenceFormStore }) => { value={`${baseURL}?m=${silenceFormStore.data.toBase64}`} onChange={() => {}} /> -
+
{ @@ -68,9 +71,9 @@ const ShareButton = ({ silenceFormStore }) => { }} > - + - +
diff --git a/ui/src/Hooks/useFlashTransition.js b/ui/src/Hooks/useFlashTransition.js new file mode 100644 index 000000000..11e7c2cc3 --- /dev/null +++ b/ui/src/Hooks/useFlashTransition.js @@ -0,0 +1,40 @@ +import { useState, useEffect, useRef } from "react"; + +import { useInView } from "react-intersection-observer"; + +const defaultProps = { + in: false, + classNames: "components-animation-flash", + timeout: 800, + appear: false, + enter: false, + exit: false, +}; + +const useFlashTransition = (flashOn) => { + const mountRef = useRef(false); + const [ref, inView] = useInView(); + const [isPending, setIsPending] = useState(false); + const [props, setProps] = useState(defaultProps); + + useEffect(() => { + if (mountRef.current) { + setIsPending(true); + } else { + mountRef.current = true; + } + }, [flashOn]); + + useEffect(() => { + setProps({ + ...defaultProps, + in: isPending && inView, + enter: isPending && inView, + onEntered: () => setIsPending(false), + }); + }, [inView, isPending]); + + return { ref, props }; +}; + +export { useFlashTransition, defaultProps }; diff --git a/ui/src/Hooks/useFlashTransition.test.js b/ui/src/Hooks/useFlashTransition.test.js new file mode 100644 index 000000000..9b6a5d6b9 --- /dev/null +++ b/ui/src/Hooks/useFlashTransition.test.js @@ -0,0 +1,97 @@ +import { renderHook, act } from "@testing-library/react-hooks"; + +import { useInView } from "react-intersection-observer"; + +import { useFlashTransition, defaultProps } from "./useFlashTransition"; + +describe("useFlashTransition", () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + useInView.setInView(true); + }); + + it("does nothing when value changes but element is out of viewport", () => { + useInView.setInView(false); + + let value = 0; + const { result, rerender } = renderHook(() => useFlashTransition(value)); + expect(result.current.props).toMatchObject(defaultProps); + + value = 1; + rerender(); + expect(result.current.props).toMatchObject(defaultProps); + }); + + it("flashes when value changes and element is in viewport", () => { + useInView.setInView(true); + + let value = 2; + const { result, rerender } = renderHook(() => useFlashTransition(value)); + + value = 3; + rerender(); + expect(result.current.props).toMatchObject({ + ...defaultProps, + in: true, + enter: true, + }); + }); + + it("flashes when value changes and element moves into viewport", () => { + useInView.setInView(false); + + let value = 2; + const { result, rerender } = renderHook(() => useFlashTransition(value)); + + value = 3; + rerender(); + expect(result.current.props).toMatchObject(defaultProps); + + act(() => useInView.setInView(true)); + rerender(); + expect(result.current.props).toMatchObject({ + ...defaultProps, + in: true, + enter: true, + }); + }); + + it("stops flashing props.onEntered is called", () => { + useInView.setInView(true); + + let value = 2; + const { result, rerender } = renderHook(() => useFlashTransition(value)); + + value = 3; + rerender(); + expect(result.current.props).toMatchObject({ + ...defaultProps, + in: true, + enter: true, + }); + + act(() => result.current.props.onEntered()); + expect(result.current.props).toMatchObject(defaultProps); + }); + + it("unmounts cleanly when not flashing", () => { + useInView.setInView(false); + + const { unmount } = renderHook(() => useFlashTransition(4)); + unmount(); + }); + + it("unmounts cleanly when flashing", () => { + useInView.setInView(false); + + let value = 5; + const { rerender, unmount } = renderHook(() => useFlashTransition(value)); + + value = 6; + rerender(); + unmount(); + }); +}); diff --git a/ui/src/Styles/Components/AlertGroup.scss b/ui/src/Styles/Components/AlertGroup.scss index 693405e61..38e4b9cd3 100644 --- a/ui/src/Styles/Components/AlertGroup.scss +++ b/ui/src/Styles/Components/AlertGroup.scss @@ -1,14 +1,12 @@ -.components-grid-alertgrid-alertgroup.components-animation-fade-appear-done { - will-change: transform; - transform: translateZ(0); - - transition-property: transform; - transition-duration: 0.4s; - transition-timing-function: ease; -} - .components-grid-alertgrid-alertgroup { padding: 0.3rem; + + &.components-animation-fade-appear-done, + &.components-animation-fade-enter-done { + transition-property: transform; + transition-duration: 0.4s; + transition-timing-function: ease; + } } .components-grid-alertgrid-alertgroup-shared-silence { diff --git a/ui/src/Styles/Components/Fade.scss b/ui/src/Styles/Components/Fade.scss new file mode 100644 index 000000000..7d6071b4f --- /dev/null +++ b/ui/src/Styles/Components/Fade.scss @@ -0,0 +1,19 @@ +$duration: 0.5s; + +.components-animation-fade-appear, +.components-animation-fade-enter { + opacity: 0; +} +.components-animation-fade-appear-active, +.components-animation-fade-enter-active { + opacity: 1; + transition: opacity $duration ease-in; +} + +.components-animation-fade-exit { + opacity: 1; +} +.components-animation-fade-exit-active { + opacity: 0.01; + transition: opacity $duration ease-out; +} diff --git a/ui/src/Styles/Components/Flash.scss b/ui/src/Styles/Components/Flash.scss new file mode 100644 index 000000000..0ab874ec4 --- /dev/null +++ b/ui/src/Styles/Components/Flash.scss @@ -0,0 +1,26 @@ +$duration: 800ms; + +.components-animation-flash-appear, +.components-animation-flash-enter { + animation-name: flash; + animation-duration: $duration; + animation-timing-function: ease-in-out; +} + +@keyframes flash { + 0% { + opacity: 1; + } + 25% { + opacity: 0; + } + 50% { + opacity: 1; + } + 75% { + opacity: 0; + } + 100% { + opacity: 1; + } +} diff --git a/ui/src/Styles/Components/NavBarSlide.scss b/ui/src/Styles/Components/NavBarSlide.scss new file mode 100644 index 000000000..c9971c5be --- /dev/null +++ b/ui/src/Styles/Components/NavBarSlide.scss @@ -0,0 +1,23 @@ +$duration: 0.5s; + +.components-animation-navbar-appear, +.components-animation-navbar-enter { + opacity: 0.01; + transform: translateY(-100%); +} +.components-animation-navbar-appear-active, +.components-animation-navbar-enter-active { + opacity: 1; + transform: translateY(0); + transition: opacity $duration ease-out, transform $duration ease-out; +} + +.components-animation-navbar-exit { + opacity: 1; + transform: translateY(0); +} +.components-animation-navbar-exit-active { + opacity: 0.01; + transform: translateY(-100%); + transition: opacity $duration ease-out, transform $duration ease-out; +} diff --git a/ui/src/Styles/DarkTheme.scss b/ui/src/Styles/DarkTheme.scss index f96d92f89..8fd88be35 100644 --- a/ui/src/Styles/DarkTheme.scss +++ b/ui/src/Styles/DarkTheme.scss @@ -98,6 +98,7 @@ $color-default: #708090; @import "Styles/Components/AlertGroup"; @import "Styles/Components/BaseLabel"; @import "Styles/Components/CenteredMessage"; +@import "Styles/Components/Fade"; @import "Styles/Components/FilterInputLabel"; @import "Styles/Components/LabelWithPercent"; @import "Styles/Components/InputRange"; @@ -105,6 +106,7 @@ $color-default: #708090; @import "Styles/Components/Modal"; @import "Styles/Components/NavBar"; @import "Styles/Components/FilterInput"; +@import "Styles/Components/Flash"; @import "Styles/Components/DateTimeSelect"; @import "Styles/Components/MountModal"; @import "Styles/Components/Placeholder"; @@ -113,6 +115,7 @@ $color-default: #708090; @import "Styles/Components/DropdownSlide"; @import "Styles/Components/History"; @import "Styles/Components/HistoryLabel"; +@import "Styles/Components/NavBarSlide"; @import "Styles/Components/SilenceModal"; @import "Styles/Components/Pagination"; @import "Styles/Components/Tooltip"; diff --git a/ui/src/Styles/LightTheme.scss b/ui/src/Styles/LightTheme.scss index 4aafa3900..bfebc1c86 100644 --- a/ui/src/Styles/LightTheme.scss +++ b/ui/src/Styles/LightTheme.scss @@ -80,6 +80,7 @@ $color-default: #708090; @import "Styles/Components/AlertGroup"; @import "Styles/Components/BaseLabel"; @import "Styles/Components/CenteredMessage"; +@import "Styles/Components/Fade"; @import "Styles/Components/FilterInputLabel"; @import "Styles/Components/LabelWithPercent"; @import "Styles/Components/InputRange"; @@ -87,6 +88,7 @@ $color-default: #708090; @import "Styles/Components/Modal"; @import "Styles/Components/NavBar"; @import "Styles/Components/FilterInput"; +@import "Styles/Components/Flash"; @import "Styles/Components/DateTimeSelect"; @import "Styles/Components/MountModal"; @import "Styles/Components/Placeholder"; @@ -95,6 +97,7 @@ $color-default: #708090; @import "Styles/Components/DropdownSlide"; @import "Styles/Components/History"; @import "Styles/Components/HistoryLabel"; +@import "Styles/Components/NavBarSlide"; @import "Styles/Components/SilenceModal"; @import "Styles/Components/Pagination"; @import "Styles/Components/Tooltip"; diff --git a/ui/src/__mocks__/Theme.js b/ui/src/__mocks__/Theme.js index b14e31708..b83dec5ec 100644 --- a/ui/src/__mocks__/Theme.js +++ b/ui/src/__mocks__/Theme.js @@ -5,7 +5,6 @@ import { const MockThemeContext = { animations: { - in: undefined, duration: 500, }, isDark: false, diff --git a/ui/src/__mocks__/react-intersection-observer.js b/ui/src/__mocks__/react-intersection-observer.js new file mode 100644 index 000000000..e3be2a32c --- /dev/null +++ b/ui/src/__mocks__/react-intersection-observer.js @@ -0,0 +1,15 @@ +import { useRef } from "react"; + +const mock = { + value: true, +}; + +const useInView = () => { + const ref = useRef(null); + return [ref, mock.value]; +}; +useInView.setInView = (val) => { + mock.value = val; +}; + +export { useInView }; diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 121d34eab..af36d7eb3 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -4,6 +4,9 @@ import "react-app-polyfill/ie11"; import "react-app-polyfill/stable"; +// https://www.npmjs.com/package/react-intersection-observer#polyfill +import "intersection-observer"; + import React from "react"; import ReactDOM from "react-dom"; diff --git a/ui/src/setupTests.js b/ui/src/setupTests.js index 81e09b890..3699143ed 100644 --- a/ui/src/setupTests.js +++ b/ui/src/setupTests.js @@ -20,10 +20,7 @@ for (const level of ["error", "warn", "info", "log", "trace"]) { // https://reactjs.org/blog/2019/08/08/react-v16.9.0.html#new-deprecations const reactDeprecationWarning = /.*has been renamed, and is not recommended for use.*/; global.console[level] = (message, ...args) => { - if ( - reactDeprecationWarning.test(message) === false && - message !== "react-reveal - animation failed" - ) { + if (reactDeprecationWarning.test(message) === false) { throw new Error(`message=${message} args=${args}`); } };