From 9d968a789b93b1db89c5353b2ea5faeab5c57a40 Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Thu, 24 Mar 2016 11:13:06 +0100 Subject: [PATCH] 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 --- client/app/scripts/actions/app-actions.js | 6 +- .../app/scripts/charts/node-shape-circle.js | 12 +-- .../app/scripts/charts/node-shape-heptagon.js | 12 +-- client/app/scripts/charts/node-shape-hex.js | 12 +-- .../app/scripts/charts/node-shape-square.js | 27 +++--- client/app/scripts/charts/nodes-chart.js | 10 ++- client/app/scripts/components/app.js | 6 +- .../components/metric-selector-item.js | 10 +-- .../app/scripts/components/metric-selector.js | 8 +- client/app/scripts/constants/styles.js | 2 + client/app/scripts/stores/app-store.js | 13 +-- ...{data-utils.js => data-generator-utils.js} | 71 --------------- client/app/scripts/utils/math-utils.js | 16 ++++ client/app/scripts/utils/metric-utils.js | 56 ++++++++++++ client/app/styles/main.less | 89 ++++++++++--------- 15 files changed, 181 insertions(+), 169 deletions(-) rename client/app/scripts/utils/{data-utils.js => data-generator-utils.js} (59%) create mode 100644 client/app/scripts/utils/metric-utils.js diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index cf3314440..905aa7e4e 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -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 }); } } diff --git a/client/app/scripts/charts/node-shape-circle.js b/client/app/scripts/charts/node-shape-circle.js index 19b086c97..997dfebdd 100644 --- a/client/app/scripts/charts/node-shape-circle.js +++ b/client/app/scripts/charts/node-shape-circle.js @@ -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 = ; - 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 ( diff --git a/client/app/scripts/charts/node-shape-heptagon.js b/client/app/scripts/charts/node-shape-heptagon.js index 301e6b612..d807e843c 100644 --- a/client/app/scripts/charts/node-shape-heptagon.js +++ b/client/app/scripts/charts/node-shape-heptagon.js @@ -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 ( diff --git a/client/app/scripts/charts/node-shape-hex.js b/client/app/scripts/charts/node-shape-hex.js index 25b61aedb..4ae2fa39d 100644 --- a/client/app/scripts/charts/node-shape-hex.js +++ b/client/app/scripts/charts/node-shape-hex.js @@ -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 ( diff --git a/client/app/scripts/charts/node-shape-square.js b/client/app/scripts/charts/node-shape-square.js index 818fc5446..adc4c0240 100644 --- a/client/app/scripts/charts/node-shape-square.js +++ b/client/app/scripts/charts/node-shape-square.js @@ -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 ( diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index 6d6395272..172f82893 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -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} diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index 45a6c5df2..b85025b31 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -139,14 +139,14 @@ export default class App extends React.Component { topologyId={this.state.currentTopologyId} /> + {this.state.availableCanvasMetrics.length > 0 && } - diff --git a/client/app/scripts/components/metric-selector-item.js b/client/app/scripts/components/metric-selector-item.js index 24d628e0a..6b2081b33 100644 --- a/client/app/scripts/components/metric-selector-item.js +++ b/client/app/scripts/components/metric-selector-item.js @@ -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 && - - } + {isLocked && } ); } diff --git a/client/app/scripts/components/metric-selector.js b/client/app/scripts/components/metric-selector.js index d6ae3777e..75fa8128c 100644 --- a/client/app/scripts/components/metric-selector.js +++ b/client/app/scripts/components/metric-selector.js @@ -28,12 +28,10 @@ export default class MetricSelector extends React.Component { return (
-
- METRICS + className="metric-selector"> +
+ {items}
- {items}
); } diff --git a/client/app/scripts/constants/styles.js b/client/app/scripts/constants/styles.js index 44d3afc85..f626c6733 100644 --- a/client/app/scripts/constants/styles.js +++ b/client/app/scripts/constants/styles.js @@ -8,3 +8,5 @@ export const DETAILS_PANEL_MARGINS = { }; export const DETAILS_PANEL_OFFSET = 8; + +export const CANVAS_METRIC_FONT_SIZE = 0.19; diff --git a/client/app/scripts/stores/app-store.js b/client/app/scripts/stores/app-store.js index 4eea62d0d..33f427a50 100644 --- a/client/app/scripts/stores/app-store.js +++ b/client/app/scripts/stores/app-store.js @@ -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; } diff --git a/client/app/scripts/utils/data-utils.js b/client/app/scripts/utils/data-generator-utils.js similarity index 59% rename from client/app/scripts/utils/data-utils.js rename to client/app/scripts/utils/data-generator-utils.js index 7f45c7382..e79717eeb 100644 --- a/client/app/scripts/utils/data-utils.js +++ b/client/app/scripts/utils/data-generator-utils.js @@ -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'; -} diff --git a/client/app/scripts/utils/math-utils.js b/client/app/scripts/utils/math-utils.js index 7389776d4..31911ade8 100644 --- a/client/app/scripts/utils/math-utils.js +++ b/client/app/scripts/utils/math-utils.js @@ -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; } diff --git a/client/app/scripts/utils/metric-utils.js b/client/app/scripts/utils/metric-utils.js new file mode 100644 index 000000000..148c2e022 --- /dev/null +++ b/client/app/scripts/utils/metric-utils.js @@ -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'; +} diff --git a/client/app/styles/main.less b/client/app/styles/main.less index be28b7e8a..14b79d5b2 100644 --- a/client/app/styles/main.less +++ b/client/app/styles/main.less @@ -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 {