clear nodes cache when websocket closes connection

* also show reconnection status

fixes #162
This commit is contained in:
David Kaltschmidt
2015-06-15 18:10:02 +02:00
parent 5627658a2e
commit c87cc872ee
10 changed files with 121 additions and 39 deletions

View File

@@ -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,

View File

@@ -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');

View File

@@ -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;

View File

@@ -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({
<div className="header">
<Logo />
<Topologies topologies={this.state.topologies} currentTopology={this.state.currentTopology} />
<Status connectionState={this.state.connectionState} />
<Status errorUrl={this.state.errorUrl} />
</div>
<Nodes nodes={this.state.nodes} highlightedNodeIds={this.state.highlightedNodeIds}

View File

@@ -2,21 +2,22 @@ const React = require('react');
const Status = React.createClass({
renderConnectionState: function() {
return (
<div className="status-connection">
<span className="status-icon fa fa-exclamation-circle" />
<span className="status-label">Scope is disconnected</span>
</div>
);
renderConnectionState: function(errorUrl) {
if (errorUrl) {
const title = 'Cannot reach Scope. Make sure the following URL is reachable: ' + errorUrl;
return (
<div className="status-connection" title={title}>
<span className="status-icon fa fa-exclamation-circle" />
<span className="status-label">Trying to reconnect...</span>
</div>
);
}
},
render: function() {
const isDisconnected = this.props.connectionState === 'disconnected';
return (
<div className="status">
{isDisconnected && this.renderConnectionState()}
{this.renderConnectionState(this.props.errorUrl)}
</div>
);
}

View File

@@ -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
});

View File

@@ -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({});
});
});

View File

@@ -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;

View File

@@ -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);
}
});
}

View File

@@ -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