Render filtered node count in status bar

* always set updated topology object when received
* track whether route was set on initial load
* removed connection count from status bar (was not deemed important)
* fixed issue where topology option changes did not affect details pane
* only show filtered nodes when count > 0
* clear nodes graph when empty topology is loaded
* also prevent JS error if nodes are hovered over that should be gone
* fixed options sync issue between graph and status bar
* implemented topology options with immutable DS
This commit is contained in:
David Kaltschmidt
2015-09-24 18:16:18 +02:00
committed by Tom Wilkie
parent 5c0555ae39
commit 2d7e546ae5
10 changed files with 126 additions and 64 deletions

View File

@@ -5,17 +5,27 @@ let RouterUtils;
let WebapiUtils;
module.exports = {
changeTopologyOption: function(option, value) {
changeTopologyOption: function(option, value, topologyId) {
AppDispatcher.dispatch({
type: ActionTypes.CHANGE_TOPOLOGY_OPTION,
topologyId: topologyId,
option: option,
value: value
});
RouterUtils.updateRoute();
// update all request workers with new options
WebapiUtils.getTopologies(
AppStore.getActiveTopologyOptions()
);
WebapiUtils.getNodesDelta(
AppStore.getCurrentTopologyUrl(),
AppStore.getActiveTopologyOptions()
);
WebapiUtils.getNodeDetails(
AppStore.getCurrentTopologyUrl(),
AppStore.getSelectedNodeId(),
AppStore.getActiveTopologyOptions()
);
},
clickCloseDetails: function() {
@@ -146,6 +156,9 @@ module.exports = {
state: state,
type: ActionTypes.ROUTE_TOPOLOGY
});
WebapiUtils.getTopologies(
AppStore.getActiveTopologyOptions()
);
WebapiUtils.getNodesDelta(
AppStore.getCurrentTopologyUrl(),
AppStore.getActiveTopologyOptions()

View File

@@ -394,7 +394,10 @@ const NodesChart = React.createClass({
const n = props.nodes.size;
if (n === 0) {
return {};
return {
nodes: {},
edges: {}
};
}
const nodes = this.initNodes(props.nodes, state.nodes);

View File

@@ -48,7 +48,10 @@ const App = React.createClass({
window.addEventListener('keyup', this.onKeyPress);
RouterUtils.getRouter().start({hashbang: true});
WebapiUtils.getTopologies();
if (!AppStore.isRouteSet()) {
// dont request topologies when already done via router
WebapiUtils.getTopologies(AppStore.getActiveTopologyOptions());
}
WebapiUtils.getApiDetails();
},
@@ -93,6 +96,7 @@ const App = React.createClass({
<Sidebar>
<TopologyOptions options={this.state.currentTopologyOptions}
topologyId={this.state.currentTopologyId}
activeOptions={this.state.activeTopologyOptions} />
<Status errorUrl={this.state.errorUrl} topology={this.state.currentTopology}
topologiesLoaded={this.state.topologiesLoaded}

View File

@@ -21,7 +21,10 @@ const Status = React.createClass({
showWarningIcon = true;
} else if (this.props.topology) {
const stats = this.props.topology.stats;
text = `${stats.node_count} nodes, ${stats.edge_count} connections`;
text = `${stats.node_count} nodes`;
if (stats.filtered_nodes) {
text = `${text} (${stats.filtered_nodes} filtered)`;
}
classNames += ' status-stats';
showWarningIcon = false;
}

View File

@@ -2,7 +2,6 @@ const React = require('react');
const _ = require('lodash');
const AppActions = require('../actions/app-actions');
const AppStore = require('../stores/app-store');
const Topologies = React.createClass({
@@ -13,7 +12,7 @@ const Topologies = React.createClass({
renderSubTopology: function(subTopology) {
const isActive = subTopology.name === this.props.currentTopology.name;
const topologyId = AppStore.getTopologyIdForUrl(subTopology.url);
const topologyId = subTopology.id;
const title = this.renderTitle(subTopology);
const className = isActive ? 'topologies-sub-item topologies-sub-item-active' : 'topologies-sub-item';
@@ -35,7 +34,7 @@ const Topologies = React.createClass({
renderTopology: function(topology) {
const isActive = topology.name === this.props.currentTopology.name;
const className = isActive ? 'topologies-item-main topologies-item-main-active' : 'topologies-item-main';
const topologyId = AppStore.getTopologyIdForUrl(topology.url);
const topologyId = topology.id;
const title = this.renderTitle(topology);
return (

View File

@@ -6,7 +6,7 @@ const TopologyOptionAction = React.createClass({
onClick: function(ev) {
ev.preventDefault();
AppActions.changeTopologyOption(this.props.option, this.props.value);
AppActions.changeTopologyOption(this.props.option, this.props.value, this.props.topologyId);
},
render: function() {

View File

@@ -5,9 +5,9 @@ const TopologyOptionAction = require('./topology-option-action');
const TopologyOptions = React.createClass({
renderAction: function(action, option) {
renderAction: function(action, option, topologyId) {
return (
<TopologyOptionAction option={option} value={action} />
<TopologyOptionAction option={option} value={action} topologyId={topologyId} />
);
},
@@ -15,11 +15,12 @@ const TopologyOptions = React.createClass({
let activeText;
const actions = [];
const activeOptions = this.props.activeOptions;
const topologyId = this.props.topologyId;
items.forEach(function(item) {
if (activeOptions[item.option] && activeOptions[item.option] === item.value) {
if (activeOptions && activeOptions.has(item.option) && activeOptions.get(item.option) === item.value) {
activeText = item.display;
} else {
actions.push(this.renderAction(item.value, item.option));
actions.push(this.renderAction(item.value, item.option, topologyId));
}
}, this);

View File

@@ -1,4 +1,4 @@
// Appstore test suite using Jasmine matchers
describe('AppStore', function() {
const ActionTypes = require('../../constants/action-types');
@@ -30,12 +30,14 @@ describe('AppStore', function() {
const ChangeTopologyOptionAction = {
type: ActionTypes.CHANGE_TOPOLOGY_OPTION,
topologyId: 'topo1',
option: 'option1',
value: 'on'
};
const ChangeTopologyOptionAction2 = {
type: ActionTypes.CHANGE_TOPOLOGY_OPTION,
topologyId: 'topo1',
option: 'option1',
value: 'off'
};
@@ -159,39 +161,40 @@ describe('AppStore', function() {
// default options
registeredCallback(ReceiveTopologiesAction);
registeredCallback(ClickTopologyAction);
expect(AppStore.getActiveTopologyOptions().option1).toBe('off');
expect(AppStore.getAppState().topologyOptions.option1).toBe('off');
expect(AppStore.getActiveTopologyOptions().has('option1')).toBeTruthy();
expect(AppStore.getActiveTopologyOptions().get('option1')).toBe('off');
expect(AppStore.getAppState().topologyOptions.topo1.option1).toBe('off');
// turn on
registeredCallback(ChangeTopologyOptionAction);
expect(AppStore.getActiveTopologyOptions().option1).toBe('on');
expect(AppStore.getAppState().topologyOptions.option1).toBe('on');
expect(AppStore.getActiveTopologyOptions().get('option1')).toBe('on');
expect(AppStore.getAppState().topologyOptions.topo1.option1).toBe('on');
// turn off
registeredCallback(ChangeTopologyOptionAction2);
expect(AppStore.getActiveTopologyOptions().option1).toBe('off');
expect(AppStore.getAppState().topologyOptions.option1).toBe('off');
expect(AppStore.getActiveTopologyOptions().get('option1')).toBe('off');
expect(AppStore.getAppState().topologyOptions.topo1.option1).toBe('off');
// other topology w/o options
// other topology w/o options dont return options, but keep in app state
registeredCallback(ClickSubTopologyAction);
expect(AppStore.getActiveTopologyOptions().option1).toBeUndefined();
expect(AppStore.getAppState().topologyOptions.option1).toBeUndefined();
expect(AppStore.getActiveTopologyOptions()).toBeUndefined();
expect(AppStore.getAppState().topologyOptions.topo1.option1).toBe('off');
});
it('sets topology options from route', function() {
RouteAction.state = {
"topologyId":"topo1",
"selectedNodeId": null,
"topologyOptions": {'option1': 'on'}};
"topologyOptions": {'topo1':{'option1': 'on'}}};
registeredCallback(RouteAction);
expect(AppStore.getActiveTopologyOptions().option1).toBe('on');
expect(AppStore.getAppState().topologyOptions.option1).toBe('on');
expect(AppStore.getActiveTopologyOptions().get('option1')).toBe('on');
expect(AppStore.getAppState().topologyOptions.topo1.option1).toBe('on');
// stay same after topos have been received
registeredCallback(ReceiveTopologiesAction);
registeredCallback(ClickTopologyAction);
expect(AppStore.getActiveTopologyOptions().option1).toBe('on');
expect(AppStore.getAppState().topologyOptions.option1).toBe('on');
expect(AppStore.getActiveTopologyOptions().get('option1')).toBe('on');
expect(AppStore.getAppState().topologyOptions.topo1.option1).toBe('on');
});
it('uses default topology options from route', function() {
@@ -202,8 +205,8 @@ describe('AppStore', function() {
registeredCallback(RouteAction);
registeredCallback(ReceiveTopologiesAction);
registeredCallback(ClickTopologyAction);
expect(AppStore.getActiveTopologyOptions().option1).toBe('off');
expect(AppStore.getAppState().topologyOptions.option1).toBe('off');
expect(AppStore.getActiveTopologyOptions().get('option1')).toBe('off');
expect(AppStore.getAppState().topologyOptions.topo1.option1).toBe('off');
});
// nodes delta
@@ -222,6 +225,12 @@ describe('AppStore', function() {
expect(AppStore.getNodes().toJS()).toEqual(NODE_SET);
});
it('knows a route was set', function() {
expect(AppStore.isRouteSet()).toBeFalsy();
registeredCallback(RouteAction);
expect(AppStore.isRouteSet()).toBeTruthy();
});
it('gets selected node after click', function() {
registeredCallback(ReceiveNodesDeltaAction);

View File

@@ -44,7 +44,7 @@ function makeNode(node) {
// Initial values
let activeTopologyOptions = null;
let topologyOptions = makeOrderedMap();
let adjacentNodes = makeSet();
let currentTopology = null;
let currentTopologyId = 'containers';
@@ -57,24 +57,43 @@ let nodeDetails = null;
let selectedNodeId = null;
let topologies = [];
let topologiesLoaded = false;
let routeSet = false;
let websocketClosed = true;
function processTopologies(topologyList) {
// adds ID field to topology, based on last part of URL path
_.each(topologyList, function(topology) {
topology.id = topology.url.split('/').pop();
processTopologies(topology.sub_topologies);
});
return topologyList;
}
function setTopology(topologyId) {
currentTopologyId = topologyId;
currentTopology = findCurrentTopology(topologies, topologyId);
}
function setDefaultTopologyOptions() {
if (currentTopology) {
activeTopologyOptions = {};
_.each(currentTopology.options, function(items, option) {
function setDefaultTopologyOptions(topologyList) {
_.each(topologyList, function(topology) {
let defaultOptions = makeOrderedMap();
_.each(topology.options, function(items, option) {
_.each(items, function(item) {
if (item.default === true) {
activeTopologyOptions[option] = item.value;
defaultOptions = defaultOptions.set(option, item.value);
}
});
});
}
if (defaultOptions.size) {
topologyOptions = topologyOptions.set(
topology.id,
defaultOptions
);
}
setDefaultTopologyOptions(topology.sub_topologies);
});
}
// Store API
@@ -88,12 +107,13 @@ const AppStore = assign({}, EventEmitter.prototype, {
return {
topologyId: currentTopologyId,
selectedNodeId: this.getSelectedNodeId(),
topologyOptions: this.getActiveTopologyOptions()
topologyOptions: topologyOptions.toJS() // all options
};
},
getActiveTopologyOptions: function() {
return activeTopologyOptions;
// options for current topology
return topologyOptions.get(currentTopologyId);
},
getAdjacentNodes: function(nodeId) {
@@ -136,7 +156,7 @@ const AppStore = assign({}, EventEmitter.prototype, {
},
getHighlightedEdgeIds: function() {
if (mouseOverNodeId) {
if (mouseOverNodeId && nodes.has(mouseOverNodeId)) {
// all neighbour combinations because we dont know which direction exists
const adjacency = nodes.get(mouseOverNodeId).get('adjacency');
if (adjacency) {
@@ -185,14 +205,14 @@ const AppStore = assign({}, EventEmitter.prototype, {
return topologies;
},
getTopologyIdForUrl: function(url) {
return url.split('/').pop();
},
getVersion: function() {
return version;
},
isRouteSet: function() {
return routeSet;
},
isTopologiesLoaded: function() {
return topologiesLoaded;
},
@@ -209,10 +229,14 @@ AppStore.registeredCallback = function(payload) {
switch (payload.type) {
case ActionTypes.CHANGE_TOPOLOGY_OPTION:
if (activeTopologyOptions[payload.option] !== payload.value) {
if (topologyOptions.getIn([payload.topologyId, payload.option])
!== payload.value) {
nodes = nodes.clear();
}
activeTopologyOptions[payload.option] = payload.value;
topologyOptions = topologyOptions.setIn(
[payload.topologyId, payload.option],
payload.value
);
AppStore.emit(AppStore.CHANGE_EVENT);
break;
@@ -230,7 +254,6 @@ AppStore.registeredCallback = function(payload) {
selectedNodeId = null;
if (payload.topologyId !== currentTopologyId) {
setTopology(payload.topologyId);
setDefaultTopologyOptions();
nodes = nodes.clear();
}
AppStore.emit(AppStore.CHANGE_EVENT);
@@ -321,15 +344,13 @@ AppStore.registeredCallback = function(payload) {
case ActionTypes.RECEIVE_TOPOLOGIES:
errorUrl = null;
topologiesLoaded = true;
topologies = payload.topologies;
if (!currentTopology) {
setTopology(currentTopologyId);
// only set on first load
if (activeTopologyOptions === null) {
setDefaultTopologyOptions();
}
topologies = processTopologies(payload.topologies);
setTopology(currentTopologyId);
// only set on first load, if options are not already set via route
if (!topologiesLoaded && topologyOptions.size === 0) {
setDefaultTopologyOptions(topologies);
}
topologiesLoaded = true;
AppStore.emit(AppStore.CHANGE_EVENT);
break;
@@ -340,13 +361,15 @@ AppStore.registeredCallback = function(payload) {
break;
case ActionTypes.ROUTE_TOPOLOGY:
routeSet = true;
if (currentTopologyId !== payload.state.topologyId) {
nodes = nodes.clear();
}
setTopology(payload.state.topologyId);
setDefaultTopologyOptions();
setDefaultTopologyOptions(topologies);
selectedNodeId = payload.state.selectedNodeId;
activeTopologyOptions = payload.state.topologyOptions || activeTopologyOptions;
topologyOptions = Immutable.fromJS(payload.state.topologyOptions)
|| topologyOptions;
AppStore.emit(AppStore.CHANGE_EVENT);
break;

View File

@@ -1,4 +1,3 @@
const _ = require('lodash');
const debug = require('debug')('scope:web-api-utils');
const reqwest = require('reqwest');
@@ -21,9 +20,12 @@ let apiDetailsTimer = 0;
function buildOptionsQuery(options) {
return _.map(options, function(value, param) {
return param + '=' + value;
}).join('&');
if (options) {
return options.reduce(function(query, value, param) {
return `${query}&${param}=${value}`;
}, '');
}
return '';
}
function createWebsocket(topologyUrl, optionsQuery) {
@@ -66,19 +68,24 @@ function createWebsocket(topologyUrl, optionsQuery) {
/* keep URLs relative */
function getTopologies() {
function getTopologies(options) {
clearTimeout(topologyTimer);
const url = 'api/topology';
const optionsQuery = buildOptionsQuery(options);
const url = `api/topology?${optionsQuery}`;
reqwest({
url: url,
success: function(res) {
AppActions.receiveTopologies(res);
topologyTimer = setTimeout(getTopologies, topologyTimerInterval);
topologyTimer = setTimeout(function() {
getTopologies(options);
}, topologyTimerInterval / 2);
},
error: function(err) {
debug('Error in topology request: ' + err);
AppActions.receiveError(url);
topologyTimer = setTimeout(getTopologies, topologyTimerInterval / 2);
topologyTimer = setTimeout(function() {
getTopologies(options);
}, topologyTimerInterval / 2);
}
});
}