fe: add routing + proto incident hist page

This commit is contained in:
slice 2018-07-19 14:49:43 -07:00
parent a0b6c8f01d
commit d66aa77aa7
No known key found for this signature in database
GPG Key ID: 1508C19D7436A26D
10 changed files with 125 additions and 15 deletions

View File

@ -34,6 +34,18 @@
"prop-types": "^15.5.10"
}
},
"@reach/router": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@reach/router/-/router-1.1.1.tgz",
"integrity": "sha1-JKWyDxzJ5V4svNxFT7gslNtHmoE=",
"requires": {
"create-react-context": "^0.2.1",
"invariant": "^2.2.3",
"prop-types": "^15.6.1",
"react-lifecycles-compat": "^3.0.4",
"warning": "^3.0.0"
}
},
"abab": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz",
@ -2258,6 +2270,15 @@
"sha.js": "^2.4.8"
}
},
"create-react-context": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.2.2.tgz",
"integrity": "sha512-KkpaLARMhsTsgp0d2NA/R94F/eDLbhXERdIq3LvX2biCAXcDvHYoOqHfWCHf1+OLj+HKBotLG3KqaOOf+C1C+A==",
"requires": {
"fbjs": "^0.8.0",
"gud": "^1.0.0"
}
},
"cross-spawn": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
@ -4645,6 +4666,11 @@
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE="
},
"gud": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz",
"integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw=="
},
"gzip-size": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-3.0.0.tgz",
@ -10838,6 +10864,14 @@
"makeerror": "1.0.x"
}
},
"warning": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz",
"integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=",
"requires": {
"loose-envify": "^1.0.0"
}
},
"watch": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/watch/-/watch-0.10.0.tgz",

View File

@ -7,6 +7,7 @@
"@fortawesome/fontawesome-svg-core": "^1.2.0-14",
"@fortawesome/free-solid-svg-icons": "^5.1.0-11",
"@fortawesome/react-fontawesome": "0.1.0-11",
"@reach/router": "^1.1.1",
"classnames": "^2.2.6",
"ms": "^2.1.1",
"nanoevents": "^1.0.5",

View File

@ -0,0 +1,15 @@
import React from 'react'
import { Router } from '@reach/router'
import Dashboard from './Dashboard'
import Incidents from './Incidents'
export default function App () {
return (
<Router>
<Dashboard path="/"/>
<Incidents path="/incidents/"/>
</Router>
)
}

View File

@ -1,11 +1,11 @@
import React, { Component } from 'react'
import './Dashboard.css'
import Service from './Service.js'
import ServicePlaceholder from './ServicePlaceholder.js'
import DegradedNotice from './DegradedNotice.js'
import StreamingClient from '../ws/client'
import Status from './Status'
import Page from './Page'
import { log, objectFromEntries, strictFetch } from '../util.js'
import { domain as DOMAIN } from '../config.json'
@ -164,10 +164,10 @@ export default class Dashboard extends Component {
render () {
return (
<div className="dashboard">
<Page>
<h1>elstatus</h1>
{this.renderDashboardContent()}
</div>
</Page>
)
}
}

View File

@ -14,8 +14,13 @@ const OUTAGE_TYPES = {
export default function Incident ({ 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()}` +
(endDate ? `, ended ${endDate.toLocaleString()}` : '')
const ago = ms(Date.now() - start * 1000)
const agoEnd = end ? ms(Date.now - end * 1000) : null
const agoEnd = end ? ms(Date.now() - end * 1000) : null
const stageNodes = stages.map(
(stage) => <Stage key={stage.title} stage={stage}/>
@ -25,7 +30,7 @@ export default function Incident ({ incident }) {
<div className="incident">
<h2>{OUTAGE_TYPES[type] || type}: {title}</h2>
<p>{content}</p>
<footer>
<footer title={tooltip}>
Started {ago} ago
{end ? `, ended ${agoEnd} ago` : null}
</footer>

View File

@ -0,0 +1,41 @@
import React from 'react'
import Page from './Page'
import Incident from './Incident'
import { strictFetch } from '../util'
import { domain as DOMAIN } from '../config.json'
export default class Incidents extends React.Component {
state = {
incidents: null,
page: 0,
}
componentDidMount () {
this.fetchIncidents()
}
async fetchIncidents () {
const resp = await strictFetch(`${DOMAIN}/api/incidents/${this.state.page}`)
this.setState({ incidents: await resp.json() })
}
renderIncidents () {
if (!this.state.incidents) {
return null
}
return this.state.incidents.map(
(incident) => <Incident key={incident.id} incident={incident}/>
)
}
render () {
return (
<Page>
<h1>Incidents</h1>
{this.renderIncidents()}
</Page>
)
}
}

View File

@ -1,4 +1,4 @@
.dashboard {
.page {
max-width: 40em;
margin: 0 auto;
}

View File

@ -0,0 +1,16 @@
import React from 'react'
import PropTypes from 'prop-types'
import './Page.css'
export default function Page ({ children }) {
return (
<div className="page">
{children}
</div>
)
}
Page.propTypes = {
children: PropTypes.any,
}

View File

@ -6,16 +6,14 @@ import './Status.css'
import Incident from './Incident'
export default function Status ({ incident }) {
let view = null
if (incident) {
view = <Incident incident={incident}/>
} else {
view = 'All systems operational'
}
const incidentOngoing = incident && incident.ongoing === 1
const view = incidentOngoing
? <Incident incident={incident}/>
: 'All systems operational'
const className = classnames(
'status',
incident == null ? 'status-good' : 'status-bad',
incidentOngoing ? 'status-bad' : 'status-good',
)
return (
<div className={className}>

View File

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