diff --git a/lib/machine-loader.js b/lib/machine-loader.js index 24c5f5ab..5d3d23f0 100644 --- a/lib/machine-loader.js +++ b/lib/machine-loader.js @@ -9,7 +9,7 @@ const dbm = require('./postgresql_interface') const configManager = require('./new-config-manager') const settingsLoader = require('./new-settings-loader') -module.exports = {getMachineName, getMachines, getMachineNames, setMachine} +module.exports = {getMachineName, getMachines, getMachine, getMachineNames, setMachine} function getMachines () { return db.any('select * from devices where display=TRUE order by created') @@ -88,6 +88,11 @@ function getMachineName (machineId) { .then(it => it.name) } +function getMachine (machineId) { + const sql = 'select * from devices where device_id=$1' + return db.oneOrNone(sql, [machineId]).then(res => _.mapKeys(_.camelCase)(res)) +} + function renameMachine (rec) { const sql = 'update devices set name=$1 where device_id=$2' return db.none(sql, [rec.newName, rec.deviceId]) diff --git a/lib/new-admin/graphql/schema.js b/lib/new-admin/graphql/schema.js index ae4a993a..42da3110 100644 --- a/lib/new-admin/graphql/schema.js +++ b/lib/new-admin/graphql/schema.js @@ -12,6 +12,7 @@ const logs = require('../../logs') const settingsLoader = require('../../new-settings-loader') const tokenManager = require('../../token-manager') const blacklist = require('../../blacklist') +const machineEventsByIdBatch = require("../../postgresql_interface").machineEventsByIdBatch const serverVersion = require('../../../package.json').version @@ -70,6 +71,7 @@ const typeDefs = gql` cassette1: Int cassette2: Int statuses: [MachineStatus] + latestEvent: MachineEvent } type Customer { @@ -220,6 +222,16 @@ const typeDefs = gql` address: String! } + type MachineEvent { + id: ID + deviceId: String + eventType: String + note: String + created: Date + age: Float + deviceTime: Date + } + type Query { countries: [Country] currencies: [Currency] @@ -227,6 +239,7 @@ const typeDefs = gql` accountsConfig: [AccountConfig] cryptoCurrencies: [CryptoCurrency] machines: [Machine] + machine(deviceId: ID!): Machine customers: [Customer] customer(customerId: ID!): Customer machineLogs(deviceId: ID!, from: Date, until: Date, limit: Int, offset: Int): [MachineLog] @@ -267,6 +280,9 @@ const typeDefs = gql` ` const transactionsLoader = new DataLoader(ids => transactions.getCustomerTransactionsBatch(ids)) +const machineEventsLoader = new DataLoader(ids => { + return machineEventsByIdBatch(ids) +}, { cache: false }) const notify = () => got.post('http://localhost:3030/dbChange') .catch(e => console.error('Error: lamassu-server not responding')) @@ -278,6 +294,9 @@ const resolvers = { Customer: { transactions: parent => transactionsLoader.load(parent.id) }, + Machine: { + latestEvent: parent => machineEventsLoader.load(parent.deviceId) + }, Query: { countries: () => countries, currencies: () => currencies, @@ -285,6 +304,7 @@ const resolvers = { accountsConfig: () => accountsConfig, cryptoCurrencies: () => coins, machines: () => machineLoader.getMachineNames(), + machine: (...[, { deviceId }]) => machineLoader.getMachine(deviceId), customers: () => customers.getCustomersList(), customer: (...[, { customerId }]) => customers.getCustomerById(customerId), funding: () => funding.getFunding(), diff --git a/lib/postgresql_interface.js b/lib/postgresql_interface.js index aa8cc126..eb030e91 100644 --- a/lib/postgresql_interface.js +++ b/lib/postgresql_interface.js @@ -1,4 +1,6 @@ +const _ = require('lodash/fp') const db = require('./db') +const pgp = require('pg-promise')() function getInsertQuery (tableName, fields) { // outputs string like: '$1, $2, $3...' with proper No of items @@ -48,6 +50,16 @@ exports.machineEvent = function machineEvent (rec) { .then(() => db.none(deleteSql)) } +exports.machineEventsByIdBatch = function machineEventsByIdBatch (machineIds) { + const formattedIds = _.map(pgp.as.text, machineIds).join(',') + const sql = `SELECT *, (EXTRACT(EPOCH FROM (now() - created))) * 1000 AS age FROM machine_events WHERE device_id IN ($1^) ORDER BY age ASC LIMIT 1` + return db.any(sql, [formattedIds]).then(res => { + const events = _.map(_.mapKeys(_.camelCase))(res) + const eventMap = _.groupBy('deviceId', events) + return machineIds.map(id => _.prop([0], eventMap[id])) + }) +} + exports.machineEvents = function machineEvents () { const sql = 'SELECT *, (EXTRACT(EPOCH FROM (now() - created))) * 1000 AS age FROM machine_events' diff --git a/new-lamassu-admin/src/components/ConfirmDialog.js b/new-lamassu-admin/src/components/ConfirmDialog.js index 294b2bd7..bab70a09 100644 --- a/new-lamassu-admin/src/components/ConfirmDialog.js +++ b/new-lamassu-admin/src/components/ConfirmDialog.js @@ -65,6 +65,7 @@ export const ConfirmDialog = memo( onConfirmed, onDissmised, initialValue = '', + disabled = false, ...props }) => { const classes = useStyles() @@ -101,6 +102,7 @@ export const ConfirmDialog = memo( {message &&

{message}

} { + 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) @@ -69,10 +97,50 @@ const Item = ({ children, ...props }) => ( ) const MachineDetailsRow = ({ it: machine, onActionSuccess }) => { - const [action, setAction] = useState(null) + const [action, setAction] = useState({ command: null }) const [errorMessage, setErrorMessage] = useState(null) const classes = useMDStyles() + const [ + fetchMachineEvents, + { loading: loadingEvents, data: machineEventsLazy } + ] = useLazyQuery(MACHINE, { + variables: { + deviceId: machine.deviceId + } + }) + + useEffect(() => { + if (action.command === 'restartServices') { + fetchMachineEvents() + } + }, [action.command, fetchMachineEvents]) + + useEffect(() => { + if (machineEventsLazy && action.command === 'restartServices') { + const state = JSON.parse( + machineEventsLazy.machine.latestEvent?.note ?? '{"state": null}' + ).state + if (!isStaticState(state)) { + setAction(action => ({ + ...action, + message: ( + + A user may be in the middle of a transaction and they could lose + their funds if you continue. + + ) + })) + } else { + // clear message from object when state goes from not static to static + setAction(action => ({ + ...action, + message: null + })) + } + } + }, [action.command, classes.warning, machineEventsLazy]) + const [machineAction, { loading }] = useMutation(MACHINE_ACTION, { onError: ({ message }) => { const errorMessage = message ?? 'An error ocurred' @@ -80,11 +148,12 @@ const MachineDetailsRow = ({ it: machine, onActionSuccess }) => { }, onCompleted: () => { onActionSuccess && onActionSuccess() - setAction(null) + setAction({ command: null }) } }) - const confirmDialogOpen = Boolean(action) + const confirmDialogOpen = Boolean(action.command) + const disabled = !!(action?.command === 'restartServices' && loadingEvents) return ( <> @@ -127,6 +196,7 @@ const MachineDetailsRow = ({ it: machine, onActionSuccess }) => { className={classes.separator} /> { }) }} onDissmised={() => { - setAction(null) + setAction({ command: null }) setErrorMessage(null) }} /> diff --git a/new-lamassu-admin/src/pages/Maintenance/MachineDetailsCard.styles.js b/new-lamassu-admin/src/pages/Maintenance/MachineDetailsCard.styles.js index 00ac1ab8..16da4fae 100644 --- a/new-lamassu-admin/src/pages/Maintenance/MachineDetailsCard.styles.js +++ b/new-lamassu-admin/src/pages/Maintenance/MachineDetailsCard.styles.js @@ -4,7 +4,13 @@ import { detailsRowStyles, labelStyles } from 'src/pages/Transactions/Transactions.styles' -import { spacer, comet, primaryColor, fontSize4 } from 'src/styling/variables' +import { + spacer, + comet, + primaryColor, + fontSize4, + errorColor +} from 'src/styling/variables' const machineDetailsStyles = { ...detailsRowStyles, @@ -58,6 +64,9 @@ const machineDetailsStyles = { marginRight: 60, marginLeft: 'auto', background: fade(comet, 0.5) + }, + warning: { + color: errorColor } }