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')
|
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
|
CODECGEN_TARGETS=report/report.codecgen.go render/render.codecgen.go render/detailed/detailed.codecgen.go
|
||||||
RM=--rm
|
RM=--rm
|
||||||
RUN_FLAGS=-ti
|
RUN_FLAGS=-i
|
||||||
BUILD_IN_CONTAINER=true
|
BUILD_IN_CONTAINER=true
|
||||||
GO_ENV=GOGC=off
|
GO_ENV=GOGC=off
|
||||||
GO=env $(GO_ENV) go
|
GO=env $(GO_ENV) go
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
app/scripts/vendor/term.js
|
app/scripts/vendor/term.js
|
||||||
|
test/
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"comma-dangle": 0,
|
"comma-dangle": 0,
|
||||||
"object-curly-spacing": 0,
|
"object-curly-spacing": 0,
|
||||||
"react/jsx-closing-bracket-location": 0,
|
"react/jsx-closing-bracket-location": 0,
|
||||||
|
"react/prefer-stateless-function": 0,
|
||||||
"react/sort-comp": 0,
|
"react/sort-comp": 0,
|
||||||
"react/prop-types": 0
|
"react/prop-types": 0
|
||||||
}
|
}
|
||||||
|
|||||||
1
client/.gitignore
vendored
1
client/.gitignore
vendored
@@ -2,3 +2,4 @@ node_modules
|
|||||||
build/app.js
|
build/app.js
|
||||||
build/*[woff2?|ttf|eot|svg]
|
build/*[woff2?|ttf|eot|svg]
|
||||||
coverage/
|
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 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';
|
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 {
|
export default class Edge extends React.Component {
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.handleMouseEnter = this.handleMouseEnter.bind(this);
|
this.handleMouseEnter = this.handleMouseEnter.bind(this);
|
||||||
this.handleMouseLeave = this.handleMouseLeave.bind(this);
|
this.handleMouseLeave = this.handleMouseLeave.bind(this);
|
||||||
|
|
||||||
this.state = {
|
|
||||||
points: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillMount() {
|
|
||||||
this.ensureSameLength(this.props.points);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
this.ensureSameLength(nextProps.points);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const classNames = ['edge'];
|
const { hasSelectedNode, highlightedEdgeIds, id, layoutPrecision,
|
||||||
const points = flattenPoints(this.props.points);
|
path, selectedNodeId, source, target } = this.props;
|
||||||
const props = this.props;
|
|
||||||
const handleMouseEnter = this.handleMouseEnter;
|
|
||||||
const handleMouseLeave = this.handleMouseLeave;
|
|
||||||
|
|
||||||
if (this.props.highlighted) {
|
const classNames = ['edge'];
|
||||||
|
if (highlightedEdgeIds.has(id)) {
|
||||||
classNames.push('highlighted');
|
classNames.push('highlighted');
|
||||||
}
|
}
|
||||||
if (this.props.blurred) {
|
if (hasSelectedNode
|
||||||
|
&& source !== selectedNodeId
|
||||||
|
&& target !== selectedNodeId) {
|
||||||
classNames.push('blurred');
|
classNames.push('blurred');
|
||||||
}
|
}
|
||||||
|
if (hasSelectedNode && layoutPrecision === 0
|
||||||
|
&& (source === selectedNodeId || target === selectedNodeId)) {
|
||||||
|
classNames.push('focused');
|
||||||
|
}
|
||||||
const classes = classNames.join(' ');
|
const classes = classNames.join(' ');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Motion style={points}>
|
<g className={classes} onMouseEnter={this.handleMouseEnter}
|
||||||
{(interpolated) => {
|
onMouseLeave={this.handleMouseLeave} id={id}>
|
||||||
const path = line(extractPoints(interpolated));
|
<path d={path} className="shadow" />
|
||||||
return (
|
<path d={path} className="link" />
|
||||||
<g className={classes} onMouseEnter={handleMouseEnter}
|
</g>
|
||||||
onMouseLeave={handleMouseLeave} id={props.id}>
|
|
||||||
<path d={path} className="shadow" />
|
|
||||||
<path d={path} className="link" />
|
|
||||||
</g>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Motion>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
handleMouseEnter(ev) {
|
||||||
enterEdge(ev.currentTarget.id);
|
enterEdge(ev.currentTarget.id);
|
||||||
}
|
}
|
||||||
@@ -105,3 +48,5 @@ export default class Edge extends React.Component {
|
|||||||
leaveEdge(ev.currentTarget.id);
|
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 React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
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 { clickNode, enterNode, leaveNode } from '../actions/app-actions';
|
||||||
import { getNodeColor } from '../utils/color-utils';
|
import { getNodeColor } from '../utils/color-utils';
|
||||||
@@ -33,7 +35,18 @@ function getNodeShape({shape, stack}) {
|
|||||||
return stack ? stackedShape(nodeShape) : nodeShape;
|
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 {
|
export default class Node extends React.Component {
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.handleMouseClick = this.handleMouseClick.bind(this);
|
this.handleMouseClick = this.handleMouseClick.bind(this);
|
||||||
@@ -42,95 +55,54 @@ export default class Node extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const props = this.props;
|
const { blurred, focused, highlighted, label, nodeScale, pseudo, rank,
|
||||||
const nodeScale = props.focused ? props.selectedNodeScale : props.nodeScale;
|
subLabel, scaleFactor, transform, zoomScale } = this.props;
|
||||||
const zoomScale = this.props.zoomScale;
|
|
||||||
let scaleFactor = 1;
|
const color = getNodeColor(rank, label, pseudo);
|
||||||
if (props.focused) {
|
const labelText = ellipsis(label, 14, nodeScale(4 * scaleFactor));
|
||||||
scaleFactor = 1.25 / zoomScale;
|
const subLabelText = ellipsis(subLabel, 12, nodeScale(4 * scaleFactor));
|
||||||
} else if (props.blurred) {
|
|
||||||
scaleFactor = 0.75;
|
|
||||||
}
|
|
||||||
let labelOffsetY = 18;
|
let labelOffsetY = 18;
|
||||||
let subLabelOffsetY = 35;
|
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 labelFontSize = 14;
|
||||||
let subLabelFontSize = 12;
|
let subLabelFontSize = 12;
|
||||||
|
|
||||||
if (props.focused) {
|
// render focused nodes in normal size
|
||||||
|
if (focused) {
|
||||||
labelFontSize /= zoomScale;
|
labelFontSize /= zoomScale;
|
||||||
subLabelFontSize /= zoomScale;
|
subLabelFontSize /= zoomScale;
|
||||||
labelOffsetY /= zoomScale;
|
labelOffsetY /= zoomScale;
|
||||||
subLabelOffsetY /= 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);
|
const NodeShapeType = getNodeShape(this.props);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Motion style={{
|
<g className={className} transform={transform} onClick={this.handleMouseClick}
|
||||||
x: spring(this.props.dx, animConfig),
|
onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
||||||
y: spring(this.props.dy, animConfig),
|
<NodeShapeType
|
||||||
f: spring(scaleFactor, animConfig),
|
size={nodeScale(scaleFactor)}
|
||||||
labelFontSize: spring(labelFontSize, animConfig),
|
color={color}
|
||||||
subLabelFontSize: spring(subLabelFontSize, animConfig),
|
{...this.props} />
|
||||||
labelOffsetY: spring(labelOffsetY, animConfig),
|
<text className="node-label" textAnchor="middle" style={{fontSize: labelFontSize}}
|
||||||
subLabelOffsetY: spring(subLabelOffsetY, animConfig)
|
x="0" y={labelOffsetY + nodeScale(0.5 * scaleFactor)}>
|
||||||
}}>
|
{labelText}
|
||||||
{(interpolated) => {
|
</text>
|
||||||
const transform = `translate(${interpolated.x},${interpolated.y})`;
|
<text className="node-sublabel" textAnchor="middle" style={{fontSize: subLabelFontSize}}
|
||||||
return (
|
x="0" y={subLabelOffsetY + nodeScale(0.5 * scaleFactor)}>
|
||||||
<g className={classes} transform={transform} id={props.id}
|
{subLabelText}
|
||||||
onClick={onMouseClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
</text>
|
||||||
<NodeShapeType
|
</g>
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
handleMouseClick(ev) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
clickNode(this.props.id, this.props.label, ReactDOM.findDOMNode(this).getBoundingClientRect());
|
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);
|
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 d3 from 'd3';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import React from 'react';
|
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 timely from 'timely';
|
||||||
|
|
||||||
import { DETAILS_PANEL_WIDTH } from '../constants/styles';
|
|
||||||
import { clickBackground } from '../actions/app-actions';
|
import { clickBackground } from '../actions/app-actions';
|
||||||
import AppStore from '../stores/app-store';
|
|
||||||
import Edge from './edge';
|
|
||||||
import { EDGE_ID_SEPARATOR } from '../constants/naming';
|
import { EDGE_ID_SEPARATOR } from '../constants/naming';
|
||||||
import { doLayout } from './nodes-layout';
|
import { DETAILS_PANEL_WIDTH } from '../constants/styles';
|
||||||
import Node from './node';
|
|
||||||
import NodesError from './nodes-error';
|
|
||||||
import Logo from '../components/logo';
|
import Logo from '../components/logo';
|
||||||
|
import { doLayout } from './nodes-layout';
|
||||||
|
import NodesChartElements from './nodes-chart-elements';
|
||||||
|
|
||||||
const log = debug('scope:nodes-chart');
|
const log = debug('scope:nodes-chart');
|
||||||
|
|
||||||
@@ -29,20 +28,22 @@ const radiusDensity = d3.scale.threshold()
|
|||||||
.domain([3, 6]).range([2.5, 3.5, 3]);
|
.domain([3, 6]).range([2.5, 3.5, 3]);
|
||||||
|
|
||||||
export default class NodesChart extends React.Component {
|
export default class NodesChart extends React.Component {
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.handleMouseClick = this.handleMouseClick.bind(this);
|
this.handleMouseClick = this.handleMouseClick.bind(this);
|
||||||
this.zoomed = this.zoomed.bind(this);
|
this.zoomed = this.zoomed.bind(this);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
nodes: makeMap(),
|
|
||||||
edges: makeMap(),
|
edges: makeMap(),
|
||||||
panTranslate: [0, 0],
|
nodes: makeMap(),
|
||||||
scale: 1,
|
|
||||||
nodeScale: d3.scale.linear(),
|
nodeScale: d3.scale.linear(),
|
||||||
|
panTranslateX: 0,
|
||||||
|
panTranslateY: 0,
|
||||||
|
scale: 1,
|
||||||
selectedNodeScale: d3.scale.linear(),
|
selectedNodeScale: d3.scale.linear(),
|
||||||
hasZoomed: false,
|
hasZoomed: false
|
||||||
maxNodesExceeded: false
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,18 +52,6 @@ export default class NodesChart extends React.Component {
|
|||||||
this.setState(state);
|
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) {
|
componentWillReceiveProps(nextProps) {
|
||||||
// gather state, setState should be called only once here
|
// gather state, setState should be called only once here
|
||||||
const state = _.assign({}, this.state);
|
const state = _.assign({}, this.state);
|
||||||
@@ -91,9 +80,20 @@ export default class NodesChart extends React.Component {
|
|||||||
this.setState(state);
|
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() {
|
componentWillUnmount() {
|
||||||
// undoing .call(zoom)
|
// undoing .call(zoom)
|
||||||
|
|
||||||
d3.select('.nodes-chart svg')
|
d3.select('.nodes-chart svg')
|
||||||
.on('mousedown.zoom', null)
|
.on('mousedown.zoom', null)
|
||||||
.on('onwheel', null)
|
.on('onwheel', null)
|
||||||
@@ -102,170 +102,75 @@ export default class NodesChart extends React.Component {
|
|||||||
.on('touchstart.zoom', null);
|
.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() {
|
render() {
|
||||||
const nodeElements = this.renderGraphNodes(this.state.nodes, this.state.nodeScale);
|
const { edges, nodes, panTranslateX, panTranslateY, scale } = this.state;
|
||||||
const edgeElements = this.renderGraphEdges(this.state.edges, this.state.nodeScale);
|
|
||||||
const scale = this.state.scale;
|
|
||||||
|
|
||||||
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 transform = `translate(${translate}) scale(${scale})`;
|
||||||
const svgClassNames = this.state.maxNodesExceeded || nodeElements.size === 0 ? 'hide' : '';
|
const svgClassNames = this.props.isEmpty ? 'hide' : '';
|
||||||
const errorEmpty = this.renderEmptyTopologyError(AppStore.isTopologyEmpty());
|
|
||||||
const errorMaxNodesExceeded = this.renderMaxNodesError(this.state.maxNodesExceeded);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="nodes-chart">
|
<div className="nodes-chart">
|
||||||
{errorEmpty}
|
|
||||||
{errorMaxNodesExceeded}
|
|
||||||
<svg width="100%" height="100%" id="nodes-chart-canvas"
|
<svg width="100%" height="100%" id="nodes-chart-canvas"
|
||||||
className={svgClassNames} onClick={this.handleMouseClick}>
|
className={svgClassNames} onClick={this.handleMouseClick}>
|
||||||
<g transform="translate(24,24) scale(0.25)">
|
<g transform="translate(24,24) scale(0.25)">
|
||||||
<Logo />
|
<Logo />
|
||||||
</g>
|
</g>
|
||||||
<g className="canvas" transform={transform}>
|
<NodesChartElements
|
||||||
<g className="edges">
|
edges={edges}
|
||||||
{edgeElements}
|
nodes={nodes}
|
||||||
</g>
|
transform={transform}
|
||||||
<g className="nodes">
|
adjacentNodes={this.props.adjacentNodes}
|
||||||
{nodeElements}
|
layoutPrecision={this.props.layoutPrecision}
|
||||||
</g>
|
selectedMetric={this.props.selectedMetric}
|
||||||
</g>
|
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>
|
</svg>
|
||||||
</div>
|
</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
|
// copy relevant fields to state nodes
|
||||||
return topology.map((node, id) => makeMap({
|
topology.forEach((node, id) => {
|
||||||
id,
|
nextStateNodes = nextStateNodes.mergeIn([id], makeMap({
|
||||||
label: node.get('label'),
|
id,
|
||||||
pseudo: node.get('pseudo'),
|
label: node.get('label'),
|
||||||
metrics: node.get('metrics'),
|
pseudo: node.get('pseudo'),
|
||||||
subLabel: node.get('label_minor'),
|
subLabel: node.get('label_minor'),
|
||||||
nodeCount: node.get('node_count'),
|
nodeCount: node.get('node_count'),
|
||||||
rank: node.get('rank'),
|
metrics: node.get('metrics'),
|
||||||
shape: node.get('shape'),
|
rank: node.get('rank'),
|
||||||
stack: node.get('stack'),
|
shape: node.get('shape'),
|
||||||
x: 0,
|
stack: node.get('stack')
|
||||||
y: 0
|
}));
|
||||||
}));
|
});
|
||||||
|
|
||||||
|
return nextStateNodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
initEdges(topology, stateNodes) {
|
initEdges(topology, stateNodes) {
|
||||||
@@ -309,10 +214,10 @@ export default class NodesChart extends React.Component {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const adjacency = AppStore.getAdjacentNodes(props.selectedNodeId);
|
const adjacentNodes = props.adjacentNodes;
|
||||||
const adjacentLayoutNodeIds = [];
|
const adjacentLayoutNodeIds = [];
|
||||||
|
|
||||||
adjacency.forEach(adjacentId => {
|
adjacentNodes.forEach(adjacentId => {
|
||||||
// filter loopback
|
// filter loopback
|
||||||
if (adjacentId !== props.selectedNodeId) {
|
if (adjacentId !== props.selectedNodeId) {
|
||||||
adjacentLayoutNodeIds.push(adjacentId);
|
adjacentLayoutNodeIds.push(adjacentId);
|
||||||
@@ -321,7 +226,7 @@ export default class NodesChart extends React.Component {
|
|||||||
|
|
||||||
// move origin node to center of viewport
|
// move origin node to center of viewport
|
||||||
const zoomScale = state.scale;
|
const zoomScale = state.scale;
|
||||||
const translate = state.panTranslate;
|
const translate = [state.panTranslateX, state.panTranslateY];
|
||||||
const centerX = (-translate[0] + (props.width + MARGINS.left
|
const centerX = (-translate[0] + (props.width + MARGINS.left
|
||||||
- DETAILS_PANEL_WIDTH) / 2) / zoomScale;
|
- DETAILS_PANEL_WIDTH) / 2) / zoomScale;
|
||||||
const centerY = (-translate[1] + (props.height + MARGINS.top) / 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'))) {
|
|| _.includes(adjacentLayoutNodeIds, edge.get('target'))) {
|
||||||
const source = stateNodes.get(edge.get('source'));
|
const source = stateNodes.get(edge.get('source'));
|
||||||
const target = stateNodes.get(edge.get('target'));
|
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: source.get('x'), y: source.get('y')},
|
||||||
{x: target.get('x'), y: target.get('y')}
|
{x: target.get('x'), y: target.get('y')}
|
||||||
]);
|
]));
|
||||||
}
|
}
|
||||||
return edge;
|
return edge;
|
||||||
});
|
});
|
||||||
@@ -374,18 +279,10 @@ export default class NodesChart extends React.Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMouseClick() {
|
|
||||||
if (!this.isZooming) {
|
|
||||||
clickBackground();
|
|
||||||
} else {
|
|
||||||
this.isZooming = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreLayout(state) {
|
restoreLayout(state) {
|
||||||
// undo any pan/zooming that might have happened
|
// undo any pan/zooming that might have happened
|
||||||
this.zoom.scale(state.scale);
|
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({
|
const nodes = state.nodes.map(node => node.merge({
|
||||||
x: node.get('px'),
|
x: node.get('px'),
|
||||||
@@ -399,7 +296,7 @@ export default class NodesChart extends React.Component {
|
|||||||
return edge;
|
return edge;
|
||||||
});
|
});
|
||||||
|
|
||||||
return { edges, nodes};
|
return { edges, nodes };
|
||||||
}
|
}
|
||||||
|
|
||||||
updateGraphState(props, state) {
|
updateGraphState(props, state) {
|
||||||
@@ -412,9 +309,13 @@ export default class NodesChart extends React.Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let stateNodes = this.initNodes(props.nodes, state.nodes);
|
const stateNodes = this.initNodes(props.nodes, state.nodes);
|
||||||
let stateEdges = this.initEdges(props.nodes, stateNodes);
|
const stateEdges = this.initEdges(props.nodes, stateNodes);
|
||||||
|
const nodeMetrics = stateNodes.map(node => makeMap({
|
||||||
|
metrics: node.get('metrics')
|
||||||
|
}));
|
||||||
const nodeScale = this.getNodeScale(props);
|
const nodeScale = this.getNodeScale(props);
|
||||||
|
const nextState = { nodeScale };
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
width: props.width,
|
width: props.width,
|
||||||
@@ -431,21 +332,15 @@ export default class NodesChart extends React.Component {
|
|||||||
|
|
||||||
log(`graph layout took ${timedLayouter.time}ms`);
|
log(`graph layout took ${timedLayouter.time}ms`);
|
||||||
|
|
||||||
// layout was aborted
|
// inject metrics and save coordinates for restore
|
||||||
if (!graph) {
|
const layoutNodes = graph.nodes
|
||||||
return {maxNodesExceeded: true};
|
.mergeDeep(nodeMetrics)
|
||||||
}
|
.map(node => node.merge({
|
||||||
stateNodes = graph.nodes.mergeDeep(stateNodes.map(node => makeMap({
|
px: node.get('x'),
|
||||||
metrics: node.get('metrics')
|
py: node.get('y')
|
||||||
})));
|
}));
|
||||||
stateEdges = graph.edges;
|
const layoutEdges = graph.edges
|
||||||
|
.map(edge => edge.set('ppoints', edge.get('points')));
|
||||||
// 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')));
|
|
||||||
|
|
||||||
// adjust layout based on viewport
|
// adjust layout based on viewport
|
||||||
const xFactor = (props.width - MARGINS.left - MARGINS.right) / graph.width;
|
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);
|
const zoomFactor = Math.min(xFactor, yFactor);
|
||||||
let zoomScale = this.state.scale;
|
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;
|
zoomScale = zoomFactor;
|
||||||
// saving in d3's behavior cache
|
// saving in d3's behavior cache
|
||||||
this.zoom.scale(zoomFactor);
|
this.zoom.scale(zoomFactor);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
nextState.scale = zoomScale;
|
||||||
nodes: stateNodes,
|
if (!isDeepEqual(stateNodes, state.nodes)) {
|
||||||
edges: stateEdges,
|
nextState.nodes = layoutNodes;
|
||||||
scale: zoomScale,
|
}
|
||||||
nodeScale,
|
if (!isDeepEqual(stateEdges, state.edges)) {
|
||||||
maxNodesExceeded: false
|
nextState.edges = layoutEdges;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
return nextState;
|
||||||
}
|
}
|
||||||
|
|
||||||
getNodeScale(props) {
|
getNodeScale(props) {
|
||||||
@@ -483,9 +380,12 @@ export default class NodesChart extends React.Component {
|
|||||||
if (!this.props.selectedNodeId) {
|
if (!this.props.selectedNodeId) {
|
||||||
this.setState({
|
this.setState({
|
||||||
hasZoomed: true,
|
hasZoomed: true,
|
||||||
panTranslate: d3.event.translate.slice(),
|
panTranslateX: d3.event.translate[0],
|
||||||
|
panTranslateY: d3.event.translate[1],
|
||||||
scale: d3.event.scale
|
scale: d3.event.scale
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reactMixin.onClass(NodesChart, PureRenderMixin);
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import dagre from 'dagre';
|
import dagre from 'dagre';
|
||||||
import debug from 'debug';
|
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 { EDGE_ID_SEPARATOR } from '../constants/naming';
|
||||||
import { updateNodeDegrees } from '../utils/topology-utils';
|
import { updateNodeDegrees } from '../utils/topology-utils';
|
||||||
|
|
||||||
const log = debug('scope:nodes-layout');
|
const log = debug('scope:nodes-layout');
|
||||||
|
|
||||||
const MAX_NODES = 100;
|
|
||||||
const topologyCaches = {};
|
const topologyCaches = {};
|
||||||
const DEFAULT_WIDTH = 800;
|
const DEFAULT_WIDTH = 800;
|
||||||
const DEFAULT_MARGINS = {top: 0, left: 0};
|
const DEFAULT_MARGINS = {top: 0, left: 0};
|
||||||
@@ -51,11 +50,6 @@ function runLayoutEngine(graph, imNodes, imEdges, opts) {
|
|||||||
let nodes = imNodes;
|
let nodes = imNodes;
|
||||||
let edges = imEdges;
|
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 options = opts || {};
|
||||||
const scale = options.scale || DEFAULT_SCALE;
|
const scale = options.scale || DEFAULT_SCALE;
|
||||||
const ranksep = scale(RANK_SEPARATION_FACTOR);
|
const ranksep = scale(RANK_SEPARATION_FACTOR);
|
||||||
@@ -122,13 +116,13 @@ function runLayoutEngine(graph, imNodes, imEdges, opts) {
|
|||||||
graph.edges().forEach(graphEdge => {
|
graph.edges().forEach(graphEdge => {
|
||||||
const graphEdgeMeta = graph.edge(graphEdge);
|
const graphEdgeMeta = graph.edge(graphEdge);
|
||||||
const edge = edges.get(graphEdgeMeta.id);
|
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
|
// set beginning and end points to node coordinates to ignore node bounding box
|
||||||
const source = nodes.get(fromGraphNodeId(edge.get('source')));
|
const source = nodes.get(fromGraphNodeId(edge.get('source')));
|
||||||
const target = nodes.get(fromGraphNodeId(edge.get('target')));
|
const target = nodes.get(fromGraphNodeId(edge.get('target')));
|
||||||
points[0] = {x: source.get('x'), y: source.get('y')};
|
points = points.mergeIn([0], {x: source.get('x'), y: source.get('y')});
|
||||||
points[points.length - 1] = {x: target.get('x'), y: target.get('y')};
|
points = points.mergeIn([points.size - 1], {x: target.get('x'), y: target.get('y')});
|
||||||
|
|
||||||
edges = edges.setIn([graphEdgeMeta.id, 'points'], points);
|
edges = edges.setIn([graphEdgeMeta.id, 'points'], points);
|
||||||
});
|
});
|
||||||
@@ -251,13 +245,12 @@ function shiftLayoutToCenter(layout, opts) {
|
|||||||
y: node.get('y') + offsetY
|
y: node.get('y') + offsetY
|
||||||
}));
|
}));
|
||||||
|
|
||||||
result.edges = layout.edges.map(edge => {
|
result.edges = layout.edges.map(edge => edge.update('points',
|
||||||
const points = edge.get('points').map(point => ({
|
points => points.map(point => point.merge({
|
||||||
x: point.x + offsetX,
|
x: point.get('x') + offsetX,
|
||||||
y: point.y + offsetY
|
y: point.get('y') + offsetY
|
||||||
}));
|
}))
|
||||||
return edge.set('points', points);
|
));
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -271,10 +264,10 @@ function shiftLayoutToCenter(layout, opts) {
|
|||||||
function setSimpleEdgePoints(edge, nodeCache) {
|
function setSimpleEdgePoints(edge, nodeCache) {
|
||||||
const source = nodeCache.get(edge.get('source'));
|
const source = nodeCache.get(edge.get('source'));
|
||||||
const target = nodeCache.get(edge.get('target'));
|
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: source.get('x'), y: source.get('y')},
|
||||||
{x: target.get('x'), y: target.get('y')}
|
{x: target.get('x'), y: target.get('y')}
|
||||||
]);
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -300,14 +293,14 @@ export function hasUnseenNodes(nodes, cache) {
|
|||||||
*/
|
*/
|
||||||
function hasSameEndpoints(cachedEdge, nodes) {
|
function hasSameEndpoints(cachedEdge, nodes) {
|
||||||
const oldPoints = cachedEdge.get('points');
|
const oldPoints = cachedEdge.get('points');
|
||||||
const oldSourcePoint = oldPoints[0];
|
const oldSourcePoint = oldPoints.first();
|
||||||
const oldTargetPoint = oldPoints[oldPoints.length - 1];
|
const oldTargetPoint = oldPoints.last();
|
||||||
const newSource = nodes.get(cachedEdge.get('source'));
|
const newSource = nodes.get(cachedEdge.get('source'));
|
||||||
const newTarget = nodes.get(cachedEdge.get('target'));
|
const newTarget = nodes.get(cachedEdge.get('target'));
|
||||||
return (oldSourcePoint.x === newSource.get('x')
|
return (oldSourcePoint.get('x') === newSource.get('x')
|
||||||
&& oldSourcePoint.y === newSource.get('y')
|
&& oldSourcePoint.get('y') === newSource.get('y')
|
||||||
&& oldTargetPoint.x === newTarget.get('x')
|
&& oldTargetPoint.get('x') === newTarget.get('x')
|
||||||
&& oldTargetPoint.y === newTarget.get('y'));
|
&& oldTargetPoint.get('y') === newTarget.get('y'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import React from 'react';
|
|
||||||
import debug from 'debug';
|
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 Logo from './logo';
|
||||||
import AppStore from '../stores/app-store';
|
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 LEFT_ANGLE_KEY_IDENTIFIER = 'U+003E';
|
||||||
const keyPressLog = debug('scope:app-key-press');
|
const keyPressLog = debug('scope:app-key-press');
|
||||||
|
|
||||||
|
/* make sure these can all be shallow-checked for equality for PureRenderMixin */
|
||||||
function getStateFromStores() {
|
function getStateFromStores() {
|
||||||
return {
|
return {
|
||||||
activeTopologyOptions: AppStore.getActiveTopologyOptions(),
|
activeTopologyOptions: AppStore.getActiveTopologyOptions(),
|
||||||
|
adjacentNodes: AppStore.getAdjacentNodes(AppStore.getSelectedNodeId()),
|
||||||
controlStatus: AppStore.getControlStatus(),
|
controlStatus: AppStore.getControlStatus(),
|
||||||
controlPipe: AppStore.getControlPipe(),
|
controlPipe: AppStore.getControlPipe(),
|
||||||
currentTopology: AppStore.getCurrentTopology(),
|
currentTopology: AppStore.getCurrentTopology(),
|
||||||
@@ -47,6 +51,7 @@ function getStateFromStores() {
|
|||||||
selectedMetric: AppStore.getSelectedMetric(),
|
selectedMetric: AppStore.getSelectedMetric(),
|
||||||
topologies: AppStore.getTopologies(),
|
topologies: AppStore.getTopologies(),
|
||||||
topologiesLoaded: AppStore.isTopologiesLoaded(),
|
topologiesLoaded: AppStore.isTopologiesLoaded(),
|
||||||
|
topologyEmpty: AppStore.isTopologyEmpty(),
|
||||||
updatePaused: AppStore.isUpdatePaused(),
|
updatePaused: AppStore.isUpdatePaused(),
|
||||||
updatePausedAt: AppStore.getUpdatePausedAt(),
|
updatePausedAt: AppStore.getUpdatePausedAt(),
|
||||||
version: AppStore.getVersion(),
|
version: AppStore.getVersion(),
|
||||||
@@ -136,6 +141,8 @@ export default class App extends React.Component {
|
|||||||
selectedMetric={this.state.selectedMetric}
|
selectedMetric={this.state.selectedMetric}
|
||||||
forceRelayout={this.state.forceRelayout}
|
forceRelayout={this.state.forceRelayout}
|
||||||
topologyOptions={this.state.activeTopologyOptions}
|
topologyOptions={this.state.activeTopologyOptions}
|
||||||
|
topologyEmpty={this.state.topologyEmpty}
|
||||||
|
adjacentNodes={this.state.adjacentNodes}
|
||||||
topologyId={this.state.currentTopologyId} />
|
topologyId={this.state.currentTopologyId} />
|
||||||
|
|
||||||
<Sidebar>
|
<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" */
|
/* eslint react/jsx-no-bind: "off" */
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import Perf from 'react-addons-perf';
|
||||||
|
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
const log = debug('scope:debug-panel');
|
const log = debug('scope:debug-panel');
|
||||||
@@ -31,7 +32,7 @@ const LABEL_PREFIXES = _.range('A'.charCodeAt(), 'Z'.charCodeAt() + 1)
|
|||||||
.map(n => String.fromCharCode(n));
|
.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) => ({
|
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) {
|
function addNodes(n) {
|
||||||
const ns = AppStore.getNodes();
|
const ns = AppStore.getNodes();
|
||||||
const nodeNames = ns.keySeq().toJS();
|
const nodeNames = ns.keySeq().toJS();
|
||||||
const newNodeNames = _.range(ns.size, ns.size + n).map(() => (
|
const newNodeNames = _.range(ns.size, ns.size + n).map(i => (
|
||||||
`${randomLetter()}${randomLetter()}-zing`
|
// `${randomLetter()}${randomLetter()}-zing`
|
||||||
|
`zing${i}`
|
||||||
));
|
));
|
||||||
const allNodes = _(nodeNames).concat(newNodeNames).value();
|
const allNodes = _(nodeNames).concat(newNodeNames).value();
|
||||||
|
|
||||||
@@ -110,9 +125,9 @@ function addNodes(n) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function showingDebugToolbar() {
|
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>
|
</tbody>
|
||||||
</table>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
|
import reactMixin from 'react-mixin';
|
||||||
|
|
||||||
import NodeDetails from './node-details';
|
import NodeDetails from './node-details';
|
||||||
import { DETAILS_PANEL_WIDTH as WIDTH, DETAILS_PANEL_OFFSET as OFFSET,
|
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">
|
<div className="details">
|
||||||
{details.toIndexedSeq().map((obj, index) => <DetailsCard key={obj.id}
|
{details.toIndexedSeq().map((obj, index) => <DetailsCard key={obj.id}
|
||||||
index={index} cardCount={details.size} nodes={nodes}
|
index={index} cardCount={details.size} nodes={nodes}
|
||||||
nodeControlStatus={controlStatus[obj.id]} {...obj} />
|
nodeControlStatus={controlStatus.get(obj.id)} {...obj} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -135,7 +135,8 @@ export default class NodeDetails extends React.Component {
|
|||||||
const details = this.props.details;
|
const details = this.props.details;
|
||||||
const showControls = details.controls && details.controls.length > 0;
|
const showControls = details.controls && details.controls.length > 0;
|
||||||
const nodeColor = getNodeColorDark(details.rank, details.label, details.pseudo);
|
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 tools = this.renderTools();
|
||||||
const styles = {
|
const styles = {
|
||||||
controls: {
|
controls: {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
|
import reactMixin from 'react-mixin';
|
||||||
|
|
||||||
import { clickRelative } from '../../actions/app-actions';
|
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 React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
|
import reactMixin from 'react-mixin';
|
||||||
|
|
||||||
import { clickRelative } from '../../actions/app-actions';
|
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 React from 'react';
|
||||||
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
|
import reactMixin from 'react-mixin';
|
||||||
|
|
||||||
import NodesChart from '../charts/nodes-chart';
|
import NodesChart from '../charts/nodes-chart';
|
||||||
|
import NodesError from '../charts/nodes-error';
|
||||||
|
|
||||||
const navbarHeight = 160;
|
const navbarHeight = 160;
|
||||||
const marginTop = 0;
|
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 {
|
export default class Nodes extends React.Component {
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
@@ -24,8 +45,37 @@ export default class Nodes extends React.Component {
|
|||||||
window.removeEventListener('resize', this.handleResize);
|
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() {
|
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() {
|
handleResize() {
|
||||||
@@ -39,3 +89,5 @@ export default class Nodes extends React.Component {
|
|||||||
this.setState({height, width});
|
this.setState({height, width});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reactMixin.onClass(Nodes, PureRenderMixin);
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
|
import reactMixin from 'react-mixin';
|
||||||
|
|
||||||
export default class ShowMore extends React.Component {
|
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 React from 'react';
|
||||||
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
|
import reactMixin from 'react-mixin';
|
||||||
|
|
||||||
import { clickTopology } from '../actions/app-actions';
|
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 React from 'react';
|
||||||
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
|
import reactMixin from 'react-mixin';
|
||||||
|
|
||||||
import { changeTopologyOption } from '../actions/app-actions';
|
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 React from 'react';
|
||||||
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
|
import reactMixin from 'react-mixin';
|
||||||
|
|
||||||
import TopologyOptionAction from './topology-option-action';
|
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
|
// keep at the top
|
||||||
getAppState() {
|
getAppState() {
|
||||||
|
const cp = this.getControlPipe();
|
||||||
return {
|
return {
|
||||||
controlPipe: this.getControlPipe(),
|
controlPipe: cp ? cp.toJS() : null,
|
||||||
nodeDetails: this.getNodeDetailsState(),
|
nodeDetails: this.getNodeDetailsState().toJS(),
|
||||||
selectedNodeId,
|
selectedNodeId,
|
||||||
pinnedMetricType,
|
pinnedMetricType,
|
||||||
topologyId: currentTopologyId,
|
topologyId: currentTopologyId,
|
||||||
@@ -190,12 +191,11 @@ export class AppStore extends Store {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getControlStatus() {
|
getControlStatus() {
|
||||||
return controlStatus.toJS();
|
return controlStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
getControlPipe() {
|
getControlPipe() {
|
||||||
const cp = controlPipes.last();
|
return controlPipes.last();
|
||||||
return cp && cp.toJS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentTopology() {
|
getCurrentTopology() {
|
||||||
@@ -240,7 +240,7 @@ export class AppStore extends Store {
|
|||||||
getNodeDetailsState() {
|
getNodeDetailsState() {
|
||||||
return nodeDetails.toIndexedSeq().map(details => ({
|
return nodeDetails.toIndexedSeq().map(details => ({
|
||||||
id: details.id, label: details.label, topologyId: details.topologyId
|
id: details.id, label: details.label, topologyId: details.topologyId
|
||||||
})).toJS();
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
getTopCardNodeId() {
|
getTopCardNodeId() {
|
||||||
|
|||||||
@@ -333,7 +333,7 @@ h2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodes > .node {
|
.nodes-chart-nodes > .node {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: opacity .5s @base-ease;
|
transition: opacity .5s @base-ease;
|
||||||
|
|
||||||
@@ -370,6 +370,10 @@ h2 {
|
|||||||
opacity: @edge-opacity-blurred;
|
opacity: @edge-opacity-blurred;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.focused {
|
||||||
|
animation: focusing 1.5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
stroke: @text-secondary-color;
|
stroke: @text-secondary-color;
|
||||||
stroke-width: @edge-link-stroke-width;
|
stroke-width: @edge-link-stroke-width;
|
||||||
@@ -1007,7 +1011,7 @@ h2 {
|
|||||||
&:last-child {
|
&:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fa {
|
.fa {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
color: darkred;
|
color: darkred;
|
||||||
@@ -1058,6 +1062,16 @@ h2 {
|
|||||||
font-size: .7rem;
|
font-size: .7rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes focusing {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
} 33% {
|
||||||
|
opacity: 0.2;
|
||||||
|
} 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes blinking {
|
@keyframes blinking {
|
||||||
0%, 100% {
|
0%, 100% {
|
||||||
opacity: 1.0;
|
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-addons-update": "^0.14.7",
|
||||||
"react-dom": "^0.14.7",
|
"react-dom": "^0.14.7",
|
||||||
"react-motion": "0.3.1",
|
"react-motion": "0.3.1",
|
||||||
|
"react-mixin": "^3.0.3",
|
||||||
"reqwest": "~2.0.5",
|
"reqwest": "~2.0.5",
|
||||||
"timely": "0.1.0"
|
"timely": "0.1.0"
|
||||||
},
|
},
|
||||||
@@ -50,14 +51,17 @@
|
|||||||
"less": "~2.6.1",
|
"less": "~2.6.1",
|
||||||
"less-loader": "2.2.2",
|
"less-loader": "2.2.2",
|
||||||
"postcss-loader": "0.8.2",
|
"postcss-loader": "0.8.2",
|
||||||
|
"react-addons-perf": "^0.14.0",
|
||||||
"style-loader": "0.13.0",
|
"style-loader": "0.13.0",
|
||||||
"url": "0.11.0",
|
"url": "0.11.0",
|
||||||
"url-loader": "0.5.7",
|
"url-loader": "0.5.7",
|
||||||
"webpack": "~1.12.4"
|
"webpack": "~1.12.4"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
"browser-perf": "^1.4.5",
|
||||||
"express": "~4.13.3",
|
"express": "~4.13.3",
|
||||||
"http-proxy": "^1.12.0",
|
"http-proxy": "^1.12.0",
|
||||||
|
"perfjankie": "^2.1.0",
|
||||||
"react-hot-loader": "~1.3.0",
|
"react-hot-loader": "~1.3.0",
|
||||||
"webpack-dev-server": "~1.14.1"
|
"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