mirror of
https://github.com/weaveworks/scope.git
synced 2026-03-03 18:20:27 +00:00
Merge pull request #1831 from weaveworks/1829-fixes-moc-updating
Fixes metrics-on-canvas updating.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { completeNodesSelector } from '../selectors/chartSelectors';
|
||||
import NodesChartEdges from './nodes-chart-edges';
|
||||
import NodesChartNodes from './nodes-chart-nodes';
|
||||
|
||||
@@ -9,14 +10,24 @@ class NodesChartElements extends React.Component {
|
||||
const props = this.props;
|
||||
return (
|
||||
<g className="nodes-chart-elements" transform={props.transform}>
|
||||
<NodesChartEdges layoutEdges={props.layoutEdges}
|
||||
<NodesChartEdges
|
||||
layoutEdges={props.layoutEdges}
|
||||
layoutPrecision={props.layoutPrecision} />
|
||||
<NodesChartNodes layoutNodes={props.layoutNodes} nodeScale={props.nodeScale}
|
||||
scale={props.scale} selectedNodeScale={props.selectedNodeScale}
|
||||
<NodesChartNodes
|
||||
layoutNodes={props.completeNodes}
|
||||
nodeScale={props.nodeScale}
|
||||
scale={props.scale}
|
||||
selectedNodeScale={props.selectedNodeScale}
|
||||
layoutPrecision={props.layoutPrecision} />
|
||||
</g>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect()(NodesChartElements);
|
||||
function mapStateToProps(state, props) {
|
||||
return {
|
||||
completeNodes: completeNodesSelector(state, props)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(NodesChartElements);
|
||||
|
||||
@@ -3,17 +3,17 @@ import d3 from 'd3';
|
||||
import debug from 'debug';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Map as makeMap, fromJS, is as isDeepEqual } from 'immutable';
|
||||
import { Map as makeMap, fromJS } from 'immutable';
|
||||
import timely from 'timely';
|
||||
|
||||
import { nodeAdjacenciesSelector, adjacentNodesSelector } from '../selectors/chartSelectors';
|
||||
import { clickBackground } from '../actions/app-actions';
|
||||
import { EDGE_ID_SEPARATOR } from '../constants/naming';
|
||||
import { MIN_NODE_SIZE, DETAILS_PANEL_WIDTH, MAX_NODE_SIZE } from '../constants/styles';
|
||||
import Logo from '../components/logo';
|
||||
import { doLayout } from './nodes-layout';
|
||||
import NodesChartElements from './nodes-chart-elements';
|
||||
import { getActiveTopologyOptions, getAdjacentNodes,
|
||||
isSameTopology } from '../utils/topology-utils';
|
||||
import { getActiveTopologyOptions } from '../utils/topology-utils';
|
||||
|
||||
const log = debug('scope:nodes-chart');
|
||||
|
||||
@@ -24,6 +24,94 @@ const radiusDensity = d3.scale.threshold()
|
||||
.domain([3, 6])
|
||||
.range([2.5, 3.5, 3]);
|
||||
|
||||
/**
|
||||
* dynamic coords precision based on topology size
|
||||
*/
|
||||
function getLayoutPrecision(nodesCount) {
|
||||
let precision;
|
||||
if (nodesCount >= 50) {
|
||||
precision = 0;
|
||||
} else if (nodesCount > 20) {
|
||||
precision = 1;
|
||||
} else if (nodesCount > 10) {
|
||||
precision = 2;
|
||||
} else {
|
||||
precision = 3;
|
||||
}
|
||||
|
||||
return precision;
|
||||
}
|
||||
|
||||
|
||||
function initEdges(nodes) {
|
||||
let edges = makeMap();
|
||||
|
||||
nodes.forEach((node, nodeId) => {
|
||||
const adjacency = node.get('adjacency');
|
||||
if (adjacency) {
|
||||
adjacency.forEach(adjacent => {
|
||||
const edge = [nodeId, adjacent];
|
||||
const edgeId = edge.join(EDGE_ID_SEPARATOR);
|
||||
|
||||
if (!edges.has(edgeId)) {
|
||||
const source = edge[0];
|
||||
const target = edge[1];
|
||||
if (nodes.has(source) && nodes.has(target)) {
|
||||
edges = edges.set(edgeId, makeMap({
|
||||
id: edgeId,
|
||||
value: 1,
|
||||
source,
|
||||
target
|
||||
}));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return edges;
|
||||
}
|
||||
|
||||
|
||||
function getNodeScale(nodesCount, width, height) {
|
||||
const expanse = Math.min(height, width);
|
||||
const nodeSize = expanse / 3; // single node should fill a third of the screen
|
||||
const maxNodeSize = Math.min(MAX_NODE_SIZE, expanse / 10);
|
||||
const normalizedNodeSize = Math.max(MIN_NODE_SIZE,
|
||||
Math.min(nodeSize / Math.sqrt(nodesCount), maxNodeSize));
|
||||
|
||||
return d3.scale.linear().range([0, normalizedNodeSize]);
|
||||
}
|
||||
|
||||
|
||||
function updateLayout(width, height, nodes, baseOptions) {
|
||||
const nodeScale = getNodeScale(nodes.size, width, height);
|
||||
const edges = initEdges(nodes);
|
||||
|
||||
const options = Object.assign({}, baseOptions, {
|
||||
scale: nodeScale,
|
||||
});
|
||||
|
||||
const timedLayouter = timely(doLayout);
|
||||
const graph = timedLayouter(nodes, edges, options);
|
||||
|
||||
log(`graph layout took ${timedLayouter.time}ms`);
|
||||
|
||||
const layoutNodes = graph.nodes.map(node => makeMap({
|
||||
x: node.get('x'),
|
||||
y: node.get('y'),
|
||||
// extract coords and save for restore
|
||||
px: node.get('x'),
|
||||
py: node.get('y')
|
||||
}));
|
||||
|
||||
const layoutEdges = graph.edges
|
||||
.map(edge => edge.set('ppoints', edge.get('points')));
|
||||
|
||||
return { layoutNodes, layoutEdges, layoutWidth: graph.width, layoutHeight: graph.height };
|
||||
}
|
||||
|
||||
|
||||
class NodesChart extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
@@ -43,7 +131,7 @@ class NodesChart extends React.Component {
|
||||
hasZoomed: false,
|
||||
height: props.height || 0,
|
||||
width: props.width || 0,
|
||||
zoomCache: {}
|
||||
zoomCache: {},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -82,8 +170,7 @@ class NodesChart extends React.Component {
|
||||
state.height = nextProps.forceRelayout ? nextProps.height : (state.height || nextProps.height);
|
||||
state.width = nextProps.forceRelayout ? nextProps.width : (state.width || nextProps.width);
|
||||
|
||||
// _.assign(state, this.updateGraphState(nextProps, state));
|
||||
if (nextProps.forceRelayout || !isSameTopology(nextProps.nodes, this.props.nodes)) {
|
||||
if (nextProps.forceRelayout || nextProps.nodes !== this.props.nodes) {
|
||||
_.assign(state, this.updateGraphState(nextProps, state));
|
||||
}
|
||||
|
||||
@@ -127,6 +214,7 @@ class NodesChart extends React.Component {
|
||||
const transform = `translate(${translate}) scale(${scale})`;
|
||||
const svgClassNames = this.props.isEmpty ? 'hide' : '';
|
||||
|
||||
const layoutPrecision = getLayoutPrecision(nodes.size);
|
||||
return (
|
||||
<div className="nodes-chart">
|
||||
<svg width="100%" height="100%" id="nodes-chart-canvas"
|
||||
@@ -134,10 +222,14 @@ class NodesChart extends React.Component {
|
||||
<g transform="translate(24,24) scale(0.25)">
|
||||
<Logo />
|
||||
</g>
|
||||
<NodesChartElements layoutNodes={nodes} layoutEdges={edges}
|
||||
nodeScale={this.state.nodeScale} scale={scale} transform={transform}
|
||||
<NodesChartElements
|
||||
layoutNodes={nodes}
|
||||
layoutEdges={edges}
|
||||
nodeScale={this.state.nodeScale}
|
||||
scale={scale}
|
||||
transform={transform}
|
||||
selectedNodeScale={this.state.selectedNodeScale}
|
||||
layoutPrecision={this.props.layoutPrecision} />
|
||||
layoutPrecision={layoutPrecision} />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
@@ -151,70 +243,10 @@ class NodesChart extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
initNodes(topology, stateNodes) {
|
||||
let nextStateNodes = stateNodes;
|
||||
|
||||
// remove nodes that have disappeared
|
||||
stateNodes.forEach((node, id) => {
|
||||
if (!topology.has(id)) {
|
||||
nextStateNodes = nextStateNodes.delete(id);
|
||||
}
|
||||
});
|
||||
|
||||
// copy relevant fields to state nodes
|
||||
topology.forEach((node, id) => {
|
||||
nextStateNodes = nextStateNodes.mergeIn([id], makeMap({
|
||||
id,
|
||||
label: node.get('label'),
|
||||
pseudo: node.get('pseudo'),
|
||||
subLabel: node.get('label_minor'),
|
||||
nodeCount: node.get('node_count'),
|
||||
metrics: node.get('metrics'),
|
||||
rank: node.get('rank'),
|
||||
shape: node.get('shape'),
|
||||
stack: node.get('stack'),
|
||||
networks: node.get('networks'),
|
||||
}));
|
||||
});
|
||||
|
||||
return nextStateNodes;
|
||||
}
|
||||
|
||||
initEdges(topology, stateNodes) {
|
||||
let edges = makeMap();
|
||||
|
||||
topology.forEach((node, nodeId) => {
|
||||
const adjacency = node.get('adjacency');
|
||||
if (adjacency) {
|
||||
adjacency.forEach(adjacent => {
|
||||
const edge = [nodeId, adjacent];
|
||||
const edgeId = edge.join(EDGE_ID_SEPARATOR);
|
||||
|
||||
if (!edges.has(edgeId)) {
|
||||
const source = edge[0];
|
||||
const target = edge[1];
|
||||
if (stateNodes.has(source) && stateNodes.has(target)) {
|
||||
edges = edges.set(edgeId, makeMap({
|
||||
id: edgeId,
|
||||
value: 1,
|
||||
source,
|
||||
target
|
||||
}));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return edges;
|
||||
}
|
||||
|
||||
centerSelectedNode(props, state) {
|
||||
let stateNodes = state.nodes;
|
||||
let stateEdges = state.edges;
|
||||
const selectedLayoutNode = stateNodes.get(props.selectedNodeId);
|
||||
|
||||
if (!selectedLayoutNode) {
|
||||
if (!stateNodes.has(props.selectedNodeId)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -245,8 +277,8 @@ class NodesChart extends React.Component {
|
||||
const radius = Math.min(state.width, state.height) / density / zoomScale;
|
||||
const offsetAngle = Math.PI / 4;
|
||||
|
||||
stateNodes = stateNodes.map((node) => {
|
||||
const index = adjacentLayoutNodeIds.indexOf(node.get('id'));
|
||||
stateNodes = stateNodes.map((node, nodeId) => {
|
||||
const index = adjacentLayoutNodeIds.indexOf(nodeId);
|
||||
if (index > -1) {
|
||||
const angle = offsetAngle + Math.PI * 2 * index / adjacentCount;
|
||||
return node.merge({
|
||||
@@ -259,8 +291,8 @@ class NodesChart extends React.Component {
|
||||
|
||||
// fix all edges for circular nodes
|
||||
stateEdges = stateEdges.map(edge => {
|
||||
if (edge.get('source') === selectedLayoutNode.get('id')
|
||||
|| edge.get('target') === selectedLayoutNode.get('id')
|
||||
if (edge.get('source') === props.selectedNodeId
|
||||
|| edge.get('target') === props.selectedNodeId
|
||||
|| _.includes(adjacentLayoutNodeIds, edge.get('source'))
|
||||
|| _.includes(adjacentLayoutNodeIds, edge.get('target'))) {
|
||||
const source = stateNodes.get(edge.get('source'));
|
||||
@@ -274,7 +306,7 @@ class NodesChart extends React.Component {
|
||||
});
|
||||
|
||||
// auto-scale node size for selected nodes
|
||||
const selectedNodeScale = this.getNodeScale(adjacentNodes, state.width, state.height);
|
||||
const selectedNodeScale = getNodeScale(adjacentNodes.size, state.width, state.height);
|
||||
|
||||
return {
|
||||
selectedNodeScale,
|
||||
@@ -311,44 +343,23 @@ class NodesChart extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
const stateNodes = this.initNodes(props.nodes, state.nodes);
|
||||
const stateEdges = this.initEdges(props.nodes, stateNodes);
|
||||
const nodeScale = this.getNodeScale(props.nodes, state.width, state.height);
|
||||
const nextState = { nodeScale };
|
||||
|
||||
const options = {
|
||||
width: state.width,
|
||||
height: state.height,
|
||||
scale: nodeScale,
|
||||
margins: props.margins,
|
||||
forceRelayout: props.forceRelayout,
|
||||
topologyId: this.props.topologyId,
|
||||
topologyOptions: this.props.topologyOptions
|
||||
topologyId: props.topologyId,
|
||||
topologyOptions: props.topologyOptions,
|
||||
};
|
||||
|
||||
const timedLayouter = timely(doLayout);
|
||||
const graph = timedLayouter(stateNodes, stateEdges, options);
|
||||
|
||||
log(`graph layout took ${timedLayouter.time}ms`);
|
||||
|
||||
// extract coords and save for restore
|
||||
const graphNodes = graph.nodes.map(node => makeMap({
|
||||
x: node.get('x'),
|
||||
px: node.get('x'),
|
||||
y: node.get('y'),
|
||||
py: node.get('y')
|
||||
}));
|
||||
|
||||
const layoutNodes = stateNodes.mergeDeep(graphNodes);
|
||||
|
||||
const layoutEdges = graph.edges
|
||||
.map(edge => edge.set('ppoints', edge.get('points')));
|
||||
|
||||
const { layoutNodes, layoutEdges, layoutWidth, layoutHeight } = updateLayout(
|
||||
state.width, state.height, props.nodes, options);
|
||||
//
|
||||
// adjust layout based on viewport
|
||||
const xFactor = (state.width - props.margins.left - props.margins.right) / graph.width;
|
||||
const yFactor = state.height / graph.height;
|
||||
const xFactor = (state.width - props.margins.left - props.margins.right) / layoutWidth;
|
||||
const yFactor = state.height / layoutHeight;
|
||||
const zoomFactor = Math.min(xFactor, yFactor);
|
||||
let zoomScale = this.state.scale;
|
||||
let zoomScale = state.scale;
|
||||
|
||||
if (this.zoom && !state.hasZoomed && zoomFactor > 0 && zoomFactor < 1) {
|
||||
zoomScale = zoomFactor;
|
||||
@@ -356,24 +367,12 @@ class NodesChart extends React.Component {
|
||||
this.zoom.scale(zoomFactor);
|
||||
}
|
||||
|
||||
nextState.scale = zoomScale;
|
||||
if (!isDeepEqual(layoutNodes, state.nodes)) {
|
||||
nextState.nodes = layoutNodes;
|
||||
}
|
||||
if (!isDeepEqual(layoutEdges, state.edges)) {
|
||||
nextState.edges = layoutEdges;
|
||||
}
|
||||
|
||||
return nextState;
|
||||
}
|
||||
|
||||
getNodeScale(nodes, width, height) {
|
||||
const expanse = Math.min(height, width);
|
||||
const nodeSize = expanse / 3; // single node should fill a third of the screen
|
||||
const maxNodeSize = Math.min(MAX_NODE_SIZE, expanse / 10);
|
||||
const normalizedNodeSize = Math.max(MIN_NODE_SIZE,
|
||||
Math.min(nodeSize / Math.sqrt(nodes.size), maxNodeSize));
|
||||
return this.state.nodeScale.copy().range([0, normalizedNodeSize]);
|
||||
return {
|
||||
scale: zoomScale,
|
||||
nodes: layoutNodes,
|
||||
edges: layoutEdges,
|
||||
nodeScale: getNodeScale(props.nodes.size, state.width, state.height),
|
||||
};
|
||||
}
|
||||
|
||||
zoomed() {
|
||||
@@ -391,9 +390,11 @@ class NodesChart extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
adjacentNodes: getAdjacentNodes(state),
|
||||
nodes: nodeAdjacenciesSelector(state),
|
||||
adjacentNodes: adjacentNodesSelector(state),
|
||||
forceRelayout: state.get('forceRelayout'),
|
||||
selectedNodeId: state.get('selectedNodeId'),
|
||||
topologyId: state.get('currentTopologyId'),
|
||||
@@ -401,6 +402,7 @@ function mapStateToProps(state) {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{ clickBackground }
|
||||
|
||||
@@ -5,6 +5,7 @@ import { connect } from 'react-redux';
|
||||
import { List as makeList, Map as makeMap } from 'immutable';
|
||||
import NodeDetailsTable from '../components/node-details/node-details-table';
|
||||
import { clickNode, sortOrderChanged } from '../actions/app-actions';
|
||||
import { nodesSelector } from '../selectors/chartSelectors';
|
||||
|
||||
import { getNodeColor } from '../utils/color-utils';
|
||||
|
||||
@@ -142,6 +143,7 @@ class NodesGrid extends React.Component {
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
nodes: nodesSelector(state),
|
||||
gridSortBy: state.get('gridSortBy'),
|
||||
gridSortedDesc: state.get('gridSortedDesc'),
|
||||
currentTopology: state.get('currentTopology'),
|
||||
|
||||
@@ -142,6 +142,7 @@ class App extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
activeTopologyOptions: getActiveTopologyOptions(state),
|
||||
@@ -158,6 +159,7 @@ function mapStateToProps(state) {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export default connect(
|
||||
mapStateToProps
|
||||
)(App);
|
||||
|
||||
@@ -43,9 +43,6 @@ const LABEL_PREFIXES = _.range('A'.charCodeAt(), 'Z'.charCodeAt() + 1)
|
||||
.map(n => String.fromCharCode(n));
|
||||
|
||||
|
||||
// const randomLetter = () => _.sample(LABEL_PREFIXES);
|
||||
|
||||
|
||||
const deltaAdd = (
|
||||
name, adjacency = [], shape = 'circle', stack = false, nodeCount = 1,
|
||||
networks = NETWORKS
|
||||
@@ -71,7 +68,7 @@ function addMetrics(availableMetrics, node, v) {
|
||||
]);
|
||||
|
||||
return Object.assign({}, node, {
|
||||
metrics: metrics.map(m => Object.assign({}, m, {max: 100, value: v}))
|
||||
metrics: metrics.map(m => Object.assign({}, m, {label: 'zing', max: 100, value: v})).toJS()
|
||||
});
|
||||
}
|
||||
|
||||
@@ -94,14 +91,16 @@ function addAllVariants(dispatch) {
|
||||
}
|
||||
|
||||
|
||||
function addAllMetricVariants(availableMetrics, dispatch) {
|
||||
function addAllMetricVariants(availableMetrics) {
|
||||
const newNodes = _.flattenDeep(METRIC_FILLS.map((v, i) => (
|
||||
SHAPES.map(s => [addMetrics(availableMetrics, deltaAdd(label(s) + i, [], s), v)])
|
||||
)));
|
||||
|
||||
dispatch(receiveNodesDelta({
|
||||
add: newNodes
|
||||
}));
|
||||
return (dispatch) => {
|
||||
dispatch(receiveNodesDelta({
|
||||
add: newNodes
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -177,11 +176,28 @@ class DebugToolbar extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
setLoading(loading) {
|
||||
this.props.setAppState(state => state.set('topologiesLoaded', !loading));
|
||||
asyncDispatch(v) {
|
||||
setTimeout(() => this.props.dispatch(v), 0);
|
||||
}
|
||||
|
||||
addNodes(n, prefix = 'zing') {
|
||||
setLoading(loading) {
|
||||
this.asyncDispatch(setAppState(state => state.set('topologiesLoaded', !loading)));
|
||||
}
|
||||
|
||||
updateAdjacencies() {
|
||||
const ns = this.props.nodes;
|
||||
const nodeNames = ns.keySeq().toJS();
|
||||
this.asyncDispatch(receiveNodesDelta({
|
||||
add: this._addNodes(7),
|
||||
update: sample(nodeNames).map(n => ({
|
||||
id: n,
|
||||
adjacency: sample(nodeNames),
|
||||
}), nodeNames.length),
|
||||
remove: this._removeNode(),
|
||||
}));
|
||||
}
|
||||
|
||||
_addNodes(n, prefix = 'zing') {
|
||||
const ns = this.props.nodes;
|
||||
const nodeNames = ns.keySeq().toJS();
|
||||
const newNodeNames = _.range(ns.size, ns.size + n).map(i => (
|
||||
@@ -189,19 +205,35 @@ class DebugToolbar extends React.Component {
|
||||
`${prefix}${i}`
|
||||
));
|
||||
const allNodes = _(nodeNames).concat(newNodeNames).value();
|
||||
return newNodeNames.map((name) => deltaAdd(
|
||||
name,
|
||||
sample(allNodes),
|
||||
_.sample(SHAPES),
|
||||
_.sample(STACK_VARIANTS),
|
||||
_.sample(NODE_COUNTS),
|
||||
sample(NETWORKS, 10)
|
||||
));
|
||||
}
|
||||
|
||||
this.props.dispatch(receiveNodesDelta({
|
||||
add: newNodeNames.map((name) => deltaAdd(
|
||||
name,
|
||||
sample(allNodes),
|
||||
_.sample(SHAPES),
|
||||
_.sample(STACK_VARIANTS),
|
||||
_.sample(NODE_COUNTS),
|
||||
sample(NETWORKS, 10)
|
||||
))
|
||||
addNodes(n, prefix = 'zing') {
|
||||
setTimeout(() => {
|
||||
this.asyncDispatch(receiveNodesDelta({
|
||||
add: this._addNodes(n, prefix)
|
||||
}));
|
||||
log('added nodes', n);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
_removeNode() {
|
||||
const ns = this.props.nodes;
|
||||
const nodeNames = ns.keySeq().toJS();
|
||||
return [nodeNames[_.random(nodeNames.length - 1)]];
|
||||
}
|
||||
|
||||
removeNode() {
|
||||
this.asyncDispatch(receiveNodesDelta({
|
||||
remove: this._removeNode()
|
||||
}));
|
||||
|
||||
log('added nodes', n);
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -215,11 +247,13 @@ class DebugToolbar extends React.Component {
|
||||
<button onClick={() => this.addNodes(10)}>+10</button>
|
||||
<input type="number" onChange={this.onChange} value={this.state.nodesToAdd} />
|
||||
<button onClick={() => this.addNodes(this.state.nodesToAdd)}>+</button>
|
||||
<button onClick={() => addAllVariants(this.props.dispatch)}>Variants</button>
|
||||
<button onClick={() => addAllMetricVariants(availableCanvasMetrics, this.props.dispatch)}>
|
||||
<button onClick={() => this.asyncDispatch(addAllVariants)}>Variants</button>
|
||||
<button onClick={() => this.asyncDispatch(addAllMetricVariants(availableCanvasMetrics))}>
|
||||
Metric Variants
|
||||
</button>
|
||||
<button onClick={() => this.addNodes(1, LOREM)}>Long name</button>
|
||||
<button onClick={() => this.removeNode()}>Remove node</button>
|
||||
<button onClick={() => this.updateAdjacencies()}>Update adj.</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -289,6 +323,5 @@ function mapStateToProps(state) {
|
||||
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{setAppState}
|
||||
mapStateToProps
|
||||
)(DebugToolbar);
|
||||
|
||||
@@ -13,24 +13,6 @@ const navbarHeight = 194;
|
||||
const marginTop = 0;
|
||||
|
||||
|
||||
/**
|
||||
* dynamic coords precision based on topology size
|
||||
*/
|
||||
function getLayoutPrecision(nodesCount) {
|
||||
let precision;
|
||||
if (nodesCount >= 50) {
|
||||
precision = 0;
|
||||
} else if (nodesCount > 20) {
|
||||
precision = 1;
|
||||
} else if (nodesCount > 10) {
|
||||
precision = 2;
|
||||
} else {
|
||||
precision = 3;
|
||||
}
|
||||
|
||||
return precision;
|
||||
}
|
||||
|
||||
class Nodes extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
@@ -67,9 +49,8 @@ class Nodes extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { nodes, topologyEmpty, gridMode, topologiesLoaded, nodesLoaded, topologies,
|
||||
const { topologyEmpty, gridMode, topologiesLoaded, nodesLoaded, topologies,
|
||||
currentTopology } = this.props;
|
||||
const layoutPrecision = getLayoutPrecision(nodes.size);
|
||||
|
||||
return (
|
||||
<div className="nodes-wrapper">
|
||||
@@ -84,13 +65,10 @@ class Nodes extends React.Component {
|
||||
{gridMode ?
|
||||
<NodesGrid {...this.state}
|
||||
nodeSize="24"
|
||||
nodes={nodes}
|
||||
margins={CANVAS_MARGINS}
|
||||
/> :
|
||||
<NodesChart {...this.state}
|
||||
nodes={nodes}
|
||||
margins={CANVAS_MARGINS}
|
||||
layoutPrecision={layoutPrecision}
|
||||
/>}
|
||||
</div>
|
||||
);
|
||||
@@ -113,7 +91,6 @@ function mapStateToProps(state) {
|
||||
return {
|
||||
currentTopology: state.get('currentTopology'),
|
||||
gridMode: state.get('gridMode'),
|
||||
nodes: state.get('nodes').filter(node => !node.get('filtered')),
|
||||
nodesLoaded: state.get('nodesLoaded'),
|
||||
topologies: state.get('topologies'),
|
||||
topologiesLoaded: state.get('topologiesLoaded'),
|
||||
|
||||
118
client/app/scripts/selectors/chartSelectors.js
Normal file
118
client/app/scripts/selectors/chartSelectors.js
Normal file
@@ -0,0 +1,118 @@
|
||||
import debug from 'debug';
|
||||
import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect';
|
||||
import { Map as makeMap, is, Set } from 'immutable';
|
||||
|
||||
import { getAdjacentNodes } from '../utils/topology-utils';
|
||||
|
||||
|
||||
const log = debug('scope:selectors');
|
||||
|
||||
|
||||
//
|
||||
// `mergeDeepKeyIntersection` does a deep merge on keys that exists in both maps
|
||||
//
|
||||
function mergeDeepKeyIntersection(mapA, mapB) {
|
||||
const commonKeys = Set.fromKeys(mapA).intersect(mapB.keySeq());
|
||||
return makeMap(commonKeys.map(k => [k, mapA.get(k).mergeDeep(mapB.get(k))]));
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// `returnPreviousRefIfEqual` is a helper function that checks the new computed of a selector
|
||||
// against the previously computed value. If they are deeply equal return the previous result. This
|
||||
// is important for things like connect() which tests whether componentWillReceiveProps should be
|
||||
// called by doing a '===' on the values you return from mapStateToProps.
|
||||
//
|
||||
// e.g.
|
||||
//
|
||||
// const filteredThings = createSelector(
|
||||
// state => state.things,
|
||||
// (things) => things.filter(t => t > 2)
|
||||
// );
|
||||
//
|
||||
// // This will trigger componentWillReceiveProps on every store change:
|
||||
// connect(s => { things: filteredThings(s) }, ThingComponent);
|
||||
//
|
||||
// // But if we wrap it, the result will be === if it `is()` equal and...
|
||||
// const filteredThingsWrapped = returnPreviousRefIfEqual(filteredThings);
|
||||
//
|
||||
// // ...We're safe!
|
||||
// connect(s => { things: filteredThingsWrapped(s) }, ThingComponent);
|
||||
//
|
||||
// Note: This is a slightly strange way to use reselect. Selectors memoize their *arguments* not
|
||||
// "their results", so use the result of the wrapped selector as the argument to another selector
|
||||
// here to memoize it and get what we want.
|
||||
//
|
||||
const _createDeepEqualSelector = createSelectorCreator(defaultMemoize, is);
|
||||
const _identity = v => v;
|
||||
const returnPreviousRefIfEqual = (selector) => _createDeepEqualSelector(selector, _identity);
|
||||
|
||||
|
||||
//
|
||||
// Selectors!
|
||||
//
|
||||
|
||||
|
||||
const allNodesSelector = state => state.get('nodes');
|
||||
|
||||
|
||||
export const nodesSelector = returnPreviousRefIfEqual(
|
||||
createSelector(
|
||||
allNodesSelector,
|
||||
(allNodes) => allNodes.filter(node => !node.get('filtered'))
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
export const adjacentNodesSelector = returnPreviousRefIfEqual(getAdjacentNodes);
|
||||
|
||||
|
||||
export const nodeAdjacenciesSelector = returnPreviousRefIfEqual(
|
||||
createSelector(
|
||||
nodesSelector,
|
||||
(nodes) => nodes.map(n => makeMap({
|
||||
id: n.get('id'),
|
||||
adjacency: n.get('adjacency'),
|
||||
}))
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
export const dataNodesSelector = createSelector(
|
||||
nodesSelector,
|
||||
(nodes) => nodes.map((node, id) => makeMap({
|
||||
id,
|
||||
label: node.get('label'),
|
||||
pseudo: node.get('pseudo'),
|
||||
subLabel: node.get('label_minor'),
|
||||
nodeCount: node.get('node_count'),
|
||||
metrics: node.get('metrics'),
|
||||
rank: node.get('rank'),
|
||||
shape: node.get('shape'),
|
||||
stack: node.get('stack'),
|
||||
networks: node.get('networks'),
|
||||
}))
|
||||
);
|
||||
|
||||
|
||||
//
|
||||
// FIXME: this is a bit of a hack...
|
||||
//
|
||||
export const layoutNodesSelector = (_, props) => props.layoutNodes || makeMap();
|
||||
|
||||
|
||||
export const completeNodesSelector = createSelector(
|
||||
layoutNodesSelector,
|
||||
dataNodesSelector,
|
||||
(layoutNodes, dataNodes) => {
|
||||
//
|
||||
// There are no guarantees whether this selector will be computed first (when
|
||||
// node-chart-elements.mapStateToProps is called by store.subscribe before
|
||||
// nodes-chart.mapStateToProps is called), and component render batching and yadada.
|
||||
//
|
||||
if (layoutNodes.size !== dataNodes.size) {
|
||||
log('Obviously mismatched node data', layoutNodes.size, dataNodes.size);
|
||||
}
|
||||
return mergeDeepKeyIntersection(dataNodes, layoutNodes);
|
||||
}
|
||||
);
|
||||
@@ -1,5 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import { is as isDeepEqual, Map as makeMap, Set as makeSet, List as makeList } from 'immutable';
|
||||
import { Set as makeSet, List as makeList } from 'immutable';
|
||||
|
||||
|
||||
//
|
||||
@@ -143,6 +143,7 @@ export function isTopologyEmpty(state) {
|
||||
&& state.get('nodes').size === 0;
|
||||
}
|
||||
|
||||
|
||||
export function getAdjacentNodes(state, originNodeId) {
|
||||
let adjacentNodes = makeSet();
|
||||
const nodeId = originNodeId || state.get('selectedNodeId');
|
||||
@@ -171,13 +172,6 @@ export function getCurrentTopologyUrl(state) {
|
||||
return state.getIn(['currentTopology', 'url']);
|
||||
}
|
||||
|
||||
export function isSameTopology(nodes, nextNodes) {
|
||||
const mapper = node => makeMap({id: node.get('id'), adjacency: node.get('adjacency')});
|
||||
const topology = nodes.map(mapper);
|
||||
const nextTopology = nextNodes.map(mapper);
|
||||
return isDeepEqual(topology, nextTopology);
|
||||
}
|
||||
|
||||
export function isNodeMatchingQuery(node, query) {
|
||||
return node.get('label').includes(query) || node.get('subLabel').includes(query);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"redux-logger": "2.6.1",
|
||||
"redux-thunk": "2.0.1",
|
||||
"reqwest": "~2.0.5",
|
||||
"reselect": "^2.5.3",
|
||||
"timely": "0.1.0",
|
||||
"whatwg-fetch": "0.11.0"
|
||||
},
|
||||
|
||||
@@ -66,7 +66,7 @@ if (process.env.NODE_ENV !== 'production') {
|
||||
hot: true,
|
||||
noInfo: true,
|
||||
historyApiFallback: true,
|
||||
stats: { colors: true }
|
||||
stats: 'errors-only',
|
||||
}).listen(4041, '0.0.0.0', function (err, result) {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
|
||||
@@ -18,85 +18,84 @@ function clickIfVisible(list, index) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function selectNode(browser) {
|
||||
debug('select node');
|
||||
return browser.elementByCssSelector('.nodes-chart-nodes .node:nth-child(1) > g', function(err, el) {
|
||||
return el.click();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function deselectNode(browser) {
|
||||
debug('deselect node');
|
||||
return browser.elementByCssSelector('.fa-close', function(err, el) {
|
||||
return el.click();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
module.exports = function(cfg) {
|
||||
|
||||
var startUrl = 'http://' + cfg.host + '/debug.html';
|
||||
var selectedUrl = 'http://' + cfg.host + '/debug.html#!/state/{"nodeDetails":[{"id":"zing11","label":"zing11","topologyId":"containers"}],"selectedNodeId":"zing11","topologyId":"containers","topologyOptions":{"processes":{"unconnected":"hide"},"processes-by-name":{"unconnected":"hide"},"containers":{"system":"hide","stopped":"hide"},"containers-by-hostname":{"system":"hide","stopped":"hide"},"containers-by-image":{"system":"hide","stopped":"hide"}}}';
|
||||
|
||||
var startUrl = 'http://' + cfg.host + '/';
|
||||
// cfg - The configuration object. args, from the example above.
|
||||
return function(browser) {
|
||||
// browser is created using wd.promiseRemote()
|
||||
// More info about wd at https://github.com/admc/wd
|
||||
return browser.get('http://' + cfg.host + '/debug.html')
|
||||
return browser.get('http://' + cfg.host + '/')
|
||||
.then(function() {
|
||||
debug('starting run ' + cfg.run);
|
||||
return browser.sleep(2000);
|
||||
})
|
||||
.then(function() {
|
||||
return browser.execute("localStorage.debugToolbar = 1;");
|
||||
})
|
||||
.then(function() {
|
||||
return browser.sleep(5000);
|
||||
})
|
||||
.then(function() {
|
||||
return browser.elementByCssSelector('.debug-panel button:nth-child(5)');
|
||||
// return browser.elementByCssSelector('.debug-panel div:nth-child(2) button:nth-child(9)');
|
||||
})
|
||||
.then(function(el) {
|
||||
debug('debug-panel found');
|
||||
return el.click(function() {
|
||||
el.click(function() {
|
||||
el.click();
|
||||
});
|
||||
});
|
||||
return el.click();
|
||||
})
|
||||
.then(function() {
|
||||
return browser.sleep(2000);
|
||||
})
|
||||
|
||||
.then(function() {
|
||||
return browser.sleep(2000);
|
||||
})
|
||||
.then(function() {
|
||||
debug('select node');
|
||||
return browser.get(selectedUrl);
|
||||
return selectNode(browser);
|
||||
})
|
||||
.then(function() {
|
||||
return browser.sleep(5000);
|
||||
})
|
||||
.then(function() {
|
||||
debug('deselect node');
|
||||
return browser.elementByCssSelector('.fa-close', function(err, el) {
|
||||
return el.click();
|
||||
});
|
||||
return deselectNode(browser);
|
||||
})
|
||||
|
||||
.then(function() {
|
||||
return browser.sleep(2000);
|
||||
})
|
||||
.then(function() {
|
||||
debug('select node');
|
||||
return browser.get(selectedUrl);
|
||||
return selectNode(browser);
|
||||
})
|
||||
.then(function() {
|
||||
return browser.sleep(5000);
|
||||
})
|
||||
.then(function() {
|
||||
debug('deselect node');
|
||||
return browser.elementByCssSelector('.fa-close', function(err, el) {
|
||||
return el.click();
|
||||
});
|
||||
return deselectNode(browser);
|
||||
})
|
||||
|
||||
.then(function() {
|
||||
return browser.sleep(2000);
|
||||
})
|
||||
.then(function() {
|
||||
debug('select node');
|
||||
return browser.get(selectedUrl);
|
||||
return selectNode(browser);
|
||||
})
|
||||
.then(function() {
|
||||
return browser.sleep(5000);
|
||||
})
|
||||
.then(function() {
|
||||
debug('deselect node');
|
||||
return browser.elementByCssSelector('.fa-close', function(err, el) {
|
||||
return el.click();
|
||||
});
|
||||
return deselectNode(browser);
|
||||
})
|
||||
|
||||
.then(function() {
|
||||
|
||||
@@ -3,7 +3,7 @@ var options = {
|
||||
selenium: 'http://local.docker:4444/wd/hub',
|
||||
actions: [require('./custom-action.js')()]
|
||||
}
|
||||
browserPerf('http://local.docker:4040/debug.html', function(err, res){
|
||||
browserPerf('http://local.docker:4040/dev.html', function(err, res){
|
||||
console.error(err);
|
||||
console.log(res);
|
||||
}, options);
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
#
|
||||
# perfjankie --only-update-site --couch-server http://local.docker:5984 --couch-database performance
|
||||
#
|
||||
# Optional: run from localhost which can be a bit fast than rebuilding...
|
||||
# - ssh -R 0.0.0.0:4042:localhost:4042 docker@local.docker
|
||||
# - npm run build
|
||||
# - BACKEND_HOST=local.docker npm start
|
||||
# - ./run-jankie.sh 192.168.64.3:4042
|
||||
#
|
||||
# Usage:
|
||||
#
|
||||
# ./run-jankie.sh 192.168.64.3:4040
|
||||
@@ -26,9 +32,9 @@ COMMIT=$(git log --format="%h" -1)
|
||||
|
||||
echo "Testing $COMMIT on $DATE"
|
||||
|
||||
../../scope stop
|
||||
make SUDO= -C ../..
|
||||
../../scope launch
|
||||
sleep 5
|
||||
# ../../scope stop
|
||||
# make SUDO= -C ../..
|
||||
# ../../scope launch
|
||||
# sleep 5
|
||||
|
||||
COMMIT="$COMMIT" DATE=$DATE HOST=$HOST DEBUG=scope* node ./perfjankie/main.js
|
||||
|
||||
Reference in New Issue
Block a user