Skip to content

Commit

Permalink
WIP: add lazy loading for dashboard (#859)
Browse files Browse the repository at this point in the history
* WIP: add lazy loading for dashboard

* Improve error handling

* WIP

* Implement lazy loading for most reports

* Add lazy loading to conversions
  • Loading branch information
ukutaht committed Mar 31, 2021
1 parent 95845fb commit a9a770d
Show file tree
Hide file tree
Showing 16 changed files with 187 additions and 108 deletions.
13 changes: 12 additions & 1 deletion assets/js/dashboard/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ import {formatISO} from './date'
let abortController = new AbortController()
let SHARED_LINK_AUTH = null

class ApiError extends Error {
constructor(message) {
super(message);
this.name = "ApiError";
}
}

function serialize(obj) {
var str = [];
for (var p in obj)
Expand Down Expand Up @@ -44,7 +51,11 @@ export function get(url, query, ...extraQuery) {
url = url + serializeQuery(query, extraQuery)
return fetch(url, {signal: abortController.signal, headers: headers})
.then( response => {
if (!response.ok) { throw response }
if (!response.ok) {
return response.json().then((msg) => {
throw new ApiError(msg.error)
})
}
return response.json()
})
}
2 changes: 1 addition & 1 deletion assets/js/dashboard/historical.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,4 @@ class Historical extends React.Component {
}
}

export default withPinnedHeader(Historical, 'historical');
export default withPinnedHeader(Historical, '#stats-container-top');
30 changes: 30 additions & 0 deletions assets/js/dashboard/lazy-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';

export default class extends React.Component {
constructor(props) {
super(props)
}

componentDidMount() {
this.observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
this.props.onVisible && this.props.onVisible()
this.observer.unobserve(this.element);
}
}, {
threshold: 0
});

this.observer.observe(this.element);
}

componentWillUnmount() {
this.observer.unobserve(this.element);
}

render() {
return (
<div ref={(el) => this.element = el} className={this.props.className} style={this.props.style}>{this.props.children}</div>
);
}
}
2 changes: 1 addition & 1 deletion assets/js/dashboard/realtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,4 @@ class Realtime extends React.Component {
}
}

export default withPinnedHeader(Realtime, 'realtime');
export default withPinnedHeader(Realtime);
36 changes: 21 additions & 15 deletions assets/js/dashboard/stats/conversions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ import MoreLink from '../more-link'
import PropBreakdown from './prop-breakdown'
import numberFormatter from '../../number-formatter'
import * as api from '../../api'
import LazyLoader from '../../lazy-loader'

