mirror of
https://github.com/weaveworks/scope.git
synced 2026-07-28 01:31:17 +00:00
Metrics on canvas!
- Adds metric on canvas support for more shapes - More variation from the debug bar.
This commit is contained in:
@@ -238,7 +238,7 @@ export function receiveNodesDelta(delta) {
|
||||
} else {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.RECEIVE_NODES_DELTA,
|
||||
delta
|
||||
delta: delta
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,32 @@
|
||||
import React from 'react';
|
||||
import {formatCanvasMetric, getMetricValue} from '../utils/data-utils.js';
|
||||
|
||||
export default function NodeShapeCircle({onlyHighlight, highlighted, size, color}) {
|
||||
export default function NodeShapeCircle({highlighted, size, color, metrics}) {
|
||||
const hightlightNode = <circle r={size * 0.7} className="highlighted" />;
|
||||
|
||||
if (onlyHighlight) {
|
||||
return (
|
||||
<g className="shape">
|
||||
{highlighted && hightlightNode}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
const clipId = `mask-${Math.random()}`;
|
||||
const {height, vp} = getMetricValue(metrics, size);
|
||||
|
||||
return (
|
||||
<g className="shape">
|
||||
<defs>
|
||||
<clipPath id={clipId}>
|
||||
<rect
|
||||
width={size}
|
||||
height={size}
|
||||
x={-size * 0.5}
|
||||
y={size * 0.5 - height}
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
{highlighted && hightlightNode}
|
||||
<circle r={size * 0.5} className="border" stroke={color} />
|
||||
<circle r={size * 0.45} className="shadow" />
|
||||
<circle r={Math.max(2, size * 0.125)} className="node" />
|
||||
<circle r={size * 0.45} className="metric-fill" clipPath={`url(#${clipId})`} />
|
||||
{highlighted ?
|
||||
<text dy="0.35em" style={{'textAnchor': 'middle'}}>
|
||||
{formatCanvasMetric(vp)}
|
||||
</text> :
|
||||
<circle className="node" r={Math.max(2, (size * 0.125))} />}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import d3 from 'd3';
|
||||
import {formatCanvasMetric, getMetricValue} from '../utils/data-utils.js';
|
||||
|
||||
const line = d3.svg.line()
|
||||
.interpolate('cardinal-closed')
|
||||
@@ -14,29 +15,37 @@ function polygon(r, sides) {
|
||||
return points;
|
||||
}
|
||||
|
||||
export default function NodeShapeHeptagon({onlyHighlight, highlighted, size, color}) {
|
||||
export default function NodeShapeHeptagon({highlighted, size, color, metrics}) {
|
||||
const scaledSize = size * 1.0;
|
||||
const pathProps = v => ({
|
||||
d: line(polygon(scaledSize * v, 7)),
|
||||
transform: 'rotate(90)'
|
||||
});
|
||||
|
||||
const hightlightNode = <path className="highlighted" {...pathProps(0.7)} />;
|
||||
|
||||
if (onlyHighlight) {
|
||||
return (
|
||||
<g className="shape">
|
||||
{highlighted && hightlightNode}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
const clipId = `mask-${Math.random()}`;
|
||||
const {height, vp} = getMetricValue(metrics, size);
|
||||
|
||||
return (
|
||||
<g className="shape">
|
||||
{highlighted && hightlightNode}
|
||||
<defs>
|
||||
<clipPath id={clipId}>
|
||||
<rect
|
||||
width={size}
|
||||
height={size}
|
||||
y={-size * 0.5}
|
||||
x={size * 0.5 - height}
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
{highlighted && <path className="highlighted" {...pathProps(0.7)} />}
|
||||
<path className="border" stroke={color} {...pathProps(0.5)} />
|
||||
<path className="shadow" {...pathProps(0.45)} />
|
||||
<circle className="node" r={Math.max(2, (scaledSize * 0.125))} />
|
||||
<path className="metric-fill" clipPath={`url(#${clipId})`} {...pathProps(0.45)} />
|
||||
{highlighted ?
|
||||
<text dy="0.35em" style={{'textAnchor': 'middle'}}>
|
||||
{formatCanvasMetric(vp)}
|
||||
</text> :
|
||||
<circle className="node" r={Math.max(2, (size * 0.125))} />}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import d3 from 'd3';
|
||||
import {formatCanvasMetric, getMetricValue} from '../utils/data-utils.js';
|
||||
|
||||
const line = d3.svg.line()
|
||||
.interpolate('cardinal-closed')
|
||||
@@ -24,28 +25,38 @@ function getPoints(h) {
|
||||
}
|
||||
|
||||
|
||||
export default function NodeShapeHex({onlyHighlight, highlighted, size, color}) {
|
||||
export default function NodeShapeHex({highlighted, size, color, metrics}) {
|
||||
const pathProps = v => ({
|
||||
d: getPoints(size * v * 2),
|
||||
transform: `rotate(90) translate(-${size * getWidth(v)}, -${size * v})`
|
||||
});
|
||||
|
||||
const hightlightNode = <path className="highlighted" {...pathProps(0.7)} />;
|
||||
const shadowSize = 0.45;
|
||||
const upperHexBitHeight = -0.25 * size * shadowSize;
|
||||
|
||||
if (onlyHighlight) {
|
||||
return (
|
||||
<g className="shape">
|
||||
{highlighted && hightlightNode}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
const clipId = `mask-${Math.random()}`;
|
||||
const {height, vp} = getMetricValue(metrics, size);
|
||||
|
||||
return (
|
||||
<g className="shape">
|
||||
{highlighted && hightlightNode}
|
||||
<defs>
|
||||
<clipPath id={clipId}>
|
||||
<rect
|
||||
width={size}
|
||||
height={size}
|
||||
x={size - height + upperHexBitHeight}
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
{highlighted && <path className="highlighted" {...pathProps(0.7)} />}
|
||||
<path className="border" stroke={color} {...pathProps(0.5)} />
|
||||
<path className="shadow" {...pathProps(0.45)} />
|
||||
<circle className="node" r={Math.max(2, (size * 0.125))} />
|
||||
<path className="shadow" {...pathProps(shadowSize)} />
|
||||
<path className="metric-fill" clipPath={`url(#${clipId})`} {...pathProps(shadowSize)} />
|
||||
{highlighted ?
|
||||
<text dy="0.35em" style={{'textAnchor': 'middle'}}>
|
||||
{formatCanvasMetric(vp)}
|
||||
</text> :
|
||||
<circle className="node" r={Math.max(2, (size * 0.125))} />}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import NodeShapeSquare from './node-shape-square';
|
||||
|
||||
// TODO how to express a cmp in terms of another cmp? (Rather than a sub-cmp as here).
|
||||
// HOC!
|
||||
|
||||
export default function NodeShapeRoundedSquare(props) {
|
||||
return (
|
||||
|
||||
@@ -1,30 +1,42 @@
|
||||
import React from 'react';
|
||||
import {formatCanvasMetric, getMetricValue} from '../utils/data-utils.js';
|
||||
|
||||
export default function NodeShapeSquare({onlyHighlight, highlighted, size, color, rx = 0, ry = 0}) {
|
||||
export default function NodeShapeSquare({
|
||||
highlighted, size, color, rx = 0, ry = 0, metrics
|
||||
}) {
|
||||
const rectProps = v => ({
|
||||
width: v * size * 2,
|
||||
height: v * size * 2,
|
||||
rx: v * size * rx,
|
||||
ry: v * size * ry,
|
||||
transform: `translate(-${size * v}, -${size * v})`
|
||||
x: -size * v,
|
||||
y: -size * v
|
||||
});
|
||||
|
||||
const hightlightNode = <rect className="highlighted" {...rectProps(0.7)} />;
|
||||
|
||||
if (onlyHighlight) {
|
||||
return (
|
||||
<g className="shape">
|
||||
{highlighted && hightlightNode}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
const clipId = `mask-${Math.random()}`;
|
||||
const {height, vp} = getMetricValue(metrics, size);
|
||||
|
||||
return (
|
||||
<g className="shape">
|
||||
{highlighted && hightlightNode}
|
||||
<defs>
|
||||
<clipPath id={clipId}>
|
||||
<rect
|
||||
width={size}
|
||||
height={size}
|
||||
x={-size * 0.5}
|
||||
y={size * 0.5 - height}
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
{highlighted && <rect className="highlighted" {...rectProps(0.7)} />}
|
||||
<rect className="border" stroke={color} {...rectProps(0.5)} />
|
||||
<rect className="shadow" {...rectProps(0.45)} />
|
||||
<circle className="node" r={Math.max(2, (size * 0.125))} />
|
||||
<rect className="metric-fill" clipPath={`url(#${clipId})`} {...rectProps(0.45)} />
|
||||
{highlighted ?
|
||||
<text dy="0.35em" style={{'textAnchor': 'middle'}}>
|
||||
{formatCanvasMetric(vp)}
|
||||
</text> :
|
||||
<circle className="node" r={Math.max(2, (size * 0.125))} />}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { isContrastMode } from '../utils/contrast-utils';
|
||||
|
||||
function dissoc(obj, key) {
|
||||
const newObj = _.clone(obj);
|
||||
delete newObj[key];
|
||||
return newObj;
|
||||
}
|
||||
|
||||
export default function NodeShapeStack(props) {
|
||||
const propsNoHighlight = dissoc(props, 'highlighted');
|
||||
const propsOnlyHighlight = Object.assign({}, props, {onlyHighlight: true});
|
||||
const contrastMode = isContrastMode();
|
||||
|
||||
const Shape = props.shape;
|
||||
const [dx, dy] = contrastMode ? [0, 8] : [0, 5];
|
||||
const dsx = (props.size * 2 + (dx * 2)) / (props.size * 2);
|
||||
@@ -22,16 +11,16 @@ export default function NodeShapeStack(props) {
|
||||
|
||||
return (
|
||||
<g transform={`translate(${dx * -1}, ${dy * -2.5})`} className="stack">
|
||||
<g transform={`scale(${hls})translate(${dx}, ${dy}) `}>
|
||||
<Shape {...propsOnlyHighlight} />
|
||||
<g transform={`scale(${hls})translate(${dx}, ${dy})`} className="onlyHighlight">
|
||||
<Shape {...props} />
|
||||
</g>
|
||||
<g transform={`translate(${dx * 2}, ${dy * 2})`}>
|
||||
<Shape {...propsNoHighlight} />
|
||||
<Shape {...props} />
|
||||
</g>
|
||||
<g transform={`translate(${dx * 1}, ${dy * 1})`}>
|
||||
<Shape {...propsNoHighlight} />
|
||||
<Shape {...props} />
|
||||
</g>
|
||||
<Shape {...propsNoHighlight} />
|
||||
<Shape {...props} />
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -74,10 +74,10 @@ export default class NodesChart extends React.Component {
|
||||
edges: makeMap()
|
||||
});
|
||||
}
|
||||
//
|
||||
// FIXME add PureRenderMixin, Immutables, and move the following functions to render()
|
||||
if (nextProps.forceRelayout || nextProps.nodes !== this.props.nodes) {
|
||||
_.assign(state, this.updateGraphState(nextProps, state));
|
||||
}
|
||||
_.assign(state, this.updateGraphState(nextProps, state));
|
||||
|
||||
if (this.props.selectedNodeId !== nextProps.selectedNodeId) {
|
||||
_.assign(state, this.restoreLayout(state));
|
||||
}
|
||||
@@ -151,6 +151,7 @@ export default class NodesChart extends React.Component {
|
||||
pseudo={node.get('pseudo')}
|
||||
nodeCount={node.get('nodeCount')}
|
||||
subLabel={node.get('subLabel')}
|
||||
metrics={node.get('metrics')}
|
||||
rank={node.get('rank')}
|
||||
selectedNodeScale={selectedNodeScale}
|
||||
nodeScale={nodeScale}
|
||||
@@ -243,9 +244,10 @@ export default class NodesChart extends React.Component {
|
||||
initNodes(topology) {
|
||||
// copy relevant fields to state nodes
|
||||
return topology.map((node, id) => makeMap({
|
||||
id,
|
||||
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'),
|
||||
@@ -423,7 +425,9 @@ export default class NodesChart extends React.Component {
|
||||
if (!graph) {
|
||||
return {maxNodesExceeded: true};
|
||||
}
|
||||
stateNodes = graph.nodes;
|
||||
stateNodes = graph.nodes.mergeDeep(stateNodes.map(node => {
|
||||
return makeMap({metrics: node.get('metrics')});
|
||||
}));
|
||||
stateEdges = graph.edges;
|
||||
|
||||
// save coordinates for restore
|
||||
|
||||
@@ -8,12 +8,21 @@ const log = debug('scope:debug-panel');
|
||||
import { receiveNodesDelta } from '../actions/app-actions';
|
||||
import AppStore from '../stores/app-store';
|
||||
|
||||
const SHAPES = ['circle', 'hexagon', 'square', 'heptagon'];
|
||||
const SHAPES = ['square', 'hexagon', 'heptagon', 'circle'];
|
||||
const NODE_COUNTS = [1, 2, 3];
|
||||
const STACK_VARIANTS = [true, false];
|
||||
const STACK_VARIANTS = [false, true];
|
||||
|
||||
const sample = (collection) => _.range(_.random(4)).map(() => _.sample(collection));
|
||||
|
||||
const shapeTypes = {
|
||||
square: ['Process', 'Processes'],
|
||||
hexagon: ['Container', 'Containers'],
|
||||
heptagon: ['Pod', 'Pods'],
|
||||
circle: ['Host', 'Hosts']
|
||||
};
|
||||
|
||||
const LABEL_PREFIXES = _.range('A'.charCodeAt(), 'Z'.charCodeAt() + 1).map(n => String.fromCharCode(n));
|
||||
|
||||
const deltaAdd = (name, adjacency = [], shape = 'circle', stack = false, nodeCount = 1) => ({
|
||||
adjacency,
|
||||
controls: {},
|
||||
@@ -29,11 +38,20 @@ const deltaAdd = (name, adjacency = [], shape = 'circle', stack = false, nodeCou
|
||||
rank: 'alpine'
|
||||
});
|
||||
|
||||
function label(shape, stacked) {
|
||||
const type = shapeTypes[shape];
|
||||
return stacked ? `Group of ${type[1]}` : type[0];
|
||||
}
|
||||
|
||||
function addAllVariants() {
|
||||
const newNodes = _.flattenDeep(SHAPES.map(s => STACK_VARIANTS.map(stack => {
|
||||
if (!stack) return [deltaAdd([s, 1, stack].join('-'), [], s, stack, 1)];
|
||||
return NODE_COUNTS.map(n => deltaAdd([s, n, stack].join('-'), [], s, stack, n));
|
||||
})));
|
||||
const newNodes = _.flattenDeep(STACK_VARIANTS.map(stack => {
|
||||
return SHAPES.map(s => {
|
||||
if (!stack) return [deltaAdd(label(s, stack), [], s, stack, 1)];
|
||||
return NODE_COUNTS.map(n => {
|
||||
return deltaAdd(label(s, stack), [], s, stack, n);
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
receiveNodesDelta({
|
||||
add: newNodes
|
||||
@@ -43,15 +61,20 @@ function addAllVariants() {
|
||||
function addNodes(n) {
|
||||
const ns = AppStore.getNodes();
|
||||
const nodeNames = ns.keySeq().toJS();
|
||||
const newNodeNames = _.range(ns.size, ns.size + n).map((i) => `zing${i}`);
|
||||
const newNodeNames = _.range(ns.size, ns.size + n).map(() => {
|
||||
return _.sample(LABEL_PREFIXES, 2).join('') + '-zing';
|
||||
});
|
||||
const allNodes = _(nodeNames).concat(newNodeNames).value();
|
||||
|
||||
receiveNodesDelta({
|
||||
add: newNodeNames.map((name) => deltaAdd(name,
|
||||
sample(allNodes)),
|
||||
_.sample(SHAPES),
|
||||
_.sample(STACK_VARIANTS),
|
||||
_.sample(NODE_COUNTS))
|
||||
add: newNodeNames.map((name) => {
|
||||
return deltaAdd(
|
||||
name,
|
||||
sample(allNodes),
|
||||
_.sample(SHAPES),
|
||||
_.sample(STACK_VARIANTS),
|
||||
_.sample(NODE_COUNTS));
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,8 @@ function makeNode(node) {
|
||||
pseudo: node.pseudo,
|
||||
stack: node.stack,
|
||||
shape: node.shape,
|
||||
adjacency: node.adjacency
|
||||
adjacency: node.adjacency,
|
||||
metrics: node.metrics
|
||||
};
|
||||
}
|
||||
|
||||
@@ -553,9 +554,9 @@ export class AppStore extends Store {
|
||||
});
|
||||
|
||||
// update existing nodes
|
||||
_.each(payload.delta.update, (node) => {
|
||||
_.each(payload.delta.update, function(node) {
|
||||
if (nodes.has(node.id)) {
|
||||
nodes = nodes.set(node.id, nodes.get(node.id).merge(makeNode(node)));
|
||||
nodes = nodes.set(node.id, nodes.get(node.id).merge(Immutable.fromJS(node)));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
102
client/app/scripts/utils/data-utils.js
Normal file
102
client/app/scripts/utils/data-utils.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import _ from 'lodash';
|
||||
import d3 from 'd3';
|
||||
|
||||
|
||||
// Inspired by Lee Byron's test data generator.
|
||||
function bumpLayer(n, maxValue) {
|
||||
function bump(a) {
|
||||
const x = 1 / (0.1 + Math.random());
|
||||
const y = 2 * Math.random() - 0.5;
|
||||
const z = 10 / (0.1 + Math.random());
|
||||
for (let i = 0; i < n; i++) {
|
||||
const w = (i / n - y) * z;
|
||||
a[i] += x * Math.exp(-w * w);
|
||||
}
|
||||
}
|
||||
|
||||
const a = [];
|
||||
let i;
|
||||
for (i = 0; i < n; ++i) a[i] = 0;
|
||||
for (i = 0; i < 5; ++i) bump(a);
|
||||
const values = a.map(function(d) { return Math.max(0, d * maxValue); });
|
||||
const s = d3.scale.linear().domain(d3.extent(values)).range([0, maxValue]);
|
||||
return values.map(s);
|
||||
}
|
||||
|
||||
|
||||
const nodeData = {};
|
||||
|
||||
|
||||
function getNextValue(keyValues, maxValue) {
|
||||
const key = keyValues.join('-');
|
||||
let series = nodeData[key];
|
||||
if (!series || !series.length) {
|
||||
series = nodeData[key] = bumpLayer(100, maxValue);
|
||||
}
|
||||
const v = series.shift();
|
||||
return v;
|
||||
}
|
||||
|
||||
|
||||
function mergeMetrics(node) {
|
||||
return Object.assign({}, node, {
|
||||
metrics: {
|
||||
'process_cpu_usage_percent': {
|
||||
samples: [{value: getNextValue([node.id, 'cpu'], 100)}],
|
||||
max: 100
|
||||
},
|
||||
'memory': {
|
||||
samples: [{value: getNextValue([node.id, 'memory'], 1024)}],
|
||||
max: 1024
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function handleAdd(nodes) {
|
||||
if (!nodes) {
|
||||
return nodes;
|
||||
}
|
||||
return nodes.map(mergeMetrics);
|
||||
}
|
||||
|
||||
|
||||
function handleUpdated(updatedNodes, prevNodes) {
|
||||
const modifiedNodesIndex = _.zipObject((updatedNodes || []).map(n => [n.id, n]));
|
||||
return prevNodes.toIndexedSeq().toJS().map(n => {
|
||||
return Object.assign({}, mergeMetrics(n), modifiedNodesIndex[n.id]);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function addMetrics(delta, prevNodes) {
|
||||
return Object.assign({}, delta, {
|
||||
add: handleAdd(delta.add),
|
||||
update: handleUpdated(delta.update, prevNodes)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function getMetricValue(metrics, size) {
|
||||
if (!metrics) {
|
||||
return {height: 0, vp: null};
|
||||
}
|
||||
|
||||
const max = 100;
|
||||
const v = metrics.getIn(['process_cpu_usage_percent', 'samples', 0, 'value']);
|
||||
const vp = v === 0 ? 0 : v / max;
|
||||
const baseline = 0.00;
|
||||
const displayedValue = vp * (1 - baseline) + baseline;
|
||||
const height = size * displayedValue;
|
||||
|
||||
return {height, vp};
|
||||
}
|
||||
|
||||
const formatLargeValue = d3.format('s');
|
||||
export function formatCanvasMetric(v) {
|
||||
if (v === null) {
|
||||
return 'n/a';
|
||||
}
|
||||
return formatLargeValue(Number(v * 100).toFixed(1)) + '%';
|
||||
}
|
||||
@@ -389,12 +389,29 @@ h2 {
|
||||
|
||||
.shape {
|
||||
|
||||
.stack .shape .highlighted {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.stack .onlyHighlight .shape {
|
||||
.border { display: none; }
|
||||
.shadow { display: none; }
|
||||
.node { display: none; }
|
||||
.highlighted { display: inline; }
|
||||
}
|
||||
|
||||
.shape {
|
||||
/* cloud paths have stroke-width set dynamically */
|
||||
&:not(.shape-cloud) .border {
|
||||
stroke-width: @node-border-stroke-width;
|
||||
fill: @background-color;
|
||||
}
|
||||
|
||||
.metric-fill {
|
||||
stroke: none;
|
||||
fill: yellowgreen;
|
||||
}
|
||||
|
||||
.shadow {
|
||||
stroke: none;
|
||||
fill: @background-lighter-color;
|
||||
@@ -402,6 +419,12 @@ h2 {
|
||||
|
||||
.node {
|
||||
fill: @text-color;
|
||||
stroke: @background-lighter-color;
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.highlighted {
|
||||
|
||||
Reference in New Issue
Block a user