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:
parent
19cd086436
commit
00f176fccc
16 changed files with 539 additions and 227 deletions
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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 })
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
27
new-lamassu-admin/src/pages/Machines/MachineSidebar.js
Normal file
27
new-lamassu-admin/src/pages/Machines/MachineSidebar.js
Normal 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
|
||||||
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ export default {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
color: comet
|
color: comet
|
||||||
},
|
},
|
||||||
tl2: {
|
label3: {
|
||||||
color: comet,
|
color: comet,
|
||||||
marginTop: 0
|
marginTop: 0
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue