Fix: make percentage chart work properly
Fix: code review Fix: rework components according to PR requested changes Fix: fix repeated code buildRatesNoCommission Also renames id to deviceId in transactions quer Fix: pr requested changes Chore: move inline styles to classes Chore: remove comment Fix: bad equality !process.env.NODE_ENV === 'production'
This commit is contained in:
parent
5572fb0eb1
commit
ae7eaca10c
43 changed files with 818 additions and 1578 deletions
5
lib/forex.js
Normal file
5
lib/forex.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
const axios = require('axios')
|
||||
|
||||
const getFiatRates = () => axios.get('https://bitpay.com/api/rates').then(response => response.data)
|
||||
|
||||
module.exports = { getFiatRates }
|
||||
|
|
@ -18,12 +18,11 @@ const couponManager = require('../../coupons')
|
|||
const serverVersion = require('../../../package.json').version
|
||||
const transactions = require('../transactions')
|
||||
const funding = require('../funding')
|
||||
const forex = require('../../forex')
|
||||
const supervisor = require('../supervisor')
|
||||
const serverLogs = require('../server-logs')
|
||||
const pairing = require('../pairing')
|
||||
|
||||
const plugins = require('../../plugins')
|
||||
const ticker = require('../../ticker')
|
||||
|
||||
const {
|
||||
accounts: accountsConfig,
|
||||
|
|
@ -240,6 +239,8 @@ const typeDefs = gql`
|
|||
created: Date
|
||||
age: Float
|
||||
deviceTime: Date
|
||||
}
|
||||
|
||||
type Rate {
|
||||
code: String
|
||||
name: String
|
||||
|
|
@ -268,25 +269,22 @@ const typeDefs = gql`
|
|||
until: Date
|
||||
limit: Int
|
||||
offset: Int
|
||||
id: ID
|
||||
deviceId: ID
|
||||
): [Transaction]
|
||||
transactionsCsv(from: Date, until: Date, limit: Int, offset: Int): String
|
||||
accounts: JSONObject
|
||||
config: JSONObject
|
||||
blacklist: [Blacklist]
|
||||
# userTokens: [UserToken]
|
||||
<<<<<<< HEAD
|
||||
coupons: [Coupon]
|
||||
cryptoRates: JSONObject
|
||||
fiatRates: [Rate]
|
||||
}
|
||||
|
||||
type SupportLogsResponse {
|
||||
id: ID!
|
||||
timestamp: Date!
|
||||
deviceId: ID
|
||||
=======
|
||||
rates: JSONObject
|
||||
btcRates(to: String, from: String): [Rate]
|
||||
>>>>>>> 9d88b4f... Feat: make dashboard and machine profile page
|
||||
}
|
||||
|
||||
enum MachineAction {
|
||||
|
|
@ -356,15 +354,26 @@ const resolvers = {
|
|||
serverLogs.getServerLogs(from, until, limit, offset),
|
||||
serverLogsCsv: (...[, { from, until, limit, offset }]) =>
|
||||
serverLogs.getServerLogs(from, until, limit, offset).then(parseAsync),
|
||||
transactions: (...[, { from, until, limit, offset, id }]) =>
|
||||
transactions.batch(from, until, limit, offset, id),
|
||||
transactions: (...[, { from, until, limit, offset, deviceId }]) =>
|
||||
transactions.batch(from, until, limit, offset, deviceId),
|
||||
transactionsCsv: (...[, { from, until, limit, offset }]) =>
|
||||
transactions.batch(from, until, limit, offset).then(parseAsync),
|
||||
config: () => settingsLoader.loadLatestConfigOrNone(),
|
||||
accounts: () => settingsLoader.loadAccounts(),
|
||||
blacklist: () => blacklist.getBlacklist(),
|
||||
// userTokens: () => tokenManager.getTokenList()
|
||||
coupons: () => couponManager.getAvailableCoupons()
|
||||
coupons: () => couponManager.getAvailableCoupons(),
|
||||
cryptoRates: () =>
|
||||
settingsLoader.loadLatest().then(settings => {
|
||||
const pi = plugins(settings)
|
||||
return pi.getRawRates().then(r => {
|
||||
return {
|
||||
withCommissions: pi.buildRates(r),
|
||||
withoutCommissions: pi.buildRatesNoCommission(r)
|
||||
}
|
||||
})
|
||||
}),
|
||||
fiatRates: () => forex.getFiatRates()
|
||||
},
|
||||
Mutation: {
|
||||
machineAction: (...[, { deviceId, action, cashbox, cassette1, cassette2, newName }]) => machineAction({ deviceId, action, cashbox, cassette1, cassette2, newName }),
|
||||
|
|
|
|||
|
|
@ -24,19 +24,8 @@ function addNames (txs) {
|
|||
|
||||
const camelize = _.mapKeys(_.camelCase)
|
||||
|
||||
function batch (
|
||||
from = new Date(0).toISOString(),
|
||||
until = new Date().toISOString(),
|
||||
limit = null,
|
||||
offset = 0,
|
||||
id = null
|
||||
) {
|
||||
const packager = _.flow(
|
||||
_.flatten,
|
||||
_.orderBy(_.property('created'), ['desc']),
|
||||
_.map(camelize),
|
||||
addNames
|
||||
)
|
||||
function batch (from = new Date(0).toISOString(), until = new Date().toISOString(), limit = null, offset = 0, id = null) {
|
||||
const packager = _.flow(_.flatten, _.orderBy(_.property('created'), ['desc']), _.map(camelize), addNames)
|
||||
|
||||
const cashInSql = `select 'cashIn' as tx_class, txs.*,
|
||||
c.phone as customer_phone,
|
||||
|
|
|
|||
|
|
@ -35,7 +35,8 @@ const PONG_TTL = '1 week'
|
|||
const tradesQueues = {}
|
||||
|
||||
function plugins (settings, deviceId) {
|
||||
function buildRatesNoCommission (tickers) {
|
||||
|
||||
function internalBuildRates (tickers, withCommission = true) {
|
||||
const localeConfig = configManager.getLocale(deviceId, settings.config)
|
||||
const cryptoCodes = localeConfig.cryptoCurrencies
|
||||
|
||||
|
|
@ -43,30 +44,7 @@ function plugins (settings, deviceId) {
|
|||
|
||||
cryptoCodes.forEach((cryptoCode, i) => {
|
||||
const rateRec = tickers[i]
|
||||
|
||||
if (!rateRec) return
|
||||
|
||||
if (Date.now() - rateRec.timestamp > STALE_TICKER)
|
||||
return logger.warn('Stale rate for ' + cryptoCode)
|
||||
const rate = rateRec.rates
|
||||
rates[cryptoCode] = {
|
||||
cashIn: rate.ask.round(5),
|
||||
cashOut: rate.bid.round(5)
|
||||
}
|
||||
})
|
||||
|
||||
return rates
|
||||
}
|
||||
|
||||
function buildRates (tickers) {
|
||||
const localeConfig = configManager.getLocale(deviceId, settings.config)
|
||||
const cryptoCodes = localeConfig.cryptoCurrencies
|
||||
|
||||
const rates = {}
|
||||
|
||||
cryptoCodes.forEach((cryptoCode, i) => {
|
||||
const commissions = configManager.getCommissions(cryptoCode, deviceId, settings.config)
|
||||
const rateRec = tickers[i]
|
||||
|
||||
if (!rateRec) return
|
||||
|
||||
|
|
@ -78,15 +56,26 @@ function plugins (settings, deviceId) {
|
|||
|
||||
if (Date.now() - rateRec.timestamp > STALE_TICKER) return logger.warn('Stale rate for ' + cryptoCode)
|
||||
const rate = rateRec.rates
|
||||
rates[cryptoCode] = {
|
||||
|
||||
withCommission ? rates[cryptoCode] = {
|
||||
cashIn: rate.ask.mul(cashInCommission).round(5),
|
||||
cashOut: cashOutCommission && rate.bid.div(cashOutCommission).round(5)
|
||||
} : rates[cryptoCode] = {
|
||||
cashIn: rate.ask.round(5),
|
||||
cashOut: rate.bid.round(5)
|
||||
}
|
||||
})
|
||||
|
||||
return rates
|
||||
}
|
||||
|
||||
function buildRatesNoCommission (tickers) {
|
||||
return internalBuildRates(tickers, false)
|
||||
}
|
||||
|
||||
function buildRates (tickers) {
|
||||
return internalBuildRates(tickers, true)
|
||||
}
|
||||
|
||||
function getNotificationConfig () {
|
||||
return configManager.getGlobalNotifications(settings.config)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ const mem = require('mem')
|
|||
const configManager = require('./new-config-manager')
|
||||
const ph = require('./plugin-helper')
|
||||
const logger = require('./logger')
|
||||
const axios = require('axios')
|
||||
|
||||
const lastRate = {}
|
||||
|
||||
|
|
@ -40,26 +39,4 @@ const getRates = mem(_getRates, {
|
|||
cacheKey: (settings, fiatCode, cryptoCode) => JSON.stringify([fiatCode, cryptoCode])
|
||||
})
|
||||
|
||||
const getBtcRates = (to = null, from = 'USD') => {
|
||||
// if to !== null, then we return only the rates with from (default USD) and to (so an array with 2 items)
|
||||
return axios.get('https://bitpay.com/api/rates').then(response => {
|
||||
const fxRates = response.data
|
||||
if (to === null) {
|
||||
return fxRates
|
||||
}
|
||||
const toRate = fxRates.find(o => o.code === to)
|
||||
const fromRate = fxRates.find(o => o.code === from)
|
||||
|
||||
let res = []
|
||||
if (toRate && to !== from) {
|
||||
res = [...res, toRate]
|
||||
}
|
||||
if (fromRate) {
|
||||
res = [...res, fromRate]
|
||||
}
|
||||
|
||||
return res
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { getBtcRates, getRates }
|
||||
module.exports = { getRates }
|
||||
|
|
|
|||
44
new-lamassu-admin/src/components/CollapsibleCard.js
Normal file
44
new-lamassu-admin/src/components/CollapsibleCard.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import Grid from '@material-ui/core/Grid'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
|
||||
import { white } from 'src/styling/variables'
|
||||
|
||||
const cardState = Object.freeze({
|
||||
DEFAULT: 'default',
|
||||
SHRUNK: 'shrunk',
|
||||
EXPANDED: 'expanded'
|
||||
})
|
||||
|
||||
const styles = {
|
||||
card: {
|
||||
wordWrap: 'break-word',
|
||||
boxShadow: '0 0 4px 0 rgba(0, 0, 0, 0.08)',
|
||||
borderRadius: 12,
|
||||
padding: 24,
|
||||
backgroundColor: white
|
||||
}
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const CollapsibleCard = ({ className, state, shrunkComponent, children }) => {
|
||||
const classes = useStyles()
|
||||
return (
|
||||
<div className={className}>
|
||||
<Grid item>
|
||||
<div className={classes.card}>
|
||||
{state === cardState.SHRUNK ? shrunkComponent : children}
|
||||
</div>
|
||||
</Grid>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
CollapsibleCard.propTypes = {
|
||||
shrunkComponent: PropTypes.node.isRequired
|
||||
}
|
||||
|
||||
export default CollapsibleCard
|
||||
export { cardState }
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import Button from '@material-ui/core/Button'
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
import { cardState as cardState_ } from 'src/components/CollapsibleCard'
|
||||
import { Label1, H4 } from 'src/components/typography'
|
||||
|
||||
import styles from '../Dashboard.styles'
|
||||
|
||||
import styles from './Alerts.styles'
|
||||
import AlertsTable from './AlertsTable'
|
||||
|
||||
const NUM_TO_RENDER = 3
|
||||
|
|
@ -23,86 +23,42 @@ const data = {
|
|||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const Alerts = ({ cardState, setRightSideState }) => {
|
||||
const Alerts = ({ onReset, onExpand, size }) => {
|
||||
const classes = useStyles()
|
||||
const [showAllItems, setShowAllItems] = useState(false)
|
||||
const [showExpandButton, setShowExpandButton] = useState(false)
|
||||
const [numToRender, setNumToRender] = useState(NUM_TO_RENDER)
|
||||
|
||||
useEffect(() => {
|
||||
if (showAllItems) {
|
||||
setShowExpandButton(false)
|
||||
setNumToRender(data?.alerts.length)
|
||||
} else if (data && data?.alerts.length > numToRender) {
|
||||
setShowExpandButton(true)
|
||||
}
|
||||
if (cardState.cardSize === 'small' || cardState.cardSize === 'default') {
|
||||
setShowAllItems(false)
|
||||
setNumToRender(NUM_TO_RENDER)
|
||||
}
|
||||
}, [cardState.cardSize, numToRender, showAllItems])
|
||||
const showAllItems = size === cardState_.EXPANDED
|
||||
|
||||
const reset = () => {
|
||||
setRightSideState({
|
||||
systemStatus: { cardSize: 'default', buttonName: 'Show less' },
|
||||
alerts: { cardSize: 'default', buttonName: 'Show less' }
|
||||
})
|
||||
setShowAllItems(false)
|
||||
setNumToRender(NUM_TO_RENDER)
|
||||
}
|
||||
|
||||
const showAllClick = () => {
|
||||
setShowExpandButton(false)
|
||||
setShowAllItems(true)
|
||||
setRightSideState({
|
||||
systemStatus: { cardSize: 'small', buttonName: 'Show machines' },
|
||||
alerts: { cardSize: 'big', buttonName: 'Show less' }
|
||||
})
|
||||
}
|
||||
const alertsLength = () => (data ? data.alerts.length : 0)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<H4 className={classes.h4}>{`Alerts ${
|
||||
data ? `(${data.alerts.length})` : 0
|
||||
}`}</H4>
|
||||
{(showAllItems || cardState.cardSize === 'small') && (
|
||||
<>
|
||||
<Label1
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
marginBottom: 0,
|
||||
marginTop: 0
|
||||
}}>
|
||||
<div className={classes.container}>
|
||||
<H4 className={classes.h4}>{`Alerts (${alertsLength()})`}</H4>
|
||||
{showAllItems && (
|
||||
<Label1 className={classes.upperButtonLabel}>
|
||||
<Button
|
||||
onClick={reset}
|
||||
onClick={onReset}
|
||||
size="small"
|
||||
disableRipple
|
||||
disableFocusRipple
|
||||
className={classes.button}>
|
||||
{cardState.buttonName}
|
||||
{'Show less'}
|
||||
</Button>
|
||||
</Label1>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{cardState.cardSize !== 'small' && (
|
||||
<>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12}>
|
||||
<AlertsTable
|
||||
numToRender={numToRender}
|
||||
numToRender={showAllItems ? data?.alerts.length : NUM_TO_RENDER}
|
||||
alerts={data?.alerts ?? []}
|
||||
/>
|
||||
{showExpandButton && (
|
||||
{!showAllItems && (
|
||||
<>
|
||||
<Label1 style={{ textAlign: 'center', marginBottom: 0 }}>
|
||||
<Label1 className={classes.centerLabel}>
|
||||
<Button
|
||||
onClick={showAllClick}
|
||||
onClick={() => onExpand('alerts')}
|
||||
size="small"
|
||||
disableRipple
|
||||
disableFocusRipple
|
||||
|
|
@ -115,7 +71,6 @@ const Alerts = ({ cardState, setRightSideState }) => {
|
|||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,15 @@ import {
|
|||
} from 'src/styling/variables'
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
centerLabel: {
|
||||
textAlign: 'center',
|
||||
marginBottom: 0,
|
||||
marginTop: 0
|
||||
},
|
||||
label: {
|
||||
margin: 0,
|
||||
color: offColor
|
||||
|
|
|
|||
|
|
@ -1,35 +1,21 @@
|
|||
import { withStyles } from '@material-ui/core'
|
||||
import List from '@material-ui/core/List'
|
||||
import ListItem from '@material-ui/core/ListItem'
|
||||
import ListItemText from '@material-ui/core/ListItemText'
|
||||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
|
||||
import styles from './Alerts.styles'
|
||||
// const useStyles = makeStyles(styles)
|
||||
|
||||
const StyledListItem = withStyles(() => ({
|
||||
root: {
|
||||
...styles.root
|
||||
}
|
||||
}))(ListItem)
|
||||
|
||||
const AlertsTable = ({ numToRender, alerts }) => {
|
||||
// const classes = useStyles()
|
||||
|
||||
const alertsToRender = R.slice(0, numToRender, alerts)
|
||||
return (
|
||||
<>
|
||||
<List dense>
|
||||
{alerts.map((alert, idx) => {
|
||||
if (idx < numToRender) {
|
||||
{alertsToRender.map((alert, idx) => {
|
||||
return (
|
||||
<StyledListItem key={idx}>
|
||||
<ListItem key={idx}>
|
||||
<ListItemText primary={alert.text} />
|
||||
</StyledListItem>
|
||||
</ListItem>
|
||||
)
|
||||
} else return null
|
||||
})}
|
||||
</List>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import Grid from '@material-ui/core/Grid'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import classnames from 'classnames'
|
||||
import React from 'react'
|
||||
|
||||
import TitleSection from 'src/components/layout/TitleSection'
|
||||
|
|
@ -19,13 +20,17 @@ const Dashboard = () => {
|
|||
<>
|
||||
<TitleSection title="Dashboard">
|
||||
<div className={classes.headerLabels}>
|
||||
<div>
|
||||
<div
|
||||
className={classnames(
|
||||
classes.headerLabelContainer,
|
||||
classes.headerLabelContainerMargin
|
||||
)}>
|
||||
<TxOutIcon />
|
||||
<span>Cash-out</span>
|
||||
<span className={classes.headerLabelSpan}>Cash-out</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className={classes.headerLabelContainer}>
|
||||
<TxInIcon />
|
||||
<span>Cash-in</span>
|
||||
<span className={classes.headerLabelSpan}>Cash-in</span>
|
||||
</div>
|
||||
</div>
|
||||
</TitleSection>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,21 @@ import { spacer, white, primaryColor } from 'src/styling/variables'
|
|||
const { label1 } = typographyStyles
|
||||
|
||||
const styles = {
|
||||
headerLabels: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
headerLabelContainerMargin: {
|
||||
marginRight: 24
|
||||
},
|
||||
headerLabelContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
},
|
||||
headerLabelSpan: {
|
||||
extend: label1,
|
||||
marginLeft: 6
|
||||
},
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
marginBottom: 108
|
||||
|
|
@ -17,37 +32,12 @@ const styles = {
|
|||
padding: 24,
|
||||
backgroundColor: white
|
||||
},
|
||||
h4: {
|
||||
margin: 0,
|
||||
marginRight: spacer * 8
|
||||
},
|
||||
label: {
|
||||
color: primaryColor,
|
||||
minHeight: 0,
|
||||
minWidth: 0,
|
||||
padding: 0,
|
||||
textTransform: 'none',
|
||||
'&:hover': {
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
},
|
||||
actionButton: {
|
||||
marginTop: -4
|
||||
},
|
||||
headerLabels: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
'& div': {
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
},
|
||||
'& > div:first-child': {
|
||||
leftSideMargin: {
|
||||
marginRight: 24
|
||||
},
|
||||
'& span': {
|
||||
extend: label1,
|
||||
marginLeft: 6
|
||||
}
|
||||
container: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
button: {
|
||||
color: primaryColor,
|
||||
|
|
@ -58,6 +48,15 @@ const styles = {
|
|||
'&:hover': {
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
},
|
||||
upperButtonLabel: {
|
||||
textAlign: 'center',
|
||||
marginBottom: 0,
|
||||
marginTop: 16,
|
||||
marginLeft: spacer
|
||||
},
|
||||
alertsCard: {
|
||||
marginBottom: 16
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +1,21 @@
|
|||
import { useQuery } from '@apollo/react-hooks'
|
||||
import { makeStyles } from '@material-ui/core'
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
import BigNumber from 'bignumber.js'
|
||||
import classnames from 'classnames'
|
||||
import gql from 'graphql-tag'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { Label2 } from 'src/components/typography'
|
||||
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
|
||||
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
|
||||
import { white, spacer } from 'src/styling/variables'
|
||||
import { fromNamespace } from 'src/utils/config'
|
||||
|
||||
import styles from './Footer.styles'
|
||||
const GET_DATA = gql`
|
||||
query getData {
|
||||
rates
|
||||
cryptoRates
|
||||
cryptoCurrencies {
|
||||
code
|
||||
display
|
||||
|
|
@ -26,23 +27,20 @@ const GET_DATA = gql`
|
|||
}
|
||||
}
|
||||
`
|
||||
BigNumber.config({ ROUNDING_MODE: BigNumber.ROUND_HALF_UP })
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
const Footer = () => {
|
||||
const { data, loading } = useQuery(GET_DATA)
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [canExpand, setCanExpand] = useState(false)
|
||||
const [delayedExpand, setDelayedExpand] = useState(null)
|
||||
const classes = useStyles()
|
||||
|
||||
useEffect(() => {
|
||||
if (data && data.rates && data.rates.withCommissions) {
|
||||
const numItems = R.keys(data.rates.withCommissions).length
|
||||
if (numItems > 4) {
|
||||
setCanExpand(true)
|
||||
}
|
||||
}
|
||||
}, [data])
|
||||
const classes = useStyles({
|
||||
bigFooter: R.keys(data?.cryptoRates?.withCommissions).length < 8,
|
||||
expanded
|
||||
})
|
||||
|
||||
const canExpand = R.keys(data?.cryptoRates.withCommissions ?? []).length > 4
|
||||
|
||||
const wallets = fromNamespace('wallets')(data?.config)
|
||||
|
||||
|
|
@ -56,49 +54,51 @@ const Footer = () => {
|
|||
const tickerName = data.accountsConfig[tickerIdx].display
|
||||
|
||||
const cashInNoCommission = parseFloat(
|
||||
R.path(['rates', 'withoutCommissions', key, 'cashIn'])(data)
|
||||
R.path(['cryptoRates', 'withoutCommissions', key, 'cashIn'])(data)
|
||||
)
|
||||
const cashOutNoCommission = parseFloat(
|
||||
R.path(['rates', 'withoutCommissions', key, 'cashOut'])(data)
|
||||
R.path(['cryptoRates', 'withoutCommissions', key, 'cashOut'])(data)
|
||||
)
|
||||
|
||||
// check https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round
|
||||
// to see reason for this implementation. It makes 1.005 round to 1.01 and not 1
|
||||
// const monetaryValue = +(Math.round(askBidAvg + 'e+2') + 'e-2')
|
||||
const avgOfAskBid = +(
|
||||
Math.round((cashInNoCommission + cashOutNoCommission) / 2 + 'e+2') + 'e-2'
|
||||
const avgOfAskBid = new BigNumber(
|
||||
(cashInNoCommission + cashOutNoCommission) / 2
|
||||
)
|
||||
const cashIn = +(
|
||||
Math.round(
|
||||
parseFloat(R.path(['rates', 'withCommissions', key, 'cashIn'])(data)) +
|
||||
'e+2'
|
||||
) + 'e-2'
|
||||
.decimalPlaces(2)
|
||||
.toNumber()
|
||||
const cashIn = new BigNumber(
|
||||
parseFloat(
|
||||
R.path(['cryptoRates', 'withCommissions', key, 'cashIn'])(data)
|
||||
)
|
||||
const cashOut = +(
|
||||
Math.round(
|
||||
parseFloat(R.path(['rates', 'withCommissions', key, 'cashOut'])(data)) +
|
||||
'e+2'
|
||||
) + 'e-2'
|
||||
)
|
||||
.decimalPlaces(2)
|
||||
.toNumber()
|
||||
const cashOut = new BigNumber(
|
||||
parseFloat(
|
||||
R.path(['cryptoRates', 'withCommissions', key, 'cashOut'])(data)
|
||||
)
|
||||
)
|
||||
.decimalPlaces(2)
|
||||
.toNumber()
|
||||
|
||||
const localeFiatCurrency = data.config.locale_fiatCurrency
|
||||
const localeLanguage = data.config.locale_languages[0]
|
||||
|
||||
return (
|
||||
<Grid key={key} item xs={3} style={{ marginBottom: 18 }}>
|
||||
<Grid key={key} item xs={3} className={classes.footerItemContainer}>
|
||||
<Label2 className={classes.label}>
|
||||
{data.cryptoCurrencies[idx].display}
|
||||
</Label2>
|
||||
<div className={classes.headerLabels}>
|
||||
<div>
|
||||
<div className={classes.headerLabel}>
|
||||
<TxInIcon />
|
||||
<Label2>{` ${cashIn.toLocaleString(
|
||||
'en-US'
|
||||
localeLanguage
|
||||
)} ${localeFiatCurrency}`}</Label2>
|
||||
</div>
|
||||
<div>
|
||||
<div className={classnames(classes.headerLabel, classes.txOutMargin)}>
|
||||
<TxOutIcon />
|
||||
<Label2>{` ${cashOut.toLocaleString(
|
||||
'en-US'
|
||||
localeLanguage
|
||||
)} ${localeFiatCurrency}`}</Label2>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -106,62 +106,31 @@ const Footer = () => {
|
|||
className={
|
||||
classes.tickerLabel
|
||||
}>{`${tickerName}: ${avgOfAskBid.toLocaleString(
|
||||
'en-US'
|
||||
localeLanguage
|
||||
)} ${localeFiatCurrency}`}</Label2>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
const makeFooterExpandedClass = () => {
|
||||
return {
|
||||
height:
|
||||
R.keys(data.rates.withCommissions).length < 8
|
||||
? spacer * 12 * 2 + spacer * 2
|
||||
: spacer * 12 * 3 + spacer * 3,
|
||||
maxHeight: '50vh',
|
||||
position: 'fixed',
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
width: '100vw',
|
||||
backgroundColor: white,
|
||||
textAlign: 'left'
|
||||
}
|
||||
}
|
||||
|
||||
const expand = () => {
|
||||
if (canExpand) {
|
||||
setExpanded(true)
|
||||
}
|
||||
}
|
||||
|
||||
const shrink = () => {
|
||||
setExpanded(false)
|
||||
}
|
||||
const handleMouseEnter = () => {
|
||||
setDelayedExpand(
|
||||
setTimeout(() => {
|
||||
expand()
|
||||
}, 300)
|
||||
)
|
||||
setDelayedExpand(setTimeout(() => canExpand && setExpanded(true), 300))
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
clearTimeout(delayedExpand)
|
||||
shrink()
|
||||
setExpanded(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
className={classes.footer}
|
||||
style={expanded ? makeFooterExpandedClass() : null}>
|
||||
className={classes.footer}>
|
||||
<div className={classes.content}>
|
||||
{!loading && data && (
|
||||
<Grid container spacing={1}>
|
||||
<Grid container className={classes.footerContainer}>
|
||||
{R.keys(data.rates.withCommissions).map(key =>
|
||||
{R.keys(data.cryptoRates.withCommissions).map(key =>
|
||||
renderFooterItem(key)
|
||||
)}
|
||||
</Grid>
|
||||
|
|
@ -169,7 +138,6 @@ const Footer = () => {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,75 +1,39 @@
|
|||
import typographyStyles from 'src/components/typography/styles'
|
||||
import {
|
||||
backgroundColor,
|
||||
offColor,
|
||||
errorColor,
|
||||
primaryColor,
|
||||
white,
|
||||
spacer
|
||||
} from 'src/styling/variables'
|
||||
const { label1 } = typographyStyles
|
||||
import { offColor, white, spacer } from 'src/styling/variables'
|
||||
|
||||
const styles = {
|
||||
label: {
|
||||
color: offColor
|
||||
},
|
||||
tickerLabel: {
|
||||
color: offColor,
|
||||
marginTop: -5
|
||||
},
|
||||
row: {
|
||||
backgroundColor: backgroundColor,
|
||||
borderBottom: 'none'
|
||||
},
|
||||
header: {
|
||||
headerLabels: {
|
||||
whiteSpace: 'pre',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
whiteSpace: 'pre'
|
||||
flexDirection: 'row',
|
||||
marginTop: -20
|
||||
},
|
||||
error: {
|
||||
color: errorColor
|
||||
headerLabel: {
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
},
|
||||
button: {
|
||||
color: primaryColor,
|
||||
minHeight: 0,
|
||||
minWidth: 0,
|
||||
padding: 0,
|
||||
textTransform: 'none',
|
||||
'&:hover': {
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
txOutMargin: {
|
||||
marginLeft: spacer * 3
|
||||
},
|
||||
statusHeader: {
|
||||
marginLeft: 2
|
||||
},
|
||||
table: {
|
||||
maxHeight: 440,
|
||||
'&::-webkit-scrollbar': {
|
||||
width: 7
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor: offColor,
|
||||
borderRadius: 5
|
||||
}
|
||||
},
|
||||
tableBody: {
|
||||
overflow: 'auto'
|
||||
},
|
||||
h4: {
|
||||
marginTop: 0
|
||||
},
|
||||
root: {
|
||||
flexGrow: 1
|
||||
},
|
||||
footer: {
|
||||
footer: ({ expanded, bigFooter }) => ({
|
||||
height: expanded
|
||||
? bigFooter
|
||||
? spacer * 12 * 2 + spacer * 2
|
||||
: spacer * 12 * 3 + spacer * 3
|
||||
: spacer * 12,
|
||||
position: 'fixed',
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
width: '100vw',
|
||||
backgroundColor: white,
|
||||
textAlign: 'left',
|
||||
height: spacer * 12,
|
||||
boxShadow: '0px -1px 10px 0px rgba(50, 50, 50, 0.1)'
|
||||
}),
|
||||
tickerLabel: {
|
||||
color: offColor,
|
||||
marginTop: -5
|
||||
},
|
||||
content: {
|
||||
width: 1200,
|
||||
|
|
@ -77,26 +41,12 @@ const styles = {
|
|||
backgroundColor: white,
|
||||
marginTop: 4
|
||||
},
|
||||
headerLabels: {
|
||||
whiteSpace: 'pre',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
'& div': {
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
},
|
||||
'& > div:first-child': {
|
||||
marginRight: 24
|
||||
},
|
||||
'& span': {
|
||||
extend: label1,
|
||||
marginLeft: 6
|
||||
},
|
||||
marginTop: -20
|
||||
},
|
||||
footerContainer: {
|
||||
marginLeft: spacer * 5,
|
||||
marginBottom: spacer * 2
|
||||
},
|
||||
footerItemContainer: {
|
||||
marginBottom: 18
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,20 +7,18 @@ import SystemPerformance from './SystemPerformance'
|
|||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const RightSide = () => {
|
||||
const LeftSide = () => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid item xs={6}>
|
||||
<Grid item style={{ marginRight: 24 }}>
|
||||
<Grid item className={classes.leftSideMargin}>
|
||||
<div className={classes.card}>
|
||||
<SystemPerformance />
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default RightSide
|
||||
export default LeftSide
|
||||
|
|
|
|||
|
|
@ -1,49 +1,88 @@
|
|||
import Button from '@material-ui/core/Button'
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import CollapsibleCard, { cardState } from 'src/components/CollapsibleCard'
|
||||
import { H4, Label1 } from 'src/components/typography'
|
||||
|
||||
// import Alerts from './Alerts'
|
||||
import styles from './Dashboard.styles'
|
||||
import SystemStatus from './SystemStatus'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const RightSide = () => {
|
||||
const ShrunkCard = ({ title, buttonName, onUnshrink }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const [rightSideState, setRightSide] = useState({
|
||||
alerts: {
|
||||
cardSize: 'default',
|
||||
buttonName: 'Show less'
|
||||
},
|
||||
systemStatus: {
|
||||
cardSize: 'default',
|
||||
buttonName: 'Show less'
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
<H4 className={classes.h4}>{title}</H4>
|
||||
<>
|
||||
<Label1 className={classes.upperButtonLabel}>
|
||||
<Button
|
||||
onClick={onUnshrink}
|
||||
size="small"
|
||||
disableRipple
|
||||
disableFocusRipple
|
||||
className={classes.button}>
|
||||
{buttonName}
|
||||
</Button>
|
||||
</Label1>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const setRightSideState = newState => {
|
||||
setRightSide(newState)
|
||||
const RightSide = () => {
|
||||
// const classes = useStyles()
|
||||
const [systemStatusSize, setSystemStatusSize] = useState(cardState.DEFAULT)
|
||||
// const [alertsSize, setAlertsSize] = useState(cardState.DEFAULT)
|
||||
|
||||
const onReset = () => {
|
||||
// setAlertsSize(cardState.DEFAULT)
|
||||
setSystemStatusSize(cardState.DEFAULT)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid item xs={6}>
|
||||
{/* <Grid item style={{ marginBottom: 16 }}>
|
||||
<div className={classes.card}>
|
||||
{/* <CollapsibleCard
|
||||
className={classes.alertsCard}
|
||||
state={alertsSize}
|
||||
shrunkComponent={
|
||||
<ShrunkCard
|
||||
title={'Alerts'}
|
||||
buttonName={'Show alerts'}
|
||||
onUnshrink={onReset}
|
||||
/>
|
||||
}>
|
||||
<Alerts
|
||||
cardState={rightSideState.alerts}
|
||||
setRightSideState={setRightSideState}
|
||||
onExpand={() => {
|
||||
setAlertsSize(cardState.EXPANDED)
|
||||
setSystemStatusSize(cardState.SHRUNK)
|
||||
}}
|
||||
onReset={onReset}
|
||||
size={alertsSize}
|
||||
/>
|
||||
</div>
|
||||
</Grid> */}
|
||||
<Grid item>
|
||||
<div className={classes.card}>
|
||||
</CollapsibleCard> */}
|
||||
<CollapsibleCard
|
||||
state={systemStatusSize}
|
||||
shrunkComponent={
|
||||
<ShrunkCard
|
||||
title={'System status'}
|
||||
buttonName={'Show machines'}
|
||||
onUnshrink={onReset}
|
||||
/>
|
||||
}>
|
||||
<SystemStatus
|
||||
cardState={rightSideState.systemStatus}
|
||||
setRightSideState={setRightSideState}
|
||||
onExpand={() => {
|
||||
setSystemStatusSize(cardState.EXPANDED)
|
||||
// setAlertsSize(cardState.SHRUNK)
|
||||
}}
|
||||
onReset={onReset}
|
||||
size={systemStatusSize}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
</CollapsibleCard>
|
||||
</Grid>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { makeStyles } from '@material-ui/core'
|
||||
import classnames from 'classnames'
|
||||
import React from 'react'
|
||||
|
||||
import { ReactComponent as CashIn } from 'src/styling/icons/direction/cash-in.svg'
|
||||
|
|
@ -30,58 +31,46 @@ const styles = {
|
|||
fontFamily: fontSecondary,
|
||||
fontWeight: 700,
|
||||
color: fontColor
|
||||
}
|
||||
},
|
||||
cashIn: ({ value }) => ({
|
||||
width: `${value}%`,
|
||||
marginRight: 4
|
||||
}),
|
||||
cashOut: ({ value }) => ({
|
||||
width: `${100 - value}%`
|
||||
})
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const PercentageChart = () => {
|
||||
const classes = useStyles()
|
||||
|
||||
const value = 50
|
||||
const PercentageChart = ({ cashIn, cashOut }) => {
|
||||
const value = cashIn || cashOut !== 0 ? cashIn : 50
|
||||
const classes = useStyles({ value })
|
||||
|
||||
const buildPercentageView = (value, direction) => {
|
||||
switch (direction) {
|
||||
case 'cashIn':
|
||||
if (value > 20) {
|
||||
const Operation = direction === 'cashIn' ? CashIn : CashOut
|
||||
if (value > 25) {
|
||||
return (
|
||||
<>
|
||||
<CashIn />
|
||||
<span className={classes.label}>{` ${value}%`}</span>
|
||||
<Operation />
|
||||
{value > 25 && <span className={classes.label}>{` ${value}%`}</span>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
return null
|
||||
case 'cashOut':
|
||||
if (value > 20) {
|
||||
return (
|
||||
<>
|
||||
<CashOut />
|
||||
<span className={classes.label}>{` ${value}%`}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
return null
|
||||
default:
|
||||
return null
|
||||
if (value >= 10) {
|
||||
return <Operation />
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes.wrapper}>
|
||||
<div
|
||||
className={classes.percentageBox}
|
||||
style={{ width: `${value}%`, marginRight: 4 }}>
|
||||
<div className={classnames(classes.percentageBox, classes.cashIn)}>
|
||||
{buildPercentageView(value, 'cashIn')}
|
||||
</div>
|
||||
<div
|
||||
className={classes.percentageBox}
|
||||
style={{ width: `${100 - value}%` }}>
|
||||
<div className={classnames(classes.percentageBox, classes.cashOut)}>
|
||||
{buildPercentageView(100 - value, 'cashOut')}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,80 +1,66 @@
|
|||
import * as d3 from 'd3'
|
||||
import * as R from 'ramda'
|
||||
import React, { useEffect, useRef, useCallback, useState } from 'react'
|
||||
import React, { useEffect, useRef, useCallback } from 'react'
|
||||
|
||||
import { backgroundColor, zircon, primaryColor } from 'src/styling/variables'
|
||||
|
||||
const transactionProfit = tx => {
|
||||
const cashInFee = tx.cashInFee ? Number.parseFloat(tx.cashInFee) : 0
|
||||
const commission =
|
||||
Number.parseFloat(tx.commissionPercentage) * Number.parseFloat(tx.fiat)
|
||||
return commission + cashInFee
|
||||
}
|
||||
|
||||
const mockPoint = tx => {
|
||||
const date = new Date(tx.created)
|
||||
date.setHours(date.getHours() - 1)
|
||||
return { created: date.toISOString(), profit: tx.profit }
|
||||
}
|
||||
|
||||
// if we're viewing transactions for the past day, then we group by hour. If not, we group by day
|
||||
const formatDay = ({ created }) =>
|
||||
new Date(created).toISOString().substring(0, 10)
|
||||
const formatHour = ({ created }) =>
|
||||
new Date(created).toISOString().substring(0, 13)
|
||||
|
||||
const reducer = (acc, tx) => {
|
||||
const currentProfit = acc.profit || 0
|
||||
return { ...tx, profit: currentProfit + transactionProfit(tx) }
|
||||
}
|
||||
|
||||
const RefLineChart = ({ data: realData, timeFrame }) => {
|
||||
const svgRef = useRef()
|
||||
|
||||
// this variable will flip to true if there's no data points or the profit is zero
|
||||
// this will force the line graph to touch the x axis instead of centering,
|
||||
// centering is bad because it gives the impression that there could be negative values
|
||||
// so, if this is true the y domain should be [0, 0.1]
|
||||
const [zeroProfit, setZeroProfit] = useState(false)
|
||||
|
||||
const drawGraph = useCallback(() => {
|
||||
const svg = d3.select(svgRef.current)
|
||||
const margin = { top: 0, right: 0, bottom: 0, left: 0 }
|
||||
const width = 336 - margin.left - margin.right
|
||||
const height = 128 - margin.top - margin.bottom
|
||||
|
||||
const transactionProfit = tx => {
|
||||
let cashInFee = 0
|
||||
if (tx.cashInFee) {
|
||||
cashInFee = Number.parseFloat(tx.cashInFee)
|
||||
}
|
||||
const commission =
|
||||
Number.parseFloat(tx.commissionPercentage) * Number.parseFloat(tx.fiat)
|
||||
return commission + cashInFee
|
||||
}
|
||||
|
||||
const massageData = () => {
|
||||
const methods = {
|
||||
day: function(obj) {
|
||||
return new Date(obj.created).toISOString().substring(0, 10)
|
||||
},
|
||||
hour: function(obj) {
|
||||
return new Date(obj.created).toISOString().substring(0, 13)
|
||||
}
|
||||
}
|
||||
// if we're viewing transactions for the past day, then we group by hour. If not, we group by day
|
||||
const method = timeFrame === 'Day' ? formatHour : formatDay
|
||||
|
||||
const method = timeFrame === 'Day' ? 'hour' : 'day'
|
||||
const f = methods[method]
|
||||
const groupedTx = R.values(R.groupBy(f)(realData))
|
||||
let aggregatedTX = groupedTx.map(list => {
|
||||
const temp = { ...list[0], profit: transactionProfit(list[0]) }
|
||||
if (list.length > 1) {
|
||||
for (let i = 1; i < list.length; i++) {
|
||||
temp.profit += transactionProfit(list[i])
|
||||
}
|
||||
}
|
||||
return temp
|
||||
})
|
||||
|
||||
// if no point exists, then create a (0,0) point
|
||||
const aggregatedTX = R.values(R.reduceBy(reducer, [], method, realData))
|
||||
// if no point exists, then return 2 points at y = 0
|
||||
if (aggregatedTX.length === 0) {
|
||||
setZeroProfit(true)
|
||||
aggregatedTX = [{ created: new Date().toISOString(), profit: 0 }]
|
||||
} else {
|
||||
setZeroProfit(false)
|
||||
const mockPoint1 = { created: new Date().toISOString(), profit: 0 }
|
||||
const mockPoint2 = mockPoint(mockPoint1)
|
||||
return [[mockPoint1, mockPoint2], true]
|
||||
}
|
||||
// create point on the left if only one point exists, otherwise line won't be drawn
|
||||
// if only one point exists, create point on the left - otherwise the line won't be drawn
|
||||
if (aggregatedTX.length === 1) {
|
||||
const temp = { ...aggregatedTX[0] }
|
||||
const date = new Date(temp.created)
|
||||
date.setHours(date.getHours() - 1)
|
||||
temp.created = date.toISOString()
|
||||
aggregatedTX = [...aggregatedTX, temp]
|
||||
return [R.append(mockPoint(aggregatedTX[0]), aggregatedTX), false]
|
||||
}
|
||||
return aggregatedTX
|
||||
// the boolean value is for zeroProfit. It makes the line render at y = 0 instead of y = 50% of container height
|
||||
return [aggregatedTX, false]
|
||||
}
|
||||
|
||||
/* Important step to make the graph look good!
|
||||
This function groups transactions by either day or hour depending on the time grame
|
||||
This function groups transactions by either day or hour depending on the time frame
|
||||
This makes the line look smooth and not all wonky when there are many transactions in a given time
|
||||
*/
|
||||
const data = massageData()
|
||||
const [data, zeroProfit] = massageData()
|
||||
|
||||
// sets width of the graph
|
||||
svg.attr('width', width)
|
||||
|
|
@ -162,7 +148,7 @@ const RefLineChart = ({ data: realData, timeFrame }) => {
|
|||
.attr('stroke-width', '2')
|
||||
.attr('stroke-linejoin', 'round')
|
||||
.attr('stroke', primaryColor)
|
||||
}, [realData, timeFrame, zeroProfit])
|
||||
}, [realData, timeFrame])
|
||||
|
||||
useEffect(() => {
|
||||
// first we clear old chart DOM elements on component update
|
||||
|
|
|
|||
|
|
@ -18,21 +18,17 @@ const RefScatterplot = ({ data: realData, timeFrame }) => {
|
|||
// finds maximum value for the Y axis. Minimum value is 100. If value is multiple of 1000, add 100
|
||||
// (this is because the Y axis looks best with multiples of 100)
|
||||
const findMaxY = () => {
|
||||
if (realData.length === 0) {
|
||||
return 100
|
||||
}
|
||||
let maxY = d3.max(realData, t => parseFloat(t.fiat))
|
||||
maxY = 100 * Math.ceil(maxY / 100)
|
||||
if (maxY < 100) {
|
||||
return 100
|
||||
} else if (maxY % 1000 === 0) {
|
||||
return maxY + 100
|
||||
}
|
||||
if (realData.length === 0) return 100
|
||||
const maxvalueTx =
|
||||
100 * Math.ceil(d3.max(realData, t => parseFloat(t.fiat)) / 100)
|
||||
const maxY = Math.max(100, maxvalueTx)
|
||||
if (maxY % 1000 === 0) return maxY + 100
|
||||
return maxY
|
||||
}
|
||||
|
||||
// changes values of arguments in some d3 function calls to make the graph labels look good according to the selected time frame
|
||||
const findXAxisSettings = () => {
|
||||
// case 'Day' or default
|
||||
const res = {
|
||||
nice: null,
|
||||
ticks: 4,
|
||||
|
|
@ -41,11 +37,8 @@ const RefScatterplot = ({ data: realData, timeFrame }) => {
|
|||
timeRange: [50, 500]
|
||||
}
|
||||
switch (timeFrame) {
|
||||
case 'Day':
|
||||
return res
|
||||
case 'Week':
|
||||
return {
|
||||
...res,
|
||||
nice: 7,
|
||||
ticks: 7,
|
||||
subtractDays: 7,
|
||||
|
|
@ -54,7 +47,6 @@ const RefScatterplot = ({ data: realData, timeFrame }) => {
|
|||
}
|
||||
case 'Month':
|
||||
return {
|
||||
...res,
|
||||
nice: 6,
|
||||
ticks: 6,
|
||||
subtractDays: 30,
|
||||
|
|
@ -133,10 +125,8 @@ const RefScatterplot = ({ data: realData, timeFrame }) => {
|
|||
.ticks(xAxisSettings.ticks)
|
||||
.tickSize(0)
|
||||
.tickFormat(d3.timeFormat(xAxisSettings.timeFormat))
|
||||
// .tickFormat(d3.timeFormat('%H:%M'))
|
||||
)
|
||||
.selectAll('text')
|
||||
// .attr('dx', '4em')
|
||||
.attr('dy', '1.5em')
|
||||
// this is for the x axis line. It is the same color as the horizontal grid lines
|
||||
g.append('g')
|
||||
|
|
|
|||
|
|
@ -1,134 +0,0 @@
|
|||
/*eslint-disable*/
|
||||
import { scaleLinear, scaleTime, max, axisLeft, axisBottom, select } from 'd3'
|
||||
import React, { useMemo } from 'react'
|
||||
import moment from 'moment'
|
||||
|
||||
const data = [
|
||||
[0, '2020-11-08T18:00:05.664Z'],
|
||||
[40.01301, '2020-11-09T11:17:05.664Z']
|
||||
]
|
||||
|
||||
const marginTop = 10
|
||||
const marginRight = 30
|
||||
const marginBottom = 30
|
||||
const marginLeft = 60
|
||||
const width = 510 - marginLeft - marginRight
|
||||
const height = 141 - marginTop - marginBottom
|
||||
|
||||
const Scatterplot = ({ data: realData }) => {
|
||||
const x = scaleTime()
|
||||
.domain([
|
||||
moment()
|
||||
.add(-1, 'day')
|
||||
.valueOf(),
|
||||
moment().valueOf()
|
||||
])
|
||||
.range([0, width])
|
||||
.nice()
|
||||
|
||||
const y = scaleLinear()
|
||||
.domain([0, 1000])
|
||||
.range([height, 0])
|
||||
.nice()
|
||||
|
||||
// viewBox="0 0 540 141"
|
||||
return (
|
||||
<>
|
||||
<svg
|
||||
width={width + marginLeft + marginRight}
|
||||
height={height + marginTop + marginBottom}>
|
||||
<g transform={`translate(${marginLeft},${marginTop})`}>
|
||||
<XAxis
|
||||
transform={`translate(0, ${height + marginTop})`}
|
||||
scale={x}
|
||||
numTicks={6}
|
||||
/>
|
||||
<g>{axisLeft(y)}</g>
|
||||
{/* <YAxis transform={`translate(0, 0)`} scale={y} numTicks={6} /> */}
|
||||
<RenderCircles data={data} scale={{ x, y }} />
|
||||
</g>
|
||||
</svg>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const XAxis = ({
|
||||
range = [10, 500],
|
||||
transform,
|
||||
scale: xScale,
|
||||
numTicks = 7
|
||||
}) => {
|
||||
const ticks = useMemo(() => {
|
||||
return xScale.ticks(numTicks).map(value => ({
|
||||
value,
|
||||
xOffset: xScale(value)
|
||||
}))
|
||||
}, [range.join('-')])
|
||||
|
||||
return (
|
||||
<g transform={transform}>
|
||||
{ticks.map(({ value, xOffset }) => (
|
||||
<g key={value} transform={`translate(${xOffset}, 0)`}>
|
||||
<text
|
||||
key={value}
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
textAnchor: 'middle',
|
||||
transform: 'translateY(10px)'
|
||||
}}>
|
||||
{value.getHours()}
|
||||
</text>
|
||||
</g>
|
||||
))}
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
const YAxis = ({
|
||||
range = [10, 500],
|
||||
transform,
|
||||
scale: xScale,
|
||||
numTicks = 7
|
||||
}) => {
|
||||
const ticks = useMemo(() => {
|
||||
return xScale.ticks(numTicks).map(value => ({
|
||||
value,
|
||||
xOffset: xScale(value)
|
||||
}))
|
||||
}, [range.join('-')])
|
||||
|
||||
return (
|
||||
<g transform={transform}>
|
||||
{ticks.map(({ value, xOffset }) => (
|
||||
<g key={value} transform={`translate(0, ${xOffset})`}>
|
||||
<text
|
||||
key={value}
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
textAnchor: 'middle',
|
||||
transform: 'translateX(-10px)'
|
||||
}}>
|
||||
{value}
|
||||
</text>
|
||||
</g>
|
||||
))}
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
const RenderCircles = ({ data, scale }) => {
|
||||
let renderCircles = data.map((item, idx) => {
|
||||
return (
|
||||
<circle
|
||||
cx={scale.x(new Date(item[1]))}
|
||||
cy={scale.y(item[0])}
|
||||
r="4"
|
||||
style={{ fill: 'rgba(25, 158, 199, .9)' }}
|
||||
key={idx}
|
||||
/>
|
||||
)
|
||||
})
|
||||
return <g>{renderCircles}</g>
|
||||
}
|
||||
|
||||
export default Scatterplot
|
||||
|
|
@ -1,21 +1,20 @@
|
|||
// import Button from '@material-ui/core/Button'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import classnames from 'classnames'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { H4 } from 'src/components/typography'
|
||||
|
||||
import styles from './SystemPerformance.styles'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
const ranges = ['Month', 'Week', 'Day']
|
||||
|
||||
const Nav = ({ handleSetRange }) => {
|
||||
const classes = useStyles()
|
||||
const [clickedItem, setClickedItem] = useState('Day')
|
||||
|
||||
const isSelected = innerText => {
|
||||
return innerText === clickedItem
|
||||
}
|
||||
|
||||
const isSelected = R.equals(clickedItem)
|
||||
const handleClick = range => {
|
||||
setClickedItem(range)
|
||||
handleSetRange(range)
|
||||
|
|
@ -26,34 +25,21 @@ const Nav = ({ handleSetRange }) => {
|
|||
<div className={classes.titleAndButtonsContainer}>
|
||||
<H4 className={classes.h4}>{'System performance'}</H4>
|
||||
</div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<div className={classes.navContainer}>
|
||||
{ranges.map((it, idx) => {
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
onClick={e => handleClick(e.target.innerText)}
|
||||
className={
|
||||
isSelected('Month')
|
||||
isSelected(it)
|
||||
? classnames(classes.newHighlightedLabel, classes.navButton)
|
||||
: classnames(classes.label, classes.navButton)
|
||||
}>
|
||||
Month
|
||||
</div>
|
||||
<div
|
||||
onClick={e => handleClick(e.target.innerText)}
|
||||
className={
|
||||
isSelected('Week')
|
||||
? classnames(classes.newHighlightedLabel, classes.navButton)
|
||||
: classnames(classes.label, classes.navButton)
|
||||
}>
|
||||
Week
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
isSelected('Day')
|
||||
? classnames(classes.newHighlightedLabel, classes.navButton)
|
||||
: classnames(classes.label, classes.navButton)
|
||||
}
|
||||
onClick={e => handleClick(e.target.innerText)}>
|
||||
Day
|
||||
{it}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { useQuery } from '@apollo/react-hooks'
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import BigNumber from 'bignumber.js'
|
||||
import gql from 'graphql-tag'
|
||||
import moment from 'moment'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { Label2 } from 'src/components/typography/index'
|
||||
import { ReactComponent as TriangleDown } from 'src/styling/icons/arrow/triangle_down.svg'
|
||||
|
|
@ -18,18 +19,31 @@ import InfoWithLabel from './InfoWithLabel'
|
|||
import Nav from './Nav'
|
||||
import styles from './SystemPerformance.styles'
|
||||
|
||||
const isNotProp = R.curry(R.compose(R.isNil, R.prop))
|
||||
BigNumber.config({ ROUNDING_MODE: BigNumber.ROUND_HALF_UP })
|
||||
|
||||
const getFiats = R.map(R.prop('fiat'))
|
||||
const getProps = propName => R.map(R.prop(propName))
|
||||
const useStyles = makeStyles(styles)
|
||||
const mapToFee = R.map(R.prop('cashInFee'))
|
||||
|
||||
const getDateSecondsAgo = (seconds = 0, startDate = null) => {
|
||||
if (startDate) {
|
||||
return moment(startDate).subtract(seconds, 'second')
|
||||
}
|
||||
return moment().subtract(seconds, 'second')
|
||||
const date = startDate ? moment(startDate) : moment()
|
||||
return date.subtract(seconds, 'second')
|
||||
}
|
||||
|
||||
// const now = moment()
|
||||
const ranges = {
|
||||
Day: {
|
||||
left: getDateSecondsAgo(2 * 24 * 3600, moment()),
|
||||
right: getDateSecondsAgo(24 * 3600, moment())
|
||||
},
|
||||
Week: {
|
||||
left: getDateSecondsAgo(14 * 24 * 3600, moment()),
|
||||
right: getDateSecondsAgo(7 * 24 * 3600, moment())
|
||||
},
|
||||
Month: {
|
||||
left: getDateSecondsAgo(60 * 24 * 3600, moment()),
|
||||
right: getDateSecondsAgo(30 * 24 * 3600, moment())
|
||||
}
|
||||
}
|
||||
|
||||
const GET_DATA = gql`
|
||||
query getData {
|
||||
|
|
@ -42,7 +56,7 @@ const GET_DATA = gql`
|
|||
txClass
|
||||
error
|
||||
}
|
||||
btcRates {
|
||||
fiatRates {
|
||||
code
|
||||
name
|
||||
rate
|
||||
|
|
@ -51,134 +65,72 @@ const GET_DATA = gql`
|
|||
}
|
||||
`
|
||||
|
||||
const reducer = (acc, it) =>
|
||||
(acc +=
|
||||
Number.parseFloat(it.commissionPercentage) * Number.parseFloat(it.fiat))
|
||||
|
||||
const SystemPerformance = () => {
|
||||
const classes = useStyles()
|
||||
|
||||
const [selectedRange, setSelectedRange] = useState('Day')
|
||||
const [transactionsToShow, setTransactionsToShow] = useState([])
|
||||
const [transactionsLastTimePeriod, setTransactionsLastTimePeriod] = useState(
|
||||
[]
|
||||
)
|
||||
|
||||
const { data, loading } = useQuery(GET_DATA)
|
||||
|
||||
const fiatLocale = fromNamespace('locale')(data?.config).fiatCurrency
|
||||
|
||||
useEffect(() => {
|
||||
const isInRange = (getLastTimePeriod = false) => t => {
|
||||
const now = moment()
|
||||
switch (selectedRange) {
|
||||
case 'Day':
|
||||
if (getLastTimePeriod) {
|
||||
const isInRangeAndNoError = getLastTimePeriod => t => {
|
||||
if (t.error !== null) return false
|
||||
if (!getLastTimePeriod) {
|
||||
return (
|
||||
t.error === null &&
|
||||
moment(t.created).isBetween(
|
||||
getDateSecondsAgo(2 * 24 * 3600, now),
|
||||
getDateSecondsAgo(24 * 3600, now)
|
||||
)
|
||||
)
|
||||
}
|
||||
return (
|
||||
t.error === null &&
|
||||
moment(t.created).isBetween(getDateSecondsAgo(24 * 3600, now), now)
|
||||
)
|
||||
case 'Week':
|
||||
if (getLastTimePeriod) {
|
||||
return (
|
||||
t.error === null &&
|
||||
moment(t.created).isBetween(
|
||||
getDateSecondsAgo(14 * 24 * 3600, now),
|
||||
getDateSecondsAgo(7 * 24 * 3600, now)
|
||||
)
|
||||
moment(t.created).isBetween(ranges[selectedRange].right, moment())
|
||||
)
|
||||
}
|
||||
return (
|
||||
t.error === null &&
|
||||
moment(t.created).isBetween(
|
||||
getDateSecondsAgo(7 * 24 * 3600, now),
|
||||
now
|
||||
ranges[selectedRange].left,
|
||||
ranges[selectedRange].right
|
||||
)
|
||||
)
|
||||
case 'Month':
|
||||
if (getLastTimePeriod) {
|
||||
return (
|
||||
t.error === null &&
|
||||
moment(t.created).isBetween(
|
||||
getDateSecondsAgo(60 * 24 * 3600, now),
|
||||
getDateSecondsAgo(30 * 24 * 3600, now)
|
||||
)
|
||||
)
|
||||
}
|
||||
return (
|
||||
t.error === null &&
|
||||
moment(t.created).isBetween(
|
||||
getDateSecondsAgo(30 * 24 * 3600, now),
|
||||
now
|
||||
)
|
||||
)
|
||||
default:
|
||||
return t.error === null && true
|
||||
}
|
||||
}
|
||||
|
||||
const convertFiatToLocale = item => {
|
||||
if (item.fiatCode === fiatLocale) return item
|
||||
const itemRate = R.find(R.propEq('code', item.fiatCode))(data.btcRates)
|
||||
const localeRate = R.find(R.propEq('code', fiatLocale))(data.btcRates)
|
||||
const itemRate = R.find(R.propEq('code', item.fiatCode))(data.fiatRates)
|
||||
const localeRate = R.find(R.propEq('code', fiatLocale))(data.fiatRates)
|
||||
const multiplier = localeRate.rate / itemRate.rate
|
||||
return { ...item, fiat: parseFloat(item.fiat) * multiplier }
|
||||
}
|
||||
|
||||
setTransactionsToShow(
|
||||
R.map(convertFiatToLocale)(
|
||||
R.filter(isInRange(false), data?.transactions ?? [])
|
||||
const transactionsToShow = R.map(convertFiatToLocale)(
|
||||
R.filter(isInRangeAndNoError(false), data?.transactions ?? [])
|
||||
)
|
||||
const transactionsLastTimePeriod = R.map(convertFiatToLocale)(
|
||||
R.filter(isInRangeAndNoError(true), data?.transactions ?? [])
|
||||
)
|
||||
setTransactionsLastTimePeriod(
|
||||
R.map(convertFiatToLocale)(
|
||||
R.filter(isInRange(true), data?.transactions ?? [])
|
||||
)
|
||||
)
|
||||
}, [data, fiatLocale, selectedRange])
|
||||
|
||||
const handleSetRange = range => {
|
||||
setSelectedRange(range)
|
||||
}
|
||||
|
||||
const getNumTransactions = () => {
|
||||
return R.length(R.filter(isNotProp('error'), transactionsToShow))
|
||||
return R.length(transactionsToShow)
|
||||
}
|
||||
|
||||
const getFiatVolume = () => {
|
||||
// for explanation check https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round
|
||||
return +(
|
||||
Math.round(
|
||||
R.sum(getFiats(R.filter(isNotProp('error'), transactionsToShow))) +
|
||||
'e+2'
|
||||
) + 'e-2'
|
||||
)
|
||||
}
|
||||
const getFiatVolume = () =>
|
||||
new BigNumber(R.sum(getFiats(transactionsToShow)))
|
||||
.decimalPlaces(2)
|
||||
.toNumber()
|
||||
|
||||
const getProfit = (transactions = transactionsToShow) => {
|
||||
const cashInFees = R.sum(
|
||||
getProps('cashInFee')(R.filter(isNotProp('error'), transactions))
|
||||
)
|
||||
let commissionFees = 0
|
||||
transactions.forEach(t => {
|
||||
if (t.error === null) {
|
||||
commissionFees +=
|
||||
Number.parseFloat(t.commissionPercentage) * Number.parseFloat(t.fiat)
|
||||
}
|
||||
})
|
||||
return +(Math.round(commissionFees + cashInFees + 'e+2') + 'e-2')
|
||||
const getProfit = transactions => {
|
||||
const cashInFees = R.sum(mapToFee(transactions))
|
||||
const commissionFees = R.reduce(reducer, 0, transactions)
|
||||
|
||||
return new BigNumber(commissionFees + cashInFees)
|
||||
.decimalPlaces(2)
|
||||
.toNumber()
|
||||
}
|
||||
|
||||
const getPercentChange = () => {
|
||||
const thisTimePeriodProfit = getProfit(transactionsToShow)
|
||||
const previousTimePeriodProfit = getProfit(transactionsLastTimePeriod)
|
||||
if (previousTimePeriodProfit === 0) {
|
||||
return 100
|
||||
}
|
||||
|
||||
if (previousTimePeriodProfit === 0) return 100
|
||||
|
||||
return Math.round(
|
||||
(100 * (thisTimePeriodProfit - previousTimePeriodProfit)) /
|
||||
Math.abs(previousTimePeriodProfit)
|
||||
|
|
@ -186,36 +138,17 @@ const SystemPerformance = () => {
|
|||
}
|
||||
|
||||
const getDirectionPercent = () => {
|
||||
const directions = {
|
||||
cashIn: 0,
|
||||
cashOut: 0,
|
||||
length: 0
|
||||
const [cashIn, cashOut] = R.partition(R.propEq('txClass', 'cashIn'))(
|
||||
transactionsToShow
|
||||
)
|
||||
const totalLength = cashIn.length + cashOut.length
|
||||
if (totalLength === 0) {
|
||||
return { cashIn: 0, cashOut: 0 }
|
||||
}
|
||||
transactionsToShow.forEach(t => {
|
||||
if (t.error === null) {
|
||||
switch (t.txClass) {
|
||||
case 'cashIn':
|
||||
directions.cashIn += 1
|
||||
directions.length += 1
|
||||
break
|
||||
case 'cashOut':
|
||||
directions.cashOut += 1
|
||||
directions.length += 1
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
cashIn:
|
||||
directions.length > 0
|
||||
? Math.round((directions.cashIn / directions.length) * 100)
|
||||
: 0,
|
||||
cashOut:
|
||||
directions.length > 0
|
||||
? Math.round((directions.cashOut / directions.length) * 100)
|
||||
: 0
|
||||
cashIn: Math.round((cashIn.length / totalLength) * 100),
|
||||
cashOut: Math.round((cashOut.length / totalLength) * 100)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -223,7 +156,7 @@ const SystemPerformance = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Nav handleSetRange={handleSetRange} />
|
||||
<Nav handleSetRange={setSelectedRange} />
|
||||
{!loading && (
|
||||
<>
|
||||
<Grid container spacing={2}>
|
||||
|
|
@ -241,7 +174,7 @@ const SystemPerformance = () => {
|
|||
</Grid>
|
||||
{/* todo new customers */}
|
||||
</Grid>
|
||||
<Grid container style={{ marginTop: 30 }}>
|
||||
<Grid container className={classes.gridContainer}>
|
||||
<Grid item xs={12}>
|
||||
<Label2>Transactions</Label2>
|
||||
<Scatterplot
|
||||
|
|
@ -250,27 +183,23 @@ const SystemPerformance = () => {
|
|||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container style={{ marginTop: 30 }}>
|
||||
<Grid container className={classes.gridContainer}>
|
||||
<Grid item xs={8}>
|
||||
<Label2>Profit from commissions</Label2>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
margin: '0 26px -30px 16px',
|
||||
position: 'relative'
|
||||
}}>
|
||||
<div className={classes.profitContainer}>
|
||||
<div className={classes.profitLabel}>
|
||||
{`${getProfit()} ${data?.config.locale_fiatCurrency}`}
|
||||
{`${getProfit(transactionsToShow)} ${
|
||||
data?.config.locale_fiatCurrency
|
||||
}`}
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
percentChange <= 0 ? classes.percentDown : classes.percentUp
|
||||
}>
|
||||
{percentChange <= 0 ? (
|
||||
<TriangleDown style={{ height: 13 }} />
|
||||
<TriangleDown className={classes.percentDown} />
|
||||
) : (
|
||||
<TriangleUp style={{ height: 10 }} />
|
||||
<TriangleUp className={classes.percentUp} />
|
||||
)}{' '}
|
||||
{`${percentChange}%`}
|
||||
</div>
|
||||
|
|
@ -281,7 +210,10 @@ const SystemPerformance = () => {
|
|||
<Label2>Direction</Label2>
|
||||
<Grid container>
|
||||
<Grid item xs>
|
||||
<PercentageChart data={getDirectionPercent()} />
|
||||
<PercentageChart
|
||||
cashIn={getDirectionPercent().cashIn}
|
||||
cashOut={getDirectionPercent().cashOut}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import {
|
|||
fontSize3,
|
||||
fontSecondary,
|
||||
fontColor,
|
||||
secondaryColorDarker,
|
||||
linkSecondaryColor
|
||||
spring4,
|
||||
tomato
|
||||
} from 'src/styling/variables'
|
||||
|
||||
const styles = {
|
||||
|
|
@ -59,6 +59,9 @@ const styles = {
|
|||
navButton: {
|
||||
marginLeft: 24
|
||||
},
|
||||
navContainer: {
|
||||
display: 'flex'
|
||||
},
|
||||
profitLabel: {
|
||||
fontSize: fontSize3,
|
||||
fontFamily: fontSecondary,
|
||||
|
|
@ -69,13 +72,24 @@ const styles = {
|
|||
fontSize: fontSize3,
|
||||
fontFamily: fontSecondary,
|
||||
fontWeight: 700,
|
||||
color: secondaryColorDarker
|
||||
color: spring4,
|
||||
height: 10
|
||||
},
|
||||
percentDown: {
|
||||
fontSize: fontSize3,
|
||||
fontFamily: fontSecondary,
|
||||
fontWeight: 700,
|
||||
color: linkSecondaryColor
|
||||
color: tomato,
|
||||
height: 13
|
||||
},
|
||||
profitContainer: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
margin: '0 26px -30px 16px',
|
||||
position: 'relative'
|
||||
},
|
||||
gridContainer: {
|
||||
marginTop: 30
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import TableCell from '@material-ui/core/TableCell'
|
|||
import TableContainer from '@material-ui/core/TableContainer'
|
||||
import TableHead from '@material-ui/core/TableHead'
|
||||
import TableRow from '@material-ui/core/TableRow'
|
||||
import classnames from 'classnames'
|
||||
import React from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
|
||||
|
|
@ -57,7 +58,6 @@ const MachinesTable = ({ machines, numToRender }) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableContainer className={classes.table}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
|
|
@ -97,9 +97,8 @@ const MachinesTable = ({ machines, numToRender }) => {
|
|||
return (
|
||||
<TableRow
|
||||
onClick={() => redirect(machine.name)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
key={machine.deviceId + idx}
|
||||
className={classes.row}>
|
||||
className={classnames(classes.row, classes.clickableRow)}
|
||||
key={machine.deviceId + idx}>
|
||||
<StyledCell align="left">
|
||||
<TL2>{machine.name}</TL2>
|
||||
</StyledCell>
|
||||
|
|
@ -123,7 +122,6 @@ const MachinesTable = ({ machines, numToRender }) => {
|
|||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ import {
|
|||
} from 'src/styling/variables'
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
label: {
|
||||
margin: 0,
|
||||
color: offColor
|
||||
|
|
@ -14,6 +18,9 @@ const styles = {
|
|||
backgroundColor: backgroundColor,
|
||||
borderBottom: 'none'
|
||||
},
|
||||
clickableRow: {
|
||||
cursor: 'pointer'
|
||||
},
|
||||
header: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
|
|
@ -32,11 +39,21 @@ const styles = {
|
|||
backgroundColor: 'transparent'
|
||||
}
|
||||
},
|
||||
buttonLabel: {
|
||||
textAlign: 'center',
|
||||
marginBottom: 0
|
||||
},
|
||||
upperButtonLabel: {
|
||||
textAlign: 'center',
|
||||
marginBottom: 0,
|
||||
marginTop: 0
|
||||
},
|
||||
statusHeader: {
|
||||
marginLeft: 2
|
||||
},
|
||||
/* table: {
|
||||
maxHeight: 440,
|
||||
/* // temporary class, until alerts are enabled. Delete this table class and uncomment the other one
|
||||
table: {
|
||||
height: 463,
|
||||
'&::-webkit-scrollbar': {
|
||||
width: 7
|
||||
},
|
||||
|
|
@ -45,10 +62,8 @@ const styles = {
|
|||
borderRadius: 5
|
||||
}
|
||||
}, */
|
||||
// temporary, when notifications are enabled delete this one and decomment above
|
||||
table: {
|
||||
maxHeight: 465,
|
||||
minHeight: 465,
|
||||
maxHeight: 440,
|
||||
'&::-webkit-scrollbar': {
|
||||
width: 7
|
||||
},
|
||||
|
|
@ -62,6 +77,15 @@ const styles = {
|
|||
},
|
||||
h4: {
|
||||
marginTop: 0
|
||||
},
|
||||
tl2: {
|
||||
display: 'inline'
|
||||
},
|
||||
label1: {
|
||||
display: 'inline'
|
||||
},
|
||||
machinesTableContainer: {
|
||||
marginTop: 23
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { useQuery } from '@apollo/react-hooks'
|
||||
// import Button from '@material-ui/core/Button'
|
||||
import Button from '@material-ui/core/Button'
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import gql from 'graphql-tag'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
import { cardState as cardState_ } from 'src/components/CollapsibleCard'
|
||||
// import ActionButton from 'src/components/buttons/ActionButton'
|
||||
import { H4, TL2, Label1 } from 'src/components/typography'
|
||||
|
||||
|
|
@ -14,7 +15,7 @@ import styles from './MachinesTable.styles'
|
|||
const useStyles = makeStyles(styles)
|
||||
|
||||
// number of machines in the table to render on page load
|
||||
// const NUM_TO_RENDER = 3
|
||||
const NUM_TO_RENDER = 3
|
||||
|
||||
const GET_DATA = gql`
|
||||
query getData {
|
||||
|
|
@ -38,97 +39,56 @@ const GET_DATA = gql`
|
|||
}
|
||||
`
|
||||
|
||||
const parseUptime = time => {
|
||||
/* const parseUptime = time => {
|
||||
if (time < 60) return `${time}s`
|
||||
if (time < 3600) return `${Math.floor(time / 60)}m`
|
||||
if (time < 86400) return `${Math.floor(time / 60 / 60)}h`
|
||||
return `${Math.floor(time / 60 / 60 / 24)}d`
|
||||
}
|
||||
} */
|
||||
|
||||
const SystemStatus = ({ cardState, setRightSideState }) => {
|
||||
const SystemStatus = ({ onReset, onExpand, size }) => {
|
||||
const classes = useStyles()
|
||||
const { data, loading } = useQuery(GET_DATA)
|
||||
const [showAllItems, setShowAllItems] = useState(false)
|
||||
// const [showExpandButton, setShowExpandButton] = useState(false)
|
||||
// const [numToRender, setNumToRender] = useState(NUM_TO_RENDER)
|
||||
|
||||
useEffect(() => {
|
||||
/* if (showAllItems) {
|
||||
setShowExpandButton(false)
|
||||
setNumToRender(data?.machines.length)
|
||||
} else if (data && data?.machines.length > numToRender) {
|
||||
setShowExpandButton(true)
|
||||
} */
|
||||
if (cardState.cardSize === 'small' || cardState.cardSize === 'default') {
|
||||
setShowAllItems(false)
|
||||
// setNumToRender(NUM_TO_RENDER)
|
||||
}
|
||||
}, [cardState.cardSize, data, /* numToRender, */ showAllItems])
|
||||
const showAllItems = size === cardState_.EXPANDED
|
||||
|
||||
/* const reset = () => {
|
||||
setShowAllItems(false)
|
||||
setNumToRender(NUM_TO_RENDER)
|
||||
setRightSideState({
|
||||
systemStatus: { cardSize: 'default', buttonName: 'Show less' },
|
||||
alerts: { cardSize: 'default', buttonName: 'Show less' }
|
||||
})
|
||||
}
|
||||
|
||||
const showAllClick = () => {
|
||||
setShowExpandButton(false)
|
||||
setShowAllItems(true)
|
||||
setRightSideState({
|
||||
systemStatus: { cardSize: 'big', buttonName: 'Show less' },
|
||||
alerts: { cardSize: 'small', buttonName: 'Show alerts' }
|
||||
})
|
||||
} */
|
||||
|
||||
// placeholder data
|
||||
if (data) {
|
||||
data.uptime = [{ time: 1854125, state: 'RUNNING' }]
|
||||
}
|
||||
|
||||
const uptime = data?.uptime ?? [{}]
|
||||
// const uptime = data?.uptime ?? [{}]
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div className={classes.container}>
|
||||
<H4 className={classes.h4}>System status</H4>{' '}
|
||||
</div>
|
||||
{/* {(showAllItems || cardState.cardSize === 'small') && (
|
||||
<>
|
||||
<Label1
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
marginBottom: 0,
|
||||
marginTop: 0
|
||||
}}>
|
||||
{showAllItems && (
|
||||
<Label1 className={classes.upperButtonLabel}>
|
||||
<Button
|
||||
onClick={reset}
|
||||
onClick={onReset}
|
||||
size="small"
|
||||
disableRipple
|
||||
disableFocusRipple
|
||||
className={classes.button}>
|
||||
{cardState.buttonName}
|
||||
{'Show less'}
|
||||
</Button>
|
||||
</Label1>
|
||||
</>
|
||||
)}
|
||||
</div> */}
|
||||
{!loading && cardState.cardSize !== 'small' && (
|
||||
</div>
|
||||
{!loading && (
|
||||
<>
|
||||
<Grid container spacing={1}>
|
||||
{/*
|
||||
On hold until system uptime is implemented
|
||||
<Grid item xs={4}>
|
||||
<TL2 style={{ display: 'inline' }}>
|
||||
<TL2 className={classes.tl2}>
|
||||
{parseUptime(uptime[0].time)}
|
||||
</TL2>
|
||||
<Label1 style={{ display: 'inline' }}> System up time</Label1>
|
||||
<Label1 className={classes.label1}> System up time</Label1>
|
||||
</Grid> */}
|
||||
<Grid item xs={4}>
|
||||
<TL2 className={classes.tl2}>{data?.serverVersion}</TL2>
|
||||
<Label1 className={classes.label1}> server version</Label1>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<TL2 style={{ display: 'inline' }}>{data?.serverVersion}</TL2>
|
||||
<Label1 style={{ display: 'inline' }}> server version</Label1>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
{/* <ActionButton
|
||||
{/*
|
||||
On hold until system update features are implemented
|
||||
<ActionButton
|
||||
color="primary"
|
||||
className={classes.actionButton}
|
||||
onClick={() => console.log('Upgrade button clicked')}>
|
||||
|
|
@ -136,18 +96,22 @@ const SystemStatus = ({ cardState, setRightSideState }) => {
|
|||
</ActionButton> */}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container spacing={1} style={{ marginTop: 23 }}>
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
className={classes.machinesTableContainer}>
|
||||
<Grid item xs={12}>
|
||||
<MachinesTable
|
||||
/* numToRender={numToRender} */
|
||||
numToRender={Infinity}
|
||||
numToRender={
|
||||
showAllItems ? data?.machines.length : NUM_TO_RENDER
|
||||
}
|
||||
machines={data?.machines ?? []}
|
||||
/>
|
||||
{/* {showExpandButton && (
|
||||
{!showAllItems && data.machines.length > NUM_TO_RENDER && (
|
||||
<>
|
||||
<Label1 style={{ textAlign: 'center', marginBottom: 0 }}>
|
||||
<Label1 className={classes.buttonLabel}>
|
||||
<Button
|
||||
onClick={showAllClick}
|
||||
onClick={() => onExpand()}
|
||||
size="small"
|
||||
disableRipple
|
||||
disableFocusRipple
|
||||
|
|
@ -156,7 +120,7 @@ const SystemStatus = ({ cardState, setRightSideState }) => {
|
|||
</Button>
|
||||
</Label1>
|
||||
</>
|
||||
)} */}
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -115,9 +115,7 @@ const CashCassettes = ({ machine, config, refetchData }) => {
|
|||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{machine.name && (
|
||||
return machine.name ? (
|
||||
<EditableTable
|
||||
error={error?.message}
|
||||
stripeWhen={isCashOutDisabled}
|
||||
|
|
@ -129,9 +127,7 @@ const CashCassettes = ({ machine, config, refetchData }) => {
|
|||
save={onSave}
|
||||
validationSchema={ValidationSchema}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
) : null
|
||||
}
|
||||
|
||||
export default CashCassettes
|
||||
|
|
|
|||
|
|
@ -86,17 +86,15 @@ const Commissions = ({ name: SCREEN_KEY, id: deviceId }) => {
|
|||
return R.values(commissions)
|
||||
}
|
||||
|
||||
getMachineCommissions()
|
||||
const machineCommissions = getMachineCommissions()
|
||||
|
||||
return (
|
||||
<>
|
||||
<EditableTable
|
||||
name="overrides"
|
||||
save={saveOverrides}
|
||||
data={getMachineCommissions()}
|
||||
data={machineCommissions}
|
||||
elements={overrides(currency)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,10 +10,8 @@ const useStyles = makeStyles(styles)
|
|||
const Details = ({ data }) => {
|
||||
const classes = useStyles()
|
||||
return (
|
||||
<>
|
||||
<div className={classes.row}>
|
||||
<div className={classes.rowItem}>
|
||||
{' '}
|
||||
<Label3 className={classes.label3}>Paired at</Label3>
|
||||
<P>
|
||||
{data.pairedAt
|
||||
|
|
@ -26,12 +24,10 @@ const Details = ({ data }) => {
|
|||
<P>{data.model}</P>
|
||||
</div>
|
||||
<div className={classes.rowItem}>
|
||||
{' '}
|
||||
<Label3 className={classes.label3}>Software version</Label3>
|
||||
<P>{data.version}</P>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,25 @@ const MACHINE_ACTION = gql`
|
|||
}
|
||||
`
|
||||
|
||||
const makeLastPing = lastPing => {
|
||||
if (!lastPing) return null
|
||||
const now = moment()
|
||||
const secondsAgo = now.diff(lastPing, 'seconds')
|
||||
if (secondsAgo < 60) {
|
||||
return `${secondsAgo} ${secondsAgo === 1 ? 'second' : 'seconds'} ago`
|
||||
}
|
||||
if (secondsAgo < 3600) {
|
||||
const minutes = Math.round(secondsAgo / 60)
|
||||
return `${minutes} ${minutes === 1 ? 'minute' : 'minutes'} ago`
|
||||
}
|
||||
if (secondsAgo < 3600 * 24) {
|
||||
const hours = Math.round(secondsAgo / 3600)
|
||||
return `${hours} ${hours === 1 ? 'hour' : 'hours'} ago`
|
||||
}
|
||||
const days = Math.round(secondsAgo / 3600 / 24)
|
||||
return `${days} ${days === 1 ? 'day' : 'days'} ago`
|
||||
}
|
||||
|
||||
const Overview = ({ data, onActionSuccess }) => {
|
||||
const [action, setAction] = useState('')
|
||||
const [confirmActionDialogOpen, setConfirmActionDialogOpen] = useState(false)
|
||||
|
|
@ -50,24 +69,6 @@ const Overview = ({ data, onActionSuccess }) => {
|
|||
const confirmActionDialog = action =>
|
||||
setAction(action) || setConfirmActionDialogOpen(true)
|
||||
|
||||
const makeLastPing = () => {
|
||||
const now = moment()
|
||||
const secondsAgo = now.diff(data.lastPing, 'seconds')
|
||||
if (secondsAgo < 60) {
|
||||
return `${secondsAgo} ${secondsAgo === 1 ? 'second' : 'seconds'} ago`
|
||||
}
|
||||
if (secondsAgo < 3600) {
|
||||
const minutes = Math.round(secondsAgo / 60)
|
||||
return `${minutes} ${minutes === 1 ? 'minute' : 'minutes'} ago`
|
||||
}
|
||||
if (secondsAgo < 3600 * 24) {
|
||||
const hours = Math.round(secondsAgo / 3600)
|
||||
return `${hours} ${hours === 1 ? 'hour' : 'hours'} ago`
|
||||
}
|
||||
const days = Math.round(secondsAgo / 3600 / 24)
|
||||
return `${days} ${days === 1 ? 'day' : 'days'} ago`
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes.row}>
|
||||
|
|
@ -84,7 +85,7 @@ const Overview = ({ data, onActionSuccess }) => {
|
|||
<div className={classes.row}>
|
||||
<div className={classes.rowItem}>
|
||||
<Label3 className={classes.label3}>Last ping</Label3>
|
||||
<P>{data.lastPing ? makeLastPing() : ''}</P>
|
||||
<P>{makeLastPing(data.lastPing)}</P>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.row}>
|
||||
|
|
|
|||
|
|
@ -1,74 +0,0 @@
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import classnames from 'classnames'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { CopyToClipboard as ReactCopyToClipboard } from 'react-copy-to-clipboard'
|
||||
|
||||
import Popover from 'src/components/Popper'
|
||||
import { ReactComponent as CopyIcon } from 'src/styling/icons/action/copy/copy.svg'
|
||||
import { comet } from 'src/styling/variables'
|
||||
|
||||
import { cpcStyles } from './Transactions.styles'
|
||||
|
||||
const useStyles = makeStyles(cpcStyles)
|
||||
|
||||
const CopyToClipboard = ({
|
||||
className,
|
||||
buttonClassname,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (anchorEl) setTimeout(() => setAnchorEl(null), 3000)
|
||||
}, [anchorEl])
|
||||
|
||||
const classes = useStyles()
|
||||
|
||||
const handleClick = event => {
|
||||
setAnchorEl(anchorEl ? null : event.currentTarget)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const open = Boolean(anchorEl)
|
||||
const id = open ? 'simple-popper' : undefined
|
||||
|
||||
return (
|
||||
<div className={classes.wrapper}>
|
||||
{children && (
|
||||
<>
|
||||
<div className={classnames(classes.address, className)}>
|
||||
{children}
|
||||
</div>
|
||||
<div className={classnames(classes.buttonWrapper, buttonClassname)}>
|
||||
<ReactCopyToClipboard text={R.replace(/\s/g, '')(children)}>
|
||||
<button
|
||||
aria-describedby={id}
|
||||
onClick={event => handleClick(event)}>
|
||||
<CopyIcon />
|
||||
</button>
|
||||
</ReactCopyToClipboard>
|
||||
</div>
|
||||
<Popover
|
||||
id={id}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
arrowSize={3}
|
||||
bgColor={comet}
|
||||
placement="top">
|
||||
<div className={classes.popoverContent}>
|
||||
<div>Copied to clipboard!</div>
|
||||
</div>
|
||||
</Popover>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CopyToClipboard
|
||||
|
|
@ -17,12 +17,11 @@ import {
|
|||
Td,
|
||||
Th
|
||||
} from 'src/components/fake-table/Table'
|
||||
import styles from 'src/components/tables/DataTable.styles'
|
||||
import { H4 } from 'src/components/typography'
|
||||
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 styles from './DataTable.styles'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const Row = ({
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
import { zircon } from 'src/styling/variables'
|
||||
|
||||
const styles = {
|
||||
expandButton: {
|
||||
outline: 'none',
|
||||
border: 'none',
|
||||
backgroundColor: 'transparent',
|
||||
cursor: 'pointer',
|
||||
padding: 4
|
||||
},
|
||||
rowWrapper: {
|
||||
// workaround to shadows cut by r-virtualized when scroll is visible
|
||||
padding: 1
|
||||
},
|
||||
row: {
|
||||
border: [[2, 'solid', 'transparent']],
|
||||
borderRadius: 0
|
||||
},
|
||||
expanded: {
|
||||
border: [[2, 'solid', zircon]],
|
||||
boxShadow: '0 0 8px 0 rgba(0,0,0,0.08)'
|
||||
},
|
||||
before: {
|
||||
paddingTop: 12
|
||||
},
|
||||
after: {
|
||||
paddingBottom: 12
|
||||
},
|
||||
pointer: {
|
||||
cursor: 'pointer'
|
||||
},
|
||||
body: {
|
||||
flex: [[1, 1, 'auto']]
|
||||
},
|
||||
table: ({ width }) => ({
|
||||
marginBottom: 30,
|
||||
minHeight: 200,
|
||||
width,
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
})
|
||||
}
|
||||
|
||||
export default styles
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
import { makeStyles, Box } from '@material-ui/core'
|
||||
import BigNumber from 'bignumber.js'
|
||||
import moment from 'moment'
|
||||
import React, { memo } from 'react'
|
||||
|
||||
import { IDButton } from 'src/components/buttons'
|
||||
import { Label1 } from 'src/components/typography'
|
||||
import { ReactComponent as CardIdInverseIcon } from 'src/styling/icons/ID/card/white.svg'
|
||||
import { ReactComponent as CardIdIcon } from 'src/styling/icons/ID/card/zodiac.svg'
|
||||
import { ReactComponent as PhoneIdInverseIcon } from 'src/styling/icons/ID/phone/white.svg'
|
||||
import { ReactComponent as PhoneIdIcon } from 'src/styling/icons/ID/phone/zodiac.svg'
|
||||
import { ReactComponent as CamIdInverseIcon } from 'src/styling/icons/ID/photo/white.svg'
|
||||
import { ReactComponent as CamIdIcon } from 'src/styling/icons/ID/photo/zodiac.svg'
|
||||
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
|
||||
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
|
||||
import { URI } from 'src/utils/apollo'
|
||||
import { toUnit, formatCryptoAddress } from 'src/utils/coin'
|
||||
import { onlyFirstToUpper } from 'src/utils/string'
|
||||
|
||||
import CopyToClipboard from './CopyToClipboard'
|
||||
import styles from './DetailsCard.styles'
|
||||
import { getStatus } from './helper'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const formatAddress = (cryptoCode = '', address = '') =>
|
||||
formatCryptoAddress(cryptoCode, address).replace(/(.{5})/g, '$1 ')
|
||||
|
||||
const Label = ({ children }) => {
|
||||
const classes = useStyles()
|
||||
return <Label1 className={classes.label}>{children}</Label1>
|
||||
}
|
||||
|
||||
const DetailsRow = ({ it: tx }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const fiat = Number.parseFloat(tx.fiat)
|
||||
const crypto = toUnit(new BigNumber(tx.cryptoAtoms), tx.cryptoCode)
|
||||
const commissionPercentage = Number.parseFloat(tx.commissionPercentage, 2)
|
||||
const commission = Number(fiat * commissionPercentage).toFixed(2)
|
||||
const exchangeRate = Number(fiat / crypto).toFixed(3)
|
||||
const displayExRate = `1 ${tx.cryptoCode} = ${exchangeRate} ${tx.fiatCode}`
|
||||
|
||||
const customer = tx.customerIdCardData && {
|
||||
name: `${onlyFirstToUpper(
|
||||
tx.customerIdCardData.firstName
|
||||
)} ${onlyFirstToUpper(tx.customerIdCardData.lastName)}`,
|
||||
age: moment().diff(moment(tx.customerIdCardData.dateOfBirth), 'years'),
|
||||
country: tx.customerIdCardData.country,
|
||||
idCardNumber: tx.customerIdCardData.documentNumber,
|
||||
idCardExpirationDate: moment(tx.customerIdCardData.expirationDate).format(
|
||||
'DD-MM-YYYY'
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.wrapper}>
|
||||
<div className={classes.row}>
|
||||
<div className={classes.direction}>
|
||||
<Label>Direction</Label>
|
||||
<div>
|
||||
<span className={classes.txIcon}>
|
||||
{tx.txClass === 'cashOut' ? <TxOutIcon /> : <TxInIcon />}
|
||||
</span>
|
||||
<span>{tx.txClass === 'cashOut' ? 'Cash-out' : 'Cash-in'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={classes.availableIds}>
|
||||
<Label>Available IDs</Label>
|
||||
<Box display="flex" flexDirection="row">
|
||||
{tx.customerPhone && (
|
||||
<IDButton
|
||||
className={classes.idButton}
|
||||
name="phone"
|
||||
Icon={PhoneIdIcon}
|
||||
InverseIcon={PhoneIdInverseIcon}>
|
||||
{tx.customerPhone}
|
||||
</IDButton>
|
||||
)}
|
||||
{tx.customerIdCardPhotoPath && !tx.customerIdCardData && (
|
||||
<IDButton
|
||||
popoverClassname={classes.popover}
|
||||
className={classes.idButton}
|
||||
name="card"
|
||||
Icon={CardIdIcon}
|
||||
InverseIcon={CardIdInverseIcon}>
|
||||
<img
|
||||
className={classes.idCardPhoto}
|
||||
src={`${URI}/id-card-photo/${tx.customerIdCardPhotoPath}`}
|
||||
alt=""
|
||||
/>
|
||||
</IDButton>
|
||||
)}
|
||||
{tx.customerIdCardData && (
|
||||
<IDButton
|
||||
className={classes.idButton}
|
||||
name="card"
|
||||
Icon={CardIdIcon}
|
||||
InverseIcon={CardIdInverseIcon}>
|
||||
<div className={classes.idCardDataCard}>
|
||||
<div>
|
||||
<div>
|
||||
<Label>Name</Label>
|
||||
<div>{customer.name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Age</Label>
|
||||
<div>{customer.age}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Country</Label>
|
||||
<div>{customer.country}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<Label>ID number</Label>
|
||||
<div>{customer.idCardNumber}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Expiration date</Label>
|
||||
<div>{customer.idCardExpirationDate}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</IDButton>
|
||||
)}
|
||||
{tx.customerFrontCameraPath && (
|
||||
<IDButton
|
||||
name="cam"
|
||||
Icon={CamIdIcon}
|
||||
InverseIcon={CamIdInverseIcon}>
|
||||
<img
|
||||
src={`${URI}/front-camera-photo/${tx.customerFrontCameraPath}`}
|
||||
alt=""
|
||||
/>
|
||||
</IDButton>
|
||||
)}
|
||||
</Box>
|
||||
</div>
|
||||
<div className={classes.exchangeRate}>
|
||||
<Label>Exchange rate</Label>
|
||||
<div>{crypto > 0 ? displayExRate : '-'}</div>
|
||||
</div>
|
||||
<div className={classes.commission}>
|
||||
<Label>Commission</Label>
|
||||
<div>
|
||||
{`${commission} ${tx.fiatCode} (${commissionPercentage * 100} %)`}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Fixed fee</Label>
|
||||
<div>
|
||||
{tx.txClass === 'cashIn'
|
||||
? `${Number.parseFloat(tx.cashInFee)} ${tx.fiatCode}`
|
||||
: 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.secondRow}>
|
||||
<div className={classes.address}>
|
||||
<Label>Address</Label>
|
||||
<div>
|
||||
<CopyToClipboard>
|
||||
{formatAddress(tx.cryptoCode, tx.toAddress)}
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.transactionId}>
|
||||
<Label>Transaction ID</Label>
|
||||
<div>
|
||||
{tx.txClass === 'cashOut' ? (
|
||||
'N/A'
|
||||
) : (
|
||||
<CopyToClipboard>{tx.txHash}</CopyToClipboard>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.sessionId}>
|
||||
<Label>Session ID</Label>
|
||||
<CopyToClipboard>{tx.id}</CopyToClipboard>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.lastRow}>
|
||||
<div>
|
||||
<Label>Transaction status</Label>
|
||||
<span className={classes.bold}>{getStatus(tx)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(DetailsRow, (prev, next) => prev.id === next.id)
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
import typographyStyles from 'src/components/typography/styles'
|
||||
import { offColor } from 'src/styling/variables'
|
||||
|
||||
const { p } = typographyStyles
|
||||
|
||||
const styles = {
|
||||
wrapper: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginTop: 24
|
||||
},
|
||||
row: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
marginBottom: 36
|
||||
},
|
||||
secondRow: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 36
|
||||
},
|
||||
lastRow: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
marginBottom: 32
|
||||
},
|
||||
label: {
|
||||
color: offColor,
|
||||
margin: [[0, 0, 6, 0]]
|
||||
},
|
||||
txIcon: {
|
||||
marginRight: 10
|
||||
},
|
||||
popover: {
|
||||
height: 164,
|
||||
width: 215
|
||||
},
|
||||
idButton: {
|
||||
marginRight: 4
|
||||
},
|
||||
idCardDataCard: {
|
||||
extend: p,
|
||||
display: 'flex',
|
||||
padding: [[11, 8]],
|
||||
// rework this into a proper component
|
||||
'& > div': {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
'& > div': {
|
||||
width: 144,
|
||||
height: 37,
|
||||
marginBottom: 15,
|
||||
'&:last-child': {
|
||||
marginBottom: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
bold: {
|
||||
fontWeight: 700
|
||||
},
|
||||
direction: {
|
||||
width: 233
|
||||
},
|
||||
availableIds: {
|
||||
width: 232
|
||||
},
|
||||
exchangeRate: {
|
||||
width: 250
|
||||
},
|
||||
commission: {
|
||||
width: 217
|
||||
},
|
||||
address: {
|
||||
width: 280
|
||||
},
|
||||
transactionId: {
|
||||
width: 280
|
||||
},
|
||||
sessionId: {
|
||||
width: 215
|
||||
}
|
||||
}
|
||||
|
||||
export default styles
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
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
|
||||
|
|
@ -6,22 +6,26 @@ import moment from 'moment'
|
|||
import * as R from 'ramda'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import DetailsRow from 'src/pages/Transactions/DetailsCard'
|
||||
import { mainStyles } from 'src/pages/Transactions/Transactions.styles'
|
||||
import { getStatus } from 'src/pages/Transactions/helper'
|
||||
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
|
||||
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
|
||||
import { toUnit, formatCryptoAddress } from 'src/utils/coin'
|
||||
|
||||
import DataTable from './DataTable'
|
||||
import DetailsRow from './DetailsCard'
|
||||
import { mainStyles } from './Transactions.styles'
|
||||
import { getStatus } from './helper'
|
||||
|
||||
const useStyles = makeStyles(mainStyles)
|
||||
|
||||
const NUM_LOG_RESULTS = 5
|
||||
|
||||
const GET_TRANSACTIONS = gql`
|
||||
query transactions($limit: Int, $from: Date, $until: Date, $id: ID) {
|
||||
transactions(limit: $limit, from: $from, until: $until, id: $id) {
|
||||
query transactions($limit: Int, $from: Date, $until: Date, $deviceId: ID) {
|
||||
transactions(
|
||||
limit: $limit
|
||||
from: $from
|
||||
until: $until
|
||||
deviceId: $deviceId
|
||||
) {
|
||||
id
|
||||
txClass
|
||||
txHash
|
||||
|
|
@ -61,7 +65,7 @@ const Transactions = ({ id }) => {
|
|||
{
|
||||
variables: {
|
||||
limit: NUM_LOG_RESULTS,
|
||||
id
|
||||
deviceId: id
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
const getCashOutStatus = it => {
|
||||
if (it.hasError) return 'Error'
|
||||
if (it.dispense) return 'Success'
|
||||
if (it.expired) return 'Expired'
|
||||
return 'Pending'
|
||||
}
|
||||
|
||||
const getCashInStatus = it => {
|
||||
if (it.operatorCompleted) return 'Cancelled'
|
||||
if (it.hasError) return 'Error'
|
||||
if (it.sendConfirmed) return 'Sent'
|
||||
if (it.expired) return 'Expired'
|
||||
return 'Pending'
|
||||
}
|
||||
|
||||
const getStatus = it => {
|
||||
if (it.class === 'cashOut') {
|
||||
return getCashOutStatus(it)
|
||||
}
|
||||
return getCashInStatus(it)
|
||||
}
|
||||
|
||||
export { getStatus }
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
import List from '@material-ui/core/List'
|
||||
import ListItem from '@material-ui/core/ListItem'
|
||||
import ListItemText from '@material-ui/core/ListItemText'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React from 'react'
|
||||
|
||||
import styles from './Machines.styles'
|
||||
const useStyles = makeStyles(styles)
|
||||
const MachineSidebar = ({ data, getText, getKey, isSelected, selectItem }) => {
|
||||
const classes = useStyles()
|
||||
return (
|
||||
<List style={{ height: 400, overflowY: 'auto' }}>
|
||||
<List className={classes.sidebarContainer}>
|
||||
{data.map((item, idx) => {
|
||||
return (
|
||||
<ListItem
|
||||
|
|
@ -20,8 +24,6 @@ const MachineSidebar = ({ data, getText, getKey, isSelected, selectItem }) => {
|
|||
})}
|
||||
</List>
|
||||
)
|
||||
|
||||
/* return data.map(item => <button key={getKey(item)}>{getText(item)}</button>) */
|
||||
}
|
||||
|
||||
export default MachineSidebar
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import Breadcrumbs from '@material-ui/core/Breadcrumbs'
|
|||
import Grid from '@material-ui/core/Grid'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import NavigateNextIcon from '@material-ui/icons/NavigateNext'
|
||||
import classnames from 'classnames'
|
||||
import gql from 'graphql-tag'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
|
|
@ -66,13 +67,12 @@ const Machines = () => {
|
|||
}, [loading, data, location.state])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid container className={classes.grid}>
|
||||
<Grid item xs={3}>
|
||||
<Grid item xs={12}>
|
||||
<div style={{ marginTop: 32 }}>
|
||||
<div className={classes.breadcrumbsContainer}>
|
||||
<Breadcrumbs separator={<NavigateNextIcon fontSize="small" />}>
|
||||
<Link to="/dashboard" style={{ textDecoration: 'none' }}>
|
||||
<Link to="/dashboard" className={classes.breadcrumbLink}>
|
||||
<Label3 className={classes.subtitle}>Dashboard</Label3>
|
||||
</Link>
|
||||
<TL2 className={classes.subtitle}>{selectedMachine}</TL2>
|
||||
|
|
@ -92,7 +92,8 @@ const Machines = () => {
|
|||
</Grid>
|
||||
<Grid item xs={9}>
|
||||
<div className={classes.content}>
|
||||
<div className={classes.detailItem} style={{ marginTop: 24 }}>
|
||||
<div
|
||||
className={classnames(classes.detailItem, classes.detailsMargin)}>
|
||||
<TL1 className={classes.subtitle}>{'Details'}</TL1>
|
||||
<Details data={machineInfo} />
|
||||
</div>
|
||||
|
|
@ -118,7 +119,6 @@ const Machines = () => {
|
|||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -69,6 +69,19 @@ const styles = {
|
|||
},
|
||||
actionButton: {
|
||||
marginRight: 8
|
||||
},
|
||||
breadcrumbsContainer: {
|
||||
marginTop: 32
|
||||
},
|
||||
breadcrumbLink: {
|
||||
textDecoration: 'none'
|
||||
},
|
||||
detailsMargin: {
|
||||
marginTop: 24
|
||||
},
|
||||
sidebarContainer: {
|
||||
height: 400,
|
||||
overflowY: 'auto'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,18 +51,6 @@ const useStyles = makeStyles({
|
|||
})
|
||||
|
||||
const tree = [
|
||||
/* {
|
||||
key: 'dashboard',
|
||||
label: 'Dashboard',
|
||||
route: '/dashboard',
|
||||
component: Dashboard
|
||||
}, */
|
||||
/* {
|
||||
key: 'machines',
|
||||
label: 'Machines',
|
||||
route: '/machines',
|
||||
component: Machines
|
||||
}, */
|
||||
{
|
||||
key: 'transactions',
|
||||
label: 'Transactions',
|
||||
|
|
|
|||
|
|
@ -108,6 +108,13 @@ export default createMuiTheme({
|
|||
color: fontColor
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiListItem: {
|
||||
root: {
|
||||
'&:nth-of-type(odd)': {
|
||||
backgroundColor: backgroundColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as sanctuary from 'sanctuary'
|
||||
|
||||
const checkOnlyDev = () => {
|
||||
if (!process.env.NODE_ENV === 'production') return false
|
||||
if (process.env.NODE_ENV !== 'production') return false
|
||||
|
||||
return (
|
||||
process.env.NODE_ENV === 'development' &&
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue