fix: rework wallet screen
This commit is contained in:
parent
1f7ae74b42
commit
1f6d272aa0
103 changed files with 2094 additions and 3892 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ const ACCOUNT_LIST = [
|
|||
{ code: 'bitstamp', display: 'Bitstamp', 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: '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: 'no-layer2', display: 'No Layer 2', class: LAYER_2, cryptos: ALL_CRYPTOS },
|
||||
{ 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: 'itbit', display: 'itBit', class: EXCHANGE, cryptos: [BTC] },
|
||||
{ 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: 'mock-exchange', display: 'Mock exchange', class: EXCHANGE, cryptos: ALL_CRYPTOS },
|
||||
{ code: 'mock-sms', display: 'Mock SMS', class: SMS },
|
||||
{ code: 'mock-id-verify', display: 'Mock ID verifier', class: ID_VERIFIER },
|
||||
{ code: 'mock-exchange', display: 'Mock exchange', class: EXCHANGE, cryptos: ALL_CRYPTOS, dev: true },
|
||||
{ code: 'mock-sms', display: 'Mock SMS', class: SMS, dev: true },
|
||||
{ code: 'mock-id-verify', display: 'Mock ID verifier', class: ID_VERIFIER, dev: true },
|
||||
{ code: 'twilio', display: 'Twilio', class: SMS },
|
||||
{ code: 'mailgun', display: 'Mailgun', class: EMAIL },
|
||||
{ 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: '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 }
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ const funding = require('../funding')
|
|||
const supervisor = require('../supervisor')
|
||||
const serverLogs = require('../server-logs')
|
||||
const pairing = require('../pairing')
|
||||
const { accounts, coins, countries, currencies, languages } = require('../config')
|
||||
const { accounts: accountsConfig, coins, countries, currencies, languages } = require('../config')
|
||||
|
||||
// TODO why does server logs messages can be null?
|
||||
const typeDefs = gql`
|
||||
|
|
@ -61,7 +61,7 @@ const typeDefs = gql`
|
|||
}
|
||||
|
||||
type Customer {
|
||||
name: String!
|
||||
name: String
|
||||
phone: String
|
||||
totalTxs: Int
|
||||
totalSpent: String
|
||||
|
|
@ -71,7 +71,7 @@ const typeDefs = gql`
|
|||
lastTxClass: String
|
||||
}
|
||||
|
||||
type Account {
|
||||
type AccountConfig {
|
||||
code: String!
|
||||
display: String!
|
||||
class: String!
|
||||
|
|
@ -156,7 +156,7 @@ const typeDefs = gql`
|
|||
countries: [Country]
|
||||
currencies: [Currency]
|
||||
languages: [Language]
|
||||
accounts: [Account]
|
||||
accountsConfig: [AccountConfig]
|
||||
cryptoCurrencies: [CryptoCurrency]
|
||||
machines: [Machine]
|
||||
customers: [Customer]
|
||||
|
|
@ -166,6 +166,7 @@ const typeDefs = gql`
|
|||
uptime: [ProcessStatus]
|
||||
serverLogs: [ServerLog]
|
||||
transactions: [Transaction]
|
||||
accounts: [JSONObject]
|
||||
config: JSONObject
|
||||
}
|
||||
|
||||
|
|
@ -188,6 +189,8 @@ const typeDefs = gql`
|
|||
serverSupportLogs: SupportLogsResponse
|
||||
saveConfig(config: JSONObject): JSONObject
|
||||
createPairingTotem(name: String!): String
|
||||
saveAccount(account: JSONObject): [JSONObject]
|
||||
saveAccounts(accounts: [JSONObject]): [JSONObject]
|
||||
}
|
||||
`
|
||||
|
||||
|
|
@ -202,7 +205,7 @@ const resolvers = {
|
|||
countries: () => countries,
|
||||
currencies: () => currencies,
|
||||
languages: () => languages,
|
||||
accounts: () => accounts,
|
||||
accountsConfig: () => accountsConfig,
|
||||
cryptoCurrencies: () => coins,
|
||||
machines: () => machineLoader.getMachineNames(),
|
||||
customers: () => customers.getCustomersList(),
|
||||
|
|
@ -212,13 +215,16 @@ const resolvers = {
|
|||
uptime: () => supervisor.getAllProcessInfo(),
|
||||
serverLogs: () => serverLogs.getServerLogs(),
|
||||
transactions: () => transactions.batch(),
|
||||
config: () => settingsLoader.getConfig()
|
||||
config: () => settingsLoader.getConfig(),
|
||||
accounts: () => settingsLoader.getAccounts()
|
||||
},
|
||||
Mutation: {
|
||||
machineAction: (...[, { deviceId, action }]) => machineAction({ deviceId, action }),
|
||||
machineSupportLogs: (...[, { deviceId }]) => supportLogs.insert(deviceId),
|
||||
createPairingTotem: (...[, { name }]) => pairing.totem(name),
|
||||
serverSupportLogs: () => serverLogs.insert(),
|
||||
saveAccount: (...[, { account }]) => settingsLoader.saveAccounts([account]),
|
||||
saveAccounts: (...[, { accounts }]) => settingsLoader.saveAccounts(accounts),
|
||||
saveConfig: (...[, { config }]) => settingsLoader.saveConfig(config)
|
||||
.then(it => {
|
||||
notify()
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
const machineLoader = require('../machine-loader')
|
||||
const { UserInputError } = require('apollo-server-express')
|
||||
|
||||
function getMachine(machineId) {
|
||||
function getMachine (machineId) {
|
||||
return machineLoader.getMachines()
|
||||
.then(machines => machines.find(({ deviceId }) => deviceId === machineId))
|
||||
}
|
||||
|
||||
function machineAction({ deviceId, action }) {
|
||||
|
||||
function machineAction ({ deviceId, action }) {
|
||||
return getMachine(deviceId)
|
||||
.then(machine => {
|
||||
if (!machine) throw new UserInputError(`machine:${deviceId} not found`, { deviceId })
|
||||
|
|
|
|||
|
|
@ -9,23 +9,46 @@ low(adapter).then(it => {
|
|||
db = it
|
||||
})
|
||||
|
||||
function saveConfig (config) {
|
||||
const currentState = db.getState()
|
||||
// TODO this should be _.assign
|
||||
// change after flattening of schema
|
||||
const newState = _.mergeWith((objValue, srcValue) => {
|
||||
if (_.isArray(objValue)) {
|
||||
return srcValue
|
||||
}
|
||||
}, currentState, config)
|
||||
function replace (array, index, value) {
|
||||
return array.slice(0, index).concat([value]).concat(array.slice(index + 1))
|
||||
}
|
||||
|
||||
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)
|
||||
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 () {
|
||||
return db.getState()
|
||||
const state = db.getState()
|
||||
return (state && state.config) || {}
|
||||
}
|
||||
|
||||
module.exports = { getConfig, saveConfig }
|
||||
module.exports = { getConfig, saveConfig, saveAccounts, getAccounts }
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import extendJss from 'jss-plugin-extend'
|
|||
import React from 'react'
|
||||
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 global from './styling/global'
|
||||
import theme from './styling/theme'
|
||||
|
|
|
|||
|
|
@ -191,8 +191,8 @@ const LogsDownloaderPopover = ({
|
|||
}
|
||||
|
||||
const radioButtonOptions = [
|
||||
{ label: 'All logs', value: radioButtonAll },
|
||||
{ label: 'Date range', value: radioButtonRange }
|
||||
{ display: 'All logs', code: radioButtonAll },
|
||||
{ display: 'Date range', code: radioButtonRange }
|
||||
]
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { makeStyles, Modal as MaterialModal, Paper } from '@material-ui/core'
|
|||
import classnames from 'classnames'
|
||||
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'
|
||||
|
||||
const styles = {
|
||||
|
|
@ -10,42 +12,68 @@ const styles = {
|
|||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
},
|
||||
modalContentWrapper: {
|
||||
wrapper: ({ width }) => ({
|
||||
width,
|
||||
display: 'flex',
|
||||
position: 'relative',
|
||||
flexDirection: 'column',
|
||||
minHeight: 400,
|
||||
maxHeight: '90vh',
|
||||
overflowY: 'auto',
|
||||
borderRadius: 8,
|
||||
outline: 0,
|
||||
'& > div': {
|
||||
width: '100%'
|
||||
}
|
||||
outline: 0
|
||||
}),
|
||||
content: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
padding: [[0, 32]]
|
||||
},
|
||||
closeIcon: {
|
||||
position: 'absolute',
|
||||
width: 18,
|
||||
height: 18,
|
||||
button: {
|
||||
padding: 0,
|
||||
top: 20,
|
||||
right: 20
|
||||
margin: [[20, 20, 'auto', 'auto']]
|
||||
},
|
||||
header: {
|
||||
display: 'flex'
|
||||
},
|
||||
title: {
|
||||
margin: [[28, 0, 8, 32]]
|
||||
}
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const Modal = ({ handleClose, children, className, ...props }) => {
|
||||
const classes = useStyles()
|
||||
const Modal = ({
|
||||
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 (
|
||||
<MaterialModal onClose={handleClose} className={classes.modal} {...props}>
|
||||
<Paper className={classnames(classes.modalContentWrapper, className)}>
|
||||
<button
|
||||
className={classnames(classes.iconButton, classes.closeIcon)}
|
||||
<MaterialModal onClose={innerClose} className={classes.modal} {...props}>
|
||||
<Paper className={classnames(classes.wrapper, className)}>
|
||||
<div className={classes.header}>
|
||||
{title && <H1 className={classes.title}>{title}</H1>}
|
||||
<IconButton
|
||||
size={20}
|
||||
className={classes.button}
|
||||
onClick={() => handleClose()}>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
{children}
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className={classes.content}>{children}</div>
|
||||
</Paper>
|
||||
</MaterialModal>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -59,10 +59,10 @@ const styles = {
|
|||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const Stage = memo(({ stages, currentStage, color = 'spring', className }) => {
|
||||
if (currentStage < 1 || currentStage > stages)
|
||||
const Stepper = memo(({ steps, currentStep, color = 'spring', className }) => {
|
||||
if (currentStep < 1 || currentStep > steps)
|
||||
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()
|
||||
|
||||
|
|
@ -80,7 +80,7 @@ const Stage = memo(({ stages, currentStage, color = 'spring', className }) => {
|
|||
|
||||
return (
|
||||
<div className={classnames(className, classes.stages)}>
|
||||
{R.range(1, currentStage).map(idx => (
|
||||
{R.range(1, currentStep).map(idx => (
|
||||
<div key={idx} className={classes.wrapper}>
|
||||
{idx > 1 && <div className={classnames(separatorClasses)} />}
|
||||
<div className={classes.stage}>
|
||||
|
|
@ -90,13 +90,13 @@ const Stage = memo(({ stages, currentStage, color = 'spring', className }) => {
|
|||
</div>
|
||||
))}
|
||||
<div className={classes.wrapper}>
|
||||
{currentStage > 1 && <div className={classnames(separatorClasses)} />}
|
||||
{currentStep > 1 && <div className={classnames(separatorClasses)} />}
|
||||
<div className={classes.stage}>
|
||||
{color === 'spring' && <CurrentStageIconSpring />}
|
||||
{color === 'zodiac' && <CurrentStageIconZodiac />}
|
||||
</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 className={classnames(separatorEmptyClasses)} />
|
||||
<div className={classes.stage}>
|
||||
|
|
@ -109,4 +109,4 @@ const Stage = memo(({ stages, currentStage, color = 'spring', className }) => {
|
|||
)
|
||||
})
|
||||
|
||||
export default Stage
|
||||
export default Stepper
|
||||
|
|
@ -44,8 +44,8 @@ const BooleanPropertiesTable = memo(
|
|||
}
|
||||
|
||||
const radioButtonOptions = [
|
||||
{ label: 'Yes', value: true },
|
||||
{ label: 'No', value: false }
|
||||
{ display: 'Yes', code: true },
|
||||
{ display: 'No', code: false }
|
||||
]
|
||||
|
||||
if (!elements || radioGroupValues?.length === 0) return null
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@ const useStyles = makeStyles(styles)
|
|||
const ActionButton = memo(({ size = 'lg', children, className, ...props }) => {
|
||||
const classes = useStyles({ size })
|
||||
return (
|
||||
<button className={classnames(classes.button, className)} {...props}>
|
||||
<div className={classnames(className, classes.wrapper)}>
|
||||
<button className={classes.button} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import typographyStyles from 'src/components/typography/styles'
|
||||
import {
|
||||
white,
|
||||
disabledColor,
|
||||
|
|
@ -6,7 +7,6 @@ import {
|
|||
secondaryColorDarker,
|
||||
spacer
|
||||
} from 'src/styling/variables'
|
||||
import typographyStyles from 'src/components/typography/styles'
|
||||
|
||||
const { h3 } = typographyStyles
|
||||
|
||||
|
|
@ -21,6 +21,11 @@ const pickSize = size => {
|
|||
}
|
||||
|
||||
export default {
|
||||
wrapper: ({ size }) => {
|
||||
const height = pickSize(size)
|
||||
const shadowSize = height / 12
|
||||
return { height: height + shadowSize / 2 }
|
||||
},
|
||||
button: ({ size }) => {
|
||||
const height = pickSize(size)
|
||||
const shadowSize = height / 12
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
const styles = {
|
||||
label: ({ size }) => ({
|
||||
width: size,
|
||||
height: size
|
||||
}),
|
||||
root: {
|
||||
'&svg': {
|
||||
viewbox: null
|
||||
},
|
||||
'&:hover': {
|
||||
backgroundColor: 'inherit'
|
||||
}
|
||||
|
|
@ -11,15 +18,16 @@ const styles = {
|
|||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const IconButton = ({ children, onClick, ...props }) => {
|
||||
const classes = useStyles()
|
||||
const IconButton = ({ size, children, onClick, ...props }) => {
|
||||
const classes = useStyles({ size })
|
||||
return (
|
||||
<IconB
|
||||
{...props}
|
||||
classes={{ root: classes.root }}
|
||||
size="small"
|
||||
classes={{ root: classes.root, label: classes.label }}
|
||||
disableRipple
|
||||
onClick={onClick}>
|
||||
<SvgIcon>{children}</SvgIcon>
|
||||
{children}
|
||||
</IconB>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import DataTable from './DataTable'
|
||||
|
||||
export { DataTable }
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import React from 'react'
|
||||
|
||||
export default React.createContext()
|
||||
|
|
@ -1,14 +1,21 @@
|
|||
import React from 'react'
|
||||
import React, { useContext } from 'react'
|
||||
|
||||
import { Td, THead } from 'src/components/fake-table/Table'
|
||||
import { startCase } from 'src/utils/string'
|
||||
|
||||
import { ACTION_COL_SIZE, DEFAULT_COL_SIZE } from './consts'
|
||||
|
||||
const Header = ({ elements, enableEdit, enableDelete }) => {
|
||||
const actionColSize =
|
||||
enableDelete && enableEdit ? ACTION_COL_SIZE / 2 : ACTION_COL_SIZE
|
||||
import TableCtx from './Context'
|
||||
|
||||
const Header = () => {
|
||||
const {
|
||||
elements,
|
||||
enableEdit,
|
||||
editWidth,
|
||||
enableDelete,
|
||||
deleteWidth,
|
||||
enableToggle,
|
||||
toggleWidth,
|
||||
DEFAULT_COL_SIZE
|
||||
} = useContext(TableCtx)
|
||||
return (
|
||||
<THead>
|
||||
{elements.map(
|
||||
|
|
@ -19,15 +26,20 @@ const Header = ({ elements, enableEdit, enableDelete }) => {
|
|||
)
|
||||
)}
|
||||
{enableEdit && (
|
||||
<Td header width={actionColSize} textAlign="right">
|
||||
<Td header width={editWidth} textAlign="center">
|
||||
Edit
|
||||
</Td>
|
||||
)}
|
||||
{enableDelete && (
|
||||
<Td header width={actionColSize} textAlign="right">
|
||||
<Td header width={deleteWidth} textAlign="center">
|
||||
Delete
|
||||
</Td>
|
||||
)}
|
||||
{enableToggle && (
|
||||
<Td header width={toggleWidth} textAlign="center">
|
||||
Enable
|
||||
</Td>
|
||||
)}
|
||||
</THead>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -1,39 +1,46 @@
|
|||
import { makeStyles } from '@material-ui/core'
|
||||
import classnames from 'classnames'
|
||||
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 { Td, Tr } from 'src/components/fake-table/Table'
|
||||
import { Switch } from 'src/components/inputs'
|
||||
import { TL2 } from 'src/components/typography'
|
||||
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 DisabledEditIcon } from 'src/styling/icons/action/edit/disabled.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 { ACTION_COL_SIZE } from './consts'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const ActionCol = ({
|
||||
editing,
|
||||
setEditing,
|
||||
enableEdit,
|
||||
disabled,
|
||||
onDelete,
|
||||
enableDelete
|
||||
}) => {
|
||||
const ActionCol = ({ disabled, editing }) => {
|
||||
const classes = useStyles()
|
||||
const { values, submitForm, resetForm } = useFormikContext()
|
||||
const {
|
||||
editWidth,
|
||||
onEdit,
|
||||
enableEdit,
|
||||
enableDelete,
|
||||
disableRowEdit,
|
||||
onDelete,
|
||||
deleteWidth,
|
||||
enableToggle,
|
||||
onToggle,
|
||||
toggleWidth,
|
||||
actionColSize
|
||||
} = useContext(TableCtx)
|
||||
|
||||
const actionColSize =
|
||||
enableDelete && enableEdit ? ACTION_COL_SIZE / 2 : ACTION_COL_SIZE
|
||||
const disableEdit = disabled || (disableRowEdit && disableRowEdit(values))
|
||||
|
||||
return (
|
||||
<>
|
||||
{editing && (
|
||||
<Td textAlign="center" width={ACTION_COL_SIZE}>
|
||||
<Td textAlign="center" width={actionColSize}>
|
||||
<Link
|
||||
className={classes.cancelButton}
|
||||
color="secondary"
|
||||
|
|
@ -46,22 +53,32 @@ const ActionCol = ({
|
|||
</Td>
|
||||
)}
|
||||
{!editing && enableEdit && (
|
||||
<Td textAlign="right" width={actionColSize}>
|
||||
<Td textAlign="center" width={editWidth}>
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
disabled={disableEdit}
|
||||
className={classes.editButton}
|
||||
onClick={() => setEditing && setEditing(values.id)}>
|
||||
{disabled ? <DisabledEditIcon /> : <EditIcon />}
|
||||
onClick={() => onEdit && onEdit(values.id)}>
|
||||
{disableEdit ? <DisabledEditIcon /> : <EditIcon />}
|
||||
</IconButton>
|
||||
</Td>
|
||||
)}
|
||||
{!editing && enableDelete && (
|
||||
<Td textAlign="right" width={actionColSize}>
|
||||
<Td textAlign="center" width={deleteWidth}>
|
||||
<IconButton disabled={disabled} onClick={() => onDelete(values.id)}>
|
||||
{disabled ? <DisabledDeleteIcon /> : <DeleteIcon />}
|
||||
</IconButton>
|
||||
</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 {
|
||||
name,
|
||||
input,
|
||||
editable = true,
|
||||
size,
|
||||
bold,
|
||||
width,
|
||||
|
|
@ -82,11 +100,6 @@ const ECol = ({ editing, config }) => {
|
|||
const { values } = useFormikContext()
|
||||
const classes = useStyles({ textAlign, size })
|
||||
|
||||
const viewClasses = {
|
||||
[classes.bold]: bold,
|
||||
[classes.size]: true
|
||||
}
|
||||
|
||||
const iProps = {
|
||||
fullWidth: true,
|
||||
size,
|
||||
|
|
@ -105,43 +118,58 @@ const ECol = ({ editing, config }) => {
|
|||
className={{ [classes.withSuffix]: suffix }}
|
||||
width={width}
|
||||
size={size}
|
||||
bold={bold}
|
||||
textAlign={textAlign}>
|
||||
{editing && <Field name={name} component={input} {...iProps} />}
|
||||
{!editing && values && (
|
||||
<div className={classnames(viewClasses)}>{view(values[name])}</div>
|
||||
{editing && editable ? (
|
||||
<Field name={name} component={input} {...iProps} />
|
||||
) : (
|
||||
values && <>{view(values[name])}</>
|
||||
)}
|
||||
{suffix && <TL2 className={classes.suffix}>{suffix}</TL2>}
|
||||
</Td>
|
||||
)
|
||||
}
|
||||
|
||||
const ERow = ({
|
||||
const groupStriped = elements => {
|
||||
const [toStripe, noStripe] = R.partition(R.has('stripe'))(elements)
|
||||
|
||||
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,
|
||||
onDelete,
|
||||
editing,
|
||||
setEditing,
|
||||
disabled
|
||||
}) => {
|
||||
const { errors } = useFormikContext()
|
||||
enableToggle,
|
||||
stripeWhen
|
||||
} = useContext(TableCtx)
|
||||
|
||||
const { values } = useFormikContext()
|
||||
const shouldStripe = stripeWhen && stripeWhen(values) && !editing
|
||||
|
||||
const iElements = shouldStripe ? groupStriped(elements) : elements
|
||||
return (
|
||||
<Tr
|
||||
error={errors && errors.length}
|
||||
errorMessage={errors && errors.toString()}>
|
||||
{elements.map((it, idx) => (
|
||||
<ECol key={idx} config={it} editing={editing} />
|
||||
))}
|
||||
{(enableEdit || enableDelete) && (
|
||||
<ActionCol
|
||||
disabled={disabled}
|
||||
editing={editing}
|
||||
setEditing={setEditing}
|
||||
onDelete={onDelete}
|
||||
enableEdit={enableEdit}
|
||||
enableDelete={enableDelete}
|
||||
/>
|
||||
{iElements.map((it, idx) => {
|
||||
return <ECol key={idx} config={it} editing={editing} />
|
||||
})}
|
||||
{(enableEdit || enableDelete || enableToggle) && (
|
||||
<ActionCol disabled={disabled} editing={editing} />
|
||||
)}
|
||||
</Tr>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,12 +7,15 @@ import { v4 } from 'uuid'
|
|||
import Link from 'src/components/buttons/Link.js'
|
||||
import { AddButton } from 'src/components/buttons/index.js'
|
||||
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 ERow from './Row'
|
||||
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)
|
||||
|
||||
|
|
@ -24,17 +27,25 @@ const getWidth = R.compose(
|
|||
const ETable = ({
|
||||
name,
|
||||
title,
|
||||
titleLg,
|
||||
elements = [],
|
||||
data = [],
|
||||
save,
|
||||
validationSchema,
|
||||
enableCreate,
|
||||
enableEdit,
|
||||
editWidth: outerEditWidth,
|
||||
enableDelete,
|
||||
deleteWidth = ACTION_COL_SIZE,
|
||||
enableToggle,
|
||||
toggleWidth = ACTION_COL_SIZE,
|
||||
onToggle,
|
||||
forceDisable,
|
||||
disableAdd,
|
||||
enableDelete,
|
||||
initialValues,
|
||||
enableEdit,
|
||||
setEditing,
|
||||
stripeWhen,
|
||||
disableRowEdit,
|
||||
createText = 'Add override'
|
||||
}) => {
|
||||
const [editingId, setEditingId] = useState(null)
|
||||
|
|
@ -45,7 +56,7 @@ const ETable = ({
|
|||
const list = index !== -1 ? R.update(index, it, data) : R.prepend(it, data)
|
||||
|
||||
// no response means the save failed
|
||||
const response = await save({ [name]: list })
|
||||
const response = await save({ [name]: list }, it)
|
||||
if (!response) return
|
||||
setAdding(false)
|
||||
setEditingId(null)
|
||||
|
|
@ -69,15 +80,42 @@ const ETable = ({
|
|||
|
||||
const addField = () => setAdding(true)
|
||||
|
||||
const actionSize = enableEdit || enableDelete ? ACTION_COL_SIZE : 0
|
||||
const width = getWidth(elements) + actionSize
|
||||
const widthIfEditNull =
|
||||
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 showButtonOnEmpty = !data.length && enableCreate && !adding
|
||||
const canAdd = !forceDisable && !editingId && !disableAdd && !adding
|
||||
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 (
|
||||
<TableCtx.Provider value={ctxValue}>
|
||||
<div className={classes.wrapper}>
|
||||
{showButtonOnEmpty && (
|
||||
<AddButton disabled={!canAdd} onClick={addField}>
|
||||
|
|
@ -86,20 +124,23 @@ const ETable = ({
|
|||
)}
|
||||
{showTable && (
|
||||
<>
|
||||
{(title || enableCreate) && (
|
||||
<div className={classes.outerHeader}>
|
||||
{title && <Info2 className={classes.title}>{title}</Info2>}
|
||||
{title && titleLg && (
|
||||
<TL1 className={classes.title}>{title}</TL1>
|
||||
)}
|
||||
{title && !titleLg && (
|
||||
<Info2 className={classes.title}>{title}</Info2>
|
||||
)}
|
||||
{enableCreate && canAdd && (
|
||||
<Link className={classes.addLink} onClick={addField}>
|
||||
{createText}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Table>
|
||||
<Header
|
||||
elements={elements}
|
||||
enableEdit={enableEdit}
|
||||
enableDelete={enableDelete}
|
||||
/>
|
||||
<Header />
|
||||
<TBody>
|
||||
{adding && (
|
||||
<Formik
|
||||
|
|
@ -108,13 +149,7 @@ const ETable = ({
|
|||
validationSchema={validationSchema}
|
||||
onSubmit={innerSave}>
|
||||
<Form>
|
||||
<ERow
|
||||
editing={true}
|
||||
disabled={forceDisable}
|
||||
enableEdit={enableEdit}
|
||||
enableDelete={enableDelete}
|
||||
elements={elements}
|
||||
/>
|
||||
<ERow editing={true} disabled={forceDisable} />
|
||||
</Form>
|
||||
</Formik>
|
||||
)}
|
||||
|
|
@ -132,11 +167,6 @@ const ETable = ({
|
|||
disabled={
|
||||
forceDisable || (editingId && editingId !== it.id)
|
||||
}
|
||||
setEditing={onEdit}
|
||||
onDelete={onDelete}
|
||||
enableEdit={enableEdit}
|
||||
enableDelete={enableDelete}
|
||||
elements={elements}
|
||||
/>
|
||||
</Form>
|
||||
</Formik>
|
||||
|
|
@ -146,6 +176,7 @@ const ETable = ({
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
</TableCtx.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
const ACTION_COL_SIZE = 175
|
||||
const DEFAULT_COL_SIZE = 100
|
||||
|
||||
export { ACTION_COL_SIZE, DEFAULT_COL_SIZE }
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import NamespacedTable from './NamespacedTable'
|
||||
import Table from './Table'
|
||||
|
||||
export { Table }
|
||||
export { Table, NamespacedTable }
|
||||
|
|
|
|||
|
|
@ -32,8 +32,7 @@ const TDoubleLevelHead = ({ children, className }) => {
|
|||
}
|
||||
|
||||
const TBody = ({ children, className }) => {
|
||||
const classes = useStyles()
|
||||
return <div className={classnames(className, classes.body)}>{children}</div>
|
||||
return <div className={classnames(className)}>{children}</div>
|
||||
}
|
||||
|
||||
const Td = ({
|
||||
|
|
@ -42,16 +41,17 @@ const Td = ({
|
|||
className,
|
||||
width = 100,
|
||||
size,
|
||||
bold,
|
||||
textAlign,
|
||||
action
|
||||
}) => {
|
||||
const classes = useStyles({ textAlign, width })
|
||||
const classes = useStyles({ textAlign, width, size })
|
||||
const classNames = {
|
||||
[classes.td]: true,
|
||||
[classes.tdHeader]: header,
|
||||
[classes.actionCol]: action,
|
||||
[classes.large]: size === 'lg' && !header,
|
||||
[classes.md]: size === 'md' && !header
|
||||
[classes.size]: !header,
|
||||
[classes.bold]: !header && bold
|
||||
}
|
||||
|
||||
return <div className={classnames(className, classNames)}>{children}</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import typographyStyles from 'src/components/typography/styles'
|
||||
import { bySize, bold } from 'src/styling/helpers'
|
||||
import {
|
||||
tableHeaderColor,
|
||||
tableHeaderHeight,
|
||||
|
|
@ -9,18 +10,11 @@ import {
|
|||
offColor
|
||||
} from 'src/styling/variables'
|
||||
|
||||
const { tl1, info2, tl2, p, label1 } = typographyStyles
|
||||
const { tl2, p, label1 } = typographyStyles
|
||||
|
||||
export default {
|
||||
body: {
|
||||
borderSpacing: [[0, 4]]
|
||||
},
|
||||
large: {
|
||||
extend: tl1
|
||||
},
|
||||
md: {
|
||||
extend: info2
|
||||
},
|
||||
size: ({ size }) => bySize(size),
|
||||
bold,
|
||||
header: {
|
||||
extend: tl2,
|
||||
backgroundColor: tableHeaderColor,
|
||||
|
|
@ -79,7 +73,7 @@ export default {
|
|||
mainContent: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
minHeight: 54
|
||||
minHeight: 48
|
||||
},
|
||||
// mui-overrides
|
||||
cardContentRoot: {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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 }
|
||||
|
|
@ -10,34 +10,45 @@ const Autocomplete = ({
|
|||
limit = 5,
|
||||
options,
|
||||
label,
|
||||
shouldAdd,
|
||||
getOptionSelected,
|
||||
forceShowValue,
|
||||
value,
|
||||
onChange,
|
||||
valueProp,
|
||||
multiple,
|
||||
onChange,
|
||||
getLabel,
|
||||
value: outsideValue,
|
||||
error,
|
||||
fullWidth,
|
||||
textAlign,
|
||||
size,
|
||||
...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 find = R.find(it => compare(value, it))
|
||||
const getValue = () => {
|
||||
if (!valueProp) return outsideValue
|
||||
|
||||
if (forceShowValue && !multiple && value && !find(options)) {
|
||||
iOptions = R.concat(options, [value])
|
||||
const transform = multiple
|
||||
? 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 (
|
||||
<MAutocomplete
|
||||
options={iOptions}
|
||||
options={options}
|
||||
multiple={multiple}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onChange={iOnChange}
|
||||
getOptionLabel={getLabel}
|
||||
forcePopupIcon={false}
|
||||
filterOptions={createFilterOptions({ ignoreAccents: true, limit })}
|
||||
|
|
@ -47,14 +58,14 @@ const Autocomplete = ({
|
|||
ChipProps={{ onDelete: null }}
|
||||
blurOnSelect
|
||||
clearOnEscape
|
||||
getOptionSelected={getOptionSelected}
|
||||
getOptionSelected={R.eqProps(valueProp)}
|
||||
{...props}
|
||||
renderInput={params => {
|
||||
return (
|
||||
<TextInput
|
||||
{...params}
|
||||
label={label}
|
||||
value={value}
|
||||
value={outsideValue}
|
||||
error={error}
|
||||
size={size}
|
||||
fullWidth={fullWidth}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -1,64 +1,55 @@
|
|||
import {
|
||||
Radio as MaterialRadio,
|
||||
RadioGroup as MaterialRadioGroup,
|
||||
FormControlLabel
|
||||
Radio,
|
||||
RadioGroup as MRadioGroup,
|
||||
FormControlLabel,
|
||||
makeStyles
|
||||
} from '@material-ui/core'
|
||||
import { withStyles } from '@material-ui/styles'
|
||||
import classnames from 'classnames'
|
||||
import React from 'react'
|
||||
|
||||
import { secondaryColor } from '../../../styling/variables'
|
||||
import typographyStyles from '../../typography/styles'
|
||||
import { Label1 } from 'src/components/typography'
|
||||
|
||||
const { p } = typographyStyles
|
||||
|
||||
const GreenRadio = withStyles({
|
||||
root: {
|
||||
color: secondaryColor,
|
||||
padding: [[9, 8, 9, 9]],
|
||||
'&$checked': {
|
||||
color: secondaryColor
|
||||
}
|
||||
},
|
||||
checked: {}
|
||||
})(props => <MaterialRadio color="default" {...props} />)
|
||||
|
||||
const Label = withStyles({
|
||||
const styles = {
|
||||
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 = ({
|
||||
name,
|
||||
label,
|
||||
value,
|
||||
options,
|
||||
ariaLabel,
|
||||
onChange,
|
||||
className,
|
||||
...props
|
||||
labelClassName,
|
||||
radioClassName
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<>
|
||||
{options && (
|
||||
<MaterialRadioGroup
|
||||
aria-label={ariaLabel}
|
||||
{label && <Label1 className={classes.label}>{label}</Label1>}
|
||||
<MRadioGroup
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className={classnames(className)}>
|
||||
{options.map((option, idx) => (
|
||||
<Label
|
||||
<FormControlLabel
|
||||
key={idx}
|
||||
value={option.value}
|
||||
control={<GreenRadio />}
|
||||
label={option.label}
|
||||
value={option.code}
|
||||
control={<Radio className={radioClassName} />}
|
||||
label={option.display}
|
||||
className={classnames(labelClassName)}
|
||||
/>
|
||||
))}
|
||||
</MaterialRadioGroup>
|
||||
)}
|
||||
</MRadioGroup>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
36
new-lamassu-admin/src/components/inputs/base/SecretInput.js
Normal file
36
new-lamassu-admin/src/components/inputs/base/SecretInput.js
Normal 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
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import Autocomplete from './Autocomplete'
|
||||
import Checkbox from './Checkbox'
|
||||
import RadioGroup from './RadioGroup'
|
||||
import SecretInput from './SecretInput'
|
||||
import Switch from './Switch'
|
||||
import TextInput from './TextInput'
|
||||
|
||||
export { Checkbox, TextInput, Switch, RadioGroup, Autocomplete }
|
||||
export { Checkbox, TextInput, Switch, SecretInput, RadioGroup, Autocomplete }
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
import { useFormikContext } from 'formik'
|
||||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
|
||||
import { Autocomplete } from '../base'
|
||||
|
||||
const AutocompleteFormik = props => {
|
||||
const AutocompleteFormik = ({ options, ...props }) => {
|
||||
const { name, onBlur, value } = props.field
|
||||
const { touched, errors, setFieldValue } = props.form
|
||||
const error = !!(touched[name] && errors[name])
|
||||
const { initialValues } = useFormikContext()
|
||||
|
||||
const iOptions =
|
||||
R.type(options) === 'Function' ? options(initialValues) : options
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
|
|
@ -14,6 +20,7 @@ const AutocompleteFormik = props => {
|
|||
onBlur={onBlur}
|
||||
value={value}
|
||||
error={error}
|
||||
options={iOptions}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,31 +1,14 @@
|
|||
import React, { memo } from 'react'
|
||||
import { makeStyles } from '@material-ui/core'
|
||||
|
||||
import { Label1 } from 'src/components/typography'
|
||||
|
||||
import { RadioGroup } from '../base'
|
||||
|
||||
const styles = {
|
||||
label: {
|
||||
height: 16,
|
||||
lineHeight: '16px',
|
||||
margin: [[0, 0, 4, 0]],
|
||||
paddingLeft: 3
|
||||
}
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const RadioGroupFormik = memo(({ ...props }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const RadioGroupFormik = memo(({ label, ...props }) => {
|
||||
const { name, onChange, value } = props.field
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.label && <Label1 className={classes.label}>{props.label}</Label1>}
|
||||
<RadioGroup
|
||||
name={name}
|
||||
label={label}
|
||||
value={value}
|
||||
options={props.options}
|
||||
ariaLabel={name}
|
||||
|
|
@ -36,7 +19,6 @@ const RadioGroupFormik = memo(({ ...props }) => {
|
|||
className={props.className}
|
||||
{...props}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,45 +1,22 @@
|
|||
import { makeStyles } from '@material-ui/core'
|
||||
import classnames from 'classnames'
|
||||
import React, { memo, useState } from 'react'
|
||||
import React, { memo } from 'react'
|
||||
|
||||
import TextInputFormik from './TextInput'
|
||||
import { styles } from './TextInput.styles'
|
||||
import { SecretInput } from '../base'
|
||||
|
||||
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 { 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
|
||||
}
|
||||
const error = !!(touched[name] && errors[name])
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={classnames(spanClass)} aria-hidden="true">
|
||||
⚬ ⚬ ⚬ This field is set ⚬ ⚬ ⚬
|
||||
</span>
|
||||
<TextInputFormik
|
||||
<SecretInput
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
value={value}
|
||||
error={error}
|
||||
{...props}
|
||||
onFocus={handleFocus}
|
||||
className={classnames(inputClass, className)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,8 @@
|
|||
import AutocompleteSelect from './autocomplete/AutocompleteSelect'
|
||||
import Autocomplete from './base/Autocomplete'
|
||||
import Checkbox from './base/Checkbox'
|
||||
import Radio from './base/Radio'
|
||||
import RadioGroup from './base/RadioGroup'
|
||||
import Select from './base/Select'
|
||||
import Switch from './base/Switch'
|
||||
import TextInput from './base/TextInput'
|
||||
|
||||
export {
|
||||
AutocompleteSelect,
|
||||
TextInput,
|
||||
Radio,
|
||||
Checkbox,
|
||||
Switch,
|
||||
Select,
|
||||
RadioGroup
|
||||
}
|
||||
export { Autocomplete, TextInput, Checkbox, Switch, Select, RadioGroup }
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ import classnames from 'classnames'
|
|||
import React, { memo, useState } from 'react'
|
||||
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 AddMachine from 'src/pages/AddMachine'
|
||||
|
||||
import styles from './Header.styles'
|
||||
import { Link } from './buttons'
|
||||
import { H4 } from './typography'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import typographyStyles from 'src/components/typography/styles'
|
||||
import {
|
||||
version,
|
||||
mainWidth,
|
||||
|
|
@ -9,8 +10,6 @@ import {
|
|||
fontColor
|
||||
} from 'src/styling/variables'
|
||||
|
||||
import typographyStyles from './typography/styles'
|
||||
|
||||
const { tl2, p } = typographyStyles
|
||||
|
||||
let headerHeight = spacer * 7
|
||||
|
|
@ -2,7 +2,7 @@ import { makeStyles } from '@material-ui/core'
|
|||
import React from 'react'
|
||||
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import { TL1 } from 'src/components/typography'
|
||||
import Subtitle from 'src/components/Subtitle'
|
||||
|
||||
import styles from './Section.styles'
|
||||
|
||||
|
|
@ -12,10 +12,12 @@ const Section = ({ error, children, title }) => {
|
|||
const classes = useStyles()
|
||||
return (
|
||||
<div className={classes.section}>
|
||||
{(title || error) && (
|
||||
<div className={classes.sectionHeader}>
|
||||
<TL1 className={classes.sectionTitle}>{title}</TL1>
|
||||
<Subtitle className={classes.sectionTitle}>{title}</Subtitle>
|
||||
{error && <ErrorMessage>Failed to save changes</ErrorMessage>}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { offColor } from 'src/styling/variables'
|
||||
|
||||
export default {
|
||||
section: {
|
||||
marginBottom: 72
|
||||
|
|
@ -9,7 +7,6 @@ export default {
|
|||
alignItems: 'center'
|
||||
},
|
||||
sectionTitle: {
|
||||
color: offColor,
|
||||
margin: [[16, 20, 23, 0]]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import typographyStyles from 'src/components/typography/styles'
|
||||
import { respondTo } from 'src/styling/helpers'
|
||||
import {
|
||||
primaryColor,
|
||||
|
|
@ -7,8 +8,6 @@ import {
|
|||
xxl
|
||||
} from 'src/styling/variables'
|
||||
|
||||
import typographyStyles from './typography/styles'
|
||||
|
||||
const { tl2, p } = typographyStyles
|
||||
|
||||
const sidebarColor = zircon
|
||||
|
|
@ -1,18 +1,22 @@
|
|||
import { makeStyles } from '@material-ui/core'
|
||||
import React from 'react'
|
||||
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import Title from 'src/components/Title'
|
||||
|
||||
import styles from './TitleSection.styles'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const TitleSection = ({ title }) => {
|
||||
const TitleSection = ({ title, error }) => {
|
||||
const classes = useStyles()
|
||||
return (
|
||||
<div className={classes.titleWrapper}>
|
||||
<div className={classes.titleAndButtonsContainer}>
|
||||
<Title>{title}</Title>
|
||||
{error && (
|
||||
<ErrorMessage className={classes.error}>Failed to save</ErrorMessage>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,10 +8,7 @@ export default {
|
|||
titleAndButtonsContainer: {
|
||||
display: 'flex'
|
||||
},
|
||||
iconButton: {
|
||||
border: 'none',
|
||||
outline: 0,
|
||||
backgroundColor: 'transparent',
|
||||
cursor: 'pointer'
|
||||
error: {
|
||||
marginLeft: 12
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,130 +2,46 @@ import { makeStyles } from '@material-ui/core'
|
|||
import classnames from 'classnames'
|
||||
import React from 'react'
|
||||
|
||||
import { Table, THead, TBody, Td, Th } from 'src/components/fake-table/Table'
|
||||
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 { IconButton } from 'src/components/buttons'
|
||||
import {
|
||||
offColor,
|
||||
tableDisabledHeaderColor,
|
||||
tableNewDisabledHeaderColor,
|
||||
secondaryColorDarker
|
||||
} from 'src/styling/variables'
|
||||
Table,
|
||||
THead,
|
||||
TBody,
|
||||
Td,
|
||||
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 = ({
|
||||
width = 380,
|
||||
height = 160,
|
||||
width = 378,
|
||||
height = 128,
|
||||
title,
|
||||
items,
|
||||
onEdit,
|
||||
disabled,
|
||||
newService,
|
||||
className,
|
||||
...props
|
||||
className
|
||||
}) => {
|
||||
const editButtonSize = 54
|
||||
|
||||
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
|
||||
}
|
||||
const classes = useStyles({ width, height })
|
||||
|
||||
return (
|
||||
<>
|
||||
{items && (
|
||||
<Table className={classnames(className, classes.wrapper)}>
|
||||
<THead className={classnames(headerClasses)}>
|
||||
<Th width={width - editButtonSize}>
|
||||
<Table className={classnames(className, classes.table)}>
|
||||
<THead>
|
||||
<Th className={classes.head}>
|
||||
{title}
|
||||
{newService && <span className={classes.spanNew}>New</span>}
|
||||
</Th>
|
||||
<Th width={editButtonSize} className={classes.buttonTh}>
|
||||
{!disabled && (
|
||||
<button className={classes.editButton} onClick={onEdit}>
|
||||
<IconButton onClick={onEdit} className={classes.button}>
|
||||
<EditIcon />
|
||||
</button>
|
||||
)}
|
||||
{disabled && (
|
||||
<button className={classes.editButton}>
|
||||
<DeleteIcon />
|
||||
</button>
|
||||
)}
|
||||
</IconButton>
|
||||
</Th>
|
||||
</THead>
|
||||
<TBody className={classnames(bodyClasses)}>
|
||||
<TBody>
|
||||
<Tr className={classes.tr}>
|
||||
<Td width={width}>
|
||||
{!disabled && (
|
||||
{items && (
|
||||
<>
|
||||
{items[0] && (
|
||||
<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>
|
||||
</Tr>
|
||||
</TBody>
|
||||
</Table>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import classnames from 'classnames'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
import {
|
||||
AutoSizer,
|
||||
|
|
@ -8,33 +9,31 @@ import {
|
|||
CellMeasurerCache
|
||||
} 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 ExpandOpenIcon } from 'src/styling/icons/action/expand/open.svg'
|
||||
import { mainWidth } from 'src/styling/variables'
|
||||
|
||||
const styles = {
|
||||
expandButton: {
|
||||
border: 'none',
|
||||
backgroundColor: 'transparent',
|
||||
cursor: 'pointer',
|
||||
padding: 4
|
||||
},
|
||||
row: {
|
||||
borderRadius: 0
|
||||
}
|
||||
}
|
||||
import styles from './DataTable.styles'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const ExpRow = ({
|
||||
const Row = ({
|
||||
id,
|
||||
elements,
|
||||
data,
|
||||
width,
|
||||
Details,
|
||||
expanded,
|
||||
expandRow,
|
||||
...props
|
||||
expWidth,
|
||||
expandable
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
|
||||
|
|
@ -44,23 +43,13 @@ const ExpRow = ({
|
|||
className={classnames(classes.row)}
|
||||
error={data.error}
|
||||
errorMessage={data.errorMessage}>
|
||||
{elements
|
||||
.slice(0, -1)
|
||||
.map(
|
||||
(
|
||||
{ width, className, textAlign, view = it => it?.toString() },
|
||||
idx
|
||||
) => (
|
||||
<Td
|
||||
key={idx}
|
||||
width={width}
|
||||
className={className}
|
||||
textAlign={textAlign}>
|
||||
{elements.map(({ view = it => it?.toString(), ...props }, idx) => (
|
||||
<Td key={idx} {...props}>
|
||||
{view(data)}
|
||||
</Td>
|
||||
)
|
||||
)}
|
||||
<Td width={elements[elements.length - 1].width}>
|
||||
))}
|
||||
{expandable && (
|
||||
<Td width={expWidth} textAlign="center">
|
||||
<button
|
||||
onClick={() => expandRow(id)}
|
||||
className={classes.expandButton}>
|
||||
|
|
@ -68,10 +57,11 @@ const ExpRow = ({
|
|||
{!expanded && <ExpandClosedIcon />}
|
||||
</button>
|
||||
</Td>
|
||||
)}
|
||||
</Tr>
|
||||
{expanded && (
|
||||
{expandable && expanded && (
|
||||
<Tr className={classes.detailsRow}>
|
||||
<Td width={mainWidth}>
|
||||
<Td width={width}>
|
||||
<Details it={data} />
|
||||
</Td>
|
||||
</Tr>
|
||||
|
|
@ -80,18 +70,22 @@ const ExpRow = ({
|
|||
)
|
||||
}
|
||||
|
||||
/* rows = [{ columns = [{ name, value, className, textAlign, width }], details, className, error, errorMessage }]
|
||||
* Don't forget to include the width of the last (expand button) column!
|
||||
*/
|
||||
const ExpTable = ({
|
||||
const DataTable = ({
|
||||
elements = [],
|
||||
data = [],
|
||||
Details,
|
||||
className,
|
||||
expandable,
|
||||
...props
|
||||
}) => {
|
||||
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 => {
|
||||
setExpanded(id === expanded ? null : id)
|
||||
}
|
||||
|
|
@ -101,7 +95,7 @@ const ExpTable = ({
|
|||
fixedWidth: true
|
||||
})
|
||||
|
||||
function rowRenderer({ index, isScrolling, key, parent, style }) {
|
||||
function rowRenderer({ index, key, parent, style }) {
|
||||
return (
|
||||
<CellMeasurer
|
||||
cache={cache}
|
||||
|
|
@ -110,13 +104,16 @@ const ExpTable = ({
|
|||
parent={parent}
|
||||
rowIndex={index}>
|
||||
<div style={style}>
|
||||
<ExpRow
|
||||
<Row
|
||||
width={width}
|
||||
id={index}
|
||||
expWidth={expWidth}
|
||||
elements={elements}
|
||||
data={data[index]}
|
||||
Details={Details}
|
||||
expanded={index === expanded}
|
||||
expandRow={expandRow}
|
||||
expandable={expandable}
|
||||
/>
|
||||
</div>
|
||||
</CellMeasurer>
|
||||
|
|
@ -124,8 +121,7 @@ const ExpTable = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Table className={classes.table}>
|
||||
<THead>
|
||||
{elements.map(({ width, className, textAlign, header }, idx) => (
|
||||
<Th
|
||||
|
|
@ -136,15 +132,17 @@ const ExpTable = ({
|
|||
{header}
|
||||
</Th>
|
||||
))}
|
||||
{expandable && <Th width={expWidth}></Th>}
|
||||
</THead>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 auto' }}>
|
||||
<TBody className={classes.body}>
|
||||
<AutoSizer disableWidth>
|
||||
{({ height }) => (
|
||||
<List
|
||||
// this has to be in a style because of how the component works
|
||||
style={{ overflow: 'inherit', outline: 'none' }}
|
||||
{...props}
|
||||
height={height}
|
||||
width={mainWidth}
|
||||
width={width}
|
||||
rowCount={data.length}
|
||||
rowHeight={cache.rowHeight}
|
||||
rowRenderer={rowRenderer}
|
||||
|
|
@ -153,9 +151,9 @@ const ExpTable = ({
|
|||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
</>
|
||||
</TBody>
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
|
||||
export default ExpTable
|
||||
export default DataTable
|
||||
20
new-lamassu-admin/src/components/tables/DataTable.styles.js
Normal file
20
new-lamassu-admin/src/components/tables/DataTable.styles.js
Normal 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'
|
||||
})
|
||||
}
|
||||
12
new-lamassu-admin/src/components/tables/Stripes.js
Normal file
12
new-lamassu-admin/src/components/tables/Stripes.js
Normal 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
|
||||
|
|
@ -7,7 +7,7 @@ import * as R from 'ramda'
|
|||
import React from 'react'
|
||||
|
||||
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 TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
|
||||
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import moment from 'moment'
|
|||
import QRCode from 'qrcode.react'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import Sidebar from 'src/components/Sidebar'
|
||||
import TableLabel from 'src/components/TableLabel'
|
||||
import Title from 'src/components/Title'
|
||||
import { Tr, Td, THead, TBody, Table } from 'src/components/fake-table/Table'
|
||||
import Sidebar from 'src/components/layout/Sidebar'
|
||||
import {
|
||||
H3,
|
||||
Info1,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import React from 'react'
|
|||
import { Table as EditableTable } from 'src/components/editableTable'
|
||||
import Section from 'src/components/layout/Section'
|
||||
import TitleSection from 'src/components/layout/TitleSection'
|
||||
import { fromServer, toServer } from 'src/utils/config'
|
||||
import { fromNamespace, toNamespace } from 'src/utils/config'
|
||||
|
||||
import {
|
||||
mainFields,
|
||||
|
|
@ -55,25 +55,27 @@ const Locales = ({ name: SCREEN_KEY }) => {
|
|||
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 save = it => {
|
||||
const config = toServer(SCREEN_KEY)(it.locale[0])
|
||||
const config = toNamespace(SCREEN_KEY)(it.locale[0])
|
||||
return saveConfig({ variables: { config } })
|
||||
}
|
||||
|
||||
const saveOverrides = it => {
|
||||
const config = toServer(SCREEN_KEY)(it)
|
||||
const config = toNamespace(SCREEN_KEY)(it)
|
||||
return saveConfig({ variables: { config } })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TitleSection title="Locales" />
|
||||
<Section title="Default settings">
|
||||
<Section>
|
||||
<EditableTable
|
||||
title="Default settings"
|
||||
titleLg
|
||||
name="locale"
|
||||
enableEdit
|
||||
initialValues={locale}
|
||||
|
|
@ -83,8 +85,10 @@ const Locales = ({ name: SCREEN_KEY }) => {
|
|||
elements={mainFields(data)}
|
||||
/>
|
||||
</Section>
|
||||
<Section title="Overrides">
|
||||
<Section>
|
||||
<EditableTable
|
||||
title="Overrides"
|
||||
titleLg
|
||||
name="overrides"
|
||||
enableDelete
|
||||
enableEdit
|
||||
|
|
|
|||
|
|
@ -3,60 +3,80 @@ import * as Yup from 'yup'
|
|||
|
||||
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) => {
|
||||
return R.filter(it => R.includes(it.name, names), allFields(getData))
|
||||
}
|
||||
|
||||
const allFields = getData => [
|
||||
const allFields = getData => {
|
||||
const getView = (data, code, compare) => it => {
|
||||
if (!data) return ''
|
||||
|
||||
return R.compose(
|
||||
R.prop(code),
|
||||
R.find(R.propEq(compare ?? 'code', it))
|
||||
)(data)
|
||||
}
|
||||
|
||||
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: R.path(['name']),
|
||||
view: getView(machineData, 'name', 'deviceId'),
|
||||
input: Autocomplete,
|
||||
inputProps: {
|
||||
options: getData(['machines']),
|
||||
limit: null,
|
||||
forceShowValue: true,
|
||||
getOptionSelected: R.eqProps('machineId')
|
||||
options: machineData,
|
||||
valueProp: 'deviceId',
|
||||
getLabel: R.path(['name']),
|
||||
limit: null
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'country',
|
||||
width: 200,
|
||||
size: 'sm',
|
||||
view: R.path(['display']),
|
||||
view: getView(countryData, 'display'),
|
||||
input: Autocomplete,
|
||||
inputProps: {
|
||||
options: getData(['countries']),
|
||||
getOptionSelected: R.eqProps('display')
|
||||
options: countryData,
|
||||
valueProp: 'code',
|
||||
getLabel: R.path(['display'])
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'fiatCurrency',
|
||||
width: 150,
|
||||
size: 'sm',
|
||||
view: R.path(['code']),
|
||||
view: getView(currencyData, 'code'),
|
||||
input: Autocomplete,
|
||||
inputProps: {
|
||||
options: getData(['currencies']),
|
||||
getOptionSelected: R.eqProps('display')
|
||||
options: currencyData,
|
||||
valueProp: 'code',
|
||||
getLabel: R.path(['code'])
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'languages',
|
||||
width: 240,
|
||||
size: 'sm',
|
||||
view: displayCodeArray,
|
||||
view: displayCodeArray(languageData),
|
||||
input: Autocomplete,
|
||||
inputProps: {
|
||||
options: getData(['languages']),
|
||||
options: languageData,
|
||||
valueProp: 'code',
|
||||
getLabel: R.path(['code']),
|
||||
getOptionSelected: R.eqProps('code'),
|
||||
multiple: true
|
||||
}
|
||||
},
|
||||
|
|
@ -64,16 +84,17 @@ const allFields = getData => [
|
|||
name: 'cryptoCurrencies',
|
||||
width: 270,
|
||||
size: 'sm',
|
||||
view: displayCodeArray,
|
||||
view: displayCodeArray(cryptoData),
|
||||
input: Autocomplete,
|
||||
inputProps: {
|
||||
options: getData(['cryptoCurrencies']),
|
||||
getLabel: it => R.path(['code'])(it) ?? it,
|
||||
getOptionSelected: R.eqProps('code'),
|
||||
options: cryptoData,
|
||||
valueProp: 'code',
|
||||
getLabel: R.path(['code']),
|
||||
multiple: true
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
const mainFields = auxData => {
|
||||
const getData = R.path(R.__, auxData)
|
||||
|
|
@ -98,29 +119,29 @@ const overrides = auxData => {
|
|||
}
|
||||
|
||||
const LocaleSchema = Yup.object().shape({
|
||||
country: Yup.object().required('Required'),
|
||||
fiatCurrency: Yup.object().required('Required'),
|
||||
country: Yup.string().required('Required'),
|
||||
fiatCurrency: Yup.string().required('Required'),
|
||||
languages: Yup.array().required('Required'),
|
||||
cryptoCurrencies: Yup.array().required('Required')
|
||||
})
|
||||
|
||||
const OverridesSchema = Yup.object().shape({
|
||||
machine: Yup.object().required('Required'),
|
||||
country: Yup.object().required('Required'),
|
||||
machine: Yup.string().required('Required'),
|
||||
country: Yup.string().required('Required'),
|
||||
languages: Yup.array().required('Required'),
|
||||
cryptoCurrencies: Yup.array().required('Required')
|
||||
})
|
||||
|
||||
const localeDefaults = {
|
||||
country: null,
|
||||
fiatCurrency: null,
|
||||
country: '',
|
||||
fiatCurrency: '',
|
||||
languages: [],
|
||||
cryptoCurrencies: []
|
||||
}
|
||||
|
||||
const overridesDefaults = {
|
||||
machine: null,
|
||||
country: null,
|
||||
machine: '',
|
||||
country: '',
|
||||
languages: [],
|
||||
cryptoCurrencies: []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ import * as R from 'ramda'
|
|||
import React, { useState } from 'react'
|
||||
|
||||
import LogsDowloaderPopover from 'src/components/LogsDownloaderPopper'
|
||||
import Sidebar from 'src/components/Sidebar'
|
||||
import Title from 'src/components/Title'
|
||||
import { FeatureButton, SimpleButton } from 'src/components/buttons'
|
||||
import Sidebar from 'src/components/layout/Sidebar'
|
||||
import {
|
||||
Table,
|
||||
TableHead,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import * as R from 'ramda'
|
|||
import React, { useState } from 'react'
|
||||
|
||||
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'
|
||||
|
||||
|
|
@ -49,19 +49,19 @@ const Notifications = ({ name: SCREEN_KEY }) => {
|
|||
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 cryptoCurrencies = data?.cryptoCurrencies
|
||||
|
||||
// TODO check path when locales is finished
|
||||
const currency = R.path(['locales_currency'])(data?.config ?? {})
|
||||
// TODO improve the way of fetching this
|
||||
const currency = R.path(['locale_fiatCurrency', 'code'])(data?.config ?? {})
|
||||
|
||||
const save = (section, rawConfig) => {
|
||||
const config = toServer(SCREEN_KEY)(rawConfig)
|
||||
const save = R.curry((section, rawConfig) => {
|
||||
const config = toNamespace(SCREEN_KEY)(rawConfig)
|
||||
setSection(section)
|
||||
setError(null)
|
||||
return saveConfig({ variables: { config } })
|
||||
}
|
||||
})
|
||||
|
||||
const setEditing = (key, state) => {
|
||||
if (!state) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { makeStyles } from '@material-ui/core'
|
||||
import React from 'react'
|
||||
|
||||
import { Link } from 'src/components/buttons'
|
||||
import { Link, IconButton } from 'src/components/buttons'
|
||||
import { H4 } from 'src/components/typography'
|
||||
import { ReactComponent as DisabledEditIcon } from 'src/styling/icons/action/edit/disabled.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}>
|
||||
<H4 className={classes.title}>{title}</H4>
|
||||
{!editing && (
|
||||
<button
|
||||
<IconButton
|
||||
onClick={() => setEditing(true)}
|
||||
className={classes.button}
|
||||
disabled={disabled}>
|
||||
{disabled ? <DisabledEditIcon /> : <EditIcon />}
|
||||
</button>
|
||||
</IconButton>
|
||||
)}
|
||||
{editing && (
|
||||
<div className={classes.editingButtons}>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ const NAME = 'cryptoBalanceOverrides'
|
|||
|
||||
const CryptoBalanceOverrides = ({ section }) => {
|
||||
const {
|
||||
cryptoCurrencies,
|
||||
cryptoCurrencies = [],
|
||||
data,
|
||||
save,
|
||||
currency,
|
||||
|
|
@ -32,12 +32,17 @@ const CryptoBalanceOverrides = ({ section }) => {
|
|||
return save(newOverrides)
|
||||
}
|
||||
|
||||
const getSuggestions = () => {
|
||||
const overridenCryptos = R.map(
|
||||
override => override[CRYPTOCURRENCY_KEY],
|
||||
setupValues
|
||||
const overridenCryptos = R.map(R.prop(CRYPTOCURRENCY_KEY))(setupValues)
|
||||
const suggestionFilter = R.filter(
|
||||
it => !R.contains(it.code, overridenCryptos)
|
||||
)
|
||||
return R.without(overridenCryptos, cryptoCurrencies ?? [])
|
||||
const suggestions = suggestionFilter(cryptoCurrencies)
|
||||
|
||||
const findSuggestion = it => {
|
||||
const coin = R.compose(R.find(R.propEq('code', it?.cryptoCurrency)))(
|
||||
cryptoCurrencies
|
||||
)
|
||||
return coin ? [coin] : []
|
||||
}
|
||||
|
||||
const initialValues = {
|
||||
|
|
@ -60,7 +65,11 @@ const CryptoBalanceOverrides = ({ section }) => {
|
|||
.required()
|
||||
})
|
||||
|
||||
const suggestions = getSuggestions()
|
||||
const viewCrypto = it =>
|
||||
R.compose(
|
||||
R.path(['display']),
|
||||
R.find(R.propEq('code', it))
|
||||
)(cryptoCurrencies)
|
||||
|
||||
const elements = [
|
||||
{
|
||||
|
|
@ -68,13 +77,13 @@ const CryptoBalanceOverrides = ({ section }) => {
|
|||
header: 'Cryptocurrency',
|
||||
width: 166,
|
||||
size: 'sm',
|
||||
view: R.path(['display']),
|
||||
view: viewCrypto,
|
||||
input: Autocomplete,
|
||||
inputProps: {
|
||||
options: suggestions,
|
||||
options: it => R.concat(suggestions, findSuggestion(it)),
|
||||
limit: null,
|
||||
forceShowValue: true,
|
||||
getOptionSelected: R.eqProps('display')
|
||||
valueProp: 'code',
|
||||
getLabel: R.path(['display'])
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -14,16 +14,22 @@ const MACHINE_KEY = 'machine'
|
|||
const NAME = 'fiatBalanceOverrides'
|
||||
|
||||
const FiatBalanceOverrides = ({ section }) => {
|
||||
const { machines, data, save, isDisabled, setEditing } = useContext(
|
||||
const { machines = [], data, save, isDisabled, setEditing } = useContext(
|
||||
NotificationsCtx
|
||||
)
|
||||
|
||||
const setupValues = data?.fiatBalanceOverrides ?? []
|
||||
const innerSetEditing = it => setEditing(NAME, it)
|
||||
|
||||
const getSuggestions = () => {
|
||||
const overridenMachines = R.map(override => override.machine, setupValues)
|
||||
return R.without(overridenMachines, machines ?? [])
|
||||
const suggestionFilter = R.filter(
|
||||
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 = {
|
||||
|
|
@ -44,8 +50,6 @@ const FiatBalanceOverrides = ({ section }) => {
|
|||
.required()
|
||||
})
|
||||
|
||||
const suggestions = getSuggestions()
|
||||
|
||||
const elements = [
|
||||
{
|
||||
name: MACHINE_KEY,
|
||||
|
|
@ -54,9 +58,8 @@ const FiatBalanceOverrides = ({ section }) => {
|
|||
view: R.path(['name']),
|
||||
input: Autocomplete,
|
||||
inputProps: {
|
||||
options: suggestions,
|
||||
options: it => R.concat(suggestions, findSuggestion(it)),
|
||||
limit: null,
|
||||
forceShowValue: true,
|
||||
getOptionSelected: R.eqProps('display')
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
Th
|
||||
} from 'src/components/fake-table/Table'
|
||||
import { Switch } from 'src/components/inputs'
|
||||
import { fromNamespace, toNamespace } from 'src/utils/config'
|
||||
import { startCase } from 'src/utils/string'
|
||||
|
||||
import NotificationsCtx from '../NotificationsContext'
|
||||
|
|
@ -26,12 +27,15 @@ const sizes = {
|
|||
const width = R.sum(R.values(sizes)) + channelSize
|
||||
|
||||
const Row = ({ namespace }) => {
|
||||
const { data, save } = useContext(NotificationsCtx)
|
||||
const disabled = !data || !data[`${namespace}_active`]
|
||||
const { data: rawData, save: rawSave } = useContext(NotificationsCtx)
|
||||
|
||||
const save = R.compose(rawSave(null), toNamespace(namespace))
|
||||
const data = fromNamespace(namespace)(rawData)
|
||||
|
||||
const disabled = !data || !data.active
|
||||
|
||||
const Cell = ({ name, disabled }) => {
|
||||
const namespaced = `${namespace}_${name}`
|
||||
const value = !!(data && data[namespaced])
|
||||
const value = !!(data && data[name])
|
||||
|
||||
return (
|
||||
<Td width={sizes[name]} textAlign="center">
|
||||
|
|
@ -39,7 +43,7 @@ const Row = ({ namespace }) => {
|
|||
disabled={disabled}
|
||||
checked={value}
|
||||
onChange={event => {
|
||||
save(null, { [namespaced]: event.target.checked })
|
||||
save({ [name]: event.target.checked })
|
||||
}}
|
||||
value={value}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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 { makeStyles } from '@material-ui/core/styles'
|
||||
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 { BooleanPropertiesTable } from 'src/components/booleanPropertiesTable'
|
||||
import { Button } from 'src/components/buttons'
|
||||
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 { mainStyles } from './CoinATMRadar.styles'
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import { makeStyles } from '@material-ui/core'
|
|||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import Sidebar from 'src/components/Sidebar'
|
||||
import Title from 'src/components/Title'
|
||||
import Sidebar from 'src/components/layout/Sidebar'
|
||||
|
||||
import logsStyles from '../Logs.styles'
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
67
new-lamassu-admin/src/pages/Services/FormRenderer.js
Normal file
67
new-lamassu-admin/src/pages/Services/FormRenderer.js
Normal 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
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 { 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 { BitstampCard, BitstampForm } from './Bitstamp'
|
||||
import { BlockcypherCard, BlockcypherForm } from './Blockcypher'
|
||||
import { InfuraCard, InfuraForm } from './Infura'
|
||||
import { ItbitCard, ItbitForm } from './Itbit'
|
||||
import { KrakenCard, KrakenForm } from './Kraken'
|
||||
import { MailgunCard, MailgunForm } from './Mailgun'
|
||||
import { StrikeCard, StrikeForm } from './Strike'
|
||||
import { TwilioCard, TwilioForm } from './Twilio'
|
||||
import { servicesStyles as styles } from './Services.styles'
|
||||
import FormRenderer from './FormRenderer'
|
||||
import schemas from './schemas'
|
||||
|
||||
const GET_INFO = gql`
|
||||
query getData {
|
||||
accounts
|
||||
}
|
||||
`
|
||||
|
||||
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 GET_CONFIG = gql`
|
||||
{
|
||||
config
|
||||
}
|
||||
`
|
||||
const Services = ({ key: SCREEN_KEY }) => {
|
||||
const [editingSchema, setEditingSchema] = useState(null)
|
||||
|
||||
const GET_ACCOUNTS = gql`
|
||||
{
|
||||
accounts {
|
||||
code
|
||||
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 { data } = useQuery(GET_INFO)
|
||||
const [saveAccount] = useMutation(SAVE_ACCOUNT, {
|
||||
onCompleted: () => setEditingSchema(null),
|
||||
refetchQueries: ['getData']
|
||||
})
|
||||
|
||||
const classes = useStyles()
|
||||
|
||||
useQuery(GET_CONFIG, {
|
||||
onCompleted: data => setAccountsConfig(data.config.accounts ?? {})
|
||||
})
|
||||
const { data: accountsResponse } = useQuery(GET_ACCOUNTS)
|
||||
const accounts = data?.accounts ?? []
|
||||
|
||||
const accounts = accountsResponse?.accounts
|
||||
const getValue = code => R.find(R.propEq('code', code))(accounts)
|
||||
|
||||
const save = (code, it) => {
|
||||
const newAccounts = R.clone(accountsConfig)
|
||||
newAccounts[code] = it
|
||||
return saveConfig({ variables: { config: { accounts: newAccounts } } })
|
||||
const getItems = (code, elements) => {
|
||||
const faceElements = R.filter(R.prop('face'))(elements)
|
||||
const values = getValue(code) || {}
|
||||
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 (
|
||||
<>
|
||||
<div className={classes.titleWrapper}>
|
||||
<div className={classes.titleContainer}>
|
||||
<Title>3rd Party Services</Title>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.mainWrapper}>
|
||||
<BitgoCard
|
||||
account={bitgo}
|
||||
onEdit={() =>
|
||||
handleOpen(
|
||||
<BitgoForm
|
||||
account={bitgo}
|
||||
handleClose={handleClose}
|
||||
save={save}
|
||||
<div className={classes.wrapper}>
|
||||
<TitleSection title="3rd Party Services" />
|
||||
<Grid container spacing={4}>
|
||||
{R.values(schemas).map(schema => (
|
||||
<Grid item key={schema.code}>
|
||||
<SingleRowTable
|
||||
title={schema.title}
|
||||
onEdit={() => setEditingSchema(schema)}
|
||||
items={getItems(schema.code, schema.elements)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<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 && (
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
{editingSchema && (
|
||||
<Modal
|
||||
aria-labelledby="simple-modal-title"
|
||||
aria-describedby="simple-modal-description"
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
className={classes.modal}>
|
||||
<div>{modalContent}</div>
|
||||
title={`Edit ${editingSchema.name}`}
|
||||
width={478}
|
||||
handleClose={() => setEditingSchema(null)}
|
||||
open={true}>
|
||||
<FormRenderer
|
||||
save={it =>
|
||||
saveAccount({
|
||||
variables: { account: { code: editingSchema.code, ...it } }
|
||||
})
|
||||
}
|
||||
elements={editingSchema.elements}
|
||||
validationSchema={editingSchema.validationSchema}
|
||||
value={getValue(editingSchema.code)}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
120
new-lamassu-admin/src/pages/Services/schemas/bitgo.js
Normal file
120
new-lamassu-admin/src/pages/Services/schemas/bitgo.js
Normal 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')
|
||||
})
|
||||
}
|
||||
43
new-lamassu-admin/src/pages/Services/schemas/bitstamp.js
Normal file
43
new-lamassu-admin/src/pages/Services/schemas/bitstamp.js
Normal 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')
|
||||
})
|
||||
}
|
||||
34
new-lamassu-admin/src/pages/Services/schemas/blockcypher.js
Normal file
34
new-lamassu-admin/src/pages/Services/schemas/blockcypher.js
Normal 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')
|
||||
})
|
||||
}
|
||||
21
new-lamassu-admin/src/pages/Services/schemas/index.js
Normal file
21
new-lamassu-admin/src/pages/Services/schemas/index.js
Normal 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
|
||||
}
|
||||
41
new-lamassu-admin/src/pages/Services/schemas/infura.js
Normal file
41
new-lamassu-admin/src/pages/Services/schemas/infura.js
Normal 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')
|
||||
})
|
||||
}
|
||||
50
new-lamassu-admin/src/pages/Services/schemas/itbit.js
Normal file
50
new-lamassu-admin/src/pages/Services/schemas/itbit.js
Normal 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')
|
||||
})
|
||||
}
|
||||
32
new-lamassu-admin/src/pages/Services/schemas/kraken.js
Normal file
32
new-lamassu-admin/src/pages/Services/schemas/kraken.js
Normal 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')
|
||||
})
|
||||
}
|
||||
49
new-lamassu-admin/src/pages/Services/schemas/mailgun.js
Normal file
49
new-lamassu-admin/src/pages/Services/schemas/mailgun.js
Normal 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')
|
||||
})
|
||||
}
|
||||
23
new-lamassu-admin/src/pages/Services/schemas/strike.js
Normal file
23
new-lamassu-admin/src/pages/Services/schemas/strike.js
Normal 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')
|
||||
})
|
||||
}
|
||||
48
new-lamassu-admin/src/pages/Services/schemas/twilio.js
Normal file
48
new-lamassu-admin/src/pages/Services/schemas/twilio.js
Normal 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')
|
||||
})
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ import React, { useState } from 'react'
|
|||
import LogsDowloaderPopover from 'src/components/LogsDownloaderPopper'
|
||||
import Title from 'src/components/Title'
|
||||
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 Download } from 'src/styling/icons/button/download/zodiac.svg'
|
||||
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
|
||||
|
|
@ -71,29 +71,34 @@ const Transactions = () => {
|
|||
{
|
||||
header: '',
|
||||
width: 62,
|
||||
size: 'sm',
|
||||
view: it => (it.txClass === 'cashOut' ? <TxOutIcon /> : <TxInIcon />)
|
||||
},
|
||||
{
|
||||
header: 'Machine',
|
||||
name: 'machineName',
|
||||
width: 180,
|
||||
size: 'sm',
|
||||
view: R.path(['machineName'])
|
||||
},
|
||||
{
|
||||
header: 'Customer',
|
||||
width: 162,
|
||||
size: 'sm',
|
||||
view: getCustomerDisplayName
|
||||
},
|
||||
{
|
||||
header: 'Cash',
|
||||
width: 110,
|
||||
textAlign: 'right',
|
||||
size: 'sm',
|
||||
view: it => `${Number.parseFloat(it.fiat)} ${it.fiatCode}`
|
||||
},
|
||||
{
|
||||
header: 'Crypto',
|
||||
width: 141,
|
||||
textAlign: 'right',
|
||||
size: 'sm',
|
||||
view: it =>
|
||||
`${toUnit(new BigNumber(it.cryptoAtoms), it.cryptoCode).toFormat(5)} ${
|
||||
it.cryptoCode
|
||||
|
|
@ -103,27 +108,22 @@ const Transactions = () => {
|
|||
header: 'Address',
|
||||
view: R.path(['toAddress']),
|
||||
className: classes.overflowTd,
|
||||
size: 'sm',
|
||||
width: 136
|
||||
},
|
||||
{
|
||||
header: 'Date (UTC)',
|
||||
view: it => moment.utc(it.created).format('YYYY-MM-D'),
|
||||
textAlign: 'right',
|
||||
size: 'sm',
|
||||
width: 124
|
||||
},
|
||||
{
|
||||
header: 'Time (UTC)',
|
||||
view: it => moment.utc(it.created).format('HH:mm:ss'),
|
||||
textAlign: 'right',
|
||||
size: 'sm',
|
||||
width: 124
|
||||
},
|
||||
{
|
||||
header: '', // Trade
|
||||
view: () => {},
|
||||
width: 90
|
||||
},
|
||||
{
|
||||
width: 71
|
||||
}
|
||||
]
|
||||
|
||||
|
|
@ -176,10 +176,11 @@ const Transactions = () => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ExpTable
|
||||
<DataTable
|
||||
elements={elements}
|
||||
data={R.path(['transactions'])(txResponse)}
|
||||
Details={DetailsRow}
|
||||
expandable
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
99
new-lamassu-admin/src/pages/Wallet/Wallet.js
Normal file
99
new-lamassu-admin/src/pages/Wallet/Wallet.js
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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 React, { useState, useEffect } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import Stage from 'src/components/Stage'
|
||||
import { Button } from 'src/components/buttons'
|
||||
import { RadioGroup, AutocompleteSelect } from 'src/components/inputs'
|
||||
import { H1, Info2, H4 } from 'src/components/typography'
|
||||
import { startCase } from 'src/utils/string'
|
||||
import Modal from 'src/components/Modal'
|
||||
import schema from 'src/pages/Services/schemas'
|
||||
import { toNamespace } from 'src/utils/config'
|
||||
|
||||
import { getBitgoFields, getBitgoFormik } from '../Services/Bitgo'
|
||||
import { getBitstampFields, getBitstampFormik } from '../Services/Bitstamp'
|
||||
import {
|
||||
getBlockcypherFields,
|
||||
getBlockcypherFormik
|
||||
} from '../Services/Blockcypher'
|
||||
import { getInfuraFields, getInfuraFormik } from '../Services/Infura'
|
||||
import { getKrakenFields, getKrakenFormik } from '../Services/Kraken'
|
||||
import { getStrikeFields, getStrikeFormik } from '../Services/Strike'
|
||||
import WizardSplash from './WizardSplash'
|
||||
import WizardStep from './WizardStep'
|
||||
|
||||
const styles = {
|
||||
modalContent: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: [[24, 32, 0]],
|
||||
'& > h1': {
|
||||
margin: [[0, 0, 10]]
|
||||
},
|
||||
'& > h4': {
|
||||
margin: [[32, 0, 32 - 9, 0]]
|
||||
},
|
||||
'& > p': {
|
||||
margin: 0
|
||||
}
|
||||
},
|
||||
submitButtonWrapper: {
|
||||
display: 'flex',
|
||||
alignSelf: 'flex-end',
|
||||
margin: [['auto', 0, 0]]
|
||||
},
|
||||
submitButton: {
|
||||
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 LAST_STEP = 4
|
||||
const MODAL_WIDTH = 554
|
||||
|
||||
const contains = crypto => R.compose(R.contains(crypto), R.prop('cryptos'))
|
||||
const sameClass = type => R.propEq('class', type)
|
||||
const filterConfig = (crypto, type) =>
|
||||
R.filter(it => sameClass(type)(it) && contains(crypto)(it))
|
||||
|
||||
const getItems = (accountsConfig, accounts, type, crypto) => {
|
||||
const fConfig = filterConfig(crypto, type)(accountsConfig)
|
||||
const find = code => R.find(R.propEq('code', code))(accounts)
|
||||
|
||||
const [filled, unfilled] = R.partition(({ code }) => {
|
||||
const account = find(code)
|
||||
if (!schema[code]) return true
|
||||
|
||||
const { validationSchema } = schema[code]
|
||||
return validationSchema.isValidSync(account)
|
||||
})(fConfig)
|
||||
|
||||
return { filled, unfilled }
|
||||
}
|
||||
|
||||
const getNewServiceForm = serviceName => {
|
||||
switch (serviceName) {
|
||||
case 'bitgo':
|
||||
return { fields: getBitgoFields(), formik: getBitgoFormik() }
|
||||
case 'bitstamp':
|
||||
return { fields: getBitstampFields(), formik: getBitstampFormik() }
|
||||
case 'blockcypher':
|
||||
return { fields: getBlockcypherFields(), formik: getBlockcypherFormik() }
|
||||
case 'infura':
|
||||
return { fields: getInfuraFields(), formik: getInfuraFormik() }
|
||||
case 'kraken':
|
||||
return { fields: getKrakenFields(), formik: getKrakenFormik() }
|
||||
case 'strike':
|
||||
return { fields: getStrikeFields(), formik: getStrikeFormik() }
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const SubmitButton = ({ error, ...props }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<div className={classes.submitButtonWrapper}>
|
||||
{error && <ErrorMessage>Failed to save</ErrorMessage>}
|
||||
<Button {...props}>Next</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Wizard = ({
|
||||
crypto,
|
||||
coinName,
|
||||
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)()
|
||||
const Wizard = ({ coin, onClose, accountsConfig, accounts, save, error }) => {
|
||||
const [{ step, config, accountsToSave }, setState] = useState({
|
||||
step: 0,
|
||||
config: { active: true },
|
||||
accountsToSave: []
|
||||
})
|
||||
.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)
|
||||
}}
|
||||
|
||||
const title = `Enable ${coin.display}`
|
||||
const isLastStep = step === LAST_STEP
|
||||
|
||||
const tickers = { filled: filterConfig(coin.code, 'ticker')(accountsConfig) }
|
||||
const wallets = getItems(accountsConfig, accounts, 'wallet', coin.code)
|
||||
const exchanges = getItems(accountsConfig, accounts, 'exchange', coin.code)
|
||||
const zeroConfs = getItems(accountsConfig, accounts, 'zeroConf', coin.code)
|
||||
|
||||
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 getStepData = () => {
|
||||
switch (step) {
|
||||
case 1:
|
||||
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 (
|
||||
<Modal
|
||||
title={step === 0 ? null : title}
|
||||
handleClose={onClose}
|
||||
width={MODAL_WIDTH}
|
||||
open={true}>
|
||||
{step === 0 && (
|
||||
<WizardSplash
|
||||
code={coin.code}
|
||||
name={coin.display}
|
||||
onContinue={() => onContinue()}
|
||||
/>
|
||||
</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)}
|
||||
)}
|
||||
{step !== 0 && (
|
||||
<WizardStep
|
||||
step={step}
|
||||
error={error}
|
||||
lastStep={isLastStep}
|
||||
{...getStepData()}
|
||||
onContinue={onContinue}
|
||||
getValue={getValue}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
const styles = {
|
||||
logoWrapper: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: 80,
|
||||
margin: [[40, 0, 24]],
|
||||
'& > svg': {
|
||||
maxHeight: '100%',
|
||||
width: '100%'
|
||||
}
|
||||
logo: {
|
||||
maxHeight: 80,
|
||||
maxWidth: 200
|
||||
},
|
||||
title: {
|
||||
margin: [[24, 0, 32, 0]]
|
||||
},
|
||||
text: {
|
||||
margin: 0
|
||||
},
|
||||
button: {
|
||||
marginTop: 'auto',
|
||||
marginBottom: 58
|
||||
},
|
||||
modalContent: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
padding: [[0, 66]],
|
||||
'& > h1': {
|
||||
margin: [[0, 0, 32]]
|
||||
},
|
||||
'& > p': {
|
||||
margin: 0
|
||||
},
|
||||
'& > button': {
|
||||
margin: [['auto', 0, 56]],
|
||||
'&:active': {
|
||||
margin: [['auto', 0, 56]]
|
||||
}
|
||||
}
|
||||
padding: [[0, 42]],
|
||||
flex: 1
|
||||
}
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const renderLogo = code => {
|
||||
const getLogo = code => {
|
||||
switch (code) {
|
||||
case 'BTC':
|
||||
return <BitcoinLogo />
|
||||
return BitcoinLogo
|
||||
case 'BCH':
|
||||
return <BitcoinCashLogo />
|
||||
return BitcoinCashLogo
|
||||
case 'DASH':
|
||||
return <DashLogo />
|
||||
return DashLogo
|
||||
case 'ETH':
|
||||
return <EthereumLogo />
|
||||
return EthereumLogo
|
||||
case 'LTC':
|
||||
return <LitecoinLogo />
|
||||
return LitecoinLogo
|
||||
case 'ZEC':
|
||||
return <ZCashLogo />
|
||||
return ZCashLogo
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const WizardSplash = ({ code, coinName, handleModalNavigation }) => {
|
||||
const WizardSplash = ({ code, name, onContinue }) => {
|
||||
const classes = useStyles()
|
||||
const Logo = getLogo(code)
|
||||
|
||||
return (
|
||||
<div className={classes.modalContent}>
|
||||
<div className={classes.logoWrapper}>{renderLogo(code)}</div>
|
||||
<H1>Enable {coinName}</H1>
|
||||
<P>
|
||||
You are about to enable {coinName} on your system. This will allow you
|
||||
to use this cryptocurrency on your machines. To able to do that, you’ll
|
||||
<Logo className={classes.logo} />
|
||||
<H1 className={classes.title}>Enable {name}</H1>
|
||||
<P className={classes.text}>
|
||||
You are about to enable {name} on your system. This will allow you to
|
||||
use this cryptocurrency on your machines. To be able to do that, you’ll
|
||||
have to setup all the necessary 3rd party services.
|
||||
</P>
|
||||
<Button onClick={() => handleModalNavigation(1)}>
|
||||
<Button className={classes.button} onClick={onContinue}>
|
||||
Start configuration
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
155
new-lamassu-admin/src/pages/Wallet/WizardStep.js
Normal file
155
new-lamassu-admin/src/pages/Wallet/WizardStep.js
Normal 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
|
||||
42
new-lamassu-admin/src/pages/Wallet/WizardStep.styles.js
Normal file
42
new-lamassu-admin/src/pages/Wallet/WizardStep.styles.js
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
103
new-lamassu-admin/src/pages/Wallet/helper.js
Normal file
103
new-lamassu-admin/src/pages/Wallet/helper.js
Normal 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 }
|
||||
|
|
@ -5,9 +5,10 @@ import moment from 'moment'
|
|||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
|
||||
import DataTable from 'src/components/tables/DataTable'
|
||||
|
||||
import { MainStatus } from '../../components/Status'
|
||||
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 ErrorIcon } from '../../styling/icons/status/tomato.svg'
|
||||
import { mainStyles } from '../Transactions/Transactions.styles'
|
||||
|
|
@ -42,35 +43,37 @@ const MachineStatus = () => {
|
|||
{
|
||||
header: 'Machine Name',
|
||||
width: 232,
|
||||
size: 'sm',
|
||||
textAlign: 'left',
|
||||
view: m => m.name
|
||||
},
|
||||
{
|
||||
header: 'Status',
|
||||
width: 349,
|
||||
size: 'sm',
|
||||
textAlign: 'left',
|
||||
view: m => <MainStatus statuses={m.statuses} />
|
||||
},
|
||||
{
|
||||
header: 'Last ping',
|
||||
width: 192,
|
||||
size: 'sm',
|
||||
textAlign: 'left',
|
||||
view: m => moment(m.lastPing).fromNow()
|
||||
},
|
||||
{
|
||||
header: 'Ping Time',
|
||||
width: 155,
|
||||
size: 'sm',
|
||||
textAlign: 'left',
|
||||
view: m => m.pingTime || 'unknown'
|
||||
},
|
||||
{
|
||||
header: 'Software Version',
|
||||
width: 201,
|
||||
size: 'sm',
|
||||
textAlign: 'left',
|
||||
view: m => m.softwareVersion || 'unknown'
|
||||
},
|
||||
{
|
||||
width: 71
|
||||
}
|
||||
]
|
||||
|
||||
|
|
@ -91,10 +94,11 @@ const MachineStatus = () => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ExpTable
|
||||
<DataTable
|
||||
elements={elements}
|
||||
data={R.path(['machines'])(machinesResponse)}
|
||||
Details={MachineDetailsRow}
|
||||
expandable
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import OperatorInfo from 'src/pages/OperatorInfo/OperatorInfo'
|
|||
import ServerLogs from 'src/pages/ServerLogs'
|
||||
import Services from 'src/pages/Services/Services'
|
||||
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'
|
||||
|
||||
const tree = [
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import extendJss from 'jss-plugin-extend'
|
|||
import React from 'react'
|
||||
|
||||
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 AuthorizeIcon } from 'src/styling/icons/button/authorize/zodiac.svg'
|
||||
|
||||
|
|
@ -178,8 +178,6 @@ story.add('ConfirmDialog', () => (
|
|||
</Wrapper>
|
||||
))
|
||||
|
||||
story.add('Radio', () => <Radio label="Hehe" />)
|
||||
|
||||
const typographyStory = storiesOf('Typography', module)
|
||||
typographyStory.add('H1', () => <H1>Hehehe</H1>)
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@ export default {
|
|||
},
|
||||
'button::-moz-focus-inner': {
|
||||
border: 0
|
||||
},
|
||||
// forcing styling onto inner container
|
||||
'.ReactVirtualized__Grid__innerScrollContainer': {
|
||||
overflow: 'inherit !important'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
28
new-lamassu-admin/src/styling/icons/stripes.svg
Normal file
28
new-lamassu-admin/src/styling/icons/stripes.svg
Normal 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 |
|
|
@ -1,4 +1,7 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="104" height="64" version="1.1">
|
||||
<circle fill="#F7931A" cx="52" cy="32" r="32"/>
|
||||
<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"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="104" height="64">
|
||||
<circle cx="52" cy="32" r="32" fill="#F7931A" />
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 758 B |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue