metrics-on-canvas review feedback updates.

- Refactor some things.
- Fixes heptagon moc rendering
- Experiment w/ duller colors.
This commit is contained in:
Simon Howe
2016-04-04 12:27:48 +02:00
parent 4a840c2d2b
commit 4ec8b97fef
15 changed files with 211 additions and 159 deletions

View File

@@ -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();
}

View File

@@ -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 = <circle r={size * 0.7} className="highlighted" />;
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 (
<g className={className}>
<defs>
<clipPath id={clipId}>
<rect
width={size}
height={size}
x={-size * 0.5}
y={size * 0.5 - height}
/>
</clipPath>
</defs>
{highlighted && hightlightNode}
{hasMetric && getClipPathDefinition(clipId, size, height)}
{highlighted && <circle r={size * 0.7} className="highlighted" />}
<circle r={size * 0.5} className="border" stroke={color} />
<circle r={size * 0.45} className="shadow" />
<circle r={size * 0.45} className="metric-fill" style={metricStyle}
clipPath={`url(#${clipId})`} />
{highlighted && value !== null ?
{hasMetric && <circle r={size * 0.45} className="metric-fill" style={metricStyle}
clipPath={`url(#${clipId})`} />}
{highlighted && hasMetric ?
<text style={{fontSize}}>{formattedValue}</text> :
<circle className="node" r={Math.max(2, (size * 0.125))} />}
</g>

View File

@@ -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 (
<g className={className}>
<defs>
<clipPath id={clipId}>
<rect
width={size}
height={size}
y={-size * 0.5}
x={size * 0.5 - height}
/>
</clipPath>
</defs>
{hasMetric && getClipPathDefinition(clipId, size, height, size * 0.5 - height, -size * 0.5)}
{highlighted && <path className="highlighted" {...pathProps(0.7)} />}
<path className="border" stroke={color} {...pathProps(0.5)} />
<path className="shadow" {...pathProps(0.45)} />
<path className="metric-fill" clipPath={`url(#${clipId})`}
style={metricStyle} {...pathProps(0.45)} />
{highlighted && value !== null ?
{hasMetric && <path className="metric-fill" clipPath={`url(#${clipId})`}
style={metricStyle} {...pathProps(0.45)} />}
{highlighted && hasMetric ?
<text style={{fontSize}}>{formattedValue}</text> :
<circle className="node" r={Math.max(2, (size * 0.125))} />}
</g>

View File

@@ -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 (
<g className={className}>
<defs>
<clipPath id={clipId}>
<rect
width={size}
height={size}
x={size - height + upperHexBitHeight}
/>
</clipPath>
</defs>
{hasMetric && getClipPathDefinition(clipId, size, height, size - height +
upperHexBitHeight, 0)}
{highlighted && <path className="highlighted" {...pathProps(0.7)} />}
<path className="border" stroke={color} {...pathProps(0.5)} />
<path className="shadow" {...pathProps(shadowSize)} />
<path className="metric-fill" style={metricStyle}
clipPath={`url(#${clipId})`} {...pathProps(shadowSize)} />
{highlighted && value !== null ?
{hasMetric && <path className="metric-fill" style={metricStyle}
clipPath={`url(#${clipId})`} {...pathProps(shadowSize)} />}
{highlighted && hasMetric ?
<text style={{fontSize}}>
{formattedValue}
</text> :

View File

@@ -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 (
<g className={className}>
<defs>
<clipPath id={clipId}>
<rect
width={size}
height={size}
x={-size * 0.5}
y={size * 0.5 - height}
/>
</clipPath>
</defs>
{hasMetric && getClipPathDefinition(clipId, size, height)}
{highlighted && <rect className="highlighted" {...rectProps(0.7)} />}
<rect className="border" stroke={color} {...rectProps(0.5, 0.5)} />
<rect className="shadow" {...rectProps(0.45, 0.39)} />
<rect className="metric-fill" style={metricStyle}
clipPath={`url(#${clipId})`} {...rectProps(0.45, 0.39)} />
{highlighted && value !== null ?
{hasMetric && <rect className="metric-fill" style={metricStyle}
clipPath={`url(#${clipId})`} {...rectProps(0.45, 0.39)} />}
{highlighted && hasMetric ?
<text style={{fontSize}}>
{formattedValue}
</text> :

View File

@@ -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()

View File

@@ -142,7 +142,7 @@ export default class App extends React.Component {
<Status errorUrl={this.state.errorUrl} topology={this.state.currentTopology}
topologiesLoaded={this.state.topologiesLoaded}
websocketClosed={this.state.websocketClosed} />
{this.state.availableCanvasMetrics.length > 0 && <MetricSelector
{this.state.availableCanvasMetrics.count() > 0 && <MetricSelector
availableCanvasMetrics={this.state.availableCanvasMetrics}
pinnedMetric={this.state.pinnedMetric}
selectedMetric={this.state.selectedMetric}

View File

@@ -7,11 +7,14 @@ const log = debug('scope:debug-panel');
import { receiveNodesDelta } from '../actions/app-actions';
import AppStore from '../stores/app-store';
import { getNodeColor, getNodeColorDark } from '../utils/color-utils';
const SHAPES = ['square', 'hexagon', 'heptagon', 'circle'];
const NODE_COUNTS = [1, 2, 3];
const STACK_VARIANTS = [false, true];
const METRIC_FILLS = [0, 0.1, 50, 99.9, 100];
const sample = (collection) => _.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 {
<input type="number" onChange={this.onChange} value={this.state.nodesToAdd} />
<button onClick={() => addNodes(this.state.nodesToAdd)}>+</button>
<button onClick={() => addAllVariants()}>Variants</button>
<button onClick={() => addAllMetricVariants()}>Metric Variants</button>
</div>
<div>
@@ -139,6 +177,25 @@ export class DebugToolbar extends React.Component {
<button onClick={() => enableLog('app-key-press')}>scope:app-key-press</button>
<button onClick={() => disableLog()}>Disable log</button>
</div>
<div>
<label>Colors</label>
<button onClick={this.toggleColors}>toggle</button>
</div>
{this.state.showColors && [getNodeColor, getNodeColorDark].map(fn => (
<table>
<tbody>
{LABEL_PREFIXES.map(r => (
<tr key={r}>
{LABEL_PREFIXES.map(c => (
<td key={c} title={`(${r}, ${c})`} style={{backgroundColor: fn(r, c)}}></td>
))}
</tr>
))}
</tbody>
</table>
))}
</div>
);
}

View File

@@ -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 && <span className="fa fa-thumb-tack"></span>}
</div>
);

View File

@@ -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 => (
<MetricSelectorItem key={metric.id} metric={metric} {...this.props} />
<MetricSelectorItem key={metric.get('id')} metric={metric} {...this.props} />
));
return (

View File

@@ -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;
}

View File

@@ -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 (
<defs>
<clipPath id={clipId}>
<rect
width={size}
height={size}
x={x}
y={y}
/>
</clipPath>
</defs>
);
}
//
// 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';
}

View File

@@ -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 (
<span className="metric-formatted">
@@ -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;

View File

@@ -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;
}
}
}

View File

@@ -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": "<rootDir>/node_modules/babel-jest",