Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f8f890ff7 | |||
| f9ea9948f0 | |||
| 2b120e44ca | |||
| 0df15d6109 | |||
| 56df3a2561 | |||
| 7f2ff39ec9 | |||
| 739fc45500 | |||
| d6d03bf7c6 | |||
| 2624cf04c9 | |||
| dcec6e0188 | |||
| 480f6dc7f9 | |||
| a62243fe5c | |||
| ec42eafc2b | |||
| f90778d338 | |||
| 650d100dd2 | |||
| 960ffb8fdd | |||
| be8b88516b | |||
| 43b3a45d90 | |||
| 20ed3cb387 | |||
| b190cac706 | |||
| 6f645dc6c3 | |||
| af88027ba9 | |||
| 5748c9b609 | |||
| 5ace7629f2 | |||
| 465df17741 | |||
| 3bcf2ac688 | |||
| a2bff60270 | |||
| 353db10080 | |||
| bec84051a9 | |||
| 92c727e5f5 | |||
| 4074a9de5d | |||
| 9fee1aa68a | |||
| 08fdac9366 | |||
| ba9b498488 | |||
| 07f987e069 | |||
| a603a86c1a | |||
| aab0462047 |
+1
-1
@@ -3,5 +3,5 @@ REACT_APP_BEE_HOST=http://localhost:1633
|
||||
REACT_APP_BEE_DEBUG_HOST=http://localhost:1635
|
||||
REACT_APP_BEE_DOCS_HOST=https://docs.ethswarm.org/docs/
|
||||
REACT_APP_BEE_DISCORD_HOST=https://discord.gg/eKr9XPv7
|
||||
REACT_APP_ETHERSCAN_HOST=etherscan.io
|
||||
REACT_APP_BLOCKCHAIN_EXPLORER_URL=https://blockscout.com/xdai/mainnet
|
||||
REACT_APP_BEE_GITHUB_REPO_URL=https://api.github.com/repos/ethersphere/bee
|
||||
@@ -1,4 +1,6 @@
|
||||
REACT_APP_BEE_HOST=http://localhost:1633
|
||||
REACT_APP_BEE_DEBUG_HOST=http://localhost:1635
|
||||
REACT_APP_BEE_DOCS_HOST=https://docs.ethswarm.org/docs/
|
||||
REACT_APP_BEE_DISCORD_HOST=https://discord.gg/eKr9XPv7
|
||||
REACT_APP_BLOCKCHAIN_EXPLORER_URL=https://blockscout.com/xdai/mainnet
|
||||
REACT_APP_BEE_GITHUB_REPO_URL=https://api.github.com/repos/ethersphere/bee
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
# Denote all files that are truly binary and should not be modified.
|
||||
*.png binary
|
||||
@@ -1,5 +1,74 @@
|
||||
# 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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* updated troubleshooting instructions and links for mainnet ([#161](https://www.github.com/ethersphere/bee-dashboard/issues/161)) ([960ffb8](https://www.github.com/ethersphere/bee-dashboard/commit/960ffb8fdd6cbfe4928b758da6cac9ba94050c00))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* amend readme ([#155](https://www.github.com/ethersphere/bee-dashboard/issues/155)) ([be8b885](https://www.github.com/ethersphere/bee-dashboard/commit/be8b88516b00d79a623798588d3d4dac3340e8b2))
|
||||
|
||||
## [0.4.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.3.1...v0.4.0) (2021-06-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* display postage batch usage percentage ([#149](https://www.github.com/ethersphere/bee-dashboard/issues/149)) ([6f645dc](https://www.github.com/ethersphere/bee-dashboard/commit/6f645dc6c357cb9d86cec15e454b361bc871bec6))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* clear dropzone state after upload ([#150](https://www.github.com/ethersphere/bee-dashboard/issues/150)) ([b190cac](https://www.github.com/ethersphere/bee-dashboard/commit/b190cac7064ad3dffb770c5a83d3db4a14d39607))
|
||||
|
||||
### [0.3.1](https://www.github.com/ethersphere/bee-dashboard/compare/v0.3.0...v0.3.1) (2021-06-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* don't display version alert when unable to retrieve version from bee node ([#138](https://www.github.com/ethersphere/bee-dashboard/issues/138)) ([5ace762](https://www.github.com/ethersphere/bee-dashboard/commit/5ace7629f2479499fe975dec8be4187ece6221ed))
|
||||
* typeerror in the postage stamps form ([#137](https://www.github.com/ethersphere/bee-dashboard/issues/137)) ([465df17](https://www.github.com/ethersphere/bee-dashboard/commit/465df177413afba5376682bd23a712066bd0385c))
|
||||
|
||||
## [0.3.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.2.0...v0.3.0) (2021-06-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* added Dockerfile ([#75](https://www.github.com/ethersphere/bee-dashboard/issues/75)) ([aab0462](https://www.github.com/ethersphere/bee-dashboard/commit/aab0462047a3fcd87ba258b5486aede922865b1e))
|
||||
* added tolerance to version check and warning if not exact to what we tested ([#133](https://www.github.com/ethersphere/bee-dashboard/issues/133)) ([353db10](https://www.github.com/ethersphere/bee-dashboard/commit/353db10080b85b0e12e13991665297ec262d2806))
|
||||
* postage stamps support ([#115](https://www.github.com/ethersphere/bee-dashboard/issues/115)) ([4074a9d](https://www.github.com/ethersphere/bee-dashboard/commit/4074a9de5dae4aaa1654f7dfdd3e3343eaf2bf9b))
|
||||
* unified notification with notistack ([#127](https://www.github.com/ethersphere/bee-dashboard/issues/127)) ([bec8405](https://www.github.com/ethersphere/bee-dashboard/commit/bec84051a9582bf62a23f2080a6587a9f458b969))
|
||||
* upload files with postage stamps ([#126](https://www.github.com/ethersphere/bee-dashboard/issues/126)) ([92c727e](https://www.github.com/ethersphere/bee-dashboard/commit/92c727e5f5772f612fe04b750ef5373780ccba5c))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add git attributes ([#123](https://www.github.com/ethersphere/bee-dashboard/issues/123)) ([07f987e](https://www.github.com/ethersphere/bee-dashboard/commit/07f987e069cda2f28bc5ebf8958b9b0aa9d875dc))
|
||||
* add prod env variables ([#121](https://www.github.com/ethersphere/bee-dashboard/issues/121)) ([a603a86](https://www.github.com/ethersphere/bee-dashboard/commit/a603a86c1adcfb0dcc9995c95c4ee4411c41c25a))
|
||||
* replace http-serve with serve-handler ([#122](https://www.github.com/ethersphere/bee-dashboard/issues/122)) ([ba9b498](https://www.github.com/ethersphere/bee-dashboard/commit/ba9b498488dca989bbbda6110d0d22753b33ae8c))
|
||||
* troubleshooting on a mac and clearer CORS setup guide ([#131](https://www.github.com/ethersphere/bee-dashboard/issues/131)) ([9fee1aa](https://www.github.com/ethersphere/bee-dashboard/commit/9fee1aa68ac6dbc53615332bc0142a06f3e5f03f))
|
||||
|
||||
## [0.2.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.1.0...v0.2.0) (2021-05-20)
|
||||
|
||||
This release supports the [Bee's 0.6.0 release](https://github.com/ethersphere/bee/releases/tag/v0.6.0) and is fully
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
* nugaon vojtechsimetka
|
||||
* @Cafe137 @vojtechsimetka
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
FROM node:15.14-alpine AS build
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN npm ci
|
||||
RUN npm run build
|
||||
|
||||
FROM node:15.14-alpine AS final
|
||||
RUN npm i -g serve
|
||||
WORKDIR /app
|
||||
COPY --from=build /src/build .
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["serve", "-l", "8080"]
|
||||
@@ -6,7 +6,7 @@
|
||||

|
||||

|
||||
|
||||
> An app which helps users to setup their Bee node and do actions like cash out cheques.
|
||||
> An app which helps users to setup their Bee node and do actions like cash out cheques, upload and download files or manage your postage stamps.
|
||||
|
||||
**Warning: This project is in alpha state. There might (and most probably will) be changes in the future to its API and working. Also, no guarantees can be made about its stability, efficiency, and security at this stage.**
|
||||
|
||||
@@ -21,29 +21,58 @@
|
||||
|
||||
- [Install](#install)
|
||||
- [Usage](#usage)
|
||||
- [Terminal](#terminal)
|
||||
- [Docker](#docker)
|
||||
- [Contribute](#contribute)
|
||||
- [Development](#development)
|
||||
- [Maintainers](#maintainers)
|
||||
- [License](#license)
|
||||
|
||||
## Install
|
||||
|
||||
```
|
||||
$ npm install -g @ethersphere/bee-dashboard
|
||||
$ bee-dashboard
|
||||
Install globally with npm. We require Node.js's version of at least 12.x and npm v6.x (or yarn v2.x).
|
||||
|
||||
```sh
|
||||
npm install -g @ethersphere/bee-dashboard
|
||||
```
|
||||
|
||||
## Development
|
||||
## Usage
|
||||
|
||||
:warning: To successfully connect to the Bee node, you will need to enable the Debug API and CORS. You can do so by setting `cors-allowed-origins: ['*']` and `debug-api-enable: true` in the Bee config file and then restart the Bee node. To see where the config file is, consult the [official Bee documentation](https://docs.ethswarm.org/docs/working-with-bee/configuration#configuring-bee-installed-using-a-package-manager)
|
||||
|
||||
### Terminal
|
||||
|
||||
To start use:
|
||||
```sh
|
||||
bee-dashboard
|
||||
```
|
||||
|
||||
This should open the webpage on [`http://localhost:8080`](http://localhost:8080)
|
||||
|
||||
### Docker
|
||||
|
||||
To build Docker image and run it, execute the following from inside project directory:
|
||||
|
||||
```sh
|
||||
docker build . -t bee-dashboard
|
||||
docker run --rm -p 127.0.0.1:8080:8080 bee-dashboard
|
||||
```
|
||||
|
||||
Bee dashboard is now available on [`http://localhost:8080`](http://localhost:8080)
|
||||
|
||||
### Development
|
||||
|
||||
```sh
|
||||
git clone git@github.com:ethersphere/bee-dashboard.git
|
||||
|
||||
cd bee-dashboard
|
||||
|
||||
npm ci
|
||||
npm run build
|
||||
npm run serve
|
||||
npm i
|
||||
|
||||
npm start
|
||||
```
|
||||
|
||||
You can now access Bee Dashboard on [http://localhost:8080/](http://localhost:8080/)
|
||||
The Bee Dashboard runs in development mode on [http://localhost:3031/](http://localhost:3031/)
|
||||
|
||||
## Contribute
|
||||
|
||||
@@ -51,12 +80,14 @@ There are some ways you can make this module better:
|
||||
|
||||
- Consult our [open issues](https://github.com/ethersphere/bee-dashboard/issues) and take on one of them
|
||||
- Help our tests reach 100% coverage!
|
||||
- Join us in our [Mattermost chat](https://beehive.ethswarm.org/swarm/channels/swarm-javascript) if you have questions or want to give feedback
|
||||
- Join us in our [Discord chat](https://discord.gg/wdghaQsGq5) in the #develop-on-swarm channel if you have questions or want to give feedback
|
||||
|
||||
## Maintainers
|
||||
|
||||
- [nugaon](https://github.com/nugaon)
|
||||
- [vojtechsimetka](https://github.com/vojtechsimetka)
|
||||
- [Cafe137](https://github.com/Cafe137)
|
||||
|
||||
See what "Maintainer" means [here](https://github.com/ethersphere/repo-maintainer).
|
||||
|
||||
## License
|
||||
|
||||
|
||||
Generated
+469
-381
File diff suppressed because it is too large
Load Diff
+17
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ethersphere/bee-dashboard",
|
||||
"version": "0.2.0",
|
||||
"version": "0.6.0",
|
||||
"description": "An app which helps users to setup their Bee node and do actions like cash out cheques",
|
||||
"keywords": [
|
||||
"bee",
|
||||
@@ -24,8 +24,8 @@
|
||||
"url": "https://github.com/ethersphere/bee-dashboard.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ethersphere/bee-js": "^0.9.0",
|
||||
"@material-ui/core": "^4.11.3",
|
||||
"@ethersphere/bee-js": "^1.2.0",
|
||||
"@material-ui/core": "^4.11.4",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@material-ui/lab": "^4.0.0-alpha.57",
|
||||
"@types/react-router": "^5.1.13",
|
||||
@@ -33,8 +33,11 @@
|
||||
"axios": "^0.21.1",
|
||||
"bignumber.js": "^9.0.1",
|
||||
"feather-icons": "^4.28.0",
|
||||
"http-serve": "^1.0.1",
|
||||
"formik": "^2.2.8",
|
||||
"formik-material-ui": "^3.0.1",
|
||||
"material-ui-dropzone": "^3.5.0",
|
||||
"notistack": "^1.0.9",
|
||||
"opener": "^1.5.2",
|
||||
"qrcode.react": "^1.0.1",
|
||||
"react": "^17.0.2",
|
||||
"react-copy-to-clipboard": "^5.0.3",
|
||||
@@ -42,7 +45,9 @@
|
||||
"react-feather": "^2.0.9",
|
||||
"react-identicons": "^1.2.5",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-syntax-highlighter": "^15.4.3"
|
||||
"react-syntax-highlighter": "^15.4.3",
|
||||
"semver": "^7.3.2",
|
||||
"serve-handler": "^6.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^5.12.0",
|
||||
@@ -55,6 +60,7 @@
|
||||
"@types/react-copy-to-clipboard": "^5.0.0",
|
||||
"@types/react-dom": "^17.0.3",
|
||||
"@types/react-syntax-highlighter": "^13.5.0",
|
||||
"@types/semver": "^7.3.6",
|
||||
"eslint": "^7.24.0",
|
||||
"eslint-config-prettier": "^8.2.0",
|
||||
"eslint-plugin-jest": "^24.3.5",
|
||||
@@ -70,7 +76,7 @@
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"serve": "http-serve ./build -o",
|
||||
"serve": "node ./serve.js",
|
||||
"lint": "eslint --fix \"src/**/*.ts\" \"src/**/*.tsx\" && prettier --write \"src/**/*.ts\" \"src/**/*.tsx\"",
|
||||
"lint:check": "eslint \"src/**/*.ts\" \"src/**/*.tsx\" && prettier --check \"src/**/*.ts\" \"src/**/*.tsx\""
|
||||
},
|
||||
@@ -95,5 +101,10 @@
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0",
|
||||
"npm": ">=6.0.0",
|
||||
"bee": ">=0.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"trailingSlash": false,
|
||||
"headers": [
|
||||
{
|
||||
"source" : "*",
|
||||
"headers" : [{
|
||||
"key" : "Cache-Control",
|
||||
"value" : "max-age=3600"
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,15 +1,33 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const path = require('path')
|
||||
const serve = require('http-serve')
|
||||
const handler = require('serve-handler');
|
||||
const http = require('http');
|
||||
const opener = require('opener')
|
||||
|
||||
const server = serve.createServer({
|
||||
root: path.join(__dirname, 'build')
|
||||
const serverConfig = {
|
||||
public: path.join(__dirname, 'build'),
|
||||
trailingSlash: false,
|
||||
rewrites: [
|
||||
{ source: "**", destination: "/index.html" },
|
||||
],
|
||||
headers: [
|
||||
{
|
||||
source: "*",
|
||||
headers: [{
|
||||
key: "Cache-Control",
|
||||
value: "max-age=3600"
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const server = http.createServer((request, response) => {
|
||||
|
||||
return handler(request, response, serverConfig);
|
||||
})
|
||||
|
||||
server.listen(8080, '127.0.0.1', function () {
|
||||
server.listen(8080, () => {
|
||||
console.log('Starting up Bee Dashboard on address http://localhost:8080')
|
||||
console.log('Hit CTRL-C to stop the server')
|
||||
opener('http://localhost:8080')
|
||||
|
||||
+17
@@ -4,9 +4,14 @@ import './App.css'
|
||||
|
||||
import { ThemeProvider } from '@material-ui/styles'
|
||||
import CssBaseline from '@material-ui/core/CssBaseline'
|
||||
import { SnackbarProvider } from 'notistack'
|
||||
|
||||
import BaseRouter from './routes/routes'
|
||||
import { lightTheme, darkTheme } from './theme'
|
||||
import { Provider as StampsProvider } from './providers/Stamps'
|
||||
import { Provider as PlatformProvider } from './providers/Platform'
|
||||
import { Provider as BeeProvider } from './providers/Bee'
|
||||
import { Provider as SettingsProvider } from './providers/Settings'
|
||||
|
||||
const App = (): ReactElement => {
|
||||
const [themeMode, toggleThemeMode] = useState('light')
|
||||
@@ -33,10 +38,22 @@ const App = (): ReactElement => {
|
||||
return (
|
||||
<div className="App">
|
||||
<ThemeProvider theme={themeMode === 'light' ? lightTheme : darkTheme}>
|
||||
<SettingsProvider>
|
||||
<BeeProvider>
|
||||
<StampsProvider>
|
||||
<PlatformProvider>
|
||||
<SnackbarProvider>
|
||||
<>
|
||||
<CssBaseline />
|
||||
<Router>
|
||||
<BaseRouter />
|
||||
</Router>
|
||||
</>
|
||||
</SnackbarProvider>
|
||||
</PlatformProvider>
|
||||
</StampsProvider>
|
||||
</BeeProvider>
|
||||
</SettingsProvider>
|
||||
</ThemeProvider>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import Collapse from '@material-ui/core/Collapse'
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import { Alert, AlertTitle } from '@material-ui/lab'
|
||||
import { ReactElement } from 'react'
|
||||
|
||||
const LIMIT = 100_000_000 // 100 megabytes
|
||||
|
||||
interface Props {
|
||||
file: File
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
width: '100%',
|
||||
marginTop: theme.spacing(2),
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
export default function UploadSizeAlert(props: Props): ReactElement | null {
|
||||
const classes = useStyles()
|
||||
|
||||
const aboveLimit = props.file.size >= LIMIT
|
||||
|
||||
return (
|
||||
<Collapse in={aboveLimit}>
|
||||
<div className={classes.root}>
|
||||
<Alert severity="warning">
|
||||
<AlertTitle>Warning</AlertTitle>
|
||||
The file you are trying to upload is above the recommended size. The chunks may not be synchronised properly
|
||||
over the network.
|
||||
</Alert>
|
||||
</div>
|
||||
</Collapse>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { ReactElement, useState, useContext } from 'react'
|
||||
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
|
||||
import { Alert, AlertTitle } from '@material-ui/lab'
|
||||
import Collapse from '@material-ui/core/Collapse'
|
||||
import IconButton from '@material-ui/core/IconButton'
|
||||
import CloseIcon from '@material-ui/icons/Close'
|
||||
import { Context } from '../providers/Bee'
|
||||
import { SUPPORTED_BEE_VERSION_EXACT } from '@ethersphere/bee-js'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
width: '100%',
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
export default function VersionAlert(): ReactElement | null {
|
||||
const classes = useStyles()
|
||||
const { isLoading, latestUserVersionExact } = useContext(Context)
|
||||
const [open, setOpen] = useState<boolean>(true)
|
||||
|
||||
const isExactlySupportedBeeVersion = SUPPORTED_BEE_VERSION_EXACT === latestUserVersionExact
|
||||
|
||||
if (isLoading || !latestUserVersionExact) return null
|
||||
|
||||
return (
|
||||
<Collapse in={!isExactlySupportedBeeVersion && open}>
|
||||
<div className={classes.root}>
|
||||
<Alert
|
||||
severity="warning"
|
||||
action={
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<CloseIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<AlertTitle>Warning</AlertTitle>
|
||||
Your Bee node version (<code>{latestUserVersionExact}</code>) does not exactly match the Bee version we tested
|
||||
the Bee Dashboard against (<code>{SUPPORTED_BEE_VERSION_EXACT}</code>). Please note that some functionality
|
||||
may not work properly.
|
||||
</Alert>
|
||||
</div>
|
||||
</Collapse>
|
||||
)
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
import { ReactElement, useState } from 'react'
|
||||
import { CircularProgress, Container } from '@material-ui/core'
|
||||
import Button from '@material-ui/core/Button'
|
||||
import Dialog from '@material-ui/core/Dialog'
|
||||
import DialogActions from '@material-ui/core/DialogActions'
|
||||
import DialogContent from '@material-ui/core/DialogContent'
|
||||
import DialogContentText from '@material-ui/core/DialogContentText'
|
||||
import DialogTitle from '@material-ui/core/DialogTitle'
|
||||
import { Snackbar, Container, CircularProgress } from '@material-ui/core'
|
||||
|
||||
import { beeDebugApi } from '../services/bee'
|
||||
|
||||
import { useSnackbar } from 'notistack'
|
||||
import { ReactElement, useState, useContext } from 'react'
|
||||
import { Context as SettingsContext } from '../providers/Settings'
|
||||
import EthereumAddress from './EthereumAddress'
|
||||
|
||||
interface Props {
|
||||
@@ -16,11 +15,11 @@ interface Props {
|
||||
uncashedAmount: string
|
||||
}
|
||||
|
||||
export default function DepositModal({ peerId, uncashedAmount }: Props): ReactElement {
|
||||
export default function CheckoutModal({ peerId, uncashedAmount }: Props): ReactElement {
|
||||
const [open, setOpen] = useState<boolean>(false)
|
||||
const [loadingCashout, setLoadingCashout] = useState<boolean>(false)
|
||||
const [showToast, setToastVisibility] = useState<boolean>(false)
|
||||
const [toastContent, setToastContent] = useState<JSX.Element | null>(null)
|
||||
const { enqueueSnackbar } = useSnackbar()
|
||||
const { beeDebugApi } = useContext(SettingsContext)
|
||||
|
||||
const handleClickOpen = () => {
|
||||
setOpen(true)
|
||||
@@ -31,43 +30,38 @@ export default function DepositModal({ peerId, uncashedAmount }: Props): ReactEl
|
||||
}
|
||||
|
||||
const handleCashout = () => {
|
||||
if (!beeDebugApi) return
|
||||
|
||||
if (peerId) {
|
||||
setLoadingCashout(true)
|
||||
beeDebugApi.chequebook
|
||||
.peerCashout(peerId)
|
||||
beeDebugApi
|
||||
.cashoutLastCheque(peerId)
|
||||
.then(res => {
|
||||
setOpen(false)
|
||||
handleToast(
|
||||
enqueueSnackbar(
|
||||
<span>
|
||||
Successfully cashed out cheque. Transaction
|
||||
<EthereumAddress hideBlockie transaction address={res.transactionHash} network={'goerli'} />
|
||||
<EthereumAddress hideBlockie transaction address={res} />
|
||||
</span>,
|
||||
{ variant: 'success' },
|
||||
)
|
||||
})
|
||||
.catch(() => {
|
||||
// FIXME: handle errors more gracefully
|
||||
handleToast(<span>Error with cashout</span>)
|
||||
.catch((e: Error) => {
|
||||
enqueueSnackbar(<span>Error: {e.message}</span>, { variant: 'error' })
|
||||
})
|
||||
.finally(() => {
|
||||
setLoadingCashout(false)
|
||||
})
|
||||
} else {
|
||||
handleToast(<span>Peer Id invalid</span>)
|
||||
enqueueSnackbar(<span>Peer Id invalid</span>, { variant: 'error' })
|
||||
}
|
||||
}
|
||||
|
||||
const handleToast = (text: JSX.Element) => {
|
||||
setToastContent(text)
|
||||
setToastVisibility(true)
|
||||
setTimeout(() => setToastVisibility(false), 7000)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button variant="contained" color="primary" onClick={handleClickOpen} style={{ marginLeft: '7px' }}>
|
||||
Cashout
|
||||
</Button>
|
||||
<Snackbar anchorOrigin={{ vertical: 'top', horizontal: 'center' }} open={showToast} message={toastContent} />
|
||||
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title">
|
||||
<DialogTitle id="form-dialog-title">Cashout Cheque</DialogTitle>
|
||||
<DialogContent>
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
import { ReactElement, useState } from 'react'
|
||||
import { IconButton, Snackbar } from '@material-ui/core'
|
||||
import type { ReactElement } from 'react'
|
||||
import IconButton from '@material-ui/core/IconButton'
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard'
|
||||
import { Clipboard } from 'react-feather'
|
||||
import { useSnackbar } from 'notistack'
|
||||
|
||||
interface Props {
|
||||
value: string
|
||||
}
|
||||
|
||||
export default function ClipboardCopy(props: Props): ReactElement {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleCopy = () => {
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 3000)
|
||||
}
|
||||
export default function ClipboardCopy({ value }: Props): ReactElement {
|
||||
const { enqueueSnackbar } = useSnackbar()
|
||||
const handleCopy = () => enqueueSnackbar(`Copied: ${value}`, { variant: 'success' })
|
||||
|
||||
return (
|
||||
<div style={{ marginRight: '3px', marginLeft: '3px' }}>
|
||||
<Snackbar anchorOrigin={{ vertical: 'top', horizontal: 'center' }} open={copied} message="Copied" />
|
||||
<IconButton color="primary" size="small" onClick={handleCopy}>
|
||||
<CopyToClipboard text={props.value}>
|
||||
<CopyToClipboard text={value}>
|
||||
<Clipboard style={{ height: '20px' }} />
|
||||
</CopyToClipboard>
|
||||
</IconButton>
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import React, { ReactElement, useEffect } from 'react'
|
||||
import { withStyles, Theme, createStyles } from '@material-ui/core/styles'
|
||||
import { Tabs, Tab, Box, Typography } from '@material-ui/core'
|
||||
import { ReactElement, useContext } from 'react'
|
||||
import TabsContainer from './TabsContainer'
|
||||
import CodeBlock from './CodeBlock'
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode
|
||||
index: number
|
||||
value: number
|
||||
}
|
||||
import { Context } from '../providers/Platform'
|
||||
|
||||
interface Props {
|
||||
linux: string
|
||||
@@ -15,133 +9,23 @@ interface Props {
|
||||
showLineNumbers?: boolean
|
||||
}
|
||||
|
||||
function a11yProps(index: number) {
|
||||
return {
|
||||
id: `simple-tab-${index}`,
|
||||
'aria-controls': `simple-tabpanel-${index}`,
|
||||
}
|
||||
}
|
||||
|
||||
function getOS() {
|
||||
const userAgent = window.navigator.userAgent
|
||||
const platform = window.navigator.platform
|
||||
const macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K']
|
||||
const windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE']
|
||||
const iosPlatforms = ['iPhone', 'iPad', 'iPod']
|
||||
|
||||
if (macosPlatforms.includes(platform)) return 'macOS'
|
||||
|
||||
if (iosPlatforms.includes(platform)) return 'iOS'
|
||||
|
||||
if (windowsPlatforms.includes(platform)) return 'windows'
|
||||
|
||||
if (/Android/.test(userAgent)) return 'android'
|
||||
|
||||
if (/Linux/.test(platform)) return 'linux'
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default function CodeBlockTabs(props: Props): ReactElement {
|
||||
const [value, setValue] = React.useState(0)
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<unknown>, newValue: number) => {
|
||||
setValue(newValue)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const os = getOS()
|
||||
|
||||
if (os === 'windows') {
|
||||
setValue(0)
|
||||
} else if (os === 'linux') {
|
||||
setValue(0)
|
||||
} else if (os === 'macOS') {
|
||||
setValue(1)
|
||||
}
|
||||
}, [])
|
||||
|
||||
function TabPanel(props: TabPanelProps) {
|
||||
const { children, value, index, ...other } = props
|
||||
const { platform, setPlatform } = useContext(Context)
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
id={`simple-tabpanel-${index}`}
|
||||
aria-labelledby={`simple-tab-${index}`}
|
||||
{...other}
|
||||
>
|
||||
{value === index && (
|
||||
<Box style={{ marginTop: '-12px' }}>
|
||||
<Typography component="div">{children}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const AntTabs = withStyles({
|
||||
root: {
|
||||
borderBottom: '1px solid #e8e8e8',
|
||||
},
|
||||
indicator: {
|
||||
backgroundColor: '#3f51b5',
|
||||
},
|
||||
})(Tabs)
|
||||
|
||||
interface StyledTabProps {
|
||||
label: string
|
||||
}
|
||||
|
||||
const AntTab = withStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
textTransform: 'none',
|
||||
minWidth: 72,
|
||||
backgroundColor: 'transparent',
|
||||
fontWeight: theme.typography.fontWeightRegular,
|
||||
marginRight: theme.spacing(4),
|
||||
fontFamily: [
|
||||
'-apple-system',
|
||||
'BlinkMacSystemFont',
|
||||
'"Segoe UI"',
|
||||
'Roboto',
|
||||
'"Helvetica Neue"',
|
||||
'Arial',
|
||||
'sans-serif',
|
||||
'"Apple Color Emoji"',
|
||||
'"Segoe UI Emoji"',
|
||||
'"Segoe UI Symbol"',
|
||||
].join(','),
|
||||
'&:hover': {
|
||||
color: '#3f51b5',
|
||||
opacity: 1,
|
||||
},
|
||||
'&$selected': {
|
||||
color: '#3f51b5',
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
},
|
||||
'&:focus': {
|
||||
color: '#3f51b5',
|
||||
},
|
||||
},
|
||||
selected: {},
|
||||
}),
|
||||
)((props: StyledTabProps) => <Tab disableRipple {...props} />)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AntTabs style={{ marginTop: '12px' }} value={value} onChange={handleChange} aria-label="ant example">
|
||||
<AntTab label="Linux" {...a11yProps(0)} />
|
||||
<AntTab label="MacOS" {...a11yProps(1)} />
|
||||
</AntTabs>
|
||||
<TabPanel value={value} index={0}>
|
||||
<CodeBlock showLineNumbers={props.showLineNumbers} language="bash" code={props.linux} />
|
||||
</TabPanel>
|
||||
<TabPanel value={value} index={1}>
|
||||
<CodeBlock showLineNumbers={props.showLineNumbers} language="bash" code={props.mac} />
|
||||
</TabPanel>
|
||||
</div>
|
||||
<TabsContainer
|
||||
index={platform}
|
||||
indexChanged={setPlatform}
|
||||
values={[
|
||||
{
|
||||
label: 'Linux',
|
||||
component: <CodeBlock showLineNumbers={props.showLineNumbers} language="bash" code={props.linux} />,
|
||||
},
|
||||
{
|
||||
label: 'macOS',
|
||||
component: <CodeBlock showLineNumbers={props.showLineNumbers} language="bash" code={props.mac} />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,31 +1,25 @@
|
||||
import React, { ReactElement, useState } from 'react'
|
||||
import { TextField, Button, CircularProgress, Container } from '@material-ui/core'
|
||||
import { TextField, Button } from '@material-ui/core'
|
||||
|
||||
interface Props {
|
||||
defaultHost?: string
|
||||
hostName: string
|
||||
setHost: (host: string) => void
|
||||
}
|
||||
|
||||
export default function ConnectToHost(props: Props): ReactElement {
|
||||
const [hostInputVisible, toggleHostInputVisibility] = useState(false)
|
||||
const [connectingToHost, setConnectingToHost] = useState(false)
|
||||
const [host, setHost] = useState('')
|
||||
|
||||
const handleNewHostConnection = () => {
|
||||
if (host) {
|
||||
setConnectingToHost(true)
|
||||
sessionStorage.setItem(props.hostName, host)
|
||||
props.setHost(host)
|
||||
toggleHostInputVisibility(!hostInputVisible)
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
// FIXME: this should be broken up
|
||||
/* eslint-disable no-nested-ternary */
|
||||
hostInputVisible ? (
|
||||
{hostInputVisible ? (
|
||||
<div style={{ display: 'flex' }}>
|
||||
<TextField
|
||||
defaultValue={props.defaultHost}
|
||||
@@ -46,17 +40,11 @@ export default function ConnectToHost(props: Props): ReactElement {
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
) : connectingToHost ? (
|
||||
<Container style={{ textAlign: 'center', padding: '0px' }}>
|
||||
<CircularProgress size={20} />
|
||||
</Container>
|
||||
) : (
|
||||
<Button onClick={() => toggleHostInputVisibility(!hostInputVisible)} size="small" variant="outlined">
|
||||
Change host
|
||||
</Button>
|
||||
)
|
||||
/* eslint-enable no-nested-ternary */
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { ReactElement } from 'react'
|
||||
|
||||
interface Props {
|
||||
address: string | undefined
|
||||
network?: string
|
||||
hideBlockie?: boolean
|
||||
transaction?: boolean
|
||||
truncate?: boolean
|
||||
@@ -37,9 +36,9 @@ export default function EthereumAddress(props: Props): ReactElement {
|
||||
}
|
||||
: { marginRight: '7px' }
|
||||
}
|
||||
href={`https://${props.network}.${process.env.REACT_APP_ETHERSCAN_HOST}/${
|
||||
props.transaction ? 'tx' : 'address'
|
||||
}/${props.address}`}
|
||||
href={`${process.env.REACT_APP_BLOCKCHAIN_EXPLORER_URL}/${props.transaction ? 'tx' : 'address'}/${
|
||||
props.address
|
||||
}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import { ReactElement } from 'react'
|
||||
|
||||
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
||||
import { Card, CardContent, Typography } from '@material-ui/core/'
|
||||
|
||||
import EthereumAddress from '../components/EthereumAddress'
|
||||
import { Skeleton } from '@material-ui/lab'
|
||||
|
||||
import type { ChequebookAddressResponse, NodeAddresses } from '@ethersphere/bee-js'
|
||||
|
||||
@@ -28,9 +27,7 @@ const useStyles = makeStyles(() =>
|
||||
|
||||
interface Props {
|
||||
nodeAddresses: NodeAddresses | null
|
||||
isLoadingNodeAddresses: boolean
|
||||
chequebookAddress: ChequebookAddressResponse | null
|
||||
isLoadingChequebookAddress: boolean
|
||||
}
|
||||
|
||||
function EthereumAddressCard(props: Props): ReactElement {
|
||||
@@ -38,36 +35,23 @@ function EthereumAddressCard(props: Props): ReactElement {
|
||||
|
||||
return (
|
||||
<Card className={classes.root}>
|
||||
{props.isLoadingNodeAddresses ? (
|
||||
<div style={{ padding: '16px' }}>
|
||||
<Skeleton width={300} height={30} animation="wave" />
|
||||
<Skeleton width={300} height={50} animation="wave" />
|
||||
</div>
|
||||
) : (
|
||||
<div className={classes.details}>
|
||||
<CardContent className={classes.content}>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
Ethereum Address
|
||||
</Typography>
|
||||
<EthereumAddress address={props.nodeAddresses?.ethereum} network={'goerli'} />
|
||||
<EthereumAddress address={props.nodeAddresses?.ethereum} />
|
||||
</CardContent>
|
||||
</div>
|
||||
)}
|
||||
{props.isLoadingChequebookAddress ? (
|
||||
<div style={{ padding: '16px' }}>
|
||||
<Skeleton width={300} height={30} animation="wave" />
|
||||
<Skeleton width={300} height={50} animation="wave" />
|
||||
</div>
|
||||
) : (
|
||||
|
||||
<div className={classes.details}>
|
||||
<CardContent className={classes.content}>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
Chequebook Contract Address
|
||||
</Typography>
|
||||
<EthereumAddress address={props.chequebookAddress?.chequebookAddress} network={'goerli'} />
|
||||
<EthereumAddress address={props.chequebookAddress?.chequebookAddress} />
|
||||
</CardContent>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, ReactElement } from 'react'
|
||||
|
||||
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
||||
import { Toolbar, Chip, IconButton } from '@material-ui/core/'
|
||||
import { Toolbar, IconButton } from '@material-ui/core/'
|
||||
|
||||
import { Sun, Moon } from 'react-feather'
|
||||
|
||||
@@ -13,7 +13,6 @@ const useStyles = makeStyles(() =>
|
||||
width: `calc(100% - ${drawerWidth}px)`,
|
||||
marginLeft: drawerWidth,
|
||||
},
|
||||
network: {},
|
||||
}),
|
||||
)
|
||||
interface Props {
|
||||
@@ -42,16 +41,11 @@ export default function SideBar(props: Props): ReactElement {
|
||||
<div>
|
||||
<div style={{ display: 'fixed' }} className={classes.appBar}>
|
||||
<Toolbar style={{ display: 'flex' }}>
|
||||
<Chip style={{ marginLeft: '7px' }} size="small" label="Goerli" className={classes.network} />
|
||||
<div style={{ width: '100%' }}>
|
||||
<div style={{ float: 'right' }}>
|
||||
<IconButton style={{ marginRight: '10px' }} aria-label="dark-mode" onClick={() => switchTheme()}>
|
||||
{props.themeMode === 'dark' ? <Moon /> : <Sun />}
|
||||
</IconButton>
|
||||
{/* <Chip
|
||||
label="Connect Wallet"
|
||||
color="primary"
|
||||
/> */}
|
||||
</div>
|
||||
</div>
|
||||
</Toolbar>
|
||||
|
||||
@@ -7,9 +7,10 @@ function truncStringPortion(str: string, firstCharCount = 10, endCharCount = 10)
|
||||
|
||||
interface Props {
|
||||
peerId: string
|
||||
characterLength?: number
|
||||
}
|
||||
|
||||
export default function PeerDetail(props: Props): ReactElement {
|
||||
export default function PeerDetail({ peerId, characterLength }: Props): ReactElement {
|
||||
return (
|
||||
<Typography
|
||||
variant="button"
|
||||
@@ -17,7 +18,7 @@ export default function PeerDetail(props: Props): ReactElement {
|
||||
fontFamily: 'monospace, monospace',
|
||||
}}
|
||||
>
|
||||
{truncStringPortion(props.peerId)}
|
||||
{truncStringPortion(peerId, characterLength, characterLength)}
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { Link, RouteComponentProps } from 'react-router-dom'
|
||||
import { createStyles, Theme, makeStyles } from '@material-ui/core/styles'
|
||||
import { ListItemText, ListItemIcon, ListItem, Divider, List, Drawer, Link as MUILink } from '@material-ui/core'
|
||||
import { OpenInNewSharp } from '@material-ui/icons'
|
||||
import { Activity, FileText, DollarSign, Share2, Settings } from 'react-feather'
|
||||
import { Activity, FileText, DollarSign, Share2, Settings, Layers } from 'react-feather'
|
||||
|
||||
import SwarmLogoOrange from '../assets/swarm-logo-orange.svg'
|
||||
import { Health } from '@ethersphere/bee-js'
|
||||
@@ -24,6 +24,12 @@ const navBarItems = [
|
||||
path: '/files/',
|
||||
icon: FileText,
|
||||
},
|
||||
{
|
||||
label: 'Stamps',
|
||||
id: 'stamps',
|
||||
path: '/stamps/',
|
||||
icon: Layers,
|
||||
},
|
||||
{
|
||||
label: 'Accounting',
|
||||
id: 'accounting',
|
||||
|
||||
@@ -1,28 +1,23 @@
|
||||
import type { ReactElement } from 'react'
|
||||
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { Card, CardContent, Typography } from '@material-ui/core/'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { Skeleton } from '@material-ui/lab'
|
||||
import type { ReactElement } from 'react'
|
||||
import { Title } from './Title'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
minWidth: 275,
|
||||
},
|
||||
title: {
|
||||
fontSize: 16,
|
||||
},
|
||||
pos: {
|
||||
marginBottom: 12,
|
||||
},
|
||||
})
|
||||
|
||||
interface Props {
|
||||
label: string
|
||||
statistic?: string
|
||||
loading?: boolean
|
||||
tooltip?: string
|
||||
}
|
||||
|
||||
export default function StatCard({ loading, label, statistic }: Props): ReactElement {
|
||||
export default function StatCard({ loading, label, statistic, tooltip }: Props): ReactElement {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
@@ -36,9 +31,7 @@ export default function StatCard({ loading, label, statistic }: Props): ReactEle
|
||||
)}
|
||||
{!loading && (
|
||||
<>
|
||||
<Typography className={classes.title} color="textSecondary" gutterBottom>
|
||||
{label}
|
||||
</Typography>
|
||||
<Title label={label} tooltip={tooltip} />
|
||||
<Typography variant="h5" component="h2">
|
||||
{statistic}
|
||||
</Typography>
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import React, { ReactElement, ReactNode } from 'react'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import Tabs from '@material-ui/core/Tabs'
|
||||
import Tab from '@material-ui/core/Tab'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
import Box from '@material-ui/core/Box'
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: ReactNode
|
||||
index: number
|
||||
value: number
|
||||
}
|
||||
|
||||
function TabPanel(props: TabPanelProps) {
|
||||
const { children, value, index, ...other } = props
|
||||
|
||||
return (
|
||||
<div role="tabpanel" hidden={value !== index} {...other}>
|
||||
{value === index && (
|
||||
<Box p={3}>
|
||||
<Typography>{children}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
}))
|
||||
|
||||
interface TabsValues {
|
||||
component: ReactNode
|
||||
label: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
values: TabsValues[]
|
||||
index?: number
|
||||
indexChanged?: (index: number) => void
|
||||
}
|
||||
|
||||
export default function SimpleTabs({ values, index, indexChanged }: Props): ReactElement {
|
||||
const classes = useStyles()
|
||||
const [value, setValue] = React.useState<number>(index || 0)
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<Record<string, never>>, newValue: number) => {
|
||||
if (indexChanged) indexChanged(newValue)
|
||||
else setValue(newValue)
|
||||
}
|
||||
|
||||
const v = index !== undefined ? index : value
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Tabs value={v} onChange={handleChange}>
|
||||
{values.map(({ label }, idx) => (
|
||||
<Tab key={idx} label={label} />
|
||||
))}
|
||||
</Tabs>
|
||||
{values.map(({ component }, idx) => (
|
||||
<TabPanel key={idx} value={v} index={idx}>
|
||||
{component}
|
||||
</TabPanel>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1,25 +1,63 @@
|
||||
import type { Topology } from '@ethersphere/bee-js'
|
||||
import { Grid } from '@material-ui/core/'
|
||||
import type { ReactElement } from 'react'
|
||||
import { pickThreshold, ThresholdValues } from '../utils/threshold'
|
||||
import StatCard from './StatCard'
|
||||
|
||||
interface Props {
|
||||
isLoading: boolean
|
||||
interface RootProps {
|
||||
topology: Topology | null
|
||||
error: Error | null // FIXME: should display error
|
||||
}
|
||||
|
||||
const TopologyStats = ({ isLoading, topology }: Props): ReactElement => (
|
||||
interface Props extends RootProps {
|
||||
thresholds: ThresholdValues
|
||||
}
|
||||
|
||||
const TopologyStats = (props: RootProps): ReactElement => {
|
||||
const thresholds: ThresholdValues = {
|
||||
connectedPeers: pickThreshold('connectedPeers', props.topology?.connected || 0),
|
||||
population: pickThreshold('population', props.topology?.population || 0),
|
||||
depth: pickThreshold('depth', props.topology?.depth || 0),
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Indicator {...props} thresholds={thresholds} />
|
||||
<Metrics {...props} thresholds={thresholds} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Indicator = ({ thresholds }: Props): ReactElement => {
|
||||
const maximumTotalScore = Object.values(thresholds).reduce((sum, item) => sum + item.maximumScore, 0)
|
||||
const actualTotalScore = Object.values(thresholds).reduce((sum, item) => sum + item.score, 0)
|
||||
const percentageText = Math.round((actualTotalScore / maximumTotalScore) * 100) + '%'
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<StatCard label="Overall Health Indicator" statistic={percentageText} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Metrics = ({ topology, thresholds }: Props): ReactElement => (
|
||||
<Grid style={{ marginBottom: '20px', flexGrow: 1 }}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid key={1} item xs={12} sm={12} md={6} lg={4} xl={4}>
|
||||
<StatCard label="Connected Peers" statistic={topology?.connected.toString()} loading={isLoading} />
|
||||
<StatCard
|
||||
label="Connected Peers"
|
||||
statistic={topology?.connected.toString()}
|
||||
tooltip={thresholds.connectedPeers.explanation}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid key={2} item xs={12} sm={12} md={6} lg={4} xl={4}>
|
||||
<StatCard label="Population" statistic={topology?.population.toString()} loading={isLoading} />
|
||||
<StatCard
|
||||
label="Population"
|
||||
statistic={topology?.population.toString()}
|
||||
tooltip={thresholds.population.explanation}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid key={3} item xs={12} sm={12} md={6} lg={4} xl={4}>
|
||||
<StatCard label="Depth" statistic={topology?.depth.toString()} loading={isLoading} />
|
||||
<StatCard label="Depth" statistic={topology?.depth.toString()} tooltip={thresholds.depth.explanation} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -6,9 +6,10 @@ import DialogActions from '@material-ui/core/DialogActions'
|
||||
import DialogContent from '@material-ui/core/DialogContent'
|
||||
import DialogContentText from '@material-ui/core/DialogContentText'
|
||||
import DialogTitle from '@material-ui/core/DialogTitle'
|
||||
import { FormHelperText, Snackbar } from '@material-ui/core'
|
||||
import FormHelperText from '@material-ui/core/FormHelperText'
|
||||
import { Token } from '../models/Token'
|
||||
import type { BigNumber } from 'bignumber.js'
|
||||
import { useSnackbar } from 'notistack'
|
||||
|
||||
interface Props {
|
||||
successMessage: string
|
||||
@@ -17,10 +18,10 @@ interface Props {
|
||||
label: string
|
||||
max?: BigNumber
|
||||
min?: BigNumber
|
||||
action: (amount: bigint) => Promise<{ transactionHash: string }>
|
||||
action: (amount: bigint) => Promise<string>
|
||||
}
|
||||
|
||||
export default function WithdrawModal({
|
||||
export default function WithdrawDepositModal({
|
||||
successMessage,
|
||||
errorMessage,
|
||||
dialogMessage,
|
||||
@@ -33,8 +34,7 @@ export default function WithdrawModal({
|
||||
const [amount, setAmount] = useState('')
|
||||
const [amountToken, setAmountToken] = useState<Token | null>(null)
|
||||
const [amountError, setAmountError] = useState<Error | null>(null)
|
||||
const [showToast, setToastVisibility] = useState(false)
|
||||
const [toastContent, setToastContent] = useState('')
|
||||
const { enqueueSnackbar } = useSnackbar()
|
||||
|
||||
const handleClickOpen = () => {
|
||||
setOpen(true)
|
||||
@@ -48,20 +48,14 @@ export default function WithdrawModal({
|
||||
if (amountToken === null) return
|
||||
|
||||
try {
|
||||
const { transactionHash } = await action(amountToken.toBigInt as bigint)
|
||||
const transactionHash = await action(amountToken.toBigInt as bigint)
|
||||
setOpen(false)
|
||||
handleToast(`${successMessage} Transaction ${transactionHash}`)
|
||||
enqueueSnackbar(`${successMessage} Transaction ${transactionHash}`, { variant: 'success' })
|
||||
} catch (e) {
|
||||
handleToast(`${errorMessage} Error: ${e.message}`)
|
||||
enqueueSnackbar(`${errorMessage} Error: ${e.message}`, { variant: 'error' })
|
||||
}
|
||||
}
|
||||
|
||||
const handleToast = (text: string) => {
|
||||
setToastContent(text)
|
||||
setToastVisibility(true)
|
||||
setTimeout(() => setToastVisibility(false), 7000)
|
||||
}
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
|
||||
const value = e.target.value
|
||||
setAmount(value)
|
||||
@@ -83,7 +77,6 @@ export default function WithdrawModal({
|
||||
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
|
||||
{label}
|
||||
</Button>
|
||||
<Snackbar anchorOrigin={{ vertical: 'top', horizontal: 'center' }} open={showToast} message={toastContent} />
|
||||
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title">
|
||||
<DialogTitle id="form-dialog-title">{label}</DialogTitle>
|
||||
<DialogContent>
|
||||
@@ -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'
|
||||
@@ -1,18 +1,24 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import { beeDebugApi } from '../services/bee'
|
||||
import { ReactElement, useContext } from 'react'
|
||||
import { Context as SettingsContext } from '../providers/Settings'
|
||||
|
||||
import WDModal from '../components/WDModal'
|
||||
import WithdrawDepositModal from '../components/WithdrawDepositModal'
|
||||
import { BigNumber } from 'bignumber.js'
|
||||
|
||||
export default function DepositModal(): ReactElement {
|
||||
const { beeDebugApi } = useContext(SettingsContext)
|
||||
|
||||
return (
|
||||
<WDModal
|
||||
<WithdrawDepositModal
|
||||
successMessage="Successful deposit."
|
||||
errorMessage="Error with depositing"
|
||||
dialogMessage="Specify the amount of BZZ you would like to withdraw from your node."
|
||||
label="Deposit"
|
||||
min={new BigNumber(0)}
|
||||
action={beeDebugApi.chequebook.deposit}
|
||||
action={(amount: bigint) => {
|
||||
if (!beeDebugApi) throw new Error('Bee Debug URL is not valid')
|
||||
|
||||
return beeDebugApi.depositTokens(amount.toString())
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import { beeDebugApi } from '../services/bee'
|
||||
import { ReactElement, useContext } from 'react'
|
||||
import { Context as SettingsContext } from '../providers/Settings'
|
||||
|
||||
import WDModal from '../components/WDModal'
|
||||
import WithdrawDepositModal from '../components/WithdrawDepositModal'
|
||||
import { BigNumber } from 'bignumber.js'
|
||||
|
||||
export default function WithdrawModal(): ReactElement {
|
||||
const { beeDebugApi } = useContext(SettingsContext)
|
||||
|
||||
return (
|
||||
<WDModal
|
||||
<WithdrawDepositModal
|
||||
successMessage="Successful withdrawl."
|
||||
errorMessage="Error with withdrawing."
|
||||
dialogMessage="Specify the amount of BZZ you would like to withdraw from your node."
|
||||
label="Withdraw"
|
||||
min={new BigNumber(0)}
|
||||
action={beeDebugApi.chequebook.withdraw}
|
||||
action={(amount: bigint) => {
|
||||
if (!beeDebugApi) throw new Error('Bee Debug URL is not valid')
|
||||
|
||||
return beeDebugApi.withdrawTokens(amount.toString())
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
+18
-27
@@ -1,15 +1,11 @@
|
||||
import { LastCashoutActionResponse } from '@ethersphere/bee-js'
|
||||
import { LastCashoutActionResponse, BeeDebug } from '@ethersphere/bee-js'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Token } from '../models/Token'
|
||||
import { beeDebugApi } from '../services/bee'
|
||||
import { Balance, Settlement, useApiPeerBalances, useApiSettlements } from './apiHooks'
|
||||
import { makeRetriablePromise, unwrapPromiseSettlements } from '../utils'
|
||||
import { Balance, Settlements, Settlement } from '../types'
|
||||
|
||||
interface UseAccountingHook {
|
||||
isLoading: boolean
|
||||
isLoadingUncashed: boolean
|
||||
error: Error | null
|
||||
totalsent: Token
|
||||
totalreceived: Token
|
||||
accounting: Accounting[] | null
|
||||
}
|
||||
|
||||
@@ -76,39 +72,34 @@ function mergeAccounting(
|
||||
)
|
||||
}
|
||||
|
||||
export const useAccounting = (): UseAccountingHook => {
|
||||
const settlements = useApiSettlements()
|
||||
const balances = useApiPeerBalances()
|
||||
|
||||
const [err, setErr] = useState<Error | null>(null)
|
||||
export const useAccounting = (
|
||||
beeDebugApi: BeeDebug | null,
|
||||
settlements: Settlements | null,
|
||||
balances: Balance[] | null,
|
||||
): UseAccountingHook => {
|
||||
const [isLoadingUncashed, setIsloadingUncashed] = useState<boolean>(false)
|
||||
const [uncashedAmounts, setUncashedAmounts] = useState<LastCashoutActionResponse[] | undefined>(undefined)
|
||||
|
||||
const error = balances.error || settlements.error || err
|
||||
|
||||
useEffect(() => {
|
||||
// We don't have any settlements loaded yet or we are already loading/have loaded the uncashed amounts
|
||||
if (isLoadingUncashed || !settlements.settlements || uncashedAmounts || error) return
|
||||
if (isLoadingUncashed || !beeDebugApi || !settlements || uncashedAmounts) return
|
||||
|
||||
setIsloadingUncashed(true)
|
||||
const promises = settlements.settlements.settlements
|
||||
const promises = settlements.settlements
|
||||
.filter(({ received }) => received.toBigNumber.gt('0'))
|
||||
.map(({ peer }) => beeDebugApi.chequebook.getPeerLastCashout(peer))
|
||||
.map(({ peer }) => makeRetriablePromise(() => beeDebugApi.getLastCashoutAction(peer)))
|
||||
|
||||
Promise.all(promises)
|
||||
.then(setUncashedAmounts)
|
||||
.catch(setErr)
|
||||
.finally(() => setIsloadingUncashed(false))
|
||||
}, [settlements, isLoadingUncashed, uncashedAmounts, error])
|
||||
Promise.allSettled(promises).then(settlements => {
|
||||
const results = unwrapPromiseSettlements(settlements)
|
||||
setUncashedAmounts(results.fulfilled)
|
||||
setIsloadingUncashed(false)
|
||||
})
|
||||
}, [settlements, isLoadingUncashed, uncashedAmounts])
|
||||
|
||||
const accounting = mergeAccounting(balances.peerBalances, settlements.settlements?.settlements, uncashedAmounts)
|
||||
const accounting = mergeAccounting(balances, settlements?.settlements, uncashedAmounts)
|
||||
|
||||
return {
|
||||
isLoading: settlements.isLoadingSettlements || balances.isLoadingPeerBalances,
|
||||
isLoadingUncashed,
|
||||
error,
|
||||
accounting,
|
||||
totalsent: settlements.settlements?.totalSent || new Token('0'),
|
||||
totalreceived: settlements.settlements?.totalReceived || new Token('0'),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,406 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
import {
|
||||
NodeAddresses,
|
||||
ChequebookAddressResponse,
|
||||
LastChequesResponse,
|
||||
Health,
|
||||
Peer,
|
||||
Topology,
|
||||
LastChequesForPeerResponse,
|
||||
} from '@ethersphere/bee-js'
|
||||
|
||||
import { beeDebugApi, beeApi } from '../services/bee'
|
||||
import axios from 'axios'
|
||||
import { Token } from '../models/Token'
|
||||
|
||||
export interface HealthHook {
|
||||
health: boolean
|
||||
isLoadingHealth: boolean
|
||||
error: Error | null
|
||||
}
|
||||
export const useApiHealth = (): HealthHook => {
|
||||
const [health, setHealth] = useState<boolean>(false)
|
||||
const [isLoadingHealth, setLoading] = useState<boolean>(true)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
beeApi.status
|
||||
.health()
|
||||
.then(res => {
|
||||
setHealth(res)
|
||||
})
|
||||
.catch(error => {
|
||||
setError(error)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return { health, isLoadingHealth, error }
|
||||
}
|
||||
|
||||
export interface DebugHealthHook {
|
||||
nodeHealth: Health | null
|
||||
isLoadingNodeHealth: boolean
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
export const useDebugApiHealth = (): DebugHealthHook => {
|
||||
const [nodeHealth, setNodeHealth] = useState<Health | null>(null)
|
||||
const [isLoadingNodeHealth, setLoading] = useState<boolean>(true)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
beeDebugApi.status
|
||||
.nodeHealth()
|
||||
.then(res => {
|
||||
setNodeHealth(res)
|
||||
})
|
||||
.catch(error => {
|
||||
setError(error)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return { nodeHealth, isLoadingNodeHealth, error }
|
||||
}
|
||||
|
||||
export interface NodeAddressesHook {
|
||||
nodeAddresses: NodeAddresses | null
|
||||
isLoadingNodeAddresses: boolean
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
export const useApiNodeAddresses = (): NodeAddressesHook => {
|
||||
const [nodeAddresses, setNodeAddresses] = useState<NodeAddresses | null>(null)
|
||||
const [isLoadingNodeAddresses, setLoading] = useState<boolean>(true)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
beeDebugApi.connectivity
|
||||
.addresses()
|
||||
.then(res => {
|
||||
setNodeAddresses(res)
|
||||
})
|
||||
.catch(error => {
|
||||
setError(error)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return { nodeAddresses, isLoadingNodeAddresses, error }
|
||||
}
|
||||
|
||||
export interface NodeTopologyHook {
|
||||
topology: Topology | null
|
||||
isLoading: boolean
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
export const useApiNodeTopology = (): NodeTopologyHook => {
|
||||
const [topology, setNodeTopology] = useState<Topology | null>(null)
|
||||
const [isLoading, setLoading] = useState<boolean>(true)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
beeDebugApi.connectivity
|
||||
.topology()
|
||||
.then(res => {
|
||||
setNodeTopology(res)
|
||||
})
|
||||
.catch(error => {
|
||||
setError(error)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return { topology, isLoading, error }
|
||||
}
|
||||
export interface ChequebookAddressHook {
|
||||
chequebookAddress: ChequebookAddressResponse | null
|
||||
isLoadingChequebookAddress: boolean
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
export const useApiChequebookAddress = (): ChequebookAddressHook => {
|
||||
const [chequebookAddress, setChequebookAddress] = useState<ChequebookAddressResponse | null>(null)
|
||||
const [isLoadingChequebookAddress, setLoading] = useState<boolean>(true)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
beeDebugApi.chequebook
|
||||
.address()
|
||||
.then(res => {
|
||||
setChequebookAddress(res)
|
||||
})
|
||||
.catch(error => {
|
||||
setError(error)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return { chequebookAddress, isLoadingChequebookAddress, error }
|
||||
}
|
||||
|
||||
export interface NodePeersHook {
|
||||
peers: Peer[] | null
|
||||
isLoading: boolean
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
export const useApiNodePeers = (): NodePeersHook => {
|
||||
const [peers, setPeers] = useState<Peer[] | null>(null)
|
||||
const [isLoading, setLoading] = useState<boolean>(true)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
beeDebugApi.connectivity
|
||||
.listPeers()
|
||||
.then(res => {
|
||||
setPeers(res)
|
||||
})
|
||||
.catch(error => {
|
||||
setError(error)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return { peers, isLoading, error }
|
||||
}
|
||||
|
||||
export interface ChequebookBalance {
|
||||
totalBalance: Token
|
||||
availableBalance: Token
|
||||
}
|
||||
|
||||
export interface ChequebookBalanceHook {
|
||||
chequebookBalance: ChequebookBalance | null
|
||||
isLoadingChequebookBalance: boolean
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
export const useApiChequebookBalance = (): ChequebookBalanceHook => {
|
||||
const [chequebookBalance, setChequebookBalance] = useState<ChequebookBalance | null>(null)
|
||||
const [isLoadingChequebookBalance, setLoading] = useState<boolean>(true)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
beeDebugApi.chequebook
|
||||
.balance()
|
||||
.then(({ totalBalance, availableBalance }) => {
|
||||
const balance = {
|
||||
totalBalance: new Token(totalBalance),
|
||||
availableBalance: new Token(availableBalance),
|
||||
}
|
||||
setChequebookBalance(balance)
|
||||
})
|
||||
.catch(error => {
|
||||
setError(error)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return { chequebookBalance, isLoadingChequebookBalance, error }
|
||||
}
|
||||
|
||||
export interface Balance {
|
||||
peer: string
|
||||
balance: Token
|
||||
}
|
||||
|
||||
export interface PeerBalanceHook {
|
||||
peerBalances: Balance[] | null
|
||||
isLoadingPeerBalances: boolean
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
export const useApiPeerBalances = (): PeerBalanceHook => {
|
||||
const [peerBalances, setPeerBalances] = useState<Balance[] | null>(null)
|
||||
const [isLoadingPeerBalances, setLoading] = useState<boolean>(true)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
beeDebugApi.balance
|
||||
.balances()
|
||||
.then(res => {
|
||||
// for some reason sometimes these are numbers and not BigInts
|
||||
const balances = res.balances.map(({ peer, balance }) => ({ peer, balance: new Token(balance) }))
|
||||
setPeerBalances(balances)
|
||||
})
|
||||
.catch(error => {
|
||||
setError(error)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return { peerBalances, isLoadingPeerBalances, error }
|
||||
}
|
||||
|
||||
export interface PeerChequesHook {
|
||||
peerCheques: LastChequesResponse | null
|
||||
isLoadingPeerCheques: boolean
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
export const useApiPeerCheques = (): PeerChequesHook => {
|
||||
const [peerCheques, setPeerCheques] = useState<LastChequesResponse | null>(null)
|
||||
const [isLoadingPeerCheques, setLoading] = useState<boolean>(true)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
beeDebugApi.chequebook
|
||||
.getLastCheques()
|
||||
.then(res => {
|
||||
setPeerCheques(res)
|
||||
})
|
||||
.catch(error => {
|
||||
setError(error)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return { peerCheques, isLoadingPeerCheques, error }
|
||||
}
|
||||
|
||||
export interface PeerLastChequesHook {
|
||||
peerCheque: LastChequesForPeerResponse | null
|
||||
isLoadingPeerCheque: boolean
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
export const useApiPeerLastCheque = (peerId: string): PeerLastChequesHook => {
|
||||
const [peerCheque, setPeerCheque] = useState<LastChequesForPeerResponse | null>(null)
|
||||
const [isLoadingPeerCheque, setLoading] = useState<boolean>(true)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
beeDebugApi.chequebook
|
||||
.getPeerLastCheques(peerId)
|
||||
.then(res => {
|
||||
setPeerCheque(res)
|
||||
})
|
||||
.catch(error => {
|
||||
setError(error)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}, [peerId])
|
||||
|
||||
return { peerCheque, isLoadingPeerCheque, error }
|
||||
}
|
||||
|
||||
export interface Settlement {
|
||||
peer: string
|
||||
received: Token
|
||||
sent: Token
|
||||
}
|
||||
|
||||
export interface Settlements {
|
||||
totalReceived: Token
|
||||
totalSent: Token
|
||||
settlements: Settlement[]
|
||||
}
|
||||
|
||||
export interface SettlementsHook {
|
||||
settlements: Settlements | null
|
||||
isLoadingSettlements: boolean
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
export const useApiSettlements = (): SettlementsHook => {
|
||||
const [settlements, setSettlements] = useState<Settlements | null>(null)
|
||||
const [isLoadingSettlements, setLoading] = useState<boolean>(true)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
beeDebugApi.settlements
|
||||
.getSettlements()
|
||||
.then(({ totalReceived, settlements, totalSent }) => {
|
||||
const set = {
|
||||
totalReceived: new Token(totalReceived),
|
||||
totalSent: new Token(totalSent),
|
||||
settlements: settlements.map(({ peer, received, sent }) => ({
|
||||
peer,
|
||||
received: new Token(received),
|
||||
sent: new Token(sent),
|
||||
})),
|
||||
}
|
||||
setSettlements(set)
|
||||
})
|
||||
.catch(error => {
|
||||
setError(error)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return { settlements, isLoadingSettlements, error }
|
||||
}
|
||||
|
||||
export interface LastCashout {
|
||||
peer: string
|
||||
uncashedAmount: Token
|
||||
}
|
||||
|
||||
export interface PeerLastCashoutHook {
|
||||
peerCashout: LastCashout | null
|
||||
isLoadingPeerCashout: boolean
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
export const useApiPeerLastCashout = (peerId: string): PeerLastCashoutHook => {
|
||||
const [peerCashout, setPeerCashout] = useState<LastCashout | null>(null)
|
||||
const [isLoadingPeerCashout, setLoading] = useState<boolean>(true)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
beeDebugApi.chequebook
|
||||
.getPeerLastCashout(peerId)
|
||||
.then(({ peer, uncashedAmount }) => {
|
||||
setPeerCashout({ peer, uncashedAmount: new Token(uncashedAmount.toString()) })
|
||||
})
|
||||
.catch(error => {
|
||||
setError(error)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}, [peerId])
|
||||
|
||||
return { peerCashout, isLoadingPeerCashout, error }
|
||||
}
|
||||
|
||||
export interface LatestBeeReleaseHook {
|
||||
latestBeeRelease: LatestBeeRelease | null
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import { ChequebookAddressResponse } from '@ethersphere/bee-js'
|
||||
import {
|
||||
ChequebookBalance,
|
||||
useApiChequebookAddress,
|
||||
useApiChequebookBalance,
|
||||
useApiHealth,
|
||||
useApiNodeAddresses,
|
||||
useApiNodeTopology,
|
||||
useDebugApiHealth,
|
||||
useLatestBeeRelease,
|
||||
} from './apiHooks'
|
||||
|
||||
export interface StatusChequebookHook extends StatusHookCommon {
|
||||
chequebookBalance: ChequebookBalance | null
|
||||
chequebookAddress: ChequebookAddressResponse | null
|
||||
}
|
||||
|
||||
export const useStatusNodeVersion = (): StatusNodeVersionHook => {
|
||||
const { latestBeeRelease, isLoadingLatestBeeRelease } = useLatestBeeRelease()
|
||||
const { nodeHealth, isLoadingNodeHealth } = useDebugApiHealth()
|
||||
|
||||
return {
|
||||
isLoading: isLoadingNodeHealth || isLoadingLatestBeeRelease,
|
||||
isOk: Boolean(latestBeeRelease && latestBeeRelease.name === `v${nodeHealth?.version?.split('-')[0]}`),
|
||||
userVersion: nodeHealth?.version?.split('-')[0] || '-',
|
||||
latestVersion: latestBeeRelease?.name.substring(1) || '-',
|
||||
latestUrl: latestBeeRelease?.html_url || 'https://github.com/ethersphere/bee/releases/latest',
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
+17
-22
@@ -1,17 +1,19 @@
|
||||
import { useState, useEffect, ReactElement } from 'react'
|
||||
import { useState, useEffect, useContext, ReactElement } from 'react'
|
||||
import ErrorBoundary from '../components/ErrorBoundary'
|
||||
import AlertVersion from '../components/AlertVersion'
|
||||
import { Container, CircularProgress } from '@material-ui/core'
|
||||
|
||||
import { createStyles, Theme, makeStyles } from '@material-ui/core/styles'
|
||||
|
||||
import SideBar from '../components/SideBar'
|
||||
import NavBar from '../components/NavBar'
|
||||
|
||||
import { useApiHealth, useDebugApiHealth } from '../hooks/apiHooks'
|
||||
import { Context } from '../providers/Bee'
|
||||
|
||||
import { RouteComponentProps } from 'react-router'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
toolbar: theme.mixins.toolbar,
|
||||
content: {
|
||||
marginLeft: '240px',
|
||||
flexGrow: 1,
|
||||
@@ -19,20 +21,6 @@ const useStyles = makeStyles((theme: Theme) =>
|
||||
padding: theme.spacing(3),
|
||||
paddingBottom: '65px',
|
||||
},
|
||||
footer: {
|
||||
marginLeft: '240px',
|
||||
backgroundColor: theme.palette.background.default,
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
flexGrow: 1,
|
||||
width: '-webkit-fill-available',
|
||||
padding: theme.spacing(2),
|
||||
textAlign: 'center',
|
||||
},
|
||||
logo: {
|
||||
height: '20px',
|
||||
marginRight: '7px',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -45,9 +33,7 @@ const Dashboard = (props: Props): ReactElement => {
|
||||
|
||||
const [themeMode, toggleThemeMode] = useState('light')
|
||||
|
||||
// FIXME: handle errrors and loading
|
||||
const { health } = useApiHealth()
|
||||
const { nodeHealth } = useDebugApiHealth()
|
||||
const { isLoading, apiHealth, debugApiHealth } = useContext(Context)
|
||||
|
||||
useEffect(() => {
|
||||
const theme = localStorage.getItem('theme')
|
||||
@@ -70,10 +56,19 @@ const Dashboard = (props: Props): ReactElement => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SideBar {...props} themeMode={themeMode} health={health} nodeHealth={nodeHealth} />
|
||||
<SideBar {...props} themeMode={themeMode} health={apiHealth} nodeHealth={debugApiHealth} />
|
||||
<NavBar themeMode={themeMode} />
|
||||
<ErrorBoundary>
|
||||
<main className={classes.content}>{props.children}</main>
|
||||
<main className={classes.content}>
|
||||
<AlertVersion />
|
||||
{isLoading ? (
|
||||
<Container style={{ textAlign: 'center', padding: '50px' }}>
|
||||
<CircularProgress />
|
||||
</Container>
|
||||
) : (
|
||||
props.children
|
||||
)}
|
||||
</main>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -2,7 +2,6 @@ import { ReactElement } from 'react'
|
||||
|
||||
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
||||
import { Card, CardContent, Typography, Theme } from '@material-ui/core/'
|
||||
import { Skeleton } from '@material-ui/lab'
|
||||
import WithdrawModal from '../../containers/WithdrawModal'
|
||||
import DepositModal from '../../containers/DepositModal'
|
||||
|
||||
@@ -45,12 +44,11 @@ interface ChequebookBalance {
|
||||
interface Props {
|
||||
chequebookAddress: ChequebookAddressResponse | null
|
||||
chequebookBalance: ChequebookBalance | null
|
||||
totalsent: Token
|
||||
totalreceived: Token
|
||||
isLoading: boolean
|
||||
totalsent?: Token
|
||||
totalreceived?: Token
|
||||
}
|
||||
|
||||
function AccountCard({ totalreceived, totalsent, chequebookBalance, isLoading }: Props): ReactElement {
|
||||
function AccountCard({ totalreceived, totalsent, chequebookBalance }: Props): ReactElement {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
@@ -66,7 +64,6 @@ function AccountCard({ totalreceived, totalsent, chequebookBalance, isLoading }:
|
||||
</div>
|
||||
|
||||
<Card className={classes.root}>
|
||||
{!isLoading && (
|
||||
<CardContent className={classes.gridContainer}>
|
||||
<div>
|
||||
<Typography component="h2" variant="h6" color="primary" gutterBottom>
|
||||
@@ -85,18 +82,10 @@ function AccountCard({ totalreceived, totalsent, chequebookBalance, isLoading }:
|
||||
Total Sent / Received
|
||||
</Typography>
|
||||
<Typography variant="h5">
|
||||
{totalsent.toFixedDecimal()} / {totalreceived.toFixedDecimal()} BZZ
|
||||
{totalsent?.toFixedDecimal()} / {totalreceived?.toFixedDecimal()} BZZ
|
||||
</Typography>
|
||||
</div>
|
||||
</CardContent>
|
||||
)}
|
||||
{isLoading && (
|
||||
<div className={classes.gridContainer}>
|
||||
<Skeleton width={180} height={110} animation="wave" />
|
||||
<Skeleton width={180} height={110} animation="wave" />
|
||||
<Skeleton width={180} height={110} animation="wave" />
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Table, TableBody, TableCell, TableContainer, TableRow, TableHead, Paper
|
||||
|
||||
import ClipboardCopy from '../../components/ClipboardCopy'
|
||||
import CashoutModal from '../../components/CashoutModal'
|
||||
import PeerDetailDrawer from './PeerDetail'
|
||||
import PeerDetailDrawer from '../../components/PeerDetail'
|
||||
import { Accounting } from '../../hooks/accounting'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import { ReactElement, useContext } from 'react'
|
||||
import { Theme, createStyles, makeStyles } from '@material-ui/core/styles'
|
||||
import { Container, CircularProgress } from '@material-ui/core'
|
||||
|
||||
import AccountCard from '../accounting/AccountCard'
|
||||
import BalancesTable from './BalancesTable'
|
||||
import EthereumAddressCard from '../../components/EthereumAddressCard'
|
||||
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
|
||||
|
||||
import {
|
||||
useApiNodeAddresses,
|
||||
useApiChequebookAddress,
|
||||
useApiChequebookBalance,
|
||||
useApiHealth,
|
||||
useDebugApiHealth,
|
||||
} from '../../hooks/apiHooks'
|
||||
import { Context as BeeContext } from '../../providers/Bee'
|
||||
import { Context as SettingsContext } from '../../providers/Settings'
|
||||
import { useAccounting } from '../../hooks/accounting'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
@@ -29,44 +22,25 @@ const useStyles = makeStyles((theme: Theme) =>
|
||||
export default function Accounting(): ReactElement {
|
||||
const classes = useStyles()
|
||||
|
||||
const { chequebookAddress, isLoadingChequebookAddress } = useApiChequebookAddress()
|
||||
const { chequebookBalance, isLoadingChequebookBalance } = useApiChequebookBalance()
|
||||
const { nodeAddresses, isLoadingNodeAddresses } = useApiNodeAddresses()
|
||||
const { health, isLoadingHealth } = useApiHealth()
|
||||
const { nodeHealth, isLoadingNodeHealth } = useDebugApiHealth()
|
||||
const { isLoading, totalsent, totalreceived, accounting, isLoadingUncashed, error } = useAccounting()
|
||||
|
||||
if (isLoadingHealth || isLoadingNodeHealth) {
|
||||
return (
|
||||
<Container style={{ textAlign: 'center', padding: '50px' }}>
|
||||
<CircularProgress />
|
||||
</Container>
|
||||
const { status, nodeAddresses, chequebookAddress, chequebookBalance, settlements, peerBalances } = useContext(
|
||||
BeeContext,
|
||||
)
|
||||
}
|
||||
const { beeDebugApi } = useContext(SettingsContext)
|
||||
|
||||
if (nodeHealth?.status !== 'ok' || !health) return <TroubleshootConnectionCard />
|
||||
const { accounting, isLoadingUncashed } = useAccounting(beeDebugApi, settlements, peerBalances)
|
||||
|
||||
if (!status.all) return <TroubleshootConnectionCard />
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<AccountCard
|
||||
chequebookAddress={chequebookAddress}
|
||||
isLoading={isLoadingChequebookAddress || isLoading || isLoadingChequebookBalance}
|
||||
chequebookBalance={chequebookBalance}
|
||||
totalsent={totalsent}
|
||||
totalreceived={totalreceived}
|
||||
totalsent={settlements?.totalSent}
|
||||
totalreceived={settlements?.totalReceived}
|
||||
/>
|
||||
<EthereumAddressCard
|
||||
nodeAddresses={nodeAddresses}
|
||||
isLoadingNodeAddresses={isLoadingNodeAddresses}
|
||||
chequebookAddress={chequebookAddress}
|
||||
isLoadingChequebookAddress={isLoadingChequebookAddress}
|
||||
/>
|
||||
{error && (
|
||||
<Container style={{ textAlign: 'center', padding: '50px' }}>
|
||||
Error loading accounting details: {error.message}
|
||||
</Container>
|
||||
)}
|
||||
{!error && <BalancesTable accounting={accounting} isLoadingUncashed={isLoadingUncashed} />}
|
||||
<EthereumAddressCard nodeAddresses={nodeAddresses} chequebookAddress={chequebookAddress} />
|
||||
<BalancesTable accounting={accounting} isLoadingUncashed={isLoadingUncashed} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { ReactElement, useState, useContext } from 'react'
|
||||
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
|
||||
import { Paper, InputBase, IconButton, FormHelperText } from '@material-ui/core'
|
||||
import { Search } from '@material-ui/icons'
|
||||
import { Context as SettingsContext } from '../../providers/Settings'
|
||||
import { Utils } from '@ethersphere/bee-js'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
padding: theme.spacing(0.25),
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
input: {
|
||||
marginLeft: theme.spacing(1),
|
||||
flex: 1,
|
||||
},
|
||||
iconButton: {
|
||||
padding: 10,
|
||||
},
|
||||
divider: {
|
||||
height: 28,
|
||||
margin: 4,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
export default function Files(): ReactElement {
|
||||
const classes = useStyles()
|
||||
const { apiUrl } = useContext(SettingsContext)
|
||||
|
||||
const [referenceInput, setReferenceInput] = useState('')
|
||||
const [referenceError, setReferenceError] = useState<Error | null>(null)
|
||||
|
||||
const handleReferenceChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setReferenceInput(e.target.value)
|
||||
|
||||
if (Utils.Hex.isHexString(e.target.value, 64) || Utils.Hex.isHexString(e.target.value, 128)) setReferenceError(null)
|
||||
else setReferenceError(new Error('Incorrect format of swarm hash'))
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paper className={classes.root}>
|
||||
<InputBase
|
||||
className={classes.input}
|
||||
placeholder="Enter swarm reference e.g. 0773a91efd6547c754fc1d95fb1c62c7d1b47f959c2caa685dfec8736da95c1c"
|
||||
inputProps={{ 'aria-label': 'retrieve file from swarm' }}
|
||||
value={referenceInput}
|
||||
onChange={handleReferenceChange}
|
||||
/>
|
||||
<IconButton
|
||||
href={`${apiUrl}/bzz/${referenceInput}`}
|
||||
target="_blank"
|
||||
disabled={referenceError !== null || !referenceInput}
|
||||
className={classes.iconButton}
|
||||
aria-label="download"
|
||||
>
|
||||
<Search />
|
||||
</IconButton>
|
||||
</Paper>
|
||||
{referenceError && <FormHelperText error>{referenceError.message}</FormHelperText>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import Button from '@material-ui/core/Button'
|
||||
import ListItemIcon from '@material-ui/core/ListItemIcon'
|
||||
import Menu from '@material-ui/core/Menu'
|
||||
import MenuItem from '@material-ui/core/MenuItem'
|
||||
import React, { ReactElement } from 'react'
|
||||
import PeerDetailDrawer from '../../components/PeerDetail'
|
||||
import { EnrichedPostageBatch } from '../../providers/Stamps'
|
||||
|
||||
interface Props {
|
||||
stamps: EnrichedPostageBatch[] | null
|
||||
selectedStamp: EnrichedPostageBatch | null
|
||||
setSelected: (stamp: EnrichedPostageBatch) => void
|
||||
}
|
||||
|
||||
export default function SimpleMenu({ stamps, selectedStamp, setSelected }: Props): ReactElement | null {
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null)
|
||||
|
||||
if (!stamps) return null
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget)
|
||||
}
|
||||
|
||||
const handleClose = () => setAnchorEl(null)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button aria-controls="simple-menu" aria-haspopup="true" onClick={handleClick}>
|
||||
Change
|
||||
</Button>
|
||||
<Menu id="simple-menu" anchorEl={anchorEl} keepMounted open={Boolean(anchorEl)} onClose={handleClose}>
|
||||
{stamps.map(stamp => (
|
||||
<MenuItem
|
||||
key={stamp.batchID}
|
||||
onClick={() => {
|
||||
setSelected(stamp)
|
||||
handleClose()
|
||||
}}
|
||||
selected={stamp.batchID === selectedStamp?.batchID}
|
||||
>
|
||||
<ListItemIcon>{stamp.usageText}</ListItemIcon>
|
||||
<PeerDetailDrawer peerId={stamp.batchID} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { Button, CircularProgress, Container } from '@material-ui/core'
|
||||
import Avatar from '@material-ui/core/Avatar'
|
||||
import Chip from '@material-ui/core/Chip'
|
||||
import { DropzoneArea } from 'material-ui-dropzone'
|
||||
import { useSnackbar } from 'notistack'
|
||||
import { ReactElement, useContext, useEffect, useState } from 'react'
|
||||
import UploadSizeAlert from '../../components/AlertUploadSize'
|
||||
import ClipboardCopy from '../../components/ClipboardCopy'
|
||||
import PeerDetailDrawer from '../../components/PeerDetail'
|
||||
import { Context, EnrichedPostageBatch } from '../../providers/Stamps'
|
||||
import { Context as SettingsContext } from '../../providers/Settings'
|
||||
import CreatePostageStamp from '../stamps/CreatePostageStampModal'
|
||||
import SelectStamp from './SelectStamp'
|
||||
|
||||
const MAX_FILE_SIZE = 1_000_000_000 // 1 gigabyte
|
||||
|
||||
export default function Files(): ReactElement {
|
||||
const [dropzoneKey, setDropzoneKey] = useState(0)
|
||||
const [file, setFile] = useState<File | null>(null)
|
||||
const [uploadReference, setUploadReference] = useState('')
|
||||
const [isUploadingFile, setIsUploadingFile] = useState(false)
|
||||
|
||||
const [selectedStamp, setSelectedStamp] = useState<EnrichedPostageBatch | null>(null)
|
||||
|
||||
const { isLoading, error, stamps } = useContext(Context)
|
||||
const { beeApi } = useContext(SettingsContext)
|
||||
const { enqueueSnackbar } = useSnackbar()
|
||||
|
||||
// Choose a postage stamp that has the lowest usage
|
||||
useEffect(() => {
|
||||
if (!selectedStamp && stamps && stamps.length > 0) {
|
||||
const stamp = stamps.reduce((prev, curr) => {
|
||||
if (curr.usage < prev.usage) return curr
|
||||
|
||||
return prev
|
||||
}, stamps[0])
|
||||
|
||||
setSelectedStamp(stamp)
|
||||
}
|
||||
}, [isLoading, error, stamps, selectedStamp])
|
||||
|
||||
const uploadFile = () => {
|
||||
if (file === null || selectedStamp === null) return
|
||||
|
||||
if (!beeApi) return
|
||||
|
||||
setIsUploadingFile(true)
|
||||
beeApi
|
||||
.uploadFile(selectedStamp.batchID, file)
|
||||
.then(hash => {
|
||||
window.setTimeout(() => {
|
||||
setFile(null)
|
||||
setUploadReference(hash)
|
||||
setDropzoneKey(dropzoneKey + 1)
|
||||
}, 0)
|
||||
})
|
||||
.catch(e => enqueueSnackbar(`Error uploading: ${e.message}`, { variant: 'error' }))
|
||||
.finally(() => {
|
||||
setIsUploadingFile(false)
|
||||
})
|
||||
}
|
||||
|
||||
const handleChange = (files?: File[]) => {
|
||||
if (files) {
|
||||
setFile(files[0])
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<DropzoneArea
|
||||
key={'dropzone-' + dropzoneKey}
|
||||
onChange={handleChange}
|
||||
filesLimit={1}
|
||||
maxFileSize={MAX_FILE_SIZE}
|
||||
/>
|
||||
<div style={{ marginTop: '15px' }}>
|
||||
{selectedStamp && (
|
||||
<div style={{ display: 'flex' }}>
|
||||
<small>
|
||||
with Postage Stamp{' '}
|
||||
<Chip
|
||||
avatar={<Avatar>{selectedStamp.usageText}</Avatar>}
|
||||
label={<PeerDetailDrawer peerId={selectedStamp.batchID} characterLength={6} />}
|
||||
deleteIcon={<ClipboardCopy value={selectedStamp.batchID} />}
|
||||
onDelete={() => {} /* eslint-disable-line*/}
|
||||
variant="outlined"
|
||||
/>
|
||||
</small>
|
||||
<SelectStamp stamps={stamps} selectedStamp={selectedStamp} setSelected={setSelectedStamp} />
|
||||
</div>
|
||||
)}
|
||||
{!selectedStamp && <CreatePostageStamp />}
|
||||
<Button disabled={!file && isUploadingFile && !selectedStamp} onClick={() => uploadFile()}>
|
||||
Upload
|
||||
</Button>
|
||||
{file && <UploadSizeAlert file={file} />}
|
||||
{isUploadingFile && (
|
||||
<Container style={{ textAlign: 'center', padding: '50px' }}>
|
||||
<CircularProgress />
|
||||
</Container>
|
||||
)}
|
||||
{uploadReference && (
|
||||
<div style={{ marginBottom: '15px', display: 'flex' }}>
|
||||
<span>{uploadReference}</span>
|
||||
<ClipboardCopy value={uploadReference} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+19
-81
@@ -1,94 +1,32 @@
|
||||
import { ReactElement, useState } from 'react'
|
||||
import { ReactElement, useContext } from 'react'
|
||||
|
||||
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
|
||||
import {
|
||||
Paper,
|
||||
InputBase,
|
||||
IconButton,
|
||||
Typography,
|
||||
Container,
|
||||
CircularProgress,
|
||||
FormHelperText,
|
||||
} from '@material-ui/core'
|
||||
import { Search } from '@material-ui/icons'
|
||||
import { Container } from '@material-ui/core'
|
||||
|
||||
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
|
||||
import { useApiHealth, useDebugApiHealth } from '../../hooks/apiHooks'
|
||||
import { apiHost } from '../../constants'
|
||||
import { Utils } from '@ethersphere/bee-js'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
padding: '2px 4px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
input: {
|
||||
marginLeft: theme.spacing(1),
|
||||
flex: 1,
|
||||
},
|
||||
iconButton: {
|
||||
padding: 10,
|
||||
},
|
||||
divider: {
|
||||
height: 28,
|
||||
margin: 4,
|
||||
},
|
||||
}),
|
||||
)
|
||||
import { Context } from '../../providers/Bee'
|
||||
import Download from './Download'
|
||||
import Upload from './Upload'
|
||||
import TabsContainer from '../../components/TabsContainer'
|
||||
|
||||
export default function Files(): ReactElement {
|
||||
const classes = useStyles()
|
||||
const { status } = useContext(Context)
|
||||
|
||||
const [referenceInput, setReferenceInput] = useState('')
|
||||
const [referenceError, setReferenceError] = useState<Error | null>(null)
|
||||
const { health, isLoadingHealth } = useApiHealth()
|
||||
const { nodeHealth, isLoadingNodeHealth } = useDebugApiHealth()
|
||||
|
||||
const handleReferenceChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setReferenceInput(e.target.value)
|
||||
|
||||
if (Utils.Hex.isHexString(e.target.value, 64)) setReferenceError(null)
|
||||
else setReferenceError(new Error('Incorrect format of swarm hash'))
|
||||
}
|
||||
|
||||
if (isLoadingHealth || isLoadingNodeHealth) {
|
||||
return (
|
||||
<Container style={{ textAlign: 'center', padding: '50px' }}>
|
||||
<CircularProgress />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
if (!health || nodeHealth?.status !== 'ok') return <TroubleshootConnectionCard />
|
||||
if (!status.all) return <TroubleshootConnectionCard />
|
||||
|
||||
return (
|
||||
<Container maxWidth="sm">
|
||||
<div style={{ marginBottom: '7px' }}>
|
||||
<Typography variant="button" color="primary" style={{ marginRight: '7px' }}>
|
||||
Download
|
||||
</Typography>
|
||||
</div>
|
||||
<Paper className={classes.root}>
|
||||
<InputBase
|
||||
className={classes.input}
|
||||
placeholder="Enter swarm reference e.g. 0773a91efd6547c754fc1d95fb1c62c7d1b47f959c2caa685dfec8736da95c1c"
|
||||
inputProps={{ 'aria-label': 'retrieve file from swarm' }}
|
||||
value={referenceInput}
|
||||
onChange={handleReferenceChange}
|
||||
<TabsContainer
|
||||
values={[
|
||||
{
|
||||
label: 'download',
|
||||
component: <Download />,
|
||||
},
|
||||
{
|
||||
label: 'upload',
|
||||
component: <Upload />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<IconButton
|
||||
href={`${apiHost}/files/${referenceInput}`}
|
||||
target="_blank"
|
||||
disabled={referenceError !== null || !referenceInput}
|
||||
className={classes.iconButton}
|
||||
aria-label="download"
|
||||
>
|
||||
<Search />
|
||||
</IconButton>
|
||||
</Paper>
|
||||
{referenceError && <FormHelperText error>{referenceError.message}</FormHelperText>}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { ReactElement, useState } from 'react'
|
||||
import { ReactElement, useState, useContext } from 'react'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import {
|
||||
Table,
|
||||
@@ -10,12 +10,11 @@ import {
|
||||
Button,
|
||||
Paper,
|
||||
Tooltip,
|
||||
Container,
|
||||
CircularProgress,
|
||||
} from '@material-ui/core'
|
||||
import { Autorenew } from '@material-ui/icons'
|
||||
|
||||
import { beeDebugApi } from '../../services/bee'
|
||||
import { Context as SettingsContext } from '../../providers/Settings'
|
||||
import type { Peer } from '@ethersphere/bee-js'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
@@ -26,47 +25,43 @@ const useStyles = makeStyles({
|
||||
|
||||
interface Props {
|
||||
peers: Peer[] | null
|
||||
isLoading: boolean
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
interface PeerLatency {
|
||||
rtt: string
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
function getPingState(peerLatency: Record<string, PeerLatency>, peer: Peer): ReactElement {
|
||||
if (peerLatency[peer.address]?.loading) return <CircularProgress size={20} />
|
||||
|
||||
if (peerLatency[peer.address]?.rtt) return <span>{peerLatency[peer.address]?.rtt}</span>
|
||||
|
||||
return <Autorenew />
|
||||
}
|
||||
|
||||
function PeerTable(props: Props): ReactElement {
|
||||
const classes = useStyles()
|
||||
const { beeDebugApi } = useContext(SettingsContext)
|
||||
|
||||
const [peerLatency, setPeerLatency] = useState([{ peerId: '', rtt: '', loading: false }])
|
||||
const [peerLatency, setPeerLatency] = useState<Record<string, PeerLatency>>({})
|
||||
|
||||
const PingPeer = (peerId: string) => {
|
||||
setPeerLatency([...peerLatency, { peerId: peerId, rtt: '', loading: true }])
|
||||
beeDebugApi.connectivity
|
||||
.ping(peerId)
|
||||
const pingPeer = (peerId: string) => {
|
||||
setPeerLatency(prevPeerLatency => ({ ...prevPeerLatency, [peerId]: { rtt: '', loading: true } }))
|
||||
beeDebugApi
|
||||
?.pingPeer(peerId)
|
||||
.then(res => {
|
||||
setPeerLatency([...peerLatency, { peerId: peerId, rtt: res.rtt, loading: false }])
|
||||
setPeerLatency(prevPeerLatency => ({ ...prevPeerLatency, [peerId]: { rtt: res.rtt, loading: false } }))
|
||||
})
|
||||
.catch(() => {
|
||||
setPeerLatency([...peerLatency, { peerId: peerId, rtt: 'error', loading: false }])
|
||||
setPeerLatency(prevPeerLatency => ({ ...prevPeerLatency, [peerId]: { rtt: 'error', loading: false } }))
|
||||
})
|
||||
}
|
||||
|
||||
if (props.isLoading) {
|
||||
return (
|
||||
<Container style={{ textAlign: 'center', padding: '50px' }}>
|
||||
<CircularProgress />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
if (props.error || props.peers === null) {
|
||||
return (
|
||||
<Container style={{ textAlign: 'center', padding: '50px' }}>
|
||||
<p>Failed to load peers</p>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TableContainer component={Paper}>
|
||||
<Table className={classes.table} aria-label="simple table">
|
||||
<Table className={classes.table}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Index</TableCell>
|
||||
@@ -75,7 +70,7 @@ function PeerTable(props: Props): ReactElement {
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{props.peers.map((peer: Peer, idx: number) => (
|
||||
{props.peers?.map((peer: Peer, idx: number) => (
|
||||
<TableRow key={peer.address}>
|
||||
<TableCell component="th" scope="row">
|
||||
{idx + 1}
|
||||
@@ -83,21 +78,8 @@ function PeerTable(props: Props): ReactElement {
|
||||
<TableCell>{peer.address}</TableCell>
|
||||
<TableCell align="right">
|
||||
<Tooltip title="Ping node">
|
||||
<Button color="primary" onClick={() => PingPeer(peer.address)}>
|
||||
{
|
||||
// FIXME: this should be broken up
|
||||
/* eslint-disable no-nested-ternary */
|
||||
peerLatency.find(item => item.peerId === peer.address) ? (
|
||||
peerLatency.filter(item => item.peerId === peer.address)[0].loading ? (
|
||||
<CircularProgress size={20} />
|
||||
) : (
|
||||
peerLatency.filter(item => item.peerId === peer.address)[0].rtt
|
||||
)
|
||||
) : (
|
||||
<Autorenew />
|
||||
)
|
||||
/* eslint-enable no-nested-ternary */
|
||||
}
|
||||
<Button color="primary" onClick={() => pingPeer(peer.address)}>
|
||||
{getPingState(peerLatency, peer)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
|
||||
@@ -1,32 +1,21 @@
|
||||
import { Container, CircularProgress } from '@material-ui/core/'
|
||||
import PeerTable from './PeerTable'
|
||||
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
|
||||
|
||||
import { useApiNodeTopology, useApiNodePeers, useDebugApiHealth } from '../../hooks/apiHooks'
|
||||
import { Context } from '../../providers/Bee'
|
||||
import TopologyStats from '../../components/TopologyStats'
|
||||
import { ReactElement } from 'react'
|
||||
import { ReactElement, useContext } from 'react'
|
||||
|
||||
export default function Peers(): ReactElement {
|
||||
const topology = useApiNodeTopology()
|
||||
const debugHealth = useDebugApiHealth()
|
||||
const peers = useApiNodePeers()
|
||||
const { topology, peers, status } = useContext(Context)
|
||||
|
||||
if (debugHealth.isLoadingNodeHealth) {
|
||||
return (
|
||||
<Container style={{ textAlign: 'center', padding: '50px' }}>
|
||||
<CircularProgress />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
if (debugHealth.error) {
|
||||
if (!status.all) {
|
||||
return <TroubleshootConnectionCard />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TopologyStats {...topology} />
|
||||
<PeerTable {...peers} />
|
||||
<TopologyStats topology={topology} />
|
||||
<PeerTable peers={peers} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 { Context as SettingsContext } from '../../providers/Settings'
|
||||
|
||||
export default function Settings(): ReactElement {
|
||||
const [refreshVisibility, toggleRefreshVisibility] = useState(false)
|
||||
const [host, setHost] = useState('')
|
||||
const [debugHost, setDebugHost] = useState('')
|
||||
const { apiUrl, apiDebugUrl, setApiUrl, setDebugApiUrl } = useContext(SettingsContext)
|
||||
const [host, setHost] = useState(apiUrl)
|
||||
const [debugHost, setDebugHost] = useState(apiDebugUrl)
|
||||
|
||||
const handleNewHostConnection = () => {
|
||||
if (host) {
|
||||
sessionStorage.setItem('api_host', host)
|
||||
const submit = () => {
|
||||
if (host !== apiUrl) setApiUrl(host)
|
||||
|
||||
if (debugHost !== apiDebugUrl) setDebugApiUrl(debugHost)
|
||||
}
|
||||
|
||||
if (debugHost) {
|
||||
sessionStorage.setItem('debug_api_host', debugHost)
|
||||
}
|
||||
|
||||
if (host || debugHost) {
|
||||
toggleRefreshVisibility(!refreshVisibility)
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
const touched = host !== apiUrl || debugHost !== apiDebugUrl
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -34,16 +28,13 @@ export default function Settings(): ReactElement {
|
||||
placeholder="ex: 127.0.0.0.1:1633"
|
||||
helperText="Enter node host override / port"
|
||||
fullWidth
|
||||
defaultValue={
|
||||
sessionStorage.getItem('api_host') ? sessionStorage.getItem('api_host') : process.env.REACT_APP_BEE_HOST
|
||||
}
|
||||
defaultValue={apiUrl}
|
||||
margin="normal"
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
}}
|
||||
onChange={e => {
|
||||
setHost(e.target.value)
|
||||
toggleRefreshVisibility(true)
|
||||
}}
|
||||
variant="filled"
|
||||
/>
|
||||
@@ -55,14 +46,9 @@ export default function Settings(): ReactElement {
|
||||
placeholder="ex: 127.0.0.0.1:1635"
|
||||
helperText="Enter node debug host override / port"
|
||||
fullWidth
|
||||
defaultValue={
|
||||
sessionStorage.getItem('debug_api_host')
|
||||
? sessionStorage.getItem('debug_api_host')
|
||||
: process.env.REACT_APP_BEE_DEBUG_HOST
|
||||
}
|
||||
defaultValue={apiDebugUrl}
|
||||
onChange={e => {
|
||||
setDebugHost(e.target.value)
|
||||
toggleRefreshVisibility(true)
|
||||
}}
|
||||
margin="normal"
|
||||
InputLabelProps={{
|
||||
@@ -71,9 +57,9 @@ export default function Settings(): ReactElement {
|
||||
variant="filled"
|
||||
/>
|
||||
</Paper>
|
||||
{refreshVisibility ? (
|
||||
{touched ? (
|
||||
<div style={{ marginTop: '20px' }}>
|
||||
<Button variant="outlined" color="primary" onClick={() => handleNewHostConnection()}>
|
||||
<Button variant="outlined" color="primary" onClick={submit}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
import React, { ReactElement, useContext } from 'react'
|
||||
import Button from '@material-ui/core/Button'
|
||||
import Dialog from '@material-ui/core/Dialog'
|
||||
import DialogActions from '@material-ui/core/DialogActions'
|
||||
import DialogContent from '@material-ui/core/DialogContent'
|
||||
import DialogContentText from '@material-ui/core/DialogContentText'
|
||||
import CircularProgress from '@material-ui/core/CircularProgress'
|
||||
import DialogTitle from '@material-ui/core/DialogTitle'
|
||||
import BigNumber from 'bignumber.js'
|
||||
import { FormikHelpers, Form, Field, Formik } from 'formik'
|
||||
import { TextField } from 'formik-material-ui'
|
||||
import { Context as SettingsContext } from '../../providers/Settings'
|
||||
import { Context } from '../../providers/Stamps'
|
||||
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
|
||||
import { useSnackbar } from 'notistack'
|
||||
|
||||
interface FormValues {
|
||||
depth?: string
|
||||
amount?: string
|
||||
label?: string
|
||||
}
|
||||
type FormErrors = Partial<FormValues>
|
||||
const initialFormValues: FormValues = {
|
||||
depth: '',
|
||||
amount: '',
|
||||
label: '',
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
wrapper: {
|
||||
margin: theme.spacing(1),
|
||||
position: 'relative',
|
||||
},
|
||||
field: {
|
||||
marginTop: theme.spacing(1),
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
buttonProgress: {
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
marginTop: -12,
|
||||
marginBottom: -12,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
interface Props {
|
||||
label?: string
|
||||
}
|
||||
|
||||
export default function FormDialog({ label }: Props): ReactElement {
|
||||
const classes = useStyles()
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const { refresh } = useContext(Context)
|
||||
const { beeApi } = useContext(SettingsContext)
|
||||
const handleClickOpen = () => setOpen(true)
|
||||
const handleClose = () => setOpen(false)
|
||||
const { enqueueSnackbar } = useSnackbar()
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={initialFormValues}
|
||||
onSubmit={async (values: FormValues, actions: FormikHelpers<FormValues>) => {
|
||||
try {
|
||||
// This is really just a typeguard, the validation pretty much guarantees these will have the right values
|
||||
if (!values.depth || !values.amount) return
|
||||
|
||||
if (!beeApi) return
|
||||
|
||||
const amount = BigInt(values.amount)
|
||||
const depth = Number.parseInt(values.depth)
|
||||
const options = values.label ? { label: values.label } : undefined
|
||||
await beeApi.createPostageBatch(amount.toString(), depth, options)
|
||||
actions.resetForm()
|
||||
await refresh()
|
||||
handleClose()
|
||||
} catch (e) {
|
||||
enqueueSnackbar(`Error: ${e.message}`, { variant: 'error' })
|
||||
actions.setSubmitting(false)
|
||||
}
|
||||
}}
|
||||
validate={(values: FormValues) => {
|
||||
const errors: FormErrors = {}
|
||||
|
||||
// Depth
|
||||
if (!values.depth) errors.depth = 'Required field'
|
||||
else {
|
||||
const depth = new BigNumber(values.depth)
|
||||
|
||||
if (!depth.isInteger()) errors.depth = 'Depth must be an integer'
|
||||
else if (depth.isLessThan(16)) errors.depth = 'Minimal depth is 16'
|
||||
else if (depth.isGreaterThan(255)) errors.depth = 'Depth has to be at most 255'
|
||||
}
|
||||
|
||||
// Amount
|
||||
if (!values.amount) errors.amount = 'Required field'
|
||||
else {
|
||||
const amount = new BigNumber(values.amount)
|
||||
|
||||
if (!amount.isInteger()) errors.amount = 'Amount must be an integer'
|
||||
else if (amount.isLessThanOrEqualTo(0)) errors.amount = 'Amount must be greater than 0'
|
||||
}
|
||||
|
||||
// Label
|
||||
if (values.label && !/^[0-9a-z]*$/i.test(values.label)) errors.label = 'Label must be an alphanumeric string'
|
||||
|
||||
return errors
|
||||
}}
|
||||
>
|
||||
{({ submitForm, isValid, isSubmitting, values }) => (
|
||||
<Form>
|
||||
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
|
||||
{label || 'Buy Postage Stamp'}
|
||||
{isSubmitting && <CircularProgress size={24} className={classes.buttonProgress} />}
|
||||
</Button>
|
||||
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title">
|
||||
<DialogTitle id="form-dialog-title">Purchase new postage stamp</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
Provide the depth, amount and optionally the label of the postage stamp. Please refer to the{' '}
|
||||
<a href="https://docs.ethswarm.org/docs/access-the-swarm/keep-your-data-alive" target="blank">
|
||||
official bee docs
|
||||
</a>{' '}
|
||||
to understand these values.
|
||||
</DialogContentText>
|
||||
<Field
|
||||
component={TextField}
|
||||
required
|
||||
name="depth"
|
||||
autoFocus
|
||||
label="Depth"
|
||||
fullWidth
|
||||
className={classes.field}
|
||||
/>
|
||||
<Field component={TextField} required name="amount" label="Amount" fullWidth className={classes.field} />
|
||||
<Field component={TextField} name="label" label="Label" fullWidth className={classes.field} />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<div className={classes.wrapper}>
|
||||
<Button
|
||||
color="primary"
|
||||
disabled={isSubmitting || !isValid || !values.amount || !values.depth}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
onClick={submitForm}
|
||||
>
|
||||
Create
|
||||
{isSubmitting && <CircularProgress size={24} className={classes.buttonProgress} />}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@material-ui/core'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import type { ReactElement } from 'react'
|
||||
import ClipboardCopy from '../../components/ClipboardCopy'
|
||||
import PeerDetailDrawer from '../../components/PeerDetail'
|
||||
import { EnrichedPostageBatch } from '../../providers/Stamps'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
table: {
|
||||
minWidth: 650,
|
||||
},
|
||||
values: {
|
||||
textAlign: 'right',
|
||||
fontFamily: 'monospace, monospace',
|
||||
},
|
||||
})
|
||||
interface Props {
|
||||
postageStamps: EnrichedPostageBatch[] | null
|
||||
}
|
||||
|
||||
function StampsTable({ postageStamps }: Props): ReactElement | null {
|
||||
if (postageStamps === null) return null
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table className={classes.table} size="small" aria-label="Balances Table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Batch ID</TableCell>
|
||||
<TableCell align="right">Usage</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{postageStamps.map(({ batchID, usageText }) => (
|
||||
<TableRow key={batchID}>
|
||||
<TableCell>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<small>
|
||||
<PeerDetailDrawer peerId={batchID} />
|
||||
</small>
|
||||
<ClipboardCopy value={batchID} />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className={classes.values}>{usageText}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default StampsTable
|
||||
@@ -0,0 +1,70 @@
|
||||
import { ReactElement, useContext, useEffect } from 'react'
|
||||
import { Theme, createStyles, makeStyles } from '@material-ui/core/styles'
|
||||
import { Container, CircularProgress } from '@material-ui/core'
|
||||
|
||||
import StampsTable from './StampsTable'
|
||||
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
|
||||
import CreatePostageStampModal from './CreatePostageStampModal'
|
||||
|
||||
import { Context } from '../../providers/Stamps'
|
||||
import { Context as BeeContext } from '../../providers/Bee'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
width: '100%',
|
||||
display: 'grid',
|
||||
rowGap: theme.spacing(2),
|
||||
},
|
||||
actions: {
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
columnGap: theme.spacing(1),
|
||||
rowGap: theme.spacing(1),
|
||||
flex: '0 1 auto',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
export default function Accounting(): ReactElement {
|
||||
const classes = useStyles()
|
||||
|
||||
const beeContext = useContext(BeeContext)
|
||||
const { stamps, isLoading, error, start, stop } = useContext(Context)
|
||||
useEffect(() => {
|
||||
start()
|
||||
|
||||
return () => stop()
|
||||
}, [])
|
||||
|
||||
if (beeContext.isLoading) {
|
||||
return (
|
||||
<Container style={{ textAlign: 'center', padding: '50px' }}>
|
||||
<CircularProgress />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
if (!beeContext.status.all) return <TroubleshootConnectionCard />
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
{error && (
|
||||
<Container style={{ textAlign: 'center', padding: '50px' }}>
|
||||
Error loading postage stamps details: {error.message}
|
||||
</Container>
|
||||
)}
|
||||
{!error && (
|
||||
<>
|
||||
<div className={classes.actions}>
|
||||
<CreatePostageStampModal />
|
||||
<div style={{ height: '5px' }}>{isLoading && <CircularProgress />}</div>
|
||||
</div>
|
||||
<StampsTable postageStamps={stamps} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ReactElement, useEffect, useState } from 'react'
|
||||
import { ReactElement, useEffect, useState, useContext } from 'react'
|
||||
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
|
||||
import { Typography, Paper, Button, Step, StepLabel, StepContent, Stepper, StepButton } from '@material-ui/core/'
|
||||
import { CheckCircle, Error, Sync, ExpandLessSharp, ExpandMoreSharp, Autorenew } from '@material-ui/icons/'
|
||||
import { CheckCircle, Error, ExpandLessSharp, ExpandMoreSharp, Autorenew } from '@material-ui/icons/'
|
||||
|
||||
import DebugConnectionCheck from './SetupSteps/DebugConnectionCheck'
|
||||
import NodeConnectionCheck from './SetupSteps/NodeConnectionCheck'
|
||||
@@ -9,7 +9,7 @@ import VersionCheck from './SetupSteps/VersionCheck'
|
||||
import EthereumConnectionCheck from './SetupSteps/EthereumConnectionCheck'
|
||||
import ChequebookDeployFund from './SetupSteps/ChequebookDeployFund'
|
||||
import PeerConnection from './SetupSteps/PeerConnection'
|
||||
import { StatusChequebookHook } from '../../hooks/status'
|
||||
import { Context } from '../../providers/Bee'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
@@ -30,66 +30,65 @@ const useStyles = makeStyles((theme: Theme) =>
|
||||
interface Step {
|
||||
label: string
|
||||
isOk: boolean
|
||||
isLoading: boolean
|
||||
component: ReactElement
|
||||
}
|
||||
|
||||
interface Props {
|
||||
nodeVersion: StatusNodeVersionHook
|
||||
ethereumConnection: StatusEthereumConnectionHook
|
||||
debugApiConnection: StatusHookCommon
|
||||
apiConnection: StatusHookCommon
|
||||
topology: StatusTopologyHook
|
||||
chequebook: StatusChequebookHook
|
||||
}
|
||||
|
||||
export default function NodeSetupWorkflow({
|
||||
nodeVersion,
|
||||
ethereumConnection,
|
||||
debugApiConnection,
|
||||
apiConnection,
|
||||
topology,
|
||||
chequebook,
|
||||
}: Props): ReactElement {
|
||||
export default function NodeSetupWorkflow(): ReactElement {
|
||||
const classes = useStyles()
|
||||
const [activeStep, setActiveStep] = useState(-1)
|
||||
|
||||
const {
|
||||
status,
|
||||
isLoading,
|
||||
latestUserVersion,
|
||||
latestPublishedVersion,
|
||||
isLatestBeeVersion,
|
||||
latestBeeVersionUrl,
|
||||
topology,
|
||||
nodeAddresses,
|
||||
chequebookAddress,
|
||||
} = useContext(Context)
|
||||
|
||||
const steps: Step[] = [
|
||||
{
|
||||
label: 'Connected to Node DebugAPI',
|
||||
isOk: debugApiConnection.isOk,
|
||||
isLoading: debugApiConnection.isLoading,
|
||||
component: <DebugConnectionCheck {...debugApiConnection} />,
|
||||
isOk: status.debugApiConnection,
|
||||
component: <DebugConnectionCheck isOk={status.debugApiConnection} />,
|
||||
},
|
||||
{
|
||||
label: 'Running latest Bee version',
|
||||
isOk: nodeVersion.isOk,
|
||||
isLoading: nodeVersion.isLoading,
|
||||
component: <VersionCheck {...nodeVersion} />,
|
||||
isOk: status.version,
|
||||
component: (
|
||||
<VersionCheck
|
||||
isOk={status.version}
|
||||
isLatestBeeVersion={isLatestBeeVersion}
|
||||
userVersion={latestUserVersion}
|
||||
latestVersion={latestPublishedVersion}
|
||||
latestUrl={latestBeeVersionUrl}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Connected to Ethereum Blockchain',
|
||||
isOk: ethereumConnection.isOk,
|
||||
isLoading: ethereumConnection.isLoading,
|
||||
component: <EthereumConnectionCheck {...ethereumConnection} />,
|
||||
label: 'Connected to xDai Blockchain',
|
||||
isOk: status.blockchainConnection,
|
||||
component: <EthereumConnectionCheck isOk={status.blockchainConnection} nodeAddresses={nodeAddresses} />,
|
||||
},
|
||||
{
|
||||
label: 'Deployed and Funded Chequebook',
|
||||
isOk: chequebook.isOk,
|
||||
isLoading: chequebook.isLoading,
|
||||
component: <ChequebookDeployFund ethereumAddress={ethereumConnection.nodeAddresses?.ethereum} {...chequebook} />,
|
||||
isOk: status.chequebook,
|
||||
component: (
|
||||
<ChequebookDeployFund chequebookAddress={chequebookAddress?.chequebookAddress} isOk={status.chequebook} />
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Connected to Node API',
|
||||
isOk: apiConnection.isOk,
|
||||
isLoading: apiConnection.isLoading,
|
||||
component: <NodeConnectionCheck {...apiConnection} />,
|
||||
isOk: status.apiConnection,
|
||||
component: <NodeConnectionCheck isOk={status.apiConnection} />,
|
||||
},
|
||||
{
|
||||
label: 'Connected to Peers',
|
||||
isOk: topology.isOk,
|
||||
isLoading: topology.isLoading,
|
||||
component: <PeerConnection {...topology} />,
|
||||
isOk: status.topology,
|
||||
component: <PeerConnection isOk={status.topology} topology={topology} />,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -98,7 +97,7 @@ export default function NodeSetupWorkflow({
|
||||
if (activeStep >= 0 && activeStep < steps.length) return
|
||||
|
||||
// If any step is not fully loaded yet return
|
||||
if (!steps.every(step => !step.isLoading)) return
|
||||
if (!isLoading) return
|
||||
|
||||
// Select first step that is not OK
|
||||
// This is deliberately a for loop (and not forEach) so that we can terminate the useEffect from within the cycle
|
||||
@@ -123,18 +122,11 @@ export default function NodeSetupWorkflow({
|
||||
<Paper className={classes.root}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Node Setup
|
||||
<span style={{ marginLeft: '25px' }}>
|
||||
<Button variant="outlined" size="small" onClick={() => window.location.reload()}>
|
||||
<Sync />
|
||||
<span style={{ marginLeft: '7px' }}>Refresh Checks</span>
|
||||
</Button>
|
||||
</span>
|
||||
</Typography>
|
||||
<Stepper nonLinear activeStep={activeStep} orientation="vertical">
|
||||
{steps.map(({ label, isOk, component, isLoading }, index) => (
|
||||
{steps.map(({ label, isOk, component }, index) => (
|
||||
<Step key={label}>
|
||||
<StepLabel
|
||||
disabled={isLoading}
|
||||
onClick={() => setActiveStep(index === activeStep ? steps.length : index)}
|
||||
StepIconComponent={() => {
|
||||
if (isLoading) return <Autorenew style={{ height: '25px', cursor: 'pointer' }} />
|
||||
|
||||
@@ -2,33 +2,25 @@ import { Typography } from '@material-ui/core/'
|
||||
import EthereumAddress from '../../../components/EthereumAddress'
|
||||
import DepositModal from '../../../containers/DepositModal'
|
||||
import type { ReactElement } from 'react'
|
||||
import type { StatusChequebookHook } from '../../../hooks/status'
|
||||
|
||||
interface Props extends StatusChequebookHook {
|
||||
ethereumAddress?: string
|
||||
interface Props extends StatusHookCommon {
|
||||
chequebookAddress?: string
|
||||
}
|
||||
|
||||
const ChequebookDeployFund = ({
|
||||
isLoading,
|
||||
chequebookAddress,
|
||||
chequebookBalance,
|
||||
ethereumAddress,
|
||||
}: Props): ReactElement | null => {
|
||||
if (isLoading) return null
|
||||
|
||||
const ChequebookDeployFund = ({ chequebookAddress, isOk }: Props): ReactElement | null => {
|
||||
return (
|
||||
<div>
|
||||
<p style={{ marginBottom: '20px', display: 'flex' }}>
|
||||
{chequebookAddress?.chequebookAddress && <DepositModal />}
|
||||
</p>
|
||||
<p style={{ marginBottom: '20px', display: 'flex' }}>{chequebookAddress && <DepositModal />}</p>
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
{!(chequebookAddress?.chequebookAddress && chequebookBalance?.totalBalance.toBigNumber.isGreaterThan(0)) && (
|
||||
{!isOk && (
|
||||
<div>
|
||||
<span>
|
||||
Your chequebook is either not deployed or funded. Join{' '}
|
||||
<a href="https://discord.gg/ykCupZMuww">our discord channel</a>, get verified and send a message{' '}
|
||||
<pre>sprinkle {ethereumAddress || '<YOUR BEE NODE ETH ADDRESS>'}</pre> in the <pre>#faucet-request</pre>{' '}
|
||||
channel to get Goerli ETH and Goerli BZZ token.
|
||||
Your chequebook is either not deployed or funded. To run the node you will need xDAI and xBZZ on the xDai
|
||||
network. You may need to aquire BZZ through (e.g. <a href="https://bzz.exchange/">bzz.exchange</a>) and
|
||||
bridge it to the xDai network through the <a href="https://omni.xdaichain.com/bridge">omni bridge</a>. To
|
||||
pay the transaction fees, you will also need xDAI token. You can purchase DAI on the network and bridge it
|
||||
to xDai network through the <a href="https://bridge.xdaichain.com/">xDai Bridge</a>. See the{' '}
|
||||
<a href="https://www.xdaichain.com/#xdai-stable-chain">official xDai website</a> for more information.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -36,7 +28,7 @@ const ChequebookDeployFund = ({
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
Chequebook Address
|
||||
</Typography>
|
||||
<EthereumAddress address={chequebookAddress?.chequebookAddress} network={'goerli'} />
|
||||
<EthereumAddress address={chequebookAddress} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import { ReactElement, useContext } from 'react'
|
||||
import { Typography, Accordion, AccordionSummary, AccordionDetails } from '@material-ui/core/'
|
||||
import MuiAlert from '@material-ui/lab/Alert'
|
||||
import { ExpandMoreSharp } from '@material-ui/icons/'
|
||||
|
||||
import ConnectToHost from '../../../components/ConnectToHost'
|
||||
import CodeBlockTabs from '../../../components/CodeBlockTabs'
|
||||
import { debugApiHost } from '../../../constants'
|
||||
import { Context as SettingsContext } from '../../../providers/Settings'
|
||||
|
||||
type Props = StatusHookCommon
|
||||
|
||||
export default function NodeConnectionCheck({ isLoading, isOk }: Props): ReactElement | null {
|
||||
if (isLoading) return null
|
||||
export default function NodeConnectionCheck({ isOk }: Props): ReactElement | null {
|
||||
const { setDebugApiUrl, apiDebugUrl } = useContext(SettingsContext)
|
||||
|
||||
const changeDebugApiUrl = (
|
||||
<div style={{ display: 'flex', marginTop: '25px', marginBottom: '25px' }}>
|
||||
<span style={{ marginRight: '15px' }}>
|
||||
Debug API (<Typography variant="button">{debugApiHost}</Typography>)
|
||||
Debug API (<Typography variant="button">{apiDebugUrl}</Typography>)
|
||||
</span>
|
||||
<ConnectToHost hostName={'debug_api_host'} defaultHost={debugApiHost} />
|
||||
<ConnectToHost
|
||||
setHost={(host: string) => {
|
||||
console.log(host) // eslint-disable-line
|
||||
setDebugApiUrl(host)
|
||||
}}
|
||||
defaultHost={apiDebugUrl}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -31,7 +37,7 @@ export default function NodeConnectionCheck({ isLoading, isOk }: Props): ReactEl
|
||||
|
||||
<div>
|
||||
<Typography component="div" variant="body2" gutterBottom style={{ margin: '15px' }}>
|
||||
We cannot connect to your nodes debug API at <Typography variant="button">{debugApiHost}</Typography>. Please
|
||||
We cannot connect to your nodes debug API at <Typography variant="button">{apiDebugUrl}</Typography>. Please
|
||||
check the following to troubleshoot your issue.
|
||||
<Accordion style={{ marginTop: '20px' }}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreSharp />} aria-controls="panel1a-content" id="panel1a-header">
|
||||
@@ -41,11 +47,7 @@ export default function NodeConnectionCheck({ isLoading, isOk }: Props): ReactEl
|
||||
<Typography component="div">
|
||||
<ol>
|
||||
<li>Check the status of your node by running the below command to see if your node is running.</li>
|
||||
<CodeBlockTabs
|
||||
showLineNumbers
|
||||
linux={`sudo systemctl status bee`}
|
||||
mac={`brew services status swarm-bee`}
|
||||
/>
|
||||
<CodeBlockTabs showLineNumbers linux={`sudo systemctl status bee`} mac={`brew services list`} />
|
||||
<li>
|
||||
If your node is running, check your firewall settings to make sure that port 1635 (or your custom
|
||||
specified port) is bound to localhost. If your node is not running try executing the below command
|
||||
@@ -71,19 +73,20 @@ export default function NodeConnectionCheck({ isLoading, isOk }: Props): ReactEl
|
||||
<CodeBlockTabs
|
||||
showLineNumbers
|
||||
linux={`sudo systemctl status bee \njournalctl --lines=100 --follow --unit bee`}
|
||||
mac={`brew services status swarm-bee \ntail -f /usr/local/var/log/swarm-bee/bee.log`}
|
||||
mac={`brew services list \ntail -f /usr/local/var/log/swarm-bee/bee.log`}
|
||||
/>
|
||||
<li>
|
||||
Lastly, check your nodes configuration settings to validate the debug API is enabled and the Cross
|
||||
Origin Resource Sharing (CORS) setting is configured to allow your host. Config parameter{' '}
|
||||
<strong>debug-api-enable</strong> must be set to <strong>true</strong> and{' '}
|
||||
<strong>cors-allowed-origins</strong> must be set to your host domain or IP. If edits are made to
|
||||
the configuration run the restart command below for changes to take effect.
|
||||
<strong>cors-allowed-origins</strong> must be set to your host domain or IP (you can also use the
|
||||
wildcard <code>{"cors-allowed-origins: ['*']"}</code>). If edits are made to the configuration run
|
||||
the restart command below for changes to take effect.
|
||||
</li>
|
||||
<CodeBlockTabs
|
||||
showLineNumbers
|
||||
linux={`sudo vi /etc/bee/bee.yaml\nsudo systemctl restart bee`}
|
||||
mac={`sudo vi /etc/bee/bee.yaml \nbrew services restart swarm-bee`}
|
||||
mac={`sudo vi /usr/local/etc/swarm-bee/bee.yaml \nbrew services restart swarm-bee`}
|
||||
/>
|
||||
</ol>
|
||||
</Typography>
|
||||
|
||||
@@ -4,37 +4,31 @@ import EthereumAddress from '../../../components/EthereumAddress'
|
||||
|
||||
type Props = StatusEthereumConnectionHook
|
||||
|
||||
export default function EthereumConnectionCheck({ isLoading, isOk, nodeAddresses }: Props): ReactElement | null {
|
||||
if (isLoading) return null
|
||||
|
||||
export default function EthereumConnectionCheck({ isOk, nodeAddresses }: Props): ReactElement | null {
|
||||
if (isOk) {
|
||||
return (
|
||||
<div>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
Node Address
|
||||
</Typography>
|
||||
<EthereumAddress address={nodeAddresses?.ethereum} network={'goerli'} />
|
||||
<EthereumAddress address={nodeAddresses?.ethereum} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<p>
|
||||
Your Bee node must have access to the Ethereum blockchain, so that it can interact and deploy your chequebook
|
||||
Your Bee node must have access to the xDai blockchain, so that it can interact and deploy your chequebook
|
||||
contract. You can run{' '}
|
||||
<a href="https://github.com/goerli/testnet" rel="noreferrer" target="_blank">
|
||||
your own Goerli node
|
||||
<a href="https://www.xdaichain.com/" rel="noreferrer" target="_blank">
|
||||
your own xDai node
|
||||
</a>
|
||||
, or use a provider such as{' '}
|
||||
<a href="https://rpc.slock.it/goerli" rel="noreferrer" target="_blank">
|
||||
rpc.slock.it/goerli
|
||||
</a>{' '}
|
||||
or{' '}
|
||||
<a href="https://infura.io/" rel="noreferrer" target="_blank">
|
||||
Infura
|
||||
, or use a provider instead - we recommend{' '}
|
||||
<a href="https://getblock.io/" rel="noreferrer" target="_blank">
|
||||
Getblock
|
||||
</a>
|
||||
. By default, Bee expects a local Goerli node at http://localhost:8545. To use a provider instead, simply change
|
||||
your <strong>--swap-endpoint</strong> in your configuration file.
|
||||
. By default, Bee expects a local node at http://localhost:8545. To use a provider instead, simply change the{' '}
|
||||
<strong>swap-endpoint</strong> in your configuration file.
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import { ReactElement, useContext } from 'react'
|
||||
import { Typography, Accordion, AccordionSummary, AccordionDetails } from '@material-ui/core/'
|
||||
import { ExpandMoreSharp } from '@material-ui/icons/'
|
||||
|
||||
import ConnectToHost from '../../../components/ConnectToHost'
|
||||
import CodeBlockTabs from '../../../components/CodeBlockTabs'
|
||||
import { apiHost } from '../../../constants'
|
||||
import { Context as SettingsContext } from '../../../providers/Settings'
|
||||
|
||||
type Props = StatusHookCommon
|
||||
|
||||
export default function NodeConnectionCheck({ isLoading, isOk }: Props): ReactElement | null {
|
||||
if (isLoading) return null
|
||||
export default function NodeConnectionCheck({ isOk }: Props): ReactElement | null {
|
||||
const { setApiUrl, apiUrl } = useContext(SettingsContext)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', marginBottom: '25px' }}>
|
||||
<span style={{ marginRight: '15px' }}>
|
||||
Node API (<Typography variant="button">{apiHost}</Typography>)
|
||||
Node API (<Typography variant="button">{apiUrl}</Typography>)
|
||||
</span>
|
||||
<ConnectToHost hostName="api_host" defaultHost={apiHost} />
|
||||
<ConnectToHost setHost={setApiUrl} defaultHost={apiUrl} />
|
||||
</div>
|
||||
<div>
|
||||
{!isOk && (
|
||||
<Typography component="div" variant="body2" gutterBottom style={{ margin: '15px' }}>
|
||||
We cannot connect to your nodes API at <Typography variant="button">{apiHost}</Typography>. Please check the
|
||||
We cannot connect to your nodes API at <Typography variant="button">{apiUrl}</Typography>. Please check the
|
||||
following to troubleshoot your issue.
|
||||
<Accordion style={{ marginTop: '20px' }}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreSharp />} aria-controls="panel1a-content" id="panel1a-header">
|
||||
@@ -32,11 +32,7 @@ export default function NodeConnectionCheck({ isLoading, isOk }: Props): ReactEl
|
||||
<Typography component="div">
|
||||
<ol>
|
||||
<li>Check the status of your node by running the below command to see if your node is running.</li>
|
||||
<CodeBlockTabs
|
||||
showLineNumbers
|
||||
linux={`sudo systemctl status bee`}
|
||||
mac={`brew services status swarm-bee`}
|
||||
/>
|
||||
<CodeBlockTabs showLineNumbers linux={`sudo systemctl status bee`} mac={`brew services list`} />
|
||||
<li>
|
||||
If your node is running, check your firewall settings to make sure that port 1633 (or your custom
|
||||
specified port) is exposed to the internet. If your node is not running try executing the below
|
||||
@@ -51,7 +47,7 @@ export default function NodeConnectionCheck({ isLoading, isOk }: Props): ReactEl
|
||||
<CodeBlockTabs
|
||||
showLineNumbers
|
||||
linux={`sudo systemctl status bee \njournalctl --lines=100 --follow --unit bee`}
|
||||
mac={`brew services status swarm-bee \ntail -f /usr/local/var/log/swarm-bee/bee.log`}
|
||||
mac={`brew services list \ntail -f /usr/local/var/log/swarm-bee/bee.log`}
|
||||
/>
|
||||
</ol>
|
||||
</Typography>
|
||||
|
||||
@@ -3,9 +3,7 @@ import { Typography } from '@material-ui/core/'
|
||||
|
||||
type Props = StatusTopologyHook
|
||||
|
||||
export default function PeerConnection({ isLoading, isOk, topology }: Props): ReactElement | null {
|
||||
if (isLoading) return null
|
||||
|
||||
export default function PeerConnection({ isOk, topology }: Props): ReactElement | null {
|
||||
const peers = (
|
||||
<div style={{ display: 'flex', marginTop: '15px' }}>
|
||||
<div style={{ marginRight: '30px' }}>
|
||||
|
||||
@@ -4,15 +4,7 @@ import CodeBlockTabs from '../../../components/CodeBlockTabs'
|
||||
|
||||
type Props = StatusNodeVersionHook
|
||||
|
||||
export default function VersionCheck({
|
||||
isLoading,
|
||||
isOk,
|
||||
userVersion,
|
||||
latestVersion,
|
||||
latestUrl,
|
||||
}: Props): ReactElement | null {
|
||||
if (isLoading) return null
|
||||
|
||||
export default function VersionCheck({ isOk, userVersion, latestVersion, latestUrl }: Props): ReactElement | null {
|
||||
const version = (
|
||||
<div style={{ display: 'flex' }}>
|
||||
<div style={{ marginRight: '30px' }}>
|
||||
|
||||
@@ -23,17 +23,19 @@ const useStyles = makeStyles(() =>
|
||||
interface Props {
|
||||
nodeAddresses: NodeAddresses | null
|
||||
nodeTopology: Topology | null
|
||||
userBeeVersion: string | null
|
||||
latestBeeVersion: string | null
|
||||
userBeeVersion?: string
|
||||
isLatestBeeVersion: boolean
|
||||
isOk: boolean
|
||||
latestUrl: string
|
||||
}
|
||||
|
||||
function StatusCard({
|
||||
userBeeVersion,
|
||||
nodeAddresses,
|
||||
nodeTopology,
|
||||
latestBeeVersion,
|
||||
isOk,
|
||||
isLatestBeeVersion,
|
||||
latestUrl,
|
||||
}: Props): ReactElement | null {
|
||||
const classes = useStyles()
|
||||
|
||||
@@ -72,7 +74,7 @@ function StatusCard({
|
||||
Bee
|
||||
</a>{' '}
|
||||
<span>{userBeeVersion || '-'}</span>
|
||||
{userBeeVersion && latestBeeVersion && userBeeVersion === latestBeeVersion ? (
|
||||
{isLatestBeeVersion ? (
|
||||
<Chip
|
||||
style={{ marginLeft: '7px', color: '#2145a0' }}
|
||||
size="small"
|
||||
@@ -80,7 +82,9 @@ function StatusCard({
|
||||
className={classes.status}
|
||||
/>
|
||||
) : (
|
||||
<Typography variant="button">update</Typography>
|
||||
<Button size="small" variant="outlined" href={latestUrl}>
|
||||
update
|
||||
</Button>
|
||||
)}
|
||||
</Typography>
|
||||
<Typography component="div" variant="subtitle2" gutterBottom>
|
||||
|
||||
+20
-47
@@ -1,18 +1,10 @@
|
||||
import { ReactElement } from 'react'
|
||||
import { Container, CircularProgress } from '@material-ui/core'
|
||||
import { ReactElement, useContext } from 'react'
|
||||
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
|
||||
|
||||
import NodeSetupWorkflow from './NodeSetupWorkflow'
|
||||
import StatusCard from './StatusCard'
|
||||
import EthereumAddressCard from '../../components/EthereumAddressCard'
|
||||
import {
|
||||
useStatusEthereumConnection,
|
||||
useStatusNodeVersion,
|
||||
useStatusDebugConnection,
|
||||
useStatusConnection,
|
||||
useStatusTopology,
|
||||
useStatusChequebook,
|
||||
} from '../../hooks/status'
|
||||
import { Context as BeeContext } from '../../providers/Bee'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
@@ -27,49 +19,30 @@ const useStyles = makeStyles((theme: Theme) =>
|
||||
export default function Status(): ReactElement {
|
||||
const classes = useStyles()
|
||||
|
||||
const nodeVersion = useStatusNodeVersion()
|
||||
const ethereumConnection = useStatusEthereumConnection()
|
||||
const debugApiConnection = useStatusDebugConnection()
|
||||
const apiConnection = useStatusConnection()
|
||||
const topology = useStatusTopology()
|
||||
const chequebook = useStatusChequebook()
|
||||
|
||||
const checks = [nodeVersion, ethereumConnection, debugApiConnection, apiConnection, topology, chequebook]
|
||||
|
||||
// If any check data are still loading
|
||||
if (!checks.every(c => !c.isLoading)) {
|
||||
return (
|
||||
<Container style={{ textAlign: 'center', padding: '50px' }}>
|
||||
<CircularProgress />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
const {
|
||||
status,
|
||||
latestUserVersion,
|
||||
isLatestBeeVersion,
|
||||
latestBeeVersionUrl,
|
||||
topology,
|
||||
nodeAddresses,
|
||||
chequebookAddress,
|
||||
} = useContext(BeeContext)
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<StatusCard
|
||||
userBeeVersion={nodeVersion.userVersion}
|
||||
latestBeeVersion={nodeVersion.latestVersion}
|
||||
isOk={checks.every(c => c.isOk)}
|
||||
nodeTopology={topology.topology}
|
||||
nodeAddresses={ethereumConnection.nodeAddresses}
|
||||
/>
|
||||
{ethereumConnection.nodeAddresses && chequebook.chequebookAddress && (
|
||||
<EthereumAddressCard
|
||||
nodeAddresses={ethereumConnection.nodeAddresses}
|
||||
isLoadingNodeAddresses={ethereumConnection.isLoading}
|
||||
chequebookAddress={chequebook.chequebookAddress}
|
||||
isLoadingChequebookAddress={chequebook.isLoading}
|
||||
userBeeVersion={latestUserVersion}
|
||||
isLatestBeeVersion={isLatestBeeVersion}
|
||||
isOk={status.all}
|
||||
nodeTopology={topology}
|
||||
latestUrl={latestBeeVersionUrl}
|
||||
nodeAddresses={nodeAddresses}
|
||||
/>
|
||||
{nodeAddresses && chequebookAddress && (
|
||||
<EthereumAddressCard nodeAddresses={nodeAddresses} chequebookAddress={chequebookAddress} />
|
||||
)}
|
||||
<NodeSetupWorkflow
|
||||
nodeVersion={nodeVersion}
|
||||
ethereumConnection={ethereumConnection}
|
||||
debugApiConnection={debugApiConnection}
|
||||
apiConnection={apiConnection}
|
||||
topology={topology}
|
||||
chequebook={chequebook}
|
||||
/>
|
||||
<NodeSetupWorkflow />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { PostageBatch } from '@ethersphere/bee-js'
|
||||
import { createContext, ReactChild, ReactElement, useEffect, useState, useContext } from 'react'
|
||||
import { Context as SettingsContext } from './Settings'
|
||||
|
||||
export interface EnrichedPostageBatch extends PostageBatch {
|
||||
usage: number
|
||||
usageText: string
|
||||
}
|
||||
|
||||
interface ContextInterface {
|
||||
stamps: EnrichedPostageBatch[] | null
|
||||
error: Error | null
|
||||
isLoading: boolean
|
||||
lastUpdate: number | null
|
||||
start: (frequency?: number) => void
|
||||
stop: () => void
|
||||
refresh: () => Promise<void>
|
||||
}
|
||||
|
||||
const initialValues: ContextInterface = {
|
||||
stamps: null,
|
||||
error: null,
|
||||
isLoading: 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 enrichStamp(postageBatch: PostageBatch): EnrichedPostageBatch {
|
||||
const { depth, bucketDepth, utilization } = postageBatch
|
||||
|
||||
const usage = utilization / Math.pow(2, depth - bucketDepth)
|
||||
const usageText = `${Math.ceil(usage * 100)}%`
|
||||
|
||||
return {
|
||||
...postageBatch,
|
||||
usage,
|
||||
usageText,
|
||||
}
|
||||
}
|
||||
|
||||
export function Provider({ children }: Props): ReactElement {
|
||||
const { beeApi } = useContext(SettingsContext)
|
||||
const [stamps, setStamps] = useState<EnrichedPostageBatch[] | null>(initialValues.stamps)
|
||||
const [error, setError] = useState<Error | null>(initialValues.error)
|
||||
const [isLoading, setIsLoading] = useState<boolean>(initialValues.isLoading)
|
||||
const [lastUpdate, setLastUpdate] = useState<number | null>(initialValues.lastUpdate)
|
||||
const [frequency, setFrequency] = useState<number | null>(null)
|
||||
|
||||
const refresh = async () => {
|
||||
// Don't want to refresh when already refreshing
|
||||
if (isLoading) return
|
||||
|
||||
if (!beeApi) return
|
||||
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const stamps = await beeApi.getAllPostageBatch()
|
||||
|
||||
setStamps(stamps.map(enrichStamp))
|
||||
setLastUpdate(Date.now())
|
||||
} catch (e) {
|
||||
setError(e)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
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])
|
||||
|
||||
return (
|
||||
<Context.Provider value={{ stamps, error, isLoading, lastUpdate, start, stop, refresh }}>
|
||||
{children}
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
Vendored
+3
-3
@@ -6,14 +6,14 @@ interface LatestBeeRelease {
|
||||
}
|
||||
|
||||
interface StatusHookCommon {
|
||||
isLoading: boolean
|
||||
isOk: boolean
|
||||
}
|
||||
|
||||
interface StatusNodeVersionHook extends StatusHookCommon {
|
||||
userVersion: string
|
||||
latestVersion: string
|
||||
userVersion?: string
|
||||
latestVersion?: string
|
||||
latestUrl: string
|
||||
isLatestBeeVersion: boolean
|
||||
}
|
||||
interface StatusEthereumConnectionHook extends StatusHookCommon {
|
||||
nodeAddresses: NodeAddresses | null
|
||||
|
||||
@@ -7,11 +7,12 @@ import AppRoute from './AppRoute'
|
||||
import Dashboard from '../layout/Dashboard'
|
||||
|
||||
// pages
|
||||
import Status from '../pages/status/index'
|
||||
import Files from '../pages/files/index'
|
||||
import Peers from '../pages/peers/index'
|
||||
import Accounting from '../pages/accounting/index'
|
||||
import Settings from '../pages/settings/index'
|
||||
import Status from '../pages/status'
|
||||
import Files from '../pages/files'
|
||||
import Peers from '../pages/peers'
|
||||
import Accounting from '../pages/accounting'
|
||||
import Settings from '../pages/settings'
|
||||
import Stamps from '../pages/stamps'
|
||||
|
||||
const BaseRouter = (): ReactElement => (
|
||||
<Switch>
|
||||
@@ -20,6 +21,7 @@ const BaseRouter = (): ReactElement => (
|
||||
<AppRoute exact path="/peers/" layout={Dashboard} component={Peers} />
|
||||
<AppRoute exact path="/accounting/" layout={Dashboard} component={Accounting} />
|
||||
<AppRoute exact path="/settings/" layout={Dashboard} component={Settings} />
|
||||
<AppRoute exact path="/stamps/" layout={Dashboard} component={Stamps} />
|
||||
</Switch>
|
||||
)
|
||||
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import {
|
||||
Address,
|
||||
AllSettlements,
|
||||
BalanceResponse,
|
||||
Bee,
|
||||
BeeDebug,
|
||||
CashoutResponse,
|
||||
ChequebookAddressResponse,
|
||||
ChequebookBalanceResponse,
|
||||
Data,
|
||||
DepositTokensResponse,
|
||||
FileData,
|
||||
Health,
|
||||
LastCashoutActionResponse,
|
||||
LastChequesForPeerResponse,
|
||||
LastChequesResponse,
|
||||
NodeAddresses,
|
||||
Peer,
|
||||
PingResponse,
|
||||
Reference,
|
||||
Topology,
|
||||
WithdrawTokensResponse,
|
||||
} 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)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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<CashoutResponse> {
|
||||
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<WithdrawTokensResponse> {
|
||||
return beeJSDebugClient().withdrawTokens(amount)
|
||||
},
|
||||
deposit(amount: bigint): Promise<DepositTokensResponse> {
|
||||
return beeJSDebugClient().depositTokens(amount)
|
||||
},
|
||||
},
|
||||
settlements: {
|
||||
getSettlements(): Promise<AllSettlements> {
|
||||
return beeJSDebugClient().getAllSettlements()
|
||||
},
|
||||
},
|
||||
}
|
||||
+60
-2
@@ -1,4 +1,5 @@
|
||||
import { createMuiTheme } from '@material-ui/core/styles'
|
||||
import { createMuiTheme, Theme } from '@material-ui/core/styles'
|
||||
import { orange } from '@material-ui/core/colors'
|
||||
|
||||
declare module '@material-ui/core/styles/createPalette' {
|
||||
interface TypeBackground {
|
||||
@@ -6,6 +7,54 @@ declare module '@material-ui/core/styles/createPalette' {
|
||||
}
|
||||
}
|
||||
|
||||
// Overwriting default components styles
|
||||
const componentsOverrides = (theme: Theme) => ({
|
||||
MuiTab: {
|
||||
root: {
|
||||
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: theme.palette.secondary,
|
||||
opacity: 1,
|
||||
},
|
||||
'&$selected': {
|
||||
color: theme.palette.secondary,
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
},
|
||||
'&:focus': {
|
||||
color: theme.palette.secondary,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTabs: {
|
||||
root: {
|
||||
borderBottom: 'none',
|
||||
},
|
||||
indicator: {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const propsOverrides = {
|
||||
MuiTab: {
|
||||
disableRipple: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const lightTheme = createMuiTheme({
|
||||
palette: {
|
||||
type: 'light',
|
||||
@@ -13,7 +62,9 @@ export const lightTheme = createMuiTheme({
|
||||
default: '#fafafa',
|
||||
},
|
||||
primary: {
|
||||
main: '#6a6a6a',
|
||||
light: orange.A200,
|
||||
main: '#dd7700',
|
||||
dark: orange[800],
|
||||
},
|
||||
secondary: {
|
||||
main: '#333333',
|
||||
@@ -32,7 +83,9 @@ export const darkTheme = createMuiTheme({
|
||||
paper: '#161b22',
|
||||
},
|
||||
primary: {
|
||||
light: orange.A200,
|
||||
main: '#dd7700',
|
||||
dark: orange[800],
|
||||
},
|
||||
secondary: {
|
||||
main: '#1f2937',
|
||||
@@ -42,3 +95,8 @@ export const darkTheme = createMuiTheme({
|
||||
fontFamily: ['Work Sans', 'Montserrat', 'Nunito', 'Roboto', '"Helvetica Neue"', 'Arial', 'sans-serif'].join(','),
|
||||
},
|
||||
})
|
||||
|
||||
darkTheme.overrides = componentsOverrides(darkTheme)
|
||||
darkTheme.props = propsOverrides
|
||||
lightTheme.overrides = componentsOverrides(lightTheme)
|
||||
lightTheme.props = propsOverrides
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
@@ -32,3 +32,77 @@ export function makeBigNumber(value: BigNumber | BigInt | number | string): BigN
|
||||
|
||||
throw new TypeError(`Not a BigNumber or BigNumber convertible value. Type: ${typeof value} value: ${value}`)
|
||||
}
|
||||
|
||||
export type PromiseSettlements<T> = {
|
||||
fulfilled: PromiseFulfilledResult<T>[]
|
||||
rejected: PromiseRejectedResult[]
|
||||
}
|
||||
|
||||
export type UnwrappedPromiseSettlements<T> = {
|
||||
fulfilled: T[]
|
||||
rejected: string[]
|
||||
}
|
||||
|
||||
export async function sleepMs(ms: number): Promise<void> {
|
||||
await new Promise<void>(resolve =>
|
||||
setTimeout(() => {
|
||||
resolve()
|
||||
}, ms),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the returned results of `Promise.allSettled` to an object
|
||||
* with `fulfilled` and `rejected` arrays for easy access.
|
||||
*
|
||||
* The results still need to be unwrapped to get the fulfilled values or rejection reasons.
|
||||
*/
|
||||
export function mapPromiseSettlements<T>(promises: PromiseSettledResult<T>[]): PromiseSettlements<T> {
|
||||
const fulfilled = promises.filter(promise => promise.status === 'fulfilled') as PromiseFulfilledResult<T>[]
|
||||
const rejected = promises.filter(promise => promise.status === 'rejected') as PromiseRejectedResult[]
|
||||
|
||||
return { fulfilled, rejected }
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the returned values of `Promise.allSettled` to an object
|
||||
* with `fulfilled` and `rejected` arrays for easy access.
|
||||
*
|
||||
* For rejected promises, the value is the stringified `reason`,
|
||||
* or `'Unknown error'` string when it is unavailable.
|
||||
*/
|
||||
export function unwrapPromiseSettlements<T>(
|
||||
promiseSettledResults: PromiseSettledResult<T>[],
|
||||
): UnwrappedPromiseSettlements<T> {
|
||||
const values = mapPromiseSettlements(promiseSettledResults)
|
||||
const fulfilled = values.fulfilled.map(x => x.value)
|
||||
const rejected = values.rejected.map(x => (x.reason ? String(x.reason) : 'Unknown error'))
|
||||
|
||||
return { fulfilled, rejected }
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a `Promise<T>` or async function inside a new `Promise<T>`,
|
||||
* which retries the original function up to `maxRetries` times,
|
||||
* waiting `delayMs` milliseconds between failed attempts.
|
||||
*
|
||||
* If all attempts fail, then this `Promise<T>` also rejects.
|
||||
*/
|
||||
export function makeRetriablePromise<T>(fn: () => Promise<T>, maxRetries = 3, delayMs = 1000): Promise<T> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
for (let tries = 0; tries < maxRetries; tries++) {
|
||||
try {
|
||||
const results = await fn()
|
||||
resolve(results)
|
||||
|
||||
return
|
||||
} catch (error) {
|
||||
if (tries < maxRetries - 1) {
|
||||
await sleepMs(delayMs)
|
||||
} else {
|
||||
reject(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user