From a104962aa257968dbbedb8d488e0d5346759cfaf Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Tue, 8 Mar 2016 20:10:24 +0100 Subject: [PATCH] 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;