Compare commits

...

37 Commits

Author SHA1 Message Date
bee-worker e3d03ed4d1 chore: release 0.11.0 (#263) 2021-12-14 14:22:49 +01:00
nugaon 153b007387 fix(build): bee-dashboard component building (#267)
* fix(build): lib

* ci: build component check

* fix(ci): typeo npm run

* ci: add types build check
2021-12-14 14:18:39 +01:00
nugaon 2a13da1a6c feat: modularisation (#244)
* chore: gitignore for lib directory

* build: packageing for webpack lib build

* build: webpack config

* feat: expose App component with beeApiUrl parameter

* build: tsconfig for library build

* build: main property of package json for tsc build

* refactor: rename beeUrl option to beeApiUrl

* refactor: manange config class instead of process.env calls

* build: babelrc config

* build: babel plugins and presets for webpack build

* chore: serve.js chmod

* build(refactor): webpack build

* refactor: number notation

* chore: webpack and package config change

* build: add babel preset-env

* chore: prepare script also builds component lib

* feat: typegen

* revert: set back prepare command

* build: assets loader config

* feat: beeDebugApiUrl

* refactor: move test files to the test folder because of typegen

* feat: locked api settings

* chore: depcheck ignores

* chore: types check script

* ci: check types

* ci: publish with library

* chore: add webpack as devDep

* chore: locked semver

* chore: remove debug logging

* style: webpack config

* chore: react and react-dom as dependency

* chore: package-lock

* fix: clean package-lock init

* refactor: fix versions in package.json
2021-12-09 11:12:45 +01:00
bee-worker 1a3e58c89b chore: release 0.10.0 (#262) 2021-12-07 20:06:05 +01:00
Cafe137 3ef1ad9574 feat: add website and folder upload and download (#260)
* feat: add website and folder upload and download

* feat: download-share-upload navigation

* fix: check for files length in hasIndexDocument

* fix: change router dependency

* refactor: switch to @ethersphere/manfest-js

* fix: hide previews on dropzone, fix spinner align, hide 0 size display

* feat: add upload and download history

* refactor: change drag and drop text

* feat: make history ux better

* refactor: improve code based on review

* build: add missing react-router dependency

* ci: remove beeload

* revert(ci): remove beeload

This reverts commit 4ce6cb0045a2d9aea3047ab395d214d8d368c532.
2021-12-07 16:06:21 +01:00
Adam Uhlíř dec812be45 ci: upload build to testnet (#259) 2021-12-02 15:29:23 +01:00
bee-worker d399a5c556 chore: release 0.9.0 (#249) 2021-11-25 15:20:26 +01:00
Cafe137 59dd1a3c81 chore: bump bee-js to 3.0.0 (#258) 2021-11-25 15:10:36 +01:00
Cafe137 635621b04a feat: improve upload flow (#240)
* feat: separate flow for folder and file uploads

* feat: add basic index document detection

* feat(wip): separate preview step

* fix: fix kb and mb units

* feat: add post upload summary, add some styling

* feat: upload flow

* fix: change element order and add conditional rendering

* refactor: remove unused variables for now

* fix: put back stamp creation to stamp page

* refactor: rework postage stamps and grid

* feat: add website and folder icons

* feat: add asset preview to download flow, add file icon

* feat: add basic design to postage stamp selection dialog

* feat: add web icon, shorten stamp in preview

* feat: extract swarm hash in download flow

* fix: extract swarmbutton and solve icon hover and focus color

* fix: always show buy button on stamp page

* refactor: downgrade

* refactor: speed up icon transition

* style: improve download buttons

* style: change [back to upload] icon

* style: add spacing before swarm gateway text

* style: post upload summary spacing

* refactor: drop verticalspacing and use box

* refactor: merge icons to one component

* refactor: use conditions instead of weird assignment

* docs: explain filter(x => x)

* refactor: generalize capacity

* refactor: avoid passing arrow functions

* refactor: get rid of PaperGridContainer and Container

* fix: fix hover color for postage stamps

* feat: add disabled and loading state to buttons

* fix: make drag and drop work for websites

* feat: handle folders and non existing hashes

* fix: provide empty default value to select to avoid console warning

* style: remove body2 font variants

* fix: remove typo

* feat: disable folder upload, add website upload

* fix: disable showPreviews to avoid flickering

* feat(temp): remove folder upload

* fix: remove stuck focus on buttons even after rendering different buttons

* style: merge hover and focus styles, fix safari text wrap issue

* style: remove dropbox outline in safari
2021-11-25 09:54:03 +01:00
Cafe137 82cf6d9c01 chore: bump bee-js to 2.1.1 (#257)
* chore: bump bee-js to 2.1.1

* docs: update supported bee version
2021-11-24 15:37:33 +01:00
Cafe137 3bb00771d6 feat: move postage stamp operations to bee debug api (#256) 2021-11-24 15:35:37 +01:00
Attila Gazso b354ef724b build: change test server address (#255) 2021-11-23 14:21:24 +01:00
Cafe137 844383bea7 feat: enable setting devMode from queryParams (#254)
* feat: enable setting devMode from queryParams

* docs: update dev mode docs

* docs: move dev mode comment

* docs: move dev mode comment
2021-11-23 13:39:47 +01:00
Cafe137 49350b0570 feat: add dev mode flag (#246)
* feat: add dev mode flag

* docs: add REACT_APP_DEV_MODE fixme comment

* feat: also ignore chequebook status in dev mode

* fix: print undefined overlay as empty string (#248)

* docs: add dev mode to readme

* docs: revert autoformat

Co-authored-by: Attila Gazso <agazso@gmail.com>
2021-11-19 14:31:36 +01:00
Vojtech Simetka 7fdf38bba1 ci: preview pr on swarm (#225)
* ci: add preview in bee action

* chore: set Bee API and Bee Debug API
2021-11-08 13:38:23 +01:00
Vojtech Simetka 7883d053ed ci: enable depcheck and fix dependency and linter issues (#233)
* ci: enable depcheck and fix dependency and linter issues

* chore: lock dependency versions

* chore: update dependencies to latest working ones

* chore: fix deprecation createMuiTheme

* chore: revert notistack to v1
2021-11-08 13:33:12 +01:00
Vojtech Simetka 15b4b0e561 ci: check supported bee version (#231) 2021-11-07 14:43:54 +01:00
fossabot c1a219c2e2 chore: add license scan report and status (#232)
Signed off by: fossabot <badges@fossa.com>
2021-11-05 13:56:10 +01:00
bee-worker 643f3b24db chore: release 0.8.0 (#205)
* chore: release 0.8.0

* chore: update changelog

Co-authored-by: Vojtech Simetka <vojtech@simetka.cz>
2021-10-20 19:43:39 +02:00
Vojtech Simetka 605054895d feat: updated readme to new bee version and UI examples with new design (#230) 2021-10-20 16:24:43 +02:00
Vojtech Simetka d5649dc8c6 chore(deps): update to bee-js 2.1.0 to support 2.0.0 (#229) 2021-10-14 15:59:25 +02:00
Vojtech Simetka cc5e778f89 fix: typo in population text (#228) 2021-10-13 13:20:06 +02:00
Vojtech Simetka f11bbd5008 fix: unknown routes should point to info page (#227) 2021-10-13 11:38:09 +02:00
Vojtech Simetka b4c9d9e018 fix: size of the troubleshoot component button (#226) 2021-10-13 11:37:59 +02:00
Vojtech Simetka 6c3f6c1019 fix: hover state style of ListItems which are clickable to be in line with other buttons (#223)
* fix: hover state style of ListItems which are clickable to be in line with other buttons

* fix: hover state of the SideBarStatus button
2021-10-11 10:07:31 +02:00
Vojtech Simetka 83c6d13417 fix: style of the update bee version button (#222)
* fix: style of the update bee version button

* chore: remove unused import
2021-10-11 10:07:06 +02:00
Vojtech Simetka 93af7f35a3 feat: files page updated to latest design (#218)
* feat: altered the design of the tabs and redid the download tab

* feat: redesign the upload file

* fix: styles of tabs on hover

* fix: display troubleshoot component when the status of the node is not OK

* fix: when removing the file, remove the reference upload reference as well

* fix: on inputs the label should not be selectable

* feat: add placeholder to inputs and make the label non-selectable

* refactor: improved the readability of the upload file component

* chore: removed PeerDetail component

* fix: replaced "batch" with (postage) "stamp" for clarity

* refactor: address PR review comments

* feat: disable the download button if there is no value
2021-10-08 13:34:55 +02:00
Attila Gazso 03265687ad fix: used label in postage stamp list (#220) 2021-10-08 08:38:12 +02:00
Vojtech Simetka f241b2fc5f feat: updated the postage stamps page design (#217)
* feat: updated the settings page design

* chore: removed unused dependencies
2021-10-07 12:22:46 +02:00
Vojtech Simetka 32e5ea9e56 feat: updated the settings page design (#215) 2021-10-07 10:47:51 +02:00
Vojtech Simetka b666cd2657 feat: status page redesign (#214)
* feat: initial rewrite without status indicators

* feat: status icon as a component and add to the node setup

* feat: added input list item component

* feat: improved the topology status info

* fix: disabled state of the buttons

* chore: removed unused components

* chore: remove debug console log

* fix: deposit modal helper text
2021-10-06 18:38:54 +02:00
Vojtech Simetka ecbc116475 feat: update design of the accounting page (#209)
* feat: update design of the accounting page, fixed the worsed graphical offenders

* chore: button alignment

* chore: removed unused dependency

* chore: buttons are underneath the action

* feat: refactored the peers table to be in line with the new design

* feat: add total uncashed amount and sorting for the peers

* feat: action buttons are now properly aligned

* chore: typo in comment
2021-10-05 12:59:08 +02:00
Attila Gazso e7188f4a35 fix: wording in chequebook setup (#211)
* fix: wording in chequebook setup

* fix: code linting

Co-authored-by: Vojtech Simetka <vojtech@simetka.cz>
2021-10-04 17:06:53 +02:00
Vojtech Simetka b69e368f69 feat: updated the settings page to be a bit more consistent with the rest of the design (#208) 2021-10-04 12:26:34 +02:00
Vojtech Simetka 57f5a73f3a feat: info page redesign (#207)
* feat: info page redesign

* style: headers and nesting for expandable lists

* style: body typography

* chore: bee version check

* feat: key list item component

* style: hover state for the key displaying component

* style: left border on expanded items

* fix: word break and splitting for hexstrings divisible by 6

* chore: moved the styles to classes

* style: tooltips now have arrow

* feat: indicate value has been copied

* feat: removed the connectivity table in favor of info page

* feat: added health tooltips for connectivity

* refactor: simplified the topology into single component

* fix: spacing between the bee version and the update button/chip

* fix: spacing on devices not supporting rowGap
2021-10-04 12:26:13 +02:00
Vojtech Simetka c4c7d9619d chore(deps): update bee-js to 2.0.0 (#210) 2021-10-04 12:25:41 +02:00
Vojtech Simetka c4c1573263 feat: troubleshooting component (#204)
* feat: troubleshooting component and general layout improvements

* style: background color, links and button

* chore: disable ripples and rounded corners on buttons

* fix: spacing on the troubleshooting card
2021-09-13 12:25:01 +02:00
110 changed files with 20455 additions and 8500 deletions
+50
View File
@@ -0,0 +1,50 @@
'use strict'
module.exports = function (api) {
const targets = '>1% and not ie 11 and not dead'
api.cache(true)
api.cacheDirectory = true
return {
presets: [
'@babel/preset-typescript',
[
'@babel/preset-env',
{
targets,
modules: false,
}
],
['@babel/preset-react', {runtime: 'automatic' }]
],
plugins: [
[
"babel-plugin-tsconfig-paths",
{
"relative": true,
"extensions": [
".js",
".jsx",
".ts",
".tsx",
".es",
".es6",
".mjs"
],
"rootDir": ".",
"tsconfig": "tsconfig.lib.json",
}
],
"@babel/plugin-proposal-numeric-separator",
"syntax-dynamic-import",
'@babel/plugin-proposal-class-properties',
[
'@babel/plugin-transform-runtime',
{
helpers: false,
regenerator: true
}
]
]
}
}
+19
View File
@@ -0,0 +1,19 @@
{
"ignores": [
"@types/jest",
"@commitlint/config-conventional",
"@types/react-router",
"@babel/core",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-runtime",
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript",
"babel-loader",
"babel-plugin-syntax-dynamic-import",
"babel-plugin-tsconfig-paths",
"file-loader",
"ts-node",
"webpack-cli"
]
}
+3 -1
View File
@@ -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,
+33
View File
@@ -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,33 @@ jobs:
env: env:
CI: true CI: true
- name: Types check
run: npm run check:types
- name: Types build
run: npm run compile:types
- 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: Build Component
run: npm run build:component
- 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
+2
View File
@@ -15,6 +15,8 @@ jobs:
node-version: 14 node-version: 14
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- run: npm ci - run: npm ci
- run: npm run compile:types
- run: npm build:component
- run: npm publish --access public - run: npm publish --access public
env: env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
+1
View File
@@ -10,6 +10,7 @@
# production # production
/build /build
/lib
# misc # misc
.DS_Store .DS_Store
+56
View File
@@ -1,5 +1,61 @@
# Changelog # Changelog
## [0.11.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.10.0...v0.11.0) (2021-12-14)
### Features
* modularisation ([#244](https://www.github.com/ethersphere/bee-dashboard/issues/244)) ([2a13da1](https://www.github.com/ethersphere/bee-dashboard/commit/2a13da1a6c5925946d22666a84f975cec87df115))
### Bug Fixes
* **build:** bee-dashboard component building ([#267](https://www.github.com/ethersphere/bee-dashboard/issues/267)) ([153b007](https://www.github.com/ethersphere/bee-dashboard/commit/153b007387618e34e1d5dc7fd82d49722783e757))
## [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)
+27 -12
View File
@@ -3,28 +3,33 @@
[![](https://img.shields.io/badge/made%20by-Swarm-blue.svg?style=flat-square)](https://swarm.ethereum.org/) [![](https://img.shields.io/badge/made%20by-Swarm-blue.svg?style=flat-square)](https://swarm.ethereum.org/)
[![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme) [![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme)
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fethersphere%2Fbee-dashboard.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fethersphere%2Fbee-dashboard?ref=badge_shield)
![](https://img.shields.io/badge/npm-%3E%3D6.0.0-orange.svg?style=flat-square) ![](https://img.shields.io/badge/npm-%3E%3D6.0.0-orange.svg?style=flat-square)
![](https://img.shields.io/badge/Node.js-%3E%3D10.0.0-orange.svg?style=flat-square) ![](https://img.shields.io/badge/Node.js-%3E%3D10.0.0-orange.svg?style=flat-square)
> An app which helps users to setup their Bee node and do actions like cash out cheques, 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).
![Status page](/ui_samples/status.png) ![Status page](/ui_samples/info.png)
| Node Setup | Browse & Upload Files | Accounting | Peers | Settings |
|-------|---------|-------|----------|------|
| ![Setup](/ui_samples/node_setup.png) | ![Files](/ui_samples/file_upload.png) | ![Accounting](/ui_samples/accounting.png) | ![Peers](/ui_samples/peers.png) | ![Settings](/ui_samples/settings.png) |
| Node Setup | Upload Files | Download Content | Accounting | Postage Stamps |
| ------------------------------------ | -------------------------------------- | ------------------------------------------ | ----------------------------------------- | ---------------------------------------- |
| ![Setup](/ui_samples/node_setup.png) | ![Upload](/ui_samples/file_upload.png) | ![Download](/ui_samples/file_download.png) | ![Accounting](/ui_samples/accounting.png) | ![Peers](/ui_samples/postage_stamps.png) |
## Table of Contents ## Table of Contents
- [Install](#install) - [Install](#install)
- [Usage](#usage) - [Usage](#usage)
- [Terminal](#terminal) - [Terminal](#terminal)
- [Docker](#docker) - [Docker](#docker)
- [Contribute](#contribute) - [Contribute](#contribute)
- [Development](#development) - [Development](#development)
- [Maintainers](#maintainers) - [Maintainers](#maintainers)
@@ -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)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fethersphere%2Fbee-dashboard.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fethersphere%2Fbee-dashboard?ref=badge_large)
+17028 -6661
View File
File diff suppressed because it is too large Load Diff
+69 -39
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ethersphere/bee-dashboard", "name": "@ethersphere/bee-dashboard",
"version": "0.7.0", "version": "0.11.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",
@@ -15,6 +15,8 @@
"bin": { "bin": {
"bee-dashboard": "./serve.js" "bee-dashboard": "./serve.js"
}, },
"main": "lib/App.js",
"types": "lib/src/App.d.ts",
"bugs": { "bugs": {
"url": "https://github.com/ethersphere/bee-dashboard/issues/" "url": "https://github.com/ethersphere/bee-dashboard/issues/"
}, },
@@ -24,72 +26,100 @@
"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", "@babel/core": "7.16.0",
"@testing-library/react": "11.2.6", "@babel/plugin-proposal-class-properties": "7.16.0",
"@testing-library/user-event": "13.1.5", "@babel/plugin-transform-runtime": "7.16.4",
"@types/jest": "26.0.22", "@babel/preset-env": "7.16.4",
"@types/node": "14.14.41", "@babel/preset-react": "7.16.0",
"@types/qrcode.react": "1.0.1", "@babel/preset-typescript": "7.16.0",
"@types/react": "17.0.3", "@commitlint/config-conventional": "14.1.0",
"@types/react-copy-to-clipboard": "5.0.0", "@testing-library/jest-dom": "5.15.0",
"@types/react-dom": "17.0.3", "@testing-library/react": "12.1.2",
"@types/react-syntax-highlighter": "13.5.0", "@types/file-saver": "2.0.4",
"@types/semver": "7.3.6", "@types/jest": "27.0.2",
"eslint": "7.32.0", "@types/qrcode.react": "1.0.2",
"eslint-config-prettier": "8.3.0", "@types/react": "17.0.34",
"eslint-plugin-jest": "24.4.0", "@types/react-copy-to-clipboard": "5.0.2",
"eslint-plugin-prettier": "3.4.1", "@types/react-dom": "17.0.11",
"eslint-plugin-react": "7.24.0", "@types/react-router": "5.1.17",
"prettier": "2.3.2", "@types/react-router-dom": "5.3.2",
"@types/react-syntax-highlighter": "13.5.2",
"@types/semver": "7.3.9",
"@typescript-eslint/eslint-plugin": "4.33.0",
"@typescript-eslint/parser": "4.33.0",
"babel-eslint": "10.1.0",
"babel-loader": "8.1.0",
"babel-plugin-syntax-dynamic-import": "6.18.0",
"babel-plugin-tsconfig-paths": "1.0.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",
"file-loader": "6.2.0",
"prettier": "2.4.1",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"typescript": "4.2.4", "ts-node": "^10.4.0",
"web-vitals": "1.1.1" "typescript": "4.4.4",
"web-vitals": "2.1.2",
"webpack": "4.44.2",
"webpack-cli": "^4.9.1"
},
"peerDependencies": {
"react": ">= 17.0.2",
"react-dom": ">= 17.0.2"
}, },
"scripts": { "scripts": {
"prepare": "npm run build", "prepare": "npm run build",
"start": "react-scripts start", "start": "react-scripts start",
"build": "react-scripts build", "build": "react-scripts build",
"build:component": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' webpack --mode=production",
"compile:types": "tsc --project tsconfig.lib.json --emitDeclarationOnly --declaration",
"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\"",
"check:types": "tsc --project tsconfig.lib.json"
}, },
"files": [ "files": [
"build", "build",
"serve.js" "serve.js"
], ],
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": { "browserslist": {
"production": [ "production": [
">0.2%", ">0.2%",
Regular → Executable
View File
+31 -24
View File
@@ -1,37 +1,44 @@
import { ReactElement } from 'react' import CssBaseline from '@material-ui/core/CssBaseline'
import { ThemeProvider } from '@material-ui/core/styles'
import { SnackbarProvider } from 'notistack'
import React, { 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 => ( interface Props {
beeApiUrl?: string
beeDebugApiUrl?: string
lockedApiSettings?: boolean
}
const App = ({ beeApiUrl, beeDebugApiUrl, lockedApiSettings }: Props): ReactElement => (
<div className="App"> <div className="App">
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<SettingsProvider> <SettingsProvider beeApiUrl={beeApiUrl} beeDebugApiUrl={beeDebugApiUrl} lockedApiSettings={lockedApiSettings}>
<BeeProvider> <BeeProvider>
<StampsProvider> <StampsProvider>
<PlatformProvider> <FileProvider>
<SnackbarProvider> <PlatformProvider>
<Router> <SnackbarProvider>
<> <Router>
<CssBaseline /> <>
<Dashboard> <CssBaseline />
<BaseRouter /> <Dashboard>
</Dashboard> <BaseRouter />
</> </Dashboard>
</Router> </>
</SnackbarProvider> </Router>
</PlatformProvider> </SnackbarProvider>
</PlatformProvider>
</FileProvider>
</StampsProvider> </StampsProvider>
</BeeProvider> </BeeProvider>
</SettingsProvider> </SettingsProvider>
+6 -4
View File
@@ -3,10 +3,10 @@ import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import { Alert, AlertTitle } from '@material-ui/lab' import { Alert, AlertTitle } from '@material-ui/lab'
import { ReactElement } from 'react' import { ReactElement } from 'react'
const LIMIT = 100_000_000 // 100 megabytes const LIMIT = 100000000 // 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>
+22
View File
@@ -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>
)
}
+3 -2
View File
@@ -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>
-50
View File
@@ -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>
)
}
+5 -7
View File
@@ -1,9 +1,9 @@
import { Typography } from '@material-ui/core/' import { Typography } from '@material-ui/core/'
import QRCodeModal from './QRCodeModal'
import ClipboardCopy from './ClipboardCopy'
import Identicon from 'react-identicons'
import { ReactElement } from 'react' import { ReactElement } from 'react'
import Identicon from 'react-identicons'
import { config } from '../config'
import ClipboardCopy from './ClipboardCopy'
import QRCodeModal from './QRCodeModal'
interface Props { interface Props {
address: string | undefined address: string | undefined
@@ -36,9 +36,7 @@ export default function EthereumAddress(props: Props): ReactElement {
} }
: { marginRight: '7px' } : { marginRight: '7px' }
} }
href={`${process.env.REACT_APP_BLOCKCHAIN_EXPLORER_URL}/${props.transaction ? 'tx' : 'address'}/${ href={`${config.BLOCKCHAIN_EXPLORER_URL}/${props.transaction ? 'tx' : 'address'}/${props.address}`}
props.address
}`}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
-59
View File
@@ -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
+59
View File
@@ -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>
)
}
+82
View File
@@ -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>
)
}
+54
View File
@@ -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>
)
}
+153
View File
@@ -0,0 +1,153 @@
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
locked?: boolean
}
export default function ExpandableListItemKey({
label,
value,
onConfirm,
onChange,
confirmLabel,
confirmLabelDisabled,
expandedOnly,
helperText,
placeholder,
mapperFn,
locked,
}: 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 && !locked && (
<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
hidden={locked}
/>
</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>
</>
)
}
+115
View File
@@ -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>
)
}
+106
View File
@@ -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>
)
}
+32
View File
@@ -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>
)
}
+30
View File
@@ -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} />
}
+37
View File
@@ -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>
)
}
+41
View File
@@ -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>
)
}
+10
View File
@@ -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>
)
}
-24
View File
@@ -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>
)
}
+20 -16
View File
@@ -1,16 +1,15 @@
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 { config } from '../config'
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 +18,7 @@ const navBarItems = [
}, },
{ {
label: 'Files', label: 'Files',
path: ROUTES.FILES, path: ROUTES.UPLOAD,
icon: FileText, icon: FileText,
}, },
{ {
@@ -32,11 +31,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 +38,8 @@ const navBarItems = [
}, },
] ]
const drawerWidth = 300
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
createStyles({ createStyles({
root: { root: {
@@ -52,6 +48,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 +90,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}>
@@ -108,7 +112,7 @@ export default function SideBar(): ReactElement {
</List> </List>
<Divider className={classes.divider} /> <Divider className={classes.divider} />
<List> <List>
<MUILink href={process.env.REACT_APP_BEE_DOCS_HOST} target="_blank" className={classes.link}> <MUILink href={config.BEE_DOCS_HOST} target="_blank" className={classes.link}>
<SideBarItem <SideBarItem
iconStart={<BookOpen className={classes.icon} />} iconStart={<BookOpen className={classes.icon} />}
iconEnd={<OpenInNewSharp className={classes.iconSmall} />} iconEnd={<OpenInNewSharp className={classes.iconSmall} />}
+6 -12
View File
@@ -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>}
-43
View File
@@ -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>
)
}
+28
View File
@@ -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',
}}
/>
)
}
+31
View File
@@ -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>
}
+66
View File
@@ -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>
)
}
+22 -22
View File
@@ -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) =>
root: { createStyles({
flexGrow: 1, root: {
}, 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>
{values.map(({ component }, idx) => ( <div className={classes.content}>
<TabPanel key={idx} value={v} index={idx}> {values.map(({ component }, idx) => (
{component} <TabPanel key={idx} value={v} index={idx}>
</TabPanel> {component}
))} </TabPanel>
))}
</div>
</div> </div>
) )
} }
-41
View File
@@ -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>
)
}
+21 -44
View File
@@ -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
+52 -36
View File
@@ -1,48 +1,64 @@
import { Button, Grid, Link as MuiLink, Typography } from '@material-ui/core/'
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import type { ReactElement } from 'react' import type { ReactElement } from 'react'
import { Activity } from 'react-feather'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { config } from '../config'
import { makeStyles } from '@material-ui/core/styles'
import { Card, CardContent, Typography } from '@material-ui/core/'
import { ROUTES } from '../routes' import { ROUTES } from '../routes'
const useStyles = makeStyles({ const useStyles = makeStyles((theme: Theme) =>
root: { createStyles({
flexGrow: 1, root: {
marginTop: '20px', height: '100%',
}, },
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{' '}
Swarm Bee Docs <MuiLink href={config.BEE_DOCS_HOST} target="_blank" rel="noreferrer">
</a> Swarm Bee Docs
</strong> </MuiLink>{' '}
</div> or ask for support on the{' '}
<MuiLink href={config.BEE_DISCORD_HOST} target="_blank" rel="noreferrer">
<div style={{ marginBottom: '20px', textAlign: 'center' }}> Ethereum Swarm Discord
<p style={{ marginTop: '50px' }}> </MuiLink>
Still not working? Drop us a message on the Ethereum Swarm{' '} .
<a href={process.env.REACT_APP_BEE_DISCORD_HOST} target="_blank" rel="noreferrer"> </Typography>
Discord </Grid>
</a> <Grid item className={classes.content}>
</p> <Typography align="center">
</div> <Button
</CardContent> component={Link}
</Card> variant="contained"
startIcon={<Activity className={classes.icon} />}
to={ROUTES.STATUS}
>
Check node status
</Button>
</Typography>
</Grid>
</Grid>
) )
} }
+8 -5
View File
@@ -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">
+29
View File
@@ -0,0 +1,29 @@
function getProcessEnv(key: string): string | undefined | false {
return typeof process === 'object' && process.env[key]
}
class Config {
public readonly BEE_API_HOST: string
public readonly BEE_DEBUG_API_HOST: string
public readonly BLOCKCHAIN_EXPLORER_URL: string
public readonly BEE_DOCS_HOST: string
public readonly BEE_DISCORD_HOST: string
public readonly GITHUB_REPO_URL: string
constructor() {
this.BEE_API_HOST =
sessionStorage.getItem('api_host') || getProcessEnv('REACT_APP_BEE_HOST') || 'http://localhost:1633'
this.BEE_DEBUG_API_HOST =
sessionStorage.getItem('debug_api_host') || getProcessEnv('REACT_APP_BEE_DEBUG_HOST') || 'http://localhost:1635'
this.BLOCKCHAIN_EXPLORER_URL =
getProcessEnv('REACT_APP_BLOCKCHAIN_EXPLORER_URL') || 'https://blockscout.com/xdai/mainnet'
this.BEE_DOCS_HOST = getProcessEnv('REACT_APP_BEE_DOCS_HOST') || 'https://docs.ethswarm.org/docs/'
this.BEE_DISCORD_HOST = getProcessEnv('REACT_APP_BEE_DISCORD_HOST') || 'https://discord.gg/eKr9XPv7'
this.GITHUB_REPO_URL =
getProcessEnv('REACT_APP_BEE_GITHUB_REPO_URL') || 'https://api.github.com/repos/ethersphere/bee'
}
}
export const config = new Config()
export default config
+3 -1
View File
@@ -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')
+6 -5
View File
@@ -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')
+18 -6
View File
@@ -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,
} }
} }
+3 -2
View File
@@ -1,5 +1,6 @@
import { useState, useEffect } from 'react'
import axios from 'axios' import axios from 'axios'
import { useEffect, useState } from 'react'
import { config } from '../config'
export interface LatestBeeReleaseHook { export interface LatestBeeReleaseHook {
latestBeeRelease: LatestBeeRelease | null latestBeeRelease: LatestBeeRelease | null
@@ -14,7 +15,7 @@ export const useLatestBeeRelease = (): LatestBeeReleaseHook => {
useEffect(() => { useEffect(() => {
axios axios
.get(`${process.env.REACT_APP_BEE_GITHUB_REPO_URL}/releases/latest`) .get(`${config.GITHUB_REPO_URL}/releases/latest`)
.then(res => { .then(res => {
setLatestBeeRelease(res.data) setLatestBeeRelease(res.data)
}) })
+16 -17
View File
@@ -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 />
<ErrorBoundary> <Container className={classes.content}>
<main className={classes.content}> <ErrorBoundary>
<AlertVersion /> <>
{isLoading ? ( <AlertVersion />
<Container style={{ textAlign: 'center', padding: '50px' }}> {isLoading ? (
<CircularProgress /> <div style={{ textAlign: 'center', width: '100%' }}>
</Container> <CircularProgress />
) : ( </div>
props.children ) : (
)} props.children
</main> )}
</ErrorBoundary> </>
</ErrorBoundary>
</Container>
</div> </div>
) )
} }
-94
View File
@@ -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
-94
View File
@@ -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
+52
View File
@@ -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>
)
}
+33 -26
View File
@@ -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>
) )
} }
+10
View File
@@ -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>
}
+102
View File
@@ -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>
)
}
+24
View File
@@ -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>
</>
)
}
+79 -53
View File
@@ -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}
<IconButton placeholder="e.g. 31fb0362b1a42536134c86bc58b97ac0244e5c6630beec3e27c2d1cecb38c605"
href={`${apiUrl}/bzz/${referenceInput}`} expandedOnly
target="_blank" mapperFn={value => recognizeSwarmHash(value)}
disabled={referenceError !== null || !referenceInput} />
className={classes.iconButton} <History title="Download History" localStorageKey={HISTORY_KEYS.DOWNLOAD_HISTORY} />
aria-label="download"
>
<Search />
</IconButton>
</Paper>
{referenceError && <FormHelperText error>{referenceError.message}</FormHelperText>}
</> </>
) )
} }
+32
View File
@@ -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>
)
}
+41
View File
@@ -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>
)
}
+4 -8
View File
@@ -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>
+121
View File
@@ -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}
/>
</>
)
}
+21
View File
@@ -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>
)
}
+66 -88
View File
@@ -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} />} ) : null}
onDelete={() => {} /* eslint-disable-line*/} </>
variant="outlined"
/>
</small>
<SelectStamp stamps={stamps} selectedStamp={selectedStamp} setSelected={setSelectedStamp} />
</div>
)}
{!selectedStamp && <CreatePostageStamp />}
<Button disabled={!file && isUploadingFile && !selectedStamp} onClick={() => uploadFile()}>
Upload
</Button>
{file && <UploadSizeAlert file={file} />}
{isUploadingFile && (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress />
</Container>
)}
{uploadReference && (
<div style={{ marginBottom: '15px', display: 'flex' }}>
<span>{uploadReference}</span>
<ClipboardCopy value={uploadReference} />
</div>
)}
</div>
</div>
</div>
) )
} }
+69
View File
@@ -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}
</>
)
}
+147
View File
@@ -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>
</>
)
}
+17
View File
@@ -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} />
</>
)
}
-32
View File
@@ -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>
)
}
-113
View File
@@ -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
+38 -27
View File
@@ -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>
{nodeAddresses && chequebookAddress && ( {` ${latestUserVersion || '-'} `}
<EthereumAddressCard nodeAddresses={nodeAddresses} chequebookAddress={chequebookAddress} /> <Button size="small" variant="outlined" href={latestBeeVersionUrl} target="_blank">
)} {isLatestBeeVersion ? 'latest' : 'update'}
</Button>
</div>
}
/>
<ExpandableListItemKey label="Public key" value={nodeAddresses?.publicKey || ''} />
<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>
) )
} }
-95
View File
@@ -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
-21
View File
@@ -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} />
</>
)
}
+13 -63
View File
@@ -1,70 +1,20 @@
import React, { ReactElement, useState, useContext } from 'react' import { ReactElement, useContext } from 'react'
import { Paper, Container, TextField, Typography, Button } from '@material-ui/core' import ExpandableList from '../../components/ExpandableList'
import ExpandableListItemInput from '../../components/ExpandableListItemInput'
import { Context as SettingsContext } from '../../providers/Settings' import { Context as SettingsContext } from '../../providers/Settings'
export default function Settings(): ReactElement { export default function Settings(): ReactElement {
const { apiUrl, apiDebugUrl, setApiUrl, setDebugApiUrl } = useContext(SettingsContext) const { apiUrl, apiDebugUrl, setApiUrl, setDebugApiUrl, lockedApiSettings } = 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} locked={lockedApiSettings} />
<Typography variant="h4" gutterBottom> <ExpandableListItemInput
Settings label="Bee Debug API"
</Typography> value={apiDebugUrl}
<Paper> onConfirm={setDebugApiUrl}
<TextField locked={lockedApiSettings}
label="API Endpoint" />
style={{ margin: 0 }} </ExpandableList>
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>
) )
} }
+20 -30
View File
@@ -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>
)} )}
+20
View File
@@ -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>
)
}
+14 -39
View File
@@ -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 }) => ( </ExpandableList>
<TableRow key={batchID}>
<TableCell>
<div style={{ display: 'flex' }}>
<small>
<PeerDetailDrawer peerId={batchID} />
</small>
<ClipboardCopy value={batchID} />
</div>
</TableCell>
<TableCell className={classes.values}>{usageText}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
) )
} }
+23 -26
View File
@@ -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> }
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 <ExpandableListItemNote>
bridge it to the xDai network through the <a href="https://omni.xdaichain.com/bridge">omni bridge</a>. To {isOk ? (
pay the transaction fees, you will also need xDAI token. You can purchase DAI on the network and bridge it 'Your chequebook is deployed and funded'
to xDai network through the <a href="https://bridge.xdaichain.com/">xDai Bridge</a>. See the{' '} ) : (
<a href="https://www.xdaichain.com/#xdai-stable-chain">official xDai website</a> for more information. <>
</span> Your chequebook is either not deployed or funded. To run the node you will need xDAI and xBZZ on the xDai
</div> network. You may need to aquire BZZ (e.g. <a href="https://bzz.exchange/">bzz.exchange</a>) and bridge it to
the xDai network through the <a href="https://omni.xdaichain.com/bridge">omni bridge</a>. To pay the
transaction fees, you will also need xDAI token. You can purchase DAI on the network and bridge it to xDai
network through the <a href="https://bridge.xdaichain.com/">xDai Bridge</a>. See the{' '}
<a href="https://www.xdaichain.com/#xdai-stable-chain">official xDai website</a> for more information.
</>
)} )}
</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,99 +1,86 @@
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' }}> <ol>
<AccordionSummary expandIcon={<ExpandMoreSharp />} aria-controls="panel1a-content" id="panel1a-header"> <li>Check the status of your node by running the below command to see if your node is running.</li>
<Typography>Troubleshoot</Typography> <CodeBlockTabs showLineNumbers linux={`sudo systemctl status bee`} mac={`brew services list`} />
</AccordionSummary> <li>
<AccordionDetails> If your node is running, check your firewall settings to make sure that port 1635 (or your custom
<Typography component="div"> specified port) is bound to localhost. If your node is not running try executing the below command to
<ol> start your bee node
<li>Check the status of your node by running the below command to see if your node is running.</li> </li>
<CodeBlockTabs showLineNumbers linux={`sudo systemctl status bee`} mac={`brew services list`} /> <MuiAlert
<li> style={{ marginTop: '10px', marginBottom: '10px' }}
If your node is running, check your firewall settings to make sure that port 1635 (or your custom elevation={6}
specified port) is bound to localhost. If your node is not running try executing the below command variant="filled"
to start your bee node severity="error"
</li> >
<MuiAlert Your debug node API should never be completely open to the internet. If you want to connect remotely,
style={{ marginTop: '10px', marginBottom: '10px' }} make sure your firewall settings are set to only allow specific trusted IP addresses and block all
elevation={6} other ports. A simple google search for &quot;what is my ip&quot; will show you your computers public
variant="filled" IP address to allow.
severity="error" </MuiAlert>
> <CodeBlockTabs
Your debug node API should never be completely open to the internet. If you want to connect showLineNumbers
remotely, make sure your firewall settings are set to only allow specific trusted IP addresses and linux={`sudo systemctl start bee`}
block all other ports. A simple google search for &quot;what is my ip&quot; will show you your mac={`brew services start swarm-bee`}
computers public IP address to allow. />
</MuiAlert> <li>Run the commands to validate your node is running and see the log output.</li>
<CodeBlockTabs <CodeBlockTabs
showLineNumbers showLineNumbers
linux={`sudo systemctl start bee`} linux={`sudo systemctl status bee \njournalctl --lines=100 --follow --unit bee`}
mac={`brew services start swarm-bee`} mac={`brew services list \ntail -f /usr/local/var/log/swarm-bee/bee.log`}
/> />
<li>Run the commands to validate your node is running and see the log output.</li> <li>
<CodeBlockTabs Lastly, check your nodes configuration settings to validate the debug API is enabled and the Cross
showLineNumbers Origin Resource Sharing (CORS) setting is configured to allow your host. Config parameter{' '}
linux={`sudo systemctl status bee \njournalctl --lines=100 --follow --unit bee`} <strong>debug-api-enable</strong> must be set to <strong>true</strong> and{' '}
mac={`brew services list \ntail -f /usr/local/var/log/swarm-bee/bee.log`} <strong>cors-allowed-origins</strong> must be set to your host domain or IP (you can also use the
/> wildcard <code>{"cors-allowed-origins: ['*']"}</code>). If edits are made to the configuration run the
<li> restart command below for changes to take effect.
Lastly, check your nodes configuration settings to validate the debug API is enabled and the Cross </li>
Origin Resource Sharing (CORS) setting is configured to allow your host. Config parameter{' '} <CodeBlockTabs
<strong>debug-api-enable</strong> must be set to <strong>true</strong> and{' '} showLineNumbers
<strong>cors-allowed-origins</strong> must be set to your host domain or IP (you can also use the linux={`sudo vi /etc/bee/bee.yaml\nsudo systemctl restart bee`}
wildcard <code>{"cors-allowed-origins: ['*']"}</code>). If edits are made to the configuration run mac={`sudo vi /usr/local/etc/swarm-bee/bee.yaml \nbrew services restart swarm-bee`}
the restart command below for changes to take effect. />
</li> </ol>
<CodeBlockTabs }
showLineNumbers />
linux={`sudo vi /etc/bee/bee.yaml\nsudo systemctl restart bee`} </ExpandableList>
mac={`sudo vi /usr/local/etc/swarm-bee/bee.yaml \nbrew services restart swarm-bee`} )}
/> </ExpandableList>
</ol>
</Typography>
</AccordionDetails>
</Accordion>
</Typography>
</div>
</div>
) )
} }
@@ -1,34 +1,42 @@
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)
export default function EthereumConnectionCheck({ isOk, nodeAddresses }: Props): ReactElement | null { const isOk = status.blockchainConnection
if (isOk) {
return (
<div>
<Typography variant="subtitle1" gutterBottom>
Node Address
</Typography>
<EthereumAddress address={nodeAddresses?.ethereum} />
</div>
)
}
return ( return (
<p> <ExpandableList
Your Bee node must have access to the xDai blockchain, so that it can interact and deploy your chequebook label={
contract. You can run{' '} <>
<a href="https://www.xdaichain.com/" rel="noreferrer" target="_blank"> <StatusIcon isOk={isOk} isLoading={isLoading} /> Connection to Blockchain
your own xDai node </>
</a> }
, or use a provider instead - we recommend{' '} >
<a href="https://getblock.io/" rel="noreferrer" target="_blank"> <ExpandableListItemNote>
Getblock {isOk ? (
</a> 'Your node is connected to the xDai blockchain'
. By default, Bee expects a local node at http://localhost:8545. To use a provider instead, simply change the{' '} ) : (
<strong>swap-endpoint</strong> in your configuration file. <>
</p> Your Bee node must have access to the xDai blockchain, so that it can interact and deploy your chequebook
contract. You can run{' '}
<a href="https://www.xdaichain.com/" rel="noreferrer" target="_blank">
your own xDai node
</a>
, or use a provider instead - we recommend{' '}
<a href="https://getblock.io/" rel="noreferrer" target="_blank">
Getblock
</a>
. By default, Bee expects a local node at http://localhost:8545. To use a provider instead, simply change
the <strong>swap-endpoint</strong> in your configuration file.
</>
)}
</ExpandableListItemNote>
{nodeAddresses?.ethereum && <ExpandableListItemKey label="Ethereum Address" value={nodeAddresses?.ethereum} />}
</ExpandableList>
) )
} }
@@ -1,61 +1,61 @@
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 && ( {isOk
<Typography component="div" variant="body2" gutterBottom style={{ margin: '15px' }}> ? 'The connection to the Bee nodes API has been successful'
We cannot connect to your nodes API at <Typography variant="button">{apiUrl}</Typography>. Please check the : 'Could not connect to your Bee nodes API. Please check the troubleshoot below on how you may resolve it.'}
following to troubleshoot your issue. </ExpandableListItemNote>
<Accordion style={{ marginTop: '20px' }}> <ExpandableListItemInput label="Bee API" value={apiUrl} onConfirm={setApiUrl} />
<AccordionSummary expandIcon={<ExpandMoreSharp />} aria-controls="panel1a-content" id="panel1a-header"> {!isOk && (
<Typography>Troubleshoot</Typography> <ExpandableList level={1} label="Troubleshoot">
</AccordionSummary> <ExpandableListItem
<AccordionDetails> label={
<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 1633 (or your custom
If your node is running, check your firewall settings to make sure that port 1633 (or your custom specified port) is exposed to the internet. If your node is not running try executing the below
specified port) is exposed to the internet. If your node is not running try executing the below command to start your bee node
command to start your bee node </li>
</li> <CodeBlockTabs
<CodeBlockTabs showLineNumbers
showLineNumbers linux={`sudo systemctl start bee`}
linux={`sudo systemctl start bee`} mac={`brew services start swarm-bee`}
mac={`brew services start swarm-bee`} />
/> <li>Run the commands to validate your node is running and see the log output.</li>
<li>Run the commands to validate your node is running and see the log output.</li> <CodeBlockTabs
<CodeBlockTabs showLineNumbers
showLineNumbers linux={`sudo systemctl status bee \njournalctl --lines=100 --follow --unit bee`}
linux={`sudo systemctl status bee \njournalctl --lines=100 --follow --unit bee`} mac={`brew services 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> </ExpandableList>
</Accordion> )}
</Typography> </ExpandableList>
)}
</div>
</div>
) )
} }
+24 -38
View File
@@ -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)
export default function PeerConnection({ isOk, topology }: Props): ReactElement | null { const isOk = status.topology
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 (
<>
<span>You are connected to peers!</span>
{peers}
</>
)
}
return ( return (
<> <ExpandableList
<span>Your node is not connected to any peers</span> label={
{peers} <>
</> <StatusIcon isOk={isOk} isLoading={isLoading} /> Connection to 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>
<TopologyStats topology={topology} />
</ExpandableList>
) )
} }
+41 -54
View File
@@ -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 (
<>
<span>You are running the latest version of Bee</span>
{version}
</>
)
}
// Old version or not connected to bee debug API
return ( return (
<> <ExpandableList
<span> label={
Your Bee version is out of date. Please update to the{' '} <>
<a href={latestUrl} rel="noreferrer" target="_blank"> <StatusIcon isOk={isOk} isLoading={isLoading} /> Bee Version
latest </>
</a>{' '} }
before continuing. Rerun the installation script below to upgrade. Reference the docs for help with updating.{' '} >
<a href="https://docs.ethswarm.org/docs/installation/manual#upgrading-bee" rel="noreferrer" target="_blank"> <ExpandableListItemNote>
Docs {isOk ? (
</a> 'You are running the latest version of Bee.'
</span> ) : (
<CodeBlockTabs <>
showLineNumbers Your Bee version is out of date. Please update to the{' '}
linux={`bee version\nwget https://github.com/ethersphere/bee/releases/download/${latestVersion}/bee_${latestVersion}_amd64.deb\nsudo dpkg -i bee_${latestVersion}_amd64.deb`} <a href={latestBeeVersionUrl} rel="noreferrer" target="_blank">
mac={`bee version\nbrew tap ethersphere/tap\nbrew install swarm-bee\nbrew services start swarm-bee`} latest
/> </a>{' '}
{version} 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">
Docs
</a>
.
<CodeBlockTabs
showLineNumbers
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`}
/>
</>
)}
</ExpandableListItemNote>
<ExpandableListItem label="Your Version" value={latestUserVersion || '-'} />
<ExpandableListItem label="Latest Version" value={latestPublishedVersion || '-'} />
</ExpandableList>
) )
} }
+9 -140
View File
@@ -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}> <div>
<Typography variant="h5" gutterBottom> <DebugConnectionCheck />
Node Setup <VersionCheck />
</Typography> <EthereumConnectionCheck />
<Stepper nonLinear activeStep={activeStep} orientation="vertical"> <ChequebookDeployFund />
{steps.map(({ label, isOk, component }, index) => ( <NodeConnectionCheck />
<Step key={label}> <PeerConnection />
<StepLabel </div>
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>
<Button disabled={activeStep === 0} onClick={handleBack} className={classes.button}>
Back
</Button>
<Button variant="contained" color="primary" onClick={handleNext} className={classes.button}>
{index < steps.length - 1 ? 'Next' : 'Finish'}
</Button>
</div>
</div>
</StepContent>
</Step>
))}
</Stepper>
</Paper>
) )
} }
+21 -17
View File
@@ -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
+25
View File
@@ -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>
}
+27 -6
View File
@@ -1,5 +1,6 @@
import { createContext, ReactChild, ReactElement, useState, useEffect } from 'react'
import { Bee, BeeDebug } from '@ethersphere/bee-js' import { Bee, BeeDebug } from '@ethersphere/bee-js'
import { createContext, ReactChild, ReactElement, useEffect, useState } from 'react'
import { config } from '../config'
interface ContextInterface { interface ContextInterface {
apiUrl: string apiUrl: string
@@ -8,16 +9,17 @@ interface ContextInterface {
beeDebugApi: BeeDebug | null beeDebugApi: BeeDebug | null
setApiUrl: (url: string) => void setApiUrl: (url: string) => void
setDebugApiUrl: (url: string) => void setDebugApiUrl: (url: string) => void
lockedApiSettings: boolean
} }
const initialValues: ContextInterface = { const initialValues: ContextInterface = {
apiUrl: sessionStorage.getItem('api_host') || process.env.REACT_APP_BEE_HOST || 'http://localhost:1633', apiUrl: config.BEE_API_HOST,
apiDebugUrl: apiDebugUrl: config.BEE_DEBUG_API_HOST,
sessionStorage.getItem('debug_api_host') || process.env.REACT_APP_BEE_DEBUG_HOST || 'http://localhost:1635',
beeApi: null, beeApi: null,
beeDebugApi: null, beeDebugApi: null,
setApiUrl: () => {}, // eslint-disable-line setApiUrl: () => {}, // eslint-disable-line
setDebugApiUrl: () => {}, // eslint-disable-line setDebugApiUrl: () => {}, // eslint-disable-line
lockedApiSettings: false,
} }
export const Context = createContext<ContextInterface>(initialValues) export const Context = createContext<ContextInterface>(initialValues)
@@ -25,13 +27,22 @@ export const Consumer = Context.Consumer
interface Props { interface Props {
children: ReactChild children: ReactChild
beeApiUrl?: string
beeDebugApiUrl?: string
lockedApiSettings?: boolean
} }
export function Provider({ children }: Props): ReactElement { export function Provider({
children,
beeApiUrl,
beeDebugApiUrl,
lockedApiSettings: extLockedApiSettings,
}: Props): ReactElement {
const [apiUrl, setApiUrl] = useState<string>(initialValues.apiUrl) const [apiUrl, setApiUrl] = useState<string>(initialValues.apiUrl)
const [apiDebugUrl, setDebugApiUrl] = useState<string>(initialValues.apiDebugUrl) const [apiDebugUrl, setDebugApiUrl] = useState<string>(initialValues.apiDebugUrl)
const [beeApi, setBeeApi] = useState<Bee | null>(null) const [beeApi, setBeeApi] = useState<Bee | null>(null)
const [beeDebugApi, setBeeDebugApi] = useState<BeeDebug | null>(null) const [beeDebugApi, setBeeDebugApi] = useState<BeeDebug | null>(null)
const [lockedApiSettings] = useState<boolean>(Boolean(extLockedApiSettings))
useEffect(() => { useEffect(() => {
try { try {
@@ -42,6 +53,14 @@ export function Provider({ children }: Props): ReactElement {
} }
}, [apiUrl]) }, [apiUrl])
useEffect(() => {
if (beeApiUrl) setApiUrl(beeApiUrl)
}, [beeApiUrl])
useEffect(() => {
if (beeDebugApiUrl) setDebugApiUrl(beeDebugApiUrl)
}, [beeDebugApiUrl])
useEffect(() => { useEffect(() => {
try { try {
setBeeDebugApi(new BeeDebug(apiDebugUrl)) setBeeDebugApi(new BeeDebug(apiDebugUrl))
@@ -52,7 +71,9 @@ export function Provider({ children }: Props): ReactElement {
}, [apiDebugUrl]) }, [apiDebugUrl])
return ( return (
<Context.Provider value={{ apiUrl, apiDebugUrl, beeApi, beeDebugApi, setApiUrl, setDebugApiUrl }}> <Context.Provider
value={{ apiUrl, apiDebugUrl, beeApi, beeDebugApi, setApiUrl, setDebugApiUrl, lockedApiSettings }}
>
{children} {children}
</Context.Provider> </Context.Provider>
) )
+6 -6
View File
@@ -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 }}>
+16 -12
View File
@@ -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>
) )
+155 -29
View File
@@ -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',
},
}, },
}) })
+24
View File
@@ -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
}
}
}
+5
View File
@@ -0,0 +1,5 @@
export function getPrettyDateString(date: Date): string {
const string = date.toString()
return string.split('GMT')[0].trim()
}
+73
View File
@@ -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]
}
+3
View File
@@ -0,0 +1,3 @@
export function shortenHash(hash: string, sliceLength = 8): string {
return `${hash.slice(0, sliceLength)}[…]${hash.slice(-sliceLength)}`
}
-60
View File
@@ -1,60 +0,0 @@
import BigNumber from 'bignumber.js'
import { isInteger, makeBigNumber } from './index'
describe('utils', () => {
describe('isInteger', () => {
const correctValues = [
BigInt(0),
BigInt(1),
BigInt(-1),
new BigNumber('1'),
new BigNumber('0'),
new BigNumber('-1'),
]
const wrongValues = ['1', new BigNumber('-0.1'), new BigNumber(NaN), new BigNumber(Infinity)]
correctValues.forEach(v => {
test(`testing ${v}`, () => {
expect(isInteger(v)).toBe(true)
})
})
wrongValues.forEach(v => {
test(`testing ${v}`, () => {
expect(isInteger(v)).toBe(false)
})
})
})
describe('makeBigNumber', () => {
const correctValues = [
BigInt(0),
BigInt(1),
BigInt(-1),
'1',
'0',
'-1',
'0.1',
new BigNumber('1'),
new BigNumber('0'),
new BigNumber('-1'),
0,
1,
-1,
]
const wrongValues = [new Function()]
correctValues.forEach(v => {
test(`testing ${v}`, () => {
expect(BigNumber.isBigNumber(makeBigNumber(v))).toBe(true)
})
})
wrongValues.forEach(v => {
test(`testing ${v}`, () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect(() => makeBigNumber(v as unknown as any)).toThrow()
})
})
})
})
+6
View File
@@ -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
}
+70
View File
@@ -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)}`
}
+1 -1
View File
@@ -1,5 +1,5 @@
const OPTIMAL_CONNECTED_PEERS = 200 const OPTIMAL_CONNECTED_PEERS = 200
const OPTIMAL_POPULATION = 100_000 const OPTIMAL_POPULATION = 100000
const OPTIMAL_DEPTH = 12 const OPTIMAL_DEPTH = 12
interface Threshold { interface Threshold {
+57
View File
@@ -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)
})
})
+106
View File
@@ -0,0 +1,106 @@
import BigNumber from 'bignumber.js'
import { extractSwarmHash, isInteger, makeBigNumber } from './index'
describe('utils', () => {
describe('isInteger', () => {
const correctValues = [
BigInt(0),
BigInt(1),
BigInt(-1),
new BigNumber('1'),
new BigNumber('0'),
new BigNumber('-1'),
]
const wrongValues = ['1', new BigNumber('-0.1'), new BigNumber(NaN), new BigNumber(Infinity)]
correctValues.forEach(v => {
test(`testing ${v}`, () => {
expect(isInteger(v)).toBe(true)
})
})
wrongValues.forEach(v => {
test(`testing ${v}`, () => {
expect(isInteger(v)).toBe(false)
})
})
})
describe('makeBigNumber', () => {
const correctValues = [
BigInt(0),
BigInt(1),
BigInt(-1),
'1',
'0',
'-1',
'0.1',
new BigNumber('1'),
new BigNumber('0'),
new BigNumber('-1'),
0,
1,
-1,
]
const wrongValues = [new Function()] // eslint-disable-line no-new-func
correctValues.forEach(v => {
test(`testing ${v}`, () => {
expect(BigNumber.isBigNumber(makeBigNumber(v))).toBe(true)
})
})
wrongValues.forEach(v => {
test(`testing ${v}`, () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect(() => makeBigNumber(v as unknown as any)).toThrow()
})
})
})
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)
})
})
})
+28
View File
@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"outDir": "lib",
"rootDirs": ["src"],
"typeRoots": ["./src/@types", "node_modules/@types"]
},
"include": [
"src"
]
}

Some files were not shown because too many files have changed in this diff Show More