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 # 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) ## [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 ## Maintainers
- [nugaon](https://github.com/nugaon)
- [vojtechsimetka](https://github.com/vojtechsimetka) - [vojtechsimetka](https://github.com/vojtechsimetka)
- [Cafe137](https://github.com/Cafe137)
See what "Maintainer" means [here](https://github.com/ethersphere/repo-maintainer).
## License ## License
+29 -25
View File
@@ -1,15 +1,15 @@
{ {
"name": "@ethersphere/bee-dashboard", "name": "@ethersphere/bee-dashboard",
"version": "0.5.0", "version": "0.6.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@ethersphere/bee-dashboard", "name": "@ethersphere/bee-dashboard",
"version": "0.5.0", "version": "0.6.0",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"@ethersphere/bee-js": "^1.0.0", "@ethersphere/bee-js": "^1.2.0",
"@material-ui/core": "^4.11.4", "@material-ui/core": "^4.11.4",
"@material-ui/icons": "^4.11.2", "@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.57", "@material-ui/lab": "^4.0.0-alpha.57",
@@ -1460,19 +1460,19 @@
} }
}, },
"node_modules/@ethersphere/bee-js": { "node_modules/@ethersphere/bee-js": {
"version": "1.0.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ethersphere/bee-js/-/bee-js-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@ethersphere/bee-js/-/bee-js-1.2.0.tgz",
"integrity": "sha512-u+TnKf0loAQba7AfZ54kXIEWjNdqVMUR6NXwINhHa2IdoD2b52rcuP96eMImlUm4tEJkxi2yAS+lcIZ2U9/1Vw==", "integrity": "sha512-RtRsTOq1ZtftT//0ibPWjohIwMOgJ/4KhMynQ1yaroih6G6krAnXve2K/hZtyWA9d7dpLxcT4aKd9S4QmISOgA==",
"dependencies": { "dependencies": {
"axios": "^0.21.1", "axios": "^0.21.1",
"elliptic": "^6.5.4", "elliptic": "^6.5.4",
"isomorphic-ws": "^4.0.1", "isomorphic-ws": "^4.0.1",
"js-sha3": "^0.8.0", "js-sha3": "^0.8.0",
"tar-js": "^0.3.0", "tar-js": "^0.3.0",
"ws": "^7.4.4" "ws": "^7.5.0"
}, },
"engines": { "engines": {
"bee": "1.0.0-2572fa48", "bee": "1.1.0-80cdea19",
"node": ">=12.0.0", "node": ">=12.0.0",
"npm": ">=6.0.0" "npm": ">=6.0.0"
} }
@@ -4776,10 +4776,14 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001191", "version": "1.0.30001251",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001191.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001251.tgz",
"integrity": "sha512-xJJqzyd+7GCJXkcoBiQ1GuxEiOBCLQ0aVW9HMekifZsAVGdj5eJ4mFB9fEhSHipq9IOk/QXFJUiIr9lZT+EsGw==", "integrity": "sha512-HOe1r+9VkU4TFmnU70z+r7OLmtR+/chB1rdcJUeQlAinjEeb0cKL20tlAtOagNZhbrtLnCvV19B4FmF1rgzl6A==",
"dev": true "dev": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
}
}, },
"node_modules/capture-exit": { "node_modules/capture-exit": {
"version": "2.0.0", "version": "2.0.0",
@@ -21434,9 +21438,9 @@
} }
}, },
"node_modules/ws": { "node_modules/ws": {
"version": "7.4.5", "version": "7.5.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz",
"integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==", "integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==",
"engines": { "engines": {
"node": ">=8.3.0" "node": ">=8.3.0"
}, },
@@ -22895,16 +22899,16 @@
} }
}, },
"@ethersphere/bee-js": { "@ethersphere/bee-js": {
"version": "1.0.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ethersphere/bee-js/-/bee-js-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@ethersphere/bee-js/-/bee-js-1.2.0.tgz",
"integrity": "sha512-u+TnKf0loAQba7AfZ54kXIEWjNdqVMUR6NXwINhHa2IdoD2b52rcuP96eMImlUm4tEJkxi2yAS+lcIZ2U9/1Vw==", "integrity": "sha512-RtRsTOq1ZtftT//0ibPWjohIwMOgJ/4KhMynQ1yaroih6G6krAnXve2K/hZtyWA9d7dpLxcT4aKd9S4QmISOgA==",
"requires": { "requires": {
"axios": "^0.21.1", "axios": "^0.21.1",
"elliptic": "^6.5.4", "elliptic": "^6.5.4",
"isomorphic-ws": "^4.0.1", "isomorphic-ws": "^4.0.1",
"js-sha3": "^0.8.0", "js-sha3": "^0.8.0",
"tar-js": "^0.3.0", "tar-js": "^0.3.0",
"ws": "^7.4.4" "ws": "^7.5.0"
} }
}, },
"@hapi/address": { "@hapi/address": {
@@ -25776,9 +25780,9 @@
} }
}, },
"caniuse-lite": { "caniuse-lite": {
"version": "1.0.30001191", "version": "1.0.30001251",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001191.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001251.tgz",
"integrity": "sha512-xJJqzyd+7GCJXkcoBiQ1GuxEiOBCLQ0aVW9HMekifZsAVGdj5eJ4mFB9fEhSHipq9IOk/QXFJUiIr9lZT+EsGw==", "integrity": "sha512-HOe1r+9VkU4TFmnU70z+r7OLmtR+/chB1rdcJUeQlAinjEeb0cKL20tlAtOagNZhbrtLnCvV19B4FmF1rgzl6A==",
"dev": true "dev": true
}, },
"capture-exit": { "capture-exit": {
@@ -39876,9 +39880,9 @@
} }
}, },
"ws": { "ws": {
"version": "7.4.5", "version": "7.5.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz",
"integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==", "integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==",
"requires": {} "requires": {}
}, },
"xml-name-validator": { "xml-name-validator": {
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ethersphere/bee-dashboard", "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", "description": "An app which helps users to setup their Bee node and do actions like cash out cheques",
"keywords": [ "keywords": [
"bee", "bee",
@@ -24,7 +24,7 @@
"url": "https://github.com/ethersphere/bee-dashboard.git" "url": "https://github.com/ethersphere/bee-dashboard.git"
}, },
"dependencies": { "dependencies": {
"@ethersphere/bee-js": "^1.0.0", "@ethersphere/bee-js": "^1.2.0",
"@material-ui/core": "^4.11.4", "@material-ui/core": "^4.11.4",
"@material-ui/icons": "^4.11.2", "@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.57", "@material-ui/lab": "^4.0.0-alpha.57",
+9
View File
@@ -9,6 +9,9 @@ import { SnackbarProvider } from 'notistack'
import BaseRouter from './routes/routes' import BaseRouter from './routes/routes'
import { lightTheme, darkTheme } from './theme' import { lightTheme, darkTheme } from './theme'
import { Provider as StampsProvider } from './providers/Stamps' import { Provider as StampsProvider } from './providers/Stamps'
import { Provider as PlatformProvider } from './providers/Platform'
import { Provider as BeeProvider } from './providers/Bee'
import { Provider as SettingsProvider } from './providers/Settings'
const App = (): ReactElement => { const App = (): ReactElement => {
const [themeMode, toggleThemeMode] = useState('light') const [themeMode, toggleThemeMode] = useState('light')
@@ -35,7 +38,10 @@ const App = (): ReactElement => {
return ( return (
<div className="App"> <div className="App">
<ThemeProvider theme={themeMode === 'light' ? lightTheme : darkTheme}> <ThemeProvider theme={themeMode === 'light' ? lightTheme : darkTheme}>
<SettingsProvider>
<BeeProvider>
<StampsProvider> <StampsProvider>
<PlatformProvider>
<SnackbarProvider> <SnackbarProvider>
<> <>
<CssBaseline /> <CssBaseline />
@@ -44,7 +50,10 @@ const App = (): ReactElement => {
</Router> </Router>
</> </>
</SnackbarProvider> </SnackbarProvider>
</PlatformProvider>
</StampsProvider> </StampsProvider>
</BeeProvider>
</SettingsProvider>
</ThemeProvider> </ThemeProvider>
</div> </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 { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import { Alert, AlertTitle } from '@material-ui/lab' import { Alert, AlertTitle } from '@material-ui/lab'
import Collapse from '@material-ui/core/Collapse' import Collapse from '@material-ui/core/Collapse'
import IconButton from '@material-ui/core/IconButton' import IconButton from '@material-ui/core/IconButton'
import CloseIcon from '@material-ui/icons/Close' 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' import { SUPPORTED_BEE_VERSION_EXACT } from '@ethersphere/bee-js'
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
@@ -18,12 +18,12 @@ const useStyles = makeStyles((theme: Theme) =>
export default function VersionAlert(): ReactElement | null { export default function VersionAlert(): ReactElement | null {
const classes = useStyles() const classes = useStyles()
const { isLoading, userVersion } = useStatusNodeVersion() const { isLoading, latestUserVersionExact } = useContext(Context)
const [open, setOpen] = useState<boolean>(true) 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 ( return (
<Collapse in={!isExactlySupportedBeeVersion && open}> <Collapse in={!isExactlySupportedBeeVersion && open}>
@@ -44,9 +44,9 @@ export default function VersionAlert(): ReactElement | null {
} }
> >
<AlertTitle>Warning</AlertTitle> <AlertTitle>Warning</AlertTitle>
Your Bee node version (<code>{userVersion}</code>) does not exactly match the Bee version we tested the Bee Your Bee node version (<code>{latestUserVersionExact}</code>) does not exactly match the Bee version we tested
Dashboard against (<code>{SUPPORTED_BEE_VERSION_EXACT}</code>). Please note that some functionality may not the Bee Dashboard against (<code>{SUPPORTED_BEE_VERSION_EXACT}</code>). Please note that some functionality
work properly. may not work properly.
</Alert> </Alert>
</div> </div>
</Collapse> </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 Button from '@material-ui/core/Button'
import Dialog from '@material-ui/core/Dialog' import Dialog from '@material-ui/core/Dialog'
import DialogActions from '@material-ui/core/DialogActions' import DialogActions from '@material-ui/core/DialogActions'
import DialogContent from '@material-ui/core/DialogContent' import DialogContent from '@material-ui/core/DialogContent'
import DialogContentText from '@material-ui/core/DialogContentText' import DialogContentText from '@material-ui/core/DialogContentText'
import DialogTitle from '@material-ui/core/DialogTitle' import DialogTitle from '@material-ui/core/DialogTitle'
import { Container, CircularProgress } from '@material-ui/core'
import { useSnackbar } from 'notistack' import { useSnackbar } from 'notistack'
import { ReactElement, useState, useContext } from 'react'
import { beeDebugApi } from '../services/bee' import { Context as SettingsContext } from '../providers/Settings'
import EthereumAddress from './EthereumAddress' import EthereumAddress from './EthereumAddress'
interface Props { interface Props {
@@ -17,10 +15,11 @@ interface Props {
uncashedAmount: string 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 [open, setOpen] = useState<boolean>(false)
const [loadingCashout, setLoadingCashout] = useState<boolean>(false) const [loadingCashout, setLoadingCashout] = useState<boolean>(false)
const { enqueueSnackbar } = useSnackbar() const { enqueueSnackbar } = useSnackbar()
const { beeDebugApi } = useContext(SettingsContext)
const handleClickOpen = () => { const handleClickOpen = () => {
setOpen(true) setOpen(true)
@@ -31,10 +30,12 @@ export default function DepositModal({ peerId, uncashedAmount }: Props): ReactEl
} }
const handleCashout = () => { const handleCashout = () => {
if (!beeDebugApi) return
if (peerId) { if (peerId) {
setLoadingCashout(true) setLoadingCashout(true)
beeDebugApi.chequebook beeDebugApi
.peerCashout(peerId) .cashoutLastCheque(peerId)
.then(res => { .then(res => {
setOpen(false) setOpen(false)
enqueueSnackbar( enqueueSnackbar(
+18 -134
View File
@@ -1,13 +1,7 @@
import React, { ReactElement, useEffect } from 'react' import { ReactElement, useContext } from 'react'
import { withStyles, Theme, createStyles } from '@material-ui/core/styles' import TabsContainer from './TabsContainer'
import { Tabs, Tab, Box, Typography } from '@material-ui/core'
import CodeBlock from './CodeBlock' import CodeBlock from './CodeBlock'
import { Context } from '../providers/Platform'
interface TabPanelProps {
children?: React.ReactNode
index: number
value: number
}
interface Props { interface Props {
linux: string linux: string
@@ -15,133 +9,23 @@ interface Props {
showLineNumbers?: boolean 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 { export default function CodeBlockTabs(props: Props): ReactElement {
const [value, setValue] = React.useState(0) const { platform, setPlatform } = useContext(Context)
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 ( return (
<div <TabsContainer
role="tabpanel" index={platform}
hidden={value !== index} indexChanged={setPlatform}
id={`simple-tabpanel-${index}`} values={[
aria-labelledby={`simple-tab-${index}`} {
{...other} label: 'Linux',
> component: <CodeBlock showLineNumbers={props.showLineNumbers} language="bash" code={props.linux} />,
{value === index && ( },
<Box style={{ marginTop: '-12px' }}> {
<Typography component="div">{children}</Typography> label: 'macOS',
</Box> component: <CodeBlock showLineNumbers={props.showLineNumbers} language="bash" code={props.mac} />,
)} },
</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} />)
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>
) )
} }
+5 -17
View File
@@ -1,31 +1,25 @@
import React, { ReactElement, useState } from 'react' import React, { ReactElement, useState } from 'react'
import { TextField, Button, CircularProgress, Container } from '@material-ui/core' import { TextField, Button } from '@material-ui/core'
interface Props { interface Props {
defaultHost?: string defaultHost?: string
hostName: string setHost: (host: string) => void
} }
export default function ConnectToHost(props: Props): ReactElement { export default function ConnectToHost(props: Props): ReactElement {
const [hostInputVisible, toggleHostInputVisibility] = useState(false) const [hostInputVisible, toggleHostInputVisibility] = useState(false)
const [connectingToHost, setConnectingToHost] = useState(false)
const [host, setHost] = useState('') const [host, setHost] = useState('')
const handleNewHostConnection = () => { const handleNewHostConnection = () => {
if (host) { if (host) {
setConnectingToHost(true) props.setHost(host)
sessionStorage.setItem(props.hostName, host)
toggleHostInputVisibility(!hostInputVisible) toggleHostInputVisibility(!hostInputVisible)
window.location.reload()
} }
} }
return ( return (
<div> <div>
{ {hostInputVisible ? (
// FIXME: this should be broken up
/* eslint-disable no-nested-ternary */
hostInputVisible ? (
<div style={{ display: 'flex' }}> <div style={{ display: 'flex' }}>
<TextField <TextField
defaultValue={props.defaultHost} defaultValue={props.defaultHost}
@@ -46,17 +40,11 @@ export default function ConnectToHost(props: Props): ReactElement {
Cancel Cancel
</Button> </Button>
</div> </div>
) : connectingToHost ? (
<Container style={{ textAlign: 'center', padding: '0px' }}>
<CircularProgress size={20} />
</Container>
) : ( ) : (
<Button onClick={() => toggleHostInputVisibility(!hostInputVisible)} size="small" variant="outlined"> <Button onClick={() => toggleHostInputVisibility(!hostInputVisible)} size="small" variant="outlined">
Change host Change host
</Button> </Button>
) )}
/* eslint-enable no-nested-ternary */
}
</div> </div>
) )
} }
+1 -17
View File
@@ -4,7 +4,6 @@ import { createStyles, makeStyles } from '@material-ui/core/styles'
import { Card, CardContent, Typography } from '@material-ui/core/' import { Card, CardContent, Typography } from '@material-ui/core/'
import EthereumAddress from '../components/EthereumAddress' import EthereumAddress from '../components/EthereumAddress'
import { Skeleton } from '@material-ui/lab'
import type { ChequebookAddressResponse, NodeAddresses } from '@ethersphere/bee-js' import type { ChequebookAddressResponse, NodeAddresses } from '@ethersphere/bee-js'
@@ -28,9 +27,7 @@ const useStyles = makeStyles(() =>
interface Props { interface Props {
nodeAddresses: NodeAddresses | null nodeAddresses: NodeAddresses | null
isLoadingNodeAddresses: boolean
chequebookAddress: ChequebookAddressResponse | null chequebookAddress: ChequebookAddressResponse | null
isLoadingChequebookAddress: boolean
} }
function EthereumAddressCard(props: Props): ReactElement { function EthereumAddressCard(props: Props): ReactElement {
@@ -38,12 +35,6 @@ function EthereumAddressCard(props: Props): ReactElement {
return ( return (
<Card className={classes.root}> <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}> <div className={classes.details}>
<CardContent className={classes.content}> <CardContent className={classes.content}>
<Typography variant="subtitle1" gutterBottom> <Typography variant="subtitle1" gutterBottom>
@@ -52,13 +43,7 @@ function EthereumAddressCard(props: Props): ReactElement {
<EthereumAddress address={props.nodeAddresses?.ethereum} /> <EthereumAddress address={props.nodeAddresses?.ethereum} />
</CardContent> </CardContent>
</div> </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}> <div className={classes.details}>
<CardContent className={classes.content}> <CardContent className={classes.content}>
<Typography variant="subtitle1" gutterBottom> <Typography variant="subtitle1" gutterBottom>
@@ -67,7 +52,6 @@ function EthereumAddressCard(props: Props): ReactElement {
<EthereumAddress address={props.chequebookAddress?.chequebookAddress} /> <EthereumAddress address={props.chequebookAddress?.chequebookAddress} />
</CardContent> </CardContent>
</div> </div>
)}
</Card> </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 { Card, CardContent, Typography } from '@material-ui/core/'
import { makeStyles } from '@material-ui/core/styles'
import { Skeleton } from '@material-ui/lab' import { Skeleton } from '@material-ui/lab'
import type { ReactElement } from 'react'
import { Title } from './Title'
const useStyles = makeStyles({ const useStyles = makeStyles({
root: { root: {
minWidth: 275, minWidth: 275,
}, },
title: {
fontSize: 16,
},
pos: {
marginBottom: 12,
},
}) })
interface Props { interface Props {
label: string label: string
statistic?: string statistic?: string
loading?: boolean 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() const classes = useStyles()
return ( return (
@@ -36,9 +31,7 @@ export default function StatCard({ loading, label, statistic }: Props): ReactEle
)} )}
{!loading && ( {!loading && (
<> <>
<Typography className={classes.title} color="textSecondary" gutterBottom> <Title label={label} tooltip={tooltip} />
{label}
</Typography>
<Typography variant="h5" component="h2"> <Typography variant="h5" component="h2">
{statistic} {statistic}
</Typography> </Typography>
+14 -15
View File
@@ -15,13 +15,7 @@ function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props const { children, value, index, ...other } = props
return ( return (
<div <div role="tabpanel" hidden={value !== index} {...other}>
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && ( {value === index && (
<Box p={3}> <Box p={3}>
<Typography>{children}</Typography> <Typography>{children}</Typography>
@@ -44,25 +38,30 @@ interface TabsValues {
interface Props { interface Props {
values: TabsValues[] 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 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) => { 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 ( return (
<div className={classes.root}> <div className={classes.root}>
<Tabs value={value} onChange={handleChange} aria-label="simple tabs example"> <Tabs value={v} onChange={handleChange}>
{values.map(({ label }, index) => ( {values.map(({ label }, idx) => (
<Tab key={index} label={label} /> <Tab key={idx} label={label} />
))} ))}
</Tabs> </Tabs>
{values.map(({ component }, index) => ( {values.map(({ component }, idx) => (
<TabPanel key={index} value={value} index={index}> <TabPanel key={idx} value={v} index={idx}>
{component} {component}
</TabPanel> </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 type { Topology } from '@ethersphere/bee-js'
import { Grid } from '@material-ui/core/' import { Grid } from '@material-ui/core/'
import type { ReactElement } from 'react' import type { ReactElement } from 'react'
import { pickThreshold, ThresholdValues } from '../utils/threshold'
import StatCard from './StatCard' import StatCard from './StatCard'
interface Props { interface RootProps {
isLoading: boolean
topology: Topology | null 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 style={{ marginBottom: '20px', flexGrow: 1 }}>
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid key={1} item xs={12} sm={12} md={6} lg={4} xl={4}> <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>
<Grid key={2} item xs={12} sm={12} md={6} lg={4} xl={4}> <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>
<Grid key={3} item xs={12} sm={12} md={6} lg={4} xl={4}> <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> </Grid>
</Grid> </Grid>
@@ -21,7 +21,7 @@ interface Props {
action: (amount: bigint) => Promise<string> action: (amount: bigint) => Promise<string>
} }
export default function WithdrawModal({ export default function WithdrawDepositModal({
successMessage, successMessage,
errorMessage, errorMessage,
dialogMessage, 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 { ReactElement, useContext } from 'react'
import { beeDebugApi } from '../services/bee' import { Context as SettingsContext } from '../providers/Settings'
import WDModal from '../components/WDModal' import WithdrawDepositModal from '../components/WithdrawDepositModal'
import { BigNumber } from 'bignumber.js' import { BigNumber } from 'bignumber.js'
export default function DepositModal(): ReactElement { export default function DepositModal(): ReactElement {
const { beeDebugApi } = useContext(SettingsContext)
return ( return (
<WDModal <WithdrawDepositModal
successMessage="Successful deposit." successMessage="Successful deposit."
errorMessage="Error with depositing" errorMessage="Error with depositing"
dialogMessage="Specify the amount of BZZ you would like to withdraw from your node." dialogMessage="Specify the amount of BZZ you would like to withdraw from your node."
label="Deposit" label="Deposit"
min={new BigNumber(0)} min={new BigNumber(0)}
action={beeDebugApi.chequebook.deposit} action={(amount: bigint) => {
if (!beeDebugApi) throw new Error('Bee Debug URL is not valid')
return beeDebugApi.depositTokens(amount.toString())
}}
/> />
) )
} }
+11 -5
View File
@@ -1,18 +1,24 @@
import type { ReactElement } from 'react' import { ReactElement, useContext } from 'react'
import { beeDebugApi } from '../services/bee' import { Context as SettingsContext } from '../providers/Settings'
import WDModal from '../components/WDModal' import WithdrawDepositModal from '../components/WithdrawDepositModal'
import { BigNumber } from 'bignumber.js' import { BigNumber } from 'bignumber.js'
export default function WithdrawModal(): ReactElement { export default function WithdrawModal(): ReactElement {
const { beeDebugApi } = useContext(SettingsContext)
return ( return (
<WDModal <WithdrawDepositModal
successMessage="Successful withdrawl." successMessage="Successful withdrawl."
errorMessage="Error with withdrawing." errorMessage="Error with withdrawing."
dialogMessage="Specify the amount of BZZ you would like to withdraw from your node." dialogMessage="Specify the amount of BZZ you would like to withdraw from your node."
label="Withdraw" label="Withdraw"
min={new BigNumber(0)} min={new BigNumber(0)}
action={beeDebugApi.chequebook.withdraw} action={(amount: bigint) => {
if (!beeDebugApi) throw new Error('Bee Debug URL is not valid')
return beeDebugApi.withdrawTokens(amount.toString())
}}
/> />
) )
} }
+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 { useEffect, useState } from 'react'
import { Token } from '../models/Token' import { Token } from '../models/Token'
import { beeDebugApi } from '../services/bee' import { makeRetriablePromise, unwrapPromiseSettlements } from '../utils'
import { Balance, Settlement, useApiPeerBalances, useApiSettlements } from './apiHooks' import { Balance, Settlements, Settlement } from '../types'
interface UseAccountingHook { interface UseAccountingHook {
isLoading: boolean
isLoadingUncashed: boolean isLoadingUncashed: boolean
error: Error | null
totalsent: Token
totalreceived: Token
accounting: Accounting[] | null accounting: Accounting[] | null
} }
@@ -76,39 +72,34 @@ function mergeAccounting(
) )
} }
export const useAccounting = (): UseAccountingHook => { export const useAccounting = (
const settlements = useApiSettlements() beeDebugApi: BeeDebug | null,
const balances = useApiPeerBalances() settlements: Settlements | null,
balances: Balance[] | null,
const [err, setErr] = useState<Error | null>(null) ): UseAccountingHook => {
const [isLoadingUncashed, setIsloadingUncashed] = useState<boolean>(false) const [isLoadingUncashed, setIsloadingUncashed] = useState<boolean>(false)
const [uncashedAmounts, setUncashedAmounts] = useState<LastCashoutActionResponse[] | undefined>(undefined) const [uncashedAmounts, setUncashedAmounts] = useState<LastCashoutActionResponse[] | undefined>(undefined)
const error = balances.error || settlements.error || err
useEffect(() => { useEffect(() => {
// We don't have any settlements loaded yet or we are already loading/have loaded the uncashed amounts // We don't have any settlements loaded yet or we are already loading/have loaded the uncashed amounts
if (isLoadingUncashed || !settlements.settlements || uncashedAmounts || error) return if (isLoadingUncashed || !beeDebugApi || !settlements || uncashedAmounts) return
setIsloadingUncashed(true) setIsloadingUncashed(true)
const promises = settlements.settlements.settlements const promises = settlements.settlements
.filter(({ received }) => received.toBigNumber.gt('0')) .filter(({ received }) => received.toBigNumber.gt('0'))
.map(({ peer }) => beeDebugApi.chequebook.getPeerLastCashout(peer)) .map(({ peer }) => makeRetriablePromise(() => beeDebugApi.getLastCashoutAction(peer)))
Promise.all(promises) Promise.allSettled(promises).then(settlements => {
.then(setUncashedAmounts) const results = unwrapPromiseSettlements(settlements)
.catch(setErr) setUncashedAmounts(results.fulfilled)
.finally(() => setIsloadingUncashed(false)) setIsloadingUncashed(false)
}, [settlements, isLoadingUncashed, uncashedAmounts, error]) })
}, [settlements, isLoadingUncashed, uncashedAmounts])
const accounting = mergeAccounting(balances.peerBalances, settlements.settlements?.settlements, uncashedAmounts) const accounting = mergeAccounting(balances, settlements?.settlements, uncashedAmounts)
return { return {
isLoading: settlements.isLoadingSettlements || balances.isLoadingPeerBalances,
isLoadingUncashed, isLoadingUncashed,
error,
accounting, accounting,
totalsent: settlements.settlements?.totalSent || new Token('0'),
totalreceived: settlements.settlements?.totalReceived || new Token('0'),
} }
} }
-401
View File
@@ -1,406 +1,5 @@
import { useState, useEffect } from 'react' 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 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 { export interface LatestBeeReleaseHook {
latestBeeRelease: LatestBeeRelease | null 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 ErrorBoundary from '../components/ErrorBoundary'
import AlertVersion from '../components/AlertVersion' import AlertVersion from '../components/AlertVersion'
import { Container, CircularProgress } from '@material-ui/core'
import { createStyles, Theme, makeStyles } from '@material-ui/core/styles' import { createStyles, Theme, makeStyles } from '@material-ui/core/styles'
import SideBar from '../components/SideBar' import SideBar from '../components/SideBar'
import NavBar from '../components/NavBar' import NavBar from '../components/NavBar'
import { useApiHealth, useDebugApiHealth } from '../hooks/apiHooks' import { Context } from '../providers/Bee'
import { RouteComponentProps } from 'react-router' import { RouteComponentProps } from 'react-router'
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
@@ -31,9 +33,7 @@ const Dashboard = (props: Props): ReactElement => {
const [themeMode, toggleThemeMode] = useState('light') const [themeMode, toggleThemeMode] = useState('light')
// FIXME: handle errrors and loading const { isLoading, apiHealth, debugApiHealth } = useContext(Context)
const { health } = useApiHealth()
const { nodeHealth } = useDebugApiHealth()
useEffect(() => { useEffect(() => {
const theme = localStorage.getItem('theme') const theme = localStorage.getItem('theme')
@@ -56,12 +56,18 @@ const Dashboard = (props: Props): ReactElement => {
return ( return (
<div> <div>
<SideBar {...props} themeMode={themeMode} health={health} nodeHealth={nodeHealth} /> <SideBar {...props} themeMode={themeMode} health={apiHealth} nodeHealth={debugApiHealth} />
<NavBar themeMode={themeMode} /> <NavBar themeMode={themeMode} />
<ErrorBoundary> <ErrorBoundary>
<main className={classes.content}> <main className={classes.content}>
<AlertVersion /> <AlertVersion />
{props.children} {isLoading ? (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress />
</Container>
) : (
props.children
)}
</main> </main>
</ErrorBoundary> </ErrorBoundary>
</div> </div>
+4 -15
View File
@@ -2,7 +2,6 @@ import { ReactElement } from 'react'
import { createStyles, makeStyles } from '@material-ui/core/styles' import { createStyles, makeStyles } from '@material-ui/core/styles'
import { Card, CardContent, Typography, Theme } from '@material-ui/core/' import { Card, CardContent, Typography, Theme } from '@material-ui/core/'
import { Skeleton } from '@material-ui/lab'
import WithdrawModal from '../../containers/WithdrawModal' import WithdrawModal from '../../containers/WithdrawModal'
import DepositModal from '../../containers/DepositModal' import DepositModal from '../../containers/DepositModal'
@@ -45,12 +44,11 @@ interface ChequebookBalance {
interface Props { interface Props {
chequebookAddress: ChequebookAddressResponse | null chequebookAddress: ChequebookAddressResponse | null
chequebookBalance: ChequebookBalance | null chequebookBalance: ChequebookBalance | null
totalsent: Token totalsent?: Token
totalreceived: Token totalreceived?: Token
isLoading: boolean
} }
function AccountCard({ totalreceived, totalsent, chequebookBalance, isLoading }: Props): ReactElement { function AccountCard({ totalreceived, totalsent, chequebookBalance }: Props): ReactElement {
const classes = useStyles() const classes = useStyles()
return ( return (
@@ -66,7 +64,6 @@ function AccountCard({ totalreceived, totalsent, chequebookBalance, isLoading }:
</div> </div>
<Card className={classes.root}> <Card className={classes.root}>
{!isLoading && (
<CardContent className={classes.gridContainer}> <CardContent className={classes.gridContainer}>
<div> <div>
<Typography component="h2" variant="h6" color="primary" gutterBottom> <Typography component="h2" variant="h6" color="primary" gutterBottom>
@@ -85,18 +82,10 @@ function AccountCard({ totalreceived, totalsent, chequebookBalance, isLoading }:
Total Sent / Received Total Sent / Received
</Typography> </Typography>
<Typography variant="h5"> <Typography variant="h5">
{totalsent.toFixedDecimal()} / {totalreceived.toFixedDecimal()} BZZ {totalsent?.toFixedDecimal()} / {totalreceived?.toFixedDecimal()} BZZ
</Typography> </Typography>
</div> </div>
</CardContent> </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" />
</div>
)}
</Card> </Card>
</div> </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 { Theme, createStyles, makeStyles } from '@material-ui/core/styles'
import { Container, CircularProgress } from '@material-ui/core'
import AccountCard from '../accounting/AccountCard' import AccountCard from '../accounting/AccountCard'
import BalancesTable from './BalancesTable' import BalancesTable from './BalancesTable'
import EthereumAddressCard from '../../components/EthereumAddressCard' import EthereumAddressCard from '../../components/EthereumAddressCard'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard' import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { Context as BeeContext } from '../../providers/Bee'
import { import { Context as SettingsContext } from '../../providers/Settings'
useApiNodeAddresses,
useApiChequebookAddress,
useApiChequebookBalance,
useApiHealth,
useDebugApiHealth,
} from '../../hooks/apiHooks'
import { useAccounting } from '../../hooks/accounting' import { useAccounting } from '../../hooks/accounting'
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
@@ -29,44 +22,25 @@ const useStyles = makeStyles((theme: Theme) =>
export default function Accounting(): ReactElement { export default function Accounting(): ReactElement {
const classes = useStyles() const classes = useStyles()
const { chequebookAddress, isLoadingChequebookAddress } = useApiChequebookAddress() const { status, nodeAddresses, chequebookAddress, chequebookBalance, settlements, peerBalances } = useContext(
const { chequebookBalance, isLoadingChequebookBalance } = useApiChequebookBalance() BeeContext,
const { nodeAddresses, isLoadingNodeAddresses } = useApiNodeAddresses()
const { health, isLoadingHealth } = useApiHealth()
const { nodeHealth, isLoadingNodeHealth } = useDebugApiHealth()
const { isLoading, totalsent, totalreceived, accounting, isLoadingUncashed, error } = useAccounting()
if (isLoadingHealth || isLoadingNodeHealth) {
return (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress />
</Container>
) )
} const { beeDebugApi } = useContext(SettingsContext)
if (nodeHealth?.status !== 'ok' || !health) return <TroubleshootConnectionCard /> const { accounting, isLoadingUncashed } = useAccounting(beeDebugApi, settlements, peerBalances)
if (!status.all) return <TroubleshootConnectionCard />
return ( return (
<div className={classes.root}> <div className={classes.root}>
<AccountCard <AccountCard
chequebookAddress={chequebookAddress} chequebookAddress={chequebookAddress}
isLoading={isLoadingChequebookAddress || isLoading || isLoadingChequebookBalance}
chequebookBalance={chequebookBalance} chequebookBalance={chequebookBalance}
totalsent={totalsent} totalsent={settlements?.totalSent}
totalreceived={totalreceived} totalreceived={settlements?.totalReceived}
/> />
<EthereumAddressCard <EthereumAddressCard nodeAddresses={nodeAddresses} chequebookAddress={chequebookAddress} />
nodeAddresses={nodeAddresses} <BalancesTable accounting={accounting} isLoadingUncashed={isLoadingUncashed} />
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} />}
</div> </div>
) )
} }
+4 -3
View File
@@ -1,8 +1,8 @@
import { ReactElement, useState } from 'react' import { ReactElement, useState, useContext } from 'react'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import { Paper, InputBase, IconButton, FormHelperText } from '@material-ui/core' import { Paper, InputBase, IconButton, FormHelperText } from '@material-ui/core'
import { Search } from '@material-ui/icons' import { Search } from '@material-ui/icons'
import { apiHost } from '../../constants' import { Context as SettingsContext } from '../../providers/Settings'
import { Utils } from '@ethersphere/bee-js' import { Utils } from '@ethersphere/bee-js'
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
@@ -28,6 +28,7 @@ const useStyles = makeStyles((theme: Theme) =>
export default function Files(): ReactElement { export default function Files(): ReactElement {
const classes = useStyles() const classes = useStyles()
const { apiUrl } = useContext(SettingsContext)
const [referenceInput, setReferenceInput] = useState('') const [referenceInput, setReferenceInput] = useState('')
const [referenceError, setReferenceError] = useState<Error | null>(null) const [referenceError, setReferenceError] = useState<Error | null>(null)
@@ -50,7 +51,7 @@ export default function Files(): ReactElement {
onChange={handleReferenceChange} onChange={handleReferenceChange}
/> />
<IconButton <IconButton
href={`${apiHost}/bzz/${referenceInput}`} href={`${apiUrl}/bzz/${referenceInput}`}
target="_blank" target="_blank"
disabled={referenceError !== null || !referenceInput} disabled={referenceError !== null || !referenceInput}
className={classes.iconButton} className={classes.iconButton}
+6 -2
View File
@@ -8,7 +8,7 @@ import UploadSizeAlert from '../../components/AlertUploadSize'
import ClipboardCopy from '../../components/ClipboardCopy' import ClipboardCopy from '../../components/ClipboardCopy'
import PeerDetailDrawer from '../../components/PeerDetail' import PeerDetailDrawer from '../../components/PeerDetail'
import { Context, EnrichedPostageBatch } from '../../providers/Stamps' import { Context, EnrichedPostageBatch } from '../../providers/Stamps'
import { beeApi } from '../../services/bee' import { Context as SettingsContext } from '../../providers/Settings'
import CreatePostageStamp from '../stamps/CreatePostageStampModal' import CreatePostageStamp from '../stamps/CreatePostageStampModal'
import SelectStamp from './SelectStamp' import SelectStamp from './SelectStamp'
@@ -23,6 +23,7 @@ export default function Files(): ReactElement {
const [selectedStamp, setSelectedStamp] = useState<EnrichedPostageBatch | null>(null) const [selectedStamp, setSelectedStamp] = useState<EnrichedPostageBatch | null>(null)
const { isLoading, error, stamps } = useContext(Context) const { isLoading, error, stamps } = useContext(Context)
const { beeApi } = useContext(SettingsContext)
const { enqueueSnackbar } = useSnackbar() const { enqueueSnackbar } = useSnackbar()
// Choose a postage stamp that has the lowest usage // Choose a postage stamp that has the lowest usage
@@ -40,8 +41,11 @@ export default function Files(): ReactElement {
const uploadFile = () => { const uploadFile = () => {
if (file === null || selectedStamp === null) return if (file === null || selectedStamp === null) return
if (!beeApi) return
setIsUploadingFile(true) setIsUploadingFile(true)
beeApi.files beeApi
.uploadFile(selectedStamp.batchID, file) .uploadFile(selectedStamp.batchID, file)
.then(hash => { .then(hash => {
window.setTimeout(() => { window.setTimeout(() => {
+5 -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 TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { useApiHealth, useDebugApiHealth } from '../../hooks/apiHooks' import { Context } from '../../providers/Bee'
import Download from './Download' import Download from './Download'
import Upload from './Upload' import Upload from './Upload'
import TabsContainer from '../../components/TabsContainer' import TabsContainer from '../../components/TabsContainer'
export default function Files(): ReactElement { export default function Files(): ReactElement {
const { health, isLoadingHealth } = useApiHealth() const { status } = useContext(Context)
const { nodeHealth, isLoadingNodeHealth } = useDebugApiHealth()
if (isLoadingHealth || isLoadingNodeHealth) { if (!status.all) return <TroubleshootConnectionCard />
return (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress />
</Container>
)
}
if (!health || nodeHealth?.status !== 'ok') return <TroubleshootConnectionCard />
return ( return (
<Container maxWidth="sm"> <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 { makeStyles } from '@material-ui/core/styles'
import { import {
Table, Table,
@@ -10,12 +10,11 @@ import {
Button, Button,
Paper, Paper,
Tooltip, Tooltip,
Container,
CircularProgress, CircularProgress,
} from '@material-ui/core' } from '@material-ui/core'
import { Autorenew } from '@material-ui/icons' import { Autorenew } from '@material-ui/icons'
import { beeDebugApi } from '../../services/bee' import { Context as SettingsContext } from '../../providers/Settings'
import type { Peer } from '@ethersphere/bee-js' import type { Peer } from '@ethersphere/bee-js'
const useStyles = makeStyles({ const useStyles = makeStyles({
@@ -26,47 +25,43 @@ const useStyles = makeStyles({
interface Props { interface Props {
peers: Peer[] | null 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 { function PeerTable(props: Props): ReactElement {
const classes = useStyles() const classes = useStyles()
const { beeDebugApi } = useContext(SettingsContext)
const [peerLatency, setPeerLatency] = useState([{ peerId: '', rtt: '', loading: false }]) const [peerLatency, setPeerLatency] = useState<Record<string, PeerLatency>>({})
const PingPeer = (peerId: string) => { const pingPeer = (peerId: string) => {
setPeerLatency([...peerLatency, { peerId: peerId, rtt: '', loading: true }]) setPeerLatency(prevPeerLatency => ({ ...prevPeerLatency, [peerId]: { rtt: '', loading: true } }))
beeDebugApi.connectivity beeDebugApi
.ping(peerId) ?.pingPeer(peerId)
.then(res => { .then(res => {
setPeerLatency([...peerLatency, { peerId: peerId, rtt: res.rtt, loading: false }]) setPeerLatency(prevPeerLatency => ({ ...prevPeerLatency, [peerId]: { rtt: res.rtt, loading: false } }))
}) })
.catch(() => { .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 ( return (
<div> <div>
<TableContainer component={Paper}> <TableContainer component={Paper}>
<Table className={classes.table} aria-label="simple table"> <Table className={classes.table}>
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell>Index</TableCell> <TableCell>Index</TableCell>
@@ -75,7 +70,7 @@ function PeerTable(props: Props): ReactElement {
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{props.peers.map((peer: Peer, idx: number) => ( {props.peers?.map((peer: Peer, idx: number) => (
<TableRow key={peer.address}> <TableRow key={peer.address}>
<TableCell component="th" scope="row"> <TableCell component="th" scope="row">
{idx + 1} {idx + 1}
@@ -83,21 +78,8 @@ function PeerTable(props: Props): ReactElement {
<TableCell>{peer.address}</TableCell> <TableCell>{peer.address}</TableCell>
<TableCell align="right"> <TableCell align="right">
<Tooltip title="Ping node"> <Tooltip title="Ping node">
<Button color="primary" onClick={() => PingPeer(peer.address)}> <Button color="primary" onClick={() => pingPeer(peer.address)}>
{ {getPingState(peerLatency, peer)}
// 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> </Button>
</Tooltip> </Tooltip>
</TableCell> </TableCell>
+6 -17
View File
@@ -1,32 +1,21 @@
import { Container, CircularProgress } from '@material-ui/core/'
import PeerTable from './PeerTable' import PeerTable from './PeerTable'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard' import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { useApiNodeTopology, useApiNodePeers, useDebugApiHealth } from '../../hooks/apiHooks' import { Context } from '../../providers/Bee'
import TopologyStats from '../../components/TopologyStats' import TopologyStats from '../../components/TopologyStats'
import { ReactElement } from 'react' import { ReactElement, useContext } from 'react'
export default function Peers(): ReactElement { export default function Peers(): ReactElement {
const topology = useApiNodeTopology() const { topology, peers, status } = useContext(Context)
const debugHealth = useDebugApiHealth()
const peers = useApiNodePeers()
if (debugHealth.isLoadingNodeHealth) { if (!status.all) {
return (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress />
</Container>
)
}
if (debugHealth.error) {
return <TroubleshootConnectionCard /> return <TroubleshootConnectionCard />
} }
return ( return (
<> <>
<TopologyStats {...topology} /> <TopologyStats topology={topology} />
<PeerTable {...peers} /> <PeerTable peers={peers} />
</> </>
) )
} }
+14 -28
View File
@@ -1,25 +1,19 @@
import React, { ReactElement, useState } from 'react' import React, { ReactElement, useState, useContext } from 'react'
import { Paper, Container, TextField, Typography, Button } from '@material-ui/core' import { Paper, Container, TextField, Typography, Button } from '@material-ui/core'
import { Context as SettingsContext } from '../../providers/Settings'
export default function Settings(): ReactElement { export default function Settings(): ReactElement {
const [refreshVisibility, toggleRefreshVisibility] = useState(false) const { apiUrl, apiDebugUrl, setApiUrl, setDebugApiUrl } = useContext(SettingsContext)
const [host, setHost] = useState('') const [host, setHost] = useState(apiUrl)
const [debugHost, setDebugHost] = useState('') const [debugHost, setDebugHost] = useState(apiDebugUrl)
const handleNewHostConnection = () => { const submit = () => {
if (host) { if (host !== apiUrl) setApiUrl(host)
sessionStorage.setItem('api_host', host)
if (debugHost !== apiDebugUrl) setDebugApiUrl(debugHost)
} }
if (debugHost) { const touched = host !== apiUrl || debugHost !== apiDebugUrl
sessionStorage.setItem('debug_api_host', debugHost)
}
if (host || debugHost) {
toggleRefreshVisibility(!refreshVisibility)
window.location.reload()
}
}
return ( return (
<div> <div>
@@ -34,16 +28,13 @@ export default function Settings(): ReactElement {
placeholder="ex: 127.0.0.0.1:1633" placeholder="ex: 127.0.0.0.1:1633"
helperText="Enter node host override / port" helperText="Enter node host override / port"
fullWidth fullWidth
defaultValue={ defaultValue={apiUrl}
sessionStorage.getItem('api_host') ? sessionStorage.getItem('api_host') : process.env.REACT_APP_BEE_HOST
}
margin="normal" margin="normal"
InputLabelProps={{ InputLabelProps={{
shrink: true, shrink: true,
}} }}
onChange={e => { onChange={e => {
setHost(e.target.value) setHost(e.target.value)
toggleRefreshVisibility(true)
}} }}
variant="filled" variant="filled"
/> />
@@ -55,14 +46,9 @@ export default function Settings(): ReactElement {
placeholder="ex: 127.0.0.0.1:1635" placeholder="ex: 127.0.0.0.1:1635"
helperText="Enter node debug host override / port" helperText="Enter node debug host override / port"
fullWidth fullWidth
defaultValue={ defaultValue={apiDebugUrl}
sessionStorage.getItem('debug_api_host')
? sessionStorage.getItem('debug_api_host')
: process.env.REACT_APP_BEE_DEBUG_HOST
}
onChange={e => { onChange={e => {
setDebugHost(e.target.value) setDebugHost(e.target.value)
toggleRefreshVisibility(true)
}} }}
margin="normal" margin="normal"
InputLabelProps={{ InputLabelProps={{
@@ -71,9 +57,9 @@ export default function Settings(): ReactElement {
variant="filled" variant="filled"
/> />
</Paper> </Paper>
{refreshVisibility ? ( {touched ? (
<div style={{ marginTop: '20px' }}> <div style={{ marginTop: '20px' }}>
<Button variant="outlined" color="primary" onClick={() => handleNewHostConnection()}> <Button variant="outlined" color="primary" onClick={submit}>
Save Save
</Button> </Button>
</div> </div>
+5 -2
View File
@@ -9,7 +9,7 @@ import DialogTitle from '@material-ui/core/DialogTitle'
import BigNumber from 'bignumber.js' import BigNumber from 'bignumber.js'
import { FormikHelpers, Form, Field, Formik } from 'formik' import { FormikHelpers, Form, Field, Formik } from 'formik'
import { TextField } from 'formik-material-ui' import { TextField } from 'formik-material-ui'
import { beeApi } from '../../services/bee' import { Context as SettingsContext } from '../../providers/Settings'
import { Context } from '../../providers/Stamps' import { Context } from '../../providers/Stamps'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import { useSnackbar } from 'notistack' import { useSnackbar } from 'notistack'
@@ -54,6 +54,7 @@ export default function FormDialog({ label }: Props): ReactElement {
const classes = useStyles() const classes = useStyles()
const [open, setOpen] = React.useState(false) const [open, setOpen] = React.useState(false)
const { refresh } = useContext(Context) const { refresh } = useContext(Context)
const { beeApi } = useContext(SettingsContext)
const handleClickOpen = () => setOpen(true) const handleClickOpen = () => setOpen(true)
const handleClose = () => setOpen(false) const handleClose = () => setOpen(false)
const { enqueueSnackbar } = useSnackbar() const { enqueueSnackbar } = useSnackbar()
@@ -66,10 +67,12 @@ export default function FormDialog({ label }: Props): ReactElement {
// This is really just a typeguard, the validation pretty much guarantees these will have the right values // This is really just a typeguard, the validation pretty much guarantees these will have the right values
if (!values.depth || !values.amount) return if (!values.depth || !values.amount) return
if (!beeApi) return
const amount = BigInt(values.amount) const amount = BigInt(values.amount)
const depth = Number.parseInt(values.depth) const depth = Number.parseInt(values.depth)
const options = values.label ? { label: values.label } : undefined const options = values.label ? { label: values.label } : undefined
await beeApi.stamps.buyPostageStamp(amount, depth, options) await beeApi.createPostageBatch(amount.toString(), depth, options)
actions.resetForm() actions.resetForm()
await refresh() await refresh()
handleClose() handleClose()
+5 -8
View File
@@ -5,10 +5,9 @@ import { Container, CircularProgress } from '@material-ui/core'
import StampsTable from './StampsTable' import StampsTable from './StampsTable'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard' import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import CreatePostageStampModal from './CreatePostageStampModal' import CreatePostageStampModal from './CreatePostageStampModal'
import LastUpdate from '../../components/LastUpdate'
import { useApiHealth, useDebugApiHealth } from '../../hooks/apiHooks'
import { Context } from '../../providers/Stamps' import { Context } from '../../providers/Stamps'
import { Context as BeeContext } from '../../providers/Bee'
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
createStyles({ createStyles({
@@ -32,16 +31,15 @@ const useStyles = makeStyles((theme: Theme) =>
export default function Accounting(): ReactElement { export default function Accounting(): ReactElement {
const classes = useStyles() const classes = useStyles()
const { health, isLoadingHealth } = useApiHealth() const beeContext = useContext(BeeContext)
const { nodeHealth, isLoadingNodeHealth } = useDebugApiHealth() const { stamps, isLoading, error, start, stop } = useContext(Context)
const { stamps, isLoading, error, lastUpdate, start, stop } = useContext(Context)
useEffect(() => { useEffect(() => {
start() start()
return () => stop() return () => stop()
}, []) }, [])
if (isLoadingHealth || isLoadingNodeHealth) { if (beeContext.isLoading) {
return ( return (
<Container style={{ textAlign: 'center', padding: '50px' }}> <Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress /> <CircularProgress />
@@ -49,7 +47,7 @@ export default function Accounting(): ReactElement {
) )
} }
if (nodeHealth?.status !== 'ok' || !health) return <TroubleshootConnectionCard /> if (!beeContext.status.all) return <TroubleshootConnectionCard />
return ( return (
<div className={classes.root}> <div className={classes.root}>
@@ -62,7 +60,6 @@ export default function Accounting(): ReactElement {
<> <>
<div className={classes.actions}> <div className={classes.actions}>
<CreatePostageStampModal /> <CreatePostageStampModal />
<LastUpdate date={lastUpdate} />
<div style={{ height: '5px' }}>{isLoading && <CircularProgress />}</div> <div style={{ height: '5px' }}>{isLoading && <CircularProgress />}</div>
</div> </div>
<StampsTable postageStamps={stamps} /> <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 { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import { Typography, Paper, Button, Step, StepLabel, StepContent, Stepper, StepButton } from '@material-ui/core/' 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 DebugConnectionCheck from './SetupSteps/DebugConnectionCheck'
import NodeConnectionCheck from './SetupSteps/NodeConnectionCheck' import NodeConnectionCheck from './SetupSteps/NodeConnectionCheck'
@@ -9,7 +9,7 @@ import VersionCheck from './SetupSteps/VersionCheck'
import EthereumConnectionCheck from './SetupSteps/EthereumConnectionCheck' import EthereumConnectionCheck from './SetupSteps/EthereumConnectionCheck'
import ChequebookDeployFund from './SetupSteps/ChequebookDeployFund' import ChequebookDeployFund from './SetupSteps/ChequebookDeployFund'
import PeerConnection from './SetupSteps/PeerConnection' import PeerConnection from './SetupSteps/PeerConnection'
import { StatusChequebookHook } from '../../hooks/status' import { Context } from '../../providers/Bee'
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
createStyles({ createStyles({
@@ -30,66 +30,65 @@ const useStyles = makeStyles((theme: Theme) =>
interface Step { interface Step {
label: string label: string
isOk: boolean isOk: boolean
isLoading: boolean
component: ReactElement component: ReactElement
} }
interface Props { export default function NodeSetupWorkflow(): ReactElement {
nodeVersion: StatusNodeVersionHook
ethereumConnection: StatusEthereumConnectionHook
debugApiConnection: StatusHookCommon
apiConnection: StatusHookCommon
topology: StatusTopologyHook
chequebook: StatusChequebookHook
}
export default function NodeSetupWorkflow({
nodeVersion,
ethereumConnection,
debugApiConnection,
apiConnection,
topology,
chequebook,
}: Props): ReactElement {
const classes = useStyles() const classes = useStyles()
const [activeStep, setActiveStep] = useState(-1) const [activeStep, setActiveStep] = useState(-1)
const {
status,
isLoading,
latestUserVersion,
latestPublishedVersion,
isLatestBeeVersion,
latestBeeVersionUrl,
topology,
nodeAddresses,
chequebookAddress,
} = useContext(Context)
const steps: Step[] = [ const steps: Step[] = [
{ {
label: 'Connected to Node DebugAPI', label: 'Connected to Node DebugAPI',
isOk: debugApiConnection.isOk, isOk: status.debugApiConnection,
isLoading: debugApiConnection.isLoading, component: <DebugConnectionCheck isOk={status.debugApiConnection} />,
component: <DebugConnectionCheck {...debugApiConnection} />,
}, },
{ {
label: 'Running latest Bee version', label: 'Running latest Bee version',
isOk: nodeVersion.isOk, isOk: status.version,
isLoading: nodeVersion.isLoading, component: (
component: <VersionCheck {...nodeVersion} />, <VersionCheck
isOk={status.version}
isLatestBeeVersion={isLatestBeeVersion}
userVersion={latestUserVersion}
latestVersion={latestPublishedVersion}
latestUrl={latestBeeVersionUrl}
/>
),
}, },
{ {
label: 'Connected to Ethereum Blockchain', label: 'Connected to xDai Blockchain',
isOk: ethereumConnection.isOk, isOk: status.blockchainConnection,
isLoading: ethereumConnection.isLoading, component: <EthereumConnectionCheck isOk={status.blockchainConnection} nodeAddresses={nodeAddresses} />,
component: <EthereumConnectionCheck {...ethereumConnection} />,
}, },
{ {
label: 'Deployed and Funded Chequebook', label: 'Deployed and Funded Chequebook',
isOk: chequebook.isOk, isOk: status.chequebook,
isLoading: chequebook.isLoading, component: (
component: <ChequebookDeployFund ethereumAddress={ethereumConnection.nodeAddresses?.ethereum} {...chequebook} />, <ChequebookDeployFund chequebookAddress={chequebookAddress?.chequebookAddress} isOk={status.chequebook} />
),
}, },
{ {
label: 'Connected to Node API', label: 'Connected to Node API',
isOk: apiConnection.isOk, isOk: status.apiConnection,
isLoading: apiConnection.isLoading, component: <NodeConnectionCheck isOk={status.apiConnection} />,
component: <NodeConnectionCheck {...apiConnection} />,
}, },
{ {
label: 'Connected to Peers', label: 'Connected to Peers',
isOk: topology.isOk, isOk: status.topology,
isLoading: topology.isLoading, component: <PeerConnection isOk={status.topology} topology={topology} />,
component: <PeerConnection {...topology} />,
}, },
] ]
@@ -98,7 +97,7 @@ export default function NodeSetupWorkflow({
if (activeStep >= 0 && activeStep < steps.length) return if (activeStep >= 0 && activeStep < steps.length) return
// If any step is not fully loaded yet 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 // 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 // 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}> <Paper className={classes.root}>
<Typography variant="h5" gutterBottom> <Typography variant="h5" gutterBottom>
Node Setup 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> </Typography>
<Stepper nonLinear activeStep={activeStep} orientation="vertical"> <Stepper nonLinear activeStep={activeStep} orientation="vertical">
{steps.map(({ label, isOk, component, isLoading }, index) => ( {steps.map(({ label, isOk, component }, index) => (
<Step key={label}> <Step key={label}>
<StepLabel <StepLabel
disabled={isLoading}
onClick={() => setActiveStep(index === activeStep ? steps.length : index)} onClick={() => setActiveStep(index === activeStep ? steps.length : index)}
StepIconComponent={() => { StepIconComponent={() => {
if (isLoading) return <Autorenew style={{ height: '25px', cursor: 'pointer' }} /> 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 EthereumAddress from '../../../components/EthereumAddress'
import DepositModal from '../../../containers/DepositModal' import DepositModal from '../../../containers/DepositModal'
import type { ReactElement } from 'react' import type { ReactElement } from 'react'
import type { StatusChequebookHook } from '../../../hooks/status'
interface Props extends StatusChequebookHook { interface Props extends StatusHookCommon {
ethereumAddress?: string chequebookAddress?: string
} }
const ChequebookDeployFund = ({ isLoading, chequebookAddress, chequebookBalance }: Props): ReactElement | null => { const ChequebookDeployFund = ({ chequebookAddress, isOk }: Props): ReactElement | null => {
if (isLoading) return null
return ( return (
<div> <div>
<p style={{ marginBottom: '20px', display: 'flex' }}> <p style={{ marginBottom: '20px', display: 'flex' }}>{chequebookAddress && <DepositModal />}</p>
{chequebookAddress?.chequebookAddress && <DepositModal />}
</p>
<div style={{ marginBottom: '10px' }}> <div style={{ marginBottom: '10px' }}>
{!(chequebookAddress?.chequebookAddress && chequebookBalance?.totalBalance.toBigNumber.isGreaterThan(0)) && ( {!isOk && (
<div> <div>
<span> <span>
Your chequebook is either not deployed or funded. To run the node you will need xDAI and xBZZ on the xDai 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> <Typography variant="subtitle1" gutterBottom>
Chequebook Address Chequebook Address
</Typography> </Typography>
<EthereumAddress address={chequebookAddress?.chequebookAddress} /> <EthereumAddress address={chequebookAddress} />
</div> </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 { Typography, Accordion, AccordionSummary, AccordionDetails } from '@material-ui/core/'
import MuiAlert from '@material-ui/lab/Alert' import MuiAlert from '@material-ui/lab/Alert'
import { ExpandMoreSharp } from '@material-ui/icons/' import { ExpandMoreSharp } from '@material-ui/icons/'
import ConnectToHost from '../../../components/ConnectToHost' import ConnectToHost from '../../../components/ConnectToHost'
import CodeBlockTabs from '../../../components/CodeBlockTabs' import CodeBlockTabs from '../../../components/CodeBlockTabs'
import { debugApiHost } from '../../../constants' import { Context as SettingsContext } from '../../../providers/Settings'
type Props = StatusHookCommon type Props = StatusHookCommon
export default function NodeConnectionCheck({ isLoading, isOk }: Props): ReactElement | null { export default function NodeConnectionCheck({ isOk }: Props): ReactElement | null {
if (isLoading) return null const { setDebugApiUrl, apiDebugUrl } = useContext(SettingsContext)
const changeDebugApiUrl = ( const changeDebugApiUrl = (
<div style={{ display: 'flex', marginTop: '25px', marginBottom: '25px' }}> <div style={{ display: 'flex', marginTop: '25px', marginBottom: '25px' }}>
<span style={{ marginRight: '15px' }}> <span style={{ marginRight: '15px' }}>
Debug API (<Typography variant="button">{debugApiHost}</Typography>) Debug API (<Typography variant="button">{apiDebugUrl}</Typography>)
</span> </span>
<ConnectToHost hostName={'debug_api_host'} defaultHost={debugApiHost} /> <ConnectToHost
setHost={(host: string) => {
console.log(host) // eslint-disable-line
setDebugApiUrl(host)
}}
defaultHost={apiDebugUrl}
/>
</div> </div>
) )
@@ -31,7 +37,7 @@ export default function NodeConnectionCheck({ isLoading, isOk }: Props): ReactEl
<div> <div>
<Typography component="div" variant="body2" gutterBottom style={{ margin: '15px' }}> <Typography component="div" variant="body2" gutterBottom style={{ margin: '15px' }}>
We cannot connect to your nodes debug API at <Typography variant="button">{debugApiHost}</Typography>. Please We cannot connect to your nodes debug API at <Typography variant="button">{apiDebugUrl}</Typography>. Please
check the following to troubleshoot your issue. check the following to troubleshoot your issue.
<Accordion style={{ marginTop: '20px' }}> <Accordion style={{ marginTop: '20px' }}>
<AccordionSummary expandIcon={<ExpandMoreSharp />} aria-controls="panel1a-content" id="panel1a-header"> <AccordionSummary expandIcon={<ExpandMoreSharp />} aria-controls="panel1a-content" id="panel1a-header">
@@ -4,9 +4,7 @@ import EthereumAddress from '../../../components/EthereumAddress'
type Props = StatusEthereumConnectionHook type Props = StatusEthereumConnectionHook
export default function EthereumConnectionCheck({ isLoading, isOk, nodeAddresses }: Props): ReactElement | null { export default function EthereumConnectionCheck({ isOk, nodeAddresses }: Props): ReactElement | null {
if (isLoading) return null
if (isOk) { if (isOk) {
return ( return (
<div> <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 { Typography, Accordion, AccordionSummary, AccordionDetails } from '@material-ui/core/'
import { ExpandMoreSharp } from '@material-ui/icons/' import { ExpandMoreSharp } from '@material-ui/icons/'
import ConnectToHost from '../../../components/ConnectToHost' import ConnectToHost from '../../../components/ConnectToHost'
import CodeBlockTabs from '../../../components/CodeBlockTabs' import CodeBlockTabs from '../../../components/CodeBlockTabs'
import { apiHost } from '../../../constants' import { Context as SettingsContext } from '../../../providers/Settings'
type Props = StatusHookCommon type Props = StatusHookCommon
export default function NodeConnectionCheck({ isLoading, isOk }: Props): ReactElement | null { export default function NodeConnectionCheck({ isOk }: Props): ReactElement | null {
if (isLoading) return null const { setApiUrl, apiUrl } = useContext(SettingsContext)
return ( return (
<div> <div>
<div style={{ display: 'flex', marginBottom: '25px' }}> <div style={{ display: 'flex', marginBottom: '25px' }}>
<span style={{ marginRight: '15px' }}> <span style={{ marginRight: '15px' }}>
Node API (<Typography variant="button">{apiHost}</Typography>) Node API (<Typography variant="button">{apiUrl}</Typography>)
</span> </span>
<ConnectToHost hostName="api_host" defaultHost={apiHost} /> <ConnectToHost setHost={setApiUrl} defaultHost={apiUrl} />
</div> </div>
<div> <div>
{!isOk && ( {!isOk && (
<Typography component="div" variant="body2" gutterBottom style={{ margin: '15px' }}> <Typography component="div" variant="body2" gutterBottom style={{ margin: '15px' }}>
We cannot connect to your nodes API at <Typography variant="button">{apiHost}</Typography>. Please check the We cannot connect to your nodes API at <Typography variant="button">{apiUrl}</Typography>. Please check the
following to troubleshoot your issue. following to troubleshoot your issue.
<Accordion style={{ marginTop: '20px' }}> <Accordion style={{ marginTop: '20px' }}>
<AccordionSummary expandIcon={<ExpandMoreSharp />} aria-controls="panel1a-content" id="panel1a-header"> <AccordionSummary expandIcon={<ExpandMoreSharp />} aria-controls="panel1a-content" id="panel1a-header">
@@ -3,9 +3,7 @@ import { Typography } from '@material-ui/core/'
type Props = StatusTopologyHook type Props = StatusTopologyHook
export default function PeerConnection({ isLoading, isOk, topology }: Props): ReactElement | null { export default function PeerConnection({ isOk, topology }: Props): ReactElement | null {
if (isLoading) return null
const peers = ( const peers = (
<div style={{ display: 'flex', marginTop: '15px' }}> <div style={{ display: 'flex', marginTop: '15px' }}>
<div style={{ marginRight: '30px' }}> <div style={{ marginRight: '30px' }}>
+1 -9
View File
@@ -4,15 +4,7 @@ import CodeBlockTabs from '../../../components/CodeBlockTabs'
type Props = StatusNodeVersionHook type Props = StatusNodeVersionHook
export default function VersionCheck({ export default function VersionCheck({ isOk, userVersion, latestVersion, latestUrl }: Props): ReactElement | null {
isLoading,
isOk,
userVersion,
latestVersion,
latestUrl,
}: Props): ReactElement | null {
if (isLoading) return null
const version = ( const version = (
<div style={{ display: 'flex' }}> <div style={{ display: 'flex' }}>
<div style={{ marginRight: '30px' }}> <div style={{ marginRight: '30px' }}>
+20 -48
View File
@@ -1,18 +1,10 @@
import { ReactElement } from 'react' import { ReactElement, useContext } from 'react'
import { Container, CircularProgress } from '@material-ui/core'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import NodeSetupWorkflow from './NodeSetupWorkflow' import NodeSetupWorkflow from './NodeSetupWorkflow'
import StatusCard from './StatusCard' import StatusCard from './StatusCard'
import EthereumAddressCard from '../../components/EthereumAddressCard' import EthereumAddressCard from '../../components/EthereumAddressCard'
import { import { Context as BeeContext } from '../../providers/Bee'
useStatusEthereumConnection,
useStatusNodeVersion,
useStatusDebugConnection,
useStatusConnection,
useStatusTopology,
useStatusChequebook,
} from '../../hooks/status'
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
createStyles({ createStyles({
@@ -27,50 +19,30 @@ const useStyles = makeStyles((theme: Theme) =>
export default function Status(): ReactElement { export default function Status(): ReactElement {
const classes = useStyles() const classes = useStyles()
const nodeVersion = useStatusNodeVersion() const {
const ethereumConnection = useStatusEthereumConnection() status,
const debugApiConnection = useStatusDebugConnection() latestUserVersion,
const apiConnection = useStatusConnection() isLatestBeeVersion,
const topology = useStatusTopology() latestBeeVersionUrl,
const chequebook = useStatusChequebook() topology,
nodeAddresses,
const checks = [nodeVersion, ethereumConnection, debugApiConnection, apiConnection, topology, chequebook] chequebookAddress,
} = useContext(BeeContext)
// If any check data are still loading
if (!checks.every(c => !c.isLoading)) {
return (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress />
</Container>
)
}
return ( return (
<div className={classes.root}> <div className={classes.root}>
<StatusCard <StatusCard
userBeeVersion={nodeVersion.userVersion} userBeeVersion={latestUserVersion}
isLatestBeeVersion={nodeVersion.isLatestBeeVersion} isLatestBeeVersion={isLatestBeeVersion}
isOk={checks.every(c => c.isOk)} isOk={status.all}
nodeTopology={topology.topology} nodeTopology={topology}
latestUrl={nodeVersion.latestUrl} latestUrl={latestBeeVersionUrl}
nodeAddresses={ethereumConnection.nodeAddresses} 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 <NodeSetupWorkflow />
nodeVersion={nodeVersion}
ethereumConnection={ethereumConnection}
debugApiConnection={debugApiConnection}
apiConnection={apiConnection}
topology={topology}
chequebook={chequebook}
/>
</div> </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 { PostageBatch } from '@ethersphere/bee-js'
import { createContext, ReactChild, ReactElement, useEffect, useState } from 'react' import { createContext, ReactChild, ReactElement, useEffect, useState, useContext } from 'react'
import { beeApi } from '../services/bee' import { Context as SettingsContext } from './Settings'
export interface EnrichedPostageBatch extends PostageBatch { export interface EnrichedPostageBatch extends PostageBatch {
usage: number usage: number
@@ -48,6 +48,7 @@ function enrichStamp(postageBatch: PostageBatch): EnrichedPostageBatch {
} }
export function Provider({ children }: Props): ReactElement { export function Provider({ children }: Props): ReactElement {
const { beeApi } = useContext(SettingsContext)
const [stamps, setStamps] = useState<EnrichedPostageBatch[] | null>(initialValues.stamps) const [stamps, setStamps] = useState<EnrichedPostageBatch[] | null>(initialValues.stamps)
const [error, setError] = useState<Error | null>(initialValues.error) const [error, setError] = useState<Error | null>(initialValues.error)
const [isLoading, setIsLoading] = useState<boolean>(initialValues.isLoading) const [isLoading, setIsLoading] = useState<boolean>(initialValues.isLoading)
@@ -58,9 +59,11 @@ export function Provider({ children }: Props): ReactElement {
// Don't want to refresh when already refreshing // Don't want to refresh when already refreshing
if (isLoading) return if (isLoading) return
if (!beeApi) return
try { try {
setIsLoading(true) setIsLoading(true)
const stamps = await beeApi.stamps.getPostageStamps() const stamps = await beeApi.getAllPostageBatch()
setStamps(stamps.map(enrichStamp)) setStamps(stamps.map(enrichStamp))
setLastUpdate(Date.now()) setLastUpdate(Date.now())
-1
View File
@@ -6,7 +6,6 @@ interface LatestBeeRelease {
} }
interface StatusHookCommon { interface StatusHookCommon {
isLoading: boolean
isOk: 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}`) 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,
}
}