Skip to content

Commit

Permalink
Merge pull request #1485 from weaveworks/loading-indicator
Browse files Browse the repository at this point in the history
Loading indicator
  • Loading branch information
foot authored Aug 3, 2016
2 parents e42444c + 19f08ee commit e7a0a96
Show file tree
Hide file tree
Showing 10 changed files with 208 additions and 18 deletions.
7 changes: 7 additions & 0 deletions client/app/scripts/actions/app-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,13 @@ export function receiveNodeDetails(details) {

export function receiveNodesDelta(delta) {
return (dispatch, getState) => {
//
// allow css-animation to run smoothly by scheduling it to run on the
// next tick after any potentially expensive canvas re-draws have been
// completed.
//
setTimeout(() => dispatch({ type: ActionTypes.SET_RECEIVED_NODES_DELTA }), 0);

if (delta.add || delta.update || delta.remove) {
const state = getState();
if (state.get('updatePausedAt') !== null) {
Expand Down
19 changes: 11 additions & 8 deletions client/app/scripts/charts/nodes-error.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import React from 'react';
import classnames from 'classnames';

export default function NodesError({children, faIconClass, hidden}) {
let classNames = 'nodes-chart-error';
if (hidden) {
classNames += ' hide';
}
export default function NodesError({children, faIconClass, hidden,
mainClassName = 'nodes-chart-error'}) {
const className = classnames(mainClassName, {
hide: hidden
});
const iconClassName = `fa ${faIconClass}`;

return (
<div className={classNames}>
<div className="nodes-chart-error-icon">
<span className={iconClassName} />
<div className={className}>
<div className="nodes-chart-error-icon-container">
<div className="nodes-chart-error-icon">
<span className={iconClassName} />
</div>
</div>
{children}
</div>
Expand Down
30 changes: 29 additions & 1 deletion client/app/scripts/components/debug-toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { fromJS } from 'immutable';
import debug from 'debug';
const log = debug('scope:debug-panel');

import ActionTypes from '../constants/action-types';
import { receiveNodesDelta } from '../actions/app-actions';
import { getNodeColor, getNodeColorDark, text2degree } from '../utils/color-utils';

Expand Down Expand Up @@ -111,11 +112,13 @@ function stopPerf() {
Perf.printWasted(measurements);
}


function startPerf(delay) {
Perf.start();
setTimeout(stopPerf, delay * 1000);
}


export function showingDebugToolbar() {
return (('debugToolbar' in localStorage && JSON.parse(localStorage.debugToolbar))
|| location.pathname.indexOf('debug') > -1);
Expand All @@ -134,11 +137,23 @@ function enableLog(ns) {
window.location.reload();
}


function disableLog() {
debug.disable();
window.location.reload();
}


function setAppState(fn) {
return (dispatch) => {
dispatch({
type: ActionTypes.DEBUG_TOOLBAR_INTERFERING,
fn
});
};
}


class DebugToolbar extends React.Component {

constructor(props, context) {
Expand All @@ -162,6 +177,10 @@ class DebugToolbar extends React.Component {
});
}

setLoading(loading) {
this.props.setAppState(state => state.set('topologiesLoaded', !loading));
}

addNodes(n, prefix = 'zing') {
const ns = this.props.nodes;
const nodeNames = ns.keySeq().toJS();
Expand Down Expand Up @@ -243,6 +262,12 @@ class DebugToolbar extends React.Component {
</table>
))}

<div>
<label>state</label>
<button onClick={() => this.setLoading(true)}>Set doing initial load</button>
<button onClick={() => this.setLoading(false)}>Stop</button>
</div>

<div>
<label>Measure React perf for </label>
<button onClick={() => startPerf(2)}>2s</button>
Expand All @@ -254,13 +279,16 @@ class DebugToolbar extends React.Component {
}
}


function mapStateToProps(state) {
return {
nodes: state.get('nodes'),
availableCanvasMetrics: state.get('availableCanvasMetrics')
};
}


export default connect(
mapStateToProps
mapStateToProps,
{setAppState}
)(DebugToolbar);
58 changes: 58 additions & 0 deletions client/app/scripts/components/loading.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from 'react';
import _ from 'lodash';

import { findTopologyById } from '../utils/topology-utils';
import NodesError from '../charts/nodes-error';


const LOADING_TEMPLATES = [
'Loading THINGS',
'Verifying THINGS',
'Fetching THINGS',
'Processing THINGS',
'Reticulating THINGS',
'Locating THINGS',
'Optimizing THINGS',
'Transporting THINGS',
];


export function getNodeType(topology, topologies) {
if (!topology || topologies.size === 0) {
return '';
}
let name = topology.get('name');
if (topology.get('parentId')) {
const parentTopology = findTopologyById(topologies, topology.get('parentId'));
name = parentTopology.get('name');
}
return name.toLowerCase();
}


function renderTemplate(nodeType, template) {
return template.replace('THINGS', nodeType);
}


export class Loading extends React.Component {

constructor(props, context) {
super(props, context);

this.state = {
template: _.sample(LOADING_TEMPLATES)
};
}

render() {
const { itemType, show } = this.props;
const message = renderTemplate(itemType, this.state.template);
return (
<NodesError mainClassName="nodes-chart-loading" faIconClass="fa-circle-thin" hidden={!show}>
<div className="heading">{message}</div>
</NodesError>
);
}

}
21 changes: 17 additions & 4 deletions client/app/scripts/components/nodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import { connect } from 'react-redux';

import NodesChart from '../charts/nodes-chart';
import NodesError from '../charts/nodes-error';
import { DelayedShow } from '../utils/delayed-show';
import { Loading, getNodeType } from './loading';
import { isTopologyEmpty } from '../utils/topology-utils';

const navbarHeight = 160;
const marginTop = 0;
const detailsWidth = 450;


/**
* dynamic coords precision based on topology size
*/
Expand All @@ -34,7 +37,7 @@ class Nodes extends React.Component {

this.state = {
width: window.innerWidth,
height: window.innerHeight - navbarHeight - marginTop
height: window.innerHeight - navbarHeight - marginTop,
};
}

Expand Down Expand Up @@ -63,14 +66,20 @@ class Nodes extends React.Component {
}

render() {
const { nodes, selectedNodeId, topologyEmpty } = this.props;
const { nodes, selectedNodeId, topologyEmpty, topologiesLoaded, nodesLoaded, topologies,
topology } = this.props;
const layoutPrecision = getLayoutPrecision(nodes.size);
const hasSelectedNode = selectedNodeId && nodes.has(selectedNodeId);
const errorEmpty = this.renderEmptyTopologyError(topologyEmpty);

return (
<div className="nodes-wrapper">
{topologyEmpty && errorEmpty}
<DelayedShow delay={1000} show={!topologiesLoaded || (topologiesLoaded && !nodesLoaded)}>
<Loading itemType="topologies" show={!topologiesLoaded} />
<Loading
itemType={getNodeType(topology, topologies)}
show={topologiesLoaded && !nodesLoaded} />
</DelayedShow>
{this.renderEmptyTopologyError(topologiesLoaded && nodesLoaded && topologyEmpty)}
<NodesChart {...this.state}
detailsWidth={detailsWidth}
layoutPrecision={layoutPrecision}
Expand All @@ -95,8 +104,12 @@ class Nodes extends React.Component {
function mapStateToProps(state) {
return {
nodes: state.get('nodes'),
nodesLoaded: state.get('nodesLoaded'),
selectedNodeId: state.get('selectedNodeId'),
topologies: state.get('topologies'),
topologiesLoaded: state.get('topologiesLoaded'),
topologyEmpty: isTopologyEmpty(state),
topology: state.get('currentTopology'),
};
}

Expand Down
3 changes: 1 addition & 2 deletions client/app/scripts/components/status.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import { connect } from 'react-redux';

class Status extends React.Component {
render() {
const {errorUrl, filteredNodeCount, topologiesLoaded, topology,
websocketClosed} = this.props;
const {errorUrl, topologiesLoaded, filteredNodeCount, topology, websocketClosed} = this.props;

let title = '';
let text = 'Trying to reconnect...';
Expand Down
2 changes: 2 additions & 0 deletions client/app/scripts/constants/action-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const ACTION_TYPES = [
'CLICK_TERMINAL',
'CLICK_TOPOLOGY',
'CLOSE_WEBSOCKET',
'DEBUG_TOOLBAR_INTERFERING',
'DESELECT_NODE',
'DO_CONTROL',
'DO_CONTROL_ERROR',
Expand Down Expand Up @@ -53,6 +54,7 @@ const ACTION_TYPES = [
'PIN_NETWORK',
'UNPIN_NETWORK',
'SHOW_NETWORKS',
'SET_RECEIVED_NODES_DELTA',
];

export default _.zipObject(ACTION_TYPES, ACTION_TYPES);
11 changes: 10 additions & 1 deletion client/app/scripts/reducers/root.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const initialState = makeMap({
networkNodes: makeMap(),
nodeDetails: makeOrderedMap(), // nodeId -> details
nodes: makeOrderedMap(), // nodeId -> node
nodesLoaded: false,
// nodes cache, infrequently updated, used for search
nodesByTopology: makeMap(), // topologyId -> nodes
pinnedMetric: null,
Expand All @@ -61,7 +62,7 @@ export const initialState = makeMap({
updatePausedAt: null, // Date
version: '...',
versionUpdate: null,
websocketClosed: true,
websocketClosed: false,
exportingGraph: false
});

Expand Down Expand Up @@ -480,6 +481,10 @@ export function rootReducer(state = initialState, action) {
return state;
}

case ActionTypes.SET_RECEIVED_NODES_DELTA: {
return state.set('nodesLoaded', true);
}

case ActionTypes.RECEIVE_NODES_DELTA: {
const emptyMessage = !action.delta.add && !action.delta.remove
&& !action.delta.update;
Expand Down Expand Up @@ -656,6 +661,10 @@ export function rootReducer(state = initialState, action) {
return applyPinnedSearches(state);
}

case ActionTypes.DEBUG_TOOLBAR_INTERFERING: {
return action.fn(state);
}

default: {
return state;
}
Expand Down
61 changes: 61 additions & 0 deletions client/app/scripts/utils/delayed-show.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React from 'react';


export class DelayedShow extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
show: false
};
}

componentWillMount() {
if (this.props.show) {
this.scheduleShow();
}
}

componentWillUnmount() {
this.cancelShow();
}

componentWillReceiveProps(nextProps) {
if (nextProps.show === this.props.show) {
return;
}

if (nextProps.show) {
this.scheduleShow();
} else {
this.cancelShow();
this.setState({ show: false });
}
}

scheduleShow() {
this.showTimeout = setTimeout(() => this.setState({ show: true }), this.props.delay);
}

cancelShow() {
clearTimeout(this.showTimeout);
}

render() {
const { children } = this.props;
const { show } = this.state;
const style = {
opacity: show ? 1 : 0,
transition: 'opacity 0.5s ease-in-out',
};
return (
<div style={style}>
{children}
</div>
);
}
}


DelayedShow.defaultProps = {
delay: 1000
};
Loading

0 comments on commit e7a0a96

Please sign in to comment.