Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: progressive dashboard loading #244

Merged
merged 7 commits into from
Feb 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
29 changes: 29 additions & 0 deletions src/__tests__/memoizeOne.spec.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
91 changes: 91 additions & 0 deletions src/components/Item/ProgressiveLoadingContainer.js
Original file line number Diff line number Diff line change
@@ -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 (
<div ref={ref => (this.containerRef = ref)} {...props}>
{shouldLoad && children}
</div>
);
}
}

export default ProgressiveLoadingContainer;
66 changes: 33 additions & 33 deletions src/components/Item/VisualizationItem/Item.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -44,6 +47,8 @@ export class Item extends Component {
showFooter: false,
};

getUniqueKey = memoizeOne(filter => uniqueId());

pluginCredentials = null;

onToggleFooter = () => {
Expand Down Expand Up @@ -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 (
<div id={elementId} className="dashboard-item-content">
<ChartPlugin
config={this.props.visualization}
filters={this.props.itemFilter}
forDashboard={true}
style={contentStyle}
/>
</div>
);
}
default: {
return (
<div
id={elementId}
className="dashboard-item-content"
style={contentStyle}
>
<DefaultPlugin {...this.props} />
</div>
);
}
}
};

getPluginComponent = () =>
this.props.item.type === CHART ? (
<ChartPlugin
config={this.props.visualization}
filters={this.props.itemFilter}
forDashboard={true}
style={this.getContentStyle()}
/>
) : (
<DefaultPlugin {...this.props} />
);

render() {
const { item, editMode } = this.props;
const { item, editMode, itemFilter } = this.props;
const { showFooter } = this.state;

return (
Expand All @@ -172,7 +161,18 @@ export class Item extends Component {
actionButtons={this.getActionButtons()}
editMode={editMode}
/>
{this.getPluginComponent()}
<ProgressiveLoadingContainer
id={getGridItemDomId(item.id)}
key={
this.getUniqueKey(
itemFilter
) /* remount the progressive loader every time itemFilter changes */
}
className="dashboard-item-content"
style={this.getContentStyle()}
>
{this.getPluginComponent()}
</ProgressiveLoadingContainer>
{!editMode && showFooter ? <ItemFooter item={item} /> : null}
</Fragment>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/ItemGrid/ItemGrid.css
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
/* dashboard item - content */

.dashboard-item-content {
padding: 0 4px 4px;
margin: 0 4px 4px;
overflow: auto;
}

Expand Down
23 changes: 23 additions & 0 deletions src/modules/memoizeOne.js
Original file line number Diff line number Diff line change
@@ -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;
5 changes: 0 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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==

[email protected]:
version "3.8.1"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.8.1.tgz#b16968a81100abe61608b0153c9159ef8bb2bd83"
Expand Down