From da67ba05f8c69c149d8debf25b63dcf078d2e2ae Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Thu, 19 May 2016 09:59:44 +0100 Subject: [PATCH 01/12] Adds node.networks to the debug toolbar nodes --- client/app/scripts/charts/node-shape-hex.js | 4 +++- client/app/scripts/charts/nodes-chart-nodes.js | 1 + client/app/scripts/charts/nodes-chart.js | 3 ++- client/app/scripts/components/debug-toolbar.js | 4 ++-- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/client/app/scripts/charts/node-shape-hex.js b/client/app/scripts/charts/node-shape-hex.js index d7045c9ab..ce5f84f00 100644 --- a/client/app/scripts/charts/node-shape-hex.js +++ b/client/app/scripts/charts/node-shape-hex.js @@ -30,12 +30,14 @@ function getPoints(h) { } -export default function NodeShapeHex({id, highlighted, size, color, metric}) { +export default function NodeShapeHex({id, highlighted, size, color, metric, networks}) { const pathProps = v => ({ d: getPoints(size * v * 2), transform: `rotate(90) translate(-${size * getWidth(v)}, -${size * v})` }); + console.log('netz', networks); + const shadowSize = 0.45; const upperHexBitHeight = -0.25 * size * shadowSize; diff --git a/client/app/scripts/charts/nodes-chart-nodes.js b/client/app/scripts/charts/nodes-chart-nodes.js index 8304aa634..215509628 100644 --- a/client/app/scripts/charts/nodes-chart-nodes.js +++ b/client/app/scripts/charts/nodes-chart-nodes.js @@ -63,6 +63,7 @@ class NodesChartNodes extends React.Component { matches={searchNodeMatches.get(node.get('id'))} highlighted={node.get('highlighted')} shape={node.get('shape')} + networks={node.get('networks')} stack={node.get('stack')} key={node.get('id')} id={node.get('id')} diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index 3e957a4f5..ef23dae70 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -178,7 +178,8 @@ class NodesChart extends React.Component { metrics: node.get('metrics'), rank: node.get('rank'), shape: node.get('shape'), - stack: node.get('stack') + stack: node.get('stack'), + networks: node.get('networknetworks'), })); }); diff --git a/client/app/scripts/components/debug-toolbar.js b/client/app/scripts/components/debug-toolbar.js index 214f1a1e3..e36bc506e 100644 --- a/client/app/scripts/components/debug-toolbar.js +++ b/client/app/scripts/components/debug-toolbar.js @@ -51,9 +51,9 @@ const deltaAdd = (name, adjacency = [], shape = 'circle', stack = false, nodeCou label: name, label_minor: name, latest: {}, - metadata: {}, origins: [], - rank: name + rank: name, + networks: ['fe', 'be'], }); From 6b4b07d0bc638b8fe7f4deb8ab0b788806de850d Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Thu, 19 May 2016 16:47:09 +0100 Subject: [PATCH 02/12] Copy paste of MoC controls as initial networks-view legend Needs a bit of de-dup / customization oops, bad typo More fleshing out the structure for network-view onHover netview-legend: highlight relevant nodes. And the bool rolls on. Handle nodes w/ no networks better Corrects deselect-node when used w/ new network-view behaviour Net view details "node" can be open when with no nodes selected. Hitting "esc" from: - card 0: network-a - card 1: node-a was not deselecting node-a Deselect selectedNetwork correctly onEsc Ooops, trailing ws breaks linting. Adds NodeNetworksOverlay stub Expands on NodeNetworksOverlay stub and adds arcs and colors Expand and collapse networks legend Open arc for network circle, shift for stack Show our base hue range in the debug bar too.. Was trying to smooth out our hue selector but turned out to be tricky.. Uniquify random data generator! --- client/app/scripts/actions/app-actions.js | 47 ++++++++ .../scripts/charts/node-networks-overlay.js | 47 ++++++++ client/app/scripts/charts/node-shape-hex.js | 4 +- client/app/scripts/charts/node.js | 16 ++- .../app/scripts/charts/nodes-chart-nodes.js | 8 +- client/app/scripts/charts/nodes-chart.js | 2 +- client/app/scripts/components/app.js | 6 +- .../app/scripts/components/debug-toolbar.js | 33 ++++-- .../components/network-selector-item.js | 69 +++++++++++ .../scripts/components/networks-selector.js | 59 ++++++++++ client/app/scripts/constants/action-types.js | 7 +- client/app/scripts/reducers/root.js | 109 ++++++++++++++++-- client/app/scripts/utils/color-utils.js | 2 +- client/app/styles/main.less | 5 + 14 files changed, 381 insertions(+), 33 deletions(-) create mode 100644 client/app/scripts/charts/node-networks-overlay.js create mode 100644 client/app/scripts/components/network-selector-item.js create mode 100644 client/app/scripts/components/networks-selector.js diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 792d9b821..b9348dd23 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -32,6 +32,53 @@ export function toggleHelp() { }; } +// +// Networks +// + + +export function showNetworks(visible) { + return { + type: ActionTypes.SHOW_NETWORKS, + visible + }; +} + + +export function selectNetwork(networkId) { + return { + type: ActionTypes.SELECT_NETWORK, + networkId + }; +} + +export function pinNetwork(networkId) { + return (dispatch, getState) => { + dispatch({ + type: ActionTypes.PIN_NETWORK, + networkId, + }); + + updateRoute(getState); + }; +} + +export function unpinNetwork(networkId) { + return (dispatch, getState) => { + dispatch({ + type: ActionTypes.UNPIN_NETWORK, + networkId, + }); + + updateRoute(getState); + }; +} + + +// +// Metrics +// + export function selectMetric(metricId) { return { type: ActionTypes.SELECT_METRIC, diff --git a/client/app/scripts/charts/node-networks-overlay.js b/client/app/scripts/charts/node-networks-overlay.js new file mode 100644 index 000000000..b728de051 --- /dev/null +++ b/client/app/scripts/charts/node-networks-overlay.js @@ -0,0 +1,47 @@ +import React from 'react'; +import d3 from 'd3'; +import { List as makeList } from 'immutable'; +import { getNodeColor } from '../utils/color-utils'; +import { isContrastMode } from '../utils/contrast-utils'; + + +const padding = 0.05; +const offset = Math.PI; +const arc = d3.svg.arc() + .startAngle(d => d.startAngle + offset) + .endAngle(d => d.endAngle + offset); +const arcScale = d3.scale.linear() + .range([Math.PI * 0.25 + padding, Math.PI * 1.75 - padding]); + + +function NodeNetworksOverlay({size, stack, networks = makeList()}) { + arcScale.domain([0, networks.size]); + const radius = size * 0.9; + + const paths = networks.map((n, i) => { + const d = arc({ + padAngle: 0.05, + innerRadius: radius, + outerRadius: radius + 4, + startAngle: arcScale(i), + endAngle: arcScale(i + 1) + }); + + return (); + }); + + let transform = ''; + if (stack) { + const contrastMode = isContrastMode(); + const [dx, dy] = contrastMode ? [0, 8] : [0, 5]; + transform = `translate(${dx}, ${dy * -1.5})`; + } + + return ( + + {paths.toJS()} + + ); +} + +export default NodeNetworksOverlay; diff --git a/client/app/scripts/charts/node-shape-hex.js b/client/app/scripts/charts/node-shape-hex.js index ce5f84f00..d7045c9ab 100644 --- a/client/app/scripts/charts/node-shape-hex.js +++ b/client/app/scripts/charts/node-shape-hex.js @@ -30,14 +30,12 @@ function getPoints(h) { } -export default function NodeShapeHex({id, highlighted, size, color, metric, networks}) { +export default function NodeShapeHex({id, highlighted, size, color, metric}) { const pathProps = v => ({ d: getPoints(size * v * 2), transform: `rotate(90) translate(-${size * getWidth(v)}, -${size * v})` }); - console.log('netz', networks); - const shadowSize = 0.45; const upperHexBitHeight = -0.25 * size * shadowSize; diff --git a/client/app/scripts/charts/node.js b/client/app/scripts/charts/node.js index f28f52719..acd942702 100644 --- a/client/app/scripts/charts/node.js +++ b/client/app/scripts/charts/node.js @@ -15,6 +15,7 @@ import NodeShapeRoundedSquare from './node-shape-rounded-square'; import NodeShapeHex from './node-shape-hex'; import NodeShapeHeptagon from './node-shape-heptagon'; import NodeShapeCloud from './node-shape-cloud'; +import NodeNetworksOverlay from './node-networks-overlay'; function stackedShape(Shape) { const factory = React.createFactory(NodeShapeStack); @@ -75,8 +76,9 @@ class Node extends React.Component { } render() { - const { blurred, focused, highlighted, label, matches = makeMap(), - pseudo, rank, subLabel, scaleFactor, transform, zoomScale, exportingGraph } = this.props; + const { blurred, focused, highlighted, label, matches = makeMap(), networks, + pseudo, rank, subLabel, scaleFactor, transform, zoomScale, exportingGraph, + showingNetworks, stack } = this.props; const { hovered, matched } = this.state; const nodeScale = focused ? this.props.selectedNodeScale : this.props.nodeScale; @@ -100,11 +102,14 @@ class Node extends React.Component { const NodeShapeType = getNodeShape(this.props); const useSvgLabels = exportingGraph; - + const size = nodeScale(scaleFactor); return ( + {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%; From 8481223181427de639e22ca447dd03c3cea19ac9 Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Tue, 24 May 2016 15:34:46 +0200 Subject: [PATCH 03/12] Rollback network view details-panel integration --- client/app/scripts/reducers/root.js | 67 +++++------------------------ 1 file changed, 11 insertions(+), 56 deletions(-) diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js index 298ee2909..9f530f077 100644 --- a/client/app/scripts/reducers/root.js +++ b/client/app/scripts/reducers/root.js @@ -106,46 +106,25 @@ 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) { - return state; + 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]); } - 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) { + if (state.get('nodeDetails').size === 0 || state.get('selectedNodeId') === nodeId) { state = state.set('selectedNodeId', null); } return state; } function closeAllNodeDetails(state) { - state.get('nodeDetails').keySeq().forEach(nodeId => { - state = closeNodeDetails(state, nodeId); - }); + while (state.get('nodeDetails').size) { + state = closeNodeDetails(state); + } return state; } @@ -208,10 +187,8 @@ 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) { @@ -305,17 +282,6 @@ export function rootReducer(state = initialState, action) { } 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 @@ -323,7 +289,6 @@ export function rootReducer(state = initialState, action) { } case ActionTypes.UNPIN_NETWORK: { - state = closeNodeDetails(state, action.networkId); return state.merge({ pinnedNetwork: null, }); @@ -656,16 +621,6 @@ 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()); } From b198c88fb1a90a73a1139aa3173cd604235bb6bf Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Tue, 24 May 2016 16:22:25 +0200 Subject: [PATCH 04/12] Correct url-state handling. Testing out colored borders rather than BGs for net-view legend --- client/app/scripts/actions/app-actions.js | 10 +++++++--- .../scripts/components/network-selector-item.js | 6 +++--- client/app/scripts/components/networks-selector.js | 14 +++++++++----- client/app/scripts/reducers/root.js | 10 ++++++++++ client/app/scripts/utils/router-utils.js | 11 ++++++++++- client/app/styles/main.less | 7 ++++++- 6 files changed, 45 insertions(+), 13 deletions(-) diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index b9348dd23..5704ccdd8 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -38,9 +38,13 @@ export function toggleHelp() { export function showNetworks(visible) { - return { - type: ActionTypes.SHOW_NETWORKS, - visible + return (dispatch, getState) => { + dispatch({ + type: ActionTypes.SHOW_NETWORKS, + visible + }); + + updateRoute(getState); }; } diff --git a/client/app/scripts/components/network-selector-item.js b/client/app/scripts/components/network-selector-item.js index d0bc5557f..ac7321697 100644 --- a/client/app/scripts/components/network-selector-item.js +++ b/client/app/scripts/components/network-selector-item.js @@ -35,11 +35,11 @@ class NetworkSelectorItem extends React.Component { 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 className = classNames('network-selector-action', { + 'network-selector-action-selected': isSelected }); const style = { - backgroundColor: getNodeColor(id) + borderBottomColor: getNodeColor(id) }; return ( diff --git a/client/app/scripts/components/networks-selector.js b/client/app/scripts/components/networks-selector.js index 2653478ec..d1a8fae32 100644 --- a/client/app/scripts/components/networks-selector.js +++ b/client/app/scripts/components/networks-selector.js @@ -28,14 +28,18 @@ class NetworkSelector extends React.Component { )); - const className = classNames('metric-selector-action', { - 'metric-selector-action-selected': showingNetworks + const className = classNames('network-selector-action', { + 'network-selector-action-selected': showingNetworks }); + const style = { + borderBottomColor: 'transparent' + }; + return ( -
-
-
+
+
+
Networks
{showingNetworks && items} diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js index 9f530f077..de6abea2a 100644 --- a/client/app/scripts/reducers/root.js +++ b/client/app/scripts/reducers/root.js @@ -274,6 +274,10 @@ export function rootReducer(state = initialState, action) { // case ActionTypes.SHOW_NETWORKS: { + if (!action.visible) { + state = state.set('selectedNetwork', null); + state = state.set('pinnedNetwork', null); + } return state.set('showingNetworks', action.visible); } @@ -606,6 +610,12 @@ export function rootReducer(state = initialState, action) { selectedNodeId: action.state.selectedNodeId, pinnedMetricType: action.state.pinnedMetricType }); + if (action.state.showingNetworks) { + state = state.set('showingNetworks', action.state.showingNetworks); + } + if (action.state.pinnedNetwork) { + state = state.set('pinnedNetwork', action.state.pinnedNetwork); + } if (action.state.controlPipe) { state = state.set('controlPipes', makeOrderedMap({ [action.state.controlPipe.id]: diff --git a/client/app/scripts/utils/router-utils.js b/client/app/scripts/utils/router-utils.js index 9e92a0bf9..0aba5a157 100644 --- a/client/app/scripts/utils/router-utils.js +++ b/client/app/scripts/utils/router-utils.js @@ -37,7 +37,7 @@ export function getUrlState(state) { id: details.id, label: details.label, topologyId: details.topologyId })); - return { + const urlState = { controlPipe: cp ? cp.toJS() : null, nodeDetails: nodeDetails.toJS(), pinnedMetricType: state.get('pinnedMetricType'), @@ -47,6 +47,15 @@ export function getUrlState(state) { topologyId: state.get('currentTopologyId'), topologyOptions: state.get('topologyOptions').toJS() // all options }; + + if (state.get('showingNetworks')) { + urlState.showingNetworks = true; + if (state.get('pinnedNetwork')) { + urlState.pinnedNetwork = state.get('pinnedNetwork'); + } + } + + return urlState; } export function updateRoute(getState) { diff --git a/client/app/styles/main.less b/client/app/styles/main.less index 7fbe30e8b..bff5b4f64 100644 --- a/client/app/styles/main.less +++ b/client/app/styles/main.less @@ -1121,7 +1121,7 @@ h2 { } } -.topology-option, .metric-selector { +.topology-option, .metric-selector, .network-selector { color: @text-secondary-color; margin: 6px 0; @@ -1172,6 +1172,11 @@ h2 { } } +.network-selector-action { + border-top: 3px solid transparent; + border-bottom: 3px solid @background-dark-color; +} + .warning { display: inline-block; cursor: pointer; From 10ced2d09d4e639d6d781924741560586984adcd Mon Sep 17 00:00:00 2001 From: Peter Bourgon Date: Wed, 18 May 2016 13:03:00 +0100 Subject: [PATCH 05/12] First cut of network data --- probe/docker/container.go | 15 ++++++++++++++- probe/docker/reporter.go | 7 ++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/probe/docker/container.go b/probe/docker/container.go index 24727b46c..a559ba4b9 100644 --- a/probe/docker/container.go +++ b/probe/docker/container.go @@ -27,6 +27,7 @@ const ( ContainerCommand = "docker_container_command" ContainerPorts = "docker_container_ports" ContainerCreated = "docker_container_created" + ContainerNetworks = "docker_container_networks" ContainerIPs = "docker_container_ips" ContainerHostname = "docker_container_hostname" ContainerIPsWithScopes = "docker_container_ips_with_scopes" @@ -309,17 +310,29 @@ func addScopeToIPs(hostID string, ips []string) []string { func (c *container) NetworkInfo(localAddrs []net.IP) report.Sets { c.RLock() defer c.RUnlock() + + // For now, for the proof-of-concept, we just add networks as a set of + // names. For the next iteration, we will probably want to create a new + // Network topology, populate the network nodes with all of the details + // here, and provide foreign key links from nodes to networks. + networks := make([]string, 0, len(c.container.NetworkSettings.Networks)) + for name := range c.container.NetworkSettings.Networks { + networks = append(networks, name) + } + ips := c.container.NetworkSettings.SecondaryIPAddresses if c.container.NetworkSettings.IPAddress != "" { ips = append(ips, c.container.NetworkSettings.IPAddress) } + // Treat all Docker IPs as local scoped. ipsWithScopes := addScopeToIPs(c.hostID, ips) + return report.EmptySets. + Add(ContainerNetworks, report.MakeStringSet(networks...)). Add(ContainerPorts, c.ports(localAddrs)). Add(ContainerIPs, report.MakeStringSet(ips...)). Add(ContainerIPsWithScopes, report.MakeStringSet(ipsWithScopes...)) - } func (c *container) memoryUsageMetric(stats []docker.Stats) report.Metric { diff --git a/probe/docker/reporter.go b/probe/docker/reporter.go index 640689707..8fdbedfaf 100644 --- a/probe/docker/reporter.go +++ b/probe/docker/reporter.go @@ -26,9 +26,10 @@ var ( ImageID: {ID: ImageID, Label: "Image ID", From: report.FromLatest, Truncate: 12, Priority: 11}, ContainerUptime: {ID: ContainerUptime, Label: "Uptime", From: report.FromLatest, Priority: 12}, ContainerRestartCount: {ID: ContainerRestartCount, Label: "Restart #", From: report.FromLatest, Priority: 13}, - ContainerIPs: {ID: ContainerIPs, Label: "IPs", From: report.FromSets, Priority: 14}, - ContainerPorts: {ID: ContainerPorts, Label: "Ports", From: report.FromSets, Priority: 15}, - ContainerCreated: {ID: ContainerCreated, Label: "Created", From: report.FromLatest, Priority: 16}, + ContainerNetworks: {ID: ContainerNetworks, Label: "Networks", From: report.FromSets, Priority: 14}, + ContainerIPs: {ID: ContainerIPs, Label: "IPs", From: report.FromSets, Priority: 15}, + ContainerPorts: {ID: ContainerPorts, Label: "Ports", From: report.FromSets, Priority: 16}, + ContainerCreated: {ID: ContainerCreated, Label: "Created", From: report.FromLatest, Priority: 17}, } ContainerMetricTemplates = report.MetricTemplates{ From e0fab36351f8a6d5cdfa4da02b3c1a32d8161397 Mon Sep 17 00:00:00 2001 From: Peter Bourgon Date: Wed, 18 May 2016 14:41:30 +0100 Subject: [PATCH 06/12] Re-sync container on network dis/connect --- probe/docker/registry.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/probe/docker/registry.go b/probe/docker/registry.go index 8014a87ab..e633dd255 100644 --- a/probe/docker/registry.go +++ b/probe/docker/registry.go @@ -16,14 +16,16 @@ import ( // Consts exported for testing. const ( - CreateEvent = "create" - DestroyEvent = "destroy" - RenameEvent = "rename" - StartEvent = "start" - DieEvent = "die" - PauseEvent = "pause" - UnpauseEvent = "unpause" - endpoint = "unix:///var/run/docker.sock" + CreateEvent = "create" + DestroyEvent = "destroy" + RenameEvent = "rename" + StartEvent = "start" + DieEvent = "die" + PauseEvent = "pause" + UnpauseEvent = "unpause" + NetworkConnectEvent = "network:connect" + NetworkDisconnectEvent = "network:disconnect" + endpoint = "unix:///var/run/docker.sock" ) // Vars exported for testing. @@ -249,7 +251,7 @@ func (r *registry) updateImages() error { func (r *registry) handleEvent(event *docker_client.APIEvents) { switch event.Status { - case CreateEvent, RenameEvent, StartEvent, DieEvent, DestroyEvent, PauseEvent, UnpauseEvent: + case CreateEvent, RenameEvent, StartEvent, DieEvent, DestroyEvent, PauseEvent, UnpauseEvent, NetworkConnectEvent, NetworkDisconnectEvent: r.updateContainerState(event.ID, stateAfterEvent(event.Status)) } } From e95f46bfd8f510dc185e2f9e0fc9c3858225ff79 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Tue, 24 May 2016 16:59:03 +0200 Subject: [PATCH 07/12] Extract networks from metadata (should be toplevel field) --- client/app/scripts/reducers/root.js | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js index de6abea2a..8ab87ff16 100644 --- a/client/app/scripts/reducers/root.js +++ b/client/app/scripts/reducers/root.js @@ -518,14 +518,26 @@ export function rootReducer(state = initialState, action) { // apply pinned searches, filters nodes that dont match state = applyPinnedSearches(state); + // TODO move this setting of networks as toplevel node field to backend, + // to not rely on field IDs here. should be determined by topology implementer + state = state.update('nodes', nodes => nodes.map(node => { + if (node.has('metadata')) { + const networks = node.get('metadata') + .find(field => field.get('id') === 'docker_container_networks'); + if (networks) { + return node.set('networks', fromJS(networks.get('value').split(', '))); + } + } + return node; + })); + state = state.set('availableNetworks', state.get('nodes') - .valueSeq() - .flatMap(node => (node.get('networks') || makeList()).map(n => ( - makeMap({id: n, label: n}) - ))) - .toSet() - .toList() - .sort()); + .valueSeq() + .flatMap(node => node.get('networks') || makeList()) + .toSet() + .toList() + .sort() + .map(n => makeMap({id: n, label: n}))); state = state.set('availableCanvasMetrics', state.get('nodes') .valueSeq() From 478a4a6d6658893d640dfbfc8919fefeee9ef180 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Wed, 25 May 2016 11:26:31 +0200 Subject: [PATCH 08/12] Remove common prefix from networks to increase color separation --- .../scripts/components/network-selector-item.js | 2 +- client/app/scripts/reducers/root.js | 15 +++++++++++++++ .../scripts/utils/__tests__/string-utils-test.js | 11 +++++++++++ client/app/scripts/utils/string-utils.js | 6 +++++- client/package.json | 1 + 5 files changed, 33 insertions(+), 2 deletions(-) diff --git a/client/app/scripts/components/network-selector-item.js b/client/app/scripts/components/network-selector-item.js index ac7321697..fe69fd676 100644 --- a/client/app/scripts/components/network-selector-item.js +++ b/client/app/scripts/components/network-selector-item.js @@ -39,7 +39,7 @@ class NetworkSelectorItem extends React.Component { 'network-selector-action-selected': isSelected }); const style = { - borderBottomColor: getNodeColor(id) + borderBottomColor: getNodeColor(network.get('colorKey', id)) }; return ( diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js index 8ab87ff16..f304d1a5b 100644 --- a/client/app/scripts/reducers/root.js +++ b/client/app/scripts/reducers/root.js @@ -6,6 +6,7 @@ import { fromJS, is as isDeepEqual, List as makeList, Map as makeMap, import ActionTypes from '../constants/action-types'; import { EDGE_ID_SEPARATOR } from '../constants/naming'; import { applyPinnedSearches, updateNodeMatches } from '../utils/search-utils'; +import { longestCommonPrefix } from '../utils/string-utils'; import { findTopologyById, getAdjacentNodes, setTopologyUrlsById, updateTopologyIds, filterHiddenTopologies } from '../utils/topology-utils'; @@ -539,6 +540,20 @@ export function rootReducer(state = initialState, action) { .sort() .map(n => makeMap({id: n, label: n}))); + // optimize color coding for networks + const networkPrefix = longestCommonPrefix(state.get('availableNetworks') + .map(n => n.get('id')).toJS()); + + if (networkPrefix) { + state = state.update('nodes', + nodes => nodes.map(node => node.update('networks', + networks => networks.map(n => n.substr(networkPrefix.length))))); + + state = state.update('availableNetworks', + networks => networks.map(network => network + .set('colorKey', network.get('id').substr(networkPrefix.length)))); + } + state = state.set('availableCanvasMetrics', state.get('nodes') .valueSeq() .flatMap(n => (n.get('metrics') || makeList()).map(m => ( diff --git a/client/app/scripts/utils/__tests__/string-utils-test.js b/client/app/scripts/utils/__tests__/string-utils-test.js index 2426d6a35..a6ffb501e 100644 --- a/client/app/scripts/utils/__tests__/string-utils-test.js +++ b/client/app/scripts/utils/__tests__/string-utils-test.js @@ -10,4 +10,15 @@ describe('StringUtils', () => { expect(formatMetric(0)).toBe('0.00'); }); }); + + describe('longestCommonPrefix', () => { + const fun = StringUtils.longestCommonPrefix; + + it('it should return the longest common prefix', () => { + expect(fun(['interspecies', 'interstellar'])).toBe('inters'); + expect(fun(['space', 'space'])).toBe('space'); + expect(fun([''])).toBe(''); + expect(fun(['prefix', 'suffix'])).toBe(''); + }); + }); }); diff --git a/client/app/scripts/utils/string-utils.js b/client/app/scripts/utils/string-utils.js index 5f8d180d5..788431693 100644 --- a/client/app/scripts/utils/string-utils.js +++ b/client/app/scripts/utils/string-utils.js @@ -1,7 +1,7 @@ import React from 'react'; import filesize from 'filesize'; import d3 from 'd3'; - +import LCP from 'lcp'; const formatLargeValue = d3.format('s'); @@ -69,3 +69,7 @@ const CLEAN_LABEL_REGEX = /\W/g; export function slugify(label) { return label.replace(CLEAN_LABEL_REGEX, '').toLowerCase(); } + +export function longestCommonPrefix(strArr) { + return (new LCP(strArr)).lcp(); +} diff --git a/client/package.json b/client/package.json index f09870513..f2d2a888e 100644 --- a/client/package.json +++ b/client/package.json @@ -15,6 +15,7 @@ "font-awesome": "4.5.0", "font-awesome-webpack": "0.0.4", "immutable": "~3.7.4", + "lcp": "1.0.0", "lodash": "~4.6.1", "materialize-css": "0.97.5", "moment": "2.12.0", From b1cd16d92d32b6394919ac13990c97cd62d446e2 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Wed, 25 May 2016 12:36:32 +0200 Subject: [PATCH 09/12] Fix node blurring for network hover Fixes edge focus/blurring when selecting networks Configurable arc size net-view variant: Smaller arc (just the top) net-view revision: Add white container around arcs. To join them together in a sense. Trying to avoid the edge/arc position association. Not sure if this really helps net-view variations: shadows. bg fill, align network rotation w/ rank. Cute little dots Stacked lines Rounded rects Slightly thicker "pills" to repr networks Handle edge case by making line longer Fix network bar offset --- .../scripts/charts/node-networks-overlay.js | 49 ++++++++++--------- client/app/scripts/charts/node.js | 6 +-- .../app/scripts/charts/nodes-chart-edges.js | 22 ++++++--- .../app/scripts/charts/nodes-chart-nodes.js | 3 +- .../app/scripts/components/debug-toolbar.js | 8 +-- client/app/scripts/reducers/root.js | 35 +++++++++---- client/app/styles/main.less | 6 +-- 7 files changed, 81 insertions(+), 48 deletions(-) diff --git a/client/app/scripts/charts/node-networks-overlay.js b/client/app/scripts/charts/node-networks-overlay.js index b728de051..b50841239 100644 --- a/client/app/scripts/charts/node-networks-overlay.js +++ b/client/app/scripts/charts/node-networks-overlay.js @@ -5,41 +5,46 @@ import { getNodeColor } from '../utils/color-utils'; import { isContrastMode } from '../utils/contrast-utils'; +const h = 5; const padding = 0.05; -const offset = Math.PI; -const arc = d3.svg.arc() - .startAngle(d => d.startAngle + offset) - .endAngle(d => d.endAngle + offset); -const arcScale = d3.scale.linear() - .range([Math.PI * 0.25 + padding, Math.PI * 1.75 - padding]); - +const rx = 1; +const ry = rx; +const labelOffset = 38; function NodeNetworksOverlay({size, stack, networks = makeList()}) { - arcScale.domain([0, networks.size]); - const radius = size * 0.9; + const r = size * 0.5; + const offset = r + labelOffset; + const w = Math.max(size, (size / 4) * networks.size); + const x = d3.scale.ordinal() + .domain(networks.map((n, i) => i).toJS()) + .rangeBands([w * -0.5, w * 0.5], padding, 0); - const paths = networks.map((n, i) => { - const d = arc({ - padAngle: 0.05, - innerRadius: radius, - outerRadius: radius + 4, - startAngle: arcScale(i), - endAngle: arcScale(i + 1) - }); - - return (); - }); + const bars = networks.map((n, i) => ( + + )); let transform = ''; if (stack) { const contrastMode = isContrastMode(); - const [dx, dy] = contrastMode ? [0, 8] : [0, 5]; + const [dx, dy] = contrastMode ? [0, 8] : [0, 0]; transform = `translate(${dx}, ${dy * -1.5})`; } return ( - {paths.toJS()} + {bars.toJS()} ); } diff --git a/client/app/scripts/charts/node.js b/client/app/scripts/charts/node.js index acd942702..fae17bf7d 100644 --- a/client/app/scripts/charts/node.js +++ b/client/app/scripts/charts/node.js @@ -107,9 +107,6 @@ class Node extends React.Component { - {showingNetworks && } - {useSvgLabels ? svgLabels(label, subLabel, labelClassName, subLabelClassName, labelOffsetY) : @@ -133,6 +130,9 @@ class Node extends React.Component { color={color} {...this.props} /> + + {showingNetworks && } ); } diff --git a/client/app/scripts/charts/nodes-chart-edges.js b/client/app/scripts/charts/nodes-chart-edges.js index 29d26ffe0..a67eb5287 100644 --- a/client/app/scripts/charts/nodes-chart-edges.js +++ b/client/app/scripts/charts/nodes-chart-edges.js @@ -1,6 +1,6 @@ import React from 'react'; import { connect } from 'react-redux'; -import { Map as makeMap } from 'immutable'; +import { Map as makeMap, List as makeList } from 'immutable'; import { hasSelectedNode as hasSelectedNodeFn } from '../utils/topology-utils'; import EdgeContainer from './edge-container'; @@ -9,7 +9,7 @@ class NodesChartEdges extends React.Component { render() { const { hasSelectedNode, highlightedEdgeIds, layoutEdges, layoutPrecision, searchNodeMatches = makeMap(), searchQuery, - selectedNodeId } = this.props; + selectedNodeId, selectedNetwork, selectedNetworkNodes } = this.props; return ( @@ -18,10 +18,16 @@ class NodesChartEdges extends React.Component { const targetSelected = selectedNodeId === edge.get('target'); const highlighted = highlightedEdgeIds.has(edge.get('id')); const focused = hasSelectedNode && (sourceSelected || targetSelected); - const blurred = !(highlightedEdgeIds.size > 0 && highlighted) - && ((hasSelectedNode && !sourceSelected && !targetSelected) - || !focused && searchQuery && !(searchNodeMatches.has(edge.get('source')) - && searchNodeMatches.has(edge.get('target')))); + const otherNodesSelected = hasSelectedNode && !sourceSelected && !targetSelected; + const noMatches = searchQuery && + !(searchNodeMatches.has(edge.get('source')) && + searchNodeMatches.has(edge.get('target'))); + const noSelectedNetworks = selectedNetwork && + !(selectedNetworkNodes.contains(edge.get('source')) && + selectedNetworkNodes.contains(edge.get('target'))); + const blurred = !highlighted && (otherNodesSelected || + !focused && noMatches || + !focused && noSelectedNetworks); return ( n.get('id') === selectedNetwork)); // make sure blurred nodes are in the background const sortNodes = node => { diff --git a/client/app/scripts/components/debug-toolbar.js b/client/app/scripts/components/debug-toolbar.js index e52e22876..97145ff7c 100644 --- a/client/app/scripts/components/debug-toolbar.js +++ b/client/app/scripts/components/debug-toolbar.js @@ -17,7 +17,9 @@ 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 NETWORKS = [ + 'be', 'fe', 'zb', 'db', 're', 'gh', 'jk', 'lol', 'nw' +].map(n => ({id: n, label: n, colorKey: n})); 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 @@ -45,7 +47,7 @@ const LABEL_PREFIXES = _.range('A'.charCodeAt(), 'Z'.charCodeAt() + 1) const deltaAdd = ( name, adjacency = [], shape = 'circle', stack = false, nodeCount = 1, - networks = ['fe', 'be'] + networks = NETWORKS ) => ({ adjacency, controls: {}, @@ -176,7 +178,7 @@ class DebugToolbar extends React.Component { _.sample(SHAPES), _.sample(STACK_VARIANTS), _.sample(NODE_COUNTS), - sample(NETWORKS, 3) + sample(NETWORKS, 10) )) })); diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js index f304d1a5b..d94c35cf5 100644 --- a/client/app/scripts/reducers/root.js +++ b/client/app/scripts/reducers/root.js @@ -33,6 +33,7 @@ export const initialState = makeMap({ hostname: '...', mouseOverEdgeId: null, mouseOverNodeId: null, + networkNodes: makeMap(), nodeDetails: makeOrderedMap(), // nodeId -> details nodes: makeOrderedMap(), // nodeId -> node // nodes cache, infrequently updated, used for search @@ -81,6 +82,24 @@ function processTopologies(state, nextTopologies) { return state.mergeDeepIn(['topologies'], immNextTopologies); } +function getNetworkNodes(nodes) { + const networks = {}; + nodes.forEach(node => (node.get('networks') || makeList()).forEach(n => { + const networkId = n.get('id'); + networks[networkId] = (networks[networkId] || []).concat([node.get('id')]); + })); + return fromJS(networks); +} + +function getAvailableNetworks(nodes) { + return nodes + .valueSeq() + .flatMap(node => node.get('networks') || makeList()) + .toSet() + .toList() + .sortBy(m => m.get('label')); +} + function setTopology(state, topologyId) { state = state.set('currentTopology', findTopologyById( state.get('topologies'), topologyId)); @@ -526,19 +545,15 @@ export function rootReducer(state = initialState, action) { const networks = node.get('metadata') .find(field => field.get('id') === 'docker_container_networks'); if (networks) { - return node.set('networks', fromJS(networks.get('value').split(', '))); + return node.set('networks', fromJS( + networks.get('value').split(', ').map(n => ({id: n, label: n, colorKey: n})))); } } return node; })); - state = state.set('availableNetworks', state.get('nodes') - .valueSeq() - .flatMap(node => node.get('networks') || makeList()) - .toSet() - .toList() - .sort() - .map(n => makeMap({id: n, label: n}))); + state = state.set('networkNodes', getNetworkNodes(state.get('nodes'))); + state = state.set('availableNetworks', getAvailableNetworks(state.get('nodes'))); // optimize color coding for networks const networkPrefix = longestCommonPrefix(state.get('availableNetworks') @@ -547,7 +562,8 @@ export function rootReducer(state = initialState, action) { if (networkPrefix) { state = state.update('nodes', nodes => nodes.map(node => node.update('networks', - networks => networks.map(n => n.substr(networkPrefix.length))))); + networks => networks.map(n => n.set('colorKey', + n.get('colorKey').substr(networkPrefix.length)))))); state = state.update('availableNetworks', networks => networks.map(network => network @@ -642,6 +658,7 @@ export function rootReducer(state = initialState, action) { } if (action.state.pinnedNetwork) { state = state.set('pinnedNetwork', action.state.pinnedNetwork); + state = state.set('selectedNetwork', action.state.pinnedNetwork); } if (action.state.controlPipe) { state = state.set('controlPipes', makeOrderedMap({ diff --git a/client/app/styles/main.less b/client/app/styles/main.less index bff5b4f64..900d23a91 100644 --- a/client/app/styles/main.less +++ b/client/app/styles/main.less @@ -341,9 +341,9 @@ h2 { transition: opacity .5s @base-ease; text-align: center; - .node-networks-overlay { - fill: none; - stroke: @background-darker-secondary-color; + .node-network { + // stroke: @background-lighter-color; + // stroke-width: 4px; } .node-label, From 05ce661c19311f3d3182315d741b0df39cf91f02 Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Thu, 2 Jun 2016 12:42:23 +0200 Subject: [PATCH 10/12] Gets go-tests passing! --- probe/docker/container_test.go | 1 + render/detailed/metadata_test.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/probe/docker/container_test.go b/probe/docker/container_test.go index c77e92007..0c8faad37 100644 --- a/probe/docker/container_test.go +++ b/probe/docker/container_test.go @@ -112,6 +112,7 @@ func TestContainer(t *testing.T) { { want := report.EmptySets. Add("docker_container_ports", report.MakeStringSet("1.2.3.4:80->80/tcp", "81/tcp")). + Add("docker_container_networks", nil). Add("docker_container_ips", report.MakeStringSet("1.2.3.4")). Add("docker_container_ips_with_scopes", report.MakeStringSet(";1.2.3.4")) diff --git a/render/detailed/metadata_test.go b/render/detailed/metadata_test.go index 21830cdd0..b2b98262a 100644 --- a/render/detailed/metadata_test.go +++ b/render/detailed/metadata_test.go @@ -29,7 +29,7 @@ func TestNodeMetadata(t *testing.T) { want: []report.MetadataRow{ {ID: docker.ContainerID, Label: "ID", Value: fixture.ClientContainerID, Priority: 1}, {ID: docker.ContainerStateHuman, Label: "State", Value: "running", Priority: 2}, - {ID: docker.ContainerIPs, Label: "IPs", Value: "10.10.10.0/24, 10.10.10.1/24", Priority: 14}, + {ID: docker.ContainerIPs, Label: "IPs", Value: "10.10.10.0/24, 10.10.10.1/24", Priority: 15}, }, }, { From 570124fbe0448532c21a291e0a2c56dfe7c19ac9 Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Mon, 6 Jun 2016 11:44:51 +0200 Subject: [PATCH 11/12] Netview review feedback --- .../scripts/charts/node-networks-overlay.js | 26 ++++++++++++------- client/app/scripts/charts/node.js | 8 +++--- .../app/scripts/charts/nodes-chart-edges.js | 2 +- client/app/scripts/charts/nodes-layout.js | 2 +- .../app/scripts/components/matched-results.js | 4 +-- .../scripts/components/networks-selector.js | 2 +- client/app/scripts/reducers/root.js | 19 +------------- client/app/styles/main.less | 1 + 8 files changed, 28 insertions(+), 36 deletions(-) diff --git a/client/app/scripts/charts/node-networks-overlay.js b/client/app/scripts/charts/node-networks-overlay.js index b50841239..91f387caa 100644 --- a/client/app/scripts/charts/node-networks-overlay.js +++ b/client/app/scripts/charts/node-networks-overlay.js @@ -5,26 +5,32 @@ import { getNodeColor } from '../utils/color-utils'; import { isContrastMode } from '../utils/contrast-utils'; -const h = 5; +const barHeight = 5; +const barMarginTop = 6; +const labelHeight = 32; +// Gap size between bar segments. const padding = 0.05; const rx = 1; const ry = rx; -const labelOffset = 38; +const x = d3.scale.ordinal(); -function NodeNetworksOverlay({size, stack, networks = makeList()}) { - const r = size * 0.5; - const offset = r + labelOffset; - const w = Math.max(size, (size / 4) * networks.size); - const x = d3.scale.ordinal() - .domain(networks.map((n, i) => i).toJS()) - .rangeBands([w * -0.5, w * 0.5], padding, 0); +function NodeNetworksOverlay({labelOffsetY, size, stack, networks = makeList()}) { + const offset = labelOffsetY + labelHeight + barMarginTop; + + // Min size is about a quarter of the width, feels about right. + const minBarWidth = (size / 4); + const barWidth = Math.max(size, minBarWidth * networks.size); + + // Update singleton scale. + x.domain(networks.map((n, i) => i).toJS()); + x.rangeBands([barWidth * -0.5, barWidth * 0.5], padding, 0); const bars = networks.map((n, i) => (
- {!blurred && } + {!blurred && }
} @@ -131,8 +133,8 @@ class Node extends React.Component { {...this.props} /> - {showingNetworks && } + {showingNetworks && } ); } diff --git a/client/app/scripts/charts/nodes-chart-edges.js b/client/app/scripts/charts/nodes-chart-edges.js index a67eb5287..fd764b819 100644 --- a/client/app/scripts/charts/nodes-chart-edges.js +++ b/client/app/scripts/charts/nodes-chart-edges.js @@ -56,7 +56,7 @@ function mapStateToProps(state) { searchNodeMatches: state.getIn(['searchNodeMatches', currentTopologyId]), searchQuery: state.get('searchQuery'), selectedNetwork: state.get('selectedNetwork'), - selectedNetworkNodes: state.get('networkNodes').get(state.get('selectedNetwork'), makeList()), + selectedNetworkNodes: state.getIn(['networkNodes', state.get('selectedNetwork')], makeList()), selectedNodeId: state.get('selectedNodeId'), }; } diff --git a/client/app/scripts/charts/nodes-layout.js b/client/app/scripts/charts/nodes-layout.js index f700dd477..3da6dcb66 100644 --- a/client/app/scripts/charts/nodes-layout.js +++ b/client/app/scripts/charts/nodes-layout.js @@ -13,7 +13,7 @@ const DEFAULT_MARGINS = {top: 0, left: 0}; const DEFAULT_SCALE = val => val * 2; const NODE_SIZE_FACTOR = 1; const NODE_SEPARATION_FACTOR = 3.0; -const RANK_SEPARATION_FACTOR = 2.5; +const RANK_SEPARATION_FACTOR = 3.0; let layoutRuns = 0; let layoutRunsTrivial = 0; diff --git a/client/app/scripts/components/matched-results.js b/client/app/scripts/components/matched-results.js index e044b69f5..5f97384d7 100644 --- a/client/app/scripts/components/matched-results.js +++ b/client/app/scripts/components/matched-results.js @@ -25,7 +25,7 @@ class MatchedResults extends React.Component { } render() { - const { matches } = this.props; + const { matches, style } = this.props; if (!matches) { return null; @@ -42,7 +42,7 @@ class MatchedResults extends React.Component { } return ( -
+
{matches.keySeq().take(SHOW_ROW_COUNT).map(fieldId => this.renderMatch(matches, fieldId))} {moreFieldMatches &&
{`${moreFieldMatches.size} more matches`} diff --git a/client/app/scripts/components/networks-selector.js b/client/app/scripts/components/networks-selector.js index d1a8fae32..2a8d55525 100644 --- a/client/app/scripts/components/networks-selector.js +++ b/client/app/scripts/components/networks-selector.js @@ -33,7 +33,7 @@ class NetworkSelector extends React.Component { }); const style = { - borderBottomColor: 'transparent' + borderBottomColor: showingNetworks ? '#A2A0B3' : 'transparent' }; return ( diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js index d94c35cf5..968400a01 100644 --- a/client/app/scripts/reducers/root.js +++ b/client/app/scripts/reducers/root.js @@ -6,6 +6,7 @@ import { fromJS, is as isDeepEqual, List as makeList, Map as makeMap, import ActionTypes from '../constants/action-types'; import { EDGE_ID_SEPARATOR } from '../constants/naming'; import { applyPinnedSearches, updateNodeMatches } from '../utils/search-utils'; +import { getNetworkNodes, getAvailableNetworks } from '../utils/network-view-utils'; import { longestCommonPrefix } from '../utils/string-utils'; import { findTopologyById, getAdjacentNodes, setTopologyUrlsById, updateTopologyIds, filterHiddenTopologies } from '../utils/topology-utils'; @@ -82,24 +83,6 @@ function processTopologies(state, nextTopologies) { return state.mergeDeepIn(['topologies'], immNextTopologies); } -function getNetworkNodes(nodes) { - const networks = {}; - nodes.forEach(node => (node.get('networks') || makeList()).forEach(n => { - const networkId = n.get('id'); - networks[networkId] = (networks[networkId] || []).concat([node.get('id')]); - })); - return fromJS(networks); -} - -function getAvailableNetworks(nodes) { - return nodes - .valueSeq() - .flatMap(node => node.get('networks') || makeList()) - .toSet() - .toList() - .sortBy(m => m.get('label')); -} - function setTopology(state, topologyId) { state = state.set('currentTopology', findTopologyById( state.get('topologies'), topologyId)); diff --git a/client/app/styles/main.less b/client/app/styles/main.less index 900d23a91..2a23d40ff 100644 --- a/client/app/styles/main.less +++ b/client/app/styles/main.less @@ -1145,6 +1145,7 @@ h2 { padding: 3px 12px; cursor: pointer; display: inline-block; + background-color: @background-color; &-selected, &:hover { color: @text-darker-color; From 53768b52ec1a548db43447fd8bc90e248513f774 Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Mon, 6 Jun 2016 16:30:20 +0200 Subject: [PATCH 12/12] Fixes tests + missing files oops (network view stuff) --- .../scripts/reducers/__tests__/root-test.js | 1 + .../app/scripts/utils/network-view-utils.js | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 client/app/scripts/utils/network-view-utils.js diff --git a/client/app/scripts/reducers/__tests__/root-test.js b/client/app/scripts/reducers/__tests__/root-test.js index cfde7428e..0121da97d 100644 --- a/client/app/scripts/reducers/__tests__/root-test.js +++ b/client/app/scripts/reducers/__tests__/root-test.js @@ -2,6 +2,7 @@ jest.dontMock('../../utils/router-utils'); jest.dontMock('../../utils/search-utils'); jest.dontMock('../../utils/string-utils'); jest.dontMock('../../utils/topology-utils'); +jest.dontMock('../../utils/network-view-utils'); jest.dontMock('../../constants/action-types'); jest.dontMock('../root'); diff --git a/client/app/scripts/utils/network-view-utils.js b/client/app/scripts/utils/network-view-utils.js new file mode 100644 index 000000000..8565fc8e6 --- /dev/null +++ b/client/app/scripts/utils/network-view-utils.js @@ -0,0 +1,20 @@ +import { fromJS, List as makeList } from 'immutable'; + +export function getNetworkNodes(nodes) { + const networks = {}; + nodes.forEach(node => (node.get('networks') || makeList()).forEach(n => { + const networkId = n.get('id'); + networks[networkId] = (networks[networkId] || []).concat([node.get('id')]); + })); + return fromJS(networks); +} + + +export function getAvailableNetworks(nodes) { + return nodes + .valueSeq() + .flatMap(node => node.get('networks') || makeList()) + .toSet() + .toList() + .sortBy(m => m.get('label')); +}