From ec1705a0f03da827590b3de6074513e590b9779d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Sun, 29 Mar 2020 20:01:11 +0100 Subject: [PATCH] feat(ui): support multiple grids in the UI --- ui/src/Components/Grid/AlertGrid/Grid.js | 202 ++++++++++++++ ui/src/Components/Grid/AlertGrid/index.js | 112 +------- .../Components/Grid/AlertGrid/index.test.js | 253 ++++++++++++------ ui/src/__mocks__/Stories.js | 8 +- 4 files changed, 391 insertions(+), 184 deletions(-) create mode 100644 ui/src/Components/Grid/AlertGrid/Grid.js diff --git a/ui/src/Components/Grid/AlertGrid/Grid.js b/ui/src/Components/Grid/AlertGrid/Grid.js new file mode 100644 index 000000000..06803eab6 --- /dev/null +++ b/ui/src/Components/Grid/AlertGrid/Grid.js @@ -0,0 +1,202 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; + +import { observable, action } from "mobx"; +import { observer } from "mobx-react"; + +import debounce from "lodash/debounce"; + +import MasonryInfiniteScroller from "react-masonry-infinite"; + +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faChevronUp } from "@fortawesome/free-solid-svg-icons/faChevronUp"; +import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown"; +import { faAngleDoubleDown } from "@fortawesome/free-solid-svg-icons/faAngleDoubleDown"; + +import { AlertStore } from "Stores/AlertStore"; +import { Settings } from "Stores/Settings"; +import { SilenceFormStore } from "Stores/SilenceFormStore"; +import { APIGroup } from "Models/API"; +import { FilteringLabel } from "Components/Labels/FilteringLabel"; +import { TooltipWrapper } from "Components/TooltipWrapper"; +import { AlertGroup } from "./AlertGroup"; + +const Grid = observer( + class Grid extends Component { + static propTypes = { + alertStore: PropTypes.instanceOf(AlertStore).isRequired, + settingsStore: PropTypes.instanceOf(Settings).isRequired, + silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired, + gridSizesConfig: PropTypes.array.isRequired, + groupWidth: PropTypes.number.isRequired, + gridLabelName: PropTypes.string.isRequired, + gridLabelValue: PropTypes.string.isRequired, + gridAlertGroups: PropTypes.arrayOf(APIGroup).isRequired, + }; + + // store reference to generated masonry component so we can call it + // to repack the grid after any component was re-rendered, which could + // alter its size breaking grid layout + masonryComponentReference = observable( + { ref: false }, + {}, + { name: "Masonry reference" } + ); + // store it for later + storeMasonryRef = action((ref) => { + this.masonryComponentReference.ref = ref; + }); + // used to call forcePack() which will repack all grid elements + // (alert groups), this needs to be called if any group size changes + masonryRepack = debounce( + action(() => { + if (this.masonryComponentReference.ref) { + this.masonryComponentReference.ref.forcePack(); + } + }), + 10 + ); + + initial = 50; + groupsToRender = observable( + { + value: this.initial, + setValue(value) { + this.value = value; + }, + }, + { + setValue: action.bound, + }, + { name: "Groups to render" } + ); + // how many groups add to render count when user scrolls to the bottom + loadMoreStep = 30; + + loadMore = action(() => { + const { gridAlertGroups } = this.props; + + this.groupsToRender.value = Math.min( + this.groupsToRender.value + this.loadMoreStep, + gridAlertGroups.length + ); + }); + + gridToggle = observable( + { + show: true, + toggle() { + this.show = !this.show; + }, + }, + { + toggle: action.bound, + } + ); + + componentDidUpdate() { + const { gridAlertGroups } = this.props; + + this.masonryRepack(); + + if (this.groupsToRender.value > gridAlertGroups.length) { + this.groupsToRender.setValue( + Math.max(this.initial, gridAlertGroups.length) + ); + } + } + + render() { + const { + alertStore, + settingsStore, + silenceFormStore, + gridSizesConfig, + groupWidth, + gridLabelName, + gridLabelValue, + gridAlertGroups, + } = this.props; + + return ( + + {gridLabelName !== "" && gridLabelValue !== "" && ( +
+ + {gridAlertGroups.length} + + + + + + + + + +
+ )} + + {this.gridToggle.show + ? gridAlertGroups + .slice(0, this.groupsToRender.value) + .map((group) => ( + + 1 + } + afterUpdate={this.masonryRepack} + alertStore={alertStore} + settingsStore={settingsStore} + silenceFormStore={silenceFormStore} + style={{ + width: groupWidth, + }} + /> + )) + : []} + + {gridAlertGroups.length > this.groupsToRender.value && ( +
+ + + +
+ )} +
+ ); + } + } +); + +export { Grid }; diff --git a/ui/src/Components/Grid/AlertGrid/index.js b/ui/src/Components/Grid/AlertGrid/index.js index 7ad215873..2bfba1616 100644 --- a/ui/src/Components/Grid/AlertGrid/index.js +++ b/ui/src/Components/Grid/AlertGrid/index.js @@ -10,15 +10,10 @@ import debounce from "lodash/debounce"; import ReactResizeDetector from "react-resize-detector"; -import MasonryInfiniteScroller from "react-masonry-infinite"; - -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch"; - import { AlertStore } from "Stores/AlertStore"; import { Settings } from "Stores/Settings"; import { SilenceFormStore } from "Stores/SilenceFormStore"; -import { AlertGroup } from "./AlertGroup"; +import { Grid } from "./Grid"; import { GridSizesConfig, GetGridElementWidth } from "./GridSize"; const AlertGrid = observer( @@ -69,54 +64,6 @@ const AlertGrid = observer( this.viewport.updateWidths(document.body.clientWidth, window.innerWidth); }, 100); - // store reference to generated masonry component so we can call it - // to repack the grid after any component was re-rendered, which could - // alter its size breaking grid layout - masonryComponentReference = observable( - { ref: false }, - {}, - { name: "Masonry reference" } - ); - // store it for later - storeMasonryRef = action((ref) => { - this.masonryComponentReference.ref = ref; - }); - // used to call forcePack() which will repack all grid elements - // (alert groups), this needs to be called if any group size changes - masonryRepack = debounce( - action(() => { - if (this.masonryComponentReference.ref) { - this.masonryComponentReference.ref.forcePack(); - } - }), - 10 - ); - - initial = 50; - groupsToRender = observable( - { - value: this.initial, - setValue(value) { - this.value = value; - }, - }, - { - setValue: action.bound, - }, - { name: "Groups to render" } - ); - // how many groups add to render count when user scrolls to the bottom - loadMoreStep = 30; - - loadMore = action(() => { - const { alertStore } = this.props; - - this.groupsToRender.value = Math.min( - this.groupsToRender.value + this.loadMoreStep, - alertStore.data.groups.length - ); - }); - componentDidMount() { // We have font-display:swap set for font assets, this means that on initial // render a fallback font might be used and later swapped for the final one @@ -135,16 +82,6 @@ const AlertGrid = observer( window.addEventListener("resize", this.handleResize); } - componentDidUpdate() { - const { alertStore } = this.props; - - if (this.groupsToRender.value > alertStore.data.groups.length) { - this.groupsToRender.setValue( - Math.max(this.initial, alertStore.data.groups.length) - ); - } - } - componentWillUnmount() { window.removeEventListener("resize", this.handleResize); } @@ -159,40 +96,19 @@ const AlertGrid = observer( handleHeight onResize={debounce(this.handleResize, 100)} /> - - - - } - > - {alertStore.data.groups - .slice(0, this.groupsToRender.value) - .map((group) => ( - 1 - } - afterUpdate={this.masonryRepack} - alertStore={alertStore} - settingsStore={settingsStore} - silenceFormStore={silenceFormStore} - style={{ - width: this.viewport.groupWidth, - }} - /> - ))} - + {alertStore.data.grids.map((grid) => ( + + ))} ); } diff --git a/ui/src/Components/Grid/AlertGrid/index.test.js b/ui/src/Components/Grid/AlertGrid/index.test.js index dd2628f6f..3639f9dd5 100644 --- a/ui/src/Components/Grid/AlertGrid/index.test.js +++ b/ui/src/Components/Grid/AlertGrid/index.test.js @@ -9,7 +9,8 @@ import { mockMatchMedia } from "__mocks__/matchMedia"; import { AlertStore } from "Stores/AlertStore"; import { Settings } from "Stores/Settings"; import { SilenceFormStore } from "Stores/SilenceFormStore"; -import { GetGridElementWidth } from "./GridSize"; +import { GetGridElementWidth, GridSizesConfig } from "./GridSize"; +import { Grid } from "./Grid"; import { AlertGrid } from "."; let alertStore; @@ -45,12 +46,36 @@ const ShallowAlertGrid = () => { ); }; -const MountedAlertGroup = () => { - return mount( - { + return shallow( + + ); +}; + +const MountedGrid = () => { + return mount( + ); }; @@ -83,58 +108,54 @@ const MockGroupList = (count, alertPerGroup) => { instances: [{ name: "am", uri: "http://am", error: "" }], clusters: { am: ["am"] }, }; - alertStore.data.groups = groups; + alertStore.data.grids = [ + { + labelName: "", + labelValue: "", + alertGroups: groups, + }, + ]; }; -const VerifyColumnCount = (innerWidth, outerWidth, columns) => { - MockGroupList(60, 5); - const tree = ShallowAlertGrid(); - tree.instance().viewport.updateWidths(innerWidth, outerWidth); - expect(tree.find("AlertGroup").at(0).props().style.width).toBe( - Math.floor(innerWidth / columns) - ); -}; - -describe("", () => { +describe("", () => { it("renders only first 50 alert groups", () => { MockGroupList(60, 5); - const tree = ShallowAlertGrid(); + const tree = ShallowGrid(); const alertGroups = tree.find("AlertGroup"); expect(alertGroups).toHaveLength(50); }); - it("appends 30 groups after loadMore() call", () => { + it("appends 30 groups after clicking 'Load More' button", () => { MockGroupList(100, 5); - const tree = ShallowAlertGrid(); - // call it directly, it should happen on scroll to the bottom of the page - tree.instance().loadMore(); + const tree = ShallowGrid(); + tree.find("button").simulate("click"); const alertGroups = tree.find("AlertGroup"); expect(alertGroups).toHaveLength(80); }); - it("resets groupsToRender.value back to 50 if current value is > alertStore.data.groups.length", () => { + it("resets groupsToRender.value back to 50 if current value is more than group alerts", () => { MockGroupList(100, 5); - const tree = ShallowAlertGrid(); + const tree = ShallowGrid(); expect(tree.find("AlertGroup")).toHaveLength(50); expect(tree.instance().groupsToRender.value).toBe(50); - tree.instance().loadMore(); + tree.find("button").simulate("click"); expect(tree.find("AlertGroup")).toHaveLength(80); expect(tree.instance().groupsToRender.value).toBe(80); MockGroupList(10, 5); - tree.instance().componentDidUpdate(); + tree.setProps({ gridAlertGroups: alertStore.data.grids[0].alertGroups }); expect(tree.find("AlertGroup")).toHaveLength(10); expect(tree.instance().groupsToRender.value).toBe(50); MockGroupList(100, 5); - tree.instance().componentDidUpdate(); + tree.setProps({ gridAlertGroups: alertStore.data.grids[0].alertGroups }); expect(tree.find("AlertGroup")).toHaveLength(50); expect(tree.instance().groupsToRender.value).toBe(50); }); it("calling masonryRepack() calls forcePack() on Masonry instance`", () => { - const tree = ShallowAlertGrid(); + const tree = ShallowGrid(); const instance = tree.instance(); // it's a shallow render so we don't really have masonry mounted, fake it instance.masonryComponentReference.ref = { @@ -145,28 +166,28 @@ describe("", () => { }); it("masonryRepack() doesn't crash when masonryComponentReference.ref=false`", () => { - const tree = ShallowAlertGrid(); + const tree = ShallowGrid(); const instance = tree.instance(); instance.masonryComponentReference.ref = false; instance.masonryRepack(); }); it("masonryRepack() doesn't crash when masonryComponentReference.ref=null`", () => { - const tree = ShallowAlertGrid(); + const tree = ShallowGrid(); const instance = tree.instance(); instance.masonryComponentReference.ref = null; instance.masonryRepack(); }); it("masonryRepack() doesn't crash when masonryComponentReference.ref=undefined`", () => { - const tree = ShallowAlertGrid(); + const tree = ShallowGrid(); const instance = tree.instance(); instance.masonryComponentReference.ref = undefined; instance.masonryRepack(); }); it("calling storeMasonryRef() saves the ref in local store", () => { - const tree = ShallowAlertGrid(); + const tree = ShallowGrid(); const instance = tree.instance(); instance.storeMasonryRef("foo"); expect(instance.masonryComponentReference.ref).toEqual("foo"); @@ -177,7 +198,7 @@ describe("", () => { settingsStore.gridConfig.options.disabled.value; settingsStore.gridConfig.config.reverseSort = false; MockGroupList(3, 1); - const tree = ShallowAlertGrid(); + const tree = ShallowGrid(); const alertGroups = tree.find("AlertGroup"); expect(alertGroups.map((g) => g.props().group.id)).toEqual([ "id1", @@ -186,6 +207,85 @@ describe("", () => { ]); }); + it("click on the grid toggle toggles all groups", () => { + MockGroupList(10, 3); + const tree = MountedGrid(); + tree.setProps({ + gridLabelName: "foo", + gridLabelValue: "bar", + }); + expect(tree.find("AlertGroup")).toHaveLength(10); + + tree.find("span.cursor-pointer").at(0).simulate("click"); + expect(tree.find("AlertGroup")).toHaveLength(0); + + tree.find("span.cursor-pointer").at(0).simulate("click"); + expect(tree.find("AlertGroup")).toHaveLength(10); + }); + + it("left click on a group collapse toggle only toggles clicked group", () => { + MockGroupList(10, 3); + const tree = MountedGrid(); + + for (let i = 0; i <= 9; i++) { + expect(tree.find("AlertGroup").at(i).find("Alert")).toHaveLength(3); + } + + tree + .find("AlertGroup") + .at(2) + .find("GroupHeader") + .find("span.cursor-pointer") + .at(1) + .simulate("click"); + + for (let i = 0; i <= 9; i++) { + expect(tree.find("AlertGroup").at(i).find("Alert")).toHaveLength( + i === 2 ? 0 : 3 + ); + } + }); + + it("left click + alt on a group collapse toggle toggles all groups", () => { + MockGroupList(10, 3); + const tree = MountedGrid(); + + for (let i = 0; i <= 9; i++) { + expect(tree.find("AlertGroup").at(i).find("Alert")).toHaveLength(3); + } + + tree + .find("AlertGroup") + .at(2) + .find("GroupHeader") + .find("span.cursor-pointer") + .at(1) + .simulate("click", { altKey: true }); + + for (let i = 0; i <= 9; i++) { + expect(tree.find("AlertGroup").at(i).find("Alert")).toHaveLength(0); + } + }); +}); + +describe("", () => { + const VerifyColumnCount = (innerWidth, outerWidth, columns) => { + MockGroupList(60, 5); + + const wrapper = ShallowAlertGrid(); + wrapper.instance().viewport.updateWidths(innerWidth, outerWidth); + + const tree = ShallowGrid(); + tree.setProps({ + gridSizesConfig: wrapper.instance().viewport.gridSizesConfig, + groupWidth: wrapper.instance().viewport.groupWidth, + }); + + expect(tree.find("AlertGroup").at(0).props().style.width).toBe( + Math.floor(innerWidth / columns) + ); + }; + it("doesn't throw errors after FontFaceObserver timeout", () => { MockGroupList(60, 5); ShallowAlertGrid(); @@ -261,13 +361,24 @@ describe("", () => { it("viewport resize also resizes alert groups", () => { MockGroupList(60, 5); - const tree = ShallowAlertGrid(); + + const wrapper = ShallowAlertGrid(); + const tree = ShallowGrid(); + // set initial width - tree.instance().viewport.updateWidths(1980, 1980); + wrapper.instance().viewport.updateWidths(1980, 1980); + tree.setProps({ + gridSizesConfig: wrapper.instance().viewport.gridSizesConfig, + groupWidth: wrapper.instance().viewport.groupWidth, + }); expect(tree.find("AlertGroup").at(0).props().style.width).toBe(1980 / 4); // then resize and verify if column count was changed - tree.instance().viewport.updateWidths(1000, 1000); + wrapper.instance().viewport.updateWidths(1000, 1000); + tree.setProps({ + gridSizesConfig: wrapper.instance().viewport.gridSizesConfig, + groupWidth: wrapper.instance().viewport.groupWidth, + }); expect(tree.find("AlertGroup").at(0).props().style.width).toBe(1000 / 2); }); @@ -275,13 +386,23 @@ describe("", () => { settingsStore.gridConfig.config.groupWidth = 400; MockGroupList(60, 5); - const tree = ShallowAlertGrid(); + const wrapper = ShallowAlertGrid(); + const tree = ShallowGrid(); + // set initial width - tree.instance().viewport.updateWidths(1600, 1600); + wrapper.instance().viewport.updateWidths(1600, 1600); + tree.setProps({ + gridSizesConfig: wrapper.instance().viewport.gridSizesConfig, + groupWidth: wrapper.instance().viewport.groupWidth, + }); expect(tree.find("AlertGroup").at(0).props().style.width).toBe(400); // then resize and verify if column count was changed - tree.instance().viewport.updateWidths(1584, 1600); + wrapper.instance().viewport.updateWidths(1584, 1600); + tree.setProps({ + gridSizesConfig: wrapper.instance().viewport.gridSizesConfig, + groupWidth: wrapper.instance().viewport.groupWidth, + }); expect(tree.find("AlertGroup").at(0).props().style.width).toBe(396); }); @@ -297,14 +418,20 @@ describe("", () => { it("viewport resize doesn't allow loops", () => { settingsStore.gridConfig.config.groupWidth = 400; - const tree = ShallowAlertGrid(); + + MockGroupList(60, 5); + const wrapper = ShallowAlertGrid(); + const tree = ShallowGrid(); let results = []; for (var index = 0; index < 14; index++) { - MockGroupList(60, 5); - tree + wrapper .instance() .viewport.updateWidths(index % 2 === 0 ? 1600 : 1584, 1600); + tree.setProps({ + gridSizesConfig: wrapper.instance().viewport.gridSizesConfig, + groupWidth: wrapper.instance().viewport.groupWidth, + }); results.push(tree.find("AlertGroup").at(0).props().style.width); } @@ -326,50 +453,6 @@ describe("", () => { ]); }); - it("left click on a group collapse toggle only toggles clicked group", () => { - MockGroupList(10, 3); - const tree = MountedAlertGroup(); - - for (let i = 0; i <= 9; i++) { - expect(tree.find("AlertGroup").at(i).find("Alert")).toHaveLength(3); - } - - tree - .find("AlertGroup") - .at(2) - .find("GroupHeader") - .find("span.cursor-pointer") - .at(1) - .simulate("click"); - - for (let i = 0; i <= 9; i++) { - expect(tree.find("AlertGroup").at(i).find("Alert")).toHaveLength( - i === 2 ? 0 : 3 - ); - } - }); - - it("left click + alt on a group collapse toggle toggles all groups", () => { - MockGroupList(10, 3); - const tree = MountedAlertGroup(); - - for (let i = 0; i <= 9; i++) { - expect(tree.find("AlertGroup").at(i).find("Alert")).toHaveLength(3); - } - - tree - .find("AlertGroup") - .at(2) - .find("GroupHeader") - .find("span.cursor-pointer") - .at(1) - .simulate("click", { altKey: true }); - - for (let i = 0; i <= 9; i++) { - expect(tree.find("AlertGroup").at(i).find("Alert")).toHaveLength(0); - } - }); - it("doesn't crash on unmount", () => { MockGroupList(60, 5); const tree = ShallowAlertGrid(); diff --git a/ui/src/__mocks__/Stories.js b/ui/src/__mocks__/Stories.js index b459229ea..4b9dffa4a 100644 --- a/ui/src/__mocks__/Stories.js +++ b/ui/src/__mocks__/Stories.js @@ -243,7 +243,13 @@ const MockGrid = (alertStore) => { ], clusters: { am: ["am1", "am2"], failed: ["failed"] }, }; - alertStore.data.groups = groups; + alertStore.data.grids = [ + { + labelName: "", + labelValue: "", + alertGroups: groups, + }, + ]; }; export { MockGrid };