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

View file

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

View file

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react'
import PropTypes from 'prop-types'; import PropTypes from 'prop-types'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import './Service.css'; import './Service.css'
import Graph from './Graph.js'; import Graph from './Graph.js'
const Service = ({ graph, name, status, latency, description }) => ( const Service = ({ graph, name, status, latency, description }) => (
<div className="service"> <div className="service">
@ -28,12 +28,12 @@ const Service = ({ graph, name, status, latency, description }) => (
</p> </p>
{graph ? <Graph data={graph} /> : null} {graph ? <Graph data={graph} /> : null}
</div> </div>
); )
Service.defaultProps = { Service.defaultProps = {
graph: null, graph: null,
latency: null, latency: null,
}; }
Service.propTypes = { Service.propTypes = {
graph: PropTypes.arrayOf( graph: PropTypes.arrayOf(
@ -43,6 +43,6 @@ Service.propTypes = {
status: PropTypes.bool.isRequired, status: PropTypes.bool.isRequired,
latency: PropTypes.number, latency: PropTypes.number,
description: PropTypes.string.isRequired, 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 ReactPlaceholder from 'react-placeholder'
import 'react-placeholder/lib/reactPlaceholder.css'; import 'react-placeholder/lib/reactPlaceholder.css'
import './ServicePlaceholder.css'; import './ServicePlaceholder.css'
const ServicePlaceholder = () => ( const ServicePlaceholder = () => (
<div className="service-placeholder"> <div className="service-placeholder">
@ -44,6 +44,6 @@ const ServicePlaceholder = () => (
{' '} {' '}
</ReactPlaceholder> </ReactPlaceholder>
</div> </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 { import {
faCheckCircle, faCheckCircle,
faExclamationCircle, faExclamationCircle,
} from '@fortawesome/free-solid-svg-icons'; } from '@fortawesome/free-solid-svg-icons'
export default function register() { export default function register () {
library.add( library.add(
faCheckCircle, faCheckCircle,
faExclamationCircle, faExclamationCircle,
); )
} }

View file

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

View file

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

View file

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