mirror of
https://github.com/weaveworks/scope.git
synced 2026-03-03 02:00:43 +00:00
Merge pull request #519 from weaveworks/509-filtered-stats
Report number of filtered nodes in topology stats.
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user