fix: rework wallet screen

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

View file

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

View file

@ -17,7 +17,7 @@ const ACCOUNT_LIST = [
{ code: 'bitstamp', display: 'Bitstamp', class: TICKER, cryptos: [BTC, ETH, LTC, BCH] },
{ code: '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 }

View file

@ -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()

View file

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

View file

@ -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 }

View file

@ -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'

View file

@ -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 (

View file

@ -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)}
onClick={() => handleClose()}>
<CloseIcon />
</button>
{children}
<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 />
</IconButton>
</div>
<div className={classes.content}>{children}</div>
</Paper>
</MaterialModal>
)

View file

@ -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

View file

@ -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

View file

@ -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}>
{children}
</button>
<div className={classnames(className, classes.wrapper)}>
<button className={classes.button} {...props}>
{children}
</button>
</div>
)
})

View file

@ -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

View file

@ -1,8 +1,15 @@
import { makeStyles, IconButton as IconB, SvgIcon } from '@material-ui/core'
import { makeStyles, IconButton as IconB } from '@material-ui/core'
import React from 'react'
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>
)
}

View file

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

View file

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

View file

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

View file

@ -1,14 +1,21 @@
import React from 'react'
import React, { useContext } from 'react'
import { Td, THead } from 'src/components/fake-table/Table'
import { 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>
)
}

View file

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

View file

@ -1,39 +1,46 @@
import { makeStyles } from '@material-ui/core'
import 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 = ({
elements,
enableEdit,
enableDelete,
onDelete,
editing,
setEditing,
disabled
}) => {
const { errors } = useFormikContext()
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,
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>
)

View file

@ -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,83 +80,103 @@ 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 (
<div className={classes.wrapper}>
{showButtonOnEmpty && (
<AddButton disabled={!canAdd} onClick={addField}>
{createText}
</AddButton>
)}
{showTable && (
<>
<div className={classes.outerHeader}>
{title && <Info2 className={classes.title}>{title}</Info2>}
{enableCreate && canAdd && (
<Link className={classes.addLink} onClick={addField}>
{createText}
</Link>
<TableCtx.Provider value={ctxValue}>
<div className={classes.wrapper}>
{showButtonOnEmpty && (
<AddButton disabled={!canAdd} onClick={addField}>
{createText}
</AddButton>
)}
{showTable && (
<>
{(title || enableCreate) && (
<div className={classes.outerHeader}>
{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>
)}
</div>
<Table>
<Header
elements={elements}
enableEdit={enableEdit}
enableDelete={enableDelete}
/>
<TBody>
{adding && (
<Formik
initialValues={{ id: v4(), ...initialValues }}
onReset={onReset}
validationSchema={validationSchema}
onSubmit={innerSave}>
<Form>
<ERow
editing={true}
disabled={forceDisable}
enableEdit={enableEdit}
enableDelete={enableDelete}
elements={elements}
/>
</Form>
</Formik>
)}
{data.map((it, idx) => (
<Formik
key={it.id ?? idx}
enableReinitialize
initialValues={it}
onReset={onReset}
validationSchema={validationSchema}
onSubmit={innerSave}>
<Form>
<ERow
editing={editingId === it.id}
disabled={
forceDisable || (editingId && editingId !== it.id)
}
setEditing={onEdit}
onDelete={onDelete}
enableEdit={enableEdit}
enableDelete={enableDelete}
elements={elements}
/>
</Form>
</Formik>
))}
</TBody>
</Table>
</>
)}
</div>
<Table>
<Header />
<TBody>
{adding && (
<Formik
initialValues={{ id: v4(), ...initialValues }}
onReset={onReset}
validationSchema={validationSchema}
onSubmit={innerSave}>
<Form>
<ERow editing={true} disabled={forceDisable} />
</Form>
</Formik>
)}
{data.map((it, idx) => (
<Formik
key={it.id ?? idx}
enableReinitialize
initialValues={it}
onReset={onReset}
validationSchema={validationSchema}
onSubmit={innerSave}>
<Form>
<ERow
editing={editingId === it.id}
disabled={
forceDisable || (editingId && editingId !== it.id)
}
/>
</Form>
</Formik>
))}
</TBody>
</Table>
</>
)}
</div>
</TableCtx.Provider>
)
}

View file

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

View file

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

View file

@ -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>

View file

@ -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: {

View file

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

View file

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

View file

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

View file

@ -10,34 +10,45 @@ const Autocomplete = ({
limit = 5,
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}

View file

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

View file

@ -1,64 +1,55 @@
import {
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}
name={name}
value={value}
onChange={onChange}
className={classnames(className)}>
{options.map((option, idx) => (
<Label
key={idx}
value={option.value}
control={<GreenRadio />}
label={option.label}
/>
))}
</MaterialRadioGroup>
)}
{label && <Label1 className={classes.label}>{label}</Label1>}
<MRadioGroup
name={name}
value={value}
onChange={onChange}
className={classnames(className)}>
{options.map((option, idx) => (
<FormControlLabel
key={idx}
value={option.code}
control={<Radio className={radioClassName} />}
label={option.display}
className={classnames(labelClassName)}
/>
))}
</MRadioGroup>
</>
)
}

