Files
weave-scope/client/app/scripts/components/debug-toolbar.js
Filip Barl 69fd397217 Initial version of the resource view (#2296)
* Added resource view selector button

* Showing resource boxes in the resource view

* Crude CPU resource view prototype

* Improved the viewMode state logic

* Extracted zooming into a separate wrapper component

* Split the layout selectors between graph-view and resource-view

* Proper zooming logic for the resource view

* Moved all node networks utils to selectors

* Improved the zoom caching logic

* Further refactoring of selectors

* Added sticky labels to the resource boxes

* Added panning translation limits in the resource view

* Renamed GridModeSelector -> ViewModeSelector

* Polished the topology resource view selection logic

* Search bar hidden in the resource view

* Added per-layer topology names to the resource view

* Made metric selectors work for the resource view

* Adjusted the viewport selectors

* Renamed viewport selector to canvas (+ maximal zoom fix)

* Showing more useful metric info in the resource box labels

* Fetching only necessary nodes for the resource view

* Refactored the resource view layer component

* Addressed first batch UI comments (from the Scope meeting)

* Switch to deep zooming transform in the resource view to avoid SVG precision errors

* Renamed and moved resource view components

* Polished all the resource view components

* Changing the available metrics selection

* Improved and polished the state transition logic for the resource view

* Separated zoom limits from the zoom active state

* Renaming and bunch of comments

* Addressed all the UI comments (@davkal + @fons)

* Made graph view selectors independent from resource view selectors
2017-03-24 14:51:53 +01:00

391 lines
11 KiB
JavaScript

/* eslint react/jsx-no-bind: "off" */
import React from 'react';
import Perf from 'react-addons-perf';
import { connect } from 'react-redux';
import { sampleSize, sample, random, range, flattenDeep, times } from 'lodash';
import { fromJS, Set as makeSet } from 'immutable';
import { hsl } from 'd3-color';
import debug from 'debug';
import ActionTypes from '../constants/action-types';
import { receiveNodesDelta } from '../actions/app-actions';
import { getNodeColor, getNodeColorDark, text2degree } from '../utils/color-utils';
import { availableMetricsSelector } from '../selectors/node-metric';
const SHAPES = ['square', 'hexagon', 'heptagon', 'circle'];
const STACK_VARIANTS = [false, true];
const METRIC_FILLS = [0, 0.1, 50, 99.9, 100];
const NETWORKS = [
'be', 'fe', 'zb', 'db', 're', 'gh', 'jk', 'lol', 'nw'
].map(n => ({id: n, label: n, colorKey: n}));
const INTERNET = 'the-internet';
const LOREM = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.`;
const sampleArray = (collection, n = 4) => sampleSize(collection, random(n));
const log = debug('scope:debug-panel');
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, networks = NETWORKS) => ({
adjacency,
controls: {},
shape,
stack,
id: name,
label: name,
labelMinor: name,
latest: {},
origins: [],
rank: name,
networks
});
function addMetrics(availableMetrics, node, v) {
const metrics = availableMetrics.size > 0 ? availableMetrics : fromJS([
{id: 'host_cpu_usage_percent', label: 'CPU'}
]);
return Object.assign({}, node, {
metrics: metrics.map(m => Object.assign({}, m, {
id: 'zing', label: 'zing', max: 100, value: v
})).toJS()
});
}
function label(shape, stacked) {
const type = shapeTypes[shape];
return stacked ? `Group of ${type[1]}` : type[0];
}
function addAllVariants(dispatch) {
const newNodes = flattenDeep(STACK_VARIANTS.map(stack => (SHAPES.map((s) => {
if (!stack) return [deltaAdd(label(s, stack), [], s, stack)];
return times(3).map(() => deltaAdd(label(s, stack), [], s, stack));
}))));
dispatch(receiveNodesDelta({
add: newNodes
}));
}
function addAllMetricVariants(availableMetrics) {
const newNodes = flattenDeep(METRIC_FILLS.map((v, i) => (
SHAPES.map(s => [addMetrics(availableMetrics, deltaAdd(label(s) + i, [], s), v)])
)));
return (dispatch) => {
dispatch(receiveNodesDelta({
add: newNodes
}));
};
}
function stopPerf() {
Perf.stop();
const measurements = Perf.getLastMeasurements();
Perf.printInclusive(measurements);
Perf.printWasted(measurements);
}
function startPerf(delay) {
Perf.start();
setTimeout(stopPerf, delay * 1000);
}
export function showingDebugToolbar() {
return (('debugToolbar' in localStorage && JSON.parse(localStorage.debugToolbar))
|| location.pathname.indexOf('debug') > -1);
}
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();
}
function setAppState(fn) {
return (dispatch) => {
dispatch({
type: ActionTypes.DEBUG_TOOLBAR_INTERFERING,
fn
});
};
}
class DebugToolbar extends React.Component {
constructor(props, context) {
super(props, context);
this.onChange = this.onChange.bind(this);
this.toggleColors = this.toggleColors.bind(this);
this.addNodes = this.addNodes.bind(this);
this.intermittentTimer = null;
this.intermittentNodes = makeSet();
this.shortLivedTimer = null;
this.shortLivedNodes = makeSet();
this.state = {
nodesToAdd: 30,
showColors: false
};
}
onChange(ev) {
this.setState({nodesToAdd: parseInt(ev.target.value, 10)});
}
toggleColors() {
this.setState({
showColors: !this.state.showColors
});
}
asyncDispatch(v) {
setTimeout(() => this.props.dispatch(v), 0);
}
setLoading(loading) {
this.asyncDispatch(setAppState(state => state.set('topologiesLoaded', !loading)));
}
setIntermittent() {
// simulate epheremal nodes
if (this.intermittentTimer) {
clearInterval(this.intermittentTimer);
this.intermittentTimer = null;
} else {
this.intermittentTimer = setInterval(() => {
// add new node
this.addNodes(1);
// remove random node
const ns = this.props.nodes;
const nodeNames = ns.keySeq().toJS();
const randomNode = sample(nodeNames);
this.asyncDispatch(receiveNodesDelta({
remove: [randomNode]
}));
}, 1000);
}
}
setShortLived() {
// simulate nodes with same ID popping in and out
if (this.shortLivedTimer) {
clearInterval(this.shortLivedTimer);
this.shortLivedTimer = null;
} else {
this.shortLivedTimer = setInterval(() => {
// filter random node
const ns = this.props.nodes;
const nodeNames = ns.keySeq().toJS();
const randomNode = sample(nodeNames);
if (randomNode) {
let nextNodes = ns.setIn([randomNode, 'filtered'], true);
this.shortLivedNodes = this.shortLivedNodes.add(randomNode);
// bring nodes back after a bit
if (this.shortLivedNodes.size > 5) {
const returningNode = this.shortLivedNodes.first();
this.shortLivedNodes = this.shortLivedNodes.rest();
nextNodes = nextNodes.setIn([returningNode, 'filtered'], false);
}
this.asyncDispatch(setAppState(state => state.set('nodes', nextNodes)));
}
}, 1000);
}
}
updateAdjacencies() {
const ns = this.props.nodes;
const nodeNames = ns.keySeq().toJS();
this.asyncDispatch(receiveNodesDelta({
add: this.createRandomNodes(7),
update: sampleArray(nodeNames).map(n => ({
id: n,
adjacency: sampleArray(nodeNames),
}), nodeNames.length),
remove: this.randomExistingNode(),
}));
}
createRandomNodes(n, prefix = 'zing') {
const ns = this.props.nodes;
const nodeNames = ns.keySeq().toJS();
const newNodeNames = range(ns.size, ns.size + n).map(i => (
// `${randomLetter()}${randomLetter()}-zing`
`${prefix}${i}`
));
const allNodes = nodeNames.concat(newNodeNames);
return newNodeNames.map(name => deltaAdd(
name,
sampleArray(allNodes),
sample(SHAPES),
sample(STACK_VARIANTS),
sampleArray(NETWORKS, 10)
));
}
addInternetNode() {
setTimeout(() => {
this.asyncDispatch(receiveNodesDelta({
add: [{id: INTERNET, label: INTERNET, shape: 'cloud'}]
}));
}, 0);
}
addNodes(n, prefix = 'zing') {
setTimeout(() => {
this.asyncDispatch(receiveNodesDelta({
add: this.createRandomNodes(n, prefix)
}));
log('added nodes', n);
}, 0);
}
randomExistingNode() {
const ns = this.props.nodes;
const nodeNames = ns.keySeq().toJS();
return [nodeNames[random(nodeNames.length - 1)]];
}
removeNode() {
this.asyncDispatch(receiveNodesDelta({
remove: this.randomExistingNode()
}));
}
render() {
const { availableMetrics } = this.props;
return (
<div className="debug-panel">
<div>
<strong>Add nodes </strong>
<button onClick={() => this.addNodes(1)}>+1</button>
<button onClick={() => this.addNodes(10)}>+10</button>
<input type="number" onChange={this.onChange} value={this.state.nodesToAdd} />
<button onClick={() => this.addNodes(this.state.nodesToAdd)}>+</button>
<button onClick={() => this.asyncDispatch(addAllVariants)}>Variants</button>
<button onClick={() => this.asyncDispatch(addAllMetricVariants(availableMetrics))}>
Metric Variants
</button>
<button onClick={() => this.addNodes(1, LOREM)}>Long name</button>
<button onClick={() => this.addInternetNode()}>Internet</button>
<button onClick={() => this.removeNode()}>Remove node</button>
<button onClick={() => this.updateAdjacencies()}>Update adj.</button>
</div>
<div>
<strong>Logging </strong>
<button onClick={() => enableLog('*')}>scope:*</button>
<button onClick={() => enableLog('dispatcher')}>scope:dispatcher</button>
<button onClick={() => enableLog('app-key-press')}>scope:app-key-press</button>
<button onClick={() => enableLog('terminal')}>scope:terminal</button>
<button onClick={() => disableLog()}>Disable log</button>
</div>
<div>
<strong>Colors </strong>
<button onClick={this.toggleColors}>toggle</button>
</div>
{this.state.showColors &&
<table>
<tbody>
{LABEL_PREFIXES.map(r => (
<tr key={r}>
<td
title={`${r}`}
style={{backgroundColor: hsl(text2degree(r), 0.5, 0.5).toString()}} />
</tr>
))}
</tbody>
</table>}
{this.state.showColors && [getNodeColor, getNodeColorDark].map((fn, i) => (
<table key={i}>
<tbody>
{LABEL_PREFIXES.map(r => (
<tr key={r}>
{LABEL_PREFIXES.map(c => (
<td key={c} title={`(${r}, ${c})`} style={{backgroundColor: fn(r, c)}} />
))}
</tr>
))}
</tbody>
</table>
))}
<div>
<strong>State </strong>
<button onClick={() => this.setLoading(true)}>Set doing initial load</button>
<button onClick={() => this.setLoading(false)}>Stop</button>
</div>
<div>
<strong>Short-lived nodes </strong>
<button onClick={() => this.setShortLived()}>Toggle short-lived nodes</button>
<button onClick={() => this.setIntermittent()}>Toggle intermittent nodes</button>
</div>
<div>
<strong>Measure React perf for </strong>
<button onClick={() => startPerf(2)}>2s</button>
<button onClick={() => startPerf(5)}>5s</button>
<button onClick={() => startPerf(10)}>10s</button>
</div>
</div>
);
}
}
function mapStateToProps(state) {
return {
nodes: state.get('nodes'),
availableMetrics: availableMetricsSelector(state),
};
}
export default connect(
mapStateToProps
)(DebugToolbar);