From d31aadf7b12519b735f1efeda8ba2167a091ed81 Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Tue, 1 Mar 2016 17:27:29 +0100 Subject: [PATCH 01/21] Metrics on canvas! - Adds metric on canvas support for more shapes - More variation from the debug bar. --- client/app/scripts/actions/app-actions.js | 2 +- .../app/scripts/charts/node-shape-circle.js | 30 ++++-- .../app/scripts/charts/node-shape-heptagon.js | 33 +++--- client/app/scripts/charts/node-shape-hex.js | 35 +++--- .../charts/node-shape-rounded-square.js | 1 + .../app/scripts/charts/node-shape-square.js | 38 ++++--- client/app/scripts/charts/node-shape-stack.js | 21 +--- client/app/scripts/charts/nodes-chart.js | 14 ++- .../app/scripts/components/debug-toolbar.js | 47 +++++--- client/app/scripts/stores/app-store.js | 7 +- client/app/scripts/utils/data-utils.js | 102 ++++++++++++++++++ client/app/styles/main.less | 23 ++++ 12 files changed, 269 insertions(+), 84 deletions(-) create mode 100644 client/app/scripts/utils/data-utils.js 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 { From ef1c69eb2ae2da07af1b988b7205425e7a1d64bb Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Tue, 8 Mar 2016 16:47:45 +0100 Subject: [PATCH 02/21] Basic hover to select metric --- client/app/scripts/actions/app-actions.js | 7 ++++ .../app/scripts/charts/node-shape-circle.js | 6 ++-- .../app/scripts/charts/node-shape-heptagon.js | 6 ++-- client/app/scripts/charts/node-shape-hex.js | 6 ++-- .../app/scripts/charts/node-shape-square.js | 6 ++-- client/app/scripts/charts/nodes-chart.js | 2 +- client/app/scripts/components/app.js | 14 ++++++-- .../app/scripts/components/metric-selector.js | 36 +++++++++++++++++++ client/app/scripts/constants/action-types.js | 3 +- client/app/scripts/stores/app-store.js | 10 ++++++ client/app/scripts/utils/data-utils.js | 16 ++++----- 11 files changed, 87 insertions(+), 25 deletions(-) create mode 100644 client/app/scripts/components/metric-selector.js diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 7387cbc10..c780fb0e8 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -12,6 +12,13 @@ import AppStore from '../stores/app-store'; const log = debug('scope:app-actions'); +export function selectMetric(metricId) { + AppDispatcher.dispatch({ + type: ActionTypes.SELECT_METRIC, + metricId: metricId + }); +} + export function changeTopologyOption(option, value, topologyId) { AppDispatcher.dispatch({ type: ActionTypes.CHANGE_TOPOLOGY_OPTION, diff --git a/client/app/scripts/charts/node-shape-circle.js b/client/app/scripts/charts/node-shape-circle.js index 53d0c7dcd..c234e02a8 100644 --- a/client/app/scripts/charts/node-shape-circle.js +++ b/client/app/scripts/charts/node-shape-circle.js @@ -1,10 +1,10 @@ import React from 'react'; import {formatCanvasMetric, getMetricValue} from '../utils/data-utils.js'; -export default function NodeShapeCircle({highlighted, size, color, metrics}) { +export default function NodeShapeCircle({highlighted, size, color, metric}) { const hightlightNode = ; const clipId = `mask-${Math.random()}`; - const {height, vp} = getMetricValue(metrics, size); + const {height, v} = getMetricValue(metric, size); return ( @@ -24,7 +24,7 @@ export default function NodeShapeCircle({highlighted, size, color, metrics}) { {highlighted ? - {formatCanvasMetric(vp)} + {formatCanvasMetric(v)} : } diff --git a/client/app/scripts/charts/node-shape-heptagon.js b/client/app/scripts/charts/node-shape-heptagon.js index 41e1ce189..84a7324aa 100644 --- a/client/app/scripts/charts/node-shape-heptagon.js +++ b/client/app/scripts/charts/node-shape-heptagon.js @@ -15,7 +15,7 @@ function polygon(r, sides) { return points; } -export default function NodeShapeHeptagon({highlighted, size, color, metrics}) { +export default function NodeShapeHeptagon({highlighted, size, color, metric}) { const scaledSize = size * 1.0; const pathProps = v => ({ d: line(polygon(scaledSize * v, 7)), @@ -23,7 +23,7 @@ export default function NodeShapeHeptagon({highlighted, size, color, metrics}) { }); const clipId = `mask-${Math.random()}`; - const {height, vp} = getMetricValue(metrics, size); + const {height, v} = getMetricValue(metric, size); return ( @@ -43,7 +43,7 @@ export default function NodeShapeHeptagon({highlighted, size, color, metrics}) { {highlighted ? - {formatCanvasMetric(vp)} + {formatCanvasMetric(v)} : } diff --git a/client/app/scripts/charts/node-shape-hex.js b/client/app/scripts/charts/node-shape-hex.js index dd4ab6cfb..f558c812a 100644 --- a/client/app/scripts/charts/node-shape-hex.js +++ b/client/app/scripts/charts/node-shape-hex.js @@ -25,7 +25,7 @@ function getPoints(h) { } -export default function NodeShapeHex({highlighted, size, color, metrics}) { +export default function NodeShapeHex({highlighted, size, color, metric}) { const pathProps = v => ({ d: getPoints(size * v * 2), transform: `rotate(90) translate(-${size * getWidth(v)}, -${size * v})` @@ -35,7 +35,7 @@ export default function NodeShapeHex({highlighted, size, color, metrics}) { const upperHexBitHeight = -0.25 * size * shadowSize; const clipId = `mask-${Math.random()}`; - const {height, vp} = getMetricValue(metrics, size); + const {height, v} = getMetricValue(metric, size); return ( @@ -54,7 +54,7 @@ export default function NodeShapeHex({highlighted, size, color, metrics}) { {highlighted ? - {formatCanvasMetric(vp)} + {formatCanvasMetric(v)} : } diff --git a/client/app/scripts/charts/node-shape-square.js b/client/app/scripts/charts/node-shape-square.js index 2c28e82bd..5da8184a3 100644 --- a/client/app/scripts/charts/node-shape-square.js +++ b/client/app/scripts/charts/node-shape-square.js @@ -2,7 +2,7 @@ import React from 'react'; import {formatCanvasMetric, getMetricValue} from '../utils/data-utils.js'; export default function NodeShapeSquare({ - highlighted, size, color, rx = 0, ry = 0, metrics + highlighted, size, color, rx = 0, ry = 0, metric }) { const rectProps = v => ({ width: v * size * 2, @@ -14,7 +14,7 @@ export default function NodeShapeSquare({ }); const clipId = `mask-${Math.random()}`; - const {height, vp} = getMetricValue(metrics, size); + const {height, v} = getMetricValue(metric, size); return ( @@ -34,7 +34,7 @@ export default function NodeShapeSquare({ {highlighted ? - {formatCanvasMetric(vp)} + {formatCanvasMetric(v)} : } diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index a9e411b06..81ec16a25 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -151,7 +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')} + metric={node.getIn(['metrics', this.props.selectedMetric])} 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 d2e37fa58..cbf0ecd9c 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -11,6 +11,7 @@ import { getApiDetails, getTopologies } from '../utils/web-api-utils'; import { hitEsc } from '../actions/app-actions'; import Details from './details'; import Nodes from './nodes'; +import MetricSelector from './metric-selector'; import EmbeddedTerminal from './embedded-terminal'; import { getRouter } from '../utils/router-utils'; import { showingDebugToolbar, DebugToolbar } from './debug-toolbar.js'; @@ -33,6 +34,7 @@ function getStateFromStores() { nodeDetails: AppStore.getNodeDetails(), nodes: AppStore.getNodes(), selectedNodeId: AppStore.getSelectedNodeId(), + selectedMetric: AppStore.getSelectedMetric(), topologies: AppStore.getTopologies(), topologiesLoaded: AppStore.isTopologiesLoaded(), updatePaused: AppStore.isUpdatePaused(), @@ -103,14 +105,20 @@ export default class App extends React.Component { currentTopology={this.state.currentTopology} /> - + diff --git a/client/app/scripts/components/metric-selector.js b/client/app/scripts/components/metric-selector.js new file mode 100644 index 000000000..b76da5109 --- /dev/null +++ b/client/app/scripts/components/metric-selector.js @@ -0,0 +1,36 @@ +import React from 'react'; +import _ from 'lodash'; +import { selectMetric } from '../actions/app-actions'; +import classNames from 'classnames'; + +const METRICS = { + 'CPU': 'process_cpu_usage_percent', + 'Memory': 'process_memory_usage_bytes', + 'Open Files': 'open_files_count' +}; + +// docker_cpu_total_usage +// docker_memory_usage + +function onMouseOver(k) { + return selectMetric(k); +} + +export default function MetricSelector({selectedMetric}) { + return ( +
+ {_.map(METRICS, (key, name) => { + return ( +
onMouseOver(key)}> + {name} +
+ ); + })} +
+ ); +} diff --git a/client/app/scripts/constants/action-types.js b/client/app/scripts/constants/action-types.js index 047dc8274..87aac25b9 100644 --- a/client/app/scripts/constants/action-types.js +++ b/client/app/scripts/constants/action-types.js @@ -33,7 +33,8 @@ const ACTION_TYPES = [ 'RECEIVE_TOPOLOGIES', 'RECEIVE_API_DETAILS', 'RECEIVE_ERROR', - 'ROUTE_TOPOLOGY' + 'ROUTE_TOPOLOGY', + 'SELECT_METRIC' ]; export default _.zipObject(ACTION_TYPES, ACTION_TYPES); diff --git a/client/app/scripts/stores/app-store.js b/client/app/scripts/stores/app-store.js index ebfef2e41..8e3115666 100644 --- a/client/app/scripts/stores/app-store.js +++ b/client/app/scripts/stores/app-store.js @@ -58,6 +58,7 @@ let routeSet = false; let controlPipes = makeOrderedMap(); // pipeId -> controlPipe let updatePausedAt = null; // Date let websocketClosed = true; +let selectedMetric = 'process_cpu_usage_percent'; const topologySorter = topology => topology.get('rank'); @@ -164,6 +165,10 @@ export class AppStore extends Store { return adjacentNodes; } + getSelectedMetric() { + return selectedMetric; + } + getControlStatus() { return controlStatus.toJS(); } @@ -401,6 +406,11 @@ export class AppStore extends Store { } break; } + case ActionTypes.SELECT_METRIC: { + selectedMetric = payload.metricId; + this.__emitChange(); + break; + } case ActionTypes.DESELECT_NODE: { closeNodeDetails(); this.__emitChange(); diff --git a/client/app/scripts/utils/data-utils.js b/client/app/scripts/utils/data-utils.js index 600abaffc..d7276ce51 100644 --- a/client/app/scripts/utils/data-utils.js +++ b/client/app/scripts/utils/data-utils.js @@ -78,19 +78,19 @@ export function addMetrics(delta, prevNodes) { } -export function getMetricValue(metrics, size) { - if (!metrics) { - return {height: 0, vp: null}; +export function getMetricValue(metric, size) { + if (!metric) { + return {height: 0, v: null}; } - const max = 100; - const v = metrics.getIn(['process_cpu_usage_percent', 'samples', 0, 'value']); + const max = metric.getIn(['max']); + const v = metric.getIn(['samples', 0, 'value']); const vp = v === 0 ? 0 : v / max; - const baseline = 0.00; + const baseline = 0.05; const displayedValue = vp * (1 - baseline) + baseline; const height = size * displayedValue; - return {height, vp}; + return {height, v}; } const formatLargeValue = d3.format('s'); @@ -98,5 +98,5 @@ export function formatCanvasMetric(v) { if (v === null) { return 'n/a'; } - return formatLargeValue(Number(v * 100).toFixed(1)) + '%'; + return formatLargeValue(Number(v).toFixed(1)); } From a104962aa257968dbbedb8d488e0d5346759cfaf Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Tue, 8 Mar 2016 20:10:24 +0100 Subject: [PATCH 03/21] Simple metric selection clicking! Locks onto a metric after you mouseout. --- client/app/scripts/actions/app-actions.js | 7 ++ .../app/scripts/charts/node-shape-circle.js | 8 +- .../app/scripts/charts/node-shape-heptagon.js | 8 +- client/app/scripts/charts/node-shape-hex.js | 6 +- .../app/scripts/charts/node-shape-square.js | 8 +- client/app/scripts/components/app.js | 6 +- .../app/scripts/components/metric-selector.js | 21 +++++- client/app/scripts/constants/action-types.js | 1 + client/app/scripts/stores/app-store.js | 11 +++ client/app/scripts/utils/data-utils.js | 23 +++--- client/app/scripts/utils/string-utils.js | 73 +++++++++++-------- 11 files changed, 105 insertions(+), 67 deletions(-) diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index c780fb0e8..1237dc4de 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -19,6 +19,13 @@ export function selectMetric(metricId) { }); } +export function lockMetric(metricId) { + AppDispatcher.dispatch({ + type: ActionTypes.LOCK_METRIC, + metricId: metricId + }); +} + export function changeTopologyOption(option, value, topologyId) { AppDispatcher.dispatch({ type: ActionTypes.CHANGE_TOPOLOGY_OPTION, diff --git a/client/app/scripts/charts/node-shape-circle.js b/client/app/scripts/charts/node-shape-circle.js index c234e02a8..6f26c9173 100644 --- a/client/app/scripts/charts/node-shape-circle.js +++ b/client/app/scripts/charts/node-shape-circle.js @@ -1,10 +1,10 @@ import React from 'react'; -import {formatCanvasMetric, getMetricValue} from '../utils/data-utils.js'; +import {getMetricValue} from '../utils/data-utils.js'; export default function NodeShapeCircle({highlighted, size, color, metric}) { const hightlightNode = ; const clipId = `mask-${Math.random()}`; - const {height, v} = getMetricValue(metric, size); + const {height, formattedValue} = getMetricValue(metric, size); return ( @@ -23,9 +23,7 @@ export default function NodeShapeCircle({highlighted, size, color, metric}) { {highlighted ? - - {formatCanvasMetric(v)} - : + {formattedValue} : } ); diff --git a/client/app/scripts/charts/node-shape-heptagon.js b/client/app/scripts/charts/node-shape-heptagon.js index 84a7324aa..246b22032 100644 --- a/client/app/scripts/charts/node-shape-heptagon.js +++ b/client/app/scripts/charts/node-shape-heptagon.js @@ -1,6 +1,6 @@ import React from 'react'; import d3 from 'd3'; -import {formatCanvasMetric, getMetricValue} from '../utils/data-utils.js'; +import {getMetricValue} from '../utils/data-utils.js'; const line = d3.svg.line() .interpolate('cardinal-closed') @@ -23,7 +23,7 @@ export default function NodeShapeHeptagon({highlighted, size, color, metric}) { }); const clipId = `mask-${Math.random()}`; - const {height, v} = getMetricValue(metric, size); + const {height, formattedValue} = getMetricValue(metric, size); return ( @@ -42,9 +42,7 @@ export default function NodeShapeHeptagon({highlighted, size, color, metric}) { {highlighted ? - - {formatCanvasMetric(v)} - : + {formattedValue} : } ); diff --git a/client/app/scripts/charts/node-shape-hex.js b/client/app/scripts/charts/node-shape-hex.js index f558c812a..479ff5dbb 100644 --- a/client/app/scripts/charts/node-shape-hex.js +++ b/client/app/scripts/charts/node-shape-hex.js @@ -1,6 +1,6 @@ import React from 'react'; import d3 from 'd3'; -import {formatCanvasMetric, getMetricValue} from '../utils/data-utils.js'; +import {getMetricValue} from '../utils/data-utils.js'; const line = d3.svg.line() .interpolate('cardinal-closed') @@ -35,7 +35,7 @@ export default function NodeShapeHex({highlighted, size, color, metric}) { const upperHexBitHeight = -0.25 * size * shadowSize; const clipId = `mask-${Math.random()}`; - const {height, v} = getMetricValue(metric, size); + const {height, formattedValue} = getMetricValue(metric, size); return ( @@ -54,7 +54,7 @@ export default function NodeShapeHex({highlighted, size, color, metric}) { {highlighted ? - {formatCanvasMetric(v)} + {formattedValue} : } diff --git a/client/app/scripts/charts/node-shape-square.js b/client/app/scripts/charts/node-shape-square.js index 5da8184a3..48bb101ab 100644 --- a/client/app/scripts/charts/node-shape-square.js +++ b/client/app/scripts/charts/node-shape-square.js @@ -1,5 +1,5 @@ import React from 'react'; -import {formatCanvasMetric, getMetricValue} from '../utils/data-utils.js'; +import {getMetricValue} from '../utils/data-utils.js'; export default function NodeShapeSquare({ highlighted, size, color, rx = 0, ry = 0, metric @@ -14,7 +14,7 @@ export default function NodeShapeSquare({ }); const clipId = `mask-${Math.random()}`; - const {height, v} = getMetricValue(metric, size); + const {height, formattedValue} = getMetricValue(metric, size); return ( @@ -33,9 +33,7 @@ export default function NodeShapeSquare({ {highlighted ? - - {formatCanvasMetric(v)} - : + {formattedValue} : } ); diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index cbf0ecd9c..e2a9cc7eb 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -31,6 +31,7 @@ function getStateFromStores() { highlightedEdgeIds: AppStore.getHighlightedEdgeIds(), highlightedNodeIds: AppStore.getHighlightedNodeIds(), hostname: AppStore.getHostname(), + lockedMetric: AppStore.getLockedMetric(), nodeDetails: AppStore.getNodeDetails(), nodes: AppStore.getNodes(), selectedNodeId: AppStore.getSelectedNodeId(), @@ -118,7 +119,10 @@ export default class App extends React.Component { topologyId={this.state.currentTopologyId} /> - + diff --git a/client/app/scripts/components/metric-selector.js b/client/app/scripts/components/metric-selector.js index b76da5109..e2d79f3f3 100644 --- a/client/app/scripts/components/metric-selector.js +++ b/client/app/scripts/components/metric-selector.js @@ -1,6 +1,6 @@ import React from 'react'; import _ from 'lodash'; -import { selectMetric } from '../actions/app-actions'; +import { selectMetric, lockMetric } from '../actions/app-actions'; import classNames from 'classnames'; const METRICS = { @@ -16,17 +16,30 @@ function onMouseOver(k) { return selectMetric(k); } -export default function MetricSelector({selectedMetric}) { +function onMouseClick(k) { + return lockMetric(k); +} + +function onMouseOut(k) { + console.log('onMouseOut', k); + selectMetric(k); +} + +export default function MetricSelector({selectedMetric, lockedMetric}) { return ( -
+
onMouseOut(lockedMetric)}> {_.map(METRICS, (key, name) => { return (
onMouseOver(key)}> + onMouseOver={() => onMouseOver(key)} + onClick={() => onMouseClick(key)}> {name}
); diff --git a/client/app/scripts/constants/action-types.js b/client/app/scripts/constants/action-types.js index 87aac25b9..ab5a20ca2 100644 --- a/client/app/scripts/constants/action-types.js +++ b/client/app/scripts/constants/action-types.js @@ -23,6 +23,7 @@ const ACTION_TYPES = [ 'ENTER_NODE', 'LEAVE_EDGE', 'LEAVE_NODE', + 'LOCK_METRIC', 'OPEN_WEBSOCKET', 'RECEIVE_CONTROL_PIPE', 'RECEIVE_CONTROL_PIPE_STATUS', diff --git a/client/app/scripts/stores/app-store.js b/client/app/scripts/stores/app-store.js index 8e3115666..569142208 100644 --- a/client/app/scripts/stores/app-store.js +++ b/client/app/scripts/stores/app-store.js @@ -58,7 +58,9 @@ let routeSet = false; let controlPipes = makeOrderedMap(); // pipeId -> controlPipe let updatePausedAt = null; // Date let websocketClosed = true; + let selectedMetric = 'process_cpu_usage_percent'; +let lockedMetric = selectedMetric; const topologySorter = topology => topology.get('rank'); @@ -165,6 +167,10 @@ export class AppStore extends Store { return adjacentNodes; } + getLockedMetric() { + return lockedMetric; + } + getSelectedMetric() { return selectedMetric; } @@ -411,6 +417,11 @@ export class AppStore extends Store { this.__emitChange(); break; } + case ActionTypes.LOCK_METRIC: { + lockedMetric = payload.metricId; + this.__emitChange(); + break; + } case ActionTypes.DESELECT_NODE: { closeNodeDetails(); this.__emitChange(); diff --git a/client/app/scripts/utils/data-utils.js b/client/app/scripts/utils/data-utils.js index d7276ce51..d37d7d399 100644 --- a/client/app/scripts/utils/data-utils.js +++ b/client/app/scripts/utils/data-utils.js @@ -1,5 +1,6 @@ import _ from 'lodash'; import d3 from 'd3'; +import { formatMetric } from './string-utils'; // Inspired by Lee Byron's test data generator. @@ -80,23 +81,19 @@ export function addMetrics(delta, prevNodes) { export function getMetricValue(metric, size) { if (!metric) { - return {height: 0, v: null}; + return {height: 0, value: null, formattedValue: 'n/a'}; } const max = metric.getIn(['max']); - const v = metric.getIn(['samples', 0, 'value']); - const vp = v === 0 ? 0 : v / max; + const value = metric.getIn(['samples', 0, 'value']); + const valuePercentage = value === 0 ? 0 : value / max; const baseline = 0.05; - const displayedValue = vp * (1 - baseline) + baseline; + const displayedValue = valuePercentage * (1 - baseline) + baseline; const height = size * displayedValue; - return {height, v}; -} - -const formatLargeValue = d3.format('s'); -export function formatCanvasMetric(v) { - if (v === null) { - return 'n/a'; - } - return formatLargeValue(Number(v).toFixed(1)); + return { + height: height, + value: value, + formattedValue: formatMetric(value, metric.toJS(), true) + }; } diff --git a/client/app/scripts/utils/string-utils.js b/client/app/scripts/utils/string-utils.js index e4f3b9bdb..40dee2791 100644 --- a/client/app/scripts/utils/string-utils.js +++ b/client/app/scripts/utils/string-utils.js @@ -4,43 +4,54 @@ import d3 from 'd3'; const formatLargeValue = d3.format('s'); -const formatters = { - filesize(value) { - const obj = filesize(value, {output: 'object'}); - return formatters.metric(obj.value, obj.suffix); - }, +function toHtml(text, unit) { + return ( + + {text} + {unit} + + ); +} - integer(value) { - if (value < 1100 && value >= 0) { - return Number(value).toFixed(0); + +function makeFormatters(renderFn) { + const formatters = { + filesize(value) { + const obj = filesize(value, {output: 'object'}); + console.log('rendering', value); + return renderFn(obj.value, obj.suffix); + }, + + integer(value) { + if (value < 1100 && value >= 0) { + return Number(value).toFixed(0); + } + return formatLargeValue(value); + }, + + number(value) { + if (value < 1100 && value >= 0) { + return Number(value).toFixed(2); + } + return formatLargeValue(value); + }, + + percent(value) { + return renderFn(formatters.number(value), '%'); } - return formatLargeValue(value); - }, + }; - number(value) { - if (value < 1100 && value >= 0) { - return Number(value).toFixed(2); - } - return formatLargeValue(value); - }, + return formatters; +} - percent(value) { - return formatters.metric(formatters.number(value), '%'); - }, - metric(text, unit) { - return ( - - {text} - {unit} - - ); - } -}; +const formatters = makeFormatters(toHtml); +const svgFormatters = makeFormatters((text, unit) => `${text}${unit}`); -export function formatMetric(value, opts) { - const formatter = opts && formatters[opts.format] ? opts.format : 'number'; - return formatters[formatter](value); +export function formatMetric(value, opts, svg) { + const formatterBase = svg ? svgFormatters : formatters; + const formatter = opts && formatterBase[opts.format] ? opts.format : 'number'; + return formatterBase[formatter](value); } export const formatDate = d3.time.format.iso; From cc3d3920109af0cba6c040b2bb9dfba86d896a65 Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Wed, 9 Mar 2016 10:15:59 +0100 Subject: [PATCH 04/21] Adds < and > keyboard shortcuts for next/prev metric-on-canvas --- client/app/scripts/actions/app-actions.js | 12 ++++++++++ client/app/scripts/components/app.js | 10 +++++++- .../app/scripts/components/metric-selector.js | 24 +++++++------------ client/app/scripts/stores/app-store.js | 11 +++++++++ client/app/scripts/utils/math-utils.js | 6 +++++ client/app/scripts/utils/string-utils.js | 5 ++-- 6 files changed, 48 insertions(+), 20 deletions(-) create mode 100644 client/app/scripts/utils/math-utils.js diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 1237dc4de..272d7088d 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -3,6 +3,7 @@ import debug from 'debug'; import AppDispatcher from '../dispatcher/app-dispatcher'; 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 { bufferDeltaUpdate, resumeUpdate, resetUpdateBuffer } from '../utils/update-buffer-utils'; @@ -19,6 +20,17 @@ export function selectMetric(metricId) { }); } +export function lockNextMetric(delta) { + const metrics = AppStore.getAvailableCanvasMetrics().map(m => m.id); + const currentIndex = metrics.indexOf(AppStore.getSelectedMetric()); + const nextMetric = metrics[modulo(currentIndex + delta, metrics.length)]; + + AppDispatcher.dispatch({ + type: ActionTypes.LOCK_METRIC, + metricId: nextMetric + }); +} + export function lockMetric(metricId) { AppDispatcher.dispatch({ type: ActionTypes.LOCK_METRIC, diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index e2a9cc7eb..8fc19e087 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -8,7 +8,7 @@ import Status from './status.js'; import Topologies from './topologies.js'; import TopologyOptions from './topology-options.js'; import { getApiDetails, getTopologies } from '../utils/web-api-utils'; -import { hitEsc } from '../actions/app-actions'; +import { lockNextMetric, hitEsc } from '../actions/app-actions'; import Details from './details'; import Nodes from './nodes'; import MetricSelector from './metric-selector'; @@ -17,6 +17,8 @@ import { getRouter } from '../utils/router-utils'; import { showingDebugToolbar, DebugToolbar } from './debug-toolbar.js'; const ESC_KEY_CODE = 27; +const RIGHT_ANGLE_KEY_IDENTIFIER = 'U+003C'; +const LEFT_ANGLE_KEY_IDENTIFIER = 'U+003E'; function getStateFromStores() { return { @@ -32,6 +34,7 @@ function getStateFromStores() { highlightedNodeIds: AppStore.getHighlightedNodeIds(), hostname: AppStore.getHostname(), lockedMetric: AppStore.getLockedMetric(), + availableCanvasMetrics: AppStore.getAvailableCanvasMetrics(), nodeDetails: AppStore.getNodeDetails(), nodes: AppStore.getNodes(), selectedNodeId: AppStore.getSelectedNodeId(), @@ -73,6 +76,10 @@ export default class App extends React.Component { onKeyPress(ev) { if (ev.keyCode === ESC_KEY_CODE) { hitEsc(); + } else if (ev.keyIdentifier === RIGHT_ANGLE_KEY_IDENTIFIER) { + lockNextMetric(-1); + } else if (ev.keyIdentifier === LEFT_ANGLE_KEY_IDENTIFIER) { + lockNextMetric(1); } } @@ -120,6 +127,7 @@ export default class App extends React.Component { diff --git a/client/app/scripts/components/metric-selector.js b/client/app/scripts/components/metric-selector.js index e2d79f3f3..89761daa0 100644 --- a/client/app/scripts/components/metric-selector.js +++ b/client/app/scripts/components/metric-selector.js @@ -1,14 +1,7 @@ import React from 'react'; -import _ from 'lodash'; import { selectMetric, lockMetric } from '../actions/app-actions'; import classNames from 'classnames'; -const METRICS = { - 'CPU': 'process_cpu_usage_percent', - 'Memory': 'process_memory_usage_bytes', - 'Open Files': 'open_files_count' -}; - // docker_cpu_total_usage // docker_memory_usage @@ -21,26 +14,25 @@ function onMouseClick(k) { } function onMouseOut(k) { - console.log('onMouseOut', k); selectMetric(k); } -export default function MetricSelector({selectedMetric, lockedMetric}) { +export default function MetricSelector({availableCanvasMetrics, selectedMetric, lockedMetric}) { return (
onMouseOut(lockedMetric)}> - {_.map(METRICS, (key, name) => { + {availableCanvasMetrics.map(({id, label}) => { return (
onMouseOver(key)} - onClick={() => onMouseClick(key)}> - {name} + onMouseOver={() => onMouseOver(id)} + onClick={() => onMouseClick(id)}> + {label}
); })} diff --git a/client/app/scripts/stores/app-store.js b/client/app/scripts/stores/app-store.js index 569142208..37d66125c 100644 --- a/client/app/scripts/stores/app-store.js +++ b/client/app/scripts/stores/app-store.js @@ -61,6 +61,12 @@ let websocketClosed = true; let selectedMetric = 'process_cpu_usage_percent'; let lockedMetric = selectedMetric; +const availableCanvasMetrics = [ + {label: 'CPU', id: 'process_cpu_usage_percent'}, + {label: 'Memory', id: 'process_memory_usage_bytes'}, + {label: 'Open Files', id: 'open_files_count'} +]; + const topologySorter = topology => topology.get('rank'); @@ -175,6 +181,10 @@ export class AppStore extends Store { return selectedMetric; } + getAvailableCanvasMetrics() { + return availableCanvasMetrics; + } + getControlStatus() { return controlStatus.toJS(); } @@ -419,6 +429,7 @@ export class AppStore extends Store { } case ActionTypes.LOCK_METRIC: { lockedMetric = payload.metricId; + selectedMetric = payload.metricId; this.__emitChange(); break; } diff --git a/client/app/scripts/utils/math-utils.js b/client/app/scripts/utils/math-utils.js new file mode 100644 index 000000000..7389776d4 --- /dev/null +++ b/client/app/scripts/utils/math-utils.js @@ -0,0 +1,6 @@ + +// http://stackoverflow.com/questions/4467539/javascript-modulo-not-behaving +export function modulo(i, n) { + return ((i % n) + n) % n; +} + diff --git a/client/app/scripts/utils/string-utils.js b/client/app/scripts/utils/string-utils.js index 40dee2791..046efb71c 100644 --- a/client/app/scripts/utils/string-utils.js +++ b/client/app/scripts/utils/string-utils.js @@ -4,7 +4,7 @@ import d3 from 'd3'; const formatLargeValue = d3.format('s'); -function toHtml(text, unit) { +function renderHtml(text, unit) { return ( {text} @@ -18,7 +18,6 @@ function makeFormatters(renderFn) { const formatters = { filesize(value) { const obj = filesize(value, {output: 'object'}); - console.log('rendering', value); return renderFn(obj.value, obj.suffix); }, @@ -45,7 +44,7 @@ function makeFormatters(renderFn) { } -const formatters = makeFormatters(toHtml); +const formatters = makeFormatters(renderHtml); const svgFormatters = makeFormatters((text, unit) => `${text}${unit}`); export function formatMetric(value, opts, svg) { From 319fe31356d2e4d7fab316e03aeb737aeeed9688 Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Wed, 9 Mar 2016 14:31:02 +0100 Subject: [PATCH 05/21] Show metrics that are available for displayed nodes. - change color to bg - show "x" to remove the metric. - Small debugToolbar enhancements. --- client/app/scripts/actions/app-actions.js | 6 +++ .../app/scripts/charts/node-shape-circle.js | 4 +- .../app/scripts/charts/node-shape-heptagon.js | 4 +- client/app/scripts/charts/node-shape-hex.js | 4 +- .../app/scripts/charts/node-shape-square.js | 4 +- client/app/scripts/components/app.js | 7 ++- .../app/scripts/components/debug-toolbar.js | 48 ++++++++++++++++--- .../app/scripts/components/metric-selector.js | 35 ++++++++++---- client/app/scripts/constants/action-types.js | 1 + client/app/scripts/stores/app-store.js | 23 +++++++-- client/app/styles/main.less | 10 +++- 11 files changed, 114 insertions(+), 32 deletions(-) diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 272d7088d..9f8e34608 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -38,6 +38,12 @@ export function lockMetric(metricId) { }); } +export function unlockMetric() { + AppDispatcher.dispatch({ + type: ActionTypes.UNLOCK_METRIC, + }); +} + export function changeTopologyOption(option, value, topologyId) { AppDispatcher.dispatch({ type: ActionTypes.CHANGE_TOPOLOGY_OPTION, diff --git a/client/app/scripts/charts/node-shape-circle.js b/client/app/scripts/charts/node-shape-circle.js index 6f26c9173..176af9d59 100644 --- a/client/app/scripts/charts/node-shape-circle.js +++ b/client/app/scripts/charts/node-shape-circle.js @@ -4,7 +4,7 @@ import {getMetricValue} from '../utils/data-utils.js'; export default function NodeShapeCircle({highlighted, size, color, metric}) { const hightlightNode = ; const clipId = `mask-${Math.random()}`; - const {height, formattedValue} = getMetricValue(metric, size); + const {height, value, formattedValue} = getMetricValue(metric, size); return ( @@ -22,7 +22,7 @@ export default function NodeShapeCircle({highlighted, size, color, metric}) { - {highlighted ? + {highlighted && value !== null ? {formattedValue} : } diff --git a/client/app/scripts/charts/node-shape-heptagon.js b/client/app/scripts/charts/node-shape-heptagon.js index 246b22032..68eb1274d 100644 --- a/client/app/scripts/charts/node-shape-heptagon.js +++ b/client/app/scripts/charts/node-shape-heptagon.js @@ -23,7 +23,7 @@ export default function NodeShapeHeptagon({highlighted, size, color, metric}) { }); const clipId = `mask-${Math.random()}`; - const {height, formattedValue} = getMetricValue(metric, size); + const {height, value, formattedValue} = getMetricValue(metric, size); return ( @@ -41,7 +41,7 @@ export default function NodeShapeHeptagon({highlighted, size, color, metric}) { - {highlighted ? + {highlighted && value !== null ? {formattedValue} : } diff --git a/client/app/scripts/charts/node-shape-hex.js b/client/app/scripts/charts/node-shape-hex.js index 479ff5dbb..c1b6ffc4e 100644 --- a/client/app/scripts/charts/node-shape-hex.js +++ b/client/app/scripts/charts/node-shape-hex.js @@ -35,7 +35,7 @@ export default function NodeShapeHex({highlighted, size, color, metric}) { const upperHexBitHeight = -0.25 * size * shadowSize; const clipId = `mask-${Math.random()}`; - const {height, formattedValue} = getMetricValue(metric, size); + const {height, value, formattedValue} = getMetricValue(metric, size); return ( @@ -52,7 +52,7 @@ 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 48bb101ab..e2d12f5bc 100644 --- a/client/app/scripts/charts/node-shape-square.js +++ b/client/app/scripts/charts/node-shape-square.js @@ -14,7 +14,7 @@ export default function NodeShapeSquare({ }); const clipId = `mask-${Math.random()}`; - const {height, formattedValue} = getMetricValue(metric, size); + const {height, value, formattedValue} = getMetricValue(metric, size); return ( @@ -32,7 +32,7 @@ export default function NodeShapeSquare({ - {highlighted ? + {highlighted && value !== null ? {formattedValue} : } diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index 8fc19e087..74435858e 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -14,9 +14,11 @@ import Nodes from './nodes'; import MetricSelector from './metric-selector'; import EmbeddedTerminal from './embedded-terminal'; import { getRouter } from '../utils/router-utils'; -import { showingDebugToolbar, DebugToolbar } from './debug-toolbar.js'; +import { showingDebugToolbar, toggleDebugToolbar, + DebugToolbar } from './debug-toolbar.js'; const ESC_KEY_CODE = 27; +const D_KEY_CODE = 68; const RIGHT_ANGLE_KEY_IDENTIFIER = 'U+003C'; const LEFT_ANGLE_KEY_IDENTIFIER = 'U+003E'; @@ -80,6 +82,9 @@ export default class App extends React.Component { lockNextMetric(-1); } else if (ev.keyIdentifier === LEFT_ANGLE_KEY_IDENTIFIER) { lockNextMetric(1); + } else if (ev.keyCode === D_KEY_CODE) { + toggleDebugToolbar(); + this.forceUpdate(); } } diff --git a/client/app/scripts/components/debug-toolbar.js b/client/app/scripts/components/debug-toolbar.js index 5c880053a..60bd30c2d 100644 --- a/client/app/scripts/components/debug-toolbar.js +++ b/client/app/scripts/components/debug-toolbar.js @@ -8,12 +8,14 @@ const log = debug('scope:debug-panel'); import { receiveNodesDelta } from '../actions/app-actions'; import AppStore from '../stores/app-store'; + const SHAPES = ['square', 'hexagon', 'heptagon', 'circle']; const NODE_COUNTS = [1, 2, 3]; const STACK_VARIANTS = [false, true]; const sample = (collection) => _.range(_.random(4)).map(() => _.sample(collection)); + const shapeTypes = { square: ['Process', 'Processes'], hexagon: ['Container', 'Containers'], @@ -21,6 +23,7 @@ const shapeTypes = { 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) => ({ @@ -38,11 +41,13 @@ 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(STACK_VARIANTS.map(stack => { return SHAPES.map(s => { @@ -58,6 +63,7 @@ function addAllVariants() { }); } + function addNodes(n) { const ns = AppStore.getNodes(); const nodeNames = ns.keySeq().toJS(); @@ -78,8 +84,27 @@ function addNodes(n) { }); } + export function showingDebugToolbar() { - return Boolean(localStorage.debugToolbar); + return 'debugToolbar' in localStorage && JSON.parse(localStorage.debugToolbar); +} + + +export function toggleDebugToolbar() { + if ('debugToolbar' in localStorage) { + localStorage.debugToolbar = !showingDebugToolbar(); + } +} + + +function enableLog(ns) { + debug.enable(`scope:${ns}`); + window.location.reload(); +} + +function disableLog() { + debug.disable(); + window.location.reload(); } export class DebugToolbar extends React.Component { @@ -101,12 +126,21 @@ export class DebugToolbar extends React.Component { return (
- - - - - - +
+ + + + + + +
+ +
+ + + + +
); } diff --git a/client/app/scripts/components/metric-selector.js b/client/app/scripts/components/metric-selector.js index 89761daa0..cb5d4255f 100644 --- a/client/app/scripts/components/metric-selector.js +++ b/client/app/scripts/components/metric-selector.js @@ -1,16 +1,24 @@ import React from 'react'; -import { selectMetric, lockMetric } from '../actions/app-actions'; +import { selectMetric, lockMetric, unlockMetric } from '../actions/app-actions'; import classNames from 'classnames'; +const CROSS = '\u274C'; +// const MINUS = '\u2212'; +// const DOT = '\u2022'; + // docker_cpu_total_usage // docker_memory_usage function onMouseOver(k) { - return selectMetric(k); + selectMetric(k); } -function onMouseClick(k) { - return lockMetric(k); +function onMouseClick(k, lockedMetric) { + if (k === lockedMetric) { + unlockMetric(k); + } else { + lockMetric(k); + } } function onMouseOut(k) { @@ -23,16 +31,25 @@ export default function MetricSelector({availableCanvasMetrics, selectedMetric, className="available-metrics" onMouseLeave={() => onMouseOut(lockedMetric)}> {availableCanvasMetrics.map(({id, label}) => { + const isLocked = (id === lockedMetric); + const isSelected = (id === selectedMetric); + const className = classNames('sidebar-item', { + 'locked': isLocked, + 'selected': isSelected + }); + return (
onMouseOver(id)} - onClick={() => onMouseClick(id)}> + onClick={() => onMouseClick(id, lockedMetric)}> {label} + {isLocked && + + {CROSS} + + }
); })} diff --git a/client/app/scripts/constants/action-types.js b/client/app/scripts/constants/action-types.js index ab5a20ca2..151143483 100644 --- a/client/app/scripts/constants/action-types.js +++ b/client/app/scripts/constants/action-types.js @@ -24,6 +24,7 @@ const ACTION_TYPES = [ 'LEAVE_EDGE', 'LEAVE_NODE', 'LOCK_METRIC', + 'UNLOCK_METRIC', 'OPEN_WEBSOCKET', 'RECEIVE_CONTROL_PIPE', 'RECEIVE_CONTROL_PIPE_STATUS', diff --git a/client/app/scripts/stores/app-store.js b/client/app/scripts/stores/app-store.js index 37d66125c..ca61ad508 100644 --- a/client/app/scripts/stores/app-store.js +++ b/client/app/scripts/stores/app-store.js @@ -61,11 +61,7 @@ let websocketClosed = true; let selectedMetric = 'process_cpu_usage_percent'; let lockedMetric = selectedMetric; -const availableCanvasMetrics = [ - {label: 'CPU', id: 'process_cpu_usage_percent'}, - {label: 'Memory', id: 'process_memory_usage_bytes'}, - {label: 'Open Files', id: 'open_files_count'} -]; +let availableCanvasMetrics = []; const topologySorter = topology => topology.get('rank'); @@ -402,6 +398,7 @@ export class AppStore extends Store { setTopology(payload.topologyId); nodes = nodes.clear(); } + availableCanvasMetrics = []; this.__emitChange(); break; } @@ -412,6 +409,7 @@ export class AppStore extends Store { setTopology(payload.topologyId); nodes = nodes.clear(); } + availableCanvasMetrics = []; this.__emitChange(); break; } @@ -433,6 +431,11 @@ export class AppStore extends Store { this.__emitChange(); break; } + case ActionTypes.UNLOCK_METRIC: { + lockedMetric = null; + this.__emitChange(); + break; + } case ActionTypes.DESELECT_NODE: { closeNodeDetails(); this.__emitChange(); @@ -623,6 +626,16 @@ export class AppStore extends Store { setDefaultTopologyOptions(topologies); } 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/styles/main.less b/client/app/styles/main.less index e9d80b75b..8e1a0cf36 100644 --- a/client/app/styles/main.less +++ b/client/app/styles/main.less @@ -409,7 +409,7 @@ h2 { .metric-fill { stroke: none; - fill: yellowgreen; + fill: @background-darker-color; } .shadow { @@ -1054,9 +1054,15 @@ h2 { .debug-panel { .shadow-2; background-color: #fff; - top: 10px; + top: 80px; position: absolute; padding: 10px; left: 10px; z-index: 10000; + + opacity: 0.3; + + &:hover { + opacity: 1; + } } From 0f21c1b5e72fe97ec68f13b293e04bec83e0b427 Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Wed, 9 Mar 2016 19:21:37 +0100 Subject: [PATCH 06/21] Play w/ metric on canvas color and fix nested radius in squircles - Remove node's grey-inner-border when showing metrics --- client/app/scripts/charts/node-shape-circle.js | 7 ++++++- .../app/scripts/charts/node-shape-heptagon.js | 7 ++++++- client/app/scripts/charts/node-shape-hex.js | 6 +++++- client/app/scripts/charts/node-shape-square.js | 18 +++++++++++------- .../app/scripts/components/metric-selector.js | 12 +++++------- client/app/styles/main.less | 6 +++++- 6 files changed, 38 insertions(+), 18 deletions(-) diff --git a/client/app/scripts/charts/node-shape-circle.js b/client/app/scripts/charts/node-shape-circle.js index 176af9d59..227ea41f3 100644 --- a/client/app/scripts/charts/node-shape-circle.js +++ b/client/app/scripts/charts/node-shape-circle.js @@ -1,4 +1,5 @@ import React from 'react'; +import classNames from 'classnames'; import {getMetricValue} from '../utils/data-utils.js'; export default function NodeShapeCircle({highlighted, size, color, metric}) { @@ -6,8 +7,12 @@ export default function NodeShapeCircle({highlighted, size, color, metric}) { const clipId = `mask-${Math.random()}`; const {height, value, formattedValue} = getMetricValue(metric, size); + const className = classNames('shape', { + 'metrics': value !== null + }); + return ( - + + + ({ + const rectProps = (v, vr) => ({ width: v * size * 2, height: v * size * 2, - rx: v * size * rx, - ry: v * size * ry, + rx: (vr || v) * size * rx, + ry: (vr || v) * size * ry, x: -size * v, y: -size * v }); const clipId = `mask-${Math.random()}`; const {height, value, formattedValue} = getMetricValue(metric, size); + const className = classNames('shape', { + 'metrics': value !== null + }); return ( - + {highlighted && } - - - + + + {highlighted && value !== null ? {formattedValue} : } diff --git a/client/app/scripts/components/metric-selector.js b/client/app/scripts/components/metric-selector.js index cb5d4255f..16b4ce825 100644 --- a/client/app/scripts/components/metric-selector.js +++ b/client/app/scripts/components/metric-selector.js @@ -2,13 +2,10 @@ import React from 'react'; import { selectMetric, lockMetric, unlockMetric } from '../actions/app-actions'; import classNames from 'classnames'; -const CROSS = '\u274C'; +// const CROSS = '\u274C'; // const MINUS = '\u2212'; // const DOT = '\u2022'; -// docker_cpu_total_usage -// docker_memory_usage - function onMouseOver(k) { selectMetric(k); } @@ -30,6 +27,9 @@ export default function MetricSelector({availableCanvasMetrics, selectedMetric,
onMouseOut(lockedMetric)}> +
+ METRICS +
{availableCanvasMetrics.map(({id, label}) => { const isLocked = (id === lockedMetric); const isSelected = (id === selectedMetric); @@ -46,9 +46,7 @@ export default function MetricSelector({availableCanvasMetrics, selectedMetric, onClick={() => onMouseClick(id, lockedMetric)}> {label} {isLocked && - - {CROSS} - + }
); diff --git a/client/app/styles/main.less b/client/app/styles/main.less index 8e1a0cf36..3a0f32078 100644 --- a/client/app/styles/main.less +++ b/client/app/styles/main.less @@ -407,9 +407,13 @@ h2 { fill: @background-color; } + &.metrics .border { + fill: @background-lighter-color; + } + .metric-fill { stroke: none; - fill: @background-darker-color; + fill: #A0BE7E; } .shadow { From f8a69fa1fa0d154038c9d78839b5bd5eb102bd75 Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Wed, 9 Mar 2016 20:17:11 +0100 Subject: [PATCH 07/21] Better labels for metrics-on-canvas, log scale for load and open-files - Small fixes after the rebase --- .../app/scripts/components/metric-selector.js | 24 +++++++++++++++++-- client/app/scripts/utils/data-utils.js | 14 ++++++++++- client/app/styles/main.less | 2 -- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/client/app/scripts/components/metric-selector.js b/client/app/scripts/components/metric-selector.js index 16b4ce825..debfdb99c 100644 --- a/client/app/scripts/components/metric-selector.js +++ b/client/app/scripts/components/metric-selector.js @@ -1,10 +1,25 @@ import React from 'react'; +import _ from 'lodash'; import { selectMetric, lockMetric, unlockMetric } from '../actions/app-actions'; import classNames from 'classnames'; // 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); @@ -22,6 +37,10 @@ function onMouseOut(k) { selectMetric(k); } +function label(m) { + return METRIC_LABELS[m.id]; +} + export default function MetricSelector({availableCanvasMetrics, selectedMetric, lockedMetric}) { return (
METRICS
- {availableCanvasMetrics.map(({id, label}) => { + {_.sortBy(availableCanvasMetrics, label).map(m => { + const id = m.id; const isLocked = (id === lockedMetric); const isSelected = (id === selectedMetric); const className = classNames('sidebar-item', { @@ -44,7 +64,7 @@ export default function MetricSelector({availableCanvasMetrics, selectedMetric, className={className} onMouseOver={() => onMouseOver(id)} onClick={() => onMouseClick(id, lockedMetric)}> - {label} + {label(m)} {isLocked && } diff --git a/client/app/scripts/utils/data-utils.js b/client/app/scripts/utils/data-utils.js index d37d7d399..d60bfff9c 100644 --- a/client/app/scripts/utils/data-utils.js +++ b/client/app/scripts/utils/data-utils.js @@ -1,6 +1,7 @@ import _ from 'lodash'; import d3 from 'd3'; import { formatMetric } from './string-utils'; +import AppStore from '../stores/app-store'; // Inspired by Lee Byron's test data generator. @@ -78,6 +79,10 @@ export function addMetrics(delta, 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) { @@ -86,7 +91,14 @@ export function getMetricValue(metric, size) { const max = metric.getIn(['max']); const value = metric.getIn(['samples', 0, 'value']); - const valuePercentage = value === 0 ? 0 : value / max; + + let valuePercentage = value === 0 ? 0 : value / max; + if (AppStore.getSelectedMetric() === 'open_files_count') { + valuePercentage = openFilesScale(value); + } else if (_.includes(['load1', 'load5', 'load15'], AppStore.getSelectedMetric())) { + valuePercentage = loadScale(value); + } + const baseline = 0.05; const displayedValue = valuePercentage * (1 - baseline) + baseline; const height = size * displayedValue; diff --git a/client/app/styles/main.less b/client/app/styles/main.less index 3a0f32078..670cd1e1f 100644 --- a/client/app/styles/main.less +++ b/client/app/styles/main.less @@ -387,8 +387,6 @@ h2 { } } - .shape { - .stack .shape .highlighted { display: none; } From eb6680049602455e802a8fce77dacb1370b71fcd Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Thu, 10 Mar 2016 14:28:06 +0100 Subject: [PATCH 08/21] Adds metric formatting clientside - Bump non-zero metrics up to start at 10% fill so we can see them. --- client/app/scripts/utils/data-utils.js | 29 ++++++++++++++++++++---- client/app/scripts/utils/string-utils.js | 2 +- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/client/app/scripts/utils/data-utils.js b/client/app/scripts/utils/data-utils.js index d60bfff9c..8d57ba9d2 100644 --- a/client/app/scripts/utils/data-utils.js +++ b/client/app/scripts/utils/data-utils.js @@ -79,6 +79,19 @@ 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: 'number', + 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 :). @@ -91,21 +104,27 @@ export function getMetricValue(metric, size) { const max = metric.getIn(['max']); const value = metric.getIn(['samples', 0, 'value']); + const selectedMetric = AppStore.getSelectedMetric(); let valuePercentage = value === 0 ? 0 : value / max; - if (AppStore.getSelectedMetric() === 'open_files_count') { + if (selectedMetric === 'open_files_count') { valuePercentage = openFilesScale(value); - } else if (_.includes(['load1', 'load5', 'load15'], AppStore.getSelectedMetric())) { + } else if (_.includes(['load1', 'load5', 'load15'], selectedMetric)) { valuePercentage = loadScale(value); } - const baseline = 0.05; - const displayedValue = valuePercentage * (1 - baseline) + baseline; + let displayedValue = value; + 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: height, value: value, - formattedValue: formatMetric(value, metric.toJS(), true) + formattedValue: formatMetric(value, metricWithFormat, true) }; } diff --git a/client/app/scripts/utils/string-utils.js b/client/app/scripts/utils/string-utils.js index 046efb71c..98ed1703f 100644 --- a/client/app/scripts/utils/string-utils.js +++ b/client/app/scripts/utils/string-utils.js @@ -17,7 +17,7 @@ function renderHtml(text, unit) { function makeFormatters(renderFn) { const formatters = { filesize(value) { - const obj = filesize(value, {output: 'object'}); + const obj = filesize(value, {output: 'object', round: 1}); return renderFn(obj.value, obj.suffix); }, From 0df2a2bce40598bf806a64f9669036d296714af7 Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Thu, 10 Mar 2016 19:29:38 +0100 Subject: [PATCH 09/21] Metric fill color based on node color --- client/app/scripts/charts/node-shape-circle.js | 6 +++++- client/app/scripts/charts/node-shape-hex.js | 6 +++++- client/app/scripts/charts/node-shape-square.js | 7 ++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/client/app/scripts/charts/node-shape-circle.js b/client/app/scripts/charts/node-shape-circle.js index 227ea41f3..528ff0057 100644 --- a/client/app/scripts/charts/node-shape-circle.js +++ b/client/app/scripts/charts/node-shape-circle.js @@ -10,6 +10,10 @@ export default function NodeShapeCircle({highlighted, size, color, metric}) { const className = classNames('shape', { 'metrics': value !== null }); + const metricStyle = { + fillOpacity: 0.5, + fill: color + }; return ( @@ -26,7 +30,7 @@ export default function NodeShapeCircle({highlighted, size, color, metric}) { {highlighted && hightlightNode} - + {highlighted && value !== null ? {formattedValue} : } diff --git a/client/app/scripts/charts/node-shape-hex.js b/client/app/scripts/charts/node-shape-hex.js index ba40003e4..00317097e 100644 --- a/client/app/scripts/charts/node-shape-hex.js +++ b/client/app/scripts/charts/node-shape-hex.js @@ -40,6 +40,10 @@ export default function NodeShapeHex({highlighted, size, color, metric}) { const className = classNames('shape', { 'metrics': value !== null }); + const metricStyle = { + fillOpacity: 0.5, + fill: color + }; return ( @@ -55,7 +59,7 @@ 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 ad553e67f..f708db485 100644 --- a/client/app/scripts/charts/node-shape-square.js +++ b/client/app/scripts/charts/node-shape-square.js @@ -20,6 +20,11 @@ export default function NodeShapeSquare({ 'metrics': value !== null }); + const metricStyle = { + fillOpacity: 0.5, + fill: color + }; + return ( @@ -35,7 +40,7 @@ export default function NodeShapeSquare({ {highlighted && } - + {highlighted && value !== null ? {formattedValue} : } From 439b3aaed808e9427b18bcbc93a6e42462dc38fc Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Mon, 14 Mar 2016 12:45:23 +0100 Subject: [PATCH 10/21] Different metrics get different fill colors --- client/app/scripts/charts/node-shape-circle.js | 4 ++-- client/app/scripts/charts/node-shape-heptagon.js | 8 ++++++-- client/app/scripts/charts/node-shape-hex.js | 4 ++-- client/app/scripts/charts/node-shape-square.js | 4 ++-- client/app/scripts/charts/node.js | 5 +++-- client/app/scripts/utils/data-utils.js | 16 +++++++++++++++- 6 files changed, 30 insertions(+), 11 deletions(-) diff --git a/client/app/scripts/charts/node-shape-circle.js b/client/app/scripts/charts/node-shape-circle.js index 528ff0057..1d1769d33 100644 --- a/client/app/scripts/charts/node-shape-circle.js +++ b/client/app/scripts/charts/node-shape-circle.js @@ -1,6 +1,6 @@ import React from 'react'; import classNames from 'classnames'; -import {getMetricValue} from '../utils/data-utils.js'; +import {getMetricValue, getMetricColor} from '../utils/data-utils.js'; export default function NodeShapeCircle({highlighted, size, color, metric}) { const hightlightNode = ; @@ -12,7 +12,7 @@ export default function NodeShapeCircle({highlighted, size, color, metric}) { }); const metricStyle = { fillOpacity: 0.5, - fill: color + fill: getMetricColor() }; return ( diff --git a/client/app/scripts/charts/node-shape-heptagon.js b/client/app/scripts/charts/node-shape-heptagon.js index ef27e0b0e..bac92324f 100644 --- a/client/app/scripts/charts/node-shape-heptagon.js +++ b/client/app/scripts/charts/node-shape-heptagon.js @@ -1,7 +1,7 @@ import React from 'react'; import d3 from 'd3'; import classNames from 'classnames'; -import {getMetricValue} from '../utils/data-utils.js'; +import {getMetricValue, getMetricColor} from '../utils/data-utils.js'; const line = d3.svg.line() .interpolate('cardinal-closed') @@ -29,6 +29,10 @@ export default function NodeShapeHeptagon({highlighted, size, color, metric}) { const className = classNames('shape', { 'metrics': value !== null }); + const metricStyle = { + fillOpacity: 0.5, + fill: getMetricColor() + }; return ( @@ -45,7 +49,7 @@ export default function NodeShapeHeptagon({highlighted, size, color, metric}) { {highlighted && } - + {highlighted && value !== null ? {formattedValue} : } diff --git a/client/app/scripts/charts/node-shape-hex.js b/client/app/scripts/charts/node-shape-hex.js index 00317097e..823daa382 100644 --- a/client/app/scripts/charts/node-shape-hex.js +++ b/client/app/scripts/charts/node-shape-hex.js @@ -1,7 +1,7 @@ import React from 'react'; import d3 from 'd3'; import classNames from 'classnames'; -import {getMetricValue} from '../utils/data-utils.js'; +import {getMetricValue, getMetricColor} from '../utils/data-utils.js'; const line = d3.svg.line() .interpolate('cardinal-closed') @@ -42,7 +42,7 @@ export default function NodeShapeHex({highlighted, size, color, metric}) { }); const metricStyle = { fillOpacity: 0.5, - fill: color + fill: getMetricColor() }; return ( diff --git a/client/app/scripts/charts/node-shape-square.js b/client/app/scripts/charts/node-shape-square.js index f708db485..1984352a3 100644 --- a/client/app/scripts/charts/node-shape-square.js +++ b/client/app/scripts/charts/node-shape-square.js @@ -1,6 +1,6 @@ import React from 'react'; import classNames from 'classnames'; -import {getMetricValue} from '../utils/data-utils.js'; +import {getMetricValue, getMetricColor} from '../utils/data-utils.js'; export default function NodeShapeSquare({ highlighted, size, color, rx = 0, ry = 0, metric @@ -22,7 +22,7 @@ export default function NodeShapeSquare({ const metricStyle = { fillOpacity: 0.5, - fill: color + fill: getMetricColor() }; return ( diff --git a/client/app/scripts/charts/node.js b/client/app/scripts/charts/node.js index 927a05725..b603a1b70 100644 --- a/client/app/scripts/charts/node.js +++ b/client/app/scripts/charts/node.js @@ -53,8 +53,9 @@ export default class Node extends React.Component { } let labelOffsetY = 18; let subLabelOffsetY = 35; - const color = getNodeColor(this.props.rank, this.props.label, - this.props.pseudo); + // const color = this.props.metric ? '#e2e2ec' : getNodeColor( + const color = getNodeColor( + this.props.rank, this.props.label, this.props.pseudo); const onMouseEnter = this.handleMouseEnter; const onMouseLeave = this.handleMouseLeave; const onMouseClick = this.handleMouseClick; diff --git a/client/app/scripts/utils/data-utils.js b/client/app/scripts/utils/data-utils.js index 8d57ba9d2..5a42a770c 100644 --- a/client/app/scripts/utils/data-utils.js +++ b/client/app/scripts/utils/data-utils.js @@ -87,7 +87,7 @@ const METRIC_FORMATS = { load1: 'number', load15: 'number', load5: 'number', - open_files_count: 'number', + open_files_count: 'integer', process_cpu_usage_percent: 'percent', process_memory_usage_bytes: 'filesize' }; @@ -128,3 +128,17 @@ export function getMetricValue(metric, size) { formattedValue: formatMetric(value, metricWithFormat, true) }; } + +export function getMetricColor() { + const selectedMetric = AppStore.getSelectedMetric(); + if (/memory/.test(selectedMetric)) { + return '#1f77b4'; + } else if (/cpu/.test(selectedMetric)) { + return '#2ca02c'; + } else if (/files/.test(selectedMetric)) { + return '#9467bd'; + } else if (/load/.test(selectedMetric)) { + return '#17becf'; + } + return 'steelBlue'; +} From 13a625a2b35689bbc26a5528d643adb53729c6c5 Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Mon, 14 Mar 2016 12:46:20 +0100 Subject: [PATCH 11/21] Fade out rank border color for more emphesis on metric - Fade out rank color opacity when showing metric --- client/app/scripts/charts/node.js | 2 +- client/app/scripts/utils/data-utils.js | 2 +- client/app/styles/main.less | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/client/app/scripts/charts/node.js b/client/app/scripts/charts/node.js index b603a1b70..8fe5ab61a 100644 --- a/client/app/scripts/charts/node.js +++ b/client/app/scripts/charts/node.js @@ -53,7 +53,7 @@ export default class Node extends React.Component { } let labelOffsetY = 18; let subLabelOffsetY = 35; - // const color = this.props.metric ? '#e2e2ec' : getNodeColor( + // const color = this.props.metric ? '#e2e2ec' : getNodeColor( const color = getNodeColor( this.props.rank, this.props.label, this.props.pseudo); const onMouseEnter = this.handleMouseEnter; diff --git a/client/app/scripts/utils/data-utils.js b/client/app/scripts/utils/data-utils.js index 5a42a770c..8426e00b8 100644 --- a/client/app/scripts/utils/data-utils.js +++ b/client/app/scripts/utils/data-utils.js @@ -138,7 +138,7 @@ export function getMetricColor() { } else if (/files/.test(selectedMetric)) { return '#9467bd'; } else if (/load/.test(selectedMetric)) { - return '#17becf'; + return '#e6550d'; } return 'steelBlue'; } diff --git a/client/app/styles/main.less b/client/app/styles/main.less index 670cd1e1f..8aab819cf 100644 --- a/client/app/styles/main.less +++ b/client/app/styles/main.less @@ -403,10 +403,13 @@ h2 { &:not(.shape-cloud) .border { stroke-width: @node-border-stroke-width; fill: @background-color; + transition: stroke-opacity 0.5s cubic-bezier(0,0,0.21,1), fill 0.5s cubic-bezier(0,0,0.21,1); + stroke-opacity: 1; } &.metrics .border { fill: @background-lighter-color; + stroke-opacity: 0.3; } .metric-fill { From 31ee76a1d93b5eb06c4487e0abdd7bdc3a99545b Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Mon, 14 Mar 2016 16:21:05 +0100 Subject: [PATCH 12/21] Key binding to clear metrics "q" Going w/ the `top` key mappings, needs discussion. --- client/app/scripts/charts/node-shape-stack.js | 4 +++- client/app/scripts/components/app.js | 10 +++++++++- client/app/scripts/components/debug-toolbar.js | 1 + client/app/styles/main.less | 7 +++++++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/client/app/scripts/charts/node-shape-stack.js b/client/app/scripts/charts/node-shape-stack.js index 13b6c0f58..3d691888c 100644 --- a/client/app/scripts/charts/node-shape-stack.js +++ b/client/app/scripts/charts/node-shape-stack.js @@ -20,7 +20,9 @@ export default function NodeShapeStack(props) { - + + + ); } diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index 74435858e..e4a249193 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -1,4 +1,5 @@ import React from 'react'; +import debug from 'debug'; import Logo from './logo'; import AppStore from '../stores/app-store'; @@ -8,7 +9,8 @@ import Status from './status.js'; import Topologies from './topologies.js'; import TopologyOptions from './topology-options.js'; import { getApiDetails, getTopologies } from '../utils/web-api-utils'; -import { lockNextMetric, hitEsc } from '../actions/app-actions'; +import { lockNextMetric, hitEsc, unlockMetric, + selectMetric } from '../actions/app-actions'; import Details from './details'; import Nodes from './nodes'; import MetricSelector from './metric-selector'; @@ -19,8 +21,10 @@ import { showingDebugToolbar, toggleDebugToolbar, const ESC_KEY_CODE = 27; const D_KEY_CODE = 68; +const Q_KEY_CODE = 81; const RIGHT_ANGLE_KEY_IDENTIFIER = 'U+003C'; const LEFT_ANGLE_KEY_IDENTIFIER = 'U+003E'; +const keyPressLog = debug('scope:app-key-press'); function getStateFromStores() { return { @@ -76,12 +80,16 @@ export default class App extends React.Component { } onKeyPress(ev) { + keyPressLog('onKeyPress', 'keyCode', ev.keyCode, ev); if (ev.keyCode === ESC_KEY_CODE) { hitEsc(); } else if (ev.keyIdentifier === RIGHT_ANGLE_KEY_IDENTIFIER) { lockNextMetric(-1); } else if (ev.keyIdentifier === LEFT_ANGLE_KEY_IDENTIFIER) { lockNextMetric(1); + } else if (ev.keyCode === Q_KEY_CODE) { + unlockMetric(); + selectMetric(null); } else if (ev.keyCode === D_KEY_CODE) { toggleDebugToolbar(); this.forceUpdate(); diff --git a/client/app/scripts/components/debug-toolbar.js b/client/app/scripts/components/debug-toolbar.js index 60bd30c2d..2a5c3bfa9 100644 --- a/client/app/scripts/components/debug-toolbar.js +++ b/client/app/scripts/components/debug-toolbar.js @@ -139,6 +139,7 @@ export class DebugToolbar extends React.Component { +
diff --git a/client/app/styles/main.less b/client/app/styles/main.less index 8aab819cf..fd33a1164 100644 --- a/client/app/styles/main.less +++ b/client/app/styles/main.less @@ -398,6 +398,13 @@ h2 { .highlighted { display: inline; } } + .stack .shape .metric-fill { + display: none; + } + .stack .onlyMetrics .shape .metric-fill { + display: inline-block; + } + .shape { /* cloud paths have stroke-width set dynamically */ &:not(.shape-cloud) .border { From 1ad7c2c7a3e2dbdf5d26bd616b7387177df3f935 Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Wed, 16 Mar 2016 23:10:10 +0100 Subject: [PATCH 13/21] 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 --- client/app/scripts/actions/app-actions.js | 11 ++- .../app/scripts/charts/node-shape-circle.js | 7 +- .../app/scripts/charts/node-shape-heptagon.js | 7 +- client/app/scripts/charts/node-shape-hex.js | 7 +- .../app/scripts/charts/node-shape-square.js | 7 +- client/app/scripts/charts/nodes-chart.js | 8 +- .../app/scripts/components/debug-toolbar.js | 41 ++++----- .../components/metric-selector-item.js | 73 +++++++++++++++ .../app/scripts/components/metric-selector.js | 92 ++++++------------- client/app/scripts/debug.js | 3 + client/app/scripts/stores/app-store.js | 21 ++--- client/app/scripts/utils/data-utils.js | 80 ++++++++++------ client/app/scripts/utils/string-utils.js | 5 +- client/package.json | 1 + client/webpack.local.config.js | 3 +- 15 files changed, 221 insertions(+), 145 deletions(-) create mode 100644 client/app/scripts/components/metric-selector-item.js create mode 100644 client/app/scripts/debug.js 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', From f64908acf286fe59da25a22dd254be3568a1a5ec Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Mon, 21 Mar 2016 20:28:57 +0100 Subject: [PATCH 14/21] No metric selected by default. - Fixes metric keyboard selection order --- .../components/metric-selector-item.js | 20 +------------------ .../app/scripts/components/metric-selector.js | 5 ++--- client/app/scripts/stores/app-store.js | 5 +++-- client/app/scripts/utils/data-utils.js | 18 +++++++++++++++++ 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/client/app/scripts/components/metric-selector-item.js b/client/app/scripts/components/metric-selector-item.js index e9dcb9aaa..b04fce624 100644 --- a/client/app/scripts/components/metric-selector-item.js +++ b/client/app/scripts/components/metric-selector-item.js @@ -1,25 +1,7 @@ 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]; -} +import { label } from '../utils/data-utils'; export class MetricSelectorItem extends React.Component { diff --git a/client/app/scripts/components/metric-selector.js b/client/app/scripts/components/metric-selector.js index f88a8dcae..d6ae3777e 100644 --- a/client/app/scripts/components/metric-selector.js +++ b/client/app/scripts/components/metric-selector.js @@ -1,7 +1,6 @@ import React from 'react'; -import _ from 'lodash'; import { selectMetric } from '../actions/app-actions'; -import { MetricSelectorItem, label } from './metric-selector-item'; +import { MetricSelectorItem } from './metric-selector-item'; // const CROSS = '\u274C'; // const MINUS = '\u2212'; @@ -23,7 +22,7 @@ export default class MetricSelector extends React.Component { render() { const {availableCanvasMetrics} = this.props; - const items = _.sortBy(availableCanvasMetrics, label).map(metric => ( + const items = availableCanvasMetrics.map(metric => ( )); diff --git a/client/app/scripts/stores/app-store.js b/client/app/scripts/stores/app-store.js index c8b7d1042..1d95e636d 100644 --- a/client/app/scripts/stores/app-store.js +++ b/client/app/scripts/stores/app-store.js @@ -8,6 +8,7 @@ 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; @@ -59,7 +60,7 @@ let controlPipes = makeOrderedMap(); // pipeId -> controlPipe let updatePausedAt = null; // Date let websocketClosed = true; -let selectedMetric = 'process_cpu_usage_percent'; +let selectedMetric = null; let lockedMetric = selectedMetric; let availableCanvasMetrics = []; @@ -604,7 +605,7 @@ export class AppStore extends Store { .valueSeq() .flatMap(n => (n.get('metrics') || makeMap()).keys()) .toSet() - .sort() + .sortBy(n => METRIC_LABELS[n]) .toJS() .map(v => ({id: v, label: v})); diff --git a/client/app/scripts/utils/data-utils.js b/client/app/scripts/utils/data-utils.js index 04d09018b..29ef13caf 100644 --- a/client/app/scripts/utils/data-utils.js +++ b/client/app/scripts/utils/data-utils.js @@ -41,6 +41,24 @@ function getNextValue(keyValues, maxValue) { return v; } +export 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]; +} + const METRIC_FORMATS = { docker_cpu_total_usage: 'percent', From 1710db62626388ef3af921e2f4c365f23affb015 Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Wed, 23 Mar 2016 12:47:53 +0100 Subject: [PATCH 15/21] Save locked metric in the url. - introduces "metric type" so we can flick across topos and keep the "type" of metric selected. Cheating and using label as the type atm. --- client/app/scripts/actions/app-actions.js | 9 ++- client/app/scripts/components/app.js | 4 +- .../components/metric-selector-item.js | 3 +- client/app/scripts/stores/app-store.js | 17 +++++- client/app/scripts/utils/data-utils.js | 61 +++++++++++-------- 5 files changed, 63 insertions(+), 31 deletions(-) diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 04d29ffe2..cf3314440 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -28,21 +28,26 @@ export function lockNextMetric(delta) { AppDispatcher.dispatch({ type: ActionTypes.LOCK_METRIC, - metricId: nextMetric + metricId: nextMetric, + metricType: AppStore.getAvailableCanvasMetricsTypes()[nextMetric] }); + updateRoute(); } export function lockMetric(metricId) { AppDispatcher.dispatch({ type: ActionTypes.LOCK_METRIC, - metricId + metricId, + metricType: AppStore.getAvailableCanvasMetricsTypes()[metricId] }); + updateRoute(); } export function unlockMetric() { AppDispatcher.dispatch({ type: ActionTypes.UNLOCK_METRIC, }); + updateRoute(); } export function changeTopologyOption(option, value, topologyId) { diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index e4a249193..45a6c5df2 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -139,11 +139,11 @@ export default class App extends React.Component { topologyId={this.state.currentTopologyId} /> - 0 && + />} diff --git a/client/app/scripts/components/metric-selector-item.js b/client/app/scripts/components/metric-selector-item.js index b04fce624..24d628e0a 100644 --- a/client/app/scripts/components/metric-selector-item.js +++ b/client/app/scripts/components/metric-selector-item.js @@ -1,7 +1,6 @@ import React from 'react'; import classNames from 'classnames'; import { selectMetric, lockMetric, unlockMetric } from '../actions/app-actions'; -import { label } from '../utils/data-utils'; export class MetricSelectorItem extends React.Component { @@ -45,7 +44,7 @@ export class MetricSelectorItem extends React.Component { className={className} onMouseOver={this.onMouseOver} onClick={this.onMouseClick}> - {label(metric)} + {metric.label} {isLocked && } diff --git a/client/app/scripts/stores/app-store.js b/client/app/scripts/stores/app-store.js index 1d95e636d..4eea62d0d 100644 --- a/client/app/scripts/stores/app-store.js +++ b/client/app/scripts/stores/app-store.js @@ -62,6 +62,7 @@ let websocketClosed = true; let selectedMetric = null; let lockedMetric = selectedMetric; +let lockedMetricType = null; let availableCanvasMetrics = []; @@ -144,6 +145,7 @@ export class AppStore extends Store { controlPipe: this.getControlPipe(), nodeDetails: this.getNodeDetailsState(), selectedNodeId, + lockedMetricType, topologyId: currentTopologyId, topologyOptions: topologyOptions.toJS() // all options }; @@ -182,6 +184,10 @@ export class AppStore extends Store { return availableCanvasMetrics; } + getAvailableCanvasMetricsTypes() { + return _.fromPairs(this.getAvailableCanvasMetrics().map(m => [m.id, m.label])); + } + getControlStatus() { return controlStatus.toJS(); } @@ -428,12 +434,14 @@ export class AppStore extends Store { } case ActionTypes.LOCK_METRIC: { lockedMetric = payload.metricId; + lockedMetricType = payload.metricType; selectedMetric = payload.metricId; this.__emitChange(); break; } case ActionTypes.UNLOCK_METRIC: { lockedMetric = null; + lockedMetricType = null; this.__emitChange(); break; } @@ -607,7 +615,13 @@ export class AppStore extends Store { .toSet() .sortBy(n => METRIC_LABELS[n]) .toJS() - .map(v => ({id: v, label: v})); + .map(v => ({id: v, label: METRIC_LABELS[v]})); + + const similarTypeMetric = availableCanvasMetrics.find(m => m.label === lockedMetricType); + lockedMetric = similarTypeMetric && similarTypeMetric.id; + if (!availableCanvasMetrics.map(m => m.id).includes(selectedMetric)) { + selectedMetric = lockedMetric; + } if (emitChange) { this.__emitChange(); @@ -654,6 +668,7 @@ export class AppStore extends Store { setTopology(payload.state.topologyId); setDefaultTopologyOptions(topologies); selectedNodeId = payload.state.selectedNodeId; + lockedMetricType = payload.state.lockedMetricType; if (payload.state.controlPipe) { controlPipes = makeOrderedMap({ [payload.state.controlPipe.id]: diff --git a/client/app/scripts/utils/data-utils.js b/client/app/scripts/utils/data-utils.js index 29ef13caf..e9bff6bd0 100644 --- a/client/app/scripts/utils/data-utils.js +++ b/client/app/scripts/utils/data-utils.js @@ -42,16 +42,16 @@ function getNextValue(keyValues, maxValue) { } export 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' + docker_cpu_total_usage: 'CPU', + docker_memory_usage: 'Memory', + host_cpu_usage_percent: 'CPU', + host_mem_usage_bytes: 'Memory', + load1: 'Load 1', + load15: 'Load 15', + load5: 'Load 5', + open_files_count: 'Open files', + process_cpu_usage_percent: 'CPU', + process_memory_usage_bytes: 'Memory' }; @@ -84,7 +84,7 @@ const cpuMetric = (node, name, max = 100) => ({ max }); -const fileMetric = (node, name, max = 10000) => ({ +const fileMetric = (node, name, max = 1000) => ({ samples: [{value: getNextValue([node.id, name], max)}], max }); @@ -94,23 +94,36 @@ const loadMetric = (node, name, max = 10) => ({ max }); +const metrics = { + // process + square: { + process_cpu_usage_percent: cpuMetric, + process_memory_usage_bytes: memoryMetric, + open_files_count: fileMetric + }, + // container + hexagon: { + docker_cpu_total_usage: cpuMetric, + docker_memory_usage: memoryMetric + }, + // host + circle: { + load5: loadMetric, + host_cpu_usage_percent: cpuMetric, + host_mem_usage_bytes: memoryMetric + } +}; + + function mergeMetrics(node) { - if (node.pseudo) { + if (node.pseudo || node.stack) { return node; } return Object.assign({}, node, { - metrics: { - 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') - } + metrics: _(metrics[node.shape]) + .map((fn, name) => [name, fn(node)]) + .fromPairs() + .value() }); } From 3fdd7809f7fa50487ea9921096f3b458ecb8bb1f Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Wed, 23 Mar 2016 13:35:33 +0100 Subject: [PATCH 16/21] Fixes a couple of MoC visual bugs - Fixes metric font-size on selected nodes - Round metric-height value to be the same as rounded displayed value. - No red/green colors in the MoC! They have to much association w/ success/failure --- client/app/scripts/charts/node-shape-circle.js | 3 ++- client/app/scripts/charts/node-shape-heptagon.js | 3 ++- client/app/scripts/charts/node-shape-hex.js | 3 ++- client/app/scripts/charts/node-shape-square.js | 5 ++++- client/app/scripts/utils/color-utils.js | 2 +- client/app/scripts/utils/data-utils.js | 10 +++++++--- client/app/styles/main.less | 2 ++ client/package.json | 5 ++++- 8 files changed, 24 insertions(+), 9 deletions(-) diff --git a/client/app/scripts/charts/node-shape-circle.js b/client/app/scripts/charts/node-shape-circle.js index 091b7d3bd..19b086c97 100644 --- a/client/app/scripts/charts/node-shape-circle.js +++ b/client/app/scripts/charts/node-shape-circle.js @@ -14,6 +14,7 @@ export default function NodeShapeCircle({highlighted, size, color, metric}) { fillOpacity: 0.5, fill: getMetricColor() }; + const fontSize = size * 0.19; return ( @@ -33,7 +34,7 @@ export default function NodeShapeCircle({highlighted, size, color, metric}) { {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 71e012e50..301e6b612 100644 --- a/client/app/scripts/charts/node-shape-heptagon.js +++ b/client/app/scripts/charts/node-shape-heptagon.js @@ -33,6 +33,7 @@ export default function NodeShapeHeptagon({highlighted, size, color, metric}) { fillOpacity: 0.5, fill: getMetricColor() }; + const fontSize = size * 0.19; return ( @@ -52,7 +53,7 @@ export default function NodeShapeHeptagon({highlighted, size, color, metric}) { {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 245779c39..25b61aedb 100644 --- a/client/app/scripts/charts/node-shape-hex.js +++ b/client/app/scripts/charts/node-shape-hex.js @@ -34,6 +34,7 @@ export default function NodeShapeHex({highlighted, size, color, metric}) { const shadowSize = 0.45; const upperHexBitHeight = -0.25 * size * shadowSize; + const fontSize = size * 0.19; const clipId = `mask-${Math.random()}`; const {height, value, formattedValue} = getMetricValue(metric, size); @@ -62,7 +63,7 @@ export default function NodeShapeHex({highlighted, size, color, metric}) { {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 8f14938a9..818fc5446 100644 --- a/client/app/scripts/charts/node-shape-square.js +++ b/client/app/scripts/charts/node-shape-square.js @@ -19,6 +19,7 @@ export default function NodeShapeSquare({ const className = classNames('shape', { metrics: value !== null }); + const fontSize = size * 0.19; const metricStyle = { fillOpacity: 0.5, @@ -43,7 +44,9 @@ export default function NodeShapeSquare({ {highlighted && value !== null ? - {formattedValue} : + + {formattedValue} + : } ); diff --git a/client/app/scripts/utils/color-utils.js b/client/app/scripts/utils/color-utils.js index e8d2af804..7d997d405 100644 --- a/client/app/scripts/utils/color-utils.js +++ b/client/app/scripts/utils/color-utils.js @@ -23,7 +23,7 @@ function text2degree(text) { return hueScale(num); } -function colors(text, secondText) { +export function colors(text, secondText) { let hue = text2degree(text); // skip green and shift to the end of the color wheel if (hue > 70 && hue < 150) { diff --git a/client/app/scripts/utils/data-utils.js b/client/app/scripts/utils/data-utils.js index e9bff6bd0..7f45c7382 100644 --- a/client/app/scripts/utils/data-utils.js +++ b/client/app/scripts/utils/data-utils.js @@ -1,6 +1,7 @@ import _ from 'lodash'; import d3 from 'd3'; import { formatMetric } from './string-utils'; +import { colors } from './color-utils'; import AppStore from '../stores/app-store'; @@ -172,7 +173,7 @@ export function getMetricValue(metric, size) { valuePercentage = loadScale(value); } - let displayedValue = value; + let displayedValue = Number(value).toFixed(1); if (displayedValue > 0) { const baseline = 0.1; displayedValue = valuePercentage * (1 - baseline) + baseline; @@ -190,14 +191,17 @@ export function getMetricValue(metric, size) { export function getMetricColor() { const selectedMetric = AppStore.getSelectedMetric(); + // bluey if (/memory/.test(selectedMetric)) { return '#1f77b4'; } else if (/cpu/.test(selectedMetric)) { - return '#2ca02c'; + return colors('cpu'); } else if (/files/.test(selectedMetric)) { + // return colors('files'); + // purple return '#9467bd'; } else if (/load/.test(selectedMetric)) { - return '#e6550d'; + return colors('load'); } return 'steelBlue'; } diff --git a/client/app/styles/main.less b/client/app/styles/main.less index fd33a1164..be28b7e8a 100644 --- a/client/app/styles/main.less +++ b/client/app/styles/main.less @@ -437,6 +437,8 @@ h2 { text { font-size: 12px; + dominant-baseline: middle; + text-anchor: middle; } .highlighted { diff --git a/client/package.json b/client/package.json index 2e00b91d7..5554ad976 100644 --- a/client/package.json +++ b/client/package.json @@ -68,7 +68,10 @@ "test": "jest", "coveralls": "cat coverage/lcov.info | coveralls", "lint": "eslint app", - "clean": "rm build/app.js" + "clean": "rm build/app.js", + "noprobe": "../scope stop && ../scope launch --no-probe --app.window 24h", + "loadreport": "npm run noprobe && sleep 1 && curl -X POST -H \"Content-Type: application/json\" http://$BACKEND_HOST/api/report", + "loadreportjson": "npm run loadreport -- -d @../k8s_report.json" }, "jest": { "scriptPreprocessor": "/node_modules/babel-jest", From 9d968a789b93b1db89c5353b2ea5faeab5c57a40 Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Thu, 24 Mar 2016 11:13:06 +0100 Subject: [PATCH 17/21] 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 { From 432ea920fe7dcd4fbed2ae004c0af5454e3d64b4 Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Thu, 31 Mar 2016 12:34:38 +0200 Subject: [PATCH 18/21] Terminology change from lock -> pin In meetings etc the term pin is more often used. --- client/app/scripts/actions/app-actions.js | 12 ++++---- client/app/scripts/charts/nodes-chart.js | 1 - client/app/scripts/components/app.js | 12 ++++---- .../components/metric-selector-item.js | 17 +++++------ .../app/scripts/components/metric-selector.js | 2 +- client/app/scripts/constants/action-types.js | 4 +-- client/app/scripts/stores/app-store.js | 30 +++++++++---------- 7 files changed, 38 insertions(+), 40 deletions(-) diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 905aa7e4e..ddc79977b 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -20,31 +20,31 @@ export function selectMetric(metricId) { }); } -export function lockNextMetric(delta) { +export function pinNextMetric(delta) { const metrics = AppStore.getAvailableCanvasMetrics().map(m => m.id); const currentIndex = metrics.indexOf(AppStore.getSelectedMetric()); const nextMetric = metrics[modulo(currentIndex + delta, metrics.length)]; AppDispatcher.dispatch({ - type: ActionTypes.LOCK_METRIC, + type: ActionTypes.PIN_METRIC, metricId: nextMetric, metricType: AppStore.getAvailableCanvasMetricsTypes()[nextMetric] }); updateRoute(); } -export function lockMetric(metricId) { +export function pinMetric(metricId) { AppDispatcher.dispatch({ - type: ActionTypes.LOCK_METRIC, + type: ActionTypes.PIN_METRIC, metricId, metricType: AppStore.getAvailableCanvasMetricsTypes()[metricId] }); updateRoute(); } -export function unlockMetric() { +export function unpinMetric() { AppDispatcher.dispatch({ - type: ActionTypes.UNLOCK_METRIC, + type: ActionTypes.UNPIN_METRIC, }); updateRoute(); } diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index 172f82893..ac8578872 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -135,7 +135,6 @@ export default class NodesChart extends React.Component { const met = node.get('metrics') && node.get('metrics') .filter(m => m.get('id') === this.props.selectedMetric) .first(); - console.log(met); return met; }; diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index b85025b31..b257142af 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -9,7 +9,7 @@ import Status from './status.js'; import Topologies from './topologies.js'; import TopologyOptions from './topology-options.js'; import { getApiDetails, getTopologies } from '../utils/web-api-utils'; -import { lockNextMetric, hitEsc, unlockMetric, +import { pinNextMetric, hitEsc, unpinMetric, selectMetric } from '../actions/app-actions'; import Details from './details'; import Nodes from './nodes'; @@ -39,7 +39,7 @@ function getStateFromStores() { highlightedEdgeIds: AppStore.getHighlightedEdgeIds(), highlightedNodeIds: AppStore.getHighlightedNodeIds(), hostname: AppStore.getHostname(), - lockedMetric: AppStore.getLockedMetric(), + pinnedMetric: AppStore.getPinnedMetric(), availableCanvasMetrics: AppStore.getAvailableCanvasMetrics(), nodeDetails: AppStore.getNodeDetails(), nodes: AppStore.getNodes(), @@ -84,11 +84,11 @@ export default class App extends React.Component { if (ev.keyCode === ESC_KEY_CODE) { hitEsc(); } else if (ev.keyIdentifier === RIGHT_ANGLE_KEY_IDENTIFIER) { - lockNextMetric(-1); + pinNextMetric(-1); } else if (ev.keyIdentifier === LEFT_ANGLE_KEY_IDENTIFIER) { - lockNextMetric(1); + pinNextMetric(1); } else if (ev.keyCode === Q_KEY_CODE) { - unlockMetric(); + unpinMetric(); selectMetric(null); } else if (ev.keyCode === D_KEY_CODE) { toggleDebugToolbar(); @@ -144,7 +144,7 @@ export default class App extends React.Component { websocketClosed={this.state.websocketClosed} /> {this.state.availableCanvasMetrics.length > 0 && } {metric.label} - {isLocked && } + {isPinned && }
); } diff --git a/client/app/scripts/components/metric-selector.js b/client/app/scripts/components/metric-selector.js index 75fa8128c..76878019c 100644 --- a/client/app/scripts/components/metric-selector.js +++ b/client/app/scripts/components/metric-selector.js @@ -16,7 +16,7 @@ export default class MetricSelector extends React.Component { } onMouseOut() { - selectMetric(this.props.lockedMetric); + selectMetric(this.props.pinnedMetric); } render() { diff --git a/client/app/scripts/constants/action-types.js b/client/app/scripts/constants/action-types.js index 151143483..a6b3f5912 100644 --- a/client/app/scripts/constants/action-types.js +++ b/client/app/scripts/constants/action-types.js @@ -23,8 +23,8 @@ const ACTION_TYPES = [ 'ENTER_NODE', 'LEAVE_EDGE', 'LEAVE_NODE', - 'LOCK_METRIC', - 'UNLOCK_METRIC', + 'PIN_METRIC', + 'UNPIN_METRIC', 'OPEN_WEBSOCKET', 'RECEIVE_CONTROL_PIPE', 'RECEIVE_CONTROL_PIPE_STATUS', diff --git a/client/app/scripts/stores/app-store.js b/client/app/scripts/stores/app-store.js index 33f427a50..1f4012ab2 100644 --- a/client/app/scripts/stores/app-store.js +++ b/client/app/scripts/stores/app-store.js @@ -60,8 +60,8 @@ let updatePausedAt = null; // Date let websocketClosed = true; let selectedMetric = null; -let lockedMetric = selectedMetric; -let lockedMetricType = null; +let pinnedMetric = selectedMetric; +let pinnedMetricType = null; let availableCanvasMetrics = []; @@ -144,7 +144,7 @@ export class AppStore extends Store { controlPipe: this.getControlPipe(), nodeDetails: this.getNodeDetailsState(), selectedNodeId, - lockedMetricType, + pinnedMetricType, topologyId: currentTopologyId, topologyOptions: topologyOptions.toJS() // all options }; @@ -171,8 +171,8 @@ export class AppStore extends Store { return adjacentNodes; } - getLockedMetric() { - return lockedMetric; + getPinnedMetric() { + return pinnedMetric; } getSelectedMetric() { @@ -431,16 +431,16 @@ export class AppStore extends Store { this.__emitChange(); break; } - case ActionTypes.LOCK_METRIC: { - lockedMetric = payload.metricId; - lockedMetricType = payload.metricType; + case ActionTypes.PIN_METRIC: { + pinnedMetric = payload.metricId; + pinnedMetricType = payload.metricType; selectedMetric = payload.metricId; this.__emitChange(); break; } - case ActionTypes.UNLOCK_METRIC: { - lockedMetric = null; - lockedMetricType = null; + case ActionTypes.UNPIN_METRIC: { + pinnedMetric = null; + pinnedMetricType = null; this.__emitChange(); break; } @@ -617,11 +617,11 @@ export class AppStore extends Store { .sortBy(m => m.get('label')) .toJS(); - const similarTypeMetric = availableCanvasMetrics.find(m => m.label === lockedMetricType); - lockedMetric = similarTypeMetric && similarTypeMetric.id; + const similarTypeMetric = availableCanvasMetrics.find(m => m.label === pinnedMetricType); + pinnedMetric = similarTypeMetric && similarTypeMetric.id; // if something in the current topo is not already selected, select it. if (availableCanvasMetrics.map(m => m.id).indexOf(selectedMetric) === -1) { - selectedMetric = lockedMetric; + selectedMetric = pinnedMetric; } if (emitChange) { @@ -669,7 +669,7 @@ export class AppStore extends Store { setTopology(payload.state.topologyId); setDefaultTopologyOptions(topologies); selectedNodeId = payload.state.selectedNodeId; - lockedMetricType = payload.state.lockedMetricType; + pinnedMetricType = payload.state.pinnedMetricType; if (payload.state.controlPipe) { controlPipes = makeOrderedMap({ [payload.state.controlPipe.id]: From 4a840c2d2b0091b8b25c74e906755a0eddf1aa41 Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Thu, 31 Mar 2016 12:58:03 +0200 Subject: [PATCH 19/21] Tidy up code, remove small comment --- client/app/scripts/charts/node.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/app/scripts/charts/node.js b/client/app/scripts/charts/node.js index 8fe5ab61a..927a05725 100644 --- a/client/app/scripts/charts/node.js +++ b/client/app/scripts/charts/node.js @@ -53,9 +53,8 @@ export default class Node extends React.Component { } let labelOffsetY = 18; let subLabelOffsetY = 35; - // const color = this.props.metric ? '#e2e2ec' : getNodeColor( - const color = getNodeColor( - this.props.rank, this.props.label, this.props.pseudo); + const color = getNodeColor(this.props.rank, this.props.label, + this.props.pseudo); const onMouseEnter = this.handleMouseEnter; const onMouseLeave = this.handleMouseLeave; const onMouseClick = this.handleMouseClick; From 4ec8b97fefdc5d72decd0e8e5c682c2d596560e1 Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Mon, 4 Apr 2016 12:27:48 +0200 Subject: [PATCH 20/21] metrics-on-canvas review feedback updates. - Refactor some things. - Fixes heptagon moc rendering - Experiment w/ duller colors. --- client/app/scripts/actions/app-actions.js | 7 +-- .../app/scripts/charts/node-shape-circle.js | 36 ++++------- .../app/scripts/charts/node-shape-heptagon.js | 33 ++++------- client/app/scripts/charts/node-shape-hex.js | 34 ++++------- .../app/scripts/charts/node-shape-square.js | 30 +++------- client/app/scripts/charts/nodes-chart.js | 15 +++-- client/app/scripts/components/app.js | 2 +- .../app/scripts/components/debug-toolbar.js | 59 ++++++++++++++++++- .../components/metric-selector-item.js | 8 +-- .../app/scripts/components/metric-selector.js | 7 +-- client/app/scripts/stores/app-store.js | 23 ++++---- client/app/scripts/utils/metric-utils.js | 51 +++++++++++----- client/app/scripts/utils/string-utils.js | 23 +++++--- client/app/styles/main.less | 37 +++++++----- client/package.json | 5 +- 15 files changed, 211 insertions(+), 159 deletions(-) diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index ddc79977b..401ad9a54 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -21,14 +21,14 @@ export function selectMetric(metricId) { } export function pinNextMetric(delta) { - const metrics = AppStore.getAvailableCanvasMetrics().map(m => m.id); + const metrics = AppStore.getAvailableCanvasMetrics().map(m => m.get('id')); const currentIndex = metrics.indexOf(AppStore.getSelectedMetric()); - const nextMetric = metrics[modulo(currentIndex + delta, metrics.length)]; + const nextIndex = modulo(currentIndex + delta, metrics.count()); + const nextMetric = metrics.get(nextIndex); AppDispatcher.dispatch({ type: ActionTypes.PIN_METRIC, metricId: nextMetric, - metricType: AppStore.getAvailableCanvasMetricsTypes()[nextMetric] }); updateRoute(); } @@ -37,7 +37,6 @@ export function pinMetric(metricId) { AppDispatcher.dispatch({ type: ActionTypes.PIN_METRIC, metricId, - metricType: AppStore.getAvailableCanvasMetricsTypes()[metricId] }); updateRoute(); } diff --git a/client/app/scripts/charts/node-shape-circle.js b/client/app/scripts/charts/node-shape-circle.js index 997dfebdd..dcf2327eb 100644 --- a/client/app/scripts/charts/node-shape-circle.js +++ b/client/app/scripts/charts/node-shape-circle.js @@ -1,39 +1,25 @@ import React from 'react'; import classNames from 'classnames'; -import {getMetricValue, getMetricColor} from '../utils/metric-utils.js'; +import {getMetricValue, getMetricColor, getClipPathDefinition} from '../utils/metric-utils.js'; import {CANVAS_METRIC_FONT_SIZE} from '../constants/styles.js'; -export default function NodeShapeCircle({id, highlighted, size, color, metric}) { - const hightlightNode = ; - const clipId = `mask-${id}`; - const {height, value, formattedValue} = getMetricValue(metric, size); - const className = classNames('shape', { - metrics: value !== null - }); - const metricStyle = { - fill: getMetricColor(metric) - }; +export default function NodeShapeCircle({id, highlighted, size, color, metric}) { + const clipId = `mask-${id}`; + const {height, hasMetric, formattedValue} = getMetricValue(metric, size); + const metricStyle = { fill: getMetricColor(metric) }; + const className = classNames('shape', { metrics: hasMetric }); const fontSize = size * CANVAS_METRIC_FONT_SIZE; return ( - - - - - - {highlighted && hightlightNode} + {hasMetric && getClipPathDefinition(clipId, size, height)} + {highlighted && } - - {highlighted && value !== null ? + {hasMetric && } + {highlighted && hasMetric ? {formattedValue} : } diff --git a/client/app/scripts/charts/node-shape-heptagon.js b/client/app/scripts/charts/node-shape-heptagon.js index d807e843c..f4f4b715a 100644 --- a/client/app/scripts/charts/node-shape-heptagon.js +++ b/client/app/scripts/charts/node-shape-heptagon.js @@ -1,13 +1,15 @@ import React from 'react'; import d3 from 'd3'; import classNames from 'classnames'; -import {getMetricValue, getMetricColor} from '../utils/metric-utils.js'; +import {getMetricValue, getMetricColor, getClipPathDefinition} from '../utils/metric-utils.js'; import {CANVAS_METRIC_FONT_SIZE} from '../constants/styles.js'; + const line = d3.svg.line() .interpolate('cardinal-closed') .tension(0.25); + function polygon(r, sides) { const a = (Math.PI * 2) / sides; const points = [[r, 0]]; @@ -17,6 +19,7 @@ function polygon(r, sides) { return points; } + export default function NodeShapeHeptagon({id, highlighted, size, color, metric}) { const scaledSize = size * 1.0; const pathProps = v => ({ @@ -25,34 +28,20 @@ export default function NodeShapeHeptagon({id, highlighted, size, color, metric} }); const clipId = `mask-${id}`; - const {height, value, formattedValue} = getMetricValue(metric, size); - - const className = classNames('shape', { - metrics: value !== null - }); + const {height, hasMetric, formattedValue} = getMetricValue(metric, size); + const metricStyle = { fill: getMetricColor(metric) }; + const className = classNames('shape', { metrics: hasMetric }); const fontSize = size * CANVAS_METRIC_FONT_SIZE; - const metricStyle = { - fill: getMetricColor(metric) - }; return ( - - - - - + {hasMetric && getClipPathDefinition(clipId, size, height, size * 0.5 - height, -size * 0.5)} {highlighted && } - - {highlighted && value !== null ? + {hasMetric && } + {highlighted && hasMetric ? {formattedValue} : } diff --git a/client/app/scripts/charts/node-shape-hex.js b/client/app/scripts/charts/node-shape-hex.js index 4ae2fa39d..d7045c9ab 100644 --- a/client/app/scripts/charts/node-shape-hex.js +++ b/client/app/scripts/charts/node-shape-hex.js @@ -1,17 +1,20 @@ import React from 'react'; import d3 from 'd3'; import classNames from 'classnames'; -import {getMetricValue, getMetricColor} from '../utils/metric-utils.js'; +import {getMetricValue, getMetricColor, getClipPathDefinition} from '../utils/metric-utils.js'; import {CANVAS_METRIC_FONT_SIZE} from '../constants/styles.js'; + const line = d3.svg.line() .interpolate('cardinal-closed') .tension(0.25); + function getWidth(h) { return (Math.sqrt(3) / 2) * h; } + function getPoints(h) { const w = getWidth(h); const points = [ @@ -35,34 +38,23 @@ export default function NodeShapeHex({id, highlighted, size, color, metric}) { const shadowSize = 0.45; const upperHexBitHeight = -0.25 * size * shadowSize; - const fontSize = size * CANVAS_METRIC_FONT_SIZE; const clipId = `mask-${id}`; - const {height, value, formattedValue} = getMetricValue(metric, size); - const className = classNames('shape', { - metrics: value !== null - }); - const metricStyle = { - fill: getMetricColor(metric) - }; + const {height, hasMetric, formattedValue} = getMetricValue(metric, size); + const metricStyle = { fill: getMetricColor(metric) }; + const className = classNames('shape', { metrics: hasMetric }); + const fontSize = size * CANVAS_METRIC_FONT_SIZE; return ( - - - - - + {hasMetric && getClipPathDefinition(clipId, size, height, size - height + + upperHexBitHeight, 0)} {highlighted && } - - {highlighted && value !== null ? + {hasMetric && } + {highlighted && hasMetric ? {formattedValue} : diff --git a/client/app/scripts/charts/node-shape-square.js b/client/app/scripts/charts/node-shape-square.js index adc4c0240..d4cd116bc 100644 --- a/client/app/scripts/charts/node-shape-square.js +++ b/client/app/scripts/charts/node-shape-square.js @@ -1,8 +1,9 @@ import React from 'react'; import classNames from 'classnames'; -import {getMetricValue, getMetricColor} from '../utils/metric-utils.js'; +import {getMetricValue, getMetricColor, getClipPathDefinition} from '../utils/metric-utils.js'; import {CANVAS_METRIC_FONT_SIZE} from '../constants/styles.js'; + export default function NodeShapeSquare({ id, highlighted, size, color, rx = 0, ry = 0, metric }) { @@ -16,33 +17,20 @@ export default function NodeShapeSquare({ }); const clipId = `mask-${id}`; - const {height, value, formattedValue} = getMetricValue(metric, size); - const className = classNames('shape', { - metrics: value !== null - }); + const {height, hasMetric, formattedValue} = getMetricValue(metric, size); + const metricStyle = { fill: getMetricColor(metric) }; + const className = classNames('shape', { metrics: hasMetric }); const fontSize = size * CANVAS_METRIC_FONT_SIZE; - const metricStyle = { - fill: getMetricColor(metric) - }; return ( - - - - - + {hasMetric && getClipPathDefinition(clipId, size, height)} {highlighted && } - - {highlighted && value !== null ? + {hasMetric && } + {highlighted && hasMetric ? {formattedValue} : diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index ac8578872..8260aa17e 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -76,7 +76,10 @@ export default class NodesChart extends React.Component { } // // FIXME add PureRenderMixin, Immutables, and move the following functions to render() - _.assign(state, this.updateGraphState(nextProps, state)); + // _.assign(state, this.updateGraphState(nextProps, state)); + if (nextProps.forceRelayout || nextProps.nodes !== this.props.nodes) { + _.assign(state, this.updateGraphState(nextProps, state)); + } if (this.props.selectedNodeId !== nextProps.selectedNodeId) { _.assign(state, this.restoreLayout(state)); @@ -131,12 +134,12 @@ export default class NodesChart extends React.Component { return 1; }; - const metric = node => { - const met = node.get('metrics') && node.get('metrics') + // TODO: think about pulling this up into the store. + const metric = node => ( + node.get('metrics') && node.get('metrics') .filter(m => m.get('id') === this.props.selectedMetric) - .first(); - return met; - }; + .first() + ); return nodes .toIndexedSeq() diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index b257142af..c759948d4 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -142,7 +142,7 @@ export default class App extends React.Component { - {this.state.availableCanvasMetrics.length > 0 && 0 && _.range(_.random(4)).map(() => _.sample(collection)); @@ -26,8 +29,11 @@ const shapeTypes = { 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, controls: {}, @@ -44,6 +50,18 @@ const deltaAdd = (name, adjacency = [], shape = 'circle', stack = false, nodeCou }); +function addMetrics(node, v) { + const availableMetrics = AppStore.getAvailableCanvasMetrics().toJS(); + const metrics = availableMetrics.length > 0 ? availableMetrics : [ + {id: 'host_cpu_usage_percent', label: 'CPU'} + ]; + + return Object.assign({}, node, { + metrics: metrics.map(m => Object.assign({}, m, {max: 100, value: v})) + }); +} + + function label(shape, stacked) { const type = shapeTypes[shape]; return stacked ? `Group of ${type[1]}` : type[0]; @@ -62,6 +80,17 @@ function addAllVariants() { } +function addAllMetricVariants() { + const newNodes = _.flattenDeep(METRIC_FILLS.map((v, i) => ( + SHAPES.map(s => [addMetrics(deltaAdd(label(s) + i, [], s), v)]) + ))); + + receiveNodesDelta({ + add: newNodes + }); +} + + function addNodes(n) { const ns = AppStore.getNodes(); const nodeNames = ns.keySeq().toJS(); @@ -109,8 +138,10 @@ export class DebugToolbar extends React.Component { constructor(props, context) { super(props, context); this.onChange = this.onChange.bind(this); + this.toggleColors = this.toggleColors.bind(this); this.state = { - nodesToAdd: 30 + nodesToAdd: 30, + showColors: false }; } @@ -118,6 +149,12 @@ export class DebugToolbar extends React.Component { this.setState({nodesToAdd: parseInt(ev.target.value, 10)}); } + toggleColors() { + this.setState({ + showColors: !this.state.showColors + }); + } + render() { log('rending debug panel'); @@ -130,6 +167,7 @@ export class DebugToolbar extends React.Component { +
@@ -139,6 +177,25 @@ export class DebugToolbar extends React.Component {
+ +
+ + +
+ + {this.state.showColors && [getNodeColor, getNodeColorDark].map(fn => ( + + + {LABEL_PREFIXES.map(r => ( + + {LABEL_PREFIXES.map(c => ( + + ))} + + ))} + +
+ ))} ); } diff --git a/client/app/scripts/components/metric-selector-item.js b/client/app/scripts/components/metric-selector-item.js index 2fa3708ad..d3890e0a4 100644 --- a/client/app/scripts/components/metric-selector-item.js +++ b/client/app/scripts/components/metric-selector-item.js @@ -13,12 +13,12 @@ export class MetricSelectorItem extends React.Component { } onMouseOver() { - const k = this.props.metric.id; + const k = this.props.metric.get('id'); selectMetric(k); } onMouseClick() { - const k = this.props.metric.id; + const k = this.props.metric.get('id'); const pinnedMetric = this.props.pinnedMetric; if (k === pinnedMetric) { @@ -30,7 +30,7 @@ export class MetricSelectorItem extends React.Component { render() { const {metric, selectedMetric, pinnedMetric} = this.props; - const id = metric.id; + const id = metric.get('id'); const isPinned = (id === pinnedMetric); const isSelected = (id === selectedMetric); const className = classNames('metric-selector-action', { @@ -43,7 +43,7 @@ export class MetricSelectorItem extends React.Component { className={className} onMouseOver={this.onMouseOver} onClick={this.onMouseClick}> - {metric.label} + {metric.get('label')} {isPinned && } ); diff --git a/client/app/scripts/components/metric-selector.js b/client/app/scripts/components/metric-selector.js index 76878019c..a00578e33 100644 --- a/client/app/scripts/components/metric-selector.js +++ b/client/app/scripts/components/metric-selector.js @@ -2,11 +2,6 @@ import React from 'react'; import { selectMetric } from '../actions/app-actions'; import { MetricSelectorItem } from './metric-selector-item'; -// const CROSS = '\u274C'; -// const MINUS = '\u2212'; -// const DOT = '\u2022'; -// - export default class MetricSelector extends React.Component { @@ -23,7 +18,7 @@ export default class MetricSelector extends React.Component { const {availableCanvasMetrics} = this.props; const items = availableCanvasMetrics.map(metric => ( - + )); return ( diff --git a/client/app/scripts/stores/app-store.js b/client/app/scripts/stores/app-store.js index 1f4012ab2..31e99bbb1 100644 --- a/client/app/scripts/stores/app-store.js +++ b/client/app/scripts/stores/app-store.js @@ -61,8 +61,10 @@ let websocketClosed = true; let selectedMetric = null; let pinnedMetric = selectedMetric; +// class of metric, e.g. 'cpu', rather than 'host_cpu' or 'process_cpu'. +// allows us to keep the same metric "type" selected when the topology changes. let pinnedMetricType = null; -let availableCanvasMetrics = []; +let availableCanvasMetrics = makeList(); const topologySorter = topology => topology.get('rank'); @@ -184,7 +186,7 @@ export class AppStore extends Store { } getAvailableCanvasMetricsTypes() { - return _.fromPairs(this.getAvailableCanvasMetrics().map(m => [m.id, m.label])); + return makeMap(this.getAvailableCanvasMetrics().map(m => [m.get('id'), m.get('label')])); } getControlStatus() { @@ -404,7 +406,7 @@ export class AppStore extends Store { setTopology(payload.topologyId); nodes = nodes.clear(); } - availableCanvasMetrics = []; + availableCanvasMetrics = makeList(); this.__emitChange(); break; } @@ -415,7 +417,7 @@ export class AppStore extends Store { setTopology(payload.topologyId); nodes = nodes.clear(); } - availableCanvasMetrics = []; + availableCanvasMetrics = makeList(); this.__emitChange(); break; } @@ -433,7 +435,7 @@ export class AppStore extends Store { } case ActionTypes.PIN_METRIC: { pinnedMetric = payload.metricId; - pinnedMetricType = payload.metricType; + pinnedMetricType = this.getAvailableCanvasMetricsTypes().get(payload.metricId); selectedMetric = payload.metricId; this.__emitChange(); break; @@ -614,13 +616,14 @@ export class AppStore extends Store { makeMap({id: m.get('id'), label: m.get('label')}) ))) .toSet() - .sortBy(m => m.get('label')) - .toJS(); + .toList() + .sortBy(m => m.get('label')); - const similarTypeMetric = availableCanvasMetrics.find(m => m.label === pinnedMetricType); - pinnedMetric = similarTypeMetric && similarTypeMetric.id; + const similarTypeMetric = availableCanvasMetrics + .find(m => m.get('label') === pinnedMetricType); + pinnedMetric = similarTypeMetric && similarTypeMetric.get('id'); // if something in the current topo is not already selected, select it. - if (availableCanvasMetrics.map(m => m.id).indexOf(selectedMetric) === -1) { + if (!availableCanvasMetrics.map(m => m.get('id')).toSet().has(selectedMetric)) { selectedMetric = pinnedMetric; } diff --git a/client/app/scripts/utils/metric-utils.js b/client/app/scripts/utils/metric-utils.js index 148c2e022..12650b77b 100644 --- a/client/app/scripts/utils/metric-utils.js +++ b/client/app/scripts/utils/metric-utils.js @@ -1,14 +1,35 @@ import _ from 'lodash'; import d3 from 'd3'; -import { formatMetric } from './string-utils'; -import { colors } from './color-utils'; +import { formatMetricSvg } from './string-utils'; +import { getNodeColorDark as colors } from './color-utils'; +import React from 'react'; +export function getClipPathDefinition(clipId, size, height, + x = -size * 0.5, y = size * 0.5 - height) { + return ( + + + + + + ); +} + + +// +// Open files, 100k should be enought for anyone? 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'}; @@ -17,40 +38,42 @@ export function getMetricValue(metric, size) { const value = m.value; let valuePercentage = value === 0 ? 0 : value / m.max; + let max = m.max; if (m.id === 'open_files_count') { valuePercentage = openFilesScale(value); + max = null; } else if (_.includes(['load1', 'load5', 'load15'], m.id)) { valuePercentage = loadScale(value); + max = null; } let displayedValue = Number(value).toFixed(1); - if (displayedValue > 0) { + if (displayedValue > 0 && (!max || displayedValue < max)) { const baseline = 0.1; - displayedValue = valuePercentage * (1 - baseline) + baseline; + displayedValue = valuePercentage * (1 - baseline * 2) + baseline; + } else if (displayedValue >= m.max && displayedValue > 0) { + displayedValue = 1; } const height = size * displayedValue; return { height, - value, - formattedValue: formatMetric(value, m, true) + hasMetric: value !== null, + formattedValue: formatMetricSvg(value, m) }; } export function getMetricColor(metric) { const selectedMetric = metric && metric.get('id'); - // bluey - if (/memory/.test(selectedMetric)) { - return '#1f77b4'; + if (/mem/.test(selectedMetric)) { + return colors('p', 'a'); } else if (/cpu/.test(selectedMetric)) { - return colors('cpu'); + return colors('z', 'a'); } else if (/files/.test(selectedMetric)) { - // return colors('files'); - // purple - return '#9467bd'; + return colors('t', 'a'); } else if (/load/.test(selectedMetric)) { - return colors('load'); + return colors('a', 'a'); } return 'steelBlue'; } diff --git a/client/app/scripts/utils/string-utils.js b/client/app/scripts/utils/string-utils.js index f75677583..9b7c22df4 100644 --- a/client/app/scripts/utils/string-utils.js +++ b/client/app/scripts/utils/string-utils.js @@ -2,8 +2,10 @@ import React from 'react'; import filesize from 'filesize'; import d3 from 'd3'; + const formatLargeValue = d3.format('s'); + function renderHtml(text, unit) { return ( @@ -14,6 +16,11 @@ function renderHtml(text, unit) { } +function renderSvg(text, unit) { + return `${text}${unit}`; +} + + function makeFormatters(renderFn) { const formatters = { filesize(value) { @@ -45,13 +52,15 @@ function makeFormatters(renderFn) { } -const formatters = makeFormatters(renderHtml); -const svgFormatters = makeFormatters((text, unit) => `${text}${unit}`); - -export function formatMetric(value, opts, svg) { - const formatterBase = svg ? svgFormatters : formatters; - const formatter = opts && formatterBase[opts.format] ? opts.format : 'number'; - return formatterBase[formatter](value); +function makeFormatMetric(renderFn) { + const formatters = makeFormatters(renderFn); + return (value, opts) => { + const formatter = opts && formatters[opts.format] ? opts.format : 'number'; + return formatters[formatter](value); + }; } + +export const formatMetric = makeFormatMetric(renderHtml); +export const formatMetricSvg = makeFormatMetric(renderSvg); export const formatDate = d3.time.format.iso; diff --git a/client/app/styles/main.less b/client/app/styles/main.less index 14b79d5b2..8505871b1 100644 --- a/client/app/styles/main.less +++ b/client/app/styles/main.less @@ -21,6 +21,8 @@ @base-font: "Roboto", sans-serif; @mono-font: "Menlo", "DejaVu Sans Mono", "Liberation Mono", monospace; +@base-ease: ease-in-out; + @primary-color: @weave-charcoal-blue; @background-color: lighten(@primary-color, 66%); @background-lighter-color: lighten(@background-color, 8%); @@ -65,15 +67,15 @@ } .colorable { - transition: background-color .3s ease-in-out; + transition: background-color .3s @base-ease; } .palable { - transition: all .2s ease-in-out; + transition: all .2s @base-ease; } .hideable { - transition: opacity .5s ease-in-out; + transition: opacity .5s @base-ease; } .hang-around { @@ -221,7 +223,7 @@ h2 { &-active { border: 1px solid @text-tertiary-color; - animation: blinking 1.5s infinite ease-in-out; + animation: blinking 1.5s infinite @base-ease; } } @@ -333,7 +335,7 @@ h2 { .nodes > .node { cursor: pointer; - transition: opacity .5s ease-in-out; + transition: opacity .5s @base-ease; &.pseudo { cursor: default; @@ -362,7 +364,7 @@ h2 { } .edge { - transition: opacity .5s ease-in-out; + transition: opacity .5s @base-ease; &.blurred { opacity: @edge-opacity-blurred; @@ -402,16 +404,12 @@ h2 { display: none; } - .stack .onlyMetrics .shape .metric-fill { - display: inline-block; - } - .shape { /* cloud paths have stroke-width set dynamically */ &:not(.shape-cloud) .border { stroke-width: @node-border-stroke-width; fill: @background-color; - transition: stroke-opacity 0.5s cubic-bezier(0,0,0.21,1), fill 0.5s cubic-bezier(0,0,0.21,1); + transition: stroke-opacity 0.333s @base-ease, fill 0.333s @base-ease; stroke-opacity: 1; } @@ -423,7 +421,7 @@ h2 { .metric-fill { stroke: none; fill: #A0BE7E; - fill-opacity: 0.7; + fill-opacity: 0.5; } .shadow { @@ -608,7 +606,7 @@ h2 { &-icon { margin-right: 0.5em; - animation: blinking 2.0s infinite ease-in-out; + animation: blinking 2.0s infinite @base-ease; } } } @@ -996,7 +994,7 @@ h2 { } &.status-loading { - animation: blinking 2.0s infinite ease-in-out; + animation: blinking 2.0s infinite @base-ease; text-transform: none; color: @text-color; } @@ -1086,4 +1084,15 @@ h2 { &:hover { opacity: 1; } + + table { + display: inline-block; + border-collapse: collapse; + margin: 4px 2px; + + td { + width: 10px; + height: 10px; + } + } } diff --git a/client/package.json b/client/package.json index 5554ad976..746925294 100644 --- a/client/package.json +++ b/client/package.json @@ -69,9 +69,8 @@ "coveralls": "cat coverage/lcov.info | coveralls", "lint": "eslint app", "clean": "rm build/app.js", - "noprobe": "../scope stop && ../scope launch --no-probe --app.window 24h", - "loadreport": "npm run noprobe && sleep 1 && curl -X POST -H \"Content-Type: application/json\" http://$BACKEND_HOST/api/report", - "loadreportjson": "npm run loadreport -- -d @../k8s_report.json" + "noprobe": "../scope stop && ../scope launch --no-probe --app.window 8760h", + "loadreport": "npm run noprobe && sleep 1 && curl -X POST -H \"Content-Type: application/json\" http://$BACKEND_HOST/api/report -d" }, "jest": { "scriptPreprocessor": "/node_modules/babel-jest", From 4ec975018c63bc7d0f7930fb168de6b0c9dcb9a9 Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Mon, 4 Apr 2016 21:01:59 +0200 Subject: [PATCH 21/21] Back to brighter colors for metrics-on-canvas --- client/app/scripts/utils/metric-utils.js | 11 ++++++----- client/app/styles/main.less | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/client/app/scripts/utils/metric-utils.js b/client/app/scripts/utils/metric-utils.js index 12650b77b..3a9f8ee62 100644 --- a/client/app/scripts/utils/metric-utils.js +++ b/client/app/scripts/utils/metric-utils.js @@ -1,7 +1,7 @@ import _ from 'lodash'; import d3 from 'd3'; import { formatMetricSvg } from './string-utils'; -import { getNodeColorDark as colors } from './color-utils'; +import { colors } from './color-utils'; import React from 'react'; @@ -67,13 +67,14 @@ export function getMetricValue(metric, size) { export function getMetricColor(metric) { const selectedMetric = metric && metric.get('id'); if (/mem/.test(selectedMetric)) { - return colors('p', 'a'); + return 'steelBlue'; } else if (/cpu/.test(selectedMetric)) { - return colors('z', 'a'); + return colors('cpu'); } else if (/files/.test(selectedMetric)) { - return colors('t', 'a'); + // purple + return '#9467bd'; } else if (/load/.test(selectedMetric)) { - return colors('a', 'a'); + return colors('load'); } return 'steelBlue'; } diff --git a/client/app/styles/main.less b/client/app/styles/main.less index 8505871b1..eb2162b1f 100644 --- a/client/app/styles/main.less +++ b/client/app/styles/main.less @@ -421,7 +421,7 @@ h2 { .metric-fill { stroke: none; fill: #A0BE7E; - fill-opacity: 0.5; + fill-opacity: 0.7; } .shadow {