mirror of
https://github.com/weaveworks/scope.git
synced 2026-03-03 10:11:03 +00:00
Merge pull request #1186 from weaveworks/pure-mixin
Performance improvements for canvas
This commit is contained in:
2
Makefile
2
Makefile
@@ -20,7 +20,7 @@ CODECGEN_EXE=$(CODECGEN_DIR)/bin/codecgen_$(shell go env GOHOSTOS)_$(shell go en
|
||||
GET_CODECGEN_DEPS=$(shell find $(1) -maxdepth 1 -type f -name '*.go' -not -name '*_test.go' -not -name '*.codecgen.go' -not -name '*.generated.go')
|
||||
CODECGEN_TARGETS=report/report.codecgen.go render/render.codecgen.go render/detailed/detailed.codecgen.go
|
||||
RM=--rm
|
||||
RUN_FLAGS=-ti
|
||||
RUN_FLAGS=-i
|
||||
BUILD_IN_CONTAINER=true
|
||||
GO_ENV=GOGC=off
|
||||
GO=env $(GO_ENV) go
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
app/scripts/vendor/term.js
|
||||
test/
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"comma-dangle": 0,
|
||||
"object-curly-spacing": 0,
|
||||
"react/jsx-closing-bracket-location": 0,
|
||||
"react/prefer-stateless-function": 0,
|
||||
"react/sort-comp": 0,
|
||||
"react/prop-types": 0
|
||||
}
|
||||
|
||||
1
client/.gitignore
vendored
1
client/.gitignore
vendored
@@ -2,3 +2,4 @@ node_modules
|
||||
build/app.js
|
||||
build/*[woff2?|ttf|eot|svg]
|
||||
coverage/
|
||||
test/*png
|
||||
|
||||
96
client/app/scripts/charts/edge-container.js
Normal file
96
client/app/scripts/charts/edge-container.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import _ from 'lodash';
|
||||
import d3 from 'd3';
|
||||
import React from 'react';
|
||||
import { Motion, spring } from 'react-motion';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
import { Map as makeMap } from 'immutable';
|
||||
|
||||
import Edge from './edge';
|
||||
|
||||
const animConfig = [80, 20]; // stiffness, damping
|
||||
const pointCount = 30;
|
||||
|
||||
const line = d3.svg.line()
|
||||
.interpolate('basis')
|
||||
.x(d => d.x)
|
||||
.y(d => d.y);
|
||||
|
||||
const buildPath = (points, layoutPrecision) => {
|
||||
const extracted = [];
|
||||
_.each(points, (value, key) => {
|
||||
const axis = key[0];
|
||||
const index = key.slice(1);
|
||||
if (!extracted[index]) {
|
||||
extracted[index] = {};
|
||||
}
|
||||
extracted[index][axis] = d3.round(value, layoutPrecision);
|
||||
});
|
||||
return extracted;
|
||||
};
|
||||
|
||||
export default class EdgeContainer extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
pointsMap: makeMap()
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.preparePoints(this.props.points);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.preparePoints(nextProps.points);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { layoutPrecision, points } = this.props;
|
||||
const other = _.omit(this.props, 'points');
|
||||
|
||||
if (layoutPrecision === 0) {
|
||||
const path = line(points.toJS());
|
||||
return <Edge {...other} path={path} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Motion style={this.state.pointsMap.toJS()}>
|
||||
{(interpolated) => {
|
||||
// convert points to path string, because that lends itself to
|
||||
// JS-equality checks in the child component
|
||||
const path = line(buildPath(interpolated, layoutPrecision));
|
||||
return <Edge {...other} path={path} />;
|
||||
}}
|
||||
</Motion>
|
||||
);
|
||||
}
|
||||
|
||||
preparePoints(nextPoints) {
|
||||
// Spring needs constant field count, hoping that dagre will insert never more than `pointCount`
|
||||
let { pointsMap } = this.state;
|
||||
|
||||
// filling up the map with copies of the first point
|
||||
const filler = nextPoints.first();
|
||||
const missing = pointCount - nextPoints.size;
|
||||
let index = 0;
|
||||
if (missing > 0) {
|
||||
while (index < missing) {
|
||||
pointsMap = pointsMap.set(`x${index}`, spring(filler.get('x'), animConfig));
|
||||
pointsMap = pointsMap.set(`y${index}`, spring(filler.get('y'), animConfig));
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
nextPoints.forEach((point, i) => {
|
||||
pointsMap = pointsMap.set(`x${index + i}`, spring(point.get('x'), animConfig));
|
||||
pointsMap = pointsMap.set(`y${index + i}`, spring(point.get('y'), animConfig));
|
||||
});
|
||||
|
||||
this.setState({ pointsMap });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
reactMixin.onClass(EdgeContainer, PureRenderMixin);
|
||||
@@ -1,102 +1,45 @@
|
||||
import _ from 'lodash';
|
||||
import d3 from 'd3';
|
||||
import React from 'react';
|
||||
import { Motion, spring } from 'react-motion';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
|
||||
import { enterEdge, leaveEdge } from '../actions/app-actions';
|
||||
|
||||
const line = d3.svg.line()
|
||||
.interpolate('basis')
|
||||
.x(d => d.x)
|
||||
.y(d => d.y);
|
||||
|
||||
const animConfig = [80, 20]; // stiffness, damping
|
||||
|
||||
const flattenPoints = points => {
|
||||
const flattened = {};
|
||||
points.forEach((point, i) => {
|
||||
flattened[`x${i}`] = spring(point.x, animConfig);
|
||||
flattened[`y${i}`] = spring(point.y, animConfig);
|
||||
});
|
||||
return flattened;
|
||||
};
|
||||
|
||||
const extractPoints = points => {
|
||||
const extracted = [];
|
||||
_.each(points, (value, key) => {
|
||||
const axis = key[0];
|
||||
const index = key.slice(1);
|
||||
if (!extracted[index]) {
|
||||
extracted[index] = {};
|
||||
}
|
||||
extracted[index][axis] = value;
|
||||
});
|
||||
return extracted;
|
||||
};
|
||||
|
||||
export default class Edge extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.handleMouseEnter = this.handleMouseEnter.bind(this);
|
||||
this.handleMouseLeave = this.handleMouseLeave.bind(this);
|
||||
|
||||
this.state = {
|
||||
points: []
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.ensureSameLength(this.props.points);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.ensureSameLength(nextProps.points);
|
||||
}
|
||||
|
||||
render() {
|
||||
const classNames = ['edge'];
|
||||
const points = flattenPoints(this.props.points);
|
||||
const props = this.props;
|
||||
const handleMouseEnter = this.handleMouseEnter;
|
||||
const handleMouseLeave = this.handleMouseLeave;
|
||||
const { hasSelectedNode, highlightedEdgeIds, id, layoutPrecision,
|
||||
path, selectedNodeId, source, target } = this.props;
|
||||
|
||||
if (this.props.highlighted) {
|
||||
const classNames = ['edge'];
|
||||
if (highlightedEdgeIds.has(id)) {
|
||||
classNames.push('highlighted');
|
||||
}
|
||||
if (this.props.blurred) {
|
||||
if (hasSelectedNode
|
||||
&& source !== selectedNodeId
|
||||
&& target !== selectedNodeId) {
|
||||
classNames.push('blurred');
|
||||
}
|
||||
if (hasSelectedNode && layoutPrecision === 0
|
||||
&& (source === selectedNodeId || target === selectedNodeId)) {
|
||||
classNames.push('focused');
|
||||
}
|
||||
const classes = classNames.join(' ');
|
||||
|
||||
return (
|
||||
<Motion style={points}>
|
||||
{(interpolated) => {
|
||||
const path = line(extractPoints(interpolated));
|
||||
return (
|
||||
<g className={classes} onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave} id={props.id}>
|
||||
<path d={path} className="shadow" />
|
||||
<path d={path} className="link" />
|
||||
</g>
|
||||
);
|
||||
}}
|
||||
</Motion>
|
||||
<g className={classes} onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave} id={id}>
|
||||
<path d={path} className="shadow" />
|
||||
<path d={path} className="link" />
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
ensureSameLength(points) {
|
||||
// Spring needs constant list length, hoping that dagre will insert never more than 30
|
||||
const length = 30;
|
||||
let missing = length - points.length;
|
||||
|
||||
while (missing > 0) {
|
||||
points.unshift(points[0]);
|
||||
missing = length - points.length;
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
handleMouseEnter(ev) {
|
||||
enterEdge(ev.currentTarget.id);
|
||||
}
|
||||
@@ -105,3 +48,5 @@ export default class Edge extends React.Component {
|
||||
leaveEdge(ev.currentTarget.id);
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(Edge, PureRenderMixin);
|
||||
|
||||
33
client/app/scripts/charts/node-container.js
Normal file
33
client/app/scripts/charts/node-container.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
import d3 from 'd3';
|
||||
import { Motion, spring } from 'react-motion';
|
||||
|
||||
import Node from './node';
|
||||
|
||||
export default class NodeContainer extends React.Component {
|
||||
render() {
|
||||
const { dx, dy, focused, layoutPrecision, zoomScale } = this.props;
|
||||
const animConfig = [80, 20]; // stiffness, damping
|
||||
const scaleFactor = focused ? (2 / zoomScale) : 1;
|
||||
const other = _.omit(this.props, 'dx', 'dy');
|
||||
|
||||
return (
|
||||
<Motion style={{
|
||||
x: spring(dx, animConfig),
|
||||
y: spring(dy, animConfig),
|
||||
f: spring(scaleFactor, animConfig)
|
||||
}}>
|
||||
{interpolated => {
|
||||
const transform = `translate(${d3.round(interpolated.x, layoutPrecision)},`
|
||||
+ `${d3.round(interpolated.y, layoutPrecision)})`;
|
||||
return <Node {...other} transform={transform} scaleFactor={interpolated.f} />;
|
||||
}}
|
||||
</Motion>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(NodeContainer, PureRenderMixin);
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Motion, spring } from 'react-motion';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { clickNode, enterNode, leaveNode } from '../actions/app-actions';
|
||||
import { getNodeColor } from '../utils/color-utils';
|
||||
@@ -33,7 +35,18 @@ function getNodeShape({shape, stack}) {
|
||||
return stack ? stackedShape(nodeShape) : nodeShape;
|
||||
}
|
||||
|
||||
function ellipsis(text, fontSize, maxWidth) {
|
||||
const averageCharLength = fontSize / 1.5;
|
||||
const allowedChars = maxWidth / averageCharLength;
|
||||
let truncatedText = text;
|
||||
if (text && text.length > allowedChars) {
|
||||
truncatedText = `${text.slice(0, allowedChars)}...`;
|
||||
}
|
||||
return truncatedText;
|
||||
}
|
||||
|
||||
export default class Node extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.handleMouseClick = this.handleMouseClick.bind(this);
|
||||
@@ -42,95 +55,54 @@ export default class Node extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
const nodeScale = props.focused ? props.selectedNodeScale : props.nodeScale;
|
||||
const zoomScale = this.props.zoomScale;
|
||||
let scaleFactor = 1;
|
||||
if (props.focused) {
|
||||
scaleFactor = 1.25 / zoomScale;
|
||||
} else if (props.blurred) {
|
||||
scaleFactor = 0.75;
|
||||
}
|
||||
const { blurred, focused, highlighted, label, nodeScale, pseudo, rank,
|
||||
subLabel, scaleFactor, transform, zoomScale } = this.props;
|
||||
|
||||
const color = getNodeColor(rank, label, pseudo);
|
||||
const labelText = ellipsis(label, 14, nodeScale(4 * scaleFactor));
|
||||
const subLabelText = ellipsis(subLabel, 12, nodeScale(4 * scaleFactor));
|
||||
|
||||
let labelOffsetY = 18;
|
||||
let subLabelOffsetY = 35;
|
||||
const color = getNodeColor(this.props.rank, this.props.label,
|
||||
this.props.pseudo);
|
||||
const onMouseEnter = this.handleMouseEnter;
|
||||
const onMouseLeave = this.handleMouseLeave;
|
||||
const onMouseClick = this.handleMouseClick;
|
||||
const classNames = ['node'];
|
||||
const animConfig = [80, 20]; // stiffness, damping
|
||||
const label = this.ellipsis(props.label, 14, nodeScale(4 * scaleFactor));
|
||||
const subLabel = this.ellipsis(props.subLabel, 12, nodeScale(4 * scaleFactor));
|
||||
let labelFontSize = 14;
|
||||
let subLabelFontSize = 12;
|
||||
|
||||
if (props.focused) {
|
||||
// render focused nodes in normal size
|
||||
if (focused) {
|
||||
labelFontSize /= zoomScale;
|
||||
subLabelFontSize /= zoomScale;
|
||||
labelOffsetY /= zoomScale;
|
||||
subLabelOffsetY /= zoomScale;
|
||||
}
|
||||
if (this.props.highlighted) {
|
||||
classNames.push('highlighted');
|
||||
}
|
||||
if (this.props.blurred) {
|
||||
classNames.push('blurred');
|
||||
}
|
||||
if (this.props.pseudo) {
|
||||
classNames.push('pseudo');
|
||||
}
|
||||
|
||||
const classes = classNames.join(' ');
|
||||
const className = classNames({
|
||||
node: true,
|
||||
highlighted,
|
||||
blurred,
|
||||
pseudo
|
||||
});
|
||||
|
||||
const NodeShapeType = getNodeShape(this.props);
|
||||
|
||||
return (
|
||||
<Motion style={{
|
||||
x: spring(this.props.dx, animConfig),
|
||||
y: spring(this.props.dy, animConfig),
|
||||
f: spring(scaleFactor, animConfig),
|
||||
labelFontSize: spring(labelFontSize, animConfig),
|
||||
subLabelFontSize: spring(subLabelFontSize, animConfig),
|
||||
labelOffsetY: spring(labelOffsetY, animConfig),
|
||||
subLabelOffsetY: spring(subLabelOffsetY, animConfig)
|
||||
}}>
|
||||
{(interpolated) => {
|
||||
const transform = `translate(${interpolated.x},${interpolated.y})`;
|
||||
return (
|
||||
<g className={classes} transform={transform} id={props.id}
|
||||
onClick={onMouseClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||
<NodeShapeType
|
||||
size={nodeScale(interpolated.f)}
|
||||
color={color}
|
||||
{...props} />
|
||||
<text className="node-label" textAnchor="middle"
|
||||
style={{fontSize: interpolated.labelFontSize}}
|
||||
x="0" y={interpolated.labelOffsetY + nodeScale(0.5 * interpolated.f)}>
|
||||
{label}
|
||||
</text>
|
||||
<text className="node-sublabel" textAnchor="middle"
|
||||
style={{fontSize: interpolated.subLabelFontSize}}
|
||||
x="0" y={interpolated.subLabelOffsetY + nodeScale(0.5 * interpolated.f)}>
|
||||
{subLabel}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
}}
|
||||
</Motion>
|
||||
<g className={className} transform={transform} onClick={this.handleMouseClick}
|
||||
onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
||||
<NodeShapeType
|
||||
size={nodeScale(scaleFactor)}
|
||||
color={color}
|
||||
{...this.props} />
|
||||
<text className="node-label" textAnchor="middle" style={{fontSize: labelFontSize}}
|
||||
x="0" y={labelOffsetY + nodeScale(0.5 * scaleFactor)}>
|
||||
{labelText}
|
||||
</text>
|
||||
<text className="node-sublabel" textAnchor="middle" style={{fontSize: subLabelFontSize}}
|
||||
x="0" y={subLabelOffsetY + nodeScale(0.5 * scaleFactor)}>
|
||||
{subLabelText}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
ellipsis(text, fontSize, maxWidth) {
|
||||
const averageCharLength = fontSize / 1.5;
|
||||
const allowedChars = maxWidth / averageCharLength;
|
||||
let truncatedText = text;
|
||||
if (text && text.length > allowedChars) {
|
||||
truncatedText = `${text.slice(0, allowedChars)}...`;
|
||||
}
|
||||
return truncatedText;
|
||||
}
|
||||
|
||||
handleMouseClick(ev) {
|
||||
ev.stopPropagation();
|
||||
clickNode(this.props.id, this.props.label, ReactDOM.findDOMNode(this).getBoundingClientRect());
|
||||
@@ -144,3 +116,5 @@ export default class Node extends React.Component {
|
||||
leaveNode(this.props.id);
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(Node, PureRenderMixin);
|
||||
|
||||
24
client/app/scripts/charts/nodes-chart-edges.js
Normal file
24
client/app/scripts/charts/nodes-chart-edges.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
|
||||
import EdgeContainer from './edge-container';
|
||||
|
||||
export default class NodesChartEdges extends React.Component {
|
||||
render() {
|
||||
const {hasSelectedNode, highlightedEdgeIds, layoutEdges, layoutPrecision,
|
||||
selectedNodeId} = this.props;
|
||||
|
||||
return (
|
||||
<g className="nodes-chart-edges">
|
||||
{layoutEdges.toIndexedSeq().map(edge => <EdgeContainer key={edge.get('id')}
|
||||
id={edge.get('id')} source={edge.get('source')} target={edge.get('target')}
|
||||
points={edge.get('points')} layoutPrecision={layoutPrecision}
|
||||
highlightedEdgeIds={highlightedEdgeIds} hasSelectedNode={hasSelectedNode}
|
||||
selectedNodeId={selectedNodeId} />)}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(NodesChartEdges, PureRenderMixin);
|
||||
30
client/app/scripts/charts/nodes-chart-elements.js
Normal file
30
client/app/scripts/charts/nodes-chart-elements.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
|
||||
import NodesChartEdges from './nodes-chart-edges';
|
||||
import NodesChartNodes from './nodes-chart-nodes';
|
||||
|
||||
export default class NodesChartElements extends React.Component {
|
||||
render() {
|
||||
const props = this.props;
|
||||
return (
|
||||
<g className="nodes-chart-elements" transform={props.transform}>
|
||||
<NodesChartEdges layoutEdges={props.edges} selectedNodeId={props.selectedNodeId}
|
||||
highlightedEdgeIds={props.highlightedEdgeIds}
|
||||
hasSelectedNode={props.hasSelectedNode}
|
||||
layoutPrecision={props.layoutPrecision} />
|
||||
<NodesChartNodes layoutNodes={props.nodes} selectedNodeId={props.selectedNodeId}
|
||||
selectedMetric={props.selectedMetric}
|
||||
highlightedNodeIds={props.highlightedNodeIds}
|
||||
hasSelectedNode={props.hasSelectedNode}
|
||||
adjacentNodes={props.adjacentNodes}
|
||||
nodeScale={props.nodeScale} onNodeClick={props.onNodeClick}
|
||||
scale={props.scale} selectedNodeScale={props.selectedNodeScale}
|
||||
topologyId={props.topologyId} layoutPrecision={props.layoutPrecision} />
|
||||
</g>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(NodesChartElements, PureRenderMixin);
|
||||
76
client/app/scripts/charts/nodes-chart-nodes.js
Normal file
76
client/app/scripts/charts/nodes-chart-nodes.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
|
||||
import NodeContainer from './node-container';
|
||||
|
||||
export default class NodesChartNodes extends React.Component {
|
||||
render() {
|
||||
const {adjacentNodes, highlightedNodeIds,
|
||||
layoutNodes, layoutPrecision, nodeScale, onNodeClick, scale,
|
||||
selectedMetric, selectedNodeScale, selectedNodeId, topologyId} = this.props;
|
||||
|
||||
const zoomScale = scale;
|
||||
|
||||
// highlighter functions
|
||||
const setHighlighted = node => node.set('highlighted',
|
||||
highlightedNodeIds.has(node.get('id')) || selectedNodeId === node.get('id'));
|
||||
const setFocused = node => node.set('focused', selectedNodeId
|
||||
&& (selectedNodeId === node.get('id')
|
||||
|| (adjacentNodes && adjacentNodes.includes(node.get('id')))));
|
||||
const setBlurred = node => node.set('blurred', selectedNodeId && !node.get('focused'));
|
||||
|
||||
// make sure blurred nodes are in the background
|
||||
const sortNodes = node => {
|
||||
if (node.get('blurred')) {
|
||||
return 0;
|
||||
}
|
||||
if (node.get('highlighted')) {
|
||||
return 2;
|
||||
}
|
||||
return 1;
|
||||
};
|
||||
|
||||
// TODO: think about pulling this up into the store.
|
||||
const metric = node => (
|
||||
node.get('metrics') && node.get('metrics')
|
||||
.filter(m => m.get('id') === selectedMetric)
|
||||
.first()
|
||||
);
|
||||
|
||||
const nodesToRender = layoutNodes.toIndexedSeq()
|
||||
.map(setHighlighted)
|
||||
.map(setFocused)
|
||||
.map(setBlurred)
|
||||
.sortBy(sortNodes);
|
||||
|
||||
return (
|
||||
<g className="nodes-chart-nodes">
|
||||
{nodesToRender.map(node => <NodeContainer
|
||||
blurred={node.get('blurred')}
|
||||
focused={node.get('focused')}
|
||||
highlighted={node.get('highlighted')}
|
||||
topologyId={topologyId}
|
||||
shape={node.get('shape')}
|
||||
stack={node.get('stack')}
|
||||
onClick={onNodeClick}
|
||||
key={node.get('id')}
|
||||
id={node.get('id')}
|
||||
label={node.get('label')}
|
||||
pseudo={node.get('pseudo')}
|
||||
nodeCount={node.get('nodeCount')}
|
||||
subLabel={node.get('subLabel')}
|
||||
metric={metric(node)}
|
||||
rank={node.get('rank')}
|
||||
layoutPrecision={layoutPrecision}
|
||||
selectedNodeScale={selectedNodeScale}
|
||||
nodeScale={nodeScale}
|
||||
zoomScale={zoomScale}
|
||||
dx={node.get('x')}
|
||||
dy={node.get('y')} />)}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(NodesChartNodes, PureRenderMixin);
|
||||
@@ -2,18 +2,17 @@ import _ from 'lodash';
|
||||
import d3 from 'd3';
|
||||
import debug from 'debug';
|
||||
import React from 'react';
|
||||
import { Map as makeMap } from 'immutable';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
import { Map as makeMap, fromJS, is as isDeepEqual } from 'immutable';
|
||||
import timely from 'timely';
|
||||
|
||||
import { DETAILS_PANEL_WIDTH } from '../constants/styles';
|
||||
import { clickBackground } from '../actions/app-actions';
|
||||
import AppStore from '../stores/app-store';
|
||||
import Edge from './edge';
|
||||
import { EDGE_ID_SEPARATOR } from '../constants/naming';
|
||||
import { doLayout } from './nodes-layout';
|
||||
import Node from './node';
|
||||
import NodesError from './nodes-error';
|
||||
import { DETAILS_PANEL_WIDTH } from '../constants/styles';
|
||||
import Logo from '../components/logo';
|
||||
import { doLayout } from './nodes-layout';
|
||||
import NodesChartElements from './nodes-chart-elements';
|
||||
|
||||
const log = debug('scope:nodes-chart');
|
||||
|
||||
@@ -29,20 +28,22 @@ const radiusDensity = d3.scale.threshold()
|
||||
.domain([3, 6]).range([2.5, 3.5, 3]);
|
||||
|
||||
export default class NodesChart extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.handleMouseClick = this.handleMouseClick.bind(this);
|
||||
this.zoomed = this.zoomed.bind(this);
|
||||
|
||||
this.state = {
|
||||
nodes: makeMap(),
|
||||
edges: makeMap(),
|
||||
panTranslate: [0, 0],
|
||||
scale: 1,
|
||||
nodes: makeMap(),
|
||||
nodeScale: d3.scale.linear(),
|
||||
panTranslateX: 0,
|
||||
panTranslateY: 0,
|
||||
scale: 1,
|
||||
selectedNodeScale: d3.scale.linear(),
|
||||
hasZoomed: false,
|
||||
maxNodesExceeded: false
|
||||
hasZoomed: false
|
||||
};
|
||||
}
|
||||
|
||||
@@ -51,18 +52,6 @@ export default class NodesChart extends React.Component {
|
||||
this.setState(state);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// distinguish pan/zoom from click
|
||||
this.isZooming = false;
|
||||
|
||||
this.zoom = d3.behavior.zoom()
|
||||
.scaleExtent([0.1, 2])
|
||||
.on('zoom', this.zoomed);
|
||||
|
||||
d3.select('.nodes-chart svg')
|
||||
.call(this.zoom);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
// gather state, setState should be called only once here
|
||||
const state = _.assign({}, this.state);
|
||||
@@ -91,9 +80,20 @@ export default class NodesChart extends React.Component {
|
||||
this.setState(state);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// distinguish pan/zoom from click
|
||||
this.isZooming = false;
|
||||
|
||||
this.zoom = d3.behavior.zoom()
|
||||
.scaleExtent([0.1, 2])
|
||||
.on('zoom', this.zoomed);
|
||||
|
||||
d3.select('.nodes-chart svg')
|
||||
.call(this.zoom);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// undoing .call(zoom)
|
||||
|
||||
d3.select('.nodes-chart svg')
|
||||
.on('mousedown.zoom', null)
|
||||
.on('onwheel', null)
|
||||
@@ -102,170 +102,75 @@ export default class NodesChart extends React.Component {
|
||||
.on('touchstart.zoom', null);
|
||||
}
|
||||
|
||||
renderGraphNodes(nodes, nodeScale) {
|
||||
const hasSelectedNode = this.props.selectedNodeId
|
||||
&& this.props.nodes.has(this.props.selectedNodeId);
|
||||
const adjacency = hasSelectedNode ? AppStore.getAdjacentNodes(this.props.selectedNodeId) : null;
|
||||
const onNodeClick = this.props.onNodeClick;
|
||||
const zoomScale = this.state.scale;
|
||||
const selectedNodeScale = this.state.selectedNodeScale;
|
||||
|
||||
// highlighter functions
|
||||
const setHighlighted = node => {
|
||||
const highlighted = this.props.highlightedNodeIds.has(node.get('id'))
|
||||
|| this.props.selectedNodeId === node.get('id');
|
||||
return node.set('highlighted', highlighted);
|
||||
};
|
||||
const setFocused = node => {
|
||||
const focused = hasSelectedNode
|
||||
&& (this.props.selectedNodeId === node.get('id') || adjacency.includes(node.get('id')));
|
||||
return node.set('focused', focused);
|
||||
};
|
||||
const setBlurred = node => node.set('blurred', hasSelectedNode && !node.get('focused'));
|
||||
|
||||
// make sure blurred nodes are in the background
|
||||
const sortNodes = node => {
|
||||
if (node.get('blurred')) {
|
||||
return 0;
|
||||
}
|
||||
if (node.get('highlighted')) {
|
||||
return 2;
|
||||
}
|
||||
return 1;
|
||||
};
|
||||
|
||||
// TODO: think about pulling this up into the store.
|
||||
const metric = node => (
|
||||
node.get('metrics') && node.get('metrics')
|
||||
.filter(m => m.get('id') === this.props.selectedMetric)
|
||||
.first()
|
||||
);
|
||||
|
||||
return nodes
|
||||
.toIndexedSeq()
|
||||
.map(setHighlighted)
|
||||
.map(setFocused)
|
||||
.map(setBlurred)
|
||||
.sortBy(sortNodes)
|
||||
.map(node => <Node
|
||||
blurred={node.get('blurred')}
|
||||
focused={node.get('focused')}
|
||||
highlighted={node.get('highlighted')}
|
||||
topologyId={this.props.topologyId}
|
||||
shape={node.get('shape')}
|
||||
stack={node.get('stack')}
|
||||
onClick={onNodeClick}
|
||||
key={node.get('id')}
|
||||
id={node.get('id')}
|
||||
label={node.get('label')}
|
||||
pseudo={node.get('pseudo')}
|
||||
nodeCount={node.get('nodeCount')}
|
||||
subLabel={node.get('subLabel')}
|
||||
metric={metric(node)}
|
||||
rank={node.get('rank')}
|
||||
selectedNodeScale={selectedNodeScale}
|
||||
nodeScale={nodeScale}
|
||||
zoomScale={zoomScale}
|
||||
dx={node.get('x')}
|
||||
dy={node.get('y')}
|
||||
/>);
|
||||
}
|
||||
|
||||
renderGraphEdges(edges) {
|
||||
const selectedNodeId = this.props.selectedNodeId;
|
||||
const hasSelectedNode = selectedNodeId && this.props.nodes.has(selectedNodeId);
|
||||
|
||||
const setHighlighted = edge => edge.set('highlighted', this.props.highlightedEdgeIds.has(
|
||||
edge.get('id')));
|
||||
|
||||
const setBlurred = edge => edge.set('blurred', hasSelectedNode
|
||||
&& edge.get('source') !== selectedNodeId
|
||||
&& edge.get('target') !== selectedNodeId);
|
||||
|
||||
return edges
|
||||
.toIndexedSeq()
|
||||
.map(setHighlighted)
|
||||
.map(setBlurred)
|
||||
.map(edge => <Edge key={edge.get('id')} id={edge.get('id')}
|
||||
points={edge.get('points')}
|
||||
blurred={edge.get('blurred')} highlighted={edge.get('highlighted')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderMaxNodesError(show) {
|
||||
const errorHint = 'We\u0027re working on it, but for now, try a different view?';
|
||||
return (
|
||||
<NodesError faIconClass="fa-ban" hidden={!show}>
|
||||
<div className="centered">Too many nodes to show in the browser.<br />{errorHint}</div>
|
||||
</NodesError>
|
||||
);
|
||||
}
|
||||
|
||||
renderEmptyTopologyError(show) {
|
||||
return (
|
||||
<NodesError faIconClass="fa-circle-thin" hidden={!show}>
|
||||
<div className="heading">Nothing to show. This can have any of these reasons:</div>
|
||||
<ul>
|
||||
<li>We haven't received any reports from probes recently.
|
||||
Are the probes properly configured?</li>
|
||||
<li>There are nodes, but they're currently hidden. Check the view options
|
||||
in the bottom-left if they allow for showing hidden nodes.</li>
|
||||
<li>Containers view only: you're not running Docker,
|
||||
or you don't have any containers.</li>
|
||||
</ul>
|
||||
</NodesError>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const nodeElements = this.renderGraphNodes(this.state.nodes, this.state.nodeScale);
|
||||
const edgeElements = this.renderGraphEdges(this.state.edges, this.state.nodeScale);
|
||||
const scale = this.state.scale;
|
||||
const { edges, nodes, panTranslateX, panTranslateY, scale } = this.state;
|
||||
|
||||
const translate = this.state.panTranslate;
|
||||
// not passing translates into child components for perf reasons, use getTranslate instead
|
||||
const translate = [panTranslateX, panTranslateY];
|
||||
const transform = `translate(${translate}) scale(${scale})`;
|
||||
const svgClassNames = this.state.maxNodesExceeded || nodeElements.size === 0 ? 'hide' : '';
|
||||
const errorEmpty = this.renderEmptyTopologyError(AppStore.isTopologyEmpty());
|
||||
const errorMaxNodesExceeded = this.renderMaxNodesError(this.state.maxNodesExceeded);
|
||||
const svgClassNames = this.props.isEmpty ? 'hide' : '';
|
||||
|
||||
return (
|
||||
<div className="nodes-chart">
|
||||
{errorEmpty}
|
||||
{errorMaxNodesExceeded}
|
||||
<svg width="100%" height="100%" id="nodes-chart-canvas"
|
||||
className={svgClassNames} onClick={this.handleMouseClick}>
|
||||
<g transform="translate(24,24) scale(0.25)">
|
||||
<Logo />
|
||||
</g>
|
||||
<g className="canvas" transform={transform}>
|
||||
<g className="edges">
|
||||
{edgeElements}
|
||||
</g>
|
||||
<g className="nodes">
|
||||
{nodeElements}
|
||||
</g>
|
||||
</g>
|
||||
<NodesChartElements
|
||||
edges={edges}
|
||||
nodes={nodes}
|
||||
transform={transform}
|
||||
adjacentNodes={this.props.adjacentNodes}
|
||||
layoutPrecision={this.props.layoutPrecision}
|
||||
selectedMetric={this.props.selectedMetric}
|
||||
selectedNodeId={this.props.selectedNodeId}
|
||||
highlightedEdgeIds={this.props.highlightedEdgeIds}
|
||||
highlightedNodeIds={this.props.highlightedNodeIds}
|
||||
hasSelectedNode={this.props.hasSelectedNode}
|
||||
nodeScale={this.state.nodeScale}
|
||||
scale={this.state.scale}
|
||||
selectedNodeScale={this.state.selectedNodeScale}
|
||||
topologyId={this.props.topologyId} />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
initNodes(topology) {
|
||||
handleMouseClick() {
|
||||
if (!this.isZooming) {
|
||||
clickBackground();
|
||||
} else {
|
||||
this.isZooming = false;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
return topology.map((node, id) => makeMap({
|
||||
id,
|
||||
label: node.get('label'),
|
||||
pseudo: node.get('pseudo'),
|
||||
metrics: node.get('metrics'),
|
||||
subLabel: node.get('label_minor'),
|
||||
nodeCount: node.get('node_count'),
|
||||
rank: node.get('rank'),
|
||||
shape: node.get('shape'),
|
||||
stack: node.get('stack'),
|
||||
x: 0,
|
||||
y: 0
|
||||
}));
|
||||
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')
|
||||
}));
|
||||
});
|
||||
|
||||
return nextStateNodes;
|
||||
}
|
||||
|
||||
initEdges(topology, stateNodes) {
|
||||
@@ -309,10 +214,10 @@ export default class NodesChart extends React.Component {
|
||||
return {};
|
||||
}
|
||||
|
||||
const adjacency = AppStore.getAdjacentNodes(props.selectedNodeId);
|
||||
const adjacentNodes = props.adjacentNodes;
|
||||
const adjacentLayoutNodeIds = [];
|
||||
|
||||
adjacency.forEach(adjacentId => {
|
||||
adjacentNodes.forEach(adjacentId => {
|
||||
// filter loopback
|
||||
if (adjacentId !== props.selectedNodeId) {
|
||||
adjacentLayoutNodeIds.push(adjacentId);
|
||||
@@ -321,7 +226,7 @@ export default class NodesChart extends React.Component {
|
||||
|
||||
// move origin node to center of viewport
|
||||
const zoomScale = state.scale;
|
||||
const translate = state.panTranslate;
|
||||
const translate = [state.panTranslateX, state.panTranslateY];
|
||||
const centerX = (-translate[0] + (props.width + MARGINS.left
|
||||
- DETAILS_PANEL_WIDTH) / 2) / zoomScale;
|
||||
const centerY = (-translate[1] + (props.height + MARGINS.top) / 2) / zoomScale;
|
||||
@@ -356,10 +261,10 @@ export default class NodesChart extends React.Component {
|
||||
|| _.includes(adjacentLayoutNodeIds, edge.get('target'))) {
|
||||
const source = stateNodes.get(edge.get('source'));
|
||||
const target = stateNodes.get(edge.get('target'));
|
||||
return edge.set('points', [
|
||||
return edge.set('points', fromJS([
|
||||
{x: source.get('x'), y: source.get('y')},
|
||||
{x: target.get('x'), y: target.get('y')}
|
||||
]);
|
||||
]));
|
||||
}
|
||||
return edge;
|
||||
});
|
||||
@@ -374,18 +279,10 @@ export default class NodesChart extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
handleMouseClick() {
|
||||
if (!this.isZooming) {
|
||||
clickBackground();
|
||||
} else {
|
||||
this.isZooming = false;
|
||||
}
|
||||
}
|
||||
|
||||
restoreLayout(state) {
|
||||
// undo any pan/zooming that might have happened
|
||||
this.zoom.scale(state.scale);
|
||||
this.zoom.translate(state.panTranslate);
|
||||
this.zoom.translate([state.panTranslateX, state.panTranslateY]);
|
||||
|
||||
const nodes = state.nodes.map(node => node.merge({
|
||||
x: node.get('px'),
|
||||
@@ -399,7 +296,7 @@ export default class NodesChart extends React.Component {
|
||||
return edge;
|
||||
});
|
||||
|
||||
return { edges, nodes};
|
||||
return { edges, nodes };
|
||||
}
|
||||
|
||||
updateGraphState(props, state) {
|
||||
@@ -412,9 +309,13 @@ export default class NodesChart extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
let stateNodes = this.initNodes(props.nodes, state.nodes);
|
||||
let stateEdges = this.initEdges(props.nodes, stateNodes);
|
||||
const stateNodes = this.initNodes(props.nodes, state.nodes);
|
||||
const stateEdges = this.initEdges(props.nodes, stateNodes);
|
||||
const nodeMetrics = stateNodes.map(node => makeMap({
|
||||
metrics: node.get('metrics')
|
||||
}));
|
||||
const nodeScale = this.getNodeScale(props);
|
||||
const nextState = { nodeScale };
|
||||
|
||||
const options = {
|
||||
width: props.width,
|
||||
@@ -431,21 +332,15 @@ export default class NodesChart extends React.Component {
|
||||
|
||||
log(`graph layout took ${timedLayouter.time}ms`);
|
||||
|
||||
// layout was aborted
|
||||
if (!graph) {
|
||||
return {maxNodesExceeded: true};
|
||||
}
|
||||
stateNodes = graph.nodes.mergeDeep(stateNodes.map(node => makeMap({
|
||||
metrics: node.get('metrics')
|
||||
})));
|
||||
stateEdges = graph.edges;
|
||||
|
||||
// save coordinates for restore
|
||||
stateNodes = stateNodes.map(node => node.merge({
|
||||
px: node.get('x'),
|
||||
py: node.get('y')
|
||||
}));
|
||||
stateEdges = stateEdges.map(edge => edge.set('ppoints', edge.get('points')));
|
||||
// inject metrics and save coordinates for restore
|
||||
const layoutNodes = graph.nodes
|
||||
.mergeDeep(nodeMetrics)
|
||||
.map(node => node.merge({
|
||||
px: node.get('x'),
|
||||
py: node.get('y')
|
||||
}));
|
||||
const layoutEdges = graph.edges
|
||||
.map(edge => edge.set('ppoints', edge.get('points')));
|
||||
|
||||
// adjust layout based on viewport
|
||||
const xFactor = (props.width - MARGINS.left - MARGINS.right) / graph.width;
|
||||
@@ -453,19 +348,21 @@ export default class NodesChart extends React.Component {
|
||||
const zoomFactor = Math.min(xFactor, yFactor);
|
||||
let zoomScale = this.state.scale;
|
||||
|
||||
if (this.zoom && !this.state.hasZoomed && zoomFactor > 0 && zoomFactor < 1) {
|
||||
if (!this.props.hasZoomed && zoomFactor > 0 && zoomFactor < 1) {
|
||||
zoomScale = zoomFactor;
|
||||
// saving in d3's behavior cache
|
||||
this.zoom.scale(zoomFactor);
|
||||
}
|
||||
|
||||
return {
|
||||
nodes: stateNodes,
|
||||
edges: stateEdges,
|
||||
scale: zoomScale,
|
||||
nodeScale,
|
||||
maxNodesExceeded: false
|
||||
};
|
||||
nextState.scale = zoomScale;
|
||||
if (!isDeepEqual(stateNodes, state.nodes)) {
|
||||
nextState.nodes = layoutNodes;
|
||||
}
|
||||
if (!isDeepEqual(stateEdges, state.edges)) {
|
||||
nextState.edges = layoutEdges;
|
||||
}
|
||||
|
||||
return nextState;
|
||||
}
|
||||
|
||||
getNodeScale(props) {
|
||||
@@ -483,9 +380,12 @@ export default class NodesChart extends React.Component {
|
||||
if (!this.props.selectedNodeId) {
|
||||
this.setState({
|
||||
hasZoomed: true,
|
||||
panTranslate: d3.event.translate.slice(),
|
||||
panTranslateX: d3.event.translate[0],
|
||||
panTranslateY: d3.event.translate[1],
|
||||
scale: d3.event.scale
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(NodesChart, PureRenderMixin);
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import dagre from 'dagre';
|
||||
import debug from 'debug';
|
||||
import { Map as makeMap, Set as ImmSet } from 'immutable';
|
||||
import { fromJS, Map as makeMap, Set as ImmSet } from 'immutable';
|
||||
|
||||
import { EDGE_ID_SEPARATOR } from '../constants/naming';
|
||||
import { updateNodeDegrees } from '../utils/topology-utils';
|
||||
|
||||
const log = debug('scope:nodes-layout');
|
||||
|
||||
const MAX_NODES = 100;
|
||||
const topologyCaches = {};
|
||||
const DEFAULT_WIDTH = 800;
|
||||
const DEFAULT_MARGINS = {top: 0, left: 0};
|
||||
@@ -51,11 +50,6 @@ function runLayoutEngine(graph, imNodes, imEdges, opts) {
|
||||
let nodes = imNodes;
|
||||
let edges = imEdges;
|
||||
|
||||
if (nodes.size > MAX_NODES) {
|
||||
log(`Too many nodes for graph layout engine. Limit: ${MAX_NODES}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const options = opts || {};
|
||||
const scale = options.scale || DEFAULT_SCALE;
|
||||
const ranksep = scale(RANK_SEPARATION_FACTOR);
|
||||
@@ -122,13 +116,13 @@ function runLayoutEngine(graph, imNodes, imEdges, opts) {
|
||||
graph.edges().forEach(graphEdge => {
|
||||
const graphEdgeMeta = graph.edge(graphEdge);
|
||||
const edge = edges.get(graphEdgeMeta.id);
|
||||
const points = graphEdgeMeta.points;
|
||||
let points = fromJS(graphEdgeMeta.points);
|
||||
|
||||
// set beginning and end points to node coordinates to ignore node bounding box
|
||||
const source = nodes.get(fromGraphNodeId(edge.get('source')));
|
||||
const target = nodes.get(fromGraphNodeId(edge.get('target')));
|
||||
points[0] = {x: source.get('x'), y: source.get('y')};
|
||||
points[points.length - 1] = {x: target.get('x'), y: target.get('y')};
|
||||
points = points.mergeIn([0], {x: source.get('x'), y: source.get('y')});
|
||||
points = points.mergeIn([points.size - 1], {x: target.get('x'), y: target.get('y')});
|
||||
|
||||
edges = edges.setIn([graphEdgeMeta.id, 'points'], points);
|
||||
});
|
||||
@@ -251,13 +245,12 @@ function shiftLayoutToCenter(layout, opts) {
|
||||
y: node.get('y') + offsetY
|
||||
}));
|
||||
|
||||
result.edges = layout.edges.map(edge => {
|
||||
const points = edge.get('points').map(point => ({
|
||||
x: point.x + offsetX,
|
||||
y: point.y + offsetY
|
||||
}));
|
||||
return edge.set('points', points);
|
||||
});
|
||||
result.edges = layout.edges.map(edge => edge.update('points',
|
||||
points => points.map(point => point.merge({
|
||||
x: point.get('x') + offsetX,
|
||||
y: point.get('y') + offsetY
|
||||
}))
|
||||
));
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -271,10 +264,10 @@ function shiftLayoutToCenter(layout, opts) {
|
||||
function setSimpleEdgePoints(edge, nodeCache) {
|
||||
const source = nodeCache.get(edge.get('source'));
|
||||
const target = nodeCache.get(edge.get('target'));
|
||||
return edge.set('points', [
|
||||
return edge.set('points', fromJS([
|
||||
{x: source.get('x'), y: source.get('y')},
|
||||
{x: target.get('x'), y: target.get('y')}
|
||||
]);
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -300,14 +293,14 @@ export function hasUnseenNodes(nodes, cache) {
|
||||
*/
|
||||
function hasSameEndpoints(cachedEdge, nodes) {
|
||||
const oldPoints = cachedEdge.get('points');
|
||||
const oldSourcePoint = oldPoints[0];
|
||||
const oldTargetPoint = oldPoints[oldPoints.length - 1];
|
||||
const oldSourcePoint = oldPoints.first();
|
||||
const oldTargetPoint = oldPoints.last();
|
||||
const newSource = nodes.get(cachedEdge.get('source'));
|
||||
const newTarget = nodes.get(cachedEdge.get('target'));
|
||||
return (oldSourcePoint.x === newSource.get('x')
|
||||
&& oldSourcePoint.y === newSource.get('y')
|
||||
&& oldTargetPoint.x === newTarget.get('x')
|
||||
&& oldTargetPoint.y === newTarget.get('y'));
|
||||
return (oldSourcePoint.get('x') === newSource.get('x')
|
||||
&& oldSourcePoint.get('y') === newSource.get('y')
|
||||
&& oldTargetPoint.get('x') === newTarget.get('x')
|
||||
&& oldTargetPoint.get('y') === newTarget.get('y'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
import debug from 'debug';
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
|
||||
import Logo from './logo';
|
||||
import AppStore from '../stores/app-store';
|
||||
@@ -26,9 +28,11 @@ const RIGHT_ANGLE_KEY_IDENTIFIER = 'U+003C';
|
||||
const LEFT_ANGLE_KEY_IDENTIFIER = 'U+003E';
|
||||
const keyPressLog = debug('scope:app-key-press');
|
||||
|
||||
/* make sure these can all be shallow-checked for equality for PureRenderMixin */
|
||||
function getStateFromStores() {
|
||||
return {
|
||||
activeTopologyOptions: AppStore.getActiveTopologyOptions(),
|
||||
adjacentNodes: AppStore.getAdjacentNodes(AppStore.getSelectedNodeId()),
|
||||
controlStatus: AppStore.getControlStatus(),
|
||||
controlPipe: AppStore.getControlPipe(),
|
||||
currentTopology: AppStore.getCurrentTopology(),
|
||||
@@ -47,6 +51,7 @@ function getStateFromStores() {
|
||||
selectedMetric: AppStore.getSelectedMetric(),
|
||||
topologies: AppStore.getTopologies(),
|
||||
topologiesLoaded: AppStore.isTopologiesLoaded(),
|
||||
topologyEmpty: AppStore.isTopologyEmpty(),
|
||||
updatePaused: AppStore.isUpdatePaused(),
|
||||
updatePausedAt: AppStore.getUpdatePausedAt(),
|
||||
version: AppStore.getVersion(),
|
||||
@@ -136,6 +141,8 @@ export default class App extends React.Component {
|
||||
selectedMetric={this.state.selectedMetric}
|
||||
forceRelayout={this.state.forceRelayout}
|
||||
topologyOptions={this.state.activeTopologyOptions}
|
||||
topologyEmpty={this.state.topologyEmpty}
|
||||
adjacentNodes={this.state.adjacentNodes}
|
||||
topologyId={this.state.currentTopologyId} />
|
||||
|
||||
<Sidebar>
|
||||
@@ -157,3 +164,5 @@ export default class App extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(App, PureRenderMixin);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint react/jsx-no-bind: "off" */
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import Perf from 'react-addons-perf';
|
||||
|
||||
import debug from 'debug';
|
||||
const log = debug('scope:debug-panel');
|
||||
@@ -31,7 +32,7 @@ const LABEL_PREFIXES = _.range('A'.charCodeAt(), 'Z'.charCodeAt() + 1)
|
||||
.map(n => String.fromCharCode(n));
|
||||
|
||||
|
||||
const randomLetter = () => _.sample(LABEL_PREFIXES);
|
||||
// const randomLetter = () => _.sample(LABEL_PREFIXES);
|
||||
|
||||
|
||||
const deltaAdd = (name, adjacency = [], shape = 'circle', stack = false, nodeCount = 1) => ({
|
||||
@@ -91,11 +92,25 @@ function addAllMetricVariants() {
|
||||
}
|
||||
|
||||
|
||||
function stopPerf() {
|
||||
Perf.stop();
|
||||
const measurements = Perf.getLastMeasurements();
|
||||
Perf.printInclusive(measurements);
|
||||
Perf.printWasted(measurements);
|
||||
}
|
||||
|
||||
function startPerf(delay) {
|
||||
Perf.start();
|
||||
setTimeout(stopPerf, delay * 1000);
|
||||
}
|
||||
|
||||
|
||||
function addNodes(n) {
|
||||
const ns = AppStore.getNodes();
|
||||
const nodeNames = ns.keySeq().toJS();
|
||||
const newNodeNames = _.range(ns.size, ns.size + n).map(() => (
|
||||
`${randomLetter()}${randomLetter()}-zing`
|
||||
const newNodeNames = _.range(ns.size, ns.size + n).map(i => (
|
||||
// `${randomLetter()}${randomLetter()}-zing`
|
||||
`zing${i}`
|
||||
));
|
||||
const allNodes = _(nodeNames).concat(newNodeNames).value();
|
||||
|
||||
@@ -110,9 +125,9 @@ function addNodes(n) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function showingDebugToolbar() {
|
||||
return 'debugToolbar' in localStorage && JSON.parse(localStorage.debugToolbar);
|
||||
return (('debugToolbar' in localStorage && JSON.parse(localStorage.debugToolbar))
|
||||
|| location.pathname.indexOf('debug') > -1);
|
||||
}
|
||||
|
||||
|
||||
@@ -196,6 +211,13 @@ export class DebugToolbar extends React.Component {
|
||||
</tbody>
|
||||
</table>
|
||||
))}
|
||||
|
||||
<div>
|
||||
<label>Measure React perf for </label>
|
||||
<button onClick={() => startPerf(2)}>2s</button>
|
||||
<button onClick={() => startPerf(5)}>5s</button>
|
||||
<button onClick={() => startPerf(10)}>10s</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
|
||||
import NodeDetails from './node-details';
|
||||
import { DETAILS_PANEL_WIDTH as WIDTH, DETAILS_PANEL_OFFSET as OFFSET,
|
||||
@@ -51,3 +53,5 @@ export default class DetailsCard extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(DetailsCard, PureRenderMixin);
|
||||
|
||||
@@ -8,7 +8,7 @@ export default function Details({controlStatus, details, nodes}) {
|
||||
<div className="details">
|
||||
{details.toIndexedSeq().map((obj, index) => <DetailsCard key={obj.id}
|
||||
index={index} cardCount={details.size} nodes={nodes}
|
||||
nodeControlStatus={controlStatus[obj.id]} {...obj} />
|
||||
nodeControlStatus={controlStatus.get(obj.id)} {...obj} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -135,7 +135,8 @@ export default class NodeDetails extends React.Component {
|
||||
const details = this.props.details;
|
||||
const showControls = details.controls && details.controls.length > 0;
|
||||
const nodeColor = getNodeColorDark(details.rank, details.label, details.pseudo);
|
||||
const {error, pending} = (this.props.nodeControlStatus || {});
|
||||
const {error, pending} = this.props.nodeControlStatus
|
||||
? this.props.nodeControlStatus.toJS() : {};
|
||||
const tools = this.renderTools();
|
||||
const styles = {
|
||||
controls: {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
|
||||
import { clickRelative } from '../../actions/app-actions';
|
||||
|
||||
@@ -25,3 +27,5 @@ export default class NodeDetailsRelativesLink extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(NodeDetailsRelativesLink, PureRenderMixin);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
|
||||
import { clickRelative } from '../../actions/app-actions';
|
||||
|
||||
@@ -32,3 +34,5 @@ export default class NodeDetailsTableNodeLink extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(NodeDetailsTableNodeLink, PureRenderMixin);
|
||||
|
||||
@@ -1,10 +1,31 @@
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
|
||||
import NodesChart from '../charts/nodes-chart';
|
||||
import NodesError from '../charts/nodes-error';
|
||||
|
||||
const navbarHeight = 160;
|
||||
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;
|
||||
}
|
||||
|
||||
export default class Nodes extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
@@ -24,8 +45,37 @@ export default class Nodes extends React.Component {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
}
|
||||
|
||||
renderEmptyTopologyError(show) {
|
||||
return (
|
||||
<NodesError faIconClass="fa-circle-thin" hidden={!show}>
|
||||
<div className="heading">Nothing to show. This can have any of these reasons:</div>
|
||||
<ul>
|
||||
<li>We haven't received any reports from probes recently.
|
||||
Are the probes properly configured?</li>
|
||||
<li>There are nodes, but they're currently hidden. Check the view options
|
||||
in the bottom-left if they allow for showing hidden nodes.</li>
|
||||
<li>Containers view only: you're not running Docker,
|
||||
or you don't have any containers.</li>
|
||||
</ul>
|
||||
</NodesError>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <NodesChart {...this.props} {...this.state} />;
|
||||
const { nodes, selectedNodeId, topologyEmpty } = this.props;
|
||||
const layoutPrecision = getLayoutPrecision(nodes.size);
|
||||
const hasSelectedNode = selectedNodeId && nodes.has(selectedNodeId);
|
||||
const errorEmpty = this.renderEmptyTopologyError(topologyEmpty);
|
||||
|
||||
return (
|
||||
<div className="nodes-wrapper">
|
||||
{topologyEmpty && errorEmpty}
|
||||
<NodesChart {...this.props} {...this.state}
|
||||
layoutPrecision={layoutPrecision}
|
||||
hasSelectedNode={hasSelectedNode}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
@@ -39,3 +89,5 @@ export default class Nodes extends React.Component {
|
||||
this.setState({height, width});
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(Nodes, PureRenderMixin);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
|
||||
export default class ShowMore extends React.Component {
|
||||
|
||||
@@ -28,3 +30,5 @@ export default class ShowMore extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(ShowMore, PureRenderMixin);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
|
||||
import { clickTopology } from '../actions/app-actions';
|
||||
|
||||
@@ -69,3 +71,5 @@ export default class Topologies extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(Topologies, PureRenderMixin);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
|
||||
import { changeTopologyOption } from '../actions/app-actions';
|
||||
|
||||
@@ -26,3 +28,5 @@ export default class TopologyOptionAction extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(TopologyOptionAction, PureRenderMixin);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
|
||||
import TopologyOptionAction from './topology-option-action';
|
||||
|
||||
@@ -29,3 +31,5 @@ export default class TopologyOptions extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(TopologyOptions, PureRenderMixin);
|
||||
|
||||
@@ -142,9 +142,10 @@ export class AppStore extends Store {
|
||||
|
||||
// keep at the top
|
||||
getAppState() {
|
||||
const cp = this.getControlPipe();
|
||||
return {
|
||||
controlPipe: this.getControlPipe(),
|
||||
nodeDetails: this.getNodeDetailsState(),
|
||||
controlPipe: cp ? cp.toJS() : null,
|
||||
nodeDetails: this.getNodeDetailsState().toJS(),
|
||||
selectedNodeId,
|
||||
pinnedMetricType,
|
||||
topologyId: currentTopologyId,
|
||||
@@ -190,12 +191,11 @@ export class AppStore extends Store {
|
||||
}
|
||||
|
||||
getControlStatus() {
|
||||
return controlStatus.toJS();
|
||||
return controlStatus;
|
||||
}
|
||||
|
||||
getControlPipe() {
|
||||
const cp = controlPipes.last();
|
||||
return cp && cp.toJS();
|
||||
return controlPipes.last();
|
||||
}
|
||||
|
||||
getCurrentTopology() {
|
||||
@@ -240,7 +240,7 @@ export class AppStore extends Store {
|
||||
getNodeDetailsState() {
|
||||
return nodeDetails.toIndexedSeq().map(details => ({
|
||||
id: details.id, label: details.label, topologyId: details.topologyId
|
||||
})).toJS();
|
||||
}));
|
||||
}
|
||||
|
||||
getTopCardNodeId() {
|
||||
|
||||
@@ -333,7 +333,7 @@ h2 {
|
||||
}
|
||||
}
|
||||
|
||||
.nodes > .node {
|
||||
.nodes-chart-nodes > .node {
|
||||
cursor: pointer;
|
||||
transition: opacity .5s @base-ease;
|
||||
|
||||
@@ -370,6 +370,10 @@ h2 {
|
||||
opacity: @edge-opacity-blurred;
|
||||
}
|
||||
|
||||
&.focused {
|
||||
animation: focusing 1.5s ease-in-out;
|
||||
}
|
||||
|
||||
.link {
|
||||
stroke: @text-secondary-color;
|
||||
stroke-width: @edge-link-stroke-width;
|
||||
@@ -1007,7 +1011,7 @@ h2 {
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
.fa {
|
||||
margin-left: 4px;
|
||||
color: darkred;
|
||||
@@ -1058,6 +1062,16 @@ h2 {
|
||||
font-size: .7rem;
|
||||
}
|
||||
|
||||
@keyframes focusing {
|
||||
0% {
|
||||
opacity: 0;
|
||||
} 33% {
|
||||
opacity: 0.2;
|
||||
} 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blinking {
|
||||
0%, 100% {
|
||||
opacity: 1.0;
|
||||
|
||||
20
client/build/debug.html
Normal file
20
client/build/debug.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<!doctype html>
|
||||
<html class="no-js">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Weave Scope</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body>
|
||||
<!--[if lt IE 10]>
|
||||
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
|
||||
<![endif]-->
|
||||
<div class="wrap">
|
||||
<div id="app"></div>
|
||||
</div>
|
||||
|
||||
<script src="vendors.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -25,6 +25,7 @@
|
||||
"react-addons-update": "^0.14.7",
|
||||
"react-dom": "^0.14.7",
|
||||
"react-motion": "0.3.1",
|
||||
"react-mixin": "^3.0.3",
|
||||
"reqwest": "~2.0.5",
|
||||
"timely": "0.1.0"
|
||||
},
|
||||
@@ -50,14 +51,17 @@
|
||||
"less": "~2.6.1",
|
||||
"less-loader": "2.2.2",
|
||||
"postcss-loader": "0.8.2",
|
||||
"react-addons-perf": "^0.14.0",
|
||||
"style-loader": "0.13.0",
|
||||
"url": "0.11.0",
|
||||
"url-loader": "0.5.7",
|
||||
"webpack": "~1.12.4"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"browser-perf": "^1.4.5",
|
||||
"express": "~4.13.3",
|
||||
"http-proxy": "^1.12.0",
|
||||
"perfjankie": "^2.1.0",
|
||||
"react-hot-loader": "~1.3.0",
|
||||
"webpack-dev-server": "~1.14.1"
|
||||
},
|
||||
|
||||
121
client/test/actions/90-nodes-select.js
Normal file
121
client/test/actions/90-nodes-select.js
Normal file
@@ -0,0 +1,121 @@
|
||||
/* eslint-disable */
|
||||
|
||||
var fs = require('fs');
|
||||
var debug = require('debug')('scope:test:action:90-nodes-select');
|
||||
|
||||
function clickIfVisible(list, index) {
|
||||
var el = list[index++];
|
||||
el.isDisplayed(function(err, visible) {
|
||||
if (err) {
|
||||
debug(err);
|
||||
} else if (visible) {
|
||||
el.click();
|
||||
} else {
|
||||
if (index < list.length) {
|
||||
clickIfVisible(list, index);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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"}}}';
|
||||
|
||||
// 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')
|
||||
.then(function() {
|
||||
debug('starting run ' + cfg.run);
|
||||
return browser.sleep(2000);
|
||||
})
|
||||
.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();
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(function() {
|
||||
return browser.sleep(2000);
|
||||
})
|
||||
|
||||
.then(function() {
|
||||
return browser.sleep(2000);
|
||||
})
|
||||
.then(function() {
|
||||
debug('select node');
|
||||
return browser.get(selectedUrl);
|
||||
})
|
||||
.then(function() {
|
||||
return browser.sleep(5000);
|
||||
})
|
||||
.then(function() {
|
||||
debug('deselect node');
|
||||
return browser.elementByCssSelector('.fa-close', function(err, el) {
|
||||
return el.click();
|
||||
});
|
||||
})
|
||||
|
||||
.then(function() {
|
||||
return browser.sleep(2000);
|
||||
})
|
||||
.then(function() {
|
||||
debug('select node');
|
||||
return browser.get(selectedUrl);
|
||||
})
|
||||
.then(function() {
|
||||
return browser.sleep(5000);
|
||||
})
|
||||
.then(function() {
|
||||
debug('deselect node');
|
||||
return browser.elementByCssSelector('.fa-close', function(err, el) {
|
||||
return el.click();
|
||||
});
|
||||
})
|
||||
|
||||
.then(function() {
|
||||
return browser.sleep(2000);
|
||||
})
|
||||
.then(function() {
|
||||
debug('select node');
|
||||
return browser.get(selectedUrl);
|
||||
})
|
||||
.then(function() {
|
||||
return browser.sleep(5000);
|
||||
})
|
||||
.then(function() {
|
||||
debug('deselect node');
|
||||
return browser.elementByCssSelector('.fa-close', function(err, el) {
|
||||
return el.click();
|
||||
});
|
||||
})
|
||||
|
||||
.then(function() {
|
||||
return browser.sleep(2000, function() {
|
||||
debug('scenario done');
|
||||
});
|
||||
})
|
||||
.fail(function(err) {
|
||||
debug('exception. taking screenshot', err);
|
||||
browser.takeScreenshot(function(err, data) {
|
||||
if (err) {
|
||||
debug(err);
|
||||
} else {
|
||||
var base64Data = data.replace(/^data:image\/png;base64,/,"");
|
||||
fs.writeFile('90-nodes-select-' + cfg.run + '.png', base64Data, 'base64', function(err) {
|
||||
if(err) debug(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
9
client/test/browser-perf/main.js
Normal file
9
client/test/browser-perf/main.js
Normal file
@@ -0,0 +1,9 @@
|
||||
var browserPerf = require('browser-perf');
|
||||
var options = {
|
||||
selenium: 'http://local.docker:4444/wd/hub',
|
||||
actions: [require('./custom-action.js')()]
|
||||
}
|
||||
browserPerf('http://local.docker:4040/debug.html', function(err, res){
|
||||
console.error(err);
|
||||
console.log(res);
|
||||
}, options);
|
||||
55
client/test/perfjankie/main.js
Normal file
55
client/test/perfjankie/main.js
Normal file
@@ -0,0 +1,55 @@
|
||||
var perfjankie = require('perfjankie');
|
||||
|
||||
var run = process.env.COMMIT || 'commit#Hash'; // A hash for the commit, displayed in the x-axis in the dashboard
|
||||
var time = process.env.DATE || new Date().getTime() // Used to sort the data when displaying graph. Can be the time when a commit was made
|
||||
var scenario = process.env.ACTIONS || '90-nodes-select';
|
||||
var host = process.env.HOST || 'localhost:4040';
|
||||
var actions = require('../actions/' + scenario)({host: host, run: run});
|
||||
|
||||
perfjankie({
|
||||
/* The next set of values identify the test */
|
||||
suite: 'Scope',
|
||||
name: scenario, // A friendly name for the URL. This is shown as component name in the dashboard
|
||||
time: time,
|
||||
run: run,
|
||||
repeat: 10, // Run the tests 10 times. Default is 1 time
|
||||
|
||||
/* Identifies where the data and the dashboard are saved */
|
||||
couch: {
|
||||
server: 'http://local.docker:5984',
|
||||
database: 'performance'
|
||||
// updateSite: !process.env.CI, // If true, updates the couchApp that shows the dashboard. Set to false in when running Continuous integration, run this the first time using command line.
|
||||
// onlyUpdateSite: false // No data to upload, just update the site. Recommended to do from dev box as couchDB instance may require special access to create views.
|
||||
},
|
||||
|
||||
callback: function(err, res) {
|
||||
if (err)
|
||||
console.log(err);
|
||||
// The callback function, err is falsy if all of the following happen
|
||||
// 1. Browsers perf tests ran
|
||||
// 2. Data has been saved in couchDB
|
||||
// err is not falsy even if update site fails.
|
||||
},
|
||||
|
||||
/* OPTIONS PASSED TO BROWSER-PERF */
|
||||
// Properties identifying the test environment */
|
||||
browsers: [{ // This can also be a ['chrome', 'firefox'] or 'chrome,firefox'
|
||||
browserName: 'chrome',
|
||||
chromeOptions: {
|
||||
perfLoggingPrefs: {
|
||||
'traceCategories': 'toplevel,disabled-by-default-devtools.timeline.frame,blink.console,disabled-by-default-devtools.timeline,benchmark'
|
||||
},
|
||||
args: ['--enable-gpu-benchmarking', '--enable-thread-composting']
|
||||
},
|
||||
loggingPrefs: {
|
||||
performance: 'ALL'
|
||||
}
|
||||
}], // See browser perf browser configuration for all options.
|
||||
actions: actions,
|
||||
|
||||
selenium: {
|
||||
hostname: 'local.docker', // or localhost or hub.browserstack.com
|
||||
port: 4444,
|
||||
}
|
||||
|
||||
});
|
||||
34
client/test/run-jankie.sh
Executable file
34
client/test/run-jankie.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
# run jankie on one commit
|
||||
|
||||
# These need to be running:
|
||||
#
|
||||
# docker run --net=host -d -p 4444:4444 -v /dev/shm:/dev/shm selenium/standalone-chrome:2.52.0
|
||||
# docker run -d -p 5984:5984 --name couchdb klaemo/couchdb
|
||||
#
|
||||
# Initialize the results DB
|
||||
#
|
||||
# perfjankie --only-update-site --couch-server http://local.docker:5984 --couch-database performance
|
||||
#
|
||||
# Usage:
|
||||
#
|
||||
# ./run-jankie.sh 192.168.64.3:4040
|
||||
#
|
||||
# View results: http://local.docker:5984/performance/_design/site/index.html
|
||||
#
|
||||
|
||||
set -x
|
||||
set -e
|
||||
|
||||
HOST="$1"
|
||||
DATE=$(git log --format="%at" -1)
|
||||
COMMIT=$(git log --format="%h" -1)
|
||||
|
||||
echo "Testing $COMMIT on $DATE"
|
||||
|
||||
../../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