From 19bd63b5f8f0774bd1e7233d82c5e54eca56754e Mon Sep 17 00:00:00 2001 From: Austin McGee <947888+amcgee@users.noreply.github.com> Date: Tue, 19 Feb 2019 11:00:43 +0000 Subject: [PATCH] feat: progressive dashboard loading (#244) This restricts the loading of dashboard items to the current viewport, with a buffer of one half-viewport above and below. As such, only 1.5x the viewport is initially loaded. Other items will be loaded when the user scrolls them into this buffered viewport. * feat: progressive dashboard loading * feat: support initial buffer factor * feat: support progressive loading after a filter change * fix: use margin instead of padding to prevent overflow * Move memoize to its own file * chore: remove unused import --- package.json | 1 + src/__tests__/memoizeOne.spec.js | 29 ++++++ .../Item/ProgressiveLoadingContainer.js | 91 +++++++++++++++++++ src/components/Item/VisualizationItem/Item.js | 66 +++++++------- src/components/ItemGrid/ItemGrid.css | 2 +- src/modules/memoizeOne.js | 23 +++++ yarn.lock | 5 - 7 files changed, 178 insertions(+), 39 deletions(-) create mode 100644 src/__tests__/memoizeOne.spec.js create mode 100644 src/components/Item/ProgressiveLoadingContainer.js create mode 100644 src/modules/memoizeOne.js diff --git a/package.json b/package.json index e94888e4a..f9985fc5e 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "jest": "^23.6.0", "jsdoc": "^3.5.5", "lint-staged": "^7.0.0", + "lodash": "^4.17.11", "material-design-icons": "^3.0.1", "material-ui": "^0.20.0", "object-assign": "4.1.1", diff --git a/src/__tests__/memoizeOne.spec.js b/src/__tests__/memoizeOne.spec.js new file mode 100644 index 000000000..b23b9f63b --- /dev/null +++ b/src/__tests__/memoizeOne.spec.js @@ -0,0 +1,29 @@ +import memoizeOne from '../modules/memoizeOne'; + +describe('memoizeOne', () => { + it('Should return the same value when called twice with shallow-equal arguments', () => { + const object = { a: 0, b: 0 }; + const fn = x => x.a + x.b; + const memoizedFn = memoizeOne(fn); + + const val0 = memoizedFn(object); + expect(val0).toBe(0); + object.a = 1; // maintain shallow equality + expect(fn(object)).toBe(1); + const val1 = memoizedFn(object); + expect(val1).toBe(0); + }); + + it('Should forget the first value when called thrice', () => { + const object = { a: 0, b: 0 }; + const memoizedFn = memoizeOne(x => x.a + x.b); + + const val0 = memoizedFn(object); + expect(val0).toBe(0); + object.a = 1; // maintain shallow equality + const val1 = memoizedFn({ a: 42, b: 84 }); // This will bump val0 out of the cache + expect(val1).toBe(42 + 84); + const val2 = memoizedFn(object); + expect(val2).toBe(1); + }); +}); diff --git a/src/components/Item/ProgressiveLoadingContainer.js b/src/components/Item/ProgressiveLoadingContainer.js new file mode 100644 index 000000000..b26aa5536 --- /dev/null +++ b/src/components/Item/ProgressiveLoadingContainer.js @@ -0,0 +1,91 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import debounce from 'lodash/debounce'; + +const defaultDebounceMs = 100; +const defaultBufferPx = 0; +const defaultInitialBufferFactor = 0.5; + +class ProgressiveLoadingContainer extends Component { + static propTypes = { + children: PropTypes.node.isRequired, + debounceMs: PropTypes.number, + bufferPx: PropTypes.number, + initialBufferFactor: PropTypes.number, + }; + static defaultProps = { + debounceMs: defaultDebounceMs, + bufferPx: defaultBufferPx, + initialBufferFactor: defaultInitialBufferFactor, + }; + + state = { + shouldLoad: false, + }; + containerRef = null; + shouldLoadHandler = null; + + checkShouldLoad(customBufferPx) { + const bufferPx = customBufferPx || this.props.bufferPx; + + if (!this.containerRef) { + return; + } + + const rect = this.containerRef.getBoundingClientRect(); + if ( + rect.bottom > -bufferPx && + rect.top < window.innerHeight + bufferPx + ) { + this.setState({ + shouldLoad: true, + }); + + this.removeHandler(); + } + } + + registerHandler() { + this.shouldLoadHandler = debounce( + () => this.checkShouldLoad(), + this.props.debounceMs + ); + + window.addEventListener('scroll', this.shouldLoadHandler); + } + removeHandler() { + window.removeEventListener('scroll', this.shouldLoadHandler); + } + + componentDidMount() { + this.registerHandler(); + + const initialBufferPx = this.props.initialBufferFactor + ? this.props.initialBufferFactor * window.innerHeight + : undefined; + this.checkShouldLoad(initialBufferPx); + } + + componentWillUnmount() { + this.removeHandler(); + } + + render() { + const { + children, + debounceMs, + bufferPx, + initialBufferFactor, + ...props + } = this.props; + const { shouldLoad } = this.state; + + return ( +
(this.containerRef = ref)} {...props}> + {shouldLoad && children} +
+ ); + } +} + +export default ProgressiveLoadingContainer; diff --git a/src/components/Item/VisualizationItem/Item.js b/src/components/Item/VisualizationItem/Item.js index e84111fdf..6650cf028 100644 --- a/src/components/Item/VisualizationItem/Item.js +++ b/src/components/Item/VisualizationItem/Item.js @@ -16,6 +16,9 @@ import VisualizationItemHeaderButtons from './ItemHeaderButtons'; import DefaultPlugin from './DefaultPlugin'; import { colors } from '../../../modules/colors'; import ChartPlugin from 'data-visualizer-plugin'; +import ProgressiveLoadingContainer from '../ProgressiveLoadingContainer'; +import uniqueId from 'lodash/uniqueId'; +import memoizeOne from '../../../modules/memoizeOne'; const styles = { icon: { @@ -44,6 +47,8 @@ export class Item extends Component { showFooter: false, }; + getUniqueKey = memoizeOne(filter => uniqueId()); + pluginCredentials = null; onToggleFooter = () => { @@ -123,46 +128,30 @@ export class Item extends Component { /> ) : null; - getPluginComponent = () => { - const { item } = this.props; - const elementId = getGridItemDomId(item.id); - + getContentStyle = () => { + const { item, editMode } = this.props; const PADDING_BOTTOM = 4; - const contentStyle = !this.props.editMode + return !editMode ? { height: item.originalHeight - HEADER_HEIGHT - PADDING_BOTTOM, } : null; - - switch (item.type) { - case CHART: { - return ( -
- -
- ); - } - default: { - return ( -
- -
- ); - } - } }; + getPluginComponent = () => + this.props.item.type === CHART ? ( + + ) : ( + + ); + render() { - const { item, editMode } = this.props; + const { item, editMode, itemFilter } = this.props; const { showFooter } = this.state; return ( @@ -172,7 +161,18 @@ export class Item extends Component { actionButtons={this.getActionButtons()} editMode={editMode} /> - {this.getPluginComponent()} + + {this.getPluginComponent()} + {!editMode && showFooter ? : null} ); diff --git a/src/components/ItemGrid/ItemGrid.css b/src/components/ItemGrid/ItemGrid.css index 5ee208939..3996f18dc 100644 --- a/src/components/ItemGrid/ItemGrid.css +++ b/src/components/ItemGrid/ItemGrid.css @@ -71,7 +71,7 @@ /* dashboard item - content */ .dashboard-item-content { - padding: 0 4px 4px; + margin: 0 4px 4px; overflow: auto; } diff --git a/src/modules/memoizeOne.js b/src/modules/memoizeOne.js new file mode 100644 index 000000000..4e42a1ab5 --- /dev/null +++ b/src/modules/memoizeOne.js @@ -0,0 +1,23 @@ +// Note that this ignores discrepancies in 'this', so shouldn't be used with bound functions +// This is useful instead of lodash/memoize when we only need to memoize a single value +// Inspiration: https://github.com/alexreardon/memoize-one + +const memoizeOne = fn => { + let lastArgs = undefined; + let lastValue = undefined; + + return (...args) => { + if ( + lastArgs && + args.length === lastArgs.length && + args.every((arg, i) => arg === lastArgs[i]) + ) { + return lastValue; + } + lastArgs = args; + lastValue = fn(...args); + return lastValue; + }; +}; + +export default memoizeOne; diff --git a/yarn.lock b/yarn.lock index aa72adf83..a298184b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11069,11 +11069,6 @@ webpack-sources@^1.0.1: source-list-map "^2.0.0" source-map "~0.6.1" -webpack-stats-plugin@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/webpack-stats-plugin/-/webpack-stats-plugin-0.2.1.tgz#1f5bac13fc25d62cbb5fd0ff646757dc802b8595" - integrity sha512-OYMZLpZrK/qLA79NE4kC4DCt85h/5ipvWJcsefKe9MMw0qU4/ck/IJg+4OmWA+5EfrZZpHXDq92IptfYDWVfkw== - webpack@3.8.1: version "3.8.1" resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.8.1.tgz#b16968a81100abe61608b0153c9159ef8bb2bd83"