diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js
index 563046f88..7387cbc10 100644
--- a/client/app/scripts/actions/app-actions.js
+++ b/client/app/scripts/actions/app-actions.js
@@ -238,7 +238,7 @@ export function receiveNodesDelta(delta) {
} else {
AppDispatcher.dispatch({
type: ActionTypes.RECEIVE_NODES_DELTA,
- delta
+ delta: delta
});
}
}
diff --git a/client/app/scripts/charts/node-shape-circle.js b/client/app/scripts/charts/node-shape-circle.js
index e4c32ade8..53d0c7dcd 100644
--- a/client/app/scripts/charts/node-shape-circle.js
+++ b/client/app/scripts/charts/node-shape-circle.js
@@ -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 = ;
-
- if (onlyHighlight) {
- return (
-
- {highlighted && hightlightNode}
-
- );
- }
+ const clipId = `mask-${Math.random()}`;
+ const {height, vp} = getMetricValue(metrics, size);
return (
+
+
+
+
+
{highlighted && hightlightNode}
-
+
+ {highlighted ?
+
+ {formatCanvasMetric(vp)}
+ :
+ }
);
}
diff --git a/client/app/scripts/charts/node-shape-heptagon.js b/client/app/scripts/charts/node-shape-heptagon.js
index 98652e5f1..41e1ce189 100644
--- a/client/app/scripts/charts/node-shape-heptagon.js
+++ b/client/app/scripts/charts/node-shape-heptagon.js
@@ -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 = ;
-
- if (onlyHighlight) {
- return (
-
- {highlighted && hightlightNode}
-
- );
- }
+ const clipId = `mask-${Math.random()}`;
+ const {height, vp} = getMetricValue(metrics, size);
return (
- {highlighted && hightlightNode}
+
+
+
+
+
+ {highlighted && }
-
+
+ {highlighted ?
+
+ {formatCanvasMetric(vp)}
+ :
+ }
);
}
diff --git a/client/app/scripts/charts/node-shape-hex.js b/client/app/scripts/charts/node-shape-hex.js
index 42236d8b5..dd4ab6cfb 100644
--- a/client/app/scripts/charts/node-shape-hex.js
+++ b/client/app/scripts/charts/node-shape-hex.js
@@ -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 = ;
+ const shadowSize = 0.45;
+ const upperHexBitHeight = -0.25 * size * shadowSize;
- if (onlyHighlight) {
- return (
-
- {highlighted && hightlightNode}
-
- );
- }
+ const clipId = `mask-${Math.random()}`;
+ const {height, vp} = getMetricValue(metrics, size);
return (
- {highlighted && hightlightNode}
+
+
+
+
+
+ {highlighted && }
-
-
+
+
+ {highlighted ?
+
+ {formatCanvasMetric(vp)}
+ :
+ }
);
}
diff --git a/client/app/scripts/charts/node-shape-rounded-square.js b/client/app/scripts/charts/node-shape-rounded-square.js
index 4542595b7..dbbe4da81 100644
--- a/client/app/scripts/charts/node-shape-rounded-square.js
+++ b/client/app/scripts/charts/node-shape-rounded-square.js
@@ -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 (
diff --git a/client/app/scripts/charts/node-shape-square.js b/client/app/scripts/charts/node-shape-square.js
index 5308a72a5..2c28e82bd 100644
--- a/client/app/scripts/charts/node-shape-square.js
+++ b/client/app/scripts/charts/node-shape-square.js
@@ -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 = ;
-
- if (onlyHighlight) {
- return (
-
- {highlighted && hightlightNode}
-
- );
- }
+ const clipId = `mask-${Math.random()}`;
+ const {height, vp} = getMetricValue(metrics, size);
return (
- {highlighted && hightlightNode}
+
+
+
+
+
+ {highlighted && }
-
+
+ {highlighted ?
+
+ {formatCanvasMetric(vp)}
+ :
+ }
);
}
diff --git a/client/app/scripts/charts/node-shape-stack.js b/client/app/scripts/charts/node-shape-stack.js
index e669a0d61..13b6c0f58 100644
--- a/client/app/scripts/charts/node-shape-stack.js
+++ b/client/app/scripts/charts/node-shape-stack.js
@@ -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 (
-
-
+
+
-
+
-
+
-
+
);
}
diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js
index ee131c663..a9e411b06 100644
--- a/client/app/scripts/charts/nodes-chart.js
+++ b/client/app/scripts/charts/nodes-chart.js
@@ -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
diff --git a/client/app/scripts/components/debug-toolbar.js b/client/app/scripts/components/debug-toolbar.js
index bdb545ec5..5c880053a 100644
--- a/client/app/scripts/components/debug-toolbar.js
+++ b/client/app/scripts/components/debug-toolbar.js
@@ -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));
+ })
});
}
diff --git a/client/app/scripts/stores/app-store.js b/client/app/scripts/stores/app-store.js
index d6b266884..ebfef2e41 100644
--- a/client/app/scripts/stores/app-store.js
+++ b/client/app/scripts/stores/app-store.js
@@ -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)));
}
});
diff --git a/client/app/scripts/utils/data-utils.js b/client/app/scripts/utils/data-utils.js
new file mode 100644
index 000000000..600abaffc
--- /dev/null
+++ b/client/app/scripts/utils/data-utils.js
@@ -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)) + '%';
+}
diff --git a/client/app/styles/main.less b/client/app/styles/main.less
index ff19b8562..e9d80b75b 100644
--- a/client/app/styles/main.less
+++ b/client/app/styles/main.less
@@ -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 {