Move to nivo from nx and add WS support

This commit is contained in:
slice 2018-07-13 12:07:53 -07:00
parent 60208b29db
commit f3f500b344
No known key found for this signature in database
GPG Key ID: 1508C19D7436A26D
8 changed files with 11695 additions and 109 deletions

11501
priv/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -3,13 +3,8 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@vx/axis": "^0.0.165",
"@vx/curve": "^0.0.165",
"@vx/gradient": "^0.0.165",
"@vx/group": "^0.0.165",
"@vx/scale": "^0.0.165",
"@vx/shape": "^0.0.165",
"d3-array": "^1.2.1",
"@nivo/line": "^0.42.1",
"ms": "^2.1.1",
"react": "^16.4.0",
"react-dom": "^16.4.0",
"react-scripts": "1.1.4"

View File

@ -3,45 +3,145 @@ import React, { Component } from 'react';
import Service from './Service.js';
import './App.css';
const ENDPOINT = 'https://elstatus.stayathomeserver.club/api/status'
import OP from './ws/op.js';
const log = (...args) => {
console.log(
'%c[elstat]%c',
'color: purple; font-weight: bold',
'color: inherit; font-weight: inherit',
...args,
);
};
const DOMAIN = 'https://elstatus.stayathomeserver.club';
// const ENDPOINT = 'http://localhost:8069/api/status'
export default class App extends Component {
websocket = null;
state = {
loading: true,
error: null,
metrics: null,
};
componentDidMount() {
fetch(ENDPOINT)
.then((resp) => resp.json())
.then((data) => {
this.setState({ metrics: data, loading: false });
})
.catch((error) => {
this.setState({ error: error.toString() });
})
_send(payload) {
this.websocket.send(JSON.stringify(payload));
}
_subscribe() {
const channels = Object.keys(this.state.metrics.graph).map((channel) => `latency:${channel}`);
log('subscribing to channels:', channels);
this._send({
op: OP.SUBSCRIBE,
channels,
});
}
_handleMessage(packet) {
const { op, c: channel, d: data } = packet;
if (op !== OP.DATA) {
log('ignoring boring packet:', packet);
return;
}
const [, name] = channel.split(':');
const { metrics } = this.state;
const graph = metrics.graph[name].slice(1);
const newGraph = [...graph, data];
log('adding data:', data);
const clone = Object.assign({}, this.state.metrics);
clone.graph[name] = newGraph;
log('current graph:', graph, 'new graph:', newGraph);
log('current state', this.state, 'new state:', { metrics: clone });
this.setState({
metrics: clone,
});
}
_connectWs() {
log('connecting to ws');
const endpoint = (`${DOMAIN}/api/streaming`).replace('https', 'wss');
this.websocket = new WebSocket(endpoint);
this.websocket.onopen = () => {
log('ws opened');
this._subscribe();
};
this.websocket.onmessage = (message) => {
const { data } = message;
const parsed = JSON.parse(data);
log('ws recv:', parsed);
this._handleMessage(parsed);
};
this.websocket.onerror = (error) => {
log('ws error:', error);
};
}
async componentDidMount() {
await this._loadMetrics();
this._connectWs();
}
async _loadMetrics() {
log('loading metrics');
try {
const resp = await fetch(`${DOMAIN}/api/status`);
const json = await resp.json();
this.setState({ metrics: json, loading: false });
} catch (err) {
this.setState({ error: err.toString() });
}
}
render() {
const metrics = !this.state.metrics ? null : (
<div className="services">
{Object.entries(this.state.metrics.status)
.map(([name, info]) =>
<Service name={name} key={name}
.map(([name, info]) => (
<Service
name={name}
key={name}
graph={this.state.metrics.graph[name]}
{...info}
/>)
/>
))
}
</div>
);
return (
<div className="dashboard">
<h1>elstatus</h1>
{this.state.loading ? <div>Loading metrics...</div> : null}
{this.state.error ? <div>Error: {this.state.error}</div> : null}
<h1>
elstatus
</h1>
{this.state.loading ? (
<div>
Loading metrics...
</div>
) : null}
{this.state.error ? (
<div>
Error:
{this.state.error}
</div>
) : null}
{metrics}
</div>
);

View File

@ -1,3 +1,4 @@
.graph {
margin-top: 1em;
.graph-container {
width: 100%;
height: 10rem;
}

View File

@ -1,94 +1,68 @@
import React, { Component } from 'react';
import ms from 'ms';
import { ResponsiveLine } from '@nivo/line';
import './Graph.css';
import { curveNatural } from '@vx/curve';
import { GradientOrangeRed } from '@vx/gradient';
import { AxisLeft, AxisBottom } from '@vx/axis';
import { Group } from '@vx/group';
import { AreaClosed } from '@vx/shape';
import { scaleTime, scaleLinear } from '@vx/scale';
import { extent, max } from 'd3-array';
const x = ([timestamp,]) => new Date(timestamp);
const y = ([, latency]) => latency;
const margin = {
top: 0,
bottom: 50,
left: 50,
right: 0,
};
const tick = {
textAnchor: 'middle',
fontSize: 8,
fontFamily: 'system-ui, sans-serif',
};
const leftTick = {
...tick,
dx: '-0.25em',
dy: '0.25em',
textAnchor: 'end',
};
const label = {
...tick,
fontSize: 10,
};
export default class Graph extends Component {
constructor(props) {
super(props);
processData() {
const { data: unprocessedData } = this.props;
const { width, height, data } = props;
this.xMax = width - margin.left - margin.right;
this.yMax = height - margin.top - margin.bottom;
const data = unprocessedData.map(([timestamp, latency]) => ({
x: timestamp,
y: latency,
})).reverse();
this.xScale = scaleTime({
range: [0, this.xMax],
domain: extent(data, x),
});
this.yScale = scaleLinear({
range: [this.yMax, 0],
domain: [0, max(data, y)],
nice: true,
});
return [
{
id: 'latency',
color: 'hsl(220, 100%, 75%)',
data,
},
];
}
render() {
return (
<svg className="graph" width={this.props.width} height={this.props.height}>
<GradientOrangeRed id="gradient"/>
<div className="graph-container">
<ResponsiveLine
data={this.processData()}
margin={{ top: 30, left: 70, bottom: 50 }}
minY="auto"
maxY={900}
curve="monotoneX"
<Group top={margin.top} left={margin.left}>
<AreaClosed
data={this.props.data}
xScale={this.xScale} yScale={this.yScale}
x={x} y={y}
stroke=""
fill="url(#gradient)"
curve={curveNatural}
/>
<AxisLeft
scale={this.yScale}
top={0} left={0}
labelProps={label}
tickFormat={(value, index) => `${value}ms`}
tickLabelProps={(value, index) => leftTick}
label=''
/>
<AxisBottom
scale={this.xScale}
top={this.yMax}
tickFormat={(value, index) => `${value.toLocaleTimeString()}`}
labelProps={label}
tickLabelProps={(value, index) => tick}
label=''
/>
</Group>
</svg>
tooltipFormat={(d) => `${d}ms`}
axisLeft={{
format: (d) => `${d}ms`,
tickCount: 3,
legend: 'latency',
legendPosition: 'center',
legendOffset: -55,
tickSize: 0,
}}
axisBottom={{
format: (epoch) => {
const minutesAgo = Math.floor((Date.now() - epoch) / (1000 * 60));
if (minutesAgo % 5 !== 0 || minutesAgo === 0) {
return undefined;
}
return ms(Date.now() - epoch);
},
tickSize: 0,
legend: 'time ago',
legendPosition: 'center',
legendOffset: 40,
}}
enableDots={false}
enableArea
/>
</div>
);
}
}

View File

@ -3,19 +3,27 @@ import React from 'react';
import Graph from './Graph.js';
import './Service.css';
const Service = ({ graph, name, status, latency, description }) =>
const Service = ({ graph, name, status, latency, description }) => (
<div className="service">
<header className="service__header">
<div className="service__header__emoji">{status ? '✅' : '🚫'}</div>
<div className="service__header__emoji">
{status ? '✅' : '🚫'}
</div>
<h2 className="service__title">
{name}
{latency ? <span className="service__title__latency">{latency}ms</span> : null}
{latency ? (
<span className="service__title__latency">
{latency}
ms
</span>
) : null}
</h2>
</header>
<p className="service__description">
{description}
</p>
{graph ? <Graph width={500} height={175} data={graph}/> : null}
{graph ? <Graph width={500} height={175} data={graph} /> : null}
</div>
);
export default Service;

View File

@ -6,5 +6,5 @@ import App from './App';
ReactDOM.render(
<App />,
document.getElementById('root')
document.getElementById('root'),
);

View File

@ -0,0 +1,7 @@
module.exports = {
UNSUBSCRIBE: -1,
SUBSCRIBE: 0,
SUBSCRIBED: 1,
UNSUBSCRIBED: 2,
DATA: 3,
};