feat: add services page

fix: change styles, fix hook trigger, add ux

feat: setup custom error messages

refactor: conform to new style guide

refactor: migrate to graphql

refactor: migrate to Ramda

fix: update state on mutation

refactor: migrate error ux to graphql

fix: change structure of accounts config

fix: use absolute imports

fix: move makeStyles out of components

fix: correct Strike behaviour
This commit is contained in:
Luis Félix 2020-01-10 14:26:51 +00:00 committed by Josh Harvey
parent 1dba321052
commit b9d2341cd1
30 changed files with 2579 additions and 306 deletions

0
bin/insecure-dev.sh Normal file → Executable file
View file

124
lib/new-admin/accounts.js Normal file
View file

@ -0,0 +1,124 @@
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
}

File diff suppressed because it is too large Load diff

View file

@ -71,9 +71,9 @@ const Table = ({ children, className, ...props }) => (
</div>
)
const THead = ({ children }) => {
const THead = ({ children, className }) => {
const classes = useStyles()
return <div className={classes.header}>{children}</div>
return <div className={classnames(className, classes.header)}>{children}</div>
}
const TBody = ({ children, className }) => {

View file

@ -0,0 +1,42 @@
import React, { memo, useState } from 'react'
import { makeStyles } from '@material-ui/core'
import TextInputFormik from './TextInput'
import { styles } from './TextInput.styles'
const useStyles = makeStyles(styles)
const mask = /(\+9[976]\d|8[987530]\d|6[987]\d|5[90]\d|42\d|3[875]\d|2[98654321]\d|9[8543210]|8[6421]|6[6543210]|5[87654321]|4[987654310]|3[9643210]|2[70]|7|1)(\d{1,3}){0,1}(\d{1,3}){0,1}(\d{1,3}){0,1}(\d{1,3}){0,1}(\d{1,2}){0,1}$/
const maskValue = value => value.replace(mask, '$1 $2 $3 $4 $5 $6')
const PhoneNumberInputFormik = memo(({ ...props }) => {
const { onChange, value } = props.field
const classes = useStyles()
// Regex adapted from http://phoneregex.com/
const [maskedValue, setMaskedValue] = useState(maskValue(value))
const handleChange = event => {
setMaskedValue(maskValue(event.target.value))
onChange(event)
}
return (
<>
<span className={classes.masked} aria-hidden="true">
{maskedValue}
</span>
<TextInputFormik
inputProps={{ maxLength: 17 }}
className={classes.maskedInput}
onChange={handleChange}
{...props}
/>
</>
)
})
export { PhoneNumberInputFormik, mask, maskValue }

View file

@ -0,0 +1,46 @@
import React, { memo, useState } from 'react'
import classnames from 'classnames'
import { makeStyles } from '@material-ui/core'
import TextInputFormik from './TextInput'
import { styles } from './TextInput.styles'
const useStyles = makeStyles(styles)
const SecretInputFormik = memo(({ ...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
}
return (
<>
<span className={classnames(spanClass)} aria-hidden="true">
This field is set
</span>
<TextInputFormik
{...props}
onFocus={handleFocus}
className={classnames(inputClass)}
/>
</>
)
})
export default SecretInputFormik

View file

@ -0,0 +1,28 @@
import { fontColor, offColor } from 'src/styling/variables'
import typographyStyles from 'src/components/typography/styles'
const { info3 } = typographyStyles
const styles = {
masked: {
position: 'absolute',
bottom: 5,
color: fontColor
},
secretSpan: {
extend: info3,
color: offColor
},
hideSpan: {
display: 'none'
},
maskedInput: {
'& input': {
pointerEvents: 'none',
backgroundColor: 'transparent',
zIndex: -1
}
}
}
export { styles }

View file

@ -0,0 +1,160 @@
import React from 'react'
import classnames from 'classnames'
import { makeStyles } from '@material-ui/core'
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/white.svg'
import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg'
import { ReactComponent as WarningIcon } from 'src/styling/icons/warning-icon/comet.svg'
import {
offColor,
tableDisabledHeaderColor,
tableNewDisabledHeaderColor,
secondaryColorDarker
} from 'src/styling/variables'
import { Table, THead, TBody, Td, Th } from 'src/components/fake-table/Table'
import typographyStyles from 'src/components/typography/styles'
const { label1, p } = typographyStyles
const SingleRowTable = ({
width = 380,
height = 160,
title,
items,
onEdit,
disabled,
newService,
className,
...props
}) => {
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
}
return (
<>
{items && (
<Table className={classnames(className, classes.wrapper)}>
<THead className={classnames(headerClasses)}>
<Th size={width - editButtonSize}>
{title}
{newService && <span className={classes.spanNew}>New</span>}
</Th>
<Th size={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)}>
<Td size={width}>
{!disabled && (
<>
{items[0] && (
<div className={classes.itemWrapper}>
<div className={classes.label}>{items[0].label}</div>
<div className={classes.item}>{items[0].value}</div>
</div>
)}
{items[1] && (
<div className={classes.itemWrapper}>
<div className={classes.label}>{items[1].label}</div>
<div className={classes.item}>{items[1].value}</div>
</div>
)}
</>
)}
{disabled && (
<div className={classes.disabledWrapper}>
<span>
<WarningIcon />
</span>
<span>This service is not being used</span>
</div>
)}
</Td>
</TBody>
</Table>
)}
</>
)
}
export default SingleRowTable

