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