diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index a3a52593e8..7fb9181298 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -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 ( + + ); + }, + + renderEmptyTopologyError: function(show) { + return ( + + ); + }, + 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 (
-
- -
Too many nodes to show in the browser.
We're working on it, but for now, try a different view?
-
+ {errorEmpty} + {errorMaxNodesExceeded} {function(interpolated) { diff --git a/client/app/scripts/charts/nodes-error.js b/client/app/scripts/charts/nodes-error.js new file mode 100644 index 0000000000..5c152678ba --- /dev/null +++ b/client/app/scripts/charts/nodes-error.js @@ -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 ( +
+
+ +
+ {this.props.children} +
+ ); + } + +}); + +module.exports = NodesError; diff --git a/client/app/scripts/stores/__tests__/app-store-test.js b/client/app/scripts/stores/__tests__/app-store-test.js index 197c586107..5e524e1ce7 100644 --- a/client/app/scripts/stores/__tests__/app-store-test.js +++ b/client/app/scripts/stores/__tests__/app-store-test.js @@ -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(); + }); + }); diff --git a/client/app/scripts/stores/app-store.js b/client/app/scripts/stores/app-store.js index 17d9f1976b..a5ce2873e6 100644 --- a/client/app/scripts/stores/app-store.js +++ b/client/app/scripts/stores/app-store.js @@ -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; diff --git a/client/app/scripts/utils/web-api-utils.js b/client/app/scripts/utils/web-api-utils.js index 2da49c7a49..4f839cb76d 100644 --- a/client/app/scripts/utils/web-api-utils.js +++ b/client/app/scripts/utils/web-api-utils.js @@ -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 }; - diff --git a/client/app/styles/main.less b/client/app/styles/main.less index 2cde182b38..1410685b8f 100644 --- a/client/app/styles/main.less +++ b/client/app/styles/main.less @@ -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; }