Return to mock-metrics for now.

- Bring MoC under new linting rules
- adds support for immutable-console-renderer
- fixes up metrics actually displaying after bad fixes when rebasing.
- Mock all metrics client side for demoing
This commit is contained in:
Simon Howe
2016-03-16 23:10:10 +01:00
parent 31ee76a1d9
commit 1ad7c2c7a3
15 changed files with 221 additions and 145 deletions

View File

@@ -5,6 +5,7 @@ import ActionTypes from '../constants/action-types';
import { saveGraph } from '../utils/file-utils';
import { modulo } from '../utils/math-utils';
import { updateRoute } from '../utils/router-utils';
import { addMetrics } from '../utils/data-utils';
import { bufferDeltaUpdate, resumeUpdate,
resetUpdateBuffer } from '../utils/update-buffer-utils';
import { doControlRequest, getNodesDelta, getNodeDetails,
@@ -16,7 +17,7 @@ const log = debug('scope:app-actions');
export function selectMetric(metricId) {
AppDispatcher.dispatch({
type: ActionTypes.SELECT_METRIC,
metricId: metricId
metricId
});
}
@@ -34,7 +35,7 @@ export function lockNextMetric(delta) {
export function lockMetric(metricId) {
AppDispatcher.dispatch({
type: ActionTypes.LOCK_METRIC,
metricId: metricId
metricId
});
}
@@ -265,16 +266,18 @@ export function receiveNodeDetails(details) {
}
export function receiveNodesDelta(delta) {
const deltaWithMetrics = addMetrics(delta, AppStore.getNodes());
if (AppStore.isUpdatePaused()) {
bufferDeltaUpdate(delta);
bufferDeltaUpdate(deltaWithMetrics);
} else {
AppDispatcher.dispatch({
type: ActionTypes.RECEIVE_NODES_DELTA,
delta: delta
delta: deltaWithMetrics
});
}
}
export function receiveTopologies(topologies) {
AppDispatcher.dispatch({
type: ActionTypes.RECEIVE_TOPOLOGIES,

View File

@@ -8,7 +8,7 @@ export default function NodeShapeCircle({highlighted, size, color, metric}) {
const {height, value, formattedValue} = getMetricValue(metric, size);
const className = classNames('shape', {
'metrics': value !== null
metrics: value !== null
});
const metricStyle = {
fillOpacity: 0.5,
@@ -30,9 +30,10 @@ export default function NodeShapeCircle({highlighted, size, color, metric}) {
{highlighted && hightlightNode}
<circle r={size * 0.5} className="border" stroke={color} />
<circle r={size * 0.45} className="shadow" />
<circle r={size * 0.45} className="metric-fill" style={metricStyle} clipPath={`url(#${clipId})`} />
<circle r={size * 0.45} className="metric-fill" style={metricStyle}
clipPath={`url(#${clipId})`} />
{highlighted && value !== null ?
<text dy="0.35em" style={{'textAnchor': 'middle'}}>{formattedValue}</text> :
<text dy="0.35em" style={{textAnchor: 'middle'}}>{formattedValue}</text> :
<circle className="node" r={Math.max(2, (size * 0.125))} />}
</g>
);

View File

@@ -27,7 +27,7 @@ export default function NodeShapeHeptagon({highlighted, size, color, metric}) {
const {height, value, formattedValue} = getMetricValue(metric, size);
const className = classNames('shape', {
'metrics': value !== null
metrics: value !== null
});
const metricStyle = {
fillOpacity: 0.5,
@@ -49,9 +49,10 @@ export default function NodeShapeHeptagon({highlighted, size, color, metric}) {
{highlighted && <path className="highlighted" {...pathProps(0.7)} />}
<path className="border" stroke={color} {...pathProps(0.5)} />
<path className="shadow" {...pathProps(0.45)} />
<path className="metric-fill" clipPath={`url(#${clipId})`} style={metricStyle} {...pathProps(0.45)} />
<path className="metric-fill" clipPath={`url(#${clipId})`}
style={metricStyle} {...pathProps(0.45)} />
{highlighted && value !== null ?
<text dy="0.35em" style={{'textAnchor': 'middle'}}>{formattedValue}</text> :
<text dy="0.35em" style={{textAnchor: 'middle'}}>{formattedValue}</text> :
<circle className="node" r={Math.max(2, (size * 0.125))} />}
</g>
);

View File

@@ -38,7 +38,7 @@ export default function NodeShapeHex({highlighted, size, color, metric}) {
const clipId = `mask-${Math.random()}`;
const {height, value, formattedValue} = getMetricValue(metric, size);
const className = classNames('shape', {
'metrics': value !== null
metrics: value !== null
});
const metricStyle = {
fillOpacity: 0.5,
@@ -59,9 +59,10 @@ export default function NodeShapeHex({highlighted, size, color, metric}) {
{highlighted && <path className="highlighted" {...pathProps(0.7)} />}
<path className="border" stroke={color} {...pathProps(0.5)} />
<path className="shadow" {...pathProps(shadowSize)} />
<path className="metric-fill" style={metricStyle} clipPath={`url(#${clipId})`} {...pathProps(shadowSize)} />
<path className="metric-fill" style={metricStyle}
clipPath={`url(#${clipId})`} {...pathProps(shadowSize)} />
{highlighted && value !== null ?
<text dy="0.35em" style={{'textAnchor': 'middle'}}>
<text dy="0.35em" style={{textAnchor: 'middle'}}>
{formattedValue}
</text> :
<circle className="node" r={Math.max(2, (size * 0.125))} />}

View File

@@ -17,7 +17,7 @@ export default function NodeShapeSquare({
const clipId = `mask-${Math.random()}`;
const {height, value, formattedValue} = getMetricValue(metric, size);
const className = classNames('shape', {
'metrics': value !== null
metrics: value !== null
});
const metricStyle = {
@@ -40,9 +40,10 @@ export default function NodeShapeSquare({
{highlighted && <rect className="highlighted" {...rectProps(0.7)} />}
<rect className="border" stroke={color} {...rectProps(0.5, 0.5)} />
<rect className="shadow" {...rectProps(0.45, 0.39)} />
<rect className="metric-fill" style={metricStyle} clipPath={`url(#${clipId})`} {...rectProps(0.45, 0.39)} />
<rect className="metric-fill" style={metricStyle}
clipPath={`url(#${clipId})`} {...rectProps(0.45, 0.39)} />
{highlighted && value !== null ?
<text dy="0.35em" style={{'textAnchor': 'middle'}}>{formattedValue}</text> :
<text dy="0.35em" style={{textAnchor: 'middle'}}>{formattedValue}</text> :
<circle className="node" r={Math.max(2, (size * 0.125))} />}
</g>
);

View File

@@ -244,7 +244,7 @@ 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'),
@@ -425,9 +425,9 @@ export default class NodesChart extends React.Component {
if (!graph) {
return {maxNodesExceeded: true};
}
stateNodes = graph.nodes.mergeDeep(stateNodes.map(node => {
return makeMap({metrics: node.get('metrics')});
}));
stateNodes = graph.nodes.mergeDeep(stateNodes.map(node => makeMap({
metrics: node.get('metrics')
})));
stateEdges = graph.edges;
// save coordinates for restore

View File

@@ -24,7 +24,9 @@ const shapeTypes = {
};
const LABEL_PREFIXES = _.range('A'.charCodeAt(), 'Z'.charCodeAt() + 1).map(n => String.fromCharCode(n));
const LABEL_PREFIXES = _.range('A'.charCodeAt(), 'Z'.charCodeAt() + 1)
.map(n => String.fromCharCode(n));
const randomLetter = () => _.sample(LABEL_PREFIXES);
const deltaAdd = (name, adjacency = [], shape = 'circle', stack = false, nodeCount = 1) => ({
adjacency,
@@ -34,11 +36,11 @@ const deltaAdd = (name, adjacency = [], shape = 'circle', stack = false, nodeCou
node_count: nodeCount,
id: name,
label: name,
label_minor: 'weave-1',
label_minor: name,
latest: {},
metadata: {},
origins: [],
rank: 'alpine'
rank: name
});
@@ -49,14 +51,10 @@ function label(shape, stacked) {
function addAllVariants() {
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);
});
});
}));
const newNodes = _.flattenDeep(STACK_VARIANTS.map(stack => (SHAPES.map(s => {
if (!stack) return [deltaAdd(label(s, stack), [], s, stack, 1)];
return NODE_COUNTS.map(n => deltaAdd(label(s, stack), [], s, stack, n));
}))));
receiveNodesDelta({
add: newNodes
@@ -67,20 +65,19 @@ function addAllVariants() {
function addNodes(n) {
const ns = AppStore.getNodes();
const nodeNames = ns.keySeq().toJS();
const newNodeNames = _.range(ns.size, ns.size + n).map(() => {
return _.sample(LABEL_PREFIXES, 2).join('') + '-zing';
});
const newNodeNames = _.range(ns.size, ns.size + n).map(() => (
`${randomLetter()}${randomLetter()}-zing`
));
const allNodes = _(nodeNames).concat(newNodeNames).value();
receiveNodesDelta({
add: newNodeNames.map((name) => {
return deltaAdd(
name,
sample(allNodes),
_.sample(SHAPES),
_.sample(STACK_VARIANTS),
_.sample(NODE_COUNTS));
})
add: newNodeNames.map((name) => deltaAdd(
name,
sample(allNodes),
_.sample(SHAPES),
_.sample(STACK_VARIANTS),
_.sample(NODE_COUNTS)
))
});
}

View File

@@ -0,0 +1,73 @@
import React from 'react';
import classNames from 'classnames';
import { selectMetric, lockMetric, unlockMetric } from '../actions/app-actions';
const METRIC_LABELS = {
docker_cpu_total_usage: 'Container CPU',
docker_memory_usage: 'Container Memory',
host_cpu_usage_percent: 'Host CPU',
host_mem_usage_bytes: 'Host Memory',
load1: 'Host Load 1',
load15: 'Host Load 15',
load5: 'Host Load 5',
open_files_count: 'Process Open files',
process_cpu_usage_percent: 'Process CPU',
process_memory_usage_bytes: 'Process Memory'
};
export function label(m) {
return METRIC_LABELS[m.id];
}
export class MetricSelectorItem extends React.Component {
constructor(props, context) {
super(props, context);
this.onMouseOver = this.onMouseOver.bind(this);
this.onMouseClick = this.onMouseClick.bind(this);
}
onMouseOver() {
const k = this.props.metric.id;
selectMetric(k);
}
onMouseClick() {
const k = this.props.metric.id;
const lockedMetric = this.props.lockedMetric;
if (k === lockedMetric) {
unlockMetric(k);
} else {
lockMetric(k);
}
}
render() {
const {metric, selectedMetric, lockedMetric} = this.props;
const id = metric.id;
const isLocked = (id === lockedMetric);
const isSelected = (id === selectedMetric);
const className = classNames('sidebar-item', {
locked: isLocked,
selected: isSelected
});
return (
<div
key={id}
className={className}
onMouseOver={this.onMouseOver}
onClick={this.onMouseClick}>
{label(metric)}
{isLocked && <span className="sidebar-item-actions">
<span className="sidebar-item-action fa fa-thumb-tack"></span>
</span>}
</div>
);
}
}

View File

@@ -1,76 +1,42 @@
import React from 'react';
import _ from 'lodash';
import { selectMetric, lockMetric, unlockMetric } from '../actions/app-actions';
import classNames from 'classnames';
import { selectMetric } from '../actions/app-actions';
import { MetricSelectorItem, label } from './metric-selector-item';
// const CROSS = '\u274C';
// const MINUS = '\u2212';
// const DOT = '\u2022';
//
const METRIC_LABELS = {
docker_cpu_total_usage: 'Container CPU',
docker_memory_usage: 'Container Memory',
host_cpu_usage_percent: 'Host CPU',
host_mem_usage_bytes: 'Host Memory',
load1: 'Host Load 1',
load15: 'Host Load 15',
load5: 'Host Load 5',
open_files_count: 'Process Open files',
process_cpu_usage_percent: 'Process CPU',
process_memory_usage_bytes: 'Process Memory'
};
function onMouseOver(k) {
selectMetric(k);
}
export default class MetricSelector extends React.Component {
function onMouseClick(k, lockedMetric) {
if (k === lockedMetric) {
unlockMetric(k);
} else {
lockMetric(k);
constructor(props, context) {
super(props, context);
this.onMouseOut = this.onMouseOut.bind(this);
}
onMouseOut() {
selectMetric(this.props.lockedMetric);
}
render() {
const {availableCanvasMetrics} = this.props;
const items = _.sortBy(availableCanvasMetrics, label).map(metric => (
<MetricSelectorItem key={metric.id} metric={metric} {...this.props} />
));
return (
<div
className="available-metrics"
onMouseLeave={this.onMouseOut}>
<div className="sidebar-item">
METRICS
</div>
{items}
</div>
);
}
}
function onMouseOut(k) {
selectMetric(k);
}
function label(m) {
return METRIC_LABELS[m.id];
}
export default function MetricSelector({availableCanvasMetrics, selectedMetric, lockedMetric}) {
return (
<div
className="available-metrics"
onMouseLeave={() => onMouseOut(lockedMetric)}>
<div className="sidebar-item">
METRICS
</div>
{_.sortBy(availableCanvasMetrics, label).map(m => {
const id = m.id;
const isLocked = (id === lockedMetric);
const isSelected = (id === selectedMetric);
const className = classNames('sidebar-item', {
'locked': isLocked,
'selected': isSelected
});
return (
<div
key={id}
className={className}
onMouseOver={() => onMouseOver(id)}
onClick={() => onMouseClick(id, lockedMetric)}>
{label(m)}
{isLocked && <span className="sidebar-item-actions">
<span className="sidebar-item-action fa fa-thumb-tack"></span>
</span>}
</div>
);
})}
</div>
);
}

View File

@@ -0,0 +1,3 @@
import Immutable from 'immutable';
import installDevTools from 'immutable-devtools';
installDevTools(Immutable);

View File

@@ -589,9 +589,9 @@ export class AppStore extends Store {
});
// update existing nodes
_.each(payload.delta.update, function(node) {
_.each(payload.delta.update, (node) => {
if (nodes.has(node.id)) {
nodes = nodes.set(node.id, nodes.get(node.id).merge(Immutable.fromJS(node)));
nodes = nodes.set(node.id, nodes.get(node.id).merge(fromJS(node)));
}
});
@@ -600,6 +600,14 @@ export class AppStore extends Store {
nodes = nodes.set(node.id, fromJS(makeNode(node)));
});
availableCanvasMetrics = nodes
.valueSeq()
.flatMap(n => (n.get('metrics') || makeMap()).keys())
.toSet()
.sort()
.toJS()
.map(v => ({id: v, label: v}));
if (emitChange) {
this.__emitChange();
}
@@ -627,15 +635,6 @@ export class AppStore extends Store {
}
topologiesLoaded = true;
availableCanvasMetrics = nodes
.valueSeq()
.flatMap(n => (n.get('metrics') || makeMap()).keys())
.toSet()
.sort()
.toJS()
.map(v => {
return {id: v, label: v};
});
this.__emitChange();
break;
}

View File

@@ -5,6 +5,7 @@ import AppStore from '../stores/app-store';
// Inspired by Lee Byron's test data generator.
/*eslint-disable */
function bumpLayer(n, maxValue) {
function bump(a) {
const x = 1 / (0.1 + Math.random());
@@ -24,6 +25,7 @@ function bumpLayer(n, maxValue) {
const s = d3.scale.linear().domain(d3.extent(values)).range([0, maxValue]);
return values.map(s);
}
/*eslint-enable */
const nodeData = {};
@@ -40,17 +42,56 @@ function getNextValue(keyValues, maxValue) {
}
const METRIC_FORMATS = {
docker_cpu_total_usage: 'percent',
docker_memory_usage: 'filesize',
host_cpu_usage_percent: 'percent',
host_mem_usage_bytes: 'filesize',
load1: 'number',
load15: 'number',
load5: 'number',
open_files_count: 'integer',
process_cpu_usage_percent: 'percent',
process_memory_usage_bytes: 'filesize'
};
const memoryMetric = (node, name, max = 1024 * 1024 * 1024) => ({
samples: [{value: getNextValue([node.id, name], max)}],
max
});
const cpuMetric = (node, name, max = 100) => ({
samples: [{value: getNextValue([node.id, name], max)}],
max
});
const fileMetric = (node, name, max = 10000) => ({
samples: [{value: getNextValue([node.id, name], max)}],
max
});
const loadMetric = (node, name, max = 10) => ({
samples: [{value: getNextValue([node.id, name], max)}],
max
});
function mergeMetrics(node) {
if (node.pseudo) {
return 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
}
process_cpu_usage_percent: cpuMetric(node, 'process_cpu_usage_percent'),
process_memory_usage_bytes: memoryMetric(node, 'process_memory_usage_bytes'),
open_files_count: fileMetric(node, 'open_files_count'),
load1: loadMetric(node, 'load1'),
load5: loadMetric(node, 'load5'),
load15: loadMetric(node, 'load15'),
docker_cpu_total_usage: cpuMetric(node, 'docker_cpu_total_usage'),
docker_memory_usage: memoryMetric(node, 'docker_memory_usage'),
host_cpu_usage_percent: cpuMetric(node, 'host_cpu_usage_percent'),
host_mem_usage_bytes: memoryMetric(node, 'host_mem_usage_bytes')
}
});
}
@@ -66,9 +107,9 @@ function handleAdd(nodes) {
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]);
});
return prevNodes.toIndexedSeq().toJS().map(n => (
Object.assign({}, mergeMetrics(n), modifiedNodesIndex[n.id])
));
}
@@ -79,19 +120,6 @@ export function addMetrics(delta, prevNodes) {
});
}
const METRIC_FORMATS = {
docker_cpu_total_usage: 'percent',
docker_memory_usage: 'filesize',
host_cpu_usage_percent: 'percent',
host_mem_usage_bytes: 'filesize',
load1: 'number',
load15: 'number',
load5: 'number',
open_files_count: 'integer',
process_cpu_usage_percent: 'percent',
process_memory_usage_bytes: 'filesize'
};
const openFilesScale = d3.scale.log().domain([1, 100000]).range([0, 1]);
//
// loadScale(1) == 0.5; E.g. a nicely balanced system :).
@@ -123,8 +151,8 @@ export function getMetricValue(metric, size) {
{}, {format: METRIC_FORMATS[selectedMetric]}, metric.toJS());
return {
height: height,
value: value,
height,
value,
formattedValue: formatMetric(value, metricWithFormat, true)
};
}

View File

@@ -22,10 +22,11 @@ function makeFormatters(renderFn) {
},
integer(value) {
const intNumber = Number(value).toFixed(0);
if (value < 1100 && value >= 0) {
return Number(value).toFixed(0);
return intNumber;
}
return formatLargeValue(value);
return formatLargeValue(intNumber);
},
number(value) {

View File

@@ -44,6 +44,7 @@
"eslint-plugin-react": "4.2.2",
"file-loader": "0.8.5",
"http-proxy-rules": "^1.0.1",
"immutable-devtools": "0.0.6",
"jest-cli": "~0.9.2",
"json-loader": "0.5.4",
"less": "~2.6.1",

View File

@@ -26,7 +26,8 @@ module.exports = {
'app': [
'./app/scripts/main',
'webpack-dev-server/client?http://localhost:4041',
'webpack/hot/only-dev-server'
'webpack/hot/only-dev-server',
'./app/scripts/debug'
],
'contrast-app': [
'./app/scripts/contrast-main',