fix: rework wallet screen

This commit is contained in:
Taranto 2020-04-07 19:03:18 +01:00 committed by Josh Harvey
parent 1f7ae74b42
commit 1f6d272aa0
103 changed files with 2094 additions and 3892 deletions

View file

@ -1,124 +0,0 @@
const _ = require('lodash/fp')
const db = require('../db')
const config = require('./config')
const ph = require('../plugin-helper')
const schemas = ph.loadSchemas()
function fetchAccounts () {
return db.oneOrNone('select data from user_config where type=$1', ['accounts'])
.then(row => {
// Hard code this for now
const accounts = [{
code: 'blockcypher',
display: 'Blockcypher',
fields: [
{ code: 'confidenceFactor', display: 'Confidence Factor', fieldType: 'integer', required: true, value: 40 }
]
}]
return row
? Promise.resolve(row.data.accounts)
: db.none('insert into user_config (type, data, valid) values ($1, $2, $3)', ['accounts', { accounts }, true])
.then(fetchAccounts)
})
}
function selectedAccounts () {
const mapAccount = v => v.fieldLocator.fieldType === 'account' &&
v.fieldValue.value
const mapSchema = code => schemas[code]
return config.fetchConfig()
.then(conf => {
const accountCodes = _.uniq(conf.map(mapAccount)
.filter(_.identity))
return _.sortBy(_.get('display'), accountCodes.map(mapSchema)
.filter(_.identity))
})
}
function fetchAccountSchema (account) {
return schemas[account]
}
function mergeAccount (oldAccount, newAccount) {
if (!newAccount) return oldAccount
const newFields = newAccount.fields
const updateWithData = oldField => {
const newField = _.find(r => r.code === oldField.code, newFields)
const newValue = _.isUndefined(newField) ? oldField.value : newField.value
return _.set('value', newValue, oldField)
}
const updatedFields = oldAccount.fields.map(updateWithData)
return _.set('fields', updatedFields, oldAccount)
}
function getAccounts (accountCode) {
const schema = fetchAccountSchema(accountCode)
if (!schema) return Promise.reject(new Error('No schema for: ' + accountCode))
return fetchAccounts()
.then(accounts => {
if (_.isEmpty(accounts)) return [schema]
const account = _.find(r => r.code === accountCode, accounts)
const mergedAccount = mergeAccount(schema, account)
return updateAccounts(mergedAccount, accounts)
})
}
function elideSecrets (account) {
const elideSecret = field => {
return field.fieldType === 'password'
? _.set('value', !_.isEmpty(field.value), field)
: field
}
return _.set('fields', account.fields.map(elideSecret), account)
}
function getAccount (accountCode) {
return getAccounts(accountCode)
.then(accounts => _.find(r => r.code === accountCode, accounts))
.then(elideSecrets)
}
function save (accounts) {
return db.none('update user_config set data=$1 where type=$2', [{ accounts: accounts }, 'accounts'])
}
function updateAccounts (newAccount, accounts) {
const accountCode = newAccount.code
const isPresent = _.some(_.matchesProperty('code', accountCode), accounts)
const updateAccount = r => r.code === accountCode
? newAccount
: r
return isPresent
? _.map(updateAccount, accounts)
: _.concat(accounts, newAccount)
}
function updateAccount (account) {
return getAccounts(account.code)
.then(accounts => {
const merged = mergeAccount(_.find(_.matchesProperty('code', account.code), accounts), account)
return save(updateAccounts(merged, accounts))
})
.then(() => getAccount(account.code))
.catch((err) => console.log(err))
}
module.exports = {
selectedAccounts,
getAccount,
updateAccount,
fetchAccounts
}

View file

@ -17,7 +17,7 @@ const ACCOUNT_LIST = [
{ code: 'bitstamp', display: 'Bitstamp', class: TICKER, cryptos: [BTC, ETH, LTC, BCH] }, { code: 'bitstamp', display: 'Bitstamp', class: TICKER, cryptos: [BTC, ETH, LTC, BCH] },
{ code: 'coinbase', display: 'Coinbase', class: TICKER, cryptos: [BTC, ETH, LTC, BCH] }, { code: 'coinbase', display: 'Coinbase', class: TICKER, cryptos: [BTC, ETH, LTC, BCH] },
{ code: 'itbit', display: 'itBit', class: TICKER, cryptos: [BTC] }, { code: 'itbit', display: 'itBit', class: TICKER, cryptos: [BTC] },
{ code: 'mock-ticker', display: 'Mock (Caution!)', class: TICKER, cryptos: ALL_CRYPTOS }, { code: 'mock-ticker', display: 'Mock (Caution!)', class: TICKER, cryptos: ALL_CRYPTOS, dev: true },
{ code: 'bitcoind', display: 'bitcoind', class: WALLET, cryptos: [BTC] }, { code: 'bitcoind', display: 'bitcoind', class: WALLET, cryptos: [BTC] },
{ code: 'no-layer2', display: 'No Layer 2', class: LAYER_2, cryptos: ALL_CRYPTOS }, { code: 'no-layer2', display: 'No Layer 2', class: LAYER_2, cryptos: ALL_CRYPTOS },
{ code: 'infura', display: 'Infura', class: WALLET, cryptos: [ETH] }, { code: 'infura', display: 'Infura', class: WALLET, cryptos: [ETH] },
@ -30,17 +30,17 @@ const ACCOUNT_LIST = [
{ code: 'bitstamp', display: 'Bitstamp', class: EXCHANGE, cryptos: [BTC, ETH, LTC, BCH] }, { code: 'bitstamp', display: 'Bitstamp', class: EXCHANGE, cryptos: [BTC, ETH, LTC, BCH] },
{ code: 'itbit', display: 'itBit', class: EXCHANGE, cryptos: [BTC] }, { code: 'itbit', display: 'itBit', class: EXCHANGE, cryptos: [BTC] },
{ code: 'kraken', display: 'Kraken', class: EXCHANGE, cryptos: [BTC, ETH, LTC, DASH, ZEC, BCH] }, { code: 'kraken', display: 'Kraken', class: EXCHANGE, cryptos: [BTC, ETH, LTC, DASH, ZEC, BCH] },
{ code: 'mock-wallet', display: 'Mock (Caution!)', class: WALLET, cryptos: ALL_CRYPTOS }, { code: 'mock-wallet', display: 'Mock (Caution!)', class: WALLET, cryptos: ALL_CRYPTOS, dev: true },
{ code: 'no-exchange', display: 'No exchange', class: EXCHANGE, cryptos: ALL_CRYPTOS }, { code: 'no-exchange', display: 'No exchange', class: EXCHANGE, cryptos: ALL_CRYPTOS },
{ code: 'mock-exchange', display: 'Mock exchange', class: EXCHANGE, cryptos: ALL_CRYPTOS }, { code: 'mock-exchange', display: 'Mock exchange', class: EXCHANGE, cryptos: ALL_CRYPTOS, dev: true },
{ code: 'mock-sms', display: 'Mock SMS', class: SMS }, { code: 'mock-sms', display: 'Mock SMS', class: SMS, dev: true },
{ code: 'mock-id-verify', display: 'Mock ID verifier', class: ID_VERIFIER }, { code: 'mock-id-verify', display: 'Mock ID verifier', class: ID_VERIFIER, dev: true },
{ code: 'twilio', display: 'Twilio', class: SMS }, { code: 'twilio', display: 'Twilio', class: SMS },
{ code: 'mailgun', display: 'Mailgun', class: EMAIL }, { code: 'mailgun', display: 'Mailgun', class: EMAIL },
{ code: 'all-zero-conf', display: 'Always 0-conf', class: ZERO_CONF, cryptos: [BTC, ZEC, LTC, DASH, BCH] }, { code: 'all-zero-conf', display: 'Always 0-conf', class: ZERO_CONF, cryptos: [BTC, ZEC, LTC, DASH, BCH] },
{ code: 'no-zero-conf', display: 'Always 1-conf', class: ZERO_CONF, cryptos: ALL_CRYPTOS }, { code: 'no-zero-conf', display: 'Always 1-conf', class: ZERO_CONF, cryptos: ALL_CRYPTOS },
{ code: 'blockcypher', display: 'Blockcypher', class: ZERO_CONF, cryptos: [BTC] }, { code: 'blockcypher', display: 'Blockcypher', class: ZERO_CONF, cryptos: [BTC] },
{ code: 'mock-zero-conf', display: 'Mock 0-conf', class: ZERO_CONF, cryptos: [BTC, ZEC, LTC, DASH, BCH, ETH] } { code: 'mock-zero-conf', display: 'Mock 0-conf', class: ZERO_CONF, cryptos: [BTC, ZEC, LTC, DASH, BCH, ETH], dev: true }
] ]
module.exports = { ACCOUNT_LIST } module.exports = { ACCOUNT_LIST }

View file