export default class Conversions extends React.Component {
constructor(props) {
super(props)
this.state = {loading: true}
this.onVisible = this.onVisible.bind(this)
}

componentDidMount() {
onVisible() {
this.fetchConversions()
}

Expand All @@ -37,7 +39,7 @@ export default class Conversions extends React.Component {
query.set('goal', goalName)

return (
<Link to={{pathname: window.location.pathname, search: query.toString()}} style={{marginTop: '-26px'}} className="hover:underline block px-2">
<Link to={{pathname: window.location.pathname, search: query.toString()}} style={{marginTop: '-26px'}} className="block px-2 hover:underline">
{ goalName }
</Link>
)
Expand All @@ -50,33 +52,29 @@ export default class Conversions extends React.Component {
return (
<div className="my-2 text-sm" key={goal.name}>
<div className="flex items-center justify-between my-2">
<div className="w-full h-8 relative dark:text-gray-300" style={{maxWidth: 'calc(100% - 16rem)'}}>
<div className="relative w-full h-8 dark:text-gray-300" style={{maxWidth: 'calc(100% - 16rem)'}}>
<Bar count={goal.count} all={this.state.goals} bg="bg-red-50 dark:bg-gray-500 dark:bg-opacity-15" />
{this.renderGoalText(goal.name)}
</div>
<div className="dark:text-gray-200">
<span className="font-medium inline-block w-20 text-right">{numberFormatter(goal.count)}</span>
<span className="font-medium inline-block w-20 text-right">{numberFormatter(goal.total_count)}</span>
<span className="font-medium inline-block w-20 text-right">{goal.conversion_rate}%</span>
<span className="inline-block w-20 font-medium text-right">{numberFormatter(goal.count)}</span>
<span className="inline-block w-20 font-medium text-right">{numberFormatter(goal.total_count)}</span>
<span className="inline-block w-20 font-medium text-right">{goal.conversion_rate}%</span>
</div>
</div>
{ renderProps && <PropBreakdown site={this.props.site} query={this.props.query} goal={goal} /> }
</div>
)
}

render() {
renderInner() {
if (this.state.loading) {
return (
<div className="w-full bg-white dark:bg-gray-825 shadow-xl rounded p-4" style={{height: '94px'}}>
<div className="loading my-2 mx-auto"><div></div></div>
</div>
)
return <div className="mx-auto my-2 loading"><div></div></div>
} else if (this.state.goals) {
return (
<div className="w-full bg-white dark:bg-gray-825 shadow-xl rounded p-4">
<React.Fragment>
<h3 className="font-bold dark:text-gray-100">{this.props.title || "Goal Conversions"}</h3>
<div className="flex items-center mt-3 mb-2 justify-between text-gray-500 dark:text-gray-400 text-xs font-bold tracking-wide">
<div className="flex items-center justify-between mt-3 mb-2 text-xs font-bold tracking-wide text-gray-500 dark:text-gray-400">
<span>Goal</span>
<div className="text-right">
<span className="inline-block w-20">Uniques</span>
Expand All @@ -86,8 +84,16 @@ export default class Conversions extends React.Component {
</div>

{ this.state.goals.map(this.renderGoal.bind(this)) }
</div>
</React.Fragment>
)
}
}

render() {
return (
<LazyLoader className="w-full p-4 bg-white rounded shadow-xl dark:bg-gray-825" style={{minHeight: '94px'}} onVisible={this.onVisible}>
{ this.renderInner() }
</LazyLoader>
)
}
}
19 changes: 12 additions & 7 deletions assets/js/dashboard/stats/countries.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,24 @@ import { withRouter } from 'react-router-dom'

import numberFormatter from '../number-formatter'
import FadeIn from '../fade-in'
import LazyLoader from '../lazy-loader'
import Bar from './bar'
import MoreLink from './more-link'
import * as api from '../api'
import { navigateToQuery } from '../query'
import { withThemeConsumer } from '../theme-consumer-hoc';

class Countries extends React.Component {
constructor(props) {
super(props)
this.resizeMap = this.resizeMap.bind(this)
this.drawMap = this.drawMap.bind(this)
this.getDataset = this.getDataset.bind(this)
this.state = {loading: true}
this.onVisible = this.onVisible.bind(this)
}

componentDidMount() {
onVisible() {
this.fetchCountries().then(this.drawMap.bind(this))
window.addEventListener('resize', this.resizeMap);
if (this.props.timer) this.props.timer.onTick(this.updateCountries.bind(this))
Expand Down Expand Up @@ -127,7 +130,7 @@ class Countries extends React.Component {
return (
<React.Fragment>
<h3 className="font-bold dark:text-gray-100">Countries</h3>
<div className="mt-6 mx-auto" style={{width: '100%', maxWidth: '475px', height: '320px'}} id="map-container"></div>
<div className="mx-auto mt-6" style={{width: '100%', maxWidth: '475px', height: '320px'}} id="map-container"></div>
<MoreLink site={this.props.site} list={this.state.countries} endpoint="countries" />
</React.Fragment>
)
Expand All @@ -136,11 +139,13 @@ class Countries extends React.Component {

render() {
return (
<div className="stats-item relative bg-white dark:bg-gray-825 shadow-xl rounded p-4" style={{height: '436px'}}>
{ this.state.loading && <div className="loading my-32 mx-auto"><div></div></div> }
<FadeIn show={!this.state.loading}>
{ this.renderBody() }
</FadeIn>
<div className="relative p-4 bg-white rounded shadow-xl stats-item dark:bg-gray-825" style={{height: '436px'}}>
<LazyLoader onVisible={this.onVisible}>
{ this.state.loading && <div className="mx-auto my-32 loading"><div></div></div> }
<FadeIn show={!this.state.loading}>
{ this.renderBody() }
</FadeIn>
</LazyLoader>
</div>
)
}
Expand Down
16 changes: 9 additions & 7 deletions assets/js/dashboard/stats/devices/browsers.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ import FadeIn from '../../fade-in'
import numberFormatter from '../../number-formatter'
import Bar from '../bar'
import * as api from '../../api'
import LazyLoader from '../../lazy-loader'

export default class Browsers extends React.Component {
constructor(props) {
super(props)
this.state = {loading: true}
this.onVisible = this.onVisible.bind(this)
}

componentDidMount() {
onVisible() {
this.fetchBrowsers()
if (this.props.timer) this.props.timer.onTick(this.fetchBrowsers.bind(this))
}
Expand Down Expand Up @@ -52,7 +54,7 @@ export default class Browsers extends React.Component {
</Link>
</span>
</div>
<span className="font-medium dark:text-gray-200">{numberFormatter(browser.count)} <span className="inline-block text-xs w-8 text-right">({browser.percentage}%)</span></span>
<span className="font-medium dark:text-gray-200">{numberFormatter(browser.count)} <span className="inline-block w-8 text-xs text-right">({browser.percentage}%)</span></span>
</div>
)
}
Expand All @@ -68,26 +70,26 @@ export default class Browsers extends React.Component {
if (this.state.browsers && this.state.browsers.length > 0) {
return (
<React.Fragment>
<div className="flex items-center mt-3 mb-2 justify-between text-gray-500 dark:text-gray-400 text-xs font-bold tracking-wide">
<div className="flex items-center justify-between mt-3 mb-2 text-xs font-bold tracking-wide text-gray-500 dark:text-gray-400">
<span>{ key }</span>
<span>{ this.label() }</span>
</div>
{ this.state.browsers && this.state.browsers.map(this.renderBrowser.bind(this)) }
</React.Fragment>
)
} else {
return <div className="text-center mt-44 font-medium text-gray-500 dark:text-gray-400">No data yet</div>
return <div className="font-medium text-center text-gray-500 mt-44 dark:text-gray-400">No data yet</div>
}
}

render() {
return (
<React.Fragment>
{ this.state.loading && <div className="loading mt-44 mx-auto"><div></div></div> }
<LazyLoader onVisible={this.onVisible}>
{ this.state.loading && <div className="mx-auto loading mt-44"><div></div></div> }
<FadeIn show={!this.state.loading}>
{ this.renderList() }
</FadeIn>
</React.Fragment>
</LazyLoader>
)
}
}
Expand Down
34 changes: 18 additions & 16 deletions assets/js/dashboard/stats/devices/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import { Link } from 'react-router-dom'

import LazyLoader from '../../lazy-loader'
import Browsers from './browsers'
import OperatingSystems from './operating-systems'
import FadeIn from '../../fade-in'
Expand All @@ -20,19 +21,19 @@ const EXPLANATION = {
function iconFor(screenSize) {
if (screenSize === 'Mobile') {
return (
<svg width="16px" height="16px" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather -mt-px"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12" y2="18"/></svg>
<svg width="16px" height="16px" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="-mt-px feather"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12" y2="18"/></svg>
)
} else if (screenSize === 'Tablet') {
return (
<svg width="16px" height="16px" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather -mt-px"><rect x="4" y="2" width="16" height="20" rx="2" ry="2" transform="rotate(180 12 12)"/><line x1="12" y1="18" x2="12" y2="18"/></svg>
<svg width="16px" height="16px" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="-mt-px feather"><rect x="4" y="2" width="16" height="20" rx="2" ry="2" transform="rotate(180 12 12)"/><line x1="12" y1="18" x2="12" y2="18"/></svg>
)
} else if (screenSize === 'Laptop') {
return (
<svg width="16px" height="16px" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather -mt-px"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="2" y1="20" x2="22" y2="20"/></svg>
<svg width="16px" height="16px" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="-mt-px feather"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="2" y1="20" x2="22" y2="20"/></svg>
)
} else if (screenSize === 'Desktop') {
return (
<svg width="16px" height="16px" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather -mt-px"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
<svg width="16px" height="16px" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="-mt-px feather"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
)
}
}
Expand All @@ -41,9 +42,10 @@ class ScreenSizes extends React.Component {
constructor(props) {
super(props)
this.state = {loading: true}
this.onVisible = this.onVisible.bind(this)
}

componentDidMount() {
onVisible() {
this.fetchScreenSizes()
if (this.props.timer) this.props.timer.onTick(this.fetchScreenSizes.bind(this))
}
Expand Down Expand Up @@ -74,7 +76,7 @@ class ScreenSizes extends React.Component {
</Link>
</span>
</div>
<span className="font-medium dark:text-gray-200">{numberFormatter(size.count)} <span className="inline-block text-xs w-8 text-right">({size.percentage}%)</span></span>
<span className="font-medium dark:text-gray-200">{numberFormatter(size.count)} <span className="inline-block w-8 text-xs text-right">({size.percentage}%)</span></span>
</div>
)
}
Expand All @@ -87,26 +89,26 @@ class ScreenSizes extends React.Component {
if (this.state.sizes && this.state.sizes.length > 0) {
return (
<React.Fragment>
<div className="flex items-center mt-3 mb-2 justify-between text-gray-500 text-xs font-bold tracking-wide">
<div className="flex items-center justify-between mt-3 mb-2 text-xs font-bold tracking-wide text-gray-500">
<span>Screen size</span>
<span>{ this.label() }</span>
</div>
{ this.state.sizes && this.state.sizes.map(this.renderScreenSize.bind(this)) }
</React.Fragment>
)
} else {
return <div className="text-center mt-44 font-medium text-gray-500 dark:text-gray-400">No data yet</div>
return <div className="font-medium text-center text-gray-500 mt-44 dark:text-gray-400">No data yet</div>
}
}

render() {
return (
<React.Fragment>
{ this.state.loading && <div className="loading mt-44 mx-auto"><div></div></div> }
<LazyLoader onVisible={this.onVisible}>
{ this.state.loading && <div className="mx-auto loading mt-44"><div></div></div> }
<FadeIn show={!this.state.loading}>
{ this.renderList() }
</FadeIn>
</React.Fragment>
</LazyLoader>
)
}
}
Expand Down Expand Up @@ -142,21 +144,21 @@ export default class Devices extends React.Component {
const isActive = this.state.mode === mode

if (isActive) {
return <li className="inline-block h-5 text-indigo-700 dark:text-indigo-500 font-bold border-b-2 border-indigo-700 dark:border-indigo-500">{name}</li>
return <li className="inline-block h-5 font-bold text-indigo-700 border-b-2 border-indigo-700 dark:text-indigo-500 dark:border-indigo-500">{name}</li>
} else {
return <li className="hover:text-indigo-600 cursor-pointer" onClick={this.setMode(mode)}>{name}</li>
return <li className="cursor-pointer hover:text-indigo-600" onClick={this.setMode(mode)}>{name}</li>
}
}

render() {
return (
<div className="stats-item">
<div className="bg-white dark:bg-gray-825 shadow-xl rounded p-4 relative" style={{height: '436px'}}>
<div className="relative p-4 bg-white rounded shadow-xl dark:bg-gray-825" style={{height: '436px'}}>

<div className="w-full flex justify-between">
<div className="flex justify-between w-full">
<h3 className="font-bold dark:text-gray-100">Devices</h3>

<ul className="flex font-medium text-xs text-gray-500 dark:text-gray-400 space-x-2">
<ul className="flex text-xs font-medium text-gray-500 dark:text-gray-400 space-x-2">
{ this.renderPill('Size', 'size') }
{ this.renderPill('Browser', 'browser') }
{ this.renderPill('OS', 'os') }
Expand Down
Loading

0 comments on commit a9a770d

Please sign in to comment.