Add gzipped download endpoint

This commit is contained in:
John Lyon-Smith
2018-04-26 18:00:16 -07:00
parent 5e5daa2e78
commit 96e2599cdb
9 changed files with 390 additions and 164 deletions

View File

@@ -66,6 +66,7 @@ export class WorkItem extends React.Component {
}, },
photos: { photos: {
isValid: (r, v) => v && v.length > 0, isValid: (r, v) => v && v.length > 0,
initValue: [],
}, },
details: { details: {
isValid: (r, v) => v !== "", isValid: (r, v) => v !== "",
@@ -278,6 +279,7 @@ export class WorkItem extends React.Component {
height: 400, height: 400,
marginBottom: 10, marginBottom: 10,
}} }}
showsUserLocation
showsBuildings={false} showsBuildings={false}
showsTraffic={false} showsTraffic={false}
showsIndoors={false} showsIndoors={false}

View File

@@ -69,7 +69,7 @@ export const dotify = (s) => {
export const regionContainingPoints = (points, inset) => { export const regionContainingPoints = (points, inset) => {
let minX, maxX, minY, maxY let minX, maxX, minY, maxY
if (!points) { if (!points || points.length === 0) {
return null return null
} }

View File

@@ -38,6 +38,13 @@ export class ActivityRoutes {
passport.authenticate("bearer", { session: false }), passport.authenticate("bearer", { session: false }),
catchAll(this.deleteActivity) catchAll(this.deleteActivity)
) )
app
.route("/activities/all")
.delete(
passport.authenticate("bearer", { session: false }),
catchAll(this.deleteAllActivities)
)
} }
async listActivities(req, res, next) { async listActivities(req, res, next) {
@@ -156,4 +163,12 @@ export class ActivityRoutes {
res.json({}) res.json({})
} }
async deleteAllActivities(req, res, next) {
const Activity = this.db.Activity
await Activity.remove({})
res.json({})
}
} }

View File

@@ -1,6 +1,8 @@
import passport from "passport" import passport from "passport"
import createError from "http-errors" import createError from "http-errors"
import autobind from "autobind-decorator" import autobind from "autobind-decorator"
import zlib from "zlib"
import { Readable } from "stream"
import { catchAll } from "." import { catchAll } from "."
@autobind @autobind
@@ -38,6 +40,13 @@ export class TeamRoutes {
passport.authenticate("bearer", { session: false }), passport.authenticate("bearer", { session: false }),
catchAll(this.deleteTeam) catchAll(this.deleteTeam)
) )
app
.route("/teams/status")
.get(
passport.authenticate("bearer", { session: false }),
catchAll(this.getTeamStatus)
)
} }
async listTeams(req, res, next) { async listTeams(req, res, next) {
@@ -146,4 +155,32 @@ export class TeamRoutes {
res.json({}) res.json({})
} }
async getTeamStatus(req, res, next) {
const Team = this.db.Team
const Activity = this.db.Activity
let teams = await Team.find({}).exec()
teams = teams.map((doc) => doc.toObject({ versionKey: false }))
for (let team of teams) {
let activities = await Activity.find({ team: team._id }).exec()
team.activities = activities.map((doc) =>
doc.toObject({ versionKey: false })
)
}
const gzip = zlib.createGzip()
let readable = new Readable()
readable.push(JSON.stringify(teams, null, " "))
readable.push(null)
res.writeHead(200, {
"Content-Type": "text/html",
"Content-Encoding": "gzip",
})
readable.pipe(gzip).pipe(res)
}
} }

View File

@@ -45,6 +45,13 @@ export class WorkItemRoutes {
passport.authenticate("bearer", { session: false }), passport.authenticate("bearer", { session: false }),
catchAll(this.deleteWorkItem) catchAll(this.deleteWorkItem)
) )
app
.route("/workitems/all")
.delete(
passport.authenticate("bearer", { session: false }),
catchAll(this.deleteAllWorkItems)
)
} }
async listWorkItems(req, res, next) { async listWorkItems(req, res, next) {
@@ -189,4 +196,14 @@ export class WorkItemRoutes {
res.json({}) res.json({})
} }
async deleteAllWorkItems(req, res, next) {
const Activity = this.db.Activity
const WorkItem = this.db.WorkItem
await Activity.remove({})
await WorkItem.remove({})
res.json({})
}
} }

View File

