Fix grouping bar for topologies that don't support grouping

- fixes #61
- dont clear nodes cache if topo is the same
- combined stores that were interdependent
- dont show topology grouping menu if unsupported
- also harmonized some variable names
- removed old grouping switcher
- moved topo actions to app actions, removed search
- get correct topology url
- first JS test using Jest
- make tests run on circle using the build container.
- replaced jest test runner with karma/jasmine
- Use debian for UI build and test container.
- updated karma-browserify
This commit is contained in:
David Kaltschmidt
2015-05-19 17:19:56 +02:00
committed by Tom Wilkie
parent 8dae139bdf
commit 66d66a4d01
21 changed files with 283 additions and 238 deletions

View File

@@ -15,7 +15,7 @@ module.exports = {
grouping: grouping
});
RouterUtils.updateRoute();
WebapiUtils.getNodesDelta(AppStore.getUrlForTopology(AppStore.getCurrentTopology()));
WebapiUtils.getNodesDelta(AppStore.getCurrentTopologyUrl());
},
clickNode: function(nodeId) {
@@ -24,7 +24,7 @@ module.exports = {
nodeId: nodeId
});
RouterUtils.updateRoute();
WebapiUtils.getNodeDetails(AppStore.getUrlForTopology(AppStore.getCurrentTopology()), AppStore.getSelectedNodeId());
WebapiUtils.getNodeDetails(AppStore.getCurrentTopologyUrl(), AppStore.getSelectedNodeId());
},
clickTopology: function(topologyId) {
@@ -33,7 +33,14 @@ module.exports = {
topologyId: topologyId
});
RouterUtils.updateRoute();
WebapiUtils.getNodesDelta(AppStore.getUrlForTopology(AppStore.getCurrentTopology()));
WebapiUtils.getNodesDelta(AppStore.getCurrentTopologyUrl());
},
enterNode: function(nodeId) {
AppDispatcher.dispatch({
type: ActionTypes.ENTER_NODE,
nodeId: nodeId
});
},
hitEsc: function() {
@@ -43,6 +50,13 @@ module.exports = {
RouterUtils.updateRoute();
},
leaveNode: function(nodeId) {
AppDispatcher.dispatch({
type: ActionTypes.LEAVE_NODE,
nodeId: nodeId
});
},
receiveNodeDetails: function(details) {
AppDispatcher.dispatch({
type: ActionTypes.RECEIVE_NODE_DETAILS,
@@ -50,13 +64,20 @@ module.exports = {
});
},
receiveNodesDelta: function(delta) {
AppDispatcher.dispatch({
type: ActionTypes.RECEIVE_NODES_DELTA,
delta: delta
});
},
receiveTopologies: function(topologies) {
AppDispatcher.dispatch({
type: ActionTypes.RECEIVE_TOPOLOGIES,
topologies: topologies
});
WebapiUtils.getNodesDelta(AppStore.getUrlForTopology(AppStore.getCurrentTopology()));
WebapiUtils.getNodeDetails(AppStore.getUrlForTopology(AppStore.getCurrentTopology()), AppStore.getSelectedNodeId());
WebapiUtils.getNodesDelta(AppStore.getCurrentTopologyUrl());
WebapiUtils.getNodeDetails(AppStore.getCurrentTopologyUrl(), AppStore.getSelectedNodeId());
},
route: function(state) {
@@ -64,8 +85,8 @@ module.exports = {
state: state,
type: ActionTypes.ROUTE_TOPOLOGY
});
WebapiUtils.getNodesDelta(AppStore.getUrlForTopology(AppStore.getCurrentTopology()));
WebapiUtils.getNodeDetails(AppStore.getUrlForTopology(AppStore.getCurrentTopology()), AppStore.getSelectedNodeId());
WebapiUtils.getNodesDelta(AppStore.getCurrentTopologyUrl());
WebapiUtils.getNodeDetails(AppStore.getCurrentTopologyUrl(), AppStore.getSelectedNodeId());
}
};

View File