View file

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

View file

@ -1,7 +1,8 @@
import Autocomplete from './Autocomplete'
import 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 }

View file

@ -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}
/>
)

View file

@ -1,42 +1,24 @@
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}
value={value}
options={props.options}
ariaLabel={name}
onChange={e => {
onChange(e)
props.resetError()
}}
className={props.className}
{...props}
/>
</>
<RadioGroup
name={name}
label={label}
value={value}
options={props.options}
ariaLabel={name}
onChange={e => {
onChange(e)
props.resetError()
}}
className={props.className}
{...props}
/>
)
})

View file

@ -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
{...props}
onFocus={handleFocus}
className={classnames(inputClass, className)}
/>
</>
<SecretInput
name={name}
onChange={onChange}
onBlur={onBlur}
value={value}
error={error}
{...props}
/>
)
})

View file

@ -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 }

View file

@ -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)

View file

@ -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

View file

@ -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}>
<div className={classes.sectionHeader}>
<TL1 className={classes.sectionTitle}>{title}</TL1>
{error && <ErrorMessage>Failed to save changes</ErrorMessage>}
</div>
{(title || error) && (
<div className={classes.sectionHeader}>
<Subtitle className={classes.sectionTitle}>{title}</Subtitle>
{error && <ErrorMessage>Failed to save changes</ErrorMessage>}
</div>
)}
{children}
</div>
)

View file

@ -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]]
}
}

View file

@ -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

View file

@ -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>
)

View file

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

View file

@ -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}>
{title}
{newService && <span className={classes.spanNew}>New</span>}
</Th>
<Th width={editButtonSize} className={classes.buttonTh}>
{!disabled && (
<button className={classes.editButton} onClick={onEdit}>
<EditIcon />
</button>
)}
{disabled && (
<button className={classes.editButton}>
<DeleteIcon />
</button>
)}
</Th>
</THead>
<TBody className={classnames(bodyClasses)}>
<Table className={classnames(className, classes.table)}>
<THead>
<Th className={classes.head}>
{title}
<IconButton onClick={onEdit} className={classes.button}>
<EditIcon />
</IconButton>
</Th>
</THead>
<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>
</TBody>
</Table>
)}
</Tr>
</TBody>
</Table>
</>
)
}

View file

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

View file

