show message if topology is empty

This commit is contained in:
David Kaltschmidt
2015-09-21 19:02:30 +02:00
parent 8f2dc64762
commit 5c086aeeb3
6 changed files with 100 additions and 16 deletions

View File

@@ -11,6 +11,7 @@ const Edge = require('./edge');
const Naming = require('../constants/naming');
const NodesLayout = require('./nodes-layout');
const Node = require('./node');
const NodesError = require('./nodes-error');
const MARGINS = {
top: 130,
@@ -151,6 +152,27 @@ const NodesChart = React.createClass({
}, this);
},
renderMaxNodesError: function(show) {
return (
<NodesError faIconClass="fa-ban" hidden={!show}>
<div className="centered">Too many nodes to show in the browser.<br />We're working on it, but for now, try a different view?</div>
</NodesError>
);
},
renderEmptyTopologyError: function(show) {
return (
<NodesError faIconClass="fa-circle-thin" hidden={!show}>
<div className="heading">Nothing to show. This can have any of these reasons:</div>
<ul>
<li>We haven't received any reports from probes recently. Are the probes properly configured?</li>
<li>There are nodes, but they're currently hidden. Check the view options in the bottom-left if they allow for showing hidden nodes.</li>
<li>Containers view only: you're not running Docker, or you don't have any containers.</li>
</ul>
</NodesError>
);
},
render: function() {
const nodeElements = this.renderGraphNodes(this.state.nodes, this.state.nodeScale);
const edgeElements = this.renderGraphEdges(this.state.edges, this.state.nodeScale);
@@ -165,15 +187,14 @@ const NodesChart = React.createClass({
translate = shiftTranslate;
wasShifted = true;
}
const errorClassNames = this.state.maxNodesExceeded ? 'nodes-chart-error' : 'nodes-chart-error hide';
const svgClassNames = this.state.maxNodesExceeded || _.size(nodeElements) === 0 ? 'hide' : '';
const errorEmpty = this.renderEmptyTopologyError(AppStore.isTopologyEmpty());
const errorMaxNodesExceeded = this.renderMaxNodesError(this.state.maxNodesExceeded);
return (
<div className="nodes-chart">
<div className={errorClassNames}>
<span className="nodes-chart-error-icon fa fa-ban" />
<div>Too many nodes to show in the browser.<br />We're working on it, but for now, try a different view?</div>
</div>
{errorEmpty}
{errorMaxNodesExceeded}
<svg width="100%" height="100%" className={svgClassNames} onMouseUp={this.handleMouseUp}>
<Spring endValue={{val: translate, config: [80, 20]}}>
{function(interpolated) {

View File

@@ -0,0 +1,24 @@
const React = require('react');
const NodesError = React.createClass({
render: function() {
let classNames = 'nodes-chart-error';
if (this.props.hidden) {
classNames += ' hide';
}
let iconClassName = 'fa ' + this.props.faIconClass;
return (
<div className={classNames}>
<div className="nodes-chart-error-icon">
<span className={iconClassName} />
</div>
{this.props.children}
</div>
);
}
});
module.exports = NodesError;

View File

@@ -61,6 +61,11 @@ describe('AppStore', function() {
topologyId: 'topo1'
};
const ClickTopology2Action = {
type: ActionTypes.CLICK_TOPOLOGY,
topologyId: 'topo2'
};
const ClickGroupingAction = {
type: ActionTypes.CLICK_GROUPING,
grouping: 'grouped'
@@ -112,10 +117,19 @@ describe('AppStore', function() {
{value: 'off', default: true}
]
},
stats: {
node_count: 1
},
sub_topologies: [{
url: '/topo1-grouped',
name: 'topo 1 grouped'
}]
}, {
url: '/topo2',
name: 'Topo2',
stats: {
node_count: 0
}
}]
};
@@ -142,7 +156,7 @@ describe('AppStore', function() {
registeredCallback(ClickTopologyAction);
registeredCallback(ReceiveTopologiesAction);
expect(AppStore.getTopologies().length).toBe(1);
expect(AppStore.getTopologies().length).toBe(2);
expect(AppStore.getCurrentTopology().name).toBe('Topo1');
expect(AppStore.getCurrentTopologyUrl()).toBe('/topo1');
expect(AppStore.getCurrentTopologyOptions().option1).toBeDefined();
@@ -152,7 +166,7 @@ describe('AppStore', function() {
registeredCallback(ReceiveTopologiesAction);
registeredCallback(ClickSubTopologyAction);
expect(AppStore.getTopologies().length).toBe(1);
expect(AppStore.getTopologies().length).toBe(2);
expect(AppStore.getCurrentTopology().name).toBe('topo 1 grouped');
expect(AppStore.getCurrentTopologyUrl()).toBe('/topo1-grouped');
expect(AppStore.getCurrentTopologyOptions()).toBeUndefined();
@@ -312,4 +326,18 @@ describe('AppStore', function() {
expect(AppStore.getAdjacentNodes().size).toEqual(0);
});
// empty topology
it('detects that the topology is empty', function() {
registeredCallback(ReceiveTopologiesAction);
registeredCallback(ClickTopologyAction);
expect(AppStore.isTopologyEmpty()).toBeFalsy();
registeredCallback(ClickTopology2Action);
expect(AppStore.isTopologyEmpty()).toBeTruthy();
registeredCallback(ClickTopologyAction);
expect(AppStore.isTopologyEmpty()).toBeFalsy();
});
});

View File

@@ -217,6 +217,10 @@ const AppStore = assign({}, EventEmitter.prototype, {
return topologiesLoaded;
},
isTopologyEmpty: function() {
return currentTopology && currentTopology.stats && currentTopology.stats.node_count === 0 && nodes.size === 0;
},
isWebsocketClosed: function() {
return websocketClosed;
}
@@ -310,10 +314,15 @@ AppStore.registeredCallback = function(payload) {
break;
case ActionTypes.RECEIVE_NODES_DELTA:
debug('RECEIVE_NODES_DELTA',
'remove', _.size(payload.delta.remove),
'update', _.size(payload.delta.update),
'add', _.size(payload.delta.add));
const emptyMessage = !payload.delta.add && !payload.delta.remove
&& payload.delta.update;
if (!emptyMessage) {
debug('RECEIVE_NODES_DELTA',
'remove', _.size(payload.delta.remove),
'update', _.size(payload.delta.update),
'add', _.size(payload.delta.add));
}
errorUrl = null;

View File

@@ -60,9 +60,7 @@ function createWebsocket(topologyUrl, optionsQuery) {
socket.onmessage = function(event) {
const msg = JSON.parse(event.data);
if (msg.add || msg.remove || msg.update) {
AppActions.receiveNodesDelta(msg);
}
AppActions.receiveNodesDelta(msg);
};
}
@@ -146,4 +144,3 @@ module.exports = {
getNodesDelta: getTopology
};

View File

@@ -191,10 +191,15 @@ h2 {
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: @text-secondary-color;
width: 33%;
.heading {
font-size: 125%;
}
&-icon {
text-align: center;
opacity: 0.25;
font-size: 320px;
}