Compare commits

...

54 Commits

Author SHA1 Message Date
bee-worker caf5814e96 chore: release 0.12.0 (#274) 2021-12-21 11:19:38 +01:00
bee-worker 4f0abefa1d docs: update supported bee (#275) 2021-12-21 11:11:14 +01:00
Cafe137 25b65c3fb7 feat: add identity and feed management (#272)
* feat(wip): add basic feed operations

* ci: bump checks

* ci: bump checks

* feat: rework stamps and add feed functionalities

* refactor: polish and fixes

* feat(wip): add formulas

* feat: show bzz.link for websites

* feat: add stamp empty states and formatBzz

* feat: add feed download

* chore: update manifest-js version

* feat: dev mode support with bee-js 3.1.0 (#273)

* feat: dev mode support with bee-js 3.1.0

* fix: added missing package-lock.json file

* build: remove PR preview

* style: work on design

* feat: add TroubleshootConnectionCard

* build: remove depcheck

Co-authored-by: Attila Gazso <agazso@gmail.com>
2021-12-21 10:58:54 +01:00
bee-worker d7c59a1495 chore: release 0.11.2 (#271) 2021-12-15 17:24:41 +01:00
nugaon 5ac0f01bf5 fix(ci): add lib folder to the package.json files prop (#270) 2021-12-15 17:17:59 +01:00
bee-worker 362c129abd chore: release 0.11.1 (#269) 2021-12-14 16:03:14 +01:00
nugaon c1e77bfc0d fix: typo in publish script (#268) 2021-12-14 15:56:34 +01:00
bee-worker e3d03ed4d1 chore: release 0.11.0 (#263) 2021-12-14 14:22:49 +01:00
nugaon 153b007387 fix(build): bee-dashboard component building (#267)
* fix(build): lib

* ci: build component check

* fix(ci): typeo npm run

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

* build: packageing for webpack lib build

* build: webpack config

* feat: expose App component with beeApiUrl parameter

* build: tsconfig for library build

* build: main property of package json for tsc build

* refactor: rename beeUrl option to beeApiUrl

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

* build: babelrc config

* build: babel plugins and presets for webpack build

* chore: serve.js chmod

* build(refactor): webpack build

* refactor: number notation

* chore: webpack and package config change

* build: add babel preset-env

* chore: prepare script also builds component lib

* feat: typegen

* revert: set back prepare command

* build: assets loader config

* feat: beeDebugApiUrl

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

* feat: locked api settings

* chore: depcheck ignores

* chore: types check script

* ci: check types

* ci: publish with library

* chore: add webpack as devDep

* chore: locked semver

* chore: remove debug logging

* style: webpack config

* chore: react and react-dom as dependency

* chore: package-lock

* fix: clean package-lock init

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

* feat: download-share-upload navigation

* fix: check for files length in hasIndexDocument

* fix: change router dependency

* refactor: switch to @ethersphere/manfest-js

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

* feat: add upload and download history

* refactor: change drag and drop text

* feat: make history ux better

* refactor: improve code based on review

* build: add missing react-router dependency

* ci: remove beeload

* revert(ci): remove beeload

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

* feat: add basic index document detection

* feat(wip): separate preview step

* fix: fix kb and mb units

* feat: add post upload summary, add some styling

* feat: upload flow

* fix: change element order and add conditional rendering

* refactor: remove unused variables for now

* fix: put back stamp creation to stamp page

* refactor: rework postage stamps and grid

* feat: add website and folder icons

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

* feat: add basic design to postage stamp selection dialog

* feat: add web icon, shorten stamp in preview

* feat: extract swarm hash in download flow

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

* fix: always show buy button on stamp page

* refactor: downgrade

* refactor: speed up icon transition

* style: improve download buttons

* style: change [back to upload] icon

* style: add spacing before swarm gateway text

* style: post upload summary spacing

* refactor: drop verticalspacing and use box

* refactor: merge icons to one component

* refactor: use conditions instead of weird assignment

* docs: explain filter(x => x)

* refactor: generalize capacity

* refactor: avoid passing arrow functions

* refactor: get rid of PaperGridContainer and Container

* fix: fix hover color for postage stamps

* feat: add disabled and loading state to buttons

* fix: make drag and drop work for websites

* feat: handle folders and non existing hashes

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

* style: remove body2 font variants

* fix: remove typo

* feat: disable folder upload, add website upload

* fix: disable showPreviews to avoid flickering

* feat(temp): remove folder upload

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

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

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

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

* docs: update dev mode docs

* docs: move dev mode comment

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

* docs: add REACT_APP_DEV_MODE fixme comment

* feat: also ignore chequebook status in dev mode

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

* docs: add dev mode to readme

* docs: revert autoformat

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

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

* chore: lock dependency versions

* chore: update dependencies to latest working ones

* chore: fix deprecation createMuiTheme

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

* chore: update changelog

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

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

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

* feat: redesign the upload file

* fix: styles of tabs on hover

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

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

* fix: on inputs the label should not be selectable

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

* refactor: improved the readability of the upload file component

* chore: removed PeerDetail component

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

* refactor: address PR review comments

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

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

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

* feat: added input list item component

* feat: improved the topology status info

* fix: disabled state of the buttons

* chore: removed unused components

* chore: remove debug console log

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

* chore: button alignment

* chore: removed unused dependency

* chore: buttons are underneath the action

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

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

* feat: action buttons are now properly aligned

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

* fix: code linting

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

* style: headers and nesting for expandable lists

* style: body typography

* chore: bee version check

* feat: key list item component

* style: hover state for the key displaying component

* style: left border on expanded items

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

* chore: moved the styles to classes

* style: tooltips now have arrow

* feat: indicate value has been copied

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

* feat: added health tooltips for connectivity

* refactor: simplified the topology into single component

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

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

* style: background color, links and button

* chore: disable ripples and rounded corners on buttons

* fix: spacing on the troubleshooting card
2021-09-13 12:25:01 +02:00
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
143 changed files with 24092 additions and 10169 deletions
+50
View File
@@ -0,0 +1,50 @@
'use strict'
module.exports = function (api) {
const targets = '>1% and not ie 11 and not dead'
api.cache(true)
api.cacheDirectory = true
return {
presets: [
'@babel/preset-typescript',
[
'@babel/preset-env',
{
targets,
modules: false,
}
],
['@babel/preset-react', {runtime: 'automatic' }]
],
plugins: [
[
"babel-plugin-tsconfig-paths",
{
"relative": true,
"extensions": [
".js",
".jsx",
".ts",
".tsx",
".es",
".es6",
".mjs"
],
"rootDir": ".",
"tsconfig": "tsconfig.lib.json",
}
],
"@babel/plugin-proposal-numeric-separator",
"syntax-dynamic-import",
'@babel/plugin-proposal-class-properties',
[
'@babel/plugin-transform-runtime',
{
helpers: false,
regenerator: true
}
]
]
}
}
+19
View File
@@ -0,0 +1,19 @@
{
"ignores": [
"@types/jest",
"@commitlint/config-conventional",
"@types/react-router",
"@babel/core",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-runtime",
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript",
"babel-loader",
"babel-plugin-syntax-dynamic-import",
"babel-plugin-tsconfig-paths",
"file-loader",
"ts-node",
"webpack-cli"
]
}
+8 -1
View File
@@ -1,9 +1,16 @@
{
"settings": {
"react": {
"version": "detect"
}
},
"extends": [
"plugin:@typescript-eslint/recommended",
"prettier",
"plugin:prettier/recommended",
"plugin:react/recommended"
"plugin:react/recommended",
"react-app",
"react-app/jest"
],
"env": {
"browser": true,
+24 -4
View File
@@ -16,6 +16,11 @@ jobs:
matrix:
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:
- uses: actions/checkout@v2
with:
@@ -42,10 +47,25 @@ jobs:
- name: Commit linting
uses: wagoid/commitlint-github-action@v2
# - name: Code linting
# run: npm run lint:check
# env:
# CI: true
- name: Code linting
run: npm run lint:check
env:
CI: true
- name: Types check
run: npm run check:types
- name: Types build
run: npm run compile:types
- 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
run: npm run build
- name: Build Component
run: npm run build:component
+2
View File
@@ -15,6 +15,8 @@ jobs:
node-version: 14
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm run compile:types
- run: npm run build:component
- run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
+1
View File
@@ -10,6 +10,7 @@
# production
/build
/lib
# misc
.DS_Store
+90
View File
@@ -1,5 +1,95 @@
# Changelog
## [0.12.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.11.2...v0.12.0) (2021-12-21)
### Features
* add identity and feed management ([#272](https://www.github.com/ethersphere/bee-dashboard/issues/272)) ([25b65c3](https://www.github.com/ethersphere/bee-dashboard/commit/25b65c3fb770b09c685fe66596e372dfbb616625))
### [0.11.2](https://www.github.com/ethersphere/bee-dashboard/compare/v0.11.1...v0.11.2) (2021-12-15)
### Bug Fixes
* **ci:** add lib folder to the package.json files prop ([#270](https://www.github.com/ethersphere/bee-dashboard/issues/270)) ([5ac0f01](https://www.github.com/ethersphere/bee-dashboard/commit/5ac0f01bf50ee23b474ab9c8d61c6af418544083))
### [0.11.1](https://www.github.com/ethersphere/bee-dashboard/compare/v0.11.0...v0.11.1) (2021-12-14)
### Bug Fixes
* typo in publish script ([#268](https://www.github.com/ethersphere/bee-dashboard/issues/268)) ([c1e77bf](https://www.github.com/ethersphere/bee-dashboard/commit/c1e77bfc0d3ac442d6bacec7402f576a6422927e))
## [0.11.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.10.0...v0.11.0) (2021-12-14)
### Features
* modularisation ([#244](https://www.github.com/ethersphere/bee-dashboard/issues/244)) ([2a13da1](https://www.github.com/ethersphere/bee-dashboard/commit/2a13da1a6c5925946d22666a84f975cec87df115))
### Bug Fixes
* **build:** bee-dashboard component building ([#267](https://www.github.com/ethersphere/bee-dashboard/issues/267)) ([153b007](https://www.github.com/ethersphere/bee-dashboard/commit/153b007387618e34e1d5dc7fd82d49722783e757))
## [0.10.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.9.0...v0.10.0) (2021-12-07)
### Features
* add website and folder upload and download ([#260](https://www.github.com/ethersphere/bee-dashboard/issues/260)) ([3ef1ad9](https://www.github.com/ethersphere/bee-dashboard/commit/3ef1ad9574c9193f83d8a1447fddb79266c1a4f4))
## [0.9.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.8.0...v0.9.0) (2021-11-25)
### Features
* add dev mode flag ([#246](https://www.github.com/ethersphere/bee-dashboard/issues/246)) ([49350b0](https://www.github.com/ethersphere/bee-dashboard/commit/49350b05709053ecfbc4fc98f8b1df1aa0345e95))
* enable setting devMode from queryParams ([#254](https://www.github.com/ethersphere/bee-dashboard/issues/254)) ([844383b](https://www.github.com/ethersphere/bee-dashboard/commit/844383bea7b2118232a74ac23c9e9a38fc47d3fd))
* improve upload flow ([#240](https://www.github.com/ethersphere/bee-dashboard/issues/240)) ([635621b](https://www.github.com/ethersphere/bee-dashboard/commit/635621b04aea7124a99d00f9e31a86983063f5ce))
* move postage stamp operations to bee debug api ([#256](https://www.github.com/ethersphere/bee-dashboard/issues/256)) ([3bb0077](https://www.github.com/ethersphere/bee-dashboard/commit/3bb00771d684ad93fd7acd921b648574013aec5c))
## [0.8.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.7.0...v0.8.0) (2021-10-20)
In this version we are adding support for the bee release 1.2.0. The app also went through a graphical redesign. More to come soon!
### Features
* support for bee 1.2.0
* update files page design ([#218](https://www.github.com/ethersphere/bee-dashboard/issues/218)) ([93af7f3](https://www.github.com/ethersphere/bee-dashboard/commit/93af7f35a371d54864c068be6e1d8a70092afe28))
* update info page design ([#207](https://www.github.com/ethersphere/bee-dashboard/issues/207)) ([57f5a73](https://www.github.com/ethersphere/bee-dashboard/commit/57f5a73f3a8d957bf967c51612dc09c802bb68dc))
* update status page design ([#214](https://www.github.com/ethersphere/bee-dashboard/issues/214)) ([b666cd2](https://www.github.com/ethersphere/bee-dashboard/commit/b666cd2657cf1003651c44b6b4fa5bdcf11e895f))
* update troubleshooting component design ([#204](https://www.github.com/ethersphere/bee-dashboard/issues/204)) ([c4c1573](https://www.github.com/ethersphere/bee-dashboard/commit/c4c1573263868b6dc8a863124e4aee824dceadbb))
* update accounting page design ([#209](https://www.github.com/ethersphere/bee-dashboard/issues/209)) ([ecbc116](https://www.github.com/ethersphere/bee-dashboard/commit/ecbc1164756de912d14ce44aa9b2c155dded6dac))
* update postage stamps page design ([#217](https://www.github.com/ethersphere/bee-dashboard/issues/217)) ([f241b2f](https://www.github.com/ethersphere/bee-dashboard/commit/f241b2fc5f6ec0741e275498ebef5a18ce710b81))
* update settings page design ([#215](https://www.github.com/ethersphere/bee-dashboard/issues/215)) ([32e5ea9](https://www.github.com/ethersphere/bee-dashboard/commit/32e5ea9e56fdf957b758ec714bb6a4fe1903082a))
### Bug Fixes
* hover state style of ListItems which are clickable to be in line with other buttons ([#223](https://www.github.com/ethersphere/bee-dashboard/issues/223)) ([6c3f6c1](https://www.github.com/ethersphere/bee-dashboard/commit/6c3f6c1019801267aa5e51002f6e21f769edc210))
* size of the troubleshoot component button ([#226](https://www.github.com/ethersphere/bee-dashboard/issues/226)) ([b4c9d9e](https://www.github.com/ethersphere/bee-dashboard/commit/b4c9d9e0182c4bee5ebb2d4e43e0aaad2aeb616b))
* style of the update bee version button ([#222](https://www.github.com/ethersphere/bee-dashboard/issues/222)) ([83c6d13](https://www.github.com/ethersphere/bee-dashboard/commit/83c6d1341790d664c7986dd2a816fe6a3b069e5c))
* typo in population text ([#228](https://www.github.com/ethersphere/bee-dashboard/issues/228)) ([cc5e778](https://www.github.com/ethersphere/bee-dashboard/commit/cc5e778f892b73b0b7ff5e0fa00c4816f3298ac7))
* unknown routes should point to info page ([#227](https://www.github.com/ethersphere/bee-dashboard/issues/227)) ([f11bbd5](https://www.github.com/ethersphere/bee-dashboard/commit/f11bbd5008a78ef7d5c73fc2758ee4e2dafae01e))
* used label in postage stamp list ([#220](https://www.github.com/ethersphere/bee-dashboard/issues/220)) ([0326568](https://www.github.com/ethersphere/bee-dashboard/commit/03265687ad630b0100da3134518b680327af1636))
* wording in chequebook setup ([#211](https://www.github.com/ethersphere/bee-dashboard/issues/211)) ([e7188f4](https://www.github.com/ethersphere/bee-dashboard/commit/e7188f4a35c85204eef6a01ae6f1e679d076180c))
## [0.7.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.6.0...v0.7.0) (2021-08-31)
### 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)
+25 -8
View File
@@ -3,19 +3,26 @@
[![](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)
[![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/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.1-238867f1<!-- 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 |
|-------|---------|-------|----------|------|
| ![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) |
![Status page](/ui_samples/info.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
@@ -38,11 +45,15 @@ npm install -g @ethersphere/bee-dashboard
## 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
To start use:
```sh
bee-dashboard
```
@@ -74,13 +85,16 @@ npm start
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
There are some ways you can make this module better:
- Consult our [open issues](https://github.com/ethersphere/bee-dashboard/issues) and take on one of them
- Help our tests reach 100% coverage!
- Join us in our [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
@@ -92,3 +106,6 @@ See what "Maintainer" means [here](https://github.com/ethersphere/repo-maintaine
## 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)
+18567 -7724
View File
File diff suppressed because it is too large Load Diff
+84 -51
View File
@@ -1,6 +1,6 @@
{
"name": "@ethersphere/bee-dashboard",
"version": "0.6.0",
"version": "0.12.0",
"description": "An app which helps users to setup their Bee node and do actions like cash out cheques",
"keywords": [
"bee",
@@ -15,6 +15,8 @@
"bin": {
"bee-dashboard": "./serve.js"
},
"main": "lib/App.js",
"types": "lib/src/App.d.ts",
"bugs": {
"url": "https://github.com/ethersphere/bee-dashboard/issues/"
},
@@ -24,72 +26,103 @@
"url": "https://github.com/ethersphere/bee-dashboard.git"
},
"dependencies": {
"@ethersphere/bee-js": "^1.2.0",
"@material-ui/core": "^4.11.4",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.57",
"@types/react-router": "^5.1.13",
"@types/react-router-dom": "^5.1.7",
"axios": "^0.21.1",
"bignumber.js": "^9.0.1",
"feather-icons": "^4.28.0",
"formik": "^2.2.8",
"formik-material-ui": "^3.0.1",
"material-ui-dropzone": "^3.5.0",
"notistack": "^1.0.9",
"opener": "^1.5.2",
"qrcode.react": "^1.0.1",
"react": "^17.0.2",
"react-copy-to-clipboard": "^5.0.3",
"react-dom": "^17.0.2",
"react-feather": "^2.0.9",
"react-identicons": "^1.2.5",
"react-router-dom": "^5.2.0",
"react-syntax-highlighter": "^15.4.3",
"semver": "^7.3.2",
"serve-handler": "^6.1.3"
"@ethersphere/bee-js": "3.1.0",
"@ethersphere/manifest-js": "1.1.0",
"@ethersphere/swarm-cid": "^0.1.0",
"@material-ui/core": "4.12.3",
"@material-ui/icons": "4.11.2",
"@material-ui/lab": "4.0.0-alpha.57",
"axios": "0.24.0",
"bignumber.js": "9.0.1",
"ethereumjs-wallet": "^1.0.2",
"file-saver": "^2.0.5",
"formik": "2.2.9",
"formik-material-ui": "3.0.1",
"jszip": "^3.7.1",
"material-ui-dropzone": "3.5.0",
"notistack": "1.0.10",
"opener": "1.5.2",
"qrcode.react": "1.0.1",
"react": ">= 17.0.2",
"react-copy-to-clipboard": "5.0.4",
"react-dom": ">= 17.0.2",
"react-feather": "2.0.9",
"react-identicons": "1.2.5",
"react-router": "5.2.0",
"react-router-dom": "5.2.0",
"react-syntax-highlighter": "15.4.4",
"semver": "7.3.5",
"serve-handler": "6.1.3"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.12.0",
"@testing-library/react": "^11.2.6",
"@testing-library/user-event": "^13.1.5",
"@types/jest": "^26.0.22",
"@types/node": "^14.14.41",
"@types/qrcode.react": "^1.0.1",
"@types/react": "^17.0.3",
"@types/react-copy-to-clipboard": "^5.0.0",
"@types/react-dom": "^17.0.3",
"@types/react-syntax-highlighter": "^13.5.0",
"@types/semver": "^7.3.6",
"eslint": "^7.24.0",
"eslint-config-prettier": "^8.2.0",
"eslint-plugin-jest": "^24.3.5",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-react": "^7.23.2",
"prettier": "^2.2.1",
"@babel/core": "7.16.0",
"@babel/plugin-proposal-class-properties": "7.16.0",
"@babel/plugin-transform-runtime": "7.16.4",
"@babel/preset-env": "7.16.4",
"@babel/preset-react": "7.16.0",
"@babel/preset-typescript": "7.16.0",
"@commitlint/config-conventional": "14.1.0",
"@testing-library/jest-dom": "5.15.0",
"@testing-library/react": "12.1.2",
"@types/file-saver": "2.0.4",
"@types/jest": "27.0.2",
"@types/qrcode.react": "1.0.2",
"@types/react": "17.0.34",
"@types/react-copy-to-clipboard": "5.0.2",
"@types/react-dom": "17.0.11",
"@types/react-router": "5.1.17",
"@types/react-router-dom": "5.3.2",
"@types/react-syntax-highlighter": "13.5.2",
"@types/semver": "7.3.9",
"@typescript-eslint/eslint-plugin": "4.33.0",
"@typescript-eslint/parser": "4.33.0",
"babel-eslint": "10.1.0",
"babel-loader": "8.1.0",
"babel-plugin-syntax-dynamic-import": "6.18.0",
"babel-plugin-tsconfig-paths": "1.0.2",
"depcheck": "1.4.2",
"eslint": "7.24.0",
"eslint-config-prettier": "8.2.0",
"eslint-config-react-app": "6.0.0",
"eslint-plugin-flowtype": "5.10.0",
"eslint-plugin-import": "2.25.2",
"eslint-plugin-jest": "24.3.5",
"eslint-plugin-jsx-a11y": "6.4.1",
"eslint-plugin-prettier": "3.4.0",
"eslint-plugin-react": "7.23.2",
"eslint-plugin-react-hooks": "4.2.0",
"eslint-plugin-testing-library": "3.10.2",
"file-loader": "6.2.0",
"prettier": "2.4.1",
"react-scripts": "4.0.3",
"typescript": "^4.2.4",
"web-vitals": "^1.1.1"
"ts-node": "^10.4.0",
"typescript": "4.4.4",
"web-vitals": "2.1.2",
"webpack": "4.44.2",
"webpack-cli": "^4.9.1"
},
"peerDependencies": {
"react": ">= 17.0.2",
"react-dom": ">= 17.0.2"
},
"scripts": {
"prepare": "npm run build",
"start": "react-scripts start",
"build": "react-scripts build",
"build:component": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' webpack --mode=production",
"compile:types": "tsc --project tsconfig.lib.json --emitDeclarationOnly --declaration",
"test": "react-scripts test",
"serve": "node ./serve.js",
"depcheck": "depcheck .",
"lint": "eslint --fix \"src/**/*.ts\" \"src/**/*.tsx\" && prettier --write \"src/**/*.ts\" \"src/**/*.tsx\"",
"lint:check": "eslint \"src/**/*.ts\" \"src/**/*.tsx\" && prettier --check \"src/**/*.ts\" \"src/**/*.tsx\""
"lint:check": "eslint \"src/**/*.ts\" \"src/**/*.tsx\" && prettier --check \"src/**/*.ts\" \"src/**/*.tsx\"",
"check:types": "tsc --project tsconfig.lib.json"
},
"files": [
"lib",
"build",
"serve.js"
],
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
Regular → Executable
View File
+1 -1
View File
@@ -10,6 +10,6 @@ declare module 'react-identicons' {
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
}
+26 -36
View File
@@ -1,62 +1,52 @@
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 React, { ReactElement } from 'react'
import { BrowserRouter as Router } from 'react-router-dom'
import './App.css'
import { ThemeProvider } from '@material-ui/styles'
import CssBaseline from '@material-ui/core/CssBaseline'
import { SnackbarProvider } from 'notistack'
import BaseRouter from './routes/routes'
import { lightTheme, darkTheme } from './theme'
import { Provider as StampsProvider } from './providers/Stamps'
import { Provider as PlatformProvider } from './providers/Platform'
import Dashboard from './layout/Dashboard'
import { Provider as BeeProvider } from './providers/Bee'
import { Provider as FeedsProvider } from './providers/Feeds'
import { Provider as FileProvider } from './providers/File'
import { Provider as PlatformProvider } from './providers/Platform'
import { Provider as SettingsProvider } from './providers/Settings'
import { Provider as StampsProvider } from './providers/Stamps'
import BaseRouter from './routes'
import { theme } from './theme'
const App = (): ReactElement => {
const [themeMode, toggleThemeMode] = useState('light')
useEffect(() => {
const theme = localStorage.getItem('theme')
if (theme) {
toggleThemeMode(String(localStorage.getItem('theme')))
} else if (window?.matchMedia('(prefers-color-scheme: dark)')?.matches) {
toggleThemeMode('dark')
interface Props {
beeApiUrl?: string
beeDebugApiUrl?: string
lockedApiSettings?: boolean
}
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 (
const App = ({ beeApiUrl, beeDebugApiUrl, lockedApiSettings }: Props): ReactElement => (
<div className="App">
<ThemeProvider theme={themeMode === 'light' ? lightTheme : darkTheme}>
<SettingsProvider>
<ThemeProvider theme={theme}>
<SettingsProvider beeApiUrl={beeApiUrl} beeDebugApiUrl={beeDebugApiUrl} lockedApiSettings={lockedApiSettings}>
<BeeProvider>
<StampsProvider>
<FileProvider>
<FeedsProvider>
<PlatformProvider>
<SnackbarProvider>
<Router>
<>
<CssBaseline />
<Router>
<Dashboard>
<BaseRouter />
</Router>
</Dashboard>
</>
</Router>
</SnackbarProvider>
</PlatformProvider>
</FeedsProvider>
</FileProvider>
</StampsProvider>
</BeeProvider>
</SettingsProvider>
</ThemeProvider>
</div>
)
}
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

+6 -4
View File
@@ -3,10 +3,10 @@ import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import { Alert, AlertTitle } from '@material-ui/lab'
import { ReactElement } from 'react'
const LIMIT = 100_000_000 // 100 megabytes
const LIMIT = 100000000 // 100 megabytes
interface Props {
file: File
files: File[]
}
const useStyles = makeStyles((theme: Theme) =>
@@ -22,14 +22,16 @@ const useStyles = makeStyles((theme: Theme) =>
export default function UploadSizeAlert(props: Props): ReactElement | null {
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 (
<Collapse in={aboveLimit}>
<div className={classes.root}>
<Alert severity="warning">
<AlertTitle>Warning</AlertTitle>
The file you are trying to upload is above the recommended size. The chunks may not be synchronised properly
The files you are trying to upload are above the recommended size. The chunks may not be synchronised properly
over the network.
</Alert>
</div>
+22
View File
@@ -0,0 +1,22 @@
import { ReactElement } from 'react'
interface Props {
width: string
usage: number
}
export function Capacity({ width, usage }: Props): ReactElement {
const integerUsage = Math.round(usage * 100)
const used = integerUsage + '%'
const free = 100 - 2 - integerUsage + '%'
return (
<div style={{ display: 'flex', alignItems: 'center', height: '100%', width }}>
<div style={{ display: 'flex', height: '4px', width: '100%' }}>
<div style={{ width: used, background: '#dd7200' }} />
<div style={{ width: '2%' }} />
<div style={{ width: free, background: '#c9c9c9' }} />
</div>
</div>
)
}
+3 -2
View File
@@ -7,6 +7,7 @@ import DialogContentText from '@material-ui/core/DialogContentText'
import DialogTitle from '@material-ui/core/DialogTitle'
import { useSnackbar } from 'notistack'
import { ReactElement, useState, useContext } from 'react'
import { Zap } from 'react-feather'
import { Context as SettingsContext } from '../providers/Settings'
import EthereumAddress from './EthereumAddress'
@@ -59,8 +60,8 @@ export default function CheckoutModal({ peerId, uncashedAmount }: Props): ReactE
return (
<div>
<Button variant="contained" color="primary" onClick={handleClickOpen} style={{ marginLeft: '7px' }}>
Cashout
<Button variant="contained" onClick={handleClickOpen} startIcon={<Zap size="1rem" />}>
Cash out peer {peerId.substr(0, 8)}[]
</Button>
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">Cashout Cheque</DialogTitle>
+26
View File
@@ -0,0 +1,26 @@
import { createStyles, makeStyles, Theme } from '@material-ui/core'
import { Close } from '@material-ui/icons'
import { ReactElement } from 'react'
interface Props {
onClose: () => void
}
const useStyles = makeStyles((theme: Theme) =>
createStyles({
wrapper: {
padding: theme.spacing(1),
cursor: 'pointer',
},
}),
)
export function CloseButton({ onClose }: Props): ReactElement {
const classes = useStyles()
return (
<div className={classes.wrapper} onClick={onClose}>
<Close />
</div>
)
}
+38
View File
@@ -0,0 +1,38 @@
import { createStyles, makeStyles, Theme } from '@material-ui/core'
import { ReactElement } from 'react'
interface Props {
children: string
prettify?: boolean
}
const useStyles = makeStyles((theme: Theme) =>
createStyles({
wrapper: {
overflow: 'scroll',
background: '#ffffff',
},
pre: {
maxHeight: '6em',
padding: theme.spacing(2),
},
}),
)
function prettifyString(string: string): string {
try {
return JSON.stringify(JSON.parse(string), null, 4)
} catch {
return string
}
}
export function Code({ children, prettify }: Props): ReactElement {
const classes = useStyles()
return (
<div className={classes.wrapper}>
<pre className={classes.pre}>{prettify ? prettifyString(children) : children}</pre>
</div>
)
}
-50
View File
@@ -1,50 +0,0 @@
import React, { ReactElement, useState } from 'react'
import { TextField, Button } from '@material-ui/core'
interface Props {
defaultHost?: string
setHost: (host: string) => void
}
export default function ConnectToHost(props: Props): ReactElement {
const [hostInputVisible, toggleHostInputVisibility] = useState(false)
const [host, setHost] = useState('')
const handleNewHostConnection = () => {
if (host) {
props.setHost(host)
toggleHostInputVisibility(!hostInputVisible)
}
}
return (
<div>
{hostInputVisible ? (
<div style={{ display: 'flex' }}>
<TextField
defaultValue={props.defaultHost}
label="Enter host"
variant="outlined"
size="small"
onChange={e => setHost(e.target.value)}
style={{ marginRight: '15px', minWidth: '300px' }}
/>
<Button onClick={() => handleNewHostConnection()} size="small" variant="outlined">
Connect
</Button>
<Button
style={{ marginLeft: '7px' }}
onClick={() => toggleHostInputVisibility(!hostInputVisible)}
size="small"
>
Cancel
</Button>
</div>
) : (
<Button onClick={() => toggleHostInputVisibility(!hostInputVisible)} size="small" variant="outlined">
Change host
</Button>
)}
</div>
)
}
+21
View File
@@ -0,0 +1,21 @@
import { createStyles, makeStyles, Typography } from '@material-ui/core'
import { ReactElement } from 'react'
interface Props {
children: (string | ReactElement)[] | (string | ReactElement)
}
const useStyles = makeStyles(() =>
createStyles({
text: {
color: '#606060',
fontSize: '0.9rem',
},
}),
)
export function DocumentationText({ children }: Props): ReactElement {
const classes = useStyles()
return <Typography className={classes.text}>{children}</Typography>
}
+5 -7
View File
@@ -1,9 +1,9 @@
import { Typography } from '@material-ui/core/'
import QRCodeModal from './QRCodeModal'
import ClipboardCopy from './ClipboardCopy'
import Identicon from 'react-identicons'
import { ReactElement } from 'react'
import Identicon from 'react-identicons'
import { config } from '../config'
import ClipboardCopy from './ClipboardCopy'
import QRCodeModal from './QRCodeModal'
interface Props {
address: string | undefined
@@ -36,9 +36,7 @@ export default function EthereumAddress(props: Props): ReactElement {
}
: { marginRight: '7px' }
}
href={`${process.env.REACT_APP_BLOCKCHAIN_EXPLORER_URL}/${props.transaction ? 'tx' : 'address'}/${
props.address
}`}
href={`${config.BLOCKCHAIN_EXPLORER_URL}/${props.transaction ? 'tx' : 'address'}/${props.address}`}
target="_blank"
rel="noreferrer"
>
-59
View File
@@ -1,59 +0,0 @@
import { ReactElement } from 'react'
import { createStyles, makeStyles } from '@material-ui/core/styles'
import { Card, CardContent, Typography } from '@material-ui/core/'
import EthereumAddress from '../components/EthereumAddress'
import type { ChequebookAddressResponse, NodeAddresses } from '@ethersphere/bee-js'
const useStyles = makeStyles(() =>
createStyles({
root: {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-around',
flexWrap: 'wrap',
},
details: {
display: 'flex',
flexDirection: 'column',
},
content: {
flex: '1 0 auto',
},
}),
)
interface Props {
nodeAddresses: NodeAddresses | null
chequebookAddress: ChequebookAddressResponse | null
}
function EthereumAddressCard(props: Props): ReactElement {
const classes = useStyles()
return (
<Card className={classes.root}>
<div className={classes.details}>
<CardContent className={classes.content}>
<Typography variant="subtitle1" gutterBottom>
Ethereum Address
</Typography>
<EthereumAddress address={props.nodeAddresses?.ethereum} />
</CardContent>
</div>
<div className={classes.details}>
<CardContent className={classes.content}>
<Typography variant="subtitle1" gutterBottom>
Chequebook Contract Address
</Typography>
<EthereumAddress address={props.chequebookAddress?.chequebookAddress} />
</CardContent>
</div>
</Card>
)
}
export default EthereumAddressCard
+59
View File
@@ -0,0 +1,59 @@
import { Collapse, ListItem } from '@material-ui/core'
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import { ExpandLess, ExpandMore } from '@material-ui/icons'
import { ReactElement, ReactNode, useState } from 'react'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
width: '100%',
padding: 0,
margin: 0,
marginTop: theme.spacing(4),
'&:first-child': {
marginTop: 0,
},
},
rootLevel1: { marginTop: theme.spacing(1) },
rootLevel2: { marginTop: theme.spacing(0.5) },
header: {
backgroundColor: theme.palette.background.paper,
},
contentLevel0: {
marginTop: theme.spacing(1),
},
contentLevel12: {
marginTop: theme.spacing(0.25),
},
infoText: {
color: '#c9c9c9',
},
}),
)
interface Props {
children: ReactNode
expandable: ReactNode
defaultOpen?: boolean
}
export default function ExpandableElement({ children, expandable, defaultOpen }: Props): ReactElement | null {
const classes = useStyles()
const [open, setOpen] = useState<boolean>(Boolean(defaultOpen))
const handleClick = () => {
setOpen(!open)
}
return (
<div className={`${classes.root} ${classes.rootLevel2}`}>
<ListItem button onClick={handleClick} className={classes.header}>
{children}
{open ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
<div className={classes.contentLevel12}>{expandable}</div>
</Collapse>
</div>
)
}
+82
View File
@@ -0,0 +1,82 @@
import { ReactElement, ReactNode, useState } from 'react'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import { Collapse, ListItem, ListItemText, Typography } from '@material-ui/core'
import { ExpandLess, ExpandMore } from '@material-ui/icons'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
width: '100%',
padding: 0,
margin: 0,
marginTop: theme.spacing(4),
'&:first-child': {
marginTop: 0,
},
},
rootLevel1: { marginTop: theme.spacing(1) },
rootLevel2: { marginTop: theme.spacing(0.5) },
header: {
backgroundColor: theme.palette.background.paper,
},
contentLevel0: {
marginTop: theme.spacing(1),
},
contentLevel12: {
marginTop: theme.spacing(0.25),
},
infoText: {
color: '#c9c9c9',
},
}),
)
interface Props {
children?: ReactNode
label: ReactNode
info?: ReactNode
level?: 0 | 1 | 2
defaultOpen?: boolean
}
export default function ExpandableList({ children, label, level, defaultOpen, info }: Props): ReactElement | null {
const classes = useStyles()
const [open, setOpen] = useState<boolean>(Boolean(defaultOpen))
const handleClick = () => {
setOpen(!open)
}
let rootLevelClass = ''
let typographyVariant: 'h1' | 'h2' | 'h3' = 'h1'
let contentLevelClass = classes.contentLevel0
if (level === 1) {
rootLevelClass = classes.rootLevel1
typographyVariant = 'h2'
contentLevelClass = classes.contentLevel12
} else if (level === 2) {
rootLevelClass = classes.rootLevel2
typographyVariant = 'h3'
contentLevelClass = classes.contentLevel12
}
return (
<div className={`${classes.root} ${rootLevelClass}`}>
<ListItem button onClick={handleClick} className={classes.header}>
<ListItemText primary={<Typography variant={typographyVariant}>{label}</Typography>} />
<div style={{ display: 'flex' }}>
{!open && (
<Typography variant="body2" className={classes.infoText}>
{info}
</Typography>
)}
{open ? <ExpandLess /> : <ExpandMore />}
</div>
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
<div className={contentLevelClass}>{children}</div>
</Collapse>
</div>
)
}
+54
View File
@@ -0,0 +1,54 @@
import { ReactElement, ReactNode } from 'react'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import { Typography, Grid, IconButton, Tooltip } from '@material-ui/core'
import { Info } from 'react-feather'
import ListItem from '@material-ui/core/ListItem'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
header: {
backgroundColor: theme.palette.background.paper,
marginBottom: theme.spacing(0.25),
wordBreak: 'break-word',
},
copyValue: {
cursor: 'pointer',
padding: theme.spacing(1),
borderRadius: 0,
'&:hover': {
backgroundColor: '#fcf2e8',
color: theme.palette.primary.main,
},
},
}),
)
interface Props {
label?: ReactNode
value?: ReactNode
tooltip?: string
}
export default function ExpandableListItem({ label, value, tooltip }: Props): ReactElement | null {
const classes = useStyles()
return (
<ListItem className={classes.header}>
<Grid container direction="row" justifyContent="space-between" alignItems="center">
{label && <Typography variant="body1">{label}</Typography>}
{value && (
<Typography variant="body2">
{value}
{tooltip && (
<Tooltip title={tooltip} placement="top" arrow>
<IconButton size="small" className={classes.copyValue}>
<Info strokeWidth={1} />
</IconButton>
</Tooltip>
)}
</Typography>
)}
</Grid>
</ListItem>
)
}
@@ -0,0 +1,46 @@
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({
wrapper: {
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
},
action: {
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 (
<div className={classes.wrapper}>
{children
// Exclude falsy values to allow conditional rendering
.filter(x => x)
.map((a, i) => (
<div key={i} className={classes.action}>
{a}
</div>
))}
</div>
)
}
return (
<Grid container direction="row">
<Grid className={classes.action}>{children}</Grid>
</Grid>
)
}
+157
View File
@@ -0,0 +1,157 @@
import { 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 { Edit, Minus, Search, X } from 'react-feather'
import ExpandableListItemActions from './ExpandableListItemActions'
import ExpandableListItemNote from './ExpandableListItemNote'
import { SwarmButton } from './SwarmButton'
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
loading?: boolean
onChange?: (value: string) => void
onConfirm: (value: string) => void
mapperFn?: (value: string) => string
locked?: boolean
}
export default function ExpandableListItemKey({
label,
value,
onConfirm,
onChange,
confirmLabel,
confirmLabelDisabled,
expandedOnly,
helperText,
placeholder,
loading,
mapperFn,
locked,
}: Props): ReactElement | null {
const classes = useStyles()
const [open, setOpen] = useState(Boolean(expandedOnly))
const [inputValue, setInputValue] = useState<string>(value || '')
const toggleOpen = () => setOpen(!open)
const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
if (mapperFn) {
e.target.value = mapperFn(e.target.value)
}
setInputValue(e.target.value)
if (onChange) onChange(e.target.value)
}
return (
<>
<ListItem className={`${classes.header} ${open ? classes.headerOpen : ''}`}>
<Grid container direction="column" justifyContent="space-between" alignItems="stretch">
<Grid container direction="row" justifyContent="space-between" alignItems="center">
{label && (
<Typography variant="body1" className={classes.unselectableLabel}>
{label}
</Typography>
)}
<Typography variant="body2">
<div>
{!open && value}
{!expandedOnly && !locked && (
<IconButton size="small" className={classes.copyValue}>
{open ? (
<Minus onClick={toggleOpen} strokeWidth={1} />
) : (
<Edit onClick={toggleOpen} strokeWidth={1} />
)}
</IconButton>
)}
</div>
</Typography>
</Grid>
<Collapse in={open} timeout="auto" unmountOnExit>
<InputBase
value={inputValue}
placeholder={placeholder}
onChange={handleChange}
fullWidth
className={classes.content}
autoFocus
hidden={locked}
/>
</Collapse>
</Grid>
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
{helperText && <ExpandableListItemNote>{helperText}</ExpandableListItemNote>}
<ExpandableListItemActions>
<SwarmButton
disabled={
loading ||
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
}
loading={loading}
iconType={Search}
onClick={() => onConfirm(inputValue)}
>
{confirmLabel || 'Save'}
</SwarmButton>
<SwarmButton
disabled={loading || inputValue === value || inputValue === ''}
iconType={X}
onClick={() => setInputValue(value || '')}
cancel
>
Cancel
</SwarmButton>
</ExpandableListItemActions>
</Collapse>
</>
)
}
+115
View File
@@ -0,0 +1,115 @@
import { Grid, IconButton, ListItem, Tooltip, Typography } from '@material-ui/core'
import Collapse from '@material-ui/core/Collapse'
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import { ReactElement, useState } from 'react'
import { CopyToClipboard } from 'react-copy-to-clipboard'
import { Eye, Minus } from 'react-feather'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
header: {
backgroundColor: theme.palette.background.paper,
marginBottom: theme.spacing(0.25),
borderLeft: `${theme.spacing(0.25)}px solid rgba(0,0,0,0)`,
wordBreak: 'break-word',
},
headerOpen: {
borderLeft: `${theme.spacing(0.25)}px solid ${theme.palette.primary.main}`,
},
copyValue: {
cursor: 'pointer',
padding: theme.spacing(1),
borderRadius: 0,
'&:hover': {
backgroundColor: '#fcf2e8',
color: theme.palette.primary.main,
},
},
content: {
marginTop: theme.spacing(2),
marginBottom: theme.spacing(2),
},
keyMargin: {
marginRight: theme.spacing(1),
},
}),
)
interface Props {
label: string
value: string
}
const lengthWithoutPrefix = (s: string) => s.replace(/^0x/i, '').length
function isPrefixedHexString(s: unknown): boolean {
return typeof s === 'string' && /^0x[0-9a-f]+$/i.test(s)
}
const split = (s: string): string[] => {
const nonPrefixLength = lengthWithoutPrefix(s)
if (nonPrefixLength % 6 === 0) return s.match(/(0x|.{6})/gi) || []
return s.match(/(0x|.{1,8})/gi) || []
}
export default function ExpandableListItemKey({ label, value }: Props): ReactElement | null {
const classes = useStyles()
const [open, setOpen] = useState(false)
const [copied, setCopied] = useState(false)
const toggleOpen = () => setOpen(!open)
const tooltipClickHandler = () => setCopied(true)
const tooltipCloseHandler = () => setCopied(false)
const splitValues = split(value)
const hasPrefix = isPrefixedHexString(value)
const spanText = `${hasPrefix ? `${splitValues[0]} ${splitValues[1]}` : splitValues[0]}[…]${
splitValues[splitValues.length - 1]
}`
return (
<ListItem className={`${classes.header} ${open ? classes.headerOpen : ''}`}>
<Grid container direction="column" justifyContent="space-between" alignItems="stretch">
<Grid container direction="row" justifyContent="space-between" alignItems="center">
{label && <Typography variant="body1">{label}</Typography>}
<Typography variant="body2">
<div>
{!open && (
<span className={classes.copyValue}>
<Tooltip title={copied ? 'Copied' : 'Copy'} placement="top" arrow onClose={tooltipCloseHandler}>
<CopyToClipboard text={value}>
<span onClick={tooltipClickHandler}>{value ? spanText : ''}</span>
</CopyToClipboard>
</Tooltip>
</span>
)}
<IconButton size="small" className={classes.copyValue}>
{open ? <Minus onClick={toggleOpen} strokeWidth={1} /> : <Eye onClick={toggleOpen} strokeWidth={1} />}
</IconButton>
</div>
</Typography>
</Grid>
<Collapse in={open} timeout="auto" unmountOnExit>
<div className={classes.content}>
<Tooltip title={copied ? 'Copied' : 'Copy'} placement="top" arrow onClose={tooltipCloseHandler}>
<CopyToClipboard text={value}>
{/* This has to be wrapped in two spans otherwise either the tooltip or the highlighting does not work*/}
<span onClick={tooltipClickHandler}>
<span className={classes.copyValue}>
{splitValues.map((s, i) => (
<Typography variant="body2" key={i} className={classes.keyMargin} component="span">
{s}
</Typography>
))}
</span>
</span>
</CopyToClipboard>
</Tooltip>
</div>
</Collapse>
</Grid>
</ListItem>
)
}
+106
View File
@@ -0,0 +1,106 @@
import { Grid, IconButton, ListItem, Tooltip, Typography } from '@material-ui/core'
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import { ArrowForward, OpenInNewSharp } from '@material-ui/icons'
import { ReactElement, useState } from 'react'
import CopyToClipboard from 'react-copy-to-clipboard'
import { useHistory } from 'react-router'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
header: {
backgroundColor: theme.palette.background.paper,
marginBottom: theme.spacing(0.25),
borderLeft: `${theme.spacing(0.25)}px solid rgba(0,0,0,0)`,
wordBreak: 'break-word',
},
headerOpen: {
borderLeft: `${theme.spacing(0.25)}px solid ${theme.palette.primary.main}`,
},
openLinkIcon: {
cursor: 'pointer',
padding: theme.spacing(1),
borderRadius: 0,
'&:hover': {
backgroundColor: '#fcf2e8',
color: theme.palette.primary.main,
},
},
content: {
marginTop: theme.spacing(2),
marginBottom: theme.spacing(2),
},
keyMargin: {
marginRight: theme.spacing(1),
},
copyValue: {
cursor: 'pointer',
padding: theme.spacing(1),
borderRadius: 0,
'&:hover': {
backgroundColor: '#fcf2e8',
color: theme.palette.primary.main,
},
},
}),
)
interface Props {
label: string
value: string
link?: string
navigationType?: 'NEW_WINDOW' | 'HISTORY_PUSH'
allowClipboard?: boolean
}
export default function ExpandableListItemLink({
label,
value,
link,
navigationType = 'NEW_WINDOW',
allowClipboard = true,
}: Props): ReactElement | null {
const classes = useStyles()
const [copied, setCopied] = useState(false)
const history = useHistory()
const tooltipClickHandler = () => setCopied(true)
const tooltipCloseHandler = () => setCopied(false)
const displayValue = value.length > 22 ? value.slice(0, 19) + '...' : value
function onNavigation() {
if (navigationType === 'NEW_WINDOW') {
window.open(link || value)
} else {
history.push(link || value)
}
}
return (
<ListItem className={classes.header}>
<Grid container direction="column" justifyContent="space-between" alignItems="stretch">
<Grid container direction="row" justifyContent="space-between" alignItems="center">
{label && <Typography variant="body1">{label}</Typography>}
<Typography variant="body2">
<div>
{allowClipboard && (
<span className={classes.copyValue}>
<Tooltip title={copied ? 'Copied' : 'Copy'} placement="top" arrow onClose={tooltipCloseHandler}>
<CopyToClipboard text={value}>
<span onClick={tooltipClickHandler}>{displayValue}</span>
</CopyToClipboard>
</Tooltip>
</span>
)}
{!allowClipboard && <span onClick={onNavigation}>{displayValue}</span>}
<IconButton size="small" className={classes.openLinkIcon}>
{navigationType === 'NEW_WINDOW' && <OpenInNewSharp onClick={onNavigation} strokeWidth={1} />}
{navigationType === 'HISTORY_PUSH' && <ArrowForward onClick={onNavigation} strokeWidth={1} />}
</IconButton>
</div>
</Typography>
</Grid>
</Grid>
</ListItem>
)
}
+32
View File
@@ -0,0 +1,32 @@
import { ReactElement, ReactNode } from 'react'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import { Typography } from '@material-ui/core'
import ListItem from '@material-ui/core/ListItem'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
header: {
backgroundColor: '#F7F7F7',
marginBottom: theme.spacing(0.25),
},
typography: {
color: '#242424',
},
}),
)
interface Props {
children?: ReactNode | ReactNode[]
}
export default function ExpandableListItemNote({ children }: Props): ReactElement | null {
const classes = useStyles()
return (
<ListItem className={classes.header}>
<Typography variant="body1" className={classes.typography}>
{children}
</Typography>
</ListItem>
)
}
+30
View File
@@ -0,0 +1,30 @@
import { createStyles, makeStyles } from '@material-ui/core'
import { ReactElement } from 'react'
const useStyles = makeStyles(() =>
createStyles({
image: {
width: '100%',
height: '100%',
objectFit: 'cover',
},
}),
)
interface Props {
alt: string
src: string | undefined
maxHeight?: string
maxWidth?: string
}
export function FitImage(props: Props): ReactElement {
const classes = useStyles()
const inlineStyles: Record<string, string> = {}
props.maxHeight && (inlineStyles.maxHeight = props.maxHeight)
props.maxWidth && (inlineStyles.maxWidth = props.maxWidth)
return <img className={classes.image} alt={props.alt} src={props.src} style={inlineStyles} />
}
+37
View File
@@ -0,0 +1,37 @@
import { ReactElement, useEffect, useState } from 'react'
import { getPrettyDateString } from '../utils/date'
import { getHistorySafe, HistoryItem, HISTORY_KEYS } from '../utils/local-storage'
import ExpandableList from './ExpandableList'
import ExpandableListItemLink from './ExpandableListItemLink'
interface Props {
title: string
localStorageKey: HISTORY_KEYS
}
export function History({ title, localStorageKey }: Props): ReactElement | null {
const [items, setItems] = useState<HistoryItem[]>([])
useEffect(() => {
setItems(getHistorySafe(localStorageKey))
}, [localStorageKey])
if (!items.length) {
return null
}
return (
<ExpandableList label={title} defaultOpen>
{items.map((x, i) => (
<ExpandableListItemLink
label={getPrettyDateString(new Date(x.createdAt))}
value={x.name}
link={'/files/hash/' + x.hash}
key={i}
navigationType="HISTORY_PUSH"
allowClipboard={false}
/>
))}
</ExpandableList>
)
}
+41
View File
@@ -0,0 +1,41 @@
import { Box, createStyles, Grid, makeStyles, Typography } from '@material-ui/core'
import { ArrowBack } from '@material-ui/icons'
import { ReactElement } from 'react'
import { useHistory } from 'react-router-dom'
interface Props {
children: string
}
const useStyles = makeStyles(() =>
createStyles({
pressable: {
cursor: 'pointer',
},
icon: {
color: '#242424',
},
}),
)
export function HistoryHeader({ children }: Props): ReactElement {
const classes = useStyles()
const history = useHistory()
function goBack() {
history.goBack()
}
return (
<Box mb={4}>
<Grid container direction="row">
<Box mr={2}>
<div className={classes.pressable} onClick={goBack}>
<ArrowBack className={classes.icon} />
</div>
</Box>
<Typography variant="h1">{children}</Typography>
</Grid>
</Box>
)
}
+10
View File
@@ -0,0 +1,10 @@
import { CircularProgress, Grid } from '@material-ui/core'
import { ReactElement } from 'react'
export function Loading(): ReactElement {
return (
<Grid container direction="row" justifyContent="center" alignItems="center">
<CircularProgress />
</Grid>
)
}
-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>
)
}
+55
View File
@@ -0,0 +1,55 @@
import { createStyles, Grid, makeStyles, Typography } from '@material-ui/core'
import { ReactElement } from 'react'
interface Props {
steps: string[]
index: number
}
const useStyles = makeStyles(() =>
createStyles({
wrapper: {
height: '52px',
display: 'flex',
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
todo: {
background: '#f7f7f7',
color: '#c9c9c9',
},
inProgress: {
background: '#ffffff',
color: '#242424',
height: '52px',
},
done: {
background: '#f7f7f7',
color: '#606060',
height: '52px',
},
}),
)
export function ProgressIndicator({ steps, index }: Props): ReactElement {
const classes = useStyles()
function pickClass(i: number): string {
if (i === index) {
return classes.inProgress
}
return i < index ? classes.done : classes.todo
}
return (
<Grid container justifyContent="space-between">
{steps.map((x, i) => (
<div key={i} className={`${classes.wrapper} ${pickClass(i)}`}>
<Typography>{x}</Typography>
</div>
))}
</Grid>
)
}
+83 -120
View File
@@ -1,67 +1,57 @@
import { ReactElement } from 'react'
import { Link, RouteComponentProps } from 'react-router-dom'
import { createStyles, Theme, makeStyles } from '@material-ui/core/styles'
import { ListItemText, ListItemIcon, ListItem, Divider, List, Drawer, Link as MUILink } from '@material-ui/core'
import { Divider, Drawer, Grid, Link as MUILink, List } from '@material-ui/core'
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import { OpenInNewSharp } from '@material-ui/icons'
import { Activity, FileText, DollarSign, Share2, Settings, Layers } from 'react-feather'
import SwarmLogoOrange from '../assets/swarm-logo-orange.svg'
import { Health } from '@ethersphere/bee-js'
const drawerWidth = 240
import type { ReactElement } from 'react'
import { Bookmark, BookOpen, DollarSign, FileText, Home, Layers, Settings } from 'react-feather'
import { Link } from 'react-router-dom'
import Logo from '../assets/logo.svg'
import { config } from '../config'
import { ROUTES } from '../routes'
import SideBarItem from './SideBarItem'
import SideBarStatus from './SideBarStatus'
const navBarItems = [
{
label: 'Status',
id: 'status',
path: '/',
icon: Activity,
label: 'Info',
path: ROUTES.INFO,
icon: Home,
},
{
label: 'Files',
id: 'files',
path: '/files/',
path: ROUTES.UPLOAD,
icon: FileText,
},
{
label: 'Feeds',
path: ROUTES.FEEDS,
icon: Bookmark,
},
{
label: 'Stamps',
id: 'stamps',
path: '/stamps/',
path: ROUTES.STAMPS,
icon: Layers,
},
{
label: 'Accounting',
id: 'accounting',
path: '/accounting/',
path: ROUTES.ACCOUNTING,
icon: DollarSign,
},
{
label: 'Peers',
id: 'peers',
path: '/peers/',
icon: Share2,
},
{
label: 'Settings',
id: 'settings',
path: '/settings/',
path: ROUTES.SETTINGS,
icon: Settings,
},
]
const drawerWidth = 300
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
display: 'flex',
},
appBar: {
width: `calc(100% - ${drawerWidth}px)`,
marginLeft: drawerWidth,
},
logo: {
padding: 1,
marginTop: 20,
flexWrap: 'nowrap',
minHeight: '100vh',
paddingTop: theme.spacing(8),
paddingBottom: theme.spacing(8),
},
drawer: {
width: drawerWidth,
@@ -69,106 +59,79 @@ const useStyles = makeStyles((theme: Theme) =>
},
drawerPaper: {
width: drawerWidth,
backgroundColor: '#212121',
},
logo: {
marginLeft: theme.spacing(8),
marginRight: theme.spacing(8),
},
icon: {
height: theme.spacing(4),
},
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',
},
activeSideBar: {
color: '#dd7700',
},
activeSideBarItem: {
borderLeft: '4px solid #dd7700',
backgroundColor: 'inherit !important',
},
toolbar: theme.mixins.toolbar,
}),
)
interface Props extends RouteComponentProps {
themeMode: string
health: boolean
nodeHealth: Health | null
}
export default function SideBar(props: Props): ReactElement {
export default function SideBar(): ReactElement {
const classes = useStyles()
return (
<div className={classes.root}>
<Drawer
className={classes.drawer}
variant="permanent"
classes={{
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' }}
/>
<Drawer className={classes.drawer} variant="permanent" anchor="left" classes={{ paper: classes.drawerPaper }}>
<Grid container direction="column" justifyContent="space-between" className={classes.root}>
<Grid className={classes.logo}>
<Link to={ROUTES.INFO}>
<img alt="swarm" src={Logo} />
</Link>
</div>
</Grid>
<Grid>
<List>
{navBarItems.map(item => (
<Link to={item.path} key={item.id} style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem
button
selected={props.location.pathname === item.path}
className={props.location.pathname === item.path ? classes.activeSideBarItem : ''}
>
<ListItemIcon className={props.location.pathname === item.path ? classes.activeSideBar : ''}>
<item.icon style={{ height: '20px' }} />
</ListItemIcon>
<ListItemText
primary={item.label}
className={props.location.pathname === item.path ? classes.activeSideBar : ''}
{navBarItems.map(p => (
<Link to={p.path} key={p.path} className={classes.link}>
<SideBarItem
key={p.path}
iconStart={<p.icon className={classes.icon} />}
path={p.path}
label={p.label}
/>
</ListItem>
</Link>
))}
</List>
<Divider />
<Divider className={classes.divider} />
<List>
<MUILink href={process.env.REACT_APP_BEE_DOCS_HOST} target="_blank" style={{ textDecoration: 'none' }}>
<ListItem button>
<ListItemText primary={'Docs'} />
<OpenInNewSharp fontSize="small" />
</ListItem>
<MUILink href={config.BEE_DOCS_HOST} target="_blank" className={classes.link}>
<SideBarItem
iconStart={<BookOpen className={classes.icon} />}
iconEnd={<OpenInNewSharp className={classes.iconSmall} />}
label={<span>Docs</span>}
/>
</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>
</div>
<div>
<div
style={{
backgroundColor: props.nodeHealth?.status === 'ok' ? '#32c48d' : '#c9201f',
marginRight: '7px',
height: '10px',
width: '10px',
borderRadius: '50%',
display: 'inline-block',
}}
/>
<span>Debug API</span>
</div>
</ListItem>
</div>
</Grid>
<Grid>
<Link to={ROUTES.STATUS} className={classes.link}>
<SideBarStatus path={ROUTES.STATUS} />
</Link>
</Grid>
</Grid>
</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>
)
}
-43
View File
@@ -1,43 +0,0 @@
import { Card, CardContent, Typography } from '@material-ui/core/'
import { makeStyles } from '@material-ui/core/styles'
import { Skeleton } from '@material-ui/lab'
import type { ReactElement } from 'react'
import { Title } from './Title'
const useStyles = makeStyles({
root: {
minWidth: 275,
},
})
interface Props {
label: string
statistic?: string
loading?: boolean
tooltip?: string
}
export default function StatCard({ loading, label, statistic, tooltip }: Props): ReactElement {
const classes = useStyles()
return (
<Card className={classes.root}>
<CardContent>
{loading && (
<>
<Skeleton width={180} height={25} animation="wave" />
<Skeleton width={180} height={35} animation="wave" />
</>
)}
{!loading && (
<>
<Title label={label} tooltip={tooltip} />
<Typography variant="h5" component="h2">
{statistic}
</Typography>
</>
)}
</CardContent>
</Card>
)
}
+28
View File
@@ -0,0 +1,28 @@
import type { ReactElement } from 'react'
import { CircularProgress } from '@material-ui/core'
interface Props {
isOk: boolean
isLoading?: boolean
size?: number | string
className?: string
}
export default function StatusIcon({ isOk, size, className, isLoading }: Props): ReactElement {
const s = size || '1rem'
if (isLoading) return <CircularProgress size={s} className={className} />
return (
<span
className={className}
style={{
backgroundColor: isOk ? '#1de600' : '#ff3a52',
height: s,
width: s,
borderRadius: '50%',
display: 'inline-block',
}}
/>
)
}
+31
View File
@@ -0,0 +1,31 @@
import { createStyles, makeStyles } from '@material-ui/core'
import { ReactElement } from 'react'
interface Props {
children: ReactElement | ReactElement[]
}
const useStyles = makeStyles(() =>
createStyles({
wrapper: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: '175px',
height: '175px',
background: `repeating-linear-gradient(
45deg,
#efefef,
#efefef 4px,
#ffffff 4px,
#ffffff 8px
)`,
},
}),
)
export function StripedWrapper({ children }: Props): ReactElement {
const classes = useStyles()
return <div className={classes.wrapper}>{children}</div>
}
+91
View File
@@ -0,0 +1,91 @@
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
cancel?: boolean
}
const useStyles = makeStyles(() =>
createStyles({
button: {
height: '52px',
position: 'relative',
whiteSpace: 'nowrap',
color: '#242424',
'&:hover, &:focus': {
'& svg': {
stroke: '#fff',
transition: '0.1s',
},
},
},
cancelButton: {
background: '#f7f7f7',
color: '#606060',
},
spinnerWrapper: {
position: 'absolute',
left: '50%',
top: '50%',
width: '40px',
height: '40px',
transform: 'translate(-50%, -50%)',
},
}),
)
export function SwarmButton({
children,
onClick,
iconType,
className,
disabled,
loading,
cancel,
}: Props): ReactElement {
const classes = useStyles()
function getIconColor() {
if (loading || disabled) {
return 'rgba(0, 0, 0, 0.26)'
}
return cancel ? '#606060' : '#dd7700'
}
function getButtonClassName() {
return [className, classes.button, cancel && classes.cancelButton].filter(x => x).join(' ')
}
const icon = React.createElement(iconType, {
size: '1.25rem',
color: getIconColor(),
})
return (
<Button
className={getButtonClassName()}
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
onClick()
event.currentTarget.blur()
}}
variant="contained"
startIcon={icon}
disabled={disabled}
>
{children}
{loading && (
<div className={classes.spinnerWrapper}>
<CircularProgress />
</div>
)}
</Button>
)
}
+23
View File
@@ -0,0 +1,23 @@
import { Box, Dialog, Grid } from '@material-ui/core'
import { ReactElement } from 'react'
interface Props {
children: ReactElement | ReactElement[]
}
export function SwarmDialog({ children }: Props): ReactElement {
return (
<Dialog
open={true}
PaperProps={{
style: { borderRadius: 0, background: '#efefef' },
}}
>
<Box p={4} sx={{ maxWidth: '100%', width: '650px' }}>
<Grid container direction="column">
{children}
</Grid>
</Box>
</Dialog>
)
}
+83
View File
@@ -0,0 +1,83 @@
import { createStyles, FormHelperText, makeStyles, MenuItem, Select as SimpleSelect, Theme } from '@material-ui/core'
import { Field } from 'formik'
import { Select } from 'formik-material-ui'
import { ReactElement } from 'react'
export type SelectEvent = React.ChangeEvent<{
name?: string | undefined
value: unknown
}>
interface Props {
label?: string
name?: string
options: { value: string; label: string }[]
onChange?: (event: SelectEvent) => void
formik?: boolean
defaultValue?: string
}
const useStyles = makeStyles((theme: Theme) =>
createStyles({
select: {
borderRadius: 0,
background: theme.palette.background.paper,
'& fieldset': {
border: 0,
},
},
option: {
height: '52px',
},
}),
)
export function SwarmSelect({ defaultValue, formik, name, options, onChange, label }: Props): ReactElement {
const classes = useStyles()
if (formik) {
return (
<>
{label && <FormHelperText>{label}</FormHelperText>}
<Field
required
component={Select}
name={name}
fullWidth
variant="outlined"
defaultValue={defaultValue || ''}
className={classes.select}
placeholder={label}
>
{options.map((x, i) => (
<MenuItem key={i} value={x.value} className={classes.option}>
{x.label}
</MenuItem>
))}
</Field>
</>
)
}
return (
<>
{label && <FormHelperText>{label}</FormHelperText>}
<SimpleSelect
required
name={name}
fullWidth
variant="outlined"
className={classes.select}
defaultValue={defaultValue || ''}
onChange={onChange}
placeholder={label}
>
{options.map((x, i) => (
<MenuItem key={i} value={x.value} className={classes.option}>
{x.label}
</MenuItem>
))}
</SimpleSelect>
</>
)
}
+58
View File
@@ -0,0 +1,58 @@
import { createStyles, makeStyles, TextField as SimpleTextField, Theme } from '@material-ui/core'
import { Field } from 'formik'
import { TextField } from 'formik-material-ui'
import { ChangeEvent, ReactElement } from 'react'
interface Props {
name: string
label: string
password?: boolean
formik?: boolean
optional?: boolean
onChange?: (event: ChangeEvent<HTMLTextAreaElement>) => void
}
const useStyles = makeStyles((theme: Theme) =>
createStyles({
field: {
background: theme.palette.background.paper,
height: '52px',
'& fieldset': {
border: 0,
},
},
}),
)
export function SwarmTextInput({ name, label, password, optional, formik, onChange }: Props): ReactElement {
const classes = useStyles()
if (formik) {
return (
<Field
component={TextField}
type={password ? 'password' : undefined}
required={!optional}
name={name}
label={label}
fullWidth
variant="outlined"
className={classes.field}
defaultValue=""
/>
)
}
return (
<SimpleTextField
type={password ? 'password' : undefined}
required
label={label}
fullWidth
variant="outlined"
className={classes.field}
defaultValue=""
onChange={onChange}
/>
)
}
+14 -14
View File
@@ -1,9 +1,6 @@
import React, { ReactElement, ReactNode } from 'react'
import { makeStyles } from '@material-ui/core/styles'
import Tabs from '@material-ui/core/Tabs'
import Tab from '@material-ui/core/Tab'
import Typography from '@material-ui/core/Typography'
import Box from '@material-ui/core/Box'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import { Tab, Tabs } from '@material-ui/core'
interface TabPanelProps {
children?: ReactNode
@@ -16,24 +13,25 @@ function TabPanel(props: TabPanelProps) {
return (
<div role="tabpanel" hidden={value !== index} {...other}>
{value === index && (
<Box p={3}>
<Typography>{children}</Typography>
</Box>
)}
{value === index && children}
</div>
)
}
const useStyles = makeStyles(() => ({
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
flexGrow: 1,
},
}))
content: {
marginTop: theme.spacing(2),
},
}),
)
interface TabsValues {
component: ReactNode
label: string
label: ReactNode
}
interface Props {
@@ -55,16 +53,18 @@ export default function SimpleTabs({ values, index, indexChanged }: Props): Reac
return (
<div className={classes.root}>
<Tabs value={v} onChange={handleChange}>
<Tabs value={v} onChange={handleChange} variant="fullWidth">
{values.map(({ label }, idx) => (
<Tab key={idx} label={label} />
))}
</Tabs>
<div className={classes.content}>
{values.map(({ component }, idx) => (
<TabPanel key={idx} value={v} index={idx}>
{component}
</TabPanel>
))}
</div>
</div>
)
}
-41
View File
@@ -1,41 +0,0 @@
import { Grid, Tooltip, Typography } from '@material-ui/core/'
import { makeStyles } from '@material-ui/core/styles'
import { Info } from '@material-ui/icons'
import type { ReactElement } from 'react'
interface TitleProps {
label: string
tooltip?: string
}
const useStyles = makeStyles({
title: {
fontSize: 16,
},
})
export function Title({ label, tooltip }: TitleProps): ReactElement {
const classes = useStyles()
if (!tooltip) {
return (
<Typography className={classes.title} color="textSecondary" gutterBottom>
{label}
</Typography>
)
}
// span is needed as Tooltip expects a non-functional element!
return (
<Tooltip title={tooltip}>
<span>
<Grid container direction="row" justify="space-between">
<Typography className={classes.title} color="textSecondary" gutterBottom>
{label}
</Typography>
<Info />
</Grid>
</span>
</Tooltip>
)
}
+31
View File
@@ -0,0 +1,31 @@
import { createStyles, Grid, makeStyles, Typography } from '@material-ui/core'
import { ReactElement } from 'react'
import { CloseButton } from './CloseButton'
interface Props {
children: string
onClose: () => void
}
const useStyles = makeStyles(() =>
createStyles({
text: {
color: '#606060',
fontWeight: 'bold',
},
}),
)
export function TitleWithClose({ children, onClose }: Props): ReactElement {
const classes = useStyles()
return (
<Grid container justifyContent="space-between" alignItems="center">
<span>&nbsp;</span>
<Typography className={classes.text} align="center">
{children}
</Typography>
<CloseButton onClose={onClose} />
</Grid>
)
}
+21 -44
View File
@@ -1,66 +1,43 @@
import type { Topology } from '@ethersphere/bee-js'
import { Grid } from '@material-ui/core/'
import type { ReactElement } from 'react'
import { pickThreshold, ThresholdValues } from '../utils/threshold'
import StatCard from './StatCard'
import ExpandableListItem from './ExpandableListItem'
interface RootProps {
interface Props {
topology: Topology | null
}
interface Props extends RootProps {
thresholds: ThresholdValues
}
const TopologyStats = (props: RootProps): ReactElement => {
const TopologyStats = (props: Props): ReactElement => {
const thresholds: ThresholdValues = {
connectedPeers: pickThreshold('connectedPeers', props.topology?.connected || 0),
population: pickThreshold('population', props.topology?.population || 0),
depth: pickThreshold('depth', props.topology?.depth || 0),
}
return (
<>
<Indicator {...props} thresholds={thresholds} />
<Metrics {...props} thresholds={thresholds} />
</>
)
}
const Indicator = ({ thresholds }: Props): ReactElement => {
const maximumTotalScore = Object.values(thresholds).reduce((sum, item) => sum + item.maximumScore, 0)
const actualTotalScore = Object.values(thresholds).reduce((sum, item) => sum + item.score, 0)
const percentageText = Math.round((actualTotalScore / maximumTotalScore) * 100) + '%'
return (
<div style={{ marginBottom: '20px' }}>
<StatCard label="Overall Health Indicator" statistic={percentageText} />
</div>
<>
<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}
/>
</>
)
}
const Metrics = ({ topology, thresholds }: Props): ReactElement => (
<Grid style={{ marginBottom: '20px', flexGrow: 1 }}>
<Grid container spacing={3}>
<Grid key={1} item xs={12} sm={12} md={6} lg={4} xl={4}>
<StatCard
label="Connected Peers"
statistic={topology?.connected.toString()}
tooltip={thresholds.connectedPeers.explanation}
/>
</Grid>
<Grid key={2} item xs={12} sm={12} md={6} lg={4} xl={4}>
<StatCard
label="Population"
statistic={topology?.population.toString()}
tooltip={thresholds.population.explanation}
/>
</Grid>
<Grid key={3} item xs={12} sm={12} md={6} lg={4} xl={4}>
<StatCard label="Depth" statistic={topology?.depth.toString()} tooltip={thresholds.depth.explanation} />
</Grid>
</Grid>
</Grid>
)
export default TopologyStats
+49 -32
View File
@@ -1,47 +1,64 @@
import { Button, Grid, Link as MuiLink, Typography } from '@material-ui/core/'
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import type { ReactElement } from 'react'
import { Activity } from 'react-feather'
import { Link } from 'react-router-dom'
import { config } from '../config'
import { ROUTES } from '../routes'
import { makeStyles } from '@material-ui/core/styles'
import { Card, CardContent, Typography } from '@material-ui/core/'
const useStyles = makeStyles({
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
flexGrow: 1,
marginTop: '20px',
height: '100%',
},
title: {
textAlign: 'center',
fontSize: 26,
content: {
maxWidth: 500,
marginBottom: theme.spacing(4),
'&:last-child': {
marginBottom: 0,
},
})
},
icon: {
height: '1rem',
},
}),
)
export default function TroubleshootConnectionCard(): ReactElement {
const classes = useStyles()
return (
<Card className={classes.root}>
<CardContent>
<Typography className={classes.title} gutterBottom>
Looks like your node is not connected
<Grid container direction="column" justifyContent="center" alignItems="center" className={classes.root}>
<Grid item className={classes.content}>
<Typography variant="h1" align="center">
Uh oh, it looks like your node is not connected.
</Typography>
<div style={{ marginBottom: '20px', textAlign: 'center' }}>
<strong>
<Link to="/">Click to run status checks</Link> on your nodes connection or check out the{' '}
<a href={process.env.REACT_APP_BEE_DOCS_HOST} target="_blank" rel="noreferrer">
</Grid>
<Grid item className={classes.content}>
<Typography align="center">
Please check your node status to fix the problem. You can also check out the{' '}
<MuiLink href={config.BEE_DOCS_HOST} target="_blank" rel="noreferrer">
Swarm Bee Docs
</a>
</strong>
</div>
<div style={{ marginBottom: '20px', textAlign: 'center' }}>
<p style={{ marginTop: '50px' }}>
Still not working? Drop us a message on the Ethereum Swarm{' '}
<a href={process.env.REACT_APP_BEE_DISCORD_HOST} target="_blank" rel="noreferrer">
Discord
</a>
</p>
</div>
</CardContent>
</Card>
</MuiLink>{' '}
or ask for support on the{' '}
<MuiLink href={config.BEE_DISCORD_HOST} target="_blank" rel="noreferrer">
Ethereum Swarm Discord
</MuiLink>
.
</Typography>
</Grid>
<Grid item className={classes.content}>
<Typography align="center">
<Button
component={Link}
variant="contained"
startIcon={<Activity className={classes.icon} />}
to={ROUTES.STATUS}
>
Check node status
</Button>
</Typography>
</Grid>
</Grid>
)
}
+8 -5
View File
@@ -1,4 +1,4 @@
import { ReactElement, useState } from 'react'
import { ReactElement, ReactNode, useState } from 'react'
import Button from '@material-ui/core/Button'
import Input from '@material-ui/core/Input'
import Dialog from '@material-ui/core/Dialog'
@@ -19,6 +19,7 @@ interface Props {
max?: BigNumber
min?: BigNumber
action: (amount: bigint) => Promise<string>
icon?: ReactNode
}
export default function WithdrawDepositModal({
@@ -29,6 +30,7 @@ export default function WithdrawDepositModal({
max,
label,
action,
icon,
}: Props): ReactElement {
const [open, setOpen] = useState(false)
const [amount, setAmount] = useState('')
@@ -36,8 +38,9 @@ export default function WithdrawDepositModal({
const [amountError, setAmountError] = useState<Error | null>(null)
const { enqueueSnackbar } = useSnackbar()
const handleClickOpen = () => {
const handleClickOpen = (e: React.MouseEvent<HTMLButtonElement>) => {
setOpen(true)
e.stopPropagation()
}
const handleClose = () => {
@@ -52,7 +55,7 @@ export default function WithdrawDepositModal({
setOpen(false)
enqueueSnackbar(`${successMessage} Transaction ${transactionHash}`, { variant: 'success' })
} catch (e) {
enqueueSnackbar(`${errorMessage} Error: ${e.message}`, { variant: 'error' })
enqueueSnackbar(`${errorMessage} Error: ${(e as Error).message}`, { variant: 'error' })
}
}
@@ -68,13 +71,13 @@ export default function WithdrawDepositModal({
if (max && t.toDecimal.isGreaterThan(max)) setAmountError(new Error(`Needs to be less than ${max}`))
} catch (e) {
setAmountError(e)
setAmountError(e as Error)
}
}
return (
<div>
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
<Button variant="contained" onClick={handleClickOpen} startIcon={icon}>
{label}
</Button>
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title">
+29
View File
@@ -0,0 +1,29 @@
function getProcessEnv(key: string): string | undefined | false {
return typeof process === 'object' && process.env[key]
}
class Config {
public readonly BEE_API_HOST: string
public readonly BEE_DEBUG_API_HOST: string
public readonly BLOCKCHAIN_EXPLORER_URL: string
public readonly BEE_DOCS_HOST: string
public readonly BEE_DISCORD_HOST: string
public readonly GITHUB_REPO_URL: string
constructor() {
this.BEE_API_HOST =
sessionStorage.getItem('api_host') || getProcessEnv('REACT_APP_BEE_HOST') || 'http://localhost:1633'
this.BEE_DEBUG_API_HOST =
sessionStorage.getItem('debug_api_host') || getProcessEnv('REACT_APP_BEE_DEBUG_HOST') || 'http://localhost:1635'
this.BLOCKCHAIN_EXPLORER_URL =
getProcessEnv('REACT_APP_BLOCKCHAIN_EXPLORER_URL') || 'https://blockscout.com/xdai/mainnet'
this.BEE_DOCS_HOST = getProcessEnv('REACT_APP_BEE_DOCS_HOST') || 'https://docs.ethswarm.org/docs/'
this.BEE_DISCORD_HOST = getProcessEnv('REACT_APP_BEE_DISCORD_HOST') || 'https://discord.gg/eKr9XPv7'
this.GITHUB_REPO_URL =
getProcessEnv('REACT_APP_BEE_GITHUB_REPO_URL') || 'https://api.github.com/repos/ethersphere/bee'
}
}
export const config = new Config()
export default config
+3 -1
View File
@@ -1,4 +1,5 @@
import { ReactElement, useContext } from 'react'
import { Download } from 'react-feather'
import { Context as SettingsContext } from '../providers/Settings'
import WithdrawDepositModal from '../components/WithdrawDepositModal'
@@ -11,8 +12,9 @@ export default function DepositModal(): ReactElement {
<WithdrawDepositModal
successMessage="Successful deposit."
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"
icon={<Download size="1rem" />}
min={new BigNumber(0)}
action={(amount: bigint) => {
if (!beeDebugApi) throw new Error('Bee Debug URL is not valid')
+6 -5
View File
@@ -1,18 +1,19 @@
import { ReactElement, useContext } from 'react'
import { Context as SettingsContext } from '../providers/Settings'
import WithdrawDepositModal from '../components/WithdrawDepositModal'
import { BigNumber } from 'bignumber.js'
import { 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 {
const { beeDebugApi } = useContext(SettingsContext)
return (
<WithdrawDepositModal
successMessage="Successful withdrawl."
successMessage="Successful withdrawal."
errorMessage="Error with withdrawing."
dialogMessage="Specify the amount of BZZ you would like to withdraw from your node."
label="Withdraw"
icon={<Upload size="1rem" />}
min={new BigNumber(0)}
action={(amount: bigint) => {
if (!beeDebugApi) throw new Error('Bee Debug URL is not valid')
+18 -6
View File
@@ -6,6 +6,7 @@ import { Balance, Settlements, Settlement } from '../types'
interface UseAccountingHook {
isLoadingUncashed: boolean
totalUncashed: Token
accounting: Accounting[] | null
}
@@ -60,16 +61,21 @@ function mergeAccounting(
}),
)
// If there are no cheques (and hence last cashout actions), we don't need to sort and can return values right away
if (!uncashedAmounts) return Object.values(accounting)
// If there are no cheques (and hence last cashout actions)
if (!uncashedAmounts) return Object.values(accounting).sort((a, b) => (a.peer < b.peer ? -1 : 1))
uncashedAmounts?.forEach(({ peer, uncashedAmount }) => {
accounting[peer].uncashedAmount = new Token(uncashedAmount)
})
return Object.values(accounting).sort((a, b) =>
b.uncashedAmount.toBigNumber.minus(a.uncashedAmount.toBigNumber).toNumber(),
)
// Return sorted by the uncashed amount first and then by the peer id
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 = (
@@ -94,12 +100,18 @@ export const useAccounting = (
setUncashedAmounts(results.fulfilled)
setIsloadingUncashed(false)
})
}, [settlements, isLoadingUncashed, uncashedAmounts])
}, [settlements, isLoadingUncashed, uncashedAmounts, beeDebugApi])
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 {
isLoadingUncashed,
totalUncashed,
accounting,
}
}
+3 -2
View File
@@ -1,5 +1,6 @@
import { useState, useEffect } from 'react'
import axios from 'axios'
import { useEffect, useState } from 'react'
import { config } from '../config'
export interface LatestBeeReleaseHook {
latestBeeRelease: LatestBeeRelease | null
@@ -14,7 +15,7 @@ export const useLatestBeeRelease = (): LatestBeeReleaseHook => {
useEffect(() => {
axios
.get(`${process.env.REACT_APP_BEE_GITHUB_REPO_URL}/releases/latest`)
.get(`${config.GITHUB_REPO_URL}/releases/latest`)
.then(res => {
setLatestBeeRelease(res.data)
})
+12 -38
View File
@@ -1,4 +1,4 @@
import { useState, useEffect, useContext, ReactElement } from 'react'
import { useContext, ReactElement } from 'react'
import ErrorBoundary from '../components/ErrorBoundary'
import AlertVersion from '../components/AlertVersion'
import { Container, CircularProgress } from '@material-ui/core'
@@ -6,70 +6,44 @@ import { Container, CircularProgress } from '@material-ui/core'
import { createStyles, Theme, makeStyles } from '@material-ui/core/styles'
import SideBar from '../components/SideBar'
import NavBar from '../components/NavBar'
import { Context } from '../providers/Bee'
import { RouteComponentProps } from 'react-router'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
content: {
marginLeft: '240px',
flexGrow: 1,
backgroundColor: theme.palette.background.default,
padding: theme.spacing(3),
paddingBottom: '65px',
minHeight: '100vh',
},
}),
)
interface Props extends RouteComponentProps {
interface Props {
children?: ReactElement
}
const Dashboard = (props: Props): ReactElement => {
const classes = useStyles()
const [themeMode, toggleThemeMode] = useState('light')
const { isLoading, apiHealth, debugApiHealth } = useContext(Context)
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')
})
}, [])
const { isLoading } = useContext(Context)
return (
<div>
<SideBar {...props} themeMode={themeMode} health={apiHealth} nodeHealth={debugApiHealth} />
<NavBar themeMode={themeMode} />
<div style={{ display: 'flex' }}>
<SideBar />
<Container className={classes.content}>
<ErrorBoundary>
<main className={classes.content}>
<>
<AlertVersion />
{isLoading ? (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<div style={{ textAlign: 'center', width: '100%' }}>
<CircularProgress />
</Container>
</div>
) : (
props.children
)}
</main>
</>
</ErrorBoundary>
</Container>
</div>
)
}
-94
View File
@@ -1,94 +0,0 @@
import { ReactElement } from 'react'
import { createStyles, makeStyles } from '@material-ui/core/styles'
import { Card, CardContent, Typography, Theme } from '@material-ui/core/'
import WithdrawModal from '../../containers/WithdrawModal'
import DepositModal from '../../containers/DepositModal'
import type { ChequebookAddressResponse } from '@ethersphere/bee-js'
import { Token } from '../../models/Token'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
display: 'flex',
},
buttons: {
display: 'flex',
columnGap: theme.spacing(1),
},
gridContainer: {
display: 'flex',
width: '100%',
marginLeft: theme.spacing(1),
marginRight: theme.spacing(1),
columnGap: theme.spacing(1),
rowGap: theme.spacing(1),
flex: '0 1 auto',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
chequebookActions: {
justifyContent: 'space-between',
display: 'flex',
marginBottom: theme.spacing(1),
},
}),
)
interface ChequebookBalance {
totalBalance: Token
availableBalance: Token
}
interface Props {
chequebookAddress: ChequebookAddressResponse | null
chequebookBalance: ChequebookBalance | null
totalsent?: Token
totalreceived?: Token
}
function AccountCard({ totalreceived, totalsent, chequebookBalance }: Props): ReactElement {
const classes = useStyles()
return (
<div>
<div className={classes.chequebookActions}>
<Typography component="h2" variant="h6">
Chequebook
</Typography>
<div className={classes.buttons}>
<WithdrawModal />
<DepositModal />
</div>
</div>
<Card className={classes.root}>
<CardContent className={classes.gridContainer}>
<div>
<Typography component="h2" variant="h6" color="primary" gutterBottom>
Total Balance
</Typography>
<Typography variant="h5">{chequebookBalance?.totalBalance.toFixedDecimal()} BZZ</Typography>
</div>
<div>
<Typography component="h2" variant="h6" color="primary" gutterBottom>
Available Uncommitted Balance
</Typography>
<Typography variant="h5">{chequebookBalance?.availableBalance.toFixedDecimal()} BZZ</Typography>
</div>
<div>
<Typography component="h2" variant="h6" color="primary" gutterBottom>
Total Sent / Received
</Typography>
<Typography variant="h5">
{totalsent?.toFixedDecimal()} / {totalreceived?.toFixedDecimal()} BZZ
</Typography>
</div>
</CardContent>
</Card>
</div>
)
}
export default AccountCard
-94
View File
@@ -1,94 +0,0 @@
import type { ReactElement } from 'react'
import { makeStyles } from '@material-ui/core/styles'
import { Table, TableBody, TableCell, TableContainer, TableRow, TableHead, Paper } from '@material-ui/core'
import ClipboardCopy from '../../components/ClipboardCopy'
import CashoutModal from '../../components/CashoutModal'
import PeerDetailDrawer from '../../components/PeerDetail'
import { Accounting } from '../../hooks/accounting'
const useStyles = makeStyles({
table: {
minWidth: 650,
},
values: {
textAlign: 'right',
fontFamily: 'monospace, monospace',
},
})
interface Props {
isLoadingUncashed: boolean
accounting: Accounting[] | null
}
function BalancesTable({ accounting, isLoadingUncashed }: Props): ReactElement | null {
if (accounting === null) return null
const classes = useStyles()
return (
<TableContainer component={Paper}>
<Table className={classes.table} size="small" aria-label="Balances Table">
<TableHead>
<TableRow>
<TableCell>Peer</TableCell>
<TableCell align="right">Outstanding Balance</TableCell>
<TableCell align="right">Settlements Sent / Received</TableCell>
<TableCell align="right">Total</TableCell>
<TableCell align="right">Uncashed Amount</TableCell>
<TableCell />
</TableRow>
</TableHead>
<TableBody>
{accounting.map(({ peer, balance, received, sent, uncashedAmount, total }) => (
<TableRow key={peer}>
<TableCell>
<div style={{ display: 'flex' }}>
<small>
<PeerDetailDrawer peerId={peer} />
</small>
<ClipboardCopy value={peer} />
</div>
</TableCell>
<TableCell className={classes.values}>
<span
style={{
color: balance.toBigNumber.isPositive() ? '#32c48d' : '#c9201f',
}}
>
{balance.toFixedDecimal()}
</span>{' '}
BZZ
</TableCell>
<TableCell className={classes.values}>
-{sent.toFixedDecimal()} / {received.toFixedDecimal()} BZZ
</TableCell>
<TableCell className={classes.values}>
<span
style={{
color: total.toBigNumber.isPositive() ? '#32c48d' : '#c9201f',
}}
>
{total.toFixedDecimal()}
</span>{' '}
BZZ
</TableCell>
<TableCell className={classes.values}>
{isLoadingUncashed && 'loading...'}
{!isLoadingUncashed && (
<>{uncashedAmount.toBigNumber.isGreaterThan('0') ? uncashedAmount.toFixedDecimal() : '0'} BZZ</>
)}
</TableCell>
<TableCell className={classes.values}>
{uncashedAmount.toBigNumber.isGreaterThan('0') && (
<CashoutModal uncashedAmount={uncashedAmount.toFixedDecimal()} peerId={peer} />
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)
}
export default BalancesTable
+52
View File
@@ -0,0 +1,52 @@
import type { ReactElement } from 'react'
import ExpandableList from '../../components/ExpandableList'
import ExpandableListItem from '../../components/ExpandableListItem'
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
import ExpandableListItemKey from '../../components/ExpandableListItemKey'
import CashoutModal from '../../components/CashoutModal'
import { Accounting } from '../../hooks/accounting'
import type { Token } from '../../models/Token'
interface Props {
isLoadingUncashed: boolean
totalUncashed: Token
accounting: Accounting[] | null
}
export default function PeerBalances({ accounting, isLoadingUncashed, totalUncashed }: Props): ReactElement | null {
return (
<ExpandableList
label={`Peers (${accounting?.length || 0})`}
info={`${totalUncashed.toFixedDecimal()} BZZ (uncashed)`}
>
<ExpandableListItem label="Uncashed Amount Total" value={`${totalUncashed.toFixedDecimal()} BZZ`} />
{accounting?.map(({ peer, balance, received, sent, uncashedAmount, total }) => (
<ExpandableList
key={peer}
label={`Peer ${peer.substr(0, 8)}[…]`}
level={1}
info={`${uncashedAmount.toFixedDecimal()} BZZ (uncashed)`}
>
<ExpandableListItemKey label="Peer ID" value={peer} />
<ExpandableListItem label="Outstanding Balance" value={`${balance.toFixedDecimal()} BZZ`} />
<ExpandableListItem
label="Settlements Sent / Received"
value={`-${sent.toFixedDecimal()} / ${received.toFixedDecimal()} BZZ`}
/>
<ExpandableListItem label="Total" value={`${total.toFixedDecimal()} BZZ`} />
<ExpandableListItem
label="Uncashed Amount"
value={isLoadingUncashed ? 'loading…' : `${uncashedAmount.toFixedDecimal()} BZZ`}
/>
{uncashedAmount.toBigNumber.isGreaterThan('0') && (
<ExpandableListItemActions>
<CashoutModal uncashedAmount={uncashedAmount.toFixedDecimal()} peerId={peer} />
</ExpandableListItemActions>
)}
</ExpandableList>
))}
</ExpandableList>
)
}
+34 -28
View File
@@ -1,46 +1,52 @@
import { ReactElement, useContext } from 'react'
import { Theme, createStyles, makeStyles } from '@material-ui/core/styles'
import AccountCard from '../accounting/AccountCard'
import BalancesTable from './BalancesTable'
import EthereumAddressCard from '../../components/EthereumAddressCard'
import PeerBalances from './PeerBalances'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { Context as BeeContext } from '../../providers/Bee'
import { Context as SettingsContext } from '../../providers/Settings'
import { useAccounting } from '../../hooks/accounting'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
width: '100%',
display: 'grid',
rowGap: theme.spacing(3),
},
}),
)
import ExpandableList from '../../components/ExpandableList'
import ExpandableListItem from '../../components/ExpandableListItem'
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
import ExpandableListItemKey from '../../components/ExpandableListItemKey'
import WithdrawModal from '../../containers/WithdrawModal'
import DepositModal from '../../containers/DepositModal'
export default function Accounting(): ReactElement {
const classes = useStyles()
const { status, nodeAddresses, chequebookAddress, chequebookBalance, settlements, peerBalances } = useContext(
BeeContext,
)
const { status, nodeAddresses, chequebookAddress, chequebookBalance, settlements, peerBalances } =
useContext(BeeContext)
const { beeDebugApi } = useContext(SettingsContext)
const { accounting, isLoadingUncashed } = useAccounting(beeDebugApi, settlements, peerBalances)
const { accounting, totalUncashed, isLoadingUncashed } = useAccounting(beeDebugApi, settlements, peerBalances)
if (!status.all) return <TroubleshootConnectionCard />
return (
<div className={classes.root}>
<AccountCard
chequebookAddress={chequebookAddress}
chequebookBalance={chequebookBalance}
totalsent={settlements?.totalSent}
totalreceived={settlements?.totalReceived}
<div>
<ExpandableList label="Chequebook" defaultOpen>
<ExpandableListItem label="Total Balance" value={`${chequebookBalance?.totalBalance.toFixedDecimal()} BZZ`} />
<ExpandableListItem
label="Available Uncommitted Balance"
value={`${chequebookBalance?.availableBalance.toFixedDecimal()} BZZ`}
/>
<EthereumAddressCard nodeAddresses={nodeAddresses} chequebookAddress={chequebookAddress} />
<BalancesTable accounting={accounting} isLoadingUncashed={isLoadingUncashed} />
<ExpandableListItem
label="Total Cheques Amount Sent"
value={`${settlements?.totalSent.toFixedDecimal()} BZZ`}
/>
<ExpandableListItem
label="Total Cheques Amount Received"
value={`${settlements?.totalReceived.toFixedDecimal()} BZZ`}
/>
<ExpandableListItemActions>
<WithdrawModal />
<DepositModal />
</ExpandableListItemActions>
</ExpandableList>
<ExpandableList label="Blockchain" defaultOpen>
<ExpandableListItemKey label="Ethereum address" value={nodeAddresses?.ethereum || ''} />
<ExpandableListItemKey label="Chequebook contract address" value={chequebookAddress?.chequebookAddress || ''} />
</ExpandableList>
<PeerBalances accounting={accounting} isLoadingUncashed={isLoadingUncashed} totalUncashed={totalUncashed} />
</div>
)
}
+133
View File
@@ -0,0 +1,133 @@
import { Box, Grid, Typography } from '@material-ui/core'
import { Form, Formik } from 'formik'
import { useSnackbar } from 'notistack'
import { ReactElement, useContext, useState } from 'react'
import { Check, X } from 'react-feather'
import { useHistory } from 'react-router'
import { DocumentationText } from '../../components/DocumentationText'
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
import ExpandableListItemKey from '../../components/ExpandableListItemKey'
import { HistoryHeader } from '../../components/HistoryHeader'
import { SwarmButton } from '../../components/SwarmButton'
import { SwarmSelect } from '../../components/SwarmSelect'
import { SwarmTextInput } from '../../components/SwarmTextInput'
import { Context as FeedsContext, IdentityType } from '../../providers/Feeds'
import { Context as SettingsContext } from '../../providers/Settings'
import { ROUTES } from '../../routes'
import { convertWalletToIdentity, generateWallet, persistIdentity } from '../../utils/identity'
interface FormValues {
identityName?: string
type?: IdentityType
password?: string
}
const initialValues: FormValues = {
identityName: '',
type: 'PRIVATE_KEY',
password: '',
}
export default function CreateNewFeed(): ReactElement {
const { beeApi, beeDebugApi } = useContext(SettingsContext)
const { identities, setIdentities } = useContext(FeedsContext)
const [loading, setLoading] = useState(false)
const { enqueueSnackbar } = useSnackbar()
const history = useHistory()
async function onSubmit(values: FormValues) {
setLoading(true)
if (!beeApi) {
enqueueSnackbar(<span>Bee API unavailabe</span>, { variant: 'error' })
setLoading(false)
return
}
const wallet = generateWallet()
const stamps = await beeDebugApi?.getAllPostageBatch()
if (!stamps || !stamps.length) {
enqueueSnackbar(<span>No stamp available</span>, { variant: 'error' })
setLoading(false)
return
}
if (!values.identityName || !values.type) {
enqueueSnackbar(<span>Form is unfinished</span>, { variant: 'error' })
setLoading(false)
return
}
const identity = await convertWalletToIdentity(wallet, values.type, values.identityName, values.password)
persistIdentity(identities, identity)
setIdentities(identities)
history.push(ROUTES.FEEDS)
setLoading(false)
}
function cancel() {
history.goBack()
}
return (
<div>
<HistoryHeader>Create new feed</HistoryHeader>
<Box mb={4}>
<DocumentationText>
To create a feed you will need to create an identity. Please refer to the{' '}
<a
href="https://docs.ethswarm.org/api/#tag/Feed/paths/~1feeds~1{owner}~1{topic}/post"
target="_blank"
rel="noreferrer"
>
official Bee documentation
</a>{' '}
to understand how feeds work.
</DocumentationText>
</Box>
<Formik initialValues={initialValues} onSubmit={onSubmit}>
{({ submitForm, values }) => (
<Form>
<Box mb={0.25}>
<SwarmTextInput name="identityName" label="Identity name" formik />
</Box>
<Box mb={0.25}>
<SwarmSelect
formik
name="type"
options={[
{ label: 'Keypair Only', value: 'PRIVATE_KEY' },
{ label: 'Password Protected', value: 'V3' },
]}
/>
</Box>
{values.type === 'V3' && <SwarmTextInput name="password" label="Password" password formik />}
<Box mt={2}>
<ExpandableListItemKey label="Topic" value={'00'.repeat(32)} />
</Box>
<Box mt={2} sx={{ bgcolor: '#fcf2e8' }} p={2}>
<Grid container justifyContent="space-between">
<Typography>Feeds name</Typography>
<Typography>{values.identityName} Website</Typography>
</Grid>
</Box>
<Box mt={1.25}>
<ExpandableListItemActions>
<SwarmButton onClick={submitForm} iconType={Check} disabled={loading} loading={loading}>
Create Feed
</SwarmButton>
<SwarmButton onClick={cancel} iconType={X} disabled={loading} cancel>
Cancel
</SwarmButton>
</ExpandableListItemActions>
</Box>
</Form>
)}
</Formik>
</div>
)
}
+35
View File
@@ -0,0 +1,35 @@
import { Box, Typography } from '@material-ui/core'
import { ReactElement } from 'react'
import { Trash, X } from 'react-feather'
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
import { SwarmButton } from '../../components/SwarmButton'
import { SwarmDialog } from '../../components/SwarmDialog'
import { TitleWithClose } from '../../components/TitleWithClose'
import { Identity } from '../../providers/Feeds'
interface Props {
identity: Identity
onConfirm: (identity: Identity) => void
onClose: () => void
}
export function DeleteFeedDialog({ identity, onConfirm, onClose }: Props): ReactElement {
return (
<SwarmDialog>
<Box mb={4}>
<TitleWithClose onClose={onClose}>Delete</TitleWithClose>
</Box>
<Box mb={2}>
<Typography align="center">{`You are about to delete feed ${identity.name} Website. It is strongly advised to export this feed first.`}</Typography>
</Box>
<ExpandableListItemActions>
<SwarmButton iconType={Trash} onClick={() => onConfirm(identity)}>
Delete
</SwarmButton>
<SwarmButton iconType={X} onClick={onClose} cancel>
Cancel
</SwarmButton>
</ExpandableListItemActions>
</SwarmDialog>
)
}
+71
View File
@@ -0,0 +1,71 @@
import { Box, createStyles, makeStyles, Typography } from '@material-ui/core'
import { saveAs } from 'file-saver'
import { useSnackbar } from 'notistack'
import { ReactElement } from 'react'
import { Clipboard, Download } from 'react-feather'
import { Code } from '../../components/Code'
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
import { SwarmButton } from '../../components/SwarmButton'
import { SwarmDialog } from '../../components/SwarmDialog'
import { TitleWithClose } from '../../components/TitleWithClose'
import { Identity } from '../../providers/Feeds'
interface Props {
identity: Identity
onClose: () => void
}
const useStyles = makeStyles(() =>
createStyles({
wrapper: {
maxWidth: '100%',
},
}),
)
export function ExportFeedDialog({ identity, onClose }: Props): ReactElement {
const { enqueueSnackbar } = useSnackbar()
const classes = useStyles()
function onDownload() {
saveAs(
new Blob([identity.identity], {
type: 'application/json',
}),
identity.name + '.json',
)
}
function getExportText() {
return identity.type === 'V3' ? 'JSON file' : 'the private key string'
}
function onCopy() {
navigator.clipboard
.writeText(identity.identity)
.then(() => enqueueSnackbar('Copied to Clipboard', { variant: 'success' }))
}
return (
<SwarmDialog>
<Box mb={4}>
<TitleWithClose onClose={onClose}>Export</TitleWithClose>
</Box>
<Box mb={2}>
<Typography align="center">{`We exported the identity associated with this feed as ${getExportText()}.`}</Typography>
</Box>
<Box mb={4} className={classes.wrapper}>
<Code prettify>{identity.identity}</Code>
</Box>
<ExpandableListItemActions>
<SwarmButton iconType={Download} onClick={onDownload}>
Download JSON File
</SwarmButton>
<SwarmButton iconType={Clipboard} onClick={onCopy}>
Copy To Clipboard
</SwarmButton>
</ExpandableListItemActions>
</SwarmDialog>
)
}
+52
View File
@@ -0,0 +1,52 @@
import { Box, Typography } from '@material-ui/core'
import { ReactElement, useState } from 'react'
import { Check, X } from 'react-feather'
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
import { SwarmButton } from '../../components/SwarmButton'
import { SwarmDialog } from '../../components/SwarmDialog'
import { SwarmTextInput } from '../../components/SwarmTextInput'
import { TitleWithClose } from '../../components/TitleWithClose'
interface Props {
feedName: string
onProceed: (password: string) => void
onCancel: () => void
loading: boolean
}
export function FeedPasswordDialog({ feedName, onProceed, onCancel, loading }: Props): ReactElement {
const [password, setPassword] = useState('')
function onProceedClick() {
return onProceed(password)
}
return (
<SwarmDialog>
<Box mb={4}>
<TitleWithClose onClose={onCancel}>Update Feed</TitleWithClose>
</Box>
<Box mb={2}>
<Typography>Please enter the password for {feedName}:</Typography>
</Box>
<Box mb={4}>
<SwarmTextInput
label="Password"
name="password"
onChange={event => {
setPassword(event.target.value)
}}
password
/>
</Box>
<ExpandableListItemActions>
<SwarmButton iconType={Check} onClick={onProceedClick} disabled={loading} loading={loading}>
Proceed
</SwarmButton>
<SwarmButton iconType={X} onClick={onCancel} cancel disabled={loading}>
Cancel
</SwarmButton>
</ExpandableListItemActions>
</SwarmDialog>
)
}
+94
View File
@@ -0,0 +1,94 @@
import * as swarmCid from '@ethersphere/swarm-cid'
import { Box } from '@material-ui/core'
import { ReactElement, useContext, useEffect, useState } from 'react'
import { X } from 'react-feather'
import { RouteComponentProps, useHistory } from 'react-router-dom'
import { DocumentationText } from '../../components/DocumentationText'
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
import ExpandableListItemKey from '../../components/ExpandableListItemKey'
import ExpandableListItemLink from '../../components/ExpandableListItemLink'
import { HistoryHeader } from '../../components/HistoryHeader'
import { SwarmButton } from '../../components/SwarmButton'
import { Context as BeeContext } from '../../providers/Bee'
import { Context as IdentityContext } from '../../providers/Feeds'
import { Context as SettingsContext } from '../../providers/Settings'
import { ROUTES } from '../../routes'
import { UploadArea } from '../files/UploadArea'
interface MatchParams {
uuid: string
}
export function FeedSubpage(props: RouteComponentProps<MatchParams>): ReactElement {
const { identities } = useContext(IdentityContext)
const { beeApi } = useContext(SettingsContext)
const { status } = useContext(BeeContext)
const history = useHistory()
const [available, setAvailable] = useState(false)
const uuid = props.match.params.uuid
const identity = identities.find(x => x.uuid === uuid)
useEffect(() => {
if (!identity || !identity.feedHash) {
return
}
try {
beeApi?.downloadData(identity.feedHash).then(() => setAvailable(true))
} catch {
setAvailable(false)
}
}, [beeApi, uuid, identity])
if (!identity || !status.all) {
history.replace(ROUTES.FEEDS)
return <></>
}
function onClose() {
history.push(ROUTES.FEEDS)
}
return (
<div>
<HistoryHeader>{`${identity.name} Website`}</HistoryHeader>
<UploadArea showHelp={false} uploadOrigin={{ origin: 'FEED', uuid }} />
{available && identity.feedHash ? (
<>
<Box mb={0.25}>
<ExpandableListItemKey label="Feed hash" value={identity.feedHash} />
</Box>
<Box mb={4}>
<ExpandableListItemLink
label="BZZ Link"
value={`https://${swarmCid.encodeFeedReference(identity.feedHash)}.bzz.link`}
/>
</Box>
</>
) : (
<Box mb={4}>
<DocumentationText>
This feed is curently not pointing anywhere, you can update the feed to fix this. Please refer to the{' '}
<a
href="https://docs.ethswarm.org/api/#tag/Feed/paths/~1feeds~1{owner}~1{topic}/post"
target="_blank"
rel="noreferrer"
>
official Bee documentation
</a>
.
</DocumentationText>
</Box>
)}
<ExpandableListItemActions>
<SwarmButton iconType={X} onClick={onClose} cancel>
Close
</SwarmButton>
</ExpandableListItemActions>
</div>
)
}
+113
View File
@@ -0,0 +1,113 @@
import { Box, createStyles, makeStyles, TextareaAutosize, Theme } from '@material-ui/core'
import { useSnackbar } from 'notistack'
import React, { ReactElement, useContext, useRef, useState } from 'react'
import { Check, Upload } from 'react-feather'
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
import { SwarmButton } from '../../components/SwarmButton'
import { SwarmDialog } from '../../components/SwarmDialog'
import { SwarmTextInput } from '../../components/SwarmTextInput'
import { TitleWithClose } from '../../components/TitleWithClose'
import { Context, Identity } from '../../providers/Feeds'
import { importIdentity, persistIdentity } from '../../utils/identity'
interface Props {
onClose: () => void
}
const useStyles = makeStyles((theme: Theme) =>
createStyles({
textarea: {
width: '100%',
border: 0,
padding: theme.spacing(1),
},
displayNone: {
display: 'none',
},
}),
)
export function ImportFeedDialog({ onClose }: Props): ReactElement {
const [textareaValue, setTextareaValue] = useState('')
const [name, setName] = useState('')
const fileInputRef = useRef(null)
const { identities, setIdentities } = useContext(Context)
const { enqueueSnackbar } = useSnackbar()
const classes = useStyles()
async function onImport() {
const feed = await importIdentity(name, textareaValue)
if (feed) {
onFeedReady(feed)
} else {
enqueueSnackbar('Feed is not valid', { variant: 'error' })
}
}
function onUploadIdentityFile() {
if (fileInputRef.current) {
const input = fileInputRef.current as HTMLInputElement
input.click()
}
}
function onIdentityFileSelected(event: React.ChangeEvent<HTMLInputElement>) {
const fileReader = new FileReader()
const file = event.target?.files?.[0]
fileReader.onload = async event => {
const string = event.target?.result
if (string) {
const feed = await importIdentity(name, string as string)
if (feed) {
onFeedReady(feed)
} else {
enqueueSnackbar('Feed is not valid', { variant: 'error' })
}
}
}
if (file) {
fileReader.readAsText(file)
}
}
function onFeedReady(identity: Identity) {
persistIdentity(identities, identity)
setIdentities(identities)
enqueueSnackbar('Feed imported successfully', { variant: 'success' })
onClose()
}
return (
<SwarmDialog>
<input onChange={onIdentityFileSelected} ref={fileInputRef} className={classes.displayNone} type="file" />
<Box mb={4}>
<TitleWithClose onClose={onClose}>Import</TitleWithClose>
</Box>
<Box mb={2}>
<SwarmTextInput label="Identity Name" name="name" onChange={event => setName(event.target.value)} />
</Box>
<Box mb={4}>
<TextareaAutosize
className={classes.textarea}
minRows={5}
onChange={event => setTextareaValue(event.target.value)}
/>
</Box>
<ExpandableListItemActions>
<SwarmButton iconType={Upload} onClick={onUploadIdentityFile}>
Upload Json File
</SwarmButton>
<SwarmButton iconType={Check} onClick={onImport}>
Use Pasted Text
</SwarmButton>
</ExpandableListItemActions>
</SwarmDialog>
)
}
+151
View File
@@ -0,0 +1,151 @@
import { Box, Grid, Typography } from '@material-ui/core'
import { useSnackbar } from 'notistack'
import { ReactElement, useContext, useEffect, useState } from 'react'
import { Bookmark, X } from 'react-feather'
import { RouteComponentProps, useHistory } from 'react-router'
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
import { HistoryHeader } from '../../components/HistoryHeader'
import { SwarmButton } from '../../components/SwarmButton'
import { SelectEvent, SwarmSelect } from '../../components/SwarmSelect'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { Context as BeeContext } from '../../providers/Bee'
import { Context as IdentityContext, Identity } from '../../providers/Feeds'
import { Context as SettingsContext } from '../../providers/Settings'
import { Context as StampContext } from '../../providers/Stamps'
import { ROUTES } from '../../routes'
import { persistIdentity, updateFeed } from '../../utils/identity'
import { FeedPasswordDialog } from './FeedPasswordDialog'
interface MatchParams {
hash: string
}
export default function UpdateFeed(props: RouteComponentProps<MatchParams>): ReactElement {
const { identities, setIdentities } = useContext(IdentityContext)
const { beeApi, beeDebugApi } = useContext(SettingsContext)
const { stamps, refresh } = useContext(StampContext)
const { status } = useContext(BeeContext)
const [selectedStamp, setSelectedStamp] = useState<string | null>(null)
const [selectedIdentity, setSelectedIdentity] = useState<Identity | null>(null)
const [loading, setLoading] = useState(false)
const { enqueueSnackbar } = useSnackbar()
const [showPasswordPrompt, setShowPasswordPrompt] = useState(false)
const history = useHistory()
useEffect(() => {
refresh()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
function onFeedChange(event: SelectEvent) {
const uuid = event.target.value
setSelectedIdentity(identities.find(x => x.uuid === uuid) || null)
}
function onStampChange(event: SelectEvent) {
const batchId = event.target.value as string
setSelectedStamp(batchId)
}
function onCancel() {
history.goBack()
}
function onBeginUpdatingFeed() {
if (!selectedIdentity) {
return
}
if (selectedIdentity.type === 'V3') {
setShowPasswordPrompt(true)
} else {
onFeedUpdate(selectedIdentity)
}
}
async function onFeedUpdate(identity: Identity, password?: string) {
setLoading(true)
if (!beeApi || !beeDebugApi || !selectedStamp) {
enqueueSnackbar(<span>Bee API unavailabe</span>, { variant: 'error' })
setLoading(false)
return
}
try {
await updateFeed(beeApi, identity, props.match.params.hash, selectedStamp, password as string)
persistIdentity(identities, identity)
setIdentities([...identities])
history.push(ROUTES.FEEDS_PAGE.replace(':uuid', identity.uuid))
} catch (error: unknown) {
setLoading(false)
const message = (typeof error === 'object' && error !== null && Reflect.get(error, 'message')) || ''
if (message.includes('possibly wrong passphrase')) {
enqueueSnackbar('Wrong password, please try again', { variant: 'error' })
} else {
enqueueSnackbar('Could not update feed at this time, please try again later', { variant: 'error' })
}
}
}
if (!status.all) return <TroubleshootConnectionCard />
return (
<div>
{showPasswordPrompt && selectedIdentity && (
<FeedPasswordDialog
feedName={selectedIdentity.name + ' Website'}
onCancel={() => {
setShowPasswordPrompt(false)
}}
onProceed={(password: string) => {
onFeedUpdate(selectedIdentity, password)
}}
loading={loading}
/>
)}
<HistoryHeader>Update feed</HistoryHeader>
<Box mb={2}>
<Grid container>
<SwarmSelect
options={identities.map(x => ({ value: x.uuid, label: `${x.name} Website` }))}
onChange={onFeedChange}
label="Feed"
/>
</Grid>
</Box>
<Box mb={4}>
<Grid container>
{stamps ? (
<SwarmSelect
options={stamps.map(x => ({ value: x.batchID, label: x.batchID.slice(0, 8) }))}
onChange={onStampChange}
label="Stamp"
/>
) : (
<Typography>You need to buy a stamp first to be able to update a feed.</Typography>
)}
</Grid>
</Box>
<ExpandableListItemActions>
<SwarmButton
onClick={onBeginUpdatingFeed}
iconType={Bookmark}
loading={!showPasswordPrompt && loading}
disabled={loading || !selectedStamp || !selectedIdentity}
>
Update Selected Feed
</SwarmButton>
<SwarmButton onClick={onCancel} iconType={X} disabled={loading} cancel>
Close
</SwarmButton>
</ExpandableListItemActions>
</div>
)
}
+115
View File
@@ -0,0 +1,115 @@
import { Box, Typography } from '@material-ui/core'
import { ReactElement, useContext, useState } from 'react'
import { Download, Info, PlusSquare, Trash } from 'react-feather'
import { useHistory } from 'react-router'
import ExpandableList from '../../components/ExpandableList'
import ExpandableListItem from '../../components/ExpandableListItem'
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
import ExpandableListItemKey from '../../components/ExpandableListItemKey'
import { SwarmButton } from '../../components/SwarmButton'
import { Context as BeeContext } from '../../providers/Bee'
import { Context as IdentityContext, Identity } from '../../providers/Feeds'
import { ROUTES } from '../../routes'
import { formatEnum } from '../../utils'
import { persistIdentitiesWithoutUpdate } from '../../utils/identity'
import { DeleteFeedDialog } from './DeleteFeedDialog'
import { ExportFeedDialog } from './ExportFeedDialog'
import { ImportFeedDialog } from './ImportFeedDialog'
export default function Feeds(): ReactElement {
const { identities, setIdentities } = useContext(IdentityContext)
const { status } = useContext(BeeContext)
const history = useHistory()
const [selectedIdentity, setSelectedIdentity] = useState<Identity | null>(null)
const [showImport, setShowImport] = useState(false)
const [showExport, setShowExport] = useState(false)
const [showDelete, setShowDelete] = useState(false)
function createNewFeed() {
return history.push(ROUTES.FEEDS_NEW)
}
function viewFeed(uuid: string) {
history.push(ROUTES.FEEDS_PAGE.replace(':uuid', uuid))
}
function onDialogClose() {
setShowDelete(false)
setShowExport(false)
setShowImport(false)
setSelectedIdentity(null)
}
function onDelete(identity: Identity) {
onDialogClose()
const updatedFeeds = identities.filter(x => x.uuid !== identity.uuid)
setIdentities(updatedFeeds)
persistIdentitiesWithoutUpdate(updatedFeeds)
}
function onShowExport(identity: Identity) {
setSelectedIdentity(identity)
setShowExport(true)
}
function onShowDelete(identity: Identity) {
setSelectedIdentity(identity)
setShowDelete(true)
}
return (
<div>
{showImport && <ImportFeedDialog onClose={() => setShowImport(false)} />}
{showExport && selectedIdentity && <ExportFeedDialog identity={selectedIdentity} onClose={onDialogClose} />}
{showDelete && selectedIdentity && (
<DeleteFeedDialog
identity={selectedIdentity}
onClose={onDialogClose}
onConfirm={(identity: Identity) => onDelete(identity)}
/>
)}
<Box mb={4}>
<Typography variant="h1">Feeds</Typography>
</Box>
<Box mb={4}>
<ExpandableListItemActions>
<SwarmButton iconType={PlusSquare} onClick={createNewFeed}>
Create New Feed
</SwarmButton>
<SwarmButton iconType={PlusSquare} onClick={() => setShowImport(true)}>
Import Feed
</SwarmButton>
</ExpandableListItemActions>
</Box>
{identities.map((x, i) => (
<ExpandableList key={i} label={`${x.name} Website`} defaultOpen>
<Box mb={0.5}>
<ExpandableList label={x.name} level={1}>
<ExpandableListItemKey label="Identity address" value={x.address} />
<ExpandableListItem label="Identity type" value={formatEnum(x.type)} />
</ExpandableList>
</Box>
<ExpandableListItemKey label="Topic" value={'00'.repeat(32)} />
{x.feedHash && <ExpandableListItemKey label="Feed hash" value={x.feedHash} />}
<Box mt={0.75}>
<ExpandableListItemActions>
{status.all && (
<SwarmButton onClick={() => viewFeed(x.uuid)} iconType={Info}>
View Feed Page
</SwarmButton>
)}
<SwarmButton onClick={() => onShowExport(x)} iconType={Download}>
Export...
</SwarmButton>
<SwarmButton onClick={() => onShowDelete(x)} iconType={Trash}>
Delete...
</SwarmButton>
</ExpandableListItemActions>
</Box>
</ExpandableList>
))}
</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>
)
}
+35
View File
@@ -0,0 +1,35 @@
import * as swarmCid from '@ethersphere/swarm-cid'
import { Box } from '@material-ui/core'
import { ReactElement } from 'react'
import { DocumentationText } from '../../components/DocumentationText'
import ExpandableListItemKey from '../../components/ExpandableListItemKey'
import ExpandableListItemLink from '../../components/ExpandableListItemLink'
import { detectIndexHtml } from '../../utils/file'
import { SwarmFile } from '../../utils/SwarmFile'
interface Props {
files: SwarmFile[]
hash: string
}
export function AssetSummary({ files, 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}`} />
{detectIndexHtml(files) && (
<ExpandableListItemLink
label="BZZ Link"
value={`https://${swarmCid.encodeManifestReference(hash).toString()}.bzz.link`}
/>
)}
</Box>
<DocumentationText>
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>.
</DocumentationText>
</>
)
}
+92 -52
View File
@@ -1,66 +1,106 @@
import { ReactElement, useState, useContext } from 'react'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import { Paper, InputBase, IconButton, FormHelperText } from '@material-ui/core'
import { Search } from '@material-ui/icons'
import { Context as SettingsContext } from '../../providers/Settings'
import { Utils } from '@ethersphere/bee-js'
import { 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, defaultUploadOrigin } from '../../providers/File'
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) =>
createStyles({
root: {
padding: theme.spacing(0.25),
display: 'flex',
alignItems: 'center',
},
input: {
marginLeft: theme.spacing(1),
flex: 1,
},
iconButton: {
padding: 10,
},
divider: {
height: 28,
margin: 4,
},
}),
)
export function Download(): ReactElement {
const [loading, setLoading] = useState(false)
const { beeApi } = useContext(SettingsContext)
const [referenceError, setReferenceError] = useState<string | undefined>(undefined)
export default function Files(): ReactElement {
const classes = useStyles()
const { apiUrl } = useContext(SettingsContext)
const { setUploadOrigin } = useContext(Context)
const [referenceInput, setReferenceInput] = useState('')
const [referenceError, setReferenceError] = useState<Error | null>(null)
const { enqueueSnackbar } = useSnackbar()
const history = useHistory()
const handleReferenceChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setReferenceInput(e.target.value)
const validateChange = (value: string) => {
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.')
}
}
if (Utils.Hex.isHexString(e.target.value, 64) || Utils.Hex.isHexString(e.target.value, 128)) setReferenceError(null)
else setReferenceError(new Error('Incorrect format of swarm hash'))
async function onSwarmIdentifier(identifier: string) {
setLoading(true)
if (!beeApi) {
setLoading(false)
return
}
try {
const manifestJs = new ManifestJs(beeApi)
const feedIdentifier = await manifestJs.resolveFeedManifest(identifier)
if (feedIdentifier) {
identifier = feedIdentifier
}
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))
setUploadOrigin(defaultUploadOrigin)
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 (
<>
<Paper className={classes.root}>
<InputBase
className={classes.input}
placeholder="Enter swarm reference e.g. 0773a91efd6547c754fc1d95fb1c62c7d1b47f959c2caa685dfec8736da95c1c"
inputProps={{ 'aria-label': 'retrieve file from swarm' }}
value={referenceInput}
onChange={handleReferenceChange}
<FileNavigation active="DOWNLOAD" />
<ExpandableListItemInput
label="Swarm Hash"
onConfirm={value => onSwarmIdentifier(value)}
onChange={validateChange}
helperText={referenceError}
confirmLabel={'Find'}
confirmLabelDisabled={Boolean(referenceError) || loading}
placeholder="e.g. 31fb0362b1a42536134c86bc58b97ac0244e5c6630beec3e27c2d1cecb38c605"
expandedOnly
mapperFn={value => recognizeSwarmHash(value)}
loading={loading}
/>
<IconButton
href={`${apiUrl}/bzz/${referenceInput}`}
target="_blank"
disabled={referenceError !== null || !referenceInput}
className={classes.iconButton}
aria-label="download"
>
<Search />
</IconButton>
</Paper>
{referenceError && <FormHelperText error>{referenceError.message}</FormHelperText>}
<History title="Download History" localStorageKey={HISTORY_KEYS.DOWNLOAD_HISTORY} />
</>
)
}
+46
View File
@@ -0,0 +1,46 @@
import { Box, Grid } from '@material-ui/core'
import { ReactElement } from 'react'
import { Bookmark, Download, Link, X } from 'react-feather'
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
import { SwarmButton } from '../../components/SwarmButton'
interface Props {
onOpen: () => void
onCancel: () => void
onDownload: () => void
onUpdateFeed: () => void
hasIndexDocument: boolean
loading: boolean
}
export function DownloadActionBar({
onOpen,
onCancel,
onDownload,
onUpdateFeed,
hasIndexDocument,
loading,
}: Props): ReactElement {
return (
<Grid container justifyContent="space-between">
<ExpandableListItemActions>
{hasIndexDocument && (
<SwarmButton onClick={onOpen} iconType={Link} disabled={loading}>
View Website
</SwarmButton>
)}
<SwarmButton onClick={onDownload} iconType={Download} disabled={loading} loading={loading}>
Download
</SwarmButton>
<SwarmButton onClick={onCancel} iconType={X} disabled={loading} loading={loading} cancel>
Close
</SwarmButton>
</ExpandableListItemActions>
<Box mb={1} mr={1}>
<SwarmButton onClick={onUpdateFeed} iconType={Bookmark}>
Update Feed
</SwarmButton>
</Box>
</Grid>
)
}
+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 PeerDetailDrawer from '../../components/PeerDetail'
import { Button, ListItemIcon, Typography, Menu, MenuItem } from '@material-ui/core'
import { EnrichedPostageBatch } from '../../providers/Stamps'
interface Props {
@@ -25,10 +21,10 @@ export default function SimpleMenu({ stamps, selectedStamp, setSelected }: Props
return (
<div>
<Button aria-controls="simple-menu" aria-haspopup="true" onClick={handleClick}>
<Button variant="contained" aria-haspopup="true" onClick={handleClick}>
Change
</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 => (
<MenuItem
key={stamp.batchID}
@@ -39,7 +35,7 @@ export default function SimpleMenu({ stamps, selectedStamp, setSelected }: Props
selected={stamp.batchID === selectedStamp?.batchID}
>
<ListItemIcon>{stamp.usageText}</ListItemIcon>
<PeerDetailDrawer peerId={stamp.batchID} />
<Typography variant="body2">{stamp.batchID.substr(0, 8)}[]</Typography>
</MenuItem>
))}
</Menu>
+149
View File
@@ -0,0 +1,149 @@
import { ManifestJs } from '@ethersphere/manifest-js'
import { Box, Typography } from '@material-ui/core'
import { saveAs } from 'file-saver'
import JSZip from 'jszip'
import { useSnackbar } from 'notistack'
import { ReactElement, useContext, useEffect, useState } from 'react'
import { RouteComponentProps, useHistory } from 'react-router-dom'
import { HistoryHeader } from '../../components/HistoryHeader'
import { Loading } from '../../components/Loading'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { Context as BeeContext } from '../../providers/Bee'
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 { status } = useContext(BeeContext)
const reference = props.match.params.hash
const history = useHistory()
const { enqueueSnackbar } = useSnackbar()
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)
const [notFound, setNotFound] = useState(false)
async function prepare() {
if (!beeApi || !status.all) {
return
}
const manifestJs = new ManifestJs(beeApi)
const isManifest = await manifestJs.isManifest(reference)
if (!isManifest) {
setNotFound(true)
enqueueSnackbar('The specified hash does not contain valid content.', { variant: 'error' })
return
}
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()
}
}
function onUpdateFeed() {
history.push(ROUTES.FEEDS_UPDATE.replace(':hash', reference))
}
useEffect(() => {
setLoading(true)
prepare().finally(() => {
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 (!status.all) return <TroubleshootConnectionCard />
if (loading) {
return <Loading />
}
if (notFound) {
return (
<>
<HistoryHeader>Not Found</HistoryHeader>
<Typography>The specified hash is not found.</Typography>
</>
)
}
return (
<>
<Box mb={4}>
<AssetPreview files={files} assetName={assetName} />
</Box>
<Box mb={4}>
<AssetSummary files={files} hash={reference} />
</Box>
<DownloadActionBar
onOpen={onOpen}
onCancel={onClose}
onDownload={onDownload}
onUpdateFeed={onUpdateFeed}
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>
)
}
+135 -85
View File
@@ -1,114 +1,164 @@
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 { Box } from '@material-ui/core'
import { useSnackbar } from 'notistack'
import { ReactElement, useContext, useEffect, useState } from 'react'
import UploadSizeAlert from '../../components/AlertUploadSize'
import ClipboardCopy from '../../components/ClipboardCopy'
import PeerDetailDrawer from '../../components/PeerDetail'
import { Context, EnrichedPostageBatch } from '../../providers/Stamps'
import { useHistory } from 'react-router-dom'
import { DocumentationText } from '../../components/DocumentationText'
import { HistoryHeader } from '../../components/HistoryHeader'
import { ProgressIndicator } from '../../components/ProgressIndicator'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { Context as BeeContext } from '../../providers/Bee'
import { Context as IdentityContext, Identity } from '../../providers/Feeds'
import { Context as FileContext } from '../../providers/File'
import { Context as SettingsContext } from '../../providers/Settings'
import CreatePostageStamp from '../stamps/CreatePostageStampModal'
import SelectStamp from './SelectStamp'
import { Context as StampsContext, EnrichedPostageBatch } from '../../providers/Stamps'
import { ROUTES } from '../../routes'
import { detectIndexHtml, getAssetNameFromFiles } from '../../utils/file'
import { persistIdentity, updateFeed } from '../../utils/identity'
import { HISTORY_KEYS, putHistory } from '../../utils/local-storage'
import { FeedPasswordDialog } from '../feeds/FeedPasswordDialog'
import { PostageStampCreation } from '../stamps/PostageStampCreation'
import { PostageStampSelector } from '../stamps/PostageStampSelector'
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 [step, setStep] = useState(0)
const [stampMode, setStampMode] = useState<'SELECT' | 'BUY'>('SELECT')
const [stamp, setStamp] = useState<EnrichedPostageBatch | null>(null)
const [isUploading, setUploading] = useState(false)
const [showPasswordPrompt, setShowPasswordPrompt] = useState(false)
export default function Files(): ReactElement {
const [dropzoneKey, setDropzoneKey] = useState(0)
const [file, setFile] = useState<File | null>(null)
const [uploadReference, setUploadReference] = useState('')
const [isUploadingFile, setIsUploadingFile] = useState(false)
const [selectedStamp, setSelectedStamp] = useState<EnrichedPostageBatch | null>(null)
const { isLoading, error, stamps } = useContext(Context)
const { refresh } = useContext(StampsContext)
const { beeApi } = useContext(SettingsContext)
const { files, setFiles, uploadOrigin } = useContext(FileContext)
const { identities, setIdentities } = useContext(IdentityContext)
const { status } = useContext(BeeContext)
const { enqueueSnackbar } = useSnackbar()
const history = useHistory()
// Choose a postage stamp that has the lowest usage
useEffect(() => {
if (!selectedStamp && stamps && stamps.length > 0) {
const stamp = stamps.reduce((prev, curr) => {
if (curr.usage < prev.usage) return curr
refresh()
}, []) // eslint-disable-line react-hooks/exhaustive-deps
return prev
}, stamps[0])
if (!status.all) return <TroubleshootConnectionCard />
setSelectedStamp(stamp)
if (!files.length) {
setFiles([])
history.replace(ROUTES.UPLOAD)
return <></>
}
}, [isLoading, error, stamps, selectedStamp])
const uploadFile = () => {
if (file === null || selectedStamp === null) return
const identity = uploadOrigin.uuid ? identities.find(x => x.uuid === uploadOrigin.uuid) : null
if (!beeApi) return
const onUpload = () => {
if (uploadOrigin.origin === 'UPLOAD') {
uploadFiles()
} else {
if ((identity as Identity).type === 'PRIVATE_KEY') {
uploadFiles()
} else {
setShowPasswordPrompt(true)
}
}
}
const uploadFiles = (password?: string) => {
if (!beeApi || !files.length || !stamp) {
return
}
const indexDocument = files.length === 1 ? files[0].name : detectIndexHtml(files) || undefined
setUploading(true)
setIsUploadingFile(true)
beeApi
.uploadFile(selectedStamp.batchID, file)
.uploadFiles(stamp.batchID, files as unknown as File[], { indexDocument })
.then(hash => {
window.setTimeout(() => {
setFile(null)
setUploadReference(hash)
setDropzoneKey(dropzoneKey + 1)
}, 0)
putHistory(HISTORY_KEYS.UPLOAD_HISTORY, hash.reference, getAssetNameFromFiles(files))
if (uploadOrigin.origin === 'UPLOAD') {
history.replace(ROUTES.HASH.replace(':hash', hash.reference))
} else {
updateFeed(beeApi, identity as Identity, hash.reference, stamp.batchID, password as string).then(() => {
persistIdentity(identities, identity as Identity)
setIdentities([...identities])
history.replace(ROUTES.FEEDS_PAGE.replace(':uuid', uploadOrigin.uuid as string))
})
.catch(e => enqueueSnackbar(`Error uploading: ${e.message}`, { variant: 'error' }))
.finally(() => {
setIsUploadingFile(false)
}
})
.catch(e => {
enqueueSnackbar(`Error uploading: ${e.message}`, { variant: 'error' })
setUploading(false)
})
}
const handleChange = (files?: File[]) => {
if (files) {
setFile(files[0])
const reset = () => {
setStep(0)
setFiles([])
setStamp(null)
setUploading(false)
}
const onFeedPasswordGiven = (password: string) => {
uploadFiles(password)
}
return (
<div>
<div>
<DropzoneArea
key={'dropzone-' + dropzoneKey}
onChange={handleChange}
filesLimit={1}
maxFileSize={MAX_FILE_SIZE}
<>
{showPasswordPrompt && (
<FeedPasswordDialog
loading={isUploading}
feedName={(identity as Identity).name}
onCancel={() => setShowPasswordPrompt(false)}
onProceed={onFeedPasswordGiven}
/>
<div style={{ marginTop: '15px' }}>
{selectedStamp && (
<div style={{ display: 'flex' }}>
<small>
with Postage Stamp{' '}
<Chip
avatar={<Avatar>{selectedStamp.usageText}</Avatar>}
label={<PeerDetailDrawer peerId={selectedStamp.batchID} characterLength={6} />}
deleteIcon={<ClipboardCopy value={selectedStamp.batchID} />}
onDelete={() => {} /* eslint-disable-line*/}
variant="outlined"
)}
{identity && <HistoryHeader>{`Update "${identity.name}"`}</HistoryHeader>}
{!identity && <HistoryHeader>Upload</HistoryHeader>}
<Box mb={4}>
<ProgressIndicator steps={['Preview', 'Add postage stamp', 'Upload to node']} index={step} />
</Box>
{(step === 0 || step === 2) && <AssetPreview files={files} />}
{step === 1 && (
<>
<Box mb={2}>
{stampMode === 'SELECT' ? (
<PostageStampSelector onSelect={stamp => setStamp(stamp)} defaultValue={stamp?.batchID} />
) : (
<PostageStampCreation onFinished={() => setStampMode('SELECT')} />
)}
</Box>
<Box mb={4}>
<DocumentationText>
Please refer to the{' '}
<a
href="https://docs.ethswarm.org/debug-api/#tag/Postage-Stamps/paths/~1stamps~1{amount}~1{depth}/post"
target="_blank"
rel="noreferrer"
>
official Bee documentation
</a>{' '}
to understand these values.
</DocumentationText>
</Box>
</>
)}
{step === 2 && stamp && <StampPreview stamp={stamp} />}
<UploadActionBar
step={step}
onCancel={reset}
onGoBack={() => setStep(step => step - 1)}
onProceed={() => setStep(step => step + 1)}
onUpload={onUpload}
isUploading={isUploading}
hasStamp={Boolean(stamp)}
uploadLabel={identity ? 'Update Feed' : 'Upload To Your Node'}
stampMode={stampMode}
setStampMode={setStampMode}
/>
</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>
</>
)
}
+88
View File
@@ -0,0 +1,88 @@
import { Box, Grid } from '@material-ui/core'
import { ReactElement } from 'react'
import { ArrowLeft, Check, Layers, PlusSquare, X } from 'react-feather'
import { DocumentationText } from '../../components/DocumentationText'
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
import { SwarmButton } from '../../components/SwarmButton'
interface Props {
step: number
onUpload: () => void
onCancel: () => void
onGoBack: () => void
onProceed: () => void
isUploading: boolean
hasStamp: boolean
uploadLabel: string
stampMode: 'BUY' | 'SELECT'
setStampMode: (mode: 'BUY' | 'SELECT') => void
}
export function UploadActionBar({
step,
onUpload,
onCancel,
onGoBack,
onProceed,
isUploading,
hasStamp,
uploadLabel,
stampMode,
setStampMode,
}: Props): ReactElement {
if (step === 0) {
return (
<>
<Box mb={1}>
<ExpandableListItemActions>
<SwarmButton onClick={onProceed} iconType={Layers}>
Add Postage Stamp
</SwarmButton>
<SwarmButton onClick={onCancel} iconType={X} cancel>
Cancel
</SwarmButton>
</ExpandableListItemActions>
</Box>
<DocumentationText>You need a postage stamp to upload.</DocumentationText>
</>
)
}
if (step === 1) {
return (
<Grid container direction="row" justifyContent="space-between">
<ExpandableListItemActions>
{stampMode === 'SELECT' && (
<SwarmButton onClick={onProceed} iconType={Check} disabled={!hasStamp}>
Proceed With Selected Stamp
</SwarmButton>
)}
<SwarmButton onClick={onGoBack} iconType={ArrowLeft} cancel>
Back To Preview
</SwarmButton>
</ExpandableListItemActions>
<SwarmButton
onClick={() => setStampMode(stampMode === 'BUY' ? 'SELECT' : 'BUY')}
iconType={stampMode === 'BUY' ? Layers : PlusSquare}
>
{stampMode === 'BUY' ? 'Use Existing Stamp' : 'Buy New Stamp'}
</SwarmButton>
</Grid>
)
}
if (step === 2) {
return (
<ExpandableListItemActions>
<SwarmButton onClick={onUpload} iconType={Check} disabled={isUploading} loading={isUploading}>
{uploadLabel}
</SwarmButton>
<SwarmButton onClick={onGoBack} iconType={ArrowLeft} disabled={isUploading} cancel>
Change Postage Stamp
</SwarmButton>
</ExpandableListItemActions>
)
}
return <></>
}
+154
View File
@@ -0,0 +1,154 @@
import { createStyles, makeStyles, Theme } 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 { DocumentationText } from '../../components/DocumentationText'
import { SwarmButton } from '../../components/SwarmButton'
import { Context, UploadOrigin } from '../../providers/File'
import { ROUTES } from '../../routes'
import { detectIndexHtml } from '../../utils/file'
import { SwarmFile } from '../../utils/SwarmFile'
interface Props {
uploadOrigin: UploadOrigin
showHelp: boolean
}
const MAX_FILE_SIZE = 1_000_000_000 // 1 gigabyte
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({ uploadOrigin, showHelp }: Props): ReactElement {
const { setFiles, setUploadOrigin } = 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) {
setUploadOrigin(uploadOrigin)
history.push(ROUTES.UPLOAD_IN_PROGRESS)
}
}
}
return (
<>
<div className={classes.areaWrapper}>
<DropzoneArea
key={version}
dropzoneClass={classes.dropzone}
onChange={handleChange}
filesLimit={1e9}
maxFileSize={MAX_FILE_SIZE}
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>
{showHelp && (
<DocumentationText>
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.
</DocumentationText>
)}
</>
)
}
+22
View File
@@ -0,0 +1,22 @@
import { ReactElement, useContext } from 'react'
import { History } from '../../components/History'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { Context as BeeContext } from '../../providers/Bee'
import { defaultUploadOrigin } from '../../providers/File'
import { HISTORY_KEYS } from '../../utils/local-storage'
import { FileNavigation } from './FileNavigation'
import { UploadArea } from './UploadArea'
export function UploadLander(): ReactElement {
const { status } = useContext(BeeContext)
if (!status.all) return <TroubleshootConnectionCard />
return (
<>
<FileNavigation active="UPLOAD" />
<UploadArea showHelp={true} uploadOrigin={defaultUploadOrigin} />
<History title="Upload History" localStorageKey={HISTORY_KEYS.UPLOAD_HISTORY} />
</>
)
}
-32
View File
@@ -1,32 +0,0 @@
import { ReactElement, useContext } from 'react'
import { Container } from '@material-ui/core'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { Context } from '../../providers/Bee'
import Download from './Download'
import Upload from './Upload'
import TabsContainer from '../../components/TabsContainer'
export default function Files(): ReactElement {
const { status } = useContext(Context)
if (!status.all) return <TroubleshootConnectionCard />
return (
<Container maxWidth="sm">
<TabsContainer
values={[
{
label: 'download',
component: <Download />,
},
{
label: 'upload',
component: <Upload />,
},
]}
/>
</Container>
)
}
+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>
)
}
-95
View File
@@ -1,95 +0,0 @@
import { ReactElement, useState, useContext } from 'react'
import { makeStyles } from '@material-ui/core/styles'
import {
Table,
TableBody,
TableCell,
TableContainer,
TableRow,
TableHead,
Button,
Paper,
Tooltip,
CircularProgress,
} from '@material-ui/core'
import { Autorenew } from '@material-ui/icons'
import { Context as SettingsContext } from '../../providers/Settings'
import type { Peer } from '@ethersphere/bee-js'
const useStyles = makeStyles({
table: {
minWidth: 650,
},
})
interface Props {
peers: Peer[] | null
}
interface PeerLatency {
rtt: string
loading: boolean
}
function getPingState(peerLatency: Record<string, PeerLatency>, peer: Peer): ReactElement {
if (peerLatency[peer.address]?.loading) return <CircularProgress size={20} />
if (peerLatency[peer.address]?.rtt) return <span>{peerLatency[peer.address]?.rtt}</span>
return <Autorenew />
}
function PeerTable(props: Props): ReactElement {
const classes = useStyles()
const { beeDebugApi } = useContext(SettingsContext)
const [peerLatency, setPeerLatency] = useState<Record<string, PeerLatency>>({})
const pingPeer = (peerId: string) => {
setPeerLatency(prevPeerLatency => ({ ...prevPeerLatency, [peerId]: { rtt: '', loading: true } }))
beeDebugApi
?.pingPeer(peerId)
.then(res => {
setPeerLatency(prevPeerLatency => ({ ...prevPeerLatency, [peerId]: { rtt: res.rtt, loading: false } }))
})
.catch(() => {
setPeerLatency(prevPeerLatency => ({ ...prevPeerLatency, [peerId]: { rtt: 'error', loading: false } }))
})
}
return (
<div>
<TableContainer component={Paper}>
<Table className={classes.table}>
<TableHead>
<TableRow>
<TableCell>Index</TableCell>
<TableCell>Peer Id</TableCell>
<TableCell align="right">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{props.peers?.map((peer: Peer, idx: number) => (
<TableRow key={peer.address}>
<TableCell component="th" scope="row">
{idx + 1}
</TableCell>
<TableCell>{peer.address}</TableCell>
<TableCell align="right">
<Tooltip title="Ping node">
<Button color="primary" onClick={() => pingPeer(peer.address)}>
{getPingState(peerLatency, peer)}
</Button>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</div>
)
}
export default PeerTable
-21
View File
@@ -1,21 +0,0 @@
import PeerTable from './PeerTable'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { Context } from '../../providers/Bee'
import TopologyStats from '../../components/TopologyStats'
import { ReactElement, useContext } from 'react'
export default function Peers(): ReactElement {
const { topology, peers, status } = useContext(Context)
if (!status.all) {
return <TroubleshootConnectionCard />
}
return (
<>
<TopologyStats topology={topology} />
<PeerTable peers={peers} />
</>
)
}
+12 -62
View File
@@ -1,70 +1,20 @@
import React, { ReactElement, useState, useContext } from 'react'
import { Paper, Container, TextField, Typography, Button } from '@material-ui/core'
import { ReactElement, useContext } from 'react'
import ExpandableList from '../../components/ExpandableList'
import ExpandableListItemInput from '../../components/ExpandableListItemInput'
import { Context as SettingsContext } from '../../providers/Settings'
export default function Settings(): ReactElement {
const { apiUrl, apiDebugUrl, setApiUrl, setDebugApiUrl } = useContext(SettingsContext)
const [host, setHost] = useState(apiUrl)
const [debugHost, setDebugHost] = useState(apiDebugUrl)
const submit = () => {
if (host !== apiUrl) setApiUrl(host)
if (debugHost !== apiDebugUrl) setDebugApiUrl(debugHost)
}
const touched = host !== apiUrl || debugHost !== apiDebugUrl
const { apiUrl, apiDebugUrl, setApiUrl, setDebugApiUrl, lockedApiSettings } = useContext(SettingsContext)
return (
<div>
<Container>
<Typography variant="h4" gutterBottom>
Settings
</Typography>
<Paper>
<TextField
label="API Endpoint"
style={{ margin: 0 }}
placeholder="ex: 127.0.0.0.1:1633"
helperText="Enter node host override / port"
fullWidth
defaultValue={apiUrl}
margin="normal"
InputLabelProps={{
shrink: true,
}}
onChange={e => {
setHost(e.target.value)
}}
variant="filled"
<ExpandableList label="API Settings" defaultOpen>
<ExpandableListItemInput label="Bee API" value={apiUrl} onConfirm={setApiUrl} locked={lockedApiSettings} />
<ExpandableListItemInput
label="Bee Debug API"
value={apiDebugUrl}
onConfirm={setDebugApiUrl}
locked={lockedApiSettings}
/>
</Paper>
<Paper style={{ marginTop: '20px' }}>
<TextField
label="Debug API Endpoint"
style={{ margin: 0 }}
placeholder="ex: 127.0.0.0.1:1635"
helperText="Enter node debug host override / port"
fullWidth
defaultValue={apiDebugUrl}
onChange={e => {
setDebugHost(e.target.value)
}}
margin="normal"
InputLabelProps={{
shrink: true,
}}
variant="filled"
/>
</Paper>
{touched ? (
<div style={{ marginTop: '20px' }}>
<Button variant="outlined" color="primary" onClick={submit}>
Save
</Button>
</div>
) : null}
</Container>
</div>
</ExpandableList>
)
}
@@ -1,162 +0,0 @@
import React, { ReactElement, useContext } from 'react'
import Button from '@material-ui/core/Button'
import Dialog from '@material-ui/core/Dialog'
import DialogActions from '@material-ui/core/DialogActions'
import DialogContent from '@material-ui/core/DialogContent'
import DialogContentText from '@material-ui/core/DialogContentText'
import CircularProgress from '@material-ui/core/CircularProgress'
import DialogTitle from '@material-ui/core/DialogTitle'
import BigNumber from 'bignumber.js'
import { FormikHelpers, Form, Field, Formik } from 'formik'
import { TextField } from 'formik-material-ui'
import { Context as SettingsContext } from '../../providers/Settings'
import { Context } from '../../providers/Stamps'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import { useSnackbar } from 'notistack'
interface FormValues {
depth?: string
amount?: string
label?: string
}
type FormErrors = Partial<FormValues>
const initialFormValues: FormValues = {
depth: '',
amount: '',
label: '',
}
const useStyles = makeStyles((theme: Theme) =>
createStyles({
wrapper: {
margin: theme.spacing(1),
position: 'relative',
},
field: {
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
},
buttonProgress: {
position: 'absolute',
top: '50%',
left: '50%',
marginTop: -12,
marginBottom: -12,
},
}),
)
interface Props {
label?: string
}
export default function FormDialog({ label }: Props): ReactElement {
const classes = useStyles()
const [open, setOpen] = React.useState(false)
const { refresh } = useContext(Context)
const { beeApi } = useContext(SettingsContext)
const handleClickOpen = () => setOpen(true)
const handleClose = () => setOpen(false)
const { enqueueSnackbar } = useSnackbar()
return (
<Formik
initialValues={initialFormValues}
onSubmit={async (values: FormValues, actions: FormikHelpers<FormValues>) => {
try {
// This is really just a typeguard, the validation pretty much guarantees these will have the right values
if (!values.depth || !values.amount) return
if (!beeApi) return
const amount = BigInt(values.amount)
const depth = Number.parseInt(values.depth)
const options = values.label ? { label: values.label } : undefined
await beeApi.createPostageBatch(amount.toString(), depth, options)
actions.resetForm()
await refresh()
handleClose()
} catch (e) {
enqueueSnackbar(`Error: ${e.message}`, { variant: 'error' })
actions.setSubmitting(false)
}
}}
validate={(values: FormValues) => {
const errors: FormErrors = {}
// Depth
if (!values.depth) errors.depth = 'Required field'
else {
const depth = new BigNumber(values.depth)
if (!depth.isInteger()) errors.depth = 'Depth must be an integer'
else if (depth.isLessThan(16)) errors.depth = 'Minimal depth is 16'
else if (depth.isGreaterThan(255)) errors.depth = 'Depth has to be at most 255'
}
// Amount
if (!values.amount) errors.amount = 'Required field'
else {
const amount = new BigNumber(values.amount)
if (!amount.isInteger()) errors.amount = 'Amount must be an integer'
else if (amount.isLessThanOrEqualTo(0)) errors.amount = 'Amount must be greater than 0'
}
// Label
if (values.label && !/^[0-9a-z]*$/i.test(values.label)) errors.label = 'Label must be an alphanumeric string'
return errors
}}
>
{({ submitForm, isValid, isSubmitting, values }) => (
<Form>
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
{label || 'Buy Postage Stamp'}
{isSubmitting && <CircularProgress size={24} className={classes.buttonProgress} />}
</Button>
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">Purchase new postage stamp</DialogTitle>
<DialogContent>
<DialogContentText>
Provide the depth, amount and optionally the label of the postage stamp. Please refer to the{' '}
<a href="https://docs.ethswarm.org/docs/access-the-swarm/keep-your-data-alive" target="blank">
official bee docs
</a>{' '}
to understand these values.
</DialogContentText>
<Field
component={TextField}
required
name="depth"
autoFocus
label="Depth"
fullWidth
className={classes.field}
/>
<Field component={TextField} required name="amount" label="Amount" fullWidth className={classes.field} />
<Field component={TextField} name="label" label="Label" fullWidth className={classes.field} />
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Cancel
</Button>
<div className={classes.wrapper}>
<Button
color="primary"
disabled={isSubmitting || !isValid || !values.amount || !values.depth}
type="submit"
variant="contained"
onClick={submitForm}
>
Create
{isSubmitting && <CircularProgress size={24} className={classes.buttonProgress} />}
</Button>
</div>
</DialogActions>
</Dialog>
</Form>
)}
</Formik>
)
}
@@ -0,0 +1,20 @@
import { ReactElement } from 'react'
import { useHistory } from 'react-router'
import { HistoryHeader } from '../../components/HistoryHeader'
import { ROUTES } from '../../routes'
import { PostageStampCreation } from './PostageStampCreation'
export function CreatePostageStampPage(): ReactElement {
const history = useHistory()
function onFinished() {
history.push(ROUTES.STAMPS)
}
return (
<div>
<HistoryHeader>Buy new postage stamp</HistoryHeader>
<PostageStampCreation onFinished={onFinished} />
</div>
)
}
+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>
)
}
+157
View File
@@ -0,0 +1,157 @@
import { Box, Grid, Typography } from '@material-ui/core'
import BigNumber from 'bignumber.js'
import { Form, Formik, FormikHelpers } from 'formik'
import { useSnackbar } from 'notistack'
import React, { ReactElement, useContext } from 'react'
import { Check } from 'react-feather'
import { SwarmButton } from '../../components/SwarmButton'
import { SwarmTextInput } from '../../components/SwarmTextInput'
import { Context as SettingsContext } from '../../providers/Settings'
import { Context } from '../../providers/Stamps'
import {
calculateStampPrice,
convertAmountToSeconds,
convertDepthToBytes,
formatBzz,
secondsToTimeString,
} from '../../utils'
import { getHumanReadableFileSize } from '../../utils/file'
interface FormValues {
depth?: string
amount?: string
label?: string
}
type FormErrors = Partial<FormValues>
const initialFormValues: FormValues = {
depth: '',
amount: '',
label: '',
}
interface Props {
onFinished: () => void
}
export function PostageStampCreation({ onFinished }: Props): ReactElement {
const { refresh } = useContext(Context)
const { beeDebugApi } = useContext(SettingsContext)
const { enqueueSnackbar } = useSnackbar()
function getFileSize(depth: number): string {
if (isNaN(depth) || depth < 17 || depth > 255) {
return '-'
}
return `~${getHumanReadableFileSize(convertDepthToBytes(depth))}`
}
function getTtl(amount: number): string {
if (isNaN(amount) || amount <= 0) {
return '-'
}
return secondsToTimeString(convertAmountToSeconds(amount))
}
function getPrice(depth: number, amount: number): string {
if (isNaN(amount) || amount <= 0 || isNaN(depth) || depth < 17 || depth > 255) {
return '-'
}
const price = calculateStampPrice(depth, amount)
return `${formatBzz(price)} BZZ`
}
return (
<Formik
initialValues={initialFormValues}
onSubmit={async (values: FormValues, actions: FormikHelpers<FormValues>) => {
try {
// This is really just a typeguard, the validation pretty much guarantees these will have the right values
if (!values.depth || !values.amount) return
if (!beeDebugApi) return
const amount = BigInt(values.amount)
const depth = Number.parseInt(values.depth)
const options = values.label ? { label: values.label } : undefined
await beeDebugApi.createPostageBatch(amount.toString(), depth, options)
actions.resetForm()
await refresh()
onFinished()
} catch (e) {
enqueueSnackbar(`Error: ${(e as Error).message}`, { variant: 'error' })
actions.setSubmitting(false)
}
}}
validate={(values: FormValues) => {
const errors: FormErrors = {}
// Depth
if (!values.depth) errors.depth = 'Required field'
else {
const depth = new BigNumber(values.depth)
if (!depth.isInteger()) errors.depth = 'Depth must be an integer'
else if (depth.isLessThan(16)) errors.depth = 'Minimal depth is 16'
else if (depth.isGreaterThan(255)) errors.depth = 'Depth has to be at most 255'
}
// Amount
if (!values.amount) errors.amount = 'Required field'
else {
const amount = new BigNumber(values.amount)
if (!amount.isInteger()) errors.amount = 'Amount must be an integer'
else if (amount.isLessThanOrEqualTo(0)) errors.amount = 'Amount must be greater than 0'
}
// Label
if (values.label && !/^[0-9a-z]*$/i.test(values.label)) errors.label = 'Label must be an alphanumeric string'
return errors
}}
>
{({ submitForm, isValid, isSubmitting, values }) => (
<Form>
<Box mb={2}>
<SwarmTextInput name="depth" label="Depth" formik />
<Box mt={0.25} sx={{ bgcolor: '#f6f6f6' }} p={2}>
<Grid container justifyContent="space-between">
<Typography>Corresponding file size</Typography>
<Typography>{getFileSize(parseInt(values.depth || '0', 10))}</Typography>
</Grid>
</Box>
</Box>
<Box mb={2}>
<SwarmTextInput name="amount" label="Amount" formik />
<Box mt={0.25} sx={{ bgcolor: '#f6f6f6' }} p={2}>
<Grid container justifyContent="space-between">
<Typography>Corresponding TTL (Time to live)</Typography>
<Typography>{getTtl(parseInt(values.amount || '0', 10))}</Typography>
</Grid>
</Box>
</Box>
<Box mb={2}>
<SwarmTextInput name="label" label="Label" optional formik />
</Box>
<Box mb={4} sx={{ bgcolor: '#fcf2e8' }} p={2}>
<Grid container justifyContent="space-between">
<Typography>Indicative Price</Typography>
<Typography>{getPrice(parseInt(values.depth || '0', 10), parseInt(values.amount || '0', 10))}</Typography>
</Grid>
</Box>
<SwarmButton
disabled={isSubmitting || !isValid || !values.amount || !values.depth}
onClick={submitForm}
iconType={Check}
loading={isSubmitting}
>
Buy New Stamp
</SwarmButton>
</Form>
)}
</Formik>
)
}
+31
View File
@@ -0,0 +1,31 @@
import React, { ReactElement, useContext } from 'react'
import { SwarmSelect } from '../../components/SwarmSelect'
import { Context, EnrichedPostageBatch } from '../../providers/Stamps'
interface Props {
onSelect: (stamp: EnrichedPostageBatch) => void
defaultValue?: string
}
export function PostageStampSelector({ onSelect, defaultValue }: Props): ReactElement {
const { stamps } = useContext(Context)
function onChange(stampId: string) {
if (!stamps) {
return
}
const stamp = stamps.find(x => x.batchID === stampId)
if (stamp) {
onSelect(stamp)
}
}
return (
<SwarmSelect
options={(stamps || []).map(x => ({ label: x.batchID.slice(0, 8), value: x.batchID }))}
onChange={event => onChange(event.target.value as string)}
defaultValue={defaultValue}
/>
)
}

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