Compare commits

..

14 Commits

Author SHA1 Message Date
bee-worker 1f8f890ff7 chore: release 0.6.0 (#167) 2021-08-24 18:58:13 +02:00
Vojtech Simetka f9ea9948f0 chore: update to latest bee version (#182) 2021-08-24 13:42:19 +02:00
Vojtech Simetka 2b120e44ca fix: remove nested ternary operator and simplify ping peer functionality (#181)
* fix: remove nested ternary operator and simplify ping peer functionality

* chore: arrow function in the setPeerLatency
2021-08-23 19:27:55 +02:00
Vojtech Simetka 0df15d6109 chore: renamed WDModal to WithdrawDepositModal (#178)
* chore: renamed WDModal to WithdrawDepositModal

* chore: change the name of inports to WithdrawDepositModal as well
2021-08-23 16:02:24 +02:00
Vojtech Simetka 56df3a2561 feat: remove the last update component (#179) 2021-08-23 16:00:22 +02:00
Vojtech Simetka 7f2ff39ec9 chore: removed unnecessary and unused vars (#177) 2021-08-23 15:57:54 +02:00
Vojtech Simetka 739fc45500 chore: remove refresh button on node setup (#174) 2021-08-23 15:55:22 +02:00
Vojtech Simetka d6d03bf7c6 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
2021-08-20 15:14:14 +02:00
Vojtech Simetka 2624cf04c9 feat: bee provider caching the state of the app and refreshing periodically (#172)
* feat: bee provider caching the state of the app and refreshing periodically

* chore: added error handling
2021-08-18 11:10:12 +02:00
Vojtech Simetka dcec6e0188 fix: enum index for supported platforms (#170) 2021-08-16 11:14:34 +02:00
Cafe137 480f6dc7f9 feat: add tooltips and health indicator to peers (#169)
* feat: add value thresholds and explanations to topology stats

* feat: extract title and row, refactor threshold, add tooltip, add overall health

* refactor: clean up code

* refactor: reword Node to Bee node
2021-08-16 11:12:42 +02:00
Cafe137 a62243fe5c feat: add retry to accounting (#166)
* feat: add retry to accounting

* fix: fix off by one bug in retry logic

* docs: add jsdocs to new utility functions

* style: rename DepositModal to CheckoutModal
2021-08-12 14:40:33 +02:00
Vojtech Simetka ec42eafc2b feat: synchronized platform tabs (#165)
* feat: synchronized platform tabs

* chore: rename enums to pascal case

* chore: fixed typo
2021-08-11 19:46:09 +02:00
Adam Uhlíř f90778d338 docs: update maintainers (#164) 2021-08-11 08:17:46 +02:00
51 changed files with 1198 additions and 1314 deletions
+18
View File
@@ -1,5 +1,23 @@
# Changelog
## [0.6.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.5.0...v0.6.0) (2021-08-24)
### Features
* add retry to accounting ([#166](https://www.github.com/ethersphere/bee-dashboard/issues/166)) ([a62243f](https://www.github.com/ethersphere/bee-dashboard/commit/a62243fe5c45b7dd9be6e92f82ebdf0b64bd8f0d))
* add tooltips and health indicator to peers ([#169](https://www.github.com/ethersphere/bee-dashboard/issues/169)) ([480f6dc](https://www.github.com/ethersphere/bee-dashboard/commit/480f6dc7f9c58a4aae87e0dea7082a4bd3dc900b))
* bee provider caching the state of the app and refreshing periodically ([#172](https://www.github.com/ethersphere/bee-dashboard/issues/172)) ([2624cf0](https://www.github.com/ethersphere/bee-dashboard/commit/2624cf04c939e87f025c1f4ff417808073742dab))
* changing API urls does not need the app refresh ([#173](https://www.github.com/ethersphere/bee-dashboard/issues/173)) ([d6d03bf](https://www.github.com/ethersphere/bee-dashboard/commit/d6d03bf7c6d2705de22f43825b85b32c2f0181fb))
* remove the last update component ([#179](https://www.github.com/ethersphere/bee-dashboard/issues/179)) ([56df3a2](https://www.github.com/ethersphere/bee-dashboard/commit/56df3a2561c3c00237b5d107eb054403af3012f8))
* synchronized platform tabs ([#165](https://www.github.com/ethersphere/bee-dashboard/issues/165)) ([ec42eaf](https://www.github.com/ethersphere/bee-dashboard/commit/ec42eafc2b768ba06649f628c733e8d3440fdcaf))
### Bug Fixes
* enum index for supported platforms ([#170](https://www.github.com/ethersphere/bee-dashboard/issues/170)) ([dcec6e0](https://www.github.com/ethersphere/bee-dashboard/commit/dcec6e01887465c74a68feede52b476791bbefa7))
* remove nested ternary operator and simplify ping peer functionality ([#181](https://www.github.com/ethersphere/bee-dashboard/issues/181)) ([2b120e4](https://www.github.com/ethersphere/bee-dashboard/commit/2b120e44ca5e01451cc43e362195c04587836a03))
## [0.5.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.4.0...v0.5.0) (2021-08-09)
+1 -1
View File
@@ -1 +1 @@
* nugaon vojtechsimetka
* @Cafe137 @vojtechsimetka
+3 -1
View File
@@ -84,8 +84,10 @@ There are some ways you can make this module better:
## Maintainers
- [nugaon](https://github.com/nugaon)
- [vojtechsimetka](https://github.com/vojtechsimetka)
- [Cafe137](https://github.com/Cafe137)
See what "Maintainer" means [here](https://github.com/ethersphere/repo-maintainer).
## License
+29 -25
View File
@@ -1,15 +1,15 @@
{
"name": "@ethersphere/bee-dashboard",
"version": "0.5.0",
"version": "0.6.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ethersphere/bee-dashboard",
"version": "0.5.0",
"version": "0.6.0",
"license": "BSD-3-Clause",
"dependencies": {
"@ethersphere/bee-js": "^1.0.0",
"@ethersphere/bee-js": "^1.2.0",
"@material-ui/core": "^4.11.4",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.57",
@@ -1460,19 +1460,19 @@
}
},
"node_modules/@ethersphere/bee-js": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@ethersphere/bee-js/-/bee-js-1.0.0.tgz",
"integrity": "sha512-u+TnKf0loAQba7AfZ54kXIEWjNdqVMUR6NXwINhHa2IdoD2b52rcuP96eMImlUm4tEJkxi2yAS+lcIZ2U9/1Vw==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ethersphere/bee-js/-/bee-js-1.2.0.tgz",
"integrity": "sha512-RtRsTOq1ZtftT//0ibPWjohIwMOgJ/4KhMynQ1yaroih6G6krAnXve2K/hZtyWA9d7dpLxcT4aKd9S4QmISOgA==",
"dependencies": {
"axios": "^0.21.1",
"elliptic": "^6.5.4",
"isomorphic-ws": "^4.0.1",
"js-sha3": "^0.8.0",
"tar-js": "^0.3.0",
"ws": "^7.4.4"
"ws": "^7.5.0"
},
"engines": {
"bee": "1.0.0-2572fa48",
"bee": "1.1.0-80cdea19",
"node": ">=12.0.0",
"npm": ">=6.0.0"
}
@@ -4776,10 +4776,14 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001191",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001191.tgz",
"integrity": "sha512-xJJqzyd+7GCJXkcoBiQ1GuxEiOBCLQ0aVW9HMekifZsAVGdj5eJ4mFB9fEhSHipq9IOk/QXFJUiIr9lZT+EsGw==",
"dev": true
"version": "1.0.30001251",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001251.tgz",
"integrity": "sha512-HOe1r+9VkU4TFmnU70z+r7OLmtR+/chB1rdcJUeQlAinjEeb0cKL20tlAtOagNZhbrtLnCvV19B4FmF1rgzl6A==",
"dev": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
}
},
"node_modules/capture-exit": {
"version": "2.0.0",
@@ -21434,9 +21438,9 @@
}
},
"node_modules/ws": {
"version": "7.4.5",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz",
"integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==",
"version": "7.5.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz",
"integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==",
"engines": {
"node": ">=8.3.0"
},
@@ -22895,16 +22899,16 @@
}
},
"@ethersphere/bee-js": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@ethersphere/bee-js/-/bee-js-1.0.0.tgz",
"integrity": "sha512-u+TnKf0loAQba7AfZ54kXIEWjNdqVMUR6NXwINhHa2IdoD2b52rcuP96eMImlUm4tEJkxi2yAS+lcIZ2U9/1Vw==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ethersphere/bee-js/-/bee-js-1.2.0.tgz",
"integrity": "sha512-RtRsTOq1ZtftT//0ibPWjohIwMOgJ/4KhMynQ1yaroih6G6krAnXve2K/hZtyWA9d7dpLxcT4aKd9S4QmISOgA==",
"requires": {
"axios": "^0.21.1",
"elliptic": "^6.5.4",
"isomorphic-ws": "^4.0.1",
"js-sha3": "^0.8.0",
"tar-js": "^0.3.0",
"ws": "^7.4.4"
"ws": "^7.5.0"
}
},
"@hapi/address": {
@@ -25776,9 +25780,9 @@
}
},
"caniuse-lite": {
"version": "1.0.30001191",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001191.tgz",
"integrity": "sha512-xJJqzyd+7GCJXkcoBiQ1GuxEiOBCLQ0aVW9HMekifZsAVGdj5eJ4mFB9fEhSHipq9IOk/QXFJUiIr9lZT+EsGw==",
"version": "1.0.30001251",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001251.tgz",
"integrity": "sha512-HOe1r+9VkU4TFmnU70z+r7OLmtR+/chB1rdcJUeQlAinjEeb0cKL20tlAtOagNZhbrtLnCvV19B4FmF1rgzl6A==",
"dev": true
},
"capture-exit": {
@@ -39876,9 +39880,9 @@
}
},
"ws": {
"version": "7.4.5",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz",
"integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==",
"version": "7.5.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz",
"integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==",
"requires": {}
},
"xml-name-validator": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@ethersphere/bee-dashboard",
"version": "0.5.0",
"version": "0.6.0",
"description": "An app which helps users to setup their Bee node and do actions like cash out cheques",
"keywords": [
"bee",
@@ -24,7 +24,7 @@
"url": "https://github.com/ethersphere/bee-dashboard.git"
},
"dependencies": {
"@ethersphere/bee-js": "^1.0.0",
"@ethersphere/bee-js": "^1.2.0",
"@material-ui/core": "^4.11.4",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.57",
+19 -10
View File
@@ -9,6 +9,9 @@ import { SnackbarProvider } from 'notistack'
import BaseRouter from './routes/routes'
import { lightTheme, darkTheme } from './theme'
import { Provider as StampsProvider } from './providers/Stamps'
import { Provider as PlatformProvider } from './providers/Platform'
import { Provider as BeeProvider } from './providers/Bee'
import { Provider as SettingsProvider } from './providers/Settings'
const App = (): ReactElement => {
const [themeMode, toggleThemeMode] = useState('light')
@@ -35,16 +38,22 @@ const App = (): ReactElement => {
return (
<div className="App">
<ThemeProvider theme={themeMode === 'light' ? lightTheme : darkTheme}>
<StampsProvider>
<SnackbarProvider>
<>
<CssBaseline />
<Router>
<BaseRouter />
</Router>
</>
</SnackbarProvider>
</StampsProvider>
<SettingsProvider>
<BeeProvider>
<StampsProvider>
<PlatformProvider>
<SnackbarProvider>
<>
<CssBaseline />
<Router>
<BaseRouter />
</Router>
</>
</SnackbarProvider>
</PlatformProvider>
</StampsProvider>
</BeeProvider>
</SettingsProvider>
</ThemeProvider>
</div>
)
+8 -8
View File
@@ -1,10 +1,10 @@
import { ReactElement, useState } from 'react'
import { ReactElement, useState, useContext } from 'react'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import { Alert, AlertTitle } from '@material-ui/lab'
import Collapse from '@material-ui/core/Collapse'
import IconButton from '@material-ui/core/IconButton'
import CloseIcon from '@material-ui/icons/Close'
import { useStatusNodeVersion } from '../hooks/status'
import { Context } from '../providers/Bee'
import { SUPPORTED_BEE_VERSION_EXACT } from '@ethersphere/bee-js'
const useStyles = makeStyles((theme: Theme) =>
@@ -18,12 +18,12 @@ const useStyles = makeStyles((theme: Theme) =>
export default function VersionAlert(): ReactElement | null {
const classes = useStyles()
const { isLoading, userVersion } = useStatusNodeVersion()
const { isLoading, latestUserVersionExact } = useContext(Context)
const [open, setOpen] = useState<boolean>(true)
const isExactlySupportedBeeVersion = SUPPORTED_BEE_VERSION_EXACT === userVersion
const isExactlySupportedBeeVersion = SUPPORTED_BEE_VERSION_EXACT === latestUserVersionExact
if (isLoading || !userVersion) return null
if (isLoading || !latestUserVersionExact) return null
return (
<Collapse in={!isExactlySupportedBeeVersion && open}>
@@ -44,9 +44,9 @@ export default function VersionAlert(): ReactElement | null {
}
>
<AlertTitle>Warning</AlertTitle>
Your Bee node version (<code>{userVersion}</code>) does not exactly match the Bee version we tested the Bee
Dashboard against (<code>{SUPPORTED_BEE_VERSION_EXACT}</code>). Please note that some functionality may not
work properly.
Your Bee node version (<code>{latestUserVersionExact}</code>) does not exactly match the Bee version we tested
the Bee Dashboard against (<code>{SUPPORTED_BEE_VERSION_EXACT}</code>). Please note that some functionality
may not work properly.
</Alert>
</div>
</Collapse>
+9 -8
View File
@@ -1,15 +1,13 @@
import { ReactElement, useState } from 'react'
import { CircularProgress, Container } from '@material-ui/core'
import Button from '@material-ui/core/Button'
import Dialog from '@material-ui/core/Dialog'
import DialogActions from '@material-ui/core/DialogActions'
import DialogContent from '@material-ui/core/DialogContent'
import DialogContentText from '@material-ui/core/DialogContentText'
import DialogTitle from '@material-ui/core/DialogTitle'
import { Container, CircularProgress } from '@material-ui/core'
import { useSnackbar } from 'notistack'
import { beeDebugApi } from '../services/bee'
import { ReactElement, useState, useContext } from 'react'
import { Context as SettingsContext } from '../providers/Settings'
import EthereumAddress from './EthereumAddress'
interface Props {
@@ -17,10 +15,11 @@ interface Props {
uncashedAmount: string
}
export default function DepositModal({ peerId, uncashedAmount }: Props): ReactElement {
export default function CheckoutModal({ peerId, uncashedAmount }: Props): ReactElement {
const [open, setOpen] = useState<boolean>(false)
const [loadingCashout, setLoadingCashout] = useState<boolean>(false)
const { enqueueSnackbar } = useSnackbar()
const { beeDebugApi } = useContext(SettingsContext)
const handleClickOpen = () => {
setOpen(true)
@@ -31,10 +30,12 @@ export default function DepositModal({ peerId, uncashedAmount }: Props): ReactEl
}
const handleCashout = () => {
if (!beeDebugApi) return
if (peerId) {
setLoadingCashout(true)
beeDebugApi.chequebook
.peerCashout(peerId)
beeDebugApi
.cashoutLastCheque(peerId)
.then(res => {
setOpen(false)
enqueueSnackbar(
+18 -134
View File
@@ -1,13 +1,7 @@
import React, { ReactElement, useEffect } from 'react'
import { withStyles, Theme, createStyles } from '@material-ui/core/styles'
import { Tabs, Tab, Box, Typography } from '@material-ui/core'
import { ReactElement, useContext } from 'react'
import TabsContainer from './TabsContainer'
import CodeBlock from './CodeBlock'
interface TabPanelProps {
children?: React.ReactNode
index: number
value: number
}
import { Context } from '../providers/Platform'
interface Props {
linux: string
@@ -15,133 +9,23 @@ interface Props {
showLineNumbers?: boolean
}
function a11yProps(index: number) {
return {
id: `simple-tab-${index}`,
'aria-controls': `simple-tabpanel-${index}`,
}
}
function getOS() {
const userAgent = window.navigator.userAgent
const platform = window.navigator.platform
const macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K']
const windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE']
const iosPlatforms = ['iPhone', 'iPad', 'iPod']
if (macosPlatforms.includes(platform)) return 'macOS'
if (iosPlatforms.includes(platform)) return 'iOS'
if (windowsPlatforms.includes(platform)) return 'windows'
if (/Android/.test(userAgent)) return 'android'
if (/Linux/.test(platform)) return 'linux'
return null
}
export default function CodeBlockTabs(props: Props): ReactElement {
const [value, setValue] = React.useState(0)
const handleChange = (event: React.ChangeEvent<unknown>, newValue: number) => {
setValue(newValue)
}
useEffect(() => {
const os = getOS()
if (os === 'windows') {
setValue(0)
} else if (os === 'linux') {
setValue(0)
} else if (os === 'macOS') {
setValue(1)
}
}, [])
function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<Box style={{ marginTop: '-12px' }}>
<Typography component="div">{children}</Typography>
</Box>
)}
</div>
)
}
const AntTabs = withStyles({
root: {
borderBottom: '1px solid #e8e8e8',
},
indicator: {
backgroundColor: '#3f51b5',
},
})(Tabs)
interface StyledTabProps {
label: string
}
const AntTab = withStyles((theme: Theme) =>
createStyles({
root: {
textTransform: 'none',
minWidth: 72,
backgroundColor: 'transparent',
fontWeight: theme.typography.fontWeightRegular,
marginRight: theme.spacing(4),
fontFamily: [
'-apple-system',
'BlinkMacSystemFont',
'"Segoe UI"',
'Roboto',
'"Helvetica Neue"',
'Arial',
'sans-serif',
'"Apple Color Emoji"',
'"Segoe UI Emoji"',
'"Segoe UI Symbol"',
].join(','),
'&:hover': {
color: '#3f51b5',
opacity: 1,
},
'&$selected': {
color: '#3f51b5',
fontWeight: theme.typography.fontWeightMedium,
},
'&:focus': {
color: '#3f51b5',
},
},
selected: {},
}),
)((props: StyledTabProps) => <Tab disableRipple {...props} />)
const { platform, setPlatform } = useContext(Context)
return (
<div>
<AntTabs style={{ marginTop: '12px' }} value={value} onChange={handleChange} aria-label="ant example">
<AntTab label="Linux" {...a11yProps(0)} />
<AntTab label="MacOS" {...a11yProps(1)} />
</AntTabs>
<TabPanel value={value} index={0}>
<CodeBlock showLineNumbers={props.showLineNumbers} language="bash" code={props.linux} />
</TabPanel>
<TabPanel value={value} index={1}>
<CodeBlock showLineNumbers={props.showLineNumbers} language="bash" code={props.mac} />
</TabPanel>
</div>
<TabsContainer
index={platform}
indexChanged={setPlatform}
values={[
{
label: 'Linux',
component: <CodeBlock showLineNumbers={props.showLineNumbers} language="bash" code={props.linux} />,
},
{
label: 'macOS',
component: <CodeBlock showLineNumbers={props.showLineNumbers} language="bash" code={props.mac} />,
},
]}
/>
)
}
+28 -40
View File
@@ -1,62 +1,50 @@
import React, { ReactElement, useState } from 'react'
import { TextField, Button, CircularProgress, Container } from '@material-ui/core'
import { TextField, Button } from '@material-ui/core'
interface Props {
defaultHost?: string
hostName: string
setHost: (host: string) => void
}
export default function ConnectToHost(props: Props): ReactElement {
const [hostInputVisible, toggleHostInputVisibility] = useState(false)
const [connectingToHost, setConnectingToHost] = useState(false)
const [host, setHost] = useState('')
const handleNewHostConnection = () => {
if (host) {
setConnectingToHost(true)
sessionStorage.setItem(props.hostName, host)
props.setHost(host)
toggleHostInputVisibility(!hostInputVisible)
window.location.reload()
}
}
return (
<div>
{
// FIXME: this should be broken up
/* eslint-disable no-nested-ternary */
hostInputVisible ? (
<div style={{ display: 'flex' }}>
<TextField
defaultValue={props.defaultHost}
label="Enter host"
variant="outlined"
size="small"
onChange={e => setHost(e.target.value)}
style={{ marginRight: '15px', minWidth: '300px' }}
/>
<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
{hostInputVisible ? (
<div style={{ display: 'flex' }}>
<TextField
defaultValue={props.defaultHost}
label="Enter host"
variant="outlined"
size="small"
onChange={e => setHost(e.target.value)}
style={{ marginRight: '15px', minWidth: '300px' }}
/>
<Button onClick={() => handleNewHostConnection()} size="small" variant="outlined">
Connect
</Button>
)
/* eslint-enable no-nested-ternary */
}
<Button
style={{ marginLeft: '7px' }}
onClick={() => toggleHostInputVisibility(!hostInputVisible)}
size="small"
>
Cancel
</Button>
</div>
) : (
<Button onClick={() => toggleHostInputVisibility(!hostInputVisible)} size="small" variant="outlined">
Change host
</Button>
)}
</div>
)
}
+17 -33
View File
@@ -4,7 +4,6 @@ import { createStyles, makeStyles } from '@material-ui/core/styles'
import { Card, CardContent, Typography } from '@material-ui/core/'
import EthereumAddress from '../components/EthereumAddress'
import { Skeleton } from '@material-ui/lab'
import type { ChequebookAddressResponse, NodeAddresses } from '@ethersphere/bee-js'
@@ -28,9 +27,7 @@ const useStyles = makeStyles(() =>
interface Props {
nodeAddresses: NodeAddresses | null
isLoadingNodeAddresses: boolean
chequebookAddress: ChequebookAddressResponse | null
isLoadingChequebookAddress: boolean
}
function EthereumAddressCard(props: Props): ReactElement {
@@ -38,36 +35,23 @@ function EthereumAddressCard(props: Props): ReactElement {
return (
<Card className={classes.root}>
{props.isLoadingNodeAddresses ? (
<div style={{ padding: '16px' }}>
<Skeleton width={300} height={30} animation="wave" />
<Skeleton width={300} height={50} animation="wave" />
</div>
) : (
<div className={classes.details}>
<CardContent className={classes.content}>
<Typography variant="subtitle1" gutterBottom>
Ethereum Address
</Typography>
<EthereumAddress address={props.nodeAddresses?.ethereum} />
</CardContent>
</div>
)}
{props.isLoadingChequebookAddress ? (
<div style={{ padding: '16px' }}>
<Skeleton width={300} height={30} animation="wave" />
<Skeleton width={300} height={50} animation="wave" />
</div>
) : (
<div className={classes.details}>
<CardContent className={classes.content}>
<Typography variant="subtitle1" gutterBottom>
Chequebook Contract Address
</Typography>
<EthereumAddress address={props.chequebookAddress?.chequebookAddress} />
</CardContent>
</div>
)}
<div className={classes.details}>
<CardContent className={classes.content}>
<Typography variant="subtitle1" gutterBottom>
Ethereum Address
</Typography>
<EthereumAddress address={props.nodeAddresses?.ethereum} />
</CardContent>
</div>
<div className={classes.details}>
<CardContent className={classes.content}>
<Typography variant="subtitle1" gutterBottom>
Chequebook Contract Address
</Typography>
<EthereumAddress address={props.chequebookAddress?.chequebookAddress} />
</CardContent>
</div>
</Card>
)
}
-23
View File
@@ -1,23 +0,0 @@
import { ReactElement, useEffect, useState } from 'react'
interface Props {
date: number | null
}
export default function LastUpdate({ date }: Props): ReactElement {
const [duration, setDuration] = useState<string>('never')
const refresh = () => {
if (!date) setDuration('never')
else setDuration(`${((Date.now() - date) / 1000).toFixed()} seconds ago`)
}
useEffect(() => {
refresh()
const i = setInterval(refresh, 1000)
return () => clearInterval(i)
}, [date])
return <span>Last Update: {duration}</span>
}
+6 -13
View File
@@ -1,28 +1,23 @@
import type { ReactElement } from 'react'
import { makeStyles } from '@material-ui/core/styles'
import { Card, CardContent, Typography } from '@material-ui/core/'
import { makeStyles } from '@material-ui/core/styles'
import { Skeleton } from '@material-ui/lab'
import type { ReactElement } from 'react'
import { Title } from './Title'
const useStyles = makeStyles({
root: {
minWidth: 275,
},
title: {
fontSize: 16,
},
pos: {
marginBottom: 12,
},
})
interface Props {
label: string
statistic?: string
loading?: boolean
tooltip?: string
}
export default function StatCard({ loading, label, statistic }: Props): ReactElement {
export default function StatCard({ loading, label, statistic, tooltip }: Props): ReactElement {
const classes = useStyles()
return (
@@ -36,9 +31,7 @@ export default function StatCard({ loading, label, statistic }: Props): ReactEle
)}
{!loading && (
<>
<Typography className={classes.title} color="textSecondary" gutterBottom>
{label}
</Typography>
<Title label={label} tooltip={tooltip} />
<Typography variant="h5" component="h2">
{statistic}
</Typography>
+14 -15
View File
@@ -15,13 +15,7 @@ function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
<div role="tabpanel" hidden={value !== index} {...other}>
{value === index && (
<Box p={3}>
<Typography>{children}</Typography>
@@ -44,25 +38,30 @@ interface TabsValues {
interface Props {
values: TabsValues[]
index?: number
indexChanged?: (index: number) => void
}
export default function SimpleTabs({ values }: Props): ReactElement {
export default function SimpleTabs({ values, index, indexChanged }: Props): ReactElement {
const classes = useStyles()
const [value, setValue] = React.useState<number>(0)
const [value, setValue] = React.useState<number>(index || 0)
const handleChange = (event: React.ChangeEvent<Record<string, never>>, newValue: number) => {
setValue(newValue)
if (indexChanged) indexChanged(newValue)
else setValue(newValue)
}
const v = index !== undefined ? index : value
return (
<div className={classes.root}>
<Tabs value={value} onChange={handleChange} aria-label="simple tabs example">
{values.map(({ label }, index) => (
<Tab key={index} label={label} />
<Tabs value={v} onChange={handleChange}>
{values.map(({ label }, idx) => (
<Tab key={idx} label={label} />
))}
</Tabs>
{values.map(({ component }, index) => (
<TabPanel key={index} value={value} index={index}>
{values.map(({ component }, idx) => (
<TabPanel key={idx} value={v} index={idx}>
{component}
</TabPanel>
))}
+41
View File
@@ -0,0 +1,41 @@
import { Grid, Tooltip, Typography } from '@material-ui/core/'
import { makeStyles } from '@material-ui/core/styles'
import { Info } from '@material-ui/icons'
import type { ReactElement } from 'react'
interface TitleProps {
label: string
tooltip?: string
}
const useStyles = makeStyles({
title: {
fontSize: 16,
},
})
export function Title({ label, tooltip }: TitleProps): ReactElement {
const classes = useStyles()
if (!tooltip) {
return (
<Typography className={classes.title} color="textSecondary" gutterBottom>
{label}
</Typography>
)
}
// span is needed as Tooltip expects a non-functional element!
return (
<Tooltip title={tooltip}>
<span>
<Grid container direction="row" justify="space-between">
<Typography className={classes.title} color="textSecondary" gutterBottom>
{label}
</Typography>
<Info />
</Grid>
</span>
</Tooltip>
)
}
+45 -7
View File
@@ -1,25 +1,63 @@
import type { Topology } from '@ethersphere/bee-js'
import { Grid } from '@material-ui/core/'
import type { ReactElement } from 'react'
import { pickThreshold, ThresholdValues } from '../utils/threshold'
import StatCard from './StatCard'
interface Props {
isLoading: boolean
interface RootProps {
topology: Topology | null
error: Error | null // FIXME: should display error
}
const TopologyStats = ({ isLoading, topology }: Props): ReactElement => (
interface Props extends RootProps {
thresholds: ThresholdValues
}
const TopologyStats = (props: RootProps): ReactElement => {
const thresholds: ThresholdValues = {
connectedPeers: pickThreshold('connectedPeers', props.topology?.connected || 0),
population: pickThreshold('population', props.topology?.population || 0),
depth: pickThreshold('depth', props.topology?.depth || 0),
}
return (
<>
<Indicator {...props} thresholds={thresholds} />
<Metrics {...props} thresholds={thresholds} />
</>
)
}
const Indicator = ({ thresholds }: Props): ReactElement => {
const maximumTotalScore = Object.values(thresholds).reduce((sum, item) => sum + item.maximumScore, 0)
const actualTotalScore = Object.values(thresholds).reduce((sum, item) => sum + item.score, 0)
const percentageText = Math.round((actualTotalScore / maximumTotalScore) * 100) + '%'
return (
<div style={{ marginBottom: '20px' }}>
<StatCard label="Overall Health Indicator" statistic={percentageText} />
</div>
)
}
const Metrics = ({ topology, thresholds }: Props): ReactElement => (
<Grid style={{ marginBottom: '20px', flexGrow: 1 }}>
<Grid container spacing={3}>
<Grid key={1} item xs={12} sm={12} md={6} lg={4} xl={4}>
<StatCard label="Connected Peers" statistic={topology?.connected.toString()} loading={isLoading} />
<StatCard
label="Connected Peers"
statistic={topology?.connected.toString()}
tooltip={thresholds.connectedPeers.explanation}
/>
</Grid>
<Grid key={2} item xs={12} sm={12} md={6} lg={4} xl={4}>
<StatCard label="Population" statistic={topology?.population.toString()} loading={isLoading} />
<StatCard
label="Population"
statistic={topology?.population.toString()}
tooltip={thresholds.population.explanation}
/>
</Grid>
<Grid key={3} item xs={12} sm={12} md={6} lg={4} xl={4}>
<StatCard label="Depth" statistic={topology?.depth.toString()} loading={isLoading} />
<StatCard label="Depth" statistic={topology?.depth.toString()} tooltip={thresholds.depth.explanation} />
</Grid>
</Grid>
</Grid>
@@ -21,7 +21,7 @@ interface Props {
action: (amount: bigint) => Promise<string>
}
export default function WithdrawModal({
export default function WithdrawDepositModal({
successMessage,
errorMessage,
dialogMessage,
-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'
+11 -5
View File
@@ -1,18 +1,24 @@
import type { ReactElement } from 'react'
import { beeDebugApi } from '../services/bee'
import { ReactElement, useContext } from 'react'
import { Context as SettingsContext } from '../providers/Settings'
import WDModal from '../components/WDModal'
import WithdrawDepositModal from '../components/WithdrawDepositModal'
import { BigNumber } from 'bignumber.js'
export default function DepositModal(): ReactElement {
const { beeDebugApi } = useContext(SettingsContext)
return (
<WDModal
<WithdrawDepositModal
successMessage="Successful deposit."
errorMessage="Error with depositing"
dialogMessage="Specify the amount of BZZ you would like to withdraw from your node."
label="Deposit"
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())
}}
/>
)
}
+11 -5
View File
@@ -1,18 +1,24 @@
import type { ReactElement } from 'react'
import { beeDebugApi } from '../services/bee'
import { ReactElement, useContext } from 'react'
import { Context as SettingsContext } from '../providers/Settings'
import WDModal from '../components/WDModal'
import WithdrawDepositModal from '../components/WithdrawDepositModal'
import { BigNumber } from 'bignumber.js'
export default function WithdrawModal(): ReactElement {
const { beeDebugApi } = useContext(SettingsContext)
return (
<WDModal
<WithdrawDepositModal
successMessage="Successful withdrawl."
errorMessage="Error with withdrawing."
dialogMessage="Specify the amount of BZZ you would like to withdraw from your node."
label="Withdraw"
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())
}}
/>
)
}
+18 -27
View File
@@ -1,15 +1,11 @@
import { LastCashoutActionResponse } from '@ethersphere/bee-js'
import { LastCashoutActionResponse, BeeDebug } from '@ethersphere/bee-js'
import { useEffect, useState } from 'react'
import { Token } from '../models/Token'
import { beeDebugApi } from '../services/bee'
import { Balance, Settlement, useApiPeerBalances, useApiSettlements } from './apiHooks'
import { makeRetriablePromise, unwrapPromiseSettlements } from '../utils'
import { Balance, Settlements, Settlement } from '../types'
interface UseAccountingHook {
isLoading: boolean
isLoadingUncashed: boolean
error: Error | null
totalsent: Token
totalreceived: Token
accounting: Accounting[] | null
}
@@ -76,39 +72,34 @@ function mergeAccounting(
)
}
export const useAccounting = (): UseAccountingHook => {
const settlements = useApiSettlements()
const balances = useApiPeerBalances()
const [err, setErr] = useState<Error | null>(null)
export const useAccounting = (
beeDebugApi: BeeDebug | null,
settlements: Settlements | null,
balances: Balance[] | null,
): UseAccountingHook => {
const [isLoadingUncashed, setIsloadingUncashed] = useState<boolean>(false)
const [uncashedAmounts, setUncashedAmounts] = useState<LastCashoutActionResponse[] | undefined>(undefined)
const error = balances.error || settlements.error || err
useEffect(() => {
// 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)
const promises = settlements.settlements.settlements
const promises = settlements.settlements
.filter(({ received }) => received.toBigNumber.gt('0'))
.map(({ peer }) => beeDebugApi.chequebook.getPeerLastCashout(peer))
.map(({ peer }) => makeRetriablePromise(() => beeDebugApi.getLastCashoutAction(peer)))
Promise.all(promises)
.then(setUncashedAmounts)
.catch(setErr)
.finally(() => setIsloadingUncashed(false))
}, [settlements, isLoadingUncashed, uncashedAmounts, error])
Promise.allSettled(promises).then(settlements => {
const results = unwrapPromiseSettlements(settlements)
setUncashedAmounts(results.fulfilled)
setIsloadingUncashed(false)
})
}, [settlements, isLoadingUncashed, uncashedAmounts])
const accounting = mergeAccounting(balances.peerBalances, settlements.settlements?.settlements, uncashedAmounts)
const accounting = mergeAccounting(balances, settlements?.settlements, uncashedAmounts)
return {
isLoading: settlements.isLoadingSettlements || balances.isLoadingPeerBalances,
isLoadingUncashed,
error,
accounting,
totalsent: settlements.settlements?.totalSent || new Token('0'),
totalreceived: settlements.settlements?.totalReceived || new Token('0'),
}
}
-401
View File
@@ -1,406 +1,5 @@
import { useState, useEffect } from 'react'
import {
NodeAddresses,
ChequebookAddressResponse,
LastChequesResponse,
Health,
Peer,
Topology,
LastChequesForPeerResponse,
} from '@ethersphere/bee-js'
import { beeDebugApi, beeApi } from '../services/bee'
import axios from 'axios'
import { Token } from '../models/Token'
export interface HealthHook {
health: boolean
isLoadingHealth: boolean
error: Error | null
}
export const useApiHealth = (): HealthHook => {
const [health, setHealth] = useState<boolean>(false)
const [isLoadingHealth, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeApi.status
.health()
.then(res => {
setHealth(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { health, isLoadingHealth, error }
}
export interface DebugHealthHook {
nodeHealth: Health | null
isLoadingNodeHealth: boolean
error: Error | null
}
export const useDebugApiHealth = (): DebugHealthHook => {
const [nodeHealth, setNodeHealth] = useState<Health | null>(null)
const [isLoadingNodeHealth, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.status
.nodeHealth()
.then(res => {
setNodeHealth(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { nodeHealth, isLoadingNodeHealth, error }
}
export interface NodeAddressesHook {
nodeAddresses: NodeAddresses | null
isLoadingNodeAddresses: boolean
error: Error | null
}
export const useApiNodeAddresses = (): NodeAddressesHook => {
const [nodeAddresses, setNodeAddresses] = useState<NodeAddresses | null>(null)
const [isLoadingNodeAddresses, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.connectivity
.addresses()
.then(res => {
setNodeAddresses(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { nodeAddresses, isLoadingNodeAddresses, error }
}
export interface NodeTopologyHook {
topology: Topology | null
isLoading: boolean
error: Error | null
}
export const useApiNodeTopology = (): NodeTopologyHook => {
const [topology, setNodeTopology] = useState<Topology | null>(null)
const [isLoading, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.connectivity
.topology()
.then(res => {
setNodeTopology(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { topology, isLoading, error }
}
export interface ChequebookAddressHook {
chequebookAddress: ChequebookAddressResponse | null
isLoadingChequebookAddress: boolean
error: Error | null
}
export const useApiChequebookAddress = (): ChequebookAddressHook => {
const [chequebookAddress, setChequebookAddress] = useState<ChequebookAddressResponse | null>(null)
const [isLoadingChequebookAddress, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.chequebook
.address()
.then(res => {
setChequebookAddress(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { chequebookAddress, isLoadingChequebookAddress, error }
}
export interface NodePeersHook {
peers: Peer[] | null
isLoading: boolean
error: Error | null
}
export const useApiNodePeers = (): NodePeersHook => {
const [peers, setPeers] = useState<Peer[] | null>(null)
const [isLoading, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.connectivity
.listPeers()
.then(res => {
setPeers(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { peers, isLoading, error }
}
export interface ChequebookBalance {
totalBalance: Token
availableBalance: Token
}
export interface ChequebookBalanceHook {
chequebookBalance: ChequebookBalance | null
isLoadingChequebookBalance: boolean
error: Error | null
}
export const useApiChequebookBalance = (): ChequebookBalanceHook => {
const [chequebookBalance, setChequebookBalance] = useState<ChequebookBalance | null>(null)
const [isLoadingChequebookBalance, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.chequebook
.balance()
.then(({ totalBalance, availableBalance }) => {
const balance = {
totalBalance: new Token(totalBalance),
availableBalance: new Token(availableBalance),
}
setChequebookBalance(balance)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { chequebookBalance, isLoadingChequebookBalance, error }
}
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 PeerChequesHook {
peerCheques: LastChequesResponse | null
isLoadingPeerCheques: boolean
error: Error | null
}
export const useApiPeerCheques = (): PeerChequesHook => {
const [peerCheques, setPeerCheques] = useState<LastChequesResponse | null>(null)
const [isLoadingPeerCheques, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.chequebook
.getLastCheques()
.then(res => {
setPeerCheques(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { peerCheques, isLoadingPeerCheques, error }
}
export interface PeerLastChequesHook {
peerCheque: LastChequesForPeerResponse | null
isLoadingPeerCheque: boolean
error: Error | null
}
export const useApiPeerLastCheque = (peerId: string): PeerLastChequesHook => {
const [peerCheque, setPeerCheque] = useState<LastChequesForPeerResponse | null>(null)
const [isLoadingPeerCheque, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.chequebook
.getPeerLastCheques(peerId)
.then(res => {
setPeerCheque(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [peerId])
return { peerCheque, isLoadingPeerCheque, 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 LastCashout {
peer: string
uncashedAmount: Token
}
export interface PeerLastCashoutHook {
peerCashout: LastCashout | null
isLoadingPeerCashout: boolean
error: Error | null
}
export const useApiPeerLastCashout = (peerId: string): PeerLastCashoutHook => {
const [peerCashout, setPeerCashout] = useState<LastCashout | null>(null)
const [isLoadingPeerCashout, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.chequebook
.getPeerLastCashout(peerId)
.then(({ peer, uncashedAmount }) => {
setPeerCashout({ peer, uncashedAmount: new Token(uncashedAmount.toString()) })
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [peerId])
return { peerCashout, isLoadingPeerCashout, error }
}
export interface LatestBeeReleaseHook {
latestBeeRelease: LatestBeeRelease | null
-101
View File
@@ -1,101 +0,0 @@
import { ChequebookAddressResponse } from '@ethersphere/bee-js'
import {
ChequebookBalance,
useApiChequebookAddress,
useApiChequebookBalance,
useApiHealth,
useApiNodeAddresses,
useApiNodeTopology,
useDebugApiHealth,
useLatestBeeRelease,
} from './apiHooks'
import semver from 'semver'
import { engines } from '../../package.json'
export interface StatusChequebookHook extends StatusHookCommon {
chequebookBalance: ChequebookBalance | null
chequebookAddress: ChequebookAddressResponse | null
}
export const useStatusNodeVersion = (): StatusNodeVersionHook => {
const { latestBeeRelease, isLoadingLatestBeeRelease } = useLatestBeeRelease()
const { nodeHealth, isLoadingNodeHealth } = useDebugApiHealth()
const latestVersion = semver.coerce(latestBeeRelease?.name)?.version
const latestUserVersion = semver.coerce(nodeHealth?.version)?.version
const isLatestBeeVersion = Boolean(
latestVersion &&
latestUserVersion &&
semver.satisfies(latestVersion, latestUserVersion, {
includePrerelease: true,
}),
)
return {
isLoading: isLoadingNodeHealth || isLoadingLatestBeeRelease,
isOk: Boolean(
nodeHealth &&
semver.satisfies(nodeHealth.version, engines.bee, {
includePrerelease: true,
}),
),
userVersion: nodeHealth?.version,
latestVersion,
latestUrl: latestBeeRelease?.html_url || 'https://github.com/ethersphere/bee/releases/latest',
isLatestBeeVersion,
}
}
export const useStatusEthereumConnection = (): StatusEthereumConnectionHook => {
const { isLoadingNodeAddresses, nodeAddresses } = useApiNodeAddresses()
return {
isLoading: isLoadingNodeAddresses,
isOk: Boolean(nodeAddresses?.ethereum),
nodeAddresses,
}
}
export const useStatusDebugConnection = (): StatusHookCommon => {
const { isLoadingNodeHealth, nodeHealth } = useDebugApiHealth()
return {
isLoading: isLoadingNodeHealth,
isOk: Boolean(nodeHealth?.status === 'ok'),
}
}
export const useStatusConnection = (): StatusHookCommon => {
const { isLoadingHealth, health } = useApiHealth()
return {
isLoading: isLoadingHealth,
isOk: health,
}
}
export const useStatusTopology = (): StatusTopologyHook => {
const { topology, isLoading } = useApiNodeTopology()
return {
isLoading,
isOk: Boolean(topology?.connected && topology?.connected > 0),
topology,
}
}
export const useStatusChequebook = (): StatusChequebookHook => {
const { chequebookAddress, isLoadingChequebookAddress } = useApiChequebookAddress()
const { chequebookBalance, isLoadingChequebookBalance } = useApiChequebookBalance()
return {
isLoading: isLoadingChequebookAddress || isLoadingChequebookBalance,
isOk:
Boolean(chequebookAddress?.chequebookAddress) &&
chequebookBalance !== null &&
chequebookBalance?.totalBalance.toBigNumber.isGreaterThan(0),
chequebookBalance,
chequebookAddress,
}
}
+13 -7
View File
@@ -1,13 +1,15 @@
import { useState, useEffect, ReactElement } from 'react'
import { useState, useEffect, useContext, ReactElement } from 'react'
import ErrorBoundary from '../components/ErrorBoundary'
import AlertVersion from '../components/AlertVersion'
import { Container, CircularProgress } from '@material-ui/core'
import { createStyles, Theme, makeStyles } from '@material-ui/core/styles'
import SideBar from '../components/SideBar'
import NavBar from '../components/NavBar'
import { useApiHealth, useDebugApiHealth } from '../hooks/apiHooks'
import { Context } from '../providers/Bee'
import { RouteComponentProps } from 'react-router'
const useStyles = makeStyles((theme: Theme) =>
@@ -31,9 +33,7 @@ const Dashboard = (props: Props): ReactElement => {
const [themeMode, toggleThemeMode] = useState('light')
// FIXME: handle errrors and loading
const { health } = useApiHealth()
const { nodeHealth } = useDebugApiHealth()
const { isLoading, apiHealth, debugApiHealth } = useContext(Context)
useEffect(() => {
const theme = localStorage.getItem('theme')
@@ -56,12 +56,18 @@ const Dashboard = (props: Props): ReactElement => {
return (
<div>
<SideBar {...props} themeMode={themeMode} health={health} nodeHealth={nodeHealth} />
<SideBar {...props} themeMode={themeMode} health={apiHealth} nodeHealth={debugApiHealth} />
<NavBar themeMode={themeMode} />
<ErrorBoundary>
<main className={classes.content}>
<AlertVersion />
{props.children}
{isLoading ? (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress />
</Container>
) : (
props.children
)}
</main>
</ErrorBoundary>
</div>
+24 -35
View File
@@ -2,7 +2,6 @@ import { ReactElement } from 'react'
import { createStyles, makeStyles } from '@material-ui/core/styles'
import { Card, CardContent, Typography, Theme } from '@material-ui/core/'
import { Skeleton } from '@material-ui/lab'
import WithdrawModal from '../../containers/WithdrawModal'
import DepositModal from '../../containers/DepositModal'
@@ -45,12 +44,11 @@ interface ChequebookBalance {
interface Props {
chequebookAddress: ChequebookAddressResponse | null
chequebookBalance: ChequebookBalance | null
totalsent: Token
totalreceived: Token
isLoading: boolean
totalsent?: Token
totalreceived?: Token
}
function AccountCard({ totalreceived, totalsent, chequebookBalance, isLoading }: Props): ReactElement {
function AccountCard({ totalreceived, totalsent, chequebookBalance }: Props): ReactElement {
const classes = useStyles()
return (
@@ -66,37 +64,28 @@ function AccountCard({ totalreceived, totalsent, chequebookBalance, isLoading }:
</div>
<Card className={classes.root}>
{!isLoading && (
<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>
)}
{isLoading && (
<div className={classes.gridContainer}>
<Skeleton width={180} height={110} animation="wave" />
<Skeleton width={180} height={110} animation="wave" />
<Skeleton width={180} height={110} animation="wave" />
<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>
)
+13 -39
View File
@@ -1,19 +1,12 @@
import type { ReactElement } from 'react'
import { ReactElement, useContext } from 'react'
import { Theme, createStyles, makeStyles } from '@material-ui/core/styles'
import { Container, CircularProgress } from '@material-ui/core'
import AccountCard from '../accounting/AccountCard'
import BalancesTable from './BalancesTable'
import EthereumAddressCard from '../../components/EthereumAddressCard'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import {
useApiNodeAddresses,
useApiChequebookAddress,
useApiChequebookBalance,
useApiHealth,
useDebugApiHealth,
} from '../../hooks/apiHooks'
import { Context as BeeContext } from '../../providers/Bee'
import { Context as SettingsContext } from '../../providers/Settings'
import { useAccounting } from '../../hooks/accounting'
const useStyles = makeStyles((theme: Theme) =>
@@ -29,44 +22,25 @@ const useStyles = makeStyles((theme: Theme) =>
export default function Accounting(): ReactElement {
const classes = useStyles()
const { chequebookAddress, isLoadingChequebookAddress } = useApiChequebookAddress()
const { chequebookBalance, isLoadingChequebookBalance } = useApiChequebookBalance()
const { nodeAddresses, isLoadingNodeAddresses } = useApiNodeAddresses()
const { health, isLoadingHealth } = useApiHealth()
const { nodeHealth, isLoadingNodeHealth } = useDebugApiHealth()
const { isLoading, totalsent, totalreceived, accounting, isLoadingUncashed, error } = useAccounting()
const { status, nodeAddresses, chequebookAddress, chequebookBalance, settlements, peerBalances } = useContext(
BeeContext,
)
const { beeDebugApi } = useContext(SettingsContext)
if (isLoadingHealth || isLoadingNodeHealth) {
return (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress />
</Container>
)
}
const { accounting, isLoadingUncashed } = useAccounting(beeDebugApi, settlements, peerBalances)
if (nodeHealth?.status !== 'ok' || !health) return <TroubleshootConnectionCard />
if (!status.all) return <TroubleshootConnectionCard />
return (
<div className={classes.root}>
<AccountCard
chequebookAddress={chequebookAddress}
isLoading={isLoadingChequebookAddress || isLoading || isLoadingChequebookBalance}
chequebookBalance={chequebookBalance}
totalsent={totalsent}
totalreceived={totalreceived}
totalsent={settlements?.totalSent}
totalreceived={settlements?.totalReceived}
/>
<EthereumAddressCard
nodeAddresses={nodeAddresses}
isLoadingNodeAddresses={isLoadingNodeAddresses}
chequebookAddress={chequebookAddress}
isLoadingChequebookAddress={isLoadingChequebookAddress}
/>
{error && (
<Container style={{ textAlign: 'center', padding: '50px' }}>
Error loading accounting details: {error.message}
</Container>
)}
{!error && <BalancesTable accounting={accounting} isLoadingUncashed={isLoadingUncashed} />}
<EthereumAddressCard nodeAddresses={nodeAddresses} chequebookAddress={chequebookAddress} />
<BalancesTable accounting={accounting} isLoadingUncashed={isLoadingUncashed} />
</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 { Paper, InputBase, IconButton, FormHelperText } from '@material-ui/core'
import { Search } from '@material-ui/icons'
import { apiHost } from '../../constants'
import { Context as SettingsContext } from '../../providers/Settings'
import { Utils } from '@ethersphere/bee-js'
const useStyles = makeStyles((theme: Theme) =>
@@ -28,6 +28,7 @@ const useStyles = makeStyles((theme: Theme) =>
export default function Files(): ReactElement {
const classes = useStyles()
const { apiUrl } = useContext(SettingsContext)
const [referenceInput, setReferenceInput] = useState('')
const [referenceError, setReferenceError] = useState<Error | null>(null)
@@ -50,7 +51,7 @@ export default function Files(): ReactElement {
onChange={handleReferenceChange}
/>
<IconButton
href={`${apiHost}/bzz/${referenceInput}`}
href={`${apiUrl}/bzz/${referenceInput}`}
target="_blank"
disabled={referenceError !== null || !referenceInput}
className={classes.iconButton}
+6 -2
View File
@@ -8,7 +8,7 @@ import UploadSizeAlert from '../../components/AlertUploadSize'
import ClipboardCopy from '../../components/ClipboardCopy'
import PeerDetailDrawer from '../../components/PeerDetail'
import { Context, EnrichedPostageBatch } from '../../providers/Stamps'
import { beeApi } from '../../services/bee'
import { Context as SettingsContext } from '../../providers/Settings'
import CreatePostageStamp from '../stamps/CreatePostageStampModal'
import SelectStamp from './SelectStamp'
@@ -23,6 +23,7 @@ export default function Files(): ReactElement {
const [selectedStamp, setSelectedStamp] = useState<EnrichedPostageBatch | null>(null)
const { isLoading, error, stamps } = useContext(Context)
const { beeApi } = useContext(SettingsContext)
const { enqueueSnackbar } = useSnackbar()
// Choose a postage stamp that has the lowest usage
@@ -40,8 +41,11 @@ export default function Files(): ReactElement {
const uploadFile = () => {
if (file === null || selectedStamp === null) return
if (!beeApi) return
setIsUploadingFile(true)
beeApi.files
beeApi
.uploadFile(selectedStamp.batchID, file)
.then(hash => {
window.setTimeout(() => {
+5 -14
View File
@@ -1,26 +1,17 @@
import { ReactElement } from 'react'
import { ReactElement, useContext } from 'react'
import { Container, CircularProgress } from '@material-ui/core'
import { Container } from '@material-ui/core'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { useApiHealth, useDebugApiHealth } from '../../hooks/apiHooks'
import { Context } from '../../providers/Bee'
import Download from './Download'
import Upload from './Upload'
import TabsContainer from '../../components/TabsContainer'
export default function Files(): ReactElement {
const { health, isLoadingHealth } = useApiHealth()
const { nodeHealth, isLoadingNodeHealth } = useDebugApiHealth()
const { status } = useContext(Context)
if (isLoadingHealth || isLoadingNodeHealth) {
return (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress />
</Container>
)
}
if (!health || nodeHealth?.status !== 'ok') return <TroubleshootConnectionCard />
if (!status.all) return <TroubleshootConnectionCard />
return (
<Container maxWidth="sm">
+27 -45
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 {
Table,
@@ -10,12 +10,11 @@ import {
Button,
Paper,
Tooltip,
Container,
CircularProgress,
} from '@material-ui/core'
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'
const useStyles = makeStyles({
@@ -26,47 +25,43 @@ const useStyles = makeStyles({
interface Props {
peers: Peer[] | null
isLoading: boolean
error: Error | null
}
interface PeerLatency {
rtt: string
loading: boolean
}
function getPingState(peerLatency: Record<string, PeerLatency>, peer: Peer): ReactElement {
if (peerLatency[peer.address]?.loading) return <CircularProgress size={20} />
if (peerLatency[peer.address]?.rtt) return <span>{peerLatency[peer.address]?.rtt}</span>
return <Autorenew />
}
function PeerTable(props: Props): ReactElement {
const classes = useStyles()
const { beeDebugApi } = useContext(SettingsContext)
const [peerLatency, setPeerLatency] = useState([{ peerId: '', rtt: '', loading: false }])
const [peerLatency, setPeerLatency] = useState<Record<string, PeerLatency>>({})
const PingPeer = (peerId: string) => {
setPeerLatency([...peerLatency, { peerId: peerId, rtt: '', loading: true }])
beeDebugApi.connectivity
.ping(peerId)
const pingPeer = (peerId: string) => {
setPeerLatency(prevPeerLatency => ({ ...prevPeerLatency, [peerId]: { rtt: '', loading: true } }))
beeDebugApi
?.pingPeer(peerId)
.then(res => {
setPeerLatency([...peerLatency, { peerId: peerId, rtt: res.rtt, loading: false }])
setPeerLatency(prevPeerLatency => ({ ...prevPeerLatency, [peerId]: { rtt: res.rtt, loading: false } }))
})
.catch(() => {
setPeerLatency([...peerLatency, { peerId: peerId, rtt: 'error', loading: false }])
setPeerLatency(prevPeerLatency => ({ ...prevPeerLatency, [peerId]: { rtt: 'error', loading: false } }))
})
}
if (props.isLoading) {
return (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress />
</Container>
)
}
if (props.error || props.peers === null) {
return (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<p>Failed to load peers</p>
</Container>
)
}
return (
<div>
<TableContainer component={Paper}>
<Table className={classes.table} aria-label="simple table">
<Table className={classes.table}>
<TableHead>
<TableRow>
<TableCell>Index</TableCell>
@@ -75,7 +70,7 @@ function PeerTable(props: Props): ReactElement {
</TableRow>
</TableHead>
<TableBody>
{props.peers.map((peer: Peer, idx: number) => (
{props.peers?.map((peer: Peer, idx: number) => (
<TableRow key={peer.address}>
<TableCell component="th" scope="row">
{idx + 1}
@@ -83,21 +78,8 @@ function PeerTable(props: Props): ReactElement {
<TableCell>{peer.address}</TableCell>
<TableCell align="right">
<Tooltip title="Ping node">
<Button color="primary" onClick={() => PingPeer(peer.address)}>
{
// FIXME: this should be broken up
/* eslint-disable no-nested-ternary */
peerLatency.find(item => item.peerId === peer.address) ? (
peerLatency.filter(item => item.peerId === peer.address)[0].loading ? (
<CircularProgress size={20} />
) : (
peerLatency.filter(item => item.peerId === peer.address)[0].rtt
)
) : (
<Autorenew />
)
/* eslint-enable no-nested-ternary */
}
<Button color="primary" onClick={() => pingPeer(peer.address)}>
{getPingState(peerLatency, peer)}
</Button>
</Tooltip>
</TableCell>
+6 -17
View File
@@ -1,32 +1,21 @@
import { Container, CircularProgress } from '@material-ui/core/'
import PeerTable from './PeerTable'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { useApiNodeTopology, useApiNodePeers, useDebugApiHealth } from '../../hooks/apiHooks'
import { Context } from '../../providers/Bee'
import TopologyStats from '../../components/TopologyStats'
import { ReactElement } from 'react'
import { ReactElement, useContext } from 'react'
export default function Peers(): ReactElement {
const topology = useApiNodeTopology()
const debugHealth = useDebugApiHealth()
const peers = useApiNodePeers()
const { topology, peers, status } = useContext(Context)
if (debugHealth.isLoadingNodeHealth) {
return (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress />
</Container>
)
}
if (debugHealth.error) {
if (!status.all) {
return <TroubleshootConnectionCard />
}
return (
<>
<TopologyStats {...topology} />
<PeerTable {...peers} />
<TopologyStats topology={topology} />
<PeerTable peers={peers} />
</>
)
}
+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 { Context as SettingsContext } from '../../providers/Settings'
export default function Settings(): ReactElement {
const [refreshVisibility, toggleRefreshVisibility] = useState(false)
const [host, setHost] = useState('')
const [debugHost, setDebugHost] = useState('')
const { apiUrl, apiDebugUrl, setApiUrl, setDebugApiUrl } = useContext(SettingsContext)
const [host, setHost] = useState(apiUrl)
const [debugHost, setDebugHost] = useState(apiDebugUrl)
const handleNewHostConnection = () => {
if (host) {
sessionStorage.setItem('api_host', host)
}
const submit = () => {
if (host !== apiUrl) setApiUrl(host)
if (debugHost) {
sessionStorage.setItem('debug_api_host', debugHost)
}
if (host || debugHost) {
toggleRefreshVisibility(!refreshVisibility)
window.location.reload()
}
if (debugHost !== apiDebugUrl) setDebugApiUrl(debugHost)
}
const touched = host !== apiUrl || debugHost !== apiDebugUrl
return (
<div>
<Container>
@@ -34,16 +28,13 @@ export default function Settings(): ReactElement {
placeholder="ex: 127.0.0.0.1:1633"
helperText="Enter node host override / port"
fullWidth
defaultValue={
sessionStorage.getItem('api_host') ? sessionStorage.getItem('api_host') : process.env.REACT_APP_BEE_HOST
}
defaultValue={apiUrl}
margin="normal"
InputLabelProps={{
shrink: true,
}}
onChange={e => {
setHost(e.target.value)
toggleRefreshVisibility(true)
}}
variant="filled"
/>
@@ -55,14 +46,9 @@ export default function Settings(): ReactElement {
placeholder="ex: 127.0.0.0.1:1635"
helperText="Enter node debug host override / port"
fullWidth
defaultValue={
sessionStorage.getItem('debug_api_host')
? sessionStorage.getItem('debug_api_host')
: process.env.REACT_APP_BEE_DEBUG_HOST
}
defaultValue={apiDebugUrl}
onChange={e => {
setDebugHost(e.target.value)
toggleRefreshVisibility(true)
}}
margin="normal"
InputLabelProps={{
@@ -71,9 +57,9 @@ export default function Settings(): ReactElement {
variant="filled"
/>
</Paper>
{refreshVisibility ? (
{touched ? (
<div style={{ marginTop: '20px' }}>
<Button variant="outlined" color="primary" onClick={() => handleNewHostConnection()}>
<Button variant="outlined" color="primary" onClick={submit}>
Save
</Button>
</div>
+5 -2
View File
@@ -9,7 +9,7 @@ import DialogTitle from '@material-ui/core/DialogTitle'
import BigNumber from 'bignumber.js'
import { FormikHelpers, Form, Field, Formik } from 'formik'
import { TextField } from 'formik-material-ui'
import { beeApi } from '../../services/bee'
import { Context as SettingsContext } from '../../providers/Settings'
import { Context } from '../../providers/Stamps'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import { useSnackbar } from 'notistack'
@@ -54,6 +54,7 @@ export default function FormDialog({ label }: Props): ReactElement {
const classes = useStyles()
const [open, setOpen] = React.useState(false)
const { refresh } = useContext(Context)
const { beeApi } = useContext(SettingsContext)
const handleClickOpen = () => setOpen(true)
const handleClose = () => setOpen(false)
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
if (!values.depth || !values.amount) return
if (!beeApi) return
const amount = BigInt(values.amount)
const depth = Number.parseInt(values.depth)
const options = values.label ? { label: values.label } : undefined
await beeApi.stamps.buyPostageStamp(amount, depth, options)
await beeApi.createPostageBatch(amount.toString(), depth, options)
actions.resetForm()
await refresh()
handleClose()
+5 -8
View File
@@ -5,10 +5,9 @@ import { Container, CircularProgress } from '@material-ui/core'
import StampsTable from './StampsTable'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import CreatePostageStampModal from './CreatePostageStampModal'
import LastUpdate from '../../components/LastUpdate'
import { useApiHealth, useDebugApiHealth } from '../../hooks/apiHooks'
import { Context } from '../../providers/Stamps'
import { Context as BeeContext } from '../../providers/Bee'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
@@ -32,16 +31,15 @@ const useStyles = makeStyles((theme: Theme) =>
export default function Accounting(): ReactElement {
const classes = useStyles()
const { health, isLoadingHealth } = useApiHealth()
const { nodeHealth, isLoadingNodeHealth } = useDebugApiHealth()
const { stamps, isLoading, error, lastUpdate, start, stop } = useContext(Context)
const beeContext = useContext(BeeContext)
const { stamps, isLoading, error, start, stop } = useContext(Context)
useEffect(() => {
start()
return () => stop()
}, [])
if (isLoadingHealth || isLoadingNodeHealth) {
if (beeContext.isLoading) {
return (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress />
@@ -49,7 +47,7 @@ export default function Accounting(): ReactElement {
)
}
if (nodeHealth?.status !== 'ok' || !health) return <TroubleshootConnectionCard />
if (!beeContext.status.all) return <TroubleshootConnectionCard />
return (
<div className={classes.root}>
@@ -62,7 +60,6 @@ export default function Accounting(): ReactElement {
<>
<div className={classes.actions}>
<CreatePostageStampModal />
<LastUpdate date={lastUpdate} />
<div style={{ height: '5px' }}>{isLoading && <CircularProgress />}</div>
</div>
<StampsTable postageStamps={stamps} />
+41 -49
View File
@@ -1,7 +1,7 @@
import { ReactElement, useEffect, useState } from 'react'
import { ReactElement, useEffect, useState, useContext } from 'react'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import { Typography, Paper, Button, Step, StepLabel, StepContent, Stepper, StepButton } from '@material-ui/core/'
import { CheckCircle, Error, Sync, ExpandLessSharp, ExpandMoreSharp, Autorenew } from '@material-ui/icons/'
import { CheckCircle, Error, ExpandLessSharp, ExpandMoreSharp, Autorenew } from '@material-ui/icons/'
import DebugConnectionCheck from './SetupSteps/DebugConnectionCheck'
import NodeConnectionCheck from './SetupSteps/NodeConnectionCheck'
@@ -9,7 +9,7 @@ import VersionCheck from './SetupSteps/VersionCheck'
import EthereumConnectionCheck from './SetupSteps/EthereumConnectionCheck'
import ChequebookDeployFund from './SetupSteps/ChequebookDeployFund'
import PeerConnection from './SetupSteps/PeerConnection'
import { StatusChequebookHook } from '../../hooks/status'
import { Context } from '../../providers/Bee'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
@@ -30,66 +30,65 @@ const useStyles = makeStyles((theme: Theme) =>
interface Step {
label: string
isOk: boolean
isLoading: boolean
component: ReactElement
}
interface Props {
nodeVersion: StatusNodeVersionHook
ethereumConnection: StatusEthereumConnectionHook
debugApiConnection: StatusHookCommon
apiConnection: StatusHookCommon
topology: StatusTopologyHook
chequebook: StatusChequebookHook
}
export default function NodeSetupWorkflow({
nodeVersion,
ethereumConnection,
debugApiConnection,
apiConnection,
topology,
chequebook,
}: Props): ReactElement {
export default function NodeSetupWorkflow(): ReactElement {
const classes = useStyles()
const [activeStep, setActiveStep] = useState(-1)
const {
status,
isLoading,
latestUserVersion,
latestPublishedVersion,
isLatestBeeVersion,
latestBeeVersionUrl,
topology,
nodeAddresses,
chequebookAddress,
} = useContext(Context)
const steps: Step[] = [
{
label: 'Connected to Node DebugAPI',
isOk: debugApiConnection.isOk,
isLoading: debugApiConnection.isLoading,
component: <DebugConnectionCheck {...debugApiConnection} />,
isOk: status.debugApiConnection,
component: <DebugConnectionCheck isOk={status.debugApiConnection} />,
},
{
label: 'Running latest Bee version',
isOk: nodeVersion.isOk,
isLoading: nodeVersion.isLoading,
component: <VersionCheck {...nodeVersion} />,
isOk: status.version,
component: (
<VersionCheck
isOk={status.version}
isLatestBeeVersion={isLatestBeeVersion}
userVersion={latestUserVersion}
latestVersion={latestPublishedVersion}
latestUrl={latestBeeVersionUrl}
/>
),
},
{
label: 'Connected to Ethereum Blockchain',
isOk: ethereumConnection.isOk,
isLoading: ethereumConnection.isLoading,
component: <EthereumConnectionCheck {...ethereumConnection} />,
label: 'Connected to xDai Blockchain',
isOk: status.blockchainConnection,
component: <EthereumConnectionCheck isOk={status.blockchainConnection} nodeAddresses={nodeAddresses} />,
},
{
label: 'Deployed and Funded Chequebook',
isOk: chequebook.isOk,
isLoading: chequebook.isLoading,
component: <ChequebookDeployFund ethereumAddress={ethereumConnection.nodeAddresses?.ethereum} {...chequebook} />,
isOk: status.chequebook,
component: (
<ChequebookDeployFund chequebookAddress={chequebookAddress?.chequebookAddress} isOk={status.chequebook} />
),
},
{
label: 'Connected to Node API',
isOk: apiConnection.isOk,
isLoading: apiConnection.isLoading,
component: <NodeConnectionCheck {...apiConnection} />,
isOk: status.apiConnection,
component: <NodeConnectionCheck isOk={status.apiConnection} />,
},
{
label: 'Connected to Peers',
isOk: topology.isOk,
isLoading: topology.isLoading,
component: <PeerConnection {...topology} />,
isOk: status.topology,
component: <PeerConnection isOk={status.topology} topology={topology} />,
},
]
@@ -98,7 +97,7 @@ export default function NodeSetupWorkflow({
if (activeStep >= 0 && activeStep < steps.length) return
// If any step is not fully loaded yet return
if (!steps.every(step => !step.isLoading)) return
if (!isLoading) return
// Select first step that is not OK
// This is deliberately a for loop (and not forEach) so that we can terminate the useEffect from within the cycle
@@ -123,18 +122,11 @@ export default function NodeSetupWorkflow({
<Paper className={classes.root}>
<Typography variant="h5" gutterBottom>
Node Setup
<span style={{ marginLeft: '25px' }}>
<Button variant="outlined" size="small" onClick={() => window.location.reload()}>
<Sync />
<span style={{ marginLeft: '7px' }}>Refresh Checks</span>
</Button>
</span>
</Typography>
<Stepper nonLinear activeStep={activeStep} orientation="vertical">
{steps.map(({ label, isOk, component, isLoading }, index) => (
{steps.map(({ label, isOk, component }, index) => (
<Step key={label}>
<StepLabel
disabled={isLoading}
onClick={() => setActiveStep(index === activeStep ? steps.length : index)}
StepIconComponent={() => {
if (isLoading) return <Autorenew style={{ height: '25px', cursor: 'pointer' }} />
@@ -2,22 +2,17 @@ import { Typography } from '@material-ui/core/'
import EthereumAddress from '../../../components/EthereumAddress'
import DepositModal from '../../../containers/DepositModal'
import type { ReactElement } from 'react'
import type { StatusChequebookHook } from '../../../hooks/status'
interface Props extends StatusChequebookHook {
ethereumAddress?: string
interface Props extends StatusHookCommon {
chequebookAddress?: string
}
const ChequebookDeployFund = ({ isLoading, chequebookAddress, chequebookBalance }: Props): ReactElement | null => {
if (isLoading) return null
const ChequebookDeployFund = ({ chequebookAddress, isOk }: Props): ReactElement | null => {
return (
<div>
<p style={{ marginBottom: '20px', display: 'flex' }}>
{chequebookAddress?.chequebookAddress && <DepositModal />}
</p>
<p style={{ marginBottom: '20px', display: 'flex' }}>{chequebookAddress && <DepositModal />}</p>
<div style={{ marginBottom: '10px' }}>
{!(chequebookAddress?.chequebookAddress && chequebookBalance?.totalBalance.toBigNumber.isGreaterThan(0)) && (
{!isOk && (
<div>
<span>
Your chequebook is either not deployed or funded. To run the node you will need xDAI and xBZZ on the xDai
@@ -33,7 +28,7 @@ const ChequebookDeployFund = ({ isLoading, chequebookAddress, chequebookBalance
<Typography variant="subtitle1" gutterBottom>
Chequebook Address
</Typography>
<EthereumAddress address={chequebookAddress?.chequebookAddress} />
<EthereumAddress address={chequebookAddress} />
</div>
)
}
@@ -1,23 +1,29 @@
import type { ReactElement } from 'react'
import { ReactElement, useContext } from 'react'
import { Typography, Accordion, AccordionSummary, AccordionDetails } from '@material-ui/core/'
import MuiAlert from '@material-ui/lab/Alert'
import { ExpandMoreSharp } from '@material-ui/icons/'
import ConnectToHost from '../../../components/ConnectToHost'
import CodeBlockTabs from '../../../components/CodeBlockTabs'
import { debugApiHost } from '../../../constants'
import { Context as SettingsContext } from '../../../providers/Settings'
type Props = StatusHookCommon
export default function NodeConnectionCheck({ isLoading, isOk }: Props): ReactElement | null {
if (isLoading) return null
export default function NodeConnectionCheck({ isOk }: Props): ReactElement | null {
const { setDebugApiUrl, apiDebugUrl } = useContext(SettingsContext)
const changeDebugApiUrl = (
<div style={{ display: 'flex', marginTop: '25px', marginBottom: '25px' }}>
<span style={{ marginRight: '15px' }}>
Debug API (<Typography variant="button">{debugApiHost}</Typography>)
Debug API (<Typography variant="button">{apiDebugUrl}</Typography>)
</span>
<ConnectToHost hostName={'debug_api_host'} defaultHost={debugApiHost} />
<ConnectToHost
setHost={(host: string) => {
console.log(host) // eslint-disable-line
setDebugApiUrl(host)
}}
defaultHost={apiDebugUrl}
/>
</div>
)
@@ -31,7 +37,7 @@ export default function NodeConnectionCheck({ isLoading, isOk }: Props): ReactEl
<div>
<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.
<Accordion style={{ marginTop: '20px' }}>
<AccordionSummary expandIcon={<ExpandMoreSharp />} aria-controls="panel1a-content" id="panel1a-header">
@@ -4,9 +4,7 @@ import EthereumAddress from '../../../components/EthereumAddress'
type Props = StatusEthereumConnectionHook
export default function EthereumConnectionCheck({ isLoading, isOk, nodeAddresses }: Props): ReactElement | null {
if (isLoading) return null
export default function EthereumConnectionCheck({ isOk, nodeAddresses }: Props): ReactElement | null {
if (isOk) {
return (
<div>
@@ -1,28 +1,28 @@
import React, { ReactElement } from 'react'
import { ReactElement, useContext } from 'react'
import { Typography, Accordion, AccordionSummary, AccordionDetails } from '@material-ui/core/'
import { ExpandMoreSharp } from '@material-ui/icons/'
import ConnectToHost from '../../../components/ConnectToHost'
import CodeBlockTabs from '../../../components/CodeBlockTabs'
import { apiHost } from '../../../constants'
import { Context as SettingsContext } from '../../../providers/Settings'
type Props = StatusHookCommon
export default function NodeConnectionCheck({ isLoading, isOk }: Props): ReactElement | null {
if (isLoading) return null
export default function NodeConnectionCheck({ isOk }: Props): ReactElement | null {
const { setApiUrl, apiUrl } = useContext(SettingsContext)
return (
<div>
<div style={{ display: 'flex', marginBottom: '25px' }}>
<span style={{ marginRight: '15px' }}>
Node API (<Typography variant="button">{apiHost}</Typography>)
Node API (<Typography variant="button">{apiUrl}</Typography>)
</span>
<ConnectToHost hostName="api_host" defaultHost={apiHost} />
<ConnectToHost setHost={setApiUrl} defaultHost={apiUrl} />
</div>
<div>
{!isOk && (
<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.
<Accordion style={{ marginTop: '20px' }}>
<AccordionSummary expandIcon={<ExpandMoreSharp />} aria-controls="panel1a-content" id="panel1a-header">
@@ -3,9 +3,7 @@ import { Typography } from '@material-ui/core/'
type Props = StatusTopologyHook
export default function PeerConnection({ isLoading, isOk, topology }: Props): ReactElement | null {
if (isLoading) return null
export default function PeerConnection({ isOk, topology }: Props): ReactElement | null {
const peers = (
<div style={{ display: 'flex', marginTop: '15px' }}>
<div style={{ marginRight: '30px' }}>
+1 -9
View File
@@ -4,15 +4,7 @@ import CodeBlockTabs from '../../../components/CodeBlockTabs'
type Props = StatusNodeVersionHook
export default function VersionCheck({
isLoading,
isOk,
userVersion,
latestVersion,
latestUrl,
}: Props): ReactElement | null {
if (isLoading) return null
export default function VersionCheck({ isOk, userVersion, latestVersion, latestUrl }: Props): ReactElement | null {
const version = (
<div style={{ display: 'flex' }}>
<div style={{ marginRight: '30px' }}>
+20 -48
View File
@@ -1,18 +1,10 @@
import { ReactElement } from 'react'
import { Container, CircularProgress } from '@material-ui/core'
import { ReactElement, useContext } from 'react'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import NodeSetupWorkflow from './NodeSetupWorkflow'
import StatusCard from './StatusCard'
import EthereumAddressCard from '../../components/EthereumAddressCard'
import {
useStatusEthereumConnection,
useStatusNodeVersion,
useStatusDebugConnection,
useStatusConnection,
useStatusTopology,
useStatusChequebook,
} from '../../hooks/status'
import { Context as BeeContext } from '../../providers/Bee'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
@@ -27,50 +19,30 @@ const useStyles = makeStyles((theme: Theme) =>
export default function Status(): ReactElement {
const classes = useStyles()
const nodeVersion = useStatusNodeVersion()
const ethereumConnection = useStatusEthereumConnection()
const debugApiConnection = useStatusDebugConnection()
const apiConnection = useStatusConnection()
const topology = useStatusTopology()
const chequebook = useStatusChequebook()
const checks = [nodeVersion, ethereumConnection, debugApiConnection, apiConnection, topology, chequebook]
// If any check data are still loading
if (!checks.every(c => !c.isLoading)) {
return (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress />
</Container>
)
}
const {
status,
latestUserVersion,
isLatestBeeVersion,
latestBeeVersionUrl,
topology,
nodeAddresses,
chequebookAddress,
} = useContext(BeeContext)
return (
<div className={classes.root}>
<StatusCard
userBeeVersion={nodeVersion.userVersion}
isLatestBeeVersion={nodeVersion.isLatestBeeVersion}
isOk={checks.every(c => c.isOk)}
nodeTopology={topology.topology}
latestUrl={nodeVersion.latestUrl}
nodeAddresses={ethereumConnection.nodeAddresses}
userBeeVersion={latestUserVersion}
isLatestBeeVersion={isLatestBeeVersion}
isOk={status.all}
nodeTopology={topology}
latestUrl={latestBeeVersionUrl}
nodeAddresses={nodeAddresses}
/>
{ethereumConnection.nodeAddresses && chequebook.chequebookAddress && (
<EthereumAddressCard
nodeAddresses={ethereumConnection.nodeAddresses}
isLoadingNodeAddresses={ethereumConnection.isLoading}
chequebookAddress={chequebook.chequebookAddress}
isLoadingChequebookAddress={chequebook.isLoading}
/>
{nodeAddresses && chequebookAddress && (
<EthereumAddressCard nodeAddresses={nodeAddresses} chequebookAddress={chequebookAddress} />
)}
<NodeSetupWorkflow
nodeVersion={nodeVersion}
ethereumConnection={ethereumConnection}
debugApiConnection={debugApiConnection}
apiConnection={apiConnection}
topology={topology}
chequebook={chequebook}
/>
<NodeSetupWorkflow />
</div>
)
}
+351
View File
@@ -0,0 +1,351 @@
import type { ChequebookBalance, Balance, Settlements } from '../types'
import { createContext, ReactChild, ReactElement, useEffect, useState, useContext } from 'react'
import { Token } from '../models/Token'
import semver from 'semver'
import { engines } from '../../package.json'
import { Context as SettingsContext } from './Settings'
import type {
NodeAddresses,
ChequebookAddressResponse,
LastChequesResponse,
Health,
Peer,
Topology,
} from '@ethersphere/bee-js'
import { useLatestBeeRelease } from '../hooks/apiHooks'
interface Status {
all: boolean
version: boolean
blockchainConnection: boolean
debugApiConnection: boolean
apiConnection: boolean
topology: boolean
chequebook: boolean
}
interface ContextInterface {
status: Status
latestPublishedVersion?: string
latestUserVersion?: string
latestUserVersionExact?: string
isLatestBeeVersion: boolean
latestBeeVersionUrl: string
error: Error | null
apiHealth: boolean
debugApiHealth: Health | null
nodeAddresses: NodeAddresses | null
topology: Topology | null
chequebookAddress: ChequebookAddressResponse | null
peers: Peer[] | null
chequebookBalance: ChequebookBalance | null
peerBalances: Balance[] | null
peerCheques: LastChequesResponse | null
settlements: Settlements | null
latestBeeRelease: LatestBeeRelease | null
isLoading: boolean
isRefreshing: boolean
lastUpdate: number | null
start: (frequency?: number) => void
stop: () => void
refresh: () => Promise<void>
}
const initialValues: ContextInterface = {
status: {
all: false,
version: false,
blockchainConnection: false,
debugApiConnection: false,
apiConnection: false,
topology: false,
chequebook: false,
},
latestPublishedVersion: undefined,
latestUserVersion: undefined,
latestUserVersionExact: undefined,
isLatestBeeVersion: false,
latestBeeVersionUrl: 'https://github.com/ethersphere/bee/releases/latest',
error: null,
apiHealth: false,
debugApiHealth: null,
nodeAddresses: null,
topology: null,
chequebookAddress: null,
peers: null,
chequebookBalance: null,
peerBalances: null,
peerCheques: null,
settlements: null,
latestBeeRelease: null,
isLoading: true,
isRefreshing: false,
lastUpdate: null,
start: () => {}, // eslint-disable-line
stop: () => {}, // eslint-disable-line
refresh: () => Promise.reject(),
}
export const Context = createContext<ContextInterface>(initialValues)
export const Consumer = Context.Consumer
interface Props {
children: ReactChild
}
function getStatus(
debugApiHealth: Health | null,
nodeAddresses: NodeAddresses | null,
apiHealth: boolean,
topology: Topology | null,
chequebookAddress: ChequebookAddressResponse | null,
chequebookBalance: ChequebookBalance | null,
error: Error | null,
): Status {
const status = {
version: Boolean(
debugApiHealth &&
semver.satisfies(debugApiHealth.version, engines.bee, {
includePrerelease: true,
}),
),
blockchainConnection: Boolean(nodeAddresses?.ethereum),
debugApiConnection: Boolean(debugApiHealth?.status === 'ok'),
apiConnection: apiHealth,
topology: Boolean(topology?.connected && topology?.connected > 0),
chequebook:
Boolean(chequebookAddress?.chequebookAddress) &&
chequebookBalance !== null &&
chequebookBalance?.totalBalance.toBigNumber.isGreaterThan(0),
}
return { ...status, all: !error && Object.values(status).every(v => v) }
}
export function Provider({ children }: Props): ReactElement {
const { beeApi, beeDebugApi } = useContext(SettingsContext)
const [apiHealth, setApiHealth] = useState<boolean>(false)
const [debugApiHealth, setDebugApiHealth] = useState<Health | null>(null)
const [nodeAddresses, setNodeAddresses] = useState<NodeAddresses | null>(null)
const [topology, setNodeTopology] = useState<Topology | null>(null)
const [chequebookAddress, setChequebookAddress] = useState<ChequebookAddressResponse | null>(null)
const [peers, setPeers] = useState<Peer[] | null>(null)
const [chequebookBalance, setChequebookBalance] = useState<ChequebookBalance | null>(null)
const [peerBalances, setPeerBalances] = useState<Balance[] | null>(null)
const [peerCheques, setPeerCheques] = useState<LastChequesResponse | null>(null)
const [settlements, setSettlements] = useState<Settlements | null>(null)
const { latestBeeRelease } = useLatestBeeRelease()
const [error, setError] = useState<Error | null>(initialValues.error)
const [isLoading, setIsLoading] = useState<boolean>(initialValues.isLoading)
const [isRefreshing, setIsRefreshing] = useState<boolean>(initialValues.isRefreshing)
const [lastUpdate, setLastUpdate] = useState<number | null>(initialValues.lastUpdate)
const [frequency, setFrequency] = useState<number | null>(30000)
const latestPublishedVersion = semver.coerce(latestBeeRelease?.name)?.version
const latestUserVersion = semver.coerce(debugApiHealth?.version)?.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 () => {
// Don't want to refresh when already refreshing
if (isRefreshing) return
// Not a valid bee api
if (!beeApi || !beeDebugApi) {
setIsLoading(false)
return
}
try {
setIsRefreshing(true)
setError(null)
// Wrap the chequebook balance call to return BZZ values as Token object
const chequeBalanceWrapper = async () => {
const { totalBalance, availableBalance } = await beeDebugApi.getChequebookBalance()
return {
totalBalance: new Token(totalBalance),
availableBalance: new Token(availableBalance),
}
}
// Wrap the balances call to return BZZ values as Token object
const peerBalanceWrapper = async () => {
const { balances } = await beeDebugApi.getAllBalances()
return balances.map(({ peer, balance }) => ({ peer, balance: new Token(balance) }))
}
// 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) {
setError(e)
} finally {
setIsLoading(false)
setIsRefreshing(false)
setLastUpdate(Date.now())
}
}
const start = (freq = 30000) => setFrequency(freq)
const stop = () => setFrequency(null)
// Start the update loop
useEffect(() => {
refresh()
// Start autorefresh only if the frequency is set
if (frequency) {
const interval = setInterval(refresh, frequency)
return () => clearInterval(interval)
}
}, [frequency, beeDebugApi, beeApi])
return (
<Context.Provider
value={{
status: getStatus(
debugApiHealth,
nodeAddresses,
apiHealth,
topology,
chequebookAddress,
chequebookBalance,
error,
),
latestUserVersion,
latestUserVersionExact,
latestPublishedVersion,
isLatestBeeVersion: Boolean(
latestPublishedVersion &&
latestUserVersion &&
semver.satisfies(latestPublishedVersion, latestUserVersion, {
includePrerelease: true,
}),
),
latestBeeVersionUrl: latestBeeRelease?.html_url || 'https://github.com/ethersphere/bee/releases/latest',
error,
apiHealth,
debugApiHealth,
nodeAddresses,
topology,
chequebookAddress,
peers,
chequebookBalance,
peerBalances,
peerCheques,
settlements,
latestBeeRelease,
isLoading,
isRefreshing,
lastUpdate,
start,
stop,
refresh,
}}
>
{children}
</Context.Provider>
)
}
+69
View File
@@ -0,0 +1,69 @@
import { createContext, ReactChild, ReactElement, useState, useEffect } from 'react'
// These need to be numeric values as they are used as indexes in the TabsContainer
export enum Platforms {
macOS = 0,
Linux,
Windows,
iOS,
Android,
}
export enum SupportedPlatforms {
macOS = Platforms.macOS,
Linux = Platforms.Linux,
}
interface ContextInterface {
platform: SupportedPlatforms
setPlatform: (platform: SupportedPlatforms) => void
}
const initialValues: ContextInterface = {
platform: SupportedPlatforms.macOS,
setPlatform: () => {}, // eslint-disable-line
}
export const Context = createContext<ContextInterface>(initialValues)
export const Consumer = Context.Consumer
interface Props {
children: ReactChild
}
function isSupportedPlatform(platform: unknown): platform is SupportedPlatforms {
return Object.keys(SupportedPlatforms).includes(platform as string)
}
function getOS(): Platforms | null {
const userAgent = window.navigator.userAgent
const platform = window.navigator.platform
const macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K']
const windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE']
const iosPlatforms = ['iPhone', 'iPad', 'iPod']
if (macosPlatforms.includes(platform)) return Platforms.macOS
if (iosPlatforms.includes(platform)) return Platforms.iOS
if (windowsPlatforms.includes(platform)) return Platforms.Windows
if (/Android/.test(userAgent)) return Platforms.Android
if (/Linux/.test(platform)) return Platforms.Linux
return null
}
export function Provider({ children }: Props): ReactElement {
const [platform, setPlatform] = useState<SupportedPlatforms>(SupportedPlatforms.Linux)
// This is in useEffect as it really just needs to run once and not on each re-render
useEffect(() => {
const os = getOS()
setPlatform(isSupportedPlatform(os) ? os : SupportedPlatforms.Linux)
}, [])
return <Context.Provider value={{ platform, setPlatform }}>{children}</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: () => {}, // eslint-disable-line
setDebugApiUrl: () => {}, // 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 { createContext, ReactChild, ReactElement, useEffect, useState } from 'react'
import { beeApi } from '../services/bee'
import { createContext, ReactChild, ReactElement, useEffect, useState, useContext } from 'react'
import { Context as SettingsContext } from './Settings'
export interface EnrichedPostageBatch extends PostageBatch {
usage: number
@@ -48,6 +48,7 @@ function enrichStamp(postageBatch: PostageBatch): EnrichedPostageBatch {
}
export function Provider({ children }: Props): ReactElement {
const { beeApi } = useContext(SettingsContext)
const [stamps, setStamps] = useState<EnrichedPostageBatch[] | null>(initialValues.stamps)
const [error, setError] = useState<Error | null>(initialValues.error)
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
if (isLoading) return
if (!beeApi) return
try {
setIsLoading(true)
const stamps = await beeApi.stamps.getPostageStamps()
const stamps = await beeApi.getAllPostageBatch()
setStamps(stamps.map(enrichStamp))
setLastUpdate(Date.now())
-1
View File
@@ -6,7 +6,6 @@ interface LatestBeeRelease {
}
interface StatusHookCommon {
isLoading: boolean
isOk: boolean
}
-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()
},
},
}
+23
View File
@@ -0,0 +1,23 @@
import type { Token } from './models/Token'
export interface ChequebookBalance {
totalBalance: Token
availableBalance: Token
}
export interface Balance {
peer: string
balance: Token
}
export interface Settlement {
peer: string
received: Token
sent: Token
}
export interface Settlements {
totalReceived: Token
totalSent: Token
settlements: Settlement[]
}
+74
View File
@@ -32,3 +32,77 @@ export function makeBigNumber(value: BigNumber | BigInt | number | string): BigN
throw new TypeError(`Not a BigNumber or BigNumber convertible value. Type: ${typeof value} value: ${value}`)
}
export type PromiseSettlements<T> = {
fulfilled: PromiseFulfilledResult<T>[]
rejected: PromiseRejectedResult[]
}
export type UnwrappedPromiseSettlements<T> = {
fulfilled: T[]
rejected: string[]
}
export async function sleepMs(ms: number): Promise<void> {
await new Promise<void>(resolve =>
setTimeout(() => {
resolve()
}, ms),
)
}
/**
* Maps the returned results of `Promise.allSettled` to an object
* with `fulfilled` and `rejected` arrays for easy access.
*
* The results still need to be unwrapped to get the fulfilled values or rejection reasons.
*/
export function mapPromiseSettlements<T>(promises: PromiseSettledResult<T>[]): PromiseSettlements<T> {
const fulfilled = promises.filter(promise => promise.status === 'fulfilled') as PromiseFulfilledResult<T>[]
const rejected = promises.filter(promise => promise.status === 'rejected') as PromiseRejectedResult[]
return { fulfilled, rejected }
}
/**
* Maps the returned values of `Promise.allSettled` to an object
* with `fulfilled` and `rejected` arrays for easy access.
*
* For rejected promises, the value is the stringified `reason`,
* or `'Unknown error'` string when it is unavailable.
*/
export function unwrapPromiseSettlements<T>(
promiseSettledResults: PromiseSettledResult<T>[],
): UnwrappedPromiseSettlements<T> {
const values = mapPromiseSettlements(promiseSettledResults)
const fulfilled = values.fulfilled.map(x => x.value)
const rejected = values.rejected.map(x => (x.reason ? String(x.reason) : 'Unknown error'))
return { fulfilled, rejected }
}
/**
* Wraps a `Promise<T>` or async function inside a new `Promise<T>`,
* which retries the original function up to `maxRetries` times,
* waiting `delayMs` milliseconds between failed attempts.
*
* If all attempts fail, then this `Promise<T>` also rejects.
*/
export function makeRetriablePromise<T>(fn: () => Promise<T>, maxRetries = 3, delayMs = 1000): Promise<T> {
return new Promise(async (resolve, reject) => {
for (let tries = 0; tries < maxRetries; tries++) {
try {
const results = await fn()
resolve(results)
return
} catch (error) {
if (tries < maxRetries - 1) {
await sleepMs(delayMs)
} else {
reject(error)
}
}
}
})
}
+105
View File
@@ -0,0 +1,105 @@
const OPTIMAL_CONNECTED_PEERS = 200
const OPTIMAL_POPULATION = 100_000
const OPTIMAL_DEPTH = 12
interface Threshold {
minimumValue: number
explanation: string
score: number
}
type Thresholds = {
connectedPeers: Threshold[]
population: Threshold[]
depth: Threshold[]
}
type ThresholdValue = {
score: number
maximumScore: number
explanation: string
}
export type ThresholdValues = {
connectedPeers: ThresholdValue
population: ThresholdValue
depth: ThresholdValue
}
const GENERIC_ERROR = 'There may be issues with your Bee node or connection.'
const THRESHOLDS: Thresholds = {
connectedPeers: [
{
minimumValue: OPTIMAL_CONNECTED_PEERS,
explanation: `Perfect! ${OPTIMAL_CONNECTED_PEERS} or more connected peers indicate a healthy topology.`,
score: 2,
},
{
minimumValue: 1,
explanation: `Your Bee node is connected to peers, but this number should ideally be above ${OPTIMAL_CONNECTED_PEERS}. If you have only started your Bee node, this number may increase quickly.`,
score: 1,
},
{
minimumValue: 0,
explanation: 'Your Bee node has not connected to any peers. ' + GENERIC_ERROR,
score: 0,
},
],
population: [
{
minimumValue: OPTIMAL_POPULATION,
explanation:
'Perfect! Your Bee node seems to have a realistic value for the network size, which means everything is working well on your end.',
score: 2,
},
{
minimumValue: 1,
explanation: `Population is usually above ${OPTIMAL_POPULATION.toLocaleString()}. If the number does not increase within a few hours, there may be issues with your Bee node.`,
score: 1,
},
{
minimumValue: 0,
explanation: 'Your Bee node has no information on the network population. ' + GENERIC_ERROR,
score: 0,
},
],
depth: [
{
minimumValue: OPTIMAL_DEPTH,
explanation: 'Perfect! Your Bee node has the highest available depth.',
score: 2,
},
{
minimumValue: 1,
explanation: `Your Bee node is supposed to reach a depth of ${OPTIMAL_DEPTH} eventually. Stagnation or decrease in this number may indicate problems with your Bee node.`,
score: 1,
},
{
minimumValue: 0,
explanation: 'Your Bee node has not started building its topology yet. ' + GENERIC_ERROR,
score: 0,
},
],
}
export function pickThreshold(key: keyof Thresholds, value: number): ThresholdValue {
const thresholds = THRESHOLDS[key]
const maximumScore = thresholds[0].score
for (const item of thresholds) {
if (value >= item.minimumValue) {
return {
score: item.score,
maximumScore,
explanation: item.explanation,
}
}
}
const last = thresholds[thresholds.length - 1]
return {
score: last.score,
maximumScore,
explanation: last.explanation,
}
}