fix: machine status layout bugs

fix: reboot icon looks cropped

fix: confirm dialog layout

fix: Status chip background colors

fix: detailed machine status layout

fix: machine detailed status layout

fix: machine status article links, status chip size

fix: confirmDialog for all machine actions

fix: confirm dialog on every action. reload when success

fix: verbose input label

fix: display software version and machine model

fix: eslint fixes

fix: removed machine version and update button

fix: get machines statuses from ping

chore: removed the support articles until they're ready

fix: reset value and error states when closing the confirm dialog

fix: removed unused info from the machine table

styles: fixed styles in the machine details card

chore: moved styles to another file

fix: fixed the version gql property
This commit is contained in:
Mauricio Navarro Miranda 2020-07-13 15:25:43 -05:00 committed by Josh Harvey
parent 825a9bfe09
commit db014a3ed4
9 changed files with 278 additions and 214 deletions

View file

@ -4,6 +4,8 @@ const axios = require('axios')
const logger = require('./logger')
const db = require('./db')
const pairing = require('./pairing')
const notifier = require('./notifier')
const dbm = require('./postgresql_interface')
const configManager = require('./new-config-manager')
const settingsLoader = require('./new-settings-loader')
@ -34,15 +36,40 @@ function getConfig (defaultConfig) {
}
function getMachineNames (config) {
const fullyFunctionalStatus = {label: 'Fully functional', type: 'success'}
const unresponsiveStatus = {label: 'Unresponsive', type: 'error'}
const stuckStatus = {label: 'Stuck', type: 'error'}
return Promise.all([getMachines(), getConfig(config)])
.then(([machines, config]) => {
.then(([machines, config]) => Promise.all(
[machines, notifier.checkPings(machines), dbm.machineEvents(), config]
))
.then(([machines, pings, events, config]) => {
const getPingStatus = (ping) => {
if (!ping) return fullyFunctionalStatus
if (ping.age) return unresponsiveStatus
return fullyFunctionalStatus
}
const getStuckStatus = (stuck) => {
if (!stuck || !stuck.age) return undefined
return stuckStatus
}
const addName = r => {
const cashOutConfig = configManager.getCashOut(r.deviceId, config)
const cashOut = !!cashOutConfig.active
// TODO new-admin actually load status based on ping.
const statuses = [{label: 'Unknown detailed status', type: 'warning'}]
const ping = getPingStatus(_.first(pings[r.deviceId]))
const stuck = getStuckStatus(_.first(notifier.checkStuckScreen(events, r.name)))
const statuses = [ping]
if (stuck) statuses.push(stuck)
return _.assign(r, {cashOut, statuses})
}

View file

@ -344,4 +344,8 @@ function buildAlertFingerprint (alertRec, notifications) {
return crypto.createHash('sha256').update(subject).digest('hex')
}
module.exports = { checkNotification }
module.exports = {
checkNotification,
checkPings,
checkStuckScreen
}

View file

@ -2,51 +2,50 @@ import {
Dialog,
DialogActions,
DialogContent,
DialogContentText,
makeStyles
} from '@material-ui/core'
import React, { useEffect, useState, memo } from 'react'
import React, { memo, useState } from 'react'
import { Button, IconButton } from 'src/components/buttons'
import { TextInput } from 'src/components/inputs'
import { H4 } from 'src/components/typography'
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg'
import { fontSize3 } from 'src/styling/variables'
import { spacer } from 'src/styling/variables'
import { TextInput } from './inputs'
import { H4, P } from './typography'
import ErrorMessage from './ErrorMessage'
const useStyles = makeStyles({
label: {
fontSize: fontSize3
dialogContent: {
width: 434,
padding: spacer * 2,
paddingRight: spacer * 3.5
},
spacing: {
padding: 32
dialogTitle: {
padding: spacer * 2,
paddingRight: spacer * 1.5,
display: 'flex',
'justify-content': 'space-between',
'& > h4': {
margin: 0
},
wrapper: {
display: 'flex'
},
title: {
margin: [[20, 0, 24, 16]]
},
closeButton: {
'& > button': {
padding: 0,
margin: [[12, 12, 'auto', 'auto']]
// position: 'absolute',
// right: spacer,
// top: spacer
marginTop: -(spacer / 2)
}
},
dialogActions: {
padding: spacer * 4,
paddingTop: spacer * 2
}
})
export const DialogTitle = ({ children, onClose }) => {
const classes = useStyles()
return (
<div className={classes.wrapper}>
<div className={classes.dialogTitle}>
{children}
{onClose && (
<IconButton
size={16}
aria-label="close"
className={classes.closeButton}
onClick={onClose}>
<IconButton size={16} aria-label="close" onClick={onClose}>
<CloseIcon />
</IconButton>
)}
@ -57,50 +56,59 @@ export const DialogTitle = ({ children, onClose }) => {
export const ConfirmDialog = memo(
({
title = 'Confirm action',
subtitle = 'This action requires confirmation',
errorMessage = 'This action requires confirmation',
open,
toBeConfirmed,
onConfirmed,
onDissmised,
className,
...props
}) => {
const classes = useStyles()
const [value, setValue] = useState('')
const [error, setError] = useState(false)
useEffect(() => setValue(''), [open])
const handleChange = event => {
setValue(event.target.value)
const handleChange = event => setValue(event.target.value)
const innerOnClose = () => {
setValue('')
setError(false)
onDissmised()
}
return (
<Dialog open={open} aria-labelledby="form-dialog-title" {...props}>
<DialogTitle id="customized-dialog-title" onClose={onDissmised}>
<H4 className={classes.title}>{title}</H4>
{subtitle && (
<DialogContentText>
<P>{subtitle}</P>
</DialogContentText>
)}
<DialogTitle id="customized-dialog-title" onClose={innerOnClose}>
<H4>{title}</H4>
</DialogTitle>
<DialogContent className={className}>
{errorMessage && (
<DialogTitle>
<ErrorMessage>
{errorMessage.split(':').map(error => (
<>
{error}
<br />
</>
))}
</ErrorMessage>
</DialogTitle>
)}
<DialogContent className={classes.dialogContent}>
<TextInput
label={`Write '${toBeConfirmed}' to confirm`}
label={`Write '${toBeConfirmed}' to confirm this action`}
name="confirm-input"
autoFocus
id="confirm-input"
type="text"
size="lg"
size="sm"
fullWidth
value={value}
touched={{}}
error={error}
InputLabelProps={{ shrink: true, className: classes.label }}
InputLabelProps={{ shrink: true }}
onChange={handleChange}
onBlur={() => setError(toBeConfirmed !== value)}
/>
</DialogContent>
<DialogActions classes={{ spacing: classes.spacing }}>
<DialogActions className={classes.dialogActions}>
<Button
color="green"
disabled={toBeConfirmed !== value}

View file

@ -11,7 +11,8 @@ import {
spring3,
smallestFontSize,
inputFontFamily,
spacer
spacer,
linen
} from '../styling/variables'
const colors = {
@ -22,7 +23,7 @@ const colors = {
const backgroundColors = {
error: mistyRose,
warning: mistyRose,
warning: linen,
success: spring3
}
@ -40,21 +41,15 @@ const useStyles = makeStyles({
fontSize: smallestFontSize,
fontWeight: inputFontWeight,
fontFamily: inputFontFamily,
padding: [[spacer / 2, spacer]],
paddingRight: spacer / 2,
paddingLeft: spacer / 2,
color: ({ type }) => colors[type]
}
})
const Status = ({ status, className }) => {
const Status = ({ status }) => {
const classes = useStyles({ type: status.type })
return (
<Chip
type={status.type}
label={status.label}
className={className ?? null}
classes={classes}
/>
)
return <Chip type={status.type} label={status.label} classes={classes} />
}
const MainStatus = ({ statuses }) => {
@ -65,7 +60,7 @@ const MainStatus = ({ statuses }) => {
const plus = { label: `+${statuses.length - 1}`, type: mainStatus.type }
return (
<div style={{ marginLeft: -3 }}>
<div>
<Status status={mainStatus} />
{statuses.length > 1 && <Status status={plus} />}
</div>

View file

@ -1,20 +1,22 @@
import { useMutation } from '@apollo/react-hooks'
import { Dialog, DialogContent } from '@material-ui/core'
import { Grid, Divider } from '@material-ui/core'
import { makeStyles } from '@material-ui/core/styles'
import gql from 'graphql-tag'
import moment from 'moment'
import React, { useState } from 'react'
import { DialogTitle, ConfirmDialog } from 'src/components/ConfirmDialog'
import { ConfirmDialog } from 'src/components/ConfirmDialog'
import { Status } from 'src/components/Status'
import ActionButton from 'src/components/buttons/ActionButton'
import { Label1, H4 } from 'src/components/typography'
import { ReactComponent as LinkIcon } from 'src/styling/icons/button/link/zodiac.svg'
import { ReactComponent as RebootReversedIcon } from 'src/styling/icons/button/reboot/white.svg'
import { ReactComponent as RebootIcon } from 'src/styling/icons/button/reboot/zodiac.svg'
import { ReactComponent as ShutdownReversedIcon } from 'src/styling/icons/button/shut down/white.svg'
import { ReactComponent as ShutdownIcon } from 'src/styling/icons/button/shut down/zodiac.svg'
import { ReactComponent as UnpairReversedIcon } from 'src/styling/icons/button/unpair/white.svg'
import { ReactComponent as UnpairIcon } from 'src/styling/icons/button/unpair/zodiac.svg'
import styles from './MachineDetailsCard.styles'
import { labelStyles, machineDetailsStyles } from './MachineDetailsCard.styles'
const MACHINE_ACTION = gql`
mutation MachineAction($deviceId: ID!, $action: MachineAction!) {
@ -24,150 +26,172 @@ const MACHINE_ACTION = gql`
}
`
const useStyles = makeStyles(styles)
const supportArtices = [
{
// Default article for non-maped statuses
code: undefined,
label: 'Troubleshooting',
article:
'https://support.lamassu.is/hc/en-us/categories/115000075249-Troubleshooting'
}
]
const article = ({ code: status }) =>
supportArtices.find(({ code: article }) => article === status)
const useLStyles = makeStyles(labelStyles)
const Label = ({ children }) => {
const classes = useStyles()
return <Label1 className={classes.label}>{children}</Label1>
const classes = useLStyles()
return <div className={classes.label}>{children}</div>
}
const MachineDetailsRow = ({ it: machine }) => {
const [errorDialog, setErrorDialog] = useState(false)
const [dialogOpen, setOpen] = useState(false)
const [actionMessage, setActionMessage] = useState(null)
const classes = useStyles()
const useMDStyles = makeStyles(machineDetailsStyles)
const unpairDialog = () => setOpen(true)
const Container = ({ children, ...props }) => (
<Grid container spacing={4} {...props}>
{children}
</Grid>
)
const Item = ({ children, ...props }) => (
<Grid item xs {...props}>
{children}
</Grid>
)
const MachineDetailsRow = ({ it: machine, onActionSuccess }) => {
const [action, setAction] = useState('')
const [dialogOpen, setOpen] = useState(false)
const [errorMessage, setErrorMessage] = useState(null)
const classes = useMDStyles()
const confirmDialog = action => setAction(action) || setOpen(true)
const [machineAction, { loading }] = useMutation(MACHINE_ACTION, {
onError: ({ graphQLErrors, message }) => {
const errorMessage = graphQLErrors[0] ? graphQLErrors[0].message : message
setActionMessage(errorMessage)
setErrorDialog(true)
onError: ({ message }) => {
const errorMessage = message ?? 'An error ocurred'
setErrorMessage(errorMessage)
},
onCompleted: () => {
// TODO: custom onActionSuccess needs to be passed down from the machinestatus table
onActionSuccess ? onActionSuccess() : window.location.reload()
setOpen(false)
}
})
return (
<>
<Dialog open={errorDialog} aria-labelledby="form-dialog-title">
<DialogTitle
id="customized-dialog-title"
onClose={() => setErrorDialog(false)}>
<H4>Error</H4>
</DialogTitle>
<DialogContent>{actionMessage}</DialogContent>
</Dialog>
<div className={classes.wrapper}>
<div className={classes.column1}>
<div className={classes.lastRow}>
<div className={classes.status}>
<Container className={classes.wrapper}>
<Item xs={5}>
<Container>
<Item>
<Label>Statuses</Label>
<div>
<ul className={classes.list}>
{machine.statuses.map((status, index) => (
<Status
className={classes.chips}
status={status}
key={index}
/>
<li key={index}>
<Status status={status} />
</li>
))}
</div>
</div>
<div>
</ul>
</Item>
<Item>
<Label>Lamassu Support article</Label>
<div>
{machine.statuses.map((...[, index]) => (
// TODO new-admin: support articles
<span key={index}></span>
<ul className={classes.list}>
{machine.statuses
.map(article)
.map(({ label, article }, index) => (
<li key={index}>
<a
target="_blank"
rel="noopener noreferrer"
href={article}>
'{label}' <LinkIcon />
</a>
</li>
))}
</div>
</div>
<div className={classes.separator} />
</div>
</div>
<div className={classes.column2}>
<div className={classes.row}>
<div className={classes.machineModel}>
<Label>Machine Model</Label>
<div>{machine.model ?? 'unknown'}</div>
</div>
<div>
<Label>Paired at</Label>
<div>
{machine.pairedAt
? moment(machine.pairedAt).format('YYYY-MM-DD HH:mm:ss')
: 'N/A'}
</div>
</div>
</div>
<div className={classes.lastRow}>
<div>
<Label>Actions</Label>
<div className={classes.actionRow}>
<ActionButton
className={classes.action}
color="primary"
Icon={UnpairIcon}
InverseIcon={UnpairReversedIcon}
disabled={loading}
onClick={unpairDialog}>
Unpair
</ActionButton>
</ul>
</Item>
</Container>
</Item>
<Divider
orientation="vertical"
flexItem
className={classes.separator}
/>
<ConfirmDialog
open={dialogOpen}
className={classes.dialog}
title="Unpair this machine?"
subtitle={false}
title={`${action} this machine?`}
errorMessage={errorMessage}
toBeConfirmed={machine.name}
onConfirmed={() => {
setOpen(false)
setErrorMessage(null)
machineAction({
variables: {
deviceId: machine.deviceId,
action: 'unpair'
action: `${action}`.toLowerCase()
}
})
}}
onDissmised={() => {
setOpen(false)
setErrorMessage(null)
}}
/>
<Item xs>
<Container className={classes.row}>
<Item xs={4}>
<Label>Machine Model</Label>
<span>{machine.model}</span>
</Item>
{/* <Item>
<Label>Address</Label>
<span>{machine.machineLocation}</span>
</Item> */}
<Item xs={4}>
<Label>Paired at</Label>
<span>
{moment(machine.pairedAt).format('YYYY-MM-DD HH:mm:ss')}
</span>
</Item>
</Container>
<Container>
<Item>
<Label>Actions</Label>
<div className={classes.stack}>
<ActionButton
className={classes.action}
color="primary"
className={classes.mr}
Icon={UnpairIcon}
InverseIcon={UnpairReversedIcon}
disabled={loading}
onClick={() => confirmDialog('Unpair')}>
Unpair
</ActionButton>
<ActionButton
color="primary"
className={classes.mr}
Icon={RebootIcon}
InverseIcon={RebootReversedIcon}
disabled={loading}
onClick={() => {
machineAction({
variables: {
deviceId: machine.deviceId,
action: 'reboot'
}
})
}}>
onClick={() => confirmDialog('Reboot')}>
Reboot
</ActionButton>
<ActionButton
className={classes.action}
className={classes.inlineChip}
disabled={loading}
color="primary"
Icon={RebootIcon}
InverseIcon={RebootReversedIcon}
onClick={() => {
machineAction({
variables: {
deviceId: machine.deviceId,
action: 'restartServices'
}
})
}}>
Restart Services
Icon={ShutdownIcon}
InverseIcon={ShutdownReversedIcon}
onClick={() => confirmDialog('Shutdown')}>
Shutdown
</ActionButton>
</div>
</div>
</div>
</div>
</div>
</Item>
</Container>
</Item>
</Container>
</>
)
}

View file

@ -1,53 +1,55 @@
import { fade } from '@material-ui/core/styles/colorManipulator'
import { fontSize4, offColor, comet } from 'src/styling/variables'
import {
detailsRowStyles,
labelStyles
} from 'src/pages/Transactions/Transactions.styles'
import { spacer, comet, primaryColor, fontSize4 } from 'src/styling/variables'
export default {
const machineDetailsStyles = {
...detailsRowStyles,
colDivider: {
width: 1,
margin: [[spacer * 2, spacer * 4]],
backgroundColor: comet,
border: 'none'
},
inlineChip: {
marginInlineEnd: '0.25em'
},
stack: {
display: 'flex',
flexDirection: 'row'
},
wrapper: {
display: 'flex',
marginTop: 24,
marginBottom: 32,
fontSize: fontSize4
},
column1: {
width: 600
},
column2: {
flex: 1
},
lastRow: {
display: 'flex',
flexDirection: 'row'
},
row: {
display: 'flex',
flexDirection: 'row',
marginBottom: 36
},
actionRow: {
display: 'flex',
flexDirection: 'row',
marginLeft: -4
list: {
padding: 0,
margin: 0,
listStyle: 'none',
'& > li': {
height: spacer * 3,
marginBottom: spacer * 1.5,
'& > a, & > a:visited': {
color: primaryColor,
textDecoration: 'none'
}
}
},
action: {
marginRight: 4,
marginLeft: 4
divider: {
margin: '0 1rem'
},
dialog: {
width: 434
},
label: {
color: offColor,
margin: [[0, 0, 6, 0]]
},
chips: {
marginLeft: -2
},
status: {
width: 248
},
machineModel: {
width: 198
mr: {
marginRight: spacer
},
separator: {
width: 1,
@ -58,3 +60,5 @@ export default {
background: fade(comet, 0.5)
}
}
export { labelStyles, machineDetailsStyles }

View file

@ -5,13 +5,12 @@ import moment from 'moment'
import * as R from 'ramda'
import React from 'react'
import { MainStatus } from 'src/components/Status'
import Title from 'src/components/Title'
import DataTable from 'src/components/tables/DataTable'
import { MainStatus } from '../../components/Status'
import Title from '../../components/Title'
import { ReactComponent as WarningIcon } from '../../styling/icons/status/pumpkin.svg'
import { ReactComponent as ErrorIcon } from '../../styling/icons/status/tomato.svg'
import { mainStyles } from '../Transactions/Transactions.styles'
import { mainStyles } from 'src/pages/Transactions/Transactions.styles'
import { ReactComponent as WarningIcon } from 'src/styling/icons/status/pumpkin.svg'
import { ReactComponent as ErrorIcon } from 'src/styling/icons/status/tomato.svg'
import MachineDetailsRow from './MachineDetailsCard'
@ -22,10 +21,13 @@ const GET_MACHINES = gql`
deviceId
lastPing
pairedAt
version
paired
cashbox
cassette1
cassette2
version
model
statuses {
label
type
@ -68,7 +70,7 @@ const MachineStatus = () => {
width: 200,
size: 'sm',
textAlign: 'left',
view: m => m.softwareVersion || 'unknown'
view: m => m.version || 'unknown'
}
]

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<svg width="12px" height="12px" viewBox="-0.493 -0.5 12.993 13" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
<desc>Created with Sketch.</desc>
<g id="icon/button/reboot/white" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<svg width="12px" height="12px" viewBox="-0.493 -0.5 12.993 13" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
<desc>Created with Sketch.</desc>
<g id="icon/button/reboot/zodiac" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After