diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js
index 64e81e98e..90557c212 100644
--- a/client/app/scripts/actions/app-actions.js
+++ b/client/app/scripts/actions/app-actions.js
@@ -39,6 +39,12 @@ module.exports = {
WebapiUtils.getNodesDelta(AppStore.getCurrentTopologyUrl());
},
+ closeWebsocket: function() {
+ AppDispatcher.dispatch({
+ type: ActionTypes.CLOSE_WEBSOCKET
+ });
+ },
+
enterEdge: function(edgeId) {
AppDispatcher.dispatch({
type: ActionTypes.ENTER_EDGE,
@@ -104,6 +110,13 @@ module.exports = {
});
},
+ receiveError: function(errorUrl) {
+ AppDispatcher.dispatch({
+ errorUrl: errorUrl,
+ type: ActionTypes.RECEIVE_ERROR
+ });
+ },
+
route: function(state) {
AppDispatcher.dispatch({
state: state,
diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js
index 476826b3f..952eff3e5 100644
--- a/client/app/scripts/charts/nodes-chart.js
+++ b/client/app/scripts/charts/nodes-chart.js
@@ -1,6 +1,6 @@
const _ = require('lodash');
const d3 = require('d3');
-const debug = require('debug')('nodes-chart');
+const debug = require('debug')('scope:nodes-chart');
const React = require('react');
const timely = require('timely');
diff --git a/client/app/scripts/charts/nodes-layout.js b/client/app/scripts/charts/nodes-layout.js
index 651aef71c..a3fad85ec 100644
--- a/client/app/scripts/charts/nodes-layout.js
+++ b/client/app/scripts/charts/nodes-layout.js
@@ -1,5 +1,5 @@
const dagre = require('dagre');
-const debug = require('debug')('nodes-layout');
+const debug = require('debug')('scope:nodes-layout');
const _ = require('lodash');
const MAX_NODES = 100;
diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js
index febbebb16..8fac2c886 100644
--- a/client/app/scripts/components/app.js
+++ b/client/app/scripts/components/app.js
@@ -15,7 +15,7 @@ const ESC_KEY_CODE = 27;
function getStateFromStores() {
return {
currentTopology: AppStore.getCurrentTopology(),
- connectionState: AppStore.getConnectionState(),
+ errorUrl: AppStore.getErrorUrl(),
currentGrouping: AppStore.getCurrentGrouping(),
highlightedEdgeIds: AppStore.getHighlightedEdgeIds(),
highlightedNodeIds: AppStore.getHighlightedNodeIds(),
@@ -66,7 +66,7 @@ const App = React.createClass({
-
+
-
- Scope is disconnected
-
- );
+ renderConnectionState: function(errorUrl) {
+ if (errorUrl) {
+ const title = 'Cannot reach Scope. Make sure the following URL is reachable: ' + errorUrl;
+ return (
+
+
+ Trying to reconnect...
+
+ );
+ }
},
render: function() {
- const isDisconnected = this.props.connectionState === 'disconnected';
-
return (
- {isDisconnected && this.renderConnectionState()}
+ {this.renderConnectionState(this.props.errorUrl)}
);
}
diff --git a/client/app/scripts/constants/action-types.js b/client/app/scripts/constants/action-types.js
index ffce74550..ca6084a4b 100644
--- a/client/app/scripts/constants/action-types.js
+++ b/client/app/scripts/constants/action-types.js
@@ -5,6 +5,7 @@ module.exports = keymirror({
CLICK_GROUPING: null,
CLICK_NODE: null,
CLICK_TOPOLOGY: null,
+ CLOSE_WEBSOCKET: null,
ENTER_EDGE: null,
ENTER_NODE: null,
HIT_ESC_KEY: null,
@@ -15,5 +16,6 @@ module.exports = keymirror({
RECEIVE_NODES_DELTA: null,
RECEIVE_TOPOLOGIES: null,
RECEIVE_API_DETAILS: null,
+ RECEIVE_ERROR: null,
ROUTE_TOPOLOGY: null
});
diff --git a/client/app/scripts/stores/__tests__/app-store-test.js b/client/app/scripts/stores/__tests__/app-store-test.js
index 7ca3c6668..c3487ea2c 100644
--- a/client/app/scripts/stores/__tests__/app-store-test.js
+++ b/client/app/scripts/stores/__tests__/app-store-test.js
@@ -31,6 +31,10 @@ describe('AppStore', function() {
grouping: 'grouped'
};
+ const CloseWebsocketAction = {
+ type: ActionTypes.CLOSE_WEBSOCKET
+ };
+
const HitEscAction = {
type: ActionTypes.HIT_ESC_KEY
};
@@ -64,6 +68,8 @@ describe('AppStore', function() {
};
beforeEach(function() {
+ // clear AppStore singleton
+ delete require.cache[require.resolve('../app-store')];
AppStore = require('../app-store');
registeredCallback = AppStore.registeredCallback;
});
@@ -114,22 +120,34 @@ describe('AppStore', function() {
});
it('keeps showing nodes on navigating back after node click', function() {
+ registeredCallback(ReceiveTopologiesAction);
+ registeredCallback(ClickTopologyAction);
registeredCallback(ReceiveNodesDeltaAction);
- // TODO clear AppStore cache
+
expect(AppStore.getAppState())
- .toEqual({"topologyId":"topo1-grouped","grouping":"none","selectedNodeId": null});
+ .toEqual({"topologyId":"topo1","grouping":"none","selectedNodeId": null});
registeredCallback(ClickNodeAction);
expect(AppStore.getAppState())
- .toEqual({"topologyId":"topo1-grouped","grouping":"none","selectedNodeId": 'n1'});
+ .toEqual({"topologyId":"topo1","grouping":"none","selectedNodeId": 'n1'});
// go back in browsing
- RouteAction.state = {"topologyId":"topo1-grouped","grouping":"none","selectedNodeId": null};
+ RouteAction.state = {"topologyId":"topo1","grouping":"none","selectedNodeId": null};
registeredCallback(RouteAction);
expect(AppStore.getSelectedNodeId()).toBe(null);
expect(AppStore.getNodes()).toEqual(NODE_SET);
});
+ // connection errors
+
+ it('resets topology on websocket reconnect', function() {
+ registeredCallback(ReceiveNodesDeltaAction);
+ expect(AppStore.getNodes()).toEqual(NODE_SET);
+
+ registeredCallback(CloseWebsocketAction);
+ expect(AppStore.getNodes()).toEqual({});
+ });
+
});
\ No newline at end of file
diff --git a/client/app/scripts/stores/app-store.js b/client/app/scripts/stores/app-store.js
index 8db061b74..16f558d55 100644
--- a/client/app/scripts/stores/app-store.js
+++ b/client/app/scripts/stores/app-store.js
@@ -1,7 +1,7 @@
const EventEmitter = require('events').EventEmitter;
const _ = require('lodash');
const assign = require('object-assign');
-const debug = require('debug')('app-store');
+const debug = require('debug')('scope:app-store');
const AppDispatcher = require('../dispatcher/app-dispatcher');
const ActionTypes = require('../constants/action-types');
@@ -29,9 +29,9 @@ function findCurrentTopology(subTree, topologyId) {
// Initial values
-let connectionState = 'disconnected';
let currentGrouping = 'none';
let currentTopologyId = 'containers';
+let errorUrl = null;
let version = '';
let mouseOverEdgeId = null;
let mouseOverNodeId = null;
@@ -54,10 +54,6 @@ const AppStore = assign({}, EventEmitter.prototype, {
};
},
- getConnectionState: function() {
- return connectionState;
- },
-
getCurrentTopology: function() {
return findCurrentTopology(topologies, currentTopologyId);
},
@@ -74,6 +70,10 @@ const AppStore = assign({}, EventEmitter.prototype, {
return currentGrouping;
},
+ getErrorUrl: function() {
+ return errorUrl;
+ },
+
getHighlightedEdgeIds: function() {
if (mouseOverNodeId) {
// all neighbour combinations because we dont know which direction exists
@@ -127,6 +127,7 @@ const AppStore = assign({}, EventEmitter.prototype, {
getVersion: function() {
return version;
}
+
});
// Store Dispatch Hooks
@@ -160,6 +161,11 @@ AppStore.registeredCallback = function(payload) {
AppStore.emit(AppStore.CHANGE_EVENT);
break;
+ case ActionTypes.CLOSE_WEBSOCKET:
+ nodes = {};
+ AppStore.emit(AppStore.CHANGE_EVENT);
+ break;
+
case ActionTypes.ENTER_EDGE:
mouseOverEdgeId = payload.edgeId;
AppStore.emit(AppStore.CHANGE_EVENT);
@@ -186,7 +192,13 @@ AppStore.registeredCallback = function(payload) {
AppStore.emit(AppStore.CHANGE_EVENT);
break;
+ case ActionTypes.RECEIVE_ERROR:
+ errorUrl = payload.errorUrl;
+ AppStore.emit(AppStore.CHANGE_EVENT);
+ break;
+
case ActionTypes.RECEIVE_NODE_DETAILS:
+ errorUrl = null;
nodeDetails = payload.details;
AppStore.emit(AppStore.CHANGE_EVENT);
break;
@@ -197,7 +209,7 @@ AppStore.registeredCallback = function(payload) {
'update', _.size(payload.delta.update),
'add', _.size(payload.delta.add));
- connectionState = 'connected';
+ errorUrl = null;
// nodes that no longer exist
_.each(payload.delta.remove, function(nodeId) {
@@ -225,11 +237,13 @@ AppStore.registeredCallback = function(payload) {
break;
case ActionTypes.RECEIVE_TOPOLOGIES:
+ errorUrl = null;
topologies = payload.topologies;
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.RECEIVE_API_DETAILS:
+ errorUrl = null;
version = payload.version;
AppStore.emit(AppStore.CHANGE_EVENT);
break;
diff --git a/client/app/scripts/utils/web-api-utils.js b/client/app/scripts/utils/web-api-utils.js
index dce7160c3..0c0e9a4f5 100644
--- a/client/app/scripts/utils/web-api-utils.js
+++ b/client/app/scripts/utils/web-api-utils.js
@@ -1,3 +1,4 @@
+const debug = require('debug')('scope:web-api-utils');
const reqwest = require('reqwest');
const AppActions = require('../actions/app-actions');
@@ -5,16 +6,21 @@ const AppActions = require('../actions/app-actions');
const WS_URL = window.WS_URL || 'ws://' + location.host;
+const apiTimerInterval = 10000;
+const reconnectTimerInterval = 5000;
+const topologyTimerInterval = apiTimerInterval;
+const updateFrequency = '5s';
+
let socket;
let reconnectTimer = 0;
let currentUrl = null;
-let updateFrequency = '5s';
let topologyTimer = 0;
let apiDetailsTimer = 0;
function createWebsocket(topologyUrl) {
if (socket) {
socket.onclose = null;
+ socket.onerror = null;
socket.close();
}
@@ -23,10 +29,17 @@ function createWebsocket(topologyUrl) {
socket.onclose = function() {
clearTimeout(reconnectTimer);
socket = null;
+ AppActions.closeWebsocket();
+ debug('Closed websocket to ' + currentUrl);
reconnectTimer = setTimeout(function() {
createWebsocket(topologyUrl);
- }, 5000);
+ }, reconnectTimerInterval);
+ };
+
+ socket.onerror = function() {
+ debug('Error in websocket to ' + currentUrl);
+ AppActions.receiveError(currentUrl);
};
socket.onmessage = function(event) {
@@ -41,26 +54,51 @@ function createWebsocket(topologyUrl) {
function getTopologies() {
clearTimeout(topologyTimer);
- reqwest('/api/topology', function(res) {
- AppActions.receiveTopologies(res);
- topologyTimer = setTimeout(getTopologies, 10000);
+ const url = '/api/topology';
+ reqwest({
+ url: url,
+ success: function(res) {
+ AppActions.receiveTopologies(res);
+ topologyTimer = setTimeout(getTopologies, topologyTimerInterval);
+ },
+ error: function(err) {
+ debug('Error in topology request: ' + err);
+ AppActions.receiveError(url);
+ topologyTimer = setTimeout(getTopologies, topologyTimerInterval / 2);
+ }
});
}
function getNodeDetails(topologyUrl, nodeId) {
if (topologyUrl && nodeId) {
const url = [topologyUrl, nodeId].join('/');
- reqwest(url, function(res) {
- AppActions.receiveNodeDetails(res.node);
+ reqwest({
+ url: url,
+ success: function(res) {
+ AppActions.receiveNodeDetails(res.node);
+ },
+ error: function(err) {
+ debug('Error in node details request: ' + err);
+ AppActions.receiveError(topologyUrl);
+ }
});
}
}
function getApiDetails() {
clearTimeout(apiDetailsTimer);
- reqwest('/api', function(res) {
- AppActions.receiveApiDetails(res);
- apiDetailsTimer = setTimeout(getApiDetails, 10000);
+ const url = '/api';
+ reqwest({
+ url: url,
+ success: function(res) {
+ AppActions.receiveApiDetails(res);
+ apiDetailsTimer = setTimeout(getApiDetails, apiTimerInterval);
+ },
+ error: function(err) {
+ debug('Error in api details request: ' + err);
+ AppActions.receiveError(url);
+ apiDetailsTimer = setTimeout(getApiDetails, apiTimerInterval / 2);
+ }
});
}
diff --git a/client/webpack.config.js b/client/webpack.config.js
index 74c314fc3..a00fec98a 100644
--- a/client/webpack.config.js
+++ b/client/webpack.config.js
@@ -13,10 +13,6 @@ var DEBUG = !argv.release;
var STYLE_LOADER = 'style-loader';
var CSS_LOADER = DEBUG ? 'css-loader' : 'css-loader?minimize';
var AUTOPREFIXER_LOADER = 'postcss-loader';
-var GLOBALS = {
- 'process.env.NODE_ENV': DEBUG ? '"development"' : '"production"',
- '__DEV__': DEBUG
-};
//
// Common configuration chunk to be used for both