Feat: make percentage chart

feat: footer expands to show more items

fix: several style fixes

feat: streak through cassettes table if machine doesnt have cashout enabled

Revert "feat: streak through cassettes table if machine doesnt have cashout enabled"

This reverts commit eaa390be8e9688c557507ff9c2984addc3f25031.

feat: Streak through cash cassettes table if cashout not enabled

feat: Machine details overview on sidebar

feat: machine prof page: breadcrumb, sidebar. dashboard: redirect on machine click

feat: Last ping shows seconds/ minutes/ hours/ days ago depending on time past

chore: Disabled cashbox % column in dashboard system performance card
This commit is contained in:
Cesar 2020-11-17 16:24:08 +00:00 committed by Josh Harvey
parent 19cd086436
commit 00f176fccc
16 changed files with 539 additions and 227 deletions

View file

@ -356,8 +356,8 @@ 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 }]) =>
transactions.batch(from, until, limit, offset),
transactions: (...[, { from, until, limit, offset, id }]) =>
transactions.batch(from, until, limit, offset, id),
transactionsCsv: (...[, { from, until, limit, offset }]) =>
transactions.batch(from, until, limit, offset).then(parseAsync),
config: () => settingsLoader.loadLatestConfigOrNone(),

View file

@ -91,7 +91,6 @@ const TL2 = pBuilder('tl2')
const Label1 = pBuilder('label1')
const Label2 = pBuilder('label2')
const Label3 = pBuilder('label3')
const Label4 = pBuilder('regularLabel')
function pBuilder(elementClass) {
return ({ inline, noMargin, className, children, ...props }) => {
@ -125,6 +124,5 @@ export {
Mono,
Label1,
Label2,
Label3,
Label4
Label3
}

View file

@ -1,13 +1,15 @@
import { useQuery } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core'
import Button from '@material-ui/core/Button'
import Grid from '@material-ui/core/Grid'
import gql from 'graphql-tag'
import * as R from 'ramda'
import React from 'react'
import React, { useState, useEffect } from 'react'
import { Label2 } from 'src/components/typography'
import { Label1, 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 } from 'src/styling/variables'
import { fromNamespace } from 'src/utils/config'
import styles from './Footer.styles'
@ -29,8 +31,32 @@ const GET_DATA = gql`
const useStyles = makeStyles(styles)
const Footer = () => {
const { data, loading } = useQuery(GET_DATA)
const [expanded, setExpanded] = useState(false)
const [showExpandBtn, setShowExpandBtn] = useState(false)
const [buttonName, setButtonName] = useState('Show all')
const classes = useStyles()
useEffect(() => {
if (data && data.rates && data.rates.withCommissions) {
const numItems = R.keys(data.rates.withCommissions).length
if (numItems > 4) {
setShowExpandBtn(true)
setButtonName(`Show all (${numItems})`)
}
}
}, [data])
const toggleExpand = () => {
if (expanded) {
const numItems = R.keys(data.rates.withCommissions).length
setExpanded(false)
setButtonName(`Show all (${numItems})`)
} else {
setExpanded(true)
setButtonName(`Show less`)
}
}
const wallets = fromNamespace('wallets')(data?.config)
const renderFooterItem = key => {
@ -99,15 +125,54 @@ const Footer = () => {
)
}
const makeFooterExpandedClass = () => {
return {
overflow: 'scroll',
// 88px for base height, then add 100 px for each row of items. Each row has 4 items. 5 items makes 2 rows so 288px of height
height:
88 + Math.ceil(R.keys(data.rates.withCommissions).length / 4) * 100,
maxHeight: '50vh',
position: 'fixed',
left: 0,
bottom: 0,
width: '100vw',
backgroundColor: white,
textAlign: 'left',
boxShadow: '0px -1px 10px 0px rgba(50, 50, 50, 0.1)'
}
}
return (
<>
<div className={classes.footer}>
<div
className={!expanded ? classes.footer : null}
style={expanded ? makeFooterExpandedClass() : null}>
<div className={classes.content}>
{!loading && data && (
<>
<Grid container spacing={1}>
{R.keys(data.rates.withCommissions).map(key =>
renderFooterItem(key)
<Grid container item xs={11} style={{ marginBottom: 18 }}>
{R.keys(data.rates.withCommissions).map(key =>
renderFooterItem(key)
)}
</Grid>
{/* {renderFooterItem(R.keys(data.rates.withCommissions)[0])} */}
{showExpandBtn && (
<Label1
style={{
textAlign: 'center',
marginBottom: 0,
marginTop: 35
}}>
<Button
onClick={toggleExpand}
size="small"
disableRipple
disableFocusRipple
className={classes.button}>
{buttonName}
</Button>
</Label1>
)}
</Grid>
</>

View file

@ -67,11 +67,14 @@ export default {
width: '100vw',
backgroundColor: white,
textAlign: 'left',
height: 88
height: 88,
boxShadow: '0px -1px 10px 0px rgba(50, 50, 50, 0.1)'
},
content: {
width: 1200,
margin: '0 auto'
margin: '0 auto',
backgroundColor: white,
marginTop: 4
},
headerLabels: {
whiteSpace: 'pre',

View file

@ -0,0 +1,100 @@
import { makeStyles } from '@material-ui/core'
import Slider from '@material-ui/core/Slider'
import React from 'react'
import { ReactComponent as CashIn } from 'src/styling/icons/direction/cash-in.svg'
import { ReactComponent as CashOut } from 'src/styling/icons/direction/cash-out.svg'
import {
zircon,
fontSize3,
fontSecondary,
fontColor
} from 'src/styling/variables'
const styles = {
wrapper: {
display: 'flex',
height: 130,
marginTop: -8
},
percentageBox: {
backgroundColor: zircon,
height: 130,
borderRadius: 4,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
whiteSpace: 'pre'
},
label: {
fontSize: fontSize3,
fontFamily: fontSecondary,
fontWeight: 700,
color: fontColor
}
}
const useStyles = makeStyles(styles)
const PercentageChart = () => {
const classes = useStyles()
const [value, setValue] = React.useState(50)
const handleChange = (event, newValue) => {
setValue(newValue)
}
const buildPercentageView = (value, direction) => {
switch (direction) {
case 'cashIn':
if (value > 20) {
return (
<>
<CashIn />
<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
}
}
return (
<>
<Slider
value={value}
onChange={handleChange}
aria-labelledby="continuous-slider"
/>
<div className={classes.wrapper}>
<div
className={classes.percentageBox}
style={{ width: `${value}%`, marginRight: 4 }}>
{/* <CashIn />
<span className={classes.label}>{` ${value}%`}</span> */}
{buildPercentageView(value, 'cashIn')}
</div>
<div
className={classes.percentageBox}
style={{ width: `${100 - value}%` }}>
{/* <CashOut />
<span className={classes.label}>{` ${100 - value}%`}</span> */}
{buildPercentageView(100 - value, 'cashOut')}
</div>
</div>
</>
)
}
export default PercentageChart

View file

@ -7,10 +7,8 @@ import { backgroundColor, java, neon } from 'src/styling/variables'
const RefScatterplot = ({ data: realData, timeFrame }) => {
const svgRef = useRef()
const cashIns = R.filter(R.propEq('txClass', 'cashIn'))(realData)
const cashOuts = R.filter(R.propEq('txClass', 'cashOut'))(realData)
const drawGraph = useCallback(() => {
const svg = d3.select(svgRef.current)
const margin = { top: 25, right: 0, bottom: 25, left: 15 }
@ -20,6 +18,9 @@ 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) {
@ -37,7 +38,7 @@ const RefScatterplot = ({ data: realData, timeFrame }) => {
ticks: 4,
subtractDays: 1,
timeFormat: '%H:%M',
timeRange: [0, 500]
timeRange: [50, 500]
}
switch (timeFrame) {
case 'Day':
@ -48,7 +49,7 @@ const RefScatterplot = ({ data: realData, timeFrame }) => {
nice: 7,
ticks: 7,
subtractDays: 7,
timeFormat: '%d',
timeFormat: '%a %d',
timeRange: [50, 500]
}
case 'Month':
@ -98,12 +99,9 @@ const RefScatterplot = ({ data: realData, timeFrame }) => {
.scaleTime()
.domain([
moment()
.endOf('day')
.add(-xAxisSettings.subtractDays, 'day')
.valueOf(),
moment()
.endOf('day')
.valueOf()
moment().valueOf()
])
.range(xAxisSettings.timeRange)
.nice(xAxisSettings.nice)

View file

@ -6,11 +6,12 @@ import moment from 'moment'
import * as R from 'ramda'
import React, { useState, useEffect } from 'react'
import { Label1, Label2 } from 'src/components/typography/index'
import { Label2 } from 'src/components/typography/index'
import { ReactComponent as TriangleDown } from 'src/styling/icons/arrow/triangle_down.svg'
import { ReactComponent as TriangleUp } from 'src/styling/icons/arrow/triangle_up.svg'
import { fromNamespace } from 'src/utils/config'
import PercentageChart from './Graphs/PercentageChart'
import LineChart from './Graphs/RefLineChart'
import Scatterplot from './Graphs/RefScatterplot'
import InfoWithLabel from './InfoWithLabel'
@ -21,8 +22,11 @@ const isNotProp = R.curry(R.compose(R.isNil, R.prop))
const getFiats = R.map(R.prop('fiat'))
const getProps = propName => R.map(R.prop(propName))
const useStyles = makeStyles(styles)
const getDateDaysAgo = (days = 0) => {
return moment().subtract(days, 'day')
const getDateSecondsAgo = (seconds = 0, startDate = null) => {
if (startDate) {
return moment(startDate).subtract(seconds, 'second')
}
return moment().subtract(seconds, 'second')
}
// const now = moment()
@ -47,8 +51,6 @@ const GET_DATA = gql`
}
`
const currentTime = new Date()
const SystemPerformance = () => {
const classes = useStyles()
@ -64,49 +66,55 @@ const SystemPerformance = () => {
useEffect(() => {
const isInRange = (getLastTimePeriod = false) => t => {
const now = moment(currentTime)
const now = moment()
switch (selectedRange) {
case 'Day':
if (getLastTimePeriod) {
return (
t.error === null &&
moment(t.created).isBetween(
getDateDaysAgo(2),
now.subtract(25, 'hours')
getDateSecondsAgo(2 * 24 * 3600, now),
getDateSecondsAgo(24 * 3600, now)
)
)
}
return (
t.error === null &&
moment(t.created).isBetween(getDateDaysAgo(1), now)
moment(t.created).isBetween(getDateSecondsAgo(24 * 3600, now), now)
)
case 'Week':
if (getLastTimePeriod) {
return (
t.error === null &&
moment(t.created).isBetween(
getDateDaysAgo(14),
now.subtract(24 * 7 + 1, 'hours')
getDateSecondsAgo(14 * 24 * 3600, now),
getDateSecondsAgo(7 * 24 * 3600, now)
)
)
}
return (
t.error === null &&
moment(t.created).isBetween(getDateDaysAgo(7), now)
moment(t.created).isBetween(
getDateSecondsAgo(7 * 24 * 3600, now),
now
)
)
case 'Month':
if (getLastTimePeriod) {
return (
t.error === null &&
moment(t.created).isBetween(
getDateDaysAgo(60),
now.subtract(24 * 30 + 1, 'hours')
getDateSecondsAgo(60 * 24 * 3600, now),
getDateSecondsAgo(30 * 24 * 3600, now)
)
)
}
return (
t.error === null &&
moment(t.created).isBetween(getDateDaysAgo(30), now)
moment(t.created).isBetween(
getDateSecondsAgo(30 * 24 * 3600, now),
now
)
)
default:
return t.error === null && true
@ -272,13 +280,8 @@ const SystemPerformance = () => {
<Grid item xs={4}>
<Label2>Direction</Label2>
<Grid container>
<Grid item xs={6}>
<Label1>CashIn: </Label1>
{` ${getDirectionPercent().cashIn}%`}
</Grid>
<Grid item xs={6}>
<Label1>CashOut: </Label1>
{` ${getDirectionPercent().cashOut}%`}
<Grid item xs>
<PercentageChart data={getDirectionPercent()} />
</Grid>
</Grid>
</Grid>

View file

@ -6,10 +6,11 @@ import TableContainer from '@material-ui/core/TableContainer'
import TableHead from '@material-ui/core/TableHead'
import TableRow from '@material-ui/core/TableRow'
import React from 'react'
import { useHistory } from 'react-router-dom'
import { Status } from 'src/components/Status'
import { Label2, TL2 } from 'src/components/typography'
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
// import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
import styles from './MachinesTable.styles'
@ -38,7 +39,7 @@ const HeaderCell = withStyles({
const MachinesTable = ({ machines, numToRender }) => {
const classes = useStyles()
const history = useHistory()
const getPercent = (notes, capacity = 500) => {
return Math.round((notes / capacity) * 100)
}
@ -50,6 +51,11 @@ const MachinesTable = ({ machines, numToRender }) => {
}
return <TL2>{`${percent}%`}</TL2>
}
const redirect = name => {
return history.push('/machines', { selectedMachine: name })
}
return (
<>
<TableContainer className={classes.table}>
@ -66,11 +72,11 @@ const MachinesTable = ({ machines, numToRender }) => {
<Label2 className={classes.label}>Status</Label2>
</div>
</HeaderCell>
<HeaderCell>
{/* <HeaderCell>
<div className={classes.header}>
<TxInIcon />
</div>
</HeaderCell>
</HeaderCell> */}
<HeaderCell>
<div className={classes.header}>
<TxOutIcon />
@ -90,6 +96,8 @@ const MachinesTable = ({ machines, numToRender }) => {
if (idx < numToRender) {
return (
<TableRow
onClick={() => redirect(machine.name)}
style={{ cursor: 'pointer' }}
key={machine.deviceId + idx}
className={classes.row}>
<StyledCell align="left">
@ -98,9 +106,9 @@ const MachinesTable = ({ machines, numToRender }) => {
<StyledCell>
<Status status={machine.statuses[0]} />
</StyledCell>
<StyledCell align="left">
{/* <StyledCell align="left">
{makePercentageText(machine.cashbox)}
</StyledCell>
</StyledCell> */}
<StyledCell align="left">
{makePercentageText(machine.cassette1)}
</StyledCell>

View file

@ -1,9 +1,12 @@
import { useMutation } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core'
import gql from 'graphql-tag'
import React from 'react'
import * as Yup from 'yup'
import { Table as EditableTable } from 'src/components/editableTable'
import { CashOut } from 'src/components/inputs/cashbox/Cashbox'
import { NumberInput } from 'src/components/inputs/formik'
import { fromNamespace } from 'src/utils/config'
import styles from './Cassettes.styles'
@ -24,7 +27,27 @@ const ValidationSchema = Yup.object().shape({
.max(500)
})
const CashCassettes = ({ machine, config }) => {
const RESET_CASHOUT_BILLS = gql`
mutation MachineAction(
$deviceId: ID!
$action: MachineAction!
$cassette1: Int!
$cassette2: Int!
) {
machineAction(
deviceId: $deviceId
action: $action
cassette1: $cassette1
cassette2: $cassette2
) {
deviceId
cassette1
cassette2
}
}
`
const CashCassettes = ({ machine, config, refetchData }) => {
const data = { machine, config }
const classes = useStyles()
@ -32,8 +55,9 @@ const CashCassettes = ({ machine, config }) => {
const locale = data?.config && fromNamespace('locale')(data.config)
const fiatCurrency = locale?.fiatCurrency
const getCashoutSettings = id => fromNamespace(id)(cashout)
// const isCashOutDisabled = ({ id }) => !getCashoutSettings(id).active
const getCashoutSettings = deviceId => fromNamespace(deviceId)(cashout)
const isCashOutDisabled = ({ deviceId }) =>
!getCashoutSettings(deviceId).active
const elements = [
{
@ -48,7 +72,11 @@ const CashCassettes = ({ machine, config }) => {
currency={{ code: fiatCurrency }}
notes={value}
/>
)
),
input: NumberInput,
inputProps: {
decimalPlaces: 0
}
},
{
name: 'cassette2',
@ -64,17 +92,41 @@ const CashCassettes = ({ machine, config }) => {
notes={value}
/>
)
},
input: NumberInput,
inputProps: {
decimalPlaces: 0
}
}
]
const [resetCashOut, { error }] = useMutation(RESET_CASHOUT_BILLS, {
refetchQueries: () => refetchData()
})
const onSave = (...[, { deviceId, cassette1, cassette2 }]) => {
return resetCashOut({
variables: {
action: 'resetCashOutBills',
deviceId: deviceId,
cassette1,
cassette2
}
})
}
return (
<>
{machine.name && (
<EditableTable
error={error?.message}
stripeWhen={isCashOutDisabled}
disableRowEdit={isCashOutDisabled}
name="cashboxes"
elements={elements}
enableEdit
data={[machine] || []}
save={onSave}
validationSchema={ValidationSchema}
/>
)}

View file

@ -1,75 +1,20 @@
import { useMutation } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core/styles'
import gql from 'graphql-tag'
import moment from 'moment'
import React, { useState } from 'react'
import React from 'react'
import { ConfirmDialog } from 'src/components/ConfirmDialog'
import { Status } from 'src/components/Status'
import ActionButton from 'src/components/buttons/ActionButton'
import { Label4, P } from 'src/components/typography'
import { ReactComponent as RebootReversedIcon } from 'src/styling/icons/button/reboot/white.svg'
import { ReactComponent as RebootIcon } from 'src/styling/icons/button/reboot/zodiac.svg'
import { ReactComponent as ShutdownReversedIcon } from 'src/styling/icons/button/shut down/white.svg'
import { ReactComponent as ShutdownIcon } from 'src/styling/icons/button/shut down/zodiac.svg'
import { ReactComponent as UnpairReversedIcon } from 'src/styling/icons/button/unpair/white.svg'
import { ReactComponent as UnpairIcon } from 'src/styling/icons/button/unpair/zodiac.svg'
import { Label3, P } from 'src/components/typography'
import styles from '../Machines.styles'
const useStyles = makeStyles(styles)
const MACHINE_ACTION = gql`
mutation MachineAction(
$deviceId: ID!
$action: MachineAction!
$newName: String
) {
machineAction(deviceId: $deviceId, action: $action, newName: $newName) {
deviceId
}
}
`
const Details = ({ data, onActionSuccess }) => {
const [action, setAction] = useState('')
const [confirmActionDialogOpen, setConfirmActionDialogOpen] = useState(false)
const [errorMessage, setErrorMessage] = useState(null)
const [machineAction] = useMutation(MACHINE_ACTION, {
onError: ({ message }) => {
const errorMessage = message ?? 'An error ocurred'
setErrorMessage(errorMessage)
},
onCompleted: () => {
onActionSuccess && onActionSuccess()
setConfirmActionDialogOpen(false)
}
})
const confirmActionDialog = action =>
setAction(action) || setConfirmActionDialogOpen(true)
const Details = ({ data }) => {
const classes = useStyles()
return (
<>
<div className={classes.row}>
<div className={classes.rowItem}>
{' '}
<Label4 className={classes.tl2}>Status</Label4>
{data && data.statuses ? <Status status={data.statuses[0]} /> : null}
{/* <Label4 className={classes.tl2}>Machine model</Label4>
<P>{data.model}</P> */}
</div>
<div className={classes.rowItem}>
<Label4 className={classes.tl2}>Machine model</Label4>
<P>{data.model}</P>
</div>
<div className={classes.rowItem}>
{' '}
<Label4 className={classes.tl2}>Software version</Label4>
<P>{data.version}</P>
</div>
<div className={classes.rowItem}>
{' '}
<Label4 className={classes.tl2}>Paired at</Label4>
<Label3 className={classes.label3}>Paired at</Label3>
<P>
{data.pairedAt
? moment(data.pairedAt).format('YYYY-MM-DD HH:mm:ss')
@ -77,68 +22,15 @@ const Details = ({ data, onActionSuccess }) => {
</P>
</div>
<div className={classes.rowItem}>
{' '}
<Label4 className={classes.tl2}>Last ping</Label4>
<P>
{data.lastPing
? moment(data.lastPing).format('YYYY-MM-DD HH:mm:ss')
: ''}
</P>
<Label3 className={classes.label3}>Machine model</Label3>
<P>{data.model}</P>
</div>
</div>
<div className={classes.row}>
<div className={classes.rowItem}>
{' '}
<Label4 className={classes.tl2}>Actions</Label4>
{data.name && (
<div className={classes.actionButtonsContainer}>
<ActionButton
color="primary"
className={classes.actionButton}
Icon={UnpairIcon}
InverseIcon={UnpairReversedIcon}
onClick={() => confirmActionDialog('Unpair')}>
Unpair
</ActionButton>
<ActionButton
color="primary"
className={classes.actionButton}
Icon={RebootIcon}
InverseIcon={RebootReversedIcon}
onClick={() => confirmActionDialog('Reboot')}>
Reboot
</ActionButton>
<ActionButton
className={classes.actionButton}
color="primary"
Icon={ShutdownIcon}
InverseIcon={ShutdownReversedIcon}
onClick={() => confirmActionDialog('Shutdown')}>
Shutdown
</ActionButton>
</div>
)}
<Label3 className={classes.label3}>Software version</Label3>
<P>{data.version}</P>
</div>
</div>
<ConfirmDialog
open={confirmActionDialogOpen}
title={`${action} this machine?`}
errorMessage={errorMessage}
toBeConfirmed={data.name}
onConfirmed={() => {
setErrorMessage(null)
machineAction({
variables: {
deviceId: data.deviceId,
action: `${action}`.toLowerCase()
}
})
}}
onDissmised={() => {
setConfirmActionDialogOpen(false)
setErrorMessage(null)
}}
/>
</>
)
}

View file

@ -0,0 +1,147 @@
import { useMutation } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core/styles'
import gql from 'graphql-tag'
import moment from 'moment'
import React, { useState } from 'react'
import { ConfirmDialog } from 'src/components/ConfirmDialog'
import { Status } from 'src/components/Status'
import ActionButton from 'src/components/buttons/ActionButton'
import { H3, Label3, P } from 'src/components/typography'
import { ReactComponent as RebootReversedIcon } from 'src/styling/icons/button/reboot/white.svg'
import { ReactComponent as RebootIcon } from 'src/styling/icons/button/reboot/zodiac.svg'
import { ReactComponent as ShutdownReversedIcon } from 'src/styling/icons/button/shut down/white.svg'
import { ReactComponent as ShutdownIcon } from 'src/styling/icons/button/shut down/zodiac.svg'
import { ReactComponent as UnpairReversedIcon } from 'src/styling/icons/button/unpair/white.svg'
import { ReactComponent as UnpairIcon } from 'src/styling/icons/button/unpair/zodiac.svg'
import styles from '../Machines.styles'
const useStyles = makeStyles(styles)
const MACHINE_ACTION = gql`
mutation MachineAction(
$deviceId: ID!
$action: MachineAction!
$newName: String
) {
machineAction(deviceId: $deviceId, action: $action, newName: $newName) {
deviceId
}
}
`
const Overview = ({ data, onActionSuccess }) => {
const [action, setAction] = useState('')
const [confirmActionDialogOpen, setConfirmActionDialogOpen] = useState(false)
const [errorMessage, setErrorMessage] = useState(null)
const classes = useStyles()
const [machineAction] = useMutation(MACHINE_ACTION, {
onError: ({ message }) => {
const errorMessage = message ?? 'An error ocurred'
setErrorMessage(errorMessage)
},
onCompleted: () => {
onActionSuccess && onActionSuccess()
setConfirmActionDialogOpen(false)
}
})
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}>
<div className={classes.rowItem}>
<H3>{data.name}</H3>
</div>
</div>
<div className={classes.row}>
<div className={classes.rowItem}>
<Label3 className={classes.label3}>Status</Label3>
{data && data.statuses ? <Status status={data.statuses[0]} /> : null}
</div>
</div>
<div className={classes.row}>
<div className={classes.rowItem}>
<Label3 className={classes.label3}>Last ping</Label3>
<P>{data.lastPing ? makeLastPing() : ''}</P>
</div>
</div>
<div className={classes.row}>
<div className={classes.rowItem}>
{' '}
<Label3 className={classes.label3}>Actions</Label3>
{data.name && (
<div className={classes.actionButtonsContainer}>
<ActionButton
color="primary"
className={classes.actionButton}
Icon={UnpairIcon}
InverseIcon={UnpairReversedIcon}
onClick={() => confirmActionDialog('Unpair')}>
Unpair
</ActionButton>
<ActionButton
color="primary"
className={classes.actionButton}
Icon={RebootIcon}
InverseIcon={RebootReversedIcon}
onClick={() => confirmActionDialog('Reboot')}>
Reboot
</ActionButton>
<ActionButton
className={classes.actionButton}
color="primary"
Icon={ShutdownIcon}
InverseIcon={ShutdownReversedIcon}
onClick={() => confirmActionDialog('Shutdown')}>
Shutdown
</ActionButton>
</div>
)}
</div>
</div>
<ConfirmDialog
open={confirmActionDialogOpen}
title={`${action} this machine?`}
errorMessage={errorMessage}
toBeConfirmed={data.name}
onConfirmed={() => {
setErrorMessage(null)
machineAction({
variables: {
deviceId: data.deviceId,
action: `${action}`.toLowerCase()
}
})
}}
onDissmised={() => {
setConfirmActionDialogOpen(false)
setErrorMessage(null)
}}
/>
</>
)
}
export default Overview

View file

@ -105,7 +105,7 @@ const DataTable = ({
useEffect(() => setExpanded(initialExpanded), [initialExpanded])
const coreWidth = R.compose(R.sum, R.map(R.prop('width')))(elements)
const expWidth = 1200 - coreWidth
const expWidth = 1000 - coreWidth
const width = coreWidth + (expandable ? expWidth : 0)
const classes = useStyles({ width })

View file

@ -20,7 +20,7 @@ const useStyles = makeStyles(mainStyles)
const NUM_LOG_RESULTS = 5
const GET_TRANSACTIONS = gql`
query transactions($limit: Int, $from: Date, $until: Date, $id: String) {
query transactions($limit: Int, $from: Date, $until: Date, $id: ID) {
transactions(limit: $limit, from: $from, until: $until, id: $id) {
id
txClass
@ -66,6 +66,10 @@ const Transactions = ({ id }) => {
}
)
if (!loading && txResponse) {
txResponse.transactions = txResponse.transactions.splice(0, 5)
}
useEffect(() => {
if (id !== null) {
getTx()
@ -91,13 +95,6 @@ const Transactions = ({ id }) => {
size: 'sm',
view: it => (it.txClass === 'cashOut' ? <TxOutIcon /> : <TxInIcon />)
},
{
header: 'Machine',
name: 'machineName',
width: 180,
size: 'sm',
view: R.path(['machineName'])
},
{
header: 'Customer',
width: 162,
@ -113,7 +110,7 @@ const Transactions = ({ id }) => {
},
{
header: 'Crypto',
width: 144,
width: 164,
textAlign: 'right',
size: 'sm',
view: it =>
@ -126,14 +123,15 @@ const Transactions = ({ id }) => {
view: it => formatCryptoAddress(it.cryptoCode, it.toAddress),
className: classes.overflowTd,
size: 'sm',
width: 140
textAlign: 'left',
width: 170
},
{
header: 'Date (UTC)',
view: it => moment.utc(it.created).format('YYYY-MM-DD HH:mm:ss'),
textAlign: 'right',
view: it => moment.utc(it.created).format('YYYY-MM-DD'),
textAlign: 'left',
size: 'sm',
width: 200
width: 150
},
{
header: 'Status',
@ -161,7 +159,8 @@ const Transactions = ({ id }) => {
loading={loading || id === null}
emptyText="No transactions so far"
elements={elements}
data={R.path(['transactions'])(txResponse)}
// need to splice because back end query could return double NUM_LOG_RESULTS because it doesnt merge the txIn and the txOut results before applying the limit
data={R.path(['transactions'])(txResponse)} // .splice(0,NUM_LOG_RESULTS)}
Details={DetailsRow}
expandable
/>

View file

@ -0,0 +1,27 @@
import List from '@material-ui/core/List'
import ListItem from '@material-ui/core/ListItem'
import ListItemText from '@material-ui/core/ListItemText'
import React from 'react'
const MachineSidebar = ({ data, getText, getKey, isSelected, selectItem }) => {
return (
<List style={{ height: 400, overflowY: 'auto' }}>
{data.map((item, idx) => {
return (
<ListItem
disableRipple
key={getKey(item) + idx}
button
selected={isSelected(getText(item))}
onClick={() => selectItem(getText(item))}>
<ListItemText primary={getText(item)} />
</ListItem>
)
})}
</List>
)
/* return data.map(item => <button key={getKey(item)}>{getText(item)}</button>) */
}
export default MachineSidebar

View file

@ -1,20 +1,22 @@
import { useQuery } from '@apollo/react-hooks'
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 gql from 'graphql-tag'
import * as R from 'ramda'
import React, { useState, useEffect } from 'react'
import { Link, useLocation } from 'react-router-dom'
import Sidebar from 'src/components/layout/Sidebar'
import TitleSection from 'src/components/layout/TitleSection'
import { TL1 } from 'src/components/typography'
import { TL1, TL2, Label3 } from 'src/components/typography'
import Cassettes from './MachineComponents/Cassettes'
import Commissions from './MachineComponents/Commissions'
import Details from './MachineComponents/Details'
import Overview from './MachineComponents/Overview'
import Transactions from './MachineComponents/Transactions'
import Sidebar from './MachineSidebar'
import styles from './Machines.styles'
const useStyles = makeStyles(styles)
const getMachineInfo = R.compose(R.find, R.propEq('name'))
@ -45,58 +47,76 @@ const getMachines = R.path(['machines'])
const Machines = () => {
const { data, refetch, loading } = useQuery(GET_INFO)
const location = useLocation()
const [selectedMachine, setSelectedMachine] = useState('')
const [touched, setTouched] = useState(false)
const classes = useStyles()
const machines = getMachines(data) ?? []
const machineInfo = getMachineInfo(selectedMachine)(machines) ?? {}
// pre-selects first machine from the list, if there is a machine configured.
// Only runs if user hasnt touched the sidebar yet
useEffect(() => {
if (!loading && data && data.machines && !touched) {
setSelectedMachine(R.path(['machines', 0, 'name'])(data) ?? '')
if (!loading && data && data.machines) {
if (location.state && location.state.selectedMachine) {
setSelectedMachine(location.state.selectedMachine)
} else {
setSelectedMachine(R.path(['machines', 0, 'name'])(data) ?? '')
}
}
}, [data, loading, touched])
}, [loading, data, location.state])
/*
const isId = R.either(R.propEq('machine', 'ALL_MACHINES'), R.propEq('machine', 'e139c9021251ecf9c5280379b885983901b3dad14963cf38b6d7c1fb33faf72e'))
R.filter(isId)(data.overrides)
*/
return (
<>
<TitleSection title="Machine details page" />
<Grid container className={classes.grid}>
<Sidebar
data={machines}
isSelected={it => it.name === selectedMachine}
displayName={it => it.name}
onClick={it => {
setTouched(true)
setSelectedMachine(it.name)
}}
/>
<div className={classes.content}>
<div className={classes.detailItem}>
<TL1 className={classes.subtitle}>{'Details'}</TL1>
<Details data={machineInfo} onActionSuccess={refetch} />
</div>
<div className={classes.detailItem}>
<TL1 className={classes.subtitle}>{'Cash cassettes'}</TL1>
<Cassettes machine={machineInfo} config={data?.config ?? false} />
</div>
<div className={classes.transactionsItem}>
<TL1 className={classes.subtitle}>{'Latest transactions'}</TL1>
<Transactions id={machineInfo?.deviceId ?? null} />
</div>
<div className={classes.detailItem}>
<TL1 className={classes.subtitle}>{'Commissions'}</TL1>
<Commissions
name={'commissions'}
id={machineInfo?.deviceId ?? null}
<Grid item xs={3}>
<Grid item xs={12}>
<div style={{ marginTop: 32 }}>
<Breadcrumbs separator={<NavigateNextIcon fontSize="small" />}>
<Link to="/dashboard" style={{ textDecoration: 'none' }}>
<Label3 className={classes.subtitle}>Dashboard</Label3>
</Link>
<TL2 className={classes.subtitle}>{selectedMachine}</TL2>
</Breadcrumbs>
<Overview data={machineInfo} onActionSuccess={refetch} />
</div>
</Grid>
<Grid item xs={12}>
<Sidebar
isSelected={R.equals(selectedMachine)}
selectItem={setSelectedMachine}
data={machines}
getText={R.prop('name')}
getKey={R.prop('deviceId')}
/>
</Grid>
</Grid>
<Grid item xs={9}>
<div className={classes.content}>
<div className={classes.detailItem} style={{ marginTop: 24 }}>
<TL1 className={classes.subtitle}>{'Details'}</TL1>
<Details data={machineInfo} />
</div>
<div className={classes.detailItem}>
<TL1 className={classes.subtitle}>{'Cash cassettes'}</TL1>
<Cassettes
refetchData={refetch}
machine={machineInfo}
config={data?.config ?? false}
/>
</div>
<div className={classes.transactionsItem}>
<TL1 className={classes.subtitle}>{'Latest transactions'}</TL1>
<Transactions id={machineInfo?.deviceId ?? null} />
</div>
<div className={classes.detailItem}>
<TL1 className={classes.subtitle}>{'Commissions'}</TL1>
<Commissions
name={'commissions'}
id={machineInfo?.deviceId ?? null}
/>
</div>
</div>
</div>
</Grid>
</Grid>
</>
)

View file

@ -35,7 +35,7 @@ export default {
flexDirection: 'row',
color: comet
},
tl2: {
label3: {
color: comet,
marginTop: 0
},