feat: update design of the accounting page (#209)

* feat: update design of the accounting page, fixed the worsed graphical offenders

* chore: button alignment

* chore: removed unused dependency

* chore: buttons are underneath the action

* feat: refactored the peers table to be in line with the new design

* feat: add total uncashed amount and sorting for the peers

* feat: action buttons are now properly aligned

* chore: typo in comment
This commit is contained in:
Vojtech Simetka
2021-10-05 12:59:08 +02:00
committed by GitHub
parent e7188f4a35
commit ecbc116475
12 changed files with 183 additions and 234 deletions
+3 -2
View File
@@ -7,6 +7,7 @@ import DialogContentText from '@material-ui/core/DialogContentText'
import DialogTitle from '@material-ui/core/DialogTitle'
import { useSnackbar } from 'notistack'
import { ReactElement, useState, useContext } from 'react'
import { Zap } from 'react-feather'
import { Context as SettingsContext } from '../providers/Settings'
import EthereumAddress from './EthereumAddress'
@@ -59,8 +60,8 @@ export default function CheckoutModal({ peerId, uncashedAmount }: Props): ReactE
return (
<div>
<Button variant="contained" color="primary" onClick={handleClickOpen} style={{ marginLeft: '7px' }}>
Cashout
<Button variant="contained" onClick={handleClickOpen} startIcon={<Zap size="1rem" />}>
Cash out peer {peerId.substr(0, 8)}[]
</Button>
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">Cashout Cheque</DialogTitle>
+21 -4
View File
@@ -19,20 +19,27 @@ const useStyles = makeStyles((theme: Theme) =>
header: {
backgroundColor: theme.palette.background.paper,
},
content: {
contentLevel0: {
marginTop: theme.spacing(1),
},
contentLevel12: {
marginTop: theme.spacing(0.25),
},
infoText: {
color: '#c9c9c9',
},
}),
)
interface Props {
children?: ReactNode
label: ReactNode
info?: ReactNode
level?: 0 | 1 | 2
defaultOpen?: boolean
}
export default function ExpandableList({ children, label, level, defaultOpen }: Props): ReactElement | null {
export default function ExpandableList({ children, label, level, defaultOpen, info }: Props): ReactElement | null {
const classes = useStyles()
const [open, setOpen] = useState<boolean>(Boolean(defaultOpen))
@@ -42,23 +49,33 @@ export default function ExpandableList({ children, label, level, defaultOpen }:
let rootLevelClass = ''
let typographyVariant: 'h1' | 'h2' | 'h3' = 'h1'
let contentLevelClass = classes.contentLevel0
if (level === 1) {
rootLevelClass = classes.rootLevel1
typographyVariant = 'h2'
contentLevelClass = classes.contentLevel12
} else if (level === 2) {
rootLevelClass = classes.rootLevel2
typographyVariant = 'h3'
contentLevelClass = classes.contentLevel12
}
return (
<div className={`${classes.root} ${rootLevelClass}`}>
<ListItem button onClick={handleClick} className={classes.header}>
<ListItemText primary={<Typography variant={typographyVariant}>{label}</Typography>} />
{open ? <ExpandLess /> : <ExpandMore />}
<div style={{ display: 'flex' }}>
{!open && (
<Typography variant="body2" className={classes.infoText}>
{info}
</Typography>
)}
{open ? <ExpandLess /> : <ExpandMore />}
</div>
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
<div className={classes.content}>{children}</div>
<div className={contentLevelClass}>{children}</div>
</Collapse>
</div>
)
@@ -0,0 +1,38 @@
import { ReactElement, ReactNode } from 'react'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import { Grid } from '@material-ui/core'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
action: {
marginTop: theme.spacing(0.75),
marginRight: theme.spacing(1),
},
}),
)
interface Props {
children: ReactNode | ReactNode[]
}
export default function ExpandableListItemActions({ children }: Props): ReactElement | null {
const classes = useStyles()
if (Array.isArray(children)) {
return (
<Grid container direction="row">
{children.map((a, i) => (
<Grid key={i} className={classes.action}>
{a}
</Grid>
))}
</Grid>
)
}
return (
<Grid container direction="row">
<Grid className={classes.action}>{children}</Grid>
</Grid>
)
}
+6 -3
View File
@@ -1,4 +1,4 @@
import { ReactElement, useState } from 'react'
import { ReactElement, ReactNode, useState } from 'react'
import Button from '@material-ui/core/Button'
import Input from '@material-ui/core/Input'
import Dialog from '@material-ui/core/Dialog'
@@ -19,6 +19,7 @@ interface Props {
max?: BigNumber
min?: BigNumber
action: (amount: bigint) => Promise<string>
icon?: ReactNode
}
export default function WithdrawDepositModal({
@@ -29,6 +30,7 @@ export default function WithdrawDepositModal({
max,
label,
action,
icon,
}: Props): ReactElement {
const [open, setOpen] = useState(false)
const [amount, setAmount] = useState('')
@@ -36,8 +38,9 @@ export default function WithdrawDepositModal({
const [amountError, setAmountError] = useState<Error | null>(null)
const { enqueueSnackbar } = useSnackbar()
const handleClickOpen = () => {
const handleClickOpen = (e: React.MouseEvent<HTMLButtonElement>) => {
setOpen(true)
e.stopPropagation()
}
const handleClose = () => {
@@ -74,7 +77,7 @@ export default function WithdrawDepositModal({
return (
<div>
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
<Button variant="contained" onClick={handleClickOpen} startIcon={icon}>
{label}
</Button>
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title">
+2
View File
@@ -1,4 +1,5 @@
import { ReactElement, useContext } from 'react'
import { Download } from 'react-feather'
import { Context as SettingsContext } from '../providers/Settings'
import WithdrawDepositModal from '../components/WithdrawDepositModal'
@@ -13,6 +14,7 @@ export default function DepositModal(): ReactElement {
errorMessage="Error with depositing"
dialogMessage="Specify the amount of BZZ you would like to withdraw from your node."
label="Deposit"
icon={<Download size="1rem" />}
min={new BigNumber(0)}
action={(amount: bigint) => {
if (!beeDebugApi) throw new Error('Bee Debug URL is not valid')
+2
View File
@@ -1,4 +1,5 @@
import { ReactElement, useContext } from 'react'
import { Upload } from 'react-feather'
import { Context as SettingsContext } from '../providers/Settings'
import WithdrawDepositModal from '../components/WithdrawDepositModal'
@@ -13,6 +14,7 @@ export default function WithdrawModal(): ReactElement {
errorMessage="Error with withdrawing."
dialogMessage="Specify the amount of BZZ you would like to withdraw from your node."
label="Withdraw"
icon={<Upload size="1rem" />}
min={new BigNumber(0)}
action={(amount: bigint) => {
if (!beeDebugApi) throw new Error('Bee Debug URL is not valid')
+17 -5
View File
@@ -6,6 +6,7 @@ import { Balance, Settlements, Settlement } from '../types'
interface UseAccountingHook {
isLoadingUncashed: boolean
totalUncashed: Token
accounting: Accounting[] | null
}
@@ -60,16 +61,21 @@ function mergeAccounting(
}),
)
// If there are no cheques (and hence last cashout actions), we don't need to sort and can return values right away
if (!uncashedAmounts) return Object.values(accounting)
// If there are no cheques (and hence last cashout actions)
if (!uncashedAmounts) return Object.values(accounting).sort((a, b) => (a.peer < b.peer ? -1 : 1))
uncashedAmounts?.forEach(({ peer, uncashedAmount }) => {
accounting[peer].uncashedAmount = new Token(uncashedAmount)
})
return Object.values(accounting).sort((a, b) =>
b.uncashedAmount.toBigNumber.minus(a.uncashedAmount.toBigNumber).toNumber(),
)
// Return sorted by the uncashed amount first and then by the peer id
return Object.values(accounting).sort((a, b) => {
const diff = b.uncashedAmount.toBigNumber.minus(a.uncashedAmount.toBigNumber).toNumber()
if (diff !== 0) return diff
return a.peer < b.peer ? -1 : 1
})
}
export const useAccounting = (
@@ -98,8 +104,14 @@ export const useAccounting = (
const accounting = mergeAccounting(balances, settlements?.settlements, uncashedAmounts)
let totalUncashed: Token = new Token('0')
accounting?.forEach(
({ uncashedAmount }) => (totalUncashed = new Token(totalUncashed.toBigNumber.plus(uncashedAmount.toBigNumber))),
)
return {
isLoadingUncashed,
totalUncashed,
accounting,
}
}
-94
View File
@@ -1,94 +0,0 @@
import { ReactElement } from 'react'
import { createStyles, makeStyles } from '@material-ui/core/styles'
import { Card, CardContent, Typography, Theme } from '@material-ui/core/'
import WithdrawModal from '../../containers/WithdrawModal'
import DepositModal from '../../containers/DepositModal'
import type { ChequebookAddressResponse } from '@ethersphere/bee-js'
import { Token } from '../../models/Token'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
display: 'flex',
},
buttons: {
display: 'flex',
columnGap: theme.spacing(1),
},
gridContainer: {
display: 'flex',
width: '100%',
marginLeft: theme.spacing(1),
marginRight: theme.spacing(1),
columnGap: theme.spacing(1),
rowGap: theme.spacing(1),
flex: '0 1 auto',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
chequebookActions: {
justifyContent: 'space-between',
display: 'flex',
marginBottom: theme.spacing(1),
},
}),
)
interface ChequebookBalance {
totalBalance: Token
availableBalance: Token
}
interface Props {
chequebookAddress: ChequebookAddressResponse | null
chequebookBalance: ChequebookBalance | null
totalsent?: Token
totalreceived?: Token
}
function AccountCard({ totalreceived, totalsent, chequebookBalance }: Props): ReactElement {
const classes = useStyles()
return (
<div>
<div className={classes.chequebookActions}>
<Typography component="h2" variant="h6">
Chequebook
</Typography>
<div className={classes.buttons}>
<WithdrawModal />
<DepositModal />
</div>
</div>
<Card className={classes.root}>
<CardContent className={classes.gridContainer}>
<div>
<Typography component="h2" variant="h6" color="primary" gutterBottom>
Total Balance
</Typography>
<Typography variant="h5">{chequebookBalance?.totalBalance.toFixedDecimal()} BZZ</Typography>
</div>
<div>
<Typography component="h2" variant="h6" color="primary" gutterBottom>
Available Uncommitted Balance
</Typography>
<Typography variant="h5">{chequebookBalance?.availableBalance.toFixedDecimal()} BZZ</Typography>
</div>
<div>
<Typography component="h2" variant="h6" color="primary" gutterBottom>
Total Sent / Received
</Typography>
<Typography variant="h5">
{totalsent?.toFixedDecimal()} / {totalreceived?.toFixedDecimal()} BZZ
</Typography>
</div>
</CardContent>
</Card>
</div>
)
}
export default AccountCard
-94
View File
@@ -1,94 +0,0 @@
import type { ReactElement } from 'react'
import { makeStyles } from '@material-ui/core/styles'
import { Table, TableBody, TableCell, TableContainer, TableRow, TableHead, Paper } from '@material-ui/core'
import ClipboardCopy from '../../components/ClipboardCopy'
import CashoutModal from '../../components/CashoutModal'
import PeerDetailDrawer from '../../components/PeerDetail'
import { Accounting } from '../../hooks/accounting'
const useStyles = makeStyles({
table: {
minWidth: 650,
},
values: {
textAlign: 'right',
fontFamily: 'monospace, monospace',
},
})
interface Props {
isLoadingUncashed: boolean
accounting: Accounting[] | null
}
function BalancesTable({ accounting, isLoadingUncashed }: Props): ReactElement | null {
if (accounting === null) return null
const classes = useStyles()
return (
<TableContainer component={Paper}>
<Table className={classes.table} size="small" aria-label="Balances Table">
<TableHead>
<TableRow>
<TableCell>Peer</TableCell>
<TableCell align="right">Outstanding Balance</TableCell>
<TableCell align="right">Settlements Sent / Received</TableCell>
<TableCell align="right">Total</TableCell>
<TableCell align="right">Uncashed Amount</TableCell>
<TableCell />
</TableRow>
</TableHead>
<TableBody>
{accounting.map(({ peer, balance, received, sent, uncashedAmount, total }) => (
<TableRow key={peer}>
<TableCell>
<div style={{ display: 'flex' }}>
<small>
<PeerDetailDrawer peerId={peer} />
</small>
<ClipboardCopy value={peer} />
</div>
</TableCell>
<TableCell className={classes.values}>
<span
style={{
color: balance.toBigNumber.isPositive() ? '#32c48d' : '#c9201f',
}}
>
{balance.toFixedDecimal()}
</span>{' '}
BZZ
</TableCell>
<TableCell className={classes.values}>
-{sent.toFixedDecimal()} / {received.toFixedDecimal()} BZZ
</TableCell>
<TableCell className={classes.values}>
<span
style={{
color: total.toBigNumber.isPositive() ? '#32c48d' : '#c9201f',
}}
>
{total.toFixedDecimal()}
</span>{' '}
BZZ
</TableCell>
<TableCell className={classes.values}>
{isLoadingUncashed && 'loading...'}
{!isLoadingUncashed && (
<>{uncashedAmount.toBigNumber.isGreaterThan('0') ? uncashedAmount.toFixedDecimal() : '0'} BZZ</>
)}
</TableCell>
<TableCell className={classes.values}>
{uncashedAmount.toBigNumber.isGreaterThan('0') && (
<CashoutModal uncashedAmount={uncashedAmount.toFixedDecimal()} peerId={peer} />
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)
}
export default BalancesTable
+52
View File
@@ -0,0 +1,52 @@
import type { ReactElement } from 'react'
import ExpandableList from '../../components/ExpandableList'
import ExpandableListItem from '../../components/ExpandableListItem'
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
import ExpandableListItemKey from '../../components/ExpandableListItemKey'
import CashoutModal from '../../components/CashoutModal'
import { Accounting } from '../../hooks/accounting'
import type { Token } from '../../models/Token'
interface Props {
isLoadingUncashed: boolean
totalUncashed: Token
accounting: Accounting[] | null
}
export default function PeerBalances({ accounting, isLoadingUncashed, totalUncashed }: Props): ReactElement | null {
return (
<ExpandableList
label={`Peers (${accounting?.length || 0})`}
info={`${totalUncashed.toFixedDecimal()} BZZ (uncashed)`}
>
<ExpandableListItem label="Uncashed Amount Total" value={`${totalUncashed.toFixedDecimal()} BZZ`} />
{accounting?.map(({ peer, balance, received, sent, uncashedAmount, total }) => (
<ExpandableList
key={peer}
label={`Peer ${peer.substr(0, 8)}[…]`}
level={1}
info={`${uncashedAmount.toFixedDecimal()} BZZ (uncashed)`}
>
<ExpandableListItemKey label="Peer ID" value={peer} />
<ExpandableListItem label="Outstanding Balance" value={`${balance.toFixedDecimal()} BZZ`} />
<ExpandableListItem
label="Settlements Sent / Received"
value={`-${sent.toFixedDecimal()} / ${received.toFixedDecimal()} BZZ`}
/>
<ExpandableListItem label="Total" value={`${total.toFixedDecimal()} BZZ`} />
<ExpandableListItem
label="Uncashed Amount"
value={isLoadingUncashed ? 'loading…' : `${uncashedAmount.toFixedDecimal()} BZZ`}
/>
{uncashedAmount.toBigNumber.isGreaterThan('0') && (
<ExpandableListItemActions>
<CashoutModal uncashedAmount={uncashedAmount.toFixedDecimal()} peerId={peer} />
</ExpandableListItemActions>
)}
</ExpandableList>
))}
</ExpandableList>
)
}
+33 -26
View File
@@ -1,45 +1,52 @@
import { ReactElement, useContext } from 'react'
import { Theme, createStyles, makeStyles } from '@material-ui/core/styles'
import AccountCard from '../accounting/AccountCard'
import BalancesTable from './BalancesTable'
import EthereumAddressCard from '../../components/EthereumAddressCard'
import PeerBalances from './PeerBalances'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { Context as BeeContext } from '../../providers/Bee'
import { Context as SettingsContext } from '../../providers/Settings'
import { useAccounting } from '../../hooks/accounting'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
width: '100%',
display: 'grid',
rowGap: theme.spacing(3),
},
}),
)
import ExpandableList from '../../components/ExpandableList'
import ExpandableListItem from '../../components/ExpandableListItem'
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
import ExpandableListItemKey from '../../components/ExpandableListItemKey'
import WithdrawModal from '../../containers/WithdrawModal'
import DepositModal from '../../containers/DepositModal'
export default function Accounting(): ReactElement {
const classes = useStyles()
const { status, nodeAddresses, chequebookAddress, chequebookBalance, settlements, peerBalances } =
useContext(BeeContext)
const { beeDebugApi } = useContext(SettingsContext)
const { accounting, isLoadingUncashed } = useAccounting(beeDebugApi, settlements, peerBalances)
const { accounting, totalUncashed, isLoadingUncashed } = useAccounting(beeDebugApi, settlements, peerBalances)
if (!status.all) return <TroubleshootConnectionCard />
return (
<div className={classes.root}>
<AccountCard
chequebookAddress={chequebookAddress}
chequebookBalance={chequebookBalance}
totalsent={settlements?.totalSent}
totalreceived={settlements?.totalReceived}
/>
<EthereumAddressCard nodeAddresses={nodeAddresses} chequebookAddress={chequebookAddress} />
<BalancesTable accounting={accounting} isLoadingUncashed={isLoadingUncashed} />
<div>
<ExpandableList label="Chequebook" defaultOpen>
<ExpandableListItem label="Total Balance" value={`${chequebookBalance?.totalBalance.toFixedDecimal()} BZZ`} />
<ExpandableListItem
label="Available Uncommitted Balance"
value={`${chequebookBalance?.availableBalance.toFixedDecimal()} BZZ`}
/>
<ExpandableListItem
label="Total Cheques Amount Sent"
value={`${settlements?.totalSent.toFixedDecimal()} BZZ`}
/>
<ExpandableListItem
label="Total Cheques Amount Received"
value={`${settlements?.totalReceived.toFixedDecimal()} BZZ`}
/>
<ExpandableListItemActions>
<WithdrawModal />
<DepositModal />
</ExpandableListItemActions>
</ExpandableList>
<ExpandableList label="Blockchain" defaultOpen>
<ExpandableListItemKey label="Ethereum address" value={nodeAddresses?.ethereum || ''} />
<ExpandableListItemKey label="Chequebook contract address" value={chequebookAddress?.chequebookAddress || ''} />
</ExpandableList>
<PeerBalances accounting={accounting} isLoadingUncashed={isLoadingUncashed} totalUncashed={totalUncashed} />
</div>
)
}
+9 -6
View File
@@ -18,13 +18,13 @@ const componentsOverrides = (theme: Theme) => ({
maxWidthXl: { padding: theme.spacing(8) },
},
MuiButton: {
textSizeLarge: {
padding: theme.spacing(2),
},
label: { margin: theme.spacing(2) },
startIcon: { marginLeft: theme.spacing(1) },
endIcon: { marginRight: theme.spacing(1) },
containedSizeLarge: {
padding: theme.spacing(2),
boxShadow: 'none',
padding: 0,
borderRadius: 0,
boxShadow: 'none',
'&:hover': {
backgroundColor: theme.palette.primary.main,
color: 'white',
@@ -40,9 +40,12 @@ const componentsOverrides = (theme: Theme) => ({
contained: {
backgroundColor: 'white',
boxShadow: 'none',
padding: 0,
borderRadius: 0,
'&:hover': {
backgroundColor: theme.palette.primary.main,
color: 'white',
boxShadow: 'none',
// https://github.com/mui-org/material-ui/issues/22543
'@media (hover: none)': {
backgroundColor: theme.palette.primary.main,
@@ -116,7 +119,7 @@ export const theme = createMuiTheme({
default: '#efefef',
},
primary: {
light: orange.A200,
light: '#fcf2e8',
main: '#dd7700',
dark: orange[800],
},