feat: add account tabs (#378)

* feat: add account tabs

* chore: bump

* refactor: change network tab for old accounting

* feat: new fonts and text transformation, chequebook tab

* feat: polishing
This commit is contained in:
Cafe137
2022-06-17 11:39:56 +02:00
committed by GitHub
parent 199516d60c
commit 41432bc346
46 changed files with 507 additions and 201 deletions
+55
View File
@@ -0,0 +1,55 @@
import { createStyles, makeStyles, Tab, Tabs, Theme } from '@material-ui/core'
import { ReactElement } from 'react'
import { useNavigate } from 'react-router-dom'
import { ACCOUNT_TABS } from '../../routes'
const tabMap = {
WALLET: 0,
CHEQUEBOOK: 1,
STAMPS: 2,
FEEDS: 3,
}
interface Props {
active: 'WALLET' | 'CHEQUEBOOK' | 'STAMPS' | 'FEEDS'
}
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
flexGrow: 1,
marginBottom: theme.spacing(4),
textTransform: 'none',
},
leftTab: {
marginRight: theme.spacing(0.125),
},
centerTab: {
marginLeft: theme.spacing(0.125),
marginRight: theme.spacing(0.125),
},
rightTab: {
marginLeft: theme.spacing(0.125),
},
}),
)
export function AccountNavigation({ active }: Props): ReactElement {
const classes = useStyles()
const navigate = useNavigate()
function onChange(event: React.ChangeEvent<Record<string, never>>, newValue: number) {
navigate(ACCOUNT_TABS[newValue])
}
return (
<div className={classes.root}>
<Tabs value={tabMap[active]} onChange={onChange} variant="fullWidth">
<Tab className={classes.leftTab} key="WALLET" label="Wallet" />
<Tab className={classes.centerTab} key="CHEQUEBOOK" label="Chequebook" />
<Tab className={classes.centerTab} key="STAMPS" label="Stamps" />
<Tab className={classes.rightTab} key="FEEDS" label="Feeds" />
</Tabs>
</div>
)
}
+10
View File
@@ -0,0 +1,10 @@
import { Box, Typography } from '@material-ui/core'
import { ReactElement } from 'react'
export function Header(): ReactElement {
return (
<Box mb={4}>
<Typography variant="h1">Account</Typography>
</Box>
)
}
@@ -0,0 +1,60 @@
import { ReactElement, useContext } from 'react'
import ExpandableList from '../../../components/ExpandableList'
import ExpandableListItem from '../../../components/ExpandableListItem'
import ExpandableListItemActions from '../../../components/ExpandableListItemActions'
import ExpandableListItemKey from '../../../components/ExpandableListItemKey'
import TroubleshootConnectionCard from '../../../components/TroubleshootConnectionCard'
import DepositModal from '../../../containers/DepositModal'
import WithdrawModal from '../../../containers/WithdrawModal'
import { useAccounting } from '../../../hooks/accounting'
import { CheckState, Context as BeeContext } from '../../../providers/Bee'
import { Context as SettingsContext } from '../../../providers/Settings'
import PeerBalances from '../../accounting/PeerBalances'
import { AccountNavigation } from '../AccountNavigation'
import { Header } from '../Header'
export function AccountChequebook(): ReactElement {
const { status, nodeAddresses, chequebookAddress, chequebookBalance, settlements, peerBalances } =
useContext(BeeContext)
const { beeDebugApi } = useContext(SettingsContext)
const { accounting, totalUncashed, isLoadingUncashed } = useAccounting(beeDebugApi, settlements, peerBalances)
if (status.all === CheckState.ERROR) return <TroubleshootConnectionCard />
return (
<>
<Header />
<AccountNavigation active="CHEQUEBOOK" />
<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>
</>
)
}
+112
View File
@@ -0,0 +1,112 @@
import { Box } from '@material-ui/core'
import { ReactElement, useContext, useState } from 'react'
import { Download, Info, PlusSquare, Trash } from 'react-feather'
import { useNavigate } from 'react-router'
import ExpandableList from '../../../components/ExpandableList'
import ExpandableListItem from '../../../components/ExpandableListItem'
import ExpandableListItemActions from '../../../components/ExpandableListItemActions'
import ExpandableListItemKey from '../../../components/ExpandableListItemKey'
import { SwarmButton } from '../../../components/SwarmButton'
import { Context as IdentityContext, Identity } from '../../../providers/Feeds'
import { ROUTES } from '../../../routes'
import { formatEnum } from '../../../utils'
import { persistIdentitiesWithoutUpdate } from '../../../utils/identity'
import { DeleteFeedDialog } from '../../feeds/DeleteFeedDialog'
import { ExportFeedDialog } from '../../feeds/ExportFeedDialog'
import { ImportFeedDialog } from '../../feeds/ImportFeedDialog'
import { AccountNavigation } from '../AccountNavigation'
import { Header } from '../Header'
export function AccountFeeds(): ReactElement {
const { identities, setIdentities } = useContext(IdentityContext)
const navigate = useNavigate()
const [selectedIdentity, setSelectedIdentity] = useState<Identity | null>(null)
const [showImport, setShowImport] = useState(false)
const [showExport, setShowExport] = useState(false)
const [showDelete, setShowDelete] = useState(false)
function createNewFeed() {
return navigate(ROUTES.ACCOUNT_FEEDS_NEW)
}
function viewFeed(uuid: string) {
navigate(ROUTES.ACCOUNT_FEEDS_VIEW.replace(':uuid', uuid))
}
function onDialogClose() {
setShowDelete(false)
setShowExport(false)
setShowImport(false)
setSelectedIdentity(null)
}
function onDelete(identity: Identity) {
onDialogClose()
const updatedFeeds = identities.filter(x => x.uuid !== identity.uuid)
setIdentities(updatedFeeds)
persistIdentitiesWithoutUpdate(updatedFeeds)
}
function onShowExport(identity: Identity) {
setSelectedIdentity(identity)
setShowExport(true)
}
function onShowDelete(identity: Identity) {
setSelectedIdentity(identity)
setShowDelete(true)
}
return (
<>
<Header />
<AccountNavigation active="FEEDS" />
{showImport && <ImportFeedDialog onClose={() => setShowImport(false)} />}
{showExport && selectedIdentity && <ExportFeedDialog identity={selectedIdentity} onClose={onDialogClose} />}
{showDelete && selectedIdentity && (
<DeleteFeedDialog
identity={selectedIdentity}
onClose={onDialogClose}
onConfirm={(identity: Identity) => onDelete(identity)}
/>
)}
<Box mb={4}>
<ExpandableListItemActions>
<SwarmButton iconType={PlusSquare} onClick={createNewFeed}>
Create New Feed
</SwarmButton>
<SwarmButton iconType={PlusSquare} onClick={() => setShowImport(true)}>
Import Feed
</SwarmButton>
</ExpandableListItemActions>
</Box>
{identities.map((x, i) => (
<ExpandableList key={i} label={`${x.name} Website`} defaultOpen>
<Box mb={0.5}>
<ExpandableList label={x.name} level={1}>
<ExpandableListItemKey label="Identity address" value={x.address} />
<ExpandableListItem label="Identity type" value={formatEnum(x.type)} />
</ExpandableList>
</Box>
<ExpandableListItemKey label="Topic" value={'00'.repeat(32)} />
{x.feedHash && <ExpandableListItemKey label="Feed hash" value={x.feedHash} />}
<Box mt={0.75}>
<ExpandableListItemActions>
<SwarmButton onClick={() => viewFeed(x.uuid)} iconType={Info}>
View Feed Page
</SwarmButton>
<SwarmButton onClick={() => onShowExport(x)} iconType={Download}>
Export...
</SwarmButton>
<SwarmButton onClick={() => onShowDelete(x)} iconType={Trash}>
Delete...
</SwarmButton>
</ExpandableListItemActions>
</Box>
</ExpandableList>
))}
</>
)
}
@@ -0,0 +1,75 @@
import { CircularProgress, Container, createStyles, makeStyles } from '@material-ui/core'
import { ReactElement, useContext, useEffect } from 'react'
import { PlusSquare } from 'react-feather'
import { useNavigate } from 'react-router'
import { SwarmButton } from '../../../components/SwarmButton'
import TroubleshootConnectionCard from '../../../components/TroubleshootConnectionCard'
import { CheckState, Context as BeeContext } from '../../../providers/Bee'
import { Context as StampsContext } from '../../../providers/Stamps'
import { ROUTES } from '../../../routes'
import StampsTable from '../../stamps/StampsTable'
import { AccountNavigation } from '../AccountNavigation'
import { Header } from '../Header'
const useStyles = makeStyles(() =>
createStyles({
root: {
width: '100%',
display: 'grid',
},
actions: {
display: 'flex',
width: '100%',
flex: '0 1 auto',
flexWrap: 'wrap',
alignItems: 'center',
},
}),
)
export function AccountStamps(): ReactElement {
const classes = useStyles()
const navigate = useNavigate()
const { stamps, isLoading, error, start, stop } = useContext(StampsContext)
const { status } = useContext(BeeContext)
useEffect(() => {
if (!status.all) return
start()
return () => stop()
}, [status]) // eslint-disable-line react-hooks/exhaustive-deps
if (status.all === CheckState.ERROR) return <TroubleshootConnectionCard />
function navigateToNewStamp() {
navigate(ROUTES.ACCOUNT_STAMPS_NEW)
}
return (
<>
<Header />
<AccountNavigation active="STAMPS" />
<div className={classes.root}>
{error && (
<Container style={{ textAlign: 'center', padding: '50px' }}>
Error loading postage stamps details: {error.message}
</Container>
)}
{!error && (
<>
<div className={classes.actions}>
<SwarmButton onClick={navigateToNewStamp} iconType={PlusSquare}>
Buy New Postage Stamp
</SwarmButton>
<div style={{ height: '5px' }}>{isLoading && <CircularProgress />}</div>
</div>
<StampsTable postageStamps={stamps} />
</>
)}
</div>
</>
)
}
@@ -0,0 +1,67 @@
import { Box, Grid, Typography } from '@material-ui/core'
import { ReactElement, useContext } from 'react'
import { Download, Gift, Link } from 'react-feather'
import { useNavigate } from 'react-router'
import ExpandableListItem from '../../../components/ExpandableListItem'
import ExpandableListItemActions from '../../../components/ExpandableListItemActions'
import ExpandableListItemKey from '../../../components/ExpandableListItemKey'
import { Loading } from '../../../components/Loading'
import { SwarmButton } from '../../../components/SwarmButton'
import { Context } from '../../../providers/Bee'
import { ROUTES } from '../../../routes'
import { AccountNavigation } from '../AccountNavigation'
import { Header } from '../Header'
export function AccountWallet(): ReactElement {
const { balance } = useContext(Context)
const navigate = useNavigate()
if (!balance) {
return <Loading />
}
function onCheckTransactions() {
window.open(`https://blockscout.com/xdai/mainnet/address/${balance?.address}/transactions`, '_blank')
}
function onInvite() {
navigate(ROUTES.ACCOUNT_INVITATIONS)
}
function onDeposit() {
navigate(ROUTES.WALLET)
}
return (
<>
<Header />
<AccountNavigation active="WALLET" />
<Box mb={4}>
<Grid container direction="row" justifyContent="space-between" alignItems="center">
<Typography variant="h2">Wallet balance</Typography>
<SwarmButton onClick={onDeposit} iconType={Download}>
Deposit
</SwarmButton>
</Grid>
</Box>
<Box mb={0.25}>
<ExpandableListItemKey label="Node wallet address" value={balance.address} expanded />
</Box>
<Box mb={0.25}>
<ExpandableListItem label="XDAI balance" value={`${balance.dai.toSignificantDigits(4)} XDAI`} />
</Box>
<Box mb={2}>
<ExpandableListItem label="BZZ balance" value={`${balance.bzz.toSignificantDigits(4)} BZZ`} />
</Box>
<ExpandableListItemActions>
<SwarmButton onClick={onCheckTransactions} iconType={Link}>
Check transactions on Blockscout
</SwarmButton>
<SwarmButton onClick={onInvite} iconType={Gift}>
Invite to Swarm...
</SwarmButton>
</ExpandableListItemActions>
</>
)
}
-52
View File
@@ -1,52 +0,0 @@
import { ReactElement, useContext } from 'react'
import PeerBalances from './PeerBalances'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { CheckState, Context as BeeContext } from '../../providers/Bee'
import { Context as SettingsContext } from '../../providers/Settings'
import { useAccounting } from '../../hooks/accounting'
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 { status, nodeAddresses, chequebookAddress, chequebookBalance, settlements, peerBalances } =
useContext(BeeContext)
const { beeDebugApi } = useContext(SettingsContext)
const { accounting, totalUncashed, isLoadingUncashed } = useAccounting(beeDebugApi, settlements, peerBalances)
if (status.all === CheckState.ERROR) return <TroubleshootConnectionCard />
return (
<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>
)
}
+1 -1
View File
@@ -65,7 +65,7 @@ export default function CreateNewFeed(): ReactElement {
const identity = await convertWalletToIdentity(wallet, values.type, values.identityName, values.password)
persistIdentity(identities, identity)
setIdentities(identities)
navigate(ROUTES.FEEDS)
navigate(ROUTES.ACCOUNT_FEEDS)
setLoading(false)
}
+3 -3
View File
@@ -2,7 +2,7 @@ import * as swarmCid from '@ethersphere/swarm-cid'
import { Box } from '@material-ui/core'
import { ReactElement, useContext, useEffect, useState } from 'react'
import { X } from 'react-feather'
import { useParams, useNavigate } from 'react-router-dom'
import { useNavigate, useParams } from 'react-router-dom'
import { DocumentationText } from '../../components/DocumentationText'
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
import ExpandableListItemKey from '../../components/ExpandableListItemKey'
@@ -40,13 +40,13 @@ export function FeedSubpage(): ReactElement {
}, [beeApi, uuid, identity])
if (!identity || !status.all) {
navigate(ROUTES.FEEDS, { replace: true })
navigate(ROUTES.ACCOUNT_FEEDS, { replace: true })
return <></>
}
function onClose() {
navigate(ROUTES.FEEDS)
navigate(ROUTES.ACCOUNT_FEEDS)
}
return (
+3 -3
View File
@@ -2,7 +2,7 @@ import { Box, Grid, Typography } from '@material-ui/core'
import { useSnackbar } from 'notistack'
import { ReactElement, useContext, useEffect, useState } from 'react'
import { Bookmark, X } from 'react-feather'
import { useParams, useNavigate } from 'react-router'
import { useNavigate, useParams } from 'react-router'
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
import { HistoryHeader } from '../../components/HistoryHeader'
import { SwarmButton } from '../../components/SwarmButton'
@@ -73,10 +73,10 @@ export default function UpdateFeed(): ReactElement {
}
try {
await updateFeed(beeApi, identity, hash!, selectedStamp, password as string) // eslint-disable-line
await updateFeed(beeApi, beeDebugApi, identity, hash!, selectedStamp, password as string) // eslint-disable-line
persistIdentity(identities, identity)
setIdentities([...identities])
navigate(ROUTES.FEEDS_PAGE.replace(':uuid', identity.uuid))
navigate(ROUTES.ACCOUNT_FEEDS_VIEW.replace(':uuid', identity.uuid))
} catch (error: unknown) {
setLoading(false)
+2 -2
View File
@@ -29,11 +29,11 @@ export default function Feeds(): ReactElement {
const [showDelete, setShowDelete] = useState(false)
function createNewFeed() {
return navigate(ROUTES.FEEDS_NEW)
return navigate(ROUTES.ACCOUNT_FEEDS_NEW)
}
function viewFeed(uuid: string) {
navigate(ROUTES.FEEDS_PAGE.replace(':uuid', uuid))
navigate(ROUTES.ACCOUNT_FEEDS_VIEW.replace(':uuid', uuid))
}
function onDialogClose() {
+1 -1
View File
@@ -99,7 +99,7 @@ export function Share(): ReactElement {
}
function onUpdateFeed() {
navigate(ROUTES.FEEDS_UPDATE.replace(':hash', reference))
navigate(ROUTES.ACCOUNT_FEEDS_UPDATE.replace(':hash', reference))
}
useEffect(() => {
+14 -7
View File
@@ -13,6 +13,7 @@ import { Context as FileContext } from '../../providers/File'
import { Context as SettingsContext } from '../../providers/Settings'
import { Context as StampsContext, EnrichedPostageBatch } from '../../providers/Stamps'
import { ROUTES } from '../../routes'
import { waitUntilStampUsable } from '../../utils'
import { detectIndexHtml, getAssetNameFromFiles, packageFile } from '../../utils/file'
import { persistIdentity, updateFeed } from '../../utils/identity'
import { HISTORY_KEYS, putHistory } from '../../utils/local-storage'
@@ -31,7 +32,7 @@ export function Upload(): ReactElement {
const [showPasswordPrompt, setShowPasswordPrompt] = useState(false)
const { refresh } = useContext(StampsContext)
const { beeApi } = useContext(SettingsContext)
const { beeApi, beeDebugApi } = useContext(SettingsContext)
const { files, setFiles, uploadOrigin, metadata, previewUri, previewBlob } = useContext(FileContext)
const { identities, setIdentities } = useContext(IdentityContext)
const { status } = useContext(BeeContext)
@@ -66,7 +67,7 @@ export function Upload(): ReactElement {
}
}
const uploadFiles = (password?: string) => {
const uploadFiles = async (password?: string) => {
if (!beeApi || !files.length || !stamp || !metadata) {
return
}
@@ -122,6 +123,10 @@ export function Upload(): ReactElement {
setUploading(true)
if (beeDebugApi) {
await waitUntilStampUsable(stamp.batchID, beeDebugApi)
}
beeApi
.uploadFiles(stamp.batchID, fls, { indexDocument })
.then(hash => {
@@ -130,11 +135,13 @@ export function Upload(): ReactElement {
if (uploadOrigin.origin === 'UPLOAD') {
navigate(ROUTES.HASH.replace(':hash', hash.reference), { replace: true })
} else {
updateFeed(beeApi, identity as Identity, hash.reference, stamp.batchID, password as string).then(() => {
persistIdentity(identities, identity as Identity)
setIdentities([...identities])
navigate(ROUTES.FEEDS_PAGE.replace(':uuid', uploadOrigin.uuid as string), { replace: true })
})
updateFeed(beeApi, beeDebugApi, identity as Identity, hash.reference, stamp.batchID, password as string).then(
() => {
persistIdentity(identities, identity as Identity)
setIdentities([...identities])
navigate(ROUTES.ACCOUNT_FEEDS_VIEW.replace(':uuid', uploadOrigin.uuid as string), { replace: true })
},
)
}
})
.catch(e => {
+1 -1
View File
@@ -8,7 +8,7 @@ export function CreatePostageStampPage(): ReactElement {
const navigate = useNavigate()
function onFinished() {
navigate(ROUTES.STAMPS)
navigate(ROUTES.ACCOUNT_STAMPS)
}
return (
+3 -10
View File
@@ -9,13 +9,7 @@ import { SwarmTextInput } from '../../components/SwarmTextInput'
import { Context as BeeContext } from '../../providers/Bee'
import { Context as SettingsContext } from '../../providers/Settings'
import { Context as StampsContext } from '../../providers/Stamps'
import {
calculateStampPrice,
convertAmountToSeconds,
convertDepthToBytes,
secondsToTimeString,
waitUntilStampUsable,
} from '../../utils'
import { calculateStampPrice, convertAmountToSeconds, convertDepthToBytes, secondsToTimeString } from '../../utils'
import { getHumanReadableFileSize } from '../../utils/file'
interface FormValues {
@@ -88,8 +82,7 @@ export function PostageStampCreation({ onFinished }: Props): ReactElement {
const amount = BigInt(values.amount)
const depth = Number.parseInt(values.depth)
const options = values.label ? { label: values.label } : undefined
const batch = await beeDebugApi.createPostageBatch(amount.toString(), depth, options)
await waitUntilStampUsable(batch, beeDebugApi)
await beeDebugApi.createPostageBatch(amount.toString(), depth, options)
actions.resetForm()
await refresh()
onFinished()
@@ -107,7 +100,7 @@ export function PostageStampCreation({ onFinished }: Props): ReactElement {
const depth = new BigNumber(values.depth)
if (!depth.isInteger()) errors.depth = 'Depth must be an integer'
else if (depth.isLessThan(16)) errors.depth = 'Minimal depth is 16'
else if (depth.isLessThan(17)) errors.depth = 'Minimal depth is 17'
else if (depth.isGreaterThan(255)) errors.depth = 'Depth has to be at most 255'
}
+1 -1
View File
@@ -44,7 +44,7 @@ export default function Stamp(): ReactElement {
if (status.all === CheckState.ERROR) return <TroubleshootConnectionCard />
function navigateToNewStamp() {
navigate(ROUTES.STAMPS_NEW)
navigate(ROUTES.ACCOUNT_STAMPS_NEW)
}
return (