feat: create add machine page

This commit is contained in:
Taranto 2020-03-15 21:51:19 +00:00 committed by Josh Harvey
parent b1b8b82260
commit 2b71c08444
7 changed files with 350 additions and 5 deletions

View file

@ -16,6 +16,7 @@ const transactions = require('../transactions')
const funding = require('../funding')
const supervisor = require('../supervisor')
const serverLogs = require('../server-logs')
const pairing = require('../pairing')
const { accounts, coins, countries, currencies, languages } = require('../config')
// TODO why does server logs messages can be null?
@ -186,6 +187,7 @@ const typeDefs = gql`
machineSupportLogs(deviceId: ID!): SupportLogsResponse
serverSupportLogs: SupportLogsResponse
saveConfig(config: JSONObject): JSONObject
createPairingTotem(name: String!): String
}
`
@ -215,6 +217,7 @@ const resolvers = {
Mutation: {
machineAction: (...[, { deviceId, action }]) => machineAction({ deviceId, action }),
machineSupportLogs: (...[, { deviceId }]) => supportLogs.insert(deviceId),
createPairingTotem: (...[, { name }]) => pairing.totem(name),
serverSupportLogs: () => serverLogs.insert(),
saveConfig: (...[, { config }]) => settingsLoader.saveConfig(config)
.then(it => {

33
lib/new-admin/pairing.js Normal file
View 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 }

View file

@ -4,6 +4,7 @@ import React, { memo, useState } from 'react'
import { NavLink } from 'react-router-dom'
import { ReactComponent as Logo } from 'src/styling/icons/menu/logo.svg'
import AddMachine from 'src/pages/AddMachine'
import styles from './Header.styles'
import { Link } from './buttons'
@ -11,8 +12,8 @@ import { H4 } from './typography'
const useStyles = makeStyles(styles)
const renderSubheader = (item, classes) => {
if (!item || !item.children) return false
const Subheader = ({ item, classes }) => {
const [open, setOpen] = useState(false)
return (
<div className={classes.subheader}>
<div className={classes.content}>
@ -31,7 +32,10 @@ const renderSubheader = (item, classes) => {
</ul>
</nav>
<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>
@ -73,7 +77,9 @@ const Header = memo(({ tree }) => {
</nav>
</div>
</div>
{renderSubheader(active, classes)}
{active && active.children && (
<Subheader item={active} classes={classes} />
)}
</header>
)
})

View file

@ -1,3 +1,4 @@
import Checkbox from './Checkbox'
import TextInput from './TextInput'
export { Checkbox }
export { Checkbox, TextInput }

View 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

View file

@ -0,0 +1,3 @@
import AddMachine from './AddMachine'
export default AddMachine

View 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