Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a3e58c89b | |||
| 3ef1ad9574 | |||
| dec812be45 | |||
| d399a5c556 | |||
| 59dd1a3c81 | |||
| 635621b04a | |||
| 82cf6d9c01 | |||
| 3bb00771d6 | |||
| b354ef724b | |||
| 844383bea7 | |||
| 49350b0570 | |||
| 7fdf38bba1 | |||
| 7883d053ed | |||
| 15b4b0e561 | |||
| c1a219c2e2 | |||
| 643f3b24db | |||
| 605054895d | |||
| d5649dc8c6 | |||
| cc5e778f89 | |||
| f11bbd5008 | |||
| b4c9d9e018 | |||
| 6c3f6c1019 | |||
| 83c6d13417 | |||
| 93af7f35a3 | |||
| 03265687ad | |||
| f241b2fc5f | |||
| 32e5ea9e56 | |||
| b666cd2657 | |||
| ecbc116475 | |||
| e7188f4a35 | |||
| b69e368f69 | |||
| 57f5a73f3a | |||
| c4c7d9619d | |||
| c4c1573263 |
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"ignores": ["@types/jest", "@commitlint/config-conventional", "@types/react-router"]
|
||||||
|
}
|
||||||
@@ -8,7 +8,9 @@
|
|||||||
"plugin:@typescript-eslint/recommended",
|
"plugin:@typescript-eslint/recommended",
|
||||||
"prettier",
|
"prettier",
|
||||||
"plugin:prettier/recommended",
|
"plugin:prettier/recommended",
|
||||||
"plugin:react/recommended"
|
"plugin:react/recommended",
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
],
|
],
|
||||||
"env": {
|
"env": {
|
||||||
"browser": true,
|
"browser": true,
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
node-version: [14.x]
|
node-version: [14.x]
|
||||||
|
|
||||||
|
env:
|
||||||
|
REACT_APP_BEE_HOST: https://api.test-node.staging.ethswarm.org/
|
||||||
|
REACT_APP_BEE_DEBUG_HOST: https://debug.test-node.staging.ethswarm.org/
|
||||||
|
REACT_APP_DEV_MODE: 1
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
@@ -47,5 +52,24 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
|
|
||||||
|
- name: Dependency check
|
||||||
|
run: npm run depcheck
|
||||||
|
|
||||||
|
- name: Update supported Bee action
|
||||||
|
uses: ethersphere/update-supported-bee-action@v1
|
||||||
|
if: github.ref == 'refs/heads/master'
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.REPO_GHA_PAT }}
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Create preview
|
||||||
|
uses: ethersphere/beeload-action@v1
|
||||||
|
with:
|
||||||
|
preview: 'true'
|
||||||
|
|
||||||
|
- name: Upload to testnet
|
||||||
|
uses: ethersphere/beeload-action@v1
|
||||||
|
with:
|
||||||
|
bee-url: https://api.gateway.testnet.ethswarm.org
|
||||||
|
|||||||
@@ -1,5 +1,49 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.10.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.9.0...v0.10.0) (2021-12-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add website and folder upload and download ([#260](https://www.github.com/ethersphere/bee-dashboard/issues/260)) ([3ef1ad9](https://www.github.com/ethersphere/bee-dashboard/commit/3ef1ad9574c9193f83d8a1447fddb79266c1a4f4))
|
||||||
|
|
||||||
|
## [0.9.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.8.0...v0.9.0) (2021-11-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add dev mode flag ([#246](https://www.github.com/ethersphere/bee-dashboard/issues/246)) ([49350b0](https://www.github.com/ethersphere/bee-dashboard/commit/49350b05709053ecfbc4fc98f8b1df1aa0345e95))
|
||||||
|
* enable setting devMode from queryParams ([#254](https://www.github.com/ethersphere/bee-dashboard/issues/254)) ([844383b](https://www.github.com/ethersphere/bee-dashboard/commit/844383bea7b2118232a74ac23c9e9a38fc47d3fd))
|
||||||
|
* improve upload flow ([#240](https://www.github.com/ethersphere/bee-dashboard/issues/240)) ([635621b](https://www.github.com/ethersphere/bee-dashboard/commit/635621b04aea7124a99d00f9e31a86983063f5ce))
|
||||||
|
* move postage stamp operations to bee debug api ([#256](https://www.github.com/ethersphere/bee-dashboard/issues/256)) ([3bb0077](https://www.github.com/ethersphere/bee-dashboard/commit/3bb00771d684ad93fd7acd921b648574013aec5c))
|
||||||
|
|
||||||
|
## [0.8.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.7.0...v0.8.0) (2021-10-20)
|
||||||
|
|
||||||
|
In this version we are adding support for the bee release 1.2.0. The app also went through a graphical redesign. More to come soon!
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
* support for bee 1.2.0
|
||||||
|
* update files page design ([#218](https://www.github.com/ethersphere/bee-dashboard/issues/218)) ([93af7f3](https://www.github.com/ethersphere/bee-dashboard/commit/93af7f35a371d54864c068be6e1d8a70092afe28))
|
||||||
|
* update info page design ([#207](https://www.github.com/ethersphere/bee-dashboard/issues/207)) ([57f5a73](https://www.github.com/ethersphere/bee-dashboard/commit/57f5a73f3a8d957bf967c51612dc09c802bb68dc))
|
||||||
|
* update status page design ([#214](https://www.github.com/ethersphere/bee-dashboard/issues/214)) ([b666cd2](https://www.github.com/ethersphere/bee-dashboard/commit/b666cd2657cf1003651c44b6b4fa5bdcf11e895f))
|
||||||
|
* update troubleshooting component design ([#204](https://www.github.com/ethersphere/bee-dashboard/issues/204)) ([c4c1573](https://www.github.com/ethersphere/bee-dashboard/commit/c4c1573263868b6dc8a863124e4aee824dceadbb))
|
||||||
|
* update accounting page design ([#209](https://www.github.com/ethersphere/bee-dashboard/issues/209)) ([ecbc116](https://www.github.com/ethersphere/bee-dashboard/commit/ecbc1164756de912d14ce44aa9b2c155dded6dac))
|
||||||
|
* update postage stamps page design ([#217](https://www.github.com/ethersphere/bee-dashboard/issues/217)) ([f241b2f](https://www.github.com/ethersphere/bee-dashboard/commit/f241b2fc5f6ec0741e275498ebef5a18ce710b81))
|
||||||
|
* update settings page design ([#215](https://www.github.com/ethersphere/bee-dashboard/issues/215)) ([32e5ea9](https://www.github.com/ethersphere/bee-dashboard/commit/32e5ea9e56fdf957b758ec714bb6a4fe1903082a))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* hover state style of ListItems which are clickable to be in line with other buttons ([#223](https://www.github.com/ethersphere/bee-dashboard/issues/223)) ([6c3f6c1](https://www.github.com/ethersphere/bee-dashboard/commit/6c3f6c1019801267aa5e51002f6e21f769edc210))
|
||||||
|
* size of the troubleshoot component button ([#226](https://www.github.com/ethersphere/bee-dashboard/issues/226)) ([b4c9d9e](https://www.github.com/ethersphere/bee-dashboard/commit/b4c9d9e0182c4bee5ebb2d4e43e0aaad2aeb616b))
|
||||||
|
* style of the update bee version button ([#222](https://www.github.com/ethersphere/bee-dashboard/issues/222)) ([83c6d13](https://www.github.com/ethersphere/bee-dashboard/commit/83c6d1341790d664c7986dd2a816fe6a3b069e5c))
|
||||||
|
* typo in population text ([#228](https://www.github.com/ethersphere/bee-dashboard/issues/228)) ([cc5e778](https://www.github.com/ethersphere/bee-dashboard/commit/cc5e778f892b73b0b7ff5e0fa00c4816f3298ac7))
|
||||||
|
* unknown routes should point to info page ([#227](https://www.github.com/ethersphere/bee-dashboard/issues/227)) ([f11bbd5](https://www.github.com/ethersphere/bee-dashboard/commit/f11bbd5008a78ef7d5c73fc2758ee4e2dafae01e))
|
||||||
|
* used label in postage stamp list ([#220](https://www.github.com/ethersphere/bee-dashboard/issues/220)) ([0326568](https://www.github.com/ethersphere/bee-dashboard/commit/03265687ad630b0100da3134518b680327af1636))
|
||||||
|
* wording in chequebook setup ([#211](https://www.github.com/ethersphere/bee-dashboard/issues/211)) ([e7188f4](https://www.github.com/ethersphere/bee-dashboard/commit/e7188f4a35c85204eef6a01ae6f1e679d076180c))
|
||||||
|
|
||||||
|
|
||||||
## [0.7.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.6.0...v0.7.0) (2021-08-31)
|
## [0.7.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.6.0...v0.7.0) (2021-08-31)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,21 +3,26 @@
|
|||||||
[](https://swarm.ethereum.org/)
|
[](https://swarm.ethereum.org/)
|
||||||
[](https://github.com/RichardLitt/standard-readme)
|
[](https://github.com/RichardLitt/standard-readme)
|
||||||
[](https://github.com/feross/standard)
|
[](https://github.com/feross/standard)
|
||||||
|
[](https://app.fossa.com/projects/git%2Bgithub.com%2Fethersphere%2Fbee-dashboard?ref=badge_shield)
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
> 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.
|
> 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.**
|
||||||
|
|
||||||
This project is intended to be used with **Bee version 1.1.0**. Using it with older or newer Bee versions is not recommended and may not work. Stay up to date by joining the [official Discord](https://discord.gg/GU22h2utj6) and by keeping an eye on the [releases tab](https://github.com/ethersphere/bee-dashboard/releases).
|
This project is intended to be used with **Bee version <!-- SUPPORTED_BEE_START -->1.4.0-8fa696a8<!-- SUPPORTED_BEE_END -->**.
|
||||||
|
Using it with older or newer Bee versions is not recommended and may not work. Stay up to date by joining the
|
||||||
|
[official Discord](https://discord.gg/GU22h2utj6) and by keeping an eye on the
|
||||||
|
[releases tab](https://github.com/ethersphere/bee-dashboard/releases).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
| Node Setup | Browse & Upload Files | Accounting | Peers | Settings |
|
|
||||||
|-------|---------|-------|----------|------|
|
|
||||||
|  |  |  |  |  |
|
|
||||||
|
|
||||||
|
| Node Setup | Upload Files | Download Content | Accounting | Postage Stamps |
|
||||||
|
| ------------------------------------ | -------------------------------------- | ------------------------------------------ | ----------------------------------------- | ---------------------------------------- |
|
||||||
|
|  |  |  |  |  |
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
@@ -40,11 +45,15 @@ npm install -g @ethersphere/bee-dashboard
|
|||||||
|
|
||||||
## Usage
|
## 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)
|
: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
|
### Terminal
|
||||||
|
|
||||||
To start use:
|
To start use:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
bee-dashboard
|
bee-dashboard
|
||||||
```
|
```
|
||||||
@@ -76,13 +85,16 @@ npm start
|
|||||||
|
|
||||||
The Bee Dashboard runs in development mode on [http://localhost:3031/](http://localhost:3031/)
|
The Bee Dashboard runs in development mode on [http://localhost:3031/](http://localhost:3031/)
|
||||||
|
|
||||||
|
> Setting the `REACT_APP_DEV_MODE=1` environment variable, or opening Bee Dashboard with the query string `?devMode=1` loosens some checks. This makes it possible to develop Bee Dashboard without having connected peers and chequebook properly set up, effectively supporting the dev mode of Bee itself.
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
There are some ways you can make this module better:
|
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 [Discord chat](https://discord.gg/wdghaQsGq5) in the #develop-on-swarm channel 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
|
||||||
|
|
||||||
@@ -94,3 +106,6 @@ See what "Maintainer" means [here](https://github.com/ethersphere/repo-maintaine
|
|||||||
## License
|
## License
|
||||||
|
|
||||||
[BSD-3-Clause](./LICENSE)
|
[BSD-3-Clause](./LICENSE)
|
||||||
|
|
||||||
|
|
||||||
|
[](https://app.fossa.com/projects/git%2Bgithub.com%2Fethersphere%2Fbee-dashboard?ref=badge_large)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ethersphere/bee-dashboard",
|
"name": "@ethersphere/bee-dashboard",
|
||||||
"version": "0.7.0",
|
"version": "0.10.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,52 +24,65 @@
|
|||||||
"url": "https://github.com/ethersphere/bee-dashboard.git"
|
"url": "https://github.com/ethersphere/bee-dashboard.git"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ethersphere/bee-js": "1.2.1",
|
"@ethersphere/bee-js": "3.0.0",
|
||||||
|
"@ethersphere/manifest-js": "^1.0.0",
|
||||||
"@material-ui/core": "4.12.3",
|
"@material-ui/core": "4.12.3",
|
||||||
"@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",
|
"axios": "0.24.0",
|
||||||
"@types/react-router-dom": "5.1.7",
|
|
||||||
"axios": "0.21.1",
|
|
||||||
"bignumber.js": "9.0.1",
|
"bignumber.js": "9.0.1",
|
||||||
"feather-icons": "4.28.0",
|
"file-saver": "^2.0.5",
|
||||||
"formik": "2.2.8",
|
"formik": "2.2.9",
|
||||||
"formik-material-ui": "3.0.1",
|
"formik-material-ui": "3.0.1",
|
||||||
|
"jszip": "^3.7.1",
|
||||||
"material-ui-dropzone": "3.5.0",
|
"material-ui-dropzone": "3.5.0",
|
||||||
"notistack": "1.0.9",
|
"notistack": "1.0.10",
|
||||||
"opener": "1.5.2",
|
"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.4",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-feather": "2.0.9",
|
"react-feather": "2.0.9",
|
||||||
"react-identicons": "1.2.5",
|
"react-identicons": "1.2.5",
|
||||||
|
"react-router": "5.2.0",
|
||||||
"react-router-dom": "5.2.0",
|
"react-router-dom": "5.2.0",
|
||||||
"react-syntax-highlighter": "15.4.3",
|
"react-syntax-highlighter": "15.4.4",
|
||||||
"semver": "7.3.2",
|
"semver": "7.3.5",
|
||||||
"serve-handler": "6.1.3"
|
"serve-handler": "6.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/jest-dom": "5.12.0",
|
"@commitlint/config-conventional": "14.1.0",
|
||||||
"@testing-library/react": "11.2.6",
|
"@testing-library/jest-dom": "5.15.0",
|
||||||
"@testing-library/user-event": "13.1.5",
|
"@testing-library/react": "12.1.2",
|
||||||
"@types/jest": "26.0.22",
|
"@types/file-saver": "^2.0.4",
|
||||||
"@types/node": "14.14.41",
|
"@types/jest": "27.0.2",
|
||||||
"@types/qrcode.react": "1.0.1",
|
"@types/qrcode.react": "1.0.2",
|
||||||
"@types/react": "17.0.3",
|
"@types/react": "17.0.34",
|
||||||
"@types/react-copy-to-clipboard": "5.0.0",
|
"@types/react-copy-to-clipboard": "5.0.2",
|
||||||
"@types/react-dom": "17.0.3",
|
"@types/react-dom": "17.0.11",
|
||||||
"@types/react-syntax-highlighter": "13.5.0",
|
"@types/react-router": "5.1.17",
|
||||||
"@types/semver": "7.3.6",
|
"@types/react-router-dom": "5.3.2",
|
||||||
"eslint": "7.32.0",
|
"@types/react-syntax-highlighter": "13.5.2",
|
||||||
"eslint-config-prettier": "8.3.0",
|
"@types/semver": "7.3.9",
|
||||||
"eslint-plugin-jest": "24.4.0",
|
"@typescript-eslint/eslint-plugin": "4.33.0",
|
||||||
"eslint-plugin-prettier": "3.4.1",
|
"@typescript-eslint/parser": "4.33.0",
|
||||||
"eslint-plugin-react": "7.24.0",
|
"babel-eslint": "10.1.0",
|
||||||
"prettier": "2.3.2",
|
"depcheck": "1.4.2",
|
||||||
|
"eslint": "7.24.0",
|
||||||
|
"eslint-config-prettier": "8.2.0",
|
||||||
|
"eslint-config-react-app": "6.0.0",
|
||||||
|
"eslint-plugin-flowtype": "5.10.0",
|
||||||
|
"eslint-plugin-import": "2.25.2",
|
||||||
|
"eslint-plugin-jest": "24.3.5",
|
||||||
|
"eslint-plugin-jsx-a11y": "6.4.1",
|
||||||
|
"eslint-plugin-prettier": "3.4.0",
|
||||||
|
"eslint-plugin-react": "7.23.2",
|
||||||
|
"eslint-plugin-react-hooks": "4.2.0",
|
||||||
|
"eslint-plugin-testing-library": "3.10.2",
|
||||||
|
"prettier": "2.4.1",
|
||||||
"react-scripts": "4.0.3",
|
"react-scripts": "4.0.3",
|
||||||
"typescript": "4.2.4",
|
"typescript": "4.4.4",
|
||||||
"web-vitals": "1.1.1"
|
"web-vitals": "2.1.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "npm run build",
|
"prepare": "npm run build",
|
||||||
@@ -77,6 +90,7 @@
|
|||||||
"build": "react-scripts build",
|
"build": "react-scripts build",
|
||||||
"test": "react-scripts test",
|
"test": "react-scripts test",
|
||||||
"serve": "node ./serve.js",
|
"serve": "node ./serve.js",
|
||||||
|
"depcheck": "depcheck .",
|
||||||
"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\""
|
||||||
},
|
},
|
||||||
@@ -84,12 +98,6 @@
|
|||||||
"build",
|
"build",
|
||||||
"serve.js"
|
"serve.js"
|
||||||
],
|
],
|
||||||
"eslintConfig": {
|
|
||||||
"extends": [
|
|
||||||
"react-app",
|
|
||||||
"react-app/jest"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
">0.2%",
|
">0.2%",
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
|
import CssBaseline from '@material-ui/core/CssBaseline'
|
||||||
|
import { ThemeProvider } from '@material-ui/core/styles'
|
||||||
|
import { SnackbarProvider } from 'notistack'
|
||||||
import { ReactElement } from 'react'
|
import { ReactElement } from 'react'
|
||||||
import { BrowserRouter as Router } from 'react-router-dom'
|
import { BrowserRouter as Router } from 'react-router-dom'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
|
|
||||||
import { ThemeProvider } from '@material-ui/core/styles'
|
|
||||||
import CssBaseline from '@material-ui/core/CssBaseline'
|
|
||||||
import { SnackbarProvider } from 'notistack'
|
|
||||||
|
|
||||||
import BaseRouter from './routes'
|
|
||||||
import Dashboard from './layout/Dashboard'
|
import Dashboard from './layout/Dashboard'
|
||||||
import { theme } 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 BeeProvider } from './providers/Bee'
|
||||||
|
import { Provider as FileProvider } from './providers/File'
|
||||||
|
import { Provider as PlatformProvider } from './providers/Platform'
|
||||||
import { Provider as SettingsProvider } from './providers/Settings'
|
import { Provider as SettingsProvider } from './providers/Settings'
|
||||||
|
import { Provider as StampsProvider } from './providers/Stamps'
|
||||||
|
import BaseRouter from './routes'
|
||||||
|
import { theme } from './theme'
|
||||||
|
|
||||||
const App = (): ReactElement => (
|
const App = (): ReactElement => (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
@@ -20,6 +19,7 @@ const App = (): ReactElement => (
|
|||||||
<SettingsProvider>
|
<SettingsProvider>
|
||||||
<BeeProvider>
|
<BeeProvider>
|
||||||
<StampsProvider>
|
<StampsProvider>
|
||||||
|
<FileProvider>
|
||||||
<PlatformProvider>
|
<PlatformProvider>
|
||||||
<SnackbarProvider>
|
<SnackbarProvider>
|
||||||
<Router>
|
<Router>
|
||||||
@@ -32,6 +32,7 @@ const App = (): ReactElement => (
|
|||||||
</Router>
|
</Router>
|
||||||
</SnackbarProvider>
|
</SnackbarProvider>
|
||||||
</PlatformProvider>
|
</PlatformProvider>
|
||||||
|
</FileProvider>
|
||||||
</StampsProvider>
|
</StampsProvider>
|
||||||
</BeeProvider>
|
</BeeProvider>
|
||||||
</SettingsProvider>
|
</SettingsProvider>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { ReactElement } from 'react'
|
|||||||
const LIMIT = 100_000_000 // 100 megabytes
|
const LIMIT = 100_000_000 // 100 megabytes
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
file: File
|
files: File[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
@@ -22,14 +22,16 @@ const useStyles = makeStyles((theme: Theme) =>
|
|||||||
export default function UploadSizeAlert(props: Props): ReactElement | null {
|
export default function UploadSizeAlert(props: Props): ReactElement | null {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
const aboveLimit = props.file.size >= LIMIT
|
const totalSize = props.files.reduce((previous, current) => previous + current.size, 0)
|
||||||
|
|
||||||
|
const aboveLimit = totalSize >= LIMIT
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapse in={aboveLimit}>
|
<Collapse in={aboveLimit}>
|
||||||
<div className={classes.root}>
|
<div className={classes.root}>
|
||||||
<Alert severity="warning">
|
<Alert severity="warning">
|
||||||
<AlertTitle>Warning</AlertTitle>
|
<AlertTitle>Warning</AlertTitle>
|
||||||
The file you are trying to upload is above the recommended size. The chunks may not be synchronised properly
|
The files you are trying to upload are above the recommended size. The chunks may not be synchronised properly
|
||||||
over the network.
|
over the network.
|
||||||
</Alert>
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { ReactElement } from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
width: string
|
||||||
|
usage: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Capacity({ width, usage }: Props): ReactElement {
|
||||||
|
const integerUsage = Math.round(usage * 100)
|
||||||
|
const used = integerUsage + '%'
|
||||||
|
const free = 100 - 2 - integerUsage + '%'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', height: '100%', width }}>
|
||||||
|
<div style={{ display: 'flex', height: '4px', width: '100%' }}>
|
||||||
|
<div style={{ width: used, background: '#dd7200' }} />
|
||||||
|
<div style={{ width: '2%' }} />
|
||||||
|
<div style={{ width: free, background: '#c9c9c9' }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import DialogContentText from '@material-ui/core/DialogContentText'
|
|||||||
import DialogTitle from '@material-ui/core/DialogTitle'
|
import DialogTitle from '@material-ui/core/DialogTitle'
|
||||||
import { useSnackbar } from 'notistack'
|
import { useSnackbar } from 'notistack'
|
||||||
import { ReactElement, useState, useContext } from 'react'
|
import { ReactElement, useState, useContext } from 'react'
|
||||||
|
import { Zap } from 'react-feather'
|
||||||
import { Context as SettingsContext } from '../providers/Settings'
|
import { Context as SettingsContext } from '../providers/Settings'
|
||||||
import EthereumAddress from './EthereumAddress'
|
import EthereumAddress from './EthereumAddress'
|
||||||
|
|
||||||
@@ -59,8 +60,8 @@ export default function CheckoutModal({ peerId, uncashedAmount }: Props): ReactE
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Button variant="contained" color="primary" onClick={handleClickOpen} style={{ marginLeft: '7px' }}>
|
<Button variant="contained" onClick={handleClickOpen} startIcon={<Zap size="1rem" />}>
|
||||||
Cashout
|
Cash out peer {peerId.substr(0, 8)}[…]
|
||||||
</Button>
|
</Button>
|
||||||
<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>
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
import React, { ReactElement, useState } from 'react'
|
|
||||||
import { TextField, Button } from '@material-ui/core'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
defaultHost?: string
|
|
||||||
setHost: (host: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ConnectToHost(props: Props): ReactElement {
|
|
||||||
const [hostInputVisible, toggleHostInputVisibility] = useState(false)
|
|
||||||
const [host, setHost] = useState('')
|
|
||||||
|
|
||||||
const handleNewHostConnection = () => {
|
|
||||||
if (host) {
|
|
||||||
props.setHost(host)
|
|
||||||
toggleHostInputVisibility(!hostInputVisible)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{hostInputVisible ? (
|
|
||||||
<div style={{ display: 'flex' }}>
|
|
||||||
<TextField
|
|
||||||
defaultValue={props.defaultHost}
|
|
||||||
label="Enter host"
|
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
onChange={e => setHost(e.target.value)}
|
|
||||||
style={{ marginRight: '15px', minWidth: '300px' }}
|
|
||||||
/>
|
|
||||||
<Button onClick={() => handleNewHostConnection()} size="small" variant="outlined">
|
|
||||||
Connect
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
style={{ marginLeft: '7px' }}
|
|
||||||
onClick={() => toggleHostInputVisibility(!hostInputVisible)}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Button onClick={() => toggleHostInputVisibility(!hostInputVisible)} size="small" variant="outlined">
|
|
||||||
Change host
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import { ReactElement } from 'react'
|
|
||||||
|
|
||||||
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
|
||||||
import { Card, CardContent, Typography } from '@material-ui/core/'
|
|
||||||
|
|
||||||
import EthereumAddress from '../components/EthereumAddress'
|
|
||||||
|
|
||||||
import type { ChequebookAddressResponse, NodeAddresses } from '@ethersphere/bee-js'
|
|
||||||
|
|
||||||
const useStyles = makeStyles(() =>
|
|
||||||
createStyles({
|
|
||||||
root: {
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-around',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
},
|
|
||||||
details: {
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
flex: '1 0 auto',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
nodeAddresses: NodeAddresses | null
|
|
||||||
chequebookAddress: ChequebookAddressResponse | null
|
|
||||||
}
|
|
||||||
|
|
||||||
function EthereumAddressCard(props: Props): ReactElement {
|
|
||||||
const classes = useStyles()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className={classes.root}>
|
|
||||||
<div className={classes.details}>
|
|
||||||
<CardContent className={classes.content}>
|
|
||||||
<Typography variant="subtitle1" gutterBottom>
|
|
||||||
Ethereum Address
|
|
||||||
</Typography>
|
|
||||||
<EthereumAddress address={props.nodeAddresses?.ethereum} />
|
|
||||||
</CardContent>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={classes.details}>
|
|
||||||
<CardContent className={classes.content}>
|
|
||||||
<Typography variant="subtitle1" gutterBottom>
|
|
||||||
Chequebook Contract Address
|
|
||||||
</Typography>
|
|
||||||
<EthereumAddress address={props.chequebookAddress?.chequebookAddress} />
|
|
||||||
</CardContent>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EthereumAddressCard
|
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { Collapse, ListItem } from '@material-ui/core'
|
||||||
|
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||||
|
import { ExpandLess, ExpandMore } from '@material-ui/icons'
|
||||||
|
import { ReactElement, ReactNode, useState } from 'react'
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
root: {
|
||||||
|
width: '100%',
|
||||||
|
padding: 0,
|
||||||
|
margin: 0,
|
||||||
|
marginTop: theme.spacing(4),
|
||||||
|
'&:first-child': {
|
||||||
|
marginTop: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rootLevel1: { marginTop: theme.spacing(1) },
|
||||||
|
rootLevel2: { marginTop: theme.spacing(0.5) },
|
||||||
|
header: {
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
},
|
||||||
|
contentLevel0: {
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
},
|
||||||
|
contentLevel12: {
|
||||||
|
marginTop: theme.spacing(0.25),
|
||||||
|
},
|
||||||
|
infoText: {
|
||||||
|
color: '#c9c9c9',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode
|
||||||
|
expandable: ReactNode
|
||||||
|
defaultOpen?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ExpandableElement({ children, expandable, defaultOpen }: Props): ReactElement | null {
|
||||||
|
const classes = useStyles()
|
||||||
|
const [open, setOpen] = useState<boolean>(Boolean(defaultOpen))
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
setOpen(!open)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${classes.root} ${classes.rootLevel2}`}>
|
||||||
|
<ListItem button onClick={handleClick} className={classes.header}>
|
||||||
|
{children}
|
||||||
|
{open ? <ExpandLess /> : <ExpandMore />}
|
||||||
|
</ListItem>
|
||||||
|
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||||
|
<div className={classes.contentLevel12}>{expandable}</div>
|
||||||
|
</Collapse>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { ReactElement, ReactNode, useState } from 'react'
|
||||||
|
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
|
||||||
|
import { Collapse, ListItem, ListItemText, Typography } from '@material-ui/core'
|
||||||
|
import { ExpandLess, ExpandMore } from '@material-ui/icons'
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
root: {
|
||||||
|
width: '100%',
|
||||||
|
padding: 0,
|
||||||
|
margin: 0,
|
||||||
|
marginTop: theme.spacing(4),
|
||||||
|
'&:first-child': {
|
||||||
|
marginTop: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rootLevel1: { marginTop: theme.spacing(1) },
|
||||||
|
rootLevel2: { marginTop: theme.spacing(0.5) },
|
||||||
|
header: {
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
},
|
||||||
|
contentLevel0: {
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
},
|
||||||
|
contentLevel12: {
|
||||||
|
marginTop: theme.spacing(0.25),
|
||||||
|
},
|
||||||
|
infoText: {
|
||||||
|
color: '#c9c9c9',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children?: ReactNode
|
||||||
|
label: ReactNode
|
||||||
|
info?: ReactNode
|
||||||
|
level?: 0 | 1 | 2
|
||||||
|
defaultOpen?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ExpandableList({ children, label, level, defaultOpen, info }: Props): ReactElement | null {
|
||||||
|
const classes = useStyles()
|
||||||
|
const [open, setOpen] = useState<boolean>(Boolean(defaultOpen))
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
setOpen(!open)
|
||||||
|
}
|
||||||
|
|
||||||
|
let rootLevelClass = ''
|
||||||
|
let typographyVariant: 'h1' | 'h2' | 'h3' = 'h1'
|
||||||
|
let contentLevelClass = classes.contentLevel0
|
||||||
|
|
||||||
|
if (level === 1) {
|
||||||
|
rootLevelClass = classes.rootLevel1
|
||||||
|
typographyVariant = 'h2'
|
||||||
|
contentLevelClass = classes.contentLevel12
|
||||||
|
} else if (level === 2) {
|
||||||
|
rootLevelClass = classes.rootLevel2
|
||||||
|
typographyVariant = 'h3'
|
||||||
|
contentLevelClass = classes.contentLevel12
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${classes.root} ${rootLevelClass}`}>
|
||||||
|
<ListItem button onClick={handleClick} className={classes.header}>
|
||||||
|
<ListItemText primary={<Typography variant={typographyVariant}>{label}</Typography>} />
|
||||||
|
<div style={{ display: 'flex' }}>
|
||||||
|
{!open && (
|
||||||
|
<Typography variant="body2" className={classes.infoText}>
|
||||||
|
{info}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{open ? <ExpandLess /> : <ExpandMore />}
|
||||||
|
</div>
|
||||||
|
</ListItem>
|
||||||
|
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||||
|
<div className={contentLevelClass}>{children}</div>
|
||||||
|
</Collapse>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { ReactElement, ReactNode } from 'react'
|
||||||
|
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
|
||||||
|
import { Typography, Grid, IconButton, Tooltip } from '@material-ui/core'
|
||||||
|
import { Info } from 'react-feather'
|
||||||
|
import ListItem from '@material-ui/core/ListItem'
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
header: {
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
marginBottom: theme.spacing(0.25),
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
},
|
||||||
|
copyValue: {
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: theme.spacing(1),
|
||||||
|
borderRadius: 0,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#fcf2e8',
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
label?: ReactNode
|
||||||
|
value?: ReactNode
|
||||||
|
tooltip?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ExpandableListItem({ label, value, tooltip }: Props): ReactElement | null {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem className={classes.header}>
|
||||||
|
<Grid container direction="row" justifyContent="space-between" alignItems="center">
|
||||||
|
{label && <Typography variant="body1">{label}</Typography>}
|
||||||
|
{value && (
|
||||||
|
<Typography variant="body2">
|
||||||
|
{value}
|
||||||
|
{tooltip && (
|
||||||
|
<Tooltip title={tooltip} placement="top" arrow>
|
||||||
|
<IconButton size="small" className={classes.copyValue}>
|
||||||
|
<Info strokeWidth={1} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</ListItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { Grid } from '@material-ui/core'
|
||||||
|
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||||
|
import { ReactElement, ReactNode } from 'react'
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
action: {
|
||||||
|
marginTop: theme.spacing(0.75),
|
||||||
|
marginBottom: theme.spacing(1),
|
||||||
|
marginRight: theme.spacing(1),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode | ReactNode[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ExpandableListItemActions({ children }: Props): ReactElement | null {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
if (Array.isArray(children)) {
|
||||||
|
return (
|
||||||
|
<Grid container direction="row">
|
||||||
|
{children
|
||||||
|
// Exclude falsy values to allow conditional rendering
|
||||||
|
.filter(x => x)
|
||||||
|
.map((a, i) => (
|
||||||
|
<Grid key={i} className={classes.action}>
|
||||||
|
{a}
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid container direction="row">
|
||||||
|
<Grid className={classes.action}>{children}</Grid>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import { Button, Grid, IconButton, InputBase, ListItem, Typography } from '@material-ui/core'
|
||||||
|
import Collapse from '@material-ui/core/Collapse'
|
||||||
|
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||||
|
import { ChangeEvent, ReactElement, useState } from 'react'
|
||||||
|
import { Check, Edit, Minus, RotateCcw } from 'react-feather'
|
||||||
|
import ExpandableListItemActions from './ExpandableListItemActions'
|
||||||
|
import ExpandableListItemNote from './ExpandableListItemNote'
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
header: {
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
marginBottom: theme.spacing(0.25),
|
||||||
|
borderLeft: `${theme.spacing(0.25)}px solid rgba(0,0,0,0)`,
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
},
|
||||||
|
headerOpen: {
|
||||||
|
borderLeft: `${theme.spacing(0.25)}px solid ${theme.palette.primary.main}`,
|
||||||
|
},
|
||||||
|
copyValue: {
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: theme.spacing(1),
|
||||||
|
borderRadius: 0,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#fcf2e8',
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
marginBottom: theme.spacing(1),
|
||||||
|
},
|
||||||
|
keyMargin: {
|
||||||
|
marginRight: theme.spacing(1),
|
||||||
|
},
|
||||||
|
unselectableLabel: {
|
||||||
|
cursor: 'default',
|
||||||
|
userSelect: 'none',
|
||||||
|
// Many browsers don't support yet the general user-select css property
|
||||||
|
WebkitUserSelect: 'none',
|
||||||
|
MozUserSelect: 'none',
|
||||||
|
msUserSelect: 'none',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
label: string
|
||||||
|
value?: string
|
||||||
|
placeholder?: string
|
||||||
|
helperText?: string
|
||||||
|
expandedOnly?: boolean
|
||||||
|
confirmLabel?: string
|
||||||
|
confirmLabelDisabled?: boolean
|
||||||
|
onChange?: (value: string) => void
|
||||||
|
onConfirm: (value: string) => void
|
||||||
|
mapperFn?: (value: string) => string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ExpandableListItemKey({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onConfirm,
|
||||||
|
onChange,
|
||||||
|
confirmLabel,
|
||||||
|
confirmLabelDisabled,
|
||||||
|
expandedOnly,
|
||||||
|
helperText,
|
||||||
|
placeholder,
|
||||||
|
mapperFn,
|
||||||
|
}: Props): ReactElement | null {
|
||||||
|
const classes = useStyles()
|
||||||
|
const [open, setOpen] = useState(Boolean(expandedOnly))
|
||||||
|
const [inputValue, setInputValue] = useState<string>(value || '')
|
||||||
|
const toggleOpen = () => setOpen(!open)
|
||||||
|
const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (mapperFn) {
|
||||||
|
e.target.value = mapperFn(e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
setInputValue(e.target.value)
|
||||||
|
|
||||||
|
if (onChange) onChange(e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ListItem className={`${classes.header} ${open ? classes.headerOpen : ''}`}>
|
||||||
|
<Grid container direction="column" justifyContent="space-between" alignItems="stretch">
|
||||||
|
<Grid container direction="row" justifyContent="space-between" alignItems="center">
|
||||||
|
{label && (
|
||||||
|
<Typography variant="body1" className={classes.unselectableLabel}>
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<Typography variant="body2">
|
||||||
|
<div>
|
||||||
|
{!open && value}
|
||||||
|
{!expandedOnly && (
|
||||||
|
<IconButton size="small" className={classes.copyValue}>
|
||||||
|
{open ? (
|
||||||
|
<Minus onClick={toggleOpen} strokeWidth={1} />
|
||||||
|
) : (
|
||||||
|
<Edit onClick={toggleOpen} strokeWidth={1} />
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||||
|
<InputBase
|
||||||
|
value={inputValue}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onChange={handleChange}
|
||||||
|
fullWidth
|
||||||
|
className={classes.content}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</Collapse>
|
||||||
|
</Grid>
|
||||||
|
</ListItem>
|
||||||
|
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||||
|
{helperText && <ExpandableListItemNote>{helperText}</ExpandableListItemNote>}
|
||||||
|
<ExpandableListItemActions>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
disabled={
|
||||||
|
inputValue === value ||
|
||||||
|
Boolean(confirmLabelDisabled) || // Disable if external validation is provided
|
||||||
|
(inputValue === '' && value === undefined) // Disable if no initial value was not provided and the field is empty. The undefined check is improtant so that it is possible to submit with empty input in other cases
|
||||||
|
}
|
||||||
|
startIcon={<Check size="1rem" />}
|
||||||
|
onClick={() => onConfirm(inputValue)}
|
||||||
|
>
|
||||||
|
{confirmLabel || 'Save'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
disabled={inputValue === value || inputValue === ''}
|
||||||
|
startIcon={<RotateCcw size="1rem" />}
|
||||||
|
onClick={() => setInputValue(value || '')}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ExpandableListItemActions>
|
||||||
|
</Collapse>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import { Grid, IconButton, ListItem, Tooltip, Typography } from '@material-ui/core'
|
||||||
|
import Collapse from '@material-ui/core/Collapse'
|
||||||
|
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||||
|
import { ReactElement, useState } from 'react'
|
||||||
|
import { CopyToClipboard } from 'react-copy-to-clipboard'
|
||||||
|
import { Eye, Minus } from 'react-feather'
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
header: {
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
marginBottom: theme.spacing(0.25),
|
||||||
|
borderLeft: `${theme.spacing(0.25)}px solid rgba(0,0,0,0)`,
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
},
|
||||||
|
headerOpen: {
|
||||||
|
borderLeft: `${theme.spacing(0.25)}px solid ${theme.palette.primary.main}`,
|
||||||
|
},
|
||||||
|
copyValue: {
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: theme.spacing(1),
|
||||||
|
borderRadius: 0,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#fcf2e8',
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
marginBottom: theme.spacing(2),
|
||||||
|
},
|
||||||
|
keyMargin: {
|
||||||
|
marginRight: theme.spacing(1),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const lengthWithoutPrefix = (s: string) => s.replace(/^0x/i, '').length
|
||||||
|
|
||||||
|
function isPrefixedHexString(s: unknown): boolean {
|
||||||
|
return typeof s === 'string' && /^0x[0-9a-f]+$/i.test(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
const split = (s: string): string[] => {
|
||||||
|
const nonPrefixLength = lengthWithoutPrefix(s)
|
||||||
|
|
||||||
|
if (nonPrefixLength % 6 === 0) return s.match(/(0x|.{6})/gi) || []
|
||||||
|
|
||||||
|
return s.match(/(0x|.{1,8})/gi) || []
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ExpandableListItemKey({ label, value }: Props): ReactElement | null {
|
||||||
|
const classes = useStyles()
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [copied, setCopied] = useState(false)
|
||||||
|
const toggleOpen = () => setOpen(!open)
|
||||||
|
|
||||||
|
const tooltipClickHandler = () => setCopied(true)
|
||||||
|
const tooltipCloseHandler = () => setCopied(false)
|
||||||
|
|
||||||
|
const splitValues = split(value)
|
||||||
|
const hasPrefix = isPrefixedHexString(value)
|
||||||
|
const spanText = `${hasPrefix ? `${splitValues[0]} ${splitValues[1]}` : splitValues[0]}[…]${
|
||||||
|
splitValues[splitValues.length - 1]
|
||||||
|
}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem className={`${classes.header} ${open ? classes.headerOpen : ''}`}>
|
||||||
|
<Grid container direction="column" justifyContent="space-between" alignItems="stretch">
|
||||||
|
<Grid container direction="row" justifyContent="space-between" alignItems="center">
|
||||||
|
{label && <Typography variant="body1">{label}</Typography>}
|
||||||
|
<Typography variant="body2">
|
||||||
|
<div>
|
||||||
|
{!open && (
|
||||||
|
<span className={classes.copyValue}>
|
||||||
|
<Tooltip title={copied ? 'Copied' : 'Copy'} placement="top" arrow onClose={tooltipCloseHandler}>
|
||||||
|
<CopyToClipboard text={value}>
|
||||||
|
<span onClick={tooltipClickHandler}>{value ? spanText : ''}</span>
|
||||||
|
</CopyToClipboard>
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<IconButton size="small" className={classes.copyValue}>
|
||||||
|
{open ? <Minus onClick={toggleOpen} strokeWidth={1} /> : <Eye onClick={toggleOpen} strokeWidth={1} />}
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||||
|
<div className={classes.content}>
|
||||||
|
<Tooltip title={copied ? 'Copied' : 'Copy'} placement="top" arrow onClose={tooltipCloseHandler}>
|
||||||
|
<CopyToClipboard text={value}>
|
||||||
|
{/* This has to be wrapped in two spans otherwise either the tooltip or the highlighting does not work*/}
|
||||||
|
<span onClick={tooltipClickHandler}>
|
||||||
|
<span className={classes.copyValue}>
|
||||||
|
{splitValues.map((s, i) => (
|
||||||
|
<Typography variant="body2" key={i} className={classes.keyMargin} component="span">
|
||||||
|
{s}
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</CopyToClipboard>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</Collapse>
|
||||||
|
</Grid>
|
||||||
|
</ListItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import { Grid, IconButton, ListItem, Tooltip, Typography } from '@material-ui/core'
|
||||||
|
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||||
|
import { ArrowForward, OpenInNewSharp } from '@material-ui/icons'
|
||||||
|
import { ReactElement, useState } from 'react'
|
||||||
|
import CopyToClipboard from 'react-copy-to-clipboard'
|
||||||
|
import { useHistory } from 'react-router'
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
header: {
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
marginBottom: theme.spacing(0.25),
|
||||||
|
borderLeft: `${theme.spacing(0.25)}px solid rgba(0,0,0,0)`,
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
},
|
||||||
|
headerOpen: {
|
||||||
|
borderLeft: `${theme.spacing(0.25)}px solid ${theme.palette.primary.main}`,
|
||||||
|
},
|
||||||
|
openLinkIcon: {
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: theme.spacing(1),
|
||||||
|
borderRadius: 0,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#fcf2e8',
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
marginBottom: theme.spacing(2),
|
||||||
|
},
|
||||||
|
keyMargin: {
|
||||||
|
marginRight: theme.spacing(1),
|
||||||
|
},
|
||||||
|
copyValue: {
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: theme.spacing(1),
|
||||||
|
borderRadius: 0,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#fcf2e8',
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
link?: string
|
||||||
|
navigationType?: 'NEW_WINDOW' | 'HISTORY_PUSH'
|
||||||
|
allowClipboard?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ExpandableListItemLink({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
link,
|
||||||
|
navigationType = 'NEW_WINDOW',
|
||||||
|
allowClipboard = true,
|
||||||
|
}: Props): ReactElement | null {
|
||||||
|
const classes = useStyles()
|
||||||
|
const [copied, setCopied] = useState(false)
|
||||||
|
const history = useHistory()
|
||||||
|
|
||||||
|
const tooltipClickHandler = () => setCopied(true)
|
||||||
|
const tooltipCloseHandler = () => setCopied(false)
|
||||||
|
|
||||||
|
const displayValue = value.length > 22 ? value.slice(0, 19) + '...' : value
|
||||||
|
|
||||||
|
function onNavigation() {
|
||||||
|
if (navigationType === 'NEW_WINDOW') {
|
||||||
|
window.open(link || value)
|
||||||
|
} else {
|
||||||
|
history.push(link || value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem className={classes.header}>
|
||||||
|
<Grid container direction="column" justifyContent="space-between" alignItems="stretch">
|
||||||
|
<Grid container direction="row" justifyContent="space-between" alignItems="center">
|
||||||
|
{label && <Typography variant="body1">{label}</Typography>}
|
||||||
|
<Typography variant="body2">
|
||||||
|
<div>
|
||||||
|
{allowClipboard && (
|
||||||
|
<span className={classes.copyValue}>
|
||||||
|
<Tooltip title={copied ? 'Copied' : 'Copy'} placement="top" arrow onClose={tooltipCloseHandler}>
|
||||||
|
<CopyToClipboard text={value}>
|
||||||
|
<span onClick={tooltipClickHandler}>{displayValue}</span>
|
||||||
|
</CopyToClipboard>
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{!allowClipboard && <span onClick={onNavigation}>{displayValue}</span>}
|
||||||
|
<IconButton size="small" className={classes.openLinkIcon}>
|
||||||
|
{navigationType === 'NEW_WINDOW' && <OpenInNewSharp onClick={onNavigation} strokeWidth={1} />}
|
||||||
|
{navigationType === 'HISTORY_PUSH' && <ArrowForward onClick={onNavigation} strokeWidth={1} />}
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</ListItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { ReactElement, ReactNode } from 'react'
|
||||||
|
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
|
||||||
|
import { Typography } from '@material-ui/core'
|
||||||
|
import ListItem from '@material-ui/core/ListItem'
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
header: {
|
||||||
|
backgroundColor: '#F7F7F7',
|
||||||
|
marginBottom: theme.spacing(0.25),
|
||||||
|
},
|
||||||
|
typography: {
|
||||||
|
color: '#242424',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children?: ReactNode | ReactNode[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ExpandableListItemNote({ children }: Props): ReactElement | null {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem className={classes.header}>
|
||||||
|
<Typography variant="body1" className={classes.typography}>
|
||||||
|
{children}
|
||||||
|
</Typography>
|
||||||
|
</ListItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { createStyles, makeStyles } from '@material-ui/core'
|
||||||
|
import { ReactElement } from 'react'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(() =>
|
||||||
|
createStyles({
|
||||||
|
image: {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'cover',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
alt: string
|
||||||
|
src: string | undefined
|
||||||
|
maxHeight?: string
|
||||||
|
maxWidth?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FitImage(props: Props): ReactElement {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const inlineStyles: Record<string, string> = {}
|
||||||
|
|
||||||
|
props.maxHeight && (inlineStyles.maxHeight = props.maxHeight)
|
||||||
|
props.maxWidth && (inlineStyles.maxWidth = props.maxWidth)
|
||||||
|
|
||||||
|
return <img className={classes.image} alt={props.alt} src={props.src} style={inlineStyles} />
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { ReactElement, useEffect, useState } from 'react'
|
||||||
|
import { getPrettyDateString } from '../utils/date'
|
||||||
|
import { getHistorySafe, HistoryItem, HISTORY_KEYS } from '../utils/local-storage'
|
||||||
|
import ExpandableList from './ExpandableList'
|
||||||
|
import ExpandableListItemLink from './ExpandableListItemLink'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string
|
||||||
|
localStorageKey: HISTORY_KEYS
|
||||||
|
}
|
||||||
|
|
||||||
|
export function History({ title, localStorageKey }: Props): ReactElement | null {
|
||||||
|
const [items, setItems] = useState<HistoryItem[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setItems(getHistorySafe(localStorageKey))
|
||||||
|
}, [localStorageKey])
|
||||||
|
|
||||||
|
if (!items.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ExpandableList label={title} defaultOpen>
|
||||||
|
{items.map((x, i) => (
|
||||||
|
<ExpandableListItemLink
|
||||||
|
label={getPrettyDateString(new Date(x.createdAt))}
|
||||||
|
value={x.name}
|
||||||
|
link={'/files/hash/' + x.hash}
|
||||||
|
key={i}
|
||||||
|
navigationType="HISTORY_PUSH"
|
||||||
|
allowClipboard={false}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ExpandableList>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { Box, createStyles, Grid, makeStyles, Typography } from '@material-ui/core'
|
||||||
|
import { ArrowBack } from '@material-ui/icons'
|
||||||
|
import { ReactElement } from 'react'
|
||||||
|
import { useHistory } from 'react-router-dom'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles(() =>
|
||||||
|
createStyles({
|
||||||
|
pressable: {
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
color: '#242424',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export function HistoryHeader({ children }: Props): ReactElement {
|
||||||
|
const classes = useStyles()
|
||||||
|
const history = useHistory()
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
history.goBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box mb={4}>
|
||||||
|
<Grid container direction="row">
|
||||||
|
<Box mr={2}>
|
||||||
|
<div className={classes.pressable} onClick={goBack}>
|
||||||
|
<ArrowBack className={classes.icon} />
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="h1">{children}</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { CircularProgress, Grid } from '@material-ui/core'
|
||||||
|
import { ReactElement } from 'react'
|
||||||
|
|
||||||
|
export function Loading(): ReactElement {
|
||||||
|
return (
|
||||||
|
<Grid container direction="row" justifyContent="center" alignItems="center">
|
||||||
|
<CircularProgress />
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import type { ReactElement } from 'react'
|
|
||||||
import { Typography } from '@material-ui/core'
|
|
||||||
|
|
||||||
function truncStringPortion(str: string, firstCharCount = 10, endCharCount = 10) {
|
|
||||||
return `${str.substring(0, firstCharCount)}...${str.substring(str.length - endCharCount, str.length)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
peerId: string
|
|
||||||
characterLength?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PeerDetail({ peerId, characterLength }: Props): ReactElement {
|
|
||||||
return (
|
|
||||||
<Typography
|
|
||||||
variant="button"
|
|
||||||
style={{
|
|
||||||
fontFamily: 'monospace, monospace',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{truncStringPortion(peerId, characterLength, characterLength)}
|
|
||||||
</Typography>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
import type { ReactElement } from 'react'
|
import { Divider, Drawer, Grid, Link as MUILink, List } from '@material-ui/core'
|
||||||
import { Link } from 'react-router-dom'
|
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||||
|
|
||||||
import { createStyles, Theme, makeStyles } from '@material-ui/core/styles'
|
|
||||||
import { OpenInNewSharp } from '@material-ui/icons'
|
import { OpenInNewSharp } from '@material-ui/icons'
|
||||||
import { Divider, List, Drawer, Grid, Link as MUILink } from '@material-ui/core'
|
import type { ReactElement } from 'react'
|
||||||
import { Home, FileText, DollarSign, Share2, Settings, Layers, BookOpen } from 'react-feather'
|
import { BookOpen, DollarSign, FileText, Home, Layers, Settings } from 'react-feather'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import Logo from '../assets/logo.svg'
|
||||||
import { ROUTES } from '../routes'
|
import { ROUTES } from '../routes'
|
||||||
import SideBarItem from './SideBarItem'
|
import SideBarItem from './SideBarItem'
|
||||||
import SideBarStatus from './SideBarStatus'
|
import SideBarStatus from './SideBarStatus'
|
||||||
|
|
||||||
import Logo from '../assets/logo.svg'
|
|
||||||
|
|
||||||
const navBarItems = [
|
const navBarItems = [
|
||||||
{
|
{
|
||||||
label: 'Info',
|
label: 'Info',
|
||||||
@@ -19,7 +17,7 @@ const navBarItems = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Files',
|
label: 'Files',
|
||||||
path: ROUTES.FILES,
|
path: ROUTES.UPLOAD,
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -32,11 +30,6 @@ const navBarItems = [
|
|||||||
path: ROUTES.ACCOUNTING,
|
path: ROUTES.ACCOUNTING,
|
||||||
icon: DollarSign,
|
icon: DollarSign,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: 'Peers',
|
|
||||||
path: ROUTES.PEERS,
|
|
||||||
icon: Share2,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: 'Settings',
|
label: 'Settings',
|
||||||
path: ROUTES.SETTINGS,
|
path: ROUTES.SETTINGS,
|
||||||
@@ -44,6 +37,8 @@ const navBarItems = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const drawerWidth = 300
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
createStyles({
|
createStyles({
|
||||||
root: {
|
root: {
|
||||||
@@ -52,6 +47,14 @@ const useStyles = makeStyles((theme: Theme) =>
|
|||||||
paddingTop: theme.spacing(8),
|
paddingTop: theme.spacing(8),
|
||||||
paddingBottom: theme.spacing(8),
|
paddingBottom: theme.spacing(8),
|
||||||
},
|
},
|
||||||
|
drawer: {
|
||||||
|
width: drawerWidth,
|
||||||
|
flexShrink: 0,
|
||||||
|
},
|
||||||
|
drawerPaper: {
|
||||||
|
width: drawerWidth,
|
||||||
|
backgroundColor: '#212121',
|
||||||
|
},
|
||||||
logo: {
|
logo: {
|
||||||
marginLeft: theme.spacing(8),
|
marginLeft: theme.spacing(8),
|
||||||
marginRight: theme.spacing(8),
|
marginRight: theme.spacing(8),
|
||||||
@@ -86,7 +89,7 @@ export default function SideBar(): ReactElement {
|
|||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer variant="permanent">
|
<Drawer className={classes.drawer} variant="permanent" anchor="left" classes={{ paper: classes.drawerPaper }}>
|
||||||
<Grid container direction="column" justifyContent="space-between" className={classes.root}>
|
<Grid container direction="column" justifyContent="space-between" className={classes.root}>
|
||||||
<Grid className={classes.logo}>
|
<Grid className={classes.logo}>
|
||||||
<Link to={ROUTES.INFO}>
|
<Link to={ROUTES.INFO}>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ArrowRight } from 'react-feather'
|
|||||||
import { createStyles, Theme, makeStyles } from '@material-ui/core/styles'
|
import { createStyles, Theme, makeStyles } from '@material-ui/core/styles'
|
||||||
import { ListItemText, ListItemIcon, ListItem, Typography } from '@material-ui/core'
|
import { ListItemText, ListItemIcon, ListItem, Typography } from '@material-ui/core'
|
||||||
import { Context } from '../providers/Bee'
|
import { Context } from '../providers/Bee'
|
||||||
|
import StatusIcon from './StatusIcon'
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
createStyles({
|
createStyles({
|
||||||
@@ -32,10 +33,12 @@ const useStyles = makeStyles((theme: Theme) =>
|
|||||||
button: {
|
button: {
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: '#2c2c2c',
|
backgroundColor: '#2c2c2c',
|
||||||
|
color: 'white',
|
||||||
|
|
||||||
// https://github.com/mui-org/material-ui/issues/22543
|
// https://github.com/mui-org/material-ui/issues/22543
|
||||||
'@media (hover: none)': {
|
'@media (hover: none)': {
|
||||||
backgroundColor: '#2c2c2c',
|
backgroundColor: '#2c2c2c',
|
||||||
|
color: 'white',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -50,7 +53,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function SideBarItem({ path }: Props): ReactElement {
|
export default function SideBarItem({ path }: Props): ReactElement {
|
||||||
const { status } = useContext(Context)
|
const { status, isLoading } = useContext(Context)
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const isSelected = Boolean(matchPath(location.pathname, { path, exact: true }))
|
const isSelected = Boolean(matchPath(location.pathname, { path, exact: true }))
|
||||||
@@ -62,17 +65,8 @@ export default function SideBarItem({ path }: Props): ReactElement {
|
|||||||
selected={isSelected}
|
selected={isSelected}
|
||||||
disableRipple
|
disableRipple
|
||||||
>
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon style={{ marginLeft: '30px' }}>
|
||||||
<span
|
<StatusIcon isOk={status.all} isLoading={isLoading} />
|
||||||
style={{
|
|
||||||
backgroundColor: status.all ? '#1de600' : '#ff3a52',
|
|
||||||
height: '14px',
|
|
||||||
width: '14px',
|
|
||||||
borderRadius: '50%',
|
|
||||||
display: 'inline-block',
|
|
||||||
marginLeft: 30,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={<Typography className={classes.smallerText}>{`Node ${status.all ? 'OK' : 'Error'}`}</Typography>}
|
primary={<Typography className={classes.smallerText}>{`Node ${status.all ? 'OK' : 'Error'}`}</Typography>}
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
import { Card, CardContent, Typography } from '@material-ui/core/'
|
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
|
||||||
import { Skeleton } from '@material-ui/lab'
|
|
||||||
import type { ReactElement } from 'react'
|
|
||||||
import { Title } from './Title'
|
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
|
||||||
root: {
|
|
||||||
minWidth: 275,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
label: string
|
|
||||||
statistic?: string
|
|
||||||
loading?: boolean
|
|
||||||
tooltip?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function StatCard({ loading, label, statistic, tooltip }: Props): ReactElement {
|
|
||||||
const classes = useStyles()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className={classes.root}>
|
|
||||||
<CardContent>
|
|
||||||
{loading && (
|
|
||||||
<>
|
|
||||||
<Skeleton width={180} height={25} animation="wave" />
|
|
||||||
<Skeleton width={180} height={35} animation="wave" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!loading && (
|
|
||||||
<>
|
|
||||||
<Title label={label} tooltip={tooltip} />
|
|
||||||
<Typography variant="h5" component="h2">
|
|
||||||
{statistic}
|
|
||||||
</Typography>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { ReactElement } from 'react'
|
||||||
|
import { CircularProgress } from '@material-ui/core'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isOk: boolean
|
||||||
|
isLoading?: boolean
|
||||||
|
size?: number | string
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function StatusIcon({ isOk, size, className, isLoading }: Props): ReactElement {
|
||||||
|
const s = size || '1rem'
|
||||||
|
|
||||||
|
if (isLoading) return <CircularProgress size={s} className={className} />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
backgroundColor: isOk ? '#1de600' : '#ff3a52',
|
||||||
|
height: s,
|
||||||
|
width: s,
|
||||||
|
borderRadius: '50%',
|
||||||
|
display: 'inline-block',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { createStyles, makeStyles } from '@material-ui/core'
|
||||||
|
import { ReactElement } from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactElement | ReactElement[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles(() =>
|
||||||
|
createStyles({
|
||||||
|
wrapper: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
width: '175px',
|
||||||
|
height: '175px',
|
||||||
|
background: `repeating-linear-gradient(
|
||||||
|
45deg,
|
||||||
|
#efefef,
|
||||||
|
#efefef 4px,
|
||||||
|
#ffffff 4px,
|
||||||
|
#ffffff 8px
|
||||||
|
)`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export function StripedWrapper({ children }: Props): ReactElement {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
return <div className={classes.wrapper}>{children}</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { Button, CircularProgress, createStyles, makeStyles } from '@material-ui/core'
|
||||||
|
import React, { ReactElement } from 'react'
|
||||||
|
import { IconProps } from 'react-feather'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClick: () => void
|
||||||
|
iconType: React.ComponentType<IconProps>
|
||||||
|
children: string
|
||||||
|
className?: string
|
||||||
|
disabled?: boolean
|
||||||
|
loading?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles(() =>
|
||||||
|
createStyles({
|
||||||
|
button: {
|
||||||
|
position: 'relative',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
'&:hover, &:focus': {
|
||||||
|
'& svg': {
|
||||||
|
stroke: '#fff',
|
||||||
|
transition: '0.1s',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
spinnerWrapper: {
|
||||||
|
position: 'absolute',
|
||||||
|
left: '50%',
|
||||||
|
top: '50%',
|
||||||
|
width: '40px',
|
||||||
|
height: '40px',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export function SwarmButton({ children, onClick, iconType, className, disabled, loading }: Props): ReactElement {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const icon = React.createElement(iconType, {
|
||||||
|
size: '1.25rem',
|
||||||
|
color: disabled ? 'rgba(0, 0, 0, 0.26)' : '#dd7700',
|
||||||
|
})
|
||||||
|
|
||||||
|
const classNames = className ? [className, classes.button].join(' ') : classes.button
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={classNames}
|
||||||
|
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
onClick()
|
||||||
|
event.currentTarget.blur()
|
||||||
|
}}
|
||||||
|
variant="contained"
|
||||||
|
startIcon={icon}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{loading && (
|
||||||
|
<div className={classes.spinnerWrapper}>
|
||||||
|
<CircularProgress />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
import React, { ReactElement, ReactNode } from 'react'
|
import React, { ReactElement, ReactNode } from 'react'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
|
||||||
import Tabs from '@material-ui/core/Tabs'
|
import { Tab, Tabs } from '@material-ui/core'
|
||||||
import Tab from '@material-ui/core/Tab'
|
|
||||||
import Typography from '@material-ui/core/Typography'
|
|
||||||
import Box from '@material-ui/core/Box'
|
|
||||||
|
|
||||||
interface TabPanelProps {
|
interface TabPanelProps {
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
@@ -16,24 +13,25 @@ function TabPanel(props: TabPanelProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div role="tabpanel" hidden={value !== index} {...other}>
|
<div role="tabpanel" hidden={value !== index} {...other}>
|
||||||
{value === index && (
|
{value === index && children}
|
||||||
<Box p={3}>
|
|
||||||
<Typography>{children}</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles(() => ({
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
root: {
|
root: {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
},
|
},
|
||||||
}))
|
content: {
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
interface TabsValues {
|
interface TabsValues {
|
||||||
component: ReactNode
|
component: ReactNode
|
||||||
label: string
|
label: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -55,16 +53,18 @@ export default function SimpleTabs({ values, index, indexChanged }: Props): Reac
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.root}>
|
<div className={classes.root}>
|
||||||
<Tabs value={v} onChange={handleChange}>
|
<Tabs value={v} onChange={handleChange} variant="fullWidth">
|
||||||
{values.map(({ label }, idx) => (
|
{values.map(({ label }, idx) => (
|
||||||
<Tab key={idx} label={label} />
|
<Tab key={idx} label={label} />
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
<div className={classes.content}>
|
||||||
{values.map(({ component }, idx) => (
|
{values.map(({ component }, idx) => (
|
||||||
<TabPanel key={idx} value={v} index={idx}>
|
<TabPanel key={idx} value={v} index={idx}>
|
||||||
{component}
|
{component}
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
import { Grid, Tooltip, Typography } from '@material-ui/core/'
|
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
|
||||||
import { Info } from '@material-ui/icons'
|
|
||||||
import type { ReactElement } from 'react'
|
|
||||||
|
|
||||||
interface TitleProps {
|
|
||||||
label: string
|
|
||||||
tooltip?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
|
||||||
title: {
|
|
||||||
fontSize: 16,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export function Title({ label, tooltip }: TitleProps): ReactElement {
|
|
||||||
const classes = useStyles()
|
|
||||||
|
|
||||||
if (!tooltip) {
|
|
||||||
return (
|
|
||||||
<Typography className={classes.title} color="textSecondary" gutterBottom>
|
|
||||||
{label}
|
|
||||||
</Typography>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// span is needed as Tooltip expects a non-functional element!
|
|
||||||
return (
|
|
||||||
<Tooltip title={tooltip}>
|
|
||||||
<span>
|
|
||||||
<Grid container direction="row" justify="space-between">
|
|
||||||
<Typography className={classes.title} color="textSecondary" gutterBottom>
|
|
||||||
{label}
|
|
||||||
</Typography>
|
|
||||||
<Info />
|
|
||||||
</Grid>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,66 +1,43 @@
|
|||||||
import type { Topology } from '@ethersphere/bee-js'
|
import type { Topology } from '@ethersphere/bee-js'
|
||||||
import { Grid } from '@material-ui/core/'
|
|
||||||
import type { ReactElement } from 'react'
|
import type { ReactElement } from 'react'
|
||||||
import { pickThreshold, ThresholdValues } from '../utils/threshold'
|
import { pickThreshold, ThresholdValues } from '../utils/threshold'
|
||||||
import StatCard from './StatCard'
|
import ExpandableListItem from './ExpandableListItem'
|
||||||
|
|
||||||
interface RootProps {
|
interface Props {
|
||||||
topology: Topology | null
|
topology: Topology | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props extends RootProps {
|
const TopologyStats = (props: Props): ReactElement => {
|
||||||
thresholds: ThresholdValues
|
|
||||||
}
|
|
||||||
|
|
||||||
const TopologyStats = (props: RootProps): ReactElement => {
|
|
||||||
const thresholds: ThresholdValues = {
|
const thresholds: ThresholdValues = {
|
||||||
connectedPeers: pickThreshold('connectedPeers', props.topology?.connected || 0),
|
connectedPeers: pickThreshold('connectedPeers', props.topology?.connected || 0),
|
||||||
population: pickThreshold('population', props.topology?.population || 0),
|
population: pickThreshold('population', props.topology?.population || 0),
|
||||||
depth: pickThreshold('depth', props.topology?.depth || 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 maximumTotalScore = Object.values(thresholds).reduce((sum, item) => sum + item.maximumScore, 0)
|
||||||
const actualTotalScore = Object.values(thresholds).reduce((sum, item) => sum + item.score, 0)
|
const actualTotalScore = Object.values(thresholds).reduce((sum, item) => sum + item.score, 0)
|
||||||
const percentageText = Math.round((actualTotalScore / maximumTotalScore) * 100) + '%'
|
const percentageText = Math.round((actualTotalScore / maximumTotalScore) * 100) + '%'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ marginBottom: '20px' }}>
|
<>
|
||||||
<StatCard label="Overall Health Indicator" statistic={percentageText} />
|
<ExpandableListItem label="Overall Health Indicator" value={percentageText} />
|
||||||
</div>
|
<ExpandableListItem
|
||||||
|
label="Connected Peers"
|
||||||
|
value={props.topology?.connected.toString()}
|
||||||
|
tooltip={thresholds.connectedPeers.explanation}
|
||||||
|
/>
|
||||||
|
<ExpandableListItem
|
||||||
|
label="Population"
|
||||||
|
value={props.topology?.population.toString()}
|
||||||
|
tooltip={thresholds.population.explanation}
|
||||||
|
/>
|
||||||
|
<ExpandableListItem
|
||||||
|
label="Depth"
|
||||||
|
value={props.topology?.depth.toString()}
|
||||||
|
tooltip={thresholds.depth.explanation}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Metrics = ({ topology, thresholds }: Props): ReactElement => (
|
|
||||||
<Grid style={{ marginBottom: '20px', flexGrow: 1 }}>
|
|
||||||
<Grid container spacing={3}>
|
|
||||||
<Grid key={1} item xs={12} sm={12} md={6} lg={4} xl={4}>
|
|
||||||
<StatCard
|
|
||||||
label="Connected Peers"
|
|
||||||
statistic={topology?.connected.toString()}
|
|
||||||
tooltip={thresholds.connectedPeers.explanation}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid key={2} item xs={12} sm={12} md={6} lg={4} xl={4}>
|
|
||||||
<StatCard
|
|
||||||
label="Population"
|
|
||||||
statistic={topology?.population.toString()}
|
|
||||||
tooltip={thresholds.population.explanation}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid key={3} item xs={12} sm={12} md={6} lg={4} xl={4}>
|
|
||||||
<StatCard label="Depth" statistic={topology?.depth.toString()} tooltip={thresholds.depth.explanation} />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default TopologyStats
|
export default TopologyStats
|
||||||
|
|||||||
@@ -1,48 +1,64 @@
|
|||||||
import type { ReactElement } from 'react'
|
import type { ReactElement } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||||
import { Card, CardContent, Typography } from '@material-ui/core/'
|
import { Button, Grid, Typography, Link as MuiLink } from '@material-ui/core/'
|
||||||
import { ROUTES } from '../routes'
|
import { ROUTES } from '../routes'
|
||||||
|
import { Activity } from 'react-feather'
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
root: {
|
root: {
|
||||||
flexGrow: 1,
|
height: '100%',
|
||||||
marginTop: '20px',
|
|
||||||
},
|
},
|
||||||
title: {
|
content: {
|
||||||
textAlign: 'center',
|
maxWidth: 500,
|
||||||
fontSize: 26,
|
marginBottom: theme.spacing(4),
|
||||||
|
'&:last-child': {
|
||||||
|
marginBottom: 0,
|
||||||
},
|
},
|
||||||
})
|
},
|
||||||
|
icon: {
|
||||||
|
height: '1rem',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
export default function TroubleshootConnectionCard(): ReactElement {
|
export default function TroubleshootConnectionCard(): ReactElement {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={classes.root}>
|
<Grid container direction="column" justifyContent="center" alignItems="center" className={classes.root}>
|
||||||
<CardContent>
|
<Grid item className={classes.content}>
|
||||||
<Typography className={classes.title} gutterBottom>
|
<Typography variant="h1" align="center">
|
||||||
Looks like your node is not connected
|
Uh oh, it looks like your node is not connected.
|
||||||
</Typography>
|
</Typography>
|
||||||
<div style={{ marginBottom: '20px', textAlign: 'center' }}>
|
</Grid>
|
||||||
<strong>
|
<Grid item className={classes.content}>
|
||||||
<Link to={ROUTES.STATUS}>Click to run status checks</Link> on your nodes connection or check out the{' '}
|
<Typography align="center">
|
||||||
<a href={process.env.REACT_APP_BEE_DOCS_HOST} target="_blank" rel="noreferrer">
|
Please check your node status to fix the problem. You can also check out the{' '}
|
||||||
|
<MuiLink href={process.env.REACT_APP_BEE_DOCS_HOST} target="_blank" rel="noreferrer">
|
||||||
Swarm Bee Docs
|
Swarm Bee Docs
|
||||||
</a>
|
</MuiLink>{' '}
|
||||||
</strong>
|
or ask for support on the{' '}
|
||||||
</div>
|
<MuiLink href={process.env.REACT_APP_BEE_DISCORD_HOST} target="_blank" rel="noreferrer">
|
||||||
|
Ethereum Swarm Discord
|
||||||
<div style={{ marginBottom: '20px', textAlign: 'center' }}>
|
</MuiLink>
|
||||||
<p style={{ marginTop: '50px' }}>
|
.
|
||||||
Still not working? Drop us a message on the Ethereum Swarm{' '}
|
</Typography>
|
||||||
<a href={process.env.REACT_APP_BEE_DISCORD_HOST} target="_blank" rel="noreferrer">
|
</Grid>
|
||||||
Discord
|
<Grid item className={classes.content}>
|
||||||
</a>
|
<Typography align="center">
|
||||||
</p>
|
<Button
|
||||||
</div>
|
component={Link}
|
||||||
</CardContent>
|
variant="contained"
|
||||||
</Card>
|
startIcon={<Activity className={classes.icon} />}
|
||||||
|
to={ROUTES.STATUS}
|
||||||
|
>
|
||||||
|
Check node status
|
||||||
|
</Button>
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ReactElement, useState } from 'react'
|
import { ReactElement, ReactNode, useState } from 'react'
|
||||||
import Button from '@material-ui/core/Button'
|
import Button from '@material-ui/core/Button'
|
||||||
import Input from '@material-ui/core/Input'
|
import Input from '@material-ui/core/Input'
|
||||||
import Dialog from '@material-ui/core/Dialog'
|
import Dialog from '@material-ui/core/Dialog'
|
||||||
@@ -19,6 +19,7 @@ interface Props {
|
|||||||
max?: BigNumber
|
max?: BigNumber
|
||||||
min?: BigNumber
|
min?: BigNumber
|
||||||
action: (amount: bigint) => Promise<string>
|
action: (amount: bigint) => Promise<string>
|
||||||
|
icon?: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WithdrawDepositModal({
|
export default function WithdrawDepositModal({
|
||||||
@@ -29,6 +30,7 @@ export default function WithdrawDepositModal({
|
|||||||
max,
|
max,
|
||||||
label,
|
label,
|
||||||
action,
|
action,
|
||||||
|
icon,
|
||||||
}: Props): ReactElement {
|
}: Props): ReactElement {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [amount, setAmount] = useState('')
|
const [amount, setAmount] = useState('')
|
||||||
@@ -36,8 +38,9 @@ export default function WithdrawDepositModal({
|
|||||||
const [amountError, setAmountError] = useState<Error | null>(null)
|
const [amountError, setAmountError] = useState<Error | null>(null)
|
||||||
const { enqueueSnackbar } = useSnackbar()
|
const { enqueueSnackbar } = useSnackbar()
|
||||||
|
|
||||||
const handleClickOpen = () => {
|
const handleClickOpen = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
setOpen(true)
|
setOpen(true)
|
||||||
|
e.stopPropagation()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
@@ -52,7 +55,7 @@ export default function WithdrawDepositModal({
|
|||||||
setOpen(false)
|
setOpen(false)
|
||||||
enqueueSnackbar(`${successMessage} Transaction ${transactionHash}`, { variant: 'success' })
|
enqueueSnackbar(`${successMessage} Transaction ${transactionHash}`, { variant: 'success' })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
enqueueSnackbar(`${errorMessage} Error: ${e.message}`, { variant: 'error' })
|
enqueueSnackbar(`${errorMessage} Error: ${(e as Error).message}`, { variant: 'error' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,13 +71,13 @@ export default function WithdrawDepositModal({
|
|||||||
|
|
||||||
if (max && t.toDecimal.isGreaterThan(max)) setAmountError(new Error(`Needs to be less than ${max}`))
|
if (max && t.toDecimal.isGreaterThan(max)) setAmountError(new Error(`Needs to be less than ${max}`))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setAmountError(e)
|
setAmountError(e as Error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
|
<Button variant="contained" onClick={handleClickOpen} startIcon={icon}>
|
||||||
{label}
|
{label}
|
||||||
</Button>
|
</Button>
|
||||||
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title">
|
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ReactElement, useContext } from 'react'
|
import { ReactElement, useContext } from 'react'
|
||||||
|
import { Download } from 'react-feather'
|
||||||
import { Context as SettingsContext } from '../providers/Settings'
|
import { Context as SettingsContext } from '../providers/Settings'
|
||||||
|
|
||||||
import WithdrawDepositModal from '../components/WithdrawDepositModal'
|
import WithdrawDepositModal from '../components/WithdrawDepositModal'
|
||||||
@@ -11,8 +12,9 @@ export default function DepositModal(): ReactElement {
|
|||||||
<WithdrawDepositModal
|
<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 deposit to your node."
|
||||||
label="Deposit"
|
label="Deposit"
|
||||||
|
icon={<Download size="1rem" />}
|
||||||
min={new BigNumber(0)}
|
min={new BigNumber(0)}
|
||||||
action={(amount: bigint) => {
|
action={(amount: bigint) => {
|
||||||
if (!beeDebugApi) throw new Error('Bee Debug URL is not valid')
|
if (!beeDebugApi) throw new Error('Bee Debug URL is not valid')
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import { ReactElement, useContext } from 'react'
|
|
||||||
import { Context as SettingsContext } from '../providers/Settings'
|
|
||||||
|
|
||||||
import WithdrawDepositModal from '../components/WithdrawDepositModal'
|
|
||||||
import { BigNumber } from 'bignumber.js'
|
import { BigNumber } from 'bignumber.js'
|
||||||
|
import { ReactElement, useContext } from 'react'
|
||||||
|
import { Upload } from 'react-feather'
|
||||||
|
import WithdrawDepositModal from '../components/WithdrawDepositModal'
|
||||||
|
import { Context as SettingsContext } from '../providers/Settings'
|
||||||
|
|
||||||
export default function WithdrawModal(): ReactElement {
|
export default function WithdrawModal(): ReactElement {
|
||||||
const { beeDebugApi } = useContext(SettingsContext)
|
const { beeDebugApi } = useContext(SettingsContext)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WithdrawDepositModal
|
<WithdrawDepositModal
|
||||||
successMessage="Successful withdrawl."
|
successMessage="Successful withdrawal."
|
||||||
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"
|
||||||
|
icon={<Upload size="1rem" />}
|
||||||
min={new BigNumber(0)}
|
min={new BigNumber(0)}
|
||||||
action={(amount: bigint) => {
|
action={(amount: bigint) => {
|
||||||
if (!beeDebugApi) throw new Error('Bee Debug URL is not valid')
|
if (!beeDebugApi) throw new Error('Bee Debug URL is not valid')
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Balance, Settlements, Settlement } from '../types'
|
|||||||
|
|
||||||
interface UseAccountingHook {
|
interface UseAccountingHook {
|
||||||
isLoadingUncashed: boolean
|
isLoadingUncashed: boolean
|
||||||
|
totalUncashed: Token
|
||||||
accounting: Accounting[] | null
|
accounting: Accounting[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,16 +61,21 @@ 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)
|
||||||
if (!uncashedAmounts) return Object.values(accounting)
|
if (!uncashedAmounts) return Object.values(accounting).sort((a, b) => (a.peer < b.peer ? -1 : 1))
|
||||||
|
|
||||||
uncashedAmounts?.forEach(({ peer, uncashedAmount }) => {
|
uncashedAmounts?.forEach(({ peer, uncashedAmount }) => {
|
||||||
accounting[peer].uncashedAmount = new Token(uncashedAmount)
|
accounting[peer].uncashedAmount = new Token(uncashedAmount)
|
||||||
})
|
})
|
||||||
|
|
||||||
return Object.values(accounting).sort((a, b) =>
|
// Return sorted by the uncashed amount first and then by the peer id
|
||||||
b.uncashedAmount.toBigNumber.minus(a.uncashedAmount.toBigNumber).toNumber(),
|
return Object.values(accounting).sort((a, b) => {
|
||||||
)
|
const diff = b.uncashedAmount.toBigNumber.minus(a.uncashedAmount.toBigNumber).toNumber()
|
||||||
|
|
||||||
|
if (diff !== 0) return diff
|
||||||
|
|
||||||
|
return a.peer < b.peer ? -1 : 1
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAccounting = (
|
export const useAccounting = (
|
||||||
@@ -94,12 +100,18 @@ export const useAccounting = (
|
|||||||
setUncashedAmounts(results.fulfilled)
|
setUncashedAmounts(results.fulfilled)
|
||||||
setIsloadingUncashed(false)
|
setIsloadingUncashed(false)
|
||||||
})
|
})
|
||||||
}, [settlements, isLoadingUncashed, uncashedAmounts])
|
}, [settlements, isLoadingUncashed, uncashedAmounts, beeDebugApi])
|
||||||
|
|
||||||
const accounting = mergeAccounting(balances, settlements?.settlements, uncashedAmounts)
|
const accounting = mergeAccounting(balances, settlements?.settlements, uncashedAmounts)
|
||||||
|
|
||||||
|
let totalUncashed: Token = new Token('0')
|
||||||
|
accounting?.forEach(
|
||||||
|
({ uncashedAmount }) => (totalUncashed = new Token(totalUncashed.toBigNumber.plus(uncashedAmount.toBigNumber))),
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isLoadingUncashed,
|
isLoadingUncashed,
|
||||||
|
totalUncashed,
|
||||||
accounting,
|
accounting,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,8 @@ import { Context } from '../providers/Bee'
|
|||||||
const useStyles = makeStyles((theme: Theme) =>
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
createStyles({
|
createStyles({
|
||||||
content: {
|
content: {
|
||||||
marginLeft: 300,
|
|
||||||
flexGrow: 1,
|
|
||||||
backgroundColor: theme.palette.background.default,
|
backgroundColor: theme.palette.background.default,
|
||||||
padding: theme.spacing(3),
|
minHeight: '100vh',
|
||||||
paddingBottom: '65px',
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -31,20 +28,22 @@ const Dashboard = (props: Props): ReactElement => {
|
|||||||
const { isLoading } = useContext(Context)
|
const { isLoading } = useContext(Context)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div style={{ display: 'flex' }}>
|
||||||
<SideBar />
|
<SideBar />
|
||||||
|
<Container className={classes.content}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<main className={classes.content}>
|
<>
|
||||||
<AlertVersion />
|
<AlertVersion />
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Container style={{ textAlign: 'center', padding: '50px' }}>
|
<div style={{ textAlign: 'center', width: '100%' }}>
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
</Container>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
props.children
|
props.children
|
||||||
)}
|
)}
|
||||||
</main>
|
</>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
import { ReactElement } from 'react'
|
|
||||||
|
|
||||||
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
|
||||||
import { Card, CardContent, Typography, Theme } from '@material-ui/core/'
|
|
||||||
import WithdrawModal from '../../containers/WithdrawModal'
|
|
||||||
import DepositModal from '../../containers/DepositModal'
|
|
||||||
|
|
||||||
import type { ChequebookAddressResponse } from '@ethersphere/bee-js'
|
|
||||||
import { Token } from '../../models/Token'
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
|
||||||
createStyles({
|
|
||||||
root: {
|
|
||||||
display: 'flex',
|
|
||||||
},
|
|
||||||
buttons: {
|
|
||||||
display: 'flex',
|
|
||||||
columnGap: theme.spacing(1),
|
|
||||||
},
|
|
||||||
gridContainer: {
|
|
||||||
display: 'flex',
|
|
||||||
width: '100%',
|
|
||||||
marginLeft: theme.spacing(1),
|
|
||||||
marginRight: theme.spacing(1),
|
|
||||||
columnGap: theme.spacing(1),
|
|
||||||
rowGap: theme.spacing(1),
|
|
||||||
flex: '0 1 auto',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
},
|
|
||||||
chequebookActions: {
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
display: 'flex',
|
|
||||||
marginBottom: theme.spacing(1),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
interface ChequebookBalance {
|
|
||||||
totalBalance: Token
|
|
||||||
availableBalance: Token
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
chequebookAddress: ChequebookAddressResponse | null
|
|
||||||
chequebookBalance: ChequebookBalance | null
|
|
||||||
totalsent?: Token
|
|
||||||
totalreceived?: Token
|
|
||||||
}
|
|
||||||
|
|
||||||
function AccountCard({ totalreceived, totalsent, chequebookBalance }: Props): ReactElement {
|
|
||||||
const classes = useStyles()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className={classes.chequebookActions}>
|
|
||||||
<Typography component="h2" variant="h6">
|
|
||||||
Chequebook
|
|
||||||
</Typography>
|
|
||||||
<div className={classes.buttons}>
|
|
||||||
<WithdrawModal />
|
|
||||||
<DepositModal />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card className={classes.root}>
|
|
||||||
<CardContent className={classes.gridContainer}>
|
|
||||||
<div>
|
|
||||||
<Typography component="h2" variant="h6" color="primary" gutterBottom>
|
|
||||||
Total Balance
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="h5">{chequebookBalance?.totalBalance.toFixedDecimal()} BZZ</Typography>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Typography component="h2" variant="h6" color="primary" gutterBottom>
|
|
||||||
Available Uncommitted Balance
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="h5">{chequebookBalance?.availableBalance.toFixedDecimal()} BZZ</Typography>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Typography component="h2" variant="h6" color="primary" gutterBottom>
|
|
||||||
Total Sent / Received
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="h5">
|
|
||||||
{totalsent?.toFixedDecimal()} / {totalreceived?.toFixedDecimal()} BZZ
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AccountCard
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import type { ReactElement } from 'react'
|
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
|
||||||
import { Table, TableBody, TableCell, TableContainer, TableRow, TableHead, Paper } from '@material-ui/core'
|
|
||||||
|
|
||||||
import ClipboardCopy from '../../components/ClipboardCopy'
|
|
||||||
import CashoutModal from '../../components/CashoutModal'
|
|
||||||
import PeerDetailDrawer from '../../components/PeerDetail'
|
|
||||||
import { Accounting } from '../../hooks/accounting'
|
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
|
||||||
table: {
|
|
||||||
minWidth: 650,
|
|
||||||
},
|
|
||||||
values: {
|
|
||||||
textAlign: 'right',
|
|
||||||
fontFamily: 'monospace, monospace',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
interface Props {
|
|
||||||
isLoadingUncashed: boolean
|
|
||||||
accounting: Accounting[] | null
|
|
||||||
}
|
|
||||||
|
|
||||||
function BalancesTable({ accounting, isLoadingUncashed }: Props): ReactElement | null {
|
|
||||||
if (accounting === null) return null
|
|
||||||
const classes = useStyles()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableContainer component={Paper}>
|
|
||||||
<Table className={classes.table} size="small" aria-label="Balances Table">
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>Peer</TableCell>
|
|
||||||
<TableCell align="right">Outstanding Balance</TableCell>
|
|
||||||
<TableCell align="right">Settlements Sent / Received</TableCell>
|
|
||||||
<TableCell align="right">Total</TableCell>
|
|
||||||
<TableCell align="right">Uncashed Amount</TableCell>
|
|
||||||
<TableCell />
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{accounting.map(({ peer, balance, received, sent, uncashedAmount, total }) => (
|
|
||||||
<TableRow key={peer}>
|
|
||||||
<TableCell>
|
|
||||||
<div style={{ display: 'flex' }}>
|
|
||||||
<small>
|
|
||||||
<PeerDetailDrawer peerId={peer} />
|
|
||||||
</small>
|
|
||||||
<ClipboardCopy value={peer} />
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className={classes.values}>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
color: balance.toBigNumber.isPositive() ? '#32c48d' : '#c9201f',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{balance.toFixedDecimal()}
|
|
||||||
</span>{' '}
|
|
||||||
BZZ
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className={classes.values}>
|
|
||||||
-{sent.toFixedDecimal()} / {received.toFixedDecimal()} BZZ
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className={classes.values}>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
color: total.toBigNumber.isPositive() ? '#32c48d' : '#c9201f',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{total.toFixedDecimal()}
|
|
||||||
</span>{' '}
|
|
||||||
BZZ
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className={classes.values}>
|
|
||||||
{isLoadingUncashed && 'loading...'}
|
|
||||||
{!isLoadingUncashed && (
|
|
||||||
<>{uncashedAmount.toBigNumber.isGreaterThan('0') ? uncashedAmount.toFixedDecimal() : '0'} BZZ</>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className={classes.values}>
|
|
||||||
{uncashedAmount.toBigNumber.isGreaterThan('0') && (
|
|
||||||
<CashoutModal uncashedAmount={uncashedAmount.toFixedDecimal()} peerId={peer} />
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BalancesTable
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import type { ReactElement } from 'react'
|
||||||
|
|
||||||
|
import ExpandableList from '../../components/ExpandableList'
|
||||||
|
import ExpandableListItem from '../../components/ExpandableListItem'
|
||||||
|
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
|
||||||
|
import ExpandableListItemKey from '../../components/ExpandableListItemKey'
|
||||||
|
|
||||||
|
import CashoutModal from '../../components/CashoutModal'
|
||||||
|
import { Accounting } from '../../hooks/accounting'
|
||||||
|
import type { Token } from '../../models/Token'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isLoadingUncashed: boolean
|
||||||
|
totalUncashed: Token
|
||||||
|
accounting: Accounting[] | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PeerBalances({ accounting, isLoadingUncashed, totalUncashed }: Props): ReactElement | null {
|
||||||
|
return (
|
||||||
|
<ExpandableList
|
||||||
|
label={`Peers (${accounting?.length || 0})`}
|
||||||
|
info={`${totalUncashed.toFixedDecimal()} BZZ (uncashed)`}
|
||||||
|
>
|
||||||
|
<ExpandableListItem label="Uncashed Amount Total" value={`${totalUncashed.toFixedDecimal()} BZZ`} />
|
||||||
|
{accounting?.map(({ peer, balance, received, sent, uncashedAmount, total }) => (
|
||||||
|
<ExpandableList
|
||||||
|
key={peer}
|
||||||
|
label={`Peer ${peer.substr(0, 8)}[…]`}
|
||||||
|
level={1}
|
||||||
|
info={`${uncashedAmount.toFixedDecimal()} BZZ (uncashed)`}
|
||||||
|
>
|
||||||
|
<ExpandableListItemKey label="Peer ID" value={peer} />
|
||||||
|
<ExpandableListItem label="Outstanding Balance" value={`${balance.toFixedDecimal()} BZZ`} />
|
||||||
|
<ExpandableListItem
|
||||||
|
label="Settlements Sent / Received"
|
||||||
|
value={`-${sent.toFixedDecimal()} / ${received.toFixedDecimal()} BZZ`}
|
||||||
|
/>
|
||||||
|
<ExpandableListItem label="Total" value={`${total.toFixedDecimal()} BZZ`} />
|
||||||
|
<ExpandableListItem
|
||||||
|
label="Uncashed Amount"
|
||||||
|
value={isLoadingUncashed ? 'loading…' : `${uncashedAmount.toFixedDecimal()} BZZ`}
|
||||||
|
/>
|
||||||
|
{uncashedAmount.toBigNumber.isGreaterThan('0') && (
|
||||||
|
<ExpandableListItemActions>
|
||||||
|
<CashoutModal uncashedAmount={uncashedAmount.toFixedDecimal()} peerId={peer} />
|
||||||
|
</ExpandableListItemActions>
|
||||||
|
)}
|
||||||
|
</ExpandableList>
|
||||||
|
))}
|
||||||
|
</ExpandableList>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,45 +1,52 @@
|
|||||||
import { ReactElement, useContext } from 'react'
|
import { ReactElement, useContext } from 'react'
|
||||||
import { Theme, createStyles, makeStyles } from '@material-ui/core/styles'
|
|
||||||
|
|
||||||
import AccountCard from '../accounting/AccountCard'
|
import PeerBalances from './PeerBalances'
|
||||||
import BalancesTable from './BalancesTable'
|
|
||||||
import EthereumAddressCard from '../../components/EthereumAddressCard'
|
|
||||||
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
|
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
|
||||||
import { Context as BeeContext } from '../../providers/Bee'
|
import { Context as BeeContext } from '../../providers/Bee'
|
||||||
import { Context as SettingsContext } from '../../providers/Settings'
|
import { Context as SettingsContext } from '../../providers/Settings'
|
||||||
import { useAccounting } from '../../hooks/accounting'
|
import { useAccounting } from '../../hooks/accounting'
|
||||||
|
import ExpandableList from '../../components/ExpandableList'
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
import ExpandableListItem from '../../components/ExpandableListItem'
|
||||||
createStyles({
|
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
|
||||||
root: {
|
import ExpandableListItemKey from '../../components/ExpandableListItemKey'
|
||||||
width: '100%',
|
import WithdrawModal from '../../containers/WithdrawModal'
|
||||||
display: 'grid',
|
import DepositModal from '../../containers/DepositModal'
|
||||||
rowGap: theme.spacing(3),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
export default function Accounting(): ReactElement {
|
export default function Accounting(): ReactElement {
|
||||||
const classes = useStyles()
|
|
||||||
|
|
||||||
const { status, nodeAddresses, chequebookAddress, chequebookBalance, settlements, peerBalances } =
|
const { status, nodeAddresses, chequebookAddress, chequebookBalance, settlements, peerBalances } =
|
||||||
useContext(BeeContext)
|
useContext(BeeContext)
|
||||||
const { beeDebugApi } = useContext(SettingsContext)
|
const { beeDebugApi } = useContext(SettingsContext)
|
||||||
|
|
||||||
const { accounting, isLoadingUncashed } = useAccounting(beeDebugApi, settlements, peerBalances)
|
const { accounting, totalUncashed, isLoadingUncashed } = useAccounting(beeDebugApi, settlements, peerBalances)
|
||||||
|
|
||||||
if (!status.all) return <TroubleshootConnectionCard />
|
if (!status.all) return <TroubleshootConnectionCard />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.root}>
|
<div>
|
||||||
<AccountCard
|
<ExpandableList label="Chequebook" defaultOpen>
|
||||||
chequebookAddress={chequebookAddress}
|
<ExpandableListItem label="Total Balance" value={`${chequebookBalance?.totalBalance.toFixedDecimal()} BZZ`} />
|
||||||
chequebookBalance={chequebookBalance}
|
<ExpandableListItem
|
||||||
totalsent={settlements?.totalSent}
|
label="Available Uncommitted Balance"
|
||||||
totalreceived={settlements?.totalReceived}
|
value={`${chequebookBalance?.availableBalance.toFixedDecimal()} BZZ`}
|
||||||
/>
|
/>
|
||||||
<EthereumAddressCard nodeAddresses={nodeAddresses} chequebookAddress={chequebookAddress} />
|
<ExpandableListItem
|
||||||
<BalancesTable accounting={accounting} isLoadingUncashed={isLoadingUncashed} />
|
label="Total Cheques Amount Sent"
|
||||||
|
value={`${settlements?.totalSent.toFixedDecimal()} BZZ`}
|
||||||
|
/>
|
||||||
|
<ExpandableListItem
|
||||||
|
label="Total Cheques Amount Received"
|
||||||
|
value={`${settlements?.totalReceived.toFixedDecimal()} BZZ`}
|
||||||
|
/>
|
||||||
|
<ExpandableListItemActions>
|
||||||
|
<WithdrawModal />
|
||||||
|
<DepositModal />
|
||||||
|
</ExpandableListItemActions>
|
||||||
|
</ExpandableList>
|
||||||
|
<ExpandableList label="Blockchain" defaultOpen>
|
||||||
|
<ExpandableListItemKey label="Ethereum address" value={nodeAddresses?.ethereum || ''} />
|
||||||
|
<ExpandableListItemKey label="Chequebook contract address" value={chequebookAddress?.chequebookAddress || ''} />
|
||||||
|
</ExpandableList>
|
||||||
|
<PeerBalances accounting={accounting} isLoadingUncashed={isLoadingUncashed} totalUncashed={totalUncashed} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { ReactElement } from 'react'
|
||||||
|
import { StripedWrapper } from '../../components/StripedWrapper'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
icon: ReactElement
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AssetIcon({ icon }: Props): ReactElement {
|
||||||
|
return <StripedWrapper>{icon}</StripedWrapper>
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { Box, Grid, Typography } from '@material-ui/core'
|
||||||
|
import { Web } from '@material-ui/icons'
|
||||||
|
import { ReactElement, useEffect, useState } from 'react'
|
||||||
|
import { File, Folder } from 'react-feather'
|
||||||
|
import { FitImage } from '../../components/FitImage'
|
||||||
|
import { detectIndexHtml, getAssetNameFromFiles, getHumanReadableFileSize } from '../../utils/file'
|
||||||
|
import { SwarmFile } from '../../utils/SwarmFile'
|
||||||
|
import { AssetIcon } from './AssetIcon'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
assetName?: string
|
||||||
|
files: SwarmFile[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add optional prop for indexDocument when it is already known (e.g. downloading a manifest)
|
||||||
|
|
||||||
|
export function AssetPreview({ assetName, files }: Props): ReactElement {
|
||||||
|
const [previewComponent, setPreviewComponent] = useState<ReactElement | undefined>(undefined)
|
||||||
|
const [previewUri, setPreviewUri] = useState<string | undefined>(undefined)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (files.length === 1) {
|
||||||
|
// single image
|
||||||
|
if (files[0].type.startsWith('image/')) {
|
||||||
|
files[0].arrayBuffer().then(value => {
|
||||||
|
const blob = new Blob([value])
|
||||||
|
setPreviewUri(URL.createObjectURL(blob))
|
||||||
|
})
|
||||||
|
// single non-image
|
||||||
|
} else {
|
||||||
|
setPreviewUri(undefined)
|
||||||
|
setPreviewComponent(<AssetIcon icon={<File />} />)
|
||||||
|
}
|
||||||
|
// collection
|
||||||
|
} else if (detectIndexHtml(files)) {
|
||||||
|
setPreviewUri(undefined)
|
||||||
|
setPreviewComponent(<AssetIcon icon={<Web />} />)
|
||||||
|
} else {
|
||||||
|
setPreviewUri(undefined)
|
||||||
|
setPreviewComponent(<AssetIcon icon={<Folder />} />)
|
||||||
|
}
|
||||||
|
}, [files])
|
||||||
|
|
||||||
|
const getPrimaryText = () => {
|
||||||
|
const name = getAssetNameFromFiles(files)
|
||||||
|
|
||||||
|
if (files.length === 1) {
|
||||||
|
return 'Filename: ' + (assetName || name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Folder name: ' + (assetName || name)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getKind = () => {
|
||||||
|
if (files.length === 1) {
|
||||||
|
return files[0].type
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detectIndexHtml(files)) {
|
||||||
|
return 'Website'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Folder'
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFolder = () => ['Folder', 'Website'].includes(getKind())
|
||||||
|
|
||||||
|
const getSize = () => {
|
||||||
|
const bytes = files.reduce((total, item) => total + item.size, 0)
|
||||||
|
|
||||||
|
return getHumanReadableFileSize(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
const size = getSize()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box mb={4}>
|
||||||
|
<Box bgcolor="background.paper">
|
||||||
|
<Grid container direction="row">
|
||||||
|
{previewComponent ? (
|
||||||
|
previewComponent
|
||||||
|
) : (
|
||||||
|
<FitImage maxWidth="250px" maxHeight="175px" alt="Upload Preview" src={previewUri} />
|
||||||
|
)}
|
||||||
|
<Box p={2}>
|
||||||
|
<Typography>{getPrimaryText()}</Typography>
|
||||||
|
<Typography>Kind: {getKind()}</Typography>
|
||||||
|
{size !== '0 bytes' && <Typography>Size: {size}</Typography>}
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
{isFolder() && (
|
||||||
|
<Box mt={0.25} p={2} bgcolor="background.paper">
|
||||||
|
<Grid container justifyContent="space-between" alignItems="center" direction="row">
|
||||||
|
<Typography variant="subtitle2">Folder content</Typography>
|
||||||
|
<Typography variant="subtitle2">{files.length} items</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { Box, Typography } from '@material-ui/core'
|
||||||
|
import { ReactElement } from 'react'
|
||||||
|
import ExpandableListItemKey from '../../components/ExpandableListItemKey'
|
||||||
|
import ExpandableListItemLink from '../../components/ExpandableListItemLink'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
hash: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AssetSummary({ hash }: Props): ReactElement {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box mb={4}>
|
||||||
|
<ExpandableListItemKey label="Swarm hash" value={hash} />
|
||||||
|
<ExpandableListItemLink label="Share on Swarm Gateway" value={`https://gateway.ethswarm.org/access/${hash}`} />
|
||||||
|
</Box>
|
||||||
|
<Typography>
|
||||||
|
The Swarm Gateway is graciously provided by the Swarm Foundation. This service is under development and provided
|
||||||
|
for testing purposes only. Learn more at{' '}
|
||||||
|
<a href="https://gateway.ethswarm.org/">https://gateway.ethswarm.org/</a>.
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,66 +1,92 @@
|
|||||||
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'
|
import { Utils } from '@ethersphere/bee-js'
|
||||||
|
import { ManifestJs } from '@ethersphere/manifest-js'
|
||||||
|
import { useSnackbar } from 'notistack'
|
||||||
|
import { ReactElement, useContext, useState } from 'react'
|
||||||
|
import { useHistory } from 'react-router-dom'
|
||||||
|
import ExpandableListItemInput from '../../components/ExpandableListItemInput'
|
||||||
|
import { History } from '../../components/History'
|
||||||
|
import { Context as SettingsContext } from '../../providers/Settings'
|
||||||
|
import { ROUTES } from '../../routes'
|
||||||
|
import { extractSwarmHash } from '../../utils'
|
||||||
|
import { determineHistoryName, HISTORY_KEYS, putHistory } from '../../utils/local-storage'
|
||||||
|
import { FileNavigation } from './FileNavigation'
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
export function Download(): ReactElement {
|
||||||
createStyles({
|
const [loading, setLoading] = useState(false)
|
||||||
root: {
|
const { beeApi } = useContext(SettingsContext)
|
||||||
padding: theme.spacing(0.25),
|
const [referenceError, setReferenceError] = useState<string | undefined>(undefined)
|
||||||
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 { enqueueSnackbar } = useSnackbar()
|
||||||
const classes = useStyles()
|
const history = useHistory()
|
||||||
const { apiUrl } = useContext(SettingsContext)
|
|
||||||
|
|
||||||
const [referenceInput, setReferenceInput] = useState('')
|
const validateChange = (value: string) => {
|
||||||
const [referenceError, setReferenceError] = useState<Error | null>(null)
|
if (Utils.isHexString(value, 64) || Utils.isHexString(value, 128) || !value.trim().length) {
|
||||||
|
setReferenceError(undefined)
|
||||||
|
} else {
|
||||||
|
setReferenceError('Incorrect format of swarm hash. Expected 64 or 128 hexstring characters.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleReferenceChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
async function onSwarmIdentifier(identifier: string) {
|
||||||
setReferenceInput(e.target.value)
|
if (!beeApi) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (Utils.Hex.isHexString(e.target.value, 64) || Utils.Hex.isHexString(e.target.value, 128)) setReferenceError(null)
|
try {
|
||||||
else setReferenceError(new Error('Incorrect format of swarm hash'))
|
const manifestJs = new ManifestJs(beeApi)
|
||||||
|
const isManifest = await manifestJs.isManifest(identifier)
|
||||||
|
|
||||||
|
if (!isManifest) {
|
||||||
|
throw Error('The specified hash does not contain valid content.')
|
||||||
|
}
|
||||||
|
const indexDocument = await manifestJs.getIndexDocumentPath(identifier)
|
||||||
|
putHistory(HISTORY_KEYS.DOWNLOAD_HISTORY, identifier, determineHistoryName(identifier, indexDocument))
|
||||||
|
history.push(ROUTES.HASH.replace(':hash', identifier))
|
||||||
|
} catch (error: unknown) {
|
||||||
|
let message = typeof error === 'object' && error !== null && Reflect.get(error, 'message')
|
||||||
|
|
||||||
|
if (message.includes('path address not found')) {
|
||||||
|
message = 'The specified hash does not have an index document set.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.includes('Not Found: Not Found')) {
|
||||||
|
message = 'The specified hash was not found.'
|
||||||
|
}
|
||||||
|
enqueueSnackbar(<span>Error: {message || 'Unknown'}</span>, { variant: 'error' })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function recognizeSwarmHash(value: string) {
|
||||||
|
if (value.length < 64) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
const hash = extractSwarmHash(value)
|
||||||
|
|
||||||
|
if (hash) {
|
||||||
|
return hash
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Paper className={classes.root}>
|
<FileNavigation active="DOWNLOAD" />
|
||||||
<InputBase
|
<ExpandableListItemInput
|
||||||
className={classes.input}
|
label="Swarm Hash"
|
||||||
placeholder="Enter swarm reference e.g. 0773a91efd6547c754fc1d95fb1c62c7d1b47f959c2caa685dfec8736da95c1c"
|
onConfirm={value => onSwarmIdentifier(value)}
|
||||||
inputProps={{ 'aria-label': 'retrieve file from swarm' }}
|
onChange={validateChange}
|
||||||
value={referenceInput}
|
helperText={referenceError}
|
||||||
onChange={handleReferenceChange}
|
confirmLabel={'Search'}
|
||||||
|
confirmLabelDisabled={Boolean(referenceError) || loading}
|
||||||
|
placeholder="e.g. 31fb0362b1a42536134c86bc58b97ac0244e5c6630beec3e27c2d1cecb38c605"
|
||||||
|
expandedOnly
|
||||||
|
mapperFn={value => recognizeSwarmHash(value)}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<History title="Download History" localStorageKey={HISTORY_KEYS.DOWNLOAD_HISTORY} />
|
||||||
href={`${apiUrl}/bzz/${referenceInput}`}
|
|
||||||
target="_blank"
|
|
||||||
disabled={referenceError !== null || !referenceInput}
|
|
||||||
className={classes.iconButton}
|
|
||||||
aria-label="download"
|
|
||||||
>
|
|
||||||
<Search />
|
|
||||||
</IconButton>
|
|
||||||
</Paper>
|
|
||||||
{referenceError && <FormHelperText error>{referenceError.message}</FormHelperText>}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { Button } from '@material-ui/core'
|
||||||
|
import { Clear } from '@material-ui/icons'
|
||||||
|
import { ReactElement } from 'react'
|
||||||
|
import { Download, Link } from 'react-feather'
|
||||||
|
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
|
||||||
|
import { SwarmButton } from '../../components/SwarmButton'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onOpen: () => void
|
||||||
|
onDownload: () => void
|
||||||
|
onCancel: () => void
|
||||||
|
hasIndexDocument: boolean
|
||||||
|
loading: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DownloadActionBar({ onOpen, onDownload, onCancel, hasIndexDocument, loading }: Props): ReactElement {
|
||||||
|
return (
|
||||||
|
<ExpandableListItemActions>
|
||||||
|
{hasIndexDocument && (
|
||||||
|
<SwarmButton onClick={onOpen} iconType={Link} disabled={loading}>
|
||||||
|
View Website
|
||||||
|
</SwarmButton>
|
||||||
|
)}
|
||||||
|
<SwarmButton onClick={onDownload} iconType={Download} disabled={loading} loading={loading}>
|
||||||
|
Download
|
||||||
|
</SwarmButton>
|
||||||
|
<Button onClick={onCancel} variant="contained" startIcon={<Clear />} disabled={loading}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</ExpandableListItemActions>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { createStyles, makeStyles, Tab, Tabs, Theme } from '@material-ui/core'
|
||||||
|
import { ReactElement } from 'react'
|
||||||
|
import { useHistory } from 'react-router-dom'
|
||||||
|
import { ROUTES } from '../../routes'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
active: 'UPLOAD' | 'DOWNLOAD'
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
root: {
|
||||||
|
flexGrow: 1,
|
||||||
|
marginBottom: theme.spacing(4),
|
||||||
|
},
|
||||||
|
leftTab: {
|
||||||
|
marginRight: theme.spacing(0.5),
|
||||||
|
},
|
||||||
|
rightTab: {
|
||||||
|
marginLeft: theme.spacing(0.5),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export function FileNavigation({ active }: Props): ReactElement {
|
||||||
|
const classes = useStyles()
|
||||||
|
const history = useHistory()
|
||||||
|
|
||||||
|
function onChange(event: React.ChangeEvent<Record<string, never>>, newValue: number) {
|
||||||
|
history.push(newValue === 1 ? ROUTES.DOWNLOAD : ROUTES.UPLOAD)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
<Tabs value={active === 'UPLOAD' ? 0 : 1} onChange={onChange} variant="fullWidth">
|
||||||
|
<Tab className={classes.leftTab} key="UPLOAD" label="Upload" />
|
||||||
|
<Tab className={classes.rightTab} key="DOWNLOAD" label="Download" />
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
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 React, { ReactElement } from 'react'
|
||||||
import PeerDetailDrawer from '../../components/PeerDetail'
|
import { Button, ListItemIcon, Typography, Menu, MenuItem } from '@material-ui/core'
|
||||||
import { EnrichedPostageBatch } from '../../providers/Stamps'
|
import { EnrichedPostageBatch } from '../../providers/Stamps'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -25,10 +21,10 @@ export default function SimpleMenu({ stamps, selectedStamp, setSelected }: Props
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Button aria-controls="simple-menu" aria-haspopup="true" onClick={handleClick}>
|
<Button variant="contained" aria-haspopup="true" onClick={handleClick}>
|
||||||
Change
|
Change
|
||||||
</Button>
|
</Button>
|
||||||
<Menu id="simple-menu" anchorEl={anchorEl} keepMounted open={Boolean(anchorEl)} onClose={handleClose}>
|
<Menu anchorEl={anchorEl} keepMounted open={Boolean(anchorEl)} onClose={handleClose}>
|
||||||
{stamps.map(stamp => (
|
{stamps.map(stamp => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={stamp.batchID}
|
key={stamp.batchID}
|
||||||
@@ -39,7 +35,7 @@ export default function SimpleMenu({ stamps, selectedStamp, setSelected }: Props
|
|||||||
selected={stamp.batchID === selectedStamp?.batchID}
|
selected={stamp.batchID === selectedStamp?.batchID}
|
||||||
>
|
>
|
||||||
<ListItemIcon>{stamp.usageText}</ListItemIcon>
|
<ListItemIcon>{stamp.usageText}</ListItemIcon>
|
||||||
<PeerDetailDrawer peerId={stamp.batchID} />
|
<Typography variant="body2">{stamp.batchID.substr(0, 8)}[…]</Typography>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import { ManifestJs } from '@ethersphere/manifest-js'
|
||||||
|
import { Box } from '@material-ui/core'
|
||||||
|
import { saveAs } from 'file-saver'
|
||||||
|
import JSZip from 'jszip'
|
||||||
|
import { ReactElement, useContext, useEffect, useState } from 'react'
|
||||||
|
import { RouteComponentProps, useHistory } from 'react-router-dom'
|
||||||
|
import { Loading } from '../../components/Loading'
|
||||||
|
import { Context as SettingsContext } from '../../providers/Settings'
|
||||||
|
import { ROUTES } from '../../routes'
|
||||||
|
import { convertBeeFileToBrowserFile, convertManifestToFiles } from '../../utils/file'
|
||||||
|
import { shortenHash } from '../../utils/hash'
|
||||||
|
import { determineHistoryName, HISTORY_KEYS, putHistory } from '../../utils/local-storage'
|
||||||
|
import { SwarmFile } from '../../utils/SwarmFile'
|
||||||
|
import { AssetPreview } from './AssetPreview'
|
||||||
|
import { AssetSummary } from './AssetSummary'
|
||||||
|
import { DownloadActionBar } from './DownloadActionBar'
|
||||||
|
|
||||||
|
interface MatchParams {
|
||||||
|
hash: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Share(props: RouteComponentProps<MatchParams>): ReactElement {
|
||||||
|
const { apiUrl, beeApi } = useContext(SettingsContext)
|
||||||
|
const reference = props.match.params.hash
|
||||||
|
const history = useHistory()
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [downloading, setDownloading] = useState(false)
|
||||||
|
const [files, setFiles] = useState<SwarmFile[]>([])
|
||||||
|
const [swarmEntries, setSwarmEntries] = useState<Record<string, string>>({})
|
||||||
|
const [indexDocument, setIndexDocument] = useState<string | null>(null)
|
||||||
|
|
||||||
|
async function prepare() {
|
||||||
|
if (!beeApi) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const manifestJs = new ManifestJs(beeApi)
|
||||||
|
const isManifest = await manifestJs.isManifest(reference)
|
||||||
|
|
||||||
|
if (!isManifest) {
|
||||||
|
throw Error('The specified hash does not contain valid content.')
|
||||||
|
}
|
||||||
|
const entries = await manifestJs.getHashes(reference)
|
||||||
|
setSwarmEntries(entries)
|
||||||
|
const indexDocument = await manifestJs.getIndexDocumentPath(reference)
|
||||||
|
setIndexDocument(indexDocument)
|
||||||
|
|
||||||
|
if (Object.keys(entries).length === 1) {
|
||||||
|
const response = await beeApi.downloadFile(reference)
|
||||||
|
setFiles([new SwarmFile(convertBeeFileToBrowserFile(response) as File)])
|
||||||
|
} else {
|
||||||
|
setFiles(convertManifestToFiles(entries))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOpen() {
|
||||||
|
window.open(`${apiUrl}/bzz/${reference}/`, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
// POP means there is no history - nowhere to go back yet
|
||||||
|
if (history.action === 'POP') {
|
||||||
|
history.push(ROUTES.UPLOAD)
|
||||||
|
} else {
|
||||||
|
history.goBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true)
|
||||||
|
prepare().then(() => {
|
||||||
|
setLoading(false)
|
||||||
|
})
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [reference])
|
||||||
|
|
||||||
|
async function onDownload() {
|
||||||
|
if (!beeApi) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
putHistory(HISTORY_KEYS.DOWNLOAD_HISTORY, reference, determineHistoryName(reference, indexDocument))
|
||||||
|
setDownloading(true)
|
||||||
|
|
||||||
|
if (Object.keys(swarmEntries).length === 1) {
|
||||||
|
window.open(`${apiUrl}/bzz/${reference}/`, '_blank')
|
||||||
|
} else {
|
||||||
|
const zip = new JSZip()
|
||||||
|
for (const [path, hash] of Object.entries(swarmEntries)) {
|
||||||
|
zip.file(path, await beeApi.downloadData(hash))
|
||||||
|
}
|
||||||
|
const content = await zip.generateAsync({ type: 'blob' })
|
||||||
|
saveAs(content, reference + '.zip')
|
||||||
|
}
|
||||||
|
setDownloading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const assetName = shortenHash(reference)
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Loading />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box mb={4}>
|
||||||
|
<AssetPreview files={files} assetName={assetName} />
|
||||||
|
</Box>
|
||||||
|
<Box mb={4}>
|
||||||
|
<AssetSummary hash={reference} />
|
||||||
|
</Box>
|
||||||
|
<DownloadActionBar
|
||||||
|
onOpen={onOpen}
|
||||||
|
onCancel={onClose}
|
||||||
|
onDownload={onDownload}
|
||||||
|
hasIndexDocument={Boolean(indexDocument && files.length > 1)}
|
||||||
|
loading={downloading}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Box, Typography } from '@material-ui/core'
|
||||||
|
import { ReactElement } from 'react'
|
||||||
|
import { EnrichedPostageBatch } from '../../providers/Stamps'
|
||||||
|
import { PostageStamp } from '../stamps/PostageStamp'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
stamp: EnrichedPostageBatch
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StampPreview({ stamp }: Props): ReactElement {
|
||||||
|
return (
|
||||||
|
<Box mb={4}>
|
||||||
|
<Box mb={0.25} p={2} bgcolor="background.paper">
|
||||||
|
<Typography variant="subtitle2">Associated postage stamp:</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box bgcolor="background.paper">
|
||||||
|
<PostageStamp stamp={stamp} shorten={true} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,114 +1,92 @@
|
|||||||
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 { useSnackbar } from 'notistack'
|
||||||
import { ReactElement, useContext, useEffect, useState } from 'react'
|
import { ReactElement, useContext, useEffect, useState } from 'react'
|
||||||
import UploadSizeAlert from '../../components/AlertUploadSize'
|
import { useHistory } from 'react-router-dom'
|
||||||
import ClipboardCopy from '../../components/ClipboardCopy'
|
import { HistoryHeader } from '../../components/HistoryHeader'
|
||||||
import PeerDetailDrawer from '../../components/PeerDetail'
|
import { Context as FileContext } from '../../providers/File'
|
||||||
import { Context, EnrichedPostageBatch } from '../../providers/Stamps'
|
|
||||||
import { Context as SettingsContext } from '../../providers/Settings'
|
import { Context as SettingsContext } from '../../providers/Settings'
|
||||||
import CreatePostageStamp from '../stamps/CreatePostageStampModal'
|
import { Context, EnrichedPostageBatch } from '../../providers/Stamps'
|
||||||
import SelectStamp from './SelectStamp'
|
import { ROUTES } from '../../routes'
|
||||||
|
import { detectIndexHtml, getAssetNameFromFiles } from '../../utils/file'
|
||||||
|
import { HISTORY_KEYS, putHistory } from '../../utils/local-storage'
|
||||||
|
import { CreatePostageStampModal } from '../stamps/CreatePostageStampModal'
|
||||||
|
import { SelectPostageStampModal } from '../stamps/SelectPostageStampModal'
|
||||||
|
import { AssetPreview } from './AssetPreview'
|
||||||
|
import { StampPreview } from './StampPreview'
|
||||||
|
import { UploadActionBar } from './UploadActionBar'
|
||||||
|
|
||||||
const MAX_FILE_SIZE = 1_000_000_000 // 1 gigabyte
|
export function Upload(): ReactElement {
|
||||||
|
const [isBuyingStamp, setBuyingStamp] = useState(false)
|
||||||
|
const [isSelectingStamp, setSelectingStamp] = useState(false)
|
||||||
|
const [stamp, setStamp] = useState<EnrichedPostageBatch | null>(null)
|
||||||
|
const [isUploading, setUploading] = useState(false)
|
||||||
|
|
||||||
export default function Files(): ReactElement {
|
const { stamps, refresh } = useContext(Context)
|
||||||
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 { beeApi } = useContext(SettingsContext)
|
||||||
|
const { files, setFiles } = useContext(FileContext)
|
||||||
const { enqueueSnackbar } = useSnackbar()
|
const { enqueueSnackbar } = useSnackbar()
|
||||||
|
const history = useHistory()
|
||||||
|
|
||||||
|
if (!files.length) {
|
||||||
|
setFiles([])
|
||||||
|
history.replace(ROUTES.UPLOAD)
|
||||||
|
}
|
||||||
|
|
||||||
// Choose a postage stamp that has the lowest usage
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedStamp && stamps && stamps.length > 0) {
|
refresh()
|
||||||
const stamp = stamps.reduce((prev, curr) => {
|
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
if (curr.usage < prev.usage) return curr
|
|
||||||
|
|
||||||
return prev
|
const uploadFiles = () => {
|
||||||
}, stamps[0])
|
if (!beeApi || !files.length || !stamp) {
|
||||||
|
return
|
||||||
setSelectedStamp(stamp)
|
|
||||||
}
|
}
|
||||||
}, [isLoading, error, stamps, selectedStamp])
|
|
||||||
|
|
||||||
const uploadFile = () => {
|
const indexDocument = files.length === 1 ? files[0].name : detectIndexHtml(files) || undefined
|
||||||
if (file === null || selectedStamp === null) return
|
|
||||||
|
|
||||||
if (!beeApi) return
|
setUploading(true)
|
||||||
|
|
||||||
setIsUploadingFile(true)
|
|
||||||
beeApi
|
beeApi
|
||||||
.uploadFile(selectedStamp.batchID, file)
|
.uploadFiles(stamp.batchID, files as unknown as File[], { indexDocument })
|
||||||
.then(hash => {
|
.then(hash => {
|
||||||
window.setTimeout(() => {
|
putHistory(HISTORY_KEYS.UPLOAD_HISTORY, hash.reference, getAssetNameFromFiles(files))
|
||||||
setFile(null)
|
history.replace(ROUTES.HASH.replace(':hash', hash.reference))
|
||||||
setUploadReference(hash)
|
|
||||||
setDropzoneKey(dropzoneKey + 1)
|
|
||||||
}, 0)
|
|
||||||
})
|
})
|
||||||
.catch(e => enqueueSnackbar(`Error uploading: ${e.message}`, { variant: 'error' }))
|
.catch(e => {
|
||||||
.finally(() => {
|
enqueueSnackbar(`Error uploading: ${e.message}`, { variant: 'error' })
|
||||||
setIsUploadingFile(false)
|
setUploading(false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChange = (files?: File[]) => {
|
const reset = () => {
|
||||||
if (files) {
|
setFiles([])
|
||||||
setFile(files[0])
|
setStamp(null)
|
||||||
}
|
setUploading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<div>
|
<HistoryHeader>Upload</HistoryHeader>
|
||||||
<DropzoneArea
|
{files.length && <AssetPreview files={files} />}
|
||||||
key={'dropzone-' + dropzoneKey}
|
{stamp !== null ? <StampPreview stamp={stamp} /> : null}
|
||||||
onChange={handleChange}
|
{files.length && (
|
||||||
filesLimit={1}
|
<UploadActionBar
|
||||||
maxFileSize={MAX_FILE_SIZE}
|
canSelectStamp={stamps !== null && stamps.length > 0}
|
||||||
|
hasSelectedStamp={stamp !== null}
|
||||||
|
onCancel={reset}
|
||||||
|
onBuy={() => setBuyingStamp(true)}
|
||||||
|
onSelect={() => setSelectingStamp(true)}
|
||||||
|
onUpload={uploadFiles}
|
||||||
|
onClearStamp={() => setStamp(null)}
|
||||||
|
isUploading={isUploading}
|
||||||
/>
|
/>
|
||||||
<div style={{ marginTop: '15px' }}>
|
)}
|
||||||
{selectedStamp && (
|
{isBuyingStamp ? <CreatePostageStampModal onClose={() => setBuyingStamp(false)} /> : null}
|
||||||
<div style={{ display: 'flex' }}>
|
{stamps && isSelectingStamp ? (
|
||||||
<small>
|
<SelectPostageStampModal
|
||||||
with Postage Stamp{' '}
|
stamps={stamps}
|
||||||
<Chip
|
onClose={() => setSelectingStamp(false)}
|
||||||
avatar={<Avatar>{selectedStamp.usageText}</Avatar>}
|
onSelect={stamp => setStamp(stamp)}
|
||||||
label={<PeerDetailDrawer peerId={selectedStamp.batchID} characterLength={6} />}
|
|
||||||
deleteIcon={<ClipboardCopy value={selectedStamp.batchID} />}
|
|
||||||
onDelete={() => {} /* eslint-disable-line*/}
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
/>
|
||||||
</small>
|
) : null}
|
||||||
<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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { Button, Typography } from '@material-ui/core'
|
||||||
|
import { Clear } from '@material-ui/icons'
|
||||||
|
import { ReactElement } from 'react'
|
||||||
|
import { Check, Layers, PlusSquare, RefreshCcw } from 'react-feather'
|
||||||
|
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
|
||||||
|
import { SwarmButton } from '../../components/SwarmButton'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
canSelectStamp: boolean
|
||||||
|
hasSelectedStamp: boolean
|
||||||
|
onUpload: () => void
|
||||||
|
onBuy: () => void
|
||||||
|
onSelect: () => void
|
||||||
|
onCancel: () => void
|
||||||
|
onClearStamp: () => void
|
||||||
|
isUploading: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UploadActionBar({
|
||||||
|
canSelectStamp,
|
||||||
|
hasSelectedStamp,
|
||||||
|
onUpload,
|
||||||
|
onBuy,
|
||||||
|
onSelect,
|
||||||
|
onCancel,
|
||||||
|
onClearStamp,
|
||||||
|
isUploading,
|
||||||
|
}: Props): ReactElement {
|
||||||
|
const showBuy = !hasSelectedStamp
|
||||||
|
const showSelect = canSelectStamp && !hasSelectedStamp
|
||||||
|
const showUpload = hasSelectedStamp
|
||||||
|
const showChange = canSelectStamp && hasSelectedStamp
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ExpandableListItemActions>
|
||||||
|
{showBuy ? (
|
||||||
|
<SwarmButton onClick={onBuy} iconType={PlusSquare}>
|
||||||
|
Buy New Postage Stamp
|
||||||
|
</SwarmButton>
|
||||||
|
) : null}
|
||||||
|
{showSelect ? (
|
||||||
|
<SwarmButton onClick={onSelect} iconType={Layers}>
|
||||||
|
Use Existing Postage Stamp
|
||||||
|
</SwarmButton>
|
||||||
|
) : null}
|
||||||
|
{showUpload ? (
|
||||||
|
<SwarmButton onClick={onUpload} iconType={Check} disabled={isUploading} loading={isUploading}>
|
||||||
|
Upload To Your Node
|
||||||
|
</SwarmButton>
|
||||||
|
) : null}
|
||||||
|
{showChange ? (
|
||||||
|
<SwarmButton onClick={onClearStamp} iconType={RefreshCcw} disabled={isUploading}>
|
||||||
|
Change Postage Stamp
|
||||||
|
</SwarmButton>
|
||||||
|
) : null}
|
||||||
|
<Button onClick={onCancel} variant="contained" startIcon={<Clear />}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ExpandableListItemActions>
|
||||||
|
{showSelect ? (
|
||||||
|
<Typography>
|
||||||
|
You need a postage stamp to upload. Please refer to the official Bee documentation to understand how postage
|
||||||
|
stamps work.
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import { createStyles, makeStyles, Theme, Typography } from '@material-ui/core'
|
||||||
|
import { DropzoneArea } from 'material-ui-dropzone'
|
||||||
|
import { useSnackbar } from 'notistack'
|
||||||
|
import { ReactElement, useContext, useState } from 'react'
|
||||||
|
import { FilePlus, FolderPlus, PlusCircle } from 'react-feather'
|
||||||
|
import { useHistory } from 'react-router-dom'
|
||||||
|
import { SwarmButton } from '../../components/SwarmButton'
|
||||||
|
import { Context } from '../../providers/File'
|
||||||
|
import { ROUTES } from '../../routes'
|
||||||
|
import { detectIndexHtml } from '../../utils/file'
|
||||||
|
import { SwarmFile } from '../../utils/SwarmFile'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
maximumSizeInBytes: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
areaWrapper: { position: 'relative', marginBottom: theme.spacing(2) },
|
||||||
|
dropzone: {
|
||||||
|
background: theme.palette.background.default,
|
||||||
|
outline: 'none',
|
||||||
|
color: 'transparent',
|
||||||
|
zIndex: 1,
|
||||||
|
'& svg': {
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
buttonWrapper: {
|
||||||
|
top: '0',
|
||||||
|
left: '0',
|
||||||
|
position: 'absolute',
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
marginLeft: theme.spacing(0.5),
|
||||||
|
marginRight: theme.spacing(0.5),
|
||||||
|
zIndex: 2,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export function UploadArea({ maximumSizeInBytes }: Props): ReactElement {
|
||||||
|
const { setFiles } = useContext(Context)
|
||||||
|
const classes = useStyles()
|
||||||
|
const history = useHistory()
|
||||||
|
const { enqueueSnackbar } = useSnackbar()
|
||||||
|
const [strictWebsiteMode, setStrictWebsiteMode] = useState(false)
|
||||||
|
const [version, setVersion] = useState(0)
|
||||||
|
|
||||||
|
const getDropzoneInputDomElement = () => document.querySelector('.MuiDropzoneArea-root input') as HTMLInputElement
|
||||||
|
|
||||||
|
const onUploadCollectionClick = () => {
|
||||||
|
const element = getDropzoneInputDomElement()
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
element.setAttribute('directory', '')
|
||||||
|
element.setAttribute('webkitdirectory', '')
|
||||||
|
element.setAttribute('mozdirectory', '')
|
||||||
|
element.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onUploadWebsiteClick = () => {
|
||||||
|
onUploadCollectionClick()
|
||||||
|
setStrictWebsiteMode(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onUploadFolderClick = () => {
|
||||||
|
onUploadCollectionClick()
|
||||||
|
setStrictWebsiteMode(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onUploadFileClick = () => {
|
||||||
|
const element = getDropzoneInputDomElement()
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
element.removeAttribute('directory')
|
||||||
|
element.removeAttribute('webkitdirectory')
|
||||||
|
element.removeAttribute('mozdirectory')
|
||||||
|
element.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetComponentOnAddingInvalidContent = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setVersion(x => x + 1)
|
||||||
|
setFiles([])
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = (files?: File[]) => {
|
||||||
|
if (files) {
|
||||||
|
const swarmFiles = files.map(x => new SwarmFile(x))
|
||||||
|
const indexDocument = files.length === 1 ? files[0].name : detectIndexHtml(swarmFiles) || undefined
|
||||||
|
|
||||||
|
if (files.length && strictWebsiteMode && !indexDocument) {
|
||||||
|
enqueueSnackbar('To upload a website, there must be an index.html or index.htm in the root of the folder.', {
|
||||||
|
variant: 'error',
|
||||||
|
})
|
||||||
|
resetComponentOnAddingInvalidContent()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setFiles(swarmFiles)
|
||||||
|
|
||||||
|
if (files.length) {
|
||||||
|
history.push(ROUTES.UPLOAD_IN_PROGRESS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={classes.areaWrapper}>
|
||||||
|
<DropzoneArea
|
||||||
|
key={version}
|
||||||
|
dropzoneClass={classes.dropzone}
|
||||||
|
onChange={handleChange}
|
||||||
|
filesLimit={1e9}
|
||||||
|
maxFileSize={maximumSizeInBytes}
|
||||||
|
showPreviews={false}
|
||||||
|
/>
|
||||||
|
<div className={classes.buttonWrapper}>
|
||||||
|
<SwarmButton className={classes.button} onClick={onUploadFileClick} iconType={FilePlus}>
|
||||||
|
Add File
|
||||||
|
</SwarmButton>
|
||||||
|
<SwarmButton className={classes.button} onClick={onUploadFolderClick} iconType={FolderPlus}>
|
||||||
|
Add Folder
|
||||||
|
</SwarmButton>
|
||||||
|
<SwarmButton className={classes.button} onClick={onUploadWebsiteClick} iconType={PlusCircle}>
|
||||||
|
Add Website
|
||||||
|
</SwarmButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Typography>
|
||||||
|
You can click the buttons above or simply drag and drop to add a file or folder. To upload a website to Swarm,
|
||||||
|
make sure that your folder contains an “index.html” file.
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { ReactElement } from 'react'
|
||||||
|
import { History } from '../../components/History'
|
||||||
|
import { HISTORY_KEYS } from '../../utils/local-storage'
|
||||||
|
import { FileNavigation } from './FileNavigation'
|
||||||
|
import { UploadArea } from './UploadArea'
|
||||||
|
|
||||||
|
const MAX_FILE_SIZE = 1_000_000_000 // 1 gigabyte
|
||||||
|
|
||||||
|
export function UploadLander(): ReactElement {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FileNavigation active="UPLOAD" />
|
||||||
|
<UploadArea maximumSizeInBytes={MAX_FILE_SIZE} />
|
||||||
|
<History title="Upload History" localStorageKey={HISTORY_KEYS.UPLOAD_HISTORY} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { ReactElement, useContext } from 'react'
|
|
||||||
|
|
||||||
import { Container } from '@material-ui/core'
|
|
||||||
|
|
||||||
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
|
|
||||||
import { Context } from '../../providers/Bee'
|
|
||||||
import Download from './Download'
|
|
||||||
import Upload from './Upload'
|
|
||||||
import TabsContainer from '../../components/TabsContainer'
|
|
||||||
|
|
||||||
export default function Files(): ReactElement {
|
|
||||||
const { status } = useContext(Context)
|
|
||||||
|
|
||||||
if (!status.all) return <TroubleshootConnectionCard />
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container maxWidth="sm">
|
|
||||||
<TabsContainer
|
|
||||||
values={[
|
|
||||||
{
|
|
||||||
label: 'download',
|
|
||||||
component: <Download />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'upload',
|
|
||||||
component: <Upload />,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
import { ReactElement, useState } from 'react'
|
|
||||||
import { Link } from 'react-router-dom'
|
|
||||||
|
|
||||||
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
|
||||||
import { Card, CardContent, Typography, Chip, Button } from '@material-ui/core/'
|
|
||||||
import { ArrowRight, ArrowDropUp } from '@material-ui/icons/'
|
|
||||||
import { NodeAddresses, Topology } from '@ethersphere/bee-js'
|
|
||||||
import { ROUTES } from '../../routes'
|
|
||||||
|
|
||||||
const useStyles = makeStyles(() =>
|
|
||||||
createStyles({
|
|
||||||
root: {
|
|
||||||
display: 'flex',
|
|
||||||
flex: '1 1 auto',
|
|
||||||
flexDirection: 'column',
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
color: '#2145a0',
|
|
||||||
backgroundColor: '#e1effe',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
nodeAddresses: NodeAddresses | null
|
|
||||||
nodeTopology: Topology | null
|
|
||||||
userBeeVersion?: string
|
|
||||||
isLatestBeeVersion: boolean
|
|
||||||
isOk: boolean
|
|
||||||
latestUrl: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function StatusCard({
|
|
||||||
userBeeVersion,
|
|
||||||
nodeAddresses,
|
|
||||||
nodeTopology,
|
|
||||||
isLatestBeeVersion,
|
|
||||||
latestUrl,
|
|
||||||
}: Props): ReactElement | null {
|
|
||||||
const classes = useStyles()
|
|
||||||
|
|
||||||
const [underlayAddressesVisible, setUnderlayAddresessVisible] = useState<boolean>(false)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardContent className={classes.root}>
|
|
||||||
<>
|
|
||||||
<div style={{ marginBottom: '20px' }}>
|
|
||||||
<span style={{ marginRight: '20px' }}>Discovered Nodes: {nodeTopology?.population}</span>
|
|
||||||
<span style={{ marginRight: '20px' }}>
|
|
||||||
<span>Connected Peers: </span>
|
|
||||||
<Link to={ROUTES.PEERS}>{nodeTopology?.connected}</Link>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Typography component="div" variant="subtitle2" gutterBottom>
|
|
||||||
<span>AGENT: </span>
|
|
||||||
<a href="https://github.com/ethersphere/bee" rel="noreferrer" target="_blank">
|
|
||||||
Bee
|
|
||||||
</a>{' '}
|
|
||||||
<span>{userBeeVersion || '-'}</span>
|
|
||||||
{isLatestBeeVersion ? (
|
|
||||||
<Chip
|
|
||||||
style={{ marginLeft: '7px', color: '#2145a0' }}
|
|
||||||
size="small"
|
|
||||||
label="latest"
|
|
||||||
className={classes.status}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Button size="small" variant="outlined" href={latestUrl}>
|
|
||||||
update
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Typography>
|
|
||||||
<Typography component="div" variant="subtitle2" gutterBottom>
|
|
||||||
<span>PUBLIC KEY: </span>
|
|
||||||
<span>{nodeAddresses?.publicKey ? nodeAddresses.publicKey : '-'}</span>
|
|
||||||
</Typography>
|
|
||||||
<Typography component="div" variant="subtitle2" gutterBottom>
|
|
||||||
<span>PSS PUBLIC KEY: </span>
|
|
||||||
<span>{nodeAddresses?.pssPublicKey ? nodeAddresses.pssPublicKey : '-'}</span>
|
|
||||||
</Typography>
|
|
||||||
<Typography component="div" variant="subtitle2" gutterBottom>
|
|
||||||
<span>OVERLAY ADDRESS (PEER ID): </span>
|
|
||||||
<span>{nodeAddresses?.overlay ? nodeAddresses.overlay : '-'}</span>
|
|
||||||
</Typography>
|
|
||||||
<Typography component="div" variant="subtitle2" gutterBottom>
|
|
||||||
<Typography component="div" onClick={() => setUnderlayAddresessVisible(!underlayAddressesVisible)}>
|
|
||||||
<Button color="primary" style={{ padding: 0, marginTop: '6px' }}>
|
|
||||||
{underlayAddressesVisible ? (
|
|
||||||
<ArrowDropUp style={{ fontSize: '12px' }} />
|
|
||||||
) : (
|
|
||||||
<ArrowRight style={{ fontSize: '12px' }} />
|
|
||||||
)}
|
|
||||||
<span>Underlay Addresses</span>
|
|
||||||
</Button>
|
|
||||||
</Typography>
|
|
||||||
{underlayAddressesVisible && (
|
|
||||||
<div>
|
|
||||||
{nodeAddresses?.underlay.map(item => (
|
|
||||||
<li key={item}>{item}</li>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default StatusCard
|
|
||||||
@@ -1,24 +1,14 @@
|
|||||||
import { ReactElement, useContext } from 'react'
|
import { ReactElement, useContext } from 'react'
|
||||||
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
|
import { Button } from '@material-ui/core'
|
||||||
|
|
||||||
import StatusCard from './StatusCard'
|
|
||||||
import EthereumAddressCard from '../../components/EthereumAddressCard'
|
|
||||||
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
|
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
|
||||||
import { Context as BeeContext } from '../../providers/Bee'
|
import { Context as BeeContext } from '../../providers/Bee'
|
||||||
|
import ExpandableList from '../../components/ExpandableList'
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
import ExpandableListItem from '../../components/ExpandableListItem'
|
||||||
createStyles({
|
import ExpandableListItemKey from '../../components/ExpandableListItemKey'
|
||||||
root: {
|
import TopologyStats from '../../components/TopologyStats'
|
||||||
width: '100%',
|
|
||||||
display: 'grid',
|
|
||||||
rowGap: theme.spacing(3),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
export default function Status(): ReactElement {
|
export default function Status(): ReactElement {
|
||||||
const classes = useStyles()
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
status,
|
status,
|
||||||
latestUserVersion,
|
latestUserVersion,
|
||||||
@@ -32,18 +22,39 @@ export default function Status(): ReactElement {
|
|||||||
if (!status.all) return <TroubleshootConnectionCard />
|
if (!status.all) return <TroubleshootConnectionCard />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.root}>
|
<div>
|
||||||
<StatusCard
|
<ExpandableList label="Bee Node" defaultOpen>
|
||||||
userBeeVersion={latestUserVersion}
|
<ExpandableListItem
|
||||||
isLatestBeeVersion={isLatestBeeVersion}
|
label="Agent"
|
||||||
isOk={status.all}
|
value={
|
||||||
nodeTopology={topology}
|
<div>
|
||||||
latestUrl={latestBeeVersionUrl}
|
<a href="https://github.com/ethersphere/bee" rel="noreferrer" target="_blank">
|
||||||
nodeAddresses={nodeAddresses}
|
Bee
|
||||||
|
</a>
|
||||||
|
{` ${latestUserVersion || '-'} `}
|
||||||
|
<Button size="small" variant="outlined" href={latestBeeVersionUrl} target="_blank">
|
||||||
|
{isLatestBeeVersion ? 'latest' : 'update'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{nodeAddresses && chequebookAddress && (
|
<ExpandableListItemKey label="Public key" value={nodeAddresses?.publicKey || ''} />
|
||||||
<EthereumAddressCard nodeAddresses={nodeAddresses} chequebookAddress={chequebookAddress} />
|
<ExpandableListItemKey label="PSS public key" value={nodeAddresses?.pssPublicKey || ''} />
|
||||||
)}
|
<ExpandableListItemKey label="Overlay address (Peer ID)" value={nodeAddresses?.overlay || ''} />
|
||||||
|
|
||||||
|
<ExpandableList level={1} label="Underlay addresses">
|
||||||
|
{nodeAddresses?.underlay.map(addr => (
|
||||||
|
<ExpandableListItem key={addr} value={addr} />
|
||||||
|
))}
|
||||||
|
</ExpandableList>
|
||||||
|
</ExpandableList>
|
||||||
|
<ExpandableList label="Blockchain" defaultOpen>
|
||||||
|
<ExpandableListItemKey label="Ethereum address" value={nodeAddresses?.ethereum || ''} />
|
||||||
|
<ExpandableListItemKey label="Chequebook contract address" value={chequebookAddress?.chequebookAddress || ''} />
|
||||||
|
</ExpandableList>
|
||||||
|
<ExpandableList label="Connectivity" defaultOpen>
|
||||||
|
<TopologyStats topology={topology} />
|
||||||
|
</ExpandableList>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
import { ReactElement, useState, useContext } from 'react'
|
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableContainer,
|
|
||||||
TableRow,
|
|
||||||
TableHead,
|
|
||||||
Button,
|
|
||||||
Paper,
|
|
||||||
Tooltip,
|
|
||||||
CircularProgress,
|
|
||||||
} from '@material-ui/core'
|
|
||||||
import { Autorenew } from '@material-ui/icons'
|
|
||||||
|
|
||||||
import { Context as SettingsContext } from '../../providers/Settings'
|
|
||||||
import type { Peer } from '@ethersphere/bee-js'
|
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
|
||||||
table: {
|
|
||||||
minWidth: 650,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
peers: Peer[] | null
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PeerLatency {
|
|
||||||
rtt: string
|
|
||||||
loading: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPingState(peerLatency: Record<string, PeerLatency>, peer: Peer): ReactElement {
|
|
||||||
if (peerLatency[peer.address]?.loading) return <CircularProgress size={20} />
|
|
||||||
|
|
||||||
if (peerLatency[peer.address]?.rtt) return <span>{peerLatency[peer.address]?.rtt}</span>
|
|
||||||
|
|
||||||
return <Autorenew />
|
|
||||||
}
|
|
||||||
|
|
||||||
function PeerTable(props: Props): ReactElement {
|
|
||||||
const classes = useStyles()
|
|
||||||
const { beeDebugApi } = useContext(SettingsContext)
|
|
||||||
|
|
||||||
const [peerLatency, setPeerLatency] = useState<Record<string, PeerLatency>>({})
|
|
||||||
|
|
||||||
const pingPeer = (peerId: string) => {
|
|
||||||
setPeerLatency(prevPeerLatency => ({ ...prevPeerLatency, [peerId]: { rtt: '', loading: true } }))
|
|
||||||
beeDebugApi
|
|
||||||
?.pingPeer(peerId)
|
|
||||||
.then(res => {
|
|
||||||
setPeerLatency(prevPeerLatency => ({ ...prevPeerLatency, [peerId]: { rtt: res.rtt, loading: false } }))
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setPeerLatency(prevPeerLatency => ({ ...prevPeerLatency, [peerId]: { rtt: 'error', loading: false } }))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<TableContainer component={Paper}>
|
|
||||||
<Table className={classes.table}>
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>Index</TableCell>
|
|
||||||
<TableCell>Peer Id</TableCell>
|
|
||||||
<TableCell align="right">Actions</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{props.peers?.map((peer: Peer, idx: number) => (
|
|
||||||
<TableRow key={peer.address}>
|
|
||||||
<TableCell component="th" scope="row">
|
|
||||||
{idx + 1}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{peer.address}</TableCell>
|
|
||||||
<TableCell align="right">
|
|
||||||
<Tooltip title="Ping node">
|
|
||||||
<Button color="primary" onClick={() => pingPeer(peer.address)}>
|
|
||||||
{getPingState(peerLatency, peer)}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PeerTable
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import PeerTable from './PeerTable'
|
|
||||||
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
|
|
||||||
|
|
||||||
import { Context } from '../../providers/Bee'
|
|
||||||
import TopologyStats from '../../components/TopologyStats'
|
|
||||||
import { ReactElement, useContext } from 'react'
|
|
||||||
|
|
||||||
export default function Peers(): ReactElement {
|
|
||||||
const { topology, peers, status } = useContext(Context)
|
|
||||||
|
|
||||||
if (!status.all) {
|
|
||||||
return <TroubleshootConnectionCard />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<TopologyStats topology={topology} />
|
|
||||||
<PeerTable peers={peers} />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,70 +1,15 @@
|
|||||||
import React, { ReactElement, useState, useContext } from 'react'
|
import { ReactElement, useContext } from 'react'
|
||||||
import { Paper, Container, TextField, Typography, Button } from '@material-ui/core'
|
|
||||||
import { Context as SettingsContext } from '../../providers/Settings'
|
import { Context as SettingsContext } from '../../providers/Settings'
|
||||||
|
import ExpandableList from '../../components/ExpandableList'
|
||||||
|
import ExpandableListItemInput from '../../components/ExpandableListItemInput'
|
||||||
|
|
||||||
export default function Settings(): ReactElement {
|
export default function Settings(): ReactElement {
|
||||||
const { apiUrl, apiDebugUrl, setApiUrl, setDebugApiUrl } = useContext(SettingsContext)
|
const { apiUrl, apiDebugUrl, setApiUrl, setDebugApiUrl } = useContext(SettingsContext)
|
||||||
const [host, setHost] = useState(apiUrl)
|
|
||||||
const [debugHost, setDebugHost] = useState(apiDebugUrl)
|
|
||||||
|
|
||||||
const submit = () => {
|
|
||||||
if (host !== apiUrl) setApiUrl(host)
|
|
||||||
|
|
||||||
if (debugHost !== apiDebugUrl) setDebugApiUrl(debugHost)
|
|
||||||
}
|
|
||||||
|
|
||||||
const touched = host !== apiUrl || debugHost !== apiDebugUrl
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<ExpandableList label="API Settings" defaultOpen>
|
||||||
<Container>
|
<ExpandableListItemInput label="Bee API" value={apiUrl} onConfirm={setApiUrl} />
|
||||||
<Typography variant="h4" gutterBottom>
|
<ExpandableListItemInput label="Bee Debug API" value={apiDebugUrl} onConfirm={setDebugApiUrl} />
|
||||||
Settings
|
</ExpandableList>
|
||||||
</Typography>
|
|
||||||
<Paper>
|
|
||||||
<TextField
|
|
||||||
label="API Endpoint"
|
|
||||||
style={{ margin: 0 }}
|
|
||||||
placeholder="ex: 127.0.0.0.1:1633"
|
|
||||||
helperText="Enter node host override / port"
|
|
||||||
fullWidth
|
|
||||||
defaultValue={apiUrl}
|
|
||||||
margin="normal"
|
|
||||||
InputLabelProps={{
|
|
||||||
shrink: true,
|
|
||||||
}}
|
|
||||||
onChange={e => {
|
|
||||||
setHost(e.target.value)
|
|
||||||
}}
|
|
||||||
variant="filled"
|
|
||||||
/>
|
|
||||||
</Paper>
|
|
||||||
<Paper style={{ marginTop: '20px' }}>
|
|
||||||
<TextField
|
|
||||||
label="Debug API Endpoint"
|
|
||||||
style={{ margin: 0 }}
|
|
||||||
placeholder="ex: 127.0.0.0.1:1635"
|
|
||||||
helperText="Enter node debug host override / port"
|
|
||||||
fullWidth
|
|
||||||
defaultValue={apiDebugUrl}
|
|
||||||
onChange={e => {
|
|
||||||
setDebugHost(e.target.value)
|
|
||||||
}}
|
|
||||||
margin="normal"
|
|
||||||
InputLabelProps={{
|
|
||||||
shrink: true,
|
|
||||||
}}
|
|
||||||
variant="filled"
|
|
||||||
/>
|
|
||||||
</Paper>
|
|
||||||
{touched ? (
|
|
||||||
<div style={{ marginTop: '20px' }}>
|
|
||||||
<Button variant="outlined" color="primary" onClick={submit}>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</Container>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import React, { ReactElement, useContext } from 'react'
|
|
||||||
import Button from '@material-ui/core/Button'
|
import Button from '@material-ui/core/Button'
|
||||||
|
import CircularProgress from '@material-ui/core/CircularProgress'
|
||||||
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 CircularProgress from '@material-ui/core/CircularProgress'
|
|
||||||
import DialogTitle from '@material-ui/core/DialogTitle'
|
import DialogTitle from '@material-ui/core/DialogTitle'
|
||||||
|
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||||
import BigNumber from 'bignumber.js'
|
import BigNumber from 'bignumber.js'
|
||||||
import { FormikHelpers, Form, Field, Formik } from 'formik'
|
import { Field, Form, Formik, FormikHelpers } from 'formik'
|
||||||
import { TextField } from 'formik-material-ui'
|
import { TextField } from 'formik-material-ui'
|
||||||
|
import { useSnackbar } from 'notistack'
|
||||||
|
import React, { ReactElement, useContext } from 'react'
|
||||||
import { Context as SettingsContext } from '../../providers/Settings'
|
import { Context as SettingsContext } from '../../providers/Settings'
|
||||||
import { Context } from '../../providers/Stamps'
|
import { Context } from '../../providers/Stamps'
|
||||||
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
|
|
||||||
import { useSnackbar } from 'notistack'
|
|
||||||
|
|
||||||
interface FormValues {
|
interface FormValues {
|
||||||
depth?: string
|
depth?: string
|
||||||
@@ -47,16 +47,13 @@ const useStyles = makeStyles((theme: Theme) =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
label?: string
|
onClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FormDialog({ label }: Props): ReactElement {
|
export function CreatePostageStampModal({ onClose }: Props): ReactElement {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const [open, setOpen] = React.useState(false)
|
|
||||||
const { refresh } = useContext(Context)
|
const { refresh } = useContext(Context)
|
||||||
const { beeApi } = useContext(SettingsContext)
|
const { beeDebugApi } = useContext(SettingsContext)
|
||||||
const handleClickOpen = () => setOpen(true)
|
|
||||||
const handleClose = () => setOpen(false)
|
|
||||||
const { enqueueSnackbar } = useSnackbar()
|
const { enqueueSnackbar } = useSnackbar()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -67,17 +64,17 @@ export default function FormDialog({ label }: Props): ReactElement {
|
|||||||
// This is really just a typeguard, the validation pretty much guarantees these will have the right values
|
// This is really just a typeguard, the validation pretty much guarantees these will have the right values
|
||||||
if (!values.depth || !values.amount) return
|
if (!values.depth || !values.amount) return
|
||||||
|
|
||||||
if (!beeApi) return
|
if (!beeDebugApi) return
|
||||||
|
|
||||||
const amount = BigInt(values.amount)
|
const amount = BigInt(values.amount)
|
||||||
const depth = Number.parseInt(values.depth)
|
const depth = Number.parseInt(values.depth)
|
||||||
const options = values.label ? { label: values.label } : undefined
|
const options = values.label ? { label: values.label } : undefined
|
||||||
await beeApi.createPostageBatch(amount.toString(), depth, options)
|
await beeDebugApi.createPostageBatch(amount.toString(), depth, options)
|
||||||
actions.resetForm()
|
actions.resetForm()
|
||||||
await refresh()
|
await refresh()
|
||||||
handleClose()
|
onClose()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
enqueueSnackbar(`Error: ${e.message}`, { variant: 'error' })
|
enqueueSnackbar(`Error: ${(e as Error).message}`, { variant: 'error' })
|
||||||
actions.setSubmitting(false)
|
actions.setSubmitting(false)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -111,20 +108,9 @@ export default function FormDialog({ label }: Props): ReactElement {
|
|||||||
>
|
>
|
||||||
{({ submitForm, isValid, isSubmitting, values }) => (
|
{({ submitForm, isValid, isSubmitting, values }) => (
|
||||||
<Form>
|
<Form>
|
||||||
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
|
<Dialog open={true} onClose={onClose} aria-labelledby="form-dialog-title">
|
||||||
{label || 'Buy Postage Stamp'}
|
<DialogTitle id="form-dialog-title">Buy new postage stamp</DialogTitle>
|
||||||
{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>
|
<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
|
<Field
|
||||||
component={TextField}
|
component={TextField}
|
||||||
required
|
required
|
||||||
@@ -138,12 +124,11 @@ export default function FormDialog({ label }: Props): ReactElement {
|
|||||||
<Field component={TextField} name="label" label="Label" fullWidth className={classes.field} />
|
<Field component={TextField} name="label" label="Label" fullWidth className={classes.field} />
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleClose} color="primary">
|
<Button onClick={onClose} variant="contained">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<div className={classes.wrapper}>
|
<div className={classes.wrapper}>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
|
||||||
disabled={isSubmitting || !isValid || !values.amount || !values.depth}
|
disabled={isSubmitting || !isValid || !values.amount || !values.depth}
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
@@ -154,6 +139,11 @@ export default function FormDialog({ label }: Props): ReactElement {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>
|
||||||
|
Please refer to the official Bee documentation to understand these values.
|
||||||
|
</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Box, Grid, Typography } from '@material-ui/core'
|
||||||
|
import { ReactElement } from 'react'
|
||||||
|
import { Capacity } from '../../components/Capacity'
|
||||||
|
import { EnrichedPostageBatch } from '../../providers/Stamps'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
stamp: EnrichedPostageBatch
|
||||||
|
shorten?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PostageStamp({ stamp, shorten }: Props): ReactElement {
|
||||||
|
return (
|
||||||
|
<Box p={2} width="100%">
|
||||||
|
<Grid container justifyContent="space-between" alignItems="center" direction="row">
|
||||||
|
<Typography variant="subtitle2">{shorten ? stamp.batchID.slice(0, 8) : stamp.batchID}</Typography>
|
||||||
|
<Capacity width="100px" usage={stamp.usage} />
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { createStyles, FormControl, makeStyles, MenuItem, Select, Theme } from '@material-ui/core'
|
||||||
|
import Button from '@material-ui/core/Button'
|
||||||
|
import Dialog from '@material-ui/core/Dialog'
|
||||||
|
import DialogContent from '@material-ui/core/DialogContent'
|
||||||
|
import DialogTitle from '@material-ui/core/DialogTitle'
|
||||||
|
import { Check, Clear } from '@material-ui/icons'
|
||||||
|
import React, { ReactElement, useState } from 'react'
|
||||||
|
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
|
||||||
|
import { EnrichedPostageBatch } from '../../providers/Stamps'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
stamps: EnrichedPostageBatch[]
|
||||||
|
onSelect: (stamp: EnrichedPostageBatch) => void
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
dialog: {
|
||||||
|
background: theme.palette.background.default,
|
||||||
|
borderRadius: 0,
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: '890px',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
color: '#606060',
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
background: theme.palette.background.paper,
|
||||||
|
borderRadius: 0,
|
||||||
|
border: 0,
|
||||||
|
},
|
||||||
|
option: {
|
||||||
|
height: '52px',
|
||||||
|
},
|
||||||
|
hint: {
|
||||||
|
marginBottom: '16px',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export function SelectPostageStampModal({ stamps, onSelect, onClose }: Props): ReactElement {
|
||||||
|
const [selectedStamp, setSelectedStamp] = useState<EnrichedPostageBatch | null>(null)
|
||||||
|
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
function onChange(stampId: string) {
|
||||||
|
const stamp = stamps.find(x => x.batchID === stampId)
|
||||||
|
|
||||||
|
if (stamp) {
|
||||||
|
setSelectedStamp(stamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFinish() {
|
||||||
|
if (selectedStamp) {
|
||||||
|
onSelect(selectedStamp)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={true}
|
||||||
|
onClose={onClose}
|
||||||
|
aria-labelledby="form-dialog-title"
|
||||||
|
fullWidth
|
||||||
|
PaperProps={{ className: classes.dialog }}
|
||||||
|
>
|
||||||
|
<DialogTitle id="form-dialog-title" className={classes.title}>
|
||||||
|
Select postage stamp
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<Select
|
||||||
|
onChange={event => onChange(event.target.value as string)}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
className={classes.select}
|
||||||
|
defaultValue=""
|
||||||
|
>
|
||||||
|
{stamps.map(x => (
|
||||||
|
<MenuItem key={x.batchID} value={x.batchID} className={classes.option}>
|
||||||
|
{x.batchID.slice(0, 8)}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogContent>
|
||||||
|
<ExpandableListItemActions>
|
||||||
|
<Button disabled={!selectedStamp} onClick={onFinish} variant="contained" startIcon={<Check />}>
|
||||||
|
Select
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onClose} variant="contained" startIcon={<Clear />}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ExpandableListItemActions>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,53 +1,28 @@
|
|||||||
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 type { ReactElement } from 'react'
|
||||||
import ClipboardCopy from '../../components/ClipboardCopy'
|
import ExpandableElement from '../../components/ExpandableElement'
|
||||||
import PeerDetailDrawer from '../../components/PeerDetail'
|
import ExpandableList from '../../components/ExpandableList'
|
||||||
|
import ExpandableListItemKey from '../../components/ExpandableListItemKey'
|
||||||
import { EnrichedPostageBatch } from '../../providers/Stamps'
|
import { EnrichedPostageBatch } from '../../providers/Stamps'
|
||||||
|
import { PostageStamp } from './PostageStamp'
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
|
||||||
table: {
|
|
||||||
minWidth: 650,
|
|
||||||
},
|
|
||||||
values: {
|
|
||||||
textAlign: 'right',
|
|
||||||
fontFamily: 'monospace, monospace',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
interface Props {
|
interface Props {
|
||||||
postageStamps: EnrichedPostageBatch[] | null
|
postageStamps: EnrichedPostageBatch[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
function StampsTable({ postageStamps }: Props): ReactElement | null {
|
function StampsTable({ postageStamps }: Props): ReactElement | null {
|
||||||
if (postageStamps === null) return null
|
if (postageStamps === null) return null
|
||||||
const classes = useStyles()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer component={Paper}>
|
<ExpandableList label="Postage Stamps" defaultOpen>
|
||||||
<Table className={classes.table} size="small" aria-label="Balances Table">
|
{postageStamps.map(stamp => (
|
||||||
<TableHead>
|
<ExpandableElement
|
||||||
<TableRow>
|
key={stamp.batchID}
|
||||||
<TableCell>Batch ID</TableCell>
|
expandable={<ExpandableListItemKey label="Batch ID" value={stamp.batchID} />}
|
||||||
<TableCell align="right">Usage</TableCell>
|
>
|
||||||
</TableRow>
|
<PostageStamp stamp={stamp} shorten={true} />
|
||||||
</TableHead>
|
</ExpandableElement>
|
||||||
<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>
|
</ExpandableList>
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,23 @@
|
|||||||
import { ReactElement, useContext, useEffect } from 'react'
|
import { CircularProgress, Container } from '@material-ui/core'
|
||||||
import { Theme, createStyles, makeStyles } from '@material-ui/core/styles'
|
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
||||||
import { Container, CircularProgress } from '@material-ui/core'
|
import { ReactElement, useContext, useEffect, useState } from 'react'
|
||||||
|
import { PlusSquare } from 'react-feather'
|
||||||
import StampsTable from './StampsTable'
|
import { SwarmButton } from '../../components/SwarmButton'
|
||||||
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
|
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
|
||||||
import CreatePostageStampModal from './CreatePostageStampModal'
|
|
||||||
|
|
||||||
import { Context } from '../../providers/Stamps'
|
|
||||||
import { Context as BeeContext } from '../../providers/Bee'
|
import { Context as BeeContext } from '../../providers/Bee'
|
||||||
|
import { Context as StampsContext } from '../../providers/Stamps'
|
||||||
|
import { CreatePostageStampModal } from './CreatePostageStampModal'
|
||||||
|
import StampsTable from './StampsTable'
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
const useStyles = makeStyles(() =>
|
||||||
createStyles({
|
createStyles({
|
||||||
root: {
|
root: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
rowGap: theme.spacing(2),
|
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
columnGap: theme.spacing(1),
|
|
||||||
rowGap: theme.spacing(1),
|
|
||||||
flex: '0 1 auto',
|
flex: '0 1 auto',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -28,26 +25,22 @@ const useStyles = makeStyles((theme: Theme) =>
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export default function Accounting(): ReactElement {
|
export default function Stamp(): ReactElement {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
const beeContext = useContext(BeeContext)
|
const [isBuyingStamp, setBuyingStamp] = useState(false)
|
||||||
const { stamps, isLoading, error, start, stop } = useContext(Context)
|
|
||||||
|
const { stamps, isLoading, error, start, stop } = useContext(StampsContext)
|
||||||
|
const { status } = useContext(BeeContext)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!status.all) return
|
||||||
start()
|
start()
|
||||||
|
|
||||||
return () => stop()
|
return () => stop()
|
||||||
}, [])
|
}, [status]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
if (beeContext.isLoading) {
|
if (!status.all) return <TroubleshootConnectionCard />
|
||||||
return (
|
|
||||||
<Container style={{ textAlign: 'center', padding: '50px' }}>
|
|
||||||
<CircularProgress />
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!beeContext.status.all) return <TroubleshootConnectionCard />
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.root}>
|
<div className={classes.root}>
|
||||||
@@ -59,7 +52,11 @@ export default function Accounting(): ReactElement {
|
|||||||
{!error && (
|
{!error && (
|
||||||
<>
|
<>
|
||||||
<div className={classes.actions}>
|
<div className={classes.actions}>
|
||||||
<CreatePostageStampModal />
|
{isBuyingStamp ? <CreatePostageStampModal onClose={() => setBuyingStamp(false)} /> : null}
|
||||||
|
|
||||||
|
<SwarmButton onClick={() => setBuyingStamp(true)} iconType={PlusSquare}>
|
||||||
|
Buy New Postage Stamp
|
||||||
|
</SwarmButton>
|
||||||
<div style={{ height: '5px' }}>{isLoading && <CircularProgress />}</div>
|
<div style={{ height: '5px' }}>{isLoading && <CircularProgress />}</div>
|
||||||
</div>
|
</div>
|
||||||
<StampsTable postageStamps={stamps} />
|
<StampsTable postageStamps={stamps} />
|
||||||
|
|||||||
@@ -1,35 +1,48 @@
|
|||||||
import { Typography } from '@material-ui/core/'
|
import { useContext } from 'react'
|
||||||
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 ExpandableList from '../../../components/ExpandableList'
|
||||||
|
import ExpandableListItemKey from '../../../components/ExpandableListItemKey'
|
||||||
|
import ExpandableListItemActions from '../../../components/ExpandableListItemActions'
|
||||||
|
import ExpandableListItemNote from '../../../components/ExpandableListItemNote'
|
||||||
|
import StatusIcon from '../../../components/StatusIcon'
|
||||||
|
import { Context } from '../../../providers/Bee'
|
||||||
|
|
||||||
interface Props extends StatusHookCommon {
|
const ChequebookDeployFund = (): ReactElement | null => {
|
||||||
chequebookAddress?: string
|
const { status, isLoading, chequebookAddress } = useContext(Context)
|
||||||
}
|
const isOk = status.chequebook
|
||||||
|
|
||||||
const ChequebookDeployFund = ({ chequebookAddress, isOk }: Props): ReactElement | null => {
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<ExpandableList
|
||||||
<p style={{ marginBottom: '20px', display: 'flex' }}>{chequebookAddress && <DepositModal />}</p>
|
label={
|
||||||
<div style={{ marginBottom: '10px' }}>
|
<>
|
||||||
{!isOk && (
|
<StatusIcon isOk={isOk} isLoading={isLoading} /> Chequebook Deployment & Funding
|
||||||
<div>
|
</>
|
||||||
<span>
|
}
|
||||||
|
>
|
||||||
|
<ExpandableListItemNote>
|
||||||
|
{isOk ? (
|
||||||
|
'Your chequebook is deployed and funded'
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
Your chequebook is either not deployed or funded. To run the node you will need xDAI and xBZZ on the xDai
|
Your chequebook is either not deployed or funded. To run the node you will need xDAI and xBZZ on the xDai
|
||||||
network. You may need to aquire BZZ through (e.g. <a href="https://bzz.exchange/">bzz.exchange</a>) and
|
network. You may need to aquire BZZ (e.g. <a href="https://bzz.exchange/">bzz.exchange</a>) and bridge it to
|
||||||
bridge it to the xDai network through the <a href="https://omni.xdaichain.com/bridge">omni bridge</a>. To
|
the xDai network through the <a href="https://omni.xdaichain.com/bridge">omni bridge</a>. To pay the
|
||||||
pay the transaction fees, you will also need xDAI token. You can purchase DAI on the network and bridge it
|
transaction fees, you will also need xDAI token. You can purchase DAI on the network and bridge it to xDai
|
||||||
to xDai network through the <a href="https://bridge.xdaichain.com/">xDai Bridge</a>. See the{' '}
|
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.
|
<a href="https://www.xdaichain.com/#xdai-stable-chain">official xDai website</a> for more information.
|
||||||
</span>
|
</>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</ExpandableListItemNote>
|
||||||
<Typography variant="subtitle1" gutterBottom>
|
{chequebookAddress && (
|
||||||
Chequebook Address
|
<>
|
||||||
</Typography>
|
<ExpandableListItemKey label="Chequebook Address" value={chequebookAddress.chequebookAddress} />
|
||||||
<EthereumAddress address={chequebookAddress} />
|
<ExpandableListItemActions>
|
||||||
</div>
|
<DepositModal />
|
||||||
|
</ExpandableListItemActions>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ExpandableList>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,57 +1,45 @@
|
|||||||
import { ReactElement, useContext } from 'react'
|
|
||||||
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 { ReactElement, useContext } from 'react'
|
||||||
|
|
||||||
import ConnectToHost from '../../../components/ConnectToHost'
|
|
||||||
import CodeBlockTabs from '../../../components/CodeBlockTabs'
|
import CodeBlockTabs from '../../../components/CodeBlockTabs'
|
||||||
|
import ExpandableList from '../../../components/ExpandableList'
|
||||||
|
import ExpandableListItem from '../../../components/ExpandableListItem'
|
||||||
|
import ExpandableListItemInput from '../../../components/ExpandableListItemInput'
|
||||||
|
import ExpandableListItemNote from '../../../components/ExpandableListItemNote'
|
||||||
|
import StatusIcon from '../../../components/StatusIcon'
|
||||||
|
import { Context } from '../../../providers/Bee'
|
||||||
import { Context as SettingsContext } from '../../../providers/Settings'
|
import { Context as SettingsContext } from '../../../providers/Settings'
|
||||||
|
|
||||||
type Props = StatusHookCommon
|
export default function NodeConnectionCheck(): ReactElement | null {
|
||||||
|
const { status, isLoading } = useContext(Context)
|
||||||
export default function NodeConnectionCheck({ isOk }: Props): ReactElement | null {
|
|
||||||
const { setDebugApiUrl, apiDebugUrl } = useContext(SettingsContext)
|
const { setDebugApiUrl, apiDebugUrl } = useContext(SettingsContext)
|
||||||
|
const isOk = status.debugApiConnection
|
||||||
const changeDebugApiUrl = (
|
|
||||||
<div style={{ display: 'flex', marginTop: '25px', marginBottom: '25px' }}>
|
|
||||||
<span style={{ marginRight: '15px' }}>
|
|
||||||
Debug API (<Typography variant="button">{apiDebugUrl}</Typography>)
|
|
||||||
</span>
|
|
||||||
<ConnectToHost
|
|
||||||
setHost={(host: string) => {
|
|
||||||
console.log(host) // eslint-disable-line
|
|
||||||
setDebugApiUrl(host)
|
|
||||||
}}
|
|
||||||
defaultHost={apiDebugUrl}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
if (isOk) {
|
|
||||||
return changeDebugApiUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<ExpandableList
|
||||||
{changeDebugApiUrl}
|
label={
|
||||||
|
<>
|
||||||
|
<StatusIcon isOk={isOk} isLoading={isLoading} /> Connection to Bee Debug API
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ExpandableListItemNote>
|
||||||
|
{isOk
|
||||||
|
? 'The connection to the Bee nodes debug API has been successful'
|
||||||
|
: 'We cannot connect to your nodes debug API. Please check the following to troubleshoot your issue.'}
|
||||||
|
</ExpandableListItemNote>
|
||||||
|
<ExpandableListItemInput label="Bee Debug API" value={apiDebugUrl} onConfirm={setDebugApiUrl} />
|
||||||
|
|
||||||
<div>
|
{!isOk && (
|
||||||
<Typography component="div" variant="body2" gutterBottom style={{ margin: '15px' }}>
|
<ExpandableList level={1} label="Troubleshoot">
|
||||||
We cannot connect to your nodes debug API at <Typography variant="button">{apiDebugUrl}</Typography>. Please
|
<ExpandableListItem
|
||||||
check the following to troubleshoot your issue.
|
label={
|
||||||
<Accordion style={{ marginTop: '20px' }}>
|
|
||||||
<AccordionSummary expandIcon={<ExpandMoreSharp />} aria-controls="panel1a-content" id="panel1a-header">
|
|
||||||
<Typography>Troubleshoot</Typography>
|
|
||||||
</AccordionSummary>
|
|
||||||
<AccordionDetails>
|
|
||||||
<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 showLineNumbers linux={`sudo systemctl status bee`} mac={`brew services list`} />
|
<CodeBlockTabs showLineNumbers linux={`sudo systemctl status bee`} mac={`brew services list`} />
|
||||||
<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 to
|
||||||
to start your bee node
|
start your bee node
|
||||||
</li>
|
</li>
|
||||||
<MuiAlert
|
<MuiAlert
|
||||||
style={{ marginTop: '10px', marginBottom: '10px' }}
|
style={{ marginTop: '10px', marginBottom: '10px' }}
|
||||||
@@ -59,10 +47,10 @@ export default function NodeConnectionCheck({ isOk }: Props): ReactElement | nul
|
|||||||
variant="filled"
|
variant="filled"
|
||||||
severity="error"
|
severity="error"
|
||||||
>
|
>
|
||||||
Your debug node API should never be completely open to the internet. If you want to connect
|
Your debug node API should never be completely open to the internet. If you want to connect remotely,
|
||||||
remotely, make sure your firewall settings are set to only allow specific trusted IP addresses and
|
make sure your firewall settings are set to only allow specific trusted IP addresses and block all
|
||||||
block all other ports. A simple google search for "what is my ip" will show you your
|
other ports. A simple google search for "what is my ip" will show you your computers public
|
||||||
computers public IP address to allow.
|
IP address to allow.
|
||||||
</MuiAlert>
|
</MuiAlert>
|
||||||
<CodeBlockTabs
|
<CodeBlockTabs
|
||||||
showLineNumbers
|
showLineNumbers
|
||||||
@@ -80,8 +68,8 @@ export default function NodeConnectionCheck({ isOk }: Props): ReactElement | nul
|
|||||||
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 (you can also use the
|
<strong>cors-allowed-origins</strong> must be set to your host domain or IP (you can also use the
|
||||||
wildcard <code>{"cors-allowed-origins: ['*']"}</code>). If edits are made to the configuration run
|
wildcard <code>{"cors-allowed-origins: ['*']"}</code>). If edits are made to the configuration run the
|
||||||
the restart command below for changes to take effect.
|
restart command below for changes to take effect.
|
||||||
</li>
|
</li>
|
||||||
<CodeBlockTabs
|
<CodeBlockTabs
|
||||||
showLineNumbers
|
showLineNumbers
|
||||||
@@ -89,11 +77,10 @@ export default function NodeConnectionCheck({ isOk }: Props): ReactElement | nul
|
|||||||
mac={`sudo vi /usr/local/etc/swarm-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>
|
}
|
||||||
</AccordionDetails>
|
/>
|
||||||
</Accordion>
|
</ExpandableList>
|
||||||
</Typography>
|
)}
|
||||||
</div>
|
</ExpandableList>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,27 @@
|
|||||||
import type { ReactElement } from 'react'
|
import { ReactElement, useContext } from 'react'
|
||||||
import { Typography } from '@material-ui/core/'
|
import ExpandableList from '../../../components/ExpandableList'
|
||||||
import EthereumAddress from '../../../components/EthereumAddress'
|
import ExpandableListItemKey from '../../../components/ExpandableListItemKey'
|
||||||
|
import ExpandableListItemNote from '../../../components/ExpandableListItemNote'
|
||||||
|
import StatusIcon from '../../../components/StatusIcon'
|
||||||
|
import { Context } from '../../../providers/Bee'
|
||||||
|
|
||||||
type Props = StatusEthereumConnectionHook
|
export default function EthereumConnectionCheck(): ReactElement | null {
|
||||||
|
const { status, isLoading, nodeAddresses } = useContext(Context)
|
||||||
|
const isOk = status.blockchainConnection
|
||||||
|
|
||||||
export default function EthereumConnectionCheck({ isOk, nodeAddresses }: Props): ReactElement | null {
|
|
||||||
if (isOk) {
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<ExpandableList
|
||||||
<Typography variant="subtitle1" gutterBottom>
|
label={
|
||||||
Node Address
|
<>
|
||||||
</Typography>
|
<StatusIcon isOk={isOk} isLoading={isLoading} /> Connection to Blockchain
|
||||||
<EthereumAddress address={nodeAddresses?.ethereum} />
|
</>
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
>
|
||||||
return (
|
<ExpandableListItemNote>
|
||||||
<p>
|
{isOk ? (
|
||||||
|
'Your node is connected to the xDai blockchain'
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
Your Bee node must have access to the xDai 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://www.xdaichain.com/" rel="noreferrer" target="_blank">
|
<a href="https://www.xdaichain.com/" rel="noreferrer" target="_blank">
|
||||||
@@ -27,8 +31,12 @@ export default function EthereumConnectionCheck({ isOk, nodeAddresses }: Props):
|
|||||||
<a href="https://getblock.io/" rel="noreferrer" target="_blank">
|
<a href="https://getblock.io/" rel="noreferrer" target="_blank">
|
||||||
Getblock
|
Getblock
|
||||||
</a>
|
</a>
|
||||||
. By default, Bee expects a local node at http://localhost:8545. To use a provider instead, simply change the{' '}
|
. By default, Bee expects a local node at http://localhost:8545. To use a provider instead, simply change
|
||||||
<strong>swap-endpoint</strong> in your configuration file.
|
the <strong>swap-endpoint</strong> in your configuration file.
|
||||||
</p>
|
</>
|
||||||
|
)}
|
||||||
|
</ExpandableListItemNote>
|
||||||
|
{nodeAddresses?.ethereum && <ExpandableListItemKey label="Ethereum Address" value={nodeAddresses?.ethereum} />}
|
||||||
|
</ExpandableList>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,37 @@
|
|||||||
import { ReactElement, useContext } from 'react'
|
import { ReactElement, useContext } from 'react'
|
||||||
import { Typography, Accordion, AccordionSummary, AccordionDetails } from '@material-ui/core/'
|
|
||||||
import { ExpandMoreSharp } from '@material-ui/icons/'
|
|
||||||
|
|
||||||
import ConnectToHost from '../../../components/ConnectToHost'
|
|
||||||
import CodeBlockTabs from '../../../components/CodeBlockTabs'
|
import CodeBlockTabs from '../../../components/CodeBlockTabs'
|
||||||
import { Context as SettingsContext } from '../../../providers/Settings'
|
import { Context as SettingsContext } from '../../../providers/Settings'
|
||||||
|
import ExpandableList from '../../../components/ExpandableList'
|
||||||
|
import ExpandableListItem from '../../../components/ExpandableListItem'
|
||||||
|
import ExpandableListItemNote from '../../../components/ExpandableListItemNote'
|
||||||
|
import ExpandableListItemInput from '../../../components/ExpandableListItemInput'
|
||||||
|
import StatusIcon from '../../../components/StatusIcon'
|
||||||
|
import { Context } from '../../../providers/Bee'
|
||||||
|
|
||||||
type Props = StatusHookCommon
|
export default function NodeConnectionCheck(): ReactElement | null {
|
||||||
|
|
||||||
export default function NodeConnectionCheck({ isOk }: Props): ReactElement | null {
|
|
||||||
const { setApiUrl, apiUrl } = useContext(SettingsContext)
|
const { setApiUrl, apiUrl } = useContext(SettingsContext)
|
||||||
|
const { status, isLoading } = useContext(Context)
|
||||||
|
const isOk = status.apiConnection
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<ExpandableList
|
||||||
<div style={{ display: 'flex', marginBottom: '25px' }}>
|
label={
|
||||||
<span style={{ marginRight: '15px' }}>
|
<>
|
||||||
Node API (<Typography variant="button">{apiUrl}</Typography>)
|
<StatusIcon isOk={isOk} isLoading={isLoading} /> Connection to Bee API
|
||||||
</span>
|
</>
|
||||||
<ConnectToHost setHost={setApiUrl} defaultHost={apiUrl} />
|
}
|
||||||
</div>
|
>
|
||||||
<div>
|
<ExpandableListItemNote>
|
||||||
|
{isOk
|
||||||
|
? 'The connection to the Bee nodes API has been successful'
|
||||||
|
: 'Could not connect to your Bee nodes API. Please check the troubleshoot below on how you may resolve it.'}
|
||||||
|
</ExpandableListItemNote>
|
||||||
|
<ExpandableListItemInput label="Bee API" value={apiUrl} onConfirm={setApiUrl} />
|
||||||
{!isOk && (
|
{!isOk && (
|
||||||
<Typography component="div" variant="body2" gutterBottom style={{ margin: '15px' }}>
|
<ExpandableList level={1} label="Troubleshoot">
|
||||||
We cannot connect to your nodes API at <Typography variant="button">{apiUrl}</Typography>. Please check the
|
<ExpandableListItem
|
||||||
following to troubleshoot your issue.
|
label={
|
||||||
<Accordion style={{ marginTop: '20px' }}>
|
|
||||||
<AccordionSummary expandIcon={<ExpandMoreSharp />} aria-controls="panel1a-content" id="panel1a-header">
|
|
||||||
<Typography>Troubleshoot</Typography>
|
|
||||||
</AccordionSummary>
|
|
||||||
<AccordionDetails>
|
|
||||||
<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 showLineNumbers linux={`sudo systemctl status bee`} mac={`brew services list`} />
|
<CodeBlockTabs showLineNumbers linux={`sudo systemctl status bee`} mac={`brew services list`} />
|
||||||
@@ -50,12 +52,10 @@ export default function NodeConnectionCheck({ isOk }: Props): ReactElement | nul
|
|||||||
mac={`brew services list \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>
|
}
|
||||||
</AccordionDetails>
|
/>
|
||||||
</Accordion>
|
</ExpandableList>
|
||||||
</Typography>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</ExpandableList>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +1,29 @@
|
|||||||
import type { ReactElement } from 'react'
|
import { ReactElement, useContext } from 'react'
|
||||||
import { Typography } from '@material-ui/core/'
|
import ExpandableList from '../../../components/ExpandableList'
|
||||||
|
import ExpandableListItemNote from '../../../components/ExpandableListItemNote'
|
||||||
|
import TopologyStats from '../../../components/TopologyStats'
|
||||||
|
import StatusIcon from '../../../components/StatusIcon'
|
||||||
|
import { Context } from '../../../providers/Bee'
|
||||||
|
|
||||||
type Props = StatusTopologyHook
|
export default function PeerConnection(): ReactElement | null {
|
||||||
|
const { status, isLoading, topology } = useContext(Context)
|
||||||
|
const isOk = status.topology
|
||||||
|
|
||||||
export default function PeerConnection({ isOk, topology }: Props): ReactElement | null {
|
|
||||||
const peers = (
|
|
||||||
<div style={{ display: 'flex', marginTop: '15px' }}>
|
|
||||||
<div style={{ marginRight: '30px' }}>
|
|
||||||
<Typography component="div" variant="subtitle1" gutterBottom color="textSecondary">
|
|
||||||
<span>Connected Peers</span>
|
|
||||||
</Typography>
|
|
||||||
<Typography component="h2" variant="h5">
|
|
||||||
{topology?.connected ? topology.connected : '-'}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Typography component="div" variant="subtitle1" gutterBottom color="textSecondary">
|
|
||||||
<span>Discovered Nodes</span>
|
|
||||||
</Typography>
|
|
||||||
<Typography component="h2" variant="h5">
|
|
||||||
{topology?.population ? topology.population : '-'}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
if (isOk) {
|
|
||||||
return (
|
return (
|
||||||
|
<ExpandableList
|
||||||
|
label={
|
||||||
<>
|
<>
|
||||||
<span>You are connected to peers!</span>
|
<StatusIcon isOk={isOk} isLoading={isLoading} /> Connection to Peers
|
||||||
{peers}
|
|
||||||
</>
|
</>
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
>
|
||||||
|
<ExpandableListItemNote>
|
||||||
|
{isOk
|
||||||
|
? 'You are connected to other Bee nodes'
|
||||||
|
: 'Your node is not connected to any peers. Please wait a bit if you just started the node, otherwise review your configuration file.'}
|
||||||
|
</ExpandableListItemNote>
|
||||||
|
|
||||||
return (
|
<TopologyStats topology={topology} />
|
||||||
<>
|
</ExpandableList>
|
||||||
<span>Your node is not connected to any peers</span>
|
|
||||||
{peers}
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +1,47 @@
|
|||||||
import type { ReactElement } from 'react'
|
import { ReactElement, useContext } from 'react'
|
||||||
import { Typography } from '@material-ui/core/'
|
|
||||||
import CodeBlockTabs from '../../../components/CodeBlockTabs'
|
import CodeBlockTabs from '../../../components/CodeBlockTabs'
|
||||||
|
import ExpandableList from '../../../components/ExpandableList'
|
||||||
|
import ExpandableListItem from '../../../components/ExpandableListItem'
|
||||||
|
import ExpandableListItemNote from '../../../components/ExpandableListItemNote'
|
||||||
|
import StatusIcon from '../../../components/StatusIcon'
|
||||||
|
import { Context } from '../../../providers/Bee'
|
||||||
|
|
||||||
type Props = StatusNodeVersionHook
|
export default function VersionCheck(): ReactElement | null {
|
||||||
|
const { status, isLoading, latestUserVersion, latestPublishedVersion, latestBeeVersionUrl } = useContext(Context)
|
||||||
|
const isOk = status.version
|
||||||
|
|
||||||
export default function VersionCheck({ isOk, userVersion, latestVersion, latestUrl }: Props): ReactElement | null {
|
|
||||||
const version = (
|
|
||||||
<div style={{ display: 'flex' }}>
|
|
||||||
<div style={{ marginRight: '30px' }}>
|
|
||||||
<p>
|
|
||||||
<span>User Version</span>
|
|
||||||
</p>
|
|
||||||
<Typography component="h5" variant="h5">
|
|
||||||
<span>{userVersion}</span>
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p>
|
|
||||||
<span>Latest Version</span>
|
|
||||||
</p>
|
|
||||||
<Typography component="h5" variant="h5">
|
|
||||||
<span>{latestVersion}</span>
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
// Running latest bee version
|
|
||||||
if (isOk) {
|
|
||||||
return (
|
return (
|
||||||
|
<ExpandableList
|
||||||
|
label={
|
||||||
<>
|
<>
|
||||||
<span>You are running the latest version of Bee</span>
|
<StatusIcon isOk={isOk} isLoading={isLoading} /> Bee Version
|
||||||
{version}
|
|
||||||
</>
|
</>
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
>
|
||||||
// Old version or not connected to bee debug API
|
<ExpandableListItemNote>
|
||||||
return (
|
{isOk ? (
|
||||||
|
'You are running the latest version of Bee.'
|
||||||
|
) : (
|
||||||
<>
|
<>
|
||||||
<span>
|
|
||||||
Your Bee version is out of date. Please update to the{' '}
|
Your Bee version is out of date. Please update to the{' '}
|
||||||
<a href={latestUrl} rel="noreferrer" target="_blank">
|
<a href={latestBeeVersionUrl} rel="noreferrer" target="_blank">
|
||||||
latest
|
latest
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
before continuing. Rerun the installation script below to upgrade. Reference the docs for help with updating.{' '}
|
before continuing. Rerun the installation script below to upgrade. For more information please see the{' '}
|
||||||
<a href="https://docs.ethswarm.org/docs/installation/manual#upgrading-bee" rel="noreferrer" target="_blank">
|
<a href="https://docs.ethswarm.org/docs/installation/manual#upgrading-bee" rel="noreferrer" target="_blank">
|
||||||
Docs
|
Docs
|
||||||
</a>
|
</a>
|
||||||
</span>
|
.
|
||||||
<CodeBlockTabs
|
<CodeBlockTabs
|
||||||
showLineNumbers
|
showLineNumbers
|
||||||
linux={`bee version\nwget https://github.com/ethersphere/bee/releases/download/${latestVersion}/bee_${latestVersion}_amd64.deb\nsudo dpkg -i bee_${latestVersion}_amd64.deb`}
|
linux={`bee version\nwget https://github.com/ethersphere/bee/releases/download/${latestPublishedVersion}/bee_${latestPublishedVersion}_amd64.deb\nsudo dpkg -i bee_${latestPublishedVersion}_amd64.deb`}
|
||||||
mac={`bee version\nbrew tap ethersphere/tap\nbrew install swarm-bee\nbrew services start swarm-bee`}
|
mac={`bee version\nbrew tap ethersphere/tap\nbrew install swarm-bee\nbrew services start swarm-bee`}
|
||||||
/>
|
/>
|
||||||
{version}
|
|
||||||
</>
|
</>
|
||||||
|
)}
|
||||||
|
</ExpandableListItemNote>
|
||||||
|
<ExpandableListItem label="Your Version" value={latestUserVersion || '-'} />
|
||||||
|
<ExpandableListItem label="Latest Version" value={latestPublishedVersion || '-'} />
|
||||||
|
</ExpandableList>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import { ReactElement, useState, useContext } from 'react'
|
import type { ReactElement } from 'react'
|
||||||
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
|
|
||||||
import { Typography, Paper, Button, Step, StepLabel, StepContent, Stepper, StepButton } from '@material-ui/core/'
|
|
||||||
import { CheckCircle, Error, 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,144 +6,16 @@ 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 { Context } from '../../providers/Bee'
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
|
||||||
createStyles({
|
|
||||||
root: {
|
|
||||||
padding: theme.spacing(2),
|
|
||||||
width: '100%',
|
|
||||||
},
|
|
||||||
button: {
|
|
||||||
marginTop: theme.spacing(1),
|
|
||||||
marginRight: theme.spacing(1),
|
|
||||||
},
|
|
||||||
actionsContainer: {
|
|
||||||
margin: theme.spacing(2),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
interface Step {
|
|
||||||
label: string
|
|
||||||
isOk: boolean
|
|
||||||
component: ReactElement
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function NodeSetupWorkflow(): ReactElement {
|
export default function NodeSetupWorkflow(): ReactElement {
|
||||||
const classes = useStyles()
|
|
||||||
const [activeStep, setActiveStep] = useState(-1)
|
|
||||||
|
|
||||||
const {
|
|
||||||
status,
|
|
||||||
isLoading,
|
|
||||||
latestUserVersion,
|
|
||||||
latestPublishedVersion,
|
|
||||||
isLatestBeeVersion,
|
|
||||||
latestBeeVersionUrl,
|
|
||||||
topology,
|
|
||||||
nodeAddresses,
|
|
||||||
chequebookAddress,
|
|
||||||
} = useContext(Context)
|
|
||||||
|
|
||||||
const steps: Step[] = [
|
|
||||||
{
|
|
||||||
label: 'Connected to Node DebugAPI',
|
|
||||||
isOk: status.debugApiConnection,
|
|
||||||
component: <DebugConnectionCheck isOk={status.debugApiConnection} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Running latest Bee version',
|
|
||||||
isOk: status.version,
|
|
||||||
component: (
|
|
||||||
<VersionCheck
|
|
||||||
isOk={status.version}
|
|
||||||
isLatestBeeVersion={isLatestBeeVersion}
|
|
||||||
userVersion={latestUserVersion}
|
|
||||||
latestVersion={latestPublishedVersion}
|
|
||||||
latestUrl={latestBeeVersionUrl}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Connected to xDai Blockchain',
|
|
||||||
isOk: status.blockchainConnection,
|
|
||||||
component: <EthereumConnectionCheck isOk={status.blockchainConnection} nodeAddresses={nodeAddresses} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Deployed and Funded Chequebook',
|
|
||||||
isOk: status.chequebook,
|
|
||||||
component: (
|
|
||||||
<ChequebookDeployFund chequebookAddress={chequebookAddress?.chequebookAddress} isOk={status.chequebook} />
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Connected to Node API',
|
|
||||||
isOk: status.apiConnection,
|
|
||||||
component: <NodeConnectionCheck isOk={status.apiConnection} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Connected to Peers',
|
|
||||||
isOk: status.topology,
|
|
||||||
component: <PeerConnection isOk={status.topology} topology={topology} />,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const handleNext = () => {
|
|
||||||
setActiveStep(prevActiveStep => prevActiveStep + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleBack = () => {
|
|
||||||
setActiveStep(prevActiveStep => prevActiveStep - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper className={classes.root}>
|
|
||||||
<Typography variant="h5" gutterBottom>
|
|
||||||
Node Setup
|
|
||||||
</Typography>
|
|
||||||
<Stepper nonLinear activeStep={activeStep} orientation="vertical">
|
|
||||||
{steps.map(({ label, isOk, component }, index) => (
|
|
||||||
<Step key={label}>
|
|
||||||
<StepLabel
|
|
||||||
onClick={() => setActiveStep(index === activeStep ? steps.length : index)}
|
|
||||||
StepIconComponent={() => {
|
|
||||||
if (isLoading) return <Autorenew style={{ height: '25px', cursor: 'pointer' }} />
|
|
||||||
|
|
||||||
if (isOk) return <CheckCircle style={{ color: '#32c48d', height: '25px', cursor: 'pointer' }} />
|
|
||||||
|
|
||||||
return <Error style={{ color: '#c9201f', height: '25px', cursor: 'pointer' }} />
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<StepButton
|
|
||||||
disabled={isLoading}
|
|
||||||
onClick={() => setActiveStep(index === activeStep ? steps.length : index)}
|
|
||||||
style={{ justifyContent: 'space-between' }}
|
|
||||||
>
|
|
||||||
<div style={{ display: 'flex' }}>
|
|
||||||
<div style={{ marginTop: '5px' }}>{label}</div>
|
|
||||||
<div style={{ marginLeft: '12px' }}>
|
|
||||||
{index === activeStep ? <ExpandLessSharp /> : <ExpandMoreSharp />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</StepButton>
|
|
||||||
</StepLabel>
|
|
||||||
<StepContent>
|
|
||||||
<Typography component="div">{component}</Typography>
|
|
||||||
<div className={classes.actionsContainer}>
|
|
||||||
<div>
|
<div>
|
||||||
<Button disabled={activeStep === 0} onClick={handleBack} className={classes.button}>
|
<DebugConnectionCheck />
|
||||||
Back
|
<VersionCheck />
|
||||||
</Button>
|
<EthereumConnectionCheck />
|
||||||
<Button variant="contained" color="primary" onClick={handleNext} className={classes.button}>
|
<ChequebookDeployFund />
|
||||||
{index < steps.length - 1 ? 'Next' : 'Finish'}
|
<NodeConnectionCheck />
|
||||||
</Button>
|
<PeerConnection />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</StepContent>
|
|
||||||
</Step>
|
|
||||||
))}
|
|
||||||
</Stepper>
|
|
||||||
</Paper>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
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 {
|
import type {
|
||||||
NodeAddresses,
|
|
||||||
ChequebookAddressResponse,
|
ChequebookAddressResponse,
|
||||||
LastChequesResponse,
|
|
||||||
Health,
|
Health,
|
||||||
|
LastChequesResponse,
|
||||||
|
NodeAddresses,
|
||||||
Peer,
|
Peer,
|
||||||
Topology,
|
Topology,
|
||||||
} from '@ethersphere/bee-js'
|
} from '@ethersphere/bee-js'
|
||||||
|
import { createContext, ReactChild, ReactElement, useContext, useEffect, useState } from 'react'
|
||||||
|
import semver from 'semver'
|
||||||
|
import { engines } from '../../package.json'
|
||||||
import { useLatestBeeRelease } from '../hooks/apiHooks'
|
import { useLatestBeeRelease } from '../hooks/apiHooks'
|
||||||
|
import { Token } from '../models/Token'
|
||||||
|
import type { Balance, ChequebookBalance, Settlements } from '../types'
|
||||||
|
import { Context as SettingsContext } from './Settings'
|
||||||
|
|
||||||
interface Status {
|
interface Status {
|
||||||
all: boolean
|
all: boolean
|
||||||
@@ -52,6 +51,8 @@ interface ContextInterface {
|
|||||||
refresh: () => Promise<void>
|
refresh: () => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const startedInDevMode = window.location.search.includes('devMode=1')
|
||||||
|
|
||||||
const initialValues: ContextInterface = {
|
const initialValues: ContextInterface = {
|
||||||
status: {
|
status: {
|
||||||
all: false,
|
all: false,
|
||||||
@@ -103,6 +104,8 @@ function getStatus(
|
|||||||
chequebookBalance: ChequebookBalance | null,
|
chequebookBalance: ChequebookBalance | null,
|
||||||
error: Error | null,
|
error: Error | null,
|
||||||
): Status {
|
): Status {
|
||||||
|
// FIXME: `devMode` is a temporary workaround to be able to develop with only one node
|
||||||
|
const devMode = startedInDevMode || Boolean(process.env.REACT_APP_DEV_MODE)
|
||||||
const status = {
|
const status = {
|
||||||
version: Boolean(
|
version: Boolean(
|
||||||
debugApiHealth &&
|
debugApiHealth &&
|
||||||
@@ -113,11 +116,12 @@ function getStatus(
|
|||||||
blockchainConnection: Boolean(nodeAddresses?.ethereum),
|
blockchainConnection: Boolean(nodeAddresses?.ethereum),
|
||||||
debugApiConnection: Boolean(debugApiHealth?.status === 'ok'),
|
debugApiConnection: Boolean(debugApiHealth?.status === 'ok'),
|
||||||
apiConnection: apiHealth,
|
apiConnection: apiHealth,
|
||||||
topology: Boolean(topology?.connected && topology?.connected > 0),
|
topology: Boolean(topology?.connected && topology?.connected > 0) || devMode,
|
||||||
chequebook:
|
chequebook:
|
||||||
Boolean(chequebookAddress?.chequebookAddress) &&
|
(Boolean(chequebookAddress?.chequebookAddress) &&
|
||||||
chequebookBalance !== null &&
|
chequebookBalance !== null &&
|
||||||
chequebookBalance?.totalBalance.toBigNumber.isGreaterThan(0),
|
chequebookBalance?.totalBalance.toBigNumber.isGreaterThan(0)) ||
|
||||||
|
devMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ...status, all: !error && Object.values(status).every(v => v) }
|
return { ...status, all: !error && Object.values(status).every(v => v) }
|
||||||
@@ -153,7 +157,7 @@ export function Provider({ children }: Props): ReactElement {
|
|||||||
setApiHealth(false)
|
setApiHealth(false)
|
||||||
|
|
||||||
refresh()
|
refresh()
|
||||||
}, [beeApi])
|
}, [beeApi]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
@@ -169,7 +173,7 @@ export function Provider({ children }: Props): ReactElement {
|
|||||||
setSettlements(null)
|
setSettlements(null)
|
||||||
|
|
||||||
refresh()
|
refresh()
|
||||||
}, [beeDebugApi])
|
}, [beeDebugApi]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
// Don't want to refresh when already refreshing
|
// Don't want to refresh when already refreshing
|
||||||
@@ -279,7 +283,7 @@ export function Provider({ children }: Props): ReactElement {
|
|||||||
|
|
||||||
await Promise.allSettled(promises)
|
await Promise.allSettled(promises)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e)
|
setError(e as Error)
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
setIsRefreshing(false)
|
setIsRefreshing(false)
|
||||||
@@ -300,7 +304,7 @@ export function Provider({ children }: Props): ReactElement {
|
|||||||
|
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}
|
}
|
||||||
}, [frequency, beeDebugApi, beeApi])
|
}, [frequency, beeDebugApi, beeApi]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Context.Provider
|
<Context.Provider
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { createContext, ReactChild, ReactElement, useState } from 'react'
|
||||||
|
import { SwarmFile } from '../utils/SwarmFile'
|
||||||
|
|
||||||
|
interface ContextInterface {
|
||||||
|
files: SwarmFile[]
|
||||||
|
setFiles: (files: SwarmFile[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialValues: ContextInterface = {
|
||||||
|
files: [],
|
||||||
|
setFiles: () => {}, // 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 [files, setFiles] = useState<SwarmFile[]>(initialValues.files)
|
||||||
|
|
||||||
|
return <Context.Provider value={{ files, setFiles }}>{children}</Context.Provider>
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { PostageBatch } from '@ethersphere/bee-js'
|
import { PostageBatch } from '@ethersphere/bee-js'
|
||||||
import { createContext, ReactChild, ReactElement, useEffect, useState, useContext } from 'react'
|
import { createContext, ReactChild, ReactElement, useContext, useEffect, useState } from 'react'
|
||||||
import { Context as SettingsContext } from './Settings'
|
import { Context as SettingsContext } from './Settings'
|
||||||
|
|
||||||
export interface EnrichedPostageBatch extends PostageBatch {
|
export interface EnrichedPostageBatch extends PostageBatch {
|
||||||
@@ -48,7 +48,7 @@ function enrichStamp(postageBatch: PostageBatch): EnrichedPostageBatch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Provider({ children }: Props): ReactElement {
|
export function Provider({ children }: Props): ReactElement {
|
||||||
const { beeApi } = useContext(SettingsContext)
|
const { beeDebugApi } = useContext(SettingsContext)
|
||||||
const [stamps, setStamps] = useState<EnrichedPostageBatch[] | null>(initialValues.stamps)
|
const [stamps, setStamps] = useState<EnrichedPostageBatch[] | null>(initialValues.stamps)
|
||||||
const [error, setError] = useState<Error | null>(initialValues.error)
|
const [error, setError] = useState<Error | null>(initialValues.error)
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(initialValues.isLoading)
|
const [isLoading, setIsLoading] = useState<boolean>(initialValues.isLoading)
|
||||||
@@ -59,16 +59,16 @@ export function Provider({ children }: Props): ReactElement {
|
|||||||
// Don't want to refresh when already refreshing
|
// Don't want to refresh when already refreshing
|
||||||
if (isLoading) return
|
if (isLoading) return
|
||||||
|
|
||||||
if (!beeApi) return
|
if (!beeDebugApi) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
const stamps = await beeApi.getAllPostageBatch()
|
const stamps = await beeDebugApi.getAllPostageBatch()
|
||||||
|
|
||||||
setStamps(stamps.map(enrichStamp))
|
setStamps(stamps.map(enrichStamp))
|
||||||
setLastUpdate(Date.now())
|
setLastUpdate(Date.now())
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e)
|
setError(e as Error)
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
@@ -87,7 +87,7 @@ export function Provider({ children }: Props): ReactElement {
|
|||||||
|
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}
|
}
|
||||||
}, [frequency])
|
}, [frequency]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Context.Provider value={{ stamps, error, isLoading, lastUpdate, start, stop, refresh }}>
|
<Context.Provider value={{ stamps, error, isLoading, lastUpdate, start, stop, refresh }}>
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
import type { ReactElement } from 'react'
|
import type { ReactElement } from 'react'
|
||||||
import { Switch } from 'react-router-dom'
|
import { Route, Switch } from 'react-router-dom'
|
||||||
|
|
||||||
import { Route } from 'react-router-dom'
|
|
||||||
|
|
||||||
import Info from './pages/info'
|
|
||||||
import Status from './pages/status'
|
|
||||||
import Files from './pages/files'
|
|
||||||
import Peers from './pages/peers'
|
|
||||||
import Accounting from './pages/accounting'
|
import Accounting from './pages/accounting'
|
||||||
|
import { Download } from './pages/files/Download'
|
||||||
|
import { Share } from './pages/files/Share'
|
||||||
|
import { Upload } from './pages/files/Upload'
|
||||||
|
import { UploadLander } from './pages/files/UploadLander'
|
||||||
|
import Info from './pages/info'
|
||||||
import Settings from './pages/settings'
|
import Settings from './pages/settings'
|
||||||
import Stamps from './pages/stamps'
|
import Stamps from './pages/stamps'
|
||||||
|
import Status from './pages/status'
|
||||||
|
|
||||||
export enum ROUTES {
|
export enum ROUTES {
|
||||||
INFO = '/',
|
INFO = '/',
|
||||||
FILES = '/files',
|
FILES = '/files',
|
||||||
PEERS = '/peers',
|
UPLOAD = '/files/upload',
|
||||||
|
UPLOAD_IN_PROGRESS = '/files/upload/workflow',
|
||||||
|
DOWNLOAD = '/files/download',
|
||||||
|
HASH = '/files/hash/:hash',
|
||||||
ACCOUNTING = '/accounting',
|
ACCOUNTING = '/accounting',
|
||||||
SETTINGS = '/settings',
|
SETTINGS = '/settings',
|
||||||
STAMPS = '/stamps',
|
STAMPS = '/stamps',
|
||||||
@@ -23,13 +25,15 @@ export enum ROUTES {
|
|||||||
|
|
||||||
const BaseRouter = (): ReactElement => (
|
const BaseRouter = (): ReactElement => (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path={ROUTES.INFO} component={Info} />
|
<Route exact path={ROUTES.UPLOAD_IN_PROGRESS} component={Upload} />
|
||||||
<Route exact path={ROUTES.FILES} component={Files} />
|
<Route exact path={ROUTES.UPLOAD} component={UploadLander} />
|
||||||
<Route exact path={ROUTES.PEERS} component={Peers} />
|
<Route exact path={ROUTES.DOWNLOAD} component={Download} />
|
||||||
|
<Route exact path={ROUTES.HASH} component={Share} />
|
||||||
<Route exact path={ROUTES.ACCOUNTING} component={Accounting} />
|
<Route exact path={ROUTES.ACCOUNTING} component={Accounting} />
|
||||||
<Route exact path={ROUTES.SETTINGS} component={Settings} />
|
<Route exact path={ROUTES.SETTINGS} component={Settings} />
|
||||||
<Route exact path={ROUTES.STAMPS} component={Stamps} />
|
<Route exact path={ROUTES.STAMPS} component={Stamps} />
|
||||||
<Route exact path={ROUTES.STATUS} component={Status} />
|
<Route exact path={ROUTES.STATUS} component={Status} />
|
||||||
|
<Route path={ROUTES.INFO} component={Info} />
|
||||||
</Switch>
|
</Switch>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createMuiTheme, Theme } from '@material-ui/core/styles'
|
import { createTheme, Theme } from '@material-ui/core/styles'
|
||||||
import { orange } from '@material-ui/core/colors'
|
import { orange } from '@material-ui/core/colors'
|
||||||
|
|
||||||
declare module '@material-ui/core/styles/createPalette' {
|
declare module '@material-ui/core/styles/createPalette' {
|
||||||
@@ -9,40 +9,146 @@ declare module '@material-ui/core/styles/createPalette' {
|
|||||||
|
|
||||||
// Overwriting default components styles
|
// Overwriting default components styles
|
||||||
const componentsOverrides = (theme: Theme) => ({
|
const componentsOverrides = (theme: Theme) => ({
|
||||||
MuiDrawer: {
|
MuiListItem: {
|
||||||
paper: {
|
button: {
|
||||||
width: 300,
|
'&:hover': {
|
||||||
backgroundColor: '#212121',
|
backgroundColor: '#fcf2e8',
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
// https://github.com/mui-org/material-ui/issues/22543
|
||||||
|
'@media (hover: none)': {
|
||||||
|
backgroundColor: '#fcf2e8',
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiContainer: {
|
||||||
|
root: { padding: theme.spacing(8) },
|
||||||
|
maxWidthXs: { padding: theme.spacing(8) },
|
||||||
|
maxWidthSm: { padding: theme.spacing(8) },
|
||||||
|
maxWidthMd: { padding: theme.spacing(8) },
|
||||||
|
maxWidthLg: { padding: theme.spacing(8) },
|
||||||
|
maxWidthXl: { padding: theme.spacing(8) },
|
||||||
|
},
|
||||||
|
MuiButton: {
|
||||||
|
startIcon: { marginLeft: theme.spacing(1) },
|
||||||
|
endIcon: { marginRight: theme.spacing(1) },
|
||||||
|
outlined: {
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: theme.spacing(10),
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
backgroundColor: '#fcf2e8',
|
||||||
|
},
|
||||||
|
outlinedSizeSmall: {
|
||||||
|
padding: theme.spacing(1),
|
||||||
|
paddingLeft: theme.spacing(2),
|
||||||
|
paddingRight: theme.spacing(2),
|
||||||
|
boxShadow: 'none',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
color: 'white',
|
||||||
|
boxShadow: 'none',
|
||||||
|
// https://github.com/mui-org/material-ui/issues/22543
|
||||||
|
'@media (hover: none)': {
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
color: 'white',
|
||||||
|
boxShadow: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outlinedSizeLarge: {
|
||||||
|
padding: theme.spacing(4),
|
||||||
|
borderRadius: 0,
|
||||||
|
boxShadow: 'none',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
color: 'white',
|
||||||
|
boxShadow: 'none',
|
||||||
|
// https://github.com/mui-org/material-ui/issues/22543
|
||||||
|
'@media (hover: none)': {
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
color: 'white',
|
||||||
|
boxShadow: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
containedSizeLarge: {
|
||||||
|
padding: theme.spacing(4),
|
||||||
|
borderRadius: 0,
|
||||||
|
boxShadow: 'none',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
color: 'white',
|
||||||
|
boxShadow: 'none',
|
||||||
|
// https://github.com/mui-org/material-ui/issues/22543
|
||||||
|
'@media (hover: none)': {
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
color: 'white',
|
||||||
|
boxShadow: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
containedSizeSmall: {
|
||||||
|
padding: theme.spacing(1),
|
||||||
|
paddingLeft: theme.spacing(2),
|
||||||
|
paddingRight: theme.spacing(2),
|
||||||
|
borderRadius: 0,
|
||||||
|
boxShadow: 'none',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
color: 'white',
|
||||||
|
boxShadow: 'none',
|
||||||
|
// https://github.com/mui-org/material-ui/issues/22543
|
||||||
|
'@media (hover: none)': {
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
color: 'white',
|
||||||
|
boxShadow: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contained: {
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
backgroundColor: 'white',
|
||||||
|
boxShadow: 'none',
|
||||||
|
borderRadius: 0,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
color: 'white',
|
||||||
|
boxShadow: 'none',
|
||||||
|
// https://github.com/mui-org/material-ui/issues/22543
|
||||||
|
'@media (hover: none)': {
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
color: 'white',
|
||||||
|
boxShadow: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'&:focus': {
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
'&:active': {
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
'&:disabled': {
|
||||||
|
backgroundColor: 'white',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
MuiTab: {
|
MuiTab: {
|
||||||
root: {
|
root: {
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: theme.palette.background.paper,
|
||||||
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': {
|
'&:hover': {
|
||||||
color: theme.palette.secondary,
|
backgroundColor: '#fcf2e8',
|
||||||
|
color: theme.palette.primary.main,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
},
|
},
|
||||||
'&$selected': {
|
'&$selected': {
|
||||||
color: theme.palette.secondary,
|
|
||||||
fontWeight: theme.typography.fontWeightMedium,
|
fontWeight: theme.typography.fontWeightMedium,
|
||||||
},
|
},
|
||||||
'&:focus': {
|
|
||||||
color: theme.palette.secondary,
|
|
||||||
},
|
},
|
||||||
|
textColorInherit: {
|
||||||
|
opacity: 0.5,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
MuiTabs: {
|
MuiTabs: {
|
||||||
@@ -50,7 +156,7 @@ const componentsOverrides = (theme: Theme) => ({
|
|||||||
borderBottom: 'none',
|
borderBottom: 'none',
|
||||||
},
|
},
|
||||||
indicator: {
|
indicator: {
|
||||||
backgroundColor: theme.palette.primary.main,
|
backgroundColor: 'transparent',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -59,16 +165,19 @@ const propsOverrides = {
|
|||||||
MuiTab: {
|
MuiTab: {
|
||||||
disableRipple: true,
|
disableRipple: true,
|
||||||
},
|
},
|
||||||
|
MuiButtonBase: {
|
||||||
|
disableRipple: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const theme = createMuiTheme({
|
export const theme = createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
type: 'light',
|
type: 'light',
|
||||||
background: {
|
background: {
|
||||||
default: '#fafafa',
|
default: '#efefef',
|
||||||
},
|
},
|
||||||
primary: {
|
primary: {
|
||||||
light: orange.A200,
|
light: '#fcf2e8',
|
||||||
main: '#dd7700',
|
main: '#dd7700',
|
||||||
dark: orange[800],
|
dark: orange[800],
|
||||||
},
|
},
|
||||||
@@ -78,6 +187,23 @@ export const theme = createMuiTheme({
|
|||||||
},
|
},
|
||||||
typography: {
|
typography: {
|
||||||
fontFamily: ['Work Sans', 'Montserrat', 'Nunito', 'Roboto', '"Helvetica Neue"', 'Arial', 'sans-serif'].join(','),
|
fontFamily: ['Work Sans', 'Montserrat', 'Nunito', 'Roboto', '"Helvetica Neue"', 'Arial', 'sans-serif'].join(','),
|
||||||
|
h1: {
|
||||||
|
fontSize: '1.3rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
|
h2: {
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
|
h3: {
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
|
body2: {
|
||||||
|
fontFamily: '"IBM Plex Mono", monospace',
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: '1rem',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
export class SwarmFile {
|
||||||
|
public name: string
|
||||||
|
public path: string
|
||||||
|
public type: string
|
||||||
|
public size: number
|
||||||
|
public webkitRelativePath: string
|
||||||
|
public arrayBuffer: () => Promise<ArrayBuffer>
|
||||||
|
private data: Promise<ArrayBuffer>
|
||||||
|
|
||||||
|
constructor(file: File) {
|
||||||
|
const path = Reflect.get(file, 'path') || file.webkitRelativePath || file.name
|
||||||
|
this.path = path.startsWith('/') ? path.slice(1) : path
|
||||||
|
this.webkitRelativePath = this.path
|
||||||
|
this.name = file.name
|
||||||
|
this.type = file.type
|
||||||
|
this.size = file.size
|
||||||
|
this.data = file.arrayBuffer()
|
||||||
|
this.arrayBuffer = async () => {
|
||||||
|
const data = await this.data
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export function getPrettyDateString(date: Date): string {
|
||||||
|
const string = date.toString()
|
||||||
|
|
||||||
|
return string.split('GMT')[0].trim()
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { detectIndexHtml } from './file'
|
||||||
|
|
||||||
|
describe('file utils', () => {
|
||||||
|
it('detectIndexHtml should find index.html', () => {
|
||||||
|
expect(
|
||||||
|
detectIndexHtml([
|
||||||
|
{ name: 'swarm.png', path: 'swarm.png' },
|
||||||
|
{ name: 'index.html', path: 'index.html' },
|
||||||
|
]),
|
||||||
|
).toBe('index.html')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('detectIndexHtml should find index.htm', () => {
|
||||||
|
expect(
|
||||||
|
detectIndexHtml([
|
||||||
|
{ name: 'index.htm', path: 'index.htm' },
|
||||||
|
{ name: 'swarm.png', path: 'swarm.png' },
|
||||||
|
]),
|
||||||
|
).toBe('index.htm')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('detectIndexHtml should find nested index.html', () => {
|
||||||
|
expect(
|
||||||
|
detectIndexHtml([
|
||||||
|
{ name: 'swarm.png', path: 'sample-folder/swarm.png' },
|
||||||
|
{ name: 'index.html', path: 'sample-folder/index.html' },
|
||||||
|
]),
|
||||||
|
).toBe('index.html')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('detectIndexHtml should not find nested index.htm when ambigous', () => {
|
||||||
|
expect(
|
||||||
|
detectIndexHtml([
|
||||||
|
{ name: 'index.htm', path: 'sample-folder/index.htm' },
|
||||||
|
{ name: 'swarm.png', path: 'other-folder/swarm.png' },
|
||||||
|
]),
|
||||||
|
).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('detectIndexHtml should not find deep index.html', () => {
|
||||||
|
expect(
|
||||||
|
detectIndexHtml([
|
||||||
|
{ name: 'index.html', path: 'sample-folder/index.html' },
|
||||||
|
{ name: 'swarm.png', path: 'swarm.png' },
|
||||||
|
]),
|
||||||
|
).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('detectIndexHtml should return false when no matches appear', () => {
|
||||||
|
expect(
|
||||||
|
detectIndexHtml([
|
||||||
|
{ name: 'swarm.png', path: 'swarm.png' },
|
||||||
|
{ name: 'swarm.jpg', path: 'swarm.jpg' },
|
||||||
|
]),
|
||||||
|
).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { FileData } from '@ethersphere/bee-js'
|
||||||
|
import { SwarmFile } from './SwarmFile'
|
||||||
|
|
||||||
|
const indexHtmls = ['index.html', 'index.htm']
|
||||||
|
|
||||||
|
export function detectIndexHtml(files: SwarmFile[]): string | false {
|
||||||
|
if (!files.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const exactMatch = files.find(x => indexHtmls.includes(x.path))
|
||||||
|
|
||||||
|
if (exactMatch) {
|
||||||
|
return exactMatch.name
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefix = files[0].path.split('/')[0] + '/'
|
||||||
|
|
||||||
|
const allStartWithSamePrefix = files.every(x => x.path.startsWith(prefix))
|
||||||
|
|
||||||
|
if (allStartWithSamePrefix) {
|
||||||
|
const match = files.find(x => indexHtmls.map(y => prefix + y).includes(x.path))
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
return match.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHumanReadableFileSize(bytes: number): string {
|
||||||
|
if (bytes >= 1e6) {
|
||||||
|
return (bytes / 1e6).toFixed(2) + ' MB'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes >= 1e3) {
|
||||||
|
return (bytes / 1e3).toFixed(2) + ' kB'
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes + ' bytes'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertBeeFileToBrowserFile(file: FileData<ArrayBuffer>): Partial<File> {
|
||||||
|
return {
|
||||||
|
name: file.name,
|
||||||
|
size: file.data.byteLength,
|
||||||
|
type: file.contentType,
|
||||||
|
arrayBuffer: () => new Promise(resolve => resolve(file.data)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertManifestToFiles(files: Record<string, string>): SwarmFile[] {
|
||||||
|
return Object.entries(files).map(
|
||||||
|
x =>
|
||||||
|
({
|
||||||
|
name: x[0],
|
||||||
|
path: x[0],
|
||||||
|
type: 'n/a',
|
||||||
|
size: 0,
|
||||||
|
webkitRelativePath: x[0],
|
||||||
|
arrayBuffer: () => new Promise(resolve => resolve(new ArrayBuffer(0))),
|
||||||
|
} as SwarmFile),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAssetNameFromFiles(files: SwarmFile[]): string {
|
||||||
|
if (files.length === 1) {
|
||||||
|
return files[0].name
|
||||||
|
}
|
||||||
|
|
||||||
|
return files[0].path.split('/')[0]
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function shortenHash(hash: string, sliceLength = 8): string {
|
||||||
|
return `${hash.slice(0, sliceLength)}[…]${hash.slice(-sliceLength)}`
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import BigNumber from 'bignumber.js'
|
import BigNumber from 'bignumber.js'
|
||||||
import { isInteger, makeBigNumber } from './index'
|
import { extractSwarmHash, isInteger, makeBigNumber } from './index'
|
||||||
|
|
||||||
describe('utils', () => {
|
describe('utils', () => {
|
||||||
describe('isInteger', () => {
|
describe('isInteger', () => {
|
||||||
@@ -42,7 +42,7 @@ describe('utils', () => {
|
|||||||
1,
|
1,
|
||||||
-1,
|
-1,
|
||||||
]
|
]
|
||||||
const wrongValues = [new Function()]
|
const wrongValues = [new Function()] // eslint-disable-line no-new-func
|
||||||
|
|
||||||
correctValues.forEach(v => {
|
correctValues.forEach(v => {
|
||||||
test(`testing ${v}`, () => {
|
test(`testing ${v}`, () => {
|
||||||
@@ -57,4 +57,50 @@ describe('utils', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('extractSwarmHash', () => {
|
||||||
|
test('should return 64 hash', () => {
|
||||||
|
expect(extractSwarmHash('7f0fe712cdd78bdea52d040369eb32b6af5ecd01fa5ae49b7506412abdd81ac3')).toBe(
|
||||||
|
'7f0fe712cdd78bdea52d040369eb32b6af5ecd01fa5ae49b7506412abdd81ac3',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should return 128 hash', () => {
|
||||||
|
expect(
|
||||||
|
extractSwarmHash(
|
||||||
|
'd1829242c4d08e9f914fedfb1f68aacd62826d75370a9a57a80c9e6e6a49983c767c013be9aa4319e34fd8323ef0f2a57426b30e66c87e219f6f6359e2595e7f',
|
||||||
|
),
|
||||||
|
).toBe(
|
||||||
|
'd1829242c4d08e9f914fedfb1f68aacd62826d75370a9a57a80c9e6e6a49983c767c013be9aa4319e34fd8323ef0f2a57426b30e66c87e219f6f6359e2595e7f',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should return 64 hash from url', () => {
|
||||||
|
expect(
|
||||||
|
extractSwarmHash('http://localhost:1633/bzz/7f0fe712cdd78bdea52d040369eb32b6af5ecd01fa5ae49b7506412abdd81ac3/'),
|
||||||
|
).toBe('7f0fe712cdd78bdea52d040369eb32b6af5ecd01fa5ae49b7506412abdd81ac3')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should return 128 hash from url', () => {
|
||||||
|
expect(
|
||||||
|
extractSwarmHash(
|
||||||
|
'http://localhost:1633/bzz/d1829242c4d08e9f914fedfb1f68aacd62826d75370a9a57a80c9e6e6a49983c767c013be9aa4319e34fd8323ef0f2a57426b30e66c87e219f6f6359e2595e7f/',
|
||||||
|
),
|
||||||
|
).toBe(
|
||||||
|
'd1829242c4d08e9f914fedfb1f68aacd62826d75370a9a57a80c9e6e6a49983c767c013be9aa4319e34fd8323ef0f2a57426b30e66c87e219f6f6359e2595e7f',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should return null when nothing is found', () => {
|
||||||
|
expect(extractSwarmHash('Bee Dashboard')).toBe(null)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should return null when length is incorrect', () => {
|
||||||
|
expect(extractSwarmHash('7f0fe712cdd78bdea52d040369eb32b6af5ecd01fa5ae49b7506412abdd81a')).toBe(null)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should return null when alphanumeric', () => {
|
||||||
|
expect(extractSwarmHash('gkQ6duo5iHJ099g908P0t17ZWFf8Ke2klrywLP5BGtLkcaEC5W0kLEfbe4wUnDI6')).toBe(null)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -106,3 +106,9 @@ export function makeRetriablePromise<T>(fn: () => Promise<T>, maxRetries = 3, de
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function extractSwarmHash(string: string): string | null {
|
||||||
|
const matches = string.match(/[a-fA-F0-9]{64,128}/)
|
||||||
|
|
||||||
|
return (matches && matches[0]) || null
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { shortenHash } from './hash'
|
||||||
|
|
||||||
|
export enum HISTORY_KEYS {
|
||||||
|
UPLOAD_HISTORY = 'UPLOAD_HISTORY',
|
||||||
|
DOWNLOAD_HISTORY = 'DOWNLOAD_HISTORY',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HistoryItem {
|
||||||
|
createdAt: number
|
||||||
|
name: string
|
||||||
|
hash: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function putHistory(key: string, hash: string, name: string): void {
|
||||||
|
const history = getHistorySafe(key)
|
||||||
|
|
||||||
|
const existingIndex = history.findIndex(x => x.hash === hash)
|
||||||
|
|
||||||
|
if (existingIndex !== -1) {
|
||||||
|
history.splice(existingIndex, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
history.unshift({
|
||||||
|
createdAt: Date.now(),
|
||||||
|
hash,
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (history.length > 10) {
|
||||||
|
history.length = 10
|
||||||
|
}
|
||||||
|
localStorage.setItem(key, JSON.stringify(history))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHistorySafe(key: string): HistoryItem[] {
|
||||||
|
const items = localStorage.getItem(key)
|
||||||
|
|
||||||
|
if (!items) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(items)
|
||||||
|
|
||||||
|
if (!Array.isArray(parsed) || !parsed.every(isHistoryItem)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHistoryItem(x: unknown): x is HistoryItem {
|
||||||
|
if (typeof x !== 'object' || x === null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'createdAt' in x && 'hash' in x
|
||||||
|
}
|
||||||
|
|
||||||
|
export function determineHistoryName(hash: string, indexDocument?: string | null): string {
|
||||||
|
if (indexDocument === 'index.html') {
|
||||||
|
return `Website ${shortenHash(hash, 4)}`
|
||||||
|
} else if (indexDocument) {
|
||||||
|
return indexDocument
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Folder ${shortenHash(hash, 4)}`
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 102 KiB |