Merge branch 'dev' into fix/coinatmradar-fix-7.5

This commit is contained in:
csrapr 2020-12-14 17:38:05 +00:00 committed by GitHub
commit bb3c9ef2ec
9 changed files with 185 additions and 131 deletions

View file

@ -3,6 +3,7 @@ const { parseAsync } = require('json2csv')
const { GraphQLDateTime } = require('graphql-iso-date') const { GraphQLDateTime } = require('graphql-iso-date')
const { GraphQLJSON, GraphQLJSONObject } = require('graphql-type-json') const { GraphQLJSON, GraphQLJSONObject } = require('graphql-type-json')
const got = require('got') const got = require('got')
const DataLoader = require('dataloader')
const machineLoader = require('../../machine-loader') const machineLoader = require('../../machine-loader')
const customers = require('../../customers') const customers = require('../../customers')
@ -265,6 +266,8 @@ const typeDefs = gql`
} }
` `
const transactionsLoader = new DataLoader(ids => transactions.getCustomerTransactionsBatch(ids))
const notify = () => got.post('http://localhost:3030/dbChange') const notify = () => got.post('http://localhost:3030/dbChange')
.catch(e => console.error('Error: lamassu-server not responding')) .catch(e => console.error('Error: lamassu-server not responding'))
@ -273,7 +276,7 @@ const resolvers = {
JSONObject: GraphQLJSONObject, JSONObject: GraphQLJSONObject,
Date: GraphQLDateTime, Date: GraphQLDateTime,
Customer: { Customer: {
transactions: parent => transactions.getCustomerTransactions(parent.id) transactions: parent => transactionsLoader.load(parent.id)
}, },
Query: { Query: {
countries: () => countries, countries: () => countries,

View file

@ -1,4 +1,5 @@
const _ = require('lodash/fp') const _ = require('lodash/fp')
const pgp = require('pg-promise')()
const db = require('../db') const db = require('../db')
const machineLoader = require('../machine-loader') const machineLoader = require('../machine-loader')
@ -65,9 +66,8 @@ function batch (from = new Date(0).toISOString(), until = new Date().toISOString
.then(packager) .then(packager)
} }
function getCustomerTransactions (customerId) { function getCustomerTransactionsBatch (ids) {
const packager = _.flow(it => { const packager = _.flow(it => {
console.log()
return it return it
}, _.flatten, _.orderBy(_.property('created'), ['desc']), _.map(camelize), addNames) }, _.flatten, _.orderBy(_.property('created'), ['desc']), _.map(camelize), addNames)
@ -82,7 +82,7 @@ function getCustomerTransactions (customerId) {
((not txs.send_confirmed) and (txs.created <= now() - interval $2)) as expired ((not txs.send_confirmed) and (txs.created <= now() - interval $2)) as expired
from cash_in_txs as txs from cash_in_txs as txs
left outer join customers c on txs.customer_id = c.id left outer join customers c on txs.customer_id = c.id
where c.id = $1 where c.id IN ($1^)
order by created desc limit $3` order by created desc limit $3`
const cashOutSql = `select 'cashOut' as tx_class, const cashOutSql = `select 'cashOut' as tx_class,
@ -100,14 +100,16 @@ function getCustomerTransactions (customerId) {
inner join cash_out_actions actions on txs.id = actions.tx_id inner join cash_out_actions actions on txs.id = actions.tx_id
and actions.action = 'provisionAddress' and actions.action = 'provisionAddress'
left outer join customers c on txs.customer_id = c.id left outer join customers c on txs.customer_id = c.id
where c.id = $1 where c.id IN ($1^)
order by created desc limit $2` order by created desc limit $2`
return Promise.all([ return Promise.all([
db.any(cashInSql, [customerId, cashInTx.PENDING_INTERVAL, NUM_RESULTS]), db.any(cashInSql, [_.map(pgp.as.text, ids).join(','), cashInTx.PENDING_INTERVAL, NUM_RESULTS]),
db.any(cashOutSql, [customerId, NUM_RESULTS, REDEEMABLE_AGE]) db.any(cashOutSql, [_.map(pgp.as.text, ids).join(','), NUM_RESULTS, REDEEMABLE_AGE])
]) ])
.then(packager) .then(packager).then(transactions => {
const transactionMap = _.groupBy('customerId', transactions)
return ids.map(id => transactionMap[id])
})
} }
function single (txId) { function single (txId) {
@ -156,4 +158,4 @@ function cancel (txId) {
.then(() => single(txId)) .then(() => single(txId))
} }
module.exports = { batch, getCustomerTransactions, single, cancel } module.exports = { batch, single, cancel, getCustomerTransactionsBatch }

View file

@ -1,4 +1,5 @@
import CssBaseline from '@material-ui/core/CssBaseline' import CssBaseline from '@material-ui/core/CssBaseline'
import Grid from '@material-ui/core/Grid'
import { import {
StylesProvider, StylesProvider,
jssPreset, jssPreset,
@ -8,12 +9,18 @@ import {
import { create } from 'jss' import { create } from 'jss'
import extendJss from 'jss-plugin-extend' import extendJss from 'jss-plugin-extend'
import React, { createContext, useContext, useState } from 'react' import React, { createContext, useContext, useState } from 'react'
import { useLocation, BrowserRouter as Router } from 'react-router-dom' import {
useLocation,
useHistory,
BrowserRouter as Router
} from 'react-router-dom'
import Sidebar from 'src/components/layout/Sidebar'
import TitleSection from 'src/components/layout/TitleSection'
import ApolloProvider from 'src/utils/apollo' import ApolloProvider from 'src/utils/apollo'
import Header from './components/layout/Header' import Header from './components/layout/Header'
import { tree, Routes } from './routing/routes' import { tree, hasSidebar, Routes, getParent } from './routing/routes'
import global from './styling/global' import global from './styling/global'
import theme from './styling/theme' import theme from './styling/theme'
import { backgroundColor, mainWidth } from './styling/variables' import { backgroundColor, mainWidth } from './styling/variables'
@ -46,6 +53,18 @@ const useStyles = makeStyles({
flex: 1, flex: 1,
display: 'flex', display: 'flex',
flexDirection flexDirection
},
grid: {
flex: 1,
height: '100%'
},
contentWithSidebar: {
flex: 1,
marginLeft: 48,
paddingTop: 15
},
contentWithoutSidebar: {
width: mainWidth
} }
}) })
@ -54,15 +73,45 @@ const AppContext = createContext()
const Main = () => { const Main = () => {
const classes = useStyles() const classes = useStyles()
const location = useLocation() const location = useLocation()
const history = useHistory()
const { wizardTested } = useContext(AppContext) const { wizardTested } = useContext(AppContext)
const route = location.pathname
const sidebar = hasSidebar(route)
const parent = sidebar ? getParent(route) : {}
const is404 = location.pathname === '/404' const is404 = location.pathname === '/404'
const isSelected = it => location.pathname === it.route
const onClick = it => history.push(it.route)
const contentClassName = sidebar
? classes.contentWithSidebar
: classes.contentWithoutSidebar
return ( return (
<div className={classes.root}> <div className={classes.root}>
{!is404 && wizardTested && <Header tree={tree} />} {!is404 && wizardTested && <Header tree={tree} />}
<main className={classes.wrapper}> <main className={classes.wrapper}>
<Routes /> {sidebar && !is404 && wizardTested && (
<TitleSection title={parent.title}></TitleSection>
)}
<Grid container className={classes.grid}>
{sidebar && !is404 && wizardTested && (
<Sidebar
data={parent.children}
isSelected={isSelected}
displayName={it => it.label}
onClick={onClick}
/>
)}
<div className={contentClassName}>
<Routes />
</div>
</Grid>
</main> </main>
</div> </div>
) )

View file

@ -3,9 +3,11 @@ import classnames from 'classnames'
import React, { memo, useState } from 'react' import React, { memo, useState } from 'react'
import { NavLink, useHistory } from 'react-router-dom' import { NavLink, useHistory } from 'react-router-dom'
import { Link } from 'src/components/buttons' import ActionButton from 'src/components/buttons/ActionButton'
import { H4 } from 'src/components/typography' import { H4 } from 'src/components/typography'
import AddMachine from 'src/pages/AddMachine' import AddMachine from 'src/pages/AddMachine'
import { ReactComponent as AddIconReverse } from 'src/styling/icons/button/add/white.svg'
import { ReactComponent as AddIcon } from 'src/styling/icons/button/add/zodiac.svg'
import { ReactComponent as Logo } from 'src/styling/icons/menu/logo.svg' import { ReactComponent as Logo } from 'src/styling/icons/menu/logo.svg'
import styles from './Header.styles' import styles from './Header.styles'
@ -76,9 +78,13 @@ const Header = memo(({ tree }) => {
</NavLink> </NavLink>
))} ))}
</ul> </ul>
<Link color="action" onClick={() => setOpen(true)}> <ActionButton
Add Machine color="secondary"
</Link> Icon={AddIcon}
InverseIcon={AddIconReverse}
onClick={() => setOpen(true)}>
Add machine
</ActionButton>
</nav> </nav>
</div> </div>
</div> </div>

View file

@ -128,19 +128,19 @@ const MachineDetailsRow = ({ it: machine, onActionSuccess }) => {
/> />
<ConfirmDialog <ConfirmDialog
open={confirmDialogOpen} open={confirmDialogOpen}
title={`${action?.command} this machine?`} title={`${action?.display} this machine?`}
errorMessage={errorMessage} errorMessage={errorMessage}
toBeConfirmed={machine.name} toBeConfirmed={machine.name}
message={action?.message} message={action?.message}
confirmationMessage={action?.confirmationMessage} confirmationMessage={action?.confirmationMessage}
saveButtonAlwaysEnabled={action?.command === 'Rename'} saveButtonAlwaysEnabled={action?.command === 'rename'}
onConfirmed={value => { onConfirmed={value => {
setErrorMessage(null) setErrorMessage(null)
machineAction({ machineAction({
variables: { variables: {
deviceId: machine.deviceId, deviceId: machine.deviceId,
action: `${action?.command}`.toLowerCase(), action: `${action?.command}`,
...(action?.command === 'Rename' && { newName: value }) ...(action?.command === 'rename' && { newName: value })
} }
}) })
}} }}
@ -174,7 +174,8 @@ const MachineDetailsRow = ({ it: machine, onActionSuccess }) => {
InverseIcon={EditReversedIcon} InverseIcon={EditReversedIcon}
onClick={() => onClick={() =>
setAction({ setAction({
command: 'Rename', command: 'rename',
display: 'Rename',
confirmationMessage: 'Write the new name for this machine' confirmationMessage: 'Write the new name for this machine'
}) })
}> }>
@ -188,7 +189,8 @@ const MachineDetailsRow = ({ it: machine, onActionSuccess }) => {
disabled={loading} disabled={loading}
onClick={() => onClick={() =>
setAction({ setAction({
command: 'Unpair' command: 'unpair',
display: 'Unpair'
}) })
}> }>
Unpair Unpair
@ -201,26 +203,42 @@ const MachineDetailsRow = ({ it: machine, onActionSuccess }) => {
disabled={loading} disabled={loading}
onClick={() => onClick={() =>
setAction({ setAction({
command: 'Reboot' command: 'reboot',
display: 'Reboot'
}) })
}> }>
Reboot Reboot
</ActionButton> </ActionButton>
<ActionButton <ActionButton
className={classes.inlineChip} className={classes.mr}
disabled={loading} disabled={loading}
color="primary" color="primary"
Icon={ShutdownIcon} Icon={ShutdownIcon}
InverseIcon={ShutdownReversedIcon} InverseIcon={ShutdownReversedIcon}
onClick={() => onClick={() =>
setAction({ setAction({
command: 'Shutdown', command: 'shutdown',
display: 'Shutdown',
message: message:
'In order to bring it back online, the machine will need to be visited and its power reset.' 'In order to bring it back online, the machine will need to be visited and its power reset.'
}) })
}> }>
Shutdown Shutdown
</ActionButton> </ActionButton>
<ActionButton
color="primary"
className={classes.inlineChip}
Icon={RebootIcon}
InverseIcon={RebootReversedIcon}
disabled={loading}
onClick={() =>
setAction({
command: 'restartServices',
display: 'Restart services for'
})
}>
Restart Services
</ActionButton>
</div> </div>
</Item> </Item>
</Container> </Container>

View file

@ -1,100 +0,0 @@
import { makeStyles } from '@material-ui/core'
import Grid from '@material-ui/core/Grid'
import React from 'react'
import {
Route,
Switch,
Redirect,
useLocation,
useHistory
} from 'react-router-dom'
import Sidebar from 'src/components/layout/Sidebar'
import TitleSection from 'src/components/layout/TitleSection'
import CoinAtmRadar from './CoinATMRadar'
import ContactInfo from './ContactInfo'
import ReceiptPrinting from './ReceiptPrinting'
import TermsConditions from './TermsConditions'
const styles = {
grid: {
flex: 1,
height: '100%'
},
content: {
flex: 1,
marginLeft: 48,
paddingTop: 15
}
}
const useStyles = makeStyles(styles)
const innerRoutes = [
{
label: 'Contact information',
route: '/settings/operator-info/contact-info',
component: ContactInfo
},
{
label: 'Receipt',
route: '/settings/operator-info/receipt-printing',
component: ReceiptPrinting
},
{
label: 'Coin ATM Radar',
route: '/settings/operator-info/coin-atm-radar',
component: CoinAtmRadar
},
{
label: 'Terms & Conditions',
route: '/settings/operator-info/terms-conditions',
component: TermsConditions
}
]
const Routes = ({ wizard }) => (
<Switch>
<Redirect
exact
from="/settings/operator-info"
to="/settings/operator-info/contact-info"
/>
<Route exact path="/" />
{innerRoutes.map(({ route, component: Page, key }) => (
<Route path={route} key={key}>
<Page name={key} wizard={wizard} />
</Route>
))}
</Switch>
)
const OperatorInfo = ({ wizard = false }) => {
const classes = useStyles()
const history = useHistory()
const location = useLocation()
const isSelected = it => location.pathname === it.route
const onClick = it => history.push(it.route)
return (
<>
<TitleSection title="Operator information"></TitleSection>
<Grid container className={classes.grid}>
<Sidebar
data={innerRoutes}
isSelected={isSelected}
displayName={it => it.label}
onClick={onClick}
/>
<div className={classes.content}>
<Routes wizard={wizard} />
</div>
</Grid>
</>
)
}
export default OperatorInfo

View file

@ -20,7 +20,10 @@ import MachineLogs from 'src/pages/MachineLogs'
import CashCassettes from 'src/pages/Maintenance/CashCassettes' import CashCassettes from 'src/pages/Maintenance/CashCassettes'
import MachineStatus from 'src/pages/Maintenance/MachineStatus' import MachineStatus from 'src/pages/Maintenance/MachineStatus'
import Notifications from 'src/pages/Notifications/Notifications' import Notifications from 'src/pages/Notifications/Notifications'
import OperatorInfo from 'src/pages/OperatorInfo/OperatorInfo' import CoinAtmRadar from 'src/pages/OperatorInfo/CoinATMRadar'
import ContactInfo from 'src/pages/OperatorInfo/ContactInfo'
import ReceiptPrinting from 'src/pages/OperatorInfo/ReceiptPrinting'
import TermsConditions from 'src/pages/OperatorInfo/TermsConditions'
import ServerLogs from 'src/pages/ServerLogs' import ServerLogs from 'src/pages/ServerLogs'
import Services from 'src/pages/Services/Services' import Services from 'src/pages/Services/Services'
import TokenManagement from 'src/pages/TokenManagement/TokenManagement' import TokenManagement from 'src/pages/TokenManagement/TokenManagement'
@ -125,7 +128,36 @@ const tree = [
key: namespaces.OPERATOR_INFO, key: namespaces.OPERATOR_INFO,
label: 'Operator Info', label: 'Operator Info',
route: '/settings/operator-info', route: '/settings/operator-info',
component: OperatorInfo title: 'Operator Information',
get component() {
return () => <Redirect to={this.children[0].route} />
},
children: [
{
key: 'contact-info',
label: 'Contact information',
route: '/settings/operator-info/contact-info',
component: ContactInfo
},
{
key: 'receipt-printing',
label: 'Receipt',
route: '/settings/operator-info/receipt-printing',
component: ReceiptPrinting
},
{
key: 'coin-atm-radar',
label: 'Coin ATM Radar',
route: '/settings/operator-info/coin-atm-radar',
component: CoinAtmRadar
},
{
key: 'terms-conditions',
label: 'Terms & Conditions',
route: '/settings/operator-info/terms-conditions',
component: TermsConditions
}
]
} }
] ]
}, },
@ -181,10 +213,34 @@ const tree = [
] ]
const map = R.map(R.when(R.has('children'), R.prop('children'))) const map = R.map(R.when(R.has('children'), R.prop('children')))
const leafRoutes = R.compose(R.flatten, map)(tree) const mappedRoutes = R.compose(R.flatten, map)(tree)
const parentRoutes = R.filter(R.has('children'))(tree) const parentRoutes = R.filter(R.has('children'))(mappedRoutes).concat(
R.filter(R.has('children'))(tree)
)
const leafRoutes = R.compose(R.flatten, map)(mappedRoutes)
const flattened = R.concat(leafRoutes, parentRoutes) const flattened = R.concat(leafRoutes, parentRoutes)
const hasSidebar = route =>
R.any(r => r.route === route)(
R.compose(
R.flatten,
R.map(R.prop('children')),
R.filter(R.has('children'))
)(mappedRoutes)
)
const getParent = route =>
R.find(
R.propEq(
'route',
R.dropLast(
1,
R.dropLastWhile(x => x !== '/', route)
)
)
)(flattened)
const Routes = () => { const Routes = () => {
const history = useHistory() const history = useHistory()
const location = useLocation() const location = useLocation()
@ -215,4 +271,4 @@ const Routes = () => {
</Switch> </Switch>
) )
} }
export { tree, Routes } export { tree, getParent, hasSidebar, Routes }

19
package-lock.json generated
View file

@ -6131,6 +6131,25 @@
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
"integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=",
"dasherize": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dasherize/-/dasherize-2.0.0.tgz",
"integrity": "sha1-bYCcnNDPe7iVLYD8hPoT1H3bEwg="
},
"data-uri-to-buffer": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz",
"integrity": "sha512-vKQ9DTQPN1FLYiiEEOQ6IBGFqvjCa5rSK3cWMy/Nespm5d/x3dGFT9UBZnkLxCwua/IXBi2TYnwTEpsOvhC4UQ=="
},
"dataloader": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.0.0.tgz",
"integrity": "sha512-YzhyDAwA4TaQIhM5go+vCLmU0UikghC/t9DTQYZR2M/UvZ1MdOhPezSDZcjj9uqQJOMqjLcpWtyW2iNINdlatQ=="
},
"date-fns": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.16.1.tgz",
"integrity": "sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ==",
"dev": true "dev": true
}, },
"expand-brackets": { "expand-brackets": {

View file

@ -23,6 +23,7 @@
"console-log-level": "^1.4.0", "console-log-level": "^1.4.0",
"cookie-parser": "^1.4.3", "cookie-parser": "^1.4.3",
"cors": "^2.8.5", "cors": "^2.8.5",
"dataloader": "^2.0.0",
"ethereumjs-tx": "^1.3.3", "ethereumjs-tx": "^1.3.3",
"ethereumjs-util": "^5.2.0", "ethereumjs-util": "^5.2.0",
"ethereumjs-wallet": "^0.6.3", "ethereumjs-wallet": "^0.6.3",