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 @@ + + + icon/status/warning + + \ 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 @@ + + + icon/status/warning + + \ 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 ( + + +

{title}

+ {subtitle && ( + +

{subtitle}

+
+ )} +
+ + + + + + +
+ ) + } +) 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 ( + <> + + setErrorDialog(false)}> +

Error

+
+ {actionMessage} +
+
+
+
+
+
+
+
+ +
+ {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 @@ + + + icon/status/warning + + \ 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 @@ + + + icon/status/warning + + \ 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,