@ -1,5 +1,6 @@
import { makeStyles } from '@material-ui/core/styles'
import 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,34 +43,25 @@ 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}>
{view(data)}
</Td>
)
)}
<Td width={elements[elements.length - 1].width}>
<button
onClick={() => expandRow(id)}
className={classes.expandButton}>
{expanded && <ExpandOpenIcon />}
{!expanded && <ExpandClosedIcon />}
</button>
</Td>
{elements.map(({ view = it => it?.toString(), ...props }, idx) => (
<Td key={idx} {...props}>
{view(data)}
</Td>
))}
{expandable && (
<Td width={expWidth} textAlign="center">
<button
onClick={() => expandRow(id)}
className={classes.expandButton}>
{expanded && <ExpandOpenIcon />}
{!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,27 +121,28 @@ const ExpTable = ({
}
return (
<>
<div>
<THead>
{elements.map(({ width, className, textAlign, header }, idx) => (
<Th
key={idx}
width={width}
className={className}
textAlign={textAlign}>
{header}
</Th>
))}
</THead>
</div>
<div style={{ flex: '1 1 auto' }}>
<Table className={classes.table}>
<THead>
{elements.map(({ width, className, textAlign, header }, idx) => (
<Th
key={idx}
width={width}
className={className}
textAlign={textAlign}>
{header}
</Th>
))}
{expandable && <Th width={expWidth}></Th>}
</THead>
<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

View file

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

View file

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

View file

@ -7,7 +7,7 @@ import * as R from 'ramda'
import React from 'react'
import 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'

View file

@ -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,

View file

@ -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

View file

@ -3,77 +3,98 @@ 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 => [
{
name: 'machine',
width: 200,
size: 'sm',
view: R.path(['name']),
input: Autocomplete,
inputProps: {
options: getData(['machines']),
limit: null,
forceShowValue: true,
getOptionSelected: R.eqProps('machineId')
}
},
{
name: 'country',
width: 200,
size: 'sm',
view: R.path(['display']),
input: Autocomplete,
inputProps: {
options: getData(['countries']),
getOptionSelected: R.eqProps('display')
}
},
{
name: 'fiatCurrency',
width: 150,
size: 'sm',
view: R.path(['code']),
input: Autocomplete,
inputProps: {
options: getData(['currencies']),
getOptionSelected: R.eqProps('display')
}
},
{
name: 'languages',
width: 240,
size: 'sm',
view: displayCodeArray,
input: Autocomplete,
inputProps: {
options: getData(['languages']),
getLabel: R.path(['code']),
getOptionSelected: R.eqProps('code'),
multiple: true
}
},
{
name: 'cryptoCurrencies',
width: 270,
size: 'sm',
view: displayCodeArray,
input: Autocomplete,
inputProps: {
options: getData(['cryptoCurrencies']),
getLabel: it => R.path(['code'])(it) ?? it,
getOptionSelected: R.eqProps('code'),
multiple: true
}
const 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: getView(machineData, 'name', 'deviceId'),
input: Autocomplete,
inputProps: {
options: machineData,
valueProp: 'deviceId',
getLabel: R.path(['name']),
limit: null
}
},
{
name: 'country',
width: 200,
size: 'sm',
view: getView(countryData, 'display'),
input: Autocomplete,
inputProps: {
options: countryData,
valueProp: 'code',
getLabel: R.path(['display'])
}
},
{
name: 'fiatCurrency',
width: 150,
size: 'sm',
view: getView(currencyData, 'code'),
input: Autocomplete,
inputProps: {
options: currencyData,
valueProp: 'code',
getLabel: R.path(['code'])
}
},
{
name: 'languages',
width: 240,
size: 'sm',
view: displayCodeArray(languageData),
input: Autocomplete,
inputProps: {
options: languageData,
valueProp: 'code',
getLabel: R.path(['code']),
multiple: true
}
},
{
name: 'cryptoCurrencies',
width: 270,
size: 'sm',
view: displayCodeArray(cryptoData),
input: Autocomplete,
inputProps: {
options: cryptoData,
valueProp: 'code',
getLabel: R.path(['code']),
multiple: true
}
}
]
}
const mainFields = auxData => {
const 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: []
}

View file

@ -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,

View file

@ -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) {

View file

@ -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}>

View file

@ -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)
)
const suggestions = suggestionFilter(cryptoCurrencies)
const findSuggestion = it => {
const coin = R.compose(R.find(R.propEq('code', it?.cryptoCurrency)))(
cryptoCurrencies
)
return R.without(overridenCryptos, cryptoCurrencies ?? [])
return coin ? [coin] : []
}
const initialValues = {
@ -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'])
}
},
{

View file

@ -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 overridenMachines = R.map(override => override.machine, setupValues)
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')
}
},

