feat: changing API urls does not need the app refresh (#173)

* feat: changing API urls does not need the app refresh

* fix: propagate beeDebugApi and beeApi change to the refresh interval

* fix: any failed request on the Bee provider does not stop the execution of other requests

* fix: error handling for incorrect bee and bee debug urls

* fix: change debug API in the settings tab
This commit is contained in:
Vojtech Simetka
2021-08-20 15:14:14 +02:00
committed by GitHub
parent 2624cf04c9
commit d6d03bf7c6
21 changed files with 336 additions and 380 deletions
+17 -14
View File
@@ -11,6 +11,7 @@ import { lightTheme, darkTheme } from './theme'
import { Provider as StampsProvider } from './providers/Stamps' import { Provider as StampsProvider } from './providers/Stamps'
import { Provider as PlatformProvider } from './providers/Platform' import { Provider as PlatformProvider } from './providers/Platform'
import { Provider as BeeProvider } from './providers/Bee' import { Provider as BeeProvider } from './providers/Bee'
import { Provider as SettingsProvider } from './providers/Settings'
const App = (): ReactElement => { const App = (): ReactElement => {
const [themeMode, toggleThemeMode] = useState('light') const [themeMode, toggleThemeMode] = useState('light')
@@ -37,20 +38,22 @@ const App = (): ReactElement => {
return ( return (
<div className="App"> <div className="App">
<ThemeProvider theme={themeMode === 'light' ? lightTheme : darkTheme}> <ThemeProvider theme={themeMode === 'light' ? lightTheme : darkTheme}>
<BeeProvider> <SettingsProvider>
<StampsProvider> <BeeProvider>
<PlatformProvider> <StampsProvider>
<SnackbarProvider> <PlatformProvider>
<> <SnackbarProvider>
<CssBaseline /> <>
<Router> <CssBaseline />
<BaseRouter /> <Router>
</Router> <BaseRouter />
</> </Router>
</SnackbarProvider> </>
</PlatformProvider> </SnackbarProvider>
</StampsProvider> </PlatformProvider>
</BeeProvider> </StampsProvider>
</BeeProvider>
</SettingsProvider>
</ThemeProvider> </ThemeProvider>
</div> </div>
) )
+7 -4
View File
@@ -6,8 +6,8 @@ import DialogContent from '@material-ui/core/DialogContent'
import DialogContentText from '@material-ui/core/DialogContentText' import DialogContentText from '@material-ui/core/DialogContentText'
import DialogTitle from '@material-ui/core/DialogTitle' import DialogTitle from '@material-ui/core/DialogTitle'
import { useSnackbar } from 'notistack' import { useSnackbar } from 'notistack'
import { ReactElement, useState } from 'react' import { ReactElement, useState, useContext } from 'react'
import { beeDebugApi } from '../services/bee' import { Context as SettingsContext } from '../providers/Settings'
import EthereumAddress from './EthereumAddress' import EthereumAddress from './EthereumAddress'
interface Props { interface Props {
@@ -19,6 +19,7 @@ export default function CheckoutModal({ peerId, uncashedAmount }: Props): ReactE
const [open, setOpen] = useState<boolean>(false) const [open, setOpen] = useState<boolean>(false)
const [loadingCashout, setLoadingCashout] = useState<boolean>(false) const [loadingCashout, setLoadingCashout] = useState<boolean>(false)
const { enqueueSnackbar } = useSnackbar() const { enqueueSnackbar } = useSnackbar()
const { beeDebugApi } = useContext(SettingsContext)
const handleClickOpen = () => { const handleClickOpen = () => {
setOpen(true) setOpen(true)
@@ -29,10 +30,12 @@ export default function CheckoutModal({ peerId, uncashedAmount }: Props): ReactE
} }
const handleCashout = () => { const handleCashout = () => {
if (!beeDebugApi) return
if (peerId) { if (peerId) {
setLoadingCashout(true) setLoadingCashout(true)
beeDebugApi.chequebook beeDebugApi
.peerCashout(peerId) .cashoutLastCheque(peerId)
.then(res => { .then(res => {
setOpen(false) setOpen(false)
enqueueSnackbar( enqueueSnackbar(
+28 -40
View File
@@ -1,62 +1,50 @@
import React, { ReactElement, useState } from 'react' import React, { ReactElement, useState } from 'react'
import { TextField, Button, CircularProgress, Container } from '@material-ui/core' import { TextField, Button } from '@material-ui/core'
interface Props { interface Props {
defaultHost?: string defaultHost?: string
hostName: string setHost: (host: string) => void
} }
export default function ConnectToHost(props: Props): ReactElement { export default function ConnectToHost(props: Props): ReactElement {
const [hostInputVisible, toggleHostInputVisibility] = useState(false) const [hostInputVisible, toggleHostInputVisibility] = useState(false)
const [connectingToHost, setConnectingToHost] = useState(false)
const [host, setHost] = useState('') const [host, setHost] = useState('')
const handleNewHostConnection = () => { const handleNewHostConnection = () => {
if (host) { if (host) {
setConnectingToHost(true) props.setHost(host)
sessionStorage.setItem(props.hostName, host)
toggleHostInputVisibility(!hostInputVisible) toggleHostInputVisibility(!hostInputVisible)
window.location.reload()
} }
} }
return ( return (
<div> <div>
{ {hostInputVisible ? (
// FIXME: this should be broken up <div style={{ display: 'flex' }}>
/* eslint-disable no-nested-ternary */ <TextField
hostInputVisible ? ( defaultValue={props.defaultHost}
<div style={{ display: 'flex' }}> label="Enter host"
<TextField variant="outlined"
defaultValue={props.defaultHost} size="small"
label="Enter host" onChange={e => setHost(e.target.value)}
variant="outlined" style={{ marginRight: '15px', minWidth: '300px' }}
size="small" />
onChange={e => setHost(e.target.value)} <Button onClick={() => handleNewHostConnection()} size="small" variant="outlined">
style={{ marginRight: '15px', minWidth: '300px' }} Connect
/>
<Button onClick={() => handleNewHostConnection()} size="small" variant="outlined">
Connect
</Button>
<Button
style={{ marginLeft: '7px' }}
onClick={() => toggleHostInputVisibility(!hostInputVisible)}
size="small"
>
Cancel
</Button>
</div>
) : connectingToHost ? (
<Container style={{ textAlign: 'center', padding: '0px' }}>
<CircularProgress size={20} />
</Container>
) : (
<Button onClick={() => toggleHostInputVisibility(!hostInputVisible)} size="small" variant="outlined">
Change host
</Button> </Button>
) <Button
/* eslint-enable no-nested-ternary */ style={{ marginLeft: '7px' }}
} onClick={() => toggleHostInputVisibility(!hostInputVisible)}
size="small"
>
Cancel
</Button>
</div>
) : (
<Button onClick={() => toggleHostInputVisibility(!hostInputVisible)} size="small" variant="outlined">
Change host
</Button>
)}
</div> </div>
) )
} }
-4
View File
@@ -1,4 +0,0 @@
// These values can for now be constants because their change in the app reloads the page
export const apiHost = sessionStorage.getItem('api_host') || process.env.REACT_APP_BEE_HOST || 'http://localhost:1633'
export const debugApiHost =
sessionStorage.getItem('debug_api_host') || process.env.REACT_APP_BEE_DEBUG_HOST || 'http://localhost:1635'
+9 -3
View File
@@ -1,10 +1,12 @@
import type { ReactElement } from 'react' import { ReactElement, useContext } from 'react'
import { beeDebugApi } from '../services/bee' import { Context as SettingsContext } from '../providers/Settings'
import WDModal from '../components/WDModal' import WDModal from '../components/WDModal'
import { BigNumber } from 'bignumber.js' import { BigNumber } from 'bignumber.js'
export default function DepositModal(): ReactElement { export default function DepositModal(): ReactElement {
const { beeDebugApi } = useContext(SettingsContext)
return ( return (
<WDModal <WDModal
successMessage="Successful deposit." successMessage="Successful deposit."
@@ -12,7 +14,11 @@ export default function DepositModal(): ReactElement {
dialogMessage="Specify the amount of BZZ you would like to withdraw from your node." dialogMessage="Specify the amount of BZZ you would like to withdraw from your node."
label="Deposit" label="Deposit"
min={new BigNumber(0)} min={new BigNumber(0)}
action={beeDebugApi.chequebook.deposit} action={(amount: bigint) => {
if (!beeDebugApi) throw new Error('Bee Debug URL is not valid')
return beeDebugApi.depositTokens(amount.toString())
}}
/> />
) )
} }
+9 -3
View File
@@ -1,10 +1,12 @@
import type { ReactElement } from 'react' import { ReactElement, useContext } from 'react'
import { beeDebugApi } from '../services/bee' import { Context as SettingsContext } from '../providers/Settings'
import WDModal from '../components/WDModal' import WDModal from '../components/WDModal'
import { BigNumber } from 'bignumber.js' import { BigNumber } from 'bignumber.js'
export default function WithdrawModal(): ReactElement { export default function WithdrawModal(): ReactElement {
const { beeDebugApi } = useContext(SettingsContext)
return ( return (
<WDModal <WDModal
successMessage="Successful withdrawl." successMessage="Successful withdrawl."
@@ -12,7 +14,11 @@ export default function WithdrawModal(): ReactElement {
dialogMessage="Specify the amount of BZZ you would like to withdraw from your node." dialogMessage="Specify the amount of BZZ you would like to withdraw from your node."
label="Withdraw" label="Withdraw"
min={new BigNumber(0)} min={new BigNumber(0)}
action={beeDebugApi.chequebook.withdraw} action={(amount: bigint) => {
if (!beeDebugApi) throw new Error('Bee Debug URL is not valid')
return beeDebugApi.withdrawTokens(amount.toString())
}}
/> />
) )
} }
+12 -22
View File
@@ -1,16 +1,11 @@
import { LastCashoutActionResponse } from '@ethersphere/bee-js' import { LastCashoutActionResponse, BeeDebug } from '@ethersphere/bee-js'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Token } from '../models/Token' import { Token } from '../models/Token'
import { beeDebugApi } from '../services/bee'
import { makeRetriablePromise, unwrapPromiseSettlements } from '../utils' import { makeRetriablePromise, unwrapPromiseSettlements } from '../utils'
import { Balance, Settlement, useApiPeerBalances, useApiSettlements } from './apiHooks' import { Balance, Settlements, Settlement } from '../types'
interface UseAccountingHook { interface UseAccountingHook {
isLoading: boolean
isLoadingUncashed: boolean isLoadingUncashed: boolean
error: Error | null
totalsent: Token
totalreceived: Token
accounting: Accounting[] | null accounting: Accounting[] | null
} }
@@ -77,39 +72,34 @@ function mergeAccounting(
) )
} }
export const useAccounting = (): UseAccountingHook => { export const useAccounting = (
const settlements = useApiSettlements() beeDebugApi: BeeDebug | null,
const balances = useApiPeerBalances() settlements: Settlements | null,
balances: Balance[] | null,
): UseAccountingHook => {
const [isLoadingUncashed, setIsloadingUncashed] = useState<boolean>(false) const [isLoadingUncashed, setIsloadingUncashed] = useState<boolean>(false)
const [uncashedAmounts, setUncashedAmounts] = useState<LastCashoutActionResponse[] | undefined>(undefined) const [uncashedAmounts, setUncashedAmounts] = useState<LastCashoutActionResponse[] | undefined>(undefined)
const error = balances.error || settlements.error
useEffect(() => { useEffect(() => {
// We don't have any settlements loaded yet or we are already loading/have loaded the uncashed amounts // We don't have any settlements loaded yet or we are already loading/have loaded the uncashed amounts
if (isLoadingUncashed || !settlements.settlements || uncashedAmounts || error) return if (isLoadingUncashed || !beeDebugApi || !settlements || uncashedAmounts) return
setIsloadingUncashed(true) setIsloadingUncashed(true)
const promises = settlements.settlements.settlements const promises = settlements.settlements
.filter(({ received }) => received.toBigNumber.gt('0')) .filter(({ received }) => received.toBigNumber.gt('0'))
.map(({ peer }) => makeRetriablePromise(() => beeDebugApi.chequebook.getPeerLastCashout(peer))) .map(({ peer }) => makeRetriablePromise(() => beeDebugApi.getLastCashoutAction(peer)))
Promise.allSettled(promises).then(settlements => { Promise.allSettled(promises).then(settlements => {
const results = unwrapPromiseSettlements(settlements) const results = unwrapPromiseSettlements(settlements)
setUncashedAmounts(results.fulfilled) setUncashedAmounts(results.fulfilled)
setIsloadingUncashed(false) setIsloadingUncashed(false)
}) })
}, [settlements, isLoadingUncashed, uncashedAmounts, error]) }, [settlements, isLoadingUncashed, uncashedAmounts])
const accounting = mergeAccounting(balances.peerBalances, settlements.settlements?.settlements, uncashedAmounts) const accounting = mergeAccounting(balances, settlements?.settlements, uncashedAmounts)
return { return {
isLoading: settlements.isLoadingSettlements || balances.isLoadingPeerBalances,
isLoadingUncashed, isLoadingUncashed,
error,
accounting, accounting,
totalsent: settlements.settlements?.totalSent || new Token('0'),
totalreceived: settlements.settlements?.totalReceived || new Token('0'),
} }
} }
-89
View File
@@ -1,94 +1,5 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { beeDebugApi } from '../services/bee'
import axios from 'axios' import axios from 'axios'
import { Token } from '../models/Token'
export interface Balance {
peer: string
balance: Token
}
export interface PeerBalanceHook {
peerBalances: Balance[] | null
isLoadingPeerBalances: boolean
error: Error | null
}
export const useApiPeerBalances = (): PeerBalanceHook => {
const [peerBalances, setPeerBalances] = useState<Balance[] | null>(null)
const [isLoadingPeerBalances, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.balance
.balances()
.then(res => {
// for some reason sometimes these are numbers and not BigInts
const balances = res.balances.map(({ peer, balance }) => ({ peer, balance: new Token(balance) }))
setPeerBalances(balances)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { peerBalances, isLoadingPeerBalances, error }
}
export interface Settlement {
peer: string
received: Token
sent: Token
}
export interface Settlements {
totalReceived: Token
totalSent: Token
settlements: Settlement[]
}
export interface SettlementsHook {
settlements: Settlements | null
isLoadingSettlements: boolean
error: Error | null
}
export const useApiSettlements = (): SettlementsHook => {
const [settlements, setSettlements] = useState<Settlements | null>(null)
const [isLoadingSettlements, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.settlements
.getSettlements()
.then(({ totalReceived, settlements, totalSent }) => {
const set = {
totalReceived: new Token(totalReceived),
totalSent: new Token(totalSent),
settlements: settlements.map(({ peer, received, sent }) => ({
peer,
received: new Token(received),
sent: new Token(sent),
})),
}
setSettlements(set)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { settlements, isLoadingSettlements, error }
}
export interface LatestBeeReleaseHook { export interface LatestBeeReleaseHook {
latestBeeRelease: LatestBeeRelease | null latestBeeRelease: LatestBeeRelease | null
+8 -10
View File
@@ -1,12 +1,12 @@
import { ReactElement, useContext } from 'react' import { ReactElement, useContext } from 'react'
import { Theme, createStyles, makeStyles } from '@material-ui/core/styles' import { Theme, createStyles, makeStyles } from '@material-ui/core/styles'
import { Container } from '@material-ui/core'
import AccountCard from '../accounting/AccountCard' import AccountCard from '../accounting/AccountCard'
import BalancesTable from './BalancesTable' import BalancesTable from './BalancesTable'
import EthereumAddressCard from '../../components/EthereumAddressCard' import EthereumAddressCard from '../../components/EthereumAddressCard'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard' import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { Context } from '../../providers/Bee' import { Context as BeeContext } from '../../providers/Bee'
import { Context as SettingsContext } from '../../providers/Settings'
import { useAccounting } from '../../hooks/accounting' import { useAccounting } from '../../hooks/accounting'
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
@@ -22,9 +22,12 @@ const useStyles = makeStyles((theme: Theme) =>
export default function Accounting(): ReactElement { export default function Accounting(): ReactElement {
const classes = useStyles() const classes = useStyles()
const { status, nodeAddresses, chequebookAddress, chequebookBalance, settlements } = useContext(Context) const { status, nodeAddresses, chequebookAddress, chequebookBalance, settlements, peerBalances } = useContext(
BeeContext,
)
const { beeDebugApi } = useContext(SettingsContext)
const { accounting, isLoadingUncashed, error } = useAccounting() const { accounting, isLoadingUncashed } = useAccounting(beeDebugApi, settlements, peerBalances)
if (!status.all) return <TroubleshootConnectionCard /> if (!status.all) return <TroubleshootConnectionCard />
@@ -37,12 +40,7 @@ export default function Accounting(): ReactElement {
totalreceived={settlements?.totalReceived} totalreceived={settlements?.totalReceived}
/> />
<EthereumAddressCard nodeAddresses={nodeAddresses} chequebookAddress={chequebookAddress} /> <EthereumAddressCard nodeAddresses={nodeAddresses} chequebookAddress={chequebookAddress} />
{error && ( <BalancesTable accounting={accounting} isLoadingUncashed={isLoadingUncashed} />
<Container style={{ textAlign: 'center', padding: '50px' }}>
Error loading accounting details: {error.message}
</Container>
)}
{!error && <BalancesTable accounting={accounting} isLoadingUncashed={isLoadingUncashed} />}
</div> </div>
) )
} }
+4 -3
View File
@@ -1,8 +1,8 @@
import { ReactElement, useState } from 'react' import { ReactElement, useState, useContext } from 'react'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import { Paper, InputBase, IconButton, FormHelperText } from '@material-ui/core' import { Paper, InputBase, IconButton, FormHelperText } from '@material-ui/core'
import { Search } from '@material-ui/icons' import { Search } from '@material-ui/icons'
import { apiHost } from '../../constants' import { Context as SettingsContext } from '../../providers/Settings'
import { Utils } from '@ethersphere/bee-js' import { Utils } from '@ethersphere/bee-js'
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
@@ -28,6 +28,7 @@ const useStyles = makeStyles((theme: Theme) =>
export default function Files(): ReactElement { export default function Files(): ReactElement {
const classes = useStyles() const classes = useStyles()
const { apiUrl } = useContext(SettingsContext)
const [referenceInput, setReferenceInput] = useState('') const [referenceInput, setReferenceInput] = useState('')
const [referenceError, setReferenceError] = useState<Error | null>(null) const [referenceError, setReferenceError] = useState<Error | null>(null)
@@ -50,7 +51,7 @@ export default function Files(): ReactElement {
onChange={handleReferenceChange} onChange={handleReferenceChange}
/> />
<IconButton <IconButton
href={`${apiHost}/bzz/${referenceInput}`} href={`${apiUrl}/bzz/${referenceInput}`}
target="_blank" target="_blank"
disabled={referenceError !== null || !referenceInput} disabled={referenceError !== null || !referenceInput}
className={classes.iconButton} className={classes.iconButton}
+6 -2
View File
@@ -8,7 +8,7 @@ import UploadSizeAlert from '../../components/AlertUploadSize'
import ClipboardCopy from '../../components/ClipboardCopy' import ClipboardCopy from '../../components/ClipboardCopy'
import PeerDetailDrawer from '../../components/PeerDetail' import PeerDetailDrawer from '../../components/PeerDetail'
import { Context, EnrichedPostageBatch } from '../../providers/Stamps' import { Context, EnrichedPostageBatch } from '../../providers/Stamps'
import { beeApi } from '../../services/bee' import { Context as SettingsContext } from '../../providers/Settings'
import CreatePostageStamp from '../stamps/CreatePostageStampModal' import CreatePostageStamp from '../stamps/CreatePostageStampModal'
import SelectStamp from './SelectStamp' import SelectStamp from './SelectStamp'
@@ -23,6 +23,7 @@ export default function Files(): ReactElement {
const [selectedStamp, setSelectedStamp] = useState<EnrichedPostageBatch | null>(null) const [selectedStamp, setSelectedStamp] = useState<EnrichedPostageBatch | null>(null)
const { isLoading, error, stamps } = useContext(Context) const { isLoading, error, stamps } = useContext(Context)
const { beeApi } = useContext(SettingsContext)
const { enqueueSnackbar } = useSnackbar() const { enqueueSnackbar } = useSnackbar()
// Choose a postage stamp that has the lowest usage // Choose a postage stamp that has the lowest usage
@@ -40,8 +41,11 @@ export default function Files(): ReactElement {
const uploadFile = () => { const uploadFile = () => {
if (file === null || selectedStamp === null) return if (file === null || selectedStamp === null) return
if (!beeApi) return
setIsUploadingFile(true) setIsUploadingFile(true)
beeApi.files beeApi
.uploadFile(selectedStamp.batchID, file) .uploadFile(selectedStamp.batchID, file)
.then(hash => { .then(hash => {
window.setTimeout(() => { window.setTimeout(() => {
+5 -4
View File
@@ -1,4 +1,4 @@
import React, { ReactElement, useState } from 'react' import { ReactElement, useState, useContext } from 'react'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import { import {
Table, Table,
@@ -14,7 +14,7 @@ import {
} from '@material-ui/core' } from '@material-ui/core'
import { Autorenew } from '@material-ui/icons' import { Autorenew } from '@material-ui/icons'
import { beeDebugApi } from '../../services/bee' import { Context as SettingsContext } from '../../providers/Settings'
import type { Peer } from '@ethersphere/bee-js' import type { Peer } from '@ethersphere/bee-js'
const useStyles = makeStyles({ const useStyles = makeStyles({
@@ -29,13 +29,14 @@ interface Props {
function PeerTable(props: Props): ReactElement { function PeerTable(props: Props): ReactElement {
const classes = useStyles() const classes = useStyles()
const { beeDebugApi } = useContext(SettingsContext)
const [peerLatency, setPeerLatency] = useState([{ peerId: '', rtt: '', loading: false }]) const [peerLatency, setPeerLatency] = useState([{ peerId: '', rtt: '', loading: false }])
const PingPeer = (peerId: string) => { const PingPeer = (peerId: string) => {
setPeerLatency([...peerLatency, { peerId: peerId, rtt: '', loading: true }]) setPeerLatency([...peerLatency, { peerId: peerId, rtt: '', loading: true }])
beeDebugApi.connectivity beeDebugApi
.ping(peerId) ?.pingPeer(peerId)
.then(res => { .then(res => {
setPeerLatency([...peerLatency, { peerId: peerId, rtt: res.rtt, loading: false }]) setPeerLatency([...peerLatency, { peerId: peerId, rtt: res.rtt, loading: false }])
}) })
+14 -28
View File
@@ -1,26 +1,20 @@
import React, { ReactElement, useState } from 'react' import React, { ReactElement, useState, useContext } from 'react'
import { Paper, Container, TextField, Typography, Button } from '@material-ui/core' import { Paper, Container, TextField, Typography, Button } from '@material-ui/core'
import { Context as SettingsContext } from '../../providers/Settings'
export default function Settings(): ReactElement { export default function Settings(): ReactElement {
const [refreshVisibility, toggleRefreshVisibility] = useState(false) const { apiUrl, apiDebugUrl, setApiUrl, setDebugApiUrl } = useContext(SettingsContext)
const [host, setHost] = useState('') const [host, setHost] = useState(apiUrl)
const [debugHost, setDebugHost] = useState('') const [debugHost, setDebugHost] = useState(apiDebugUrl)
const handleNewHostConnection = () => { const submit = () => {
if (host) { if (host !== apiUrl) setApiUrl(host)
sessionStorage.setItem('api_host', host)
}
if (debugHost) { if (debugHost !== apiDebugUrl) setDebugApiUrl(debugHost)
sessionStorage.setItem('debug_api_host', debugHost)
}
if (host || debugHost) {
toggleRefreshVisibility(!refreshVisibility)
window.location.reload()
}
} }
const touched = host !== apiUrl || debugHost !== apiDebugUrl
return ( return (
<div> <div>
<Container> <Container>
@@ -34,16 +28,13 @@ export default function Settings(): ReactElement {
placeholder="ex: 127.0.0.0.1:1633" placeholder="ex: 127.0.0.0.1:1633"
helperText="Enter node host override / port" helperText="Enter node host override / port"
fullWidth fullWidth
defaultValue={ defaultValue={apiUrl}
sessionStorage.getItem('api_host') ? sessionStorage.getItem('api_host') : process.env.REACT_APP_BEE_HOST
}
margin="normal" margin="normal"
InputLabelProps={{ InputLabelProps={{
shrink: true, shrink: true,
}} }}
onChange={e => { onChange={e => {
setHost(e.target.value) setHost(e.target.value)
toggleRefreshVisibility(true)
}} }}
variant="filled" variant="filled"
/> />
@@ -55,14 +46,9 @@ export default function Settings(): ReactElement {
placeholder="ex: 127.0.0.0.1:1635" placeholder="ex: 127.0.0.0.1:1635"
helperText="Enter node debug host override / port" helperText="Enter node debug host override / port"
fullWidth fullWidth
defaultValue={ defaultValue={apiDebugUrl}
sessionStorage.getItem('debug_api_host')
? sessionStorage.getItem('debug_api_host')
: process.env.REACT_APP_BEE_DEBUG_HOST
}
onChange={e => { onChange={e => {
setDebugHost(e.target.value) setDebugHost(e.target.value)
toggleRefreshVisibility(true)
}} }}
margin="normal" margin="normal"
InputLabelProps={{ InputLabelProps={{
@@ -71,9 +57,9 @@ export default function Settings(): ReactElement {
variant="filled" variant="filled"
/> />
</Paper> </Paper>
{refreshVisibility ? ( {touched ? (
<div style={{ marginTop: '20px' }}> <div style={{ marginTop: '20px' }}>
<Button variant="outlined" color="primary" onClick={() => handleNewHostConnection()}> <Button variant="outlined" color="primary" onClick={submit}>
Save Save
</Button> </Button>
</div> </div>
+5 -2
View File
@@ -9,7 +9,7 @@ import DialogTitle from '@material-ui/core/DialogTitle'
import BigNumber from 'bignumber.js' import BigNumber from 'bignumber.js'
import { FormikHelpers, Form, Field, Formik } from 'formik' import { FormikHelpers, Form, Field, Formik } from 'formik'
import { TextField } from 'formik-material-ui' import { TextField } from 'formik-material-ui'
import { beeApi } from '../../services/bee' import { Context as SettingsContext } from '../../providers/Settings'
import { Context } from '../../providers/Stamps' import { Context } from '../../providers/Stamps'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import { useSnackbar } from 'notistack' import { useSnackbar } from 'notistack'
@@ -54,6 +54,7 @@ export default function FormDialog({ label }: Props): ReactElement {
const classes = useStyles() const classes = useStyles()
const [open, setOpen] = React.useState(false) const [open, setOpen] = React.useState(false)
const { refresh } = useContext(Context) const { refresh } = useContext(Context)
const { beeApi } = useContext(SettingsContext)
const handleClickOpen = () => setOpen(true) const handleClickOpen = () => setOpen(true)
const handleClose = () => setOpen(false) const handleClose = () => setOpen(false)
const { enqueueSnackbar } = useSnackbar() const { enqueueSnackbar } = useSnackbar()
@@ -66,10 +67,12 @@ export default function FormDialog({ label }: Props): ReactElement {
// This is really just a typeguard, the validation pretty much guarantees these will have the right values // This is really just a typeguard, the validation pretty much guarantees these will have the right values
if (!values.depth || !values.amount) return if (!values.depth || !values.amount) return
if (!beeApi) return
const amount = BigInt(values.amount) const amount = BigInt(values.amount)
const depth = Number.parseInt(values.depth) const depth = Number.parseInt(values.depth)
const options = values.label ? { label: values.label } : undefined const options = values.label ? { label: values.label } : undefined
await beeApi.stamps.buyPostageStamp(amount, depth, options) await beeApi.createPostageBatch(amount.toString(), depth, options)
actions.resetForm() actions.resetForm()
await refresh() await refresh()
handleClose() handleClose()
@@ -1,21 +1,29 @@
import type { ReactElement } from 'react' import { ReactElement, useContext } from 'react'
import { Typography, Accordion, AccordionSummary, AccordionDetails } from '@material-ui/core/' import { Typography, Accordion, AccordionSummary, AccordionDetails } from '@material-ui/core/'
import MuiAlert from '@material-ui/lab/Alert' import MuiAlert from '@material-ui/lab/Alert'
import { ExpandMoreSharp } from '@material-ui/icons/' import { ExpandMoreSharp } from '@material-ui/icons/'
import ConnectToHost from '../../../components/ConnectToHost' import ConnectToHost from '../../../components/ConnectToHost'
import CodeBlockTabs from '../../../components/CodeBlockTabs' import CodeBlockTabs from '../../../components/CodeBlockTabs'
import { debugApiHost } from '../../../constants' import { Context as SettingsContext } from '../../../providers/Settings'
type Props = StatusHookCommon type Props = StatusHookCommon
export default function NodeConnectionCheck({ isOk }: Props): ReactElement | null { export default function NodeConnectionCheck({ isOk }: Props): ReactElement | null {
const { setDebugApiUrl, apiDebugUrl } = useContext(SettingsContext)
const changeDebugApiUrl = ( const changeDebugApiUrl = (
<div style={{ display: 'flex', marginTop: '25px', marginBottom: '25px' }}> <div style={{ display: 'flex', marginTop: '25px', marginBottom: '25px' }}>
<span style={{ marginRight: '15px' }}> <span style={{ marginRight: '15px' }}>
Debug API (<Typography variant="button">{debugApiHost}</Typography>) Debug API (<Typography variant="button">{apiDebugUrl}</Typography>)
</span> </span>
<ConnectToHost hostName={'debug_api_host'} defaultHost={debugApiHost} /> <ConnectToHost
setHost={(host: string) => {
console.log(host) // eslint-disable-line
setDebugApiUrl(host)
}}
defaultHost={apiDebugUrl}
/>
</div> </div>
) )
@@ -29,7 +37,7 @@ export default function NodeConnectionCheck({ isOk }: Props): ReactElement | nul
<div> <div>
<Typography component="div" variant="body2" gutterBottom style={{ margin: '15px' }}> <Typography component="div" variant="body2" gutterBottom style={{ margin: '15px' }}>
We cannot connect to your nodes debug API at <Typography variant="button">{debugApiHost}</Typography>. Please We cannot connect to your nodes debug API at <Typography variant="button">{apiDebugUrl}</Typography>. Please
check the following to troubleshoot your issue. check the following to troubleshoot your issue.
<Accordion style={{ marginTop: '20px' }}> <Accordion style={{ marginTop: '20px' }}>
<AccordionSummary expandIcon={<ExpandMoreSharp />} aria-controls="panel1a-content" id="panel1a-header"> <AccordionSummary expandIcon={<ExpandMoreSharp />} aria-controls="panel1a-content" id="panel1a-header">
@@ -1,26 +1,28 @@
import React, { ReactElement } from 'react' import { ReactElement, useContext } from 'react'
import { Typography, Accordion, AccordionSummary, AccordionDetails } from '@material-ui/core/' import { Typography, Accordion, AccordionSummary, AccordionDetails } from '@material-ui/core/'
import { ExpandMoreSharp } from '@material-ui/icons/' import { ExpandMoreSharp } from '@material-ui/icons/'
import ConnectToHost from '../../../components/ConnectToHost' import ConnectToHost from '../../../components/ConnectToHost'
import CodeBlockTabs from '../../../components/CodeBlockTabs' import CodeBlockTabs from '../../../components/CodeBlockTabs'
import { apiHost } from '../../../constants' import { Context as SettingsContext } from '../../../providers/Settings'
type Props = StatusHookCommon type Props = StatusHookCommon
export default function NodeConnectionCheck({ isOk }: Props): ReactElement | null { export default function NodeConnectionCheck({ isOk }: Props): ReactElement | null {
const { setApiUrl, apiUrl } = useContext(SettingsContext)
return ( return (
<div> <div>
<div style={{ display: 'flex', marginBottom: '25px' }}> <div style={{ display: 'flex', marginBottom: '25px' }}>
<span style={{ marginRight: '15px' }}> <span style={{ marginRight: '15px' }}>
Node API (<Typography variant="button">{apiHost}</Typography>) Node API (<Typography variant="button">{apiUrl}</Typography>)
</span> </span>
<ConnectToHost hostName="api_host" defaultHost={apiHost} /> <ConnectToHost setHost={setApiUrl} defaultHost={apiUrl} />
</div> </div>
<div> <div>
{!isOk && ( {!isOk && (
<Typography component="div" variant="body2" gutterBottom style={{ margin: '15px' }}> <Typography component="div" variant="body2" gutterBottom style={{ margin: '15px' }}>
We cannot connect to your nodes API at <Typography variant="button">{apiHost}</Typography>. Please check the We cannot connect to your nodes API at <Typography variant="button">{apiUrl}</Typography>. Please check the
following to troubleshoot your issue. following to troubleshoot your issue.
<Accordion style={{ marginTop: '20px' }}> <Accordion style={{ marginTop: '20px' }}>
<AccordionSummary expandIcon={<ExpandMoreSharp />} aria-controls="panel1a-content" id="panel1a-header"> <AccordionSummary expandIcon={<ExpandMoreSharp />} aria-controls="panel1a-content" id="panel1a-header">
+2 -2
View File
@@ -4,7 +4,7 @@ import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import NodeSetupWorkflow from './NodeSetupWorkflow' import NodeSetupWorkflow from './NodeSetupWorkflow'
import StatusCard from './StatusCard' import StatusCard from './StatusCard'
import EthereumAddressCard from '../../components/EthereumAddressCard' import EthereumAddressCard from '../../components/EthereumAddressCard'
import { Context } from '../../providers/Bee' import { Context as BeeContext } from '../../providers/Bee'
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
createStyles({ createStyles({
@@ -27,7 +27,7 @@ export default function Status(): ReactElement {
topology, topology,
nodeAddresses, nodeAddresses,
chequebookAddress, chequebookAddress,
} = useContext(Context) } = useContext(BeeContext)
return ( return (
<div className={classes.root}> <div className={classes.root}>
+125 -28
View File
@@ -1,9 +1,9 @@
import type { ChequebookBalance, Balance, Settlements } from '../types' import type { ChequebookBalance, Balance, Settlements } from '../types'
import { createContext, ReactChild, ReactElement, useEffect, useState } from 'react' import { createContext, ReactChild, ReactElement, useEffect, useState, useContext } from 'react'
import { beeApi, beeDebugApi } from '../services/bee'
import { Token } from '../models/Token' import { Token } from '../models/Token'
import semver from 'semver' import semver from 'semver'
import { engines } from '../../package.json' import { engines } from '../../package.json'
import { Context as SettingsContext } from './Settings'
import type { import type {
NodeAddresses, NodeAddresses,
@@ -125,6 +125,7 @@ function getStatus(
} }
export function Provider({ children }: Props): ReactElement { export function Provider({ children }: Props): ReactElement {
const { beeApi, beeDebugApi } = useContext(SettingsContext)
const [apiHealth, setApiHealth] = useState<boolean>(false) const [apiHealth, setApiHealth] = useState<boolean>(false)
const [debugApiHealth, setDebugApiHealth] = useState<Health | null>(null) const [debugApiHealth, setDebugApiHealth] = useState<Health | null>(null)
const [nodeAddresses, setNodeAddresses] = useState<NodeAddresses | null>(null) const [nodeAddresses, setNodeAddresses] = useState<NodeAddresses | null>(null)
@@ -147,47 +148,143 @@ export function Provider({ children }: Props): ReactElement {
const latestUserVersion = semver.coerce(debugApiHealth?.version)?.version const latestUserVersion = semver.coerce(debugApiHealth?.version)?.version
const latestUserVersionExact = debugApiHealth?.version const latestUserVersionExact = debugApiHealth?.version
useEffect(() => {
setIsLoading(true)
setApiHealth(false)
refresh()
}, [beeApi])
useEffect(() => {
setIsLoading(true)
setDebugApiHealth(null)
setNodeAddresses(null)
setNodeTopology(null)
setPeers(null)
setChequebookAddress(null)
setChequebookBalance(null)
setPeerBalances(null)
setPeerCheques(null)
setSettlements(null)
refresh()
}, [beeDebugApi])
const refresh = async () => { const refresh = async () => {
// Don't want to refresh when already refreshing // Don't want to refresh when already refreshing
if (isRefreshing) return if (isRefreshing) return
// Not a valid bee api
if (!beeApi || !beeDebugApi) {
setIsLoading(false)
return
}
try { try {
setIsRefreshing(true) setIsRefreshing(true)
setError(null)
setApiHealth(await beeApi.status.health()) // Wrap the chequebook balance call to return BZZ values as Token object
setDebugApiHealth(await beeDebugApi.status.nodeHealth()) const chequeBalanceWrapper = async () => {
setNodeAddresses(await beeDebugApi.connectivity.addresses()) const { totalBalance, availableBalance } = await beeDebugApi.getChequebookBalance()
setNodeTopology(await beeDebugApi.connectivity.topology())
setChequebookAddress(await beeDebugApi.chequebook.address())
setPeers(await beeDebugApi.connectivity.listPeers())
const { totalBalance, availableBalance } = await beeDebugApi.chequebook.balance() return {
setChequebookBalance({ totalBalance: new Token(totalBalance),
totalBalance: new Token(totalBalance), availableBalance: new Token(availableBalance),
availableBalance: new Token(availableBalance), }
}) }
const { balances } = await beeDebugApi.balance.balances() // Wrap the balances call to return BZZ values as Token object
setPeerBalances(balances.map(({ peer, balance }) => ({ peer, balance: new Token(balance) }))) const peerBalanceWrapper = async () => {
const { balances } = await beeDebugApi.getAllBalances()
setPeerCheques(await beeDebugApi.chequebook.getLastCheques()) return balances.map(({ peer, balance }) => ({ peer, balance: new Token(balance) }))
const { totalReceived, settlements, totalSent } = await beeDebugApi.settlements.getSettlements() }
setSettlements({
totalReceived: new Token(totalReceived),
totalSent: new Token(totalSent),
settlements: settlements.map(({ peer, received, sent }) => ({
peer,
received: new Token(received),
sent: new Token(sent),
})),
})
setLastUpdate(Date.now()) // Wrap the settlements call to return BZZ values as Token object
const settlementsWrapper = async () => {
const { totalReceived, settlements, totalSent } = await beeDebugApi.getAllSettlements()
return {
totalReceived: new Token(totalReceived),
totalSent: new Token(totalSent),
settlements: settlements.map(({ peer, received, sent }) => ({
peer,
received: new Token(received),
sent: new Token(sent),
})),
}
}
const promises = [
// API health
beeApi
.isConnected()
.then(setApiHealth)
.catch(() => setApiHealth(false)),
// Debug API health
beeDebugApi
.getHealth()
.then(setDebugApiHealth)
.catch(() => setDebugApiHealth(null)),
// Node Addresses
beeDebugApi
.getNodeAddresses()
.then(setNodeAddresses)
.catch(() => setNodeAddresses(null)),
// Network Topology
beeDebugApi
.getTopology()
.then(setNodeTopology)
.catch(() => setNodeTopology(null)),
// Peers
beeDebugApi
.getPeers()
.then(setPeers)
.catch(() => setPeers(null)),
// Chequebook address
beeDebugApi
.getChequebookAddress()
.then(setChequebookAddress)
.catch(() => setChequebookAddress(null)),
// Cheques
beeDebugApi
.getLastCheques()
.then(setPeerCheques)
.catch(() => setPeerCheques(null)),
// Chequebook balance
chequeBalanceWrapper()
.then(setChequebookBalance)
.catch(() => setChequebookBalance(null)),
// Peer balances
peerBalanceWrapper()
.then(setPeerBalances)
.catch(() => setPeerBalances(null)),
// Settlements
settlementsWrapper()
.then(setSettlements)
.catch(() => setSettlements(null)),
]
await Promise.allSettled(promises)
} catch (e) { } catch (e) {
setError(e) setError(e)
} finally { } finally {
setIsLoading(false) setIsLoading(false)
setIsRefreshing(false) setIsRefreshing(false)
setLastUpdate(Date.now())
} }
} }
@@ -204,7 +301,7 @@ export function Provider({ children }: Props): ReactElement {
return () => clearInterval(interval) return () => clearInterval(interval)
} }
}, [frequency]) }, [frequency, beeDebugApi, beeApi])
return ( return (
<Context.Provider <Context.Provider
+59
View File
@@ -0,0 +1,59 @@
import { createContext, ReactChild, ReactElement, useState, useEffect } from 'react'
import { Bee, BeeDebug } from '@ethersphere/bee-js'
interface ContextInterface {
apiUrl: string
apiDebugUrl: string
beeApi: Bee | null
beeDebugApi: BeeDebug | null
setApiUrl: (url: string) => void
setDebugApiUrl: (url: string) => void
}
const initialValues: ContextInterface = {
apiUrl: sessionStorage.getItem('api_host') || process.env.REACT_APP_BEE_HOST || 'http://localhost:1633',
apiDebugUrl:
sessionStorage.getItem('debug_api_host') || process.env.REACT_APP_BEE_DEBUG_HOST || 'http://localhost:1635',
beeApi: null,
beeDebugApi: null,
setApiUrl: (url: string) => {}, // eslint-disable-line
setDebugApiUrl: (url: string) => {}, // eslint-disable-line
}
export const Context = createContext<ContextInterface>(initialValues)
export const Consumer = Context.Consumer
interface Props {
children: ReactChild
}
export function Provider({ children }: Props): ReactElement {
const [apiUrl, setApiUrl] = useState<string>(initialValues.apiUrl)
const [apiDebugUrl, setDebugApiUrl] = useState<string>(initialValues.apiDebugUrl)
const [beeApi, setBeeApi] = useState<Bee | null>(null)
const [beeDebugApi, setBeeDebugApi] = useState<BeeDebug | null>(null)
useEffect(() => {
try {
setBeeApi(new Bee(apiUrl))
sessionStorage.setItem('api_host', apiUrl)
} catch (e) {
setBeeApi(null)
}
}, [apiUrl])
useEffect(() => {
try {
setBeeDebugApi(new BeeDebug(apiDebugUrl))
sessionStorage.setItem('debug_api_host', apiDebugUrl)
} catch (e) {
setBeeDebugApi(null)
}
}, [apiDebugUrl])
return (
<Context.Provider value={{ apiUrl, apiDebugUrl, beeApi, beeDebugApi, setApiUrl, setDebugApiUrl }}>
{children}
</Context.Provider>
)
}
+6 -3
View File
@@ -1,6 +1,6 @@
import { PostageBatch } from '@ethersphere/bee-js' import { PostageBatch } from '@ethersphere/bee-js'
import { createContext, ReactChild, ReactElement, useEffect, useState } from 'react' import { createContext, ReactChild, ReactElement, useEffect, useState, useContext } from 'react'
import { beeApi } from '../services/bee' import { Context as SettingsContext } from './Settings'
export interface EnrichedPostageBatch extends PostageBatch { export interface EnrichedPostageBatch extends PostageBatch {
usage: number usage: number
@@ -48,6 +48,7 @@ function enrichStamp(postageBatch: PostageBatch): EnrichedPostageBatch {
} }
export function Provider({ children }: Props): ReactElement { export function Provider({ children }: Props): ReactElement {
const { beeApi } = useContext(SettingsContext)
const [stamps, setStamps] = useState<EnrichedPostageBatch[] | null>(initialValues.stamps) const [stamps, setStamps] = useState<EnrichedPostageBatch[] | null>(initialValues.stamps)
const [error, setError] = useState<Error | null>(initialValues.error) const [error, setError] = useState<Error | null>(initialValues.error)
const [isLoading, setIsLoading] = useState<boolean>(initialValues.isLoading) const [isLoading, setIsLoading] = useState<boolean>(initialValues.isLoading)
@@ -58,9 +59,11 @@ export function Provider({ children }: Props): ReactElement {
// Don't want to refresh when already refreshing // Don't want to refresh when already refreshing
if (isLoading) return if (isLoading) return
if (!beeApi) return
try { try {
setIsLoading(true) setIsLoading(true)
const stamps = await beeApi.stamps.getPostageStamps() const stamps = await beeApi.getAllPostageBatch()
setStamps(stamps.map(enrichStamp)) setStamps(stamps.map(enrichStamp))
setLastUpdate(Date.now()) setLastUpdate(Date.now())
-109
View File
@@ -1,109 +0,0 @@
import {
Address,
AllSettlements,
BalanceResponse,
Bee,
BeeDebug,
ChequebookAddressResponse,
ChequebookBalanceResponse,
Data,
FileData,
Health,
LastCashoutActionResponse,
LastChequesForPeerResponse,
LastChequesResponse,
NodeAddresses,
Peer,
PingResponse,
PostageBatch,
PostageBatchOptions,
Reference,
Topology,
} from '@ethersphere/bee-js'
import { apiHost, debugApiHost } from '../constants'
const beeJSClient = () => new Bee(apiHost)
const beeJSDebugClient = () => new BeeDebug(debugApiHost)
export const beeApi = {
status: {
health(): Promise<boolean> {
return beeJSClient().isConnected()
},
},
files: {
uploadFile(postageBatchId: Address, file: File): Promise<Reference> {
return beeJSClient().uploadFile(postageBatchId, file)
},
downloadFile(hash: string | Reference): Promise<FileData<Data>> {
return beeJSClient().downloadFile(hash)
},
},
stamps: {
getPostageStamps(): Promise<PostageBatch[]> {
return beeJSClient().getAllPostageBatch()
},
buyPostageStamp(amount: bigint, depth: number, options: PostageBatchOptions = {}): Promise<Address> {
return beeJSClient().createPostageBatch(amount.toString(), depth, options)
},
},
}
export const beeDebugApi = {
status: {
nodeHealth(): Promise<Health> {
return beeJSDebugClient().getHealth()
},
},
connectivity: {
addresses(): Promise<NodeAddresses> {
return beeJSDebugClient().getNodeAddresses()
},
listPeers(): Promise<Peer[]> {
return beeJSDebugClient().getPeers()
},
topology(): Promise<Topology> {
return beeJSDebugClient().getTopology()
},
ping(peerId: string): Promise<PingResponse> {
return beeJSDebugClient().pingPeer(peerId)
},
},
balance: {
balances(): Promise<BalanceResponse> {
return beeJSDebugClient().getAllBalances()
},
},
chequebook: {
address(): Promise<ChequebookAddressResponse> {
return beeJSDebugClient().getChequebookAddress()
},
balance(): Promise<ChequebookBalanceResponse> {
return beeJSDebugClient().getChequebookBalance()
},
getLastCheques(): Promise<LastChequesResponse> {
return beeJSDebugClient().getLastCheques()
},
peerCashout(peerId: string): Promise<string> {
return beeJSDebugClient().cashoutLastCheque(peerId)
},
getPeerLastCashout(peerId: string): Promise<LastCashoutActionResponse> {
return beeJSDebugClient().getLastCashoutAction(peerId)
},
getPeerLastCheques(peerId: string): Promise<LastChequesForPeerResponse> {
return beeJSDebugClient().getLastChequesForPeer(peerId)
},
withdraw(amount: bigint): Promise<string> {
return beeJSDebugClient().withdrawTokens(amount.toString())
},
deposit(amount: bigint): Promise<string> {
return beeJSDebugClient().depositTokens(amount.toString())
},
},
settlements: {
getSettlements(): Promise<AllSettlements> {
return beeJSDebugClient().getAllSettlements()
},
},
}