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

View file

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

View file

@ -1,13 +1,15 @@
import { useQuery } from '@apollo/react-hooks' import { useQuery } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core' import { makeStyles } from '@material-ui/core'
import Button from '@material-ui/core/Button'
import Grid from '@material-ui/core/Grid' import Grid from '@material-ui/core/Grid'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import * as R from 'ramda' 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 TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.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 { fromNamespace } from 'src/utils/config'
import styles from './Footer.styles' import styles from './Footer.styles'
@ -29,8 +31,32 @@ const GET_DATA = gql`
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const Footer = () => { const Footer = () => {
const { data, loading } = useQuery(GET_DATA) 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() 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 wallets = fromNamespace('wallets')(data?.config)
const renderFooterItem = key => { const renderFooterItem = key => {
@ -99,17 +125,56 @@ 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 ( return (
<> <>
<div className={classes.footer}> <div
className={!expanded ? classes.footer : null}
style={expanded ? makeFooterExpandedClass() : null}>
<div className={classes.content}> <div className={classes.content}>
{!loading && data && ( {!loading && data && (
<> <>
<Grid container spacing={1}> <Grid container spacing={1}>
<Grid container item xs={11} style={{ marginBottom: 18 }}>
{R.keys(data.rates.withCommissions).map(key => {R.keys(data.rates.withCommissions).map(key =>
renderFooterItem(key) renderFooterItem(key)
)} )}
</Grid> </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>
</> </>
)} )}
</div> </div>

View file

@ -67,11 +67,14 @@ export default {
width: '100vw', width: '100vw',
backgroundColor: white, backgroundColor: white,
textAlign: 'left', textAlign: 'left',
height: 88 height: 88,
boxShadow: '0px -1px 10px 0px rgba(50, 50, 50, 0.1)'
}, },
content: { content: {
width: 1200, width: 1200,
margin: '0 auto' margin: '0 auto',
backgroundColor: white,
marginTop: 4
}, },
headerLabels: { headerLabels: {
whiteSpace: 'pre', 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 RefScatterplot = ({ data: realData, timeFrame }) => {
const svgRef = useRef() const svgRef = useRef()
const cashIns = R.filter(R.propEq('txClass', 'cashIn'))(realData) const cashIns = R.filter(R.propEq('txClass', 'cashIn'))(realData)
const cashOuts = R.filter(R.propEq('txClass', 'cashOut'))(realData) const cashOuts = R.filter(R.propEq('txClass', 'cashOut'))(realData)
const drawGraph = useCallback(() => { const drawGraph = useCallback(() => {
const svg = d3.select(svgRef.current) const svg = d3.select(svgRef.current)
const margin = { top: 25, right: 0, bottom: 25, left: 15 } 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 // 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) // (this is because the Y axis looks best with multiples of 100)
const findMaxY = () => { const findMaxY = () => {
if (realData.length === 0) {
return 100
}
let maxY = d3.max(realData, t => parseFloat(t.fiat)) let maxY = d3.max(realData, t => parseFloat(t.fiat))
maxY = 100 * Math.ceil(maxY / 100) maxY = 100 * Math.ceil(maxY / 100)
if (maxY < 100) { if (maxY < 100) {
@ -37,7 +38,7 @@ const RefScatterplot = ({ data: realData, timeFrame }) => {
ticks: 4, ticks: 4,
subtractDays: 1, subtractDays: 1,
timeFormat: '%H:%M', timeFormat: '%H:%M',
timeRange: [0, 500] timeRange: [50, 500]
} }
switch (timeFrame) { switch (timeFrame) {
case 'Day': case 'Day':
@ -48,7 +49,7 @@ const RefScatterplot = ({ data: realData, timeFrame }) => {
nice: 7, nice: 7,
ticks: 7, ticks: 7,
subtractDays: 7, subtractDays: 7,
timeFormat: '%d', timeFormat: '%a %d',
timeRange: [50, 500] timeRange: [50, 500]
} }
case 'Month': case 'Month':
@ -98,12 +99,9 @@ const RefScatterplot = ({ data: realData, timeFrame }) => {
.scaleTime() .scaleTime()
.domain([ .domain([
moment() moment()
.endOf('day')
.add(-xAxisSettings.subtractDays, 'day') .add(-xAxisSettings.subtractDays, 'day')
.valueOf(), .valueOf(),
moment() moment().valueOf()
.endOf('day')
.valueOf()
]) ])
.range(xAxisSettings.timeRange) .range(xAxisSettings.timeRange)
.nice(xAxisSettings.nice) .nice(xAxisSettings.nice)

View file

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

View file

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

View file

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

View file

@ -1,75 +1,20 @@
import { useMutation } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import gql from 'graphql-tag'
import moment from 'moment' import moment from 'moment'
import React, { useState } from 'react' import React from 'react'
import { ConfirmDialog } from 'src/components/ConfirmDialog' import { Label3, P } from 'src/components/typography'
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 styles from '../Machines.styles' import styles from '../Machines.styles'
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const MACHINE_ACTION = gql` const Details = ({ data }) => {
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 classes = useStyles() const classes = useStyles()
return ( return (
<> <>
<div className={classes.row}> <div className={classes.row}>
<div className={classes.rowItem}> <div className={classes.rowItem}>
{' '} {' '}
<Label4 className={classes.tl2}>Status</Label4> <Label3 className={classes.label3}>Paired at</Label3>
{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>
<P> <P>
{data.pairedAt {data.pairedAt
? moment(data.pairedAt).format('YYYY-MM-DD HH:mm:ss') ? moment(data.pairedAt).format('YYYY-MM-DD HH:mm:ss')
@ -77,68 +22,15 @@ const Details = ({ data, onActionSuccess }) => {
</P> </P>
</div> </div>
<div className={classes.rowItem}> <div className={classes.rowItem}>
{' '} <Label3 className={classes.label3}>Machine model</Label3>
<Label4 className={classes.tl2}>Last ping</Label4> <P>{data.model}</P>
<P>
{data.lastPing
? moment(data.lastPing).format('YYYY-MM-DD HH:mm:ss')
: ''}
</P>
</div> </div>
</div>
<div className={classes.row}>
<div className={classes.rowItem}> <div className={classes.rowItem}>
{' '} {' '}
<Label4 className={classes.tl2}>Actions</Label4> <Label3 className={classes.label3}>Software version</Label3>
{data.name && ( <P>{data.version}</P>
<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>
</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]) useEffect(() => setExpanded(initialExpanded), [initialExpanded])
const coreWidth = R.compose(R.sum, R.map(R.prop('width')))(elements) 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 width = coreWidth + (expandable ? expWidth : 0)
const classes = useStyles({ width }) const classes = useStyles({ width })

View file

@ -20,7 +20,7 @@ const useStyles = makeStyles(mainStyles)
const NUM_LOG_RESULTS = 5 const NUM_LOG_RESULTS = 5
const GET_TRANSACTIONS = gql` 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) { transactions(limit: $limit, from: $from, until: $until, id: $id) {
id id
txClass txClass
@ -66,6 +66,10 @@ const Transactions = ({ id }) => {
} }
) )
if (!loading && txResponse) {
txResponse.transactions = txResponse.transactions.splice(0, 5)
}
useEffect(() => { useEffect(() => {
if (id !== null) { if (id !== null) {
getTx() getTx()
@ -91,13 +95,6 @@ const Transactions = ({ id }) => {
size: 'sm', size: 'sm',
view: it => (it.txClass === 'cashOut' ? <TxOutIcon /> : <TxInIcon />) view: it => (it.txClass === 'cashOut' ? <TxOutIcon /> : <TxInIcon />)
}, },
{
header: 'Machine',
name: 'machineName',
width: 180,
size: 'sm',
view: R.path(['machineName'])
},
{ {
header: 'Customer', header: 'Customer',
width: 162, width: 162,
@ -113,7 +110,7 @@ const Transactions = ({ id }) => {
}, },
{ {
header: 'Crypto', header: 'Crypto',
width: 144, width: 164,
textAlign: 'right', textAlign: 'right',
size: 'sm', size: 'sm',
view: it => view: it =>
@ -126,14 +123,15 @@ const Transactions = ({ id }) => {
view: it => formatCryptoAddress(it.cryptoCode, it.toAddress), view: it => formatCryptoAddress(it.cryptoCode, it.toAddress),
className: classes.overflowTd, className: classes.overflowTd,
size: 'sm', size: 'sm',
width: 140 textAlign: 'left',
width: 170
}, },
{ {
header: 'Date (UTC)', header: 'Date (UTC)',
view: it => moment.utc(it.created).format('YYYY-MM-DD HH:mm:ss'), view: it => moment.utc(it.created).format('YYYY-MM-DD'),
textAlign: 'right', textAlign: 'left',
size: 'sm', size: 'sm',
width: 200 width: 150
}, },
{ {
header: 'Status', header: 'Status',
@ -161,7 +159,8 @@ const Transactions = ({ id }) => {
loading={loading || id === null} loading={loading || id === null}
emptyText="No transactions so far" emptyText="No transactions so far"
elements={elements} 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} Details={DetailsRow}
expandable 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 { useQuery } from '@apollo/react-hooks'
import Breadcrumbs from '@material-ui/core/Breadcrumbs'
import Grid from '@material-ui/core/Grid' import Grid from '@material-ui/core/Grid'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import NavigateNextIcon from '@material-ui/icons/NavigateNext'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import * as R from 'ramda' import * as R from 'ramda'
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { Link, useLocation } from 'react-router-dom'
import Sidebar from 'src/components/layout/Sidebar' import { TL1, TL2, Label3 } from 'src/components/typography'
import TitleSection from 'src/components/layout/TitleSection'
import { TL1 } from 'src/components/typography'
import Cassettes from './MachineComponents/Cassettes' import Cassettes from './MachineComponents/Cassettes'
import Commissions from './MachineComponents/Commissions' import Commissions from './MachineComponents/Commissions'
import Details from './MachineComponents/Details' import Details from './MachineComponents/Details'
import Overview from './MachineComponents/Overview'
import Transactions from './MachineComponents/Transactions' import Transactions from './MachineComponents/Transactions'
import Sidebar from './MachineSidebar'
import styles from './Machines.styles' import styles from './Machines.styles'
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const getMachineInfo = R.compose(R.find, R.propEq('name')) const getMachineInfo = R.compose(R.find, R.propEq('name'))
@ -45,45 +47,62 @@ const getMachines = R.path(['machines'])
const Machines = () => { const Machines = () => {
const { data, refetch, loading } = useQuery(GET_INFO) const { data, refetch, loading } = useQuery(GET_INFO)
const location = useLocation()
const [selectedMachine, setSelectedMachine] = useState('') const [selectedMachine, setSelectedMachine] = useState('')
const [touched, setTouched] = useState(false)
const classes = useStyles() const classes = useStyles()
const machines = getMachines(data) ?? [] const machines = getMachines(data) ?? []
const machineInfo = getMachineInfo(selectedMachine)(machines) ?? {} const machineInfo = getMachineInfo(selectedMachine)(machines) ?? {}
// pre-selects first machine from the list, if there is a machine configured. // pre-selects first machine from the list, if there is a machine configured.
// Only runs if user hasnt touched the sidebar yet
useEffect(() => { useEffect(() => {
if (!loading && data && data.machines && !touched) { if (!loading && data && data.machines) {
if (location.state && location.state.selectedMachine) {
setSelectedMachine(location.state.selectedMachine)
} else {
setSelectedMachine(R.path(['machines', 0, 'name'])(data) ?? '') 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 ( return (
<> <>
<TitleSection title="Machine details page" />
<Grid container className={classes.grid}> <Grid container className={classes.grid}>
<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 <Sidebar
isSelected={R.equals(selectedMachine)}
selectItem={setSelectedMachine}
data={machines} data={machines}
isSelected={it => it.name === selectedMachine} getText={R.prop('name')}
displayName={it => it.name} getKey={R.prop('deviceId')}
onClick={it => {
setTouched(true)
setSelectedMachine(it.name)
}}
/> />
</Grid>
</Grid>
<Grid item xs={9}>
<div className={classes.content}> <div className={classes.content}>
<div className={classes.detailItem}> <div className={classes.detailItem} style={{ marginTop: 24 }}>
<TL1 className={classes.subtitle}>{'Details'}</TL1> <TL1 className={classes.subtitle}>{'Details'}</TL1>
<Details data={machineInfo} onActionSuccess={refetch} /> <Details data={machineInfo} />
</div> </div>
<div className={classes.detailItem}> <div className={classes.detailItem}>
<TL1 className={classes.subtitle}>{'Cash cassettes'}</TL1> <TL1 className={classes.subtitle}>{'Cash cassettes'}</TL1>
<Cassettes machine={machineInfo} config={data?.config ?? false} /> <Cassettes
refetchData={refetch}
machine={machineInfo}
config={data?.config ?? false}
/>
</div> </div>
<div className={classes.transactionsItem}> <div className={classes.transactionsItem}>
<TL1 className={classes.subtitle}>{'Latest transactions'}</TL1> <TL1 className={classes.subtitle}>{'Latest transactions'}</TL1>
@ -98,6 +117,7 @@ const Machines = () => {
</div> </div>
</div> </div>
</Grid> </Grid>
</Grid>
</> </>
) )
} }

View file

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