Merge branch 'master' of gitlab.com:elixire/elstat
This commit is contained in:
commit
673e5bb0fc
19 changed files with 174 additions and 152 deletions
7
priv/frontend/.prettierrc
Normal file
7
priv/frontend/.prettierrc
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"arrowParens": "always",
|
||||
"proseWrap": "always"
|
||||
}
|
|
@ -124,9 +124,15 @@ export default class Dashboard extends Component {
|
|||
}
|
||||
|
||||
renderServices(services) {
|
||||
const { graph: graphs } = this.state.metrics
|
||||
const { graph: graphs, uptime: uptimes } = this.state.metrics
|
||||
return services.map(([name, info]) => (
|
||||
<Service name={name} key={name} graph={graphs[name]} {...info}/>
|
||||
<Service
|
||||
name={name}
|
||||
key={name}
|
||||
graph={graphs[name]}
|
||||
uptime={parseFloat(uptimes[name])}
|
||||
{...info}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,10 @@ export default function DegradedNotice ({ services }) {
|
|||
|
||||
return (
|
||||
<div className="degraded-notice">
|
||||
<header>{keys.length} service{plural} {indicative} unreachable</header>
|
||||
<header>
|
||||
{keys.length} service
|
||||
{plural} {indicative} unreachable
|
||||
</header>
|
||||
<p>
|
||||
elstat is having trouble contacting <strong>{serviceNames}</strong>.
|
||||
</p>
|
||||
|
|
|
@ -55,9 +55,7 @@ export default class Graph extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const yAxis = this.isSmallScreen()
|
||||
? null
|
||||
: (
|
||||
const yAxis = this.isSmallScreen() ? null : (
|
||||
<YAxis
|
||||
dataKey="latency"
|
||||
tickLine={false}
|
||||
|
@ -68,9 +66,7 @@ export default class Graph extends Component {
|
|||
return (
|
||||
<div className="graph-container">
|
||||
<ResponsiveContainer width="100%" height={175}>
|
||||
<AreaChart
|
||||
data={this.processData()}
|
||||
>
|
||||
<AreaChart data={this.processData()}>
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
tickFormatter={(tick) => ms(Date.now() - tick)}
|
||||
|
@ -86,11 +82,7 @@ export default class Graph extends Component {
|
|||
separator=": "
|
||||
labelFormatter={() => null}
|
||||
/>
|
||||
<ReferenceLine
|
||||
y={1000}
|
||||
label="1s"
|
||||
stroke="pink"
|
||||
/>
|
||||
<ReferenceLine y={1000} label="1s" stroke="pink" />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="latency"
|
||||
|
|
|
@ -13,35 +13,38 @@ const OUTAGE_TYPES = {
|
|||
}
|
||||
|
||||
export default function Incident({ incident }) {
|
||||
const { content, end_date: end, start_date: start, title, type, stages } = incident
|
||||
const {
|
||||
content,
|
||||
end_date: end,
|
||||
start_date: start,
|
||||
title,
|
||||
type,
|
||||
stages,
|
||||
} = incident
|
||||
|
||||
const startDate = new Date(start * 1000)
|
||||
const endDate = end ? new Date(end * 1000) : null
|
||||
const tooltip = `Started ${startDate.toLocaleString()}` +
|
||||
const tooltip =
|
||||
`Started ${startDate.toLocaleString()}` +
|
||||
(endDate ? `, ended ${endDate.toLocaleString()}` : '')
|
||||
const ago = ms(Date.now() - start * 1000)
|
||||
const agoEnd = end ? ms(Date.now() - end * 1000) : null
|
||||
|
||||
const stageNodes = stages.map(
|
||||
(stage) => <Stage key={stage.title} stage={stage}/>
|
||||
).reverse() // show newest first
|
||||
const stageNodes = stages
|
||||
.map((stage) => <Stage key={stage.title} stage={stage} />)
|
||||
.reverse() // show newest first
|
||||
|
||||
return (
|
||||
<div className="incident">
|
||||
<h2>{OUTAGE_TYPES[type] || type}: {title}</h2>
|
||||
<h2>
|
||||
{OUTAGE_TYPES[type] || type}: {title}
|
||||
</h2>
|
||||
<p>{content}</p>
|
||||
<footer title={tooltip}>
|
||||
Started {ago} ago
|
||||
{end ? `, ended ${agoEnd} ago` : null}
|
||||
</footer>
|
||||
{stageNodes.length
|
||||
? (
|
||||
<div className="stages">
|
||||
{stageNodes}
|
||||
</div>
|
||||
)
|
||||
: null
|
||||
}
|
||||
{stageNodes.length ? <div className="stages">{stageNodes}</div> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -25,9 +25,9 @@ export default class Incidents extends React.Component {
|
|||
return null
|
||||
}
|
||||
|
||||
return this.state.incidents.map(
|
||||
(incident) => <Incident key={incident.id} incident={incident}/>
|
||||
)
|
||||
return this.state.incidents.map((incident) => (
|
||||
<Incident key={incident.id} incident={incident} />
|
||||
))
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -4,11 +4,7 @@ import PropTypes from 'prop-types'
|
|||
import './Page.css'
|
||||
|
||||
export default function Page({ children }) {
|
||||
return (
|
||||
<div className="page">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
return <div className="page">{children}</div>
|
||||
}
|
||||
|
||||
Page.propTypes = {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
@ -32,16 +31,21 @@ 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 (
|
||||
<div className={className}>
|
||||
|
@ -50,16 +54,17 @@ export default function Service ({ graph, name, status, latency, description })
|
|||
<FontAwesomeIcon title={title} icon={icon} />
|
||||
</div>
|
||||
<h2 className="title">
|
||||
{name} {latency ? (
|
||||
<span className="latency">
|
||||
{Math.round(latency)}ms
|
||||
{name}{' '}
|
||||
{latency ? (
|
||||
<span className="information">
|
||||
{Math.round(latency)}
|
||||
ms ({uptimePercentage}
|
||||
%)
|
||||
</span>
|
||||
) : null}
|
||||
</h2>
|
||||
</header>
|
||||
<p className="description">
|
||||
{description}
|
||||
</p>
|
||||
<p className="description">{description}</p>
|
||||
{graph ? <Graph data={graph} /> : null}
|
||||
</div>
|
||||
)
|
||||
|
@ -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,
|
||||
|
|
|
@ -7,19 +7,17 @@ import Incident from './Incident'
|
|||
|
||||
export default function Status({ incident }) {
|
||||
const incidentOngoing = incident && incident.ongoing === 1
|
||||
const view = incidentOngoing
|
||||
? <Incident incident={incident}/>
|
||||
: 'All systems operational'
|
||||
const view = incidentOngoing ? (
|
||||
<Incident incident={incident} />
|
||||
) : (
|
||||
'All systems operational'
|
||||
)
|
||||
|
||||
const className = classnames(
|
||||
'status',
|
||||
incidentOngoing ? 'status-bad' : 'status-good',
|
||||
)
|
||||
return (
|
||||
<div className={className}>
|
||||
{view}
|
||||
</div>
|
||||
incidentOngoing ? 'status-bad' : 'status-good'
|
||||
)
|
||||
return <div className={className}>{view}</div>
|
||||
}
|
||||
|
||||
Status.propTypes = {
|
||||
|
|
|
@ -10,17 +10,28 @@ it('renders without crashing', () => {
|
|||
it('shows one service', () => {
|
||||
const comp = shallow(<DegradedNotice services={{ first: null }} />)
|
||||
expect(comp.contains(<header>1 service is unreachable</header>)).toEqual(true)
|
||||
expect(comp.contains(
|
||||
<p>elstat is having trouble contacting <strong>first</strong>.</p>
|
||||
)).toEqual(true)
|
||||
expect(
|
||||
comp.contains(
|
||||
<p>
|
||||
elstat is having trouble contacting <strong>first</strong>.
|
||||
</p>
|
||||
)
|
||||
).toEqual(true)
|
||||
})
|
||||
|
||||
it('shows multiple services', () => {
|
||||
const comp = shallow(<DegradedNotice services={{ first: null, second: null, third: null }}/>)
|
||||
expect(comp.contains(
|
||||
<header>3 services are unreachable</header>
|
||||
)).toEqual(true)
|
||||
expect(comp.contains(
|
||||
<p>elstat is having trouble contacting <strong>first, second, third</strong>.</p>
|
||||
)).toEqual(true)
|
||||
const comp = shallow(
|
||||
<DegradedNotice services={{ first: null, second: null, third: null }} />
|
||||
)
|
||||
expect(comp.contains(<header>3 services are unreachable</header>)).toEqual(
|
||||
true
|
||||
)
|
||||
expect(
|
||||
comp.contains(
|
||||
<p>
|
||||
elstat is having trouble contacting{' '}
|
||||
<strong>first, second, third</strong>.
|
||||
</p>
|
||||
)
|
||||
).toEqual(true)
|
||||
})
|
||||
|
|
|
@ -3,16 +3,13 @@ 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('<Service/>', () => {
|
||||
|
@ -21,16 +18,24 @@ describe('<Service/>', () => {
|
|||
})
|
||||
|
||||
it('omits information', () => {
|
||||
const comp = shallow(<Service graph={null} status {...props} latency={null}/>)
|
||||
const comp = shallow(
|
||||
<Service graph={null} status {...props} latency={null} />
|
||||
)
|
||||
expect(comp.find('h2.title').text()).toEqual('sample service ')
|
||||
})
|
||||
|
||||
it('renders proper information', () => {
|
||||
const comp = shallow(<Service graph={graph} status {...props} />)
|
||||
expect(comp.prop('className')).toEqual('service service-alive')
|
||||
expect(comp.find('h2.title').text()).toEqual('sample service 51ms')
|
||||
expect(comp.contains(<p className="description">a cool service</p>)).toEqual(true)
|
||||
expect(comp.contains(<span className="latency">51ms</span>)).toEqual(true)
|
||||
expect(comp.find('h2.title').text()).toEqual(
|
||||
'sample service 51ms (99.9994%)'
|
||||
)
|
||||
expect(
|
||||
comp.contains(<p className="description">a cool service</p>)
|
||||
).toEqual(true)
|
||||
expect(
|
||||
comp.contains(<span className="information">51ms (99.9994%)</span>)
|
||||
).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -6,9 +6,5 @@ import {
|
|||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
export default function register() {
|
||||
library.add(
|
||||
faCheckCircle,
|
||||
faTimesCircle,
|
||||
faExclamationCircle,
|
||||
)
|
||||
library.add(faCheckCircle, faTimesCircle, faExclamationCircle)
|
||||
}
|
||||
|
|
|
@ -7,7 +7,4 @@ import register from './icons.js'
|
|||
|
||||
register()
|
||||
|
||||
ReactDOM.render(
|
||||
<App/>,
|
||||
document.getElementById('root')
|
||||
)
|
||||
ReactDOM.render(<App />, document.getElementById('root'))
|
||||
|
|
|
@ -15,6 +15,11 @@ export function objectFromEntries (entries) {
|
|||
)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ export default class StreamingClient extends NanoEvents {
|
|||
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 = () => {
|
||||
|
|
Loading…
Reference in a new issue