diff --git a/ui/src/Components/Animations/MountFade/index.css b/ui/src/Components/Animations/MountFade/index.css index 37b4a1323..ae5a758ab 100644 --- a/ui/src/Components/Animations/MountFade/index.css +++ b/ui/src/Components/Animations/MountFade/index.css @@ -1,17 +1,7 @@ -.components-animation-fade-appear, -.components-animation-fade-enter { +.components-animation-fade-appear { opacity: 0.01; } -.components-animation-fade-appear-active, -.components-animation-fade-enter-active { +.components-animation-fade-appear-active { opacity: 1; transition: all 0.3s ease-in; } - -.components-animation-fade-exit { - opacity: 1; -} -.components-animation-fade-exit-active { - opacity: 0.01; - transition: all 0.3s ease-out; -} diff --git a/ui/src/Components/Animations/MountFade/index.js b/ui/src/Components/Animations/MountFade/index.js index 5ae94cf10..e7104b3af 100644 --- a/ui/src/Components/Animations/MountFade/index.js +++ b/ui/src/Components/Animations/MountFade/index.js @@ -10,8 +10,6 @@ const MountFade = ({ children, duration, ...props }) => ( classNames="components-animation-fade" timeout={300} appear={true} - enter={true} - exit={true} {...props} > {children} diff --git a/ui/src/Components/Grid/AlertGrid/Constants.js b/ui/src/Components/Grid/AlertGrid/Constants.js deleted file mode 100644 index 21c227abc..000000000 --- a/ui/src/Components/Grid/AlertGrid/Constants.js +++ /dev/null @@ -1,34 +0,0 @@ -// grid sizes, defines how many columns are used depending on the screen width -// this is config as expected by https://github.com/callmecavs/bricks.js#sizes -const GridSizesConfig = [ - { columns: 1, gutter: 0 }, - { mq: "800px", columns: 2, gutter: 0 }, - { mq: "1400px", columns: 3, gutter: 0 }, - { mq: "2100px", columns: 4, gutter: 0 }, - { mq: "2800px", columns: 5, gutter: 0 }, - { mq: "3500px", columns: 6, gutter: 0 }, - { mq: "4200px", columns: 7, gutter: 0 }, - { mq: "4900px", columns: 7, gutter: 0 }, - { mq: "5600px", columns: 8, gutter: 0 } -]; - -const GetGridElementWidth = canvasWidth => - Math.floor( - canvasWidth < 800 - ? canvasWidth - : canvasWidth < 1400 - ? canvasWidth / 2 - : canvasWidth < 2100 - ? canvasWidth / 3 - : canvasWidth < 2800 - ? canvasWidth / 4 - : canvasWidth < 3500 - ? canvasWidth / 5 - : canvasWidth < 4200 - ? canvasWidth / 6 - : canvasWidth < 5600 - ? canvasWidth / 7 - : canvasWidth / 8 - ); - -export { GridSizesConfig, GetGridElementWidth }; diff --git a/ui/src/Components/Grid/AlertGrid/GridSize.js b/ui/src/Components/Grid/AlertGrid/GridSize.js new file mode 100644 index 000000000..b7923e300 --- /dev/null +++ b/ui/src/Components/Grid/AlertGrid/GridSize.js @@ -0,0 +1,25 @@ +// grid sizes, defines how many columns are used depending on the screen width +// this is config as expected by https://github.com/callmecavs/bricks.js#sizes +const GridSizesConfig = (canvasWidth, baseWidth) => { + const generatedSizes = []; + for (let i = 2; i < 20; i++) { + generatedSizes.push({ + mq: `${i * baseWidth}px`, + columns: i, + gutter: 0 + }); + } + return [...[{ columns: 1, gutter: 0 }], ...generatedSizes]; +}; + +const GetColumnsCount = (canvasWidth, baseWidth) => + [{ mq: "0px", columns: 1 }, ...GridSizesConfig(canvasWidth, baseWidth)] + .filter(gs => gs.mq !== undefined) + .filter(gs => canvasWidth >= Number.parseInt(gs.mq)) + .map(gs => gs.columns) + .pop(); + +const GetGridElementWidth = (canvasWidth, baseWidth) => + Math.floor(canvasWidth / GetColumnsCount(canvasWidth, baseWidth)); + +export { GridSizesConfig, GetColumnsCount, GetGridElementWidth }; diff --git a/ui/src/Components/Grid/AlertGrid/index.js b/ui/src/Components/Grid/AlertGrid/index.js index ccfe9a800..5e5c350c6 100644 --- a/ui/src/Components/Grid/AlertGrid/index.js +++ b/ui/src/Components/Grid/AlertGrid/index.js @@ -1,7 +1,7 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; -import { observable, action } from "mobx"; +import { observable, action, computed } from "mobx"; import { observer } from "mobx-react"; import FontFaceObserver from "fontfaceobserver"; @@ -21,7 +21,7 @@ import { AlertStore } from "Stores/AlertStore"; import { Settings } from "Stores/Settings"; import { SilenceFormStore } from "Stores/SilenceFormStore"; import { AlertGroup } from "./AlertGroup"; -import { GridSizesConfig, GetGridElementWidth } from "./Constants"; +import { GridSizesConfig, GetGridElementWidth } from "./GridSize"; import "./index.css"; @@ -44,10 +44,24 @@ const AlertGrid = observer( width: document.body.clientWidth, update() { this.width = document.body.clientWidth; + }, + get gridSizesConfig() { + return GridSizesConfig( + this.width, + props.settingsStore.gridConfig.config.groupWidth + ); + }, + get groupWidth() { + return GetGridElementWidth( + this.width, + props.settingsStore.gridConfig.config.groupWidth + ); } }, { - update: action.bound + update: action.bound, + gridSizesConfig: computed, + groupWidth: computed } ); } @@ -186,12 +200,6 @@ const AlertGrid = observer( font700.load(null, 30000).then(this.masonryRepack, () => {}); } - componentDidUpdate() { - // whenever grid component re-renders we need to ensure that grid elements - // are packed correctly - this.masonryRepack(); - } - render() { const { alertStore, settingsStore, silenceFormStore } = this.props; @@ -202,9 +210,10 @@ const AlertGrid = observer( onResize={debounce(this.viewport.update, 100)} /> ))} diff --git a/ui/src/Components/Grid/AlertGrid/index.test.js b/ui/src/Components/Grid/AlertGrid/index.test.js index 7564f533f..35e2b6497 100644 --- a/ui/src/Components/Grid/AlertGrid/index.test.js +++ b/ui/src/Components/Grid/AlertGrid/index.test.js @@ -8,6 +8,7 @@ import { MockAlert, MockAlertGroup } from "__mocks__/Alerts.js"; import { AlertStore } from "Stores/AlertStore"; import { Settings } from "Stores/Settings"; import { SilenceFormStore } from "Stores/SilenceFormStore"; +import { GetGridElementWidth } from "./GridSize"; import { AlertGrid } from "."; let alertStore; @@ -109,44 +110,36 @@ describe("", () => { expect(alertGroups).toHaveLength(80); }); - it("calls masonryRepack() after update`", () => { + it("calling masonryRepack() calls forcePack() on Masonry instance`", () => { const tree = ShallowAlertGrid(); const instance = tree.instance(); - const repackSpy = jest.spyOn(instance, "masonryRepack"); // it's a shallow render so we don't really have masonry mounted, fake it instance.masonryComponentReference.ref = { forcePack: jest.fn() }; - instance.componentDidUpdate(); - expect(repackSpy).toHaveBeenCalled(); + instance.masonryRepack(); expect(instance.masonryComponentReference.ref.forcePack).toHaveBeenCalled(); }); it("masonryRepack() doesn't crash when masonryComponentReference.ref=false`", () => { const tree = ShallowAlertGrid(); const instance = tree.instance(); - const repackSpy = jest.spyOn(instance, "masonryRepack"); instance.masonryComponentReference.ref = false; - instance.componentDidUpdate(); - expect(repackSpy).toHaveBeenCalled(); + instance.masonryRepack(); }); it("masonryRepack() doesn't crash when masonryComponentReference.ref=null`", () => { const tree = ShallowAlertGrid(); const instance = tree.instance(); - const repackSpy = jest.spyOn(instance, "masonryRepack"); instance.masonryComponentReference.ref = null; - instance.componentDidUpdate(); - expect(repackSpy).toHaveBeenCalled(); + instance.masonryRepack(); }); it("masonryRepack() doesn't crash when masonryComponentReference.ref=undefined`", () => { const tree = ShallowAlertGrid(); const instance = tree.instance(); - const repackSpy = jest.spyOn(instance, "masonryRepack"); instance.masonryComponentReference.ref = undefined; - instance.componentDidUpdate(); - expect(repackSpy).toHaveBeenCalled(); + instance.masonryRepack(); }); it("calling storeMasonryRef() saves the ref in local store", () => { @@ -341,60 +334,69 @@ describe("", () => { jest.runOnlyPendingTimers(); }); - it("renders 1 column with document.body.clientWidth=799", () => { - VerifyColumnCount(799, 1); - }); + // known breakpoints calculated from GridSize logic + [ + { breakpoint: 400, columns: 1 }, + { breakpoint: 800, columns: 2 }, + { breakpoint: 1200, columns: 3 }, + { breakpoint: 1600, columns: 4 }, + { breakpoint: 2000, columns: 5 }, + { breakpoint: 2400, columns: 6 }, + { breakpoint: 3000, columns: 7 }, + { breakpoint: 3400, columns: 8 }, + { breakpoint: 3800, columns: 9 }, + { breakpoint: 4200, columns: 10 } + ].map(t => + it(`renders ${t.columns} column(s) on ${t.breakpoint} breakpoint`, () => { + settingsStore.gridConfig.config.groupWidth = 400; + VerifyColumnCount(t.canvas - 1, Math.max(1, t.columns - 1)); + VerifyColumnCount(t.canvas, t.columns); + VerifyColumnCount(t.canvas + 1, t.columns); + }) + ); - it("renders 2 columns with document.body.clientWidth=800", () => { - VerifyColumnCount(800, 2); - }); + // populare screen resolutions + [ + { canvas: 640, columns: 1 }, + { canvas: 1024, columns: 2 }, + { canvas: 1280, columns: 3 }, + { canvas: 1366, columns: 3 }, + { canvas: 1440, columns: 3 }, + { canvas: 1600, columns: 4 }, + { canvas: 1680, columns: 4 }, + { canvas: 1920, columns: 4 }, + { canvas: 2048, columns: 5 }, + { canvas: 2560, columns: 6 }, + { canvas: 3840, columns: 9 } + ].map(t => + it(`renders ${t.columns} column(s) with ${t.canvas} resolution`, () => { + settingsStore.gridConfig.config.groupWidth = 400; + VerifyColumnCount(t.canvas, t.columns); + }) + ); - it("renders 2 columns with document.body.clientWidth=1399", () => { - VerifyColumnCount(1399, 2); - }); + it("renders expected number of columns for every resolution", () => { + const minWidth = 400; + let lastColumns = 1; + for (let i = 100; i <= 4096; i++) { + const expectedColumns = Math.max(Math.floor(i / minWidth), 1); + const columns = Math.floor(i / GetGridElementWidth(i, minWidth)); - it("renders 3 columns with document.body.clientWidth=1400", () => { - VerifyColumnCount(1400, 3); - }); + expect({ + resolution: i, + minWidth: minWidth, + columns: columns + }).toEqual({ + resolution: i, + minWidth: minWidth, + columns: expectedColumns + }); + expect(columns).toBeGreaterThanOrEqual(lastColumns); - it("renders 3 columns with document.body.clientWidth=2099", () => { - VerifyColumnCount(2099, 3); - }); - - it("renders 4 columns with document.body.clientWidth=2100", () => { - VerifyColumnCount(2100, 4); - }); - - it("renders 4 columns with document.body.clientWidth=2799", () => { - VerifyColumnCount(2799, 4); - }); - - it("renders 5 columns with document.body.clientWidth=2800", () => { - VerifyColumnCount(2800, 5); - }); - - it("renders 5 columns with document.body.clientWidth=3499", () => { - VerifyColumnCount(3499, 5); - }); - - it("renders 6 columns with document.body.clientWidth=1399", () => { - VerifyColumnCount(3500, 6); - }); - - it("renders 6 columns with document.body.clientWidth=4199", () => { - VerifyColumnCount(4199, 6); - }); - - it("renders 7 columns with document.body.clientWidth=1399", () => { - VerifyColumnCount(4200, 7); - }); - - it("renders 7 columns with document.body.clientWidth=5599", () => { - VerifyColumnCount(5599, 7); - }); - - it("renders 8 columns with document.body.clientWidth=5600", () => { - VerifyColumnCount(5600, 8); + // keep track of column count to verify that each incrementing resolution + // doesn't result in lower number of columns rendered + lastColumns = columns; + } }); it("viewport resize also resizes alert groups", () => { @@ -406,7 +408,7 @@ describe("", () => { .find("AlertGroup") .at(0) .props().style.width - ).toBe(1980 / 3); + ).toBe(1980 / 4); bodyWidth = 1000; // not sure how to force ReactResizeDetector to detect width change, so diff --git a/ui/src/Components/MainModal/Configuration/AlertGroupWidthConfiguration.js b/ui/src/Components/MainModal/Configuration/AlertGroupWidthConfiguration.js new file mode 100644 index 000000000..87aefdf59 --- /dev/null +++ b/ui/src/Components/MainModal/Configuration/AlertGroupWidthConfiguration.js @@ -0,0 +1,64 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; + +import { observable, action, toJS } from "mobx"; +import { observer } from "mobx-react"; + +import { debounce } from "lodash"; + +import InputRange from "react-input-range"; + +import { Settings } from "Stores/Settings"; + +import "./InputRange.scss"; + +const AlertGroupWidthConfiguration = observer( + class AlertGroupWidthConfiguration extends Component { + static propTypes = { + settingsStore: PropTypes.instanceOf(Settings).isRequired + }; + + constructor(props) { + super(props); + + this.config = observable({ + groupWidth: toJS(props.settingsStore.gridConfig.config.groupWidth) + }); + } + + onChange = action(value => { + this.config.groupWidth = value; + }); + + onChangeComplete = debounce( + action(value => { + const { settingsStore } = this.props; + + settingsStore.gridConfig.config.groupWidth = value; + }), + 200 + ); + + render() { + return ( +
+ + +
+ ); + } + } +); + +export { AlertGroupWidthConfiguration }; diff --git a/ui/src/Components/MainModal/Configuration/AlertGroupWidthConfiguration.test.js b/ui/src/Components/MainModal/Configuration/AlertGroupWidthConfiguration.test.js new file mode 100644 index 000000000..a8f85d773 --- /dev/null +++ b/ui/src/Components/MainModal/Configuration/AlertGroupWidthConfiguration.test.js @@ -0,0 +1,42 @@ +import React from "react"; + +import { mount } from "enzyme"; + +import toDiffableHtml from "diffable-html"; + +import { Settings } from "Stores/Settings"; +import { AlertGroupWidthConfiguration } from "./AlertGroupWidthConfiguration"; + +let settingsStore; +beforeEach(() => { + settingsStore = new Settings(); +}); + +const FakeConfiguration = () => { + return mount(); +}; + +describe("", () => { + it("matches snapshot with default values", () => { + const tree = FakeConfiguration(); + expect(toDiffableHtml(tree.html())).toMatchSnapshot(); + }); + + it("call to onChange() updates internal state", () => { + const tree = FakeConfiguration(); + tree.instance().onChange(500); + expect(tree.instance().config.groupWidth).toBe(500); + }); + + it("settings are updated on completed change", () => { + const tree = FakeConfiguration(); + tree.instance().onChangeComplete(555); + expect(settingsStore.gridConfig.config.groupWidth).toBe(555); + }); + + it("custom interval value is rendered correctly", () => { + settingsStore.gridConfig.config.groupWidth = 455; + const component = FakeConfiguration(); + expect(component.find("InputRange").props().value).toBe(455); + }); +}); diff --git a/ui/src/Components/MainModal/Configuration/__snapshots__/AlertGroupWidthConfiguration.test.js.snap b/ui/src/Components/MainModal/Configuration/__snapshots__/AlertGroupWidthConfiguration.test.js.snap new file mode 100644 index 000000000..55b020c4f --- /dev/null +++ b/ui/src/Components/MainModal/Configuration/__snapshots__/AlertGroupWidthConfiguration.test.js.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` matches snapshot with default values 1`] = ` +" +
+ +
+ + + 300 + + +
+
+
+ + + + 420 + + +
+
+
+
+ + + 800 + + +
+
+" +`; diff --git a/ui/src/Components/MainModal/Configuration/__snapshots__/index.test.js.snap b/ui/src/Components/MainModal/Configuration/__snapshots__/index.test.js.snap index 12da2f170..5f0f81a6f 100644 --- a/ui/src/Components/MainModal/Configuration/__snapshots__/index.test.js.snap +++ b/ui/src/Components/MainModal/Configuration/__snapshots__/index.test.js.snap @@ -72,6 +72,51 @@ exports[` matches snapshot 1`] = `
+
+ +
+ + + 300 + + +
+
+
+ + + + 420 + + +
+
+
+
+ + + 800 + + +
+
+
+