partial: Dashboard and Locales css migration
This commit is contained in:
parent
d7b2e12f94
commit
5f81487dcc
25 changed files with 314 additions and 883 deletions
|
|
@ -1,10 +1,8 @@
|
|||
import Grid from '@mui/material/Grid'
|
||||
import { makeStyles } from '@mui/styles'
|
||||
import classnames from 'classnames'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
|
||||
import { white } from 'src/styling/variables'
|
||||
import Paper from '@mui/material/Paper'
|
||||
import classnames from 'classnames'
|
||||
|
||||
const cardState = Object.freeze({
|
||||
DEFAULT: 'default',
|
||||
|
|
@ -12,24 +10,11 @@ const cardState = Object.freeze({
|
|||
EXPANDED: 'expanded'
|
||||
})
|
||||
|
||||
const styles = {
|
||||
card: {
|
||||
wordWrap: 'break-word',
|
||||
boxShadow: '0 0 4px 0 rgba(0, 0, 0, 0.08)',
|
||||
borderRadius: 12,
|
||||
padding: 24,
|
||||
backgroundColor: white
|
||||
}
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const CollapsibleCard = ({ className, state, shrunkComponent, children }) => {
|
||||
const classes = useStyles()
|
||||
return (
|
||||
<Grid item className={classnames(className, classes.card)}>
|
||||
<Paper className={classnames('p-6', className)}>
|
||||
{state === cardState.SHRUNK ? shrunkComponent : children}
|
||||
</Grid>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
import { useQuery, gql } from "@apollo/client";
|
||||
import { useQuery, gql } from '@apollo/client'
|
||||
import Button from '@mui/material/Button'
|
||||
import Grid from '@mui/material/Grid'
|
||||
import { makeStyles } from '@mui/styles'
|
||||
import classnames from 'classnames'
|
||||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
import { cardState } from 'src/components/CollapsibleCard'
|
||||
import { Label1, H4 } from 'src/components/typography'
|
||||
|
||||
import styles from './Alerts.styles'
|
||||
import AlertsTable from './AlertsTable'
|
||||
|
||||
const NUM_TO_RENDER = 3
|
||||
|
|
@ -31,10 +29,7 @@ const GET_ALERTS = gql`
|
|||
}
|
||||
`
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const Alerts = ({ onReset, onExpand, size }) => {
|
||||
const classes = useStyles()
|
||||
const showAllItems = size === cardState.EXPANDED
|
||||
const { data } = useQuery(GET_ALERTS)
|
||||
const alerts = R.path(['alerts'])(data) ?? []
|
||||
|
|
@ -44,35 +39,30 @@ const Alerts = ({ onReset, onExpand, size }) => {
|
|||
)(data?.machines ?? [])
|
||||
const alertsLength = alerts.length
|
||||
|
||||
const alertsTableContainerClasses = {
|
||||
[classes.alertsTableContainer]: !showAllItems,
|
||||
[classes.expandedAlertsTableContainer]: showAllItems
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes.container}>
|
||||
<H4 className={classes.h4}>{`Alerts (${alertsLength})`}</H4>
|
||||
<div className="flex justify-between">
|
||||
<H4 noMargin>{`Alerts (${alertsLength})`}</H4>
|
||||
{showAllItems && (
|
||||
<Label1 className={classes.upperButtonLabel}>
|
||||
<Label1 noMargin className="-mt-1">
|
||||
<Button
|
||||
onClick={onReset}
|
||||
size="small"
|
||||
disableRipple
|
||||
disableFocusRipple
|
||||
className={classes.button}>
|
||||
className="p-0 text-zodiac normal-case">
|
||||
{'Show less'}
|
||||
</Button>
|
||||
</Label1>
|
||||
)}
|
||||
</div>
|
||||
<Grid
|
||||
className={classnames(alertsTableContainerClasses)}
|
||||
className={classnames({ 'm-0': true, 'max-h-115': showAllItems })}
|
||||
container
|
||||
spacing={1}>
|
||||
<Grid item xs={12}>
|
||||
{!alerts.length && (
|
||||
<Label1 className={classes.noAlertsLabel}>
|
||||
<Label1 className="text-comet -ml-1 h-30">
|
||||
No new alerts. Your system is running smoothly.
|
||||
</Label1>
|
||||
)}
|
||||
|
|
@ -85,13 +75,13 @@ const Alerts = ({ onReset, onExpand, size }) => {
|
|||
</Grid>
|
||||
{!showAllItems && alertsLength > NUM_TO_RENDER && (
|
||||
<Grid item xs={12}>
|
||||
<Label1 className={classes.centerLabel}>
|
||||
<Label1 className="text-center mb-0">
|
||||
<Button
|
||||
onClick={() => onExpand('alerts')}
|
||||
size="small"
|
||||
disableRipple
|
||||
disableFocusRipple
|
||||
className={classes.button}>
|
||||
className="p-0 text-zodiac normal-case">
|
||||
{`Show all (${alerts.length})`}
|
||||
</Button>
|
||||
</Label1>
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
import { primaryColor, comet } from 'src/styling/variables'
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
h4: {
|
||||
margin: 0,
|
||||
marginBottom: 10
|
||||
},
|
||||
centerLabel: {
|
||||
marginBottom: 0,
|
||||
padding: 0,
|
||||
textAlign: 'center'
|
||||
},
|
||||
upperButtonLabel: {
|
||||
marginTop: -3,
|
||||
marginBottom: 24
|
||||
},
|
||||
button: {
|
||||
color: primaryColor,
|
||||
marginTop: 0,
|
||||
minHeight: 0,
|
||||
minWidth: 0,
|
||||
padding: 0,
|
||||
textTransform: 'none',
|
||||
'&:hover': {
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
},
|
||||
alertsTableContainer: {
|
||||
margin: 0
|
||||
},
|
||||
expandedAlertsTableContainer: {
|
||||
margin: 0,
|
||||
maxHeight: 460
|
||||
},
|
||||
noAlertsLabel: {
|
||||
color: comet,
|
||||
marginLeft: -5,
|
||||
height: 100
|
||||
},
|
||||
table: {
|
||||
maxHeight: 465,
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'auto'
|
||||
},
|
||||
listItemText: {
|
||||
margin: '8px 0 8px 0'
|
||||
},
|
||||
linkIcon: {
|
||||
marginLeft: 'auto',
|
||||
cursor: 'pointer'
|
||||
}
|
||||
}
|
||||
export default styles
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
import { makeStyles } from '@mui/styles'
|
||||
import List from '@mui/material/List'
|
||||
import ListItem from '@mui/material/ListItem'
|
||||
import * as R from 'ramda'
|
||||
|
|
@ -10,9 +9,6 @@ import CashBoxEmpty from 'src/styling/icons/cassettes/cashbox-empty.svg?react'
|
|||
import AlertLinkIcon from 'src/styling/icons/month arrows/right.svg?react'
|
||||
import WarningIcon from 'src/styling/icons/warning-icon/tomato.svg?react'
|
||||
|
||||
import styles from './Alerts.styles'
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const icons = {
|
||||
error: <WarningIcon style={{ height: 20, width: 20, marginRight: 12 }} />,
|
||||
fiatBalance: (
|
||||
|
|
@ -28,7 +24,6 @@ const links = {
|
|||
|
||||
const AlertsTable = ({ numToRender, alerts, machines }) => {
|
||||
const history = useHistory()
|
||||
const classes = useStyles()
|
||||
const alertsToRender = R.slice(0, numToRender, alerts)
|
||||
|
||||
const alertMessage = alert => {
|
||||
|
|
@ -40,16 +35,16 @@ const AlertsTable = ({ numToRender, alerts, machines }) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<List dense className={classes.table}>
|
||||
<List dense className="max-h-116 overflow-y-auto overflow-x-hidden">
|
||||
{alertsToRender.map((alert, idx) => {
|
||||
return (
|
||||
<ListItem key={idx}>
|
||||
{icons[alert.type] || (
|
||||
<Wrench style={{ height: 23, width: 23, marginRight: 8 }} />
|
||||
)}
|
||||
<P className={classes.listItemText}>{alertMessage(alert)}</P>
|
||||
<P className="my-2">{alertMessage(alert)}</P>
|
||||
<AlertLinkIcon
|
||||
className={classes.linkIcon}
|
||||
className="ml-auto cursor-pointer"
|
||||
onClick={() => history.push(links[alert.type] || '/dashboard')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import { useQuery, gql } from "@apollo/client";
|
||||
import Grid from '@mui/material/Grid'
|
||||
import { makeStyles } from '@mui/styles'
|
||||
import { useQuery, gql } from '@apollo/client'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
|
|
@ -13,11 +11,10 @@ import { Button } from 'src/components/buttons'
|
|||
import AddMachine from 'src/pages/AddMachine'
|
||||
import { errorColor } from 'src/styling/variables'
|
||||
|
||||
import styles from './Dashboard.styles'
|
||||
import Footer from './Footer'
|
||||
import LeftSide from './LeftSide'
|
||||
import RightSide from './RightSide'
|
||||
const useStyles = makeStyles(styles)
|
||||
import Paper from '@mui/material/Paper'
|
||||
import SystemPerformance from './SystemPerformance/index.js'
|
||||
|
||||
const GET_DATA = gql`
|
||||
query getData {
|
||||
|
|
@ -30,7 +27,6 @@ const GET_DATA = gql`
|
|||
|
||||
const Dashboard = () => {
|
||||
const history = useHistory()
|
||||
const classes = useStyles()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const { data, loading } = useQuery(GET_DATA)
|
||||
|
|
@ -44,16 +40,16 @@ const Dashboard = () => {
|
|||
!R.isEmpty(data.machines) ? (
|
||||
<>
|
||||
<TitleSection title="Dashboard">
|
||||
<div className={classes.headerLabels}>
|
||||
<div>
|
||||
<div className="flex gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<TxInIcon />
|
||||
<span>Cash-in</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<TxOutIcon />
|
||||
<span>Cash-out</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<svg width={12} height={12}>
|
||||
<rect width={12} height={12} rx={3} fill={errorColor} />
|
||||
</svg>
|
||||
|
|
@ -61,15 +57,15 @@ const Dashboard = () => {
|
|||
</div>
|
||||
</div>
|
||||
</TitleSection>
|
||||
<div className={classes.root}>
|
||||
<Grid container>
|
||||
<Grid container direction="column" item xs={6}>
|
||||
<LeftSide />
|
||||
</Grid>
|
||||
<Grid container direction="column" item xs={6}>
|
||||
<RightSide />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<div className="flex mb-30 gap-4">
|
||||
<div className="flex flex-col flex-1">
|
||||
<Paper className="p-6">
|
||||
<SystemPerformance />
|
||||
</Paper>
|
||||
</div>
|
||||
<div className="flex flex-col flex-1">
|
||||
<RightSide />
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</>
|
||||
|
|
@ -79,17 +75,17 @@ const Dashboard = () => {
|
|||
<AddMachine close={() => setOpen(false)} onPaired={onPaired} />
|
||||
)}
|
||||
<TitleSection title="Dashboard">
|
||||
<div className={classes.headerLabels}>
|
||||
<div className="flex flex-row">
|
||||
<span>
|
||||
<TL2 className={classes.inline}>{data?.serverVersion}</TL2>{' '}
|
||||
<Label1 className={classes.inline}> server version</Label1>
|
||||
<TL2 className="inline">{data?.serverVersion}</TL2>{' '}
|
||||
<Label1 className="inline"> server version</Label1>
|
||||
</span>
|
||||
</div>
|
||||
</TitleSection>
|
||||
<div className={classes.emptyMachinesRoot}>
|
||||
<div className={classes.emptyMachinesContent}>
|
||||
<H1 className={classes.offColor}>No machines on your system yet</H1>
|
||||
<Info2 className={classes.offColor}>
|
||||
<div className="h-75 bg-zircon border-zircon2 border-2">
|
||||
<div className="flex flex-col h-full justify-center items-center gap-6">
|
||||
<H1 className="text-comet2">No machines on your system yet</H1>
|
||||
<Info2 className="text-comet2">
|
||||
To fully take advantage of Lamassu Admin, add a new machine to
|
||||
your system
|
||||
</Info2>
|
||||
|
|
|
|||
|
|
@ -1,120 +0,0 @@
|
|||
import typographyStyles from 'src/components/typography/styles'
|
||||
import {
|
||||
spacer,
|
||||
white,
|
||||
primaryColor,
|
||||
zircon,
|
||||
zircon2,
|
||||
offDarkColor
|
||||
} from 'src/styling/variables'
|
||||
const { label1 } = typographyStyles
|
||||
|
||||
const styles = {
|
||||
headerLabels: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
'& > div:first-child': {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginLeft: 0
|
||||
},
|
||||
'& > div': {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginLeft: 25
|
||||
},
|
||||
'& > div:last-child': {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginLeft: 64
|
||||
},
|
||||
'& > div > span': {
|
||||
extend: label1,
|
||||
marginLeft: 7
|
||||
}
|
||||
},
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
marginBottom: 120
|
||||
},
|
||||
emptyMachinesRoot: {
|
||||
height: 300,
|
||||
backgroundColor: zircon,
|
||||
border: `solid 2px ${zircon2}`
|
||||
},
|
||||
card: {
|
||||
wordWrap: 'break-word',
|
||||
boxShadow: '0 0 4px 0 rgba(0, 0, 0, 0.08)',
|
||||
borderRadius: 12,
|
||||
padding: 24,
|
||||
backgroundColor: white,
|
||||
flex: 1,
|
||||
marginRight: 24
|
||||
},
|
||||
container: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
button: {
|
||||
color: primaryColor,
|
||||
minHeight: 0,
|
||||
minWidth: 0,
|
||||
padding: 0,
|
||||
textTransform: 'none',
|
||||
'&:hover': {
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
},
|
||||
upperButtonLabel: {
|
||||
textAlign: 'center',
|
||||
marginBottom: 0,
|
||||
marginTop: 0,
|
||||
marginLeft: spacer
|
||||
},
|
||||
alertsCard: {
|
||||
marginBottom: spacer
|
||||
},
|
||||
h4: {
|
||||
marginTop: 0
|
||||
},
|
||||
centerLabel: {
|
||||
marginTop: 40,
|
||||
marginBottom: 0
|
||||
},
|
||||
systemStatusCard: {
|
||||
flex: 1,
|
||||
marginTop: spacer
|
||||
},
|
||||
expandedCard: {
|
||||
flex: 0.9
|
||||
},
|
||||
shrunkCard: {
|
||||
flex: 0.1
|
||||
},
|
||||
displayFlex: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
inline: {
|
||||
display: 'inline'
|
||||
},
|
||||
emptyMachinesContent: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
'& > :first-child': {
|
||||
marginTop: 0
|
||||
},
|
||||
'& > *': {
|
||||
marginTop: 25
|
||||
}
|
||||
},
|
||||
offColor: {
|
||||
color: offDarkColor
|
||||
}
|
||||
}
|
||||
|
||||
export default styles
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
import { useQuery, gql } from "@apollo/client";
|
||||
import { useQuery, gql } from '@apollo/client'
|
||||
import { makeStyles } from '@mui/styles'
|
||||
import Grid from '@mui/material/Grid'
|
||||
import BigNumber from 'bignumber.js'
|
||||
import classnames from 'classnames'
|
||||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
import { Label2 } from 'src/components/typography'
|
||||
|
|
@ -11,7 +9,8 @@ import TxOutIcon from 'src/styling/icons/direction/cash-out.svg?react'
|
|||
|
||||
import { fromNamespace } from 'src/utils/config'
|
||||
|
||||
import styles from './Footer.styles'
|
||||
import classes from './Footer.module.css'
|
||||
|
||||
const GET_DATA = gql`
|
||||
query getData {
|
||||
cryptoRates
|
||||
|
|
@ -29,12 +28,10 @@ const GET_DATA = gql`
|
|||
|
||||
BigNumber.config({ ROUNDING_MODE: BigNumber.ROUND_HALF_UP })
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
const Footer = () => {
|
||||
const { data } = useQuery(GET_DATA)
|
||||
|
||||
const withCommissions = R.path(['cryptoRates', 'withCommissions'])(data) ?? {}
|
||||
const classes = useStyles()
|
||||
const config = R.path(['config'])(data) ?? {}
|
||||
// const canExpand = R.keys(withCommissions).length > 4
|
||||
|
||||
|
|
@ -72,36 +69,31 @@ const Footer = () => {
|
|||
).toFormat(2)
|
||||
|
||||
return (
|
||||
<Grid key={key} item xs={3}>
|
||||
<Label2 className={classes.label}>
|
||||
<div className="flex flex-col w-1/4">
|
||||
<Label2 className="text-comet mt-3 mb-2">
|
||||
{cryptoCurrencies[idx].display}
|
||||
</Label2>
|
||||
<div className={classes.headerLabels}>
|
||||
<div className={classes.headerLabel}>
|
||||
<div className="flex gap-6">
|
||||
<div className="flex items-center gap-1">
|
||||
<TxInIcon />
|
||||
<Label2>{` ${cashIn} ${localeFiatCurrency}`}</Label2>
|
||||
<Label2 noMargin>{`${cashIn} ${localeFiatCurrency}`}</Label2>
|
||||
</div>
|
||||
<div className={classnames(classes.headerLabel, classes.txOutMargin)}>
|
||||
<div className="flex items-center gap-1">
|
||||
<TxOutIcon />
|
||||
<Label2>{` ${cashOut} ${localeFiatCurrency}`}</Label2>
|
||||
<Label2 noMargin>{`${cashOut} ${localeFiatCurrency}`}</Label2>
|
||||
</div>
|
||||
</div>
|
||||
<Label2
|
||||
className={
|
||||
classes.tickerLabel
|
||||
}>{`${tickerName}: ${avgOfAskBid} ${localeFiatCurrency}`}</Label2>
|
||||
</Grid>
|
||||
<Label2 className="text-comet mt-2">
|
||||
{`${tickerName}: ${avgOfAskBid} ${localeFiatCurrency}`}
|
||||
</Label2>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.footer1}>
|
||||
<div className={classes.content1}>
|
||||
<Grid container>
|
||||
<Grid container className={classes.footerContainer1}>
|
||||
{R.keys(withCommissions).map(key => renderFooterItem(key))}
|
||||
</Grid>
|
||||
</Grid>
|
||||
{R.keys(withCommissions).map(key => renderFooterItem(key))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
.footer1 {
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
background-color: white;
|
||||
text-align: left;
|
||||
z-index: 1;
|
||||
box-shadow: 0 -1px 10px 0 rgba(50, 50, 50, 0.1);
|
||||
min-height: 48px;
|
||||
transition: min-height 0.5s ease-out;
|
||||
}
|
||||
|
||||
.footer1:hover {
|
||||
transition: min-height 0.5s ease-in;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.content1 {
|
||||
width: 1200px;
|
||||
max-height: 100px;
|
||||
background-color: white;
|
||||
z-index: 2;
|
||||
bottom: -8px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
import { offColor, white, spacer } from 'src/styling/variables'
|
||||
|
||||
const styles = {
|
||||
label: {
|
||||
color: offColor
|
||||
},
|
||||
headerLabels: {
|
||||
whiteSpace: 'pre',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
marginTop: -20
|
||||
},
|
||||
headerLabel: {
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
},
|
||||
txOutMargin: {
|
||||
marginLeft: spacer * 3
|
||||
},
|
||||
tickerLabel: {
|
||||
color: offColor,
|
||||
marginTop: -5
|
||||
},
|
||||
footer1: {
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
position: 'fixed',
|
||||
width: '100vw',
|
||||
backgroundColor: white,
|
||||
textAlign: 'left',
|
||||
zIndex: 1,
|
||||
boxShadow: '0px -1px 10px 0px rgba(50, 50, 50, 0.1)',
|
||||
minHeight: spacer * 12,
|
||||
transition: 'min-height 0.5s ease-out',
|
||||
'&:hover': {
|
||||
transition: 'min-height 0.5s ease-in',
|
||||
minHeight: 200
|
||||
}
|
||||
},
|
||||
content1: {
|
||||
width: 1200,
|
||||
maxHeight: 100,
|
||||
backgroundColor: white,
|
||||
zIndex: 2,
|
||||
bottom: -spacer,
|
||||
margin: '0 auto'
|
||||
}
|
||||
}
|
||||
|
||||
export default styles
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import Grid from '@mui/material/Grid'
|
||||
import { makeStyles } from '@mui/styles'
|
||||
import React from 'react'
|
||||
|
||||
import styles from './Dashboard.styles'
|
||||
import SystemPerformance from './SystemPerformance'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const LeftSide = () => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<Grid item xs={12} className={classes.displayFlex}>
|
||||
<div className={classes.card}>
|
||||
<SystemPerformance />
|
||||
</div>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default LeftSide
|
||||
|
|
@ -1,29 +1,23 @@
|
|||
import Button from '@mui/material/Button'
|
||||
import Grid from '@mui/material/Grid'
|
||||
import { makeStyles } from '@mui/styles'
|
||||
import classnames from 'classnames'
|
||||
import React, { useState } from 'react'
|
||||
import CollapsibleCard, { cardState } from 'src/components/CollapsibleCard'
|
||||
import { H4, Label1 } from 'src/components/typography'
|
||||
|
||||
import Alerts from './Alerts'
|
||||
import styles from './Dashboard.styles'
|
||||
import SystemStatus from './SystemStatus'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const ShrunkCard = ({ title, buttonName, onUnshrink }) => {
|
||||
const classes = useStyles()
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
<H4 className={classes.h4}>{title}</H4>
|
||||
<Label1 className={classes.upperButtonLabel}>
|
||||
<div className="flex justify-between">
|
||||
<H4 className="mt-0">{title}</H4>
|
||||
<Label1 className="text-center my-0">
|
||||
<Button
|
||||
onClick={onUnshrink}
|
||||
size="small"
|
||||
disableRipple
|
||||
disableFocusRipple
|
||||
className={classes.button}>
|
||||
className="p-0 text-zodiac normal-case">
|
||||
{buttonName}
|
||||
</Button>
|
||||
</Label1>
|
||||
|
|
@ -32,7 +26,6 @@ const ShrunkCard = ({ title, buttonName, onUnshrink }) => {
|
|||
}
|
||||
|
||||
const RightSide = () => {
|
||||
const classes = useStyles()
|
||||
const [systemStatusSize, setSystemStatusSize] = useState(cardState.DEFAULT)
|
||||
const [alertsSize, setAlertsSize] = useState(cardState.DEFAULT)
|
||||
|
||||
|
|
@ -41,58 +34,55 @@ const RightSide = () => {
|
|||
setSystemStatusSize(cardState.DEFAULT)
|
||||
}
|
||||
return (
|
||||
<Grid item xs={12} className={classes.displayFlex}>
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
|
||||
<>
|
||||
<CollapsibleCard
|
||||
className={classnames({
|
||||
[classes.alertsCard]: alertsSize !== cardState.SHRUNK,
|
||||
[classes.shrunkCard]: alertsSize === cardState.SHRUNK,
|
||||
[classes.expandedCard]: alertsSize === cardState.EXPANDED
|
||||
})}
|
||||
state={alertsSize}
|
||||
shrunkComponent={
|
||||
<ShrunkCard
|
||||
title={'Alerts'}
|
||||
buttonName={'Show alerts'}
|
||||
onUnshrink={onReset}
|
||||
/>
|
||||
}>
|
||||
<Alerts
|
||||
onExpand={() => {
|
||||
setAlertsSize(cardState.EXPANDED)
|
||||
setSystemStatusSize(cardState.SHRUNK)
|
||||
}}
|
||||
onReset={onReset}
|
||||
size={alertsSize}
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="flex-1 flex flex-col gap-4">
|
||||
<CollapsibleCard
|
||||
className={classnames({
|
||||
'flex-[0.1]': alertsSize === cardState.SHRUNK,
|
||||
'flex-[0.9]': alertsSize === cardState.EXPANDED
|
||||
})}
|
||||
state={alertsSize}
|
||||
shrunkComponent={
|
||||
<ShrunkCard
|
||||
title={'Alerts'}
|
||||
buttonName={'Show alerts'}
|
||||
onUnshrink={onReset}
|
||||
/>
|
||||
</CollapsibleCard>
|
||||
<CollapsibleCard
|
||||
className={classnames({
|
||||
[classes.shrunkCard]: systemStatusSize === cardState.SHRUNK,
|
||||
[classes.systemStatusCard]: systemStatusSize !== cardState.SHRUNK,
|
||||
[classes.expandedCard]: alertsSize === cardState.EXPANDED
|
||||
})}
|
||||
state={systemStatusSize}
|
||||
shrunkComponent={
|
||||
<ShrunkCard
|
||||
title={'System status'}
|
||||
buttonName={'Show machines'}
|
||||
onUnshrink={onReset}
|
||||
/>
|
||||
}>
|
||||
<SystemStatus
|
||||
onExpand={() => {
|
||||
setSystemStatusSize(cardState.EXPANDED)
|
||||
setAlertsSize(cardState.SHRUNK)
|
||||
}}
|
||||
onReset={onReset}
|
||||
size={systemStatusSize}
|
||||
}>
|
||||
<Alerts
|
||||
onExpand={() => {
|
||||
setAlertsSize(cardState.EXPANDED)
|
||||
setSystemStatusSize(cardState.SHRUNK)
|
||||
}}
|
||||
onReset={onReset}
|
||||
size={alertsSize}
|
||||
/>
|
||||
</CollapsibleCard>
|
||||
<CollapsibleCard
|
||||
className={classnames({
|
||||
'flex-[0.1]': systemStatusSize === cardState.SHRUNK,
|
||||
'flex-1': systemStatusSize === cardState.DEFAULT,
|
||||
'flex-[0.9]': systemStatusSize === cardState.EXPANDED
|
||||
})}
|
||||
state={systemStatusSize}
|
||||
shrunkComponent={
|
||||
<ShrunkCard
|
||||
title={'System status'}
|
||||
buttonName={'Show machines'}
|
||||
onUnshrink={onReset}
|
||||
/>
|
||||
</CollapsibleCard>
|
||||
</>
|
||||
}>
|
||||
<SystemStatus
|
||||
onExpand={() => {
|
||||
setSystemStatusSize(cardState.EXPANDED)
|
||||
setAlertsSize(cardState.SHRUNK)
|
||||
}}
|
||||
onReset={onReset}
|
||||
size={systemStatusSize}
|
||||
/>
|
||||
</CollapsibleCard>
|
||||
</div>
|
||||
</Grid>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,77 +1,30 @@
|
|||
import { makeStyles } from '@mui/styles'
|
||||
import classnames from 'classnames'
|
||||
import React from 'react'
|
||||
import { Label1 } from 'src/components/typography/index'
|
||||
|
||||
import { java, neon, white } from 'src/styling/variables'
|
||||
|
||||
const styles = {
|
||||
wrapper: {
|
||||
display: 'flex',
|
||||
height: 142
|
||||
},
|
||||
percentageBox: {
|
||||
height: 142,
|
||||
borderRadius: 4,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
whiteSpace: 'pre'
|
||||
},
|
||||
label: {
|
||||
color: white
|
||||
},
|
||||
inColor: {
|
||||
backgroundColor: java
|
||||
},
|
||||
outColor: {
|
||||
backgroundColor: neon
|
||||
},
|
||||
other: {
|
||||
minWidth: '6px',
|
||||
borderRadius: 2
|
||||
},
|
||||
inWidth: {
|
||||
width: value => `${value}%`,
|
||||
marginRight: value => (value === 100 ? 0 : 4)
|
||||
},
|
||||
outWidth: {
|
||||
width: value => `${100 - value}%`
|
||||
}
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const PercentageChart = ({ cashIn, cashOut }) => {
|
||||
const value = cashIn || cashOut !== 0 ? cashIn : 50
|
||||
const classes = useStyles(value)
|
||||
|
||||
const buildPercentageView = value => {
|
||||
if (value <= 15) return
|
||||
return <Label1 className={classes.label}>{value}%</Label1>
|
||||
return <Label1 className="text-white">{value}%</Label1>
|
||||
}
|
||||
|
||||
const percentageClasses = {
|
||||
[classes.percentageBox]: true,
|
||||
[classes.other]: value < 5 && value > 0
|
||||
'h-35 rounded-sm flex items-center justify-center': true,
|
||||
'min-w-2 rounded-xs': value < 5 && value > 0
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.wrapper}>
|
||||
<div className="flex h-35 gap-1">
|
||||
<div
|
||||
className={classnames(
|
||||
percentageClasses,
|
||||
classes.inColor,
|
||||
classes.inWidth
|
||||
)}>
|
||||
className={classnames(percentageClasses, 'bg-java')}
|
||||
style={{ width: `${value}%` }}>
|
||||
{buildPercentageView(value, 'cashIn')}
|
||||
</div>
|
||||
<div
|
||||
className={classnames(
|
||||
percentageClasses,
|
||||
classes.outColor,
|
||||
classes.outWidth
|
||||
)}>
|
||||
className={classnames(percentageClasses, 'bg-neon')}
|
||||
style={{ width: `${100 - value}%` }}>
|
||||
{buildPercentageView(100 - value, 'cashOut')}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ import * as d3 from 'd3'
|
|||
import * as R from 'ramda'
|
||||
import React, { useEffect, useRef, useCallback } from 'react'
|
||||
|
||||
import { backgroundColor, zircon, primaryColor } from 'src/styling/variables'
|
||||
|
||||
const transactionProfit = R.prop('profit')
|
||||
|
||||
const mockPoint = (tx, offsetMs, profit) => {
|
||||
|
|
@ -106,7 +104,7 @@ const RefLineChart = ({
|
|||
.attr('y', -margin.top)
|
||||
.attr('width', width + margin.left + margin.right)
|
||||
.attr('height', height + margin.top)
|
||||
.attr('fill', backgroundColor)
|
||||
.attr('fill', 'var(--ghost)')
|
||||
.attr('transform', `translate(${0},${margin.top})`)
|
||||
|
||||
// gradient color for the graph (creates the "url", the color is applied by calling the url, in the area color fill )
|
||||
|
|
@ -120,9 +118,9 @@ const RefLineChart = ({
|
|||
.attr('y2', '100%')
|
||||
.selectAll('stop')
|
||||
.data([
|
||||
{ offset: '0%', color: zircon },
|
||||
{ offset: '25%', color: zircon },
|
||||
{ offset: '100%', color: backgroundColor }
|
||||
{ offset: '0%', color: 'var(--zircon)' },
|
||||
{ offset: '25%', color: 'var(--zircon)' },
|
||||
{ offset: '100%', color: 'var(--ghost)' }
|
||||
])
|
||||
.enter()
|
||||
.append('stop')
|
||||
|
|
@ -181,7 +179,7 @@ const RefLineChart = ({
|
|||
.attr('fill', 'none')
|
||||
.attr('stroke-width', '2')
|
||||
.attr('stroke-linejoin', 'round')
|
||||
.attr('stroke', primaryColor)
|
||||
.attr('stroke', 'var(--zodiac)')
|
||||
}, [realData, timeFrame, previousTimeData, previousProfit])
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -4,14 +4,6 @@ import { getTimezoneOffset } from 'date-fns-tz'
|
|||
import { add, format, startOfWeek, startOfYear } from 'date-fns/fp'
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
|
||||
import {
|
||||
java,
|
||||
neon,
|
||||
subheaderDarkColor,
|
||||
offColor,
|
||||
fontSecondary,
|
||||
backgroundColor
|
||||
} from 'src/styling/variables'
|
||||
import { numberToFiatAmount } from 'src/utils/number'
|
||||
import { MINUTE, DAY, WEEK, MONTH } from 'src/utils/time'
|
||||
|
||||
|
|
@ -143,7 +135,7 @@ const Graph = ({ data, timeFrame, timezone }) => {
|
|||
.attr('y', GRAPH_MARGIN.top)
|
||||
.attr('width', GRAPH_WIDTH)
|
||||
.attr('height', GRAPH_HEIGHT - GRAPH_MARGIN.top - GRAPH_MARGIN.bottom)
|
||||
.attr('fill', backgroundColor)
|
||||
.attr('fill', 'var(--ghost)')
|
||||
},
|
||||
[GRAPH_MARGIN]
|
||||
)
|
||||
|
|
@ -191,8 +183,8 @@ const Graph = ({ data, timeFrame, timezone }) => {
|
|||
|
||||
const buildGrid = useCallback(
|
||||
g => {
|
||||
g.attr('stroke', subheaderDarkColor)
|
||||
.attr('fill', subheaderDarkColor)
|
||||
g.attr('stroke', 'var(--zircon2)')
|
||||
.attr('fill', 'var(--zircon2)')
|
||||
// Vertical lines
|
||||
.call(g =>
|
||||
g
|
||||
|
|
@ -277,10 +269,10 @@ const Graph = ({ data, timeFrame, timezone }) => {
|
|||
() =>
|
||||
d3
|
||||
.selectAll('.tick text')
|
||||
.style('stroke', offColor)
|
||||
.style('fill', offColor)
|
||||
.style('stroke', 'var(--comet)')
|
||||
.style('fill', 'var(--comet)')
|
||||
.style('stroke-width', 0)
|
||||
.style('font-family', fontSecondary),
|
||||
.style('font-family', 'var(--museo)'),
|
||||
[]
|
||||
)
|
||||
|
||||
|
|
@ -288,10 +280,10 @@ const Graph = ({ data, timeFrame, timezone }) => {
|
|||
() =>
|
||||
d3
|
||||
.selectAll('text')
|
||||
.style('stroke', offColor)
|
||||
.style('fill', offColor)
|
||||
.style('stroke', 'var(--comet)')
|
||||
.style('fill', 'var(--comet)')
|
||||
.style('stroke-width', 0)
|
||||
.style('font-family', fontSecondary),
|
||||
.style('font-family', 'var(--museo)'),
|
||||
[]
|
||||
)
|
||||
|
||||
|
|
@ -311,7 +303,9 @@ const Graph = ({ data, timeFrame, timezone }) => {
|
|||
return x(created.setTime(created.getTime() + offset))
|
||||
})
|
||||
.attr('cy', d => y(new BigNumber(d.fiat).toNumber()))
|
||||
.attr('fill', d => (d.txClass === 'cashIn' ? java : neon))
|
||||
.attr('fill', d =>
|
||||
d.txClass === 'cashIn' ? 'var(--java)' : 'var(--neon)'
|
||||
)
|
||||
.attr('r', 3.5)
|
||||
},
|
||||
[data, offset, x, y]
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ import React from 'react'
|
|||
import { Info1, Label1 } from 'src/components/typography/index'
|
||||
const InfoWithLabel = ({ info, label }) => {
|
||||
return (
|
||||
<>
|
||||
<Info1 style={{ marginBottom: 0 }}>{info}</Info1>
|
||||
<Label1 style={{ margin: 0 }}>{label}</Label1>
|
||||
</>
|
||||
<div className="flex flex-col">
|
||||
<Info1 className="mb-0">{info}</Info1>
|
||||
<Label1 className="m-0">{label}</Label1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,11 @@
|
|||
import { makeStyles } from '@mui/styles'
|
||||
import classnames from 'classnames'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
import { H4 } from 'src/components/typography'
|
||||
|
||||
import styles from './SystemPerformance.styles'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
const ranges = ['Month', 'Week', 'Day']
|
||||
|
||||
const Nav = ({ handleSetRange, showPicker }) => {
|
||||
const classes = useStyles()
|
||||
const [clickedItem, setClickedItem] = useState('Day')
|
||||
|
||||
const isSelected = R.equals(clickedItem)
|
||||
|
|
@ -20,22 +15,20 @@ const Nav = ({ handleSetRange, showPicker }) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={classnames(classes.titleWrapper)}>
|
||||
<div className={classes.titleAndButtonsContainer}>
|
||||
<H4 className={classes.h4}>{'System performance'}</H4>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<H4 noMargin>{'System performance'}</H4>
|
||||
{showPicker && (
|
||||
<div className={classes.navContainer}>
|
||||
<div className="flex gap-6">
|
||||
{ranges.map((it, idx) => {
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
onClick={e => handleClick(e.target.innerText)}
|
||||
className={
|
||||
isSelected(it)
|
||||
? classnames(classes.newHighlightedLabel, classes.navButton)
|
||||
: classnames(classes.label, classes.navButton)
|
||||
}>
|
||||
className={classnames({
|
||||
'cursor-pointer text-comet': true,
|
||||
'font-bold text-zodiac border-b-zodiac border-b-2':
|
||||
isSelected(it)
|
||||
})}>
|
||||
{it}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import { useQuery, gql } from "@apollo/client";
|
||||
import Grid from '@mui/material/Grid'
|
||||
import { makeStyles } from '@mui/styles'
|
||||
import { useQuery, gql } from '@apollo/client'
|
||||
import BigNumber from 'bignumber.js'
|
||||
import classnames from 'classnames'
|
||||
import { isAfter } from 'date-fns/fp'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
import { Label1, Label2, P } from 'src/components/typography/index'
|
||||
import { Info2, Label1, Label2, P } from 'src/components/typography/index'
|
||||
import PercentDownIcon from 'src/styling/icons/dashboard/down.svg?react'
|
||||
import PercentNeutralIcon from 'src/styling/icons/dashboard/equal.svg?react'
|
||||
import PercentUpIcon from 'src/styling/icons/dashboard/up.svg?react'
|
||||
|
|
@ -23,12 +21,10 @@ import LineChart from './Graphs/RefLineChart'
|
|||
import Scatterplot from './Graphs/RefScatterplot'
|
||||
import InfoWithLabel from './InfoWithLabel'
|
||||
import Nav from './Nav'
|
||||
import styles from './SystemPerformance.styles'
|
||||
|
||||
BigNumber.config({ ROUNDING_MODE: BigNumber.ROUND_HALF_UP })
|
||||
|
||||
const getFiats = R.map(R.prop('fiat'))
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const GET_DATA = gql`
|
||||
query getData($excludeTestingCustomers: Boolean) {
|
||||
|
|
@ -54,7 +50,6 @@ const GET_DATA = gql`
|
|||
`
|
||||
|
||||
const SystemPerformance = () => {
|
||||
const classes = useStyles()
|
||||
const [selectedRange, setSelectedRange] = useState('Day')
|
||||
const { data, loading } = useQuery(GET_DATA, {
|
||||
variables: { excludeTestingCustomers: true }
|
||||
|
|
@ -162,17 +157,17 @@ const SystemPerformance = () => {
|
|||
const percentChange = getPercentChange()
|
||||
|
||||
const percentageClasses = {
|
||||
[classes.percentDown]: percentChange < 0,
|
||||
[classes.percentUp]: percentChange > 0,
|
||||
[classes.percentNeutral]: percentChange === 0
|
||||
'text-tomato': percentChange < 0,
|
||||
'text-spring4': percentChange > 0,
|
||||
'text-comet': percentChange === 0,
|
||||
'flex items-center justify-center gap-1': true
|
||||
}
|
||||
|
||||
const getPercentageIcon = () => {
|
||||
if (percentChange === 0)
|
||||
return <PercentNeutralIcon className={classes.directionIcon} />
|
||||
if (percentChange > 0)
|
||||
return <PercentUpIcon className={classes.directionIcon} />
|
||||
return <PercentDownIcon className={classes.directionIcon} />
|
||||
const className = 'w-4 h-4'
|
||||
if (percentChange === 0) return <PercentNeutralIcon className={className} />
|
||||
if (percentChange > 0) return <PercentUpIcon className={className} />
|
||||
return <PercentDownIcon className={className} />
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -182,74 +177,67 @@ const SystemPerformance = () => {
|
|||
handleSetRange={setSelectedRange}
|
||||
/>
|
||||
{!loading && R.isEmpty(data.transactions) && (
|
||||
<EmptyTable
|
||||
className={classes.emptyTransactions}
|
||||
message="No transactions so far"
|
||||
/>
|
||||
<EmptyTable className="pt-10" message="No transactions so far" />
|
||||
)}
|
||||
{!loading && !R.isEmpty(data.transactions) && (
|
||||
<>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={3}>
|
||||
<InfoWithLabel
|
||||
info={getNumTransactions()}
|
||||
label={'transactions'}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={3}>
|
||||
<InfoWithLabel
|
||||
info={getFiatVolume()}
|
||||
label={`${data?.config.locale_fiatCurrency} volume`}
|
||||
/>
|
||||
</Grid>
|
||||
{/* todo new customers */}
|
||||
</Grid>
|
||||
<Grid container className={classes.txGraphContainer}>
|
||||
<Grid item xs={12}>
|
||||
<div className={classes.graphHeader}>
|
||||
<Label2 noMargin>Transactions</Label2>
|
||||
<div className={classes.labelWrapper}>
|
||||
<P noMargin>
|
||||
{timezones[timezone]?.short ?? timezones[timezone]?.long}{' '}
|
||||
timezone
|
||||
</P>
|
||||
<span className={classes.verticalLine} />
|
||||
<div>
|
||||
<div className="flex flex-col gap-12">
|
||||
<div className="flex gap-16">
|
||||
<InfoWithLabel info={getNumTransactions()} label={'transactions'} />
|
||||
<InfoWithLabel
|
||||
info={getFiatVolume()}
|
||||
label={`${data?.config.locale_fiatCurrency} volume`}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-62">
|
||||
<div className="flex justify-between mb-4">
|
||||
<Label2 noMargin>Transactions</Label2>
|
||||
<div className="flex items-center">
|
||||
<P noMargin>
|
||||
{timezones[timezone]?.short ?? timezones[timezone]?.long}{' '}
|
||||
timezone
|
||||
</P>
|
||||
<span className="h-4 w-[1px] bg-comet2 mr-4 ml-8" />
|
||||
<div className="flex flex-row gap-4">
|
||||
<div className="flex items-center">
|
||||
<svg width={8} height={8}>
|
||||
<rect width={8} height={8} rx={4} fill={java} />
|
||||
</svg>
|
||||
<Label1 noMargin>In</Label1>
|
||||
<Label1 noMargin className="ml-2">
|
||||
In
|
||||
</Label1>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center">
|
||||
<svg width={8} height={8}>
|
||||
<rect width={8} height={8} rx={4} fill={neon} />
|
||||
</svg>
|
||||
<Label1 noMargin>Out</Label1>
|
||||
<Label1 noMargin className="ml-2">
|
||||
Out
|
||||
</Label1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Scatterplot
|
||||
timeFrame={selectedRange}
|
||||
data={transactionsToShow}
|
||||
timezone={timezone}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container className={classes.commissionGraphContainer}>
|
||||
<Grid item xs={8}>
|
||||
<Label2 noMargin className={classes.commissionProfitTitle}>
|
||||
</div>
|
||||
<Scatterplot
|
||||
timeFrame={selectedRange}
|
||||
data={transactionsToShow}
|
||||
timezone={timezone}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex h-62">
|
||||
<div className="flex-2">
|
||||
<Label2 noMargin className="mb-4">
|
||||
Profit from commissions
|
||||
</Label2>
|
||||
<div className={classes.profitContainer}>
|
||||
<div className={classes.profitLabel}>
|
||||
<div className="flex justify-between mt-6 mr-7 -mb-8 ml-4 relative">
|
||||
<Info2 noMargin>
|
||||
{`${getProfit(transactionsToShow).toFormat(2)} ${
|
||||
data?.config.locale_fiatCurrency
|
||||
}`}
|
||||
</div>
|
||||
<div className={classnames(percentageClasses)}>
|
||||
</Info2>
|
||||
<Info2 noMargin className={classnames(percentageClasses)}>
|
||||
{getPercentageIcon()}
|
||||
{`${new BigNumber(percentChange).toFormat(2)}%`}
|
||||
</div>
|
||||
</Info2>
|
||||
</div>
|
||||
<LineChart
|
||||
timeFrame={selectedRange}
|
||||
|
|
@ -257,34 +245,36 @@ const SystemPerformance = () => {
|
|||
previousTimeData={transactionsLastTimePeriod}
|
||||
previousProfit={getProfit(transactionsLastTimePeriod)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Grid container className={classes.graphHeader}>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Label2 noMargin>Direction</Label2>
|
||||
<div className={classes.labelWrapper}>
|
||||
<div>
|
||||
<div className="flex flex-row gap-4">
|
||||
<div className="flex items-center">
|
||||
<svg width={8} height={8}>
|
||||
<rect width={8} height={8} rx={2} fill={java} />
|
||||
</svg>
|
||||
<Label1 noMargin>In</Label1>
|
||||
<Label1 noMargin className="ml-2">
|
||||
In
|
||||
</Label1>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center">
|
||||
<svg width={8} height={8}>
|
||||
<rect width={8} height={8} rx={2} fill={neon} />
|
||||
</svg>
|
||||
<Label1 noMargin>Out</Label1>
|
||||
<Label1 noMargin className="ml-2">
|
||||
Out
|
||||
</Label1>
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid item xs>
|
||||
<PercentageChart
|
||||
cashIn={getDirectionPercent().cashIn}
|
||||
cashOut={getDirectionPercent().cashOut}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
</div>
|
||||
<PercentageChart
|
||||
cashIn={getDirectionPercent().cashIn}
|
||||
cashOut={getDirectionPercent().cashOut}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,154 +0,0 @@
|
|||
import {
|
||||
offColor,
|
||||
offDarkColor,
|
||||
spacer,
|
||||
primaryColor,
|
||||
fontSize3,
|
||||
fontSecondary,
|
||||
fontColor,
|
||||
spring4,
|
||||
tomato,
|
||||
comet
|
||||
} from 'src/styling/variables'
|
||||
|
||||
const styles = {
|
||||
titleWrapper: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
titleAndButtonsContainer: {
|
||||
display: 'flex'
|
||||
},
|
||||
error: {
|
||||
marginLeft: 12
|
||||
},
|
||||
icon: {
|
||||
marginRight: 6
|
||||
},
|
||||
h4: {
|
||||
margin: 0,
|
||||
marginRight: spacer * 8
|
||||
},
|
||||
label: {
|
||||
cursor: 'pointer',
|
||||
minHeight: 0,
|
||||
minWidth: 0,
|
||||
padding: 0,
|
||||
color: offColor,
|
||||
textTransform: 'none',
|
||||
borderBottom: `2px solid transparent`,
|
||||
display: 'inline-block',
|
||||
lineHeight: 1.5,
|
||||
'&:hover': {
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
},
|
||||
newHighlightedLabel: {
|
||||
cursor: 'pointer',
|
||||
color: primaryColor,
|
||||
fontWeight: 700,
|
||||
borderRadius: 0,
|
||||
minHeight: 0,
|
||||
minWidth: 0,
|
||||
textTransform: 'none',
|
||||
borderBottom: `2px solid ${primaryColor}`,
|
||||
display: 'inline-block',
|
||||
lineHeight: 1.5,
|
||||
'&:hover': {
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
},
|
||||
navButton: {
|
||||
marginLeft: 24
|
||||
},
|
||||
navContainer: {
|
||||
display: 'flex'
|
||||
},
|
||||
percentUp: {
|
||||
fontSize: fontSize3,
|
||||
fontFamily: fontSecondary,
|
||||
fontWeight: 700,
|
||||
color: spring4,
|
||||
height: 10
|
||||
},
|
||||
percentDown: {
|
||||
fontSize: fontSize3,
|
||||
fontFamily: fontSecondary,
|
||||
fontWeight: 700,
|
||||
color: tomato,
|
||||
height: 13
|
||||
},
|
||||
percentNeutral: {
|
||||
fontSize: fontSize3,
|
||||
fontFamily: fontSecondary,
|
||||
fontWeight: 700,
|
||||
color: comet
|
||||
},
|
||||
profitContainer: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
margin: '23px 26px -30px 16px',
|
||||
position: 'relative'
|
||||
},
|
||||
profitLabel: {
|
||||
fontSize: fontSize3,
|
||||
fontFamily: fontSecondary,
|
||||
fontWeight: 700,
|
||||
color: fontColor
|
||||
},
|
||||
directionIcon: {
|
||||
width: 16,
|
||||
height: 16,
|
||||
marginBottom: -2,
|
||||
marginRight: 4
|
||||
},
|
||||
emptyTransactions: {
|
||||
paddingTop: 40
|
||||
},
|
||||
commissionProfitTitle: {
|
||||
marginBottom: 16
|
||||
},
|
||||
graphHeader: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 16
|
||||
},
|
||||
labelWrapper: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
'& > div': {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginLeft: 15,
|
||||
'&:first-child': {
|
||||
marginLeft: 0
|
||||
},
|
||||
'& > p': {
|
||||
marginLeft: 8
|
||||
}
|
||||
}
|
||||
},
|
||||
txGraphContainer: {
|
||||
height: 300,
|
||||
marginTop: 30
|
||||
},
|
||||
commissionsGraphContainer: {
|
||||
height: 250,
|
||||
marginTop: 30
|
||||
},
|
||||
verticalLine: {
|
||||
height: 15,
|
||||
width: 1,
|
||||
backgroundColor: offDarkColor,
|
||||
marginLeft: 31,
|
||||
marginRight: 16
|
||||
}
|
||||
}
|
||||
|
||||
export default styles
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
import { useQuery, gql } from "@apollo/client";
|
||||
import { makeStyles, withStyles } from '@mui/styles'
|
||||
import { useQuery, gql } from '@apollo/client'
|
||||
import { styled } from '@mui/material/styles'
|
||||
import Table from '@mui/material/Table'
|
||||
import TableBody from '@mui/material/TableBody'
|
||||
import TableCell from '@mui/material/TableCell'
|
||||
import TableContainer from '@mui/material/TableContainer'
|
||||
import TableHead from '@mui/material/TableHead'
|
||||
import TableRow from '@mui/material/TableRow'
|
||||
import classnames from 'classnames'
|
||||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
|
|
@ -15,11 +14,8 @@ import { Label2, TL2 } from 'src/components/typography'
|
|||
import TxOutIcon from 'src/styling/icons/direction/cash-out.svg?react'
|
||||
import MachineLinkIcon from 'src/styling/icons/month arrows/right.svg?react'
|
||||
|
||||
// import TxInIcon from 'src/styling/icons/direction/cash-in.svg?react'
|
||||
import { fromNamespace } from 'src/utils/config'
|
||||
|
||||
import styles from './MachinesTable.styles'
|
||||
|
||||
// percentage threshold where below this number the text in the cash cassettes percentage turns red
|
||||
const PERCENTAGE_THRESHOLD = 20
|
||||
|
||||
|
|
@ -29,27 +25,20 @@ const GET_CONFIG = gql`
|
|||
}
|
||||
`
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
const StyledCell = styled(TableCell)({
|
||||
borderBottom: '4px solid white',
|
||||
padding: 0,
|
||||
paddingLeft: '15px'
|
||||
})
|
||||
|
||||
const StyledCell = withStyles({
|
||||
root: {
|
||||
borderBottom: '4px solid white',
|
||||
padding: 0,
|
||||
paddingLeft: 15
|
||||
}
|
||||
})(TableCell)
|
||||
|
||||
const HeaderCell = withStyles({
|
||||
root: {
|
||||
borderBottom: '4px solid white',
|
||||
padding: 0,
|
||||
paddingLeft: 15,
|
||||
backgroundColor: 'white'
|
||||
}
|
||||
})(TableCell)
|
||||
const HeaderCell = styled(TableCell)({
|
||||
borderBottom: '4px solid white',
|
||||
padding: 0,
|
||||
paddingLeft: '15px',
|
||||
backgroundColor: 'white'
|
||||
})
|
||||
|
||||
const MachinesTable = ({ machines = [], numToRender }) => {
|
||||
const classes = useStyles()
|
||||
const history = useHistory()
|
||||
|
||||
const { data } = useQuery(GET_CONFIG)
|
||||
|
|
@ -69,7 +58,7 @@ const MachinesTable = ({ machines = [], numToRender }) => {
|
|||
R.defaultTo(PERCENTAGE_THRESHOLD)
|
||||
)(fillingPercentageSettings)
|
||||
return percent < percentageThreshold ? (
|
||||
<TL2 className={classes.error}>{`${percent}%`}</TL2>
|
||||
<TL2 className="text-tomato">{`${percent}%`}</TL2>
|
||||
) : (
|
||||
<TL2>{`${percent}%`}</TL2>
|
||||
)
|
||||
|
|
@ -87,30 +76,32 @@ const MachinesTable = ({ machines = [], numToRender }) => {
|
|||
)
|
||||
|
||||
return (
|
||||
<TableContainer className={classes.table}>
|
||||
<TableContainer className="max-h-110">
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<HeaderCell>
|
||||
<div className={classes.header}>
|
||||
<Label2 className={classes.label}>Machines</Label2>
|
||||
<div className="flex items-center">
|
||||
<Label2 noMargin className="text-comet">
|
||||
Machines
|
||||
</Label2>
|
||||
</div>
|
||||
</HeaderCell>
|
||||
<HeaderCell>
|
||||
<div className={`${classes.header} ${classes.statusHeader}`}>
|
||||
<Label2 className={classes.label}>Status</Label2>
|
||||
<div className="flex items-center">
|
||||
<Label2 noMargin className="text-comet">
|
||||
Status
|
||||
</Label2>
|
||||
</div>
|
||||
</HeaderCell>
|
||||
{/* <HeaderCell>
|
||||
<div className={classes.header}>
|
||||
<TxInIcon />
|
||||
</div>
|
||||
</HeaderCell> */}
|
||||
{R.times(R.identity, maxNumberOfCassettes).map((it, idx) => (
|
||||
<HeaderCell key={idx}>
|
||||
<div className={classes.header}>
|
||||
<div className="flex items-center whitespace-pre">
|
||||
<TxOutIcon />
|
||||
<Label2 className={classes.label}> {it + 1}</Label2>
|
||||
<Label2 noMargin className="text-comet">
|
||||
{' '}
|
||||
{it + 1}
|
||||
</Label2>
|
||||
</div>
|
||||
</HeaderCell>
|
||||
))}
|
||||
|
|
@ -122,20 +113,23 @@ const MachinesTable = ({ machines = [], numToRender }) => {
|
|||
return (
|
||||
<TableRow
|
||||
onClick={() => redirect(machine)}
|
||||
className={classnames(classes.row)}
|
||||
className="boder-b-0 bg-ghost"
|
||||
key={machine.deviceId + idx}>
|
||||
<StyledCell align="left">
|
||||
<div className={classes.machineNameWrapper}>
|
||||
<TableCell
|
||||
sx={{
|
||||
borderBottom: '4px solid white',
|
||||
padding: 0,
|
||||
paddingLeft: '15px'
|
||||
}}
|
||||
align="left">
|
||||
<div className="flex items-center">
|
||||
<TL2>{machine.name}</TL2>
|
||||
<MachineLinkIcon
|
||||
className={classnames(
|
||||
classes.machineRedirectIcon,
|
||||
classes.clickableRow
|
||||
)}
|
||||
className="cursor-pointer ml-2"
|
||||
onClick={() => redirect(machine)}
|
||||
/>
|
||||
</div>
|
||||
</StyledCell>
|
||||
</TableCell>
|
||||
<StyledCell>
|
||||
<Status status={machine.statuses[0]} />
|
||||
</StyledCell>
|
||||
|
|
|
|||
|
|
@ -6,10 +6,6 @@ import {
|
|||
} from 'src/styling/variables'
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
label: {
|
||||
margin: 0,
|
||||
color: offColor
|
||||
|
|
@ -45,30 +41,12 @@ const styles = {
|
|||
bottom: 160,
|
||||
marginBottom: 0
|
||||
},
|
||||
upperButtonLabel: {
|
||||
textAlign: 'center',
|
||||
marginBottom: 0,
|
||||
marginTop: 0
|
||||
},
|
||||
statusHeader: {
|
||||
marginLeft: 2
|
||||
},
|
||||
table: {
|
||||
maxHeight: 440,
|
||||
'&::-webkit-scrollbar': {
|
||||
width: 7
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor: offColor,
|
||||
borderRadius: 5
|
||||
}
|
||||
},
|
||||
tableBody: {
|
||||
overflow: 'auto'
|
||||
},
|
||||
h4: {
|
||||
marginTop: 0
|
||||
},
|
||||
tl2: {
|
||||
display: 'inline'
|
||||
},
|
||||
|
|
@ -76,11 +54,9 @@ const styles = {
|
|||
display: 'inline'
|
||||
},
|
||||
machinesTableContainer: {
|
||||
marginTop: 10,
|
||||
height: 220
|
||||
},
|
||||
expandedMachinesTableContainer: {
|
||||
marginTop: 10,
|
||||
height: 414
|
||||
},
|
||||
centerLabel: {
|
||||
|
|
|
|||
|
|
@ -1,18 +1,12 @@
|
|||
import { useQuery, gql } from "@apollo/client";
|
||||
import { useQuery, gql } from '@apollo/client'
|
||||
import Button from '@mui/material/Button'
|
||||
import Grid from '@mui/material/Grid'
|
||||
import { makeStyles } from '@mui/styles'
|
||||
import classnames from 'classnames'
|
||||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
import { cardState as cardState_ } from 'src/components/CollapsibleCard'
|
||||
// import ActionButton from 'src/components/buttons/ActionButton'
|
||||
import { H4, TL2, Label1 } from 'src/components/typography'
|
||||
|
||||
import MachinesTable from './MachinesTable'
|
||||
import styles from './MachinesTable.styles'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
// number of machines in the table to render on page load
|
||||
const NUM_TO_RENDER = 4
|
||||
|
|
@ -59,29 +53,28 @@ const GET_DATA = gql`
|
|||
} */
|
||||
|
||||
const SystemStatus = ({ onReset, onExpand, size }) => {
|
||||
const classes = useStyles()
|
||||
const { data, loading } = useQuery(GET_DATA)
|
||||
|
||||
const machines = R.path(['machines'])(data) ?? []
|
||||
const showAllItems = size === cardState_.EXPANDED
|
||||
|
||||
const machinesTableContainerClasses = {
|
||||
[classes.machinesTableContainer]: !showAllItems,
|
||||
[classes.expandedMachinesTableContainer]: showAllItems
|
||||
'h-55': !showAllItems,
|
||||
'h-103': showAllItems
|
||||
}
|
||||
// const uptime = data?.uptime ?? [{}]
|
||||
return (
|
||||
<>
|
||||
<div className={classes.container}>
|
||||
<H4 className={classes.h4}>System status</H4>{' '}
|
||||
<div className="flex justify-between">
|
||||
<H4 className="mt-0">System status</H4>
|
||||
{showAllItems && (
|
||||
<Label1 className={classes.upperButtonLabel}>
|
||||
<Label1 noMargin className="-mt-1">
|
||||
<Button
|
||||
onClick={onReset}
|
||||
size="small"
|
||||
disableRipple
|
||||
disableFocusRipple
|
||||
className={classes.button}>
|
||||
className="p-0 text-zodiac normal-case">
|
||||
{'Show less'}
|
||||
</Button>
|
||||
</Label1>
|
||||
|
|
@ -89,54 +82,29 @@ const SystemStatus = ({ onReset, onExpand, size }) => {
|
|||
</div>
|
||||
{!loading && (
|
||||
<>
|
||||
<Grid container spacing={1}>
|
||||
{/*
|
||||
On hold until system uptime is implemented
|
||||
<Grid item xs={4}>
|
||||
<TL2 className={classes.tl2}>
|
||||
{parseUptime(uptime[0].time)}
|
||||
</TL2>
|
||||
<Label1 className={classes.label1}> System up time</Label1>
|
||||
</Grid> */}
|
||||
<Grid item xs={4}>
|
||||
<TL2 className={classes.tl2}>{data?.serverVersion}</TL2>
|
||||
<Label1 className={classes.label1}> server version</Label1>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
{/*
|
||||
On hold until system update features are implemented
|
||||
<ActionButton
|
||||
color="primary"
|
||||
className={classes.actionButton}
|
||||
onClick={() => console.log('Upgrade button clicked')}>
|
||||
Update to v10.6.0
|
||||
</ActionButton> */}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
className={classnames(machinesTableContainerClasses)}>
|
||||
<Grid item xs={12}>
|
||||
<MachinesTable
|
||||
numToRender={showAllItems ? Infinity : NUM_TO_RENDER}
|
||||
machines={machines}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<div className="mb-4">
|
||||
<TL2 className="inline">{data?.serverVersion}</TL2>
|
||||
<Label1 className="inline"> server version</Label1>
|
||||
</div>
|
||||
<div className={classnames(machinesTableContainerClasses)}>
|
||||
<MachinesTable
|
||||
numToRender={showAllItems ? Infinity : NUM_TO_RENDER}
|
||||
machines={machines}
|
||||
/>
|
||||
</div>
|
||||
{!showAllItems && machines.length > NUM_TO_RENDER && (
|
||||
<Grid item xs={12}>
|
||||
<Label1 className={classes.centerLabel}>
|
||||
<div>
|
||||
<Label1 className="text-center mb-0">
|
||||
<Button
|
||||
onClick={() => onExpand()}
|
||||
size="small"
|
||||
disableRipple
|
||||
disableFocusRipple
|
||||
className={classes.button}>
|
||||
className="p-0 text-zodiac normal-case">
|
||||
{`Show all (${machines.length})`}
|
||||
</Button>
|
||||
</Label1>
|
||||
</Grid>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { useQuery, useMutation, gql } from "@apollo/client";
|
||||
import { makeStyles } from '@mui/styles'
|
||||
import { useQuery, useMutation, gql } from '@apollo/client'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
import Modal from 'src/components/Modal'
|
||||
|
|
@ -15,7 +14,6 @@ import { Link, SupportLinkButton } from 'src/components/buttons'
|
|||
import { Table as EditableTable } from 'src/components/editableTable'
|
||||
import { fromNamespace, toNamespace, namespaces } from 'src/utils/config'
|
||||
|
||||
import { styles } from './Locales.styles'
|
||||
import {
|
||||
mainFields,
|
||||
overrides,
|
||||
|
|
@ -25,8 +23,6 @@ import {
|
|||
overridesDefaults
|
||||
} from './helper'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const GET_DATA = gql`
|
||||
query getData {
|
||||
config
|
||||
|
|
@ -75,8 +71,6 @@ const GET_MARKETS = gql`
|
|||
`
|
||||
|
||||
const FiatCurrencyChangeAlert = ({ open, close, save }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={'Change fiat currency?'}
|
||||
|
|
@ -94,11 +88,11 @@ const FiatCurrencyChangeAlert = ({ open, close, save }) => {
|
|||
Also, if you have cash-out enabled, you must define new dispenser bill
|
||||
counts for the new currency for cash-out on the new currency to work.
|
||||
</P>
|
||||
<div className={classes.rightAligned}>
|
||||
<div className="ml-auto">
|
||||
<Link onClick={close} color="secondary">
|
||||
Cancel
|
||||
</Link>
|
||||
<Link className={classes.rightLink} onClick={save} color="primary">
|
||||
<Link className="ml-5" onClick={save} color="primary">
|
||||
Save
|
||||
</Link>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
const styles = {
|
||||
rightAligned: {
|
||||
marginTop: '20px',
|
||||
marginLeft: 'auto',
|
||||
marginBottom: '20px'
|
||||
},
|
||||
rightLink: {
|
||||
marginLeft: '20px'
|
||||
}
|
||||
}
|
||||
|
||||
export { styles }
|
||||
|
|
@ -22,6 +22,7 @@
|
|||
|
||||
--ghost: #fafbff;
|
||||
--zircon: #ebefff;
|
||||
--zircon2: #dbdfed;
|
||||
|
||||
--java: #16d6d3;
|
||||
--neon: #5a67ff;
|
||||
|
|
@ -45,6 +46,8 @@
|
|||
--color-tomato: var(--tomato);
|
||||
--color-ghost: var(--ghost);
|
||||
--color-zircon: var(--zircon);
|
||||
--color-zircon2: var(--zircon2);
|
||||
--color-java: var(--java);
|
||||
--color-neon: var(--neon);
|
||||
--color-malachite: var(--malachite);
|
||||
--color-orange-yellow: var(--orange-yellow);
|
||||
|
|
|
|||
|
|
@ -151,6 +151,13 @@ theme = createTheme(theme, {
|
|||
}
|
||||
}
|
||||
},
|
||||
MuiPaper: {
|
||||
styleOverrides: {
|
||||
elevation1: {
|
||||
boxShadow: '0 0 4px 0 rgba(0, 0, 0, 0.08)'
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiChip: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue