This commit is contained in:
slice 2018-07-15 21:04:43 -07:00
parent ebe6105fb3
commit e33f0fa558
No known key found for this signature in database
GPG Key ID: 1508C19D7436A26D
8 changed files with 131 additions and 124 deletions

View File

@ -1,12 +1,12 @@
import React, { Component } from 'react';
import React, { Component } from 'react'
import './App.css';
import Service from './Service.js';
import ServicePlaceholder from './ServicePlaceholder.js';
import OP from '../ws/op.js';
import { log } from '../util.js';
import './App.css'
import Service from './Service.js'
import ServicePlaceholder from './ServicePlaceholder.js'
import OP from '../ws/op.js'
import { log } from '../util.js'
const DOMAIN = 'https://elstatus.stayathomeserver.club';
const DOMAIN = 'https://elstatus.stayathomeserver.club'
// const ENDPOINT = 'http://localhost:8069/api/status'
export default class App extends Component {
@ -20,101 +20,101 @@ export default class App extends Component {
metrics: null,
};
async componentDidMount() {
await this.loadMetrics();
this.connect();
async componentDidMount () {
await this.loadMetrics()
this.connect()
}
subscribeToChannels() {
const channels = Object.keys(this.state.metrics.graph).map((channel) => `latency:${channel}`);
log('subscribing to channels:', channels);
subscribeToChannels () {
const channels = Object.keys(this.state.metrics.graph).map((channel) => `latency:${channel}`)
log('subscribing to channels:', channels)
this._send({
op: OP.SUBSCRIBE,
channels,
});
})
}
handlePacket(packet) {
const { op, c: channel, d: data } = packet;
handlePacket (packet) {
const { op, c: channel, d: data } = packet
if (op !== OP.DATA) {
log('ignoring boring packet:', packet);
return;
log('ignoring boring packet:', packet)
return
}
const [, name] = channel.split(':');
const [, name] = channel.split(':')
log('updating from channel:', channel);
log('updating from channel:', channel)
const { metrics } = this.state;
const graph = metrics.graph[name].slice(1);
const newGraph = [data, ...graph];
const { metrics } = this.state
const graph = metrics.graph[name].slice(1)
const newGraph = [data, ...graph]
log('adding data:', data);
log('adding data:', data)
this.setState(({ metrics: oldMetrics }, _props) => {
const newMetrics = { ...oldMetrics };
newMetrics.graph[name] = newGraph;
const newMetrics = { ...oldMetrics }
newMetrics.graph[name] = newGraph
const [, latency] = data;
newMetrics.status[name].latency = latency;
const [, latency] = data
newMetrics.status[name].latency = latency
return {
metrics: newMetrics,
};
});
}
})
}
connect() {
log('connecting to ws');
connect () {
log('connecting to ws')
const endpoint = (`${DOMAIN}/api/streaming`).replace('https', 'wss');
this.websocket = new WebSocket(endpoint);
const endpoint = (`${DOMAIN}/api/streaming`).replace('https', 'wss')
this.websocket = new WebSocket(endpoint)
this.websocket.onopen = () => {
log('ws opened');
this.subscribeToChannels();
};
log('ws opened')
this.subscribeToChannels()
}
this.websocket.onclose = ({ code, reason }) => {
log(`ws closed with code ${code} (reason: ${reason || '<none>'}); `
+ `attempting to reconnect in ${this.reconnectionTime}ms`);
setTimeout(() => this.connect(), this.reconnectionTime);
this.reconnectionTime *= 2;
};
log(`ws closed with code ${code} (reason: ${reason || '<none>'}); ` +
`attempting to reconnect in ${this.reconnectionTime}ms`)
setTimeout(() => this.connect(), this.reconnectionTime)
this.reconnectionTime *= 2
}
this.websocket.onmessage = (message) => {
const { data } = message;
const parsed = JSON.parse(data);
log('ws recv:', parsed);
const { data } = message
const parsed = JSON.parse(data)
log('ws recv:', parsed)
this.handlePacket(parsed);
};
this.handlePacket(parsed)
}
this.websocket.onerror = (event) => {
log('ws error:', event);
};
}
_send(payload) {
this.websocket.send(JSON.stringify(payload));
}
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() });
log('ws error:', event)
}
}
render() {
_send (payload) {
this.websocket.send(JSON.stringify(payload))
}
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)
@ -128,7 +128,7 @@ export default class App extends Component {
))
}
</div>
);
)
return (
<div className="dashboard">
@ -148,6 +148,6 @@ export default class App extends Component {
</React.Fragment>
) : metrics}
</div>
);
)
}
}

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ms from 'ms';
import ms from 'ms'
import {
ResponsiveContainer,
AreaChart,
@ -11,9 +11,9 @@ import {
Tooltip,
Area,
ReferenceLine,
} from 'recharts';
} from 'recharts'
import './Graph.css';
import './Graph.css'
export default class Graph extends Component {
static propTypes = {
@ -24,34 +24,47 @@ export default class Graph extends Component {
screenWidth: window.innerWidth,
}
componentDidMount() {
window.addEventListener('resize', this.handleScreenChange);
componentDidMount () {
window.addEventListener('resize', this.handleScreenChange)
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleScreenChange);
componentWillUnmount () {
window.removeEventListener('resize', this.handleScreenChange)
}
handleScreenChange = () => {
this.setState({
screenWidth: window.innerWidth,
});
})
}
processData() {
const { data } = this.props;
processData () {
const { data } = this.props
return data.map(([timestamp, latency]) => ({
const objects = data.map(([timestamp, latency]) => ({
timestamp,
latency,
})).sort(({ timestamp: a }, { timestamp: b }) => a - b);
}))
// sort so that new entries are first
return objects.sort(({ timestamp: a }, { timestamp: b }) => a - b)
}
isSmallScreen() {
return this.state.screenWidth < 500;
isSmallScreen () {
return this.state.screenWidth < 500
}
render() {
render () {
const yAxis = this.isSmallScreen()
? null
: (
<YAxis
dataKey="latency"
tickLine={false}
tickFormatter={(tick) => `${tick}ms`}
/>
)
return (
<div className="graph-container">
<ResponsiveContainer width="100%" height={175}>
@ -63,14 +76,8 @@ export default class Graph extends Component {
tickFormatter={(tick) => ms(Date.now() - tick)}
tickLine={false}
/>
{this.isSmallScreen()
? null
: <YAxis
dataKey="latency"
tickLine={false}
tickFormatter={(tick) => `${tick}ms`}
/>
}
{yAxis}
<CartesianGrid strokeDasharray="1 1" />
<Tooltip
isAnimationActive={false}
@ -94,6 +101,6 @@ export default class Graph extends Component {
</AreaChart>
</ResponsiveContainer>
</div>
);
)
}
}

View File

@ -1,10 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import React from 'react'
import PropTypes from 'prop-types'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import './Service.css';
import Graph from './Graph.js';
import './Service.css'
import Graph from './Graph.js'
const Service = ({ graph, name, status, latency, description }) => (
<div className="service">
@ -28,12 +28,12 @@ const Service = ({ graph, name, status, latency, description }) => (
</p>
{graph ? <Graph data={graph} /> : null}
</div>
);
)
Service.defaultProps = {
graph: null,
latency: null,
};
}
Service.propTypes = {
graph: PropTypes.arrayOf(
@ -43,6 +43,6 @@ Service.propTypes = {
status: PropTypes.bool.isRequired,
latency: PropTypes.number,
description: PropTypes.string.isRequired,
};
}
export default Service;
export default Service

View File

@ -1,9 +1,9 @@
import React from 'react';
import React from 'react'
import ReactPlaceholder from 'react-placeholder';
import 'react-placeholder/lib/reactPlaceholder.css';
import ReactPlaceholder from 'react-placeholder'
import 'react-placeholder/lib/reactPlaceholder.css'
import './ServicePlaceholder.css';
import './ServicePlaceholder.css'
const ServicePlaceholder = () => (
<div className="service-placeholder">
@ -44,6 +44,6 @@ const ServicePlaceholder = () => (
{' '}
</ReactPlaceholder>
</div>
);
)
export default ServicePlaceholder;
export default ServicePlaceholder

View File

@ -1,12 +1,12 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faCheckCircle,
faExclamationCircle,
} from '@fortawesome/free-solid-svg-icons';
} from '@fortawesome/free-solid-svg-icons'
export default function register() {
export default function register () {
library.add(
faCheckCircle,
faExclamationCircle,
);
)
}

View File

@ -1,13 +1,13 @@
import React from 'react';
import ReactDOM from 'react-dom';
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css';
import App from './components/App';
import register from './icons.js';
import './index.css'
import App from './components/App'
import register from './icons.js'
register();
register()
ReactDOM.render(
<App />,
document.getElementById('root'),
);
)

View File

@ -1,8 +1,8 @@
export function log(...args) {
export function log (...args) {
console.log(
'%c[elstat]%c',
'color: purple; font-weight: bold',
'color: inherit; font-weight: inherit',
...args,
);
)
}

View File

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