mirror of
https://github.com/weaveworks/scope.git
synced 2026-07-28 17:51:21 +00:00
Add dropdown field for topology options, save topology option in url state
- fix nodes reset after topology option change - set topology on click - explicitly set default topology options - set default topo options when topos are received - prevent JS error on too many nodes layout abort
This commit is contained in:
committed by
Tom Wilkie
parent
1dfc725706
commit
27caf0af41
4562
app/static.go
4562
app/static.go
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,16 @@ let RouterUtils;
|
||||
let WebapiUtils;
|
||||
|
||||
module.exports = {
|
||||
changeTopologyOption: function(option, value) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.CHANGE_TOPOLOGY_OPTION,
|
||||
option: option,
|
||||
value: value
|
||||
});
|
||||
RouterUtils.updateRoute();
|
||||
WebapiUtils.getNodesDelta(AppStore.getCurrentTopologyUrl(), AppStore.getActiveTopologyOptions());
|
||||
},
|
||||
|
||||
clickCloseDetails: function() {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.CLICK_CLOSE_DETAILS
|
||||
@@ -27,7 +37,7 @@ module.exports = {
|
||||
topologyId: topologyId
|
||||
});
|
||||
RouterUtils.updateRoute();
|
||||
WebapiUtils.getNodesDelta(AppStore.getCurrentTopologyUrl());
|
||||
WebapiUtils.getNodesDelta(AppStore.getCurrentTopologyUrl(), AppStore.getActiveTopologyOptions());
|
||||
},
|
||||
|
||||
openWebsocket: function() {
|
||||
@@ -96,7 +106,7 @@ module.exports = {
|
||||
type: ActionTypes.RECEIVE_TOPOLOGIES,
|
||||
topologies: topologies
|
||||
});
|
||||
WebapiUtils.getNodesDelta(AppStore.getCurrentTopologyUrl());
|
||||
WebapiUtils.getNodesDelta(AppStore.getCurrentTopologyUrl(), AppStore.getActiveTopologyOptions());
|
||||
WebapiUtils.getNodeDetails(AppStore.getCurrentTopologyUrl(), AppStore.getSelectedNodeId());
|
||||
},
|
||||
|
||||
@@ -119,7 +129,7 @@ module.exports = {
|
||||
state: state,
|
||||
type: ActionTypes.ROUTE_TOPOLOGY
|
||||
});
|
||||
WebapiUtils.getNodesDelta(AppStore.getCurrentTopologyUrl());
|
||||
WebapiUtils.getNodesDelta(AppStore.getCurrentTopologyUrl(), AppStore.getActiveTopologyOptions());
|
||||
WebapiUtils.getNodeDetails(AppStore.getCurrentTopologyUrl(), AppStore.getSelectedNodeId());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -211,8 +211,12 @@ const NodesChart = React.createClass({
|
||||
|
||||
debug('graph layout took ' + timedLayouter.time + 'ms');
|
||||
|
||||
// adjust layout based on viewport
|
||||
// layout was aborted
|
||||
if (!graph) {
|
||||
return;
|
||||
}
|
||||
|
||||
// adjust layout based on viewport
|
||||
const xFactor = (props.width - MARGINS.left - MARGINS.right) / graph.width;
|
||||
const yFactor = props.height / graph.height;
|
||||
const zoomFactor = Math.min(xFactor, yFactor);
|
||||
|
||||
@@ -4,6 +4,7 @@ const Logo = require('./logo');
|
||||
const AppStore = require('../stores/app-store');
|
||||
const Status = require('./status.js');
|
||||
const Topologies = require('./topologies.js');
|
||||
const TopologyOptions = require('./topology-options.js');
|
||||
const WebapiUtils = require('../utils/web-api-utils');
|
||||
const AppActions = require('../actions/app-actions');
|
||||
const Details = require('./details');
|
||||
@@ -14,8 +15,10 @@ const ESC_KEY_CODE = 27;
|
||||
|
||||
function getStateFromStores() {
|
||||
return {
|
||||
activeTopologyOptions: AppStore.getActiveTopologyOptions(),
|
||||
currentTopology: AppStore.getCurrentTopology(),
|
||||
currentTopologyId: AppStore.getCurrentTopologyId(),
|
||||
currentTopologyOptions: AppStore.getCurrentTopologyOptions(),
|
||||
errorUrl: AppStore.getErrorUrl(),
|
||||
highlightedEdgeIds: AppStore.getHighlightedEdgeIds(),
|
||||
highlightedNodeIds: AppStore.getHighlightedNodeIds(),
|
||||
@@ -67,6 +70,8 @@ const App = React.createClass({
|
||||
<div className="header">
|
||||
<Logo />
|
||||
<Topologies topologies={this.state.topologies} currentTopology={this.state.currentTopology} />
|
||||
<TopologyOptions options={this.state.currentTopologyOptions}
|
||||
activeOptions={this.state.activeTopologyOptions} />
|
||||
<Status errorUrl={this.state.errorUrl} websocketClosed={this.state.websocketClosed} />
|
||||
</div>
|
||||
|
||||
|
||||
77
client/app/scripts/components/topology-options.js
Normal file
77
client/app/scripts/components/topology-options.js
Normal file
@@ -0,0 +1,77 @@
|
||||
const React = require('react');
|
||||
const _ = require('lodash');
|
||||
const mui = require('material-ui');
|
||||
const DropDownMenu = mui.DropDownMenu;
|
||||
|
||||
const AppActions = require('../actions/app-actions');
|
||||
|
||||
const TopologyOptions = React.createClass({
|
||||
|
||||
componentDidMount: function() {
|
||||
this.fixWidth();
|
||||
},
|
||||
|
||||
onChange: function(ev, index, item) {
|
||||
ev.preventDefault();
|
||||
AppActions.changeTopologyOption(item.option, item.payload);
|
||||
},
|
||||
|
||||
renderOption: function(items) {
|
||||
let selected = 0;
|
||||
let key;
|
||||
const activeOptions = this.props.activeOptions;
|
||||
const menuItems = items.map(function(item, index) {
|
||||
if (activeOptions[item.option] && activeOptions[item.option] === item.value) {
|
||||
selected = index;
|
||||
}
|
||||
key = item.option;
|
||||
return {
|
||||
option: item.option,
|
||||
payload: item.value,
|
||||
text: item.display
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<DropDownMenu menuItems={menuItems} onChange={this.onChange} key={key}
|
||||
selectedIndex={selected} />
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const options = _.sortBy(
|
||||
_.map(this.props.options, function(items, optionId) {
|
||||
_.each(items, function(item) {
|
||||
item.option = optionId;
|
||||
});
|
||||
items.option = optionId;
|
||||
return items;
|
||||
}),
|
||||
'option'
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="topology-options" ref="container">
|
||||
{options.map(function(items) {
|
||||
return this.renderOption(items);
|
||||
}, this)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
this.fixWidth();
|
||||
},
|
||||
|
||||
fixWidth: function() {
|
||||
const containerNode = this.refs.container.getDOMNode();
|
||||
_.each(containerNode.childNodes, function(child) {
|
||||
// set drop down width to length of current label
|
||||
const label = child.getElementsByClassName('mui-menu-label')[0];
|
||||
const width = label.getBoundingClientRect().width + 40;
|
||||
child.style.width = width + 'px';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = TopologyOptions;
|
||||
@@ -1,6 +1,7 @@
|
||||
const keymirror = require('keymirror');
|
||||
|
||||
module.exports = keymirror({
|
||||
CHANGE_TOPOLOGY_OPTION: null,
|
||||
CLICK_CLOSE_DETAILS: null,
|
||||
CLICK_NODE: null,
|
||||
CLICK_TOPOLOGY: null,
|
||||
|
||||
@@ -28,6 +28,18 @@ describe('AppStore', function() {
|
||||
|
||||
// actions
|
||||
|
||||
const ChangeTopologyOptionAction = {
|
||||
type: ActionTypes.CHANGE_TOPOLOGY_OPTION,
|
||||
option: 'option1',
|
||||
value: 'on'
|
||||
};
|
||||
|
||||
const ChangeTopologyOptionAction2 = {
|
||||
type: ActionTypes.CHANGE_TOPOLOGY_OPTION,
|
||||
option: 'option1',
|
||||
value: 'off'
|
||||
};
|
||||
|
||||
const ClickNodeAction = {
|
||||
type: ActionTypes.CLICK_NODE,
|
||||
nodeId: 'n1'
|
||||
@@ -88,6 +100,12 @@ describe('AppStore', function() {
|
||||
topologies: [{
|
||||
url: '/topo1',
|
||||
name: 'Topo1',
|
||||
options: {
|
||||
option1: [
|
||||
{value: 'on'},
|
||||
{value: 'off', default: true}
|
||||
]
|
||||
},
|
||||
sub_topologies: [{
|
||||
url: '/topo1-grouped',
|
||||
name: 'topo 1 grouped'
|
||||
@@ -122,6 +140,7 @@ describe('AppStore', function() {
|
||||
expect(AppStore.getTopologies().length).toBe(1);
|
||||
expect(AppStore.getCurrentTopology().name).toBe('Topo1');
|
||||
expect(AppStore.getCurrentTopologyUrl()).toBe('/topo1');
|
||||
expect(AppStore.getCurrentTopologyOptions().option1).toBeDefined();
|
||||
});
|
||||
|
||||
it('get sub-topology', function() {
|
||||
@@ -131,6 +150,32 @@ describe('AppStore', function() {
|
||||
expect(AppStore.getTopologies().length).toBe(1);
|
||||
expect(AppStore.getCurrentTopology().name).toBe('topo 1 grouped');
|
||||
expect(AppStore.getCurrentTopologyUrl()).toBe('/topo1-grouped');
|
||||
expect(AppStore.getCurrentTopologyOptions()).toBeUndefined();
|
||||
});
|
||||
|
||||
// topology options
|
||||
|
||||
it('changes topology option', function() {
|
||||
// default options
|
||||
registeredCallback(ReceiveTopologiesAction);
|
||||
registeredCallback(ClickTopologyAction);
|
||||
expect(AppStore.getActiveTopologyOptions().option1).toBe('off');
|
||||
expect(AppStore.getAppState().topologyOptions.option1).toBe('off');
|
||||
|
||||
// turn on
|
||||
registeredCallback(ChangeTopologyOptionAction);
|
||||
expect(AppStore.getActiveTopologyOptions().option1).toBe('on');
|
||||
expect(AppStore.getAppState().topologyOptions.option1).toBe('on');
|
||||
|
||||
// turn off
|
||||
registeredCallback(ChangeTopologyOptionAction2);
|
||||
expect(AppStore.getActiveTopologyOptions().option1).toBe('off');
|
||||
expect(AppStore.getAppState().topologyOptions.option1).toBe('off');
|
||||
|
||||
// other topology w/o options
|
||||
registeredCallback(ClickSubTopologyAction);
|
||||
expect(AppStore.getActiveTopologyOptions().option1).toBeUndefined();
|
||||
expect(AppStore.getAppState().topologyOptions.option1).toBeUndefined();
|
||||
});
|
||||
|
||||
// nodes delta
|
||||
@@ -166,12 +211,10 @@ describe('AppStore', function() {
|
||||
registeredCallback(ClickTopologyAction);
|
||||
registeredCallback(ReceiveNodesDeltaAction);
|
||||
|
||||
expect(AppStore.getAppState())
|
||||
.toEqual({"topologyId":"topo1","selectedNodeId": null});
|
||||
expect(AppStore.getAppState().selectedNodeId).toEqual(null);
|
||||
|
||||
registeredCallback(ClickNodeAction);
|
||||
expect(AppStore.getAppState())
|
||||
.toEqual({"topologyId":"topo1","selectedNodeId": 'n1'});
|
||||
expect(AppStore.getAppState().selectedNodeId).toEqual('n1');
|
||||
|
||||
// go back in browsing
|
||||
RouteAction.state = {"topologyId":"topo1","selectedNodeId": null};
|
||||
@@ -185,16 +228,16 @@ describe('AppStore', function() {
|
||||
registeredCallback(ClickTopologyAction);
|
||||
registeredCallback(ReceiveNodesDeltaAction);
|
||||
|
||||
expect(AppStore.getAppState())
|
||||
.toEqual({"topologyId":"topo1","selectedNodeId": null});
|
||||
expect(AppStore.getAppState().selectedNodeId).toEqual(null);
|
||||
expect(AppStore.getAppState().topologyId).toEqual('topo1');
|
||||
|
||||
registeredCallback(ClickNodeAction);
|
||||
expect(AppStore.getAppState())
|
||||
.toEqual({"topologyId":"topo1","selectedNodeId": 'n1'});
|
||||
expect(AppStore.getAppState().selectedNodeId).toEqual('n1');
|
||||
expect(AppStore.getAppState().topologyId).toEqual('topo1');
|
||||
|
||||
registeredCallback(ClickSubTopologyAction);
|
||||
expect(AppStore.getAppState())
|
||||
.toEqual({"topologyId":"topo1-grouped","selectedNodeId": null});
|
||||
expect(AppStore.getAppState().selectedNodeId).toEqual(null);
|
||||
expect(AppStore.getAppState().topologyId).toEqual('topo1-grouped');
|
||||
});
|
||||
|
||||
// connection errors
|
||||
|
||||
@@ -43,6 +43,8 @@ function makeNode(node) {
|
||||
|
||||
// Initial values
|
||||
|
||||
let activeTopologyOptions = {};
|
||||
let currentTopology = null;
|
||||
let currentTopologyId = 'containers';
|
||||
let errorUrl = null;
|
||||
let version = '';
|
||||
@@ -54,33 +56,60 @@ let selectedNodeId = null;
|
||||
let topologies = [];
|
||||
let websocketClosed = true;
|
||||
|
||||
function setTopology(topologyId) {
|
||||
currentTopologyId = topologyId;
|
||||
currentTopology = findCurrentTopology(topologies, topologyId);
|
||||
}
|
||||
|
||||
function setDefaultTopologyOptions() {
|
||||
activeTopologyOptions = {};
|
||||
if (currentTopology) {
|
||||
_.each(currentTopology.options, function(items, option) {
|
||||
_.each(items, function(item) {
|
||||
if (item.default === true) {
|
||||
activeTopologyOptions[option] = item.value;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Store API
|
||||
|
||||
const AppStore = assign({}, EventEmitter.prototype, {
|
||||
|
||||
CHANGE_EVENT: 'change',
|
||||
|
||||
// keep at the top
|
||||
getAppState: function() {
|
||||
return {
|
||||
topologyId: currentTopologyId,
|
||||
selectedNodeId: this.getSelectedNodeId()
|
||||
selectedNodeId: this.getSelectedNodeId(),
|
||||
topologyOptions: this.getActiveTopologyOptions()
|
||||
};
|
||||
},
|
||||
|
||||
getActiveTopologyOptions: function() {
|
||||
return activeTopologyOptions;
|
||||
},
|
||||
|
||||
getCurrentTopology: function() {
|
||||
return findCurrentTopology(topologies, currentTopologyId);
|
||||
if (!currentTopology) {
|
||||
currentTopology = setTopology(currentTopologyId);
|
||||
}
|
||||
return currentTopology;
|
||||
},
|
||||
|
||||
getCurrentTopologyId: function() {
|
||||
return currentTopologyId;
|
||||
},
|
||||
|
||||
getCurrentTopologyUrl: function() {
|
||||
const topology = this.getCurrentTopology();
|
||||
getCurrentTopologyOptions: function() {
|
||||
return currentTopology && currentTopology.options;
|
||||
},
|
||||
|
||||
if (topology) {
|
||||
return topology.url;
|
||||
}
|
||||
getCurrentTopologyUrl: function() {
|
||||
return currentTopology && currentTopology.url;
|
||||
},
|
||||
|
||||
getErrorUrl: function() {
|
||||
@@ -156,6 +185,14 @@ const AppStore = assign({}, EventEmitter.prototype, {
|
||||
AppStore.registeredCallback = function(payload) {
|
||||
switch (payload.type) {
|
||||
|
||||
case ActionTypes.CHANGE_TOPOLOGY_OPTION:
|
||||
if (activeTopologyOptions[payload.option] !== payload.value) {
|
||||
nodes = nodes.clear();
|
||||
}
|
||||
activeTopologyOptions[payload.option] = payload.value;
|
||||
AppStore.emit(AppStore.CHANGE_EVENT);
|
||||
break;
|
||||
|
||||
case ActionTypes.CLICK_CLOSE_DETAILS:
|
||||
selectedNodeId = null;
|
||||
AppStore.emit(AppStore.CHANGE_EVENT);
|
||||
@@ -169,7 +206,8 @@ AppStore.registeredCallback = function(payload) {
|
||||
case ActionTypes.CLICK_TOPOLOGY:
|
||||
selectedNodeId = null;
|
||||
if (payload.topologyId !== currentTopologyId) {
|
||||
currentTopologyId = payload.topologyId;
|
||||
setTopology(payload.topologyId);
|
||||
setDefaultTopologyOptions();
|
||||
nodes = nodes.clear();
|
||||
}
|
||||
AppStore.emit(AppStore.CHANGE_EVENT);
|
||||
@@ -261,6 +299,10 @@ AppStore.registeredCallback = function(payload) {
|
||||
case ActionTypes.RECEIVE_TOPOLOGIES:
|
||||
errorUrl = null;
|
||||
topologies = payload.topologies;
|
||||
if (!currentTopology) {
|
||||
setTopology(currentTopologyId);
|
||||
setDefaultTopologyOptions();
|
||||
}
|
||||
AppStore.emit(AppStore.CHANGE_EVENT);
|
||||
break;
|
||||
|
||||
@@ -274,8 +316,10 @@ AppStore.registeredCallback = function(payload) {
|
||||
if (currentTopologyId !== payload.state.topologyId) {
|
||||
nodes = nodes.clear();
|
||||
}
|
||||
currentTopologyId = payload.state.topologyId;
|
||||
setTopology(payload.state.topologyId);
|
||||
setDefaultTopologyOptions();
|
||||
selectedNodeId = payload.state.selectedNodeId;
|
||||
activeTopologyOptions = payload.state.topologyOptions || activeTopologyOptions;
|
||||
AppStore.emit(AppStore.CHANGE_EVENT);
|
||||
break;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const _ = require('lodash');
|
||||
const debug = require('debug')('scope:web-api-utils');
|
||||
const reqwest = require('reqwest');
|
||||
|
||||
@@ -14,17 +15,19 @@ const updateFrequency = '5s';
|
||||
let socket;
|
||||
let reconnectTimer = 0;
|
||||
let currentUrl = null;
|
||||
let currentOptions = null;
|
||||
let topologyTimer = 0;
|
||||
let apiDetailsTimer = 0;
|
||||
|
||||
function createWebsocket(topologyUrl) {
|
||||
function createWebsocket(topologyUrl, optionsQuery) {
|
||||
if (socket) {
|
||||
socket.onclose = null;
|
||||
socket.onerror = null;
|
||||
socket.close();
|
||||
}
|
||||
|
||||
socket = new WebSocket(WS_URL + topologyUrl + '/ws?t=' + updateFrequency);
|
||||
socket = new WebSocket(WS_URL + topologyUrl
|
||||
+ '/ws?t=' + updateFrequency + '&' + optionsQuery);
|
||||
|
||||
socket.onopen = function() {
|
||||
AppActions.openWebsocket();
|
||||
@@ -34,7 +37,7 @@ function createWebsocket(topologyUrl) {
|
||||
clearTimeout(reconnectTimer);
|
||||
socket = null;
|
||||
AppActions.closeWebsocket();
|
||||
debug('Closed websocket to ' + currentUrl);
|
||||
debug('Closed websocket to ' + topologyUrl);
|
||||
|
||||
reconnectTimer = setTimeout(function() {
|
||||
createWebsocket(topologyUrl);
|
||||
@@ -42,7 +45,7 @@ function createWebsocket(topologyUrl) {
|
||||
};
|
||||
|
||||
socket.onerror = function() {
|
||||
debug('Error in websocket to ' + currentUrl);
|
||||
debug('Error in websocket to ' + topologyUrl);
|
||||
AppActions.receiveError(currentUrl);
|
||||
};
|
||||
|
||||
@@ -52,8 +55,6 @@ function createWebsocket(topologyUrl) {
|
||||
AppActions.receiveNodesDelta(msg);
|
||||
}
|
||||
};
|
||||
|
||||
currentUrl = topologyUrl;
|
||||
}
|
||||
|
||||
/* keep URLs relative */
|
||||
@@ -75,6 +76,19 @@ function getTopologies() {
|
||||
});
|
||||
}
|
||||
|
||||
function getTopology(topologyUrl, options) {
|
||||
const optionsQuery = _.map(options, function(value, param) {
|
||||
return param + '=' + value;
|
||||
}).join('&');
|
||||
|
||||
// only recreate websocket if url changed
|
||||
if (topologyUrl && (topologyUrl !== currentUrl || currentOptions !== optionsQuery)) {
|
||||
createWebsocket(topologyUrl, optionsQuery);
|
||||
currentUrl = topologyUrl;
|
||||
currentOptions = optionsQuery;
|
||||
}
|
||||
}
|
||||
|
||||
function getNodeDetails(topologyUrl, nodeId) {
|
||||
if (topologyUrl && nodeId) {
|
||||
const url = [topologyUrl, encodeURIComponent(nodeId)].join('/').substr(1);
|
||||
@@ -115,10 +129,6 @@ module.exports = {
|
||||
|
||||
getApiDetails: getApiDetails,
|
||||
|
||||
getNodesDelta: function(topologyUrl) {
|
||||
if (topologyUrl && topologyUrl !== currentUrl) {
|
||||
createWebsocket(topologyUrl);
|
||||
}
|
||||
}
|
||||
getNodesDelta: getTopology
|
||||
};
|
||||
|
||||
|
||||
@@ -130,6 +130,10 @@ body {
|
||||
|
||||
}
|
||||
|
||||
.topology-options {
|
||||
margin-top: -16px;
|
||||
}
|
||||
|
||||
.status {
|
||||
float: right;
|
||||
margin-top: 14px;
|
||||
@@ -269,6 +273,22 @@ body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mui-drop-down-menu {
|
||||
.mui-menu-control {
|
||||
.mui-menu-label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.mui-menu-control-underline {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.mui-menu-control-bg {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.node-details {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
Reference in New Issue
Block a user