+ {showingNetworks && }
+
{useSvgLabels ?
svgLabels(label, subLabel, labelClassName, subLabelClassName, labelOffsetY) :
@@ -124,7 +129,7 @@ class Node extends React.Component {
@@ -152,7 +157,8 @@ class Node extends React.Component {
export default connect(
state => ({
searchQuery: state.get('searchQuery'),
- exportingGraph: state.get('exportingGraph')
+ exportingGraph: state.get('exportingGraph'),
+ showingNetworks: state.get('showingNetworks'),
}),
{ clickNode, enterNode, leaveNode }
)(Node);
diff --git a/client/app/scripts/charts/nodes-chart-nodes.js b/client/app/scripts/charts/nodes-chart-nodes.js
index 215509628..11d6ed6ee 100644
--- a/client/app/scripts/charts/nodes-chart-nodes.js
+++ b/client/app/scripts/charts/nodes-chart-nodes.js
@@ -1,6 +1,6 @@
import React from 'react';
import { connect } from 'react-redux';
-import { fromJS, Map as makeMap } from 'immutable';
+import { fromJS, Map as makeMap, List as makeList } from 'immutable';
import { getAdjacentNodes } from '../utils/topology-utils';
import NodeContainer from './node-container';
@@ -9,7 +9,7 @@ class NodesChartNodes extends React.Component {
render() {
const { adjacentNodes, highlightedNodeIds, layoutNodes, layoutPrecision,
mouseOverNodeId, nodeScale, scale, searchNodeMatches = makeMap(),
- searchQuery, selectedMetric, selectedNodeScale, selectedNodeId,
+ searchQuery, selectedMetric, selectedNetwork, selectedNodeScale, selectedNodeId,
topCardNode } = this.props;
const zoomScale = scale;
@@ -23,7 +23,8 @@ class NodesChartNodes extends React.Component {
const setBlurred = node => node.set('blurred',
selectedNodeId && !node.get('focused')
|| searchQuery && !searchNodeMatches.has(node.get('id'))
- && !node.get('highlighted'));
+ && !node.get('highlighted')
+ || selectedNetwork && !(node.get('networks') || makeList()).contains(selectedNetwork));
// make sure blurred nodes are in the background
const sortNodes = node => {
@@ -91,6 +92,7 @@ function mapStateToProps(state) {
highlightedNodeIds: state.get('highlightedNodeIds'),
mouseOverNodeId: state.get('mouseOverNodeId'),
selectedMetric: state.get('selectedMetric'),
+ selectedNetwork: state.get('selectedNetwork'),
selectedNodeId: state.get('selectedNodeId'),
searchNodeMatches: state.getIn(['searchNodeMatches', currentTopologyId]),
searchQuery: state.get('searchQuery'),
diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js
index ef23dae70..29bb8eb94 100644
--- a/client/app/scripts/charts/nodes-chart.js
+++ b/client/app/scripts/charts/nodes-chart.js
@@ -179,7 +179,7 @@ class NodesChart extends React.Component {
rank: node.get('rank'),
shape: node.get('shape'),
stack: node.get('stack'),
- networks: node.get('networknetworks'),
+ networks: node.get('networks'),
}));
});
diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js
index 11f27b0f0..2568c9ea0 100644
--- a/client/app/scripts/components/app.js
+++ b/client/app/scripts/components/app.js
@@ -16,6 +16,7 @@ import { focusSearch, pinNextMetric, hitBackspace, hitEnter, hitEsc, unpinMetric
import Details from './details';
import Nodes from './nodes';
import MetricSelector from './metric-selector';
+import NetworkSelector from './networks-selector';
import EmbeddedTerminal from './embedded-terminal';
import { getRouter } from '../utils/router-utils';
import DebugToolbar, { showingDebugToolbar,
@@ -97,7 +98,8 @@ class App extends React.Component {
}
render() {
- const { showingDetails, showingHelp, showingMetricsSelector, showingTerminal } = this.props;
+ const { showingDetails, showingHelp, showingMetricsSelector, showingNetworkSelector,
+ showingTerminal } = this.props;
return (
@@ -124,6 +126,7 @@ class App extends React.Component {
{showingMetricsSelector && }
+ {showingNetworkSelector && }
@@ -142,6 +145,7 @@ function mapStateToProps(state) {
showingDetails: state.get('nodeDetails').size > 0,
showingHelp: state.get('showingHelp'),
showingMetricsSelector: state.get('availableCanvasMetrics').count() > 0,
+ showingNetworkSelector: state.get('availableNetworks').count() > 0,
showingTerminal: state.get('controlPipes').size > 0,
urlState: getUrlState(state)
};
diff --git a/client/app/scripts/components/debug-toolbar.js b/client/app/scripts/components/debug-toolbar.js
index e36bc506e..e52e22876 100644
--- a/client/app/scripts/components/debug-toolbar.js
+++ b/client/app/scripts/components/debug-toolbar.js
@@ -1,5 +1,6 @@
/* eslint react/jsx-no-bind: "off" */
import React from 'react';
+import d3 from 'd3';
import _ from 'lodash';
import Perf from 'react-addons-perf';
import { connect } from 'react-redux';
@@ -9,13 +10,14 @@ import debug from 'debug';
const log = debug('scope:debug-panel');
import { receiveNodesDelta } from '../actions/app-actions';
-import { getNodeColor, getNodeColorDark } from '../utils/color-utils';
+import { getNodeColor, getNodeColorDark, text2degree } from '../utils/color-utils';
const SHAPES = ['square', 'hexagon', 'heptagon', 'circle'];
const NODE_COUNTS = [1, 2, 3];
const STACK_VARIANTS = [false, true];
const METRIC_FILLS = [0, 0.1, 50, 99.9, 100];
+const NETWORKS = ['be', 'fe', 'lb', 'db'];
const LOREM = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
@@ -23,7 +25,7 @@ ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor i
voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.`;
-const sample = (collection) => _.range(_.random(4)).map(() => _.sample(collection));
+const sample = (collection, n = 4) => _.sampleSize(collection, _.random(n));
const shapeTypes = {
@@ -41,7 +43,10 @@ const LABEL_PREFIXES = _.range('A'.charCodeAt(), 'Z'.charCodeAt() + 1)
// const randomLetter = () => _.sample(LABEL_PREFIXES);
-const deltaAdd = (name, adjacency = [], shape = 'circle', stack = false, nodeCount = 1) => ({
+const deltaAdd = (
+ name, adjacency = [], shape = 'circle', stack = false, nodeCount = 1,
+ networks = ['fe', 'be']
+) => ({
adjacency,
controls: {},
shape,
@@ -53,7 +58,7 @@ const deltaAdd = (name, adjacency = [], shape = 'circle', stack = false, nodeCou
latest: {},
origins: [],
rank: name,
- networks: ['fe', 'be'],
+ networks
});
@@ -170,7 +175,8 @@ class DebugToolbar extends React.Component {
sample(allNodes),
_.sample(SHAPES),
_.sample(STACK_VARIANTS),
- _.sample(NODE_COUNTS)
+ _.sample(NODE_COUNTS),
+ sample(NETWORKS, 3)
))
}));
@@ -208,8 +214,21 @@ class DebugToolbar extends React.Component {
- {this.state.showColors && [getNodeColor, getNodeColorDark].map(fn => (
-
+ {this.state.showColors &&
+
+
+ {LABEL_PREFIXES.map(r => (
+
+ |
+
+ ))}
+
+
}
+
+ {this.state.showColors && [getNodeColor, getNodeColorDark].map((fn, i) => (
+
{LABEL_PREFIXES.map(r => (
diff --git a/client/app/scripts/components/network-selector-item.js b/client/app/scripts/components/network-selector-item.js
new file mode 100644
index 000000000..d0bc5557f
--- /dev/null
+++ b/client/app/scripts/components/network-selector-item.js
@@ -0,0 +1,69 @@
+import React from 'react';
+import classNames from 'classnames';
+import { connect } from 'react-redux';
+
+import { selectNetwork, pinNetwork, unpinNetwork } from '../actions/app-actions';
+import { getNodeColor } from '../utils/color-utils';
+
+class NetworkSelectorItem extends React.Component {
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.onMouseOver = this.onMouseOver.bind(this);
+ this.onMouseClick = this.onMouseClick.bind(this);
+ }
+
+ onMouseOver() {
+ const k = this.props.network.get('id');
+ this.props.selectNetwork(k);
+ }
+
+ onMouseClick() {
+ const k = this.props.network.get('id');
+ const pinnedNetwork = this.props.pinnedNetwork;
+
+ if (k === pinnedNetwork) {
+ this.props.unpinNetwork(k);
+ } else {
+ this.props.pinNetwork(k);
+ }
+ }
+
+ render() {
+ const {network, selectedNetwork, pinnedNetwork} = this.props;
+ const id = network.get('id');
+ const isPinned = (id === pinnedNetwork);
+ const isSelected = (id === selectedNetwork);
+ const className = classNames('metric-selector-action', {
+ 'metric-selector-action-selected': isSelected
+ });
+ const style = {
+ backgroundColor: getNodeColor(id)
+ };
+
+ return (
+
+ {network.get('label')}
+ {isPinned && }
+
+ );
+ }
+}
+
+function mapStateToProps(state) {
+ return {
+ selectedNetwork: state.get('selectedNetwork'),
+ pinnedNetwork: state.get('pinnedNetwork')
+ };
+}
+
+export default connect(
+ mapStateToProps,
+ { selectNetwork, pinNetwork, unpinNetwork }
+)(NetworkSelectorItem);
diff --git a/client/app/scripts/components/networks-selector.js b/client/app/scripts/components/networks-selector.js
new file mode 100644
index 000000000..2653478ec
--- /dev/null
+++ b/client/app/scripts/components/networks-selector.js
@@ -0,0 +1,59 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import classNames from 'classnames';
+
+import { selectNetwork, showNetworks } from '../actions/app-actions';
+import NetworkSelectorItem from './network-selector-item';
+
+class NetworkSelector extends React.Component {
+
+ constructor(props, context) {
+ super(props, context);
+ this.onClick = this.onClick.bind(this);
+ this.onMouseOut = this.onMouseOut.bind(this);
+ }
+
+ onClick() {
+ return this.props.showNetworks(!this.props.showingNetworks);
+ }
+
+ onMouseOut() {
+ this.props.selectNetwork(this.props.pinnedNetwork);
+ }
+
+ render() {
+ const { availableNetworks, showingNetworks } = this.props;
+
+ const items = availableNetworks.map(network => (
+
+ ));
+
+ const className = classNames('metric-selector-action', {
+ 'metric-selector-action-selected': showingNetworks
+ });
+
+ return (
+
+
+
+ Networks
+
+ {showingNetworks && items}
+
+
+ );
+ }
+}
+
+function mapStateToProps(state) {
+ return {
+ availableNetworks: state.get('availableNetworks'),
+ showingNetworks: state.get('showingNetworks'),
+ pinnedNetwork: state.get('pinnedNetwork')
+ };
+}
+
+export default connect(
+ mapStateToProps,
+ { selectNetwork, showNetworks }
+)(NetworkSelector);
diff --git a/client/app/scripts/constants/action-types.js b/client/app/scripts/constants/action-types.js
index 5d2b85277..788a274f5 100644
--- a/client/app/scripts/constants/action-types.js
+++ b/client/app/scripts/constants/action-types.js
@@ -47,7 +47,12 @@ const ACTION_TYPES = [
'ROUTE_TOPOLOGY',
'SELECT_METRIC',
'SHOW_HELP',
- 'SET_EXPORTING_GRAPH'
+ 'SET_EXPORTING_GRAPH',
+
+ 'SELECT_NETWORK',
+ 'PIN_NETWORK',
+ 'UNPIN_NETWORK',
+ 'SHOW_NETWORKS',
];
export default _.zipObject(ACTION_TYPES, ACTION_TYPES);
diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js
index 244f09921..298ee2909 100644
--- a/client/app/scripts/reducers/root.js
+++ b/client/app/scripts/reducers/root.js
@@ -20,6 +20,7 @@ const topologySorter = topology => topology.get('rank');
export const initialState = makeMap({
availableCanvasMetrics: makeList(),
+ availableNetworks: makeList(),
controlPipes: makeOrderedMap(), // pipeId -> controlPipe
controlStatus: makeMap(),
currentTopology: null,
@@ -39,6 +40,7 @@ export const initialState = makeMap({
// class of metric, e.g. 'cpu', rather than 'host_cpu' or 'process_cpu'.
// allows us to keep the same metric "type" selected when the topology changes.
pinnedMetricType: null,
+ pinnedNetwork: null,
plugins: makeList(),
pinnedSearches: makeList(), // list of node filters
routeSet: false,
@@ -46,8 +48,10 @@ export const initialState = makeMap({
searchNodeMatches: makeMap(),
searchQuery: null,
selectedMetric: null,
+ selectedNetwork: null,
selectedNodeId: null,
showingHelp: false,
+ showingNetworks: false,
topologies: makeList(),
topologiesLoaded: false,
topologyOptions: makeOrderedMap(), // topologyId -> options
@@ -102,25 +106,46 @@ function setDefaultTopologyOptions(state, topologyList) {
return state;
}
+function shouldCloseExisting(state) {
+ const nodeDetails = state.get('nodeDetails');
+ if (nodeDetails.size > 0 && nodeDetails.valueSeq().last().topologyId === 'networks') {
+ return false;
+ }
+ return true;
+}
+
function closeNodeDetails(state, nodeId) {
const nodeDetails = state.get('nodeDetails');
- if (nodeDetails.size > 0) {
- const popNodeId = nodeId || nodeDetails.keySeq().last();
- // remove pipe if it belongs to the node being closed
- state = state.update('controlPipes',
- controlPipes => controlPipes.filter(pipe => pipe.get('nodeId') !== popNodeId));
- state = state.deleteIn(['nodeDetails', popNodeId]);
+ if (nodeDetails.size === 0) {
+ return state;
}
- if (state.get('nodeDetails').size === 0 || state.get('selectedNodeId') === nodeId) {
+ nodeId = nodeId || nodeDetails.keySeq().last();
+
+ // remove pipe if it belongs to the node being closed
+ state = state.update('controlPipes',
+ controlPipes => controlPipes.filter(pipe => pipe.get('nodeId') !== nodeId));
+ state = state.deleteIn(['nodeDetails', nodeId]);
+
+ // FIXME: duplicated state in a sense, look into reselect or something, we could derive the
+ // selectedNetwork from the contents of nodeDetails
+ //
+ // clear this additional state
+ if (state.get('selectedNetwork') === nodeId) {
+ state = state.set('selectedNetwork', null);
+ state = state.set('pinnedNetwork', null);
+ }
+
+ // TODO: could also be derived state.
+ if (state.get('selectedNodeId') === nodeId) {
state = state.set('selectedNodeId', null);
}
return state;
}
function closeAllNodeDetails(state) {
- while (state.get('nodeDetails').size) {
- state = closeNodeDetails(state);
- }
+ state.get('nodeDetails').keySeq().forEach(nodeId => {
+ state = closeNodeDetails(state, nodeId);
+ });
return state;
}
@@ -183,8 +208,10 @@ export function rootReducer(state = initialState, action) {
const prevSelectedNodeId = state.get('selectedNodeId');
const prevDetailsStackSize = state.get('nodeDetails').size;
+ if (shouldCloseExisting(state)) {
// click on sibling closes all
- state = closeAllNodeDetails(state);
+ state = closeAllNodeDetails(state);
+ }
// select new node if it's not the same (in that case just delesect)
if (prevDetailsStackSize > 1 || prevSelectedNodeId !== action.nodeId) {
@@ -265,6 +292,47 @@ export function rootReducer(state = initialState, action) {
return state;
}
+ //
+ // networks
+ //
+
+ case ActionTypes.SHOW_NETWORKS: {
+ return state.set('showingNetworks', action.visible);
+ }
+
+ case ActionTypes.SELECT_NETWORK: {
+ return state.set('selectedNetwork', action.networkId);
+ }
+
+ case ActionTypes.PIN_NETWORK: {
+ state = closeAllNodeDetails(state);
+
+ state = state.setIn(['nodeDetails', action.networkId],
+ {
+ id: action.networkId,
+ label: action.networkId,
+ origin: null,
+ topologyId: 'networks'
+ }
+ );
+
+ return state.merge({
+ pinnedNetwork: action.networkId,
+ selectedNetwork: action.networkId
+ });
+ }
+
+ case ActionTypes.UNPIN_NETWORK: {
+ state = closeNodeDetails(state, action.networkId);
+ return state.merge({
+ pinnedNetwork: null,
+ });
+ }
+
+ //
+ // metrics
+ //
+
case ActionTypes.SELECT_METRIC: {
return state.set('selectedMetric', action.metricId);
}
@@ -481,6 +549,15 @@ export function rootReducer(state = initialState, action) {
// apply pinned searches, filters nodes that dont match
state = applyPinnedSearches(state);
+ state = state.set('availableNetworks', state.get('nodes')
+ .valueSeq()
+ .flatMap(node => (node.get('networks') || makeList()).map(n => (
+ makeMap({id: n, label: n})
+ )))
+ .toSet()
+ .toList()
+ .sort());
+
state = state.set('availableCanvasMetrics', state.get('nodes')
.valueSeq()
.flatMap(n => (n.get('metrics') || makeList()).map(m => (
@@ -579,6 +656,16 @@ export function rootReducer(state = initialState, action) {
if (!isDeepEqual(state.get('nodeDetails').keySeq(), actionNodeDetails.keySeq())) {
state = state.set('nodeDetails', actionNodeDetails);
}
+ //
+ // load up network view state
+ // TODO: cleanup/extract.
+ //
+ const networkNodes = action.state.nodeDetails.filter(n => n.topologyId === 'networks');
+ if (networkNodes.length > 0) {
+ state = state.set('pinnedNetwork', networkNodes[0].id);
+ state = state.set('selectedNetwork', networkNodes[0].id);
+ state = state.set('showingNetworks', true);
+ }
} else {
state = state.update('nodeDetails', nodeDetails => nodeDetails.clear());
}
diff --git a/client/app/scripts/utils/color-utils.js b/client/app/scripts/utils/color-utils.js
index 7d997d405..27247b5e3 100644
--- a/client/app/scripts/utils/color-utils.js
+++ b/client/app/scripts/utils/color-utils.js
@@ -12,7 +12,7 @@ const letterRange = endLetterRange - startLetterRange;
/**
* Converts a text to a 360 degree value
*/
-function text2degree(text) {
+export function text2degree(text) {
const input = text.substr(0, 2).toUpperCase();
let num = 0;
for (let i = 0; i < input.length; i++) {
diff --git a/client/app/styles/main.less b/client/app/styles/main.less
index 1ead30216..7fbe30e8b 100644
--- a/client/app/styles/main.less
+++ b/client/app/styles/main.less
@@ -341,6 +341,11 @@ h2 {
transition: opacity .5s @base-ease;
text-align: center;
+ .node-networks-overlay {
+ fill: none;
+ stroke: @background-darker-secondary-color;
+ }
+
.node-label,
.node-sublabel {
line-height: 125%;