diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 218eb70b3..46931a6d3 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -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() diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index 0a4714c76..a3a52593e 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -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); diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index 9f1fa120b..1b30c91d0 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -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({ + ); }, @@ -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); diff --git a/client/app/scripts/stores/__tests__/app-store-test.js b/client/app/scripts/stores/__tests__/app-store-test.js index 81da4b9b9..3dd4f17e3 100644 --- a/client/app/scripts/stores/__tests__/app-store-test.js +++ b/client/app/scripts/stores/__tests__/app-store-test.js @@ -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); diff --git a/client/app/scripts/stores/app-store.js b/client/app/scripts/stores/app-store.js index aa689c7cd..17d9f1976 100644 --- a/client/app/scripts/stores/app-store.js +++ b/client/app/scripts/stores/app-store.js @@ -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; diff --git a/client/app/scripts/utils/web-api-utils.js b/client/app/scripts/utils/web-api-utils.js index 8cd525d6d..719997d31 100644 --- a/client/app/scripts/utils/web-api-utils.js +++ b/client/app/scripts/utils/web-api-utils.js @@ -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); } }); }