mirror of
https://github.com/weaveworks/scope.git
synced 2026-08-19 04:16:21 +00:00
Merge pull request #1528 from weaveworks/network-view
[WIP] Network view
This commit is contained in:
@@ -32,6 +32,57 @@ export function toggleHelp() {
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Networks
|
||||
//
|
||||
|
||||
|
||||
export function showNetworks(visible) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: ActionTypes.SHOW_NETWORKS,
|
||||
visible
|
||||
});
|
||||
|
||||
updateRoute(getState);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
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,
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
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 barHeight = 5;
|
||||
const barMarginTop = 6;
|
||||
const labelHeight = 32;
|
||||
// Gap size between bar segments.
|
||||
const padding = 0.05;
|
||||
const rx = 1;
|
||||
const ry = rx;
|
||||
const x = d3.scale.ordinal();
|
||||
|
||||
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) => (
|
||||
<rect
|
||||
x={x(i)}
|
||||
y={offset}
|
||||
width={x.rangeBand()}
|
||||
height={barHeight}
|
||||
rx={rx}
|
||||
ry={ry}
|
||||
className="node-network"
|
||||
style={{
|
||||
fill: getNodeColor(n.get('colorKey'))
|
||||
}}
|
||||
key={n.get('id')}
|
||||
/>
|
||||
));
|
||||
|
||||
let transform = '';
|
||||
if (stack) {
|
||||
const contrastMode = isContrastMode();
|
||||
const [dx, dy] = contrastMode ? [0, 8] : [0, 0];
|
||||
transform = `translate(${dx}, ${dy * -1.5})`;
|
||||
}
|
||||
|
||||
return (
|
||||
<g transform={transform}>
|
||||
{bars.toJS()}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
export default NodeNetworksOverlay;
|
||||
@@ -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;
|
||||
|
||||
@@ -97,10 +99,11 @@ class Node extends React.Component {
|
||||
|
||||
const labelClassName = classnames('node-label', { truncate });
|
||||
const subLabelClassName = classnames('node-sublabel', { truncate });
|
||||
const matchedResultsStyle = showingNetworks ? { marginTop: 6 } : null;
|
||||
|
||||
const NodeShapeType = getNodeShape(this.props);
|
||||
const useSvgLabels = exportingGraph;
|
||||
|
||||
const size = nodeScale(scaleFactor);
|
||||
return (
|
||||
<g className={nodeClassName} transform={transform}
|
||||
onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
||||
@@ -118,16 +121,20 @@ class Node extends React.Component {
|
||||
<div className={subLabelClassName}>
|
||||
<MatchedText text={subLabel} match={matches.get('sublabel')} />
|
||||
</div>
|
||||
{!blurred && <MatchedResults matches={matches.get('metadata')} />}
|
||||
{!blurred && <MatchedResults matches={matches.get('metadata')}
|
||||
style={matchedResultsStyle} />}
|
||||
</div>
|
||||
</foreignObject>}
|
||||
|
||||
<g onClick={this.handleMouseClick}>
|
||||
<NodeShapeType
|
||||
size={nodeScale(scaleFactor)}
|
||||
size={size}
|
||||
color={color}
|
||||
{...this.props} />
|
||||
</g>
|
||||
|
||||
{showingNetworks && <NodeNetworksOverlay labelOffsetY={labelOffsetY}
|
||||
size={size} networks={networks} stack={stack} />}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
@@ -152,7 +159,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);
|
||||
|
||||
@@ -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 (
|
||||
<g className="nodes-chart-edges">
|
||||
@@ -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 (
|
||||
<EdgeContainer
|
||||
@@ -49,7 +55,9 @@ function mapStateToProps(state) {
|
||||
highlightedEdgeIds: state.get('highlightedEdgeIds'),
|
||||
searchNodeMatches: state.getIn(['searchNodeMatches', currentTopologyId]),
|
||||
searchQuery: state.get('searchQuery'),
|
||||
selectedNodeId: state.get('selectedNodeId')
|
||||
selectedNetwork: state.get('selectedNetwork'),
|
||||
selectedNetworkNodes: state.getIn(['networkNodes', state.get('selectedNetwork')], makeList()),
|
||||
selectedNodeId: state.get('selectedNodeId'),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,9 @@ 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()).find(n => n.get('id') === selectedNetwork));
|
||||
|
||||
// make sure blurred nodes are in the background
|
||||
const sortNodes = node => {
|
||||
@@ -63,6 +65,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')}
|
||||
@@ -90,6 +93,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'),
|
||||
|
||||
@@ -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('networks'),
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div className="app">
|
||||
@@ -124,6 +126,7 @@ class App extends React.Component {
|
||||
<Sidebar>
|
||||
<Status />
|
||||
{showingMetricsSelector && <MetricSelector />}
|
||||
{showingNetworkSelector && <NetworkSelector />}
|
||||
<TopologyOptions />
|
||||
</Sidebar>
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
|
||||
@@ -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,16 @@ 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', '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
|
||||
@@ -23,7 +27,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 +45,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 = NETWORKS
|
||||
) => ({
|
||||
adjacency,
|
||||
controls: {},
|
||||
shape,
|
||||
@@ -51,9 +58,9 @@ const deltaAdd = (name, adjacency = [], shape = 'circle', stack = false, nodeCou
|
||||
label: name,
|
||||
label_minor: name,
|
||||
latest: {},
|
||||
metadata: {},
|
||||
origins: [],
|
||||
rank: name
|
||||
rank: name,
|
||||
networks
|
||||
});
|
||||
|
||||
|
||||
@@ -170,7 +177,8 @@ class DebugToolbar extends React.Component {
|
||||
sample(allNodes),
|
||||
_.sample(SHAPES),
|
||||
_.sample(STACK_VARIANTS),
|
||||
_.sample(NODE_COUNTS)
|
||||
_.sample(NODE_COUNTS),
|
||||
sample(NETWORKS, 10)
|
||||
))
|
||||
}));
|
||||
|
||||
@@ -208,8 +216,21 @@ class DebugToolbar extends React.Component {
|
||||
<button onClick={this.toggleColors}>toggle</button>
|
||||
</div>
|
||||
|
||||
{this.state.showColors && [getNodeColor, getNodeColorDark].map(fn => (
|
||||
<table>
|
||||
{this.state.showColors &&
|
||||
<table>
|
||||
<tbody>
|
||||
{LABEL_PREFIXES.map(r => (
|
||||
<tr key={r}>
|
||||
<td
|
||||
title={`${r}`}
|
||||
style={{backgroundColor: d3.hsl(text2degree(r), 0.5, 0.5).toString()}} />
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>}
|
||||
|
||||
{this.state.showColors && [getNodeColor, getNodeColorDark].map((fn, i) => (
|
||||
<table key={i}>
|
||||
<tbody>
|
||||
{LABEL_PREFIXES.map(r => (
|
||||
<tr key={r}>
|
||||
|
||||
@@ -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 (
|
||||
<div className="matched-results">
|
||||
<div className="matched-results" style={style}>
|
||||
{matches.keySeq().take(SHOW_ROW_COUNT).map(fieldId => this.renderMatch(matches, fieldId))}
|
||||
{moreFieldMatches && <div className="matched-results-more" title={moreFieldMatchesTitle}>
|
||||
{`${moreFieldMatches.size} more matches`}
|
||||
|
||||
@@ -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('network-selector-action', {
|
||||
'network-selector-action-selected': isSelected
|
||||
});
|
||||
const style = {
|
||||
borderBottomColor: getNodeColor(network.get('colorKey', id))
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
className={className}
|
||||
onMouseOver={this.onMouseOver}
|
||||
onClick={this.onMouseClick}
|
||||
style={style}>
|
||||
{network.get('label')}
|
||||
{isPinned && <span className="fa fa-thumb-tack"></span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
selectedNetwork: state.get('selectedNetwork'),
|
||||
pinnedNetwork: state.get('pinnedNetwork')
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{ selectNetwork, pinNetwork, unpinNetwork }
|
||||
)(NetworkSelectorItem);
|
||||
@@ -0,0 +1,63 @@
|
||||
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 => (
|
||||
<NetworkSelectorItem key={network.get('id')} network={network} />
|
||||
));
|
||||
|
||||
const className = classNames('network-selector-action', {
|
||||
'network-selector-action-selected': showingNetworks
|
||||
});
|
||||
|
||||
const style = {
|
||||
borderBottomColor: showingNetworks ? '#A2A0B3' : 'transparent'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="network-selector">
|
||||
<div className="network-selector-wrapper" onMouseLeave={this.onMouseOut}>
|
||||
<div className={className} onClick={this.onClick} style={style}>
|
||||
Networks
|
||||
</div>
|
||||
{showingNetworks && items}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
availableNetworks: state.get('availableNetworks'),
|
||||
showingNetworks: state.get('showingNetworks'),
|
||||
pinnedNetwork: state.get('pinnedNetwork')
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{ selectNetwork, showNetworks }
|
||||
)(NetworkSelector);
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ 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';
|
||||
|
||||
@@ -20,6 +22,7 @@ const topologySorter = topology => topology.get('rank');
|
||||
|
||||
export const initialState = makeMap({
|
||||
availableCanvasMetrics: makeList(),
|
||||
availableNetworks: makeList(),
|
||||
controlPipes: makeOrderedMap(), // pipeId -> controlPipe
|
||||
controlStatus: makeMap(),
|
||||
currentTopology: null,
|
||||
@@ -31,6 +34,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
|
||||
@@ -39,6 +43,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 +51,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
|
||||
@@ -265,6 +272,39 @@ export function rootReducer(state = initialState, action) {
|
||||
return state;
|
||||
}
|
||||
|
||||
//
|
||||
// networks
|
||||
//
|
||||
|
||||
case ActionTypes.SHOW_NETWORKS: {
|
||||
if (!action.visible) {
|
||||
state = state.set('selectedNetwork', null);
|
||||
state = state.set('pinnedNetwork', null);
|
||||
}
|
||||
return state.set('showingNetworks', action.visible);
|
||||
}
|
||||
|
||||
case ActionTypes.SELECT_NETWORK: {
|
||||
return state.set('selectedNetwork', action.networkId);
|
||||
}
|
||||
|
||||
case ActionTypes.PIN_NETWORK: {
|
||||
return state.merge({
|
||||
pinnedNetwork: action.networkId,
|
||||
selectedNetwork: action.networkId
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.UNPIN_NETWORK: {
|
||||
return state.merge({
|
||||
pinnedNetwork: null,
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// metrics
|
||||
//
|
||||
|
||||
case ActionTypes.SELECT_METRIC: {
|
||||
return state.set('selectedMetric', action.metricId);
|
||||
}
|
||||
@@ -481,6 +521,38 @@ 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(', ').map(n => ({id: n, label: n, colorKey: n}))));
|
||||
}
|
||||
}
|
||||
return node;
|
||||
}));
|
||||
|
||||
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')
|
||||
.map(n => n.get('id')).toJS());
|
||||
|
||||
if (networkPrefix) {
|
||||
state = state.update('nodes',
|
||||
nodes => nodes.map(node => node.update('networks',
|
||||
networks => networks.map(n => n.set('colorKey',
|
||||
n.get('colorKey').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 => (
|
||||
@@ -564,6 +636,13 @@ 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);
|
||||
state = state.set('selectedNetwork', action.state.pinnedNetwork);
|
||||
}
|
||||
if (action.state.controlPipe) {
|
||||
state = state.set('controlPipes', makeOrderedMap({
|
||||
[action.state.controlPipe.id]:
|
||||
|
||||
@@ -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('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -341,6 +341,11 @@ h2 {
|
||||
transition: opacity .5s @base-ease;
|
||||
text-align: center;
|
||||
|
||||
.node-network {
|
||||
// stroke: @background-lighter-color;
|
||||
// stroke-width: 4px;
|
||||
}
|
||||
|
||||
.node-label,
|
||||
.node-sublabel {
|
||||
line-height: 125%;
|
||||
@@ -1116,7 +1121,7 @@ h2 {
|
||||
}
|
||||
}
|
||||
|
||||
.topology-option, .metric-selector {
|
||||
.topology-option, .metric-selector, .network-selector {
|
||||
color: @text-secondary-color;
|
||||
margin: 6px 0;
|
||||
|
||||
@@ -1140,6 +1145,7 @@ h2 {
|
||||
padding: 3px 12px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
background-color: @background-color;
|
||||
|
||||
&-selected, &:hover {
|
||||
color: @text-darker-color;
|
||||
@@ -1167,6 +1173,11 @@ h2 {
|
||||
}
|
||||
}
|
||||
|
||||
.network-selector-action {
|
||||
border-top: 3px solid transparent;
|
||||
border-bottom: 3px solid @background-dark-color;
|
||||
}
|
||||
|
||||
.warning {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user