Move to nivo from nx and add WS support
This commit is contained in:
parent
60208b29db
commit
f3f500b344
8 changed files with 11695 additions and 109 deletions
11501
priv/frontend/package-lock.json
generated
Normal file
11501
priv/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
.graph {
|
||||
margin-top: 1em;
|
||||
.graph-container {
|
||||
width: 100%;
|
||||
height: 10rem;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -6,5 +6,5 @@ import App from './App';
|
|||
|
||||
ReactDOM.render(
|
||||
<App />,
|
||||
document.getElementById('root')
|
||||
document.getElementById('root'),
|
||||
);
|
||||
|
|
7
priv/frontend/src/ws/op.js
Normal file
7
priv/frontend/src/ws/op.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
module.exports = {
|
||||
UNSUBSCRIBE: -1,
|
||||
SUBSCRIBE: 0,
|
||||
SUBSCRIBED: 1,
|
||||
UNSUBSCRIBED: 2,
|
||||
DATA: 3,
|
||||
};
|
Loading…
Reference in a new issue