@@ -16,7 +16,6 @@ export let activitySchema = new Schema(
required: true, required: true,
}, },
notes: { type: String, required: true }, notes: { type: String, required: true },
when: { type: Date, required: true },
fromStreetNumber: Number, fromStreetNumber: Number,
toStreetNumber: Number, toStreetNumber: Number,
photos: [Schema.Types.ObjectId], photos: [Schema.Types.ObjectId],
@@ -25,5 +24,5 @@ export let activitySchema = new Schema(
) )
activitySchema.methods.toClient = function() { activitySchema.methods.toClient = function() {
return this.toObject() return this.toObject({ versionKey: false })
} }

View File

@@ -1,9 +1,14 @@
import { Schema } from 'mongoose' import { Schema } from "mongoose"
export let teamSchema = new Schema({ export let teamSchema = new Schema(
name: { type: String }, {
}, { timestamps: true, id: false }) name: { type: String },
start: { type: Date },
stop: { type: Date },
},
{ timestamps: true, id: false }
)
teamSchema.methods.toClient = function() { teamSchema.methods.toClient = function() {
return this.toObject() return this.toObject({ versionKey: false })
} }

View File

@@ -1,30 +1,30 @@
import EventEmitter from 'eventemitter3' import EventEmitter from "eventemitter3"
import io from 'socket.io-client' import io from "socket.io-client"
import autobind from 'autobind-decorator' import autobind from "autobind-decorator"
const authTokenName = 'AuthToken' const authTokenName = "AuthToken"
class NetworkError extends Error { class NetworkError extends Error {
constructor(message) { constructor(message) {
super(message) super(message)
this.name = this.constructor.name this.name = this.constructor.name
if (typeof Error.captureStackTrace === 'function') { if (typeof Error.captureStackTrace === "function") {
Error.captureStackTrace(this, this.constructor) Error.captureStackTrace(this, this.constructor)
} else { } else {
this.stack = (new Error(message)).stack this.stack = new Error(message).stack
} }
} }
} }
class APIError extends Error { class APIError extends Error {
constructor(status, message) { constructor(status, message) {
super(message || '') super(message || "")
this.status = status || 500 this.status = status || 500
this.name = this.constructor.name this.name = this.constructor.name
if (typeof Error.captureStackTrace === 'function') { if (typeof Error.captureStackTrace === "function") {
Error.captureStackTrace(this, this.constructor) Error.captureStackTrace(this, this.constructor)
} else { } else {
this.stack = (new Error(message)).stack this.stack = new Error(message).stack
} }
} }
} }
@@ -35,49 +35,51 @@ class API extends EventEmitter {
super() super()
this.user = null this.user = null
let token = localStorage.getItem(authTokenName) || sessionStorage.getItem(authTokenName) let token =
localStorage.getItem(authTokenName) ||
sessionStorage.getItem(authTokenName)
if (token) { if (token) {
this.token = token this.token = token
this.user = { pending: true } this.user = { pending: true }
this.who() this.who()
.then((user) => { .then((user) => {
this.user = user this.user = user
this.connectSocket() this.connectSocket()
this.emit('login') this.emit("login")
}) })
.catch(() => { .catch(() => {
localStorage.removeItem(authTokenName) localStorage.removeItem(authTokenName)
sessionStorage.removeItem(authTokenName) sessionStorage.removeItem(authTokenName)
this.token = null this.token = null
this.user = null this.user = null
this.socket = null this.socket = null
this.emit('logout') this.emit("logout")
}) })
} }
} }
connectSocket() { connectSocket() {
this.socket = io(window.location.origin, { this.socket = io(window.location.origin, {
path: '/api/socketio', path: "/api/socketio",
query: { query: {
auth_token: this.token auth_token: this.token,
} },
}) })
this.socket.on('disconnect', (reason) => { this.socket.on("disconnect", (reason) => {
// Could happen if the auth_token is bad // Could happen if the auth_token is bad
this.socket = null this.socket = null
}) })
this.socket.on('notify', (message) => { this.socket.on("notify", (message) => {
const { eventName, eventData } = message const { eventName, eventData } = message
// Filter the few massages that affect our cached user data to avoid a server round trip // Filter the few massages that affect our cached user data to avoid a server round trip
switch (eventName) { switch (eventName) {
case 'newThumbnailImage': case "newThumbnailImage":
this.user.thumbnailImageId = eventData.imageId this.user.thumbnailImageId = eventData.imageId
break break
case 'newProfileImage': case "newProfileImage":
this.user.imageId = eventData.imageId this.user.imageId = eventData.imageId
break break
default: default:
@@ -102,22 +104,27 @@ class API extends EventEmitter {
makeImageUrl(id, size) { makeImageUrl(id, size) {
if (id) { if (id) {
return '/api/assets/' + id + '?access_token=' + this.token return "/api/assets/" + id + "?access_token=" + this.token
} else if (size && size.width && size.height) { } else if (size && size.width && size.height) {
return `/api/placeholders/${size.width}x${size.height}?access_token=${this.token}` return `/api/placeholders/${size.width}x${size.height}?access_token=${
this.token
}`
} else { } else {
return null return null
} }
} }
makeAssetUrl(id) { makeAssetUrl(id) {
return id ? '/api/assets/' + id + '?access_token=' + this.token : null return id ? "/api/assets/" + id + "?access_token=" + this.token : null
} }
static makeParams(params) { static makeParams(params) {
return params ? '?' + Object.keys(params).map((key) => ( return params
[key, params[key]].map(encodeURIComponent).join('=') ? "?" +
)).join('&') : '' Object.keys(params)
.map((key) => [key, params[key]].map(encodeURIComponent).join("="))
.join("&")
: ""
} }
request(method, path, requestBody, requestOptions) { request(method, path, requestBody, requestOptions) {
@@ -125,82 +132,90 @@ class API extends EventEmitter {
var promise = new Promise((resolve, reject) => { var promise = new Promise((resolve, reject) => {
let fetchOptions = { let fetchOptions = {
method: method, method: method,
mode: 'cors', mode: "cors",
cache: 'no-store' cache: "no-store",
} }
let headers = new Headers() let headers = new Headers()
if (this.token) { if (this.token) {
headers.set('Authorization', 'Bearer ' + this.token) headers.set("Authorization", "Bearer " + this.token)
} }
if (method === 'POST' || method === 'PUT') { if (method === "POST" || method === "PUT") {
if (requestOptions.binary) { if (requestOptions.binary) {
headers.set('Content-Type', 'application/octet-stream') headers.set("Content-Type", "application/octet-stream")
headers.set('Content-Length', requestOptions.binary.length) headers.set("Content-Length", requestOptions.binary.length)
headers.set('Range', 'byte ' + requestOptions.binary.offset) headers.set("Range", "byte " + requestOptions.binary.offset)
fetchOptions.body = requestBody fetchOptions.body = requestBody
} else { } else {
headers.set('Content-Type', 'application/json') headers.set("Content-Type", "application/json")
fetchOptions.body = JSON.stringify(requestBody) fetchOptions.body = JSON.stringify(requestBody)
} }
} }
fetchOptions.headers = headers fetchOptions.headers = headers
fetch('/api' + path, fetchOptions).then((res) => { fetch("/api" + path, fetchOptions)
return Promise.all([ Promise.resolve(res), (requestOptions.binary && method === 'GET') ? res.blob() : res.json() ]) .then((res) => {
}).then((arr) => { return Promise.all([
let [ res, responseBody ] = arr Promise.resolve(res),
if (res.ok) { requestOptions.binary && method === "GET" ? res.blob() : res.json(),
if (requestOptions.wantHeaders) { ])
resolve({ body: responseBody, headers: res.headers }) })
.then((arr) => {
let [res, responseBody] = arr
if (res.ok) {
if (requestOptions.wantHeaders) {
resolve({ body: responseBody, headers: res.headers })
} else {
resolve(responseBody)
}
} else { } else {
resolve(responseBody) reject(new APIError(res.status, responseBody.message))
} }
} else { })
reject(new APIError(res.status, responseBody.message)) .catch((error) => {
} reject(new NetworkError(error.message))
}).catch((error) => { })
reject(new NetworkError(error.message))
})
}) })
return promise return promise
} }
post(path, requestBody, options) { post(path, requestBody, options) {
return this.request('POST', path, requestBody, options) return this.request("POST", path, requestBody, options)
} }
put(path, requestBody, options) { put(path, requestBody, options) {
return this.request('PUT', path, requestBody, options) return this.request("PUT", path, requestBody, options)
} }
get(path, options) { get(path, options) {
return this.request('GET', path, options) return this.request("GET", path, options)
} }
delete(path, options) { delete(path, options) {
return this.request('DELETE', path, options) return this.request("DELETE", path, options)
} }
login(email, password, remember) { login(email, password, remember) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.post('/auth/login', { email, password }, { wantHeaders: true }).then((response) => { this.post("/auth/login", { email, password }, { wantHeaders: true })
// Save bearer token for later use .then((response) => {
const authValue = response.headers.get('Authorization') // Save bearer token for later use
const [ scheme, token ] = authValue.split(' ') const authValue = response.headers.get("Authorization")
const [scheme, token] = authValue.split(" ")
if (scheme !== 'Bearer' || !token) { if (scheme !== "Bearer" || !token) {
reject(new APIError('Unexpected Authorization scheme or token')) reject(new APIError("Unexpected Authorization scheme or token"))
} }
if (remember) { if (remember) {
localStorage.setItem(authTokenName, token) localStorage.setItem(authTokenName, token)
} else { } else {
sessionStorage.setItem(authTokenName, token) sessionStorage.setItem(authTokenName, token)
} }
this.token = token this.token = token
this.user = response.body this.user = response.body
this.connectSocket() this.connectSocket()
this.emit('login') this.emit("login")
resolve(response.body) resolve(response.body)
}).catch((err) => { })
reject(err) .catch((err) => {
}) reject(err)
})
}) })
} }
logout() { logout() {
@@ -211,82 +226,91 @@ class API extends EventEmitter {
this.token = null this.token = null
this.user = null this.user = null
this.disconnectSocket() this.disconnectSocket()
this.emit('logout') this.emit("logout")
} }
return this.delete('/auth/login').then(cb, cb) return this.delete("/auth/login").then(cb, cb)
} }
who() { who() {
return this.get('/auth/who') return this.get("/auth/who")
} }
confirmEmail(emailToken) { confirmEmail(emailToken) {
return this.post('/auth/email/confirm/', { emailToken }) return this.post("/auth/email/confirm/", { emailToken })
} }
sendConfirmEmail(emails) { sendConfirmEmail(emails) {
return this.post('/auth/email/send', emails) return this.post("/auth/email/send", emails)
} }
changePassword(passwords) { changePassword(passwords) {
return this.post('/auth/password/change', passwords) return this.post("/auth/password/change", passwords)
} }
sendResetPassword(email) { sendResetPassword(email) {
return this.post('/auth/password/send', { email }) return this.post("/auth/password/send", { email })
} }
confirmResetPassword(passwordToken) { confirmResetPassword(passwordToken) {
return this.post('/auth/password/confirm', { passwordToken }) return this.post("/auth/password/confirm", { passwordToken })
} }
resetPassword(passwords) { resetPassword(passwords) {
return this.post('/auth/password/reset', passwords) return this.post("/auth/password/reset", passwords)
} }
getUser(id) { getUser(id) {
return this.get('/users/' + id) return this.get("/users/" + id)
} }
listUsers() { listUsers() {
return this.get('/users') return this.get("/users")
} }
listUsersForTeam(teamId) { listUsersForTeam(teamId) {
return this.get(`/users?team=${teamId}`) return this.get(`/users?team=${teamId}`)
} }
createUser(user) { createUser(user) {
return this.post('/users', user) return this.post("/users", user)
} }
updateUser(user) { updateUser(user) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.put('/users', user).then((user) => { this.put("/users", user)
// If we just updated ourselves, update the internal cached copy .then((user) => {
if (user._id === this.user._id) { // If we just updated ourselves, update the internal cached copy
this.user = user if (user._id === this.user._id) {
this.emit('login') this.user = user
} this.emit("login")
resolve(user) }
}).catch((reason) => { resolve(user)
reject(reason) })
}) .catch((reason) => {
reject(reason)
})
}) })
} }
deleteUser(id) { deleteUser(id) {
return this.delete('/users/' + id) return this.delete("/users/" + id)
} }
enterRoom(roomName) { enterRoom(roomName) {
return this.put('/users/enter-room/' + (roomName || '')) return this.put("/users/enter-room/" + (roomName || ""))
} }
leaveRoom() { leaveRoom() {
return this.put('/users/leave-room') return this.put("/users/leave-room")
} }
getTeam(id) { getTeam(id) {
return this.get('/teams/' + id) return this.get("/teams/" + id)
} }
listTeams() { listTeams() {
return this.get('/teams') return this.get("/teams")
} }
createTeam(team) { createTeam(team) {
return this.post('/teams', team) return this.post("/teams", team)
} }
updateTeam(team) { updateTeam(team) {
return this.put('/teams', team) return this.put("/teams", team)
} }
deleteTeam(id) { deleteTeam(id) {
return this.delete('/teams/' + id) return this.delete("/teams/" + id)
}
deleteAllActivities() {
return this.delete("/activities/all")
}
deleteAllWorkItems() {
return this.delete("/workitems/all")
} }
upload(file, progressCallback) { upload(file, progressCallback) {
@@ -302,36 +326,40 @@ class API extends EventEmitter {
const buffer = e.target.result const buffer = e.target.result
const bytesRead = buffer.byteLength const bytesRead = buffer.byteLength
this.post('/assets/upload/' + uploadId, buffer, { this.post("/assets/upload/" + uploadId, buffer, {
binary: { offset: chunk * chunkSize, length: bytesRead } binary: { offset: chunk * chunkSize, length: bytesRead },
}).then((uploadData) => {
chunk++
if (!progressCallback(uploadData)) {
return Promise.reject(new Error('Upload was canceled'))
}
if (chunk < numberOfChunks) {
let start = chunk * chunkSize
let end = Math.min(fileSize, start + chunkSize)
reader.readAsArrayBuffer(file.slice(start, end))
} else {
resolve(uploadData)
}
}).catch((err) => {
reject(err)
}) })
.then((uploadData) => {
chunk++
if (!progressCallback(uploadData)) {
return Promise.reject(new Error("Upload was canceled"))
}
if (chunk < numberOfChunks) {
let start = chunk * chunkSize
let end = Math.min(fileSize, start + chunkSize)
reader.readAsArrayBuffer(file.slice(start, end))
} else {
resolve(uploadData)
}
})
.catch((err) => {
reject(err)
})
} }
this.post('/assets/upload', { this.post("/assets/upload", {
fileName: file.name, fileName: file.name,
fileSize, fileSize,
contentType: file.type, contentType: file.type,
numberOfChunks numberOfChunks,
}).then((uploadData) => {
uploadId = uploadData.uploadId
reader.readAsArrayBuffer(file.slice(0, chunkSize))
}).catch((err) => {
reject(err)
}) })
.then((uploadData) => {
uploadId = uploadData.uploadId
reader.readAsArrayBuffer(file.slice(0, chunkSize))
})
.catch((err) => {
reject(err)
})
}) })
} }
} }

