Compare commits

...

40 Commits

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

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

* chore: change the name of inports to WithdrawDepositModal as well
2021-08-23 16:02:24 +02:00
Vojtech Simetka 56df3a2561 feat: remove the last update component (#179) 2021-08-23 16:00:22 +02:00
Vojtech Simetka 7f2ff39ec9 chore: removed unnecessary and unused vars (#177) 2021-08-23 15:57:54 +02:00
Vojtech Simetka 739fc45500 chore: remove refresh button on node setup (#174) 2021-08-23 15:55:22 +02:00
Vojtech Simetka d6d03bf7c6 feat: changing API urls does not need the app refresh (#173)
* feat: changing API urls does not need the app refresh

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

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

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

* fix: change debug API in the settings tab
2021-08-20 15:14:14 +02:00
Vojtech Simetka 2624cf04c9 feat: bee provider caching the state of the app and refreshing periodically (#172)
* feat: bee provider caching the state of the app and refreshing periodically

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

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

* refactor: clean up code

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

* fix: fix off by one bug in retry logic

* docs: add jsdocs to new utility functions

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

* chore: rename enums to pascal case

* chore: fixed typo
2021-08-11 19:46:09 +02:00
Adam Uhlíř f90778d338 docs: update maintainers (#164) 2021-08-11 08:17:46 +02:00
bee-worker 650d100dd2 chore: release 0.5.0 (#163) 2021-08-10 22:07:38 +02:00
Vojtech Simetka 960ffb8fdd feat: updated troubleshooting instructions and links for mainnet (#161) 2021-08-09 14:23:26 +02:00
significance be8b88516b fix: amend readme (#155) 2021-07-05 11:31:38 +02:00
bee-worker 43b3a45d90 chore: release 0.4.0 (#151) 2021-06-29 11:46:19 +02:00
Cafe137 20ed3cb387 build: update bee-js to 1.0.0 (#153) 2021-06-29 11:41:30 +02:00
Cafe137 b190cac706 fix: clear dropzone state after upload (#150)
* fix: clear dropzone state after upload

* fix: avoid state update on unmounted component
2021-06-20 11:39:34 +02:00
Cafe137 6f645dc6c3 feat: display postage batch usage percentage (#149)
* feat: display postage batch usage percentage

* refactor: use string template instead of concat
2021-06-19 19:04:11 +02:00
Cafe137 af88027ba9 refactor: call toString on numerical values to be bee-js compatible (#148)
* refactor: call toString on numerical values to be bee-js compatible

* feat: add upload size check
2021-06-18 12:37:09 +02:00
bee-worker 5748c9b609 chore: release 0.3.1 (#141) 2021-06-03 13:22:57 +02:00
Vojtech Simetka 5ace7629f2 fix: don't display version alert when unable to retrieve version from bee node (#138) 2021-06-03 13:19:16 +02:00
Vojtech Simetka 465df17741 fix: typeerror in the postage stamps form (#137) 2021-06-03 13:18:58 +02:00
bee-worker 3bcf2ac688 chore: release 0.3.0 (#113) 2021-06-02 16:08:11 +02:00
Vojtech Simetka a2bff60270 refactor: removed unused useGetPostageStamps hook (#132) 2021-06-02 16:00:18 +02:00
Vojtech Simetka 353db10080 feat: added tolerance to version check and warning if not exact to what we tested (#133)
* feat: added tolerance to version check and warning if not exact to what we expect

* chore: update to bee-js 0.10.0

* chore: updated interfaces that changed in bee-js 0.10.0
2021-06-02 15:59:57 +02:00
Vojtech Simetka bec84051a9 feat: unified notification with notistack (#127)
* feat: unified existing notification with notistack

* chore: replaced with useSnackbar, added missing notifications

* chore: removed FIXME as per PR review
2021-06-02 13:36:39 +02:00
Vojtech Simetka 92c727e5f5 feat: upload files with postage stamps (#126)
* chore: release 0.3.0

* feat: added postage stamp table to list all stamps

* feat: postage stamp modal to purchase stamps

* feat: postage stamps provider

* chore: added formik

* chore: proper form state handling

* chore: revert accidental release inclusion

* chore: polishing identified when developing the upload functionality

* feat: upload files with postage stamps

* style: tabs styles are defined in theme now, addressed other PR comments

* style: removed unused styles

* fix: enable encrypted hashes to download

Co-authored-by: bee-worker <70210089+bee-worker@users.noreply.github.com>
2021-06-02 13:25:49 +02:00
Vojtech Simetka 4074a9de5d feat: postage stamps support (#115)
* chore: release 0.3.0

* feat: added postage stamp table to list all stamps

* feat: postage stamp modal to purchase stamps

* feat: postage stamps provider

* chore: added formik

* chore: proper form state handling

* chore: revert accidental release inclusion

* chore: polishing identified when developing the upload functionality

Co-authored-by: bee-worker <70210089+bee-worker@users.noreply.github.com>
2021-06-02 13:13:27 +02:00
Vojtech Simetka 9fee1aa68a fix: troubleshooting on a mac and clearer CORS setup guide (#131)
* fix: service status check on macOS

* fix: config file location on mac and making the CORS setup clearer

* fix: error in the README cors instructions
2021-06-02 12:13:17 +02:00
Vojtech Simetka 08fdac9366 docs: updated readme with discord link and clearer setup steps (#129)
* docs: updated readme with discord link and clearer setup steps

* chore: typo in README.md

Co-authored-by: Attila Gazso <agazso@gmail.com>

Co-authored-by: Attila Gazso <agazso@gmail.com>
2021-06-02 11:55:09 +02:00
Matt Mertens ba9b498488 fix: replace http-serve with serve-handler (#122)
* fix: replace http-serve with serve package

Replaces the http-serve package to properly handle the single page routing

* feat: add cache invalidation

* fix: add serve command in bin

* fix: remove serve package dependency

* chore: applied PR review suggestions

Co-authored-by: Vojtech Simetka <vojtech@simetka.cz> (+1 squashed commit)
Squashed commits:
[d73baf4] Update serve.js

Co-authored-by: Vojtech Simetka <vojtech@simetka.cz>

Co-authored-by: Vojtech Simetka <vojtech@simetka.cz>
2021-05-31 09:57:44 +02:00
Matt Mertens 07f987e069 fix: add git attributes (#123) 2021-05-29 01:30:54 +02:00
Matt Mertens a603a86c1a fix: add prod env variables (#121) 2021-05-28 17:43:02 +02:00
Mirko Da Corte aab0462047 feat: added Dockerfile (#75)
* feat: added Dockerfile (#72)

* fix: applying requested fixes on Dockerfile

* fix: updated exposed port on Dockerfile
chore: updated readme with docker instructions

* Update README.md

Co-authored-by: nugaon <50576770+nugaon@users.noreply.github.com>

* fix: fixed codeblock in readme

Co-authored-by: nugaon <50576770+nugaon@users.noreply.github.com>
2021-05-25 16:43:26 +02:00
bee-worker ce949d380c chore: release 0.2.0 (#105) 2021-05-20 18:55:36 +02:00
Vojtech Simetka 7f5fbd3fb6 feat: update to bee 0.6.0 and bee-js 0.9.0 (#99)
* chore: update the interfaces to latest bee-js

* chore: update to latest bee-js

* chore: removed upload page, updated to latest bee-js

* chore: typo in src/pages/files/index.tsx

Co-authored-by: Attila Gazso <agazso@gmail.com>

* chore: update to bee-js 0.9.0

Co-authored-by: Attila Gazso <agazso@gmail.com>
2021-05-20 18:45:35 +02:00
Attila Gazso edd4a2fc11 fix: serve npm command path specification (#101)
When running the Exit 127 command the path is
not changed and it uses the default value ().

Fixes #98
2021-05-20 18:36:20 +02:00
68 changed files with 2709 additions and 1848 deletions
+2 -2
View File
@@ -3,5 +3,5 @@ REACT_APP_BEE_HOST=http://localhost:1633
REACT_APP_BEE_DEBUG_HOST=http://localhost:1635 REACT_APP_BEE_DEBUG_HOST=http://localhost:1635
REACT_APP_BEE_DOCS_HOST=https://docs.ethswarm.org/docs/ REACT_APP_BEE_DOCS_HOST=https://docs.ethswarm.org/docs/
REACT_APP_BEE_DISCORD_HOST=https://discord.gg/eKr9XPv7 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 REACT_APP_BEE_GITHUB_REPO_URL=https://api.github.com/repos/ethersphere/bee
+2
View File
@@ -1,4 +1,6 @@
REACT_APP_BEE_HOST=http://localhost:1633 REACT_APP_BEE_HOST=http://localhost:1633
REACT_APP_BEE_DEBUG_HOST=http://localhost:1635 REACT_APP_BEE_DEBUG_HOST=http://localhost:1635
REACT_APP_BEE_DOCS_HOST=https://docs.ethswarm.org/docs/ 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 REACT_APP_BEE_GITHUB_REPO_URL=https://api.github.com/repos/ethersphere/bee
+4
View File
@@ -0,0 +1,4 @@
* text=auto eol=lf
# Denote all files that are truly binary and should not be modified.
*.png binary
+83
View File
@@ -1,5 +1,88 @@
# Changelog # Changelog
## [0.6.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.5.0...v0.6.0) (2021-08-24)
### Features
* add retry to accounting ([#166](https://www.github.com/ethersphere/bee-dashboard/issues/166)) ([a62243f](https://www.github.com/ethersphere/bee-dashboard/commit/a62243fe5c45b7dd9be6e92f82ebdf0b64bd8f0d))
* add tooltips and health indicator to peers ([#169](https://www.github.com/ethersphere/bee-dashboard/issues/169)) ([480f6dc](https://www.github.com/ethersphere/bee-dashboard/commit/480f6dc7f9c58a4aae87e0dea7082a4bd3dc900b))
* bee provider caching the state of the app and refreshing periodically ([#172](https://www.github.com/ethersphere/bee-dashboard/issues/172)) ([2624cf0](https://www.github.com/ethersphere/bee-dashboard/commit/2624cf04c939e87f025c1f4ff417808073742dab))
* changing API urls does not need the app refresh ([#173](https://www.github.com/ethersphere/bee-dashboard/issues/173)) ([d6d03bf](https://www.github.com/ethersphere/bee-dashboard/commit/d6d03bf7c6d2705de22f43825b85b32c2f0181fb))
* remove the last update component ([#179](https://www.github.com/ethersphere/bee-dashboard/issues/179)) ([56df3a2](https://www.github.com/ethersphere/bee-dashboard/commit/56df3a2561c3c00237b5d107eb054403af3012f8))
* synchronized platform tabs ([#165](https://www.github.com/ethersphere/bee-dashboard/issues/165)) ([ec42eaf](https://www.github.com/ethersphere/bee-dashboard/commit/ec42eafc2b768ba06649f628c733e8d3440fdcaf))
### Bug Fixes
* enum index for supported platforms ([#170](https://www.github.com/ethersphere/bee-dashboard/issues/170)) ([dcec6e0](https://www.github.com/ethersphere/bee-dashboard/commit/dcec6e01887465c74a68feede52b476791bbefa7))
* remove nested ternary operator and simplify ping peer functionality ([#181](https://www.github.com/ethersphere/bee-dashboard/issues/181)) ([2b120e4](https://www.github.com/ethersphere/bee-dashboard/commit/2b120e44ca5e01451cc43e362195c04587836a03))
## [0.5.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.4.0...v0.5.0) (2021-08-09)
### 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
compatible with it. Due to the amount of breaking changes Bee Dashboard no longer supports the Bee version `0.5.3`.
### Features
* update to bee 0.6.0 and bee-js 0.9.0 ([#99](https://www.github.com/ethersphere/bee-dashboard/issues/99)) ([7f5fbd3](https://www.github.com/ethersphere/bee-dashboard/commit/7f5fbd3fb65fe35762cf25ddf7bbaa8b3bd623f9))
### Bug Fixes
* serve npm command path specification ([#101](https://www.github.com/ethersphere/bee-dashboard/issues/101)) ([edd4a2f](https://www.github.com/ethersphere/bee-dashboard/commit/edd4a2fc11219843860861343f0317a5f1268ff0)), closes [#98](https://www.github.com/ethersphere/bee-dashboard/issues/98)
## 0.1.0 (2021-04-29) ## 0.1.0 (2021-04-29)
+1 -1
View File
@@ -1 +1 @@
* nugaon vojtechsimetka * @Cafe137 @vojtechsimetka
+12
View File
@@ -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"]
+42 -11
View File
@@ -6,7 +6,7 @@
![](https://img.shields.io/badge/npm-%3E%3D6.0.0-orange.svg?style=flat-square) ![](https://img.shields.io/badge/npm-%3E%3D6.0.0-orange.svg?style=flat-square)
![](https://img.shields.io/badge/Node.js-%3E%3D10.0.0-orange.svg?style=flat-square) ![](https://img.shields.io/badge/Node.js-%3E%3D10.0.0-orange.svg?style=flat-square)
> 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.** **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) - [Install](#install)
- [Usage](#usage) - [Usage](#usage)
- [Terminal](#terminal)
- [Docker](#docker)
- [Contribute](#contribute) - [Contribute](#contribute)
- [Development](#development)
- [Maintainers](#maintainers)
- [License](#license) - [License](#license)
## Install ## Install
``` Install globally with npm. We require Node.js's version of at least 12.x and npm v6.x (or yarn v2.x).
$ npm install -g @ethersphere/bee-dashboard
$ bee-dashboard ```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 ```sh
git clone git@github.com:ethersphere/bee-dashboard.git git clone git@github.com:ethersphere/bee-dashboard.git
cd bee-dashboard cd bee-dashboard
npm ci npm i
npm run build
npm run serve 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 ## 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 - Consult our [open issues](https://github.com/ethersphere/bee-dashboard/issues) and take on one of them
- Help our tests reach 100% coverage! - 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 ## Maintainers
- [nugaon](https://github.com/nugaon)
- [vojtechsimetka](https://github.com/vojtechsimetka) - [vojtechsimetka](https://github.com/vojtechsimetka)
- [Cafe137](https://github.com/Cafe137)
See what "Maintainer" means [here](https://github.com/ethersphere/repo-maintainer).
## License ## License
+474 -382
View File
File diff suppressed because it is too large Load Diff
+17 -6
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ethersphere/bee-dashboard", "name": "@ethersphere/bee-dashboard",
"version": "0.1.0", "version": "0.6.0",
"description": "An app which helps users to setup their Bee node and do actions like cash out cheques", "description": "An app which helps users to setup their Bee node and do actions like cash out cheques",
"keywords": [ "keywords": [
"bee", "bee",
@@ -24,8 +24,8 @@
"url": "https://github.com/ethersphere/bee-dashboard.git" "url": "https://github.com/ethersphere/bee-dashboard.git"
}, },
"dependencies": { "dependencies": {
"@ethersphere/bee-js": "^0.8.1", "@ethersphere/bee-js": "^1.2.0",
"@material-ui/core": "^4.11.3", "@material-ui/core": "^4.11.4",
"@material-ui/icons": "^4.11.2", "@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.57", "@material-ui/lab": "^4.0.0-alpha.57",
"@types/react-router": "^5.1.13", "@types/react-router": "^5.1.13",
@@ -33,8 +33,11 @@
"axios": "^0.21.1", "axios": "^0.21.1",
"bignumber.js": "^9.0.1", "bignumber.js": "^9.0.1",
"feather-icons": "^4.28.0", "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", "material-ui-dropzone": "^3.5.0",
"notistack": "^1.0.9",
"opener": "^1.5.2",
"qrcode.react": "^1.0.1", "qrcode.react": "^1.0.1",
"react": "^17.0.2", "react": "^17.0.2",
"react-copy-to-clipboard": "^5.0.3", "react-copy-to-clipboard": "^5.0.3",
@@ -42,7 +45,9 @@
"react-feather": "^2.0.9", "react-feather": "^2.0.9",
"react-identicons": "^1.2.5", "react-identicons": "^1.2.5",
"react-router-dom": "^5.2.0", "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": { "devDependencies": {
"@testing-library/jest-dom": "^5.12.0", "@testing-library/jest-dom": "^5.12.0",
@@ -55,6 +60,7 @@
"@types/react-copy-to-clipboard": "^5.0.0", "@types/react-copy-to-clipboard": "^5.0.0",
"@types/react-dom": "^17.0.3", "@types/react-dom": "^17.0.3",
"@types/react-syntax-highlighter": "^13.5.0", "@types/react-syntax-highlighter": "^13.5.0",
"@types/semver": "^7.3.6",
"eslint": "^7.24.0", "eslint": "^7.24.0",
"eslint-config-prettier": "^8.2.0", "eslint-config-prettier": "^8.2.0",
"eslint-plugin-jest": "^24.3.5", "eslint-plugin-jest": "^24.3.5",
@@ -70,7 +76,7 @@
"start": "react-scripts start", "start": "react-scripts start",
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test", "test": "react-scripts test",
"serve": "http-serve -o ./build", "serve": "node ./serve.js",
"lint": "eslint --fix \"src/**/*.ts\" \"src/**/*.tsx\" && prettier --write \"src/**/*.ts\" \"src/**/*.tsx\"", "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\"" "lint:check": "eslint \"src/**/*.ts\" \"src/**/*.tsx\" && prettier --check \"src/**/*.ts\" \"src/**/*.tsx\""
}, },
@@ -95,5 +101,10 @@
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"
] ]
},
"engines": {
"node": ">=12.0.0",
"npm": ">=6.0.0",
"bee": ">=0.6.0"
} }
} }
+12
View File
@@ -0,0 +1,12 @@
{
"trailingSlash": false,
"headers": [
{
"source" : "*",
"headers" : [{
"key" : "Cache-Control",
"value" : "max-age=3600"
}]
}
]
}
Executable → Regular
+23 -5
View File
@@ -1,15 +1,33 @@
#!/usr/bin/env node #!/usr/bin/env node
const path = require('path') const path = require('path')
const serve = require('http-serve') const handler = require('serve-handler');
const http = require('http');
const opener = require('opener') const opener = require('opener')
const server = serve.createServer({ const serverConfig = {
root: path.join(__dirname, 'build') 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('Starting up Bee Dashboard on address http://localhost:8080')
console.log('Hit CTRL-C to stop the server') console.log('Hit CTRL-C to stop the server')
opener('http://localhost:8080') opener('http://localhost:8080')
+21 -4
View File
@@ -4,9 +4,14 @@ import './App.css'
import { ThemeProvider } from '@material-ui/styles' import { ThemeProvider } from '@material-ui/styles'
import CssBaseline from '@material-ui/core/CssBaseline' import CssBaseline from '@material-ui/core/CssBaseline'
import { SnackbarProvider } from 'notistack'
import BaseRouter from './routes/routes' import BaseRouter from './routes/routes'
import { lightTheme, darkTheme } from './theme' import { lightTheme, darkTheme } from './theme'
import { Provider as StampsProvider } from './providers/Stamps'
import { Provider as PlatformProvider } from './providers/Platform'
import { Provider as BeeProvider } from './providers/Bee'
import { Provider as SettingsProvider } from './providers/Settings'
const App = (): ReactElement => { const App = (): ReactElement => {
const [themeMode, toggleThemeMode] = useState('light') const [themeMode, toggleThemeMode] = useState('light')
@@ -33,10 +38,22 @@ const App = (): ReactElement => {
return ( return (
<div className="App"> <div className="App">
<ThemeProvider theme={themeMode === 'light' ? lightTheme : darkTheme}> <ThemeProvider theme={themeMode === 'light' ? lightTheme : darkTheme}>
<CssBaseline /> <SettingsProvider>
<Router> <BeeProvider>
<BaseRouter /> <StampsProvider>
</Router> <PlatformProvider>
<SnackbarProvider>
<>
<CssBaseline />
<Router>
<BaseRouter />
</Router>
</>
</SnackbarProvider>
</PlatformProvider>
</StampsProvider>
</BeeProvider>
</SettingsProvider>
</ThemeProvider> </ThemeProvider>
</div> </div>
) )
+38
View File
@@ -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>
)
}
+54
View File
@@ -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>
)
}
+17 -23
View File
@@ -1,14 +1,13 @@
import { ReactElement, useState } from 'react' import { CircularProgress, Container } from '@material-ui/core'
import Button from '@material-ui/core/Button' import Button from '@material-ui/core/Button'
import Dialog from '@material-ui/core/Dialog' import Dialog from '@material-ui/core/Dialog'
import DialogActions from '@material-ui/core/DialogActions' import DialogActions from '@material-ui/core/DialogActions'
import DialogContent from '@material-ui/core/DialogContent' import DialogContent from '@material-ui/core/DialogContent'
import DialogContentText from '@material-ui/core/DialogContentText' import DialogContentText from '@material-ui/core/DialogContentText'
import DialogTitle from '@material-ui/core/DialogTitle' import DialogTitle from '@material-ui/core/DialogTitle'
import { Snackbar, Container, CircularProgress } from '@material-ui/core' import { useSnackbar } from 'notistack'
import { ReactElement, useState, useContext } from 'react'
import { beeDebugApi } from '../services/bee' import { Context as SettingsContext } from '../providers/Settings'
import EthereumAddress from './EthereumAddress' import EthereumAddress from './EthereumAddress'
interface Props { interface Props {
@@ -16,11 +15,11 @@ interface Props {
uncashedAmount: string uncashedAmount: string
} }
export default function DepositModal({ peerId, uncashedAmount }: Props): ReactElement { export default function CheckoutModal({ peerId, uncashedAmount }: Props): ReactElement {
const [open, setOpen] = useState<boolean>(false) const [open, setOpen] = useState<boolean>(false)
const [loadingCashout, setLoadingCashout] = useState<boolean>(false) const [loadingCashout, setLoadingCashout] = useState<boolean>(false)
const [showToast, setToastVisibility] = useState<boolean>(false) const { enqueueSnackbar } = useSnackbar()
const [toastContent, setToastContent] = useState<JSX.Element | null>(null) const { beeDebugApi } = useContext(SettingsContext)
const handleClickOpen = () => { const handleClickOpen = () => {
setOpen(true) setOpen(true)
@@ -31,43 +30,38 @@ export default function DepositModal({ peerId, uncashedAmount }: Props): ReactEl
} }
const handleCashout = () => { const handleCashout = () => {
if (!beeDebugApi) return
if (peerId) { if (peerId) {
setLoadingCashout(true) setLoadingCashout(true)
beeDebugApi.chequebook beeDebugApi
.peerCashout(peerId) .cashoutLastCheque(peerId)
.then(res => { .then(res => {
setOpen(false) setOpen(false)
handleToast( enqueueSnackbar(
<span> <span>
Successfully cashed out cheque. Transaction Successfully cashed out cheque. Transaction
<EthereumAddress hideBlockie transaction address={res.transactionHash} network={'goerli'} /> <EthereumAddress hideBlockie transaction address={res} />
</span>, </span>,
{ variant: 'success' },
) )
}) })
.catch(() => { .catch((e: Error) => {
// FIXME: handle errors more gracefully enqueueSnackbar(<span>Error: {e.message}</span>, { variant: 'error' })
handleToast(<span>Error with cashout</span>)
}) })
.finally(() => { .finally(() => {
setLoadingCashout(false) setLoadingCashout(false)
}) })
} else { } 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 ( return (
<div> <div>
<Button variant="contained" color="primary" onClick={handleClickOpen} style={{ marginLeft: '7px' }}> <Button variant="contained" color="primary" onClick={handleClickOpen} style={{ marginLeft: '7px' }}>
Cashout Cashout
</Button> </Button>
<Snackbar anchorOrigin={{ vertical: 'top', horizontal: 'center' }} open={showToast} message={toastContent} />
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title"> <Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">Cashout Cheque</DialogTitle> <DialogTitle id="form-dialog-title">Cashout Cheque</DialogTitle>
<DialogContent> <DialogContent>
+7 -11
View File
@@ -1,25 +1,21 @@
import { ReactElement, useState } from 'react' import type { ReactElement } from 'react'
import { IconButton, Snackbar } from '@material-ui/core' import IconButton from '@material-ui/core/IconButton'
import { CopyToClipboard } from 'react-copy-to-clipboard' import { CopyToClipboard } from 'react-copy-to-clipboard'
import { Clipboard } from 'react-feather' import { Clipboard } from 'react-feather'
import { useSnackbar } from 'notistack'
interface Props { interface Props {
value: string value: string
} }
export default function ClipboardCopy(props: Props): ReactElement { export default function ClipboardCopy({ value }: Props): ReactElement {
const [copied, setCopied] = useState(false) const { enqueueSnackbar } = useSnackbar()
const handleCopy = () => enqueueSnackbar(`Copied: ${value}`, { variant: 'success' })
const handleCopy = () => {
setCopied(true)
setTimeout(() => setCopied(false), 3000)
}
return ( return (
<div style={{ marginRight: '3px', marginLeft: '3px' }}> <div style={{ marginRight: '3px', marginLeft: '3px' }}>
<Snackbar anchorOrigin={{ vertical: 'top', horizontal: 'center' }} open={copied} message="Copied" />
<IconButton color="primary" size="small" onClick={handleCopy}> <IconButton color="primary" size="small" onClick={handleCopy}>
<CopyToClipboard text={props.value}> <CopyToClipboard text={value}>
<Clipboard style={{ height: '20px' }} /> <Clipboard style={{ height: '20px' }} />
</CopyToClipboard> </CopyToClipboard>
</IconButton> </IconButton>
+18 -134
View File
@@ -1,13 +1,7 @@
import React, { ReactElement, useEffect } from 'react' import { ReactElement, useContext } from 'react'
import { withStyles, Theme, createStyles } from '@material-ui/core/styles' import TabsContainer from './TabsContainer'
import { Tabs, Tab, Box, Typography } from '@material-ui/core'
import CodeBlock from './CodeBlock' import CodeBlock from './CodeBlock'
import { Context } from '../providers/Platform'
interface TabPanelProps {
children?: React.ReactNode
index: number
value: number
}
interface Props { interface Props {
linux: string linux: string
@@ -15,133 +9,23 @@ interface Props {
showLineNumbers?: boolean showLineNumbers?: boolean
} }
function a11yProps(index: number) {
return {
id: `simple-tab-${index}`,
'aria-controls': `simple-tabpanel-${index}`,
}
}
function getOS() {
const userAgent = window.navigator.userAgent
const platform = window.navigator.platform
const macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K']
const windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE']
const iosPlatforms = ['iPhone', 'iPad', 'iPod']
if (macosPlatforms.includes(platform)) return 'macOS'
if (iosPlatforms.includes(platform)) return 'iOS'
if (windowsPlatforms.includes(platform)) return 'windows'
if (/Android/.test(userAgent)) return 'android'
if (/Linux/.test(platform)) return 'linux'
return null
}
export default function CodeBlockTabs(props: Props): ReactElement { export default function CodeBlockTabs(props: Props): ReactElement {
const [value, setValue] = React.useState(0) const { platform, setPlatform } = useContext(Context)
const handleChange = (event: React.ChangeEvent<unknown>, newValue: number) => {
setValue(newValue)
}
useEffect(() => {
const os = getOS()
if (os === 'windows') {
setValue(0)
} else if (os === 'linux') {
setValue(0)
} else if (os === 'macOS') {
setValue(1)
}
}, [])
function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props
return (
<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 ( return (
<div> <TabsContainer
<AntTabs style={{ marginTop: '12px' }} value={value} onChange={handleChange} aria-label="ant example"> index={platform}
<AntTab label="Linux" {...a11yProps(0)} /> indexChanged={setPlatform}
<AntTab label="MacOS" {...a11yProps(1)} /> values={[
</AntTabs> {
<TabPanel value={value} index={0}> label: 'Linux',
<CodeBlock showLineNumbers={props.showLineNumbers} language="bash" code={props.linux} /> component: <CodeBlock showLineNumbers={props.showLineNumbers} language="bash" code={props.linux} />,
</TabPanel> },
<TabPanel value={value} index={1}> {
<CodeBlock showLineNumbers={props.showLineNumbers} language="bash" code={props.mac} /> label: 'macOS',
</TabPanel> component: <CodeBlock showLineNumbers={props.showLineNumbers} language="bash" code={props.mac} />,
</div> },
]}
/>
) )
} }
+28 -40
View File
@@ -1,62 +1,50 @@
import React, { ReactElement, useState } from 'react' import React, { ReactElement, useState } from 'react'
import { TextField, Button, CircularProgress, Container } from '@material-ui/core' import { TextField, Button } from '@material-ui/core'
interface Props { interface Props {
defaultHost?: string defaultHost?: string
hostName: string setHost: (host: string) => void
} }
export default function ConnectToHost(props: Props): ReactElement { export default function ConnectToHost(props: Props): ReactElement {
const [hostInputVisible, toggleHostInputVisibility] = useState(false) const [hostInputVisible, toggleHostInputVisibility] = useState(false)
const [connectingToHost, setConnectingToHost] = useState(false)
const [host, setHost] = useState('') const [host, setHost] = useState('')
const handleNewHostConnection = () => { const handleNewHostConnection = () => {
if (host) { if (host) {
setConnectingToHost(true) props.setHost(host)
sessionStorage.setItem(props.hostName, host)
toggleHostInputVisibility(!hostInputVisible) toggleHostInputVisibility(!hostInputVisible)
window.location.reload()
} }
} }
return ( return (
<div> <div>
{ {hostInputVisible ? (
// FIXME: this should be broken up <div style={{ display: 'flex' }}>
/* eslint-disable no-nested-ternary */ <TextField
hostInputVisible ? ( defaultValue={props.defaultHost}
<div style={{ display: 'flex' }}> label="Enter host"
<TextField variant="outlined"
defaultValue={props.defaultHost} size="small"
label="Enter host" onChange={e => setHost(e.target.value)}
variant="outlined" style={{ marginRight: '15px', minWidth: '300px' }}
size="small" />
onChange={e => setHost(e.target.value)} <Button onClick={() => handleNewHostConnection()} size="small" variant="outlined">
style={{ marginRight: '15px', minWidth: '300px' }} Connect
/>
<Button onClick={() => handleNewHostConnection()} size="small" variant="outlined">
Connect
</Button>
<Button
style={{ marginLeft: '7px' }}
onClick={() => toggleHostInputVisibility(!hostInputVisible)}
size="small"
>
Cancel
</Button>
</div>
) : connectingToHost ? (
<Container style={{ textAlign: 'center', padding: '0px' }}>
<CircularProgress size={20} />
</Container>
) : (
<Button onClick={() => toggleHostInputVisibility(!hostInputVisible)} size="small" variant="outlined">
Change host
</Button> </Button>
) <Button
/* eslint-enable no-nested-ternary */ style={{ marginLeft: '7px' }}
} onClick={() => toggleHostInputVisibility(!hostInputVisible)}
size="small"
>
Cancel
</Button>
</div>
) : (
<Button onClick={() => toggleHostInputVisibility(!hostInputVisible)} size="small" variant="outlined">
Change host
</Button>
)}
</div> </div>
) )
} }
+3 -4
View File
@@ -7,7 +7,6 @@ import { ReactElement } from 'react'
interface Props { interface Props {
address: string | undefined address: string | undefined
network?: string
hideBlockie?: boolean hideBlockie?: boolean
transaction?: boolean transaction?: boolean
truncate?: boolean truncate?: boolean
@@ -37,9 +36,9 @@ export default function EthereumAddress(props: Props): ReactElement {
} }
: { marginRight: '7px' } : { marginRight: '7px' }
} }
href={`https://${props.network}.${process.env.REACT_APP_ETHERSCAN_HOST}/${ href={`${process.env.REACT_APP_BLOCKCHAIN_EXPLORER_URL}/${props.transaction ? 'tx' : 'address'}/${
props.transaction ? 'tx' : 'address' props.address
}/${props.address}`} }`}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
+18 -34
View File
@@ -1,10 +1,9 @@
import React, { ReactElement } from 'react' import { ReactElement } from 'react'
import { createStyles, makeStyles } from '@material-ui/core/styles' import { createStyles, makeStyles } from '@material-ui/core/styles'
import { Card, CardContent, Typography } from '@material-ui/core/' import { Card, CardContent, Typography } from '@material-ui/core/'
import EthereumAddress from '../components/EthereumAddress' import EthereumAddress from '../components/EthereumAddress'
import { Skeleton } from '@material-ui/lab'
import type { ChequebookAddressResponse, NodeAddresses } from '@ethersphere/bee-js' import type { ChequebookAddressResponse, NodeAddresses } from '@ethersphere/bee-js'
@@ -28,9 +27,7 @@ const useStyles = makeStyles(() =>
interface Props { interface Props {
nodeAddresses: NodeAddresses | null nodeAddresses: NodeAddresses | null
isLoadingNodeAddresses: boolean
chequebookAddress: ChequebookAddressResponse | null chequebookAddress: ChequebookAddressResponse | null
isLoadingChequebookAddress: boolean
} }
function EthereumAddressCard(props: Props): ReactElement { function EthereumAddressCard(props: Props): ReactElement {
@@ -38,36 +35,23 @@ function EthereumAddressCard(props: Props): ReactElement {
return ( return (
<Card className={classes.root}> <Card className={classes.root}>
{props.isLoadingNodeAddresses ? ( <div className={classes.details}>
<div style={{ padding: '16px' }}> <CardContent className={classes.content}>
<Skeleton width={300} height={30} animation="wave" /> <Typography variant="subtitle1" gutterBottom>
<Skeleton width={300} height={50} animation="wave" /> Ethereum Address
</div> </Typography>
) : ( <EthereumAddress address={props.nodeAddresses?.ethereum} />
<div className={classes.details}> </CardContent>
<CardContent className={classes.content}> </div>
<Typography variant="subtitle1" gutterBottom>
Ethereum Address <div className={classes.details}>
</Typography> <CardContent className={classes.content}>
<EthereumAddress address={props.nodeAddresses?.ethereum} network={'goerli'} /> <Typography variant="subtitle1" gutterBottom>
</CardContent> Chequebook Contract Address
</div> </Typography>
)} <EthereumAddress address={props.chequebookAddress?.chequebookAddress} />
{props.isLoadingChequebookAddress ? ( </CardContent>
<div style={{ padding: '16px' }}> </div>
<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'} />
</CardContent>
</div>
)}
</Card> </Card>
) )
} }
+1 -7
View File
@@ -1,7 +1,7 @@
import { useState, ReactElement } from 'react' import { useState, ReactElement } from 'react'
import { createStyles, makeStyles } from '@material-ui/core/styles' 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' import { Sun, Moon } from 'react-feather'
@@ -13,7 +13,6 @@ const useStyles = makeStyles(() =>
width: `calc(100% - ${drawerWidth}px)`, width: `calc(100% - ${drawerWidth}px)`,
marginLeft: drawerWidth, marginLeft: drawerWidth,
}, },
network: {},
}), }),
) )
interface Props { interface Props {
@@ -42,16 +41,11 @@ export default function SideBar(props: Props): ReactElement {
<div> <div>
<div style={{ display: 'fixed' }} className={classes.appBar}> <div style={{ display: 'fixed' }} className={classes.appBar}>
<Toolbar style={{ display: 'flex' }}> <Toolbar style={{ display: 'flex' }}>
<Chip style={{ marginLeft: '7px' }} size="small" label="Goerli" className={classes.network} />
<div style={{ width: '100%' }}> <div style={{ width: '100%' }}>
<div style={{ float: 'right' }}> <div style={{ float: 'right' }}>
<IconButton style={{ marginRight: '10px' }} aria-label="dark-mode" onClick={() => switchTheme()}> <IconButton style={{ marginRight: '10px' }} aria-label="dark-mode" onClick={() => switchTheme()}>
{props.themeMode === 'dark' ? <Moon /> : <Sun />} {props.themeMode === 'dark' ? <Moon /> : <Sun />}
</IconButton> </IconButton>
{/* <Chip
label="Connect Wallet"
color="primary"
/> */}
</div> </div>
</div> </div>
</Toolbar> </Toolbar>
@@ -7,9 +7,10 @@ function truncStringPortion(str: string, firstCharCount = 10, endCharCount = 10)
interface Props { interface Props {
peerId: string peerId: string
characterLength?: number
} }
export default function PeerDetail(props: Props): ReactElement { export default function PeerDetail({ peerId, characterLength }: Props): ReactElement {
return ( return (
<Typography <Typography
variant="button" variant="button"
@@ -17,7 +18,7 @@ export default function PeerDetail(props: Props): ReactElement {
fontFamily: 'monospace, monospace', fontFamily: 'monospace, monospace',
}} }}
> >
{truncStringPortion(props.peerId)} {truncStringPortion(peerId, characterLength, characterLength)}
</Typography> </Typography>
) )
} }
+7 -1
View File
@@ -4,7 +4,7 @@ import { Link, RouteComponentProps } from 'react-router-dom'
import { createStyles, Theme, makeStyles } from '@material-ui/core/styles' import { createStyles, Theme, makeStyles } from '@material-ui/core/styles'
import { ListItemText, ListItemIcon, ListItem, Divider, List, Drawer, Link as MUILink } from '@material-ui/core' import { ListItemText, ListItemIcon, ListItem, Divider, List, Drawer, Link as MUILink } from '@material-ui/core'
import { OpenInNewSharp } from '@material-ui/icons' 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 SwarmLogoOrange from '../assets/swarm-logo-orange.svg'
import { Health } from '@ethersphere/bee-js' import { Health } from '@ethersphere/bee-js'
@@ -24,6 +24,12 @@ const navBarItems = [
path: '/files/', path: '/files/',
icon: FileText, icon: FileText,
}, },
{
label: 'Stamps',
id: 'stamps',
path: '/stamps/',
icon: Layers,
},
{ {
label: 'Accounting', label: 'Accounting',
id: 'accounting', id: 'accounting',
+6 -13
View File
@@ -1,28 +1,23 @@
import type { ReactElement } from 'react'
import { makeStyles } from '@material-ui/core/styles'
import { Card, CardContent, Typography } from '@material-ui/core/' import { Card, CardContent, Typography } from '@material-ui/core/'
import { makeStyles } from '@material-ui/core/styles'
import { Skeleton } from '@material-ui/lab' import { Skeleton } from '@material-ui/lab'
import type { ReactElement } from 'react'
import { Title } from './Title'
const useStyles = makeStyles({ const useStyles = makeStyles({
root: { root: {
minWidth: 275, minWidth: 275,
}, },
title: {
fontSize: 16,
},
pos: {
marginBottom: 12,
},
}) })
interface Props { interface Props {
label: string label: string
statistic?: string statistic?: string
loading?: boolean loading?: boolean
tooltip?: string
} }
export default function StatCard({ loading, label, statistic }: Props): ReactElement { export default function StatCard({ loading, label, statistic, tooltip }: Props): ReactElement {
const classes = useStyles() const classes = useStyles()
return ( return (
@@ -36,9 +31,7 @@ export default function StatCard({ loading, label, statistic }: Props): ReactEle
)} )}
{!loading && ( {!loading && (
<> <>
<Typography className={classes.title} color="textSecondary" gutterBottom> <Title label={label} tooltip={tooltip} />
{label}
</Typography>
<Typography variant="h5" component="h2"> <Typography variant="h5" component="h2">
{statistic} {statistic}
</Typography> </Typography>
+70
View File
@@ -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>
)
}
+41
View File
@@ -0,0 +1,41 @@
import { Grid, Tooltip, Typography } from '@material-ui/core/'
import { makeStyles } from '@material-ui/core/styles'
import { Info } from '@material-ui/icons'
import type { ReactElement } from 'react'
interface TitleProps {
label: string
tooltip?: string
}
const useStyles = makeStyles({
title: {
fontSize: 16,
},
})
export function Title({ label, tooltip }: TitleProps): ReactElement {
const classes = useStyles()
if (!tooltip) {
return (
<Typography className={classes.title} color="textSecondary" gutterBottom>
{label}
</Typography>
)
}
// span is needed as Tooltip expects a non-functional element!
return (
<Tooltip title={tooltip}>
<span>
<Grid container direction="row" justify="space-between">
<Typography className={classes.title} color="textSecondary" gutterBottom>
{label}
</Typography>
<Info />
</Grid>
</span>
</Tooltip>
)
}
+45 -7
View File
@@ -1,25 +1,63 @@
import type { Topology } from '@ethersphere/bee-js' import type { Topology } from '@ethersphere/bee-js'
import { Grid } from '@material-ui/core/' import { Grid } from '@material-ui/core/'
import type { ReactElement } from 'react' import type { ReactElement } from 'react'
import { pickThreshold, ThresholdValues } from '../utils/threshold'
import StatCard from './StatCard' import StatCard from './StatCard'
interface Props { interface RootProps {
isLoading: boolean
topology: Topology | null topology: Topology | null
error: Error | null // FIXME: should display error
} }
const TopologyStats = ({ isLoading, topology }: Props): ReactElement => ( interface Props extends RootProps {
thresholds: ThresholdValues
}
const TopologyStats = (props: RootProps): ReactElement => {
const thresholds: ThresholdValues = {
connectedPeers: pickThreshold('connectedPeers', props.topology?.connected || 0),
population: pickThreshold('population', props.topology?.population || 0),
depth: pickThreshold('depth', props.topology?.depth || 0),
}
return (
<>
<Indicator {...props} thresholds={thresholds} />
<Metrics {...props} thresholds={thresholds} />
</>
)
}
const Indicator = ({ thresholds }: Props): ReactElement => {
const maximumTotalScore = Object.values(thresholds).reduce((sum, item) => sum + item.maximumScore, 0)
const actualTotalScore = Object.values(thresholds).reduce((sum, item) => sum + item.score, 0)
const percentageText = Math.round((actualTotalScore / maximumTotalScore) * 100) + '%'
return (
<div style={{ marginBottom: '20px' }}>
<StatCard label="Overall Health Indicator" statistic={percentageText} />
</div>
)
}
const Metrics = ({ topology, thresholds }: Props): ReactElement => (
<Grid style={{ marginBottom: '20px', flexGrow: 1 }}> <Grid style={{ marginBottom: '20px', flexGrow: 1 }}>
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid key={1} item xs={12} sm={12} md={6} lg={4} xl={4}> <Grid key={1} item xs={12} sm={12} md={6} lg={4} xl={4}>
<StatCard label="Connected Peers" statistic={topology?.connected.toString()} loading={isLoading} /> <StatCard
label="Connected Peers"
statistic={topology?.connected.toString()}
tooltip={thresholds.connectedPeers.explanation}
/>
</Grid> </Grid>
<Grid key={2} item xs={12} sm={12} md={6} lg={4} xl={4}> <Grid key={2} item xs={12} sm={12} md={6} lg={4} xl={4}>
<StatCard label="Population" statistic={topology?.population.toString()} loading={isLoading} /> <StatCard
label="Population"
statistic={topology?.population.toString()}
tooltip={thresholds.population.explanation}
/>
</Grid> </Grid>
<Grid key={3} item xs={12} sm={12} md={6} lg={4} xl={4}> <Grid key={3} item xs={12} sm={12} md={6} lg={4} xl={4}>
<StatCard label="Depth" statistic={topology?.depth.toString()} loading={isLoading} /> <StatCard label="Depth" statistic={topology?.depth.toString()} tooltip={thresholds.depth.explanation} />
</Grid> </Grid>
</Grid> </Grid>
</Grid> </Grid>
@@ -6,9 +6,10 @@ import DialogActions from '@material-ui/core/DialogActions'
import DialogContent from '@material-ui/core/DialogContent' import DialogContent from '@material-ui/core/DialogContent'
import DialogContentText from '@material-ui/core/DialogContentText' import DialogContentText from '@material-ui/core/DialogContentText'
import DialogTitle from '@material-ui/core/DialogTitle' import DialogTitle from '@material-ui/core/DialogTitle'
import { FormHelperText, Snackbar } from '@material-ui/core' import FormHelperText from '@material-ui/core/FormHelperText'
import { Token } from '../models/Token' import { Token } from '../models/Token'
import type { BigNumber } from 'bignumber.js' import type { BigNumber } from 'bignumber.js'
import { useSnackbar } from 'notistack'
interface Props { interface Props {
successMessage: string successMessage: string
@@ -17,10 +18,10 @@ interface Props {
label: string label: string
max?: BigNumber max?: BigNumber
min?: BigNumber min?: BigNumber
action: (amount: bigint) => Promise<{ transactionHash: string }> action: (amount: bigint) => Promise<string>
} }
export default function WithdrawModal({ export default function WithdrawDepositModal({
successMessage, successMessage,
errorMessage, errorMessage,
dialogMessage, dialogMessage,
@@ -33,8 +34,7 @@ export default function WithdrawModal({
const [amount, setAmount] = useState('') const [amount, setAmount] = useState('')
const [amountToken, setAmountToken] = useState<Token | null>(null) const [amountToken, setAmountToken] = useState<Token | null>(null)
const [amountError, setAmountError] = useState<Error | null>(null) const [amountError, setAmountError] = useState<Error | null>(null)
const [showToast, setToastVisibility] = useState(false) const { enqueueSnackbar } = useSnackbar()
const [toastContent, setToastContent] = useState('')
const handleClickOpen = () => { const handleClickOpen = () => {
setOpen(true) setOpen(true)
@@ -48,20 +48,14 @@ export default function WithdrawModal({
if (amountToken === null) return if (amountToken === null) return
try { try {
const { transactionHash } = await action(amountToken.toBigInt as bigint) const transactionHash = await action(amountToken.toBigInt as bigint)
setOpen(false) setOpen(false)
handleToast(`${successMessage} Transaction ${transactionHash}`) enqueueSnackbar(`${successMessage} Transaction ${transactionHash}`, { variant: 'success' })
} catch (e) { } 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 handleChange = (e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
const value = e.target.value const value = e.target.value
setAmount(value) setAmount(value)
@@ -83,7 +77,6 @@ export default function WithdrawModal({
<Button variant="outlined" color="primary" onClick={handleClickOpen}> <Button variant="outlined" color="primary" onClick={handleClickOpen}>
{label} {label}
</Button> </Button>
<Snackbar anchorOrigin={{ vertical: 'top', horizontal: 'center' }} open={showToast} message={toastContent} />
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title"> <Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">{label}</DialogTitle> <DialogTitle id="form-dialog-title">{label}</DialogTitle>
<DialogContent> <DialogContent>
-4
View File
@@ -1,4 +0,0 @@
// These values can for now be constants because their change in the app reloads the page
export const apiHost = sessionStorage.getItem('api_host') || process.env.REACT_APP_BEE_HOST || 'http://localhost:1633'
export const debugApiHost =
sessionStorage.getItem('debug_api_host') || process.env.REACT_APP_BEE_DEBUG_HOST || 'http://localhost:1635'
+11 -5
View File
@@ -1,18 +1,24 @@
import type { ReactElement } from 'react' import { ReactElement, useContext } from 'react'
import { beeDebugApi } from '../services/bee' import { Context as SettingsContext } from '../providers/Settings'
import WDModal from '../components/WDModal' import WithdrawDepositModal from '../components/WithdrawDepositModal'
import { BigNumber } from 'bignumber.js' import { BigNumber } from 'bignumber.js'
export default function DepositModal(): ReactElement { export default function DepositModal(): ReactElement {
const { beeDebugApi } = useContext(SettingsContext)
return ( return (
<WDModal <WithdrawDepositModal
successMessage="Successful deposit." successMessage="Successful deposit."
errorMessage="Error with depositing" errorMessage="Error with depositing"
dialogMessage="Specify the amount of BZZ you would like to withdraw from your node." dialogMessage="Specify the amount of BZZ you would like to withdraw from your node."
label="Deposit" label="Deposit"
min={new BigNumber(0)} min={new BigNumber(0)}
action={beeDebugApi.chequebook.deposit} action={(amount: bigint) => {
if (!beeDebugApi) throw new Error('Bee Debug URL is not valid')
return beeDebugApi.depositTokens(amount.toString())
}}
/> />
) )
} }
+11 -5
View File
@@ -1,18 +1,24 @@
import type { ReactElement } from 'react' import { ReactElement, useContext } from 'react'
import { beeDebugApi } from '../services/bee' import { Context as SettingsContext } from '../providers/Settings'
import WDModal from '../components/WDModal' import WithdrawDepositModal from '../components/WithdrawDepositModal'
import { BigNumber } from 'bignumber.js' import { BigNumber } from 'bignumber.js'
export default function WithdrawModal(): ReactElement { export default function WithdrawModal(): ReactElement {
const { beeDebugApi } = useContext(SettingsContext)
return ( return (
<WDModal <WithdrawDepositModal
successMessage="Successful withdrawl." successMessage="Successful withdrawl."
errorMessage="Error with withdrawing." errorMessage="Error with withdrawing."
dialogMessage="Specify the amount of BZZ you would like to withdraw from your node." dialogMessage="Specify the amount of BZZ you would like to withdraw from your node."
label="Withdraw" label="Withdraw"
min={new BigNumber(0)} min={new BigNumber(0)}
action={beeDebugApi.chequebook.withdraw} action={(amount: bigint) => {
if (!beeDebugApi) throw new Error('Bee Debug URL is not valid')
return beeDebugApi.withdrawTokens(amount.toString())
}}
/> />
) )
} }
+22 -32
View File
@@ -1,15 +1,11 @@
import { LastCashoutActionResponse } from '@ethersphere/bee-js' import { LastCashoutActionResponse, BeeDebug } from '@ethersphere/bee-js'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Token } from '../models/Token' import { Token } from '../models/Token'
import { beeDebugApi } from '../services/bee' import { makeRetriablePromise, unwrapPromiseSettlements } from '../utils'
import { Balance, Settlement, useApiPeerBalances, useApiSettlements } from './apiHooks' import { Balance, Settlements, Settlement } from '../types'
interface UseAccountingHook { interface UseAccountingHook {
isLoading: boolean
isLoadingUncashed: boolean isLoadingUncashed: boolean
error: Error | null
totalsent: Token
totalreceived: Token
accounting: Accounting[] | null accounting: Accounting[] | null
} }
@@ -67,10 +63,8 @@ function mergeAccounting(
// If there are no cheques (and hence last cashout actions), we don't need to sort and can return values right away // If there are no cheques (and hence last cashout actions), we don't need to sort and can return values right away
if (!uncashedAmounts) return Object.values(accounting) if (!uncashedAmounts) return Object.values(accounting)
uncashedAmounts?.forEach(({ peer, cumulativePayout }) => { uncashedAmounts?.forEach(({ peer, uncashedAmount }) => {
accounting[peer].uncashedAmount = new Token( accounting[peer].uncashedAmount = new Token(uncashedAmount)
accounting[peer].received.toBigNumber.minus(cumulativePayout.toString()),
)
}) })
return Object.values(accounting).sort((a, b) => return Object.values(accounting).sort((a, b) =>
@@ -78,38 +72,34 @@ function mergeAccounting(
) )
} }
export const useAccounting = (): UseAccountingHook => { export const useAccounting = (
const settlements = useApiSettlements() beeDebugApi: BeeDebug | null,
const balances = useApiPeerBalances() settlements: Settlements | null,
balances: Balance[] | null,
const [err, setErr] = useState<Error | null>(null) ): UseAccountingHook => {
const [isLoadingUncashed, setIsloadingUncashed] = useState<boolean>(false) const [isLoadingUncashed, setIsloadingUncashed] = useState<boolean>(false)
const [uncashedAmounts, setUncashedAmounts] = useState<LastCashoutActionResponse[] | undefined>(undefined) const [uncashedAmounts, setUncashedAmounts] = useState<LastCashoutActionResponse[] | undefined>(undefined)
const error = balances.error || settlements.error || err
useEffect(() => { useEffect(() => {
// We don't have any settlements loaded yet or we are already loading/have loaded the uncashed amounts // We don't have any settlements loaded yet or we are already loading/have loaded the uncashed amounts
if (isLoadingUncashed || !settlements.settlements || uncashedAmounts || error) return if (isLoadingUncashed || !beeDebugApi || !settlements || uncashedAmounts) return
setIsloadingUncashed(true) setIsloadingUncashed(true)
const promises = settlements.settlements.settlements.map(({ peer }) => const promises = settlements.settlements
beeDebugApi.chequebook.getPeerLastCashout(peer), .filter(({ received }) => received.toBigNumber.gt('0'))
) .map(({ peer }) => makeRetriablePromise(() => beeDebugApi.getLastCashoutAction(peer)))
Promise.all(promises)
.then(setUncashedAmounts)
.catch(setErr)
.finally(() => setIsloadingUncashed(false))
}, [settlements, isLoadingUncashed, uncashedAmounts, error])
const accounting = mergeAccounting(balances.peerBalances, settlements.settlements?.settlements, uncashedAmounts) Promise.allSettled(promises).then(settlements => {
const results = unwrapPromiseSettlements(settlements)
setUncashedAmounts(results.fulfilled)
setIsloadingUncashed(false)
})
}, [settlements, isLoadingUncashed, uncashedAmounts])
const accounting = mergeAccounting(balances, settlements?.settlements, uncashedAmounts)
return { return {
isLoading: settlements.isLoadingSettlements || balances.isLoadingPeerBalances,
isLoadingUncashed, isLoadingUncashed,
error,
accounting, accounting,
totalsent: settlements.settlements?.totalsent || new Token('0'),
totalreceived: settlements.settlements?.totalreceived || new Token('0'),
} }
} }
-401
View File
@@ -1,406 +1,5 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import {
NodeAddresses,
ChequebookAddressResponse,
LastChequesResponse,
Health,
Peer,
Topology,
LastChequesForPeerResponse,
} from '@ethersphere/bee-js'
import { beeDebugApi, beeApi } from '../services/bee'
import axios from 'axios' import axios from 'axios'
import { Token } from '../models/Token'
export interface HealthHook {
health: boolean
isLoadingHealth: boolean
error: Error | null
}
export const useApiHealth = (): HealthHook => {
const [health, setHealth] = useState<boolean>(false)
const [isLoadingHealth, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeApi.status
.health()
.then(res => {
setHealth(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { health, isLoadingHealth, error }
}
export interface DebugHealthHook {
nodeHealth: Health | null
isLoadingNodeHealth: boolean
error: Error | null
}
export const useDebugApiHealth = (): DebugHealthHook => {
const [nodeHealth, setNodeHealth] = useState<Health | null>(null)
const [isLoadingNodeHealth, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.status
.nodeHealth()
.then(res => {
setNodeHealth(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { nodeHealth, isLoadingNodeHealth, error }
}
export interface NodeAddressesHook {
nodeAddresses: NodeAddresses | null
isLoadingNodeAddresses: boolean
error: Error | null
}
export const useApiNodeAddresses = (): NodeAddressesHook => {
const [nodeAddresses, setNodeAddresses] = useState<NodeAddresses | null>(null)
const [isLoadingNodeAddresses, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.connectivity
.addresses()
.then(res => {
setNodeAddresses(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { nodeAddresses, isLoadingNodeAddresses, error }
}
export interface NodeTopologyHook {
topology: Topology | null
isLoading: boolean
error: Error | null
}
export const useApiNodeTopology = (): NodeTopologyHook => {
const [topology, setNodeTopology] = useState<Topology | null>(null)
const [isLoading, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.connectivity
.topology()
.then(res => {
setNodeTopology(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { topology, isLoading, error }
}
export interface ChequebookAddressHook {
chequebookAddress: ChequebookAddressResponse | null
isLoadingChequebookAddress: boolean
error: Error | null
}
export const useApiChequebookAddress = (): ChequebookAddressHook => {
const [chequebookAddress, setChequebookAddress] = useState<ChequebookAddressResponse | null>(null)
const [isLoadingChequebookAddress, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.chequebook
.address()
.then(res => {
setChequebookAddress(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { chequebookAddress, isLoadingChequebookAddress, error }
}
export interface NodePeersHook {
peers: Peer[] | null
isLoading: boolean
error: Error | null
}
export const useApiNodePeers = (): NodePeersHook => {
const [peers, setPeers] = useState<Peer[] | null>(null)
const [isLoading, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.connectivity
.listPeers()
.then(res => {
setPeers(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { peers, isLoading, error }
}
export interface ChequebookBalance {
totalBalance: Token
availableBalance: Token
}
export interface ChequebookBalanceHook {
chequebookBalance: ChequebookBalance | null
isLoadingChequebookBalance: boolean
error: Error | null
}
export const useApiChequebookBalance = (): ChequebookBalanceHook => {
const [chequebookBalance, setChequebookBalance] = useState<ChequebookBalance | null>(null)
const [isLoadingChequebookBalance, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.chequebook
.balance()
.then(({ totalBalance, availableBalance }) => {
const balance = {
totalBalance: new Token(totalBalance),
availableBalance: new Token(availableBalance),
}
setChequebookBalance(balance)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { chequebookBalance, isLoadingChequebookBalance, error }
}
export interface Balance {
peer: string
balance: Token
}
export interface PeerBalanceHook {
peerBalances: Balance[] | null
isLoadingPeerBalances: boolean
error: Error | null
}
export const useApiPeerBalances = (): PeerBalanceHook => {
const [peerBalances, setPeerBalances] = useState<Balance[] | null>(null)
const [isLoadingPeerBalances, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.balance
.balances()
.then(res => {
// for some reason sometimes these are numbers and not BigInts
const balances = res.balances.map(({ peer, balance }) => ({ peer, balance: new Token(balance) }))
setPeerBalances(balances)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { peerBalances, isLoadingPeerBalances, error }
}
export interface PeerChequesHook {
peerCheques: LastChequesResponse | null
isLoadingPeerCheques: boolean
error: Error | null
}
export const useApiPeerCheques = (): PeerChequesHook => {
const [peerCheques, setPeerCheques] = useState<LastChequesResponse | null>(null)
const [isLoadingPeerCheques, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.chequebook
.getLastCheques()
.then(res => {
setPeerCheques(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { peerCheques, isLoadingPeerCheques, error }
}
export interface PeerLastChequesHook {
peerCheque: LastChequesForPeerResponse | null
isLoadingPeerCheque: boolean
error: Error | null
}
export const useApiPeerLastCheque = (peerId: string): PeerLastChequesHook => {
const [peerCheque, setPeerCheque] = useState<LastChequesForPeerResponse | null>(null)
const [isLoadingPeerCheque, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.chequebook
.getPeerLastCheques(peerId)
.then(res => {
setPeerCheque(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [peerId])
return { peerCheque, isLoadingPeerCheque, error }
}
export interface Settlement {
peer: string
received: Token
sent: Token
}
export interface Settlements {
totalreceived: Token
totalsent: Token
settlements: Settlement[]
}
export interface SettlementsHook {
settlements: Settlements | null
isLoadingSettlements: boolean
error: Error | null
}
export const useApiSettlements = (): SettlementsHook => {
const [settlements, setSettlements] = useState<Settlements | null>(null)
const [isLoadingSettlements, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.settlements
.getSettlements()
.then(({ totalreceived, settlements, totalsent }) => {
const set = {
totalreceived: new Token(totalreceived),
totalsent: new Token(totalsent),
settlements: settlements.map(({ peer, received, sent }) => ({
peer,
received: new Token(received),
sent: new Token(sent),
})),
}
setSettlements(set)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { settlements, isLoadingSettlements, error }
}
export interface LastCashout {
peer: string
cumulativePayout: 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, cumulativePayout }) => {
setPeerCashout({ peer, cumulativePayout: new Token(cumulativePayout) })
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [peerId])
return { peerCashout, isLoadingPeerCashout, error }
}
export interface LatestBeeReleaseHook { export interface LatestBeeReleaseHook {
latestBeeRelease: LatestBeeRelease | null latestBeeRelease: LatestBeeRelease | null
-82
View File
@@ -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
View File
@@ -1,17 +1,19 @@
import { useState, useEffect, ReactElement } from 'react' import { useState, useEffect, useContext, ReactElement } from 'react'
import ErrorBoundary from '../components/ErrorBoundary' import ErrorBoundary from '../components/ErrorBoundary'
import AlertVersion from '../components/AlertVersion'
import { Container, CircularProgress } from '@material-ui/core'
import { createStyles, Theme, makeStyles } from '@material-ui/core/styles' import { createStyles, Theme, makeStyles } from '@material-ui/core/styles'
import SideBar from '../components/SideBar' import SideBar from '../components/SideBar'
import NavBar from '../components/NavBar' import NavBar from '../components/NavBar'
import { useApiHealth, useDebugApiHealth } from '../hooks/apiHooks' import { Context } from '../providers/Bee'
import { RouteComponentProps } from 'react-router' import { RouteComponentProps } from 'react-router'
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
createStyles({ createStyles({
toolbar: theme.mixins.toolbar,
content: { content: {
marginLeft: '240px', marginLeft: '240px',
flexGrow: 1, flexGrow: 1,
@@ -19,20 +21,6 @@ const useStyles = makeStyles((theme: Theme) =>
padding: theme.spacing(3), padding: theme.spacing(3),
paddingBottom: '65px', 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') const [themeMode, toggleThemeMode] = useState('light')
// FIXME: handle errrors and loading const { isLoading, apiHealth, debugApiHealth } = useContext(Context)
const { health } = useApiHealth()
const { nodeHealth } = useDebugApiHealth()
useEffect(() => { useEffect(() => {
const theme = localStorage.getItem('theme') const theme = localStorage.getItem('theme')
@@ -70,10 +56,19 @@ const Dashboard = (props: Props): ReactElement => {
return ( return (
<div> <div>
<SideBar {...props} themeMode={themeMode} health={health} nodeHealth={nodeHealth} /> <SideBar {...props} themeMode={themeMode} health={apiHealth} nodeHealth={debugApiHealth} />
<NavBar themeMode={themeMode} /> <NavBar themeMode={themeMode} />
<ErrorBoundary> <ErrorBoundary>
<main className={classes.content}>{props.children}</main> <main className={classes.content}>
<AlertVersion />
{isLoading ? (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress />
</Container>
) : (
props.children
)}
</main>
</ErrorBoundary> </ErrorBoundary>
</div> </div>
) )
+24 -35
View File
@@ -2,7 +2,6 @@ import { ReactElement } from 'react'
import { createStyles, makeStyles } from '@material-ui/core/styles' import { createStyles, makeStyles } from '@material-ui/core/styles'
import { Card, CardContent, Typography, Theme } from '@material-ui/core/' import { Card, CardContent, Typography, Theme } from '@material-ui/core/'
import { Skeleton } from '@material-ui/lab'
import WithdrawModal from '../../containers/WithdrawModal' import WithdrawModal from '../../containers/WithdrawModal'
import DepositModal from '../../containers/DepositModal' import DepositModal from '../../containers/DepositModal'
@@ -45,12 +44,11 @@ interface ChequebookBalance {
interface Props { interface Props {
chequebookAddress: ChequebookAddressResponse | null chequebookAddress: ChequebookAddressResponse | null
chequebookBalance: ChequebookBalance | null chequebookBalance: ChequebookBalance | null
totalsent: Token totalsent?: Token
totalreceived: Token totalreceived?: Token
isLoading: boolean
} }
function AccountCard({ totalreceived, totalsent, chequebookBalance, isLoading }: Props): ReactElement { function AccountCard({ totalreceived, totalsent, chequebookBalance }: Props): ReactElement {
const classes = useStyles() const classes = useStyles()
return ( return (
@@ -66,37 +64,28 @@ function AccountCard({ totalreceived, totalsent, chequebookBalance, isLoading }:
</div> </div>
<Card className={classes.root}> <Card className={classes.root}>
{!isLoading && ( <CardContent className={classes.gridContainer}>
<CardContent className={classes.gridContainer}> <div>
<div> <Typography component="h2" variant="h6" color="primary" gutterBottom>
<Typography component="h2" variant="h6" color="primary" gutterBottom> Total Balance
Total Balance </Typography>
</Typography> <Typography variant="h5">{chequebookBalance?.totalBalance.toFixedDecimal()} BZZ</Typography>
<Typography variant="h5">{chequebookBalance?.totalBalance.toFixedDecimal()} BZZ</Typography>
</div>
<div>
<Typography component="h2" variant="h6" color="primary" gutterBottom>
Available Uncommitted Balance
</Typography>
<Typography variant="h5">{chequebookBalance?.availableBalance.toFixedDecimal()} BZZ</Typography>
</div>
<div>
<Typography component="h2" variant="h6" color="primary" gutterBottom>
Total Sent / Received
</Typography>
<Typography variant="h5">
{totalsent.toFixedDecimal()} / {totalreceived.toFixedDecimal()} BZZ
</Typography>
</div>
</CardContent>
)}
{isLoading && (
<div className={classes.gridContainer}>
<Skeleton width={180} height={110} animation="wave" />
<Skeleton width={180} height={110} animation="wave" />
<Skeleton width={180} height={110} animation="wave" />
</div> </div>
)} <div>
<Typography component="h2" variant="h6" color="primary" gutterBottom>
Available Uncommitted Balance
</Typography>
<Typography variant="h5">{chequebookBalance?.availableBalance.toFixedDecimal()} BZZ</Typography>
</div>
<div>
<Typography component="h2" variant="h6" color="primary" gutterBottom>
Total Sent / Received
</Typography>
<Typography variant="h5">
{totalsent?.toFixedDecimal()} / {totalreceived?.toFixedDecimal()} BZZ
</Typography>
</div>
</CardContent>
</Card> </Card>
</div> </div>
) )
+1 -1
View File
@@ -4,7 +4,7 @@ import { Table, TableBody, TableCell, TableContainer, TableRow, TableHead, Paper
import ClipboardCopy from '../../components/ClipboardCopy' import ClipboardCopy from '../../components/ClipboardCopy'
import CashoutModal from '../../components/CashoutModal' import CashoutModal from '../../components/CashoutModal'
import PeerDetailDrawer from './PeerDetail' import PeerDetailDrawer from '../../components/PeerDetail'
import { Accounting } from '../../hooks/accounting' import { Accounting } from '../../hooks/accounting'
const useStyles = makeStyles({ const useStyles = makeStyles({
+13 -39
View File
@@ -1,19 +1,12 @@
import type { ReactElement } from 'react' import { ReactElement, useContext } from 'react'
import { Theme, createStyles, makeStyles } from '@material-ui/core/styles' import { Theme, createStyles, makeStyles } from '@material-ui/core/styles'
import { Container, CircularProgress } from '@material-ui/core'
import AccountCard from '../accounting/AccountCard' import AccountCard from '../accounting/AccountCard'
import BalancesTable from './BalancesTable' import BalancesTable from './BalancesTable'
import EthereumAddressCard from '../../components/EthereumAddressCard' import EthereumAddressCard from '../../components/EthereumAddressCard'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard' import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { Context as BeeContext } from '../../providers/Bee'
import { import { Context as SettingsContext } from '../../providers/Settings'
useApiNodeAddresses,
useApiChequebookAddress,
useApiChequebookBalance,
useApiHealth,
useDebugApiHealth,
} from '../../hooks/apiHooks'
import { useAccounting } from '../../hooks/accounting' import { useAccounting } from '../../hooks/accounting'
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
@@ -29,44 +22,25 @@ const useStyles = makeStyles((theme: Theme) =>
export default function Accounting(): ReactElement { export default function Accounting(): ReactElement {
const classes = useStyles() const classes = useStyles()
const { chequebookAddress, isLoadingChequebookAddress } = useApiChequebookAddress() const { status, nodeAddresses, chequebookAddress, chequebookBalance, settlements, peerBalances } = useContext(
const { chequebookBalance, isLoadingChequebookBalance } = useApiChequebookBalance() BeeContext,
const { nodeAddresses, isLoadingNodeAddresses } = useApiNodeAddresses() )
const { health, isLoadingHealth } = useApiHealth() const { beeDebugApi } = useContext(SettingsContext)
const { nodeHealth, isLoadingNodeHealth } = useDebugApiHealth()
const { isLoading, totalsent, totalreceived, accounting, isLoadingUncashed, error } = useAccounting()
if (isLoadingHealth || isLoadingNodeHealth) { const { accounting, isLoadingUncashed } = useAccounting(beeDebugApi, settlements, peerBalances)
return (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress />
</Container>
)
}
if (nodeHealth?.status !== 'ok' || !health) return <TroubleshootConnectionCard /> if (!status.all) return <TroubleshootConnectionCard />
return ( return (
<div className={classes.root}> <div className={classes.root}>
<AccountCard <AccountCard
chequebookAddress={chequebookAddress} chequebookAddress={chequebookAddress}
isLoading={isLoadingChequebookAddress || isLoading || isLoadingChequebookBalance}
chequebookBalance={chequebookBalance} chequebookBalance={chequebookBalance}
totalsent={totalsent} totalsent={settlements?.totalSent}
totalreceived={totalreceived} totalreceived={settlements?.totalReceived}
/> />
<EthereumAddressCard <EthereumAddressCard nodeAddresses={nodeAddresses} chequebookAddress={chequebookAddress} />
nodeAddresses={nodeAddresses} <BalancesTable accounting={accounting} isLoadingUncashed={isLoadingUncashed} />
isLoadingNodeAddresses={isLoadingNodeAddresses}
chequebookAddress={chequebookAddress}
isLoadingChequebookAddress={isLoadingChequebookAddress}
/>
{error && (
<Container style={{ textAlign: 'center', padding: '50px' }}>
Error loading accounting details: {error.message}
</Container>
)}
{!error && <BalancesTable accounting={accounting} isLoadingUncashed={isLoadingUncashed} />}
</div> </div>
) )
} }
+66
View File
@@ -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>}
</>
)
}
+48
View File
@@ -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>
)
}
+114
View File
@@ -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>
)
}
+20 -137
View File
@@ -1,149 +1,32 @@
import { ReactElement, useState } from 'react' import { ReactElement, useContext } from 'react'
import { beeApi } from '../../services/bee'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' import { Container } from '@material-ui/core'
import { Paper, InputBase, IconButton, Button, Container, CircularProgress, FormHelperText } from '@material-ui/core'
import { Search } from '@material-ui/icons'
import { DropzoneArea } from 'material-ui-dropzone'
import ClipboardCopy from '../../components/ClipboardCopy'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard' import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { useApiHealth, useDebugApiHealth } from '../../hooks/apiHooks' import { Context } from '../../providers/Bee'
import { apiHost } from '../../constants' import Download from './Download'
import { Utils } from '@ethersphere/bee-js' import Upload from './Upload'
import TabsContainer from '../../components/TabsContainer'
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,
},
}),
)
export default function Files(): ReactElement { export default function Files(): ReactElement {
const classes = useStyles() const { status } = useContext(Context)
const [inputMode, setInputMode] = useState<'download' | 'upload'>('download') if (!status.all) return <TroubleshootConnectionCard />
const [referenceInput, setReferenceInput] = useState('')
const [referenceError, setReferenceError] = useState<Error | null>(null)
const { health, isLoadingHealth } = useApiHealth()
const { nodeHealth, isLoadingNodeHealth } = useDebugApiHealth()
const [file, setFile] = useState<File | null>(null)
const [uploadReference, setUploadReference] = useState('')
const [uploadError, setUploadError] = useState<Error | null>(null)
const [isUploadingFile, setIsUploadingFile] = useState(false)
const uploadFile = () => {
if (file === null) return
setIsUploadingFile(true)
setUploadError(null)
beeApi.files
.uploadFile(file)
.then(hash => {
setUploadReference(hash)
setFile(null)
})
.catch(setUploadError)
.finally(() => {
setIsUploadingFile(false)
})
}
const handleChange = (files?: File[]) => {
if (files) {
setFile(files[0])
setUploadReference('')
}
}
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 />
return ( return (
<Container maxWidth="sm"> <Container maxWidth="sm">
<div style={{ marginBottom: '7px' }}> <TabsContainer
<Button color="primary" style={{ marginRight: '7px' }} onClick={() => setInputMode('download')}> values={[
Download {
</Button> label: 'download',
<Button color="primary" onClick={() => setInputMode('upload')}> component: <Download />,
Upload },
</Button> {
</div> label: 'upload',
{inputMode === 'download' && ( component: <Upload />,
<> },
<Paper className={classes.root}> ]}
<InputBase />
className={classes.input}
placeholder="Enter swarm reference e.g. 0773a91efd6547c754fc1d95fb1c62c7d1b47f959c2caa685dfec8736da95c1c"
inputProps={{ 'aria-label': 'retriefe file from swarm' }}
value={referenceInput}
onChange={handleReferenceChange}
/>
<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>}
</>
)}
{inputMode === 'upload' && (
<div>
<div>
<DropzoneArea onChange={handleChange} filesLimit={1} />
<div style={{ marginTop: '15px' }}>
<Button disabled={!file && isUploadingFile} onClick={() => uploadFile()} className={classes.iconButton}>
Upload
</Button>
{isUploadingFile && (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress />
</Container>
)}
{uploadReference && (
<div style={{ marginBottom: '15px', display: 'flex' }}>
<span>{uploadReference}</span>
<ClipboardCopy value={uploadReference} />
</div>
)}
{uploadError && <FormHelperText error>{uploadError.message}</FormHelperText>}
</div>
</div>
</div>
)}
</Container> </Container>
) )
} }
+27 -45
View File
@@ -1,4 +1,4 @@
import React, { ReactElement, useState } from 'react' import { ReactElement, useState, useContext } from 'react'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import { import {
Table, Table,
@@ -10,12 +10,11 @@ import {
Button, Button,
Paper, Paper,
Tooltip, Tooltip,
Container,
CircularProgress, CircularProgress,
} from '@material-ui/core' } from '@material-ui/core'
import { Autorenew } from '@material-ui/icons' import { Autorenew } from '@material-ui/icons'
import { beeDebugApi } from '../../services/bee' import { Context as SettingsContext } from '../../providers/Settings'
import type { Peer } from '@ethersphere/bee-js' import type { Peer } from '@ethersphere/bee-js'
const useStyles = makeStyles({ const useStyles = makeStyles({
@@ -26,47 +25,43 @@ const useStyles = makeStyles({
interface Props { interface Props {
peers: Peer[] | null peers: Peer[] | null
isLoading: boolean }
error: Error | null
interface PeerLatency {
rtt: string
loading: boolean
}
function getPingState(peerLatency: Record<string, PeerLatency>, peer: Peer): ReactElement {
if (peerLatency[peer.address]?.loading) return <CircularProgress size={20} />
if (peerLatency[peer.address]?.rtt) return <span>{peerLatency[peer.address]?.rtt}</span>
return <Autorenew />
} }
function PeerTable(props: Props): ReactElement { function PeerTable(props: Props): ReactElement {
const classes = useStyles() const classes = useStyles()
const { beeDebugApi } = useContext(SettingsContext)
const [peerLatency, setPeerLatency] = useState([{ peerId: '', rtt: '', loading: false }]) const [peerLatency, setPeerLatency] = useState<Record<string, PeerLatency>>({})
const PingPeer = (peerId: string) => { const pingPeer = (peerId: string) => {
setPeerLatency([...peerLatency, { peerId: peerId, rtt: '', loading: true }]) setPeerLatency(prevPeerLatency => ({ ...prevPeerLatency, [peerId]: { rtt: '', loading: true } }))
beeDebugApi.connectivity beeDebugApi
.ping(peerId) ?.pingPeer(peerId)
.then(res => { .then(res => {
setPeerLatency([...peerLatency, { peerId: peerId, rtt: res.rtt, loading: false }]) setPeerLatency(prevPeerLatency => ({ ...prevPeerLatency, [peerId]: { rtt: res.rtt, loading: false } }))
}) })
.catch(() => { .catch(() => {
setPeerLatency([...peerLatency, { peerId: peerId, rtt: 'error', loading: false }]) setPeerLatency(prevPeerLatency => ({ ...prevPeerLatency, [peerId]: { rtt: 'error', loading: false } }))
}) })
} }
if (props.isLoading) {
return (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress />
</Container>
)
}
if (props.error || props.peers === null) {
return (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<p>Failed to load peers</p>
</Container>
)
}
return ( return (
<div> <div>
<TableContainer component={Paper}> <TableContainer component={Paper}>
<Table className={classes.table} aria-label="simple table"> <Table className={classes.table}>
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell>Index</TableCell> <TableCell>Index</TableCell>
@@ -75,7 +70,7 @@ function PeerTable(props: Props): ReactElement {
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{props.peers.map((peer: Peer, idx: number) => ( {props.peers?.map((peer: Peer, idx: number) => (
<TableRow key={peer.address}> <TableRow key={peer.address}>
<TableCell component="th" scope="row"> <TableCell component="th" scope="row">
{idx + 1} {idx + 1}
@@ -83,21 +78,8 @@ function PeerTable(props: Props): ReactElement {
<TableCell>{peer.address}</TableCell> <TableCell>{peer.address}</TableCell>
<TableCell align="right"> <TableCell align="right">
<Tooltip title="Ping node"> <Tooltip title="Ping node">
<Button color="primary" onClick={() => PingPeer(peer.address)}> <Button color="primary" onClick={() => pingPeer(peer.address)}>
{ {getPingState(peerLatency, peer)}
// FIXME: this should be broken up
/* eslint-disable no-nested-ternary */
peerLatency.find(item => item.peerId === peer.address) ? (
peerLatency.filter(item => item.peerId === peer.address)[0].loading ? (
<CircularProgress size={20} />
) : (
peerLatency.filter(item => item.peerId === peer.address)[0].rtt
)
) : (
<Autorenew />
)
/* eslint-enable no-nested-ternary */
}
</Button> </Button>
</Tooltip> </Tooltip>
</TableCell> </TableCell>
+6 -17
View File
@@ -1,32 +1,21 @@
import { Container, CircularProgress } from '@material-ui/core/'
import PeerTable from './PeerTable' import PeerTable from './PeerTable'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard' import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { useApiNodeTopology, useApiNodePeers, useDebugApiHealth } from '../../hooks/apiHooks' import { Context } from '../../providers/Bee'
import TopologyStats from '../../components/TopologyStats' import TopologyStats from '../../components/TopologyStats'
import { ReactElement } from 'react' import { ReactElement, useContext } from 'react'
export default function Peers(): ReactElement { export default function Peers(): ReactElement {
const topology = useApiNodeTopology() const { topology, peers, status } = useContext(Context)
const debugHealth = useDebugApiHealth()
const peers = useApiNodePeers()
if (debugHealth.isLoadingNodeHealth) { if (!status.all) {
return (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress />
</Container>
)
}
if (debugHealth.error) {
return <TroubleshootConnectionCard /> return <TroubleshootConnectionCard />
} }
return ( return (
<> <>
<TopologyStats {...topology} /> <TopologyStats topology={topology} />
<PeerTable {...peers} /> <PeerTable peers={peers} />
</> </>
) )
} }
+14 -30
View File
@@ -1,26 +1,20 @@
import React, { ReactElement, useState } from 'react' import React, { ReactElement, useState, useContext } from 'react'
import { Paper, Container, TextField, Typography, Button } from '@material-ui/core' import { Paper, Container, TextField, Typography, Button } from '@material-ui/core'
import { Context as SettingsContext } from '../../providers/Settings'
export default function Settings(): ReactElement { export default function Settings(): ReactElement {
const [refreshVisibility, toggleRefreshVisibility] = useState(false) const { apiUrl, apiDebugUrl, setApiUrl, setDebugApiUrl } = useContext(SettingsContext)
const [host, setHost] = useState('') const [host, setHost] = useState(apiUrl)
const [debugHost, setDebugHost] = useState('') const [debugHost, setDebugHost] = useState(apiDebugUrl)
const handleNewHostConnection = () => { const submit = () => {
if (host) { if (host !== apiUrl) setApiUrl(host)
sessionStorage.setItem('api_host', host)
}
if (debugHost) { if (debugHost !== apiDebugUrl) setDebugApiUrl(debugHost)
sessionStorage.setItem('debug_api_host', debugHost)
}
if (host || debugHost) {
toggleRefreshVisibility(!refreshVisibility)
window.location.reload()
}
} }
const touched = host !== apiUrl || debugHost !== apiDebugUrl
return ( return (
<div> <div>
<Container> <Container>
@@ -29,42 +23,32 @@ export default function Settings(): ReactElement {
</Typography> </Typography>
<Paper> <Paper>
<TextField <TextField
id="filled-full-width"
label="API Endpoint" label="API Endpoint"
style={{ margin: 0 }} style={{ margin: 0 }}
placeholder="ex: 127.0.0.0.1:1633" placeholder="ex: 127.0.0.0.1:1633"
helperText="Enter node host override / port" helperText="Enter node host override / port"
fullWidth fullWidth
defaultValue={ defaultValue={apiUrl}
sessionStorage.getItem('api_host') ? sessionStorage.getItem('api_host') : process.env.REACT_APP_BEE_HOST
}
margin="normal" margin="normal"
InputLabelProps={{ InputLabelProps={{
shrink: true, shrink: true,
}} }}
onChange={e => { onChange={e => {
setHost(e.target.value) setHost(e.target.value)
toggleRefreshVisibility(true)
}} }}
variant="filled" variant="filled"
/> />
</Paper> </Paper>
<Paper style={{ marginTop: '20px' }}> <Paper style={{ marginTop: '20px' }}>
<TextField <TextField
id="filled-full-width"
label="Debug API Endpoint" label="Debug API Endpoint"
style={{ margin: 0 }} style={{ margin: 0 }}
placeholder="ex: 127.0.0.0.1:1635" placeholder="ex: 127.0.0.0.1:1635"
helperText="Enter node debug host override / port" helperText="Enter node debug host override / port"
fullWidth fullWidth
defaultValue={ defaultValue={apiDebugUrl}
sessionStorage.getItem('debug_api_host')
? sessionStorage.getItem('debug_api_host')
: process.env.REACT_APP_BEE_DEBUG_HOST
}
onChange={e => { onChange={e => {
setDebugHost(e.target.value) setDebugHost(e.target.value)
toggleRefreshVisibility(true)
}} }}
margin="normal" margin="normal"
InputLabelProps={{ InputLabelProps={{
@@ -73,9 +57,9 @@ export default function Settings(): ReactElement {
variant="filled" variant="filled"
/> />
</Paper> </Paper>
{refreshVisibility ? ( {touched ? (
<div style={{ marginTop: '20px' }}> <div style={{ marginTop: '20px' }}>
<Button variant="outlined" color="primary" onClick={() => handleNewHostConnection()}> <Button variant="outlined" color="primary" onClick={submit}>
Save Save
</Button> </Button>
</div> </div>
@@ -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>
)
}
+54
View File
@@ -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
+70
View File
@@ -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>
)
}
+41 -49
View File
@@ -1,7 +1,7 @@
import { ReactElement, useEffect, useState } from 'react' import { ReactElement, useEffect, useState, useContext } from 'react'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import { Typography, Paper, Button, Step, StepLabel, StepContent, Stepper, StepButton } from '@material-ui/core/' import { Typography, Paper, Button, Step, StepLabel, StepContent, Stepper, StepButton } from '@material-ui/core/'
import { CheckCircle, Error, Sync, ExpandLessSharp, ExpandMoreSharp, Autorenew } from '@material-ui/icons/' import { CheckCircle, Error, ExpandLessSharp, ExpandMoreSharp, Autorenew } from '@material-ui/icons/'
import DebugConnectionCheck from './SetupSteps/DebugConnectionCheck' import DebugConnectionCheck from './SetupSteps/DebugConnectionCheck'
import NodeConnectionCheck from './SetupSteps/NodeConnectionCheck' import NodeConnectionCheck from './SetupSteps/NodeConnectionCheck'
@@ -9,7 +9,7 @@ import VersionCheck from './SetupSteps/VersionCheck'
import EthereumConnectionCheck from './SetupSteps/EthereumConnectionCheck' import EthereumConnectionCheck from './SetupSteps/EthereumConnectionCheck'
import ChequebookDeployFund from './SetupSteps/ChequebookDeployFund' import ChequebookDeployFund from './SetupSteps/ChequebookDeployFund'
import PeerConnection from './SetupSteps/PeerConnection' import PeerConnection from './SetupSteps/PeerConnection'
import { StatusChequebookHook } from '../../hooks/status' import { Context } from '../../providers/Bee'
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
createStyles({ createStyles({
@@ -30,66 +30,65 @@ const useStyles = makeStyles((theme: Theme) =>
interface Step { interface Step {
label: string label: string
isOk: boolean isOk: boolean
isLoading: boolean
component: ReactElement component: ReactElement
} }
interface Props { export default function NodeSetupWorkflow(): ReactElement {
nodeVersion: StatusNodeVersionHook
ethereumConnection: StatusEthereumConnectionHook
debugApiConnection: StatusHookCommon
apiConnection: StatusHookCommon
topology: StatusTopologyHook
chequebook: StatusChequebookHook
}
export default function NodeSetupWorkflow({
nodeVersion,
ethereumConnection,
debugApiConnection,
apiConnection,
topology,
chequebook,
}: Props): ReactElement {
const classes = useStyles() const classes = useStyles()
const [activeStep, setActiveStep] = useState(-1) const [activeStep, setActiveStep] = useState(-1)
const {
status,
isLoading,
latestUserVersion,
latestPublishedVersion,
isLatestBeeVersion,
latestBeeVersionUrl,
topology,
nodeAddresses,
chequebookAddress,
} = useContext(Context)
const steps: Step[] = [ const steps: Step[] = [
{ {
label: 'Connected to Node DebugAPI', label: 'Connected to Node DebugAPI',
isOk: debugApiConnection.isOk, isOk: status.debugApiConnection,
isLoading: debugApiConnection.isLoading, component: <DebugConnectionCheck isOk={status.debugApiConnection} />,
component: <DebugConnectionCheck {...debugApiConnection} />,
}, },
{ {
label: 'Running latest Bee version', label: 'Running latest Bee version',
isOk: nodeVersion.isOk, isOk: status.version,
isLoading: nodeVersion.isLoading, component: (
component: <VersionCheck {...nodeVersion} />, <VersionCheck
isOk={status.version}
isLatestBeeVersion={isLatestBeeVersion}
userVersion={latestUserVersion}
latestVersion={latestPublishedVersion}
latestUrl={latestBeeVersionUrl}
/>
),
}, },
{ {
label: 'Connected to Ethereum Blockchain', label: 'Connected to xDai Blockchain',
isOk: ethereumConnection.isOk, isOk: status.blockchainConnection,
isLoading: ethereumConnection.isLoading, component: <EthereumConnectionCheck isOk={status.blockchainConnection} nodeAddresses={nodeAddresses} />,
component: <EthereumConnectionCheck {...ethereumConnection} />,
}, },
{ {
label: 'Deployed and Funded Chequebook', label: 'Deployed and Funded Chequebook',
isOk: chequebook.isOk, isOk: status.chequebook,
isLoading: chequebook.isLoading, component: (
component: <ChequebookDeployFund ethereumAddress={ethereumConnection.nodeAddresses?.ethereum} {...chequebook} />, <ChequebookDeployFund chequebookAddress={chequebookAddress?.chequebookAddress} isOk={status.chequebook} />
),
}, },
{ {
label: 'Connected to Node API', label: 'Connected to Node API',
isOk: apiConnection.isOk, isOk: status.apiConnection,
isLoading: apiConnection.isLoading, component: <NodeConnectionCheck isOk={status.apiConnection} />,
component: <NodeConnectionCheck {...apiConnection} />,
}, },
{ {
label: 'Connected to Peers', label: 'Connected to Peers',
isOk: topology.isOk, isOk: status.topology,
isLoading: topology.isLoading, component: <PeerConnection isOk={status.topology} topology={topology} />,
component: <PeerConnection {...topology} />,
}, },
] ]
@@ -98,7 +97,7 @@ export default function NodeSetupWorkflow({
if (activeStep >= 0 && activeStep < steps.length) return if (activeStep >= 0 && activeStep < steps.length) return
// If any step is not fully loaded yet return // If any step is not fully loaded yet return
if (!steps.every(step => !step.isLoading)) return if (!isLoading) return
// Select first step that is not OK // Select first step that is not OK
// This is deliberately a for loop (and not forEach) so that we can terminate the useEffect from within the cycle // This is deliberately a for loop (and not forEach) so that we can terminate the useEffect from within the cycle
@@ -123,18 +122,11 @@ export default function NodeSetupWorkflow({
<Paper className={classes.root}> <Paper className={classes.root}>
<Typography variant="h5" gutterBottom> <Typography variant="h5" gutterBottom>
Node Setup Node Setup
<span style={{ marginLeft: '25px' }}>
<Button variant="outlined" size="small" onClick={() => window.location.reload()}>
<Sync />
<span style={{ marginLeft: '7px' }}>Refresh Checks</span>
</Button>
</span>
</Typography> </Typography>
<Stepper nonLinear activeStep={activeStep} orientation="vertical"> <Stepper nonLinear activeStep={activeStep} orientation="vertical">
{steps.map(({ label, isOk, component, isLoading }, index) => ( {steps.map(({ label, isOk, component }, index) => (
<Step key={label}> <Step key={label}>
<StepLabel <StepLabel
disabled={isLoading}
onClick={() => setActiveStep(index === activeStep ? steps.length : index)} onClick={() => setActiveStep(index === activeStep ? steps.length : index)}
StepIconComponent={() => { StepIconComponent={() => {
if (isLoading) return <Autorenew style={{ height: '25px', cursor: 'pointer' }} /> if (isLoading) return <Autorenew style={{ height: '25px', cursor: 'pointer' }} />
@@ -2,33 +2,25 @@ import { Typography } from '@material-ui/core/'
import EthereumAddress from '../../../components/EthereumAddress' import EthereumAddress from '../../../components/EthereumAddress'
import DepositModal from '../../../containers/DepositModal' import DepositModal from '../../../containers/DepositModal'
import type { ReactElement } from 'react' import type { ReactElement } from 'react'
import type { StatusChequebookHook } from '../../../hooks/status'
interface Props extends StatusChequebookHook { interface Props extends StatusHookCommon {
ethereumAddress?: string chequebookAddress?: string
} }
const ChequebookDeployFund = ({ const ChequebookDeployFund = ({ chequebookAddress, isOk }: Props): ReactElement | null => {
isLoading,
chequebookAddress,
chequebookBalance,
ethereumAddress,
}: Props): ReactElement | null => {
if (isLoading) return null
return ( return (
<div> <div>
<p style={{ marginBottom: '20px', display: 'flex' }}> <p style={{ marginBottom: '20px', display: 'flex' }}>{chequebookAddress && <DepositModal />}</p>
{chequebookAddress?.chequebookaddress && <DepositModal />}
</p>
<div style={{ marginBottom: '10px' }}> <div style={{ marginBottom: '10px' }}>
{!(chequebookAddress?.chequebookaddress && chequebookBalance?.totalBalance.toBigNumber.isGreaterThan(0)) && ( {!isOk && (
<div> <div>
<span> <span>
Your chequebook is either not deployed or funded. Join{' '} Your chequebook is either not deployed or funded. To run the node you will need xDAI and xBZZ on the xDai
<a href="https://discord.gg/ykCupZMuww">our discord channel</a>, get verified and send a message{' '} network. You may need to aquire BZZ through (e.g. <a href="https://bzz.exchange/">bzz.exchange</a>) and
<pre>sprinkle {ethereumAddress || '<YOUR BEE NODE ETH ADDRESS>'}</pre> in the <pre>#faucet-request</pre>{' '} bridge it to the xDai network through the <a href="https://omni.xdaichain.com/bridge">omni bridge</a>. To
channel to get Goerli ETH and Goerli BZZ token. 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> </span>
</div> </div>
)} )}
@@ -36,7 +28,7 @@ const ChequebookDeployFund = ({
<Typography variant="subtitle1" gutterBottom> <Typography variant="subtitle1" gutterBottom>
Chequebook Address Chequebook Address
</Typography> </Typography>
<EthereumAddress address={chequebookAddress?.chequebookaddress} network={'goerli'} /> <EthereumAddress address={chequebookAddress} />
</div> </div>
) )
} }
@@ -1,23 +1,29 @@
import type { ReactElement } from 'react' import { ReactElement, useContext } from 'react'
import { Typography, Accordion, AccordionSummary, AccordionDetails } from '@material-ui/core/' import { Typography, Accordion, AccordionSummary, AccordionDetails } from '@material-ui/core/'
import MuiAlert from '@material-ui/lab/Alert' import MuiAlert from '@material-ui/lab/Alert'
import { ExpandMoreSharp } from '@material-ui/icons/' import { ExpandMoreSharp } from '@material-ui/icons/'
import ConnectToHost from '../../../components/ConnectToHost' import ConnectToHost from '../../../components/ConnectToHost'
import CodeBlockTabs from '../../../components/CodeBlockTabs' import CodeBlockTabs from '../../../components/CodeBlockTabs'
import { debugApiHost } from '../../../constants' import { Context as SettingsContext } from '../../../providers/Settings'
type Props = StatusHookCommon type Props = StatusHookCommon
export default function NodeConnectionCheck({ isLoading, isOk }: Props): ReactElement | null { export default function NodeConnectionCheck({ isOk }: Props): ReactElement | null {
if (isLoading) return null const { setDebugApiUrl, apiDebugUrl } = useContext(SettingsContext)
const changeDebugApiUrl = ( const changeDebugApiUrl = (
<div style={{ display: 'flex', marginTop: '25px', marginBottom: '25px' }}> <div style={{ display: 'flex', marginTop: '25px', marginBottom: '25px' }}>
<span style={{ marginRight: '15px' }}> <span style={{ marginRight: '15px' }}>
Debug API (<Typography variant="button">{debugApiHost}</Typography>) Debug API (<Typography variant="button">{apiDebugUrl}</Typography>)
</span> </span>
<ConnectToHost hostName={'debug_api_host'} defaultHost={debugApiHost} /> <ConnectToHost
setHost={(host: string) => {
console.log(host) // eslint-disable-line
setDebugApiUrl(host)
}}
defaultHost={apiDebugUrl}
/>
</div> </div>
) )
@@ -31,7 +37,7 @@ export default function NodeConnectionCheck({ isLoading, isOk }: Props): ReactEl
<div> <div>
<Typography component="div" variant="body2" gutterBottom style={{ margin: '15px' }}> <Typography component="div" variant="body2" gutterBottom style={{ margin: '15px' }}>
We cannot connect to your nodes debug API at <Typography variant="button">{debugApiHost}</Typography>. Please We cannot connect to your nodes debug API at <Typography variant="button">{apiDebugUrl}</Typography>. Please
check the following to troubleshoot your issue. check the following to troubleshoot your issue.
<Accordion style={{ marginTop: '20px' }}> <Accordion style={{ marginTop: '20px' }}>
<AccordionSummary expandIcon={<ExpandMoreSharp />} aria-controls="panel1a-content" id="panel1a-header"> <AccordionSummary expandIcon={<ExpandMoreSharp />} aria-controls="panel1a-content" id="panel1a-header">
@@ -41,11 +47,7 @@ export default function NodeConnectionCheck({ isLoading, isOk }: Props): ReactEl
<Typography component="div"> <Typography component="div">
<ol> <ol>
<li>Check the status of your node by running the below command to see if your node is running.</li> <li>Check the status of your node by running the below command to see if your node is running.</li>
<CodeBlockTabs <CodeBlockTabs showLineNumbers linux={`sudo systemctl status bee`} mac={`brew services list`} />
showLineNumbers
linux={`sudo systemctl status bee`}
mac={`brew services status swarm-bee`}
/>
<li> <li>
If your node is running, check your firewall settings to make sure that port 1635 (or your custom 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 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 <CodeBlockTabs
showLineNumbers showLineNumbers
linux={`sudo systemctl status bee \njournalctl --lines=100 --follow --unit bee`} 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> <li>
Lastly, check your nodes configuration settings to validate the debug API is enabled and the Cross 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{' '} 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>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 <strong>cors-allowed-origins</strong> must be set to your host domain or IP (you can also use the
the configuration run the restart command below for changes to take effect. wildcard <code>{"cors-allowed-origins: ['*']"}</code>). If edits are made to the configuration run
the restart command below for changes to take effect.
</li> </li>
<CodeBlockTabs <CodeBlockTabs
showLineNumbers showLineNumbers
linux={`sudo vi /etc/bee/bee.yaml\nsudo systemctl restart bee`} 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> </ol>
</Typography> </Typography>
@@ -4,37 +4,31 @@ import EthereumAddress from '../../../components/EthereumAddress'
type Props = StatusEthereumConnectionHook type Props = StatusEthereumConnectionHook
export default function EthereumConnectionCheck({ isLoading, isOk, nodeAddresses }: Props): ReactElement | null { export default function EthereumConnectionCheck({ isOk, nodeAddresses }: Props): ReactElement | null {
if (isLoading) return null
if (isOk) { if (isOk) {
return ( return (
<div> <div>
<Typography variant="subtitle1" gutterBottom> <Typography variant="subtitle1" gutterBottom>
Node Address Node Address
</Typography> </Typography>
<EthereumAddress address={nodeAddresses?.ethereum} network={'goerli'} /> <EthereumAddress address={nodeAddresses?.ethereum} />
</div> </div>
) )
} }
return ( return (
<p> <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{' '} contract. You can run{' '}
<a href="https://github.com/goerli/testnet" rel="noreferrer" target="_blank"> <a href="https://www.xdaichain.com/" rel="noreferrer" target="_blank">
your own Goerli node your own xDai node
</a> </a>
, or use a provider such as{' '} , or use a provider instead - we recommend{' '}
<a href="https://rpc.slock.it/goerli" rel="noreferrer" target="_blank"> <a href="https://getblock.io/" rel="noreferrer" target="_blank">
rpc.slock.it/goerli Getblock
</a>{' '}
or{' '}
<a href="https://infura.io/" rel="noreferrer" target="_blank">
Infura
</a> </a>
. By default, Bee expects a local Goerli node at http://localhost:8545. To use a provider instead, simply change . By default, Bee expects a local node at http://localhost:8545. To use a provider instead, simply change the{' '}
your <strong>--swap-endpoint</strong> in your configuration file. <strong>swap-endpoint</strong> in your configuration file.
</p> </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 { Typography, Accordion, AccordionSummary, AccordionDetails } from '@material-ui/core/'
import { ExpandMoreSharp } from '@material-ui/icons/' import { ExpandMoreSharp } from '@material-ui/icons/'
import ConnectToHost from '../../../components/ConnectToHost' import ConnectToHost from '../../../components/ConnectToHost'
import CodeBlockTabs from '../../../components/CodeBlockTabs' import CodeBlockTabs from '../../../components/CodeBlockTabs'
import { apiHost } from '../../../constants' import { Context as SettingsContext } from '../../../providers/Settings'
type Props = StatusHookCommon type Props = StatusHookCommon
export default function NodeConnectionCheck({ isLoading, isOk }: Props): ReactElement | null { export default function NodeConnectionCheck({ isOk }: Props): ReactElement | null {
if (isLoading) return null const { setApiUrl, apiUrl } = useContext(SettingsContext)
return ( return (
<div> <div>
<div style={{ display: 'flex', marginBottom: '25px' }}> <div style={{ display: 'flex', marginBottom: '25px' }}>
<span style={{ marginRight: '15px' }}> <span style={{ marginRight: '15px' }}>
Node API (<Typography variant="button">{apiHost}</Typography>) Node API (<Typography variant="button">{apiUrl}</Typography>)
</span> </span>
<ConnectToHost hostName="api_host" defaultHost={apiHost} /> <ConnectToHost setHost={setApiUrl} defaultHost={apiUrl} />
</div> </div>
<div> <div>
{!isOk && ( {!isOk && (
<Typography component="div" variant="body2" gutterBottom style={{ margin: '15px' }}> <Typography component="div" variant="body2" gutterBottom style={{ margin: '15px' }}>
We cannot connect to your nodes API at <Typography variant="button">{apiHost}</Typography>. Please check the We cannot connect to your nodes API at <Typography variant="button">{apiUrl}</Typography>. Please check the
following to troubleshoot your issue. following to troubleshoot your issue.
<Accordion style={{ marginTop: '20px' }}> <Accordion style={{ marginTop: '20px' }}>
<AccordionSummary expandIcon={<ExpandMoreSharp />} aria-controls="panel1a-content" id="panel1a-header"> <AccordionSummary expandIcon={<ExpandMoreSharp />} aria-controls="panel1a-content" id="panel1a-header">
@@ -32,11 +32,7 @@ export default function NodeConnectionCheck({ isLoading, isOk }: Props): ReactEl
<Typography component="div"> <Typography component="div">
<ol> <ol>
<li>Check the status of your node by running the below command to see if your node is running.</li> <li>Check the status of your node by running the below command to see if your node is running.</li>
<CodeBlockTabs <CodeBlockTabs showLineNumbers linux={`sudo systemctl status bee`} mac={`brew services list`} />
showLineNumbers
linux={`sudo systemctl status bee`}
mac={`brew services status swarm-bee`}
/>
<li> <li>
If your node is running, check your firewall settings to make sure that port 1633 (or your custom 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 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 <CodeBlockTabs
showLineNumbers showLineNumbers
linux={`sudo systemctl status bee \njournalctl --lines=100 --follow --unit bee`} 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> </ol>
</Typography> </Typography>
@@ -3,9 +3,7 @@ import { Typography } from '@material-ui/core/'
type Props = StatusTopologyHook type Props = StatusTopologyHook
export default function PeerConnection({ isLoading, isOk, topology }: Props): ReactElement | null { export default function PeerConnection({ isOk, topology }: Props): ReactElement | null {
if (isLoading) return null
const peers = ( const peers = (
<div style={{ display: 'flex', marginTop: '15px' }}> <div style={{ display: 'flex', marginTop: '15px' }}>
<div style={{ marginRight: '30px' }}> <div style={{ marginRight: '30px' }}>
+1 -9
View File
@@ -4,15 +4,7 @@ import CodeBlockTabs from '../../../components/CodeBlockTabs'
type Props = StatusNodeVersionHook type Props = StatusNodeVersionHook
export default function VersionCheck({ export default function VersionCheck({ isOk, userVersion, latestVersion, latestUrl }: Props): ReactElement | null {
isLoading,
isOk,
userVersion,
latestVersion,
latestUrl,
}: Props): ReactElement | null {
if (isLoading) return null
const version = ( const version = (
<div style={{ display: 'flex' }}> <div style={{ display: 'flex' }}>
<div style={{ marginRight: '30px' }}> <div style={{ marginRight: '30px' }}>
+11 -7
View File
@@ -23,17 +23,19 @@ const useStyles = makeStyles(() =>
interface Props { interface Props {
nodeAddresses: NodeAddresses | null nodeAddresses: NodeAddresses | null
nodeTopology: Topology | null nodeTopology: Topology | null
userBeeVersion: string | null userBeeVersion?: string
latestBeeVersion: string | null isLatestBeeVersion: boolean
isOk: boolean isOk: boolean
latestUrl: string
} }
function StatusCard({ function StatusCard({
userBeeVersion, userBeeVersion,
nodeAddresses, nodeAddresses,
nodeTopology, nodeTopology,
latestBeeVersion,
isOk, isOk,
isLatestBeeVersion,
latestUrl,
}: Props): ReactElement | null { }: Props): ReactElement | null {
const classes = useStyles() const classes = useStyles()
@@ -72,7 +74,7 @@ function StatusCard({
Bee Bee
</a>{' '} </a>{' '}
<span>{userBeeVersion || '-'}</span> <span>{userBeeVersion || '-'}</span>
{userBeeVersion && latestBeeVersion && userBeeVersion === latestBeeVersion ? ( {isLatestBeeVersion ? (
<Chip <Chip
style={{ marginLeft: '7px', color: '#2145a0' }} style={{ marginLeft: '7px', color: '#2145a0' }}
size="small" size="small"
@@ -80,16 +82,18 @@ function StatusCard({
className={classes.status} className={classes.status}
/> />
) : ( ) : (
<Typography variant="button">update</Typography> <Button size="small" variant="outlined" href={latestUrl}>
update
</Button>
)} )}
</Typography> </Typography>
<Typography component="div" variant="subtitle2" gutterBottom> <Typography component="div" variant="subtitle2" gutterBottom>
<span>PUBLIC KEY: </span> <span>PUBLIC KEY: </span>
<span>{nodeAddresses?.public_key ? nodeAddresses.public_key : '-'}</span> <span>{nodeAddresses?.publicKey ? nodeAddresses.publicKey : '-'}</span>
</Typography> </Typography>
<Typography component="div" variant="subtitle2" gutterBottom> <Typography component="div" variant="subtitle2" gutterBottom>
<span>PSS PUBLIC KEY: </span> <span>PSS PUBLIC KEY: </span>
<span>{nodeAddresses?.pss_public_key ? nodeAddresses.pss_public_key : '-'}</span> <span>{nodeAddresses?.pssPublicKey ? nodeAddresses.pssPublicKey : '-'}</span>
</Typography> </Typography>
<Typography component="div" variant="subtitle2" gutterBottom> <Typography component="div" variant="subtitle2" gutterBottom>
<span>OVERLAY ADDRESS (PEER ID): </span> <span>OVERLAY ADDRESS (PEER ID): </span>
+20 -47
View File
@@ -1,18 +1,10 @@
import { ReactElement } from 'react' import { ReactElement, useContext } from 'react'
import { Container, CircularProgress } from '@material-ui/core'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import NodeSetupWorkflow from './NodeSetupWorkflow' import NodeSetupWorkflow from './NodeSetupWorkflow'
import StatusCard from './StatusCard' import StatusCard from './StatusCard'
import EthereumAddressCard from '../../components/EthereumAddressCard' import EthereumAddressCard from '../../components/EthereumAddressCard'
import { import { Context as BeeContext } from '../../providers/Bee'
useStatusEthereumConnection,
useStatusNodeVersion,
useStatusDebugConnection,
useStatusConnection,
useStatusTopology,
useStatusChequebook,
} from '../../hooks/status'
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
createStyles({ createStyles({
@@ -27,49 +19,30 @@ const useStyles = makeStyles((theme: Theme) =>
export default function Status(): ReactElement { export default function Status(): ReactElement {
const classes = useStyles() const classes = useStyles()
const nodeVersion = useStatusNodeVersion() const {
const ethereumConnection = useStatusEthereumConnection() status,
const debugApiConnection = useStatusDebugConnection() latestUserVersion,
const apiConnection = useStatusConnection() isLatestBeeVersion,
const topology = useStatusTopology() latestBeeVersionUrl,
const chequebook = useStatusChequebook() topology,
nodeAddresses,
const checks = [nodeVersion, ethereumConnection, debugApiConnection, apiConnection, topology, chequebook] chequebookAddress,
} = useContext(BeeContext)
// If any check data are still loading
if (!checks.every(c => !c.isLoading)) {
return (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress />
</Container>
)
}
return ( return (
<div className={classes.root}> <div className={classes.root}>
<StatusCard <StatusCard
userBeeVersion={nodeVersion.userVersion} userBeeVersion={latestUserVersion}
latestBeeVersion={nodeVersion.latestVersion} isLatestBeeVersion={isLatestBeeVersion}
isOk={checks.every(c => c.isOk)} isOk={status.all}
nodeTopology={topology.topology} nodeTopology={topology}
nodeAddresses={ethereumConnection.nodeAddresses} latestUrl={latestBeeVersionUrl}
nodeAddresses={nodeAddresses}
/> />
{ethereumConnection.nodeAddresses && chequebook.chequebookAddress && ( {nodeAddresses && chequebookAddress && (
<EthereumAddressCard <EthereumAddressCard nodeAddresses={nodeAddresses} chequebookAddress={chequebookAddress} />
nodeAddresses={ethereumConnection.nodeAddresses}
isLoadingNodeAddresses={ethereumConnection.isLoading}
chequebookAddress={chequebook.chequebookAddress}
isLoadingChequebookAddress={chequebook.isLoading}
/>
)} )}
<NodeSetupWorkflow <NodeSetupWorkflow />
nodeVersion={nodeVersion}
ethereumConnection={ethereumConnection}
debugApiConnection={debugApiConnection}
apiConnection={apiConnection}
topology={topology}
chequebook={chequebook}
/>
</div> </div>
) )
} }
+351
View File
@@ -0,0 +1,351 @@
import type { ChequebookBalance, Balance, Settlements } from '../types'
import { createContext, ReactChild, ReactElement, useEffect, useState, useContext } from 'react'
import { Token } from '../models/Token'
import semver from 'semver'
import { engines } from '../../package.json'
import { Context as SettingsContext } from './Settings'
import type {
NodeAddresses,
ChequebookAddressResponse,
LastChequesResponse,
Health,
Peer,
Topology,
} from '@ethersphere/bee-js'
import { useLatestBeeRelease } from '../hooks/apiHooks'
interface Status {
all: boolean
version: boolean
blockchainConnection: boolean
debugApiConnection: boolean
apiConnection: boolean
topology: boolean
chequebook: boolean
}
interface ContextInterface {
status: Status
latestPublishedVersion?: string
latestUserVersion?: string
latestUserVersionExact?: string
isLatestBeeVersion: boolean
latestBeeVersionUrl: string
error: Error | null
apiHealth: boolean
debugApiHealth: Health | null
nodeAddresses: NodeAddresses | null
topology: Topology | null
chequebookAddress: ChequebookAddressResponse | null
peers: Peer[] | null
chequebookBalance: ChequebookBalance | null
peerBalances: Balance[] | null
peerCheques: LastChequesResponse | null
settlements: Settlements | null
latestBeeRelease: LatestBeeRelease | null
isLoading: boolean
isRefreshing: boolean
lastUpdate: number | null
start: (frequency?: number) => void
stop: () => void
refresh: () => Promise<void>
}
const initialValues: ContextInterface = {
status: {
all: false,
version: false,
blockchainConnection: false,
debugApiConnection: false,
apiConnection: false,
topology: false,
chequebook: false,
},
latestPublishedVersion: undefined,
latestUserVersion: undefined,
latestUserVersionExact: undefined,
isLatestBeeVersion: false,
latestBeeVersionUrl: 'https://github.com/ethersphere/bee/releases/latest',
error: null,
apiHealth: false,
debugApiHealth: null,
nodeAddresses: null,
topology: null,
chequebookAddress: null,
peers: null,
chequebookBalance: null,
peerBalances: null,
peerCheques: null,
settlements: null,
latestBeeRelease: null,
isLoading: true,
isRefreshing: false,
lastUpdate: null,
start: () => {}, // eslint-disable-line
stop: () => {}, // eslint-disable-line
refresh: () => Promise.reject(),
}
export const Context = createContext<ContextInterface>(initialValues)
export const Consumer = Context.Consumer
interface Props {
children: ReactChild
}
function getStatus(
debugApiHealth: Health | null,
nodeAddresses: NodeAddresses | null,
apiHealth: boolean,
topology: Topology | null,
chequebookAddress: ChequebookAddressResponse | null,
chequebookBalance: ChequebookBalance | null,
error: Error | null,
): Status {
const status = {
version: Boolean(
debugApiHealth &&
semver.satisfies(debugApiHealth.version, engines.bee, {
includePrerelease: true,
}),
),
blockchainConnection: Boolean(nodeAddresses?.ethereum),
debugApiConnection: Boolean(debugApiHealth?.status === 'ok'),
apiConnection: apiHealth,
topology: Boolean(topology?.connected && topology?.connected > 0),
chequebook:
Boolean(chequebookAddress?.chequebookAddress) &&
chequebookBalance !== null &&
chequebookBalance?.totalBalance.toBigNumber.isGreaterThan(0),
}
return { ...status, all: !error && Object.values(status).every(v => v) }
}
export function Provider({ children }: Props): ReactElement {
const { beeApi, beeDebugApi } = useContext(SettingsContext)
const [apiHealth, setApiHealth] = useState<boolean>(false)
const [debugApiHealth, setDebugApiHealth] = useState<Health | null>(null)
const [nodeAddresses, setNodeAddresses] = useState<NodeAddresses | null>(null)
const [topology, setNodeTopology] = useState<Topology | null>(null)
const [chequebookAddress, setChequebookAddress] = useState<ChequebookAddressResponse | null>(null)
const [peers, setPeers] = useState<Peer[] | null>(null)
const [chequebookBalance, setChequebookBalance] = useState<ChequebookBalance | null>(null)
const [peerBalances, setPeerBalances] = useState<Balance[] | null>(null)
const [peerCheques, setPeerCheques] = useState<LastChequesResponse | null>(null)
const [settlements, setSettlements] = useState<Settlements | null>(null)
const { latestBeeRelease } = useLatestBeeRelease()
const [error, setError] = useState<Error | null>(initialValues.error)
const [isLoading, setIsLoading] = useState<boolean>(initialValues.isLoading)
const [isRefreshing, setIsRefreshing] = useState<boolean>(initialValues.isRefreshing)
const [lastUpdate, setLastUpdate] = useState<number | null>(initialValues.lastUpdate)
const [frequency, setFrequency] = useState<number | null>(30000)
const latestPublishedVersion = semver.coerce(latestBeeRelease?.name)?.version
const latestUserVersion = semver.coerce(debugApiHealth?.version)?.version
const latestUserVersionExact = debugApiHealth?.version
useEffect(() => {
setIsLoading(true)
setApiHealth(false)
refresh()
}, [beeApi])
useEffect(() => {
setIsLoading(true)
setDebugApiHealth(null)
setNodeAddresses(null)
setNodeTopology(null)
setPeers(null)
setChequebookAddress(null)
setChequebookBalance(null)
setPeerBalances(null)
setPeerCheques(null)
setSettlements(null)
refresh()
}, [beeDebugApi])
const refresh = async () => {
// Don't want to refresh when already refreshing
if (isRefreshing) return
// Not a valid bee api
if (!beeApi || !beeDebugApi) {
setIsLoading(false)
return
}
try {
setIsRefreshing(true)
setError(null)
// Wrap the chequebook balance call to return BZZ values as Token object
const chequeBalanceWrapper = async () => {
const { totalBalance, availableBalance } = await beeDebugApi.getChequebookBalance()
return {
totalBalance: new Token(totalBalance),
availableBalance: new Token(availableBalance),
}
}
// Wrap the balances call to return BZZ values as Token object
const peerBalanceWrapper = async () => {
const { balances } = await beeDebugApi.getAllBalances()
return balances.map(({ peer, balance }) => ({ peer, balance: new Token(balance) }))
}
// Wrap the settlements call to return BZZ values as Token object
const settlementsWrapper = async () => {
const { totalReceived, settlements, totalSent } = await beeDebugApi.getAllSettlements()
return {
totalReceived: new Token(totalReceived),
totalSent: new Token(totalSent),
settlements: settlements.map(({ peer, received, sent }) => ({
peer,
received: new Token(received),
sent: new Token(sent),
})),
}
}
const promises = [
// API health
beeApi
.isConnected()
.then(setApiHealth)
.catch(() => setApiHealth(false)),
// Debug API health
beeDebugApi
.getHealth()
.then(setDebugApiHealth)
.catch(() => setDebugApiHealth(null)),
// Node Addresses
beeDebugApi
.getNodeAddresses()
.then(setNodeAddresses)
.catch(() => setNodeAddresses(null)),
// Network Topology
beeDebugApi
.getTopology()
.then(setNodeTopology)
.catch(() => setNodeTopology(null)),
// Peers
beeDebugApi
.getPeers()
.then(setPeers)
.catch(() => setPeers(null)),
// Chequebook address
beeDebugApi
.getChequebookAddress()
.then(setChequebookAddress)
.catch(() => setChequebookAddress(null)),
// Cheques
beeDebugApi
.getLastCheques()
.then(setPeerCheques)
.catch(() => setPeerCheques(null)),
// Chequebook balance
chequeBalanceWrapper()
.then(setChequebookBalance)
.catch(() => setChequebookBalance(null)),
// Peer balances
peerBalanceWrapper()
.then(setPeerBalances)
.catch(() => setPeerBalances(null)),
// Settlements
settlementsWrapper()
.then(setSettlements)
.catch(() => setSettlements(null)),
]
await Promise.allSettled(promises)
} catch (e) {
setError(e)
} finally {
setIsLoading(false)
setIsRefreshing(false)
setLastUpdate(Date.now())
}
}
const start = (freq = 30000) => setFrequency(freq)
const stop = () => setFrequency(null)
// Start the update loop
useEffect(() => {
refresh()
// Start autorefresh only if the frequency is set
if (frequency) {
const interval = setInterval(refresh, frequency)
return () => clearInterval(interval)
}
}, [frequency, beeDebugApi, beeApi])
return (
<Context.Provider
value={{
status: getStatus(
debugApiHealth,
nodeAddresses,
apiHealth,
topology,
chequebookAddress,
chequebookBalance,
error,
),
latestUserVersion,
latestUserVersionExact,
latestPublishedVersion,
isLatestBeeVersion: Boolean(
latestPublishedVersion &&
latestUserVersion &&
semver.satisfies(latestPublishedVersion, latestUserVersion, {
includePrerelease: true,
}),
),
latestBeeVersionUrl: latestBeeRelease?.html_url || 'https://github.com/ethersphere/bee/releases/latest',
error,
apiHealth,
debugApiHealth,
nodeAddresses,
topology,
chequebookAddress,
peers,
chequebookBalance,
peerBalances,
peerCheques,
settlements,
latestBeeRelease,
isLoading,
isRefreshing,
lastUpdate,
start,
stop,
refresh,
}}
>
{children}
</Context.Provider>
)
}
+69
View File
@@ -0,0 +1,69 @@
import { createContext, ReactChild, ReactElement, useState, useEffect } from 'react'
// These need to be numeric values as they are used as indexes in the TabsContainer
export enum Platforms {
macOS = 0,
Linux,
Windows,
iOS,
Android,
}
export enum SupportedPlatforms {
macOS = Platforms.macOS,
Linux = Platforms.Linux,
}
interface ContextInterface {
platform: SupportedPlatforms
setPlatform: (platform: SupportedPlatforms) => void
}
const initialValues: ContextInterface = {
platform: SupportedPlatforms.macOS,
setPlatform: () => {}, // eslint-disable-line
}
export const Context = createContext<ContextInterface>(initialValues)
export const Consumer = Context.Consumer
interface Props {
children: ReactChild
}
function isSupportedPlatform(platform: unknown): platform is SupportedPlatforms {
return Object.keys(SupportedPlatforms).includes(platform as string)
}
function getOS(): Platforms | null {
const userAgent = window.navigator.userAgent
const platform = window.navigator.platform
const macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K']
const windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE']
const iosPlatforms = ['iPhone', 'iPad', 'iPod']
if (macosPlatforms.includes(platform)) return Platforms.macOS
if (iosPlatforms.includes(platform)) return Platforms.iOS
if (windowsPlatforms.includes(platform)) return Platforms.Windows
if (/Android/.test(userAgent)) return Platforms.Android
if (/Linux/.test(platform)) return Platforms.Linux
return null
}
export function Provider({ children }: Props): ReactElement {
const [platform, setPlatform] = useState<SupportedPlatforms>(SupportedPlatforms.Linux)
// This is in useEffect as it really just needs to run once and not on each re-render
useEffect(() => {
const os = getOS()
setPlatform(isSupportedPlatform(os) ? os : SupportedPlatforms.Linux)
}, [])
return <Context.Provider value={{ platform, setPlatform }}>{children}</Context.Provider>
}
+59
View File
@@ -0,0 +1,59 @@
import { createContext, ReactChild, ReactElement, useState, useEffect } from 'react'
import { Bee, BeeDebug } from '@ethersphere/bee-js'
interface ContextInterface {
apiUrl: string
apiDebugUrl: string
beeApi: Bee | null
beeDebugApi: BeeDebug | null
setApiUrl: (url: string) => void
setDebugApiUrl: (url: string) => void
}
const initialValues: ContextInterface = {
apiUrl: sessionStorage.getItem('api_host') || process.env.REACT_APP_BEE_HOST || 'http://localhost:1633',
apiDebugUrl:
sessionStorage.getItem('debug_api_host') || process.env.REACT_APP_BEE_DEBUG_HOST || 'http://localhost:1635',
beeApi: null,
beeDebugApi: null,
setApiUrl: () => {}, // eslint-disable-line
setDebugApiUrl: () => {}, // eslint-disable-line
}
export const Context = createContext<ContextInterface>(initialValues)
export const Consumer = Context.Consumer
interface Props {
children: ReactChild
}
export function Provider({ children }: Props): ReactElement {
const [apiUrl, setApiUrl] = useState<string>(initialValues.apiUrl)
const [apiDebugUrl, setDebugApiUrl] = useState<string>(initialValues.apiDebugUrl)
const [beeApi, setBeeApi] = useState<Bee | null>(null)
const [beeDebugApi, setBeeDebugApi] = useState<BeeDebug | null>(null)
useEffect(() => {
try {
setBeeApi(new Bee(apiUrl))
sessionStorage.setItem('api_host', apiUrl)
} catch (e) {
setBeeApi(null)
}
}, [apiUrl])
useEffect(() => {
try {
setBeeDebugApi(new BeeDebug(apiDebugUrl))
sessionStorage.setItem('debug_api_host', apiDebugUrl)
} catch (e) {
setBeeDebugApi(null)
}
}, [apiDebugUrl])
return (
<Context.Provider value={{ apiUrl, apiDebugUrl, beeApi, beeDebugApi, setApiUrl, setDebugApiUrl }}>
{children}
</Context.Provider>
)
}
+97
View File
@@ -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>
)
}
+3 -3
View File
@@ -6,14 +6,14 @@ interface LatestBeeRelease {
} }
interface StatusHookCommon { interface StatusHookCommon {
isLoading: boolean
isOk: boolean isOk: boolean
} }
interface StatusNodeVersionHook extends StatusHookCommon { interface StatusNodeVersionHook extends StatusHookCommon {
userVersion: string userVersion?: string
latestVersion: string latestVersion?: string
latestUrl: string latestUrl: string
isLatestBeeVersion: boolean
} }
interface StatusEthereumConnectionHook extends StatusHookCommon { interface StatusEthereumConnectionHook extends StatusHookCommon {
nodeAddresses: NodeAddresses | null nodeAddresses: NodeAddresses | null
+7 -5
View File
@@ -7,11 +7,12 @@ import AppRoute from './AppRoute'
import Dashboard from '../layout/Dashboard' import Dashboard from '../layout/Dashboard'
// pages // pages
import Status from '../pages/status/index' import Status from '../pages/status'
import Files from '../pages/files/index' import Files from '../pages/files'
import Peers from '../pages/peers/index' import Peers from '../pages/peers'
import Accounting from '../pages/accounting/index' import Accounting from '../pages/accounting'
import Settings from '../pages/settings/index' import Settings from '../pages/settings'
import Stamps from '../pages/stamps'
const BaseRouter = (): ReactElement => ( const BaseRouter = (): ReactElement => (
<Switch> <Switch>
@@ -20,6 +21,7 @@ const BaseRouter = (): ReactElement => (
<AppRoute exact path="/peers/" layout={Dashboard} component={Peers} /> <AppRoute exact path="/peers/" layout={Dashboard} component={Peers} />
<AppRoute exact path="/accounting/" layout={Dashboard} component={Accounting} /> <AppRoute exact path="/accounting/" layout={Dashboard} component={Accounting} />
<AppRoute exact path="/settings/" layout={Dashboard} component={Settings} /> <AppRoute exact path="/settings/" layout={Dashboard} component={Settings} />
<AppRoute exact path="/stamps/" layout={Dashboard} component={Stamps} />
</Switch> </Switch>
) )
-101
View File
@@ -1,101 +0,0 @@
import {
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(file: File): Promise<Reference> {
return beeJSClient().uploadFile(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
View File
@@ -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' { declare module '@material-ui/core/styles/createPalette' {
interface TypeBackground { 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({ export const lightTheme = createMuiTheme({
palette: { palette: {
type: 'light', type: 'light',
@@ -13,7 +62,9 @@ export const lightTheme = createMuiTheme({
default: '#fafafa', default: '#fafafa',
}, },
primary: { primary: {
main: '#6a6a6a', light: orange.A200,
main: '#dd7700',
dark: orange[800],
}, },
secondary: { secondary: {
main: '#333333', main: '#333333',
@@ -32,7 +83,9 @@ export const darkTheme = createMuiTheme({
paper: '#161b22', paper: '#161b22',
}, },
primary: { primary: {
light: orange.A200,
main: '#dd7700', main: '#dd7700',
dark: orange[800],
}, },
secondary: { secondary: {
main: '#1f2937', main: '#1f2937',
@@ -42,3 +95,8 @@ export const darkTheme = createMuiTheme({
fontFamily: ['Work Sans', 'Montserrat', 'Nunito', 'Roboto', '"Helvetica Neue"', 'Arial', 'sans-serif'].join(','), 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
+23
View File
@@ -0,0 +1,23 @@
import type { Token } from './models/Token'
export interface ChequebookBalance {
totalBalance: Token
availableBalance: Token
}
export interface Balance {
peer: string
balance: Token
}
export interface Settlement {
peer: string
received: Token
sent: Token
}
export interface Settlements {
totalReceived: Token
totalSent: Token
settlements: Settlement[]
}
+74
View File
@@ -32,3 +32,77 @@ export function makeBigNumber(value: BigNumber | BigInt | number | string): BigN
throw new TypeError(`Not a BigNumber or BigNumber convertible value. Type: ${typeof value} value: ${value}`) throw new TypeError(`Not a BigNumber or BigNumber convertible value. Type: ${typeof value} value: ${value}`)
} }
export type PromiseSettlements<T> = {
fulfilled: PromiseFulfilledResult<T>[]
rejected: PromiseRejectedResult[]
}
export type UnwrappedPromiseSettlements<T> = {
fulfilled: T[]
rejected: string[]
}
export async function sleepMs(ms: number): Promise<void> {
await new Promise<void>(resolve =>
setTimeout(() => {
resolve()
}, ms),
)
}
/**
* Maps the returned results of `Promise.allSettled` to an object
* with `fulfilled` and `rejected` arrays for easy access.
*
* The results still need to be unwrapped to get the fulfilled values or rejection reasons.
*/
export function mapPromiseSettlements<T>(promises: PromiseSettledResult<T>[]): PromiseSettlements<T> {
const fulfilled = promises.filter(promise => promise.status === 'fulfilled') as PromiseFulfilledResult<T>[]
const rejected = promises.filter(promise => promise.status === 'rejected') as PromiseRejectedResult[]
return { fulfilled, rejected }
}
/**
* Maps the returned values of `Promise.allSettled` to an object
* with `fulfilled` and `rejected` arrays for easy access.
*
* For rejected promises, the value is the stringified `reason`,
* or `'Unknown error'` string when it is unavailable.
*/
export function unwrapPromiseSettlements<T>(
promiseSettledResults: PromiseSettledResult<T>[],
): UnwrappedPromiseSettlements<T> {
const values = mapPromiseSettlements(promiseSettledResults)
const fulfilled = values.fulfilled.map(x => x.value)
const rejected = values.rejected.map(x => (x.reason ? String(x.reason) : 'Unknown error'))
return { fulfilled, rejected }
}
/**
* Wraps a `Promise<T>` or async function inside a new `Promise<T>`,
* which retries the original function up to `maxRetries` times,
* waiting `delayMs` milliseconds between failed attempts.
*
* If all attempts fail, then this `Promise<T>` also rejects.
*/
export function makeRetriablePromise<T>(fn: () => Promise<T>, maxRetries = 3, delayMs = 1000): Promise<T> {
return new Promise(async (resolve, reject) => {
for (let tries = 0; tries < maxRetries; tries++) {
try {
const results = await fn()
resolve(results)
return
} catch (error) {
if (tries < maxRetries - 1) {
await sleepMs(delayMs)
} else {
reject(error)
}
}
}
})
}
+105
View File
@@ -0,0 +1,105 @@
const OPTIMAL_CONNECTED_PEERS = 200
const OPTIMAL_POPULATION = 100_000
const OPTIMAL_DEPTH = 12
interface Threshold {
minimumValue: number
explanation: string
score: number
}
type Thresholds = {
connectedPeers: Threshold[]
population: Threshold[]
depth: Threshold[]
}
type ThresholdValue = {
score: number
maximumScore: number
explanation: string
}
export type ThresholdValues = {
connectedPeers: ThresholdValue
population: ThresholdValue
depth: ThresholdValue
}
const GENERIC_ERROR = 'There may be issues with your Bee node or connection.'
const THRESHOLDS: Thresholds = {
connectedPeers: [
{
minimumValue: OPTIMAL_CONNECTED_PEERS,
explanation: `Perfect! ${OPTIMAL_CONNECTED_PEERS} or more connected peers indicate a healthy topology.`,
score: 2,
},
{
minimumValue: 1,
explanation: `Your Bee node is connected to peers, but this number should ideally be above ${OPTIMAL_CONNECTED_PEERS}. If you have only started your Bee node, this number may increase quickly.`,
score: 1,
},
{
minimumValue: 0,
explanation: 'Your Bee node has not connected to any peers. ' + GENERIC_ERROR,
score: 0,
},
],
population: [
{
minimumValue: OPTIMAL_POPULATION,
explanation:
'Perfect! Your Bee node seems to have a realistic value for the network size, which means everything is working well on your end.',
score: 2,
},
{
minimumValue: 1,
explanation: `Population is usually above ${OPTIMAL_POPULATION.toLocaleString()}. If the number does not increase within a few hours, there may be issues with your Bee node.`,
score: 1,
},
{
minimumValue: 0,
explanation: 'Your Bee node has no information on the network population. ' + GENERIC_ERROR,
score: 0,
},
],
depth: [
{
minimumValue: OPTIMAL_DEPTH,
explanation: 'Perfect! Your Bee node has the highest available depth.',
score: 2,
},
{
minimumValue: 1,
explanation: `Your Bee node is supposed to reach a depth of ${OPTIMAL_DEPTH} eventually. Stagnation or decrease in this number may indicate problems with your Bee node.`,
score: 1,
},
{
minimumValue: 0,
explanation: 'Your Bee node has not started building its topology yet. ' + GENERIC_ERROR,
score: 0,
},
],
}
export function pickThreshold(key: keyof Thresholds, value: number): ThresholdValue {
const thresholds = THRESHOLDS[key]
const maximumScore = thresholds[0].score
for (const item of thresholds) {
if (value >= item.minimumValue) {
return {
score: item.score,
maximumScore,
explanation: item.explanation,
}
}
}
const last = thresholds[thresholds.length - 1]
return {
score: last.score,
maximumScore,
explanation: last.explanation,
}
}