-
{keys.length} service{plural} {indicative} unreachable
+
+ {keys.length} service
+ {plural} {indicative} unreachable
+
elstat is having trouble contacting {serviceNames}.
diff --git a/priv/frontend/src/components/Graph.js b/priv/frontend/src/components/Graph.js
index 670d140..bf214fd 100644
--- a/priv/frontend/src/components/Graph.js
+++ b/priv/frontend/src/components/Graph.js
@@ -24,11 +24,11 @@ export default class Graph extends Component {
screenWidth: window.innerWidth,
}
- componentDidMount () {
+ componentDidMount() {
window.addEventListener('resize', this.handleScreenChange)
}
- componentWillUnmount () {
+ componentWillUnmount() {
window.removeEventListener('resize', this.handleScreenChange)
}
@@ -38,7 +38,7 @@ export default class Graph extends Component {
})
}
- processData () {
+ processData() {
const { data } = this.props
const objects = data.map(([timestamp, latency]) => ({
@@ -50,27 +50,23 @@ export default class Graph extends Component {
return objects.sort(({ timestamp: a }, { timestamp: b }) => a - b)
}
- isSmallScreen () {
+ isSmallScreen() {
return this.state.screenWidth < 500
}
- render () {
- const yAxis = this.isSmallScreen()
- ? null
- : (
-
`${tick}ms`}
- />
- )
+ render() {
+ const yAxis = this.isSmallScreen() ? null : (
+ `${tick}ms`}
+ />
+ )
return (
-
+
ms(Date.now() - tick)}
@@ -78,7 +74,7 @@ export default class Graph extends Component {
/>
{yAxis}
-
+
`${value}ms`}
@@ -86,11 +82,7 @@ export default class Graph extends Component {
separator=": "
labelFormatter={() => null}
/>
-
+
- ).reverse() // show newest first
+ const stageNodes = stages
+ .map((stage) => )
+ .reverse() // show newest first
return (
-
{OUTAGE_TYPES[type] || type}: {title}
+
+ {OUTAGE_TYPES[type] || type}: {title}
+
{content}
- {stageNodes.length
- ? (
-
- {stageNodes}
-
- )
- : null
- }
+ {stageNodes.length ?
{stageNodes}
: null}
)
}
diff --git a/priv/frontend/src/components/Incidents.js b/priv/frontend/src/components/Incidents.js
index 774c527..d45dcf8 100644
--- a/priv/frontend/src/components/Incidents.js
+++ b/priv/frontend/src/components/Incidents.js
@@ -11,26 +11,26 @@ export default class Incidents extends React.Component {
page: 0,
}
- componentDidMount () {
+ componentDidMount() {
this.fetchIncidents()
}
- async fetchIncidents () {
+ async fetchIncidents() {
const resp = await strictFetch(`${DOMAIN}/api/incidents/${this.state.page}`)
this.setState({ incidents: await resp.json() })
}
- renderIncidents () {
+ renderIncidents() {
if (!this.state.incidents) {
return null
}
- return this.state.incidents.map(
- (incident) =>
- )
+ return this.state.incidents.map((incident) => (
+
+ ))
}
- render () {
+ render() {
return (
Incidents
diff --git a/priv/frontend/src/components/Page.js b/priv/frontend/src/components/Page.js
index 17c8192..549b378 100644
--- a/priv/frontend/src/components/Page.js
+++ b/priv/frontend/src/components/Page.js
@@ -3,12 +3,8 @@ import PropTypes from 'prop-types'
import './Page.css'
-export default function Page ({ children }) {
- return (
-
- {children}
-
- )
+export default function Page({ children }) {
+ return {children}
}
Page.propTypes = {
diff --git a/priv/frontend/src/components/Service.css b/priv/frontend/src/components/Service.css
index 8e33eaf..f31190d 100644
--- a/priv/frontend/src/components/Service.css
+++ b/priv/frontend/src/components/Service.css
@@ -11,13 +11,13 @@
margin: 0;
}
-.service .latency {
+.service .information {
margin-left: 0.5em;
color: hsl(0, 0%, 45%);
}
@media (max-width: 500px) {
- .service .latency {
+ .service .information {
font-size: 1rem;
}
}
@@ -32,13 +32,13 @@
}
.service.service-alive .emoji {
- color: #2ECC40;
+ color: #2ecc40;
}
.service.service-slow .emoji {
- color: #FF851B;
+ color: #ff851b;
}
.service.service-dead .emoji {
- color: #FF4136;
+ color: #ff4136;
}
diff --git a/priv/frontend/src/components/Service.js b/priv/frontend/src/components/Service.js
index a6602a8..cd9f66b 100644
--- a/priv/frontend/src/components/Service.js
+++ b/priv/frontend/src/components/Service.js
@@ -7,10 +7,9 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import './Service.css'
import Graph from './Graph.js'
import config from '../config.json'
+import { truncateToTwoPlaces } from '../util'
-const {
- slow_threshold: SLOW_THRESHOLD = 1500,
-} = config
+const { slow_threshold: SLOW_THRESHOLD = 1500 } = config
const icons = {
alive: 'check-circle',
@@ -24,7 +23,7 @@ const titles = {
dead: 'This service is unresponsive.',
}
-export function getServiceState (status, latency, threshold = SLOW_THRESHOLD) {
+export function getServiceState(status, latency, threshold = SLOW_THRESHOLD) {
if (status && latency > threshold) {
return 'slow'
}
@@ -32,35 +31,41 @@ export function getServiceState (status, latency, threshold = SLOW_THRESHOLD) {
return status ? 'alive' : 'dead'
}
-export default function Service ({ graph, name, status, latency, description }) {
+export default function Service({
+ graph,
+ name,
+ status,
+ latency,
+ description,
+ uptime,
+}) {
const state = getServiceState(status, latency)
const title = titles[state]
const icon = icons[state]
- const className = classnames(
- 'service',
- `service-${state}`
- )
+ const className = classnames('service', `service-${state}`)
+ const uptimePercentage = truncateToTwoPlaces(uptime)
return (
-
+
- {name} {latency ? (
-
- {Math.round(latency)}ms
+ {name}{' '}
+ {latency ? (
+
+ {Math.round(latency)}
+ ms ({uptimePercentage}
+ %)
) : null}
-
- {description}
-
- {graph ?
: null}
+ {description}
+ {graph ? : null}
)
}
@@ -71,9 +76,7 @@ Service.defaultProps = {
}
Service.propTypes = {
- graph: PropTypes.arrayOf(
- PropTypes.arrayOf(PropTypes.number),
- ),
+ graph: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.number)),
name: PropTypes.string.isRequired,
status: PropTypes.bool.isRequired,
latency: PropTypes.number,
diff --git a/priv/frontend/src/components/ServicePlaceholder.js b/priv/frontend/src/components/ServicePlaceholder.js
index 236090f..9063802 100644
--- a/priv/frontend/src/components/ServicePlaceholder.js
+++ b/priv/frontend/src/components/ServicePlaceholder.js
@@ -5,7 +5,7 @@ import 'react-placeholder/lib/reactPlaceholder.css'
import './ServicePlaceholder.css'
-export default function ServicePlaceholder () {
+export default function ServicePlaceholder() {
return (
diff --git a/priv/frontend/src/components/Stage.js b/priv/frontend/src/components/Stage.js
index fe3a258..dfa7330 100644
--- a/priv/frontend/src/components/Stage.js
+++ b/priv/frontend/src/components/Stage.js
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types'
import ms from 'ms'
-export default function Stage ({ stage }) {
+export default function Stage({ stage }) {
const { created_at: createdAt, title, content } = stage
const ago = ms(Date.now() - createdAt * 1000)
diff --git a/priv/frontend/src/components/Status.js b/priv/frontend/src/components/Status.js
index 5b2d0f2..5da60b0 100644
--- a/priv/frontend/src/components/Status.js
+++ b/priv/frontend/src/components/Status.js
@@ -5,21 +5,19 @@ import classnames from 'classnames'
import './Status.css'
import Incident from './Incident'
-export default function Status ({ incident }) {
+export default function Status({ incident }) {
const incidentOngoing = incident && incident.ongoing === 1
- const view = incidentOngoing
- ?
- : 'All systems operational'
+ const view = incidentOngoing ? (
+
+ ) : (
+ 'All systems operational'
+ )
const className = classnames(
'status',
- incidentOngoing ? 'status-bad' : 'status-good',
- )
- return (
-
- {view}
-
+ incidentOngoing ? 'status-bad' : 'status-good'
)
+ return {view}
}
Status.propTypes = {
diff --git a/priv/frontend/src/components/__tests__/DegradedNotice.js b/priv/frontend/src/components/__tests__/DegradedNotice.js
index bde4912..7fb3fdd 100644
--- a/priv/frontend/src/components/__tests__/DegradedNotice.js
+++ b/priv/frontend/src/components/__tests__/DegradedNotice.js
@@ -4,23 +4,34 @@ import { shallow } from 'enzyme'
import DegradedNotice from '../DegradedNotice'
it('renders without crashing', () => {
- shallow()
+ shallow()
})
it('shows one service', () => {
- const comp = shallow()
+ const comp = shallow()
expect(comp.contains()).toEqual(true)
- expect(comp.contains(
- elstat is having trouble contacting first.
- )).toEqual(true)
+ expect(
+ comp.contains(
+
+ elstat is having trouble contacting first.
+
+ )
+ ).toEqual(true)
})
it('shows multiple services', () => {
- const comp = shallow()
- expect(comp.contains(
- 3 services are unreachable
- )).toEqual(true)
- expect(comp.contains(
- elstat is having trouble contacting first, second, third.
- )).toEqual(true)
+ const comp = shallow(
+
+ )
+ expect(comp.contains(3 services are unreachable)).toEqual(
+ true
+ )
+ expect(
+ comp.contains(
+
+ elstat is having trouble contacting{' '}
+ first, second, third.
+
+ )
+ ).toEqual(true)
})
diff --git a/priv/frontend/src/components/__tests__/Service.js b/priv/frontend/src/components/__tests__/Service.js
index 95bc760..028fc92 100644
--- a/priv/frontend/src/components/__tests__/Service.js
+++ b/priv/frontend/src/components/__tests__/Service.js
@@ -3,34 +3,39 @@ import { shallow } from 'enzyme'
import Service, { getServiceState } from '../Service'
-const graph = [
- [1000, 50],
- [2000, 30],
- [3000, 60],
-]
+const graph = [[1000, 50], [2000, 30], [3000, 60]]
const props = {
name: 'sample service',
description: 'a cool service',
latency: 50.5,
+ uptime: 99.9994,
}
describe('', () => {
it('renders without crashing', () => {
- shallow()
+ shallow()
})
it('omits information', () => {
- const comp = shallow()
+ const comp = shallow(
+
+ )
expect(comp.find('h2.title').text()).toEqual('sample service ')
})
it('renders proper information', () => {
- const comp = shallow()
+ const comp = shallow()
expect(comp.prop('className')).toEqual('service service-alive')
- expect(comp.find('h2.title').text()).toEqual('sample service 51ms')
- expect(comp.contains(a cool service
)).toEqual(true)
- expect(comp.contains(51ms)).toEqual(true)
+ expect(comp.find('h2.title').text()).toEqual(
+ 'sample service 51ms (99.9994%)'
+ )
+ expect(
+ comp.contains(a cool service
)
+ ).toEqual(true)
+ expect(
+ comp.contains(51ms (99.9994%))
+ ).toEqual(true)
})
})
diff --git a/priv/frontend/src/icons.js b/priv/frontend/src/icons.js
index 7ca46f3..ee01b01 100644
--- a/priv/frontend/src/icons.js
+++ b/priv/frontend/src/icons.js
@@ -5,10 +5,6 @@ import {
faExclamationCircle,
} from '@fortawesome/free-solid-svg-icons'
-export default function register () {
- library.add(
- faCheckCircle,
- faTimesCircle,
- faExclamationCircle,
- )
+export default function register() {
+ library.add(faCheckCircle, faTimesCircle, faExclamationCircle)
}
diff --git a/priv/frontend/src/index.js b/priv/frontend/src/index.js
index aa4d2ca..3787e4a 100644
--- a/priv/frontend/src/index.js
+++ b/priv/frontend/src/index.js
@@ -7,7 +7,4 @@ import register from './icons.js'
register()
-ReactDOM.render(
- ,
- document.getElementById('root')
-)
+ReactDOM.render(, document.getElementById('root'))
diff --git a/priv/frontend/src/util.js b/priv/frontend/src/util.js
index af6e6f6..6e00d52 100644
--- a/priv/frontend/src/util.js
+++ b/priv/frontend/src/util.js
@@ -8,14 +8,19 @@ export const logger = ({ name, color }) => (...args) =>
export const log = logger({ name: 'elstat', color: 'purple' })
-export function objectFromEntries (entries) {
+export function objectFromEntries(entries) {
return entries.reduce(
(object, [key, value]) => ({ ...object, [key]: value }),
{}
)
}
-export async function strictFetch (...args) {
+export function truncateToTwoPlaces(number) {
+ // https://stackoverflow.com/a/4187164/2491753
+ return number.toString().match(/^-?\d+(?:\.\d{0,4})?/)[0]
+}
+
+export async function strictFetch(...args) {
const resp = await fetch(...args)
if (!resp.ok) {
diff --git a/priv/frontend/src/ws/client.js b/priv/frontend/src/ws/client.js
index 65330d1..4d1078d 100644
--- a/priv/frontend/src/ws/client.js
+++ b/priv/frontend/src/ws/client.js
@@ -5,7 +5,7 @@ import { logger } from '../util'
const log = logger({ name: 'ws', color: 'green' })
export default class StreamingClient extends NanoEvents {
- constructor (url, metrics) {
+ constructor(url, metrics) {
super()
this.url = url
@@ -14,11 +14,11 @@ export default class StreamingClient extends NanoEvents {
this.delay = 1000
}
- send (packet) {
+ send(packet) {
this.ws.send(JSON.stringify(packet))
}
- subscribe () {
+ subscribe() {
if (!this.metrics) {
log('not subscribing to channels -- no initial metrics')
return
@@ -36,7 +36,7 @@ export default class StreamingClient extends NanoEvents {
this.send({ op: OP.SUBSCRIBE, channels })
}
- handle (packet) {
+ handle(packet) {
const { op, c: channel, d: data } = packet
if ([OP.SUBSCRIBED, OP.UNSUBSCRIBED].includes(op)) {
@@ -52,11 +52,11 @@ export default class StreamingClient extends NanoEvents {
}
}
- connect () {
+ connect() {
const begin = Date.now()
log('connecting')
- const ws = this.ws = new WebSocket(this.url)
+ const ws = (this.ws = new WebSocket(this.url))
window.ws = ws
ws.onopen = () => {
@@ -68,7 +68,7 @@ export default class StreamingClient extends NanoEvents {
ws.onclose = ({ code, reason }) => {
log(
`ws closed with code ${code} (reason: ${reason || ''}); ` +
- `attempting to reconnect in ${this.delay}ms`
+ `attempting to reconnect in ${this.delay}ms`
)
setTimeout(() => this.connect(), this.delay)