diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 9f8e34608..04d29ffe2 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -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, diff --git a/client/app/scripts/charts/node-shape-circle.js b/client/app/scripts/charts/node-shape-circle.js index 1d1769d33..091b7d3bd 100644 --- a/client/app/scripts/charts/node-shape-circle.js +++ b/client/app/scripts/charts/node-shape-circle.js @@ -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} - + {highlighted && value !== null ? - {formattedValue} : + {formattedValue} : } ); diff --git a/client/app/scripts/charts/node-shape-heptagon.js b/client/app/scripts/charts/node-shape-heptagon.js index bac92324f..71e012e50 100644 --- a/client/app/scripts/charts/node-shape-heptagon.js +++ b/client/app/scripts/charts/node-shape-heptagon.js @@ -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 && } - + {highlighted && value !== null ? - {formattedValue} : + {formattedValue} : } ); diff --git a/client/app/scripts/charts/node-shape-hex.js b/client/app/scripts/charts/node-shape-hex.js index 823daa382..245779c39 100644 --- a/client/app/scripts/charts/node-shape-hex.js +++ b/client/app/scripts/charts/node-shape-hex.js @@ -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 && } - + {highlighted && value !== null ? - + {formattedValue} : } diff --git a/client/app/scripts/charts/node-shape-square.js b/client/app/scripts/charts/node-shape-square.js index 1984352a3..8f14938a9 100644 --- a/client/app/scripts/charts/node-shape-square.js +++ b/client/app/scripts/charts/node-shape-square.js @@ -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 && } - + {highlighted && value !== null ? - {formattedValue} : + {formattedValue} : } ); diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index 81ec16a25..6d6395272 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -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 diff --git a/client/app/scripts/components/debug-toolbar.js b/client/app/scripts/components/debug-toolbar.js index 2a5c3bfa9..8c05cc0f5 100644 --- a/client/app/scripts/components/debug-toolbar.js +++ b/client/app/scripts/components/debug-toolbar.js @@ -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) + )) }); } diff --git a/client/app/scripts/components/metric-selector-item.js b/client/app/scripts/components/metric-selector-item.js new file mode 100644 index 000000000..e9dcb9aaa --- /dev/null +++ b/client/app/scripts/components/metric-selector-item.js @@ -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 ( +
+ {label(metric)} + {isLocked && + + } +
+ ); + } +} diff --git a/client/app/scripts/components/metric-selector.js b/client/app/scripts/components/metric-selector.js index debfdb99c..f88a8dcae 100644 --- a/client/app/scripts/components/metric-selector.js +++ b/client/app/scripts/components/metric-selector.js @@ -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 => ( + + )); + + return ( +
+
+ METRICS +
+ {items} +
+ ); } } -function onMouseOut(k) { - selectMetric(k); -} - -function label(m) { - return METRIC_LABELS[m.id]; -} - -export default function MetricSelector({availableCanvasMetrics, selectedMetric, lockedMetric}) { - return ( -
onMouseOut(lockedMetric)}> -
- METRICS -
- {_.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 ( -
onMouseOver(id)} - onClick={() => onMouseClick(id, lockedMetric)}> - {label(m)} - {isLocked && - - } -
- ); - })} -
- ); -} diff --git a/client/app/scripts/debug.js b/client/app/scripts/debug.js new file mode 100644 index 000000000..a546827ee --- /dev/null +++ b/client/app/scripts/debug.js @@ -0,0 +1,3 @@ +import Immutable from 'immutable'; +import installDevTools from 'immutable-devtools'; +installDevTools(Immutable); diff --git a/client/app/scripts/stores/app-store.js b/client/app/scripts/stores/app-store.js index ca61ad508..c8b7d1042 100644 --- a/client/app/scripts/stores/app-store.js +++ b/client/app/scripts/stores/app-store.js @@ -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; } diff --git a/client/app/scripts/utils/data-utils.js b/client/app/scripts/utils/data-utils.js index 8426e00b8..04d09018b 100644 --- a/client/app/scripts/utils/data-utils.js +++ b/client/app/scripts/utils/data-utils.js @@ -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) }; } diff --git a/client/app/scripts/utils/string-utils.js b/client/app/scripts/utils/string-utils.js index 98ed1703f..f75677583 100644 --- a/client/app/scripts/utils/string-utils.js +++ b/client/app/scripts/utils/string-utils.js @@ -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) { diff --git a/client/package.json b/client/package.json index a32a4b084..2e00b91d7 100644 --- a/client/package.json +++ b/client/package.json @@ -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", diff --git a/client/webpack.local.config.js b/client/webpack.local.config.js index 51604fcc3..0decb0c70 100644 --- a/client/webpack.local.config.js +++ b/client/webpack.local.config.js @@ -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',