357 lines
12 KiB
JavaScript
357 lines
12 KiB
JavaScript
var Dashboard = React.createClass({
|
|
getInitialState: function() {
|
|
return {};
|
|
},
|
|
|
|
componentDidMount: function() {
|
|
this.reload();
|
|
},
|
|
|
|
reload: function() {
|
|
getURL("http://127.0.0.1:6464/stats.json", {}, function(resp) {
|
|
if (resp.error !== undefined) {
|
|
console.log("Stats server is offline");
|
|
setTimeout(this.reload, 3000);
|
|
return
|
|
}
|
|
|
|
var newState = {};
|
|
var decode = function(point) {
|
|
return {
|
|
timestamp: point[0],
|
|
processed: point[1],
|
|
errors: point[2],
|
|
min: point[3],
|
|
p25: point[4],
|
|
mean: point[5],
|
|
median: point[6],
|
|
p75: point[7],
|
|
max: point[8],
|
|
}
|
|
};
|
|
var padWithValues = function(points, target) {
|
|
var pointsToAdd = target - points.length;
|
|
for (var i = 0; i < pointsToAdd; i++) {
|
|
points.unshift({
|
|
timestamp: 0,
|
|
processed: 0,
|
|
errors: 0,
|
|
min: 0,
|
|
p25: 0,
|
|
mean: 0,
|
|
median: 0,
|
|
p75: 0,
|
|
max: 0,
|
|
});
|
|
}
|
|
|
|
return points
|
|
};
|
|
for (name in resp) {
|
|
newState[name] = resp[name].map(decode);
|
|
newState[name] = padWithValues(newState[name], 61);
|
|
}
|
|
this.setState(newState);
|
|
setTimeout(this.reload, 3000);
|
|
}.bind(this));
|
|
},
|
|
|
|
renderDaemons: function() {
|
|
var daemons = [];
|
|
for (name in this.state) {
|
|
daemons.push(<Daemon name={name} key={name} points={this.state[name]} />);
|
|
}
|
|
|
|
return daemons;
|
|
},
|
|
|
|
render: function() {
|
|
return (
|
|
<div className="dashboard">
|
|
<Header />
|
|
<div className="daemons">{this.renderDaemons()}</div>
|
|
</div>
|
|
);
|
|
}
|
|
});
|
|
|
|
var Header = React.createClass({
|
|
render: function() {
|
|
return (
|
|
<div className="table-row table-header">
|
|
<div className="table-cell col-name">Daemon</div>
|
|
<div className="table-cell col-processed">Processed</div>
|
|
<div className="table-cell col-errors">Errors</div>
|
|
<div className="table-cell col-min">Min</div>
|
|
<div className="table-cell col-median">Median</div>
|
|
<div className="table-cell col-max">Max</div>
|
|
</div>
|
|
);
|
|
}
|
|
});
|
|
|
|
var StatsRow = React.createClass({
|
|
render: function() {
|
|
var value = this.props.value;
|
|
var errorsClasses = ["table-cell", "col-errors"];
|
|
if (value.errors > 0) {
|
|
errorsClasses.push("attn");
|
|
}
|
|
|
|
return (
|
|
<div className="table-row">
|
|
<div className="table-cell col-name">{this.props.name}</div>
|
|
<div className="table-cell col-processed">{value.processed}</div>
|
|
<div className={errorsClasses.join(" ")}>{value.errors}</div>
|
|
<div className="table-cell col-min">{formatDuration(value.min)}</div>
|
|
<div className="table-cell col-median">{formatDuration(value.median)}</div>
|
|
<div className="table-cell col-max">{formatDuration(value.max)}</div>
|
|
</div>
|
|
);
|
|
}
|
|
});
|
|
|
|
var Daemon = React.createClass({
|
|
render: function() {
|
|
var last = this.props.points[this.props.points.length - 1];
|
|
return (
|
|
<div className="daemon">
|
|
<StatsRow name={this.props.name} value={last} />
|
|
<LineChart points={this.props.points} />
|
|
<BoxPlot points={this.props.points} />
|
|
</div>
|
|
);
|
|
}
|
|
});
|
|
|
|
var BoxPlot = React.createClass({
|
|
render: function(){
|
|
var points = this.props.points,
|
|
chartWidth = 950,
|
|
chartHeight = 170,
|
|
padLeft = 30,
|
|
padTop = 10,
|
|
padBottom = 20,
|
|
valueInterval = 5,
|
|
maxHeight = chartHeight - padTop - padBottom;
|
|
|
|
// Skipping first point
|
|
points.shift();
|
|
|
|
var min, max;
|
|
points.map(function(point) {
|
|
if (min === undefined || point.min < min) {
|
|
min = point.min;
|
|
}
|
|
if (max === undefined || point.max > max) {
|
|
max = point.max;
|
|
}
|
|
});
|
|
|
|
var renderBox = function(point, i) {
|
|
if (point.max === 0) {
|
|
return;
|
|
}
|
|
|
|
var relativeY = function(val) {
|
|
return maxHeight - Math.round((val-min)/(max-min) * maxHeight);
|
|
};
|
|
|
|
var boxWidth = 10;
|
|
|
|
var x1 = (boxWidth + valueInterval)*i + padLeft + 2*valueInterval;
|
|
var x2 = x1 + boxWidth;
|
|
var minY = relativeY(point.min);
|
|
var p25Y = relativeY(point.p25);
|
|
var medianY = relativeY(point.median);
|
|
var p75Y = relativeY(point.p75);
|
|
var maxY = relativeY(point.max);
|
|
|
|
return (
|
|
<g key={i}>
|
|
<line key="max"
|
|
x1={x1+2}
|
|
x2={x2-2}
|
|
y1={maxY}
|
|
y2={maxY}
|
|
className="tick" />
|
|
<line key="max-whisker"
|
|
x1={x1+boxWidth/2}
|
|
x2={x1+boxWidth/2}
|
|
y1={maxY}
|
|
y2={p75Y}
|
|
className="whisker" />
|
|
<line key="min-whisker"
|
|
x1={x1+boxWidth/2}
|
|
x2={x1+boxWidth/2}
|
|
y1={minY}
|
|
y2={p25Y}
|
|
className="whisker" />
|
|
<rect key="iqr"
|
|
x={x1}
|
|
y={p75Y}
|
|
width={boxWidth}
|
|
height={p25Y - p75Y}
|
|
className="iqr" />
|
|
<line key="median"
|
|
x1={x1}
|
|
x2={x2}
|
|
y1={medianY}
|
|
y2={medianY}
|
|
className="median" />
|
|
<line key="min"
|
|
x1={x1+2}
|
|
x2={x2-2}
|
|
y1={minY}
|
|
y2={minY}
|
|
className="tick" />
|
|
</g>
|
|
);
|
|
};
|
|
|
|
var yMaxX = padLeft - 3,
|
|
yMaxY = padTop + 5;
|
|
|
|
var xlabels = Array.apply(null, Array(10)).map(function(_, i){
|
|
return <text key={"x-label-"+ i}
|
|
x={padLeft + (15 * 6 * i)}
|
|
y={chartHeight - padBottom}
|
|
textAnchor="middle"
|
|
className="axis-label">
|
|
{"-"+ (10-i) + "m"}
|
|
</text>
|
|
});
|
|
|
|
return (
|
|
<div className="boxplot">
|
|
<svg>
|
|
<text key="title" x={-70} y={10} textAnchor="middle" transform="rotate(270)" className="axis-label">Speed</text>
|
|
<line key="y-axis" x1={padLeft} x2={padLeft} y1={0} y2={maxHeight} className="axis" />
|
|
<path key="y-arrow" d="M30,0 32,7 28,7 Z" className="arrow" />
|
|
<text key="y-max" x={yMaxX} y={yMaxY} textAnchor="end"className="axis-label">{max.toFixed(1)}</text>
|
|
<text key="y-zero" x={27} y={140} textAnchor="end" className="axis-label">0</text>
|
|
<line key="x-axis" x1={padLeft} x2={950} y1={maxHeight} y2={maxHeight} className="axis light" />
|
|
<path key="x-arrow" d="M950,140 943,142 943,138 Z" className="arrow" />
|
|
<text key="x-label-now" x={940} y={chartHeight - padBottom} textAnchor="end" className="axis-label">now</text>
|
|
{xlabels}
|
|
{this.props.points.map(renderBox)}
|
|
</svg>
|
|
</div>
|
|
);
|
|
}
|
|
});
|
|
|
|
var LineChart = React.createClass({
|
|
render: function() {
|
|
var points = this.props.points,
|
|
chartWidth = 950,
|
|
chartHeight = 120,
|
|
padLeft = 30,
|
|
padTop = 10,
|
|
padBottom = 20,
|
|
valueInterval = 15;
|
|
maxHeight = chartHeight - padTop - padBottom;
|
|
|
|
var max = 0;
|
|
points.map(function(point) {
|
|
if (max === undefined || point.processed > max) {
|
|
max = point.processed;
|
|
}
|
|
});
|
|
|
|
var makePath = function(points, key) {
|
|
if (max === 0) {
|
|
return;
|
|
}
|
|
|
|
var npoints = points.map(function(point, i) {
|
|
var val = point[key];
|
|
var x = i * valueInterval + padLeft;
|
|
var y = maxHeight - Math.round(val/max * maxHeight) + padTop;
|
|
|
|
return [x, y];
|
|
});
|
|
|
|
var maxPointsRatio = points.map(function(point, i) {
|
|
var val = point[key];
|
|
return val === max ? 1 : 0;
|
|
}).reduce(function(sum, val) {
|
|
return sum + val;
|
|
}) / points.length;
|
|
|
|
var path = npoints.map(function(point, i) {
|
|
var x = point[0], y = point[1];
|
|
|
|
if (i === 0) {
|
|
return "M"+x+","+y;
|
|
} else {
|
|
return "L"+x+","+y;
|
|
}
|
|
}).join(" ");
|
|
|
|
var dots = npoints.map(function(point, i) {
|
|
var x = point[0], y = point[1];
|
|
|
|
var r = 2; // Radius
|
|
// Hide leftmost and zero points
|
|
if (x === padLeft || y === chartHeight - padBottom) {
|
|
r = 0;
|
|
}
|
|
// Highlight max values if less then 25% of values are max
|
|
if (y == padTop && maxPointsRatio <= .25) {
|
|
r = 4;
|
|
}
|
|
|
|
return <circle key={"dot-"+ i}
|
|
cx={x}
|
|
cy={y}
|
|
r={r}
|
|
className={"dot "+ key} />
|
|
});
|
|
|
|
return (
|
|
<g key={key}>
|
|
<path
|
|
d={path}
|
|
className={"line "+ key} />
|
|
{dots}
|
|
</g>
|
|
);
|
|
};
|
|
|
|
// TODO: Define magic numbers from below here
|
|
var yMaxX = padLeft - 3,
|
|
yMaxY = padTop + 5;
|
|
|
|
var xlabels = Array.apply(null, Array(10)).map(function(_, i){
|
|
return <text key={"x-label-"+ i}
|
|
x={padLeft + (15 * 6 * i)}
|
|
y={110}
|
|
textAnchor="middle"
|
|
className="axis-label">
|
|
{"-"+ (10-i) + "m"}
|
|
</text>
|
|
});
|
|
|
|
return (
|
|
<div className="linechart">
|
|
<svg>
|
|
<text key="title" x={-50} y={10} textAnchor="middle" transform="rotate(270)" className="axis-label">Throughput</text>
|
|
{makePath(points, "processed")}
|
|
{makePath(points, "errors")}
|
|
<line key="y-axis" x1={padLeft} x2={padLeft} y1={0} y2={100} className="axis" />
|
|
<path key="y-arrow" d="M30,0 32,7 28,7 Z" className="arrow" />
|
|
<text key="y-max" x={yMaxX} y={yMaxY} textAnchor="end"className="axis-label">{max}</text>
|
|
<text key="y-zero" x={27} y={100} textAnchor="end" className="axis-label">0</text>
|
|
<line key="x-axis" x1={30} x2={950} y1={100} y2={100} className="axis" />
|
|
<path key="x-arrow" d="M950,100 943,102 943,98 Z" className="arrow" />
|
|
<text key="x-label-now" x={940} y={110} textAnchor="end" className="axis-label">now</text>
|
|
{xlabels}
|
|
</svg>
|
|
</div>
|
|
);
|
|
}
|
|
});
|
|
|
|
ReactDOM.render(<Dashboard />, document.getElementById("app"));
|