feat: create add machine page
This commit is contained in:
parent
b1b8b82260
commit
2b71c08444
7 changed files with 350 additions and 5 deletions
|
|
@ -16,6 +16,7 @@ const transactions = require('../transactions')
|
||||||
const funding = require('../funding')
|
const funding = require('../funding')
|
||||||
const supervisor = require('../supervisor')
|
const supervisor = require('../supervisor')
|
||||||
const serverLogs = require('../server-logs')
|
const serverLogs = require('../server-logs')
|
||||||
|
const pairing = require('../pairing')
|
||||||
const { accounts, coins, countries, currencies, languages } = require('../config')
|
const { accounts, coins, countries, currencies, languages } = require('../config')
|
||||||
|
|
||||||
// TODO why does server logs messages can be null?
|
// TODO why does server logs messages can be null?
|
||||||
|
|
@ -186,6 +187,7 @@ const typeDefs = gql`
|
||||||
machineSupportLogs(deviceId: ID!): SupportLogsResponse
|
machineSupportLogs(deviceId: ID!): SupportLogsResponse
|
||||||
serverSupportLogs: SupportLogsResponse
|
serverSupportLogs: SupportLogsResponse
|
||||||
saveConfig(config: JSONObject): JSONObject
|
saveConfig(config: JSONObject): JSONObject
|
||||||
|
createPairingTotem(name: String!): String
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
@ -215,6 +217,7 @@ const resolvers = {
|
||||||
Mutation: {
|
Mutation: {
|
||||||
machineAction: (...[, { deviceId, action }]) => machineAction({ deviceId, action }),
|
machineAction: (...[, { deviceId, action }]) => machineAction({ deviceId, action }),
|
||||||
machineSupportLogs: (...[, { deviceId }]) => supportLogs.insert(deviceId),
|
machineSupportLogs: (...[, { deviceId }]) => supportLogs.insert(deviceId),
|
||||||
|
createPairingTotem: (...[, { name }]) => pairing.totem(name),
|
||||||
serverSupportLogs: () => serverLogs.insert(),
|
serverSupportLogs: () => serverLogs.insert(),
|
||||||
saveConfig: (...[, { config }]) => settingsLoader.saveConfig(config)
|
saveConfig: (...[, { config }]) => settingsLoader.saveConfig(config)
|
||||||
.then(it => {
|
.then(it => {
|
||||||
|
|
|
||||||
33
lib/new-admin/pairing.js
Normal file
33
lib/new-admin/pairing.js
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
const fs = require('fs')
|
||||||
|
const pify = require('pify')
|
||||||
|
const readFile = pify(fs.readFile)
|
||||||
|
const crypto = require('crypto')
|
||||||
|
const baseX = require('base-x')
|
||||||
|
|
||||||
|
const options = require('../options')
|
||||||
|
const db = require('../db')
|
||||||
|
const pairing = require('../pairing')
|
||||||
|
|
||||||
|
const ALPHA_BASE = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'
|
||||||
|
const bsAlpha = baseX(ALPHA_BASE)
|
||||||
|
|
||||||
|
const unpair = pairing.unpair
|
||||||
|
|
||||||
|
function totem (name) {
|
||||||
|
const caPath = options.caPath
|
||||||
|
|
||||||
|
return readFile(caPath)
|
||||||
|
.then(data => {
|
||||||
|
const caHash = crypto.createHash('sha256').update(data).digest()
|
||||||
|
const token = crypto.randomBytes(32)
|
||||||
|
const hexToken = token.toString('hex')
|
||||||
|
const caHexToken = crypto.createHash('sha256').update(hexToken).digest('hex')
|
||||||
|
const buf = Buffer.concat([caHash, token, Buffer.from(options.hostname)])
|
||||||
|
const sql = 'insert into pairing_tokens (token, name) values ($1, $3), ($2, $3)'
|
||||||
|
|
||||||
|
return db.none(sql, [hexToken, caHexToken, name])
|
||||||
|
.then(() => bsAlpha.encode(buf))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { totem, unpair }
|
||||||
|
|
@ -4,6 +4,7 @@ import React, { memo, useState } from 'react'
|
||||||
import { NavLink } from 'react-router-dom'
|
import { NavLink } from 'react-router-dom'
|
||||||
|
|
||||||
import { ReactComponent as Logo } from 'src/styling/icons/menu/logo.svg'
|
import { ReactComponent as Logo } from 'src/styling/icons/menu/logo.svg'
|
||||||
|
import AddMachine from 'src/pages/AddMachine'
|
||||||
|
|
||||||
import styles from './Header.styles'
|
import styles from './Header.styles'
|
||||||
import { Link } from './buttons'
|
import { Link } from './buttons'
|
||||||
|
|
@ -11,8 +12,8 @@ import { H4 } from './typography'
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const renderSubheader = (item, classes) => {
|
const Subheader = ({ item, classes }) => {
|
||||||
if (!item || !item.children) return false
|
const [open, setOpen] = useState(false)
|
||||||
return (
|
return (
|
||||||
<div className={classes.subheader}>
|
<div className={classes.subheader}>
|
||||||
<div className={classes.content}>
|
<div className={classes.content}>
|
||||||
|
|
@ -31,7 +32,10 @@ const renderSubheader = (item, classes) => {
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<div className={classes.addMachine}>
|
<div className={classes.addMachine}>
|
||||||
<Link color="primary">Add Machine</Link>
|
<Link color="primary" onClick={() => setOpen(true)}>
|
||||||
|
Add Machine
|
||||||
|
</Link>
|
||||||
|
{open && <AddMachine close={() => setOpen(false)} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -73,7 +77,9 @@ const Header = memo(({ tree }) => {
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{renderSubheader(active, classes)}
|
{active && active.children && (
|
||||||
|
<Subheader item={active} classes={classes} />
|
||||||
|
)}
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
import Checkbox from './Checkbox'
|
import Checkbox from './Checkbox'
|
||||||
|
import TextInput from './TextInput'
|
||||||
|
|
||||||
export { Checkbox }
|
export { Checkbox, TextInput }
|
||||||
|
|
|
||||||
200
new-lamassu-admin/src/pages/AddMachine/AddMachine.js
Normal file
200
new-lamassu-admin/src/pages/AddMachine/AddMachine.js
Normal file
|
|
@ -0,0 +1,200 @@
|
||||||
|
import React, { memo, useState } from 'react'
|
||||||
|
import QRCode from 'qrcode.react'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import { Form, Formik, FastField } from 'formik'
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import { gql } from 'apollo-boost'
|
||||||
|
import { useMutation } from '@apollo/react-hooks'
|
||||||
|
import { Dialog, DialogContent, SvgIcon, IconButton } from '@material-ui/core'
|
||||||
|
|
||||||
|
import { ReactComponent as CompleteStageIconZodiac } from 'src/styling/icons/stage/zodiac/complete.svg'
|
||||||
|
import { ReactComponent as CurrentStageIconZodiac } from 'src/styling/icons/stage/zodiac/current.svg'
|
||||||
|
import { ReactComponent as EmptyStageIconZodiac } from 'src/styling/icons/stage/zodiac/empty.svg'
|
||||||
|
import { primaryColor } from 'src/styling/variables'
|
||||||
|
import Title from 'src/components/Title'
|
||||||
|
import Sidebar from 'src/components/Sidebar'
|
||||||
|
import { Info2, P } from 'src/components/typography'
|
||||||
|
import { TextInput } from 'src/components/inputs/formik'
|
||||||
|
import { Button } from 'src/components/buttons'
|
||||||
|
import { ReactComponent as WarningIcon } from 'src/styling/icons/warning-icon/comet.svg'
|
||||||
|
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg'
|
||||||
|
|
||||||
|
import styles from './styles'
|
||||||
|
|
||||||
|
const SAVE_CONFIG = gql`
|
||||||
|
mutation createPairingTotem($name: String!) {
|
||||||
|
createPairingTotem(name: $name)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const QrCodeComponent = ({ classes, qrCode, close }) => {
|
||||||
|
const [doneButton, setDoneButton] = useState(null)
|
||||||
|
setTimeout(() => setDoneButton(true), 2000)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Info2 className={classes.qrTitle}>
|
||||||
|
Scan QR code with your new cryptomat
|
||||||
|
</Info2>
|
||||||
|
<div className={classes.qrCodeWrapper}>
|
||||||
|
<div>
|
||||||
|
<QRCode size={240} fgColor={primaryColor} value={qrCode} />
|
||||||
|
</div>
|
||||||
|
<div className={classes.qrTextWrapper}>
|
||||||
|
<div className={classes.qrTextIcon}>
|
||||||
|
<WarningIcon />
|
||||||
|
</div>
|
||||||
|
<P className={classes.qrText}>
|
||||||
|
To pair the machine you need scan the QR code with your machine. To
|
||||||
|
do this either snap a picture of this QR code or download it through
|
||||||
|
the button above and scan it with the scanning bay on your machine.
|
||||||
|
</P>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{doneButton && (
|
||||||
|
<div className={classes.button}>
|
||||||
|
<Button type="submit" onClick={close}>
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
name: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const validationSchema = Yup.object().shape({
|
||||||
|
name: Yup.string()
|
||||||
|
.required()
|
||||||
|
.max(50, 'Too long')
|
||||||
|
})
|
||||||
|
|
||||||
|
const MachineNameComponent = ({ nextStep, classes, setQrCode }) => {
|
||||||
|
const [register] = useMutation(SAVE_CONFIG, {
|
||||||
|
onCompleted: data => {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.log('totem: ', data.createPairingTotem)
|
||||||
|
}
|
||||||
|
setQrCode(data.createPairingTotem)
|
||||||
|
nextStep()
|
||||||
|
},
|
||||||
|
onError: e => console.log(e)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Info2 className={classes.nameTitle}>
|
||||||
|
Machine Name (ex: Coffee shop 01)
|
||||||
|
</Info2>
|
||||||
|
<Formik
|
||||||
|
initialValues={initialValues}
|
||||||
|
validationSchema={validationSchema}
|
||||||
|
onSubmit={({ name }) => {
|
||||||
|
register({ variables: { name } })
|
||||||
|
}}>
|
||||||
|
<Form className={classes.form}>
|
||||||
|
<div>
|
||||||
|
<FastField
|
||||||
|
name="name"
|
||||||
|
label="Enter machine name"
|
||||||
|
component={TextInput}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={classes.button}>
|
||||||
|
<Button type="submit">Submit</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
label: 'Machine name',
|
||||||
|
component: MachineNameComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Scan QR code',
|
||||||
|
component: QrCodeComponent
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const renderStepper = (step, it, idx, classes) => {
|
||||||
|
const active = step === idx
|
||||||
|
const past = idx < step
|
||||||
|
const future = idx > step
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.item}>
|
||||||
|
<span
|
||||||
|
className={classnames({
|
||||||
|
[classes.itemText]: true,
|
||||||
|
[classes.itemTextActive]: active,
|
||||||
|
[classes.itemTextPast]: past
|
||||||
|
})}>
|
||||||
|
{it.label}
|
||||||
|
</span>
|
||||||
|
{active && <CurrentStageIconZodiac />}
|
||||||
|
{past && <CompleteStageIconZodiac />}
|
||||||
|
{future && <EmptyStageIconZodiac />}
|
||||||
|
{idx < steps.length - 1 && (
|
||||||
|
<div
|
||||||
|
className={classnames({
|
||||||
|
[classes.stepperPath]: true,
|
||||||
|
[classes.stepperPast]: past
|
||||||
|
})}></div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddMachine = memo(({ close }) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
const [qrCode, setQrCode] = useState(null)
|
||||||
|
const [step, setStep] = useState(0)
|
||||||
|
|
||||||
|
const Component = steps[step].component
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
fullScreen
|
||||||
|
className={classes.dialog}
|
||||||
|
open={true}
|
||||||
|
aria-labelledby="form-dialog-title">
|
||||||
|
<DialogContent className={classes.dialog}>
|
||||||
|
<div className={classes.wrapper}>
|
||||||
|
<div className={classes.headerDiv}>
|
||||||
|
<Title>Add Machine</Title>
|
||||||
|
<IconButton disableRipple={true} onClick={close}>
|
||||||
|
<SvgIcon color="error">
|
||||||
|
<CloseIcon />
|
||||||
|
</SvgIcon>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<div className={classes.contentDiv}>
|
||||||
|
<Sidebar>
|
||||||
|
{steps.map((it, idx) => renderStepper(step, it, idx, classes))}
|
||||||
|
</Sidebar>
|
||||||
|
<div className={classes.contentWrapper}>
|
||||||
|
<Component
|
||||||
|
classes={classes}
|
||||||
|
qrCode={qrCode}
|
||||||
|
close={close}
|
||||||
|
setQrCode={setQrCode}
|
||||||
|
nextStep={() => setStep(step + 1)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default AddMachine
|
||||||
3
new-lamassu-admin/src/pages/AddMachine/index.js
Normal file
3
new-lamassu-admin/src/pages/AddMachine/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import AddMachine from './AddMachine'
|
||||||
|
|
||||||
|
export default AddMachine
|
||||||
99
new-lamassu-admin/src/pages/AddMachine/styles.js
Normal file
99
new-lamassu-admin/src/pages/AddMachine/styles.js
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
import {
|
||||||
|
placeholderColor,
|
||||||
|
backgroundColor,
|
||||||
|
primaryColor,
|
||||||
|
mainWidth
|
||||||
|
} from 'src/styling/variables'
|
||||||
|
import typographyStyles from 'src/components/typography/styles'
|
||||||
|
|
||||||
|
const { tl2, p } = typographyStyles
|
||||||
|
|
||||||
|
const fill = '100%'
|
||||||
|
const flexDirection = 'column'
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
dialog: {
|
||||||
|
backgroundColor,
|
||||||
|
width: fill,
|
||||||
|
minHeight: fill,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection,
|
||||||
|
padding: 0
|
||||||
|
},
|
||||||
|
wrapper: {
|
||||||
|
width: mainWidth,
|
||||||
|
height: fill,
|
||||||
|
margin: '0 auto',
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection
|
||||||
|
},
|
||||||
|
contentDiv: {
|
||||||
|
display: 'flex',
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'row'
|
||||||
|
},
|
||||||
|
headerDiv: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center'
|
||||||
|
},
|
||||||
|
contentWrapper: {
|
||||||
|
marginLeft: 48
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
marginTop: 64
|
||||||
|
},
|
||||||
|
nameTitle: {
|
||||||
|
marginTop: 16,
|
||||||
|
marginBottom: 25
|
||||||
|
},
|
||||||
|
qrTitle: {
|
||||||
|
marginTop: 12,
|
||||||
|
marginBottom: 40
|
||||||
|
},
|
||||||
|
qrCodeWrapper: {
|
||||||
|
display: 'flex'
|
||||||
|
},
|
||||||
|
qrTextWrapper: {
|
||||||
|
width: 381,
|
||||||
|
marginLeft: 80,
|
||||||
|
display: 'flex'
|
||||||
|
},
|
||||||
|
qrTextIcon: {
|
||||||
|
marginRight: 16
|
||||||
|
},
|
||||||
|
qrText: {
|
||||||
|
marginTop: 0
|
||||||
|
},
|
||||||
|
item: {
|
||||||
|
position: 'relative',
|
||||||
|
margin: '12px 0 12px 0',
|
||||||
|
display: 'flex'
|
||||||
|
},
|
||||||
|
itemText: {
|
||||||
|
extend: p,
|
||||||
|
color: placeholderColor,
|
||||||
|
marginRight: 24
|
||||||
|
},
|
||||||
|
itemTextActive: {
|
||||||
|
extend: tl2,
|
||||||
|
color: primaryColor
|
||||||
|
},
|
||||||
|
itemTextPast: {
|
||||||
|
color: primaryColor
|
||||||
|
},
|
||||||
|
stepperPath: {
|
||||||
|
position: 'absolute',
|
||||||
|
height: 25,
|
||||||
|
width: 1,
|
||||||
|
border: [[1, 'solid', placeholderColor]],
|
||||||
|
right: 7,
|
||||||
|
top: 15
|
||||||
|
},
|
||||||
|
stepperPast: {
|
||||||
|
border: [[1, 'solid', primaryColor]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default styles
|
||||||
Loading…
Add table
Add a link
Reference in a new issue