From 5c0555ae39cbe831e4c709f4c9a67e2e7bc482b1 Mon Sep 17 00:00:00 2001 From: Tom Wilkie Date: Thu, 24 Sep 2015 06:24:11 +0000 Subject: [PATCH 1/2] Report number of filtered nodes in topology stats. Include filtered psuedo nodes in stats. --- app/api_topologies.go | 15 ++++++++---- app/router.go | 24 +++++++++++-------- render/render.go | 50 +++++++++++++++++++++++++++++++++++++-- render/render_test.go | 3 +++ render/selectors.go | 5 ++++ render/topologies.go | 36 +++++++++++++--------------- render/topologies_test.go | 4 ++-- 7 files changed, 100 insertions(+), 37 deletions(-) diff --git a/app/api_topologies.go b/app/api_topologies.go index f62511e3f..6a3af6f1d 100644 --- a/app/api_topologies.go +++ b/app/api_topologies.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/weaveworks/scope/render" + "github.com/weaveworks/scope/report" "github.com/weaveworks/scope/xfer" ) @@ -27,6 +28,7 @@ type topologyStats struct { NodeCount int `json:"node_count"` NonpseudoNodeCount int `json:"nonpseudo_node_count"` EdgeCount int `json:"edge_count"` + FilteredNodes int `json:"filtered_nodes"` } // makeTopologyList returns a handler that yields an APITopologyList. @@ -41,16 +43,18 @@ func makeTopologyList(rep xfer.Reporter) func(w http.ResponseWriter, r *http.Req if def.parent != "" { continue } + decorateTopologyForRequest(r, &def) // Collect all sub-topologies of this one, depth=1 only. subTopologies := []APITopologyDesc{} for subName, subDef := range topologyRegistry { if subDef.parent == name { + decorateTopologyForRequest(r, &subDef) subTopologies = append(subTopologies, APITopologyDesc{ Name: subDef.human, URL: "/api/topology/" + subName, Options: makeTopologyOptions(subDef), - Stats: stats(subDef.renderer.Render(rpt)), + Stats: stats(subDef.renderer, rpt), }) } } @@ -61,7 +65,7 @@ func makeTopologyList(rep xfer.Reporter) func(w http.ResponseWriter, r *http.Req URL: "/api/topology/" + name, SubTopologies: subTopologies, Options: makeTopologyOptions(def), - Stats: stats(def.renderer.Render(rpt)), + Stats: stats(def.renderer, rpt), }) } respondWith(w, http.StatusOK, topologies) @@ -82,14 +86,14 @@ func makeTopologyOptions(view topologyView) map[string][]APITopologyOption { return options } -func stats(r render.RenderableNodes) *topologyStats { +func stats(renderer render.Renderer, rpt report.Report) *topologyStats { var ( nodes int realNodes int edges int ) - for _, n := range r { + for _, n := range renderer.Render(rpt) { nodes++ if !n.Pseudo { realNodes++ @@ -97,9 +101,12 @@ func stats(r render.RenderableNodes) *topologyStats { edges += len(n.Adjacency) } + renderStats := renderer.Stats(rpt) + return &topologyStats{ NodeCount: nodes, NonpseudoNodeCount: realNodes, EdgeCount: edges, + FilteredNodes: renderStats.FilteredNodes, } } diff --git a/app/router.go b/app/router.go index 9e99c3ed3..441580457 100644 --- a/app/router.go +++ b/app/router.go @@ -100,6 +100,17 @@ func makeReportPostHandler(a xfer.Adder) http.HandlerFunc { } } +func decorateTopologyForRequest(r *http.Request, topology *topologyView) { + for param, opts := range topology.options { + value := r.FormValue(param) + for _, opt := range opts { + if (value == "" && opt.def) || (opt.value != "" && opt.value == value) { + topology.renderer = opt.decorator(topology.renderer) + } + } + } +} + func captureTopology(rep xfer.Reporter, f func(xfer.Reporter, topologyView, http.ResponseWriter, *http.Request)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { topology, ok := topologyRegistry[mux.Vars(r)["topology"]] @@ -107,14 +118,7 @@ func captureTopology(rep xfer.Reporter, f func(xfer.Reporter, topologyView, http http.NotFound(w, r) return } - for param, opts := range topology.options { - value := r.FormValue(param) - for _, opt := range opts { - if (value == "" && opt.def) || (opt.value != "" && opt.value == value) { - topology.renderer = opt.decorator(topology.renderer) - } - } - } + decorateTopologyForRequest(r, &topology) f(rep, topology, w, r) } } @@ -135,7 +139,7 @@ var topologyRegistry = map[string]topologyView{ "applications": { human: "Applications", parent: "", - renderer: render.FilterUnconnected(render.ProcessWithContainerNameRenderer{}), + renderer: render.FilterUnconnected(render.ProcessWithContainerNameRenderer), }, "applications-by-name": { human: "by name", @@ -145,7 +149,7 @@ var topologyRegistry = map[string]topologyView{ "containers": { human: "Containers", parent: "", - renderer: render.ContainerWithImageNameRenderer{}, + renderer: render.ContainerWithImageNameRenderer, options: optionParams{"system": { {"show", "System containers shown", false, nop}, {"hide", "System containers hidden", true, render.FilterSystem}, diff --git a/render/render.go b/render/render.go index 0222c4fce..d964168e6 100644 --- a/render/render.go +++ b/render/render.go @@ -11,6 +11,18 @@ import ( type Renderer interface { Render(report.Report) RenderableNodes EdgeMetadata(rpt report.Report, localID, remoteID string) report.EdgeMetadata + Stats(report.Report) Stats +} + +// Stats is the type returned by Renderer.Stats +type Stats struct { + FilteredNodes int +} + +func (s Stats) merge(other Stats) Stats { + return Stats{ + FilteredNodes: s.FilteredNodes + other.FilteredNodes, + } } // Reduce renderer is a Renderer which merges together the output of several @@ -40,6 +52,15 @@ func (r Reduce) EdgeMetadata(rpt report.Report, localID, remoteID string) report return metadata } +// Stats implements Renderer +func (r Reduce) Stats(rpt report.Report) Stats { + var result Stats + for _, renderer := range r { + result = result.merge(renderer.Stats(rpt)) + } + return result +} + // Map is a Renderer which produces a set of RenderableNodes from the set of // RenderableNodes produced by another Renderer. type Map struct { @@ -54,6 +75,14 @@ func (m Map) Render(rpt report.Report) RenderableNodes { return output } +// Stats implements Renderer +func (m Map) Stats(rpt report.Report) Stats { + // There doesn't seem to be an instance where we want stats to recurse + // through Maps - for instance we don't want to see the number of filtered + // processes in the container renderer. + return Stats{} +} + func (m Map) render(rpt report.Report) (RenderableNodes, map[string]report.IDList) { var ( input = m.Renderer.Render(rpt) @@ -177,13 +206,21 @@ type Filter struct { // Render implements Renderer func (f Filter) Render(rpt report.Report) RenderableNodes { + nodes, _ := f.render(rpt) + return nodes +} + +func (f Filter) render(rpt report.Report) (RenderableNodes, int) { output := RenderableNodes{} inDegrees := map[string]int{} + filtered := 0 for id, node := range f.Renderer.Render(rpt) { if f.FilterFunc(node) { output[id] = node + inDegrees[id] = 0 + } else { + filtered++ } - inDegrees[id] = 0 } // Deleted nodes also need to be cut as destinations in adjacency lists. @@ -209,8 +246,17 @@ func (f Filter) Render(rpt report.Report) RenderableNodes { continue } delete(output, id) + filtered++ } - return output + return output, filtered +} + +// Stats implements Renderer +func (f Filter) Stats(rpt report.Report) Stats { + _, filtered := f.render(rpt) + var upstream = f.Renderer.Stats(rpt) + upstream.FilteredNodes += filtered + return upstream } // IsConnected is the key added to Node.Metadata by ColorConnected diff --git a/render/render_test.go b/render/render_test.go index f12f080b9..ed27099b4 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -20,6 +20,9 @@ func (m mockRenderer) Render(rpt report.Report) render.RenderableNodes { func (m mockRenderer) EdgeMetadata(rpt report.Report, localID, remoteID string) report.EdgeMetadata { return m.edgeMetadata } +func (m mockRenderer) Stats(rpt report.Report) render.Stats { + return render.Stats{} +} func TestReduceRender(t *testing.T) { renderer := render.Reduce([]render.Renderer{ diff --git a/render/selectors.go b/render/selectors.go index 88f186202..aafab995f 100644 --- a/render/selectors.go +++ b/render/selectors.go @@ -29,6 +29,11 @@ func (t TopologySelector) EdgeMetadata(rpt report.Report, srcID, dstID string) r return metadata } +// Stats implements Renderer +func (t TopologySelector) Stats(r report.Report) Stats { + return Stats{} +} + // MakeRenderableNodes converts a topology to a set of RenderableNodes func MakeRenderableNodes(t report.Topology) RenderableNodes { result := RenderableNodes{} diff --git a/render/topologies.go b/render/topologies.go index aff608093..13c10e700 100644 --- a/render/topologies.go +++ b/render/topologies.go @@ -27,14 +27,14 @@ var ProcessRenderer = MakeReduce( }, ) -// ProcessWithContainerNameRenderer is a Renderer which produces a process +// processWithContainerNameRenderer is a Renderer which produces a process // graph enriched with container names where appropriate -type ProcessWithContainerNameRenderer struct{} +type processWithContainerNameRenderer struct { + Renderer +} -// Render produces a process graph where the minor labels contain the -// container name, if found. -func (r ProcessWithContainerNameRenderer) Render(rpt report.Report) RenderableNodes { - processes := ProcessRenderer.Render(rpt) +func (r processWithContainerNameRenderer) Render(rpt report.Report) RenderableNodes { + processes := r.Renderer.Render(rpt) containers := Map{ MapFunc: MapContainerIdentity, Renderer: SelectContainer, @@ -60,10 +60,9 @@ func (r ProcessWithContainerNameRenderer) Render(rpt report.Report) RenderableNo return processes } -// EdgeMetadata produces an EdgeMetadata for a given edge. -func (r ProcessWithContainerNameRenderer) EdgeMetadata(rpt report.Report, localID, remoteID string) report.EdgeMetadata { - return ProcessRenderer.EdgeMetadata(rpt, localID, remoteID) -} +// ProcessWithContainerNameRenderer is a Renderer which produces a process +// graph enriched with container names where appropriate +var ProcessWithContainerNameRenderer = processWithContainerNameRenderer{ProcessRenderer} // ProcessRenderer is a Renderer which produces a renderable process // name graph by munging the progess graph. @@ -120,14 +119,14 @@ var ContainerRenderer = MakeReduce( }, ) -// ContainerWithImageNameRenderer is a Renderer which produces a container -// graph where the ranks are the image names, not their IDs -type ContainerWithImageNameRenderer struct{} +type containerWithImageNameRenderer struct { + Renderer +} // Render produces a process graph where the minor labels contain the // container name, if found. -func (r ContainerWithImageNameRenderer) Render(rpt report.Report) RenderableNodes { - containers := ContainerRenderer.Render(rpt) +func (r containerWithImageNameRenderer) Render(rpt report.Report) RenderableNodes { + containers := r.Renderer.Render(rpt) images := Map{ MapFunc: MapContainerImageIdentity, Renderer: SelectContainerImage, @@ -149,10 +148,9 @@ func (r ContainerWithImageNameRenderer) Render(rpt report.Report) RenderableNode return containers } -// EdgeMetadata produces an EdgeMetadata for a given edge. -func (r ContainerWithImageNameRenderer) EdgeMetadata(rpt report.Report, localID, remoteID string) report.EdgeMetadata { - return ContainerRenderer.EdgeMetadata(rpt, localID, remoteID) -} +// ContainerWithImageNameRenderer is a Renderer which produces a container +// graph where the ranks are the image names, not their IDs +var ContainerWithImageNameRenderer = containerWithImageNameRenderer{ContainerRenderer} // ContainerImageRenderer is a Renderer which produces a renderable container // image graph by merging the container graph and the container image topology. diff --git a/render/topologies_test.go b/render/topologies_test.go index e87e0b229..07c5ab865 100644 --- a/render/topologies_test.go +++ b/render/topologies_test.go @@ -27,7 +27,7 @@ func TestProcessNameRenderer(t *testing.T) { } func TestContainerRenderer(t *testing.T) { - have := (render.ContainerWithImageNameRenderer{}.Render(test.Report)).Prune() + have := (render.ContainerWithImageNameRenderer.Render(test.Report)).Prune() want := expected.RenderedContainers if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) @@ -39,7 +39,7 @@ func TestContainerFilterRenderer(t *testing.T) { // it is filtered out correctly. input := test.Report.Copy() input.Container.Nodes[test.ClientContainerNodeID].Metadata[docker.LabelPrefix+"works.weave.role"] = "system" - have := render.FilterSystem(render.ContainerWithImageNameRenderer{}).Render(input).Prune() + have := render.FilterSystem(render.ContainerWithImageNameRenderer).Render(input).Prune() want := expected.RenderedContainers.Copy() delete(want, test.ClientContainerID) if !reflect.DeepEqual(want, have) { From 2d7e546ae5848f407682928470a60987c87584da Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Thu, 24 Sep 2015 18:16:18 +0200 Subject: [PATCH 2/2] 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 --- client/app/scripts/actions/app-actions.js | 15 +++- client/app/scripts/charts/nodes-chart.js | 5 +- client/app/scripts/components/app.js | 6 +- client/app/scripts/components/status.js | 5 +- client/app/scripts/components/topologies.js | 5 +- .../components/topology-option-action.js | 2 +- .../scripts/components/topology-options.js | 9 ++- .../stores/__tests__/app-store-test.js | 43 +++++++---- client/app/scripts/stores/app-store.js | 77 ++++++++++++------- client/app/scripts/utils/web-api-utils.js | 23 ++++-- 10 files changed, 126 insertions(+), 64 deletions(-) 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); } }); }