@@ -1,25 +0,0 @@
var AppDispatcher = require('../dispatcher/app-dispatcher');
var ActionTypes = require('../constants/action-types');
module.exports = {
enterNode: function(nodeId) {
AppDispatcher.dispatch({
type: ActionTypes.ENTER_NODE,
nodeId: nodeId
});
},
leaveNode: function(nodeId) {
AppDispatcher.dispatch({
type: ActionTypes.LEAVE_NODE,
nodeId: nodeId
});
},
receiveNodesDelta: function(delta) {
AppDispatcher.dispatch({
type: ActionTypes.RECEIVE_NODES_DELTA,
delta: delta
});
}
};

View File

@@ -8,7 +8,6 @@ var AppStore = require('../stores/app-store');
var Groupings = require('./groupings.js');
var Status = require('./status.js');
var Topologies = require('./topologies.js');
var TopologyStore = require('../stores/topology-store');
var WebapiUtils = require('../utils/web-api-utils');
var AppActions = require('../actions/app-actions');
var Details = require('./details');
@@ -20,12 +19,12 @@ var ESC_KEY_CODE = 27;
function getStateFromStores() {
return {
activeTopology: AppStore.getCurrentTopology(),
currentTopology: AppStore.getCurrentTopology(),
connectionState: AppStore.getConnectionState(),
currentGrouping: AppStore.getCurrentGrouping(),
selectedNodeId: AppStore.getSelectedNodeId(),
nodeDetails: AppStore.getNodeDetails(),
nodes: TopologyStore.getNodes(),
nodes: AppStore.getNodes(),
topologies: AppStore.getTopologies()
}
}
@@ -38,7 +37,6 @@ var App = React.createClass({
},
componentDidMount: function() {
TopologyStore.on(TopologyStore.CHANGE_EVENT, this.onChange);
AppStore.on(AppStore.CHANGE_EVENT, this.onChange);
window.addEventListener('keyup', this.onKeyPress);
@@ -63,13 +61,12 @@ var App = React.createClass({
<div>
{showingDetails && <Details nodes={this.state.nodes}
nodeId={this.state.selectedNodeId}
details={this.state.nodeDetails}
topology={this.state.activeTopology} /> }
details={this.state.nodeDetails} /> }
<div className="header">
<Logo />
<Topologies topologies={this.state.topologies} active={this.state.activeTopology} />
<Groupings active={this.state.currentGrouping} />
<Topologies topologies={this.state.topologies} currentTopology={this.state.currentTopology} />
<Groupings active={this.state.currentGrouping} currentTopology={this.state.currentTopology} />
<Status connectionState={this.state.connectionState} />
</div>

View File

@@ -8,7 +8,6 @@ var IconButton = mui.IconButton;
var AppActions = require('../actions/app-actions');
var NodeDetails = require('./node-details');
var WebapiUtils = require('../utils/web-api-utils');
var Details = React.createClass({

View File

@@ -8,10 +8,12 @@ var AppStore = require('../stores/app-store');
var GROUPINGS = [{
id: 'none',
iconClass: 'fa fa-th'
iconClass: 'fa fa-th',
needsTopology: false
}, {
id: 'grouped',
iconClass: 'fa fa-th-large'
iconClass: 'fa fa-th-large',
needsTopology: 'grouped_url'
}];
var Groupings = React.createClass({
@@ -21,22 +23,40 @@ var Groupings = React.createClass({
AppActions.clickGrouping(ev.currentTarget.getAttribute('rel'));
},
renderGrouping: function(grouping, active) {
var className = grouping.id === active ? "groupings-item groupings-item-active" : "groupings-item";
isGroupingSupportedByTopology: function(topology, grouping) {
return !grouping.needsTopology || topology && topology[grouping.needsTopology];
},
getGroupingsSupportedByTopology: function(topology) {
return _.filter(GROUPINGS, _.partial(this.isGroupingSupportedByTopology, topology));
},
renderGrouping: function(grouping, activeGroupingId) {
var className = "groupings-item",
isSupportedByTopology = this.isGroupingSupportedByTopology(this.props.currentTopology, grouping);
if (grouping.id === activeGroupingId) {
className += " groupings-item-active";
} else if (!isSupportedByTopology) {
className += " groupings-item-disabled";
} else {
className += " groupings-item-default";
}
return (
<div className={className} key={grouping.id} rel={grouping.id} onClick={this.onGroupingClick}>
<div className={className} key={grouping.id} rel={grouping.id} onClick={isSupportedByTopology && this.onGroupingClick}>
<span className={grouping.iconClass} />
</div>
);
},
render: function() {
var activeGrouping = this.props.active;
var activeGrouping = this.props.active,
isGroupingSupported = _.size(this.getGroupingsSupportedByTopology(this.props.currentTopology)) > 1;
return (
<div className="groupings">
{GROUPINGS.map(function(grouping) {
{isGroupingSupported && GROUPINGS.map(function(grouping) {
return this.renderGrouping(grouping, activeGrouping);
}, this)}
</div>

View File

@@ -14,8 +14,9 @@ var Topologies = React.createClass({
},
renderTopology: function(topology, active) {
var className = AppStore.isUrlForTopology(topology.url, active) ? "topologies-item topologies-item-active" : "topologies-item",
topologyId = AppStore.getTopologyForUrl(topology.url),
var isActive = topology.name === this.props.currentTopology.name,
className = isActive ? "topologies-item topologies-item-active" : "topologies-item",
topologyId = AppStore.getTopologyIdForUrl(topology.url),
title = ['Topology: ' + topology.name,
'Nodes: ' + topology.stats.node_count,
'Connections: ' + topology.stats.node_count].join('\n');
@@ -32,15 +33,14 @@ var Topologies = React.createClass({
},
render: function() {
var activeTopologyId = this.props.active,
topologies = _.sortBy(this.props.topologies, function(topology) {
var topologies = _.sortBy(this.props.topologies, function(topology) {
return topology.name;
});
return (
<div className="topologies">
{topologies.map(function(topology) {
return this.renderTopology(topology, activeTopologyId);
{this.props.currentTopology && topologies.map(function(topology) {
return this.renderTopology(topology);
}, this)}
</div>
);

View File

@@ -0,0 +1,61 @@
describe('AppStore', function() {
var ActionTypes = require('../../constants/action-types');
var AppStore, registeredCallback;
// actions
var ClickTopologyAction = {
type: ActionTypes.CLICK_TOPOLOGY,
topologyId: 'topo1'
};
var ClickGroupingAction = {
type: ActionTypes.CLICK_GROUPING,
grouping: 'grouped'
};
var ReceiveTopologiesAction = {
type: ActionTypes.RECEIVE_TOPOLOGIES,
topologies: [{
url: '/topo1',
grouped_url: '/topo1grouped',
name: 'Topo1'
}]
};
beforeEach(function() {
AppStore = require('../app-store');
registeredCallback = AppStore.registeredCallback;
});
// topology tests
it('init with no topologies', function() {
var topos = AppStore.getTopologies();
expect(topos.length).toBe(0);
expect(AppStore.getCurrentTopology()).toBeUndefined();
});
it('get current topology', function() {
registeredCallback(ClickTopologyAction);
registeredCallback(ReceiveTopologiesAction);
expect(AppStore.getTopologies().length).toBe(1);
expect(AppStore.getCurrentTopology().name).toBe('Topo1');
expect(AppStore.getCurrentTopologyUrl()).toBe('/topo1');
});
it('get grouped topology', function() {
registeredCallback(ClickTopologyAction);
registeredCallback(ReceiveTopologiesAction);
registeredCallback(ClickGroupingAction);
expect(AppStore.getTopologies().length).toBe(1);
expect(AppStore.getCurrentTopology().name).toBe('Topo1');
expect(AppStore.getCurrentTopologyUrl()).toBe('/topo1grouped');
});
});

View File

@@ -5,15 +5,15 @@ var assign = require('object-assign');
var AppDispatcher = require('../dispatcher/app-dispatcher');
var ActionTypes = require('../constants/action-types');
var TopologyStore = require('./topology-store');
// var topologies = require('../constants/topologies');
// Initial values
var connectionState = 'disconnected';
var currentGrouping = 'none';
var currentTopology = 'applications';
var currentTopologyId = 'applications';
var mouseOverNode = null;
var nodes = {};
var nodeDetails = null;
var selectedNodeId = null;
var topologies = [];
@@ -26,8 +26,8 @@ var AppStore = assign({}, EventEmitter.prototype, {
getAppState: function() {
return {
currentTopology: this.getCurrentTopology(),
currentGrouping: this.getCurrentGrouping(),
topologyId: currentTopologyId,
grouping: this.getCurrentGrouping(),
selectedNodeId: this.getSelectedNodeId()
};
},
@@ -37,7 +37,17 @@ var AppStore = assign({}, EventEmitter.prototype, {
},
getCurrentTopology: function() {
return currentTopology;
return _.find(topologies, function(topology) {
return isUrlForTopologyId(topology.url, currentTopologyId);
});
},
getCurrentTopologyUrl: function() {
var topology = this.getCurrentTopology();
if (topology) {
return topology.grouped_url && currentGrouping == 'grouped' ? topology.grouped_url : topology.url;
}
},
getCurrentGrouping: function() {
@@ -48,6 +58,10 @@ var AppStore = assign({}, EventEmitter.prototype, {
return nodeDetails;
},
getNodes: function() {
return nodes;
},
getSelectedNodeId: function() {
return selectedNodeId;
},
@@ -56,40 +70,35 @@ var AppStore = assign({}, EventEmitter.prototype, {
return topologies;
},
getTopologyForUrl: function(url) {
getTopologyIdForUrl: function(url) {
return url.split('/').pop();
},
getUrlForTopology: function(topologyId) {
var topology = _.find(topologies, function(topology) {
return this.isUrlForTopology(topology.url, topologyId);
}, this);
if (topology) {
return topology.grouped_url && currentGrouping == 'grouped' ? topology.grouped_url : topology.url;
}
},
isUrlForTopology: function(url, topologyId) {
return _.endsWith(url, topologyId);
}
});
// Helpers
function isUrlForTopologyId(url, topologyId) {
return _.endsWith(url, topologyId);
}
// Store Dispatch Hooks
AppStore.dispatchToken = AppDispatcher.register(function(payload) {
AppStore.registeredCallback = function(payload) {
switch (payload.type) {
case ActionTypes.CLICK_CLOSE_DETAILS:
selectedNodeId = null;
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.CLICK_GROUPING:
currentGrouping = payload.grouping;
AppDispatcher.waitFor([TopologyStore.dispatchToken]);
AppStore.emit(AppStore.CHANGE_EVENT);
if (payload.grouping !== currentGrouping) {
currentGrouping = payload.grouping;
nodes = {};
AppStore.emit(AppStore.CHANGE_EVENT);
}
break;
case ActionTypes.CLICK_NODE:
@@ -98,8 +107,15 @@ AppStore.dispatchToken = AppDispatcher.register(function(payload) {
break;
case ActionTypes.CLICK_TOPOLOGY:
currentTopology = payload.topologyId;
AppDispatcher.waitFor([TopologyStore.dispatchToken]);
if (payload.topologyId !== currentTopologyId) {
currentTopologyId = payload.topologyId;
nodes = {};
}
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.ENTER_NODE:
mouseOverNode = payload.nodeId;
AppStore.emit(AppStore.CHANGE_EVENT);
break;
@@ -109,14 +125,43 @@ AppStore.dispatchToken = AppDispatcher.register(function(payload) {
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.LEAVE_NODE:
mouseOverNode = null;
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.RECEIVE_NODE_DETAILS:
nodeDetails = payload.details;
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.RECEIVE_NODES_DELTA:
console.log('RECEIVE_NODES_DELTA',
'remove', _.size(payload.delta.remove),
'update', _.size(payload.delta.update),
'add', _.size(payload.delta.add));
connectionState = "connected";
AppDispatcher.waitFor([TopologyStore.dispatchToken]);
// nodes that no longer exist
_.each(payload.delta.remove, function(nodeId) {
// in case node disappears before mouseleave event
if (mouseOverNode === nodeId) {
mouseOverNode = null;
}
delete nodes[nodeId];
});
// update existing nodes
_.each(payload.delta.update, function(node) {
nodes[node.id] = node;
});
// add new nodes
_.each(payload.delta.add, function(node) {
nodes[node.id] = node;
});
AppStore.emit(AppStore.CHANGE_EVENT);
break;
@@ -126,10 +171,10 @@ AppStore.dispatchToken = AppDispatcher.register(function(payload) {
break;
case ActionTypes.ROUTE_TOPOLOGY:
currentTopology = payload.state.currentTopology;
currentGrouping = payload.state.currentGrouping;
nodes = {};
currentTopologyId = payload.state.topologyId;
currentGrouping = payload.state.grouping;
selectedNodeId = payload.state.selectedNodeId;
AppDispatcher.waitFor([TopologyStore.dispatchToken]);
AppStore.emit(AppStore.CHANGE_EVENT);
break;
@@ -137,6 +182,8 @@ AppStore.dispatchToken = AppDispatcher.register(function(payload) {
break;
}
});
};
AppStore.dispatchToken = AppDispatcher.register(AppStore.registeredCallback);
module.exports = AppStore;

View File

@@ -1,86 +0,0 @@
var EventEmitter = require('events').EventEmitter;
var _ = require('lodash');
var assign = require('object-assign');
var AppDispatcher = require('../dispatcher/app-dispatcher');
var ActionTypes = require('../constants/action-types');
// Initial values
var nodes = {};
var mouseOverNode = null;
// Store API
var TopologyStore = assign({}, EventEmitter.prototype, {
CHANGE_EVENT: 'change',
getNodes: function() {
return nodes;
}
});
// Store Dispatch Hooks
TopologyStore.dispatchToken = AppDispatcher.register(function(payload) {
switch (payload.type) {
case ActionTypes.CLICK_GROUPING:
nodes = {};
TopologyStore.emit(TopologyStore.CHANGE_EVENT);
break;
case ActionTypes.CLICK_TOPOLOGY:
nodes = {};
TopologyStore.emit(TopologyStore.CHANGE_EVENT);
break;
case ActionTypes.ENTER_NODE:
mouseOverNode = payload.nodeId;
TopologyStore.emit(TopologyStore.CHANGE_EVENT);
break;
case ActionTypes.LEAVE_NODE:
mouseOverNode = null;
TopologyStore.emit(TopologyStore.CHANGE_EVENT);
break;
case ActionTypes.RECEIVE_NODES_DELTA:
// nodes that no longer exist
_.each(payload.delta.remove, function(nodeId) {
// in case node disappears before mouseleave event
if (mouseOverNode === nodeId) {
mouseOverNode = null;
}
delete nodes[nodeId];
});
// update existing nodes
_.each(payload.delta.update, function(node) {
nodes[node.id] = node;
});
// add new nodes
_.each(payload.delta.add, function(node) {
nodes[node.id] = node;
});
TopologyStore.emit(TopologyStore.CHANGE_EVENT);
break;
case ActionTypes.ROUTE_TOPOLOGY:
nodes = {};
TopologyStore.emit(TopologyStore.CHANGE_EVENT);
break;
default:
break;
}
});
module.exports = TopologyStore;

View File

@@ -1,8 +1,6 @@
var reqwest = require('reqwest');
var TopologyActions = require('../actions/topology-actions');
var AppActions = require('../actions/app-actions');
var AppStore = require('../stores/app-store');
var WS_URL = window.WS_URL || 'ws://' + location.host;
@@ -33,7 +31,7 @@ function createWebsocket(topologyUrl) {
socket.onmessage = function(event) {
var msg = JSON.parse(event.data);
if (msg.add || msg.remove || msg.update) {
TopologyActions.receiveNodesDelta(msg);
AppActions.receiveNodesDelta(msg);
}
};
@@ -48,9 +46,9 @@ function getTopologies() {
});
}
function getNodeDetails(topology, nodeId) {
if (nodeId) {
var url = [AppStore.getUrlForTopology(topology), nodeId].join('/');
function getNodeDetails(topologyUrl, nodeId) {
if (topologyUrl && nodeId) {
var url = [topologyUrl, nodeId].join('/');
reqwest(url, function(res) {
AppActions.receiveNodeDetails(res.node);
});