Compare commits

...

58 Commits

Author SHA1 Message Date
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
bee-worker cda1d4bbb1 chore: release 0.7.0 (#189)
* chore: release 0.7.0

* chore: update the changelog

Co-authored-by: Vojtech Simetka <vojtech@simetka.cz>
2021-08-31 14:12:36 +02:00
Vojtech Simetka e8e707a9c4 chore(deps): update to bee-js 1.2.1 (#197) 2021-08-31 12:44:18 +02:00
Vojtech Simetka 28bbdfb2f6 style: sidebar styling (#194)
* style: sidebar styling

* style: hover and selected colors

* chore: split the sidebar items into separate component

* style: pretty much finished except for status button

* feat: sidebar status button as a separate component

* chore: spacing definition

* style: size of the status text

* fix: hiden navigation on small height screens
2021-08-31 11:27:38 +02:00
Cafe137 630791cd75 docs: add intended version section (#195) 2021-08-30 17:43:49 +02:00
Vojtech Simetka f316a5caf4 chore(deps): fix dependencies version (#192)
* chore(deps): fix dependencies version

* chore: use styles through core
2021-08-30 11:17:46 +02:00
Vojtech Simetka 929f44f206 refactor: routes as constants (#191) 2021-08-26 12:57:44 +02:00
Vojtech Simetka d1720e243c feat: removed dark theme and theme switching (#190) 2021-08-26 09:40:13 +02:00
Vojtech Simetka 3ce83d987d refactor: move the router and layout directly to app.tsx (#188) 2021-08-26 07:25:04 +02:00
Vojtech Simetka 02a7bff733 feat: separate info and status page (#183)
* feat: separate info and status page

* chore: remove the detection of step to expand
2021-08-25 17:41:17 +02:00
Vojtech Simetka 766fe96d1c ci: enable code lint check (#186)
* ci: enable code lint check

* chore(deps): update and fix eslint & prettier versions
2021-08-25 16:14:09 +02:00
bee-worker 1f8f890ff7 chore: release 0.6.0 (#167) 2021-08-24 18:58:13 +02:00
Vojtech Simetka f9ea9948f0 chore: update to latest bee version (#182) 2021-08-24 13:42:19 +02:00
Vojtech Simetka 2b120e44ca fix: remove nested ternary operator and simplify ping peer functionality (#181)
* fix: remove nested ternary operator and simplify ping peer functionality

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

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

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

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

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

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

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

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

* refactor: clean up code

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

* fix: fix off by one bug in retry logic

* docs: add jsdocs to new utility functions

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

* chore: rename enums to pascal case

* chore: fixed typo
2021-08-11 19:46:09 +02:00
Adam Uhlíř f90778d338 docs: update maintainers (#164) 2021-08-11 08:17:46 +02:00
122 changed files with 21528 additions and 10834 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"ignores": ["@types/jest", "@commitlint/config-conventional", "@types/react-router"]
}
+8 -1
View File
@@ -1,9 +1,16 @@
{ {
"settings": {
"react": {
"version": "detect"
}
},
"extends": [ "extends": [
"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,
+28 -4
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:
@@ -42,10 +47,29 @@ jobs:
- name: Commit linting - name: Commit linting
uses: wagoid/commitlint-github-action@v2 uses: wagoid/commitlint-github-action@v2
# - name: Code linting - name: Code linting
# run: npm run lint:check run: npm run lint:check
# env: env:
# CI: true CI: true
- name: Dependency check
run: npm run depcheck
- name: Update supported Bee action
uses: ethersphere/update-supported-bee-action@v1
if: github.ref == 'refs/heads/master'
with:
token: ${{ secrets.REPO_GHA_PAT }}
- name: Build - name: Build
run: npm run build run: npm run build
- name: Create preview
uses: ethersphere/beeload-action@v1
with:
preview: 'true'
- name: Upload to testnet
uses: ethersphere/beeload-action@v1
with:
bee-url: https://api.gateway.testnet.ethswarm.org
+75
View File
@@ -1,5 +1,80 @@
# Changelog # Changelog
## [0.10.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.9.0...v0.10.0) (2021-12-07)
### Features
* add website and folder upload and download ([#260](https://www.github.com/ethersphere/bee-dashboard/issues/260)) ([3ef1ad9](https://www.github.com/ethersphere/bee-dashboard/commit/3ef1ad9574c9193f83d8a1447fddb79266c1a4f4))
## [0.9.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.8.0...v0.9.0) (2021-11-25)
### Features
* add dev mode flag ([#246](https://www.github.com/ethersphere/bee-dashboard/issues/246)) ([49350b0](https://www.github.com/ethersphere/bee-dashboard/commit/49350b05709053ecfbc4fc98f8b1df1aa0345e95))
* enable setting devMode from queryParams ([#254](https://www.github.com/ethersphere/bee-dashboard/issues/254)) ([844383b](https://www.github.com/ethersphere/bee-dashboard/commit/844383bea7b2118232a74ac23c9e9a38fc47d3fd))
* improve upload flow ([#240](https://www.github.com/ethersphere/bee-dashboard/issues/240)) ([635621b](https://www.github.com/ethersphere/bee-dashboard/commit/635621b04aea7124a99d00f9e31a86983063f5ce))
* move postage stamp operations to bee debug api ([#256](https://www.github.com/ethersphere/bee-dashboard/issues/256)) ([3bb0077](https://www.github.com/ethersphere/bee-dashboard/commit/3bb00771d684ad93fd7acd921b648574013aec5c))
## [0.8.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.7.0...v0.8.0) (2021-10-20)
In this version we are adding support for the bee release 1.2.0. The app also went through a graphical redesign. More to come soon!
### Features
* support for bee 1.2.0
* update files page design ([#218](https://www.github.com/ethersphere/bee-dashboard/issues/218)) ([93af7f3](https://www.github.com/ethersphere/bee-dashboard/commit/93af7f35a371d54864c068be6e1d8a70092afe28))
* update info page design ([#207](https://www.github.com/ethersphere/bee-dashboard/issues/207)) ([57f5a73](https://www.github.com/ethersphere/bee-dashboard/commit/57f5a73f3a8d957bf967c51612dc09c802bb68dc))
* update status page design ([#214](https://www.github.com/ethersphere/bee-dashboard/issues/214)) ([b666cd2](https://www.github.com/ethersphere/bee-dashboard/commit/b666cd2657cf1003651c44b6b4fa5bdcf11e895f))
* update troubleshooting component design ([#204](https://www.github.com/ethersphere/bee-dashboard/issues/204)) ([c4c1573](https://www.github.com/ethersphere/bee-dashboard/commit/c4c1573263868b6dc8a863124e4aee824dceadbb))
* update accounting page design ([#209](https://www.github.com/ethersphere/bee-dashboard/issues/209)) ([ecbc116](https://www.github.com/ethersphere/bee-dashboard/commit/ecbc1164756de912d14ce44aa9b2c155dded6dac))
* update postage stamps page design ([#217](https://www.github.com/ethersphere/bee-dashboard/issues/217)) ([f241b2f](https://www.github.com/ethersphere/bee-dashboard/commit/f241b2fc5f6ec0741e275498ebef5a18ce710b81))
* update settings page design ([#215](https://www.github.com/ethersphere/bee-dashboard/issues/215)) ([32e5ea9](https://www.github.com/ethersphere/bee-dashboard/commit/32e5ea9e56fdf957b758ec714bb6a4fe1903082a))
### Bug Fixes
* hover state style of ListItems which are clickable to be in line with other buttons ([#223](https://www.github.com/ethersphere/bee-dashboard/issues/223)) ([6c3f6c1](https://www.github.com/ethersphere/bee-dashboard/commit/6c3f6c1019801267aa5e51002f6e21f769edc210))
* size of the troubleshoot component button ([#226](https://www.github.com/ethersphere/bee-dashboard/issues/226)) ([b4c9d9e](https://www.github.com/ethersphere/bee-dashboard/commit/b4c9d9e0182c4bee5ebb2d4e43e0aaad2aeb616b))
* style of the update bee version button ([#222](https://www.github.com/ethersphere/bee-dashboard/issues/222)) ([83c6d13](https://www.github.com/ethersphere/bee-dashboard/commit/83c6d1341790d664c7986dd2a816fe6a3b069e5c))
* typo in population text ([#228](https://www.github.com/ethersphere/bee-dashboard/issues/228)) ([cc5e778](https://www.github.com/ethersphere/bee-dashboard/commit/cc5e778f892b73b0b7ff5e0fa00c4816f3298ac7))
* unknown routes should point to info page ([#227](https://www.github.com/ethersphere/bee-dashboard/issues/227)) ([f11bbd5](https://www.github.com/ethersphere/bee-dashboard/commit/f11bbd5008a78ef7d5c73fc2758ee4e2dafae01e))
* used label in postage stamp list ([#220](https://www.github.com/ethersphere/bee-dashboard/issues/220)) ([0326568](https://www.github.com/ethersphere/bee-dashboard/commit/03265687ad630b0100da3134518b680327af1636))
* wording in chequebook setup ([#211](https://www.github.com/ethersphere/bee-dashboard/issues/211)) ([e7188f4](https://www.github.com/ethersphere/bee-dashboard/commit/e7188f4a35c85204eef6a01ae6f1e679d076180c))
## [0.7.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.6.0...v0.7.0) (2021-08-31)
### Features
* removed dark theme and theme switching ([#190](https://www.github.com/ethersphere/bee-dashboard/issues/190)) ([d1720e2](https://www.github.com/ethersphere/bee-dashboard/commit/d1720e243c4415d75763a229250fa20e3664290e))
* separate info and status page ([#183](https://www.github.com/ethersphere/bee-dashboard/issues/183)) ([02a7bff](https://www.github.com/ethersphere/bee-dashboard/commit/02a7bff733b7fac70c6a36f94e6ba1425854a0af))
* styling of the sidebar ([#194](https://github.com/ethersphere/bee-dashboard/pull/194))
### Bug Fixes
* bee 1.1.0 version reporting workaround ([#197](https://github.com/ethersphere/bee-dashboard/issues/197))
## [0.6.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.5.0...v0.6.0) (2021-08-24)
### Features
* add retry to accounting ([#166](https://www.github.com/ethersphere/bee-dashboard/issues/166)) ([a62243f](https://www.github.com/ethersphere/bee-dashboard/commit/a62243fe5c45b7dd9be6e92f82ebdf0b64bd8f0d))
* add tooltips and health indicator to peers ([#169](https://www.github.com/ethersphere/bee-dashboard/issues/169)) ([480f6dc](https://www.github.com/ethersphere/bee-dashboard/commit/480f6dc7f9c58a4aae87e0dea7082a4bd3dc900b))
* bee provider caching the state of the app and refreshing periodically ([#172](https://www.github.com/ethersphere/bee-dashboard/issues/172)) ([2624cf0](https://www.github.com/ethersphere/bee-dashboard/commit/2624cf04c939e87f025c1f4ff417808073742dab))
* changing API urls does not need the app refresh ([#173](https://www.github.com/ethersphere/bee-dashboard/issues/173)) ([d6d03bf](https://www.github.com/ethersphere/bee-dashboard/commit/d6d03bf7c6d2705de22f43825b85b32c2f0181fb))
* remove the last update component ([#179](https://www.github.com/ethersphere/bee-dashboard/issues/179)) ([56df3a2](https://www.github.com/ethersphere/bee-dashboard/commit/56df3a2561c3c00237b5d107eb054403af3012f8))
* synchronized platform tabs ([#165](https://www.github.com/ethersphere/bee-dashboard/issues/165)) ([ec42eaf](https://www.github.com/ethersphere/bee-dashboard/commit/ec42eafc2b768ba06649f628c733e8d3440fdcaf))
### Bug Fixes
* enum index for supported platforms ([#170](https://www.github.com/ethersphere/bee-dashboard/issues/170)) ([dcec6e0](https://www.github.com/ethersphere/bee-dashboard/commit/dcec6e01887465c74a68feede52b476791bbefa7))
* remove nested ternary operator and simplify ping peer functionality ([#181](https://www.github.com/ethersphere/bee-dashboard/issues/181)) ([2b120e4](https://www.github.com/ethersphere/bee-dashboard/commit/2b120e44ca5e01451cc43e362195c04587836a03))
## [0.5.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.4.0...v0.5.0) (2021-08-09) ## [0.5.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.4.0...v0.5.0) (2021-08-09)
+1 -1
View File
@@ -1 +1 @@
* nugaon vojtechsimetka * @Cafe137 @vojtechsimetka
+30 -11
View File
@@ -3,26 +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.**
![Status page](/ui_samples/status.png) This project is intended to be used with **Bee version <!-- SUPPORTED_BEE_START -->1.4.0-8fa696a8<!-- SUPPORTED_BEE_END -->**.
Using it with older or newer Bee versions is not recommended and may not work. Stay up to date by joining the
[official Discord](https://discord.gg/GU22h2utj6) and by keeping an eye on the
[releases tab](https://github.com/ethersphere/bee-dashboard/releases).
| Node Setup | Browse & Upload Files | Accounting | Peers | Settings | ![Status page](/ui_samples/info.png)
|-------|---------|-------|----------|------|
| ![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)
@@ -38,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
``` ```
@@ -74,19 +85,27 @@ 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
- [nugaon](https://github.com/nugaon)
- [vojtechsimetka](https://github.com/vojtechsimetka) - [vojtechsimetka](https://github.com/vojtechsimetka)
- [Cafe137](https://github.com/Cafe137)
See what "Maintainer" means [here](https://github.com/ethersphere/repo-maintainer).
## License ## License
[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)
+17317 -7656
View File
File diff suppressed because it is too large Load Diff
+58 -50
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ethersphere/bee-dashboard", "name": "@ethersphere/bee-dashboard",
"version": "0.5.0", "version": "0.10.0",
"description": "An app which helps users to setup their Bee node and do actions like cash out cheques", "description": "An app which helps users to setup their Bee node and do actions like cash out cheques",
"keywords": [ "keywords": [
"bee", "bee",
@@ -24,52 +24,65 @@
"url": "https://github.com/ethersphere/bee-dashboard.git" "url": "https://github.com/ethersphere/bee-dashboard.git"
}, },
"dependencies": { "dependencies": {
"@ethersphere/bee-js": "^1.0.0", "@ethersphere/bee-js": "3.0.0",
"@material-ui/core": "^4.11.4", "@ethersphere/manifest-js": "^1.0.0",
"@material-ui/icons": "^4.11.2", "@material-ui/core": "4.12.3",
"@material-ui/lab": "^4.0.0-alpha.57", "@material-ui/icons": "4.11.2",
"@types/react-router": "^5.1.13", "@material-ui/lab": "4.0.0-alpha.57",
"@types/react-router-dom": "^5.1.7", "axios": "0.24.0",
"axios": "^0.21.1", "bignumber.js": "9.0.1",
"bignumber.js": "^9.0.1", "file-saver": "^2.0.5",
"feather-icons": "^4.28.0", "formik": "2.2.9",
"formik": "^2.2.8", "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-dom": "^5.2.0", "react-router": "5.2.0",
"react-syntax-highlighter": "^15.4.3", "react-router-dom": "5.2.0",
"semver": "^7.3.2", "react-syntax-highlighter": "15.4.4",
"serve-handler": "^6.1.3" "semver": "7.3.5",
"serve-handler": "6.1.3"
}, },
"devDependencies": { "devDependencies": {
"@testing-library/jest-dom": "^5.12.0", "@commitlint/config-conventional": "14.1.0",
"@testing-library/react": "^11.2.6", "@testing-library/jest-dom": "5.15.0",
"@testing-library/user-event": "^13.1.5", "@testing-library/react": "12.1.2",
"@types/jest": "^26.0.22", "@types/file-saver": "^2.0.4",
"@types/node": "^14.14.41", "@types/jest": "27.0.2",
"@types/qrcode.react": "^1.0.1", "@types/qrcode.react": "1.0.2",
"@types/react": "^17.0.3", "@types/react": "17.0.34",
"@types/react-copy-to-clipboard": "^5.0.0", "@types/react-copy-to-clipboard": "5.0.2",
"@types/react-dom": "^17.0.3", "@types/react-dom": "17.0.11",
"@types/react-syntax-highlighter": "^13.5.0", "@types/react-router": "5.1.17",
"@types/semver": "^7.3.6", "@types/react-router-dom": "5.3.2",
"eslint": "^7.24.0", "@types/react-syntax-highlighter": "13.5.2",
"eslint-config-prettier": "^8.2.0", "@types/semver": "7.3.9",
"eslint-plugin-jest": "^24.3.5", "@typescript-eslint/eslint-plugin": "4.33.0",
"eslint-plugin-prettier": "^3.4.0", "@typescript-eslint/parser": "4.33.0",
"eslint-plugin-react": "^7.23.2", "babel-eslint": "10.1.0",
"prettier": "^2.2.1", "depcheck": "1.4.2",
"eslint": "7.24.0",
"eslint-config-prettier": "8.2.0",
"eslint-config-react-app": "6.0.0",
"eslint-plugin-flowtype": "5.10.0",
"eslint-plugin-import": "2.25.2",
"eslint-plugin-jest": "24.3.5",
"eslint-plugin-jsx-a11y": "6.4.1",
"eslint-plugin-prettier": "3.4.0",
"eslint-plugin-react": "7.23.2",
"eslint-plugin-react-hooks": "4.2.0",
"eslint-plugin-testing-library": "3.10.2",
"prettier": "2.4.1",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"typescript": "^4.2.4", "typescript": "4.4.4",
"web-vitals": "^1.1.1" "web-vitals": "2.1.2"
}, },
"scripts": { "scripts": {
"prepare": "npm run build", "prepare": "npm run build",
@@ -77,6 +90,7 @@
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test", "test": "react-scripts test",
"serve": "node ./serve.js", "serve": "node ./serve.js",
"depcheck": "depcheck .",
"lint": "eslint --fix \"src/**/*.ts\" \"src/**/*.tsx\" && prettier --write \"src/**/*.ts\" \"src/**/*.tsx\"", "lint": "eslint --fix \"src/**/*.ts\" \"src/**/*.tsx\" && prettier --write \"src/**/*.ts\" \"src/**/*.tsx\"",
"lint:check": "eslint \"src/**/*.ts\" \"src/**/*.tsx\" && prettier --check \"src/**/*.ts\" \"src/**/*.tsx\"" "lint:check": "eslint \"src/**/*.ts\" \"src/**/*.tsx\" && prettier --check \"src/**/*.ts\" \"src/**/*.tsx\""
}, },
@@ -84,12 +98,6 @@
"build", "build",
"serve.js" "serve.js"
], ],
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": { "browserslist": {
"production": [ "production": [
">0.2%", ">0.2%",
+1 -1
View File
@@ -10,6 +10,6 @@ declare module 'react-identicons' {
getColor?: () => string getColor?: () => string
} }
const Identicon = (props: Props): JSXElementConstructor => ReactNode const Identicon = (props: Props): JSXElementConstructor => ReactNode //eslint-disable-line @typescript-eslint/no-unused-vars
export default Identicon export default Identicon
} }
+37 -47
View File
@@ -1,53 +1,43 @@
import { ReactElement, useEffect, useState } from 'react' import CssBaseline from '@material-ui/core/CssBaseline'
import { ThemeProvider } from '@material-ui/core/styles'
import { SnackbarProvider } from 'notistack'
import { ReactElement } from 'react'
import { BrowserRouter as Router } from 'react-router-dom' import { BrowserRouter as Router } from 'react-router-dom'
import './App.css' import './App.css'
import Dashboard from './layout/Dashboard'
import { ThemeProvider } from '@material-ui/styles' import { Provider as BeeProvider } from './providers/Bee'
import CssBaseline from '@material-ui/core/CssBaseline' import { Provider as FileProvider } from './providers/File'
import { SnackbarProvider } from 'notistack' import { Provider as PlatformProvider } from './providers/Platform'
import { Provider as SettingsProvider } from './providers/Settings'
import BaseRouter from './routes/routes'
import { lightTheme, darkTheme } from './theme'
import { Provider as StampsProvider } from './providers/Stamps' import { Provider as StampsProvider } from './providers/Stamps'
import BaseRouter from './routes'
import { theme } from './theme'
const App = (): ReactElement => { const App = (): ReactElement => (
const [themeMode, toggleThemeMode] = useState('light') <div className="App">
<ThemeProvider theme={theme}>
useEffect(() => { <SettingsProvider>
const theme = localStorage.getItem('theme') <BeeProvider>
<StampsProvider>
if (theme) { <FileProvider>
toggleThemeMode(String(localStorage.getItem('theme'))) <PlatformProvider>
} else if (window?.matchMedia('(prefers-color-scheme: dark)')?.matches) { <SnackbarProvider>
toggleThemeMode('dark') <Router>
} <>
<CssBaseline />
window?.matchMedia('(prefers-color-scheme: dark)')?.addEventListener('change', e => { <Dashboard>
toggleThemeMode(e?.matches ? 'dark' : 'light') <BaseRouter />
}) </Dashboard>
</>
return () => </Router>
window?.matchMedia('(prefers-color-scheme: dark)')?.removeEventListener('change', e => { </SnackbarProvider>
toggleThemeMode(e?.matches ? 'dark' : 'light') </PlatformProvider>
}) </FileProvider>
}, []) </StampsProvider>
</BeeProvider>
return ( </SettingsProvider>
<div className="App"> </ThemeProvider>
<ThemeProvider theme={themeMode === 'light' ? lightTheme : darkTheme}> </div>
<StampsProvider> )
<SnackbarProvider>
<>
<CssBaseline />
<Router>
<BaseRouter />
</Router>
</>
</SnackbarProvider>
</StampsProvider>
</ThemeProvider>
</div>
)
}
export default App export default App
+11
View File
@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="172" height="30" viewBox="0 0 172 30">
<g fill="none" fill-rule="evenodd">
<g>
<g>
<path d="M0 0H172V30H0z" transform="translate(-64.000000, -64.000000) translate(64.000000, 64.000000)"/>
<path fill="#F9F9F9" fill-rule="nonzero" d="M94.26 21.338c1.242 0 2.168-.427 2.779-1.28.61-.853.916-2.118.916-3.796 0-1.677-.306-2.942-.916-3.795-.611-.854-1.537-1.28-2.778-1.28h-2.836v10.15h2.836zm-.057-1.062h-1.556V12.25h1.556c.814 0 1.427.28 1.84.843.411.562.617 1.357.617 2.385v1.57c0 1.028-.206 1.823-.618 2.386-.412.562-1.025.843-1.84.843zm6.767 1.062l.858-2.894h3.563l.858 2.894h1.294l-3.112-10.151h-1.614l-3.112 10.15h1.265zm4.13-3.956h-2.981l1.425-4.9h.131l1.425 4.9zm7.698 4.13c1.135 0 2.01-.266 2.625-.8.616-.533.924-1.27.924-2.21 0-.33-.044-.645-.131-.945-.087-.3-.235-.572-.444-.815-.208-.242-.482-.45-.821-.625-.34-.175-.757-.305-1.251-.393l-1.09-.189c-.718-.126-1.215-.332-1.491-.618-.277-.286-.415-.657-.415-1.112 0-.582.184-1.018.553-1.31.368-.29.897-.436 1.585-.436.63 0 1.144.117 1.541.35.398.232.737.533 1.018.901l.858-.742c-.339-.494-.787-.877-1.345-1.148-.557-.272-1.243-.408-2.058-.408-1.037 0-1.861.238-2.472.713-.61.475-.916 1.178-.916 2.109 0 .32.044.627.13.923.088.296.234.567.437.815.204.247.473.458.807.632.335.175.75.306 1.244.393l1.134.189c.698.116 1.188.313 1.469.589.281.276.422.662.422 1.156 0 .61-.194 1.086-.582 1.425-.388.34-.95.51-1.687.51-.61 0-1.137-.122-1.578-.364-.441-.243-.86-.611-1.258-1.106l-.887.728c.378.523.858.952 1.44 1.287.581.334 1.328.501 2.24.501zm7.262-.174v-4.61h3.956v4.61h1.221V11.187h-1.221v4.48h-3.956v-4.48h-1.222v10.15h1.222zm11.988 0c.388 0 .75-.075 1.084-.226.334-.15.625-.356.872-.618.248-.261.439-.572.575-.93.135-.36.203-.742.203-1.15 0-.707-.184-1.248-.552-1.62-.369-.374-.839-.629-1.411-.764v-.044c.465-.145.846-.393 1.142-.742.295-.349.443-.829.443-1.44 0-.794-.247-1.43-.741-1.904-.495-.476-1.178-.713-2.051-.713h-3.447v10.15h3.883zm-.567-5.745h-2.094V12.25h2.094c.514 0 .916.116 1.207.349.291.232.436.581.436 1.047v.567c0 .465-.145.812-.436 1.04-.29.227-.693.341-1.207.341zm.204 4.683h-2.298v-3.68h2.298c.562 0 1.003.13 1.323.386.32.257.48.643.48 1.156v.597c0 .523-.16.911-.48 1.163-.32.252-.761.378-1.323.378zm8.774 1.236c.62 0 1.154-.118 1.6-.356.446-.237.814-.58 1.105-1.025.29-.446.504-.994.64-1.644.136-.65.203-1.39.203-2.225 0-.824-.067-1.563-.203-2.217-.136-.655-.35-1.205-.64-1.651-.29-.446-.66-.788-1.105-1.025-.446-.238-.98-.357-1.6-.357-.62 0-1.154.12-1.6.357-.446.237-.814.579-1.105 1.025-.29.446-.504.996-.64 1.65-.136.655-.204 1.394-.204 2.218 0 .834.068 1.576.204 2.225.136.65.35 1.198.64 1.644.29.446.66.788 1.105 1.025.446.238.98.356 1.6.356zm0-1.061c-.407 0-.751-.08-1.033-.24-.28-.16-.513-.386-.698-.676-.184-.291-.317-.643-.4-1.055-.082-.412-.123-.875-.123-1.389v-1.658c0-.504.041-.964.123-1.381.083-.417.216-.77.4-1.062.185-.29.417-.516.698-.676.282-.16.626-.24 1.033-.24.407 0 .751.08 1.032.24.282.16.514.385.699.676.184.291.317.645.4 1.062.082.417.123.877.123 1.381v1.658c0 .514-.041.977-.124 1.39-.082.411-.215.763-.4 1.054-.184.29-.416.516-.698.676-.28.16-.625.24-1.032.24zm6.564.887l.858-2.894h3.563l.858 2.894h1.294l-3.112-10.151h-1.614l-3.113 10.15h1.266zm4.13-3.956h-2.981l1.425-4.9h.13l1.426 4.9zm5.895 3.956v-4.334h1.614l2.472 4.334h1.367l-2.588-4.392c.814-.078 1.434-.364 1.861-.858.427-.495.64-1.154.64-1.978 0-.921-.247-1.638-.742-2.152-.494-.514-1.212-.771-2.152-.771h-3.694v10.15h1.222zm2.443-5.366h-2.443v-3.723h2.443c.514 0 .914.128 1.2.385.286.257.429.623.429 1.098v.756c0 .475-.143.841-.43 1.098-.285.257-.685.386-1.2.386zm8.454 5.366c1.241 0 2.167-.427 2.778-1.28.61-.853.916-2.118.916-3.796 0-1.677-.305-2.942-.916-3.795-.611-.854-1.537-1.28-2.778-1.28h-2.836v10.15h2.836zm-.058-1.062h-1.556V12.25h1.556c.814 0 1.428.28 1.84.843.412.562.618 1.357.618 2.385v1.57c0 1.028-.206 1.823-.618 2.386-.412.562-1.026.843-1.84.843z" transform="translate(-64.000000, -64.000000) translate(64.000000, 64.000000)"/>
<path fill="#F9F9F9" d="M5.064 14.685l4.822 2.709v5.4L5.064 25.5l-4.82-2.706v-5.4l4.82-2.71zm11.034 0l4.822 2.709v5.4L16.098 25.5l-4.82-2.706v-5.4l4.82-2.71zm42.17-3.332v7.644l1.655 1.432.017-.014-1.187 1.327h-.019l-2.097-1.815-2.505 1.465c-.41.237-.876.362-1.35.357-.709-.005-1.392-.277-1.91-.762-.539-.503-.838-1.213-.824-1.95-.002-.974.52-1.874 1.367-2.356l5.05-2.895v-.644H50.55v-1.79h7.719zm-29.357.085c.802-.392 1.705-.529 2.588-.392.845.17 1.627.57 2.258 1.159.166.145.443.417.632.606l.122.121.08.08-1.178 1.336c.014-.017-.801-.773-.883-.841-.389-.367-.863-.627-1.383-.754-.539-.12-1.104-.022-1.573.27-.323.195-.508.552-.484.928.006.372.213.71.54.885.547.291 1.136.49 1.747.586.387.075.766.172 1.102.261.335.088.662.21.974.359.314.11.602.285.844.515.233.194.416.438.537.715.152.298.232.626.236.96.034.957-.403 1.87-1.17 2.444-.81.602-1.8.911-2.809.876-.379.002-.76-.036-1.131-.114-1.007-.247-1.925-.768-2.652-1.506-.05-.043-.608-.566-.608-.566l1.145-1.378c.246.23.486.462.741.677.313.303.655.574 1.02.813.913.518 2.035.518 2.948 0 .375-.227.596-.638.58-1.074.008-.364-.223-.687-.569-.799-.634-.27-1.298-.47-1.979-.59-.864-.152-1.684-.485-2.408-.978-.602-.433-.895-1.095-.895-2.025-.018-.85.365-1.657 1.03-2.183.186-.152.386-.282.598-.39zm17.705-.135c0-.022.006.006.006.006h1.922l-1.606 10.212h-3.152l-1.15-8.808h-.024l-1.164 8.804H38.3l-1.573-10.214h1.926l1.267 8.822h.024l1.114-8.822h3.157l1.109 8.822h.025zm33.488-.29c.703-.039 1.386.244 1.858.765.435.557.655 1.252.618 1.958v7.76h-1.89V14.08c-.014-.314-.113-.621-.288-.886-.212-.26-.54-.396-.873-.359-.362-.011-.708.145-.936.427-.272.41-.4.9-.357 1.391v6.854h-1.88v-7.337c.018-.353-.097-.7-.317-.976-.206-.241-.508-.373-.824-.359-.376-.022-.74.145-.966.448-.253.366-.379.808-.358 1.252v6.978h-1.876v-10.21h1.876v.946c.153-.354.401-.657.715-.877.38-.245.821-.37 1.272-.358.465-.02.923.124 1.296.404.304.244.535.57.664.937.424-.855 1.312-1.381 2.266-1.341zm-13.503-.011c.899-.011 1.763.349 2.387.996.66.62 1.031 1.488 1.023 2.393v.588h-1.764v-.588c0-.05-.003-.099-.008-.146-.08-.877-.859-1.519-1.735-1.436-.832.111-1.449.829-1.431 1.668v5.205h2.574v1.764H61.56v-1.764h1.687v-5.62l-1.697-1.538 1.178-1.315 1.195 1.096c.626-.846 1.625-1.332 2.677-1.303zm-10.136 4.89L52.32 18.27c-.288.158-.465.46-.46.789.01.496.41.894.905.905.166-.003.328-.052.468-.143h-.022l3.254-1.89v-2.04zM10.544 5.254l1.847 1.102v3.181l.634.354 2.347 1.318v2.15l-4.82 2.707-4.82-2.706v-5.4l4.812-2.706zm5.638-.755l2.568 1.44v2.886l-2.548 1.428-.02-.011-2.553-1.43V5.951l.005-.021L16.182 4.5z" transform="translate(-64.000000, -64.000000) translate(64.000000, 64.000000)"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

-101
View File
@@ -1,101 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 230.07104 58.680001"
version="1.1"
id="svg28566"
sodipodi:docname="swarm-logo.svg"
inkscape:version="1.0.1 (3bc2e81, 2020-09-07)"
width="230.07104"
height="58.68">
<metadata
id="metadata28570">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>Swarm Logo &amp;amp; Lettering 4</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1016"
id="namedview28568"
showgrid="false"
inkscape:zoom="3.1325"
inkscape:cx="123.33"
inkscape:cy="35.939998"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_2"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<defs
id="defs28540">
<style
id="style28538">.cls-1{fill:#fafafa;}.cls-2{fill:#242424;}</style>
</defs>
<title
id="title28542">Swarm Logo &amp;amp; Lettering 4</title>
<g
id="Layer_2"
data-name="Layer 2"
transform="translate(-76.67,-71.05)">
<path
class="cls-2"
d="m 206.24,90.06 -3.54,24.65 c 0,0 -0.06,0 -0.07,0 l -3.1,-24.65 c 0,0 0,0 0,0 h -8.82 l -3.11,24.65 c 0,0 -0.06,0 -0.07,0 l -3.54,-24.65 h -5.38 c 0,0 0,0 0,0 L 183,118.6 h 8.8 L 195.05,94 c 0,0 0.06,0 0.07,0 l 3.21,24.61 h 8.81 l 4.49,-28.53 h -5.37 c 0,0 -0.02,-0.08 -0.02,-0.02 z"
id="path28546" />
<path
class="cls-2"
d="m 305,91.39 a 6.52,6.52 0 0 0 -5.19,-2.14 6.74,6.74 0 0 0 -6.33,3.75 v 0 a 5.85,5.85 0 0 0 -1.86,-2.62 5.61,5.61 0 0 0 -3.62,-1.13 6.26,6.26 0 0 0 -3.55,1 5.78,5.78 0 0 0 -2,2.45 v -2.64 c 0,0 0,0 0,0 h -5.24 c 0,0 0,0 0,0 v 28.53 h 5.24 v -19.5 a 5.72,5.72 0 0 1 1,-3.5 3.14,3.14 0 0 1 2.7,-1.25 2.85,2.85 0 0 1 2.3,1 4.08,4.08 0 0 1 0.89,2.73 v 20.5 c 0,0 0,0 0,0 h 5.25 V 99.42 a 6.08,6.08 0 0 1 1,-3.89 3.22,3.22 0 0 1 2.61,-1.19 2.75,2.75 0 0 1 2.44,1 4.9,4.9 0 0 1 0.81,2.92 v 20.28 c 0,0 0,0 0,0 h 5.28 V 96.86 A 8.18,8.18 0 0 0 305,91.39 Z"
id="path28548" />
<path
class="cls-2"
d="m 243.47,115.52 -3.32,3.71 h -0.05 l -5.86,-5.07 c 0,0 0,0 0,0 l -7,4.09 a 7.38,7.38 0 0 1 -3.77,1 7.91,7.91 0 0 1 -5.34,-2.13 7.28,7.28 0 0 1 -2.3,-5.45 7.54,7.54 0 0 1 3.82,-6.58 L 233.76,97 c 0,0 0,0 0,0 v -1.8 h -16.53 c 0,0 0,0 0,0 v -5 c 0,0 0,0 0,0 h 21.57 v 21.36 c 0,0 0,0 0,0 l 4.62,4 z m -18.8,-1.66 9.09,-5.28 c 0,0 0,0 0,0 v -5.7 a 0.03,0 0 0 0 -0.06,0 l -11.58,6.65 a 2.46,2.46 0 0 0 -1.29,2.2 2.59,2.59 0 0 0 2.53,2.53 2.51,2.51 0 0 0 1.31,-0.4 z"
id="path28550" />
<path
class="cls-2"
d="m 268.75,92 a 9.1,9.1 0 0 0 -6.67,-2.78 9,9 0 0 0 -7.48,3.64 l -3.34,-3.06 a 0.025,0 0 0 0 -0.05,0 l -3.29,3.67 4.74,4.3 v 15.7 h -4.71 c 0,0 0,0 0,0 v 4.93 h 17 c 0,0 0,0 0,0 v -4.93 h -7.19 V 98.93 a 4.61,4.61 0 0 1 4,-4.66 4.45,4.45 0 0 1 4.87,4.42 v 1.64 c 0,0 0,0 0,0 h 4.93 c 0,0 0,0 0,0 V 98.69 A 9.1,9.1 0 0 0 268.75,92 Z"
id="path28552" />
<path
class="cls-2"
d="m 173.32,106.74 a 5.41,5.41 0 0 0 -1.5,-2 6.58,6.58 0 0 0 -2.36,-1.44 15.31,15.31 0 0 0 -2.72,-1 c -0.94,-0.25 -2,-0.52 -3.08,-0.73 a 15.43,15.43 0 0 1 -4.88,-1.64 2.85,2.85 0 0 1 -1.51,-2.47 2.81,2.81 0 0 1 1.35,-2.59 5.91,5.91 0 0 1 4.4,-0.76 8.68,8.68 0 0 1 3.86,2.11 c 0.23,0.19 2.51,2.3 2.47,2.35 l 3.29,-3.73 c 0,0 -1.58,-1.6 -2.33,-2.26 a 13,13 0 0 0 -6.31,-3.24 12.18,12.18 0 0 0 -7.23,1.1 9.58,9.58 0 0 0 -1.67,1.09 7.57,7.57 0 0 0 -2.88,6.1 c 0,2.6 0.82,4.45 2.5,5.66 a 17.33,17.33 0 0 0 6.73,2.73 25.41,25.41 0 0 1 5.53,1.65 2.29,2.29 0 0 1 1.59,2.23 3.36,3.36 0 0 1 -1.62,3 8.35,8.35 0 0 1 -8.24,0 19.32,19.32 0 0 1 -2.85,-2.27 c -0.71,-0.6 -1.38,-1.25 -2.07,-1.89 v 0 l -3.2,3.85 c 0,0 1.56,1.46 1.7,1.58 a 15.66,15.66 0 0 0 7.41,4.21 15.26,15.26 0 0 0 3.16,0.32 12.45,12.45 0 0 0 7.85,-2.45 8.17,8.17 0 0 0 3.27,-6.83 6.14,6.14 0 0 0 -0.66,-2.68 z"
id="path28554" />
<polygon
class="cls-2"
points="76.67,122.17 90.14,129.73 103.61,122.17 103.61,107.08 90.14,99.51 76.67,107.08 "
id="polygon28556" />
<polygon
class="cls-2"
points="121.2,71.05 114.08,75.05 114.07,75.11 114.07,83.1 121.2,87.1 121.26,87.13 128.38,83.14 128.38,75.08 "
id="polygon28558" />
<polygon
class="cls-2"
points="134.44,107.08 120.97,99.51 107.5,107.08 107.5,122.17 120.97,129.73 134.44,122.17 "
id="polygon28560" />
<polygon
class="cls-2"
points="105.45,73.16 92,80.72 92,95.81 105.47,103.37 118.94,95.81 118.94,89.8 112.38,86.12 110.61,85.13 110.61,83.1 110.61,76.24 "
id="polygon28562" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

-1
View File
@@ -1 +0,0 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 468 142" xml:space="preserve"><style>.st1,.st2,.st3,.st4{opacity:.45;enable-background:new}.st2,.st3,.st4{opacity:.6}.st3,.st4{opacity:.3}.st4{opacity:.8}</style><g id="XMLID_41_"><g id="XMLID_1_"><g id="Swarm_typeface"><g id="XMLID_42_" opacity=".8"><path id="XMLID_31_" d="M193.7 90c0 9.1-9.5 13.4-17.4 13.4-8.5 0-17.8-3.3-17.8-13.2 0-1.7.1-3 2.2-3 1 0 1.4 1.3 1.4 2.1 0 8.5 6.8 10.6 14.2 10.6 5.8 0 13.8-2.8 13.8-9.8 0-13-31-5.3-31-20.3 0-8.6 9.6-10.8 16.5-10.8 8.1 0 17.1 3 17.1 12.6 0 1-1 1.7-1.9 1.7-1.1 0-1.7-1-1.7-1.9-.3-2.1-.6-3.5-2.1-5.2-2.6-3-7.7-3.6-11.5-3.6-4.5 0-12.9 1-12.9 7.1.1 10.9 31.1 3.5 31.1 20.3z"/><path id="XMLID_30_" d="M264.4 60.9c0 .8-13.2 37.6-14.5 40.9-.3 1-1 1.1-1.9 1.1-.9 0-1.6-.1-1.9-1.1l-12.9-36-12.9 36c-.3 1-1 1.1-1.9 1.1-.9 0-1.7-.1-1.9-1.1-1.3-3.3-14.5-40.1-14.5-40.9 0-1 .8-1.8 1.8-1.8.8 0 1.4.4 1.7 1.1l13 36.4 13-36.4c.3-.9 1-1.1 1.8-1.1.9 0 1.6.3 1.9 1.1l12.9 36.4 13-36.4c.3-.8.9-1.1 1.7-1.1.8 0 1.6.8 1.6 1.8z"/><path id="XMLID_45_" d="M315.3 60.9v39.2c0 1-.8 1.8-1.8 1.8s-1.8-.8-1.8-1.8v-8.2c-3.6 6.6-10.3 11.2-18 11.2-12.1 0-20.7-10.8-20.7-22.4s8.6-22.4 20.7-22.4c7.7 0 14.4 4.6 18 11.2v-8.6c0-.9.8-1.8 1.8-1.8s1.8.9 1.8 1.8zm-4.5 19.8c0-9.6-6.9-18.8-17.1-18.8-10 0-17.1 9.1-17.1 18.8s7.1 18.8 17.1 18.8c10.2 0 17.1-9.2 17.1-18.8z"/><path id="XMLID_27_" d="M352.4 61.3c0 1.1-.6 1.8-1.7 1.9-10.5 1.6-15.3 10.2-15.3 20.2v17.2c0 1-.8 1.8-1.8 1.8-1.1 0-1.8-.8-1.8-1.8V61.4c0-1 .8-1.8 1.8-1.8 1.1 0 1.8.8 1.8 1.8v8c3-5 8.9-9.8 15.1-9.8.8 0 1.9.6 1.9 1.7z"/><path id="XMLID_2_" d="M430.6 77.7v23c0 1-.9 1.8-1.8 1.8-1 0-1.8-.8-1.8-1.8v-23c0-7.7-4.4-15.3-13-15.3-10.9 0-15 11.6-15 20.6v17.8c0 1-.9 1.8-1.8 1.8-1 0-1.8-.8-1.8-1.8v-23c0-7.7-4.4-15.3-13-15.3-10.9 0-15.4 8.6-15.1 20.1 0 .3.1.8 0 .9v17.4c0 1-.8 1.8-1.8 1.8s-1.8-.8-1.8-1.8V61.4c0-1 .8-1.8 1.8-1.8s1.8.8 1.8 1.8V68c3.1-5.5 8.6-9.1 15.1-9.1 7.3 0 13 4.6 15.3 11.5 3-6.7 8.8-11.5 16.2-11.5 10.6-.1 16.7 9.1 16.7 18.8z"/></g></g></g><g id="Swarm_Icon"><path id="XMLID_26_" class="st1" d="M121.3 27v12.4l-11.1-6.2V20.7z"/><path id="XMLID_25_" class="st2" d="M110.2 33.2v12.4l11.1-6.2V27z"/><path id="XMLID_24_" class="st3" d="M37.4 102.7l22.1 12.4 22.1-12.4-22.1-12.5z"/><path id="XMLID_23_" class="st1" d="M59.5 90.2v24.9l-22.1-12.4V77.8z"/><path id="XMLID_22_" class="st1" d="M81.6 77.8v24.9L59.5 90.2V65.4z"/><path id="XMLID_21_" class="st2" d="M59.5 90.2v24.9l22.1-12.4V77.8z"/><path id="XMLID_20_" class="st2" d="M37.4 77.8v24.9l22.1-12.5V65.4z"/><path id="XMLID_19_" class="st4" d="M84 48.4l11 6.2 11.1-6.2L95 42.2z"/><path id="XMLID_18_" class="st4" d="M37.4 77.8l22.1 12.4 22.1-12.4-22.1-12.4z"/><path id="XMLID_17_" class="st3" d="M86.9 102.7l22.1 12.4 22.1-12.4L109 90.2z"/><path id="XMLID_16_" class="st1" d="M109 90.2v24.9l-22.1-12.4V77.8z"/><path id="XMLID_15_" class="st1" d="M131.1 77.8v24.9L109 90.2V65.4z"/><path id="XMLID_14_" class="st2" d="M109 90.2v24.9l22.1-12.4V77.8z"/><path id="XMLID_13_" class="st2" d="M86.9 77.8v24.9L109 90.2V65.4z"/><path id="XMLID_12_" class="st4" d="M86.9 77.8L109 90.2l22.1-12.4L109 65.4z"/><path id="XMLID_11_" class="st2" d="M84 35.9v12.5l11-6.2V29.7z"/><path id="XMLID_10_" class="st3" d="M61.9 60.8L84 73.2l22.1-12.4L84 48.4z"/><path id="XMLID_9_" class="st1" d="M84 48.4v24.8L61.9 60.8V35.9z"/><path id="XMLID_8_" class="st2" d="M61.9 35.9v24.9L84 48.4V23.5z"/><path id="XMLID_7_" class="st2" d="M95 54.6V42.2l-11 6.2v24.8L95 67l11.1-6.2V48.4z"/><g id="XMLID_5_"><path id="XMLID_6_" class="st1" d="M95 29.7l-11-6.2v24.9l22.1 12.4V48.4L95 42.2z"/></g><path id="XMLID_4_" class="st4" d="M84 23.5l11 6.2-11 6.2v12.5L61.9 35.9z"/><path id="XMLID_3_" class="st4" d="M99.2 27l11 6.2 11.1-6.2-11.1-6.3z"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

-35
View File
@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" viewBox="0 0 230.07104 58.680001" version="1.1" id="svg28566" sodipodi:docname="swarm-logo.svg" inkscape:version="1.0.1 (3bc2e81, 2020-09-07)" width="230.07104" height="58.68">
<metadata id="metadata28570">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title>Swarm Logo &amp;amp; Lettering 4</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview pagecolor="#ffffff" bordercolor="#666666" borderopacity="1" objecttolerance="10" gridtolerance="10" guidetolerance="10" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-width="1920" inkscape:window-height="1016" id="namedview28568" showgrid="false" inkscape:zoom="3.1325" inkscape:cx="123.33" inkscape:cy="35.939998" inkscape:window-x="0" inkscape:window-y="27" inkscape:window-maximized="1" inkscape:current-layer="Layer_2" fit-margin-top="0" fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0"/>
<defs id="defs28540">
<style id="style28538">
.cls-1 {
fill: #fafafa;
}
.cls-2 {
fill: #dd7200;
}
</style>
</defs>
<title id="title28542">Swarm Logo &amp;amp; Lettering 4</title>
<g id="Layer_2" data-name="Layer 2" transform="translate(-76.67,-71.05)">
<path class="cls-2" d="m 206.24,90.06 -3.54,24.65 c 0,0 -0.06,0 -0.07,0 l -3.1,-24.65 c 0,0 0,0 0,0 h -8.82 l -3.11,24.65 c 0,0 -0.06,0 -0.07,0 l -3.54,-24.65 h -5.38 c 0,0 0,0 0,0 L 183,118.6 h 8.8 L 195.05,94 c 0,0 0.06,0 0.07,0 l 3.21,24.61 h 8.81 l 4.49,-28.53 h -5.37 c 0,0 -0.02,-0.08 -0.02,-0.02 z" id="path28546"/>
<path class="cls-2" d="m 305,91.39 a 6.52,6.52 0 0 0 -5.19,-2.14 6.74,6.74 0 0 0 -6.33,3.75 v 0 a 5.85,5.85 0 0 0 -1.86,-2.62 5.61,5.61 0 0 0 -3.62,-1.13 6.26,6.26 0 0 0 -3.55,1 5.78,5.78 0 0 0 -2,2.45 v -2.64 c 0,0 0,0 0,0 h -5.24 c 0,0 0,0 0,0 v 28.53 h 5.24 v -19.5 a 5.72,5.72 0 0 1 1,-3.5 3.14,3.14 0 0 1 2.7,-1.25 2.85,2.85 0 0 1 2.3,1 4.08,4.08 0 0 1 0.89,2.73 v 20.5 c 0,0 0,0 0,0 h 5.25 V 99.42 a 6.08,6.08 0 0 1 1,-3.89 3.22,3.22 0 0 1 2.61,-1.19 2.75,2.75 0 0 1 2.44,1 4.9,4.9 0 0 1 0.81,2.92 v 20.28 c 0,0 0,0 0,0 h 5.28 V 96.86 A 8.18,8.18 0 0 0 305,91.39 Z" id="path28548"/>
<path class="cls-2" d="m 243.47,115.52 -3.32,3.71 h -0.05 l -5.86,-5.07 c 0,0 0,0 0,0 l -7,4.09 a 7.38,7.38 0 0 1 -3.77,1 7.91,7.91 0 0 1 -5.34,-2.13 7.28,7.28 0 0 1 -2.3,-5.45 7.54,7.54 0 0 1 3.82,-6.58 L 233.76,97 c 0,0 0,0 0,0 v -1.8 h -16.53 c 0,0 0,0 0,0 v -5 c 0,0 0,0 0,0 h 21.57 v 21.36 c 0,0 0,0 0,0 l 4.62,4 z m -18.8,-1.66 9.09,-5.28 c 0,0 0,0 0,0 v -5.7 a 0.03,0 0 0 0 -0.06,0 l -11.58,6.65 a 2.46,2.46 0 0 0 -1.29,2.2 2.59,2.59 0 0 0 2.53,2.53 2.51,2.51 0 0 0 1.31,-0.4 z" id="path28550"/>
<path class="cls-2" d="m 268.75,92 a 9.1,9.1 0 0 0 -6.67,-2.78 9,9 0 0 0 -7.48,3.64 l -3.34,-3.06 a 0.025,0 0 0 0 -0.05,0 l -3.29,3.67 4.74,4.3 v 15.7 h -4.71 c 0,0 0,0 0,0 v 4.93 h 17 c 0,0 0,0 0,0 v -4.93 h -7.19 V 98.93 a 4.61,4.61 0 0 1 4,-4.66 4.45,4.45 0 0 1 4.87,4.42 v 1.64 c 0,0 0,0 0,0 h 4.93 c 0,0 0,0 0,0 V 98.69 A 9.1,9.1 0 0 0 268.75,92 Z" id="path28552"/>
<path class="cls-2" d="m 173.32,106.74 a 5.41,5.41 0 0 0 -1.5,-2 6.58,6.58 0 0 0 -2.36,-1.44 15.31,15.31 0 0 0 -2.72,-1 c -0.94,-0.25 -2,-0.52 -3.08,-0.73 a 15.43,15.43 0 0 1 -4.88,-1.64 2.85,2.85 0 0 1 -1.51,-2.47 2.81,2.81 0 0 1 1.35,-2.59 5.91,5.91 0 0 1 4.4,-0.76 8.68,8.68 0 0 1 3.86,2.11 c 0.23,0.19 2.51,2.3 2.47,2.35 l 3.29,-3.73 c 0,0 -1.58,-1.6 -2.33,-2.26 a 13,13 0 0 0 -6.31,-3.24 12.18,12.18 0 0 0 -7.23,1.1 9.58,9.58 0 0 0 -1.67,1.09 7.57,7.57 0 0 0 -2.88,6.1 c 0,2.6 0.82,4.45 2.5,5.66 a 17.33,17.33 0 0 0 6.73,2.73 25.41,25.41 0 0 1 5.53,1.65 2.29,2.29 0 0 1 1.59,2.23 3.36,3.36 0 0 1 -1.62,3 8.35,8.35 0 0 1 -8.24,0 19.32,19.32 0 0 1 -2.85,-2.27 c -0.71,-0.6 -1.38,-1.25 -2.07,-1.89 v 0 l -3.2,3.85 c 0,0 1.56,1.46 1.7,1.58 a 15.66,15.66 0 0 0 7.41,4.21 15.26,15.26 0 0 0 3.16,0.32 12.45,12.45 0 0 0 7.85,-2.45 8.17,8.17 0 0 0 3.27,-6.83 6.14,6.14 0 0 0 -0.66,-2.68 z" id="path28554"/>
<polygon class="cls-2" points="76.67,122.17 90.14,129.73 103.61,122.17 103.61,107.08 90.14,99.51 76.67,107.08 " id="polygon28556"/>
<polygon class="cls-2" points="121.2,71.05 114.08,75.05 114.07,75.11 114.07,83.1 121.2,87.1 121.26,87.13 128.38,83.14 128.38,75.08 " id="polygon28558"/>
<polygon class="cls-2" points="134.44,107.08 120.97,99.51 107.5,107.08 107.5,122.17 120.97,129.73 134.44,122.17 " id="polygon28560"/>
<polygon class="cls-2" points="105.45,73.16 92,80.72 92,95.81 105.47,103.37 118.94,95.81 118.94,89.8 112.38,86.12 110.61,85.13 110.61,83.1 110.61,76.24 " id="polygon28562"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.0 KiB

+5 -3
View File
@@ -6,7 +6,7 @@ import { ReactElement } from 'react'
const LIMIT = 100_000_000 // 100 megabytes const LIMIT = 100_000_000 // 100 megabytes
interface Props { interface Props {
file: File files: File[]
} }
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
@@ -22,14 +22,16 @@ const useStyles = makeStyles((theme: Theme) =>
export default function UploadSizeAlert(props: Props): ReactElement | null { export default function UploadSizeAlert(props: Props): ReactElement | null {
const classes = useStyles() const classes = useStyles()
const aboveLimit = props.file.size >= LIMIT const totalSize = props.files.reduce((previous, current) => previous + current.size, 0)
const aboveLimit = totalSize >= LIMIT
return ( return (
<Collapse in={aboveLimit}> <Collapse in={aboveLimit}>
<div className={classes.root}> <div className={classes.root}>
<Alert severity="warning"> <Alert severity="warning">
<AlertTitle>Warning</AlertTitle> <AlertTitle>Warning</AlertTitle>
The file you are trying to upload is above the recommended size. The chunks may not be synchronised properly The files you are trying to upload are above the recommended size. The chunks may not be synchronised properly
over the network. over the network.
</Alert> </Alert>
</div> </div>
+8 -8
View File
@@ -1,10 +1,10 @@
import { ReactElement, useState } from 'react' import { ReactElement, useState, useContext } from 'react'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import { Alert, AlertTitle } from '@material-ui/lab' import { Alert, AlertTitle } from '@material-ui/lab'
import Collapse from '@material-ui/core/Collapse' import Collapse from '@material-ui/core/Collapse'
import IconButton from '@material-ui/core/IconButton' import IconButton from '@material-ui/core/IconButton'
import CloseIcon from '@material-ui/icons/Close' import CloseIcon from '@material-ui/icons/Close'
import { useStatusNodeVersion } from '../hooks/status' import { Context } from '../providers/Bee'
import { SUPPORTED_BEE_VERSION_EXACT } from '@ethersphere/bee-js' import { SUPPORTED_BEE_VERSION_EXACT } from '@ethersphere/bee-js'
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
@@ -18,12 +18,12 @@ const useStyles = makeStyles((theme: Theme) =>
export default function VersionAlert(): ReactElement | null { export default function VersionAlert(): ReactElement | null {
const classes = useStyles() const classes = useStyles()
const { isLoading, userVersion } = useStatusNodeVersion() const { isLoading, latestUserVersionExact } = useContext(Context)
const [open, setOpen] = useState<boolean>(true) const [open, setOpen] = useState<boolean>(true)
const isExactlySupportedBeeVersion = SUPPORTED_BEE_VERSION_EXACT === userVersion const isExactlySupportedBeeVersion = SUPPORTED_BEE_VERSION_EXACT === latestUserVersionExact
if (isLoading || !userVersion) return null if (isLoading || !latestUserVersionExact) return null
return ( return (
<Collapse in={!isExactlySupportedBeeVersion && open}> <Collapse in={!isExactlySupportedBeeVersion && open}>
@@ -44,9 +44,9 @@ export default function VersionAlert(): ReactElement | null {
} }
> >
<AlertTitle>Warning</AlertTitle> <AlertTitle>Warning</AlertTitle>
Your Bee node version (<code>{userVersion}</code>) does not exactly match the Bee version we tested the Bee Your Bee node version (<code>{latestUserVersionExact}</code>) does not exactly match the Bee version we tested
Dashboard against (<code>{SUPPORTED_BEE_VERSION_EXACT}</code>). Please note that some functionality may not the Bee Dashboard against (<code>{SUPPORTED_BEE_VERSION_EXACT}</code>). Please note that some functionality
work properly. may not work properly.
</Alert> </Alert>
</div> </div>
</Collapse> </Collapse>
+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>
)
}
+12 -10
View File
@@ -1,15 +1,14 @@
import { ReactElement, useState } from 'react' import { CircularProgress, Container } from '@material-ui/core'
import Button from '@material-ui/core/Button' import Button from '@material-ui/core/Button'
import Dialog from '@material-ui/core/Dialog' import Dialog from '@material-ui/core/Dialog'
import DialogActions from '@material-ui/core/DialogActions' import DialogActions from '@material-ui/core/DialogActions'
import DialogContent from '@material-ui/core/DialogContent' import DialogContent from '@material-ui/core/DialogContent'
import DialogContentText from '@material-ui/core/DialogContentText' import DialogContentText from '@material-ui/core/DialogContentText'
import DialogTitle from '@material-ui/core/DialogTitle' import DialogTitle from '@material-ui/core/DialogTitle'
import { Container, CircularProgress } from '@material-ui/core'
import { useSnackbar } from 'notistack' import { useSnackbar } from 'notistack'
import { ReactElement, useState, useContext } from 'react'
import { beeDebugApi } from '../services/bee' import { Zap } from 'react-feather'
import { Context as SettingsContext } from '../providers/Settings'
import EthereumAddress from './EthereumAddress' import EthereumAddress from './EthereumAddress'
interface Props { interface Props {
@@ -17,10 +16,11 @@ interface Props {
uncashedAmount: string uncashedAmount: string
} }
export default function DepositModal({ peerId, uncashedAmount }: Props): ReactElement { export default function CheckoutModal({ peerId, uncashedAmount }: Props): ReactElement {
const [open, setOpen] = useState<boolean>(false) const [open, setOpen] = useState<boolean>(false)
const [loadingCashout, setLoadingCashout] = useState<boolean>(false) const [loadingCashout, setLoadingCashout] = useState<boolean>(false)
const { enqueueSnackbar } = useSnackbar() const { enqueueSnackbar } = useSnackbar()
const { beeDebugApi } = useContext(SettingsContext)
const handleClickOpen = () => { const handleClickOpen = () => {
setOpen(true) setOpen(true)
@@ -31,10 +31,12 @@ export default function DepositModal({ peerId, uncashedAmount }: Props): ReactEl
} }
const handleCashout = () => { const handleCashout = () => {
if (!beeDebugApi) return
if (peerId) { if (peerId) {
setLoadingCashout(true) setLoadingCashout(true)
beeDebugApi.chequebook beeDebugApi
.peerCashout(peerId) .cashoutLastCheque(peerId)
.then(res => { .then(res => {
setOpen(false) setOpen(false)
enqueueSnackbar( enqueueSnackbar(
@@ -58,8 +60,8 @@ export default function DepositModal({ peerId, uncashedAmount }: Props): ReactEl
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>
+18 -134
View File
@@ -1,13 +1,7 @@
import React, { ReactElement, useEffect } from 'react' import { ReactElement, useContext } from 'react'
import { withStyles, Theme, createStyles } from '@material-ui/core/styles' import TabsContainer from './TabsContainer'
import { Tabs, Tab, Box, Typography } from '@material-ui/core'
import CodeBlock from './CodeBlock' import CodeBlock from './CodeBlock'
import { Context } from '../providers/Platform'
interface TabPanelProps {
children?: React.ReactNode
index: number
value: number
}
interface Props { interface Props {
linux: string linux: string
@@ -15,133 +9,23 @@ interface Props {
showLineNumbers?: boolean showLineNumbers?: boolean
} }
function a11yProps(index: number) {
return {
id: `simple-tab-${index}`,
'aria-controls': `simple-tabpanel-${index}`,
}
}
function getOS() {
const userAgent = window.navigator.userAgent
const platform = window.navigator.platform
const macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K']
const windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE']
const iosPlatforms = ['iPhone', 'iPad', 'iPod']
if (macosPlatforms.includes(platform)) return 'macOS'
if (iosPlatforms.includes(platform)) return 'iOS'
if (windowsPlatforms.includes(platform)) return 'windows'
if (/Android/.test(userAgent)) return 'android'
if (/Linux/.test(platform)) return 'linux'
return null
}
export default function CodeBlockTabs(props: Props): ReactElement { export default function CodeBlockTabs(props: Props): ReactElement {
const [value, setValue] = React.useState(0) const { platform, setPlatform } = useContext(Context)
const handleChange = (event: React.ChangeEvent<unknown>, newValue: number) => {
setValue(newValue)
}
useEffect(() => {
const os = getOS()
if (os === 'windows') {
setValue(0)
} else if (os === 'linux') {
setValue(0)
} else if (os === 'macOS') {
setValue(1)
}
}, [])
function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<Box style={{ marginTop: '-12px' }}>
<Typography component="div">{children}</Typography>
</Box>
)}
</div>
)
}
const AntTabs = withStyles({
root: {
borderBottom: '1px solid #e8e8e8',
},
indicator: {
backgroundColor: '#3f51b5',
},
})(Tabs)
interface StyledTabProps {
label: string
}
const AntTab = withStyles((theme: Theme) =>
createStyles({
root: {
textTransform: 'none',
minWidth: 72,
backgroundColor: 'transparent',
fontWeight: theme.typography.fontWeightRegular,
marginRight: theme.spacing(4),
fontFamily: [
'-apple-system',
'BlinkMacSystemFont',
'"Segoe UI"',
'Roboto',
'"Helvetica Neue"',
'Arial',
'sans-serif',
'"Apple Color Emoji"',
'"Segoe UI Emoji"',
'"Segoe UI Symbol"',
].join(','),
'&:hover': {
color: '#3f51b5',
opacity: 1,
},
'&$selected': {
color: '#3f51b5',
fontWeight: theme.typography.fontWeightMedium,
},
'&:focus': {
color: '#3f51b5',
},
},
selected: {},
}),
)((props: StyledTabProps) => <Tab disableRipple {...props} />)
return ( return (
<div> <TabsContainer
<AntTabs style={{ marginTop: '12px' }} value={value} onChange={handleChange} aria-label="ant example"> index={platform}
<AntTab label="Linux" {...a11yProps(0)} /> indexChanged={setPlatform}
<AntTab label="MacOS" {...a11yProps(1)} /> values={[
</AntTabs> {
<TabPanel value={value} index={0}> label: 'Linux',
<CodeBlock showLineNumbers={props.showLineNumbers} language="bash" code={props.linux} /> component: <CodeBlock showLineNumbers={props.showLineNumbers} language="bash" code={props.linux} />,
</TabPanel> },
<TabPanel value={value} index={1}> {
<CodeBlock showLineNumbers={props.showLineNumbers} language="bash" code={props.mac} /> label: 'macOS',
</TabPanel> component: <CodeBlock showLineNumbers={props.showLineNumbers} language="bash" code={props.mac} />,
</div> },
]}
/>
) )
} }
-62
View File
@@ -1,62 +0,0 @@
import React, { ReactElement, useState } from 'react'
import { TextField, Button, CircularProgress, Container } from '@material-ui/core'
interface Props {
defaultHost?: string
hostName: string
}
export default function ConnectToHost(props: Props): ReactElement {
const [hostInputVisible, toggleHostInputVisibility] = useState(false)
const [connectingToHost, setConnectingToHost] = useState(false)
const [host, setHost] = useState('')
const handleNewHostConnection = () => {
if (host) {
setConnectingToHost(true)
sessionStorage.setItem(props.hostName, host)
toggleHostInputVisibility(!hostInputVisible)
window.location.reload()
}
}
return (
<div>
{
// FIXME: this should be broken up
/* eslint-disable no-nested-ternary */
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>
) : connectingToHost ? (
<Container style={{ textAlign: 'center', padding: '0px' }}>
<CircularProgress size={20} />
</Container>
) : (
<Button onClick={() => toggleHostInputVisibility(!hostInputVisible)} size="small" variant="outlined">
Change host
</Button>
)
/* eslint-enable no-nested-ternary */
}
</div>
)
}
-75
View File
@@ -1,75 +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 { Skeleton } from '@material-ui/lab'
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
isLoadingNodeAddresses: boolean
chequebookAddress: ChequebookAddressResponse | null
isLoadingChequebookAddress: boolean
}
function EthereumAddressCard(props: Props): ReactElement {
const classes = useStyles()
return (
<Card className={classes.root}>
{props.isLoadingNodeAddresses ? (
<div style={{ padding: '16px' }}>
<Skeleton width={300} height={30} animation="wave" />
<Skeleton width={300} height={50} animation="wave" />
</div>
) : (
<div className={classes.details}>
<CardContent className={classes.content}>
<Typography variant="subtitle1" gutterBottom>
Ethereum Address
</Typography>
<EthereumAddress address={props.nodeAddresses?.ethereum} />
</CardContent>
</div>
)}
{props.isLoadingChequebookAddress ? (
<div style={{ padding: '16px' }}>
<Skeleton width={300} height={30} animation="wave" />
<Skeleton width={300} height={50} animation="wave" />
</div>
) : (
<div className={classes.details}>
<CardContent className={classes.content}>
<Typography variant="subtitle1" gutterBottom>
Chequebook Contract Address
</Typography>
<EthereumAddress address={props.chequebookAddress?.chequebookAddress} />
</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>
)
}
+150
View File
@@ -0,0 +1,150 @@
import { Button, Grid, IconButton, InputBase, ListItem, Typography } from '@material-ui/core'
import Collapse from '@material-ui/core/Collapse'
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import { ChangeEvent, ReactElement, useState } from 'react'
import { Check, Edit, Minus, RotateCcw } from 'react-feather'
import ExpandableListItemActions from './ExpandableListItemActions'
import ExpandableListItemNote from './ExpandableListItemNote'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
header: {
backgroundColor: theme.palette.background.paper,
marginBottom: theme.spacing(0.25),
borderLeft: `${theme.spacing(0.25)}px solid rgba(0,0,0,0)`,
wordBreak: 'break-word',
},
headerOpen: {
borderLeft: `${theme.spacing(0.25)}px solid ${theme.palette.primary.main}`,
},
copyValue: {
cursor: 'pointer',
padding: theme.spacing(1),
borderRadius: 0,
'&:hover': {
backgroundColor: '#fcf2e8',
color: theme.palette.primary.main,
},
},
content: {
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
},
keyMargin: {
marginRight: theme.spacing(1),
},
unselectableLabel: {
cursor: 'default',
userSelect: 'none',
// Many browsers don't support yet the general user-select css property
WebkitUserSelect: 'none',
MozUserSelect: 'none',
msUserSelect: 'none',
},
}),
)
interface Props {
label: string
value?: string
placeholder?: string
helperText?: string
expandedOnly?: boolean
confirmLabel?: string
confirmLabelDisabled?: boolean
onChange?: (value: string) => void
onConfirm: (value: string) => void
mapperFn?: (value: string) => string
}
export default function ExpandableListItemKey({
label,
value,
onConfirm,
onChange,
confirmLabel,
confirmLabelDisabled,
expandedOnly,
helperText,
placeholder,
mapperFn,
}: Props): ReactElement | null {
const classes = useStyles()
const [open, setOpen] = useState(Boolean(expandedOnly))
const [inputValue, setInputValue] = useState<string>(value || '')
const toggleOpen = () => setOpen(!open)
const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
if (mapperFn) {
e.target.value = mapperFn(e.target.value)
}
setInputValue(e.target.value)
if (onChange) onChange(e.target.value)
}
return (
<>
<ListItem className={`${classes.header} ${open ? classes.headerOpen : ''}`}>
<Grid container direction="column" justifyContent="space-between" alignItems="stretch">
<Grid container direction="row" justifyContent="space-between" alignItems="center">
{label && (
<Typography variant="body1" className={classes.unselectableLabel}>
{label}
</Typography>
)}
<Typography variant="body2">
<div>
{!open && value}
{!expandedOnly && (
<IconButton size="small" className={classes.copyValue}>
{open ? (
<Minus onClick={toggleOpen} strokeWidth={1} />
) : (
<Edit onClick={toggleOpen} strokeWidth={1} />
)}
</IconButton>
)}
</div>
</Typography>
</Grid>
<Collapse in={open} timeout="auto" unmountOnExit>
<InputBase
value={inputValue}
placeholder={placeholder}
onChange={handleChange}
fullWidth
className={classes.content}
autoFocus
/>
</Collapse>
</Grid>
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
{helperText && <ExpandableListItemNote>{helperText}</ExpandableListItemNote>}
<ExpandableListItemActions>
<Button
variant="contained"
disabled={
inputValue === value ||
Boolean(confirmLabelDisabled) || // Disable if external validation is provided
(inputValue === '' && value === undefined) // Disable if no initial value was not provided and the field is empty. The undefined check is improtant so that it is possible to submit with empty input in other cases
}
startIcon={<Check size="1rem" />}
onClick={() => onConfirm(inputValue)}
>
{confirmLabel || 'Save'}
</Button>
<Button
variant="contained"
disabled={inputValue === value || inputValue === ''}
startIcon={<RotateCcw size="1rem" />}
onClick={() => setInputValue(value || '')}
>
Cancel
</Button>
</ExpandableListItemActions>
</Collapse>
</>
)
}
+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>
)
}
-23
View File
@@ -1,23 +0,0 @@
import { ReactElement, useEffect, useState } from 'react'
interface Props {
date: number | null
}
export default function LastUpdate({ date }: Props): ReactElement {
const [duration, setDuration] = useState<string>('never')
const refresh = () => {
if (!date) setDuration('never')
else setDuration(`${((Date.now() - date) / 1000).toFixed()} seconds ago`)
}
useEffect(() => {
refresh()
const i = setInterval(refresh, 1000)
return () => clearInterval(i)
}, [date])
return <span>Last Update: {duration}</span>
}
+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>
)
}
-55
View File
@@ -1,55 +0,0 @@
import { useState, ReactElement } from 'react'
import { createStyles, makeStyles } from '@material-ui/core/styles'
import { Toolbar, IconButton } from '@material-ui/core/'
import { Sun, Moon } from 'react-feather'
const drawerWidth = 240
const useStyles = makeStyles(() =>
createStyles({
appBar: {
width: `calc(100% - ${drawerWidth}px)`,
marginLeft: drawerWidth,
},
}),
)
interface Props {
themeMode: string
}
export default function SideBar(props: Props): ReactElement {
const [darkMode, toggleDarkMode] = useState(false)
const switchTheme = () => {
const theme = localStorage.getItem('theme')
if (theme) {
localStorage.setItem('theme', theme === 'light' ? 'dark' : 'light')
} else {
localStorage.setItem('theme', darkMode ? 'dark' : 'light')
}
toggleDarkMode(!darkMode)
window.location.reload()
}
const classes = useStyles()
return (
<div>
<div style={{ display: 'fixed' }} className={classes.appBar}>
<Toolbar style={{ display: 'flex' }}>
<div style={{ width: '100%' }}>
<div style={{ float: 'right' }}>
<IconButton style={{ marginRight: '10px' }} aria-label="dark-mode" onClick={() => switchTheme()}>
{props.themeMode === 'dark' ? <Moon /> : <Sun />}
</IconButton>
</div>
</div>
</Toolbar>
</div>
</div>
)
}
-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>
)
}
+84 -127
View File
@@ -1,67 +1,51 @@
import { ReactElement } from 'react' import { Divider, Drawer, Grid, Link as MUILink, List } from '@material-ui/core'
import { Link, RouteComponentProps } from 'react-router-dom' import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import { createStyles, Theme, makeStyles } from '@material-ui/core/styles'
import { ListItemText, ListItemIcon, ListItem, Divider, List, Drawer, Link as MUILink } from '@material-ui/core'
import { OpenInNewSharp } from '@material-ui/icons' import { OpenInNewSharp } from '@material-ui/icons'
import { Activity, FileText, DollarSign, Share2, Settings, Layers } from 'react-feather' import type { ReactElement } from 'react'
import { BookOpen, DollarSign, FileText, Home, Layers, Settings } from 'react-feather'
import SwarmLogoOrange from '../assets/swarm-logo-orange.svg' import { Link } from 'react-router-dom'
import { Health } from '@ethersphere/bee-js' import Logo from '../assets/logo.svg'
import { ROUTES } from '../routes'
const drawerWidth = 240 import SideBarItem from './SideBarItem'
import SideBarStatus from './SideBarStatus'
const navBarItems = [ const navBarItems = [
{ {
label: 'Status', label: 'Info',
id: 'status', path: ROUTES.INFO,
path: '/', icon: Home,
icon: Activity,
}, },
{ {
label: 'Files', label: 'Files',
id: 'files', path: ROUTES.UPLOAD,
path: '/files/',
icon: FileText, icon: FileText,
}, },
{ {
label: 'Stamps', label: 'Stamps',
id: 'stamps', path: ROUTES.STAMPS,
path: '/stamps/',
icon: Layers, icon: Layers,
}, },
{ {
label: 'Accounting', label: 'Accounting',
id: 'accounting', path: ROUTES.ACCOUNTING,
path: '/accounting/',
icon: DollarSign, icon: DollarSign,
}, },
{
label: 'Peers',
id: 'peers',
path: '/peers/',
icon: Share2,
},
{ {
label: 'Settings', label: 'Settings',
id: 'settings', path: ROUTES.SETTINGS,
path: '/settings/',
icon: Settings, icon: Settings,
}, },
] ]
const drawerWidth = 300
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
createStyles({ createStyles({
root: { root: {
display: 'flex', flexWrap: 'nowrap',
}, minHeight: '100vh',
appBar: { paddingTop: theme.spacing(8),
width: `calc(100% - ${drawerWidth}px)`, paddingBottom: theme.spacing(8),
marginLeft: drawerWidth,
},
logo: {
padding: 1,
marginTop: 20,
}, },
drawer: { drawer: {
width: drawerWidth, width: drawerWidth,
@@ -69,106 +53,79 @@ const useStyles = makeStyles((theme: Theme) =>
}, },
drawerPaper: { drawerPaper: {
width: drawerWidth, width: drawerWidth,
backgroundColor: '#212121',
}, },
activeSideBar: { logo: {
color: '#dd7700', marginLeft: theme.spacing(8),
marginRight: theme.spacing(8),
}, },
activeSideBarItem: { icon: {
borderLeft: '4px solid #dd7700', height: theme.spacing(4),
backgroundColor: 'inherit !important', },
iconSmall: {
height: theme.spacing(2),
},
divider: {
backgroundColor: '#2c2c2c',
marginLeft: theme.spacing(4),
marginRight: theme.spacing(4),
},
link: {
color: '#9f9f9f',
textDecoration: 'none',
'&:hover': {
textDecoration: 'none',
// https://github.com/mui-org/material-ui/issues/22543
'@media (hover: none)': {
textDecoration: 'none',
},
},
}, },
toolbar: theme.mixins.toolbar,
}), }),
) )
interface Props extends RouteComponentProps { export default function SideBar(): ReactElement {
themeMode: string
health: boolean
nodeHealth: Health | null
}
export default function SideBar(props: Props): ReactElement {
const classes = useStyles() const classes = useStyles()
return ( return (
<div className={classes.root}> <Drawer className={classes.drawer} variant="permanent" anchor="left" classes={{ paper: classes.drawerPaper }}>
<Drawer <Grid container direction="column" justifyContent="space-between" className={classes.root}>
className={classes.drawer} <Grid className={classes.logo}>
variant="permanent" <Link to={ROUTES.INFO}>
classes={{ <img alt="swarm" src={Logo} />
paper: classes.drawerPaper,
}}
anchor="left"
>
<div className={classes.toolbar} style={{ textAlign: 'left', marginLeft: 20 }}>
<Link to="/">
<img
alt="swarm"
className={classes.logo}
src={props.themeMode === 'light' ? SwarmLogoOrange : SwarmLogoOrange}
style={{ maxHeight: '30px', alignItems: 'center' }}
/>
</Link> </Link>
</div> </Grid>
<List> <Grid>
{navBarItems.map(item => ( <List>
<Link to={item.path} key={item.id} style={{ color: 'inherit', textDecoration: 'none' }}> {navBarItems.map(p => (
<ListItem <Link to={p.path} key={p.path} className={classes.link}>
button <SideBarItem
selected={props.location.pathname === item.path} key={p.path}
className={props.location.pathname === item.path ? classes.activeSideBarItem : ''} iconStart={<p.icon className={classes.icon} />}
> path={p.path}
<ListItemIcon className={props.location.pathname === item.path ? classes.activeSideBar : ''}> label={p.label}
<item.icon style={{ height: '20px' }} />
</ListItemIcon>
<ListItemText
primary={item.label}
className={props.location.pathname === item.path ? classes.activeSideBar : ''}
/> />
</ListItem> </Link>
</Link> ))}
))} </List>
</List> <Divider className={classes.divider} />
<Divider /> <List>
<List> <MUILink href={process.env.REACT_APP_BEE_DOCS_HOST} target="_blank" className={classes.link}>
<MUILink href={process.env.REACT_APP_BEE_DOCS_HOST} target="_blank" style={{ textDecoration: 'none' }}> <SideBarItem
<ListItem button> iconStart={<BookOpen className={classes.icon} />}
<ListItemText primary={'Docs'} /> iconEnd={<OpenInNewSharp className={classes.iconSmall} />}
<OpenInNewSharp fontSize="small" /> label={<span>Docs</span>}
</ListItem>
</MUILink>
</List>
<div style={{ position: 'fixed', bottom: 0, width: 'inherit', padding: '10px' }}>
<ListItem>
<div style={{ marginRight: '30px' }}>
<div
style={{
backgroundColor: props.health ? '#32c48d' : '#c9201f',
marginRight: '7px',
height: '10px',
width: '10px',
borderRadius: '50%',
display: 'inline-block',
}}
/> />
<span>API</span> </MUILink>
</div> </List>
<div> </Grid>
<div <Grid>
style={{ <Link to={ROUTES.STATUS} className={classes.link}>
backgroundColor: props.nodeHealth?.status === 'ok' ? '#32c48d' : '#c9201f', <SideBarStatus path={ROUTES.STATUS} />
marginRight: '7px', </Link>
height: '10px', </Grid>
width: '10px', </Grid>
borderRadius: '50%', </Drawer>
display: 'inline-block',
}}
/>
<span>Debug API</span>
</div>
</ListItem>
</div>
</Drawer>
</div>
) )
} }
+62
View File
@@ -0,0 +1,62 @@
import type { ReactElement, ReactNode } from 'react'
import { useLocation, matchPath } from 'react-router-dom'
import { createStyles, Theme, makeStyles, withStyles } from '@material-ui/core/styles'
import { ListItemText, ListItemIcon, ListItem } from '@material-ui/core'
const StyledListItem = withStyles((theme: Theme) => ({
root: {
paddingLeft: theme.spacing(4),
paddingRight: theme.spacing(4),
borderLeft: '4px solid rgba(0,0,0,0)',
'&.Mui-selected, &.Mui-selected:hover': {
borderLeft: `4px solid ${theme.palette.primary.main}`,
backgroundColor: '#2c2c2c',
color: '#f9f9f9',
},
},
button: {
'&:hover': {
backgroundColor: '#2c2c2c',
color: '#f9f9f9',
// https://github.com/mui-org/material-ui/issues/22543
'@media (hover: none)': {
backgroundColor: '#2c2c2c',
color: '#f9f9f9',
},
},
},
}))(ListItem)
const useStyles = makeStyles((theme: Theme) =>
createStyles({
icon: {
color: 'inherit',
},
activeIcon: {
color: theme.palette.primary.main,
},
}),
)
interface Props {
iconStart?: ReactNode
iconEnd?: ReactNode
path?: string
label: ReactNode
}
export default function SideBarItem({ iconStart, iconEnd, path, label }: Props): ReactElement {
const classes = useStyles()
const location = useLocation()
const isSelected = Boolean(matchPath(location.pathname, { path, exact: true }))
return (
<StyledListItem button selected={isSelected} disableRipple>
<ListItemIcon className={isSelected ? classes.activeIcon : classes.icon}>{iconStart}</ListItemIcon>
<ListItemText primary={label} />
<ListItemIcon className={isSelected ? classes.activeIcon : classes.icon}>{iconEnd}</ListItemIcon>
</StyledListItem>
)
}
+79
View File
@@ -0,0 +1,79 @@
import { ReactElement, useContext } from 'react'
import { useLocation, matchPath } from 'react-router-dom'
import { ArrowRight } from 'react-feather'
import { createStyles, Theme, makeStyles } from '@material-ui/core/styles'
import { ListItemText, ListItemIcon, ListItem, Typography } from '@material-ui/core'
import { Context } from '../providers/Bee'
import StatusIcon from './StatusIcon'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
icon: {
color: 'inherit',
},
iconSmall: {
height: theme.spacing(2),
},
root: {
height: theme.spacing(4),
paddingLeft: theme.spacing(1),
paddingRight: theme.spacing(4),
color: '#f9f9f9',
borderLeft: '0px solid rgba(0,0,0,0)',
'&.Mui-selected, &.Mui-selected:hover': {
borderLeft: `0px solid ${theme.palette.primary.main}`,
backgroundColor: '#2c2c2c',
},
},
rootError: {
backgroundColor: 'rgba(255, 58, 82, 0.25)',
},
button: {
'&:hover': {
backgroundColor: '#2c2c2c',
color: 'white',
// https://github.com/mui-org/material-ui/issues/22543
'@media (hover: none)': {
backgroundColor: '#2c2c2c',
color: 'white',
},
},
},
smallerText: {
fontSize: '0.9rem',
},
}),
)
interface Props {
path?: string
}
export default function SideBarItem({ path }: Props): ReactElement {
const { status, isLoading } = useContext(Context)
const classes = useStyles()
const location = useLocation()
const isSelected = Boolean(matchPath(location.pathname, { path, exact: true }))
return (
<ListItem
button
classes={{ root: `${classes.root} ${status.all ? '' : classes.rootError}`, button: classes.button }}
selected={isSelected}
disableRipple
>
<ListItemIcon style={{ marginLeft: '30px' }}>
<StatusIcon isOk={status.all} isLoading={isLoading} />
</ListItemIcon>
<ListItemText
primary={<Typography className={classes.smallerText}>{`Node ${status.all ? 'OK' : 'Error'}`}</Typography>}
/>
<ListItemIcon className={classes.icon}>
{status.all ? null : <ArrowRight className={classes.iconSmall} />}
</ListItemIcon>
</ListItem>
)
}
-50
View File
@@ -1,50 +0,0 @@
import type { ReactElement } from 'react'
import { makeStyles } from '@material-ui/core/styles'
import { Card, CardContent, Typography } from '@material-ui/core/'
import { Skeleton } from '@material-ui/lab'
const useStyles = makeStyles({
root: {
minWidth: 275,
},
title: {
fontSize: 16,
},
pos: {
marginBottom: 12,
},
})
interface Props {
label: string
statistic?: string
loading?: boolean
}
export default function StatCard({ loading, label, statistic }: 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 && (
<>
<Typography className={classes.title} color="textSecondary" gutterBottom>
{label}
</Typography>
<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>
)
}
+33 -34
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
@@ -15,57 +12,59 @@ function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props const { children, value, index, ...other } = props
return ( return (
<div <div role="tabpanel" hidden={value !== index} {...other}>
role="tabpanel" {value === index && children}
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<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 {
values: TabsValues[] values: TabsValues[]
index?: number
indexChanged?: (index: number) => void
} }
export default function SimpleTabs({ values }: Props): ReactElement { export default function SimpleTabs({ values, index, indexChanged }: Props): ReactElement {
const classes = useStyles() const classes = useStyles()
const [value, setValue] = React.useState<number>(0) const [value, setValue] = React.useState<number>(index || 0)
const handleChange = (event: React.ChangeEvent<Record<string, never>>, newValue: number) => { const handleChange = (event: React.ChangeEvent<Record<string, never>>, newValue: number) => {
setValue(newValue) if (indexChanged) indexChanged(newValue)
else setValue(newValue)
} }
const v = index !== undefined ? index : value
return ( return (
<div className={classes.root}> <div className={classes.root}>
<Tabs value={value} onChange={handleChange} aria-label="simple tabs example"> <Tabs value={v} onChange={handleChange} variant="fullWidth">
{values.map(({ label }, index) => ( {values.map(({ label }, idx) => (
<Tab key={index} label={label} /> <Tab key={idx} label={label} />
))} ))}
</Tabs> </Tabs>
{values.map(({ component }, index) => ( <div className={classes.content}>
<TabPanel key={index} value={value} index={index}> {values.map(({ component }, idx) => (
{component} <TabPanel key={idx} value={v} index={idx}>
</TabPanel> {component}
))} </TabPanel>
))}
</div>
</div> </div>
) )
} }
+34 -19
View File
@@ -1,28 +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 StatCard from './StatCard' import { pickThreshold, ThresholdValues } from '../utils/threshold'
import ExpandableListItem from './ExpandableListItem'
interface Props { interface Props {
isLoading: boolean
topology: Topology | null topology: Topology | null
error: Error | null // FIXME: should display error
} }
const TopologyStats = ({ isLoading, topology }: Props): ReactElement => ( const TopologyStats = (props: Props): ReactElement => {
<Grid style={{ marginBottom: '20px', flexGrow: 1 }}> const thresholds: ThresholdValues = {
<Grid container spacing={3}> connectedPeers: pickThreshold('connectedPeers', props.topology?.connected || 0),
<Grid key={1} item xs={12} sm={12} md={6} lg={4} xl={4}> population: pickThreshold('population', props.topology?.population || 0),
<StatCard label="Connected Peers" statistic={topology?.connected.toString()} loading={isLoading} /> depth: pickThreshold('depth', props.topology?.depth || 0),
</Grid> }
<Grid key={2} item xs={12} sm={12} md={6} lg={4} xl={4}>
<StatCard label="Population" statistic={topology?.population.toString()} loading={isLoading} /> const maximumTotalScore = Object.values(thresholds).reduce((sum, item) => sum + item.maximumScore, 0)
</Grid> const actualTotalScore = Object.values(thresholds).reduce((sum, item) => sum + item.score, 0)
<Grid key={3} item xs={12} sm={12} md={6} lg={4} xl={4}> const percentageText = Math.round((actualTotalScore / maximumTotalScore) * 100) + '%'
<StatCard label="Depth" statistic={topology?.depth.toString()} loading={isLoading} />
</Grid> return (
</Grid> <>
</Grid> <ExpandableListItem label="Overall Health Indicator" value={percentageText} />
) <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}
/>
</>
)
}
export default TopologyStats export default TopologyStats
+52 -35
View File
@@ -1,47 +1,64 @@
import type { ReactElement } from 'react' import type { ReactElement } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { makeStyles } from '@material-ui/core/styles' import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import { Card, CardContent, Typography } from '@material-ui/core/' import { Button, Grid, Typography, Link as MuiLink } from '@material-ui/core/'
import { ROUTES } from '../routes'
import { Activity } from 'react-feather'
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="/">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={process.env.REACT_APP_BEE_DOCS_HOST} target="_blank" rel="noreferrer">
</a> Swarm Bee Docs
</strong> </MuiLink>{' '}
</div> or ask for support on the{' '}
<MuiLink href={process.env.REACT_APP_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>
) )
} }
@@ -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,9 +19,10 @@ 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 WithdrawModal({ export default function WithdrawDepositModal({
successMessage, successMessage,
errorMessage, errorMessage,
dialogMessage, dialogMessage,
@@ -29,6 +30,7 @@ export default function WithdrawModal({
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 WithdrawModal({
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 WithdrawModal({
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 WithdrawModal({
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">
-4
View File
@@ -1,4 +0,0 @@
// These values can for now be constants because their change in the app reloads the page
export const apiHost = sessionStorage.getItem('api_host') || process.env.REACT_APP_BEE_HOST || 'http://localhost:1633'
export const debugApiHost =
sessionStorage.getItem('debug_api_host') || process.env.REACT_APP_BEE_DEBUG_HOST || 'http://localhost:1635'
+14 -6
View File
@@ -1,18 +1,26 @@
import type { ReactElement } from 'react' import { ReactElement, useContext } from 'react'
import { beeDebugApi } from '../services/bee' import { Download } from 'react-feather'
import { Context as SettingsContext } from '../providers/Settings'
import WDModal from '../components/WDModal' import WithdrawDepositModal from '../components/WithdrawDepositModal'
import { BigNumber } from 'bignumber.js' import { BigNumber } from 'bignumber.js'
export default function DepositModal(): ReactElement { export default function DepositModal(): ReactElement {
const { beeDebugApi } = useContext(SettingsContext)
return ( return (
<WDModal <WithdrawDepositModal
successMessage="Successful deposit." successMessage="Successful deposit."
errorMessage="Error with depositing" errorMessage="Error with depositing"
dialogMessage="Specify the amount of BZZ you would like to withdraw from your node." dialogMessage="Specify the amount of BZZ you would like to deposit to your node."
label="Deposit" label="Deposit"
icon={<Download size="1rem" />}
min={new BigNumber(0)} min={new BigNumber(0)}
action={beeDebugApi.chequebook.deposit} action={(amount: bigint) => {
if (!beeDebugApi) throw new Error('Bee Debug URL is not valid')
return beeDebugApi.depositTokens(amount.toString())
}}
/> />
) )
} }
+14 -7
View File
@@ -1,18 +1,25 @@
import type { ReactElement } from 'react'
import { beeDebugApi } from '../services/bee'
import WDModal from '../components/WDModal'
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)
return ( return (
<WDModal <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={beeDebugApi.chequebook.withdraw} action={(amount: bigint) => {
if (!beeDebugApi) throw new Error('Bee Debug URL is not valid')
return beeDebugApi.withdrawTokens(amount.toString())
}}
/> />
) )
} }
+35 -32
View File
@@ -1,15 +1,12 @@
import { LastCashoutActionResponse } from '@ethersphere/bee-js' import { LastCashoutActionResponse, BeeDebug } from '@ethersphere/bee-js'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Token } from '../models/Token' import { Token } from '../models/Token'
import { beeDebugApi } from '../services/bee' import { makeRetriablePromise, unwrapPromiseSettlements } from '../utils'
import { Balance, Settlement, useApiPeerBalances, useApiSettlements } from './apiHooks' import { Balance, Settlements, Settlement } from '../types'
interface UseAccountingHook { interface UseAccountingHook {
isLoading: boolean
isLoadingUncashed: boolean isLoadingUncashed: boolean
error: Error | null totalUncashed: Token
totalsent: Token
totalreceived: Token
accounting: Accounting[] | null accounting: Accounting[] | null
} }
@@ -64,51 +61,57 @@ 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 = (): UseAccountingHook => { export const useAccounting = (
const settlements = useApiSettlements() beeDebugApi: BeeDebug | null,
const balances = useApiPeerBalances() settlements: Settlements | null,
balances: Balance[] | null,
const [err, setErr] = useState<Error | null>(null) ): UseAccountingHook => {
const [isLoadingUncashed, setIsloadingUncashed] = useState<boolean>(false) const [isLoadingUncashed, setIsloadingUncashed] = useState<boolean>(false)
const [uncashedAmounts, setUncashedAmounts] = useState<LastCashoutActionResponse[] | undefined>(undefined) const [uncashedAmounts, setUncashedAmounts] = useState<LastCashoutActionResponse[] | undefined>(undefined)
const error = balances.error || settlements.error || err
useEffect(() => { useEffect(() => {
// We don't have any settlements loaded yet or we are already loading/have loaded the uncashed amounts // We don't have any settlements loaded yet or we are already loading/have loaded the uncashed amounts
if (isLoadingUncashed || !settlements.settlements || uncashedAmounts || error) return if (isLoadingUncashed || !beeDebugApi || !settlements || uncashedAmounts) return
setIsloadingUncashed(true) setIsloadingUncashed(true)
const promises = settlements.settlements.settlements const promises = settlements.settlements
.filter(({ received }) => received.toBigNumber.gt('0')) .filter(({ received }) => received.toBigNumber.gt('0'))
.map(({ peer }) => beeDebugApi.chequebook.getPeerLastCashout(peer)) .map(({ peer }) => makeRetriablePromise(() => beeDebugApi.getLastCashoutAction(peer)))
Promise.all(promises) Promise.allSettled(promises).then(settlements => {
.then(setUncashedAmounts) const results = unwrapPromiseSettlements(settlements)
.catch(setErr) setUncashedAmounts(results.fulfilled)
.finally(() => setIsloadingUncashed(false)) setIsloadingUncashed(false)
}, [settlements, isLoadingUncashed, uncashedAmounts, error]) })
}, [settlements, isLoadingUncashed, uncashedAmounts, beeDebugApi])
const accounting = mergeAccounting(balances.peerBalances, settlements.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 {
isLoading: settlements.isLoadingSettlements || balances.isLoadingPeerBalances,
isLoadingUncashed, isLoadingUncashed,
error, totalUncashed,
accounting, accounting,
totalsent: settlements.settlements?.totalSent || new Token('0'),
totalreceived: settlements.settlements?.totalReceived || new Token('0'),
} }
} }
-401
View File
@@ -1,406 +1,5 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import {
NodeAddresses,
ChequebookAddressResponse,
LastChequesResponse,
Health,
Peer,
Topology,
LastChequesForPeerResponse,
} from '@ethersphere/bee-js'
import { beeDebugApi, beeApi } from '../services/bee'
import axios from 'axios' import axios from 'axios'
import { Token } from '../models/Token'
export interface HealthHook {
health: boolean
isLoadingHealth: boolean
error: Error | null
}
export const useApiHealth = (): HealthHook => {
const [health, setHealth] = useState<boolean>(false)
const [isLoadingHealth, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeApi.status
.health()
.then(res => {
setHealth(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { health, isLoadingHealth, error }
}
export interface DebugHealthHook {
nodeHealth: Health | null
isLoadingNodeHealth: boolean
error: Error | null
}
export const useDebugApiHealth = (): DebugHealthHook => {
const [nodeHealth, setNodeHealth] = useState<Health | null>(null)
const [isLoadingNodeHealth, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.status
.nodeHealth()
.then(res => {
setNodeHealth(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { nodeHealth, isLoadingNodeHealth, error }
}
export interface NodeAddressesHook {
nodeAddresses: NodeAddresses | null
isLoadingNodeAddresses: boolean
error: Error | null
}
export const useApiNodeAddresses = (): NodeAddressesHook => {
const [nodeAddresses, setNodeAddresses] = useState<NodeAddresses | null>(null)
const [isLoadingNodeAddresses, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.connectivity
.addresses()
.then(res => {
setNodeAddresses(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { nodeAddresses, isLoadingNodeAddresses, error }
}
export interface NodeTopologyHook {
topology: Topology | null
isLoading: boolean
error: Error | null
}
export const useApiNodeTopology = (): NodeTopologyHook => {
const [topology, setNodeTopology] = useState<Topology | null>(null)
const [isLoading, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.connectivity
.topology()
.then(res => {
setNodeTopology(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { topology, isLoading, error }
}
export interface ChequebookAddressHook {
chequebookAddress: ChequebookAddressResponse | null
isLoadingChequebookAddress: boolean
error: Error | null
}
export const useApiChequebookAddress = (): ChequebookAddressHook => {
const [chequebookAddress, setChequebookAddress] = useState<ChequebookAddressResponse | null>(null)
const [isLoadingChequebookAddress, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.chequebook
.address()
.then(res => {
setChequebookAddress(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { chequebookAddress, isLoadingChequebookAddress, error }
}
export interface NodePeersHook {
peers: Peer[] | null
isLoading: boolean
error: Error | null
}
export const useApiNodePeers = (): NodePeersHook => {
const [peers, setPeers] = useState<Peer[] | null>(null)
const [isLoading, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.connectivity
.listPeers()
.then(res => {
setPeers(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { peers, isLoading, error }
}
export interface ChequebookBalance {
totalBalance: Token
availableBalance: Token
}
export interface ChequebookBalanceHook {
chequebookBalance: ChequebookBalance | null
isLoadingChequebookBalance: boolean
error: Error | null
}
export const useApiChequebookBalance = (): ChequebookBalanceHook => {
const [chequebookBalance, setChequebookBalance] = useState<ChequebookBalance | null>(null)
const [isLoadingChequebookBalance, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.chequebook
.balance()
.then(({ totalBalance, availableBalance }) => {
const balance = {
totalBalance: new Token(totalBalance),
availableBalance: new Token(availableBalance),
}
setChequebookBalance(balance)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { chequebookBalance, isLoadingChequebookBalance, error }
}
export interface Balance {
peer: string
balance: Token
}
export interface PeerBalanceHook {
peerBalances: Balance[] | null
isLoadingPeerBalances: boolean
error: Error | null
}
export const useApiPeerBalances = (): PeerBalanceHook => {
const [peerBalances, setPeerBalances] = useState<Balance[] | null>(null)
const [isLoadingPeerBalances, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.balance
.balances()
.then(res => {
// for some reason sometimes these are numbers and not BigInts
const balances = res.balances.map(({ peer, balance }) => ({ peer, balance: new Token(balance) }))
setPeerBalances(balances)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { peerBalances, isLoadingPeerBalances, error }
}
export interface PeerChequesHook {
peerCheques: LastChequesResponse | null
isLoadingPeerCheques: boolean
error: Error | null
}
export const useApiPeerCheques = (): PeerChequesHook => {
const [peerCheques, setPeerCheques] = useState<LastChequesResponse | null>(null)
const [isLoadingPeerCheques, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.chequebook
.getLastCheques()
.then(res => {
setPeerCheques(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { peerCheques, isLoadingPeerCheques, error }
}
export interface PeerLastChequesHook {
peerCheque: LastChequesForPeerResponse | null
isLoadingPeerCheque: boolean
error: Error | null
}
export const useApiPeerLastCheque = (peerId: string): PeerLastChequesHook => {
const [peerCheque, setPeerCheque] = useState<LastChequesForPeerResponse | null>(null)
const [isLoadingPeerCheque, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.chequebook
.getPeerLastCheques(peerId)
.then(res => {
setPeerCheque(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [peerId])
return { peerCheque, isLoadingPeerCheque, error }
}
export interface Settlement {
peer: string
received: Token
sent: Token
}
export interface Settlements {
totalReceived: Token
totalSent: Token
settlements: Settlement[]
}
export interface SettlementsHook {
settlements: Settlements | null
isLoadingSettlements: boolean
error: Error | null
}
export const useApiSettlements = (): SettlementsHook => {
const [settlements, setSettlements] = useState<Settlements | null>(null)
const [isLoadingSettlements, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.settlements
.getSettlements()
.then(({ totalReceived, settlements, totalSent }) => {
const set = {
totalReceived: new Token(totalReceived),
totalSent: new Token(totalSent),
settlements: settlements.map(({ peer, received, sent }) => ({
peer,
received: new Token(received),
sent: new Token(sent),
})),
}
setSettlements(set)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { settlements, isLoadingSettlements, error }
}
export interface LastCashout {
peer: string
uncashedAmount: Token
}
export interface PeerLastCashoutHook {
peerCashout: LastCashout | null
isLoadingPeerCashout: boolean
error: Error | null
}
export const useApiPeerLastCashout = (peerId: string): PeerLastCashoutHook => {
const [peerCashout, setPeerCashout] = useState<LastCashout | null>(null)
const [isLoadingPeerCashout, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.chequebook
.getPeerLastCashout(peerId)
.then(({ peer, uncashedAmount }) => {
setPeerCashout({ peer, uncashedAmount: new Token(uncashedAmount.toString()) })
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [peerId])
return { peerCashout, isLoadingPeerCashout, error }
}
export interface LatestBeeReleaseHook { export interface LatestBeeReleaseHook {
latestBeeRelease: LatestBeeRelease | null latestBeeRelease: LatestBeeRelease | null
-101
View File
@@ -1,101 +0,0 @@
import { ChequebookAddressResponse } from '@ethersphere/bee-js'
import {
ChequebookBalance,
useApiChequebookAddress,
useApiChequebookBalance,
useApiHealth,
useApiNodeAddresses,
useApiNodeTopology,
useDebugApiHealth,
useLatestBeeRelease,
} from './apiHooks'
import semver from 'semver'
import { engines } from '../../package.json'
export interface StatusChequebookHook extends StatusHookCommon {
chequebookBalance: ChequebookBalance | null
chequebookAddress: ChequebookAddressResponse | null
}
export const useStatusNodeVersion = (): StatusNodeVersionHook => {
const { latestBeeRelease, isLoadingLatestBeeRelease } = useLatestBeeRelease()
const { nodeHealth, isLoadingNodeHealth } = useDebugApiHealth()
const latestVersion = semver.coerce(latestBeeRelease?.name)?.version
const latestUserVersion = semver.coerce(nodeHealth?.version)?.version
const isLatestBeeVersion = Boolean(
latestVersion &&
latestUserVersion &&
semver.satisfies(latestVersion, latestUserVersion, {
includePrerelease: true,
}),
)
return {
isLoading: isLoadingNodeHealth || isLoadingLatestBeeRelease,
isOk: Boolean(
nodeHealth &&
semver.satisfies(nodeHealth.version, engines.bee, {
includePrerelease: true,
}),
),
userVersion: nodeHealth?.version,
latestVersion,
latestUrl: latestBeeRelease?.html_url || 'https://github.com/ethersphere/bee/releases/latest',
isLatestBeeVersion,
}
}
export const useStatusEthereumConnection = (): StatusEthereumConnectionHook => {
const { isLoadingNodeAddresses, nodeAddresses } = useApiNodeAddresses()
return {
isLoading: isLoadingNodeAddresses,
isOk: Boolean(nodeAddresses?.ethereum),
nodeAddresses,
}
}
export const useStatusDebugConnection = (): StatusHookCommon => {
const { isLoadingNodeHealth, nodeHealth } = useDebugApiHealth()
return {
isLoading: isLoadingNodeHealth,
isOk: Boolean(nodeHealth?.status === 'ok'),
}
}
export const useStatusConnection = (): StatusHookCommon => {
const { isLoadingHealth, health } = useApiHealth()
return {
isLoading: isLoadingHealth,
isOk: health,
}
}
export const useStatusTopology = (): StatusTopologyHook => {
const { topology, isLoading } = useApiNodeTopology()
return {
isLoading,
isOk: Boolean(topology?.connected && topology?.connected > 0),
topology,
}
}
export const useStatusChequebook = (): StatusChequebookHook => {
const { chequebookAddress, isLoadingChequebookAddress } = useApiChequebookAddress()
const { chequebookBalance, isLoadingChequebookBalance } = useApiChequebookBalance()
return {
isLoading: isLoadingChequebookAddress || isLoadingChequebookBalance,
isOk:
Boolean(chequebookAddress?.chequebookAddress) &&
chequebookBalance !== null &&
chequebookBalance?.totalBalance.toBigNumber.isGreaterThan(0),
chequebookBalance,
chequebookAddress,
}
}
+22 -42
View File
@@ -1,69 +1,49 @@
import { useState, useEffect, ReactElement } from 'react' import { useContext, ReactElement } from 'react'
import ErrorBoundary from '../components/ErrorBoundary' import ErrorBoundary from '../components/ErrorBoundary'
import AlertVersion from '../components/AlertVersion' import AlertVersion from '../components/AlertVersion'
import { Container, CircularProgress } from '@material-ui/core'
import { createStyles, Theme, makeStyles } from '@material-ui/core/styles' import { createStyles, Theme, makeStyles } from '@material-ui/core/styles'
import SideBar from '../components/SideBar' import SideBar from '../components/SideBar'
import NavBar from '../components/NavBar'
import { useApiHealth, useDebugApiHealth } from '../hooks/apiHooks' import { Context } from '../providers/Bee'
import { RouteComponentProps } from 'react-router'
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
createStyles({ createStyles({
content: { content: {
marginLeft: '240px',
flexGrow: 1,
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
padding: theme.spacing(3), minHeight: '100vh',
paddingBottom: '65px',
}, },
}), }),
) )
interface Props extends RouteComponentProps { interface Props {
children?: ReactElement children?: ReactElement
} }
const Dashboard = (props: Props): ReactElement => { const Dashboard = (props: Props): ReactElement => {
const classes = useStyles() const classes = useStyles()
const [themeMode, toggleThemeMode] = useState('light') const { isLoading } = useContext(Context)
// FIXME: handle errrors and loading
const { health } = useApiHealth()
const { nodeHealth } = useDebugApiHealth()
useEffect(() => {
const theme = localStorage.getItem('theme')
if (theme) {
toggleThemeMode(String(localStorage.getItem('theme')))
} else if (window?.matchMedia('(prefers-color-scheme: dark)')?.matches) {
toggleThemeMode('dark')
}
window?.matchMedia('(prefers-color-scheme: dark)')?.addEventListener('change', e => {
toggleThemeMode(e?.matches ? 'dark' : 'light')
})
return () =>
window?.matchMedia('(prefers-color-scheme: dark)')?.removeEventListener('change', e => {
toggleThemeMode(e?.matches ? 'dark' : 'light')
})
}, [])
return ( return (
<div> <div style={{ display: 'flex' }}>
<SideBar {...props} themeMode={themeMode} health={health} nodeHealth={nodeHealth} /> <SideBar />
<NavBar themeMode={themeMode} /> <Container className={classes.content}>
<ErrorBoundary> <ErrorBoundary>
<main className={classes.content}> <>
<AlertVersion /> <AlertVersion />
{props.children} {isLoading ? (
</main> <div style={{ textAlign: 'center', width: '100%' }}>
</ErrorBoundary> <CircularProgress />
</div>
) : (
props.children
)}
</>
</ErrorBoundary>
</Container>
</div> </div>
) )
} }
-105
View File
@@ -1,105 +0,0 @@
import { ReactElement } from 'react'
import { createStyles, makeStyles } from '@material-ui/core/styles'
import { Card, CardContent, Typography, Theme } from '@material-ui/core/'
import { Skeleton } from '@material-ui/lab'
import WithdrawModal from '../../containers/WithdrawModal'
import DepositModal from '../../containers/DepositModal'
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
isLoading: boolean
}
function AccountCard({ totalreceived, totalsent, chequebookBalance, isLoading }: 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}>
{!isLoading && (
<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>
)}
{isLoading && (
<div className={classes.gridContainer}>
<Skeleton width={180} height={110} animation="wave" />
<Skeleton width={180} height={110} animation="wave" />
<Skeleton width={180} height={110} animation="wave" />
</div>
)}
</Card>
</div>
)
}
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>
)
}
+40 -60
View File
@@ -1,72 +1,52 @@
import type { ReactElement } from 'react' import { ReactElement, useContext } from 'react'
import { Theme, createStyles, makeStyles } from '@material-ui/core/styles'
import { Container, CircularProgress } from '@material-ui/core'
import AccountCard from '../accounting/AccountCard' import 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 { import { Context as SettingsContext } from '../../providers/Settings'
useApiNodeAddresses,
useApiChequebookAddress,
useApiChequebookBalance,
useApiHealth,
useDebugApiHealth,
} from '../../hooks/apiHooks'
import { useAccounting } from '../../hooks/accounting' import { useAccounting } from '../../hooks/accounting'
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 } =
useContext(BeeContext)
const { beeDebugApi } = useContext(SettingsContext)
const { chequebookAddress, isLoadingChequebookAddress } = useApiChequebookAddress() const { accounting, totalUncashed, isLoadingUncashed } = useAccounting(beeDebugApi, settlements, peerBalances)
const { chequebookBalance, isLoadingChequebookBalance } = useApiChequebookBalance()
const { nodeAddresses, isLoadingNodeAddresses } = useApiNodeAddresses()
const { health, isLoadingHealth } = useApiHealth()
const { nodeHealth, isLoadingNodeHealth } = useDebugApiHealth()
const { isLoading, totalsent, totalreceived, accounting, isLoadingUncashed, error } = useAccounting()
if (isLoadingHealth || isLoadingNodeHealth) { if (!status.all) return <TroubleshootConnectionCard />
return (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress />
</Container>
)
}
if (nodeHealth?.status !== 'ok' || !health) 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`} />
isLoading={isLoadingChequebookAddress || isLoading || isLoadingChequebookBalance} <ExpandableListItem
chequebookBalance={chequebookBalance} label="Available Uncommitted Balance"
totalsent={totalsent} value={`${chequebookBalance?.availableBalance.toFixedDecimal()} BZZ`}
totalreceived={totalreceived} />
/> <ExpandableListItem
<EthereumAddressCard label="Total Cheques Amount Sent"
nodeAddresses={nodeAddresses} value={`${settlements?.totalSent.toFixedDecimal()} BZZ`}
isLoadingNodeAddresses={isLoadingNodeAddresses} />
chequebookAddress={chequebookAddress} <ExpandableListItem
isLoadingChequebookAddress={isLoadingChequebookAddress} label="Total Cheques Amount Received"
/> value={`${settlements?.totalReceived.toFixedDecimal()} BZZ`}
{error && ( />
<Container style={{ textAlign: 'center', padding: '50px' }}> <ExpandableListItemActions>
Error loading accounting details: {error.message} <WithdrawModal />
</Container> <DepositModal />
)} </ExpandableListItemActions>
{!error && <BalancesTable accounting={accounting} isLoadingUncashed={isLoadingUncashed} />} </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 -52
View File
@@ -1,65 +1,92 @@
import { ReactElement, useState } 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 { apiHost } from '../../constants'
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 [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={`${apiHost}/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>
)
}
+70 -88
View File
@@ -1,110 +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 as SettingsContext } from '../../providers/Settings'
import { Context, EnrichedPostageBatch } from '../../providers/Stamps' import { Context, EnrichedPostageBatch } from '../../providers/Stamps'
import { beeApi } from '../../services/bee' import { ROUTES } from '../../routes'
import CreatePostageStamp from '../stamps/CreatePostageStampModal' import { detectIndexHtml, getAssetNameFromFiles } from '../../utils/file'
import SelectStamp from './SelectStamp' 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 { beeApi } = useContext(SettingsContext)
const [file, setFile] = useState<File | null>(null) const { files, setFiles } = useContext(FileContext)
const [uploadReference, setUploadReference] = useState('')
const [isUploadingFile, setIsUploadingFile] = useState(false)
const [selectedStamp, setSelectedStamp] = useState<EnrichedPostageBatch | null>(null)
const { isLoading, error, stamps } = useContext(Context)
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
setIsUploadingFile(true) setUploading(true)
beeApi.files
.uploadFile(selectedStamp.batchID, file) beeApi
.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} />
</>
)
}
-41
View File
@@ -1,41 +0,0 @@
import { ReactElement } from 'react'
import { Container, CircularProgress } from '@material-ui/core'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { useApiHealth, useDebugApiHealth } from '../../hooks/apiHooks'
import Download from './Download'
import Upload from './Upload'
import TabsContainer from '../../components/TabsContainer'
export default function Files(): ReactElement {
const { health, isLoadingHealth } = useApiHealth()
const { nodeHealth, isLoadingNodeHealth } = useDebugApiHealth()
if (isLoadingHealth || isLoadingNodeHealth) {
return (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress />
</Container>
)
}
if (!health || nodeHealth?.status !== 'ok') return <TroubleshootConnectionCard />
return (
<Container maxWidth="sm">
<TabsContainer
values={[
{
label: 'download',
component: <Download />,
},
{
label: 'upload',
component: <Upload />,
},
]}
/>
</Container>
)
}
+60
View File
@@ -0,0 +1,60 @@
import { ReactElement, useContext } from 'react'
import { Button } from '@material-ui/core'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { Context as BeeContext } from '../../providers/Bee'
import ExpandableList from '../../components/ExpandableList'
import ExpandableListItem from '../../components/ExpandableListItem'
import ExpandableListItemKey from '../../components/ExpandableListItemKey'
import TopologyStats from '../../components/TopologyStats'
export default function Status(): ReactElement {
const {
status,
latestUserVersion,
isLatestBeeVersion,
latestBeeVersionUrl,
topology,
nodeAddresses,
chequebookAddress,
} = useContext(BeeContext)
if (!status.all) return <TroubleshootConnectionCard />
return (
<div>
<ExpandableList label="Bee Node" defaultOpen>
<ExpandableListItem
label="Agent"
value={
<div>
<a href="https://github.com/ethersphere/bee" rel="noreferrer" target="_blank">
Bee
</a>
{` ${latestUserVersion || '-'} `}
<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>
)
}
-113
View File
@@ -1,113 +0,0 @@
import React, { ReactElement, useState } from 'react'
import { makeStyles } from '@material-ui/core/styles'
import {
Table,
TableBody,
TableCell,
TableContainer,
TableRow,
TableHead,
Button,
Paper,
Tooltip,
Container,
CircularProgress,
} from '@material-ui/core'
import { Autorenew } from '@material-ui/icons'
import { beeDebugApi } from '../../services/bee'
import type { Peer } from '@ethersphere/bee-js'
const useStyles = makeStyles({
table: {
minWidth: 650,
},
})
interface Props {
peers: Peer[] | null
isLoading: boolean
error: Error | null
}
function PeerTable(props: Props): ReactElement {
const classes = useStyles()
const [peerLatency, setPeerLatency] = useState([{ peerId: '', rtt: '', loading: false }])
const PingPeer = (peerId: string) => {
setPeerLatency([...peerLatency, { peerId: peerId, rtt: '', loading: true }])
beeDebugApi.connectivity
.ping(peerId)
.then(res => {
setPeerLatency([...peerLatency, { peerId: peerId, rtt: res.rtt, loading: false }])
})
.catch(() => {
setPeerLatency([...peerLatency, { peerId: peerId, rtt: 'error', loading: false }])
})
}
if (props.isLoading) {
return (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress />
</Container>
)
}
if (props.error || props.peers === null) {
return (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<p>Failed to load peers</p>
</Container>
)
}
return (
<div>
<TableContainer component={Paper}>
<Table className={classes.table} aria-label="simple table">
<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)}>
{
// FIXME: this should be broken up
/* eslint-disable no-nested-ternary */
peerLatency.find(item => item.peerId === peer.address) ? (
peerLatency.filter(item => item.peerId === peer.address)[0].loading ? (
<CircularProgress size={20} />
) : (
peerLatency.filter(item => item.peerId === peer.address)[0].rtt
)
) : (
<Autorenew />
)
/* eslint-enable no-nested-ternary */
}
</Button>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</div>
)
}
export default PeerTable
-32
View File
@@ -1,32 +0,0 @@
import { Container, CircularProgress } from '@material-ui/core/'
import PeerTable from './PeerTable'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { useApiNodeTopology, useApiNodePeers, useDebugApiHealth } from '../../hooks/apiHooks'
import TopologyStats from '../../components/TopologyStats'
import { ReactElement } from 'react'
export default function Peers(): ReactElement {
const topology = useApiNodeTopology()
const debugHealth = useDebugApiHealth()
const peers = useApiNodePeers()
if (debugHealth.isLoadingNodeHealth) {
return (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress />
</Container>
)
}
if (debugHealth.error) {
return <TroubleshootConnectionCard />
}
return (
<>
<TopologyStats {...topology} />
<PeerTable {...peers} />
</>
)
}
+9 -78
View File
@@ -1,84 +1,15 @@
import React, { ReactElement, useState } from 'react' import { ReactElement, useContext } from 'react'
import { Paper, Container, TextField, Typography, Button } from '@material-ui/core' import { Context as SettingsContext } from '../../providers/Settings'
import ExpandableList from '../../components/ExpandableList'
import ExpandableListItemInput from '../../components/ExpandableListItemInput'
export default function Settings(): ReactElement { export default function Settings(): ReactElement {
const [refreshVisibility, toggleRefreshVisibility] = useState(false) const { apiUrl, apiDebugUrl, setApiUrl, setDebugApiUrl } = useContext(SettingsContext)
const [host, setHost] = useState('')
const [debugHost, setDebugHost] = useState('')
const handleNewHostConnection = () => {
if (host) {
sessionStorage.setItem('api_host', host)
}
if (debugHost) {
sessionStorage.setItem('debug_api_host', debugHost)
}
if (host || debugHost) {
toggleRefreshVisibility(!refreshVisibility)
window.location.reload()
}
}
return ( return (
<div> <ExpandableList label="API Settings" defaultOpen>
<Container> <ExpandableListItemInput label="Bee API" value={apiUrl} onConfirm={setApiUrl} />
<Typography variant="h4" gutterBottom> <ExpandableListItemInput label="Bee Debug API" value={apiDebugUrl} onConfirm={setDebugApiUrl} />
Settings </ExpandableList>
</Typography>
<Paper>
<TextField
label="API Endpoint"
style={{ margin: 0 }}
placeholder="ex: 127.0.0.0.1:1633"
helperText="Enter node host override / port"
fullWidth
defaultValue={
sessionStorage.getItem('api_host') ? sessionStorage.getItem('api_host') : process.env.REACT_APP_BEE_HOST
}
margin="normal"
InputLabelProps={{
shrink: true,
}}
onChange={e => {
setHost(e.target.value)
toggleRefreshVisibility(true)
}}
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={
sessionStorage.getItem('debug_api_host')
? sessionStorage.getItem('debug_api_host')
: process.env.REACT_APP_BEE_DEBUG_HOST
}
onChange={e => {
setDebugHost(e.target.value)
toggleRefreshVisibility(true)
}}
margin="normal"
InputLabelProps={{
shrink: true,
}}
variant="filled"
/>
</Paper>
{refreshVisibility ? (
<div style={{ marginTop: '20px' }}>
<Button variant="outlined" color="primary" onClick={() => handleNewHostConnection()}>
Save
</Button>
</div>
) : null}
</Container>
</div>
) )
} }
+22 -29
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 { beeApi } from '../../services/bee'
import { Context } from '../../providers/Stamps'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import { useSnackbar } from 'notistack' import { useSnackbar } from 'notistack'
import React, { ReactElement, useContext } from 'react'
import { Context as SettingsContext } from '../../providers/Settings'
import { Context } from '../../providers/Stamps'
interface FormValues { interface FormValues {
depth?: string depth?: string
@@ -47,15 +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 handleClickOpen = () => setOpen(true) const { beeDebugApi } = useContext(SettingsContext)
const handleClose = () => setOpen(false)
const { enqueueSnackbar } = useSnackbar() const { enqueueSnackbar } = useSnackbar()
return ( return (
@@ -66,15 +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 (!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.stamps.buyPostageStamp(amount, 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)
} }
}} }}
@@ -108,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
@@ -135,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"
@@ -151,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>
) )
} }
+24 -30
View File
@@ -1,27 +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 as BeeContext } from '../../providers/Bee'
import LastUpdate from '../../components/LastUpdate' import { Context as StampsContext } from '../../providers/Stamps'
import { CreatePostageStampModal } from './CreatePostageStampModal'
import StampsTable from './StampsTable'
import { useApiHealth, useDebugApiHealth } from '../../hooks/apiHooks' const useStyles = makeStyles(() =>
import { Context } from '../../providers/Stamps'
const useStyles = makeStyles((theme: Theme) =>
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',
@@ -29,27 +25,22 @@ const useStyles = makeStyles((theme: Theme) =>
}), }),
) )
export default function Accounting(): ReactElement { export default function Stamp(): ReactElement {
const classes = useStyles() const classes = useStyles()
const { health, isLoadingHealth } = useApiHealth() const [isBuyingStamp, setBuyingStamp] = useState(false)
const { nodeHealth, isLoadingNodeHealth } = useDebugApiHealth()
const { stamps, isLoading, error, lastUpdate, 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 (isLoadingHealth || isLoadingNodeHealth) { if (!status.all) return <TroubleshootConnectionCard />
return (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress />
</Container>
)
}
if (nodeHealth?.status !== 'ok' || !health) return <TroubleshootConnectionCard />
return ( return (
<div className={classes.root}> <div className={classes.root}>
@@ -61,8 +52,11 @@ export default function Accounting(): ReactElement {
{!error && ( {!error && (
<> <>
<div className={classes.actions}> <div className={classes.actions}>
<CreatePostageStampModal /> {isBuyingStamp ? <CreatePostageStampModal onClose={() => setBuyingStamp(false)} /> : null}
<LastUpdate date={lastUpdate} />
<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} />
-178
View File
@@ -1,178 +0,0 @@
import { ReactElement, useEffect, useState } from 'react'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import { Typography, Paper, Button, Step, StepLabel, StepContent, Stepper, StepButton } from '@material-ui/core/'
import { CheckCircle, Error, Sync, ExpandLessSharp, ExpandMoreSharp, Autorenew } from '@material-ui/icons/'
import DebugConnectionCheck from './SetupSteps/DebugConnectionCheck'
import NodeConnectionCheck from './SetupSteps/NodeConnectionCheck'
import VersionCheck from './SetupSteps/VersionCheck'
import EthereumConnectionCheck from './SetupSteps/EthereumConnectionCheck'
import ChequebookDeployFund from './SetupSteps/ChequebookDeployFund'
import PeerConnection from './SetupSteps/PeerConnection'
import { StatusChequebookHook } from '../../hooks/status'
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
isLoading: boolean
component: ReactElement
}
interface Props {
nodeVersion: StatusNodeVersionHook
ethereumConnection: StatusEthereumConnectionHook
debugApiConnection: StatusHookCommon
apiConnection: StatusHookCommon
topology: StatusTopologyHook
chequebook: StatusChequebookHook
}
export default function NodeSetupWorkflow({
nodeVersion,
ethereumConnection,
debugApiConnection,
apiConnection,
topology,
chequebook,
}: Props): ReactElement {
const classes = useStyles()
const [activeStep, setActiveStep] = useState(-1)
const steps: Step[] = [
{
label: 'Connected to Node DebugAPI',
isOk: debugApiConnection.isOk,
isLoading: debugApiConnection.isLoading,
component: <DebugConnectionCheck {...debugApiConnection} />,
},
{
label: 'Running latest Bee version',
isOk: nodeVersion.isOk,
isLoading: nodeVersion.isLoading,
component: <VersionCheck {...nodeVersion} />,
},
{
label: 'Connected to Ethereum Blockchain',
isOk: ethereumConnection.isOk,
isLoading: ethereumConnection.isLoading,
component: <EthereumConnectionCheck {...ethereumConnection} />,
},
{
label: 'Deployed and Funded Chequebook',
isOk: chequebook.isOk,
isLoading: chequebook.isLoading,
component: <ChequebookDeployFund ethereumAddress={ethereumConnection.nodeAddresses?.ethereum} {...chequebook} />,
},
{
label: 'Connected to Node API',
isOk: apiConnection.isOk,
isLoading: apiConnection.isLoading,
component: <NodeConnectionCheck {...apiConnection} />,
},
{
label: 'Connected to Peers',
isOk: topology.isOk,
isLoading: topology.isLoading,
component: <PeerConnection {...topology} />,
},
]
useEffect(() => {
// If the user already changed the active step we don't want to overwrite it
if (activeStep >= 0 && activeStep < steps.length) return
// If any step is not fully loaded yet return
if (!steps.every(step => !step.isLoading)) return
// Select first step that is not OK
// This is deliberately a for loop (and not forEach) so that we can terminate the useEffect from within the cycle
for (let i = 0; i < steps.length; ++i) {
if (!steps[i].isOk) {
setActiveStep(i)
return
}
}
}, [steps])
const handleNext = () => {
setActiveStep(prevActiveStep => prevActiveStep + 1)
}
const handleBack = () => {
setActiveStep(prevActiveStep => prevActiveStep - 1)
}
return (
<Paper className={classes.root}>
<Typography variant="h5" gutterBottom>
Node Setup
<span style={{ marginLeft: '25px' }}>
<Button variant="outlined" size="small" onClick={() => window.location.reload()}>
<Sync />
<span style={{ marginLeft: '7px' }}>Refresh Checks</span>
</Button>
</span>
</Typography>
<Stepper nonLinear activeStep={activeStep} orientation="vertical">
{steps.map(({ label, isOk, component, isLoading }, index) => (
<Step key={label}>
<StepLabel
disabled={isLoading}
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>
)
}
@@ -1,40 +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 type { StatusChequebookHook } from '../../../hooks/status' 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 StatusChequebookHook { const ChequebookDeployFund = (): ReactElement | null => {
ethereumAddress?: string const { status, isLoading, chequebookAddress } = useContext(Context)
} const isOk = status.chequebook
const ChequebookDeployFund = ({ isLoading, chequebookAddress, chequebookBalance }: Props): ReactElement | null => {
if (isLoading) return null
return ( return (
<div> <ExpandableList
<p style={{ marginBottom: '20px', display: 'flex' }}> label={
{chequebookAddress?.chequebookAddress && <DepositModal />} <>
</p> <StatusIcon isOk={isOk} isLoading={isLoading} /> Chequebook Deployment & Funding
<div style={{ marginBottom: '10px' }}> </>
{!(chequebookAddress?.chequebookAddress && chequebookBalance?.totalBalance.toBigNumber.isGreaterThan(0)) && ( }
<div> >
<span> <ExpandableListItemNote>
Your chequebook is either not deployed or funded. To run the node you will need xDAI and xBZZ on the xDai {isOk ? (
network. You may need to aquire BZZ through (e.g. <a href="https://bzz.exchange/">bzz.exchange</a>) and 'Your chequebook is deployed and funded'
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{' '} Your chequebook is either not deployed or funded. To run the node you will need xDAI and xBZZ on the xDai
<a href="https://www.xdaichain.com/#xdai-stable-chain">official xDai website</a> for more information. network. You may need to aquire BZZ (e.g. <a href="https://bzz.exchange/">bzz.exchange</a>) and bridge it to
</span> the xDai network through the <a href="https://omni.xdaichain.com/bridge">omni bridge</a>. To pay the
</div> 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?.chequebookAddress} /> <ExpandableListItemActions>
</div> <DepositModal />
</ExpandableListItemActions>
</>
)}
</ExpandableList>
) )
} }
@@ -1,93 +1,86 @@
import type { ReactElement } 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 { debugApiHost } from '../../../constants' 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'
type Props = StatusHookCommon export default function NodeConnectionCheck(): ReactElement | null {
const { status, isLoading } = useContext(Context)
export default function NodeConnectionCheck({ isLoading, isOk }: Props): ReactElement | null { const { setDebugApiUrl, apiDebugUrl } = useContext(SettingsContext)
if (isLoading) return null const isOk = status.debugApiConnection
const changeDebugApiUrl = (
<div style={{ display: 'flex', marginTop: '25px', marginBottom: '25px' }}>
<span style={{ marginRight: '15px' }}>
Debug API (<Typography variant="button">{debugApiHost}</Typography>)
</span>
<ConnectToHost hostName={'debug_api_host'} defaultHost={debugApiHost} />
</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">{debugApiHost}</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,36 +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({ isLoading, isOk, nodeAddresses }: Props): ReactElement | null { const isOk = status.blockchainConnection
if (isLoading) return null
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 React, { ReactElement } from 'react' import { ReactElement, useContext } from 'react'
import { Typography, Accordion, AccordionSummary, AccordionDetails } from '@material-ui/core/'
import { ExpandMoreSharp } from '@material-ui/icons/'
import ConnectToHost from '../../../components/ConnectToHost'
import CodeBlockTabs from '../../../components/CodeBlockTabs' import CodeBlockTabs from '../../../components/CodeBlockTabs'
import { apiHost } from '../../../constants' 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 {
const { setApiUrl, apiUrl } = useContext(SettingsContext)
export default function NodeConnectionCheck({ isLoading, isOk }: Props): ReactElement | null { const { status, isLoading } = useContext(Context)
if (isLoading) return null const isOk = status.apiConnection
return ( return (
<div> <ExpandableList
<div style={{ display: 'flex', marginBottom: '25px' }}> label={
<span style={{ marginRight: '15px' }}> <>
Node API (<Typography variant="button">{apiHost}</Typography>) <StatusIcon isOk={isOk} isLoading={isLoading} /> Connection to Bee API
</span> </>
<ConnectToHost hostName="api_host" defaultHost={apiHost} /> }
</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">{apiHost}</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 -40
View File
@@ -1,45 +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({ isLoading, isOk, topology }: Props): ReactElement | null { const isOk = status.topology
if (isLoading) return null
const peers = (
<div style={{ display: 'flex', marginTop: '15px' }}>
<div style={{ marginRight: '30px' }}>
<Typography component="div" variant="subtitle1" gutterBottom color="textSecondary">
<span>Connected Peers</span>
</Typography>
<Typography component="h2" variant="h5">
{topology?.connected ? topology.connected : '-'}
</Typography>
</div>
<div>
<Typography component="div" variant="subtitle1" gutterBottom color="textSecondary">
<span>Discovered Nodes</span>
</Typography>
<Typography component="h2" variant="h5">
{topology?.population ? topology.population : '-'}
</Typography>
</div>
</div>
)
if (isOk) {
return (
<>
<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 -62
View File
@@ -1,68 +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({
isLoading,
isOk,
userVersion,
latestVersion,
latestUrl,
}: Props): ReactElement | null {
if (isLoading) return 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>
) )
} }
-129
View File
@@ -1,129 +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 { CheckCircle, Error, ArrowRight, ArrowDropUp } from '@material-ui/icons/'
import { NodeAddresses, Topology } from '@ethersphere/bee-js'
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,
isOk,
isLatestBeeVersion,
latestUrl,
}: Props): ReactElement | null {
const classes = useStyles()
const [underlayAddressesVisible, setUnderlayAddresessVisible] = useState<boolean>(false)
return (
<Card>
<CardContent className={classes.root}>
<Typography component="h5" variant="h5" style={{ display: 'flex', justifyContent: 'space-between' }}>
{isOk && (
<div>
<CheckCircle style={{ color: '#32c48d', marginRight: '7px' }} />
<span>Your Bee node is running as expected</span>
</div>
)}
{!isOk && (
<div>
<Error style={{ color: '#c9201f', marginRight: '7px' }} />
<span>Could not connect to Bee Node</span>
</div>
)}
</Typography>
{isOk && (
<>
<div style={{ marginBottom: '20px' }}>
<span style={{ marginRight: '20px' }}>Discovered Nodes: {nodeTopology?.population}</span>
<span style={{ marginRight: '20px' }}>
<span>Connected Peers: </span>
<Link to="/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
+15 -70
View File
@@ -1,76 +1,21 @@
import { ReactElement } from 'react' import type { ReactElement } from 'react'
import { Container, CircularProgress } from '@material-ui/core'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import NodeSetupWorkflow from './NodeSetupWorkflow' import DebugConnectionCheck from './SetupSteps/DebugConnectionCheck'
import StatusCard from './StatusCard' import NodeConnectionCheck from './SetupSteps/NodeConnectionCheck'
import EthereumAddressCard from '../../components/EthereumAddressCard' import VersionCheck from './SetupSteps/VersionCheck'
import { import EthereumConnectionCheck from './SetupSteps/EthereumConnectionCheck'
useStatusEthereumConnection, import ChequebookDeployFund from './SetupSteps/ChequebookDeployFund'
useStatusNodeVersion, import PeerConnection from './SetupSteps/PeerConnection'
useStatusDebugConnection,
useStatusConnection,
useStatusTopology,
useStatusChequebook,
} from '../../hooks/status'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
width: '100%',
display: 'grid',
rowGap: theme.spacing(3),
},
}),
)
export default function Status(): ReactElement {
const classes = useStyles()
const nodeVersion = useStatusNodeVersion()
const ethereumConnection = useStatusEthereumConnection()
const debugApiConnection = useStatusDebugConnection()
const apiConnection = useStatusConnection()
const topology = useStatusTopology()
const chequebook = useStatusChequebook()
const checks = [nodeVersion, ethereumConnection, debugApiConnection, apiConnection, topology, chequebook]
// If any check data are still loading
if (!checks.every(c => !c.isLoading)) {
return (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress />
</Container>
)
}
export default function NodeSetupWorkflow(): ReactElement {
return ( return (
<div className={classes.root}> <div>
<StatusCard <DebugConnectionCheck />
userBeeVersion={nodeVersion.userVersion} <VersionCheck />
isLatestBeeVersion={nodeVersion.isLatestBeeVersion} <EthereumConnectionCheck />
isOk={checks.every(c => c.isOk)} <ChequebookDeployFund />
nodeTopology={topology.topology} <NodeConnectionCheck />
latestUrl={nodeVersion.latestUrl} <PeerConnection />
nodeAddresses={ethereumConnection.nodeAddresses}
/>
{ethereumConnection.nodeAddresses && chequebook.chequebookAddress && (
<EthereumAddressCard
nodeAddresses={ethereumConnection.nodeAddresses}
isLoadingNodeAddresses={ethereumConnection.isLoading}
chequebookAddress={chequebook.chequebookAddress}
isLoadingChequebookAddress={chequebook.isLoading}
/>
)}
<NodeSetupWorkflow
nodeVersion={nodeVersion}
ethereumConnection={ethereumConnection}
debugApiConnection={debugApiConnection}
apiConnection={apiConnection}
topology={topology}
chequebook={chequebook}
/>
</div> </div>
) )
} }
+355
View File
@@ -0,0 +1,355 @@
import type {
ChequebookAddressResponse,
Health,
LastChequesResponse,
NodeAddresses,
Peer,
Topology,
} 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 { Token } from '../models/Token'
import type { Balance, ChequebookBalance, Settlements } from '../types'
import { Context as SettingsContext } from './Settings'
interface Status {
all: boolean
version: boolean
blockchainConnection: boolean
debugApiConnection: boolean
apiConnection: boolean
topology: boolean
chequebook: boolean
}
interface ContextInterface {
status: Status
latestPublishedVersion?: string
latestUserVersion?: string
latestUserVersionExact?: string
isLatestBeeVersion: boolean
latestBeeVersionUrl: string
error: Error | null
apiHealth: boolean
debugApiHealth: Health | null
nodeAddresses: NodeAddresses | null
topology: Topology | null
chequebookAddress: ChequebookAddressResponse | null
peers: Peer[] | null
chequebookBalance: ChequebookBalance | null
peerBalances: Balance[] | null
peerCheques: LastChequesResponse | null
settlements: Settlements | null
latestBeeRelease: LatestBeeRelease | null
isLoading: boolean
isRefreshing: boolean
lastUpdate: number | null
start: (frequency?: number) => void
stop: () => void
refresh: () => Promise<void>
}
const startedInDevMode = window.location.search.includes('devMode=1')
const initialValues: ContextInterface = {
status: {
all: false,
version: false,
blockchainConnection: false,
debugApiConnection: false,
apiConnection: false,
topology: false,
chequebook: false,
},
latestPublishedVersion: undefined,
latestUserVersion: undefined,
latestUserVersionExact: undefined,
isLatestBeeVersion: false,
latestBeeVersionUrl: 'https://github.com/ethersphere/bee/releases/latest',
error: null,
apiHealth: false,
debugApiHealth: null,
nodeAddresses: null,
topology: null,
chequebookAddress: null,
peers: null,
chequebookBalance: null,
peerBalances: null,
peerCheques: null,
settlements: null,
latestBeeRelease: null,
isLoading: true,
isRefreshing: false,
lastUpdate: null,
start: () => {}, // eslint-disable-line
stop: () => {}, // eslint-disable-line
refresh: () => Promise.reject(),
}
export const Context = createContext<ContextInterface>(initialValues)
export const Consumer = Context.Consumer
interface Props {
children: ReactChild
}
function getStatus(
debugApiHealth: Health | null,
nodeAddresses: NodeAddresses | null,
apiHealth: boolean,
topology: Topology | null,
chequebookAddress: ChequebookAddressResponse | null,
chequebookBalance: ChequebookBalance | null,
error: Error | null,
): Status {
// 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 = {
version: Boolean(
debugApiHealth &&
semver.satisfies(debugApiHealth.version, engines.bee, {
includePrerelease: true,
}),
),
blockchainConnection: Boolean(nodeAddresses?.ethereum),
debugApiConnection: Boolean(debugApiHealth?.status === 'ok'),
apiConnection: apiHealth,
topology: Boolean(topology?.connected && topology?.connected > 0) || devMode,
chequebook:
(Boolean(chequebookAddress?.chequebookAddress) &&
chequebookBalance !== null &&
chequebookBalance?.totalBalance.toBigNumber.isGreaterThan(0)) ||
devMode,
}
return { ...status, all: !error && Object.values(status).every(v => v) }
}
export function Provider({ children }: Props): ReactElement {
const { beeApi, beeDebugApi } = useContext(SettingsContext)
const [apiHealth, setApiHealth] = useState<boolean>(false)
const [debugApiHealth, setDebugApiHealth] = useState<Health | null>(null)
const [nodeAddresses, setNodeAddresses] = useState<NodeAddresses | null>(null)
const [topology, setNodeTopology] = useState<Topology | null>(null)
const [chequebookAddress, setChequebookAddress] = useState<ChequebookAddressResponse | null>(null)
const [peers, setPeers] = useState<Peer[] | null>(null)
const [chequebookBalance, setChequebookBalance] = useState<ChequebookBalance | null>(null)
const [peerBalances, setPeerBalances] = useState<Balance[] | null>(null)
const [peerCheques, setPeerCheques] = useState<LastChequesResponse | null>(null)
const [settlements, setSettlements] = useState<Settlements | null>(null)
const { latestBeeRelease } = useLatestBeeRelease()
const [error, setError] = useState<Error | null>(initialValues.error)
const [isLoading, setIsLoading] = useState<boolean>(initialValues.isLoading)
const [isRefreshing, setIsRefreshing] = useState<boolean>(initialValues.isRefreshing)
const [lastUpdate, setLastUpdate] = useState<number | null>(initialValues.lastUpdate)
const [frequency, setFrequency] = useState<number | null>(30000)
const latestPublishedVersion = semver.coerce(latestBeeRelease?.name)?.version
const latestUserVersion = semver.coerce(debugApiHealth?.version)?.version
const latestUserVersionExact = debugApiHealth?.version
useEffect(() => {
setIsLoading(true)
setApiHealth(false)
refresh()
}, [beeApi]) // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
setIsLoading(true)
setDebugApiHealth(null)
setNodeAddresses(null)
setNodeTopology(null)
setPeers(null)
setChequebookAddress(null)
setChequebookBalance(null)
setPeerBalances(null)
setPeerCheques(null)
setSettlements(null)
refresh()
}, [beeDebugApi]) // eslint-disable-line react-hooks/exhaustive-deps
const refresh = async () => {
// Don't want to refresh when already refreshing
if (isRefreshing) return
// Not a valid bee api
if (!beeApi || !beeDebugApi) {
setIsLoading(false)
return
}
try {
setIsRefreshing(true)
setError(null)
// Wrap the chequebook balance call to return BZZ values as Token object
const chequeBalanceWrapper = async () => {
const { totalBalance, availableBalance } = await beeDebugApi.getChequebookBalance()
return {
totalBalance: new Token(totalBalance),
availableBalance: new Token(availableBalance),
}
}
// Wrap the balances call to return BZZ values as Token object
const peerBalanceWrapper = async () => {
const { balances } = await beeDebugApi.getAllBalances()
return balances.map(({ peer, balance }) => ({ peer, balance: new Token(balance) }))
}
// Wrap the settlements call to return BZZ values as Token object
const settlementsWrapper = async () => {
const { totalReceived, settlements, totalSent } = await beeDebugApi.getAllSettlements()
return {
totalReceived: new Token(totalReceived),
totalSent: new Token(totalSent),
settlements: settlements.map(({ peer, received, sent }) => ({
peer,
received: new Token(received),
sent: new Token(sent),
})),
}
}
const promises = [
// API health
beeApi
.isConnected()
.then(setApiHealth)
.catch(() => setApiHealth(false)),
// Debug API health
beeDebugApi
.getHealth()
.then(setDebugApiHealth)
.catch(() => setDebugApiHealth(null)),
// Node Addresses
beeDebugApi
.getNodeAddresses()
.then(setNodeAddresses)
.catch(() => setNodeAddresses(null)),
// Network Topology
beeDebugApi
.getTopology()
.then(setNodeTopology)
.catch(() => setNodeTopology(null)),
// Peers
beeDebugApi
.getPeers()
.then(setPeers)
.catch(() => setPeers(null)),
// Chequebook address
beeDebugApi
.getChequebookAddress()
.then(setChequebookAddress)
.catch(() => setChequebookAddress(null)),
// Cheques
beeDebugApi
.getLastCheques()
.then(setPeerCheques)
.catch(() => setPeerCheques(null)),
// Chequebook balance
chequeBalanceWrapper()
.then(setChequebookBalance)
.catch(() => setChequebookBalance(null)),
// Peer balances
peerBalanceWrapper()
.then(setPeerBalances)
.catch(() => setPeerBalances(null)),
// Settlements
settlementsWrapper()
.then(setSettlements)
.catch(() => setSettlements(null)),
]
await Promise.allSettled(promises)
} catch (e) {
setError(e as Error)
} finally {
setIsLoading(false)
setIsRefreshing(false)
setLastUpdate(Date.now())
}
}
const start = (freq = 30000) => setFrequency(freq)
const stop = () => setFrequency(null)
// Start the update loop
useEffect(() => {
refresh()
// Start autorefresh only if the frequency is set
if (frequency) {
const interval = setInterval(refresh, frequency)
return () => clearInterval(interval)
}
}, [frequency, beeDebugApi, beeApi]) // eslint-disable-line react-hooks/exhaustive-deps
return (
<Context.Provider
value={{
status: getStatus(
debugApiHealth,
nodeAddresses,
apiHealth,
topology,
chequebookAddress,
chequebookBalance,
error,
),
latestUserVersion,
latestUserVersionExact,
latestPublishedVersion,
isLatestBeeVersion: Boolean(
latestPublishedVersion &&
latestUserVersion &&
semver.satisfies(latestPublishedVersion, latestUserVersion, {
includePrerelease: true,
}),
),
latestBeeVersionUrl: latestBeeRelease?.html_url || 'https://github.com/ethersphere/bee/releases/latest',
error,
apiHealth,
debugApiHealth,
nodeAddresses,
topology,
chequebookAddress,
peers,
chequebookBalance,
peerBalances,
peerCheques,
settlements,
latestBeeRelease,
isLoading,
isRefreshing,
lastUpdate,
start,
stop,
refresh,
}}
>
{children}
</Context.Provider>
)
}
+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>
}
+69
View File
@@ -0,0 +1,69 @@
import { createContext, ReactChild, ReactElement, useState, useEffect } from 'react'
// These need to be numeric values as they are used as indexes in the TabsContainer
export enum Platforms {
macOS = 0,
Linux,
Windows,
iOS,
Android,
}
export enum SupportedPlatforms {
macOS = Platforms.macOS,
Linux = Platforms.Linux,
}
interface ContextInterface {
platform: SupportedPlatforms
setPlatform: (platform: SupportedPlatforms) => void
}
const initialValues: ContextInterface = {
platform: SupportedPlatforms.macOS,
setPlatform: () => {}, // eslint-disable-line
}
export const Context = createContext<ContextInterface>(initialValues)
export const Consumer = Context.Consumer
interface Props {
children: ReactChild
}
function isSupportedPlatform(platform: unknown): platform is SupportedPlatforms {
return Object.keys(SupportedPlatforms).includes(platform as string)
}
function getOS(): Platforms | null {
const userAgent = window.navigator.userAgent
const platform = window.navigator.platform
const macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K']
const windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE']
const iosPlatforms = ['iPhone', 'iPad', 'iPod']
if (macosPlatforms.includes(platform)) return Platforms.macOS
if (iosPlatforms.includes(platform)) return Platforms.iOS
if (windowsPlatforms.includes(platform)) return Platforms.Windows
if (/Android/.test(userAgent)) return Platforms.Android
if (/Linux/.test(platform)) return Platforms.Linux
return null
}
export function Provider({ children }: Props): ReactElement {
const [platform, setPlatform] = useState<SupportedPlatforms>(SupportedPlatforms.Linux)
// This is in useEffect as it really just needs to run once and not on each re-render
useEffect(() => {
const os = getOS()
setPlatform(isSupportedPlatform(os) ? os : SupportedPlatforms.Linux)
}, [])
return <Context.Provider value={{ platform, setPlatform }}>{children}</Context.Provider>
}
+59
View File
@@ -0,0 +1,59 @@
import { createContext, ReactChild, ReactElement, useState, useEffect } from 'react'
import { Bee, BeeDebug } from '@ethersphere/bee-js'
interface ContextInterface {
apiUrl: string
apiDebugUrl: string
beeApi: Bee | null
beeDebugApi: BeeDebug | null
setApiUrl: (url: string) => void
setDebugApiUrl: (url: string) => void
}
const initialValues: ContextInterface = {
apiUrl: sessionStorage.getItem('api_host') || process.env.REACT_APP_BEE_HOST || 'http://localhost:1633',
apiDebugUrl:
sessionStorage.getItem('debug_api_host') || process.env.REACT_APP_BEE_DEBUG_HOST || 'http://localhost:1635',
beeApi: null,
beeDebugApi: null,
setApiUrl: () => {}, // eslint-disable-line
setDebugApiUrl: () => {}, // eslint-disable-line
}
export const Context = createContext<ContextInterface>(initialValues)
export const Consumer = Context.Consumer
interface Props {
children: ReactChild
}
export function Provider({ children }: Props): ReactElement {
const [apiUrl, setApiUrl] = useState<string>(initialValues.apiUrl)
const [apiDebugUrl, setDebugApiUrl] = useState<string>(initialValues.apiDebugUrl)
const [beeApi, setBeeApi] = useState<Bee | null>(null)
const [beeDebugApi, setBeeDebugApi] = useState<BeeDebug | null>(null)
useEffect(() => {
try {
setBeeApi(new Bee(apiUrl))
sessionStorage.setItem('api_host', apiUrl)
} catch (e) {
setBeeApi(null)
}
}, [apiUrl])
useEffect(() => {
try {
setBeeDebugApi(new BeeDebug(apiDebugUrl))
sessionStorage.setItem('debug_api_host', apiDebugUrl)
} catch (e) {
setBeeDebugApi(null)
}
}, [apiDebugUrl])
return (
<Context.Provider value={{ apiUrl, apiDebugUrl, beeApi, beeDebugApi, setApiUrl, setDebugApiUrl }}>
{children}
</Context.Provider>
)
}
+8 -5
View File
@@ -1,6 +1,6 @@
import { PostageBatch } from '@ethersphere/bee-js' import { PostageBatch } from '@ethersphere/bee-js'
import { createContext, ReactChild, ReactElement, useEffect, useState } from 'react' import { createContext, ReactChild, ReactElement, useContext, useEffect, useState } from 'react'
import { beeApi } from '../services/bee' import { Context as SettingsContext } from './Settings'
export interface EnrichedPostageBatch extends PostageBatch { export interface EnrichedPostageBatch extends PostageBatch {
usage: number usage: number
@@ -48,6 +48,7 @@ function enrichStamp(postageBatch: PostageBatch): EnrichedPostageBatch {
} }
export function Provider({ children }: Props): ReactElement { export function Provider({ children }: Props): ReactElement {
const { 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)
@@ -58,14 +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 (!beeDebugApi) return
try { try {
setIsLoading(true) setIsLoading(true)
const stamps = await beeApi.stamps.getPostageStamps() 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)
} }
@@ -84,7 +87,7 @@ export function Provider({ children }: Props): ReactElement {
return () => clearInterval(interval) return () => clearInterval(interval)
} }
}, [frequency]) }, [frequency]) // eslint-disable-line react-hooks/exhaustive-deps
return ( return (
<Context.Provider value={{ stamps, error, isLoading, lastUpdate, start, stop, refresh }}> <Context.Provider value={{ stamps, error, isLoading, lastUpdate, start, stop, refresh }}>
-1
View File
@@ -6,7 +6,6 @@ interface LatestBeeRelease {
} }
interface StatusHookCommon { interface StatusHookCommon {
isLoading: boolean
isOk: boolean isOk: boolean
} }
+40
View File
@@ -0,0 +1,40 @@
import type { ReactElement } from 'react'
import { Route, Switch } from 'react-router-dom'
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 Stamps from './pages/stamps'
import Status from './pages/status'
export enum ROUTES {
INFO = '/',
FILES = '/files',
UPLOAD = '/files/upload',
UPLOAD_IN_PROGRESS = '/files/upload/workflow',
DOWNLOAD = '/files/download',
HASH = '/files/hash/:hash',
ACCOUNTING = '/accounting',
SETTINGS = '/settings',
STAMPS = '/stamps',
STATUS = '/status',
}
const BaseRouter = (): ReactElement => (
<Switch>
<Route exact path={ROUTES.UPLOAD_IN_PROGRESS} component={Upload} />
<Route exact path={ROUTES.UPLOAD} component={UploadLander} />
<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.SETTINGS} component={Settings} />
<Route exact path={ROUTES.STAMPS} component={Stamps} />
<Route exact path={ROUTES.STATUS} component={Status} />
<Route path={ROUTES.INFO} component={Info} />
</Switch>
)
export default BaseRouter
-23
View File
@@ -1,23 +0,0 @@
import type { JSXElementConstructor, ReactElement } from 'react'
import { Route, RouteComponentProps } from 'react-router-dom'
interface Props {
component: JSXElementConstructor<RouteComponentProps>
layout: JSXElementConstructor<RouteComponentProps>
exact?: boolean
path: string
}
const AppRoute = ({ component: Component, layout: Layout, exact, path }: Props): ReactElement => (
<Route
exact={exact}
path={path}
render={props => (
<Layout {...props}>
<Component {...props} />
</Layout>
)}
/>
)
export default AppRoute

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