import React, { Component } from 'react' import './App.css' import Service from './Service.js' import ServicePlaceholder from './ServicePlaceholder.js' import DegradedNotice from './DegradedNotice.js' import OP from '../ws/op.js' import { log, objectFromEntries } from '../util.js' import { domain as DOMAIN } from '../config.json' export default class App extends Component { websocket = null reconnectionTime = 1000 state = { loading: true, error: null, metrics: null, } async componentDidMount () { await this.loadMetrics() this.connect() } subscribeToChannels () { if (this.state.metrics == null) { // fetching state failed, bail log('not going to subscribe to realtime updates due to fetch failure') return } const { graph } = this.state.metrics const channels = Object.keys(graph).reduce( (channels, name) => [...channels, `latency:${name}`, `status:${name}`], [] ) log( `subscribing to ${channels.length} channels: ${channels.join(', ')}` ) this._send({ op: OP.SUBSCRIBE, channels, }) } handleStatus (name, [, status]) { const { status: statuses } = this.state.metrics log('updating status on:', name) if (!(name in statuses)) { log(`failed to locate channel ${name} to update statuses`) return } if (statuses[name].status === status) { log(`ignoring stale status (${status}) for ${name}`) return } this.setState(({ metrics: old }, _props) => { const metrics = { ...old } metrics.status[name].status = status return { metrics } }) } handleLatency (name, data) { const { metrics } = this.state log('adding latency entry:', data) // latency entries come in newest to oldest, so remove the oldest entry const graph = metrics.graph[name].slice(0, metrics.graph[name].length - 1) // make new data come in first const newGraph = [data, ...graph] this.setState(({ metrics: old }, _props) => { const metrics = { ...old } metrics.graph[name] = newGraph const [, latency] = data metrics.status[name].latency = latency return { metrics } }) } handlePacket (packet) { const { op, c: channel, d: data } = packet if (op !== OP.DATA) { return } const [type, name] = channel.split(':') log('updating from channel:', channel) if (type === 'latency') { this.handleLatency(name, data) } else if (type === 'status') { this.handleStatus(name, data) } } connect () { log('connecting to ws') const endpoint = `${DOMAIN}/api/streaming` .replace('https', 'wss') .replace('http', 'ws') this.websocket = new WebSocket(endpoint) this.websocket.onopen = () => { log('ws opened') this.subscribeToChannels() } this.websocket.onclose = ({ code, reason }) => { log(`ws closed with code ${code} (reason: ${reason || ''}); ` + `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) console.log( '%c>>>%c', 'color: hsla(320, 100%, 50%, 1); font-weight: bold', 'color: inherit; font-weight: inherit', 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 { var resp = await fetch(`${DOMAIN}/api/status`) } catch (err) { this.setState({ error: `Network error: ${err}`, }) } if (!resp.ok) { this.setState({ error: `Failed to fetch stats (${resp.status} ${resp.statusText})`, }) return } const json = await resp.json() this.setState({ metrics: json, loading: false }) } render () { let metrics = null if (this.state.metrics) { const allServices = Object.entries(this.state.metrics.status) const graphs = this.state.metrics.graph const services = allServices.map(([name, info]) => ( )) const down = allServices.filter(([, { status }]) => !status) const notice = down.length > 0 ? : null metrics = (
{notice} {services}
) } return (

elstatus

{this.state.error ? (
Error: {this.state.error}
) : null} {this.state.loading && !this.state.error ? ( ) : metrics}
) } }