View File

@@ -1,10 +1,11 @@
import React, { Component, Fragment } from 'react' import React, { Component, Fragment } from "react"
import PropTypes from 'prop-types' import PropTypes from "prop-types"
import { Box, Image, Column, Row, Button } from 'ui' import { Box, Image, Column, Row, Button } from "ui"
import { MessageModal, WaitModal } from '../Modal' import { MessageModal, WaitModal, YesNoMessageModal } from "../Modal"
// import { api } from 'src/API' import { sizeInfo, colorInfo } from "ui/style"
import { sizeInfo, colorInfo } from 'ui/style' import headerLogo from "images/deighton.png"
import headerLogo from 'images/deighton.png' import autobind from "autobind-decorator"
import { api } from "../API"
export class System extends Component { export class System extends Component {
static propTypes = { static propTypes = {
@@ -16,19 +17,113 @@ export class System extends Component {
this.state = { this.state = {
messageModal: null, messageModal: null,
waitModal: null, waitModal: null,
yesNoModal: null,
} }
} }
componentDidMount(props) { componentDidMount(props) {
this.props.changeTitle('System') this.props.changeTitle("System")
} }
componentWillUnmount() { componentWillUnmount() {
this.props.changeTitle('') this.props.changeTitle("")
}
@autobind
handleDeleteActivities() {
this.setState({
yesNoModal: {
question:
"Are you sure you want to delete all activities in the system?",
onDismiss: this.handleDeleteActivitiesDismiss,
},
})
}
@autobind
handleDeleteActivitiesDismiss(yes) {
if (yes) {
this.setState({ waitModal: { message: "Deleting All Activities..." } })
api
.deleteAllActivities()
.then(() => {
this.setState({
waitModal: null,
messageModal: {
icon: "thumb",
message: "All logged activities have been deleted",
},
})
})
.catch((error) => {
this.setState({
waitModal: null,
messageModal: {
icon: "hand",
message: "Unable to request delete activities.",
detail: error.message,
},
})
})
}
this.setState({
yesNoModal: null,
})
}
@autobind
handleDeleteWorkItems() {
this.setState({
yesNoModal: {
question:
"Are you sure you want to delete all work items & activities in the system?",
onDismiss: this.handleDeleteWorkItemsDismiss,
},
})
}
@autobind
handleDeleteWorkItemsDismiss(yes) {
if (yes) {
this.setState({
waitModal: { message: "Deleting All Work Items & Activities..." },
})
api
.deleteAllWorkItems()
.then(() => {
this.setState({
waitModal: null,
messageModal: {
icon: "thumb",
message: "All work items and logged activities have been deleted",
},
})
})
.catch((error) => {
this.setState({
waitModal: null,
messageModal: {
icon: "hand",
message: "Unable to delete work items and activities.",
detail: error.message,
},
})
})
}
this.setState({
yesNoModal: null,
})
}
@autobind
handleMessageModalDismiss() {
this.setState({ messageModal: null })
} }
render() { render() {
const { messageModal, waitModal } = this.state const { messageModal, yesNoModal, waitModal } = this.state
return ( return (
<Fragment> <Fragment>
@@ -38,14 +133,22 @@ export class System extends Component {
<Row.Item grow /> <Row.Item grow />
<Row.Item width={sizeInfo.formRowSpacing} /> <Row.Item width={sizeInfo.formRowSpacing} />
<Row.Item width={sizeInfo.modalWidth}> <Row.Item width={sizeInfo.modalWidth}>
<Box border={{ width: sizeInfo.headerBorderWidth, color: colorInfo.headerBorder }} radius={sizeInfo.formBoxRadius}> <Box
border={{
width: sizeInfo.headerBorderWidth,
color: colorInfo.headerBorder,
}}
radius={sizeInfo.formBoxRadius}>
<Column> <Column>
<Column.Item minHeight={sizeInfo.formColumnSpacing} /> <Column.Item minHeight={sizeInfo.formColumnSpacing} />
<Column.Item> <Column.Item>
<Row> <Row>
<Row.Item grow /> <Row.Item grow />
<Row.Item> <Row.Item>
<Image source={headerLogo} width={sizeInfo.loginLogoWidth} /> <Image
source={headerLogo}
width={sizeInfo.loginLogoWidth}
/>
</Row.Item> </Row.Item>
<Row.Item grow /> <Row.Item grow />
</Row> </Row>
@@ -55,7 +158,11 @@ export class System extends Component {
<Row> <Row>
<Row.Item grow /> <Row.Item grow />
<Row.Item> <Row.Item>
<Button text='Delete All Activities' width={sizeInfo.buttonWideWidth} /> <Button
text="Delete All Activities"
width={sizeInfo.buttonWideWidth}
onClick={this.handleDeleteActivities}
/>
</Row.Item> </Row.Item>
<Row.Item grow /> <Row.Item grow />
</Row> </Row>
@@ -65,7 +172,11 @@ export class System extends Component {
<Row> <Row>
<Row.Item grow /> <Row.Item grow />
<Row.Item> <Row.Item>
<Button text='Delete All Work Items' width={sizeInfo.buttonWideWidth} /> <Button
text="Delete All Work Items"
width={sizeInfo.buttonWideWidth}
onClick={this.handleDeleteWorkItems}
/>
</Row.Item> </Row.Item>
<Row.Item grow /> <Row.Item grow />
</Row> </Row>
@@ -75,7 +186,10 @@ export class System extends Component {
<Row> <Row>
<Row.Item grow /> <Row.Item grow />
<Row.Item> <Row.Item>
<Button text='Delete All Teams' width={sizeInfo.buttonWideWidth} /> <Button
text="Download Team Data"
width={sizeInfo.buttonWideWidth}
/>
</Row.Item> </Row.Item>
<Row.Item grow /> <Row.Item grow />
</Row> </Row>
@@ -88,15 +202,24 @@ export class System extends Component {
</Row> </Row>
</Column.Item> </Column.Item>
<Column.Item grow> <Column.Item grow>
<YesNoMessageModal
open={!!yesNoModal}
question={yesNoModal ? yesNoModal.question : ""}
onDismiss={yesNoModal && yesNoModal.onDismiss}
/>
<MessageModal <MessageModal
open={!!messageModal} open={!!messageModal}
icon={messageModal ? messageModal.icon : ''} icon={messageModal ? messageModal.icon : ""}
message={messageModal ? messageModal.message : ''} message={messageModal ? messageModal.message : ""}
detail={messageModal ? messageModal.title : ''} detail={messageModal ? messageModal.title : ""}
onDismiss={this.handleMessageModalDismiss} /> onDismiss={this.handleMessageModalDismiss}
/>
<WaitModal active={!!waitModal} <WaitModal
message={waitModal ? waitModal.message : ''} /> active={!!waitModal}
message={waitModal ? waitModal.message : ""}
/>
</Column.Item> </Column.Item>
</Fragment> </Fragment>
) )