diff --git a/lib/machine-loader.js b/lib/machine-loader.js
index feee274f..8f4c9d85 100644
--- a/lib/machine-loader.js
+++ b/lib/machine-loader.js
@@ -15,6 +15,10 @@ function getMachines () {
cashbox: r.cashbox,
cassette1: r.cassette1,
cassette2: r.cassette2,
+ pairedAt: new Date(r.created).valueOf(),
+ lastPing: new Date(r.last_online).valueOf(),
+ // TODO: we shall start using this JSON field at some point
+ // location: r.location,
paired: r.paired
})))
}
@@ -32,8 +36,16 @@ function getMachineNames (config) {
const machineScoped = configManager.machineScoped(r.deviceId, config)
const name = _.defaultTo('', machineScoped.machineName)
const cashOut = machineScoped.cashOutEnabled
+ const machineModel = _.defaultTo('', machineScoped.machineModel)
+ const machineLocation = _.defaultTo('', machineScoped.machineLocation)
- return _.assign(r, {name, cashOut})
+ // TODO: obtain next fields from somewhere
+ const printer = null
+ const pingTime = null
+ const statuses = [{label: 'Unknown detailed status', type: 'warning'}]
+ const softwareVersion = ''
+
+ return _.assign(r, {name, cashOut, machineModel, machineLocation, printer, pingTime, statuses, softwareVersion})
}
return _.map(addName, machines)
diff --git a/lib/new-admin/graphql/schema.js b/lib/new-admin/graphql/schema.js
index b1bb4467..75f673eb 100644
--- a/lib/new-admin/graphql/schema.js
+++ b/lib/new-admin/graphql/schema.js
@@ -4,6 +4,7 @@ const { GraphQLJSON, GraphQLJSONObject } = require('graphql-type-json')
const got = require('got')
const machineLoader = require('../../machine-loader')
+const { machineAction } = require('../machines')
const logs = require('../../logs')
const supportLogs = require('../../support_logs')
const settingsLoader = require('../../new-settings-loader')
@@ -42,6 +43,11 @@ const typeDefs = gql`
display: String!
}
+ type MachineStatus {
+ label: String!
+ type: String!
+ }
+
type Machine {
name: String!
deviceId: ID!
@@ -49,6 +55,7 @@ const typeDefs = gql`
cashbox: Int
cassette1: Int
cassette2: Int
+ statuses: [MachineStatus]
}
type Account {
@@ -154,7 +161,15 @@ const typeDefs = gql`
deviceId: ID
}
+ enum MachineAction {
+ resetCashOutBills
+ unpair
+ reboot
+ restartServices
+ }
+
type Mutation {
+ machineAction(deviceId:ID!, action: MachineAction!): Machine
machineSupportLogs(deviceId: ID!): SupportLogsResponse
serverSupportLogs: SupportLogsResponse
saveConfig(config: JSONObject): JSONObject
@@ -184,6 +199,7 @@ const resolvers = {
config: () => settingsLoader.getConfig()
},
Mutation: {
+ machineAction: (...[, { deviceId, action }]) => machineAction({ deviceId, action }),
machineSupportLogs: (...[, { deviceId }]) => supportLogs.insert(deviceId),
serverSupportLogs: () => serverLogs.insert(),
saveConfig: (...[, { config }]) => settingsLoader.saveConfig(config)
diff --git a/lib/new-admin/machines.js b/lib/new-admin/machines.js
new file mode 100644
index 00000000..cc264184
--- /dev/null
+++ b/lib/new-admin/machines.js
@@ -0,0 +1,20 @@
+const machineLoader = require('../machine-loader')
+const { UserInputError } = require('apollo-server-express')
+
+function getMachine(machineId) {
+ return machineLoader.getMachines()
+ .then(machines => machines.find(({ deviceId }) => deviceId === machineId))
+}
+
+function machineAction({ deviceId, action }) {
+
+ return getMachine(deviceId)
+ .then(machine => {
+ if (!machine) throw new UserInputError(`machine:${deviceId} not found`, { deviceId })
+ return machine
+ })
+ .then(machineLoader.setMachine({ deviceId, action }))
+ .then(getMachine(deviceId))
+}
+
+module.exports = { machineAction }
diff --git a/new-lamassu-admin/public/icons/status/pumpkin.svg b/new-lamassu-admin/public/icons/status/pumpkin.svg
new file mode 100644
index 00000000..bc15ef82
--- /dev/null
+++ b/new-lamassu-admin/public/icons/status/pumpkin.svg
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/new-lamassu-admin/public/icons/status/tomato.svg b/new-lamassu-admin/public/icons/status/tomato.svg
new file mode 100644
index 00000000..9fbd17ab
--- /dev/null
+++ b/new-lamassu-admin/public/icons/status/tomato.svg
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/new-lamassu-admin/src/components/ConfirmDialog.js b/new-lamassu-admin/src/components/ConfirmDialog.js
new file mode 100644
index 00000000..1a7f9c76
--- /dev/null
+++ b/new-lamassu-admin/src/components/ConfirmDialog.js
@@ -0,0 +1,96 @@
+import {
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogContentText,
+ DialogTitle as MuiDialogTitle,
+ IconButton,
+ makeStyles
+} from '@material-ui/core'
+import React, { useState, memo } from 'react'
+
+import { Button } from '../components/buttons'
+import { ReactComponent as CloseIcon } from '../styling/icons/action/close/zodiac.svg'
+import { spacer } from '../styling/variables'
+
+import { TextInput } from './inputs'
+import { H4, P } from './typography'
+
+const useStyles = makeStyles({
+ closeButton: {
+ position: 'absolute',
+ right: spacer,
+ top: spacer
+ }
+})
+
+export const DialogTitle = ({ children, onClose }) => {
+ const classes = useStyles()
+ return (
+
+ {children}
+ {onClose && (
+
+
+
+ )}
+
+ )
+}
+
+export const ConfirmDialog = memo(
+ ({
+ title = 'Confirm action',
+ subtitle = 'This action requires confirmation',
+ open,
+ toBeConfirmed,
+ onConfirmed,
+ onDissmised,
+ ...props
+ }) => {
+ const [value, setValue] = useState('')
+ const handleChange = event => {
+ setValue(event.target.value)
+ }
+
+ return (
+
+ )
+ }
+)
diff --git a/new-lamassu-admin/src/components/Status.js b/new-lamassu-admin/src/components/Status.js
new file mode 100644
index 00000000..926f90fc
--- /dev/null
+++ b/new-lamassu-admin/src/components/Status.js
@@ -0,0 +1,69 @@
+import React from 'react'
+import Chip from '@material-ui/core/Chip'
+import { makeStyles } from '@material-ui/core/styles'
+
+import {
+ tomato,
+ mistyRose,
+ pumpkin,
+ secondaryColorDarker as spring4,
+ inputFontWeight,
+ spring3,
+ smallestFontSize,
+ inputFontFamily,
+ spacer
+} from '../styling/variables'
+
+const colors = {
+ error: tomato,
+ warning: pumpkin,
+ success: spring4
+}
+
+const backgroundColors = {
+ error: mistyRose,
+ warning: mistyRose,
+ success: spring3
+}
+
+const useStyles = makeStyles({
+ root: {
+ borderRadius: spacer / 2,
+ marginTop: spacer / 2,
+ marginRight: spacer / 4,
+ marginBottom: spacer / 2,
+ marginLeft: spacer / 4,
+ height: 18,
+ backgroundColor: ({ type }) => backgroundColors[type]
+ },
+ label: {
+ fontSize: smallestFontSize,
+ fontWeight: inputFontWeight,
+ fontFamily: inputFontFamily,
+ paddingRight: spacer / 2,
+ paddingLeft: spacer / 2,
+ color: ({ type }) => colors[type]
+ }
+})
+
+const Status = ({ status }) => {
+ const classes = useStyles({ type: status.type })
+ return
+}
+
+const MainStatus = ({ statuses }) => {
+ const mainStatus =
+ statuses.find(s => s.type === 'error') ||
+ statuses.find(s => s.type === 'warning') ||
+ statuses[0]
+ const plus = { label: `+${statuses.length - 1}`, type: mainStatus.type }
+
+ return (
+
+
+ {statuses.length > 1 && }
+
+ )
+}
+
+export { Status, MainStatus }
diff --git a/new-lamassu-admin/src/pages/maintenance/MachineDetailsCard.js b/new-lamassu-admin/src/pages/maintenance/MachineDetailsCard.js
new file mode 100644
index 00000000..510c33fe
--- /dev/null
+++ b/new-lamassu-admin/src/pages/maintenance/MachineDetailsCard.js
@@ -0,0 +1,242 @@
+import { makeStyles } from '@material-ui/core/styles'
+import classnames from 'classnames'
+import moment from 'moment'
+import React, { useState } from 'react'
+import { useMutation } from '@apollo/react-hooks'
+import { gql } from 'apollo-boost'
+import { Dialog, DialogContent } from '@material-ui/core'
+
+import { H4 } from 'src/components/typography'
+
+import ActionButton from '../../components/buttons/ActionButton'
+import { DialogTitle, ConfirmDialog } from '../../components/ConfirmDialog'
+import { Status } from '../../components/Status'
+import { ReactComponent as DownloadReversedIcon } from '../../styling/icons/button/download/white.svg'
+import { ReactComponent as DownloadIcon } from '../../styling/icons/button/download/zodiac.svg'
+import { ReactComponent as RebootReversedIcon } from '../../styling/icons/button/reboot/white.svg'
+import { ReactComponent as RebootIcon } from '../../styling/icons/button/reboot/zodiac.svg'
+import { ReactComponent as ShutdownReversedIcon } from '../../styling/icons/button/shut down/white.svg'
+import { ReactComponent as ShutdownIcon } from '../../styling/icons/button/shut down/zodiac.svg'
+import { ReactComponent as UnpairReversedIcon } from '../../styling/icons/button/unpair/white.svg'
+import { ReactComponent as UnpairIcon } from '../../styling/icons/button/unpair/zodiac.svg'
+import {
+ detailsRowStyles,
+ labelStyles
+} from '../Transactions/Transactions.styles'
+import { zircon } from '../../styling/variables'
+
+const MACHINE_ACTION = gql`
+ mutation MachineAction($deviceId: ID!, $action: MachineAction!) {
+ machineAction(deviceId: $deviceId, action: $action) {
+ deviceId
+ }
+ }
+`
+
+const colDivider = {
+ background: zircon,
+ width: 2
+}
+
+const inlineChip = {
+ marginInlineEnd: '0.25em'
+}
+
+const useLStyles = makeStyles(labelStyles)
+
+const Label = ({ children }) => {
+ const classes = useLStyles()
+
+ return {children}
+}
+
+const useMDStyles = makeStyles({ ...detailsRowStyles, colDivider, inlineChip })
+
+const MachineDetailsRow = ({ it: machine }) => {
+ const [errorDialog, setErrorDialog] = useState(false)
+ const [dialogOpen, setOpen] = useState(false)
+ const [actionMessage, setActionMessage] = useState(null)
+ const classes = useMDStyles()
+
+ const unpairDialog = () => setOpen(true)
+
+ const [machineAction, { loading }] = useMutation(MACHINE_ACTION, {
+ onError: ({ graphQLErrors, message }) => {
+ const errorMessage = graphQLErrors[0] ? graphQLErrors[0].message : message
+ setActionMessage(errorMessage)
+ setErrorDialog(true)
+ }
+ })
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ {machine.statuses.map((status, index) => (
+
+ ))}
+
+
+
+
+
+ {machine.statuses.map((...[, index]) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{machine.machineModel}
+
+
+
+
{machine.machineLocation}
+
+
+
+
+
+
+
+
+
+
+
+ {moment(machine.pairedAt).format('YYYY-MM-DD HH:mm:ss')}
+
+
+
+
+
+ {machine.softwareVersion && (
+
+ {machine.softwareVersion}
+
+ )}
+
+ Update
+
+
+
+
+
+
+
+
+
+
+
+
+
{machine.printer || 'unknown'}
+
+
+
+
+
+ Unpair
+
+
{
+ setOpen(false)
+ machineAction({
+ variables: {
+ deviceId: machine.deviceId,
+ action: 'unpair'
+ }
+ })
+ }}
+ onDissmised={() => {
+ setOpen(false)
+ }}
+ />
+ {
+ machineAction({
+ variables: {
+ deviceId: machine.deviceId,
+ action: 'reboot'
+ }
+ })
+ }}>
+ Reboot
+
+ {
+ machineAction({
+ variables: {
+ deviceId: machine.deviceId,
+ action: 'shutdown'
+ }
+ })
+ }}>
+ Shutdown
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+export default MachineDetailsRow
diff --git a/new-lamassu-admin/src/pages/maintenance/MachineStatus.js b/new-lamassu-admin/src/pages/maintenance/MachineStatus.js
new file mode 100644
index 00000000..3316a69e
--- /dev/null
+++ b/new-lamassu-admin/src/pages/maintenance/MachineStatus.js
@@ -0,0 +1,103 @@
+import { makeStyles } from '@material-ui/core'
+import { useQuery } from '@apollo/react-hooks'
+import { gql } from 'apollo-boost'
+import moment from 'moment'
+import * as R from 'ramda'
+import React from 'react'
+
+import ExpTable from '../../components/expandable-table/ExpTable'
+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 MachineDetailsRow from './MachineDetailsCard'
+
+const GET_MACHINES = gql`
+ {
+ machines {
+ name
+ deviceId
+ paired
+ cashbox
+ cassette1
+ cassette2
+ statuses {
+ label
+ type
+ }
+ }
+ }
+`
+
+const useStyles = makeStyles(mainStyles)
+
+const MachineStatus = () => {
+ const classes = useStyles()
+
+ const { data: machinesResponse } = useQuery(GET_MACHINES)
+
+ const elements = [
+ {
+ header: 'Machine Name',
+ size: 232,
+ textAlign: 'left',
+ view: m => m.name
+ },
+ {
+ header: 'Status',
+ size: 349,
+ textAlign: 'left',
+ view: m =>
+ },
+ {
+ header: 'Last ping',
+ size: 192,
+ textAlign: 'left',
+ view: m => moment(m.lastPing).fromNow()
+ },
+ {
+ header: 'Ping Time',
+ size: 155,
+ textAlign: 'left',
+ view: m => m.pingTime || 'unknown'
+ },
+ {
+ header: 'Software Version',
+ size: 201,
+ textAlign: 'left',
+ view: m => m.softwareVersion || 'unknown'
+ },
+ {
+ size: 71
+ }
+ ]
+
+ return (
+ <>
+
+
+
Machine Status
+
+
+
+
+ Warning
+
+
+
+ Error
+
+
+
+
+ >
+ )
+}
+
+export default MachineStatus
diff --git a/new-lamassu-admin/src/routing/routes.js b/new-lamassu-admin/src/routing/routes.js
index 0a253701..bc040f45 100644
--- a/new-lamassu-admin/src/routing/routes.js
+++ b/new-lamassu-admin/src/routing/routes.js
@@ -12,6 +12,8 @@ import Services from 'src/pages/Services/Services'
import AuthRegister from 'src/pages/AuthRegister'
import OperatorInfo from 'src/pages/OperatorInfo/OperatorInfo'
+import MachineStatus from '../pages/maintenance/MachineStatus'
+
const tree = [
{ key: 'transactions', label: 'Transactions', route: '/transactions' },
// maintenence: { label: 'Maintenence', children: [{ label: 'Locale', route: '/locale' }] },
@@ -27,6 +29,11 @@ const tree = [
key: 'server-logs',
label: 'Server',
route: '/maintenance/server-logs'
+ },
+ {
+ key: 'machine-status',
+ label: 'Machine Status',
+ route: '/maintenance/machine-status'
}
]
},
@@ -80,6 +87,7 @@ const Routes = () => (
+
)
diff --git a/new-lamassu-admin/src/stories/index.js b/new-lamassu-admin/src/stories/index.js
index c018ce39..e1bf61bd 100644
--- a/new-lamassu-admin/src/stories/index.js
+++ b/new-lamassu-admin/src/stories/index.js
@@ -17,6 +17,7 @@ import { ReactComponent as AuthorizeIcon } from 'src/styling/icons/button/author
import { ActionButton, Button, Link } from 'src/components/buttons'
import { Radio, TextInput, Switch } from 'src/components/inputs'
+import ConfirmDialog from '../components/ConfirmDialog'
import {
H1,
H2,
@@ -119,7 +120,11 @@ story.add('Switch', () => (
story.add('Text Input', () => (
-
+
))
@@ -141,6 +146,21 @@ story.add('Checkbox', () => (
))
+story.add('ConfirmDialog', () => (
+
+ {
+ window.alert('dissmised')
+ }}
+ onConfirmed={() => {
+ window.alert('confirmed')
+ }}
+ toBeConfirmed="there-is-no-fate"
+ />
+
+))
+
story.add('Radio', () => )
const typographyStory = storiesOf('Typography', module)
diff --git a/new-lamassu-admin/src/styling/icons/status/pumpkin.svg b/new-lamassu-admin/src/styling/icons/status/pumpkin.svg
new file mode 100644
index 00000000..bc15ef82
--- /dev/null
+++ b/new-lamassu-admin/src/styling/icons/status/pumpkin.svg
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/new-lamassu-admin/src/styling/icons/status/tomato.svg b/new-lamassu-admin/src/styling/icons/status/tomato.svg
new file mode 100644
index 00000000..9fbd17ab
--- /dev/null
+++ b/new-lamassu-admin/src/styling/icons/status/tomato.svg
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/new-lamassu-admin/src/styling/variables.js b/new-lamassu-admin/src/styling/variables.js
index 23367774..3b0902f8 100644
--- a/new-lamassu-admin/src/styling/variables.js
+++ b/new-lamassu-admin/src/styling/variables.js
@@ -119,10 +119,10 @@ export {
spring2,
spring3,
tomato,
+ pumpkin,
mistyRose,
java,
neon,
- pumpkin,
linen,
// named colors
primaryColor,