diff --git a/client/app/scripts/components/debug-toolbar.js b/client/app/scripts/components/debug-toolbar.js
index 97145ff7c..ae61fff59 100644
--- a/client/app/scripts/components/debug-toolbar.js
+++ b/client/app/scripts/components/debug-toolbar.js
@@ -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';
@@ -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);
@@ -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) {
@@ -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();
@@ -243,6 +262,12 @@ class DebugToolbar extends React.Component {
))}
+
@@ -254,6 +279,7 @@ class DebugToolbar extends React.Component {
}
}
+
function mapStateToProps(state) {
return {
nodes: state.get('nodes'),
@@ -261,6 +287,8 @@ function mapStateToProps(state) {
};
}
+
export default connect(
- mapStateToProps
+ mapStateToProps,
+ {setAppState}
)(DebugToolbar);
diff --git a/client/app/scripts/components/loading.js b/client/app/scripts/components/loading.js
new file mode 100644
index 000000000..2696f46b1
--- /dev/null
+++ b/client/app/scripts/components/loading.js
@@ -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 (
+
+ {message}
+
+ );
+ }
+
+}
diff --git a/client/app/scripts/components/nodes.js b/client/app/scripts/components/nodes.js
index d2ce010be..38ae981c2 100644
--- a/client/app/scripts/components/nodes.js
+++ b/client/app/scripts/components/nodes.js
@@ -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
*/
@@ -34,7 +37,7 @@ class Nodes extends React.Component {
this.state = {
width: window.innerWidth,
- height: window.innerHeight - navbarHeight - marginTop
+ height: window.innerHeight - navbarHeight - marginTop,
};
}
@@ -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 (
- {topologyEmpty && errorEmpty}
+
+
+
+
+ {this.renderEmptyTopologyError(topologiesLoaded && nodesLoaded && topologyEmpty)}
details
nodes: makeOrderedMap(), // nodeId -> node
+ nodesLoaded: false,
// nodes cache, infrequently updated, used for search
nodesByTopology: makeMap(), // topologyId -> nodes
pinnedMetric: null,
@@ -61,7 +62,7 @@ export const initialState = makeMap({
updatePausedAt: null, // Date
version: '...',
versionUpdate: null,
- websocketClosed: true,
+ websocketClosed: false,
exportingGraph: false
});
@@ -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;
@@ -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;
}
diff --git a/client/app/scripts/utils/delayed-show.js b/client/app/scripts/utils/delayed-show.js
new file mode 100644
index 000000000..fb4df720e
--- /dev/null
+++ b/client/app/scripts/utils/delayed-show.js
@@ -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 (
+
+ {children}
+
+ );
+ }
+}
+
+
+DelayedShow.defaultProps = {
+ delay: 1000
+};
diff --git a/client/app/styles/main.less b/client/app/styles/main.less
index acf5aa467..f1c19adce 100644
--- a/client/app/styles/main.less
+++ b/client/app/styles/main.less
@@ -296,14 +296,16 @@ h2 {
.nodes-chart {
- &-error {
+ &-error, &-loading {
.hideable;
position: absolute;
left: 50%;
top: 50%;
- transform: translate(-50%, -50%);
+ margin-left: -16.5%;
+ margin-top: -275px;
color: @text-secondary-color;
width: 33%;
+ height: 550px;
.heading {
font-size: 125%;
@@ -316,6 +318,14 @@ h2 {
}
}
+ &-loading &-error-icon-container {
+ animation: blinking 2.0s infinite @base-ease;
+ }
+
+ &-loading {
+ text-align: center;
+ }
+
svg {
.hideable;
position: absolute;