Tidying up MoC

- no rand ids, org code
- Fixes tests, no .includes in jest for now
- Small comment on moc stuff
- Patch up differences after MoC rebase
This commit is contained in:
Simon Howe
2016-03-24 11:13:06 +01:00
parent 3fdd7809f7
commit 9d968a789b
15 changed files with 181 additions and 169 deletions

View File

@@ -5,7 +5,6 @@ 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,
@@ -271,13 +270,12 @@ export function receiveNodeDetails(details) {
}
export function receiveNodesDelta(delta) {
const deltaWithMetrics = addMetrics(delta, AppStore.getNodes());
if (AppStore.isUpdatePaused()) {
bufferDeltaUpdate(deltaWithMetrics);
bufferDeltaUpdate(delta);
} else {
AppDispatcher.dispatch({
type: ActionTypes.RECEIVE_NODES_DELTA,
delta: deltaWithMetrics
delta
});
}
}

View File

@@ -1,20 +1,20 @@
import React from 'react';
import classNames from 'classnames';
import {getMetricValue, getMetricColor} from '../utils/data-utils.js';
import {getMetricValue, getMetricColor} from '../utils/metric-utils.js';
import {CANVAS_METRIC_FONT_SIZE} from '../constants/styles.js';
export default function NodeShapeCircle({highlighted, size, color, metric}) {
export default function NodeShapeCircle({id, highlighted, size, color, metric}) {
const hightlightNode = <circle r={size * 0.7} className="highlighted" />;
const clipId = `mask-${Math.random()}`;
const clipId = `mask-${id}`;
const {height, value, formattedValue} = getMetricValue(metric, size);
const className = classNames('shape', {
metrics: value !== null
});
const metricStyle = {
fillOpacity: 0.5,
fill: getMetricColor()
fill: getMetricColor(metric)
};
const fontSize = size * 0.19;
const fontSize = size * CANVAS_METRIC_FONT_SIZE;
return (
<g className={className}>

View File

@@ -1,7 +1,8 @@
import React from 'react';
import d3 from 'd3';
import classNames from 'classnames';
import {getMetricValue, getMetricColor} from '../utils/data-utils.js';
import {getMetricValue, getMetricColor} from '../utils/metric-utils.js';
import {CANVAS_METRIC_FONT_SIZE} from '../constants/styles.js';
const line = d3.svg.line()
.interpolate('cardinal-closed')
@@ -16,24 +17,23 @@ function polygon(r, sides) {
return points;
}
export default function NodeShapeHeptagon({highlighted, size, color, metric}) {
export default function NodeShapeHeptagon({id, highlighted, size, color, metric}) {
const scaledSize = size * 1.0;
const pathProps = v => ({
d: line(polygon(scaledSize * v, 7)),
transform: 'rotate(90)'
});
const clipId = `mask-${Math.random()}`;
const clipId = `mask-${id}`;
const {height, value, formattedValue} = getMetricValue(metric, size);
const className = classNames('shape', {
metrics: value !== null
});
const fontSize = size * CANVAS_METRIC_FONT_SIZE;
const metricStyle = {
fillOpacity: 0.5,
fill: getMetricColor()
fill: getMetricColor(metric)
};
const fontSize = size * 0.19;
return (
<g className={className}>

View File

@@ -1,7 +1,8 @@
import React from 'react';
import d3 from 'd3';
import classNames from 'classnames';
import {getMetricValue, getMetricColor} from '../utils/data-utils.js';
import {getMetricValue, getMetricColor} from '../utils/metric-utils.js';
import {CANVAS_METRIC_FONT_SIZE} from '../constants/styles.js';
const line = d3.svg.line()
.interpolate('cardinal-closed')
@@ -26,7 +27,7 @@ function getPoints(h) {
}
export default function NodeShapeHex({highlighted, size, color, metric}) {
export default function NodeShapeHex({id, highlighted, size, color, metric}) {
const pathProps = v => ({
d: getPoints(size * v * 2),
transform: `rotate(90) translate(-${size * getWidth(v)}, -${size * v})`
@@ -34,16 +35,15 @@ export default function NodeShapeHex({highlighted, size, color, metric}) {
const shadowSize = 0.45;
const upperHexBitHeight = -0.25 * size * shadowSize;
const fontSize = size * 0.19;
const fontSize = size * CANVAS_METRIC_FONT_SIZE;
const clipId = `mask-${Math.random()}`;
const clipId = `mask-${id}`;
const {height, value, formattedValue} = getMetricValue(metric, size);
const className = classNames('shape', {
metrics: value !== null
});
const metricStyle = {
fillOpacity: 0.5,
fill: getMetricColor()
fill: getMetricColor(metric)
};
return (

View File

@@ -1,29 +1,28 @@
import React from 'react';
import classNames from 'classnames';
import {getMetricValue, getMetricColor} from '../utils/data-utils.js';
import {getMetricValue, getMetricColor} from '../utils/metric-utils.js';
import {CANVAS_METRIC_FONT_SIZE} from '../constants/styles.js';
export default function NodeShapeSquare({
highlighted, size, color, rx = 0, ry = 0, metric
id, highlighted, size, color, rx = 0, ry = 0, metric
}) {
const rectProps = (v, vr) => ({
width: v * size * 2,
height: v * size * 2,
rx: (vr || v) * size * rx,
ry: (vr || v) * size * ry,
x: -size * v,
y: -size * v
const rectProps = (scale, radiusScale) => ({
width: scale * size * 2,
height: scale * size * 2,
rx: (radiusScale || scale) * size * rx,
ry: (radiusScale || scale) * size * ry,
x: -size * scale,
y: -size * scale
});
const clipId = `mask-${Math.random()}`;
const clipId = `mask-${id}`;
const {height, value, formattedValue} = getMetricValue(metric, size);
const className = classNames('shape', {
metrics: value !== null
});
const fontSize = size * 0.19;
const fontSize = size * CANVAS_METRIC_FONT_SIZE;
const metricStyle = {
fillOpacity: 0.5,
fill: getMetricColor()
fill: getMetricColor(metric)
};
return (

View File

@@ -131,6 +131,14 @@ export default class NodesChart extends React.Component {
return 1;
};
const metric = node => {
const met = node.get('metrics') && node.get('metrics')
.filter(m => m.get('id') === this.props.selectedMetric)
.first();
console.log(met);
return met;
};
return nodes
.toIndexedSeq()
.map(setHighlighted)
@@ -151,7 +159,7 @@ export default class NodesChart extends React.Component {
pseudo={node.get('pseudo')}
nodeCount={node.get('nodeCount')}
subLabel={node.get('subLabel')}
metric={node.getIn(['metrics', this.props.selectedMetric])}
metric={metric(node)}
rank={node.get('rank')}
selectedNodeScale={selectedNodeScale}
nodeScale={nodeScale}

View File

@@ -139,14 +139,14 @@ export default class App extends React.Component {
topologyId={this.state.currentTopologyId} />
<Sidebar>
<Status errorUrl={this.state.errorUrl} topology={this.state.currentTopology}
topologiesLoaded={this.state.topologiesLoaded}
websocketClosed={this.state.websocketClosed} />
{this.state.availableCanvasMetrics.length > 0 && <MetricSelector
availableCanvasMetrics={this.state.availableCanvasMetrics}
lockedMetric={this.state.lockedMetric}
selectedMetric={this.state.selectedMetric}
/>}
<Status errorUrl={this.state.errorUrl} topology={this.state.currentTopology}
topologiesLoaded={this.state.topologiesLoaded}
websocketClosed={this.state.websocketClosed} />
<TopologyOptions options={this.state.currentTopologyOptions}
topologyId={this.state.currentTopologyId}
activeOptions={this.state.activeTopologyOptions} />

View File

@@ -33,9 +33,9 @@ export class MetricSelectorItem extends React.Component {
const id = metric.id;
const isLocked = (id === lockedMetric);
const isSelected = (id === selectedMetric);
const className = classNames('sidebar-item', {
locked: isLocked,
selected: isSelected
const className = classNames('metric-selector-action', {
'metric-selector-action-locked': isLocked,
'metric-selector-action-selected': isSelected
});
return (
@@ -45,9 +45,7 @@ export class MetricSelectorItem extends React.Component {
onMouseOver={this.onMouseOver}
onClick={this.onMouseClick}>
{metric.label}
{isLocked && <span className="sidebar-item-actions">
<span className="sidebar-item-action fa fa-thumb-tack"></span>
</span>}
{isLocked && <span className="fa fa-thumb-tack"></span>}
</div>
);
}

View File

@@ -28,12 +28,10 @@ export default class MetricSelector extends React.Component {
return (
<div
className="available-metrics"
onMouseLeave={this.onMouseOut}>
<div className="sidebar-item">
METRICS
className="metric-selector">
<div className="metric-selector-wrapper" onMouseLeave={this.onMouseOut}>
{items}
</div>
{items}
</div>
);
}

View File

@@ -8,3 +8,5 @@ export const DETAILS_PANEL_MARGINS = {
};
export const DETAILS_PANEL_OFFSET = 8;
export const CANVAS_METRIC_FONT_SIZE = 0.19;

View File

@@ -8,7 +8,6 @@ import ActionTypes from '../constants/action-types';
import { EDGE_ID_SEPARATOR } from '../constants/naming';
import { findTopologyById, setTopologyUrlsById, updateTopologyIds,
filterHiddenTopologies } from '../utils/topology-utils';
import { METRIC_LABELS } from '../utils/data-utils';
const makeList = List;
const makeMap = Map;
@@ -611,15 +610,17 @@ export class AppStore extends Store {
availableCanvasMetrics = nodes
.valueSeq()
.flatMap(n => (n.get('metrics') || makeMap()).keys())
.flatMap(n => (n.get('metrics') || makeList()).map(m => (
makeMap({id: m.get('id'), label: m.get('label')})
)))
.toSet()
.sortBy(n => METRIC_LABELS[n])
.toJS()
.map(v => ({id: v, label: METRIC_LABELS[v]}));
.sortBy(m => m.get('label'))
.toJS();
const similarTypeMetric = availableCanvasMetrics.find(m => m.label === lockedMetricType);
lockedMetric = similarTypeMetric && similarTypeMetric.id;
if (!availableCanvasMetrics.map(m => m.id).includes(selectedMetric)) {
// if something in the current topo is not already selected, select it.
if (availableCanvasMetrics.map(m => m.id).indexOf(selectedMetric) === -1) {
selectedMetric = lockedMetric;
}

View File

@@ -1,8 +1,5 @@
import _ from 'lodash';
import d3 from 'd3';
import { formatMetric } from './string-utils';
import { colors } from './color-utils';
import AppStore from '../stores/app-store';
// Inspired by Lee Byron's test data generator.
@@ -61,20 +58,6 @@ export function label(m) {
}
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
@@ -151,57 +134,3 @@ export function addMetrics(delta, prevNodes) {
update: handleUpdated(delta.update, prevNodes)
});
}
const openFilesScale = d3.scale.log().domain([1, 100000]).range([0, 1]);
//
// loadScale(1) == 0.5; E.g. a nicely balanced system :).
const loadScale = d3.scale.log().domain([0.01, 100]).range([0, 1]);
export function getMetricValue(metric, size) {
if (!metric) {
return {height: 0, value: null, formattedValue: 'n/a'};
}
const max = metric.getIn(['max']);
const value = metric.getIn(['samples', 0, 'value']);
const selectedMetric = AppStore.getSelectedMetric();
let valuePercentage = value === 0 ? 0 : value / max;
if (selectedMetric === 'open_files_count') {
valuePercentage = openFilesScale(value);
} else if (_.includes(['load1', 'load5', 'load15'], selectedMetric)) {
valuePercentage = loadScale(value);
}
let displayedValue = Number(value).toFixed(1);
if (displayedValue > 0) {
const baseline = 0.1;
displayedValue = valuePercentage * (1 - baseline) + baseline;
}
const height = size * displayedValue;
const metricWithFormat = Object.assign(
{}, {format: METRIC_FORMATS[selectedMetric]}, metric.toJS());
return {
height,
value,
formattedValue: formatMetric(value, metricWithFormat, true)
};
}
export function getMetricColor() {
const selectedMetric = AppStore.getSelectedMetric();
// bluey
if (/memory/.test(selectedMetric)) {
return '#1f77b4';
} else if (/cpu/.test(selectedMetric)) {
return colors('cpu');
} else if (/files/.test(selectedMetric)) {
// return colors('files');
// purple
return '#9467bd';
} else if (/load/.test(selectedMetric)) {
return colors('load');
}
return 'steelBlue';
}

View File

@@ -1,5 +1,21 @@
// http://stackoverflow.com/questions/4467539/javascript-modulo-not-behaving
//
// A modulo that "behaves" w/ negatives.
//
// modulo(5, 5) => 0
// modulo(4, 5) => 4
// modulo(3, 5) => 3
// modulo(2, 5) => 2
// modulo(1, 5) => 1
// modulo(0, 5) => 0
// modulo(-1, 5) => 4
// modulo(-2, 5) => 3
// modulo(-2, 5) => 3
// modulo(-3, 5) => 2
// modulo(-4, 5) => 1
// modulo(-5, 5) => 0
//
export function modulo(i, n) {
return ((i % n) + n) % n;
}

View File

@@ -0,0 +1,56 @@
import _ from 'lodash';
import d3 from 'd3';
import { formatMetric } from './string-utils';
import { colors } from './color-utils';
const openFilesScale = d3.scale.log().domain([1, 100000]).range([0, 1]);
//
// loadScale(1) == 0.5; E.g. a nicely balanced system :).
const loadScale = d3.scale.log().domain([0.01, 100]).range([0, 1]);
export function getMetricValue(metric, size) {
if (!metric) {
return {height: 0, value: null, formattedValue: 'n/a'};
}
const m = metric.toJS();
const value = m.value;
let valuePercentage = value === 0 ? 0 : value / m.max;
if (m.id === 'open_files_count') {
valuePercentage = openFilesScale(value);
} else if (_.includes(['load1', 'load5', 'load15'], m.id)) {
valuePercentage = loadScale(value);
}
let displayedValue = Number(value).toFixed(1);
if (displayedValue > 0) {
const baseline = 0.1;
displayedValue = valuePercentage * (1 - baseline) + baseline;
}
const height = size * displayedValue;
return {
height,
value,
formattedValue: formatMetric(value, m, true)
};
}
export function getMetricColor(metric) {
const selectedMetric = metric && metric.get('id');
// bluey
if (/memory/.test(selectedMetric)) {
return '#1f77b4';
} else if (/cpu/.test(selectedMetric)) {
return colors('cpu');
} else if (/files/.test(selectedMetric)) {
// return colors('files');
// purple
return '#9467bd';
} else if (/load/.test(selectedMetric)) {
return colors('load');
}
return 'steelBlue';
}

View File

@@ -401,6 +401,7 @@ h2 {
.stack .shape .metric-fill {
display: none;
}
.stack .onlyMetrics .shape .metric-fill {
display: inline-block;
}
@@ -422,6 +423,7 @@ h2 {
.metric-fill {
stroke: none;
fill: #A0BE7E;
fill-opacity: 0.7;
}
.shadow {
@@ -1000,50 +1002,55 @@ h2 {
}
}
.topology-options {
.topology-option, .metric-selector {
color: @text-secondary-color;
margin: 6px 0;
.topology-option {
color: @text-secondary-color;
margin: 6px 0;
&:last-child {
margin-bottom: 0;
}
&-wrapper {
border-radius: @border-radius;
border: 1px solid @background-darker-color;
display: inline-block;
}
&-action {
.btn-opacity;
padding: 3px 12px;
cursor: pointer;
display: inline-block;
&-selected, &:hover {
color: @text-darker-color;
background-color: @background-darker-color;
}
&-selected {
cursor: default;
}
&:first-child {
border-left: none;
border-top-left-radius: @border-radius;
border-bottom-left-radius: @border-radius;
}
&:last-child {
border-top-right-radius: @border-radius;
border-bottom-right-radius: @border-radius;
}
}
&:last-child {
margin-bottom: 0;
}
.fa {
margin-left: 4px;
color: darkred;
}
&-wrapper {
border-radius: @border-radius;
border: 1px solid @background-darker-color;
display: inline-block;
}
&-action {
.btn-opacity;
padding: 3px 12px;
cursor: pointer;
display: inline-block;
&-selected, &:hover {
color: @text-darker-color;
background-color: @background-darker-color;
}
&:first-child {
border-left: none;
border-top-left-radius: @border-radius;
border-bottom-left-radius: @border-radius;
}
&:last-child {
border-top-right-radius: @border-radius;
border-bottom-right-radius: @border-radius;
}
}
}
.topology-option {
&-action {
&-selected {
cursor: default;
}
}
}
.sidebar {