Move to nivo from nx and add WS support

This commit is contained in:
Skip R. 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", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@vx/axis": "^0.0.165", "@nivo/line": "^0.42.1",
"@vx/curve": "^0.0.165", "ms": "^2.1.1",
"@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",
"react": "^16.4.0", "react": "^16.4.0",
"react-dom": "^16.4.0", "react-dom": "^16.4.0",
"react-scripts": "1.1.4" "react-scripts": "1.1.4"

View file

@ -3,45 +3,145 @@ import React, { Component } from 'react';
import Service from './Service.js'; import Service from './Service.js';
import './App.css'; 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' // const ENDPOINT = 'http://localhost:8069/api/status'
export default class App extends Component { export default class App extends Component {
websocket = null;
state = { state = {
loading: true, loading: true,
error: null, error: null,
metrics: null, metrics: null,
}; };
componentDidMount() { _send(payload) {
fetch(ENDPOINT) this.websocket.send(JSON.stringify(payload));
.then((resp) => resp.json())
.then((data) => {
this.setState({ metrics: data, loading: false });
})
.catch((error) => {
this.setState({ error: error.toString() });
})
} }
_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() { render() {
const metrics = !this.state.metrics ? null : ( const metrics = !this.state.metrics ? null : (
<div className="services"> <div className="services">
{Object.entries(this.state.metrics.status) {Object.entries(this.state.metrics.status)
.map(([name, info]) => .map(([name, info]) => (
<Service name={name} key={name} <Service
name={name}
key={name}
graph={this.state.metrics.graph[name]} graph={this.state.metrics.graph[name]}
{...info} {...info}
/>) />
))
} }
</div> </div>
); );
return ( return (
<div className="dashboard"> <div className="dashboard">
<h1>elstatus</h1> <h1>
{this.state.loading ? <div>Loading metrics...</div> : null} elstatus
{this.state.error ? <div>Error: {this.state.error}</div> : null} </h1>
{this.state.loading ? (
<div>
Loading metrics...
</div>
) : null}
{this.state.error ? (
<div>
Error:
{this.state.error}
</div>
) : null}
{metrics} {metrics}
</div> </div>
); );

View file

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

View file

@ -1,94 +1,68 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import ms from 'ms';
import { ResponsiveLine } from '@nivo/line';
import './Graph.css'; 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 { export default class Graph extends Component {
constructor(props) { processData() {
super(props); const { data: unprocessedData } = this.props;
const { width, height, data } = props; const data = unprocessedData.map(([timestamp, latency]) => ({
this.xMax = width - margin.left - margin.right; x: timestamp,
this.yMax = height - margin.top - margin.bottom; y: latency,
})).reverse();
this.xScale = scaleTime({ return [
range: [0, this.xMax], {
domain: extent(data, x), id: 'latency',
}); color: 'hsl(220, 100%, 75%)',
this.yScale = scaleLinear({ data,
range: [this.yMax, 0], },
domain: [0, max(data, y)], ];
nice: true,
});
} }
render() { render() {
return ( return (
<svg className="graph" width={this.props.width} height={this.props.height}> <div className="graph-container">
<GradientOrangeRed id="gradient"/> <ResponsiveLine
data={this.processData()}
margin={{ top: 30, left: 70, bottom: 50 }}
minY="auto"
maxY={900}
curve="monotoneX"
<Group top={margin.top} left={margin.left}> tooltipFormat={(d) => `${d}ms`}
<AreaClosed
data={this.props.data} axisLeft={{
xScale={this.xScale} yScale={this.yScale} format: (d) => `${d}ms`,
x={x} y={y} tickCount: 3,
stroke="" legend: 'latency',
fill="url(#gradient)" legendPosition: 'center',
curve={curveNatural} 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
/> />
<AxisLeft </div>
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>
); );
} }
} }

View file

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

View file

@ -6,5 +6,5 @@ import App from './App';
ReactDOM.render( ReactDOM.render(
<App />, <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,
};