From bf5ee5feb474c1d3108ce8bc95a54495a847828a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Leal?= <37.joao.leal@gmail.com> Date: Fri, 23 Apr 2021 15:01:39 +0100 Subject: [PATCH] fix: add machine actions to 'Dashboard > Machine Details' --- lib/machine-loader.js | 49 ++- .../machineActions/MachineActions.js | 221 ++++++++++++++ .../machineActions/MachineActions.styles.js | 82 +++++ .../Machines/MachineComponents/Overview.js | 94 +----- .../src/pages/Machines/Machines.js | 52 ++-- .../pages/Maintenance/MachineDetailsCard.js | 283 +----------------- 6 files changed, 389 insertions(+), 392 deletions(-) create mode 100644 new-lamassu-admin/src/components/machineActions/MachineActions.js create mode 100644 new-lamassu-admin/src/components/machineActions/MachineActions.styles.js diff --git a/lib/machine-loader.js b/lib/machine-loader.js index b449e68b..acc31ad1 100644 --- a/lib/machine-loader.js +++ b/lib/machine-loader.js @@ -87,9 +87,54 @@ function getMachineName (machineId) { .then(it => it.name) } -function getMachine (machineId) { +function getMachine (machineId, config) { + const fullyFunctionalStatus = { label: 'Fully functional', type: 'success' } + const unresponsiveStatus = { label: 'Unresponsive', type: 'error' } + const stuckStatus = { label: 'Stuck', type: 'error' } + const sql = 'SELECT * FROM devices WHERE device_id=$1' - return db.oneOrNone(sql, [machineId]).then(res => _.mapKeys(_.camelCase)(res)) + const queryMachine = db.oneOrNone(sql, [machineId]).then(r => ({ + deviceId: r.device_id, + cashbox: r.cashbox, + cassette1: r.cassette1, + cassette2: r.cassette2, + version: r.version, + model: r.model, + pairedAt: new Date(r.created), + lastPing: new Date(r.last_online), + name: r.name, + paired: r.paired + })) + + return Promise.all([queryMachine, dbm.machineEvents(), config]) + .then(([machine, events, config]) => { + const pings = checkPings([machine]) + + const getStatus = (ping, stuck) => { + if (ping && ping.age) return unresponsiveStatus + + if (stuck && stuck.age) return stuckStatus + + return fullyFunctionalStatus + } + + const addName = r => { + const cashOutConfig = configManager.getCashOut(r.deviceId, config) + + const cashOut = !!cashOutConfig.active + + const statuses = [ + getStatus( + _.first(pings[r.deviceId]), + _.first(checkStuckScreen(events, r.name)) + ) + ] + + return _.assign(r, { cashOut, statuses }) + } + + return addName(machine) + }) } function renameMachine (rec) { diff --git a/new-lamassu-admin/src/components/machineActions/MachineActions.js b/new-lamassu-admin/src/components/machineActions/MachineActions.js new file mode 100644 index 00000000..90953a22 --- /dev/null +++ b/new-lamassu-admin/src/components/machineActions/MachineActions.js @@ -0,0 +1,221 @@ +import { useMutation, useLazyQuery } from '@apollo/react-hooks' +import { makeStyles } from '@material-ui/core/styles' +import gql from 'graphql-tag' +// import moment from 'moment' +import React, { memo, useState } from 'react' + +import { ConfirmDialog } from 'src/components/ConfirmDialog' +import ActionButton from 'src/components/buttons/ActionButton' +// // import { Status } from 'src/components/Status' +import { ReactComponent as EditReversedIcon } from 'src/styling/icons/button/edit/white.svg' +import { ReactComponent as EditIcon } from 'src/styling/icons/button/edit/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 { machineActionsStyles } from './MachineActions.styles' + +const useStyles = makeStyles(machineActionsStyles) + +const MACHINE_ACTION = gql` + mutation MachineAction( + $deviceId: ID! + $action: MachineAction! + $newName: String + ) { + machineAction(deviceId: $deviceId, action: $action, newName: $newName) { + deviceId + } + } +` + +const MACHINE = gql` + query getMachine($deviceId: ID!) { + machine(deviceId: $deviceId) { + latestEvent { + note + } + } + } +` + +const isStaticState = machineState => { + if (!machineState) { + return true + } + const staticStates = [ + 'chooseCoin', + 'idle', + 'pendingIdle', + 'dualIdle', + 'networkDown', + 'unpaired', + 'maintenance', + 'virgin', + 'wifiList' + ] + return staticStates.includes(machineState) +} + +const getState = machineEventsLazy => + JSON.parse(machineEventsLazy.machine.latestEvent?.note ?? '{"state": null}') + .state + +const Label = ({ children }) => { + const classes = useStyles() + + return
{children}
+} + +const MachineActions = memo(({ machine, onActionSuccess }) => { + const [action, setAction] = useState({ command: null }) + const [errorMessage, setErrorMessage] = useState(null) + const classes = useStyles() + + const warningMessage = ( + + A user may be in the middle of a transaction and they could lose their + funds if you continue. + + ) + + const [fetchMachineEvents, { loading: loadingEvents }] = useLazyQuery( + MACHINE, + { + variables: { + deviceId: machine.deviceId + }, + onCompleted: machineEventsLazy => { + const message = !isStaticState(getState(machineEventsLazy)) + ? warningMessage + : null + setAction(action => ({ ...action, message })) + } + } + ) + + const [machineAction, { loading }] = useMutation(MACHINE_ACTION, { + onError: ({ message }) => { + const errorMessage = message ?? 'An error ocurred' + setErrorMessage(errorMessage) + }, + onCompleted: () => { + onActionSuccess && onActionSuccess() + setAction({ command: null }) + } + }) + + const confirmDialogOpen = Boolean(action.command) + const disabled = !!(action?.command === 'restartServices' && loadingEvents) + + return ( +
+ +
+ + setAction({ + command: 'rename', + display: 'Rename', + confirmationMessage: 'Write the new name for this machine' + }) + }> + Rename + + + setAction({ + command: 'unpair', + display: 'Unpair' + }) + }> + Unpair + + + setAction({ + command: 'reboot', + display: 'Reboot' + }) + }> + Reboot + + + setAction({ + command: 'shutdown', + display: 'Shutdown', + message: + 'In order to bring it back online, the machine will need to be visited and its power reset.' + }) + }> + Shutdown + + { + fetchMachineEvents() + setAction({ + command: 'restartServices', + display: 'Restart services for' + }) + }}> + Restart Services + +
+ { + setErrorMessage(null) + machineAction({ + variables: { + deviceId: machine.deviceId, + action: `${action?.command}`, + ...(action?.command === 'rename' && { newName: value }) + } + }) + }} + onDissmised={() => { + setAction({ command: null }) + setErrorMessage(null) + }} + /> +
+ ) +}) + +export default MachineActions diff --git a/new-lamassu-admin/src/components/machineActions/MachineActions.styles.js b/new-lamassu-admin/src/components/machineActions/MachineActions.styles.js new file mode 100644 index 00000000..672ee3b1 --- /dev/null +++ b/new-lamassu-admin/src/components/machineActions/MachineActions.styles.js @@ -0,0 +1,82 @@ +import { fade } from '@material-ui/core/styles/colorManipulator' + +import typographyStyles from 'src/components/typography/styles' +import { + offColor, + spacer, + comet, + primaryColor, + fontSize4, + errorColor +} from 'src/styling/variables' + +const { label1 } = typographyStyles + +const machineActionsStyles = { + colDivider: { + width: 1, + margin: [[spacer * 2, spacer * 4]], + backgroundColor: comet, + border: 'none' + }, + label: { + extend: label1, + color: offColor, + marginBottom: 4 + }, + inlineChip: { + marginInlineEnd: '0.25em' + }, + stack: { + display: 'flex', + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'start' + }, + wrapper: { + display: 'flex', + // marginTop: 24, + // marginBottom: 32, + marginTop: 12, + marginBottom: 16, + fontSize: fontSize4 + }, + row: { + display: 'flex', + flexDirection: 'row' + // marginBottom: 36 + }, + list: { + padding: 0, + margin: 0, + listStyle: 'none' + }, + item: { + height: spacer * 3, + marginBottom: spacer * 1.5 + }, + link: { + color: primaryColor, + textDecoration: 'none' + }, + divider: { + margin: '0 1rem' + }, + mr: { + marginRight: spacer, + marginBottom: spacer + }, + separator: { + width: 1, + height: 170, + zIndex: 1, + marginRight: 60, + marginLeft: 'auto', + background: fade(comet, 0.5) + }, + warning: { + color: errorColor + } +} + +export { machineActionsStyles } diff --git a/new-lamassu-admin/src/pages/Machines/MachineComponents/Overview.js b/new-lamassu-admin/src/pages/Machines/MachineComponents/Overview.js index 11f9f295..8e40f25d 100644 --- a/new-lamassu-admin/src/pages/Machines/MachineComponents/Overview.js +++ b/new-lamassu-admin/src/pages/Machines/MachineComponents/Overview.js @@ -1,35 +1,14 @@ -import { useMutation } from '@apollo/react-hooks' import { makeStyles } from '@material-ui/core/styles' -import gql from 'graphql-tag' import moment from 'moment' -import React, { useState } from 'react' +import React from 'react' -import { ConfirmDialog } from 'src/components/ConfirmDialog' import { Status } from 'src/components/Status' -import ActionButton from 'src/components/buttons/ActionButton' +import MachineActions from 'src/components/machineActions/MachineActions' import { H3, Label3, P } from 'src/components/typography' -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 '../Machines.styles' const useStyles = makeStyles(styles) -const MACHINE_ACTION = gql` - mutation MachineAction( - $deviceId: ID! - $action: MachineAction! - $newName: String - ) { - machineAction(deviceId: $deviceId, action: $action, newName: $newName) { - deviceId - } - } -` - const makeLastPing = lastPing => { if (!lastPing) return null const now = moment() @@ -50,25 +29,8 @@ const makeLastPing = lastPing => { } const Overview = ({ data, onActionSuccess }) => { - const [action, setAction] = useState('') - const [confirmActionDialogOpen, setConfirmActionDialogOpen] = useState(false) - const [errorMessage, setErrorMessage] = useState(null) const classes = useStyles() - const [machineAction] = useMutation(MACHINE_ACTION, { - onError: ({ message }) => { - const errorMessage = message ?? 'An error ocurred' - setErrorMessage(errorMessage) - }, - onCompleted: () => { - onActionSuccess && onActionSuccess() - setConfirmActionDialogOpen(false) - } - }) - - const confirmActionDialog = action => - setAction(action) || setConfirmActionDialogOpen(true) - return ( <>
@@ -90,57 +52,11 @@ const Overview = ({ data, onActionSuccess }) => {
- {' '} - Actions - {data.name && ( -
- confirmActionDialog('Unpair')}> - Unpair - - confirmActionDialog('Reboot')}> - Reboot - - confirmActionDialog('Shutdown')}> - Shutdown - -
- )} +
- { - setErrorMessage(null) - machineAction({ - variables: { - deviceId: data.deviceId, - action: `${action}`.toLowerCase() - } - }) - }} - onDissmised={() => { - setConfirmActionDialogOpen(false) - setErrorMessage(null) - }} - /> ) } diff --git a/new-lamassu-admin/src/pages/Machines/Machines.js b/new-lamassu-admin/src/pages/Machines/Machines.js index 75e87c2c..84980c99 100644 --- a/new-lamassu-admin/src/pages/Machines/Machines.js +++ b/new-lamassu-admin/src/pages/Machines/Machines.js @@ -5,8 +5,7 @@ import { makeStyles } from '@material-ui/core/styles' import NavigateNextIcon from '@material-ui/icons/NavigateNext' import classnames from 'classnames' import gql from 'graphql-tag' -import * as R from 'ramda' -import React from 'react' +import React, { useState, useEffect } from 'react' import { Link, useLocation } from 'react-router-dom' import { TL1, TL2, Label3 } from 'src/components/typography' @@ -19,11 +18,9 @@ import Transactions from './MachineComponents/Transactions' import styles from './Machines.styles' const useStyles = makeStyles(styles) -const getMachineInfo = R.compose(R.find, R.propEq('name')) - const GET_INFO = gql` - query getInfo { - machines { + query getMachine($deviceId: ID!) { + machine(deviceId: $deviceId) { name deviceId paired @@ -38,24 +35,34 @@ const GET_INFO = gql` label type } + latestEvent { + note + } } config } ` -const getMachines = R.path(['machines']) +const getMachineID = path => path.slice(path.lastIndexOf('/') + 1) const Machines = () => { - const { data, refetch } = useQuery(GET_INFO) const location = useLocation() + const [machine, setMachine] = useState({}) + const [config, setConfig] = useState({}) + const { data, refetch } = useQuery(GET_INFO, { + variables: { + deviceId: getMachineID(location.pathname) + }, + onCompleted: () => { + setMachine(data.machine) + setConfig(data.config) + } + }) const classes = useStyles() - const selectedMachine = - R.path(['state', 'selectedMachine'])(location) ?? - R.path(['machines', 0, 'name'])(data) ?? - '' - const machines = getMachines(data) ?? [] - const machineInfo = getMachineInfo(selectedMachine)(machines) ?? {} + useEffect(() => { + if (data) setMachine(data.machine) + }, [data]) return ( @@ -69,10 +76,10 @@ const Machines = () => { - {selectedMachine} + {machine.name} - + @@ -90,26 +97,23 @@ const Machines = () => {
{'Details'} -
+
{'Cash cassettes'}
{'Latest transactions'} - +
{'Commissions'} - +
diff --git a/new-lamassu-admin/src/pages/Maintenance/MachineDetailsCard.js b/new-lamassu-admin/src/pages/Maintenance/MachineDetailsCard.js index 75579f2a..7781470b 100644 --- a/new-lamassu-admin/src/pages/Maintenance/MachineDetailsCard.js +++ b/new-lamassu-admin/src/pages/Maintenance/MachineDetailsCard.js @@ -1,47 +1,15 @@ -import { useMutation, useLazyQuery } from '@apollo/react-hooks' 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 React from 'react' + +import MachineActions from 'src/components/machineActions/MachineActions' -import { ConfirmDialog } from 'src/components/ConfirmDialog' // import { Status } from 'src/components/Status' -import ActionButton from 'src/components/buttons/ActionButton' -import { ReactComponent as EditReversedIcon } from 'src/styling/icons/button/edit/white.svg' -import { ReactComponent as EditIcon } from 'src/styling/icons/button/edit/zodiac.svg' // 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 { labelStyles, machineDetailsStyles } from './MachineDetailsCard.styles' -const MACHINE_ACTION = gql` - mutation MachineAction( - $deviceId: ID! - $action: MachineAction! - $newName: String - ) { - machineAction(deviceId: $deviceId, action: $action, newName: $newName) { - deviceId - } - } -` - -const MACHINE = gql` - query getMachine($deviceId: ID!) { - machine(deviceId: $deviceId) { - latestEvent { - note - } - } - } -` - // const supportArtices = [ // { // // Default article for non-maped statuses @@ -53,24 +21,6 @@ const MACHINE = gql` // // TODO add Stuck and Fully Functional statuses articles for the new-admins // ] -const isStaticState = machineState => { - if (!machineState) { - return true - } - const staticStates = [ - 'chooseCoin', - 'idle', - 'pendingIdle', - 'dualIdle', - 'networkDown', - 'unpaired', - 'maintenance', - 'virgin', - 'wifiList' - ] - return staticStates.includes(machineState) -} - // const article = ({ code: status }) => // supportArtices.find(({ code: article }) => article === status) @@ -96,51 +46,9 @@ const Item = ({ children, ...props }) => ( ) -const getState = machineEventsLazy => - JSON.parse(machineEventsLazy.machine.latestEvent?.note ?? '{"state": null}') - .state - const MachineDetailsRow = ({ it: machine, onActionSuccess }) => { - const [action, setAction] = useState({ command: null }) - const [errorMessage, setErrorMessage] = useState(null) const classes = useMDStyles() - const warningMessage = ( - - A user may be in the middle of a transaction and they could lose their - funds if you continue. - - ) - - const [fetchMachineEvents, { loading: loadingEvents }] = useLazyQuery( - MACHINE, - { - variables: { - deviceId: machine.deviceId - }, - onCompleted: machineEventsLazy => { - const message = !isStaticState(getState(machineEventsLazy)) - ? warningMessage - : null - setAction(action => ({ ...action, message })) - } - } - ) - - const [machineAction, { loading }] = useMutation(MACHINE_ACTION, { - onError: ({ message }) => { - const errorMessage = message ?? 'An error ocurred' - setErrorMessage(errorMessage) - }, - onCompleted: () => { - onActionSuccess && onActionSuccess() - setAction({ command: null }) - } - }) - - const confirmDialogOpen = Boolean(action.command) - const disabled = !!(action?.command === 'restartServices' && loadingEvents) - return ( {/* @@ -180,30 +88,6 @@ const MachineDetailsRow = ({ it: machine, onActionSuccess }) => { flexItem className={classes.separator} /> */} - { - setErrorMessage(null) - machineAction({ - variables: { - deviceId: machine.deviceId, - action: `${action?.command}`, - ...(action?.command === 'rename' && { newName: value }) - } - }) - }} - onDissmised={() => { - setAction({ command: null }) - setErrorMessage(null) - }} - /> @@ -217,166 +101,11 @@ const MachineDetailsRow = ({ it: machine, onActionSuccess }) => { - -
- - setAction({ - command: 'rename', - display: 'Rename', - confirmationMessage: 'Write the new name for this machine' - }) - }> - Rename - - - setAction({ - command: 'unpair', - display: 'Unpair' - }) - }> - Unpair - - - setAction({ - command: 'reboot', - display: 'Reboot' - }) - }> - Reboot - - - setAction({ - command: 'shutdown', - display: 'Shutdown', - message: - 'In order to bring it back online, the machine will need to be visited and its power reset.' - }) - }> - Shutdown - - { - fetchMachineEvents() - setAction({ - command: 'restartServices', - display: 'Restart services for' - }) - }}> - Restart Services - -
+
- {/* - - -
- - setAction({ - command: 'rename', - display: 'Rename', - confirmationMessage: 'Write the new name for this machine' - }) - }> - Rename - - - setAction({ - command: 'unpair', - display: 'Unpair' - }) - }> - Unpair - - - setAction({ - command: 'reboot', - display: 'Reboot' - }) - }> - Reboot - - - setAction({ - command: 'shutdown', - display: 'Shutdown', - message: - 'In order to bring it back online, the machine will need to be visited and its power reset.' - }) - }> - Shutdown - - { - fetchMachineEvents() - setAction({ - command: 'restartServices', - display: 'Restart services for' - }) - }}> - Restart Services - -
-
-
*/}
)