View file

@ -0,0 +1,250 @@
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 BitgoForm = ({ account, handleSubmit, ...props }) => {
const getValue = getValueAux(account)
const { code } = 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)
const formik = {
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 fields = [
{
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
}
]
return (
<>
<EditService
title="Bitgo"
formik={formik}
code={code}
fields={fields}
{...props}
/>
</>
)
}
export { BitgoForm, BitgoCard }

View file

@ -0,0 +1,116 @@
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 BitstampForm = ({ account, ...props }) => {
const getValue = getValueAux(account)
const { code } = account
const clientId = getValue(schema.clientId.code)
const key = getValue(schema.key.code)
const secret = getValue(schema.secret.code)
const formik = {
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 fields = [
{
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
}
]
return (
<>
<EditService
title="Bitstamp"
formik={formik}
code={code}
fields={fields}
{...props}
/>
</>
)
}
export { BitstampForm, BitstampCard }

View file

@ -0,0 +1,101 @@
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 BlockcypherForm = ({ account, ...props }) => {
const getValue = getValueAux(account)
const { code } = account
const token = getValue(schema.token.code)
const confidenceFactor = getValue(schema.confidenceFactor.code)
const formik = {
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 fields = [
{
name: schema.token.code,
label: schema.token.display,
type: 'text',
component: TextInputFormik
},
{
name: schema.confidenceFactor.code,
label: schema.confidenceFactor.display,
type: 'text',
component: TextInputFormik
}
]
return (
<>
<EditService
title="Blockcypher"
code={code}
formik={formik}
fields={fields}
{...props}
/>
</>
)
}
export { BlockcypherForm, BlockcypherCard }

View file

@ -0,0 +1,94 @@
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,117 @@
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 InfuraForm = ({ account, ...props }) => {
const getValue = getValueAux(account)
const { code } = account
const apiKey = getValue(schema.apiKey.code)
const apiSecret = getValue(schema.apiSecret.code)
const endpoint = getValue(schema.endpoint.code)
const formik = {
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 fields = [
{
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
}
]
return (
<>
<EditService
title="Infura"
formik={formik}
code={code}
fields={fields}
{...props}
/>
</>
)
}
export { InfuraCard, InfuraForm }

View file

@ -0,0 +1,126 @@
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 ItbitForm = ({ account, ...props }) => {
const getValue = getValueAux(account)
const { code } = 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)
const formik = {
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 fields = [
{
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
}
]
return (
<>
<EditService
title="itBit"
formik={formik}
code={code}
fields={fields}
{...props}
/>
</>
)
}
export { ItbitCard, ItbitForm }

View file

@ -0,0 +1,101 @@
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 KrakenForm = ({ account, ...props }) => {
const getValue = getValueAux(account)
const { code } = account
const apiKey = getValue(schema.apiKey.code)
const privateKey = getValue(schema.privateKey.code)
const formik = {
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 fields = [
{
name: schema.apiKey.code,
label: schema.apiKey.display,
type: 'text',
component: TextInputFormik
},
{
name: schema.privateKey.code,
label: schema.privateKey.display,
type: 'text',
component: SecretInputFormik
}
]
return (
<>
<EditService
title="Kraken"
formik={formik}
code={code}
fields={fields}
{...props}
/>
</>
)
}
export { KrakenCard, KrakenForm }

View file

@ -0,0 +1,132 @@
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 MailgunForm = ({ account, ...props }) => {
const getValue = getValueAux(account)
const { code } = 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)
const formik = {
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 fields = [
{
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
}
]
return (
<>
<EditService
title="Mailgun"
formik={formik}
code={code}
fields={fields}
{...props}
/>
</>
)
}
export { MailgunCard, MailgunForm }

View file

@ -0,0 +1,242 @@
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 Title from 'src/components/Title'
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'
const useStyles = makeStyles(styles)
const GET_CONFIG = gql`
{
config
}
`
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 classes = useStyles()
useQuery(GET_CONFIG, {
onCompleted: data => setAccountsConfig(data.config.accounts ?? {})
})
const { data: accountsResponse } = useQuery(GET_ACCOUNTS)
const accounts = accountsResponse?.accounts
const save = (code, it) => {
const newAccounts = R.clone(accountsConfig)
newAccounts[code] = it
return saveConfig({ variables: { config: { accounts: newAccounts } } })
}
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 && (
<Modal
aria-labelledby="simple-modal-title"
aria-describedby="simple-modal-description"
open={open}
onClose={handleClose}
className={classes.modal}>
<div>{modalContent}</div>
</Modal>
)}
</>
)
}
export default Services

View file

@ -0,0 +1,177 @@
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

@ -0,0 +1,79 @@
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 StrikeForm = ({ account, ...props }) => {
const getValue = getValueAux(account)
const code = 'strike'
const token = getValue(schema.token.code)
const formik = {
initialValues: {
token: token
},
validationSchema: Yup.object().shape({
token: Yup.string()
.max(100, 'Too long')
.required('Required')
})
}
const fields = [
{
name: schema.token.code,
label: schema.token.display,
type: 'text',
component: SecretInputFormik
}
]
return (
<>
<EditService
title="Strike"
formik={formik}
code={code}
fields={fields}
{...props}
/>
</>
)
}
export { StrikeCard, StrikeForm }

View file

@ -0,0 +1,131 @@
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 TwilioForm = ({ account, ...props }) => {
const getValue = getValueAux(account)
const { code } = 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)
const formik = {
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 fields = [
{
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
}
]
return (
<>
<EditService
title="Twilio"
formik={formik}
code={code}
fields={fields}
{...props}
/>
</>
)
}
export { TwilioCard, TwilioForm }

View file

@ -0,0 +1,44 @@
import React from 'react'
import * as R from 'ramda'
import { makeStyles } from '@material-ui/core'
import SingleRowTable from 'src/components/single-row-table/SingleRowTable'
const getValue = R.curry((account, code) => 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

@ -10,6 +10,8 @@ import { comet } from 'src/styling/variables'
import { cpcStyles } from './Transactions.styles'
const useStyles = makeStyles(cpcStyles)
const CopyToClipboard = ({ className, children, ...props }) => {
const [anchorEl, setAnchorEl] = useState(null)
@ -17,8 +19,6 @@ const CopyToClipboard = ({ className, children, ...props }) => {
if (anchorEl) setTimeout(() => setAnchorEl(null), 3000)
}, [anchorEl])
const useStyles = makeStyles(cpcStyles)
const classes = useStyles()
const handleClick = event => {

View file

@ -19,18 +19,18 @@ import { onlyFirstToUpper } from 'src/utils/string'
import CopyToClipboard from './CopyToClipboard'
import { detailsRowStyles, labelStyles } from './Transactions.styles'
const Label = ({ children }) => {
const useStyles = makeStyles(labelStyles)
const labelUseStyles = makeStyles(labelStyles)
const classes = useStyles()
const Label = ({ children }) => {
const classes = labelUseStyles()
return <div className={classes.label}>{children}</div>
}
const DetailsRow = ({ it: tx, ...props }) => {
const useStyles = makeStyles(detailsRowStyles)
const detailsUseStyles = makeStyles(detailsRowStyles)
const classes = useStyles()
const DetailsRow = ({ it: tx, ...props }) => {
const classes = detailsUseStyles()
const addr = tx.toAddress
const txHash = tx.txHash

View file

@ -19,6 +19,8 @@ import { toUnit } from 'src/utils/coin'
import DetailsRow from './DetailsCard'
import { mainStyles } from './Transactions.styles'
const useStyles = makeStyles(mainStyles)
// TODO customerIdCardData
const GET_TRANSACTIONS = gql`
{
@ -49,8 +51,6 @@ const GET_TRANSACTIONS = gql`
const Transactions = () => {
const [anchorEl, setAnchorEl] = useState(null)
const useStyles = makeStyles(mainStyles)
const classes = useStyles()
const { data: txResponse } = useQuery(GET_TRANSACTIONS)

View file

@ -8,6 +8,7 @@ import Locales from 'src/pages/Locales'
import Logs from 'src/pages/Logs'
import ServerLogs from 'src/pages/ServerLogs'
import Transactions from 'src/pages/Transactions/Transactions'
import Services from 'src/pages/Services/Services'
import AuthRegister from 'src/pages/AuthRegister'
const tree = [
@ -38,7 +39,12 @@ const tree = [
label: 'Commissions',
route: '/settings/commissions'
},
{ key: 'locale', label: 'Locale', route: '/settings/locale' }
{ key: 'locale', label: 'Locale', route: '/settings/locale' },
{
key: 'services',
label: '3rd party services',
route: '/settings/3rd-party-services'
}
]
}
// compliance: { label: 'Compliance', children: [{ label: 'Locale', route: '/locale' }] }
@ -65,6 +71,7 @@ const Routes = () => (
/>
<Route path="/settings/commissions" component={Commissions} />
<Route path="/settings/locale" component={Locales} />
<Route path="/settings/3rd-party-services" component={Services} />
<Route path="/maintenance/logs" component={Logs} />
<Route path="/maintenance/funding" component={Funding} />
<Route path="/maintenance/server-logs" component={ServerLogs} />

View file

@ -1,24 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
<title>icon/action/close/zodiac</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M11.10567,8.99966734 L17.5616855,15.4562245 C18.1453995,16.0371533 18.1457502,16.9810221 17.5632744,17.5635465 C17.2826878,17.8441564 16.9043531,17.9996031 16.509308,17.9996031 C16.1150437,17.9996031 15.7367759,17.8439272 15.4563692,17.5634971 L8.99995005,11.1055617 L2.54567824,17.5603569 C2.26578723,17.8430155 1.88614994,17.9996031 1.48961505,17.9996031 C1.09473832,17.9996031 0.717380733,17.844225 0.436725633,17.5635465 C-0.145575211,16.9811971 -0.145575211,16.0373273 0.436725633,15.4578096 L6.89433001,8.99966707 L0.438314479,2.54310994 C-0.145399464,1.96218116 -0.145750215,1.01831232 0.436725633,0.435787934 C1.01746304,-0.144997872 1.95893893,-0.144997872 2.54250446,0.435787934 L8.99995074,6.89377234 L15.4580075,0.434202817 C16.0398949,-0.144908147 16.9801496,-0.144559181 17.5632744,0.435787934 C18.1455752,1.0181373 18.1455752,1.96200713 17.5632744,2.54152483 L11.10567,8.99966734 Z" id="path-1"></path>
<rect id="path-3" x="0" y="0" width="18" height="18"></rect>
</defs>
<g id="icon/action/close/zodiac" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="color/primary/zodiac" transform="translate(-0.000000, 0.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Mask" fill="#1B2559" xlink:href="#path-1"></use>
<g mask="url(#mask-2)">
<mask id="mask-4" fill="white">
<use xlink:href="#path-3"></use>
</mask>
<use id="Background" fill="#1B2559" fill-rule="evenodd" xlink:href="#path-3"></use>
</g>
</g>
</g>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 18 18" style="enable-background:new 0 0 18 18;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#1B2559;}
</style>
<title>icon/action/close/zodiac</title>
<desc>Created with Sketch.</desc>
<g id="color_x2F_primary_x2F_zodiac" transform="translate(-0.000000, 0.000000)">
<g id="Mask">
<path id="path-1_1_" class="st0" d="M11.1,9l6.5,6.5c0.6,0.6,0.6,1.5,0,2.1c-0.3,0.3-0.7,0.4-1.1,0.4c-0.4,0-0.8-0.2-1.1-0.4
L9,11.1l-6.5,6.5C2.3,17.8,1.9,18,1.5,18c-0.4,0-0.8-0.2-1.1-0.4c-0.6-0.6-0.6-1.5,0-2.1L6.9,9L0.4,2.5C-0.1,2-0.1,1,0.4,0.4
C1-0.1,2-0.1,2.5,0.4L9,6.9l6.5-6.5c0.6-0.6,1.5-0.6,2.1,0c0.6,0.6,0.6,1.5,0,2.1L11.1,9z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 964 B

Before After
Before After

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
<title>icon/warning-icon/comet</title>
<desc>Created with Sketch.</desc>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="icon/warning-icon/comet">
<rect id="Rectangle" stroke="#5F668A" stroke-width="2" x="1" y="1" width="22" height="22" rx="11"></rect>
<path d="M11.2971429,14.4857143 L11.1085714,5.82857143 L13.3714286,5.82857143 L13.2,14.4857143 L11.2971429,14.4857143 Z M11.1942857,18 L11.1942857,15.9771429 L13.3028571,15.9771429 L13.3028571,18 L11.1942857,18 Z" id="!" fill="#5F668A" fill-rule="nonzero"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 877 B

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
<title>icon/warning-icon/tomato</title>
<desc>Created with Sketch.</desc>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="icon/warning-icon/tomato">
<rect id="Rectangle" stroke="#FF584A" stroke-width="2" x="1" y="1" width="22" height="22" rx="11"></rect>
<path d="M11.2971429,14.4857143 L11.1085714,5.82857143 L13.3714286,5.82857143 L13.2,14.4857143 L11.2971429,14.4857143 Z M11.1942857,18 L11.1942857,15.9771429 L13.3028571,15.9771429 L13.3028571,18 L11.1942857,18 Z" id="!" fill="#FF584A" fill-rule="nonzero"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 879 B

View file

@ -103,6 +103,8 @@ const tableSmCellHeight = 30
const tableLgCellHeight = 76
const tableHeaderColor = primaryColor
const tableDisabledHeaderColor = zircon
const tableNewDisabledHeaderColor = spring3
const tableCellColor = white
const tableErrorColor = mistyRose
const tableSuccessColor = spring3
@ -172,6 +174,8 @@ export {
tableSmCellHeight,
tableLgCellHeight,
tableHeaderColor,
tableDisabledHeaderColor,
tableNewDisabledHeaderColor,
tableCellColor,
tableErrorColor,
tableSuccessColor

19
package-lock.json generated
View file

@ -58,7 +58,7 @@
"@ava/babel-preset-stage-4": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@ava/babel-preset-stage-4/-/babel-preset-stage-4-1.1.0.tgz",
"integrity": "sha1-rmC+iBoLq/fTX1Krp3DR9hlPdr0=",
"integrity": "sha512-oWqTnIGXW3k72UFidXzW0ONlO7hnO9x02S/QReJ7NBGeiBH9cUHY9+EfV6C8PXC6YJH++WrliEq03wMSJGNZFg==",
"dev": true,
"requires": {
"babel-plugin-check-es2015-constants": "^6.8.0",
@ -4070,7 +4070,7 @@
},
"dotenv": {
"version": "4.0.0",
"resolved": "http://registry.npmjs.org/dotenv/-/dotenv-4.0.0.tgz",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-4.0.0.tgz",
"integrity": "sha1-hk7xN5rO1Vzm+V3r7NzhefegzR0="
},
"drbg.js": {
@ -6230,7 +6230,7 @@
"globals": {
"version": "9.18.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz",
"integrity": "sha1-qjiWs+abSH8X4x7SFD1pqOMMLYo=",
"integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==",
"dev": true
},
"globby": {
@ -6257,7 +6257,7 @@
"got": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/got/-/got-7.1.0.tgz",
"integrity": "sha1-BUUP2ECU5rvqVvRRpDqcKJFmOFo=",
"integrity": "sha512-Y5WMo7xKKq1muPsxD+KmrR8DH5auG7fBdDVueZwETwV6VytKyU9OX/ddpq2/1hp1vIPvVb4T81dKQz3BivkNLw==",
"requires": {
"decompress-response": "^3.2.0",
"duplexer3": "^0.1.4",
@ -7146,7 +7146,7 @@
"hullabaloo-config-manager": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/hullabaloo-config-manager/-/hullabaloo-config-manager-1.1.1.tgz",
"integrity": "sha1-HZEXgTEprQNf2ehHfq8GaREmn+M=",
"integrity": "sha512-ztKnkZV0TmxnumCDHHgLGNiDnotu4EHCp9YMkznWuo4uTtCyJ+cu+RNcxUeXYKTllpvLFWnbfWry09yzszgg+A==",
"dev": true,
"requires": {
"dot-prop": "^4.1.0",
@ -7668,7 +7668,7 @@
"isurl": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz",
"integrity": "sha1-sn9PSfPNqj6kSgpbfzRi5u3DnWc=",
"integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==",
"requires": {
"has-to-string-tag-x": "^1.2.0",
"is-object": "^1.0.1"
@ -8586,7 +8586,7 @@
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"requires": {
"brace-expansion": "^1.1.7"
}
@ -9163,7 +9163,7 @@
"p-cancelable": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.3.0.tgz",
"integrity": "sha1-ueEjgAvOu3rBOkeb4ZW1B7mNMPo="
"integrity": "sha512-RVbZPLso8+jFeq1MfNvgXtCRED2raz/dKpacfTNxsx6pLEpEomM7gah6VeHSYV3+vo0OAi4MkArtQcWWXuQoyw=="
},
"p-each-series": {
"version": "1.0.0",
@ -12642,7 +12642,6 @@
"resolved": "https://registry.npmjs.org/web3/-/web3-0.20.6.tgz",
"integrity": "sha1-PpcwauAk+yThCj11yIQwJWIhUSA=",
"requires": {
"bignumber.js": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934",
"crypto-js": "^3.1.4",
"utf8": "^2.1.1",
"xhr2": "*",
@ -12651,7 +12650,7 @@
"dependencies": {
"bignumber.js": {
"version": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934",
"from": "git+https://github.com/frozeman/bignumber.js-nolookahead.git"
"from": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934"
}
}
},