View file

@ -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}
/>

View file

@ -1,13 +1,13 @@
import { makeStyles } from '@material-ui/core/styles'
import React, { useState, memo } from 'react'
import { useQuery, useMutation } from '@apollo/react-hooks'
import { 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'

View file

@ -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'

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,241 +1,96 @@
import React, { useState } from 'react'
import * as R from 'ramda'
import { gql } from 'apollo-boost'
import { makeStyles, Modal } from '@material-ui/core'
import { useQuery, useMutation } from '@apollo/react-hooks'
import { 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}
/>
)
}
/>
<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 && (
<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)}
/>
</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>
)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,7 +9,7 @@ import React, { useState } from 'react'
import LogsDowloaderPopover from 'src/components/LogsDownloaderPopper'
import 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
/>
</>
)

View file

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

View file

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

View file

@ -1,306 +1,107 @@
import { makeStyles } from '@material-ui/core'
import classnames from 'classnames'
import { Formik, Field as FormikField } from 'formik'
import * as R from 'ramda'
import 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 Wizard = ({ coin, onClose, accountsConfig, accounts, save, error }) => {
const [{ step, config, accountsToSave }, setState] = useState({
step: 0,
config: { active: true },
accountsToSave: []
})
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 useStyles = makeStyles(styles)
const SubmitButton = ({ error, ...props }) => {
const classes = useStyles()
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 (
<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)()
})
.catch(error => setError(error))
}>
{props => (
<form
onReset={props.handleReset}
onSubmit={props.handleSubmit}
className={classes.newServiceForm}
{...props}>
<div className={classes.newServiceFormFields}>
{formContent.fields.map((field, idx) => (
<div key={idx} className={classes.field}>
<FormikField
id={field.name}
name={field.name}
component={field.component}
placeholder={field.placeholder}
type={field.type}
label={field.label}
className={classes.formInput}
onFocus={() => {
setError(null)
}}
/>
</div>
))}
</div>
<SubmitButton
disabled={R.isEmpty(props.touched) || !props.isValid}
className={classes.submitButton}
type="submit"
error={error}
/>
</form>
)}
</Formik>
)}
{!formContent && (
<SubmitButton
className={classes.submitButton}
disabled={!isSubmittable()}
onClick={handleNext(selectedRadio || selectedFromDropdown?.code)}
error={error}
<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>
{step !== 0 && (
<WizardStep
step={step}
error={error}
lastStep={isLastStep}
{...getStepData()}
onContinue={onContinue}
getValue={getValue}
/>
)}
</Modal>
)
}

View file

@ -11,71 +11,64 @@ import { ReactComponent as LitecoinLogo } from 'src/styling/logos/icon-litecoin-
import { ReactComponent as ZCashLogo } from 'src/styling/logos/icon-zcash-colour.svg'
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, youll
<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, youll
have to setup all the necessary 3rd party services.
</P>
<Button onClick={() => handleModalNavigation(1)}>
<Button className={classes.button} onClick={onContinue}>
Start configuration
</Button>
</div>

View file

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

View file

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

View file

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

View file

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

View file

@ -5,9 +5,10 @@ import moment from 'moment'
import * as R from 'ramda'
import 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
/>
</>
)

View file

@ -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 = [

View file

@ -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>)

View file

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

View file

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

After

Width:  |  Height:  |  Size: 494 B

View file

@ -1,4 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="104" height="64" version="1.1">
<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>
<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

Before After
Before After

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