@ -17,7 +17,7 @@ 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 pairing = require('../pairing')
const { accounts, coins, countries, currencies, languages } = require('../config') const { accounts: accountsConfig, coins, countries, currencies, languages } = require('../config')
// TODO why does server logs messages can be null? // TODO why does server logs messages can be null?
const typeDefs = gql` const typeDefs = gql`
@ -61,7 +61,7 @@ const typeDefs = gql`
} }
type Customer { type Customer {
name: String! name: String
phone: String phone: String
totalTxs: Int totalTxs: Int
totalSpent: String totalSpent: String
@ -71,7 +71,7 @@ const typeDefs = gql`
lastTxClass: String lastTxClass: String
} }
type Account { type AccountConfig {
code: String! code: String!
display: String! display: String!
class: String! class: String!
@ -156,7 +156,7 @@ const typeDefs = gql`
countries: [Country] countries: [Country]
currencies: [Currency] currencies: [Currency]
languages: [Language] languages: [Language]
accounts: [Account] accountsConfig: [AccountConfig]
cryptoCurrencies: [CryptoCurrency] cryptoCurrencies: [CryptoCurrency]
machines: [Machine] machines: [Machine]
customers: [Customer] customers: [Customer]
@ -166,6 +166,7 @@ const typeDefs = gql`
uptime: [ProcessStatus] uptime: [ProcessStatus]
serverLogs: [ServerLog] serverLogs: [ServerLog]
transactions: [Transaction] transactions: [Transaction]
accounts: [JSONObject]
config: JSONObject config: JSONObject
} }
@ -188,6 +189,8 @@ const typeDefs = gql`
serverSupportLogs: SupportLogsResponse serverSupportLogs: SupportLogsResponse
saveConfig(config: JSONObject): JSONObject saveConfig(config: JSONObject): JSONObject
createPairingTotem(name: String!): String createPairingTotem(name: String!): String
saveAccount(account: JSONObject): [JSONObject]
saveAccounts(accounts: [JSONObject]): [JSONObject]
} }
` `
@ -202,7 +205,7 @@ const resolvers = {
countries: () => countries, countries: () => countries,
currencies: () => currencies, currencies: () => currencies,
languages: () => languages, languages: () => languages,
accounts: () => accounts, accountsConfig: () => accountsConfig,
cryptoCurrencies: () => coins, cryptoCurrencies: () => coins,
machines: () => machineLoader.getMachineNames(), machines: () => machineLoader.getMachineNames(),
customers: () => customers.getCustomersList(), customers: () => customers.getCustomersList(),
@ -212,13 +215,16 @@ const resolvers = {
uptime: () => supervisor.getAllProcessInfo(), uptime: () => supervisor.getAllProcessInfo(),
serverLogs: () => serverLogs.getServerLogs(), serverLogs: () => serverLogs.getServerLogs(),
transactions: () => transactions.batch(), transactions: () => transactions.batch(),
config: () => settingsLoader.getConfig() config: () => settingsLoader.getConfig(),
accounts: () => settingsLoader.getAccounts()
}, },
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), createPairingTotem: (...[, { name }]) => pairing.totem(name),
serverSupportLogs: () => serverLogs.insert(), serverSupportLogs: () => serverLogs.insert(),
saveAccount: (...[, { account }]) => settingsLoader.saveAccounts([account]),
saveAccounts: (...[, { accounts }]) => settingsLoader.saveAccounts(accounts),
saveConfig: (...[, { config }]) => settingsLoader.saveConfig(config) saveConfig: (...[, { config }]) => settingsLoader.saveConfig(config)
.then(it => { .then(it => {
notify() notify()

View file

@ -1,20 +1,19 @@
const machineLoader = require('../machine-loader') const machineLoader = require('../machine-loader')
const { UserInputError } = require('apollo-server-express') const { UserInputError } = require('apollo-server-express')
function getMachine(machineId) { function getMachine (machineId) {
return machineLoader.getMachines() return machineLoader.getMachines()
.then(machines => machines.find(({ deviceId }) => deviceId === machineId)) .then(machines => machines.find(({ deviceId }) => deviceId === machineId))
} }
function machineAction({ deviceId, action }) { function machineAction ({ deviceId, action }) {
return getMachine(deviceId)
return getMachine(deviceId) .then(machine => {
.then(machine => { if (!machine) throw new UserInputError(`machine:${deviceId} not found`, { deviceId })
if (!machine) throw new UserInputError(`machine:${deviceId} not found`, { deviceId }) return machine
return machine })
}) .then(machineLoader.setMachine({ deviceId, action }))
.then(machineLoader.setMachine({ deviceId, action })) .then(getMachine(deviceId))
.then(getMachine(deviceId))
} }
module.exports = { machineAction } module.exports = { machineAction }

View file

@ -9,23 +9,46 @@ low(adapter).then(it => {
db = it db = it
}) })
function saveConfig (config) { function replace (array, index, value) {
const currentState = db.getState() return array.slice(0, index).concat([value]).concat(array.slice(index + 1))
// TODO this should be _.assign }
// change after flattening of schema
const newState = _.mergeWith((objValue, srcValue) => {
if (_.isArray(objValue)) {
return srcValue
}
}, currentState, config)
function replaceOrAdd (accounts, account) {
const index = _.findIndex(['code', account.code], accounts)
return index !== -1 ? replace(accounts, index, account) : _.concat(accounts)(account)
}
function saveAccounts (accountsToSave) {
const currentState = db.getState() || {}
const accounts = currentState.accounts || []
const newAccounts = _.reduce(replaceOrAdd)(accounts)(accountsToSave)
const newState = _.set('accounts', newAccounts, currentState)
db.setState(newState) db.setState(newState)
return db.write() return db.write()
.then(() => newState) .then(() => newState.accounts)
}
function getAccounts () {
const state = db.getState()
return state ? state.accounts : null
}
function saveConfig (config) {
const currentState = db.getState() || {}
const currentConfig = currentState.config || {}
const newConfig = _.assign(currentConfig, config)
const newState = _.set('config', newConfig, currentState)
db.setState(newState)
return db.write()
.then(() => newState.config)
} }
function getConfig () { function getConfig () {
return db.getState() const state = db.getState()
return (state && state.config) || {}
} }
module.exports = { getConfig, saveConfig } module.exports = { getConfig, saveConfig, saveAccounts, getAccounts }

View file

@ -13,7 +13,7 @@ import extendJss from 'jss-plugin-extend'
import React from 'react' import React from 'react'
import { BrowserRouter as Router } from 'react-router-dom' import { BrowserRouter as Router } from 'react-router-dom'
import Header from './components/Header' import Header from './components/layout/Header'
import { tree, Routes } from './routing/routes' import { tree, Routes } from './routing/routes'
import global from './styling/global' import global from './styling/global'
import theme from './styling/theme' import theme from './styling/theme'

View file

@ -191,8 +191,8 @@ const LogsDownloaderPopover = ({
} }
const radioButtonOptions = [ const radioButtonOptions = [
{ label: 'All logs', value: radioButtonAll }, { display: 'All logs', code: radioButtonAll },
{ label: 'Date range', value: radioButtonRange } { display: 'Date range', code: radioButtonRange }
] ]
return ( return (

View file

@ -2,6 +2,8 @@ import { makeStyles, Modal as MaterialModal, Paper } from '@material-ui/core'
import classnames from 'classnames' import classnames from 'classnames'
import React from 'react' import React from 'react'
import { IconButton } from 'src/components/buttons'
import { H1 } from 'src/components/typography'
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg' import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg'
const styles = { const styles = {
@ -10,42 +12,68 @@ const styles = {
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center' alignItems: 'center'
}, },
modalContentWrapper: { wrapper: ({ width }) => ({
width,
display: 'flex', display: 'flex',
position: 'relative', flexDirection: 'column',
minHeight: 400, minHeight: 400,
maxHeight: '90vh', maxHeight: '90vh',
overflowY: 'auto', overflowY: 'auto',
borderRadius: 8, borderRadius: 8,
outline: 0, outline: 0
'& > div': { }),
width: '100%' content: {
} width: '100%',
display: 'flex',
flexDirection: 'column',
flex: 1,
padding: [[0, 32]]
}, },
closeIcon: { button: {
position: 'absolute',
width: 18,
height: 18,
padding: 0, padding: 0,
top: 20, margin: [[20, 20, 'auto', 'auto']]
right: 20 },
header: {
display: 'flex'
},
title: {
margin: [[28, 0, 8, 32]]
} }
} }
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const Modal = ({ handleClose, children, className, ...props }) => { const Modal = ({
const classes = useStyles() width,
title,
handleClose,
children,
className,
closeOnEscape,
closeOnBackdropClick,
...props
}) => {
const classes = useStyles({ width })
const innerClose = (evt, reason) => {
if (!closeOnBackdropClick && reason === 'backdropClick') return
if (!closeOnEscape && reason === 'escapeKeyDown') return
handleClose()
}
return ( return (
<MaterialModal onClose={handleClose} className={classes.modal} {...props}> <MaterialModal onClose={innerClose} className={classes.modal} {...props}>
<Paper className={classnames(classes.modalContentWrapper, className)}> <Paper className={classnames(classes.wrapper, className)}>
<button <div className={classes.header}>
className={classnames(classes.iconButton, classes.closeIcon)} {title && <H1 className={classes.title}>{title}</H1>}
onClick={() => handleClose()}> <IconButton
<CloseIcon /> size={20}
</button> className={classes.button}
{children} onClick={() => handleClose()}>
<CloseIcon />
</IconButton>
</div>
<div className={classes.content}>{children}</div>
</Paper> </Paper>
</MaterialModal> </MaterialModal>
) )

View file

@ -59,10 +59,10 @@ const styles = {
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const Stage = memo(({ stages, currentStage, color = 'spring', className }) => { const Stepper = memo(({ steps, currentStep, color = 'spring', className }) => {
if (currentStage < 1 || currentStage > stages) if (currentStep < 1 || currentStep > steps)
throw Error('Value of currentStage is invalid') throw Error('Value of currentStage is invalid')
if (stages < 1) throw Error('Value of stages is invalid') if (steps < 1) throw Error('Value of stages is invalid')
const classes = useStyles() const classes = useStyles()
@ -80,7 +80,7 @@ const Stage = memo(({ stages, currentStage, color = 'spring', className }) => {
return ( return (
<div className={classnames(className, classes.stages)}> <div className={classnames(className, classes.stages)}>
{R.range(1, currentStage).map(idx => ( {R.range(1, currentStep).map(idx => (
<div key={idx} className={classes.wrapper}> <div key={idx} className={classes.wrapper}>
{idx > 1 && <div className={classnames(separatorClasses)} />} {idx > 1 && <div className={classnames(separatorClasses)} />}
<div className={classes.stage}> <div className={classes.stage}>
@ -90,13 +90,13 @@ const Stage = memo(({ stages, currentStage, color = 'spring', className }) => {
</div> </div>
))} ))}
<div className={classes.wrapper}> <div className={classes.wrapper}>
{currentStage > 1 && <div className={classnames(separatorClasses)} />} {currentStep > 1 && <div className={classnames(separatorClasses)} />}
<div className={classes.stage}> <div className={classes.stage}>
{color === 'spring' && <CurrentStageIconSpring />} {color === 'spring' && <CurrentStageIconSpring />}
{color === 'zodiac' && <CurrentStageIconZodiac />} {color === 'zodiac' && <CurrentStageIconZodiac />}
</div> </div>
</div> </div>
{R.range(currentStage + 1, stages + 1).map(idx => ( {R.range(currentStep + 1, steps + 1).map(idx => (
<div key={idx} className={classes.wrapper}> <div key={idx} className={classes.wrapper}>
<div className={classnames(separatorEmptyClasses)} /> <div className={classnames(separatorEmptyClasses)} />
<div className={classes.stage}> <div className={classes.stage}>
@ -109,4 +109,4 @@ const Stage = memo(({ stages, currentStage, color = 'spring', className }) => {
) )
}) })
export default Stage export default Stepper

View file

@ -44,8 +44,8 @@ const BooleanPropertiesTable = memo(
} }
const radioButtonOptions = [ const radioButtonOptions = [
{ label: 'Yes', value: true }, { display: 'Yes', code: true },
{ label: 'No', value: false } { display: 'No', code: false }
] ]
if (!elements || radioGroupValues?.length === 0) return null if (!elements || radioGroupValues?.length === 0) return null

View file

@ -9,9 +9,11 @@ const useStyles = makeStyles(styles)
const ActionButton = memo(({ size = 'lg', children, className, ...props }) => { const ActionButton = memo(({ size = 'lg', children, className, ...props }) => {
const classes = useStyles({ size }) const classes = useStyles({ size })
return ( return (
<button className={classnames(classes.button, className)} {...props}> <div className={classnames(className, classes.wrapper)}>
{children} <button className={classes.button} {...props}>
</button> {children}
</button>
</div>
) )
}) })

View file

@ -1,3 +1,4 @@
import typographyStyles from 'src/components/typography/styles'
import { import {
white, white,
disabledColor, disabledColor,
@ -6,7 +7,6 @@ import {
secondaryColorDarker, secondaryColorDarker,
spacer spacer
} from 'src/styling/variables' } from 'src/styling/variables'
import typographyStyles from 'src/components/typography/styles'
const { h3 } = typographyStyles const { h3 } = typographyStyles
@ -21,6 +21,11 @@ const pickSize = size => {
} }
export default { export default {
wrapper: ({ size }) => {
const height = pickSize(size)
const shadowSize = height / 12
return { height: height + shadowSize / 2 }
},
button: ({ size }) => { button: ({ size }) => {
const height = pickSize(size) const height = pickSize(size)
const shadowSize = height / 12 const shadowSize = height / 12

View file

@ -1,8 +1,15 @@
import { makeStyles, IconButton as IconB, SvgIcon } from '@material-ui/core' import { makeStyles, IconButton as IconB } from '@material-ui/core'
import React from 'react' import React from 'react'
const styles = { const styles = {
label: ({ size }) => ({
width: size,
height: size
}),
root: { root: {
'&svg': {
viewbox: null
},
'&:hover': { '&:hover': {
backgroundColor: 'inherit' backgroundColor: 'inherit'
} }
@ -11,15 +18,16 @@ const styles = {
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const IconButton = ({ children, onClick, ...props }) => { const IconButton = ({ size, children, onClick, ...props }) => {
const classes = useStyles() const classes = useStyles({ size })
return ( return (
<IconB <IconB
{...props} {...props}
classes={{ root: classes.root }} size="small"
classes={{ root: classes.root, label: classes.label }}
disableRipple disableRipple
onClick={onClick}> onClick={onClick}>
<SvgIcon>{children}</SvgIcon> {children}
</IconB> </IconB>
) )
} }

View file

@ -1,90 +0,0 @@
import React, { memo } from 'react'
import {
AutoSizer,
List,
CellMeasurer,
CellMeasurerCache
} from 'react-virtualized'
import { THead, Th, Tr, Td } from 'src/components/fake-table/Table'
import { mainWidth } from 'src/styling/variables'
const DataTable = memo(({ elements, data }) => {
const cache = new CellMeasurerCache({
defaultHeight: 62,
fixedWidth: true
})
return (
<>
<div>
<THead>
{elements.map(
({ width, size, className, textAlign, header }, idx) => (
<Th
key={idx}
size={size}
width={width}
className={className}
textAlign={textAlign}>
{header}
</Th>
)
)}
</THead>
</div>
<div style={{ flex: '1 1 auto' }}>
<AutoSizer disableWidth>
{({ height }) => (
<List
height={height}
width={mainWidth}
rowCount={data.length}
rowHeight={cache.rowHeight}
rowRenderer={({ index, isScrolling, key, parent, style }) => (
<CellMeasurer
cache={cache}
columnIndex={0}
key={key}
parent={parent}
rowIndex={index}>
<div style={style}>
<Tr
error={data[index].error}
errorMessage={data[index].errorMessage}>
{elements.map(
(
{
size,
width,
className,
textAlign,
view = it => it?.toString()
},
idx
) => (
<Td
key={idx}
size={size}
width={width}
className={className}
textAlign={textAlign}>
{view(data[index])}
</Td>
)
)}
</Tr>
</div>
</CellMeasurer>
)}
overscanRowCount={50}
deferredMeasurementCache={cache}
/>
)}
</AutoSizer>
</div>
</>
)
})
export default DataTable

View file

@ -1,3 +0,0 @@
import DataTable from './DataTable'
export { DataTable }

View file

@ -0,0 +1,3 @@
import React from 'react'
export default React.createContext()

View file

@ -1,14 +1,21 @@
import React from 'react' import React, { useContext } from 'react'
import { Td, THead } from 'src/components/fake-table/Table' import { Td, THead } from 'src/components/fake-table/Table'
import { startCase } from 'src/utils/string' import { startCase } from 'src/utils/string'
import { ACTION_COL_SIZE, DEFAULT_COL_SIZE } from './consts' import TableCtx from './Context'
const Header = ({ elements, enableEdit, enableDelete }) => {
const actionColSize =
enableDelete && enableEdit ? ACTION_COL_SIZE / 2 : ACTION_COL_SIZE
const Header = () => {
const {
elements,
enableEdit,
editWidth,
enableDelete,
deleteWidth,
enableToggle,
toggleWidth,
DEFAULT_COL_SIZE
} = useContext(TableCtx)
return ( return (
<THead> <THead>
{elements.map( {elements.map(
@ -19,15 +26,20 @@ const Header = ({ elements, enableEdit, enableDelete }) => {
) )
)} )}
{enableEdit && ( {enableEdit && (
<Td header width={actionColSize} textAlign="right"> <Td header width={editWidth} textAlign="center">
Edit Edit
</Td> </Td>
)} )}
{enableDelete && ( {enableDelete && (
<Td header width={actionColSize} textAlign="right"> <Td header width={deleteWidth} textAlign="center">
Delete Delete
</Td> </Td>
)} )}
{enableToggle && (
<Td header width={toggleWidth} textAlign="center">
Enable
</Td>
)}
</THead> </THead>
) )
} }

View file

@ -0,0 +1,29 @@
import * as R from 'ramda'
import React from 'react'
import { fromNamespace, toNamespace } from 'src/utils/config'
import EditableTable from './Table'
const NamespacedTable = ({
name,
save,
data = {},
namespaces = [],
...props
}) => {
const innerSave = (...[, it]) => {
save(toNamespace(it.id)(R.omit(['id2'], it)))
}
const innerData = R.map(it => ({
id: it,
...fromNamespace(it)(data)
}))(namespaces)
return (
<EditableTable name={name} data={innerData} save={innerSave} {...props} />
)
}
export default NamespacedTable

View file

@ -1,39 +1,46 @@
import { makeStyles } from '@material-ui/core' import { makeStyles } from '@material-ui/core'
import classnames from 'classnames'
import { Field, useFormikContext } from 'formik' import { Field, useFormikContext } from 'formik'
import React from 'react' import * as R from 'ramda'
import React, { useContext } from 'react'
import { Link, IconButton } from 'src/components/buttons' import { Link, IconButton } from 'src/components/buttons'
import { Td, Tr } from 'src/components/fake-table/Table' import { Td, Tr } from 'src/components/fake-table/Table'
import { Switch } from 'src/components/inputs'
import { TL2 } from 'src/components/typography' import { TL2 } from 'src/components/typography'
import { ReactComponent as DisabledDeleteIcon } from 'src/styling/icons/action/delete/disabled.svg' import { ReactComponent as DisabledDeleteIcon } from 'src/styling/icons/action/delete/disabled.svg'
import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg' import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg'
import { ReactComponent as DisabledEditIcon } from 'src/styling/icons/action/edit/disabled.svg' import { ReactComponent as DisabledEditIcon } from 'src/styling/icons/action/edit/disabled.svg'
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg' import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg'
import { ReactComponent as StripesSvg } from 'src/styling/icons/stripes.svg'
import TableCtx from './Context'
import styles from './Row.styles' import styles from './Row.styles'
import { ACTION_COL_SIZE } from './consts'
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const ActionCol = ({ const ActionCol = ({ disabled, editing }) => {
editing,
setEditing,
enableEdit,
disabled,
onDelete,
enableDelete
}) => {
const classes = useStyles() const classes = useStyles()
const { values, submitForm, resetForm } = useFormikContext() const { values, submitForm, resetForm } = useFormikContext()
const {
editWidth,
onEdit,
enableEdit,
enableDelete,
disableRowEdit,
onDelete,
deleteWidth,
enableToggle,
onToggle,
toggleWidth,
actionColSize
} = useContext(TableCtx)
const actionColSize = const disableEdit = disabled || (disableRowEdit && disableRowEdit(values))
enableDelete && enableEdit ? ACTION_COL_SIZE / 2 : ACTION_COL_SIZE
return ( return (
<> <>
{editing && ( {editing && (
<Td textAlign="center" width={ACTION_COL_SIZE}> <Td textAlign="center" width={actionColSize}>
<Link <Link
className={classes.cancelButton} className={classes.cancelButton}
color="secondary" color="secondary"
@ -46,22 +53,32 @@ const ActionCol = ({
</Td> </Td>
)} )}
{!editing && enableEdit && ( {!editing && enableEdit && (
<Td textAlign="right" width={actionColSize}> <Td textAlign="center" width={editWidth}>
<IconButton <IconButton
disabled={disabled} disabled={disableEdit}
className={classes.editButton} className={classes.editButton}
onClick={() => setEditing && setEditing(values.id)}> onClick={() => onEdit && onEdit(values.id)}>
{disabled ? <DisabledEditIcon /> : <EditIcon />} {disableEdit ? <DisabledEditIcon /> : <EditIcon />}
</IconButton> </IconButton>
</Td> </Td>
)} )}
{!editing && enableDelete && ( {!editing && enableDelete && (
<Td textAlign="right" width={actionColSize}> <Td textAlign="center" width={deleteWidth}>
<IconButton disabled={disabled} onClick={() => onDelete(values.id)}> <IconButton disabled={disabled} onClick={() => onDelete(values.id)}>
{disabled ? <DisabledDeleteIcon /> : <DeleteIcon />} {disabled ? <DisabledDeleteIcon /> : <DeleteIcon />}
</IconButton> </IconButton>
</Td> </Td>
)} )}
{!editing && enableToggle && (
<Td textAlign="center" width={toggleWidth}>
<Switch
checked={!!values.active}
value={!!values.active}
disabled={disabled}
onChange={() => onToggle(values.id)}
/>
</Td>
)}
</> </>
) )
} }
@ -70,6 +87,7 @@ const ECol = ({ editing, config }) => {
const { const {
name, name,
input, input,
editable = true,
size, size,
bold, bold,
width, width,
@ -82,11 +100,6 @@ const ECol = ({ editing, config }) => {
const { values } = useFormikContext() const { values } = useFormikContext()
const classes = useStyles({ textAlign, size }) const classes = useStyles({ textAlign, size })
const viewClasses = {
[classes.bold]: bold,
[classes.size]: true
}
const iProps = { const iProps = {
fullWidth: true, fullWidth: true,
size, size,
@ -105,43 +118,58 @@ const ECol = ({ editing, config }) => {
className={{ [classes.withSuffix]: suffix }} className={{ [classes.withSuffix]: suffix }}
width={width} width={width}
size={size} size={size}
bold={bold}
textAlign={textAlign}> textAlign={textAlign}>
{editing && <Field name={name} component={input} {...iProps} />} {editing && editable ? (
{!editing && values && ( <Field name={name} component={input} {...iProps} />
<div className={classnames(viewClasses)}>{view(values[name])}</div> ) : (
values && <>{view(values[name])}</>
)} )}
{suffix && <TL2 className={classes.suffix}>{suffix}</TL2>} {suffix && <TL2 className={classes.suffix}>{suffix}</TL2>}
</Td> </Td>
) )
} }
const ERow = ({ const groupStriped = elements => {
elements, const [toStripe, noStripe] = R.partition(R.has('stripe'))(elements)
enableEdit,
enableDelete,
onDelete,
editing,
setEditing,
disabled
}) => {
const { errors } = useFormikContext()
if (!toStripe.length) {
return elements
}
const index = R.indexOf(toStripe[0], elements)
const width = R.compose(R.sum, R.map(R.path(['width'])))(toStripe)
return R.insert(
index,
{ width, editable: false, view: () => <StripesSvg /> },
noStripe
)
}
const ERow = ({ editing, disabled }) => {
const { errors } = useFormikContext()
const {
elements,
enableEdit,
enableDelete,
enableToggle,
stripeWhen
} = useContext(TableCtx)
const { values } = useFormikContext()
const shouldStripe = stripeWhen && stripeWhen(values) && !editing
const iElements = shouldStripe ? groupStriped(elements) : elements
return ( return (
<Tr <Tr
error={errors && errors.length} error={errors && errors.length}
errorMessage={errors && errors.toString()}> errorMessage={errors && errors.toString()}>
{elements.map((it, idx) => ( {iElements.map((it, idx) => {
<ECol key={idx} config={it} editing={editing} /> return <ECol key={idx} config={it} editing={editing} />
))} })}
{(enableEdit || enableDelete) && ( {(enableEdit || enableDelete || enableToggle) && (
<ActionCol <ActionCol disabled={disabled} editing={editing} />
disabled={disabled}
editing={editing}
setEditing={setEditing}
onDelete={onDelete}
enableEdit={enableEdit}
enableDelete={enableDelete}
/>
)} )}
</Tr> </Tr>
) )

View file

@ -7,12 +7,15 @@ import { v4 } from 'uuid'
import Link from 'src/components/buttons/Link.js' import Link from 'src/components/buttons/Link.js'
import { AddButton } from 'src/components/buttons/index.js' import { AddButton } from 'src/components/buttons/index.js'
import { TBody, Table } from 'src/components/fake-table/Table' import { TBody, Table } from 'src/components/fake-table/Table'
import { Info2 } from 'src/components/typography' import { Info2, TL1 } from 'src/components/typography'
import TableCtx from './Context'
import Header from './Header' import Header from './Header'
import ERow from './Row' import ERow from './Row'
import styles from './Table.styles' import styles from './Table.styles'
import { DEFAULT_COL_SIZE, ACTION_COL_SIZE } from './consts'
const ACTION_COL_SIZE = 87
const DEFAULT_COL_SIZE = 100
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
@ -24,17 +27,25 @@ const getWidth = R.compose(
const ETable = ({ const ETable = ({
name, name,
title, title,
titleLg,
elements = [], elements = [],
data = [], data = [],
save, save,
validationSchema, validationSchema,
enableCreate, enableCreate,
enableEdit,
editWidth: outerEditWidth,
enableDelete,
deleteWidth = ACTION_COL_SIZE,
enableToggle,
toggleWidth = ACTION_COL_SIZE,
onToggle,
forceDisable, forceDisable,
disableAdd, disableAdd,
enableDelete,
initialValues, initialValues,
enableEdit,
setEditing, setEditing,
stripeWhen,
disableRowEdit,
createText = 'Add override' createText = 'Add override'
}) => { }) => {
const [editingId, setEditingId] = useState(null) const [editingId, setEditingId] = useState(null)
@ -45,7 +56,7 @@ const ETable = ({
const list = index !== -1 ? R.update(index, it, data) : R.prepend(it, data) const list = index !== -1 ? R.update(index, it, data) : R.prepend(it, data)
// no response means the save failed // no response means the save failed
const response = await save({ [name]: list }) const response = await save({ [name]: list }, it)
if (!response) return if (!response) return
setAdding(false) setAdding(false)
setEditingId(null) setEditingId(null)
@ -69,83 +80,103 @@ const ETable = ({
const addField = () => setAdding(true) const addField = () => setAdding(true)
const actionSize = enableEdit || enableDelete ? ACTION_COL_SIZE : 0 const widthIfEditNull =
const width = getWidth(elements) + actionSize enableDelete || enableToggle ? ACTION_COL_SIZE : ACTION_COL_SIZE * 2
const editWidth = R.defaultTo(widthIfEditNull)(outerEditWidth)
const actionColSize =
((enableDelete && deleteWidth) ?? 0) +
((enableEdit && editWidth) ?? 0) +
((enableToggle && toggleWidth) ?? 0)
const width = getWidth(elements) + actionColSize
const classes = useStyles({ width }) const classes = useStyles({ width })
const showButtonOnEmpty = !data.length && enableCreate && !adding const showButtonOnEmpty = !data.length && enableCreate && !adding
const canAdd = !forceDisable && !editingId && !disableAdd && !adding const canAdd = !forceDisable && !editingId && !disableAdd && !adding
const showTable = adding || data.length !== 0 const showTable = adding || data.length !== 0
const ctxValue = {
elements,
enableEdit,
onEdit,
disableRowEdit,
editWidth,
enableDelete,
onDelete,
deleteWidth,
enableToggle,
onToggle,
toggleWidth,
actionColSize,
stripeWhen,
DEFAULT_COL_SIZE
}
return ( return (
<div className={classes.wrapper}> <TableCtx.Provider value={ctxValue}>
{showButtonOnEmpty && ( <div className={classes.wrapper}>
<AddButton disabled={!canAdd} onClick={addField}> {showButtonOnEmpty && (
{createText} <AddButton disabled={!canAdd} onClick={addField}>
</AddButton> {createText}
)} </AddButton>
{showTable && ( )}
<> {showTable && (
<div className={classes.outerHeader}> <>
{title && <Info2 className={classes.title}>{title}</Info2>} {(title || enableCreate) && (
{enableCreate && canAdd && ( <div className={classes.outerHeader}>
<Link className={classes.addLink} onClick={addField}> {title && titleLg && (
{createText} <TL1 className={classes.title}>{title}</TL1>
</Link> )}
{title && !titleLg && (
<Info2 className={classes.title}>{title}</Info2>
)}
{enableCreate && canAdd && (
<Link className={classes.addLink} onClick={addField}>
{createText}
</Link>
)}
</div>
)} )}
</div> <Table>
<Table> <Header />
<Header <TBody>
elements={elements} {adding && (
enableEdit={enableEdit} <Formik
enableDelete={enableDelete} initialValues={{ id: v4(), ...initialValues }}
/> onReset={onReset}
<TBody> validationSchema={validationSchema}
{adding && ( onSubmit={innerSave}>
<Formik <Form>
initialValues={{ id: v4(), ...initialValues }} <ERow editing={true} disabled={forceDisable} />
onReset={onReset} </Form>
validationSchema={validationSchema} </Formik>
onSubmit={innerSave}> )}
<Form> {data.map((it, idx) => (
<ERow <Formik
editing={true} key={it.id ?? idx}
disabled={forceDisable} enableReinitialize
enableEdit={enableEdit} initialValues={it}
enableDelete={enableDelete} onReset={onReset}
elements={elements} validationSchema={validationSchema}
/> onSubmit={innerSave}>
</Form> <Form>
</Formik> <ERow
)} editing={editingId === it.id}
{data.map((it, idx) => ( disabled={
<Formik forceDisable || (editingId && editingId !== it.id)
key={it.id ?? idx} }
enableReinitialize />
initialValues={it} </Form>
onReset={onReset} </Formik>
validationSchema={validationSchema} ))}
onSubmit={innerSave}> </TBody>
<Form> </Table>
<ERow </>
editing={editingId === it.id} )}
disabled={ </div>
forceDisable || (editingId && editingId !== it.id) </TableCtx.Provider>
}
setEditing={onEdit}
onDelete={onDelete}
enableEdit={enableEdit}
enableDelete={enableDelete}
elements={elements}
/>
</Form>
</Formik>
))}
</TBody>
</Table>
</>
)}
</div>
) )
} }

View file

@ -1,4 +0,0 @@
const ACTION_COL_SIZE = 175
const DEFAULT_COL_SIZE = 100
export { ACTION_COL_SIZE, DEFAULT_COL_SIZE }

View file

@ -1,3 +1,4 @@
import NamespacedTable from './NamespacedTable'
import Table from './Table' import Table from './Table'
export { Table } export { Table, NamespacedTable }

View file

@ -32,8 +32,7 @@ const TDoubleLevelHead = ({ children, className }) => {
} }
const TBody = ({ children, className }) => { const TBody = ({ children, className }) => {
const classes = useStyles() return <div className={classnames(className)}>{children}</div>
return <div className={classnames(className, classes.body)}>{children}</div>
} }
const Td = ({ const Td = ({
@ -42,16 +41,17 @@ const Td = ({
className, className,
width = 100, width = 100,
size, size,
bold,
textAlign, textAlign,
action action
}) => { }) => {
const classes = useStyles({ textAlign, width }) const classes = useStyles({ textAlign, width, size })
const classNames = { const classNames = {
[classes.td]: true, [classes.td]: true,
[classes.tdHeader]: header, [classes.tdHeader]: header,
[classes.actionCol]: action, [classes.actionCol]: action,
[classes.large]: size === 'lg' && !header, [classes.size]: !header,
[classes.md]: size === 'md' && !header [classes.bold]: !header && bold
} }
return <div className={classnames(className, classNames)}>{children}</div> return <div className={classnames(className, classNames)}>{children}</div>

View file

@ -1,4 +1,5 @@
import typographyStyles from 'src/components/typography/styles' import typographyStyles from 'src/components/typography/styles'
import { bySize, bold } from 'src/styling/helpers'
import { import {
tableHeaderColor, tableHeaderColor,
tableHeaderHeight, tableHeaderHeight,
@ -9,18 +10,11 @@ import {
offColor offColor
} from 'src/styling/variables' } from 'src/styling/variables'
const { tl1, info2, tl2, p, label1 } = typographyStyles const { tl2, p, label1 } = typographyStyles
export default { export default {
body: { size: ({ size }) => bySize(size),
borderSpacing: [[0, 4]] bold,
},
large: {
extend: tl1
},
md: {
extend: info2
},
header: { header: {
extend: tl2, extend: tl2,
backgroundColor: tableHeaderColor, backgroundColor: tableHeaderColor,
@ -79,7 +73,7 @@ export default {
mainContent: { mainContent: {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
minHeight: 54 minHeight: 48
}, },
// mui-overrides // mui-overrides
cardContentRoot: { cardContentRoot: {

View file

@ -1,121 +0,0 @@
import Paper from '@material-ui/core/Paper'
import Popper from '@material-ui/core/Popper'
import { withStyles } from '@material-ui/core/styles'
import Downshift from 'downshift'
import * as R from 'ramda'
import React, { memo, useState } from 'react'
import {
renderInput,
renderSuggestion,
filterSuggestions,
styles
} from './commons'
const Autocomplete = memo(
({
suggestions,
classes,
placeholder,
label,
itemToString,
code = 'code',
display = 'display',
...props
}) => {
const { name, value, onBlur } = props.field
const { touched, errors, setFieldValue } = props.form
const [popperNode, setPopperNode] = useState(null)
return (
<Downshift
id={name}
itemToString={it => {
if (itemToString) return itemToString(it)
if (it) return it[display]
return undefined
}}
onChange={it => setFieldValue(name, it)}
defaultHighlightedIndex={0}
selectedItem={value}>
{({
getInputProps,
getItemProps,
getMenuProps,
isOpen,
inputValue: inputValue2,
selectedItem: selectedItem2,
highlightedIndex,
inputValue,
toggleMenu,
clearSelection
}) => (
<div className={classes.container}>
{renderInput({
name,
fullWidth: true,
error:
(touched[`${name}-input`] || touched[name]) && errors[name],
success:
(touched[`${name}-input`] || touched[name] || value) &&
!errors[name],
InputProps: getInputProps({
value: inputValue2 || '',
placeholder,
onBlur,
onClick: event => {
setPopperNode(event.currentTarget.parentElement)
toggleMenu()
},
onChange: it => {
if (it.target.value === '') {
clearSelection()
}
inputValue = it.target.value
}
}),
label
})}
<Popper
open={isOpen}
anchorEl={popperNode}
modifiers={{ flip: { enabled: true } }}
style={{ zIndex: 9999 }}>
<div
{...(isOpen
? getMenuProps({}, { suppressRefError: true })
: {})}>
<Paper
square
style={{
minWidth: popperNode ? popperNode.clientWidth + 2 : null
}}>
{filterSuggestions(
suggestions,
inputValue2,
value ? R.of(value) : [],
code,
display
).map((suggestion, index) =>
renderSuggestion({
suggestion,
index,
itemProps: getItemProps({ item: suggestion }),
highlightedIndex,
selectedItem: selectedItem2,
code,
display
})
)}
</Paper>
</div>
</Popper>
</div>
)}
</Downshift>
)
}
)
export default withStyles(styles)(Autocomplete)

View file

@ -1,121 +0,0 @@
import Paper from '@material-ui/core/Paper'
import Popper from '@material-ui/core/Popper'
import { withStyles } from '@material-ui/core/styles'
import classnames from 'classnames'
import Downshift from 'downshift'
import * as R from 'ramda'
import React, { memo, useState } from 'react'
import {
renderInput,
renderSuggestion,
filterSuggestions,
styles
} from './commons'
const AutocompleteSelect = memo(
({
suggestions,
classes,
placeholder,
label,
itemToString,
code = 'code',
display = 'display',
name,
value,
touched,
error,
handleChange,
className,
...props
}) => {
const [popperNode, setPopperNode] = useState(null)
return (
<Downshift
id={name}
itemToString={it => {
if (itemToString) return itemToString(it)
if (it) return it[display]
return undefined
}}
onChange={handleChange}
defaultHighlightedIndex={0}
selectedItem={value}>
{({
getInputProps,
getItemProps,
getMenuProps,
isOpen,
inputValue: inputValue2,
selectedItem: selectedItem2,
highlightedIndex,
inputValue,
toggleMenu,
clearSelection
}) => (
<div className={classnames(classes.container, className)}>
{renderInput({
name,
fullWidth: true,
error: touched && error,
success: touched && !error,
InputProps: getInputProps({
value: inputValue2 || '',
placeholder,
onClick: event => {
setPopperNode(event.currentTarget.parentElement)
toggleMenu()
},
onChange: it => {
if (it.target.value === '') {
clearSelection()
}
inputValue = it.target.value
}
}),
label
})}
<Popper
open={isOpen}
anchorEl={popperNode}
modifiers={{ flip: { enabled: true } }}
style={{ zIndex: 9999 }}>
<div
{...(isOpen
? getMenuProps({}, { suppressRefError: true })
: {})}>
<Paper
square
style={{
minWidth: popperNode ? popperNode.clientWidth + 2 : null
}}>
{filterSuggestions(
suggestions,
inputValue2,
value ? R.of(value) : [],
code,
display
).map((suggestion, index) =>
renderSuggestion({
suggestion,
index,
itemProps: getItemProps({ item: suggestion }),
highlightedIndex,
selectedItem: selectedItem2,
code,
display
})
)}
</Paper>
</div>
</Popper>
</div>
)}
</Downshift>
)
}
)
export default withStyles(styles)(AutocompleteSelect)

View file

@ -1,132 +0,0 @@
import MenuItem from '@material-ui/core/MenuItem'
import { withStyles } from '@material-ui/core/styles'
import Fuse from 'fuse.js'
import React from 'react'
import slugify from 'slugify'
import {
fontColor,
inputFontSize,
inputFontWeight,
zircon
} from 'src/styling/variables'
import S from 'src/utils/sanctuary'
import { TextInput } from '../base'
function renderInput({ InputProps, error, name, success, ...props }) {
const { onChange, onBlur, value } = InputProps
return (
<TextInput
name={name}
onChange={onChange}
onBlur={onBlur}
value={value}
error={!!error}
InputProps={InputProps}
{...props}
/>
)
}
function renderSuggestion({
suggestion,
index,
itemProps,
highlightedIndex,
selectedItem,
code,
display
}) {
const isHighlighted = highlightedIndex === index
const StyledMenuItem = withStyles(theme => ({
root: {
fontSize: 14,
fontWeight: 400,
color: fontColor
},
selected: {
'&.Mui-selected, &.Mui-selected:hover': {
fontWeight: 500,
backgroundColor: zircon
}
}
}))(MenuItem)
return (
<StyledMenuItem
{...itemProps}
key={suggestion[code]}
selected={isHighlighted}
component="div">
{suggestion[display]}
</StyledMenuItem>
)
}
function filterSuggestions(
suggestions = [],
value = '',
currentValues = [],
code,
display
) {
const options = {
shouldSort: true,
threshold: 0.2,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [code, display]
}
const fuse = new Fuse(suggestions, options)
const result = value ? fuse.search(slugify(value, ' ')) : suggestions
const currentCodes = S.map(S.prop(code))(currentValues)
const filtered = S.filter(it => !S.elem(it[code])(currentCodes))(result)
const amountToTake = S.min(filtered.length)(5)
return S.compose(S.fromMaybe([]))(S.take(amountToTake))(filtered)
}
const styles = theme => ({
root: {
flexGrow: 1,
height: 250
},
container: {
flexGrow: 1,
position: 'relative'
},
paper: {
// position: 'absolute',
zIndex: 1,
marginTop: theme.spacing(1),
left: 0,
right: 0
},
inputRoot: {
fontSize: inputFontSize,
color: fontColor,
fontWeight: inputFontWeight,
flexWrap: 'wrap'
},
inputInput: {
flex: 1
},
success: {
'&:after': {
transform: 'scaleX(1)'
}
},
divider: {
height: theme.spacing(2)
}
})
export { renderInput, renderSuggestion, filterSuggestions, styles }

View file

@ -10,34 +10,45 @@ const Autocomplete = ({
limit = 5, limit = 5,
options, options,
label, label,
shouldAdd, valueProp,
getOptionSelected,
forceShowValue,
value,
onChange,
multiple, multiple,
onChange,
getLabel, getLabel,
value: outsideValue,
error, error,
fullWidth, fullWidth,
textAlign, textAlign,
size, size,
...props ...props
}) => { }) => {
let iOptions = options const mapFromValue = options => it => R.find(R.propEq(valueProp, it))(options)
const mapToValue = R.prop(valueProp)
const compare = getOptionSelected || R.equals const getValue = () => {
const find = R.find(it => compare(value, it)) if (!valueProp) return outsideValue
if (forceShowValue && !multiple && value && !find(options)) { const transform = multiple
iOptions = R.concat(options, [value]) ? R.map(mapFromValue(options))
: mapFromValue(options)
return transform(outsideValue)
}
const value = getValue()
const iOnChange = (evt, value) => {
if (!valueProp) return onChange(evt, value)
const rValue = multiple ? R.map(mapToValue)(value) : mapToValue(value)
onChange(evt, rValue)
} }
return ( return (
<MAutocomplete <MAutocomplete
options={iOptions} options={options}
multiple={multiple} multiple={multiple}
value={value} value={value}
onChange={onChange} onChange={iOnChange}
getOptionLabel={getLabel} getOptionLabel={getLabel}
forcePopupIcon={false} forcePopupIcon={false}
filterOptions={createFilterOptions({ ignoreAccents: true, limit })} filterOptions={createFilterOptions({ ignoreAccents: true, limit })}
@ -47,14 +58,14 @@ const Autocomplete = ({
ChipProps={{ onDelete: null }} ChipProps={{ onDelete: null }}
blurOnSelect blurOnSelect
clearOnEscape clearOnEscape
getOptionSelected={getOptionSelected} getOptionSelected={R.eqProps(valueProp)}
{...props} {...props}
renderInput={params => { renderInput={params => {
return ( return (
<TextInput <TextInput
{...params} {...params}
label={label} label={label}
value={value} value={outsideValue}
error={error} error={error}
size={size} size={size}
fullWidth={fullWidth} fullWidth={fullWidth}

View file

@ -1,14 +0,0 @@
import React from 'react'
function Radio({ label, ...props }) {
return (
<>
<label>
<input type="radio" className="with-gap" name="gruop1" />
<span>{label || ''}</span>
</label>
</>
)
}
export default Radio

View file

@ -1,64 +1,55 @@
import { import {
Radio as MaterialRadio, Radio,
RadioGroup as MaterialRadioGroup, RadioGroup as MRadioGroup,
FormControlLabel FormControlLabel,
makeStyles
} from '@material-ui/core' } from '@material-ui/core'
import { withStyles } from '@material-ui/styles'
import classnames from 'classnames' import classnames from 'classnames'
import React from 'react' import React from 'react'
import { secondaryColor } from '../../../styling/variables' import { Label1 } from 'src/components/typography'
import typographyStyles from '../../typography/styles'
const { p } = typographyStyles const styles = {
const GreenRadio = withStyles({
root: {
color: secondaryColor,
padding: [[9, 8, 9, 9]],
'&$checked': {
color: secondaryColor
}
},
checked: {}
})(props => <MaterialRadio color="default" {...props} />)
const Label = withStyles({
label: { label: {
extend: p height: 16,
lineHeight: '16px',
margin: [[0, 0, 4, 0]],
paddingLeft: 3
} }
})(props => <FormControlLabel {...props} />) }
const useStyles = makeStyles(styles)
/* options = [{ label, value }]
*/
const RadioGroup = ({ const RadioGroup = ({
name, name,
label,
value, value,
options, options,
ariaLabel,
onChange, onChange,
className, className,
...props labelClassName,
radioClassName
}) => { }) => {
const classes = useStyles()
return ( return (
<> <>
{options && ( {label && <Label1 className={classes.label}>{label}</Label1>}
<MaterialRadioGroup <MRadioGroup
aria-label={ariaLabel} name={name}
name={name} value={value}
value={value} onChange={onChange}
onChange={onChange} className={classnames(className)}>
className={classnames(className)}> {options.map((option, idx) => (
{options.map((option, idx) => ( <FormControlLabel
<Label key={idx}
key={idx} value={option.code}
value={option.value} control={<Radio className={radioClassName} />}
control={<GreenRadio />} label={option.display}
label={option.label} className={classnames(labelClassName)}
/> />
))} ))}
</MaterialRadioGroup> </MRadioGroup>
)}
</> </>
) )
} }

View file

@ -0,0 +1,36 @@
import React, { memo, useState } from 'react'
import { TextInput } from '../base'
const SecretInput = memo(({ value, onFocus, onBlur, ...props }) => {
const [focused, setFocused] = useState(false)
const placeholder = '⚬ ⚬ ⚬ This field is set ⚬ ⚬ ⚬'
const previouslyFilled = !!value
const tempValue = previouslyFilled ? '' : value
const iOnFocus = event => {
setFocused(true)
onFocus && onFocus(event)
}
const iOnBlur = event => {
setFocused(false)
onBlur && onBlur(event)
}
return (
<TextInput
{...props}
type="password"
onFocus={iOnFocus}
onBlur={iOnBlur}
value={value}
InputProps={{ value: !focused ? tempValue : value }}
InputLabelProps={{ shrink: previouslyFilled || focused }}
placeholder={previouslyFilled ? placeholder : ''}
/>
)
})
export default SecretInput

View file

@ -1,7 +1,8 @@
import Autocomplete from './Autocomplete' import Autocomplete from './Autocomplete'
import Checkbox from './Checkbox' import Checkbox from './Checkbox'
import RadioGroup from './RadioGroup' import RadioGroup from './RadioGroup'
import SecretInput from './SecretInput'
import Switch from './Switch' import Switch from './Switch'
import TextInput from './TextInput' import TextInput from './TextInput'
export { Checkbox, TextInput, Switch, RadioGroup, Autocomplete } export { Checkbox, TextInput, Switch, SecretInput, RadioGroup, Autocomplete }

View file

@ -1,11 +1,17 @@
import { useFormikContext } from 'formik'
import * as R from 'ramda'
import React from 'react' import React from 'react'
import { Autocomplete } from '../base' import { Autocomplete } from '../base'
const AutocompleteFormik = props => { const AutocompleteFormik = ({ options, ...props }) => {
const { name, onBlur, value } = props.field const { name, onBlur, value } = props.field
const { touched, errors, setFieldValue } = props.form const { touched, errors, setFieldValue } = props.form
const error = !!(touched[name] && errors[name]) const error = !!(touched[name] && errors[name])
const { initialValues } = useFormikContext()
const iOptions =
R.type(options) === 'Function' ? options(initialValues) : options
return ( return (
<Autocomplete <Autocomplete
@ -14,6 +20,7 @@ const AutocompleteFormik = props => {
onBlur={onBlur} onBlur={onBlur}
value={value} value={value}
error={error} error={error}
options={iOptions}
{...props} {...props}
/> />
) )

View file

@ -1,42 +1,24 @@
import React, { memo } from 'react' import React, { memo } from 'react'
import { makeStyles } from '@material-ui/core'
import { Label1 } from 'src/components/typography'
import { RadioGroup } from '../base' import { RadioGroup } from '../base'
const styles = { const RadioGroupFormik = memo(({ label, ...props }) => {
label: {
height: 16,
lineHeight: '16px',
margin: [[0, 0, 4, 0]],
paddingLeft: 3
}
}
const useStyles = makeStyles(styles)
const RadioGroupFormik = memo(({ ...props }) => {
const classes = useStyles()
const { name, onChange, value } = props.field const { name, onChange, value } = props.field
return ( return (
<> <RadioGroup
{props.label && <Label1 className={classes.label}>{props.label}</Label1>} name={name}
<RadioGroup label={label}
name={name} value={value}
value={value} options={props.options}
options={props.options} ariaLabel={name}
ariaLabel={name} onChange={e => {
onChange={e => { onChange(e)
onChange(e) props.resetError()
props.resetError() }}
}} className={props.className}
className={props.className} {...props}
{...props} />
/>
</>
) )
}) })

View file

@ -1,45 +1,22 @@
import { makeStyles } from '@material-ui/core' import React, { memo } from 'react'
import classnames from 'classnames'
import React, { memo, useState } from 'react'
import TextInputFormik from './TextInput' import { SecretInput } from '../base'
import { styles } from './TextInput.styles'
const useStyles = makeStyles(styles) const SecretInputFormik = memo(({ ...props }) => {
const { name, onChange, onBlur, value } = props.field
const { touched, errors } = props.form
const SecretInputFormik = memo(({ className, ...props }) => { const error = !!(touched[name] && errors[name])
const { value } = props.field
const classes = useStyles()
const [localTouched, setLocalTouched] = useState(false)
const handleFocus = event => {
setLocalTouched(true)
props.onFocus()
}
const spanClass = {
[classes.secretSpan]: true,
[classes.masked]: value && !localTouched,
[classes.hideSpan]: !value || localTouched
}
const inputClass = {
[classes.maskedInput]: value && !localTouched
}
return ( return (
<> <SecretInput
<span className={classnames(spanClass)} aria-hidden="true"> name={name}
This field is set onChange={onChange}
</span> onBlur={onBlur}
<TextInputFormik value={value}
{...props} error={error}
onFocus={handleFocus} {...props}
className={classnames(inputClass, className)} />
/>
</>
) )
}) })

View file

@ -1,17 +1,8 @@
import AutocompleteSelect from './autocomplete/AutocompleteSelect' import Autocomplete from './base/Autocomplete'
import Checkbox from './base/Checkbox' import Checkbox from './base/Checkbox'
import Radio from './base/Radio'
import RadioGroup from './base/RadioGroup' import RadioGroup from './base/RadioGroup'
import Select from './base/Select' import Select from './base/Select'
import Switch from './base/Switch' import Switch from './base/Switch'
import TextInput from './base/TextInput' import TextInput from './base/TextInput'
export { export { Autocomplete, TextInput, Checkbox, Switch, Select, RadioGroup }
AutocompleteSelect,
TextInput,
Radio,
Checkbox,
Switch,
Select,
RadioGroup
}

View file

@ -3,12 +3,12 @@ import classnames from 'classnames'
import React, { memo, useState } from 'react' import React, { memo, useState } from 'react'
import { NavLink } from 'react-router-dom' import { NavLink } from 'react-router-dom'
import { Link } from 'src/components/buttons'
import { H4 } from 'src/components/typography'
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 AddMachine from 'src/pages/AddMachine'
import styles from './Header.styles' import styles from './Header.styles'
import { Link } from './buttons'
import { H4 } from './typography'
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)

View file

@ -1,3 +1,4 @@
import typographyStyles from 'src/components/typography/styles'
import { import {
version, version,
mainWidth, mainWidth,
@ -9,8 +10,6 @@ import {
fontColor fontColor
} from 'src/styling/variables' } from 'src/styling/variables'
import typographyStyles from './typography/styles'
const { tl2, p } = typographyStyles const { tl2, p } = typographyStyles
let headerHeight = spacer * 7 let headerHeight = spacer * 7

View file

@ -2,7 +2,7 @@ import { makeStyles } from '@material-ui/core'
import React from 'react' import React from 'react'
import ErrorMessage from 'src/components/ErrorMessage' import ErrorMessage from 'src/components/ErrorMessage'
import { TL1 } from 'src/components/typography' import Subtitle from 'src/components/Subtitle'
import styles from './Section.styles' import styles from './Section.styles'
@ -12,10 +12,12 @@ const Section = ({ error, children, title }) => {
const classes = useStyles() const classes = useStyles()
return ( return (
<div className={classes.section}> <div className={classes.section}>
<div className={classes.sectionHeader}> {(title || error) && (
<TL1 className={classes.sectionTitle}>{title}</TL1> <div className={classes.sectionHeader}>
{error && <ErrorMessage>Failed to save changes</ErrorMessage>} <Subtitle className={classes.sectionTitle}>{title}</Subtitle>
</div> {error && <ErrorMessage>Failed to save changes</ErrorMessage>}
</div>
)}
{children} {children}
</div> </div>
) )

View file

@ -1,5 +1,3 @@
import { offColor } from 'src/styling/variables'
export default { export default {
section: { section: {
marginBottom: 72 marginBottom: 72
@ -9,7 +7,6 @@ export default {
alignItems: 'center' alignItems: 'center'
}, },
sectionTitle: { sectionTitle: {
color: offColor,
margin: [[16, 20, 23, 0]] margin: [[16, 20, 23, 0]]
} }
} }

View file

@ -1,3 +1,4 @@
import typographyStyles from 'src/components/typography/styles'
import { respondTo } from 'src/styling/helpers' import { respondTo } from 'src/styling/helpers'
import { import {
primaryColor, primaryColor,
@ -7,8 +8,6 @@ import {
xxl xxl
} from 'src/styling/variables' } from 'src/styling/variables'
import typographyStyles from './typography/styles'
const { tl2, p } = typographyStyles const { tl2, p } = typographyStyles
const sidebarColor = zircon const sidebarColor = zircon

View file

@ -1,18 +1,22 @@
import { makeStyles } from '@material-ui/core' import { makeStyles } from '@material-ui/core'
import React from 'react' import React from 'react'
import ErrorMessage from 'src/components/ErrorMessage'
import Title from 'src/components/Title' import Title from 'src/components/Title'
import styles from './TitleSection.styles' import styles from './TitleSection.styles'
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const TitleSection = ({ title }) => { const TitleSection = ({ title, error }) => {
const classes = useStyles() const classes = useStyles()
return ( return (
<div className={classes.titleWrapper}> <div className={classes.titleWrapper}>
<div className={classes.titleAndButtonsContainer}> <div className={classes.titleAndButtonsContainer}>
<Title>{title}</Title> <Title>{title}</Title>
{error && (
<ErrorMessage className={classes.error}>Failed to save</ErrorMessage>
)}
</div> </div>
</div> </div>
) )

View file

@ -8,10 +8,7 @@ export default {
titleAndButtonsContainer: { titleAndButtonsContainer: {
display: 'flex' display: 'flex'
}, },
iconButton: { error: {
border: 'none', marginLeft: 12
outline: 0,
backgroundColor: 'transparent',
cursor: 'pointer'
} }
} }

View file

@ -2,130 +2,46 @@ import { makeStyles } from '@material-ui/core'
import classnames from 'classnames' import classnames from 'classnames'
import React from 'react' import React from 'react'
import { Table, THead, TBody, Td, Th } from 'src/components/fake-table/Table' import { IconButton } from 'src/components/buttons'
import typographyStyles from 'src/components/typography/styles'
import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg'
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/white.svg'
import { ReactComponent as WarningIcon } from 'src/styling/icons/warning-icon/comet.svg'
import { import {
offColor, Table,
tableDisabledHeaderColor, THead,
tableNewDisabledHeaderColor, TBody,
secondaryColorDarker Td,
} from 'src/styling/variables' Th,
Tr
} from 'src/components/fake-table/Table'
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/white.svg'
const { label1, p } = typographyStyles import styles from './SingleRowTable.styles'
const useStyles = makeStyles(styles)
const SingleRowTable = ({ const SingleRowTable = ({
width = 380, width = 378,
height = 160, height = 128,
title, title,
items, items,
onEdit, onEdit,
disabled, className
newService,
className,
...props
}) => { }) => {
const editButtonSize = 54 const classes = useStyles({ width, height })
const styles = {
wrapper: {
width: width,
boxShadow: '0 0 4px 0 rgba(0, 0, 0, 0.08)'
},
buttonTh: {
padding: [[0, 16]]
},
disabledHeader: {
backgroundColor: tableDisabledHeaderColor,
color: offColor
},
newDisabledHeader: {
backgroundColor: tableNewDisabledHeaderColor
},
disabledBody: {
extend: p,
display: 'flex',
alignItems: 'center',
height: 104
},
itemWrapper: {
display: 'flex',
flexDirection: 'column',
marginTop: 16,
minHeight: 40,
'& > div:last-child': {}
},
disabledWrapper: {
display: 'flex',
alignItems: 'center',
'& > span:first-child': {
display: 'flex'
},
'& > span:last-child': {
paddingLeft: 16
}
},
label: {
extend: label1,
color: offColor,
marginBottom: 4
},
item: {
extend: p
},
editButton: {
border: 'none',
backgroundColor: 'transparent',
cursor: 'pointer',
display: 'flex',
padding: 0
},
spanNew: {
color: secondaryColorDarker,
marginLeft: 12
}
}
const useStyles = makeStyles(styles)
const classes = useStyles()
const headerClasses = {
[classes.disabledHeader]: disabled,
[classes.newDisabledHeader]: newService && disabled
}
const bodyClasses = {
[classes.disabledBody]: disabled
}
return ( return (
<> <>
{items && ( <Table className={classnames(className, classes.table)}>
<Table className={classnames(className, classes.wrapper)}> <THead>
<THead className={classnames(headerClasses)}> <Th className={classes.head}>
<Th width={width - editButtonSize}> {title}
{title} <IconButton onClick={onEdit} className={classes.button}>
{newService && <span className={classes.spanNew}>New</span>} <EditIcon />
</Th> </IconButton>
<Th width={editButtonSize} className={classes.buttonTh}> </Th>
{!disabled && ( </THead>
<button className={classes.editButton} onClick={onEdit}> <TBody>
<EditIcon /> <Tr className={classes.tr}>
</button>
)}
{disabled && (
<button className={classes.editButton}>
<DeleteIcon />
</button>
)}
</Th>
</THead>
<TBody className={classnames(bodyClasses)}>
<Td width={width}> <Td width={width}>
{!disabled && ( {items && (
<> <>
{items[0] && ( {items[0] && (
<div className={classes.itemWrapper}> <div className={classes.itemWrapper}>
@ -141,18 +57,10 @@ const SingleRowTable = ({
)} )}
</> </>
)} )}
{disabled && (
<div className={classes.disabledWrapper}>
<span>
<WarningIcon />
</span>
<span>This service is not being used</span>
</div>
)}
</Td> </Td>
</TBody> </Tr>
</Table> </TBody>
)} </Table>
</> </>
) )
} }

View file

@ -0,0 +1,41 @@
import typographyStyles from 'src/components/typography/styles'
import { offColor } from 'src/styling/variables'
const { label1, p } = typographyStyles
export default {
tr: ({ height }) => ({
margin: 0,
height
}),
table: ({ width }) => ({
width
}),
head: {
display: 'flex',
flex: 1,
justifyContent: 'space-between',
alignItems: 'center',
paddingRight: 12
},
button: {
marginBottom: 1
},
itemWrapper: {
display: 'flex',
flexDirection: 'column',
marginTop: 16,
minHeight: 35
},
label: {
extend: label1,
color: offColor,
marginBottom: 4
},
item: {
extend: p,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}
}

View file

@ -1,5 +1,6 @@
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import classnames from 'classnames' import classnames from 'classnames'
import * as R from 'ramda'
import React, { useState } from 'react' import React, { useState } from 'react'
import { import {
AutoSizer, AutoSizer,
@ -8,33 +9,31 @@ import {
CellMeasurerCache CellMeasurerCache
} from 'react-virtualized' } from 'react-virtualized'
import { THead, Tr, Td, Th } from 'src/components/fake-table/Table' import {
Table,
TBody,
THead,
Tr,
Td,
Th
} from 'src/components/fake-table/Table'
import { ReactComponent as ExpandClosedIcon } from 'src/styling/icons/action/expand/closed.svg' import { ReactComponent as ExpandClosedIcon } from 'src/styling/icons/action/expand/closed.svg'
import { ReactComponent as ExpandOpenIcon } from 'src/styling/icons/action/expand/open.svg' import { ReactComponent as ExpandOpenIcon } from 'src/styling/icons/action/expand/open.svg'
import { mainWidth } from 'src/styling/variables'
const styles = { import styles from './DataTable.styles'
expandButton: {
border: 'none',
backgroundColor: 'transparent',
cursor: 'pointer',
padding: 4
},
row: {
borderRadius: 0
}
}
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const ExpRow = ({ const Row = ({
id, id,
elements, elements,
data, data,
width,
Details, Details,
expanded, expanded,
expandRow, expandRow,
...props expWidth,
expandable
}) => { }) => {
const classes = useStyles() const classes = useStyles()
@ -44,34 +43,25 @@ const ExpRow = ({
className={classnames(classes.row)} className={classnames(classes.row)}
error={data.error} error={data.error}
errorMessage={data.errorMessage}> errorMessage={data.errorMessage}>
{elements {elements.map(({ view = it => it?.toString(), ...props }, idx) => (
.slice(0, -1) <Td key={idx} {...props}>
.map( {view(data)}
( </Td>
{ width, className, textAlign, view = it => it?.toString() }, ))}
idx {expandable && (
) => ( <Td width={expWidth} textAlign="center">
<Td <button
key={idx} onClick={() => expandRow(id)}
width={width} className={classes.expandButton}>
className={className} {expanded && <ExpandOpenIcon />}
textAlign={textAlign}> {!expanded && <ExpandClosedIcon />}
{view(data)} </button>
</Td> </Td>
) )}
)}
<Td width={elements[elements.length - 1].width}>
<button
onClick={() => expandRow(id)}
className={classes.expandButton}>
{expanded && <ExpandOpenIcon />}
{!expanded && <ExpandClosedIcon />}
</button>
</Td>
</Tr> </Tr>
{expanded && ( {expandable && expanded && (
<Tr className={classes.detailsRow}> <Tr className={classes.detailsRow}>
<Td width={mainWidth}> <Td width={width}>
<Details it={data} /> <Details it={data} />
</Td> </Td>
</Tr> </Tr>
@ -80,18 +70,22 @@ const ExpRow = ({
) )
} }
/* rows = [{ columns = [{ name, value, className, textAlign, width }], details, className, error, errorMessage }] const DataTable = ({
* Don't forget to include the width of the last (expand button) column!
*/
const ExpTable = ({
elements = [], elements = [],
data = [], data = [],
Details, Details,
className, className,
expandable,
...props ...props
}) => { }) => {
const [expanded, setExpanded] = useState(null) const [expanded, setExpanded] = useState(null)
const coreWidth = R.compose(R.sum, R.map(R.prop('width')))(elements)
const expWidth = 1200 - coreWidth
const width = coreWidth + (expandable ? expWidth : 0)
const classes = useStyles({ width })
const expandRow = id => { const expandRow = id => {
setExpanded(id === expanded ? null : id) setExpanded(id === expanded ? null : id)
} }
@ -101,7 +95,7 @@ const ExpTable = ({
fixedWidth: true fixedWidth: true
}) })
function rowRenderer({ index, isScrolling, key, parent, style }) { function rowRenderer({ index, key, parent, style }) {
return ( return (
<CellMeasurer <CellMeasurer
cache={cache} cache={cache}
@ -110,13 +104,16 @@ const ExpTable = ({
parent={parent} parent={parent}
rowIndex={index}> rowIndex={index}>
<div style={style}> <div style={style}>
<ExpRow <Row
width={width}
id={index} id={index}
expWidth={expWidth}
elements={elements} elements={elements}
data={data[index]} data={data[index]}
Details={Details} Details={Details}
expanded={index === expanded} expanded={index === expanded}
expandRow={expandRow} expandRow={expandRow}
expandable={expandable}
/> />
</div> </div>
</CellMeasurer> </CellMeasurer>
@ -124,27 +121,28 @@ const ExpTable = ({
} }
return ( return (
<> <Table className={classes.table}>
<div> <THead>
<THead> {elements.map(({ width, className, textAlign, header }, idx) => (
{elements.map(({ width, className, textAlign, header }, idx) => ( <Th
<Th key={idx}
key={idx} width={width}
width={width} className={className}
className={className} textAlign={textAlign}>
textAlign={textAlign}> {header}
{header} </Th>
</Th> ))}
))} {expandable && <Th width={expWidth}></Th>}
</THead> </THead>
</div> <TBody className={classes.body}>
<div style={{ flex: '1 1 auto' }}>
<AutoSizer disableWidth> <AutoSizer disableWidth>
{({ height }) => ( {({ height }) => (
<List <List
// this has to be in a style because of how the component works
style={{ overflow: 'inherit', outline: 'none' }}
{...props} {...props}
height={height} height={height}
width={mainWidth} width={width}
rowCount={data.length} rowCount={data.length}
rowHeight={cache.rowHeight} rowHeight={cache.rowHeight}
rowRenderer={rowRenderer} rowRenderer={rowRenderer}
@ -153,9 +151,9 @@ const ExpTable = ({
/> />
)} )}
</AutoSizer> </AutoSizer>
</div> </TBody>
</> </Table>
) )
} }
export default ExpTable export default DataTable

View file

@ -0,0 +1,20 @@
export default {
expandButton: {
border: 'none',
backgroundColor: 'transparent',
cursor: 'pointer',
padding: 4
},
row: {
borderRadius: 0
},
body: {
flex: [[1, 1, 'auto']]
},
table: ({ width }) => ({
width,
flex: 1,
display: 'flex',
flexDirection: 'column'
})
}

View file

@ -0,0 +1,12 @@
import React from 'react'
import { Td } from 'src/components/fake-table/Table'
import { ReactComponent as StripesSvg } from 'src/styling/icons/stripes.svg'
const Stripes = ({ width }) => (
<Td width={width}>
<StripesSvg />
</Td>
)
export default Stripes

View file

@ -7,7 +7,7 @@ import * as R from 'ramda'
import React from 'react' import React from 'react'
import Title from 'src/components/Title' import Title from 'src/components/Title'
import { DataTable } from 'src/components/dataTable' import DataTable from 'src/components/tables/DataTable'
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg' import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg' import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'

View file

@ -7,10 +7,10 @@ import moment from 'moment'
import QRCode from 'qrcode.react' import QRCode from 'qrcode.react'
import React, { useState } from 'react' import React, { useState } from 'react'
import Sidebar from 'src/components/Sidebar'
import TableLabel from 'src/components/TableLabel' import TableLabel from 'src/components/TableLabel'
import Title from 'src/components/Title' import Title from 'src/components/Title'
import { Tr, Td, THead, TBody, Table } from 'src/components/fake-table/Table' import { Tr, Td, THead, TBody, Table } from 'src/components/fake-table/Table'
import Sidebar from 'src/components/layout/Sidebar'
import { import {
H3, H3,
Info1, Info1,

View file

@ -6,7 +6,7 @@ import React from 'react'
import { Table as EditableTable } from 'src/components/editableTable' import { Table as EditableTable } from 'src/components/editableTable'
import Section from 'src/components/layout/Section' import Section from 'src/components/layout/Section'
import TitleSection from 'src/components/layout/TitleSection' import TitleSection from 'src/components/layout/TitleSection'
import { fromServer, toServer } from 'src/utils/config' import { fromNamespace, toNamespace } from 'src/utils/config'
import { import {
mainFields, mainFields,
@ -55,25 +55,27 @@ const Locales = ({ name: SCREEN_KEY }) => {
refetchQueries: () => ['getData'] refetchQueries: () => ['getData']
}) })
const config = data?.config && fromServer(SCREEN_KEY)(data.config) const config = data?.config && fromNamespace(SCREEN_KEY)(data.config)
const locale = config && !R.isEmpty(config) ? config : localeDefaults const locale = config && !R.isEmpty(config) ? config : localeDefaults
const save = it => { const save = it => {
const config = toServer(SCREEN_KEY)(it.locale[0]) const config = toNamespace(SCREEN_KEY)(it.locale[0])
return saveConfig({ variables: { config } }) return saveConfig({ variables: { config } })
} }
const saveOverrides = it => { const saveOverrides = it => {
const config = toServer(SCREEN_KEY)(it) const config = toNamespace(SCREEN_KEY)(it)
return saveConfig({ variables: { config } }) return saveConfig({ variables: { config } })
} }
return ( return (
<> <>
<TitleSection title="Locales" /> <TitleSection title="Locales" />
<Section title="Default settings"> <Section>
<EditableTable <EditableTable
title="Default settings"
titleLg
name="locale" name="locale"
enableEdit enableEdit
initialValues={locale} initialValues={locale}
@ -83,8 +85,10 @@ const Locales = ({ name: SCREEN_KEY }) => {
elements={mainFields(data)} elements={mainFields(data)}
/> />
</Section> </Section>
<Section title="Overrides"> <Section>
<EditableTable <EditableTable
title="Overrides"
titleLg
name="overrides" name="overrides"
enableDelete enableDelete
enableEdit enableEdit

View file

@ -3,77 +3,98 @@ import * as Yup from 'yup'
import Autocomplete from 'src/components/inputs/formik/Autocomplete.js' import Autocomplete from 'src/components/inputs/formik/Autocomplete.js'
const displayCodeArray = it => {
return it ? R.compose(R.join(', '), R.map(R.path(['code'])))(it) : it
}
const getFields = (getData, names) => { const getFields = (getData, names) => {
return R.filter(it => R.includes(it.name, names), allFields(getData)) return R.filter(it => R.includes(it.name, names), allFields(getData))
} }
const allFields = getData => [ const allFields = getData => {
{ const getView = (data, code, compare) => it => {
name: 'machine', if (!data) return ''
width: 200,
size: 'sm', return R.compose(
view: R.path(['name']), R.prop(code),
input: Autocomplete, R.find(R.propEq(compare ?? 'code', it))
inputProps: { )(data)
options: getData(['machines']),
limit: null,
forceShowValue: true,
getOptionSelected: R.eqProps('machineId')
}
},
{
name: 'country',
width: 200,
size: 'sm',
view: R.path(['display']),
input: Autocomplete,
inputProps: {
options: getData(['countries']),
getOptionSelected: R.eqProps('display')
}
},
{
name: 'fiatCurrency',
width: 150,
size: 'sm',
view: R.path(['code']),
input: Autocomplete,
inputProps: {
options: getData(['currencies']),
getOptionSelected: R.eqProps('display')
}
},
{
name: 'languages',
width: 240,
size: 'sm',
view: displayCodeArray,
input: Autocomplete,
inputProps: {
options: getData(['languages']),
getLabel: R.path(['code']),
getOptionSelected: R.eqProps('code'),
multiple: true
}
},
{
name: 'cryptoCurrencies',
width: 270,
size: 'sm',
view: displayCodeArray,
input: Autocomplete,
inputProps: {
options: getData(['cryptoCurrencies']),
getLabel: it => R.path(['code'])(it) ?? it,
getOptionSelected: R.eqProps('code'),
multiple: true
}
} }
]
const displayCodeArray = data => it => {
if (!it) return it
return R.compose(R.join(', '), R.map(getView(data, 'code')))(it)
}
const machineData = getData(['machines'])
const countryData = getData(['countries'])
const currencyData = getData(['currencies'])
const languageData = getData(['languages'])
const cryptoData = getData(['cryptoCurrencies'])
return [
{
name: 'machine',
width: 200,
size: 'sm',
view: getView(machineData, 'name', 'deviceId'),
input: Autocomplete,
inputProps: {
options: machineData,
valueProp: 'deviceId',
getLabel: R.path(['name']),
limit: null
}
},
{
name: 'country',
width: 200,
size: 'sm',
view: getView(countryData, 'display'),
input: Autocomplete,
inputProps: {
options: countryData,
valueProp: 'code',
getLabel: R.path(['display'])
}
},
{
name: 'fiatCurrency',
width: 150,
size: 'sm',
view: getView(currencyData, 'code'),
input: Autocomplete,
inputProps: {
options: currencyData,
valueProp: 'code',
getLabel: R.path(['code'])
}
},
{
name: 'languages',
width: 240,
size: 'sm',
view: displayCodeArray(languageData),
input: Autocomplete,
inputProps: {
options: languageData,
valueProp: 'code',
getLabel: R.path(['code']),
multiple: true
}
},
{
name: 'cryptoCurrencies',
width: 270,
size: 'sm',
view: displayCodeArray(cryptoData),
input: Autocomplete,
inputProps: {
options: cryptoData,
valueProp: 'code',
getLabel: R.path(['code']),
multiple: true
}
}
]
}
const mainFields = auxData => { const mainFields = auxData => {
const getData = R.path(R.__, auxData) const getData = R.path(R.__, auxData)
@ -98,29 +119,29 @@ const overrides = auxData => {
} }
const LocaleSchema = Yup.object().shape({ const LocaleSchema = Yup.object().shape({
country: Yup.object().required('Required'), country: Yup.string().required('Required'),
fiatCurrency: Yup.object().required('Required'), fiatCurrency: Yup.string().required('Required'),
languages: Yup.array().required('Required'), languages: Yup.array().required('Required'),
cryptoCurrencies: Yup.array().required('Required') cryptoCurrencies: Yup.array().required('Required')
}) })
const OverridesSchema = Yup.object().shape({ const OverridesSchema = Yup.object().shape({
machine: Yup.object().required('Required'), machine: Yup.string().required('Required'),
country: Yup.object().required('Required'), country: Yup.string().required('Required'),
languages: Yup.array().required('Required'), languages: Yup.array().required('Required'),
cryptoCurrencies: Yup.array().required('Required') cryptoCurrencies: Yup.array().required('Required')
}) })
const localeDefaults = { const localeDefaults = {
country: null, country: '',
fiatCurrency: null, fiatCurrency: '',
languages: [], languages: [],
cryptoCurrencies: [] cryptoCurrencies: []
} }
const overridesDefaults = { const overridesDefaults = {
machine: null, machine: '',
country: null, country: '',
languages: [], languages: [],
cryptoCurrencies: [] cryptoCurrencies: []
} }

View file

@ -6,9 +6,9 @@ import * as R from 'ramda'
import React, { useState } from 'react' import React, { useState } from 'react'
import LogsDowloaderPopover from 'src/components/LogsDownloaderPopper' import LogsDowloaderPopover from 'src/components/LogsDownloaderPopper'
import Sidebar from 'src/components/Sidebar'
import Title from 'src/components/Title' import Title from 'src/components/Title'
import { FeatureButton, SimpleButton } from 'src/components/buttons' import { FeatureButton, SimpleButton } from 'src/components/buttons'
import Sidebar from 'src/components/layout/Sidebar'
import { import {
Table, Table,
TableHead, TableHead,

View file

@ -4,7 +4,7 @@ import * as R from 'ramda'
import React, { useState } from 'react' import React, { useState } from 'react'
import TitleSection from 'src/components/layout/TitleSection' import TitleSection from 'src/components/layout/TitleSection'
import { fromServer, toServer } from 'src/utils/config' import { fromNamespace, toNamespace } from 'src/utils/config'
import Section from '../../components/layout/Section' import Section from '../../components/layout/Section'
@ -49,19 +49,19 @@ const Notifications = ({ name: SCREEN_KEY }) => {
onError: error => setError({ error }) onError: error => setError({ error })
}) })
const config = data?.config && fromServer(SCREEN_KEY)(data.config) const config = data?.config && fromNamespace(SCREEN_KEY)(data.config)
const machines = data?.machines const machines = data?.machines
const cryptoCurrencies = data?.cryptoCurrencies const cryptoCurrencies = data?.cryptoCurrencies
// TODO check path when locales is finished // TODO improve the way of fetching this
const currency = R.path(['locales_currency'])(data?.config ?? {}) const currency = R.path(['locale_fiatCurrency', 'code'])(data?.config ?? {})
const save = (section, rawConfig) => { const save = R.curry((section, rawConfig) => {
const config = toServer(SCREEN_KEY)(rawConfig) const config = toNamespace(SCREEN_KEY)(rawConfig)
setSection(section) setSection(section)
setError(null) setError(null)
return saveConfig({ variables: { config } }) return saveConfig({ variables: { config } })
} })
const setEditing = (key, state) => { const setEditing = (key, state) => {
if (!state) { if (!state) {

View file

@ -1,7 +1,7 @@
import { makeStyles } from '@material-ui/core' import { makeStyles } from '@material-ui/core'
import React from 'react' import React from 'react'
import { Link } from 'src/components/buttons' import { Link, IconButton } from 'src/components/buttons'
import { H4 } from 'src/components/typography' import { H4 } from 'src/components/typography'
import { ReactComponent as DisabledEditIcon } from 'src/styling/icons/action/edit/disabled.svg' import { ReactComponent as DisabledEditIcon } from 'src/styling/icons/action/edit/disabled.svg'
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg' import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg'
@ -17,12 +17,12 @@ const Header = ({ title, editing, disabled, setEditing }) => {
<div className={classes.header}> <div className={classes.header}>
<H4 className={classes.title}>{title}</H4> <H4 className={classes.title}>{title}</H4>
{!editing && ( {!editing && (
<button <IconButton
onClick={() => setEditing(true)} onClick={() => setEditing(true)}
className={classes.button} className={classes.button}
disabled={disabled}> disabled={disabled}>
{disabled ? <DisabledEditIcon /> : <EditIcon />} {disabled ? <DisabledEditIcon /> : <EditIcon />}
</button> </IconButton>
)} )}
{editing && ( {editing && (
<div className={classes.editingButtons}> <div className={classes.editingButtons}>

View file

@ -15,7 +15,7 @@ const NAME = 'cryptoBalanceOverrides'
const CryptoBalanceOverrides = ({ section }) => { const CryptoBalanceOverrides = ({ section }) => {
const { const {
cryptoCurrencies, cryptoCurrencies = [],
data, data,
save, save,
currency, currency,
@ -32,12 +32,17 @@ const CryptoBalanceOverrides = ({ section }) => {
return save(newOverrides) return save(newOverrides)
} }
const getSuggestions = () => { const overridenCryptos = R.map(R.prop(CRYPTOCURRENCY_KEY))(setupValues)
const overridenCryptos = R.map( const suggestionFilter = R.filter(
override => override[CRYPTOCURRENCY_KEY], it => !R.contains(it.code, overridenCryptos)
setupValues )
const suggestions = suggestionFilter(cryptoCurrencies)
const findSuggestion = it => {
const coin = R.compose(R.find(R.propEq('code', it?.cryptoCurrency)))(
cryptoCurrencies
) )
return R.without(overridenCryptos, cryptoCurrencies ?? []) return coin ? [coin] : []
} }
const initialValues = { const initialValues = {
@ -60,7 +65,11 @@ const CryptoBalanceOverrides = ({ section }) => {
.required() .required()
}) })
const suggestions = getSuggestions() const viewCrypto = it =>
R.compose(
R.path(['display']),
R.find(R.propEq('code', it))
)(cryptoCurrencies)
const elements = [ const elements = [
{ {
@ -68,13 +77,13 @@ const CryptoBalanceOverrides = ({ section }) => {
header: 'Cryptocurrency', header: 'Cryptocurrency',
width: 166, width: 166,
size: 'sm', size: 'sm',
view: R.path(['display']), view: viewCrypto,
input: Autocomplete, input: Autocomplete,
inputProps: { inputProps: {
options: suggestions, options: it => R.concat(suggestions, findSuggestion(it)),
limit: null, limit: null,
forceShowValue: true, valueProp: 'code',
getOptionSelected: R.eqProps('display') getLabel: R.path(['display'])
} }
}, },
{ {

View file

@ -14,16 +14,22 @@ const MACHINE_KEY = 'machine'
const NAME = 'fiatBalanceOverrides' const NAME = 'fiatBalanceOverrides'
const FiatBalanceOverrides = ({ section }) => { const FiatBalanceOverrides = ({ section }) => {
const { machines, data, save, isDisabled, setEditing } = useContext( const { machines = [], data, save, isDisabled, setEditing } = useContext(
NotificationsCtx NotificationsCtx
) )
const setupValues = data?.fiatBalanceOverrides ?? [] const setupValues = data?.fiatBalanceOverrides ?? []
const innerSetEditing = it => setEditing(NAME, it) const innerSetEditing = it => setEditing(NAME, it)
const getSuggestions = () => { const overridenMachines = R.map(override => override.machine, setupValues)
const overridenMachines = R.map(override => override.machine, setupValues) const suggestionFilter = R.filter(
return R.without(overridenMachines, machines ?? []) it => !R.contains(it.code, overridenMachines)
)
const suggestions = suggestionFilter(machines)
const findSuggestion = it => {
const coin = R.compose(R.find(R.propEq('deviceId', it?.machine)))(machines)
return coin ? [coin] : []
} }
const initialValues = { const initialValues = {
@ -44,8 +50,6 @@ const FiatBalanceOverrides = ({ section }) => {
.required() .required()
}) })
const suggestions = getSuggestions()
const elements = [ const elements = [
{ {
name: MACHINE_KEY, name: MACHINE_KEY,
@ -54,9 +58,8 @@ const FiatBalanceOverrides = ({ section }) => {
view: R.path(['name']), view: R.path(['name']),
input: Autocomplete, input: Autocomplete,
inputProps: { inputProps: {
options: suggestions, options: it => R.concat(suggestions, findSuggestion(it)),
limit: null, limit: null,
forceShowValue: true,
getOptionSelected: R.eqProps('display') getOptionSelected: R.eqProps('display')
} }
}, },

View file

@ -11,6 +11,7 @@ import {
Th Th
} from 'src/components/fake-table/Table' } from 'src/components/fake-table/Table'
import { Switch } from 'src/components/inputs' import { Switch } from 'src/components/inputs'
import { fromNamespace, toNamespace } from 'src/utils/config'
import { startCase } from 'src/utils/string' import { startCase } from 'src/utils/string'
import NotificationsCtx from '../NotificationsContext' import NotificationsCtx from '../NotificationsContext'
@ -26,12 +27,15 @@ const sizes = {
const width = R.sum(R.values(sizes)) + channelSize const width = R.sum(R.values(sizes)) + channelSize
const Row = ({ namespace }) => { const Row = ({ namespace }) => {
const { data, save } = useContext(NotificationsCtx) const { data: rawData, save: rawSave } = useContext(NotificationsCtx)
const disabled = !data || !data[`${namespace}_active`]
const save = R.compose(rawSave(null), toNamespace(namespace))
const data = fromNamespace(namespace)(rawData)
const disabled = !data || !data.active
const Cell = ({ name, disabled }) => { const Cell = ({ name, disabled }) => {
const namespaced = `${namespace}_${name}` const value = !!(data && data[name])
const value = !!(data && data[namespaced])
return ( return (
<Td width={sizes[name]} textAlign="center"> <Td width={sizes[name]} textAlign="center">
@ -39,7 +43,7 @@ const Row = ({ namespace }) => {
disabled={disabled} disabled={disabled}
checked={value} checked={value}
onChange={event => { onChange={event => {
save(null, { [namespaced]: event.target.checked }) save({ [name]: event.target.checked })
}} }}
value={value} value={value}
/> />

View file

@ -1,13 +1,13 @@
import { makeStyles } from '@material-ui/core/styles'
import React, { useState, memo } from 'react'
import { useQuery, useMutation } from '@apollo/react-hooks' import { useQuery, useMutation } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core/styles'
import { gql } from 'apollo-boost' import { gql } from 'apollo-boost'
import React, { useState, memo } from 'react'
import { BooleanPropertiesTable } from 'src/components/booleanPropertiesTable'
import { H4, P, Label2 } from 'src/components/typography'
import { Button } from 'src/components/buttons'
import Popper from 'src/components/Popper' import Popper from 'src/components/Popper'
import { BooleanPropertiesTable } from 'src/components/booleanPropertiesTable'
import { Button } from 'src/components/buttons'
import { Switch } from 'src/components/inputs' import { Switch } from 'src/components/inputs'
import { H4, P, Label2 } from 'src/components/typography'
import { ReactComponent as HelpIcon } from 'src/styling/icons/action/help/zodiac.svg' import { ReactComponent as HelpIcon } from 'src/styling/icons/action/help/zodiac.svg'
import { mainStyles } from './CoinATMRadar.styles' import { mainStyles } from './CoinATMRadar.styles'

View file

@ -2,8 +2,8 @@ import { makeStyles } from '@material-ui/core'
import * as R from 'ramda' import * as R from 'ramda'
import React, { useState } from 'react' import React, { useState } from 'react'
import Sidebar from 'src/components/Sidebar'
import Title from 'src/components/Title' import Title from 'src/components/Title'
import Sidebar from 'src/components/layout/Sidebar'
import logsStyles from '../Logs.styles' import logsStyles from '../Logs.styles'

View file

@ -1,257 +0,0 @@
import React, { memo } from 'react'
import * as Yup from 'yup'
import TextInputFormik from 'src/components/inputs/formik/TextInput'
import SecretInputFormik from 'src/components/inputs/formik/SecretInput'
import { Card, getValue as getValueAux, formatLong } from './aux'
import EditService from './EditService'
const schema = {
token: {
code: 'token',
display: 'API Token'
},
btcWalletId: {
code: 'BTCWalletId',
display: 'BTC Wallet ID'
},
btcWalletPassphrase: {
code: 'BTCWalletPassphrase',
display: 'BTC Wallet Passphrase'
},
ltcWalletId: {
code: 'LTCWalletId',
display: 'LTC Wallet ID'
},
ltcWalletPassphrase: {
code: 'LTCWalletPassphrase',
display: 'LTC Wallet Passphrase'
},
zecWalletId: {
code: 'ZECWalletId',
display: 'ZEC Wallet ID'
},
zecWalletPassphrase: {
code: 'ZECWalletPassphrase',
display: 'ZEC Wallet Passphrase'
},
bchWalletId: {
code: 'BCHWalletId',
display: 'BCH Wallet ID'
},
bchWalletPassphrase: {
code: 'BCHWalletPassphrase',
display: 'BCH Wallet Passphrase'
},
dashWalletId: {
code: 'DASHWalletId',
display: 'DASH Wallet ID'
},
dashWalletPassphrase: {
code: 'DASHWalletPassphrase',
display: 'DASH Wallet Passphrase'
},
environment: {
code: 'environment',
display: 'Environment'
}
}
const BitgoCard = memo(({ account, onEdit, ...props }) => {
const getValue = getValueAux(account)
const token = schema.token
const tokenValue = getValue(token.code)
const items = [
{
label: token.display,
value: formatLong(tokenValue)
}
]
return (
<Card
account={account}
title="BitGo (Wallet)"
items={items}
onEdit={onEdit}
/>
)
})
const getBitgoFormik = account => {
const getValue = getValueAux(account)
const token = getValue(schema.token.code)
const btcWalletId = getValue(schema.btcWalletId.code)
const btcWalletPassphrase = getValue(schema.btcWalletPassphrase.code)
const ltcWalletId = getValue(schema.ltcWalletId.code)
const ltcWalletPassphrase = getValue(schema.ltcWalletPassphrase.code)
const zecWalletId = getValue(schema.zecWalletId.code)
const zecWalletPassphrase = getValue(schema.zecWalletPassphrase.code)
const bchWalletId = getValue(schema.bchWalletId.code)
const bchWalletPassphrase = getValue(schema.bchWalletPassphrase.code)
const dashWalletId = getValue(schema.dashWalletId.code)
const dashWalletPassphrase = getValue(schema.dashWalletPassphrase.code)
const environment = getValue(schema.environment.code)
return {
initialValues: {
token: token,
BTCWalletId: btcWalletId,
BTCWalletPassphrase: btcWalletPassphrase,
LTCWalletId: ltcWalletId,
LTCWalletPassphrase: ltcWalletPassphrase,
ZECWalletId: zecWalletId,
ZECWalletPassphrase: zecWalletPassphrase,
BCHWalletId: bchWalletId,
BCHWalletPassphrase: bchWalletPassphrase,
DASHWalletId: dashWalletId,
DASHWalletPassphrase: dashWalletPassphrase,
environment: environment
},
validationSchema: Yup.object().shape({
token: Yup.string()
.max(100, 'Too long')
.required('Required'),
btcWalletId: Yup.string().max(100, 'Too long'),
btcWalletPassphrase: Yup.string().max(100, 'Too long'),
ltcWalletId: Yup.string().max(100, 'Too long'),
ltcWalletPassphrase: Yup.string().max(100, 'Too long'),
zecWalletId: Yup.string().max(100, 'Too long'),
zecWalletPassphrase: Yup.string().max(100, 'Too long'),
bchWalletId: Yup.string().max(100, 'Too long'),
bchWalletPassphrase: Yup.string().max(100, 'Too long'),
dashWalletId: Yup.string().max(100, 'Too long'),
dashWalletPassphrase: Yup.string().max(100, 'Too long'),
environment: Yup.string()
.matches(/(prod|test)/)
.required('Required')
}),
validate: values => {
const errors = {}
if (values.btcWalletId && !values.btcWalletPassphrase) {
errors.btcWalletPassphrase = 'Required'
}
if (values.ltcWalletId && !values.ltcWalletPassphrase) {
errors.ltcWalletPassphrase = 'Required'
}
if (values.zecWalletId && !values.zecWalletPassphrase) {
errors.zecWalletPassphrase = 'Required'
}
if (values.bchWalletId && !values.bchWalletPassphrase) {
errors.bchWalletPassphrase = 'Required'
}
if (values.dashWalletId && !values.dashWalletPassphrase) {
errors.dashWalletPassphrase = 'Required'
}
return errors
}
}
}
const getBitgoFields = () => [
{
name: schema.token.code,
label: schema.token.display,
type: 'text',
component: TextInputFormik
},
{
name: schema.btcWalletId.code,
label: schema.btcWalletId.display,
type: 'text',
component: TextInputFormik
},
{
name: schema.btcWalletPassphrase.code,
label: schema.btcWalletPassphrase.display,
type: 'text',
component: SecretInputFormik
},
{
name: schema.ltcWalletId.code,
label: schema.ltcWalletId.display,
type: 'text',
component: TextInputFormik
},
{
name: schema.ltcWalletPassphrase.code,
label: schema.ltcWalletPassphrase.display,
type: 'text',
component: SecretInputFormik
},
{
name: schema.zecWalletId.code,
label: schema.zecWalletId.display,
type: 'text',
component: TextInputFormik
},
{
name: schema.zecWalletPassphrase.code,
label: schema.zecWalletPassphrase.display,
type: 'text',
component: SecretInputFormik
},
{
name: schema.bchWalletId.code,
label: schema.bchWalletId.display,
type: 'text',
component: TextInputFormik
},
{
name: schema.bchWalletPassphrase.code,
label: schema.bchWalletPassphrase.display,
type: 'text',
component: SecretInputFormik
},
{
name: schema.dashWalletId.code,
label: schema.dashWalletId.display,
type: 'text',
component: TextInputFormik
},
{
name: schema.dashWalletPassphrase.code,
label: schema.dashWalletPassphrase.display,
type: 'text',
component: SecretInputFormik
},
{
name: schema.environment.code,
label: schema.environment.display,
placeholder: 'prod or test',
type: 'text',
component: TextInputFormik
}
]
const BitgoForm = ({ account, handleSubmit, ...props }) => {
const { code } = account
const formik = getBitgoFormik(account)
const fields = getBitgoFields()
return (
<>
<EditService
title="Bitgo"
formik={formik}
code={code}
fields={fields}
{...props}
/>
</>
)
}
export { BitgoForm, BitgoCard, getBitgoFormik, getBitgoFields }

View file

@ -1,123 +0,0 @@
import React, { memo } from 'react'
import * as Yup from 'yup'
import TextInputFormik from 'src/components/inputs/formik/TextInput'
import SecretInputFormik from 'src/components/inputs/formik/SecretInput'
import { Card, getValue as getValueAux, formatLong } from './aux'
import EditService from './EditService'
const schema = {
clientId: {
code: 'clientId',
display: 'Client ID'
},
key: {
code: 'key',
display: 'API Key'
},
secret: {
code: 'secret',
display: 'API Secret'
}
}
const BitstampCard = memo(({ account, onEdit, ...props }) => {
const findValue = getValueAux(account)
const clientId = schema.clientId
const key = schema.key
const clientIdValue = findValue(clientId.code)
const keyValue = findValue(key.code)
const items = [
{
label: clientId.display,
value: formatLong(clientIdValue)
},
{
label: key.display,
value: formatLong(keyValue)
}
]
return (
<Card
account={account}
title="Bitstamp (Exchange)"
items={items}
onEdit={onEdit}
/>
)
})
const getBitstampFormik = account => {
const getValue = getValueAux(account)
const clientId = getValue(schema.clientId.code)
const key = getValue(schema.key.code)
const secret = getValue(schema.secret.code)
return {
initialValues: {
clientId: clientId,
key: key,
secret: secret
},
validationSchema: Yup.object().shape({
clientId: Yup.string()
.max(100, 'Too long')
.required('Required'),
key: Yup.string()
.max(100, 'Too long')
.required('Required'),
secret: Yup.string()
.max(100, 'Too long')
.required('Required')
})
}
}
const getBitstampFields = () => [
{
name: schema.clientId.code,
label: schema.clientId.display,
type: 'text',
component: TextInputFormik
},
{
name: schema.key.code,
label: schema.key.display,
type: 'text',
component: TextInputFormik
},
{
name: schema.secret.code,
label: schema.secret.display,
type: 'text',
component: SecretInputFormik
}
]
const BitstampForm = ({ account, ...props }) => {
const { code } = account
const formik = getBitstampFormik(account)
const fields = getBitstampFields()
return (
<>
<EditService
title="Bitstamp"
formik={formik}
code={code}
fields={fields}
{...props}
/>
</>
)
}
export { BitstampForm, BitstampCard, getBitstampFormik, getBitstampFields }

View file

@ -1,113 +0,0 @@
import React, { memo } from 'react'
import * as Yup from 'yup'
import TextInputFormik from 'src/components/inputs/formik/TextInput'
import { Card, getValue as getValueAux, formatLong } from './aux'
import EditService from './EditService'
const schema = {
token: {
code: 'token',
display: 'API Token'
},
confidenceFactor: {
code: 'confidenceFactor',
display: 'Confidence Factor'
}
}
const BlockcypherCard = memo(({ account, onEdit, ...props }) => {
const getValue = getValueAux(account)
const token = schema.token
const confidenceFactor = schema.confidenceFactor
const tokenValue = getValue(token.code)
const confidenceFactorValue = getValue(confidenceFactor.code)
const items = [
{
label: token.display,
value: formatLong(tokenValue)
},
{
label: confidenceFactor.display,
value: confidenceFactorValue
}
]
return (
<Card
account={account}
title="Blockcypher (Payments)"
items={items}
onEdit={onEdit}
/>
)
})
const getBlockcypherFormik = account => {
const getValue = getValueAux(account)
const token = getValue(schema.token.code)
const confidenceFactor = getValue(schema.confidenceFactor.code)
return {
initialValues: {
token: token,
confidenceFactor: confidenceFactor
},
validationSchema: Yup.object().shape({
token: Yup.string()
.max(100, 'Too long')
.required('Required'),
confidenceFactor: Yup.number()
.integer('Please input a positive integer')
.positive('Please input a positive integer')
.required('Required')
})
}
}
const getBlockcypherFields = () => [
{
name: schema.token.code,
label: schema.token.display,
type: 'text',
component: TextInputFormik
},
{
name: schema.confidenceFactor.code,
label: schema.confidenceFactor.display,
type: 'text',
component: TextInputFormik
}
]
const BlockcypherForm = ({ account, ...props }) => {
const { code } = account
const formik = getBlockcypherFormik(account)
const fields = getBlockcypherFields()
return (
<>
<EditService
title="Blockcypher"
code={code}
formik={formik}
fields={fields}
{...props}
/>
</>
)
}
export {
BlockcypherForm,
BlockcypherCard,
getBlockcypherFormik,
getBlockcypherFields
}

View file

@ -1,94 +0,0 @@
import React, { useState } from 'react'
import { Form, Formik, Field } from 'formik'
import classnames from 'classnames'
import { makeStyles, Paper } from '@material-ui/core'
import { H2, Info3 } from 'src/components/typography'
import { Button } from 'src/components/buttons'
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg'
import { ReactComponent as ErrorIcon } from 'src/styling/icons/warning-icon/tomato.svg'
import { editServiceStyles as styles } from './Services.styles'
const useStyles = makeStyles(styles)
const DEFAULT_ERROR_MESSAGE = 'Something went wrong. Please contact support.'
const EditService = ({
title,
code,
formik,
fields,
handleClose,
save,
...props
}) => {
const [error, setError] = useState(props.error)
const classes = useStyles()
const submitWrapperClasses = {
[classes.submitWrapper]: true,
[classes.submitError]: error
}
return (
<Paper className={classes.paper}>
<button onClick={() => handleClose()}>
<CloseIcon />
</button>
<div className={classes.modalHeader}>
<H2>{`Edit ${title}`}</H2>
</div>
<div className={classes.modalBody}>
<Formik
initialValues={formik.initialValues}
validate={formik.validate}
validationSchema={formik.validationSchema}
onSubmit={values => {
save(code, values)
.then(m => handleClose())
.catch(err => {
if (err) setError(true)
})
}}>
<Form>
<div className={classes.formBody}>
{fields &&
fields.map((field, idx) => (
<div key={idx} className={classes.field}>
<Field
id={field.name}
name={field.name}
component={field.component}
placeholder={field.placeholder}
type={field.type}
label={field.label}
onFocus={() => {
setError(null)
}}
/>
</div>
))}
</div>
<div className={classnames(submitWrapperClasses)}>
<div className={classes.messageWrapper}>
{error && (
<div>
<ErrorIcon />
<Info3 className={classes.message}>
{DEFAULT_ERROR_MESSAGE}
</Info3>
</div>
)}
<Button type="submit">Save changes</Button>
</div>
</div>
</Form>
</Formik>
</div>
</Paper>
)
}
export default EditService

View file

@ -0,0 +1,67 @@
import { makeStyles, Grid } from '@material-ui/core'
import { Formik, Form, FastField } from 'formik'
import * as R from 'ramda'
import React from 'react'
import { Button } from 'src/components/buttons'
const styles = {
button: {
margin: [['auto', 0, 32, 'auto']]
},
form: {
flex: 1,
display: 'flex',
flexDirection: 'column'
},
grid: {
marginBottom: 24,
marginTop: 12
}
}
const useStyles = makeStyles(styles)
const FormRenderer = ({
validationSchema,
elements,
value,
save,
buttonLabel = 'Save changes'
}) => {
const classes = useStyles()
const initialValues = R.compose(
R.mergeAll,
R.map(({ code }) => ({ [code]: (value && value[code]) ?? '' }))
)(elements)
const values = R.merge(initialValues, value)
return (
<Formik
enableReinitialize
initialValues={values}
validationSchema={validationSchema}
onSubmit={save}>
<Form className={classes.form}>
<Grid container spacing={3} className={classes.grid}>
{elements.map(({ component, code, display }) => (
<Grid item xs={12} key={code}>
<FastField
component={component}
name={code}
label={display}
fullWidth={true}
/>
</Grid>
))}
</Grid>
<Button className={classes.button} type="submit">
{buttonLabel}
</Button>
</Form>
</Formik>
)
}
export default FormRenderer

View file

@ -1,126 +0,0 @@
import React, { memo } from 'react'
import * as Yup from 'yup'
import TextInputFormik from 'src/components/inputs/formik/TextInput'
import SecretInputFormik from 'src/components/inputs/formik/SecretInput'
import { Card, getValue as getValueAux, formatLong } from './aux'
import EditService from './EditService'
const schema = {
apiKey: {
code: 'apiKey',
display: 'API Key'
},
apiSecret: {
code: 'apiSecret',
display: 'API Secret'
},
endpoint: {
code: 'endpoint',
display: 'Endpoint'
}
}
const InfuraCard = memo(({ account, onEdit, ...props }) => {
const getValue = getValueAux(account)
const apiKey = schema.apiKey
const apiSecret = schema.apiSecret
const apiKeyValue = getValue(apiKey.code)
const apiSecretValue = getValue(apiSecret.code)
const items = [
{
label: apiKey.display,
value: formatLong(apiKeyValue)
},
{
label: apiSecret.display,
value: formatLong(apiSecretValue)
}
]
return (
<Card
account={account}
title="Infura (Wallet)"
items={items}
onEdit={onEdit}
/>
)
})
const getInfuraFormik = account => {
const getValue = getValueAux(account)
const apiKey = getValue(schema.apiKey.code)
const apiSecret = getValue(schema.apiSecret.code)
const endpoint = getValue(schema.endpoint.code)
return {
initialValues: {
apiKey: apiKey,
apiSecret: apiSecret,
endpoint: endpoint
},
validationSchema: Yup.object().shape({
apiKey: Yup.string()
.max(100, 'Too long')
.required('Required'),
apiSecret: Yup.string()
.max(100, 'Too long')
.required('Required'),
endpoint: Yup.string()
.max(100, 'Too long')
.url('Please input a valid url')
.required('Required')
})
}
}
const getInfuraFields = () => {
return [
{
name: schema.apiKey.code,
label: schema.apiKey.display,
type: 'text',
component: TextInputFormik
},
{
name: schema.apiSecret.code,
label: schema.apiSecret.display,
type: 'text',
component: SecretInputFormik
},
{
name: schema.endpoint.code,
label: schema.endpoint.display,
type: 'text',
component: TextInputFormik
}
]
}
const InfuraForm = ({ account, ...props }) => {
const { code } = account
const formik = getInfuraFormik(account)
const fields = getInfuraFields()
return (
<>
<EditService
title="Infura"
formik={formik}
code={code}
fields={fields}
{...props}
/>
</>
)
}
export { InfuraCard, InfuraForm, getInfuraFormik, getInfuraFields }

View file

@ -1,133 +0,0 @@
import React, { memo } from 'react'
import * as Yup from 'yup'
import TextInputFormik from 'src/components/inputs/formik/TextInput'
import SecretInputFormik from 'src/components/inputs/formik/SecretInput'
import { Card, getValue as getValueAux, formatLong } from './aux'
import EditService from './EditService'
const schema = {
userId: {
code: 'userId',
display: 'User ID'
},
walletId: {
code: 'walletId',
display: 'Wallet ID'
},
clientKey: {
code: 'clientKey',
display: 'Client Key'
},
clientSecret: {
code: 'clientSecret',
display: 'Client Secret'
}
}
const ItbitCard = memo(({ account, onEdit, ...props }) => {
const getValue = getValueAux(account)
const userId = schema.userId
const walletId = schema.walletId
const userIdValue = getValue(userId.code)
const walletIdValue = getValue(walletId.code)
const items = [
{
label: userId.display,
value: formatLong(userIdValue)
},
{
label: walletId.display,
value: formatLong(walletIdValue)
}
]
return (
<Card account={account} title="itBit ()" items={items} onEdit={onEdit} />
)
})
const getItbitFormik = account => {
const getValue = getValueAux(account)
const userId = getValue(schema.userId.code)
const walletId = getValue(schema.walletId.code)
const clientKey = getValue(schema.clientKey.code)
const clientSecret = getValue(schema.clientSecret.code)
return {
initialValues: {
userId: userId,
walletId: walletId,
clientKey: clientKey,
clientSecret: clientSecret
},
validationSchema: Yup.object().shape({
userId: Yup.string()
.max(100, 'Too long')
.required('Required'),
walletId: Yup.string()
.max(100, 'Too long')
.required('Required'),
clientKey: Yup.string()
.max(100, 'Too long')
.required('Required'),
clientSecret: Yup.string()
.max(100, 'Too long')
.required('Required')
})
}
}
const getItbitFields = () => [
{
name: schema.userId.code,
label: schema.userId.display,
type: 'text',
component: TextInputFormik
},
{
name: schema.walletId.code,
label: schema.walletId.display,
type: 'text',
component: TextInputFormik
},
{
name: schema.clientKey.code,
label: schema.clientKey.display,
type: 'text',
component: TextInputFormik
},
{
name: schema.clientSecret.code,
label: schema.clientSecret.display,
type: 'text',
component: SecretInputFormik
}
]
const ItbitForm = ({ account, ...props }) => {
const { code } = account
const formik = getItbitFormik(account)
const fields = getItbitFields()
return (
<>
<EditService
title="itBit"
formik={formik}
code={code}
fields={fields}
{...props}
/>
</>
)
}
export { ItbitCard, ItbitForm, getItbitFormik, getItbitFields }

View file

@ -1,108 +0,0 @@
import React, { memo } from 'react'
import * as Yup from 'yup'
import TextInputFormik from 'src/components/inputs/formik/TextInput'
import SecretInputFormik from 'src/components/inputs/formik/SecretInput'
import { Card, getValue as getValueAux, formatLong } from './aux'
import EditService from './EditService'
const schema = {
apiKey: {
code: 'apiKey',
display: 'API Key'
},
privateKey: {
code: 'privateKey',
display: 'Private Key'
}
}
const KrakenCard = memo(({ account, onEdit, ...props }) => {
const getValue = getValueAux(account)
const apiKey = schema.apiKey
const privateKey = schema.privateKey
const apiKeyValue = getValue(apiKey.code)
const privateKeyValue = getValue(privateKey.code)
const items = [
apiKey && {
label: apiKey.display,
value: formatLong(apiKeyValue)
},
privateKey && {
label: privateKey.display,
value: formatLong(privateKeyValue)
}
]
return (
<Card
account={account}
title="Kraken (Exchange)"
items={items}
onEdit={onEdit}
/>
)
})
const getKrakenFormik = account => {
const getValue = getValueAux(account)
const apiKey = getValue(schema.apiKey.code)
const privateKey = getValue(schema.privateKey.code)
return {
initialValues: {
apiKey: apiKey,
privateKey: privateKey
},
validationSchema: Yup.object().shape({
apiKey: Yup.string()
.max(100, 'Too long')
.required('Required'),
privateKey: Yup.string()
.max(100, 'Too long')
.required('Required')
})
}
}
const getKrakenFields = () => [
{
name: schema.apiKey.code,
label: schema.apiKey.display,
type: 'text',
component: TextInputFormik
},
{
name: schema.privateKey.code,
label: schema.privateKey.display,
type: 'text',
component: SecretInputFormik
}
]
const KrakenForm = ({ account, ...props }) => {
const { code } = account
const formik = getKrakenFormik(account)
const fields = getKrakenFields()
return (
<>
<EditService
title="Kraken"
formik={formik}
code={code}
fields={fields}
{...props}
/>
</>
)
}
export { KrakenCard, KrakenForm, getKrakenFormik, getKrakenFields }

View file

@ -1,139 +0,0 @@
import React, { memo } from 'react'
import * as Yup from 'yup'
import TextInputFormik from 'src/components/inputs/formik/TextInput'
import { Card, getValue as getValueAux } from './aux'
import EditService from './EditService'
const schema = {
apiKey: {
code: 'apiKey',
display: 'API Key'
},
domain: {
code: 'domain',
display: 'Domain'
},
fromEmail: {
code: 'fromEmail',
display: 'From Email'
},
toEmail: {
code: 'toEmail',
display: 'To Email'
}
}
const MailgunCard = memo(({ account, onEdit, ...props }) => {
const getValue = getValueAux(account)
const fromEmail = schema.fromEmail
const toEmail = schema.toEmail
const fromEmailValue = getValue(fromEmail.code)
const toEmailValue = getValue(toEmail.code)
const items = [
{
label: fromEmail.display,
value: fromEmailValue
},
{
label: toEmail.display,
value: toEmailValue
}
]
return (
<Card
account={account}
title="Mailgun (Email)"
items={items}
onEdit={onEdit}
/>
)
})
const getMailgunFormik = account => {
const getValue = getValueAux(account)
const apiKey = getValue(schema.apiKey.code)
const domain = getValue(schema.domain.code)
const fromEmail = getValue(schema.fromEmail.code)
const toEmail = getValue(schema.toEmail.code)
return {
initialValues: {
apiKey: apiKey,
domain: domain,
fromEmail: fromEmail,
toEmail: toEmail
},
validationSchema: Yup.object().shape({
apiKey: Yup.string()
.max(100, 'Too long')
.required('Required'),
domain: Yup.string()
.max(100, 'Too long')
.required('Required'),
fromEmail: Yup.string()
.max(100, 'Too long')
.email('Please input a valid email address')
.required('Required'),
toEmail: Yup.string()
.max(100, 'Too long')
.email('Please input a valid email address')
.required('Required')
})
}
}
const getMailgunFields = () => [
{
name: schema.apiKey.code,
label: schema.apiKey.display,
type: 'text',
component: TextInputFormik
},
{
name: schema.domain.code,
label: schema.domain.display,
type: 'text',
component: TextInputFormik
},
{
name: schema.fromEmail.code,
label: schema.fromEmail.display,
type: 'text',
component: TextInputFormik
},
{
name: schema.toEmail.code,
label: schema.toEmail.display,
type: 'text',
component: TextInputFormik
}
]
const MailgunForm = ({ account, ...props }) => {
const { code } = account
const formik = getMailgunFormik(account)
const fields = getMailgunFields()
return (
<>
<EditService
title="Mailgun"
formik={formik}
code={code}
fields={fields}
{...props}
/>
</>
)
}
export { MailgunCard, MailgunForm, getMailgunFormik, getMailgunFields }

View file

@ -1,241 +1,96 @@
import React, { useState } from 'react'
import * as R from 'ramda'
import { gql } from 'apollo-boost'
import { makeStyles, Modal } from '@material-ui/core'
import { useQuery, useMutation } from '@apollo/react-hooks' import { useQuery, useMutation } from '@apollo/react-hooks'
import { makeStyles, Grid } from '@material-ui/core'
import { gql } from 'apollo-boost'
import * as R from 'ramda'
import React, { useState } from 'react'
import Title from 'src/components/Title' import Modal from 'src/components/Modal'
import TitleSection from 'src/components/layout/TitleSection'
import SingleRowTable from 'src/components/single-row-table/SingleRowTable'
import { formatLong } from 'src/utils/string'
import { BitgoCard, BitgoForm } from './Bitgo' import FormRenderer from './FormRenderer'
import { BitstampCard, BitstampForm } from './Bitstamp' import schemas from './schemas'
import { BlockcypherCard, BlockcypherForm } from './Blockcypher'
import { InfuraCard, InfuraForm } from './Infura' const GET_INFO = gql`
import { ItbitCard, ItbitForm } from './Itbit' query getData {
import { KrakenCard, KrakenForm } from './Kraken' accounts
import { MailgunCard, MailgunForm } from './Mailgun' }
import { StrikeCard, StrikeForm } from './Strike' `
import { TwilioCard, TwilioForm } from './Twilio'
import { servicesStyles as styles } from './Services.styles' const SAVE_ACCOUNT = gql`
mutation Save($account: JSONObject) {
saveAccount(account: $account)
}
`
const styles = {
wrapper: {
// widths + spacing is a little over 1200 on the design
// this adjusts the margin after a small reduction on card size
marginLeft: 1
}
}
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const GET_CONFIG = gql` const Services = ({ key: SCREEN_KEY }) => {
{ const [editingSchema, setEditingSchema] = useState(null)
config
}
`
const GET_ACCOUNTS = gql` const { data } = useQuery(GET_INFO)
{ const [saveAccount] = useMutation(SAVE_ACCOUNT, {
accounts { onCompleted: () => setEditingSchema(null),
code refetchQueries: ['getData']
display
class
cryptos
}
}
`
const SAVE_CONFIG = gql`
mutation Save($config: JSONObject) {
saveConfig(config: $config)
}
`
const Services = () => {
const [open, setOpen] = useState(false)
const [modalContent, setModalContent] = useState(null)
const [accountsConfig, setAccountsConfig] = useState(null)
const [saveConfig, { loading }] = useMutation(SAVE_CONFIG, {
onCompleted: data => setAccountsConfig(data.saveConfig.accounts)
}) })
const classes = useStyles() const classes = useStyles()
useQuery(GET_CONFIG, { const accounts = data?.accounts ?? []
onCompleted: data => setAccountsConfig(data.config.accounts ?? {})
})
const { data: accountsResponse } = useQuery(GET_ACCOUNTS)
const accounts = accountsResponse?.accounts const getValue = code => R.find(R.propEq('code', code))(accounts)
const save = (code, it) => { const getItems = (code, elements) => {
const newAccounts = R.clone(accountsConfig) const faceElements = R.filter(R.prop('face'))(elements)
newAccounts[code] = it const values = getValue(code) || {}
return saveConfig({ variables: { config: { accounts: newAccounts } } }) return R.map(({ display, code, long }) => ({
label: display,
value: long ? formatLong(values[code]) : values[code]
}))(faceElements)
} }
const getAccount = code => {
return R.mergeDeepLeft(
R.find(R.propEq('code', code))(accounts) ?? {},
accountsConfig[code] ?? {}
)
}
const handleOpen = content => {
setOpen(true)
setModalContent(content)
}
const handleClose = (canClose = true) => {
if (canClose && !loading) {
setOpen(false)
setModalContent(null)
}
}
if (!accounts || !accountsConfig) return null
const codes = {
bitgo: 'bitgo',
bitstamp: 'bitstamp',
blockcypher: 'blockcypher',
infura: 'infura',
itbit: 'itbit',
kraken: 'kraken',
mailgun: 'mailgun',
strike: 'strike',
twilio: 'twilio'
}
const bitgo = getAccount(codes.bitgo)
const bitstamp = getAccount(codes.bitstamp)
const blockcypher = getAccount(codes.blockcypher)
const infura = getAccount(codes.infura)
const itbit = getAccount(codes.itbit)
const kraken = getAccount(codes.kraken)
const mailgun = getAccount(codes.mailgun)
const strike = getAccount(codes.strike)
const twilio = getAccount(codes.twilio)
return ( return (
<> <div className={classes.wrapper}>
<div className={classes.titleWrapper}> <TitleSection title="3rd Party Services" />
<div className={classes.titleContainer}> <Grid container spacing={4}>
<Title>3rd Party Services</Title> {R.values(schemas).map(schema => (
</div> <Grid item key={schema.code}>
</div> <SingleRowTable
<div className={classes.mainWrapper}> title={schema.title}
<BitgoCard onEdit={() => setEditingSchema(schema)}
account={bitgo} items={getItems(schema.code, schema.elements)}
onEdit={() => />
handleOpen( </Grid>
<BitgoForm ))}
account={bitgo} </Grid>
handleClose={handleClose} {editingSchema && (
save={save}
/>
)
}
/>
<BitstampCard
account={bitstamp}
onEdit={() =>
handleOpen(
<BitstampForm
account={bitstamp}
handleClose={handleClose}
save={save}
/>
)
}
/>
<BlockcypherCard
account={blockcypher}
onEdit={() =>
handleOpen(
<BlockcypherForm
account={blockcypher}
handleClose={handleClose}
save={save}
/>
)
}
/>
<InfuraCard
account={infura}
onEdit={() =>
handleOpen(
<InfuraForm
account={infura}
handleClose={handleClose}
save={save}
/>
)
}
/>
<ItbitCard
account={itbit}
onEdit={() =>
handleOpen(
<ItbitForm
account={itbit}
handleClose={handleClose}
save={save}
/>
)
}
/>
<KrakenCard
account={kraken}
onEdit={() =>
handleOpen(
<KrakenForm
account={kraken}
handleClose={handleClose}
save={save}
/>
)
}
/>
<MailgunCard
account={mailgun}
onEdit={() =>
handleOpen(
<MailgunForm
account={mailgun}
handleClose={handleClose}
save={save}
/>
)
}
/>
<StrikeCard
account={strike}
onEdit={() =>
handleOpen(
<StrikeForm
account={strike}
handleClose={handleClose}
save={save}
/>
)
}
/>
<TwilioCard
account={twilio}
onEdit={() =>
handleOpen(
<TwilioForm
account={twilio}
handleClose={handleClose}
save={save}
/>
)
}
/>
</div>
{modalContent && (
<Modal <Modal
aria-labelledby="simple-modal-title" title={`Edit ${editingSchema.name}`}
aria-describedby="simple-modal-description" width={478}
open={open} handleClose={() => setEditingSchema(null)}
onClose={handleClose} open={true}>
className={classes.modal}> <FormRenderer
<div>{modalContent}</div> save={it =>
saveAccount({
variables: { account: { code: editingSchema.code, ...it } }
})
}
elements={editingSchema.elements}
validationSchema={editingSchema.validationSchema}
value={getValue(editingSchema.code)}
/>
</Modal> </Modal>
)} )}
</> </div>
) )
} }

View file

@ -1,177 +0,0 @@
import { white, offColor, errorColor } from 'src/styling/variables'
import typographyStyles from 'src/components/typography/styles'
import baseStyles from 'src/pages/Logs.styles'
const { titleWrapper } = baseStyles
const { label1, p } = typographyStyles
const servicesStyles = {
titleWrapper,
titleContainer: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%'
},
addServiceMenu: {
width: 215,
'& > ul': {
padding: [[18, 16, 21, 16]],
'& > li': {
display: 'flex',
justifyContent: 'space-between',
listStyle: 'none',
marginBottom: 23,
cursor: 'pointer',
'& > span:first-child': {
extend: p,
fontWeight: 'bold'
},
'& > span:last-child': {
extend: label1,
color: offColor
},
'&:last-child': {
marginBottom: 0
}
}
}
},
mainWrapper: {
display: 'flex',
flexWrap: 'wrap'
},
modal: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
'& > div': {
outline: 'none'
}
},
modalHeader: {
display: 'flex',
justifyContent: 'space-between',
'& button': {
border: 'none',
backgroundColor: 'transparent',
cursor: 'pointer'
}
},
modalBody: {
'& > form': {
display: 'flex',
flexDirection: 'column',
position: 'relative',
minHeight: 400,
'& > div:last-child': {
display: 'flex',
alignItems: 'flex-end',
flex: 'auto',
alignSelf: 'flex-end',
'& > button': {
marginTop: 32
}
}
}
},
paper: {
position: 'absolute',
backgroundColor: white,
outline: '0 none',
padding: [[16, 20, 32, 24]]
},
inputField: {
width: 434
},
formLabel: {
extend: label1
}
}
const editServiceStyles = {
paper: {
padding: [[5, 20, 32, 24]],
position: 'relative',
display: 'flex',
flexDirection: 'column',
minHeight: 524,
overflow: 'hidden',
'& > button': {
position: 'absolute',
top: 16,
right: 16,
border: 'none',
backgroundColor: 'transparent',
cursor: 'pointer',
'& svg': {
width: 18
}
},
'& form': {
display: 'flex',
flexDirection: 'column',
flexGrow: 2
}
},
modalHeader: {
display: 'flex',
marginBottom: 14
},
modalBody: {
display: 'flex',
flexGrow: 2
},
formBody: {
display: 'flex',
flexDirection: 'column'
},
field: {
position: 'relative',
'& > div': {
width: 434
}
},
submitWrapper: {
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'flex-end',
flexGrow: 2,
marginTop: 32,
'& > div': {
display: 'flex',
alignItems: 'center',
width: '100%',
justifyContent: 'flex-end',
'& > button': {
'&:active': {
marginTop: 0
}
}
}
},
submitError: {
'& > div': {
justifyContent: 'space-between'
}
},
messageWrapper: {
'& > div': {
display: 'flex',
alignItems: 'center',
'& > svg': {
marginRight: 10
}
}
},
message: {
display: 'flex',
alignItems: 'center',
color: errorColor,
margin: 0,
whiteSpace: 'break-spaces',
width: 250
}
}
export { servicesStyles, editServiceStyles }

View file

@ -1,86 +0,0 @@
import React, { memo } from 'react'
import * as Yup from 'yup'
import SecretInputFormik from 'src/components/inputs/formik/SecretInput'
import { Card, getValue as getValueAux, formatLong } from './aux'
import EditService from './EditService'
const schema = {
token: {
code: 'token',
display: 'API Token'
}
}
const StrikeCard = memo(({ account, onEdit, ...props }) => {
const getValue = getValueAux(account)
const token = schema.token
const tokenValue = getValue(token.code)
const items = [
{
label: token.display,
value: formatLong(tokenValue)
}
]
return (
<Card
account={account}
title="Strike (Lightning Payments)"
items={items}
onEdit={onEdit}
/>
)
})
const getStrikeFormik = account => {
const getValue = getValueAux(account)
const token = getValue(schema.token.code)
return {
initialValues: {
token: token
},
validationSchema: Yup.object().shape({
token: Yup.string()
.max(100, 'Too long')
.required('Required')
})
}
}
const getStrikeFields = () => [
{
name: schema.token.code,
label: schema.token.display,
type: 'text',
component: SecretInputFormik
}
]
const StrikeForm = ({ account, ...props }) => {
const code = 'strike'
const formik = getStrikeFormik(account)
const fields = getStrikeFields()
return (
<>
<EditService
title="Strike"
formik={formik}
code={code}
fields={fields}
{...props}
/>
</>
)
}
export { StrikeCard, StrikeForm, getStrikeFormik, getStrikeFields }

View file

@ -1,138 +0,0 @@
import React, { memo } from 'react'
import * as Yup from 'yup'
import TextInputFormik from 'src/components/inputs/formik/TextInput'
import SecretInputFormik from 'src/components/inputs/formik/SecretInput'
import { Card, getValue as getValueAux } from './aux'
import EditService from './EditService'
const schema = {
accountSid: {
code: 'accountSid',
display: 'Account SID'
},
authToken: {
code: 'authToken',
display: 'Auth Token'
},
fromNumber: {
code: 'fromNumber',
display: 'From Number'
},
toNumber: {
code: 'toNumber',
display: 'To Number'
}
}
const TwilioCard = memo(({ account, onEdit, ...props }) => {
const getValue = getValueAux(account)
const fromNumber = schema.fromNumber
const toNumber = schema.toNumber
const fromNumberValue = getValue(fromNumber.code)
const toNumberValue = getValue(toNumber.code)
const items = [
{
label: fromNumber.display,
value: fromNumberValue
},
{
label: toNumber.display,
value: toNumberValue
}
]
return (
<Card
account={account}
title="Twilio (SMS)"
items={items}
onEdit={onEdit}
/>
)
})
const getTwilioFormik = account => {
const getValue = getValueAux(account)
const accountSid = getValue(schema.accountSid.code)
const authToken = getValue(schema.authToken.code)
const fromNumber = getValue(schema.fromNumber.code)
const toNumber = getValue(schema.toNumber.code)
return {
initialValues: {
accountSid: accountSid,
authToken: authToken,
fromNumber: fromNumber,
toNumber: toNumber
},
validationSchema: Yup.object().shape({
accountSid: Yup.string()
.max(100, 'Too long')
.required('Required'),
authToken: Yup.string()
.max(100, 'Too long')
.required('Required'),
fromNumber: Yup.string()
.max(100, 'Too long')
.required('Required'),
toNumber: Yup.string()
.max(100, 'Too long')
.required('Required')
})
}
}
const getTwilioFields = () => [
{
name: schema.accountSid.code,
label: schema.accountSid.display,
type: 'text',
component: TextInputFormik
},
{
name: schema.authToken.code,
label: schema.authToken.display,
type: 'text',
component: SecretInputFormik
},
{
name: schema.fromNumber.code,
label: schema.fromNumber.display,
type: 'text',
component: TextInputFormik
},
{
name: schema.toNumber.code,
label: schema.toNumber.display,
type: 'text',
component: TextInputFormik
}
]
const TwilioForm = ({ account, ...props }) => {
const { code } = account
const formik = getTwilioFormik(account)
const fields = getTwilioFields()
return (
<>
<EditService
title="Twilio"
formik={formik}
code={code}
fields={fields}
{...props}
/>
</>
)
}
export { TwilioCard, TwilioForm, getTwilioFormik, getTwilioFields }

View file

@ -1,44 +0,0 @@
import { makeStyles } from '@material-ui/core'
import * as R from 'ramda'
import React from 'react'
import SingleRowTable from 'src/components/single-row-table/SingleRowTable'
const getValue = R.curry((account, code) => (account ? account[code] : ''))
const formatLong = value => {
if (!value) return ''
if (value.length <= 20) return value
return `${value.slice(0, 8)}(...)${value.slice(
value.length - 8,
value.length
)}`
}
const styles = {
card: {
margin: [[0, 30, 32, 0]],
paddingBottom: 24,
'&:nth-child(3n+3)': {
marginRight: 0
}
}
}
const useStyles = makeStyles(styles)
const Card = ({ account, title, items, onEdit, ...props }) => {
const classes = useStyles()
return (
<SingleRowTable
title={title}
items={items}
className={classes.card}
onEdit={onEdit}
/>
)
}
export { Card, getValue, formatLong }

View file

@ -0,0 +1,120 @@
import * as Yup from 'yup'
import SecretInputFormik from 'src/components/inputs/formik/SecretInput'
import TextInputFormik from 'src/components/inputs/formik/TextInput'
const isDefined = it => it && it.length
export default {
code: 'bitgo',
name: 'BitGo',
title: 'BitGo (Wallet)',
elements: [
{
code: 'token',
display: 'API Token',
component: TextInputFormik,
face: true,
long: true
},
{
code: 'environment',
display: 'Environment',
component: TextInputFormik,
face: true
},
{
code: 'btcWalletId',
display: 'BTC Wallet ID',
component: TextInputFormik
},
{
code: 'btcWalletPassphrase',
display: 'BTC Wallet Passphrase',
component: SecretInputFormik
},
{
code: 'ltcWalletId',
display: 'LTC Wallet ID',
component: TextInputFormik
},
{
code: 'ltcWalletPassphrase',
display: 'LTC Wallet Passphrase',
component: SecretInputFormik
},
{
code: 'zecWalletId',
display: 'ZEC Wallet ID',
component: TextInputFormik
},
{
code: 'zecWalletPassphrase',
display: 'ZEC Wallet Passphrase',
component: SecretInputFormik
},
{
code: 'bchWalletId',
display: 'BCH Wallet ID',
component: TextInputFormik
},
{
code: 'bchWalletPassphrase',
display: 'BCH Wallet Passphrase',
component: SecretInputFormik
},
{
code: 'dashWalletId',
display: 'DASH Wallet ID',
component: TextInputFormik
},
{
code: 'dashWalletPassphrase',
display: 'DASH Wallet Passphrase',
component: SecretInputFormik
}
],
validationSchema: Yup.object().shape({
token: Yup.string()
.max(100, 'Too long')
.required('Required'),
btcWalletId: Yup.string().max(100, 'Too long'),
btcWalletPassphrase: Yup.string()
.max(100, 'Too long')
.when('btcWalletId', {
is: isDefined,
then: Yup.string().required()
}),
ltcWalletId: Yup.string().max(100, 'Too long'),
ltcWalletPassphrase: Yup.string()
.max(100, 'Too long')
.when('ltcWalletId', {
is: isDefined,
then: Yup.string().required()
}),
zecWalletId: Yup.string().max(100, 'Too long'),
zecWalletPassphrase: Yup.string()
.max(100, 'Too long')
.when('zecWalletId', {
is: isDefined,
then: Yup.string().required()
}),
bchWalletId: Yup.string().max(100, 'Too long'),
bchWalletPassphrase: Yup.string()
.max(100, 'Too long')
.when('bchWalletId', {
is: isDefined,
then: Yup.string().required()
}),
dashWalletId: Yup.string().max(100, 'Too long'),
dashWalletPassphrase: Yup.string()
.max(100, 'Too long')
.when('dashWalletId', {
is: isDefined,
then: Yup.string().required()
}),
environment: Yup.string()
.matches(/(prod|test)/)
.required('Required')
})
}

View file

@ -0,0 +1,43 @@
import * as Yup from 'yup'
import SecretInputFormik from 'src/components/inputs/formik/SecretInput'
import TextInputFormik from 'src/components/inputs/formik/TextInput'
export default {
code: 'bitstamp',
name: 'Bitstamp',
title: 'Bitstamp (Exchange)',
elements: [
{
code: 'clientId',
display: 'Client ID',
component: TextInputFormik,
face: true,
long: true
},
{
code: 'key',
display: 'API Key',
component: TextInputFormik,
face: true,
long: true
},
{
code: 'secret',
display: 'API Secret',
component: SecretInputFormik
}
],
validationSchema: Yup.object().shape({
clientId: Yup.string()
.max(100, 'Too long')
.required('Required'),
key: Yup.string()
.max(100, 'Too long')
.required('Required'),
secret: Yup.string()
.max(100, 'Too long')
.required('Required')
})
}

View file

@ -0,0 +1,34 @@
import * as Yup from 'yup'
import TextInputFormik from 'src/components/inputs/formik/TextInput'
export default {
code: 'blockcypher',
name: 'Blockcypher',
title: 'Blockcypher (Payments)',
elements: [
{
code: 'token',
display: 'API Token',
component: TextInputFormik,
face: true,
long: true
},
{
code: 'confidenceFactor',
display: 'Confidence Factor',
component: TextInputFormik,
face: true
}
],
validationSchema: Yup.object().shape({
token: Yup.string()
.max(100, 'Too long')
.required('Required'),
confidenceFactor: Yup.number()
.integer('Please input a positive integer')
.positive('Please input a positive integer')
.required('Required')
})
}

View file

@ -0,0 +1,21 @@
import bitgo from './bitgo'
import bitstamp from './bitstamp'
import blockcypher from './blockcypher'
import infura from './infura'
import itbit from './itbit'
import kraken from './kraken'
import mailgun from './mailgun'
import strike from './strike'
import twilio from './twilio'
export default {
[bitgo.code]: bitgo,
[bitstamp.code]: bitstamp,
[blockcypher.code]: blockcypher,
[infura.code]: infura,
[itbit.code]: itbit,
[kraken.code]: kraken,
[mailgun.code]: mailgun,
[strike.code]: strike,
[twilio.code]: twilio
}

View file

@ -0,0 +1,41 @@
import * as Yup from 'yup'
import SecretInputFormik from 'src/components/inputs/formik/SecretInput'
import TextInputFormik from 'src/components/inputs/formik/TextInput'
export default {
code: 'infura',
name: 'Infura',
title: 'Infura (Wallet)',
elements: [
{
code: 'apiKey',
display: 'API Key',
component: TextInputFormik,
face: true,
long: true
},
{
code: 'apiSecret',
display: 'API Secret',
component: SecretInputFormik
},
{
code: 'endpoint',
display: 'Endpoint',
component: TextInputFormik,
face: true
}
],
validationSchema: Yup.object().shape({
apiKey: Yup.string()
.max(100, 'Too long')
.required('Required'),
apiSecret: Yup.string()
.max(100, 'Too long')
.required('Required'),
endpoint: Yup.string()
.max(100, 'Too long')
.required('Required')
})
}

View file

@ -0,0 +1,50 @@
import * as Yup from 'yup'
import SecretInputFormik from 'src/components/inputs/formik/SecretInput'
import TextInputFormik from 'src/components/inputs/formik/TextInput'
export default {
code: 'itbit',
name: 'Itbit',
title: 'Itbit (Exchange)',
elements: [
{
code: 'userId',
display: 'User ID',
component: TextInputFormik,
face: true,
long: true
},
{
code: 'walletId',
display: 'Wallet ID',
component: TextInputFormik,
face: true,
long: true
},
{
code: 'clientKey',
display: 'Client Key',
component: TextInputFormik
},
{
code: 'clientSecret',
display: 'Client Secret',
component: SecretInputFormik
}
],
validationSchema: Yup.object().shape({
userId: Yup.string()
.max(100, 'Too long')
.required('Required'),
walletId: Yup.string()
.max(100, 'Too long')
.required('Required'),
clientKey: Yup.string()
.max(100, 'Too long')
.required('Required'),
clientSecret: Yup.string()
.max(100, 'Too long')
.required('Required')
})
}

View file

@ -0,0 +1,32 @@
import * as Yup from 'yup'
import SecretInputFormik from 'src/components/inputs/formik/SecretInput'
import TextInputFormik from 'src/components/inputs/formik/TextInput'
export default {
code: 'kraken',
name: 'Kraken',
title: 'Kraken (Exchange)',
elements: [
{
code: 'apiKey',
display: 'API Key',
component: TextInputFormik,
face: true,
long: true
},
{
code: 'privateKey',
display: 'Private Key',
component: SecretInputFormik
}
],
validationSchema: Yup.object().shape({
apiKey: Yup.string()
.max(100, 'Too long')
.required('Required'),
privateKey: Yup.string()
.max(100, 'Too long')
.required('Required')
})
}

View file

@ -0,0 +1,49 @@
import * as Yup from 'yup'
import TextInputFormik from 'src/components/inputs/formik/TextInput'
export default {
code: 'mailgun',
name: 'Mailgun',
title: 'Mailgun (Email)',
elements: [
{
code: 'apiKey',
display: 'API Key',
component: TextInputFormik
},
{
code: 'domain',
display: 'Domain',
component: TextInputFormik
},
{
code: 'fromEmail',
display: 'From Email',
component: TextInputFormik,
face: true
},
{
code: 'toEmail',
display: 'To Email',
component: TextInputFormik,
face: true
}
],
validationSchema: Yup.object().shape({
apiKey: Yup.string()
.max(100, 'Too long')
.required('Required'),
domain: Yup.string()
.max(100, 'Too long')
.required('Required'),
fromEmail: Yup.string()
.max(100, 'Too long')
.email('Please input a valid email address')
.required('Required'),
toEmail: Yup.string()
.max(100, 'Too long')
.email('Please input a valid email address')
.required('Required')
})
}

View file

@ -0,0 +1,23 @@
import * as Yup from 'yup'
import TextInputFormik from 'src/components/inputs/formik/TextInput'
export default {
code: 'strike',
name: 'Strike',
title: 'Strike (Lightning Payments)',
elements: [
{
code: 'token',
display: 'API Token',
component: TextInputFormik,
face: true,
long: true
}
],
validationSchema: Yup.object().shape({
token: Yup.string()
.max(100, 'Too long')
.required('Required')
})
}

View file

@ -0,0 +1,48 @@
import * as Yup from 'yup'
import SecretInputFormik from 'src/components/inputs/formik/SecretInput'
import TextInputFormik from 'src/components/inputs/formik/TextInput'
export default {
code: 'twilio',
name: 'Twilio',
title: 'Twilio (SMS)',
elements: [
{
code: 'accountSid',
display: 'Account SID',
component: TextInputFormik
},
{
code: 'authToken',
display: 'Auth Token',
component: SecretInputFormik
},
{
code: 'fromNumber',
display: 'From Number',
component: TextInputFormik,
face: true
},
{
code: 'toNumber',
display: 'To Number',
component: TextInputFormik,
face: true
}
],
validationSchema: Yup.object().shape({
accountSid: Yup.string()
.max(100, 'Too long')
.required('Required'),
authToken: Yup.string()
.max(100, 'Too long')
.required('Required'),
fromNumber: Yup.string()
.max(100, 'Too long')
.required('Required'),
toNumber: Yup.string()
.max(100, 'Too long')
.required('Required')
})
}

View file

@ -9,7 +9,7 @@ import React, { useState } from 'react'
import LogsDowloaderPopover from 'src/components/LogsDownloaderPopper' import LogsDowloaderPopover from 'src/components/LogsDownloaderPopper'
import Title from 'src/components/Title' import Title from 'src/components/Title'
import { FeatureButton } from 'src/components/buttons' import { FeatureButton } from 'src/components/buttons'
import ExpTable from 'src/components/expandable-table/ExpTable' import DataTable from 'src/components/tables/DataTable'
import { ReactComponent as DownloadInverseIcon } from 'src/styling/icons/button/download/white.svg' import { ReactComponent as DownloadInverseIcon } from 'src/styling/icons/button/download/white.svg'
import { ReactComponent as Download } from 'src/styling/icons/button/download/zodiac.svg' import { ReactComponent as Download } from 'src/styling/icons/button/download/zodiac.svg'
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg' import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
@ -71,29 +71,34 @@ const Transactions = () => {
{ {
header: '', header: '',
width: 62, width: 62,
size: 'sm',
view: it => (it.txClass === 'cashOut' ? <TxOutIcon /> : <TxInIcon />) view: it => (it.txClass === 'cashOut' ? <TxOutIcon /> : <TxInIcon />)
}, },
{ {
header: 'Machine', header: 'Machine',
name: 'machineName', name: 'machineName',
width: 180, width: 180,
size: 'sm',
view: R.path(['machineName']) view: R.path(['machineName'])
}, },
{ {
header: 'Customer', header: 'Customer',
width: 162, width: 162,
size: 'sm',
view: getCustomerDisplayName view: getCustomerDisplayName
}, },
{ {
header: 'Cash', header: 'Cash',
width: 110, width: 110,
textAlign: 'right', textAlign: 'right',
size: 'sm',
view: it => `${Number.parseFloat(it.fiat)} ${it.fiatCode}` view: it => `${Number.parseFloat(it.fiat)} ${it.fiatCode}`
}, },
{ {
header: 'Crypto', header: 'Crypto',
width: 141, width: 141,
textAlign: 'right', textAlign: 'right',
size: 'sm',
view: it => view: it =>
`${toUnit(new BigNumber(it.cryptoAtoms), it.cryptoCode).toFormat(5)} ${ `${toUnit(new BigNumber(it.cryptoAtoms), it.cryptoCode).toFormat(5)} ${
it.cryptoCode it.cryptoCode
@ -103,27 +108,22 @@ const Transactions = () => {
header: 'Address', header: 'Address',
view: R.path(['toAddress']), view: R.path(['toAddress']),
className: classes.overflowTd, className: classes.overflowTd,
size: 'sm',
width: 136 width: 136
}, },
{ {
header: 'Date (UTC)', header: 'Date (UTC)',
view: it => moment.utc(it.created).format('YYYY-MM-D'), view: it => moment.utc(it.created).format('YYYY-MM-D'),
textAlign: 'right', textAlign: 'right',
size: 'sm',
width: 124 width: 124
}, },
{ {
header: 'Time (UTC)', header: 'Time (UTC)',
view: it => moment.utc(it.created).format('HH:mm:ss'), view: it => moment.utc(it.created).format('HH:mm:ss'),
textAlign: 'right', textAlign: 'right',
size: 'sm',
width: 124 width: 124
},
{
header: '', // Trade
view: () => {},
width: 90
},
{
width: 71
} }
] ]
@ -176,10 +176,11 @@ const Transactions = () => {
</div> </div>
</div> </div>
</div> </div>
<ExpTable <DataTable
elements={elements} elements={elements}
data={R.path(['transactions'])(txResponse)} data={R.path(['transactions'])(txResponse)}
Details={DetailsRow} Details={DetailsRow}
expandable
/> />
</> </>
) )

View file

@ -0,0 +1,99 @@
import { useQuery, useMutation } from '@apollo/react-hooks'
import { gql } from 'apollo-boost'
import * as R from 'ramda'
import React, { useState } from 'react'
import { NamespacedTable as EditableTable } from 'src/components/editableTable'
import TitleSection from 'src/components/layout/TitleSection'
import { fromNamespace, toNamespace } from 'src/utils/config'
import Wizard from './Wizard'
import { WalletSchema, getElements } from './helper'
const SAVE_CONFIG = gql`
mutation Save($config: JSONObject, $accounts: [JSONObject]) {
saveConfig(config: $config)
saveAccounts(accounts: $accounts)
}
`
const GET_INFO = gql`
query getData {
config
accounts
accountsConfig {
code
display
class
cryptos
}
cryptoCurrencies {
code
display
}
}
`
const Wallet = ({ name: SCREEN_KEY }) => {
const [wizard, setWizard] = useState(false)
const [error, setError] = useState(false)
const { data } = useQuery(GET_INFO)
const [saveConfig] = useMutation(SAVE_CONFIG, {
onCompleted: () => setWizard(false),
onError: () => setError(true),
refetchQueries: () => ['getData']
})
const save = (rawConfig, accounts) => {
const config = toNamespace(SCREEN_KEY)(rawConfig)
setError(false)
return saveConfig({ variables: { config, accounts } })
}
const config = data?.config && fromNamespace(SCREEN_KEY)(data.config)
const accountsConfig = data?.accountsConfig
const cryptoCurrencies = data?.cryptoCurrencies ?? []
const accounts = data?.accounts ?? []
const onToggle = id => {
const namespaced = fromNamespace(id)(config)
if (!WalletSchema.isValidSync(namespaced)) return setWizard(id)
save(toNamespace(id, { active: !namespaced?.active }))
}
return (
<>
<TitleSection title="Wallet Settings" error={error} />
<EditableTable
name="test"
namespaces={R.map(R.path(['code']))(cryptoCurrencies)}
data={config}
stripeWhen={it => !WalletSchema.isValidSync(it)}
enableEdit
editWidth={134}
enableToggle
toggleWidth={109}
onToggle={onToggle}
save={save}
validationSchema={WalletSchema}
disableRowEdit={R.compose(R.not, R.path(['active']))}
elements={getElements(cryptoCurrencies, accountsConfig)}
/>
{wizard && (
<Wizard
coin={R.find(R.propEq('code', wizard))(cryptoCurrencies)}
onClose={() => setWizard(false)}
save={save}
error={error}
cryptoCurrencies={cryptoCurrencies}
userAccounts={data?.config?.accounts}
accounts={accounts}
accountsConfig={accountsConfig}
/>
)}
</>
)
}
export default Wallet

View file

@ -1,451 +0,0 @@
import { useQuery, useMutation } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core'
import { gql } from 'apollo-boost'
import * as R from 'ramda'
import React, { useState } from 'react'
import ErrorMessage from 'src/components/ErrorMessage'
import Modal from 'src/components/Modal'
import Title from 'src/components/Title'
import {
Table,
THead,
Th,
TBody,
Tr,
Td
} from 'src/components/fake-table/Table'
import { Switch } from 'src/components/inputs'
import { ReactComponent as DisabledEditIcon } from 'src/styling/icons/action/edit/disabled.svg'
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg'
import { zircon } from 'src/styling/variables'
import Wizard from './Wizard'
import WizardSplash from './WizardSplash'
import {
CRYPTOCURRENCY_KEY,
TICKER_KEY,
WALLET_KEY,
EXCHANGE_KEY,
ZERO_CONF_KEY,
EDIT_KEY,
ENABLE_KEY,
SIZE_KEY,
TEXT_ALIGN_KEY
} from './aux.js'
const styles = {
disabledDrawing: {
position: 'relative',
display: 'flex',
alignItems: 'center',
'& > div': {
position: 'absolute',
backgroundColor: zircon,
height: 36,
width: 678
}
},
modal: {
width: 544
},
switchErrorMessage: {
margin: [['auto', 0, 'auto', 20]]
}
}
const useStyles = makeStyles(styles)
const columns = {
[CRYPTOCURRENCY_KEY]: {
[SIZE_KEY]: 182,
[TEXT_ALIGN_KEY]: 'left'
},
[TICKER_KEY]: {
[SIZE_KEY]: 182,
[TEXT_ALIGN_KEY]: 'left'
},
[WALLET_KEY]: {
[SIZE_KEY]: 182,
[TEXT_ALIGN_KEY]: 'left'
},
[EXCHANGE_KEY]: {
[SIZE_KEY]: 182,
[TEXT_ALIGN_KEY]: 'left'
},
[ZERO_CONF_KEY]: {
[SIZE_KEY]: 229,
[TEXT_ALIGN_KEY]: 'left'
},
[EDIT_KEY]: {
[SIZE_KEY]: 134,
[TEXT_ALIGN_KEY]: 'center'
},
[ENABLE_KEY]: {
[SIZE_KEY]: 109,
[TEXT_ALIGN_KEY]: 'center'
}
}
const GET_INFO = gql`
{
config
accounts {
code
display
class
cryptos
}
cryptoCurrencies {
code
display
}
}
`
const SAVE_CONFIG = gql`
mutation Save($config: JSONObject) {
saveConfig(config: $config)
}
`
const schema = {
[TICKER_KEY]: '',
[WALLET_KEY]: '',
[EXCHANGE_KEY]: '',
[ZERO_CONF_KEY]: '',
[ENABLE_KEY]: false
}
const WalletSettings = () => {
const [cryptoCurrencies, setCryptoCurrencies] = useState(null)
const [accounts, setAccounts] = useState(null)
const [services, setServices] = useState(null)
const [state, setState] = useState(null)
const [modalContent, setModalContent] = useState(null)
const [modalOpen, setModalOpen] = useState(false)
const [error, setError] = useState(null)
const [saveConfig] = useMutation(SAVE_CONFIG, {
onCompleted: data => {
setServices(data.saveConfig.accounts)
setError(null)
}
})
useQuery(GET_INFO, {
onCompleted: data => {
const { cryptoCurrencies, config, accounts } = data
const wallet = config?.wallet ?? []
const services = config?.accounts ?? {}
const newState = R.map(crypto => {
const el = R.find(R.propEq(CRYPTOCURRENCY_KEY, crypto.code))(wallet)
if (!el) return R.assoc(CRYPTOCURRENCY_KEY, crypto.code)(schema)
return el
})(cryptoCurrencies)
setState(newState)
setCryptoCurrencies(cryptoCurrencies)
setAccounts(accounts)
setServices(services)
},
onError: error => console.error(error)
})
const classes = useStyles()
const getSize = key => columns[key][SIZE_KEY]
const getTextAlign = key => columns[key][TEXT_ALIGN_KEY]
const getDisplayName = list => code =>
R.path(['display'], R.find(R.propEq('code', code), list))
const getCryptoDisplayName = row =>
getDisplayName(cryptoCurrencies)(row[CRYPTOCURRENCY_KEY])
const getNoSetUpNeeded = accounts => {
const needs = [
'bitgo',
'bitstamp',
'blockcypher',
'infura',
'kraken',
'strike'
]
return R.filter(account => !R.includes(account.code, needs), accounts)
}
const getAlreadySetUp = serviceClass => cryptocode => {
const possible = R.filter(
service => R.includes(cryptocode, service.cryptos),
R.filter(R.propEq('class', serviceClass), accounts)
)
const isSetUp = service => R.includes(service.code, R.keys(services))
const alreadySetUp = R.filter(isSetUp, possible)
const join = [...alreadySetUp, ...getNoSetUpNeeded(possible)]
return R.isEmpty(join) ? null : join
}
const getNotSetUp = serviceClass => cryptocode => {
const possible = R.filter(
service => R.includes(cryptocode, service.cryptos),
R.filter(R.propEq('class', serviceClass), accounts)
)
const without = R.without(
getAlreadySetUp(serviceClass)(cryptocode) ?? [],
possible
)
return R.isEmpty(without) ? null : without
}
const saveNewService = (code, it) => {
const newAccounts = R.clone(services)
newAccounts[code] = it
return saveConfig({ variables: { config: { accounts: newAccounts } } })
}
const save = it => {
const idx = R.findIndex(
R.propEq(CRYPTOCURRENCY_KEY, it[CRYPTOCURRENCY_KEY]),
state
)
const merged = R.mergeDeepRight(state[idx], it)
const updated = R.update(idx, merged, state)
return saveConfig({
variables: { config: { wallet: updated } }
})
}
const isSet = crypto =>
crypto[TICKER_KEY] &&
crypto[WALLET_KEY] &&
crypto[EXCHANGE_KEY] &&
crypto[ZERO_CONF_KEY]
const handleEnable = row => event => {
if (!isSet(row)) {
setModalContent(
<WizardSplash
code={row[CRYPTOCURRENCY_KEY]}
coinName={getCryptoDisplayName(row)}
handleModalNavigation={handleModalNavigation(row)}
/>
)
setModalOpen(true)
setError(null)
return
}
save(R.assoc(ENABLE_KEY, event.target.checked, row)).catch(error =>
setError(error)
)
}
const handleEditClick = row => {
setModalOpen(true)
handleModalNavigation(row)(1)
}
const handleModalClose = () => {
setModalOpen(false)
setModalContent(null)
}
const handleModalNavigation = row => currentPage => {
const cryptocode = row[CRYPTOCURRENCY_KEY]
switch (currentPage) {
case 1:
setModalContent(
<Wizard
crypto={row}
coinName={getCryptoDisplayName(row)}
handleModalNavigation={handleModalNavigation}
pageName={TICKER_KEY}
currentStage={1}
alreadySetUp={R.filter(
ticker => R.includes(cryptocode, ticker.cryptos),
R.filter(R.propEq('class', 'ticker'), accounts)
)}
/>
)
break
case 2:
setModalContent(
<Wizard
crypto={row}
coinName={getCryptoDisplayName(row)}
handleModalNavigation={handleModalNavigation}
pageName={WALLET_KEY}
currentStage={2}
alreadySetUp={getAlreadySetUp(WALLET_KEY)(cryptocode)}
notSetUp={getNotSetUp(WALLET_KEY)(cryptocode)}
saveNewService={saveNewService}
/>
)
break
case 3:
setModalContent(
<Wizard
crypto={row}
coinName={getCryptoDisplayName(row)}
handleModalNavigation={handleModalNavigation}
pageName={EXCHANGE_KEY}
currentStage={3}
alreadySetUp={getAlreadySetUp(EXCHANGE_KEY)(cryptocode)}
notSetUp={getNotSetUp(EXCHANGE_KEY)(cryptocode)}
saveNewService={saveNewService}
/>
)
break
case 4:
setModalContent(
<Wizard
crypto={row}
coinName={getCryptoDisplayName(row)}
handleModalNavigation={handleModalNavigation}
pageName={ZERO_CONF_KEY}
currentStage={4}
alreadySetUp={getAlreadySetUp(ZERO_CONF_KEY)(cryptocode)}
notSetUp={getNotSetUp(ZERO_CONF_KEY)(cryptocode)}
saveNewService={saveNewService}
/>
)
break
case 5:
// Zero Conf
return save(R.assoc(ENABLE_KEY, true, row)).then(m => {
setModalOpen(false)
setModalContent(null)
})
default:
break
}
return new Promise(() => {})
}
if (!state) return null
return (
<>
<div className={classes.titleWrapper}>
<div className={classes.titleAndButtonsContainer}>
<Title>Wallet Settings</Title>
{error && !modalOpen && (
<ErrorMessage className={classes.switchErrorMessage}>
Failed to save
</ErrorMessage>
)}
</div>
</div>
<div className={classes.wrapper}>
<Table>
<THead>
<Th
size={getSize(CRYPTOCURRENCY_KEY)}
textAlign={getTextAlign(CRYPTOCURRENCY_KEY)}>
Cryptocurrency
</Th>
<Th size={getSize(TICKER_KEY)} textAlign={getTextAlign(TICKER_KEY)}>
Ticker
</Th>
<Th size={getSize(WALLET_KEY)} textAlign={getTextAlign(WALLET_KEY)}>
Wallet
</Th>
<Th
size={getSize(EXCHANGE_KEY)}
textAlign={getTextAlign(EXCHANGE_KEY)}>
Exchange
</Th>
<Th
size={getSize(ZERO_CONF_KEY)}
textAlign={getTextAlign(ZERO_CONF_KEY)}>
Zero Conf
</Th>
<Th size={getSize(EDIT_KEY)} textAlign={getTextAlign(EDIT_KEY)}>
Edit
</Th>
<Th size={getSize(ENABLE_KEY)} textAlign={getTextAlign(ENABLE_KEY)}>
Enable
</Th>
</THead>
<TBody>
{state.map((row, idx) => (
<Tr key={idx}>
<Td
size={getSize(CRYPTOCURRENCY_KEY)}
textAlign={getTextAlign(CRYPTOCURRENCY_KEY)}>
{getCryptoDisplayName(row)}
</Td>
{!isSet(row) && (
<Td
size={
getSize(TICKER_KEY) +
getSize(WALLET_KEY) +
getSize(EXCHANGE_KEY) +
getSize(ZERO_CONF_KEY)
}
textAlign="center"
className={classes.disabledDrawing}>
<div />
</Td>
)}
{isSet(row) && (
<>
<Td
size={getSize(TICKER_KEY)}
textAlign={getTextAlign(TICKER_KEY)}>
{getDisplayName(accounts)(row[TICKER_KEY])}
</Td>
<Td
size={getSize(WALLET_KEY)}
textAlign={getTextAlign(WALLET_KEY)}>
{getDisplayName(accounts)(row[WALLET_KEY])}
</Td>
<Td
size={getSize(EXCHANGE_KEY)}
textAlign={getTextAlign(EXCHANGE_KEY)}>
{getDisplayName(accounts)(row[EXCHANGE_KEY])}
</Td>
<Td
size={getSize(ZERO_CONF_KEY)}
textAlign={getTextAlign(ZERO_CONF_KEY)}>
{getDisplayName(accounts)(row[ZERO_CONF_KEY])}
</Td>
</>
)}
<Td size={getSize(EDIT_KEY)} textAlign={getTextAlign(EDIT_KEY)}>
{!isSet(row) && <DisabledEditIcon />}
{isSet(row) && (
<button
className={classes.iconButton}
onClick={() => handleEditClick(row)}>
<EditIcon />
</button>
)}
</Td>
<Td
size={getSize(ENABLE_KEY)}
textAlign={getTextAlign(ENABLE_KEY)}>
<Switch
checked={row[ENABLE_KEY]}
onChange={handleEnable(row)}
value={row[CRYPTOCURRENCY_KEY]}
/>
</Td>
</Tr>
))}
</TBody>
</Table>
</div>
<Modal
aria-labelledby="simple-modal-title"
aria-describedby="simple-modal-description"
open={modalOpen}
handleClose={handleModalClose}
className={classes.modal}>
{modalContent}
</Modal>
</>
)
}
export default WalletSettings

View file

@ -1,306 +1,107 @@
import { makeStyles } from '@material-ui/core'
import classnames from 'classnames'
import { Formik, Field as FormikField } from 'formik'
import * as R from 'ramda' import * as R from 'ramda'
import React, { useState, useEffect } from 'react' import React, { useState } from 'react'
import ErrorMessage from 'src/components/ErrorMessage' import Modal from 'src/components/Modal'
import Stage from 'src/components/Stage' import schema from 'src/pages/Services/schemas'
import { Button } from 'src/components/buttons' import { toNamespace } from 'src/utils/config'
import { RadioGroup, AutocompleteSelect } from 'src/components/inputs'
import { H1, Info2, H4 } from 'src/components/typography'
import { startCase } from 'src/utils/string'
import { getBitgoFields, getBitgoFormik } from '../Services/Bitgo' import WizardSplash from './WizardSplash'
import { getBitstampFields, getBitstampFormik } from '../Services/Bitstamp' import WizardStep from './WizardStep'
import {
getBlockcypherFields,
getBlockcypherFormik
} from '../Services/Blockcypher'
import { getInfuraFields, getInfuraFormik } from '../Services/Infura'
import { getKrakenFields, getKrakenFormik } from '../Services/Kraken'
import { getStrikeFields, getStrikeFormik } from '../Services/Strike'
const styles = { const LAST_STEP = 4
modalContent: { const MODAL_WIDTH = 554
display: 'flex',
flexDirection: 'column', const contains = crypto => R.compose(R.contains(crypto), R.prop('cryptos'))
padding: [[24, 32, 0]], const sameClass = type => R.propEq('class', type)
'& > h1': { const filterConfig = (crypto, type) =>
margin: [[0, 0, 10]] R.filter(it => sameClass(type)(it) && contains(crypto)(it))
},
'& > h4': { const getItems = (accountsConfig, accounts, type, crypto) => {
margin: [[32, 0, 32 - 9, 0]] const fConfig = filterConfig(crypto, type)(accountsConfig)
}, const find = code => R.find(R.propEq('code', code))(accounts)
'& > p': {
margin: 0 const [filled, unfilled] = R.partition(({ code }) => {
} const account = find(code)
}, if (!schema[code]) return true
submitButtonWrapper: {
display: 'flex', const { validationSchema } = schema[code]
alignSelf: 'flex-end', return validationSchema.isValidSync(account)
margin: [['auto', 0, 0]] })(fConfig)
},
submitButton: { return { filled, unfilled }
width: 67,
padding: [[0, 0]],
margin: [['auto', 0, 24, 20]],
'&:active': {
margin: [['auto', 0, 24, 20]]
}
},
stages: {
marginTop: 10
},
radios: {
display: 'flex'
},
radiosAsColumn: {
flexDirection: 'column'
},
radiosAsRow: {
flexDirection: 'row'
},
alreadySetupRadioButtons: {
display: 'flex',
flexDirection: 'row'
},
selectNewWrapper: {
display: 'flex',
alignItems: 'center'
},
selectNew: {
width: 204,
flexGrow: 0,
bottom: 7
},
newServiceForm: {
display: 'flex',
flexDirection: 'column'
},
newServiceFormFields: {
marginTop: 20,
marginBottom: 48
},
field: {
'&:not(:last-child)': {
marginBottom: 20
}
},
formInput: {
'& .MuiInputBase-input': {
width: 426
}
}
} }
const getNewServiceForm = serviceName => { const Wizard = ({ coin, onClose, accountsConfig, accounts, save, error }) => {
switch (serviceName) { const [{ step, config, accountsToSave }, setState] = useState({
case 'bitgo': step: 0,
return { fields: getBitgoFields(), formik: getBitgoFormik() } config: { active: true },
case 'bitstamp': accountsToSave: []
return { fields: getBitstampFields(), formik: getBitstampFormik() } })
case 'blockcypher':
return { fields: getBlockcypherFields(), formik: getBlockcypherFormik() } const title = `Enable ${coin.display}`
case 'infura': const isLastStep = step === LAST_STEP
return { fields: getInfuraFields(), formik: getInfuraFormik() }
case 'kraken': const tickers = { filled: filterConfig(coin.code, 'ticker')(accountsConfig) }
return { fields: getKrakenFields(), formik: getKrakenFormik() } const wallets = getItems(accountsConfig, accounts, 'wallet', coin.code)
case 'strike': const exchanges = getItems(accountsConfig, accounts, 'exchange', coin.code)
return { fields: getStrikeFields(), formik: getStrikeFormik() } const zeroConfs = getItems(accountsConfig, accounts, 'zeroConf', coin.code)
default:
const getValue = code => R.find(R.propEq('code', code))(accounts)
const onContinue = async (it, it2) => {
const newConfig = R.merge(config, it)
const newAccounts = it2 ? R.concat(accountsToSave, [it2]) : accountsToSave
if (isLastStep) {
return save(toNamespace(coin.code, newConfig), newAccounts)
}
setState({
step: step + 1,
config: newConfig,
accountsToSave: newAccounts
})
} }
}
const useStyles = makeStyles(styles) const getStepData = () => {
switch (step) {
const SubmitButton = ({ error, ...props }) => { case 1:
const classes = useStyles() return { type: 'ticker', ...tickers }
case 2:
return { type: 'wallet', ...wallets }
case 3:
return { type: 'exchange', ...exchanges }
case 4:
return { type: 'zeroConf', name: 'zero conf', ...zeroConfs }
default:
return null
}
}
return ( return (
<div className={classes.submitButtonWrapper}> <Modal
{error && <ErrorMessage>Failed to save</ErrorMessage>} title={step === 0 ? null : title}
<Button {...props}>Next</Button> handleClose={onClose}
</div> width={MODAL_WIDTH}
) open={true}>
} {step === 0 && (
<WizardSplash
const Wizard = ({ code={coin.code}
crypto, name={coin.display}
coinName, onContinue={() => onContinue()}
pageName,
currentStage,
alreadySetUp,
notSetUp,
handleModalNavigation,
saveNewService
}) => {
const [selectedRadio, setSelectedRadio] = useState(
crypto[pageName] !== '' ? crypto[pageName] : null
)
useEffect(() => {
setFormContent(null)
setSelectedFromDropdown(null)
setSetUpNew('')
setSelectedRadio(crypto[pageName] !== '' ? crypto[pageName] : null)
}, [crypto, pageName])
const [setUpNew, setSetUpNew] = useState(null)
const [selectedFromDropdown, setSelectedFromDropdown] = useState(null)
const [formContent, setFormContent] = useState(null)
const [error, setError] = useState(null)
const classes = useStyles()
const radiosClassNames = {
[classes.radios]: true,
[classes.radiosAsColumn]: !selectedFromDropdown,
[classes.radiosAsRow]: selectedFromDropdown
}
const radioButtonOptions =
alreadySetUp &&
R.map(el => {
return { label: el.display, value: el.code }
})(alreadySetUp)
const handleRadioButtons = event => {
R.o(setSelectedRadio, R.path(['target', 'value']))(event)
setSetUpNew('')
setFormContent(null)
setSelectedFromDropdown(null)
setError(null)
}
const handleSetUpNew = event => {
R.o(setSetUpNew, R.path(['target', 'value']))(event)
setSelectedRadio('')
setFormContent(null)
setSelectedFromDropdown(null)
setError(null)
}
const handleNext = value => event => {
const nav = handleModalNavigation(
R.mergeDeepRight(crypto, { [pageName]: value })
)(currentStage + 1)
nav.catch(error => setError(error))
}
const handleSelectFromDropdown = it => {
setSelectedFromDropdown(it)
setFormContent(getNewServiceForm(it?.code))
setError(null)
}
const isSubmittable = () => {
if (selectedRadio) return true
if (!selectedRadio && selectedFromDropdown && !formContent) return true
return false
}
console.log(formContent)
return (
<div className={classes.modalContent}>
<H1>Enable {coinName}</H1>
<Info2>{startCase(pageName)}</Info2>
<Stage
stages={4}
currentStage={currentStage}
color="spring"
className={classes.stages}
/>
<H4>{`Select a ${pageName} or set up a new one`}</H4>
<div className={classnames(radiosClassNames)}>
{alreadySetUp && (
<RadioGroup
name="already-setup-select"
value={selectedRadio || radioButtonOptions[0]}
options={radioButtonOptions}
ariaLabel="already-setup-select"
onChange={handleRadioButtons}
className={classes.alreadySetupRadioButtons}
/>
)}
{notSetUp && (
<div className={classes.selectNewWrapper}>
<RadioGroup
name="setup-new-select"
value={setUpNew || ''}
options={[{ label: 'Set up new', value: 'new' }]}
ariaLabel="setup-new-select"
onChange={handleSetUpNew}
className={classes.alreadySetupRadioButtons}
/>
{setUpNew && (
<AutocompleteSelect
id="chooseNew"
name="chooseNew"
label={`Select ${pageName}`}
suggestions={notSetUp}
value={selectedFromDropdown}
handleChange={handleSelectFromDropdown}
className={classes.selectNew}
/>
)}
</div>
)}
</div>
{formContent && (
<Formik
initialValues={formContent.formik.initialValues}
validationSchema={formContent.formik.validationSchema}
onSubmit={values =>
saveNewService(selectedFromDropdown.code, values)
.then(m => {
handleNext(selectedFromDropdown.code)()
})
.catch(error => setError(error))
}>
{props => (
<form
onReset={props.handleReset}
onSubmit={props.handleSubmit}
className={classes.newServiceForm}
{...props}>
<div className={classes.newServiceFormFields}>
{formContent.fields.map((field, idx) => (
<div key={idx} className={classes.field}>
<FormikField
id={field.name}
name={field.name}
component={field.component}
placeholder={field.placeholder}
type={field.type}
label={field.label}
className={classes.formInput}
onFocus={() => {
setError(null)
}}
/>
</div>
))}
</div>
<SubmitButton
disabled={R.isEmpty(props.touched) || !props.isValid}
className={classes.submitButton}
type="submit"
error={error}
/>
</form>
)}
</Formik>
)}
{!formContent && (
<SubmitButton
className={classes.submitButton}
disabled={!isSubmittable()}
onClick={handleNext(selectedRadio || selectedFromDropdown?.code)}
error={error}
/> />
)} )}
</div> {step !== 0 && (
<WizardStep
step={step}
error={error}
lastStep={isLastStep}
{...getStepData()}
onContinue={onContinue}
getValue={getValue}
/>
)}
</Modal>
) )
} }

View file

@ -11,71 +11,64 @@ import { ReactComponent as LitecoinLogo } from 'src/styling/logos/icon-litecoin-
import { ReactComponent as ZCashLogo } from 'src/styling/logos/icon-zcash-colour.svg' import { ReactComponent as ZCashLogo } from 'src/styling/logos/icon-zcash-colour.svg'
const styles = { const styles = {
logoWrapper: { logo: {
display: 'flex', maxHeight: 80,
justifyContent: 'center', maxWidth: 200
alignItems: 'center', },
height: 80, title: {
margin: [[40, 0, 24]], margin: [[24, 0, 32, 0]]
'& > svg': { },
maxHeight: '100%', text: {
width: '100%' margin: 0
} },
button: {
marginTop: 'auto',
marginBottom: 58
}, },
modalContent: { modalContent: {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
padding: [[0, 66]], padding: [[0, 42]],
'& > h1': { flex: 1
margin: [[0, 0, 32]]
},
'& > p': {
margin: 0
},
'& > button': {
margin: [['auto', 0, 56]],
'&:active': {
margin: [['auto', 0, 56]]
}
}
} }
} }
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const renderLogo = code => { const getLogo = code => {
switch (code) { switch (code) {
case 'BTC': case 'BTC':
return <BitcoinLogo /> return BitcoinLogo
case 'BCH': case 'BCH':
return <BitcoinCashLogo /> return BitcoinCashLogo
case 'DASH': case 'DASH':
return <DashLogo /> return DashLogo
case 'ETH': case 'ETH':
return <EthereumLogo /> return EthereumLogo
case 'LTC': case 'LTC':
return <LitecoinLogo /> return LitecoinLogo
case 'ZEC': case 'ZEC':
return <ZCashLogo /> return ZCashLogo
default: default:
return null return null
} }
} }
const WizardSplash = ({ code, coinName, handleModalNavigation }) => { const WizardSplash = ({ code, name, onContinue }) => {
const classes = useStyles() const classes = useStyles()
const Logo = getLogo(code)
return ( return (
<div className={classes.modalContent}> <div className={classes.modalContent}>
<div className={classes.logoWrapper}>{renderLogo(code)}</div> <Logo className={classes.logo} />
<H1>Enable {coinName}</H1> <H1 className={classes.title}>Enable {name}</H1>
<P> <P className={classes.text}>
You are about to enable {coinName} on your system. This will allow you You are about to enable {name} on your system. This will allow you to
to use this cryptocurrency on your machines. To able to do that, youll use this cryptocurrency on your machines. To be able to do that, youll
have to setup all the necessary 3rd party services. have to setup all the necessary 3rd party services.
</P> </P>
<Button onClick={() => handleModalNavigation(1)}> <Button className={classes.button} onClick={onContinue}>
Start configuration Start configuration
</Button> </Button>
</div> </div>

View file

@ -0,0 +1,155 @@
import { makeStyles } from '@material-ui/core'
import classnames from 'classnames'
import * as R from 'ramda'
import React, { useReducer, useEffect } from 'react'
import ErrorMessage from 'src/components/ErrorMessage'
import Stepper from 'src/components/Stepper'
import { Button } from 'src/components/buttons'
import { RadioGroup, Autocomplete } from 'src/components/inputs'
import { Info2, H4 } from 'src/components/typography'
import FormRenderer from 'src/pages/Services/FormRenderer'
import schema from 'src/pages/Services/schemas'
import { startCase } from 'src/utils/string'
import styles from './WizardStep.styles'
const useStyles = makeStyles(styles)
const initialState = {
form: null,
selected: null,
isNew: false,
iError: false
}
const reducer = (state, action) => {
switch (action.type) {
case 'select':
return {
form: null,
selected: action.selected,
isNew: null,
iError: false
}
case 'new':
return { form: state.form, selected: null, isNew: true, iError: false }
case 'form':
return {
form: action.form,
selected: action.form.code,
isNew: true,
iError: false
}
case 'error':
return R.merge(state, { iError: true })
case 'reset':
return initialState
default:
throw new Error()
}
}
const WizardStep = ({
type,
name,
step,
error,
lastStep,
onContinue,
filled,
unfilled,
getValue
}) => {
const classes = useStyles()
const [{ iError, selected, form, isNew }, dispatch] = useReducer(
reducer,
initialState
)
useEffect(() => {
dispatch({ type: 'reset' })
}, [step])
const iContinue = (config, account) => {
if (!config || !config[type]) {
return dispatch({ type: 'error' })
}
onContinue(config, account)
}
const label = lastStep ? 'Finish' : 'Next'
const displayName = name ?? type
const subtitleClass = {
[classes.subtitle]: true,
[classes.error]: iError
}
return (
<>
<Info2 className={classes.title}>{startCase(type)}</Info2>
<Stepper steps={4} currentStep={step} />
<H4 className={classnames(subtitleClass)}>
Select a {displayName} or set up a new one
</H4>
<RadioGroup
options={filled}
value={selected}
className={classes.radioGroup}
onChange={(evt, it) => {
dispatch({ type: 'select', selected: it })
}}
labelClassName={classes.radioLabel}
radioClassName={classes.radio}
/>
<div className={classes.setupNew}>
{!R.isEmpty(unfilled) && !R.isNil(unfilled) && (
<RadioGroup
value={isNew}
onChange={(evt, it) => {
dispatch({ type: 'new' })
}}
labelClassName={classes.radioLabel}
radioClassName={classes.radio}
options={[{ display: 'Set up new', code: true }]}
/>
)}
{isNew && (
<Autocomplete
fullWidth
label={`Select ${displayName}`}
className={classes.picker}
getOptionSelected={R.eqProps('code')}
getLabel={R.path(['display'])}
options={unfilled}
onChange={(evt, it) => {
dispatch({ type: 'form', form: it })
}}
/>
)}
</div>
{form && (
<FormRenderer
save={it =>
iContinue({ [type]: form.code }, R.merge(it, { code: form.code }))
}
elements={schema[form.code].elements}
validationSchema={schema[form.code].validationSchema}
value={getValue(form.code)}
buttonLabel={label}
/>
)}
{!form && (
<div className={classes.submit}>
{error && <ErrorMessage>Failed to save</ErrorMessage>}
<Button
className={classes.button}
onClick={() => iContinue({ [type]: selected })}>
{label}
</Button>
</div>
)}
</>
)
}
export default WizardStep

View file

@ -0,0 +1,42 @@
import { errorColor } from 'src/styling/variables'
const LABEL_WIDTH = 150
export default {
title: {
margin: [[0, 0, 12, 0]]
},
subtitle: {
margin: [[32, 0, 21, 0]]
},
error: {
color: errorColor
},
button: {
marginLeft: 'auto'
},
submit: {
display: 'flex',
flexDirection: 'row',
margin: [['auto', 0, 24]]
},
radioGroup: {
flexDirection: 'row'
},
radioLabel: {
width: LABEL_WIDTH,
height: 48
},
radio: {
padding: 4,
margin: 4
},
setupNew: {
display: 'flex',
alignItems: 'center',
height: 48
},
picker: {
width: LABEL_WIDTH
}
}

View file

@ -1,21 +0,0 @@
const CRYPTOCURRENCY_KEY = 'cryptocurrency'
const TICKER_KEY = 'ticker'
const WALLET_KEY = 'wallet'
const EXCHANGE_KEY = 'exchange'
const ZERO_CONF_KEY = 'zeroConf'
const EDIT_KEY = 'edit'
const ENABLE_KEY = 'enabled'
const SIZE_KEY = 'size'
const TEXT_ALIGN_KEY = 'textAlign'
export {
CRYPTOCURRENCY_KEY,
TICKER_KEY,
WALLET_KEY,
EXCHANGE_KEY,
ZERO_CONF_KEY,
EDIT_KEY,
ENABLE_KEY,
SIZE_KEY,
TEXT_ALIGN_KEY
}

View file

@ -0,0 +1,103 @@
import * as R from 'ramda'
import * as Yup from 'yup'
import Autocomplete from 'src/components/inputs/formik/Autocomplete.js'
const filterClass = type => R.filter(it => it.class === type)
const filterCoins = ({ id }) => R.filter(it => R.contains(id)(it.cryptos))
const WalletSchema = Yup.object().shape({
ticker: Yup.string().required('Required'),
wallet: Yup.string().required('Required'),
exchange: Yup.string().required('Required'),
zeroConf: Yup.string().required('Required')
})
const getElements = (cryptoCurrencies, accounts) => {
const viewCryptoCurrency = it =>
R.compose(
R.prop(['display']),
R.find(R.propEq('code', it))
)(cryptoCurrencies)
const filterOptions = type => filterClass(type)(accounts || [])
const getDisplayName = type => it =>
R.compose(
R.prop('display'),
R.find(R.propEq('code', it))
)(filterOptions(type))
const getOptions = R.curry((option, it) =>
filterCoins(it)(filterOptions(option))
)
return [
{
name: 'id',
header: 'Cryptocurrency',
width: 180,
view: viewCryptoCurrency,
size: 'sm',
editable: false
},
{
name: 'ticker',
size: 'sm',
stripe: true,
view: getDisplayName('ticker'),
width: 190,
input: Autocomplete,
inputProps: {
options: getOptions('ticker'),
valueProp: 'code',
getLabel: R.path(['display']),
limit: null
}
},
{
name: 'wallet',
size: 'sm',
stripe: true,
view: getDisplayName('wallet'),
width: 190,
input: Autocomplete,
inputProps: {
options: getOptions('wallet'),
valueProp: 'code',
getLabel: R.path(['display']),
limit: null
}
},
{
name: 'exchange',
size: 'sm',
stripe: true,
view: getDisplayName('exchange'),
width: 190,
input: Autocomplete,
inputProps: {
options: getOptions('exchange'),
valueProp: 'code',
getLabel: R.path(['display']),
limit: null
}
},
{
name: 'zeroConf',
size: 'sm',
stripe: true,
view: getDisplayName('zeroConf'),
input: Autocomplete,
width: 190,
inputProps: {
options: getOptions('zeroConf'),
valueProp: 'code',
getLabel: R.path(['display']),
limit: null
}
}
]
}
export { WalletSchema, getElements, filterClass }

View file

@ -5,9 +5,10 @@ import moment from 'moment'
import * as R from 'ramda' import * as R from 'ramda'
import React from 'react' import React from 'react'
import DataTable from 'src/components/tables/DataTable'
import { MainStatus } from '../../components/Status' import { MainStatus } from '../../components/Status'
import Title from '../../components/Title' import Title from '../../components/Title'
import ExpTable from '../../components/expandable-table/ExpTable'
import { ReactComponent as WarningIcon } from '../../styling/icons/status/pumpkin.svg' import { ReactComponent as WarningIcon } from '../../styling/icons/status/pumpkin.svg'
import { ReactComponent as ErrorIcon } from '../../styling/icons/status/tomato.svg' import { ReactComponent as ErrorIcon } from '../../styling/icons/status/tomato.svg'
import { mainStyles } from '../Transactions/Transactions.styles' import { mainStyles } from '../Transactions/Transactions.styles'
@ -42,35 +43,37 @@ const MachineStatus = () => {
{ {
header: 'Machine Name', header: 'Machine Name',
width: 232, width: 232,
size: 'sm',
textAlign: 'left', textAlign: 'left',
view: m => m.name view: m => m.name
}, },
{ {
header: 'Status', header: 'Status',
width: 349, width: 349,
size: 'sm',
textAlign: 'left', textAlign: 'left',
view: m => <MainStatus statuses={m.statuses} /> view: m => <MainStatus statuses={m.statuses} />
}, },
{ {
header: 'Last ping', header: 'Last ping',
width: 192, width: 192,
size: 'sm',
textAlign: 'left', textAlign: 'left',
view: m => moment(m.lastPing).fromNow() view: m => moment(m.lastPing).fromNow()
}, },
{ {
header: 'Ping Time', header: 'Ping Time',
width: 155, width: 155,
size: 'sm',
textAlign: 'left', textAlign: 'left',
view: m => m.pingTime || 'unknown' view: m => m.pingTime || 'unknown'
}, },
{ {
header: 'Software Version', header: 'Software Version',
width: 201, width: 201,
size: 'sm',
textAlign: 'left', textAlign: 'left',
view: m => m.softwareVersion || 'unknown' view: m => m.softwareVersion || 'unknown'
},
{
width: 71
} }
] ]
@ -91,10 +94,11 @@ const MachineStatus = () => {
</div> </div>
</div> </div>
</div> </div>
<ExpTable <DataTable
elements={elements} elements={elements}
data={R.path(['machines'])(machinesResponse)} data={R.path(['machines'])(machinesResponse)}
Details={MachineDetailsRow} Details={MachineDetailsRow}
expandable
/> />
</> </>
) )

View file

@ -13,7 +13,7 @@ import OperatorInfo from 'src/pages/OperatorInfo/OperatorInfo'
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 Transactions from 'src/pages/Transactions/Transactions' import Transactions from 'src/pages/Transactions/Transactions'
import WalletSettings from 'src/pages/Wallet/WalletSettings' import WalletSettings from 'src/pages/Wallet/Wallet'
import MachineStatus from 'src/pages/maintenance/MachineStatus' import MachineStatus from 'src/pages/maintenance/MachineStatus'
const tree = [ const tree = [

View file

@ -13,7 +13,7 @@ import extendJss from 'jss-plugin-extend'
import React from 'react' import React from 'react'
import { ActionButton, Button, Link } from 'src/components/buttons' import { ActionButton, Button, Link } from 'src/components/buttons'
import { Radio, TextInput, Switch } from 'src/components/inputs' import { TextInput, Switch } from 'src/components/inputs'
import { ReactComponent as AuthorizeIconReversed } from 'src/styling/icons/button/authorize/white.svg' import { ReactComponent as AuthorizeIconReversed } from 'src/styling/icons/button/authorize/white.svg'
import { ReactComponent as AuthorizeIcon } from 'src/styling/icons/button/authorize/zodiac.svg' import { ReactComponent as AuthorizeIcon } from 'src/styling/icons/button/authorize/zodiac.svg'
@ -178,8 +178,6 @@ story.add('ConfirmDialog', () => (
</Wrapper> </Wrapper>
)) ))
story.add('Radio', () => <Radio label="Hehe" />)
const typographyStory = storiesOf('Typography', module) const typographyStory = storiesOf('Typography', module)
typographyStory.add('H1', () => <H1>Hehehe</H1>) typographyStory.add('H1', () => <H1>Hehehe</H1>)

View file

@ -41,6 +41,10 @@ export default {
}, },
'button::-moz-focus-inner': { 'button::-moz-focus-inner': {
border: 0 border: 0
},
// forcing styling onto inner container
'.ReactVirtualized__Grid__innerScrollContainer': {
overflow: 'inherit !important'
} }
} }
} }

View file

@ -0,0 +1,28 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="36px">
<defs>
<pattern
id="pattern_68JiZ"
patternUnits="userSpaceOnUse"
width="5.5"
height="5.5"
patternTransform="rotate(45)">
<line
x1="0"
y="0"
x2="0"
y2="5.5"
stroke="#DBDFED"
stroke-width="3"
/>
</pattern>
</defs>{' '}
<rect
width="100%"
height="100%"
fill="url(#pattern_68JiZ)"
opacity="1"
/>
</svg>

After

Width:  |  Height:  |  Size: 494 B

View file

@ -1,4 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="104" height="64" version="1.1"> <svg xmlns="http://www.w3.org/2000/svg" width="104" height="64">
<circle fill="#F7931A" cx="52" cy="32" r="32"/> <circle cx="52" cy="32" r="32" fill="#F7931A" />
<path fill="#FFF" d="m66.103,27.444c0.637-4.258-2.605-6.547-7.038-8.074l1.438-5.768-3.511-0.875-1.4,5.616c-0.923-0.23-1.871-0.447-2.813-0.662l1.41-5.653-3.509-0.875-1.439,5.766c-0.764-0.174-1.514-0.346-2.242-0.527l0.004-0.018-4.842-1.209-0.934,3.75s2.605,0.597,2.55,0.634c1.422,0.355,1.679,1.296,1.636,2.042l-1.638,6.571c0.098,0.025,0.225,0.061,0.365,0.117-0.117-0.029-0.242-0.061-0.371-0.092l-2.296,9.205c-0.174,0.432-0.615,1.08-1.609,0.834,0.035,0.051-2.552-0.637-2.552-0.637l-1.743,4.019,4.569,1.139c0.85,0.213,1.683,0.436,2.503,0.646l-1.453,5.834,3.507,0.875,1.439-5.772c0.958,0.26,1.888,0.5,2.798,0.726l-1.434,5.745,3.511,0.875,1.453-5.823c5.987,1.133,10.489,0.676,12.384-4.739,1.527-4.36-0.076-6.875-3.226-8.515,2.294-0.529,4.022-2.038,4.483-5.155zm-8.022,11.249c-1.085,4.36-8.426,2.003-10.806,1.412l1.928-7.729c2.38,0.594,10.012,1.77,8.878,6.317zm1.086-11.312c-0.99,3.966-7.1,1.951-9.082,1.457l1.748-7.01c1.982,0.494,8.365,1.416,7.334,5.553z"/> <path
d="m66.1 27.4c0.6-4.3-2.6-6.5-7-8.1l1.4-5.8-3.5-0.9-1.4 5.6c-0.9-0.2-1.9-0.4-2.8-0.7l1.4-5.7-3.5-0.9-1.4 5.8c-0.8-0.2-1.5-0.3-2.2-0.5l0 0-4.8-1.2-0.9 3.8s2.6 0.6 2.6 0.6c1.4 0.4 1.7 1.3 1.6 2l-1.6 6.6c0.1 0 0.2 0.1 0.4 0.1-0.1 0-0.2-0.1-0.4-0.1l-2.3 9.2c-0.2 0.4-0.6 1.1-1.6 0.8 0 0.1-2.6-0.6-2.6-0.6l-1.7 4 4.6 1.1c0.9 0.2 1.7 0.4 2.5 0.6l-1.5 5.8 3.5 0.9 1.4-5.8c1 0.3 1.9 0.5 2.8 0.7l-1.4 5.7 3.5 0.9 1.5-5.8c6 1.1 10.5 0.7 12.4-4.7 1.5-4.4-0.1-6.9-3.2-8.5 2.3-0.5 4-2 4.5-5.2zm-8 11.2c-1.1 4.4-8.4 2-10.8 1.4l1.9-7.7c2.4 0.6 10 1.8 8.9 6.3zm1.1-11.3c-1 4-7.1 2-9.1 1.5l1.7-7c2 0.5 8.4 1.4 7.3 5.6z"
fill="#FFF"
/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 758 B

Before After
Before After

Some files were not shown because too many files have changed in this diff Show more