_.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",