fe: add routing + proto incident hist page
This commit is contained in:
parent
a0b6c8f01d
commit
d66aa77aa7
10 changed files with 125 additions and 15 deletions
34
priv/frontend/package-lock.json
generated
34
priv/frontend/package-lock.json
generated
|
@ -34,6 +34,18 @@
|
||||||
"prop-types": "^15.5.10"
|
"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": {
|
"abab": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz",
|
||||||
|
@ -2258,6 +2270,15 @@
|
||||||
"sha.js": "^2.4.8"
|
"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": {
|
"cross-spawn": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
|
||||||
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE="
|
"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": {
|
"gzip-size": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-3.0.0.tgz",
|
||||||
|
@ -10838,6 +10864,14 @@
|
||||||
"makeerror": "1.0.x"
|
"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": {
|
"watch": {
|
||||||
"version": "0.10.0",
|
"version": "0.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/watch/-/watch-0.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/watch/-/watch-0.10.0.tgz",
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.0-14",
|
"@fortawesome/fontawesome-svg-core": "^1.2.0-14",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.1.0-11",
|
"@fortawesome/free-solid-svg-icons": "^5.1.0-11",
|
||||||
"@fortawesome/react-fontawesome": "0.1.0-11",
|
"@fortawesome/react-fontawesome": "0.1.0-11",
|
||||||
|
"@reach/router": "^1.1.1",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"ms": "^2.1.1",
|
"ms": "^2.1.1",
|
||||||
"nanoevents": "^1.0.5",
|
"nanoevents": "^1.0.5",
|
||||||
|
|
15
priv/frontend/src/components/App.js
Normal file
15
priv/frontend/src/components/App.js
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,11 +1,11 @@
|
||||||
import React, { Component } from 'react'
|
import React, { Component } from 'react'
|
||||||
|
|
||||||
import './Dashboard.css'
|
|
||||||
import Service from './Service.js'
|
import Service from './Service.js'
|
||||||
import ServicePlaceholder from './ServicePlaceholder.js'
|
import ServicePlaceholder from './ServicePlaceholder.js'
|
||||||
import DegradedNotice from './DegradedNotice.js'
|
import DegradedNotice from './DegradedNotice.js'
|
||||||
import StreamingClient from '../ws/client'
|
import StreamingClient from '../ws/client'
|
||||||
import Status from './Status'
|
import Status from './Status'
|
||||||
|
import Page from './Page'
|
||||||
import { log, objectFromEntries, strictFetch } from '../util.js'
|
import { log, objectFromEntries, strictFetch } from '../util.js'
|
||||||
import { domain as DOMAIN } from '../config.json'
|
import { domain as DOMAIN } from '../config.json'
|
||||||
|
|
||||||
|
@ -164,10 +164,10 @@ export default class Dashboard extends Component {
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div className="dashboard">
|
<Page>
|
||||||
<h1>elstatus</h1>
|
<h1>elstatus</h1>
|
||||||
{this.renderDashboardContent()}
|
{this.renderDashboardContent()}
|
||||||
</div>
|
</Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,8 +14,13 @@ const OUTAGE_TYPES = {
|
||||||
|
|
||||||
export default function Incident ({ incident }) {
|
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()}` +
|
||||||
|
(endDate ? `, ended ${endDate.toLocaleString()}` : '')
|
||||||
const ago = ms(Date.now() - start * 1000)
|
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(
|
const stageNodes = stages.map(
|
||||||
(stage) => <Stage key={stage.title} stage={stage}/>
|
(stage) => <Stage key={stage.title} stage={stage}/>
|
||||||
|
@ -25,7 +30,7 @@ export default function Incident ({ incident }) {
|
||||||
<div className="incident">
|
<div className="incident">
|
||||||
<h2>{OUTAGE_TYPES[type] || type}: {title}</h2>
|
<h2>{OUTAGE_TYPES[type] || type}: {title}</h2>
|
||||||
<p>{content}</p>
|
<p>{content}</p>
|
||||||
<footer>
|
<footer title={tooltip}>
|
||||||
Started {ago} ago
|
Started {ago} ago
|
||||||
{end ? `, ended ${agoEnd} ago` : null}
|
{end ? `, ended ${agoEnd} ago` : null}
|
||||||
</footer>
|
</footer>
|
||||||
|
|
41
priv/frontend/src/components/Incidents.js
Normal file
41
priv/frontend/src/components/Incidents.js
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
.dashboard {
|
.page {
|
||||||
max-width: 40em;
|
max-width: 40em;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
16
priv/frontend/src/components/Page.js
Normal file
16
priv/frontend/src/components/Page.js
Normal 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,
|
||||||
|
}
|
|
@ -6,16 +6,14 @@ import './Status.css'
|
||||||
import Incident from './Incident'
|
import Incident from './Incident'
|
||||||
|
|
||||||
export default function Status ({ incident }) {
|
export default function Status ({ incident }) {
|
||||||
let view = null
|
const incidentOngoing = incident && incident.ongoing === 1
|
||||||
if (incident) {
|
const view = incidentOngoing
|
||||||
view = <Incident incident={incident}/>
|
? <Incident incident={incident}/>
|
||||||
} else {
|
: 'All systems operational'
|
||||||
view = 'All systems operational'
|
|
||||||
}
|
|
||||||
|
|
||||||
const className = classnames(
|
const className = classnames(
|
||||||
'status',
|
'status',
|
||||||
incident == null ? 'status-good' : 'status-bad',
|
incidentOngoing ? 'status-bad' : 'status-good',
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
|
|
|
@ -2,12 +2,12 @@ import React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
|
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import Dashboard from './components/Dashboard'
|
import App from './components/App'
|
||||||
import register from './icons.js'
|
import register from './icons.js'
|
||||||
|
|
||||||
register()
|
register()
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Dashboard/>,
|
<App/>,
|
||||||
document.getElementById('root')
|
document.getElementById('root')
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue