Compare commits

..

32 Commits

Author SHA1 Message Date
bee-worker cda1d4bbb1 chore: release 0.7.0 (#189)
* chore: release 0.7.0

* chore: update the changelog

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

* style: hover and selected colors

* chore: split the sidebar items into separate component

* style: pretty much finished except for status button

* feat: sidebar status button as a separate component

* chore: spacing definition

* style: size of the status text

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

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

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

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

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

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

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

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

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

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

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

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

* refactor: clean up code

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

* fix: fix off by one bug in retry logic

* docs: add jsdocs to new utility functions

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

* chore: rename enums to pascal case

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

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

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

* feat: add upload size check
2021-06-18 12:37:09 +02:00
79 changed files with 12971 additions and 9058 deletions
+2 -2
View File
@@ -3,5 +3,5 @@ REACT_APP_BEE_HOST=http://localhost:1633
REACT_APP_BEE_DEBUG_HOST=http://localhost:1635 REACT_APP_BEE_DEBUG_HOST=http://localhost:1635
REACT_APP_BEE_DOCS_HOST=https://docs.ethswarm.org/docs/ REACT_APP_BEE_DOCS_HOST=https://docs.ethswarm.org/docs/
REACT_APP_BEE_DISCORD_HOST=https://discord.gg/eKr9XPv7 REACT_APP_BEE_DISCORD_HOST=https://discord.gg/eKr9XPv7
REACT_APP_ETHERSCAN_HOST=etherscan.io REACT_APP_BLOCKCHAIN_EXPLORER_URL=https://blockscout.com/xdai/mainnet
REACT_APP_BEE_GITHUB_REPO_URL=https://api.github.com/repos/ethersphere/bee REACT_APP_BEE_GITHUB_REPO_URL=https://api.github.com/repos/ethersphere/bee
+1 -1
View File
@@ -2,5 +2,5 @@ REACT_APP_BEE_HOST=http://localhost:1633
REACT_APP_BEE_DEBUG_HOST=http://localhost:1635 REACT_APP_BEE_DEBUG_HOST=http://localhost:1635
REACT_APP_BEE_DOCS_HOST=https://docs.ethswarm.org/docs/ REACT_APP_BEE_DOCS_HOST=https://docs.ethswarm.org/docs/
REACT_APP_BEE_DISCORD_HOST=https://discord.gg/eKr9XPv7 REACT_APP_BEE_DISCORD_HOST=https://discord.gg/eKr9XPv7
REACT_APP_ETHERSCAN_HOST=etherscan.io REACT_APP_BLOCKCHAIN_EXPLORER_URL=https://blockscout.com/xdai/mainnet
REACT_APP_BEE_GITHUB_REPO_URL=https://api.github.com/repos/ethersphere/bee REACT_APP_BEE_GITHUB_REPO_URL=https://api.github.com/repos/ethersphere/bee
+5
View File
@@ -1,4 +1,9 @@
{ {
"settings": {
"react": {
"version": "detect"
}
},
"extends": [ "extends": [
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",
"prettier", "prettier",
+4 -4
View File
@@ -42,10 +42,10 @@ jobs:
- name: Commit linting - name: Commit linting
uses: wagoid/commitlint-github-action@v2 uses: wagoid/commitlint-github-action@v2
# - name: Code linting - name: Code linting
# run: npm run lint:check run: npm run lint:check
# env: env:
# CI: true CI: true
- name: Build - name: Build
run: npm run build run: npm run build
+55
View File
@@ -1,5 +1,60 @@
# Changelog # Changelog
## [0.7.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.6.0...v0.7.0) (2021-08-31)
### Features
* removed dark theme and theme switching ([#190](https://www.github.com/ethersphere/bee-dashboard/issues/190)) ([d1720e2](https://www.github.com/ethersphere/bee-dashboard/commit/d1720e243c4415d75763a229250fa20e3664290e))
* separate info and status page ([#183](https://www.github.com/ethersphere/bee-dashboard/issues/183)) ([02a7bff](https://www.github.com/ethersphere/bee-dashboard/commit/02a7bff733b7fac70c6a36f94e6ba1425854a0af))
* styling of the sidebar ([#194](https://github.com/ethersphere/bee-dashboard/pull/194))
### Bug Fixes
* bee 1.1.0 version reporting workaround ([#197](https://github.com/ethersphere/bee-dashboard/issues/197))
## [0.6.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.5.0...v0.6.0) (2021-08-24)
### Features
* add retry to accounting ([#166](https://www.github.com/ethersphere/bee-dashboard/issues/166)) ([a62243f](https://www.github.com/ethersphere/bee-dashboard/commit/a62243fe5c45b7dd9be6e92f82ebdf0b64bd8f0d))
* add tooltips and health indicator to peers ([#169](https://www.github.com/ethersphere/bee-dashboard/issues/169)) ([480f6dc](https://www.github.com/ethersphere/bee-dashboard/commit/480f6dc7f9c58a4aae87e0dea7082a4bd3dc900b))
* bee provider caching the state of the app and refreshing periodically ([#172](https://www.github.com/ethersphere/bee-dashboard/issues/172)) ([2624cf0](https://www.github.com/ethersphere/bee-dashboard/commit/2624cf04c939e87f025c1f4ff417808073742dab))
* changing API urls does not need the app refresh ([#173](https://www.github.com/ethersphere/bee-dashboard/issues/173)) ([d6d03bf](https://www.github.com/ethersphere/bee-dashboard/commit/d6d03bf7c6d2705de22f43825b85b32c2f0181fb))
* remove the last update component ([#179](https://www.github.com/ethersphere/bee-dashboard/issues/179)) ([56df3a2](https://www.github.com/ethersphere/bee-dashboard/commit/56df3a2561c3c00237b5d107eb054403af3012f8))
* synchronized platform tabs ([#165](https://www.github.com/ethersphere/bee-dashboard/issues/165)) ([ec42eaf](https://www.github.com/ethersphere/bee-dashboard/commit/ec42eafc2b768ba06649f628c733e8d3440fdcaf))
### Bug Fixes
* enum index for supported platforms ([#170](https://www.github.com/ethersphere/bee-dashboard/issues/170)) ([dcec6e0](https://www.github.com/ethersphere/bee-dashboard/commit/dcec6e01887465c74a68feede52b476791bbefa7))
* remove nested ternary operator and simplify ping peer functionality ([#181](https://www.github.com/ethersphere/bee-dashboard/issues/181)) ([2b120e4](https://www.github.com/ethersphere/bee-dashboard/commit/2b120e44ca5e01451cc43e362195c04587836a03))
## [0.5.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.4.0...v0.5.0) (2021-08-09)
### Features
* updated troubleshooting instructions and links for mainnet ([#161](https://www.github.com/ethersphere/bee-dashboard/issues/161)) ([960ffb8](https://www.github.com/ethersphere/bee-dashboard/commit/960ffb8fdd6cbfe4928b758da6cac9ba94050c00))
### Bug Fixes
* amend readme ([#155](https://www.github.com/ethersphere/bee-dashboard/issues/155)) ([be8b885](https://www.github.com/ethersphere/bee-dashboard/commit/be8b88516b00d79a623798588d3d4dac3340e8b2))
## [0.4.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.3.1...v0.4.0) (2021-06-29)
### Features
* display postage batch usage percentage ([#149](https://www.github.com/ethersphere/bee-dashboard/issues/149)) ([6f645dc](https://www.github.com/ethersphere/bee-dashboard/commit/6f645dc6c357cb9d86cec15e454b361bc871bec6))
### Bug Fixes
* clear dropzone state after upload ([#150](https://www.github.com/ethersphere/bee-dashboard/issues/150)) ([b190cac](https://www.github.com/ethersphere/bee-dashboard/commit/b190cac7064ad3dffb770c5a83d3db4a14d39607))
### [0.3.1](https://www.github.com/ethersphere/bee-dashboard/compare/v0.3.0...v0.3.1) (2021-06-03) ### [0.3.1](https://www.github.com/ethersphere/bee-dashboard/compare/v0.3.0...v0.3.1) (2021-06-03)
+1 -1
View File
@@ -1 +1 @@
* nugaon vojtechsimetka * @Cafe137 @vojtechsimetka
+7 -1
View File
@@ -10,6 +10,8 @@
**Warning: This project is in alpha state. There might (and most probably will) be changes in the future to its API and working. Also, no guarantees can be made about its stability, efficiency, and security at this stage.** **Warning: This project is in alpha state. There might (and most probably will) be changes in the future to its API and working. Also, no guarantees can be made about its stability, efficiency, and security at this stage.**
This project is intended to be used with **Bee version 1.1.0**. Using it with older or newer Bee versions is not recommended and may not work. Stay up to date by joining the [official Discord](https://discord.gg/GU22h2utj6) and by keeping an eye on the [releases tab](https://github.com/ethersphere/bee-dashboard/releases).
![Status page](/ui_samples/status.png) ![Status page](/ui_samples/status.png)
| Node Setup | Browse & Upload Files | Accounting | Peers | Settings | | Node Setup | Browse & Upload Files | Accounting | Peers | Settings |
@@ -67,6 +69,8 @@ git clone git@github.com:ethersphere/bee-dashboard.git
cd bee-dashboard cd bee-dashboard
npm i
npm start npm start
``` ```
@@ -82,8 +86,10 @@ There are some ways you can make this module better:
## Maintainers ## Maintainers
- [nugaon](https://github.com/nugaon)
- [vojtechsimetka](https://github.com/vojtechsimetka) - [vojtechsimetka](https://github.com/vojtechsimetka)
- [Cafe137](https://github.com/Cafe137)
See what "Maintainer" means [here](https://github.com/ethersphere/repo-maintainer).
## License ## License
+11055 -6929
View File
File diff suppressed because it is too large Load Diff
+44 -44
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ethersphere/bee-dashboard", "name": "@ethersphere/bee-dashboard",
"version": "0.3.1", "version": "0.7.0",
"description": "An app which helps users to setup their Bee node and do actions like cash out cheques", "description": "An app which helps users to setup their Bee node and do actions like cash out cheques",
"keywords": [ "keywords": [
"bee", "bee",
@@ -24,52 +24,52 @@
"url": "https://github.com/ethersphere/bee-dashboard.git" "url": "https://github.com/ethersphere/bee-dashboard.git"
}, },
"dependencies": { "dependencies": {
"@ethersphere/bee-js": "^0.10.0", "@ethersphere/bee-js": "1.2.1",
"@material-ui/core": "^4.11.4", "@material-ui/core": "4.12.3",
"@material-ui/icons": "^4.11.2", "@material-ui/icons": "4.11.2",
"@material-ui/lab": "^4.0.0-alpha.57", "@material-ui/lab": "4.0.0-alpha.57",
"@types/react-router": "^5.1.13", "@types/react-router": "5.1.13",
"@types/react-router-dom": "^5.1.7", "@types/react-router-dom": "5.1.7",
"axios": "^0.21.1", "axios": "0.21.1",
"bignumber.js": "^9.0.1", "bignumber.js": "9.0.1",
"feather-icons": "^4.28.0", "feather-icons": "4.28.0",
"formik": "^2.2.8", "formik": "2.2.8",
"formik-material-ui": "^3.0.1", "formik-material-ui": "3.0.1",
"material-ui-dropzone": "^3.5.0", "material-ui-dropzone": "3.5.0",
"notistack": "^1.0.9", "notistack": "1.0.9",
"opener": "^1.5.2", "opener": "1.5.2",
"qrcode.react": "^1.0.1", "qrcode.react": "1.0.1",
"react": "^17.0.2", "react": "17.0.2",
"react-copy-to-clipboard": "^5.0.3", "react-copy-to-clipboard": "5.0.3",
"react-dom": "^17.0.2", "react-dom": "17.0.2",
"react-feather": "^2.0.9", "react-feather": "2.0.9",
"react-identicons": "^1.2.5", "react-identicons": "1.2.5",
"react-router-dom": "^5.2.0", "react-router-dom": "5.2.0",
"react-syntax-highlighter": "^15.4.3", "react-syntax-highlighter": "15.4.3",
"semver": "^7.3.2", "semver": "7.3.2",
"serve-handler": "^6.1.3" "serve-handler": "6.1.3"
}, },
"devDependencies": { "devDependencies": {
"@testing-library/jest-dom": "^5.12.0", "@testing-library/jest-dom": "5.12.0",
"@testing-library/react": "^11.2.6", "@testing-library/react": "11.2.6",
"@testing-library/user-event": "^13.1.5", "@testing-library/user-event": "13.1.5",
"@types/jest": "^26.0.22", "@types/jest": "26.0.22",
"@types/node": "^14.14.41", "@types/node": "14.14.41",
"@types/qrcode.react": "^1.0.1", "@types/qrcode.react": "1.0.1",
"@types/react": "^17.0.3", "@types/react": "17.0.3",
"@types/react-copy-to-clipboard": "^5.0.0", "@types/react-copy-to-clipboard": "5.0.0",
"@types/react-dom": "^17.0.3", "@types/react-dom": "17.0.3",
"@types/react-syntax-highlighter": "^13.5.0", "@types/react-syntax-highlighter": "13.5.0",
"@types/semver": "^7.3.6", "@types/semver": "7.3.6",
"eslint": "^7.24.0", "eslint": "7.32.0",
"eslint-config-prettier": "^8.2.0", "eslint-config-prettier": "8.3.0",
"eslint-plugin-jest": "^24.3.5", "eslint-plugin-jest": "24.4.0",
"eslint-plugin-prettier": "^3.4.0", "eslint-plugin-prettier": "3.4.1",
"eslint-plugin-react": "^7.23.2", "eslint-plugin-react": "7.24.0",
"prettier": "^2.2.1", "prettier": "2.3.2",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"typescript": "^4.2.4", "typescript": "4.2.4",
"web-vitals": "^1.1.1" "web-vitals": "1.1.1"
}, },
"scripts": { "scripts": {
"prepare": "npm run build", "prepare": "npm run build",
+1 -1
View File
@@ -10,6 +10,6 @@ declare module 'react-identicons' {
getColor?: () => string getColor?: () => string
} }
const Identicon = (props: Props): JSXElementConstructor => ReactNode const Identicon = (props: Props): JSXElementConstructor => ReactNode //eslint-disable-line @typescript-eslint/no-unused-vars
export default Identicon export default Identicon
} }
+32 -43
View File
@@ -1,53 +1,42 @@
import { ReactElement, useEffect, useState } from 'react' import { ReactElement } from 'react'
import { BrowserRouter as Router } from 'react-router-dom' import { BrowserRouter as Router } from 'react-router-dom'
import './App.css' import './App.css'
import { ThemeProvider } from '@material-ui/styles' import { ThemeProvider } from '@material-ui/core/styles'
import CssBaseline from '@material-ui/core/CssBaseline' import CssBaseline from '@material-ui/core/CssBaseline'
import { SnackbarProvider } from 'notistack' import { SnackbarProvider } from 'notistack'
import BaseRouter from './routes/routes' import BaseRouter from './routes'
import { lightTheme, darkTheme } from './theme' import Dashboard from './layout/Dashboard'
import { theme } from './theme'
import { Provider as StampsProvider } from './providers/Stamps' import { Provider as StampsProvider } from './providers/Stamps'
import { Provider as PlatformProvider } from './providers/Platform'
import { Provider as BeeProvider } from './providers/Bee'
import { Provider as SettingsProvider } from './providers/Settings'
const App = (): ReactElement => { const App = (): ReactElement => (
const [themeMode, toggleThemeMode] = useState('light') <div className="App">
<ThemeProvider theme={theme}>
useEffect(() => { <SettingsProvider>
const theme = localStorage.getItem('theme') <BeeProvider>
<StampsProvider>
if (theme) { <PlatformProvider>
toggleThemeMode(String(localStorage.getItem('theme'))) <SnackbarProvider>
} else if (window?.matchMedia('(prefers-color-scheme: dark)')?.matches) { <Router>
toggleThemeMode('dark') <>
} <CssBaseline />
<Dashboard>
window?.matchMedia('(prefers-color-scheme: dark)')?.addEventListener('change', e => { <BaseRouter />
toggleThemeMode(e?.matches ? 'dark' : 'light') </Dashboard>
}) </>
</Router>
return () => </SnackbarProvider>
window?.matchMedia('(prefers-color-scheme: dark)')?.removeEventListener('change', e => { </PlatformProvider>
toggleThemeMode(e?.matches ? 'dark' : 'light') </StampsProvider>
}) </BeeProvider>
}, []) </SettingsProvider>
</ThemeProvider>
return ( </div>
<div className="App"> )
<ThemeProvider theme={themeMode === 'light' ? lightTheme : darkTheme}>
<StampsProvider>
<SnackbarProvider>
<>
<CssBaseline />
<Router>
<BaseRouter />
</Router>
</>
</SnackbarProvider>
</StampsProvider>
</ThemeProvider>
</div>
)
}
export default App export default App
+11
View File
@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="172" height="30" viewBox="0 0 172 30">
<g fill="none" fill-rule="evenodd">
<g>
<g>
<path d="M0 0H172V30H0z" transform="translate(-64.000000, -64.000000) translate(64.000000, 64.000000)"/>
<path fill="#F9F9F9" fill-rule="nonzero" d="M94.26 21.338c1.242 0 2.168-.427 2.779-1.28.61-.853.916-2.118.916-3.796 0-1.677-.306-2.942-.916-3.795-.611-.854-1.537-1.28-2.778-1.28h-2.836v10.15h2.836zm-.057-1.062h-1.556V12.25h1.556c.814 0 1.427.28 1.84.843.411.562.617 1.357.617 2.385v1.57c0 1.028-.206 1.823-.618 2.386-.412.562-1.025.843-1.84.843zm6.767 1.062l.858-2.894h3.563l.858 2.894h1.294l-3.112-10.151h-1.614l-3.112 10.15h1.265zm4.13-3.956h-2.981l1.425-4.9h.131l1.425 4.9zm7.698 4.13c1.135 0 2.01-.266 2.625-.8.616-.533.924-1.27.924-2.21 0-.33-.044-.645-.131-.945-.087-.3-.235-.572-.444-.815-.208-.242-.482-.45-.821-.625-.34-.175-.757-.305-1.251-.393l-1.09-.189c-.718-.126-1.215-.332-1.491-.618-.277-.286-.415-.657-.415-1.112 0-.582.184-1.018.553-1.31.368-.29.897-.436 1.585-.436.63 0 1.144.117 1.541.35.398.232.737.533 1.018.901l.858-.742c-.339-.494-.787-.877-1.345-1.148-.557-.272-1.243-.408-2.058-.408-1.037 0-1.861.238-2.472.713-.61.475-.916 1.178-.916 2.109 0 .32.044.627.13.923.088.296.234.567.437.815.204.247.473.458.807.632.335.175.75.306 1.244.393l1.134.189c.698.116 1.188.313 1.469.589.281.276.422.662.422 1.156 0 .61-.194 1.086-.582 1.425-.388.34-.95.51-1.687.51-.61 0-1.137-.122-1.578-.364-.441-.243-.86-.611-1.258-1.106l-.887.728c.378.523.858.952 1.44 1.287.581.334 1.328.501 2.24.501zm7.262-.174v-4.61h3.956v4.61h1.221V11.187h-1.221v4.48h-3.956v-4.48h-1.222v10.15h1.222zm11.988 0c.388 0 .75-.075 1.084-.226.334-.15.625-.356.872-.618.248-.261.439-.572.575-.93.135-.36.203-.742.203-1.15 0-.707-.184-1.248-.552-1.62-.369-.374-.839-.629-1.411-.764v-.044c.465-.145.846-.393 1.142-.742.295-.349.443-.829.443-1.44 0-.794-.247-1.43-.741-1.904-.495-.476-1.178-.713-2.051-.713h-3.447v10.15h3.883zm-.567-5.745h-2.094V12.25h2.094c.514 0 .916.116 1.207.349.291.232.436.581.436 1.047v.567c0 .465-.145.812-.436 1.04-.29.227-.693.341-1.207.341zm.204 4.683h-2.298v-3.68h2.298c.562 0 1.003.13 1.323.386.32.257.48.643.48 1.156v.597c0 .523-.16.911-.48 1.163-.32.252-.761.378-1.323.378zm8.774 1.236c.62 0 1.154-.118 1.6-.356.446-.237.814-.58 1.105-1.025.29-.446.504-.994.64-1.644.136-.65.203-1.39.203-2.225 0-.824-.067-1.563-.203-2.217-.136-.655-.35-1.205-.64-1.651-.29-.446-.66-.788-1.105-1.025-.446-.238-.98-.357-1.6-.357-.62 0-1.154.12-1.6.357-.446.237-.814.579-1.105 1.025-.29.446-.504.996-.64 1.65-.136.655-.204 1.394-.204 2.218 0 .834.068 1.576.204 2.225.136.65.35 1.198.64 1.644.29.446.66.788 1.105 1.025.446.238.98.356 1.6.356zm0-1.061c-.407 0-.751-.08-1.033-.24-.28-.16-.513-.386-.698-.676-.184-.291-.317-.643-.4-1.055-.082-.412-.123-.875-.123-1.389v-1.658c0-.504.041-.964.123-1.381.083-.417.216-.77.4-1.062.185-.29.417-.516.698-.676.282-.16.626-.24 1.033-.24.407 0 .751.08 1.032.24.282.16.514.385.699.676.184.291.317.645.4 1.062.082.417.123.877.123 1.381v1.658c0 .514-.041.977-.124 1.39-.082.411-.215.763-.4 1.054-.184.29-.416.516-.698.676-.28.16-.625.24-1.032.24zm6.564.887l.858-2.894h3.563l.858 2.894h1.294l-3.112-10.151h-1.614l-3.113 10.15h1.266zm4.13-3.956h-2.981l1.425-4.9h.13l1.426 4.9zm5.895 3.956v-4.334h1.614l2.472 4.334h1.367l-2.588-4.392c.814-.078 1.434-.364 1.861-.858.427-.495.64-1.154.64-1.978 0-.921-.247-1.638-.742-2.152-.494-.514-1.212-.771-2.152-.771h-3.694v10.15h1.222zm2.443-5.366h-2.443v-3.723h2.443c.514 0 .914.128 1.2.385.286.257.429.623.429 1.098v.756c0 .475-.143.841-.43 1.098-.285.257-.685.386-1.2.386zm8.454 5.366c1.241 0 2.167-.427 2.778-1.28.61-.853.916-2.118.916-3.796 0-1.677-.305-2.942-.916-3.795-.611-.854-1.537-1.28-2.778-1.28h-2.836v10.15h2.836zm-.058-1.062h-1.556V12.25h1.556c.814 0 1.428.28 1.84.843.412.562.618 1.357.618 2.385v1.57c0 1.028-.206 1.823-.618 2.386-.412.562-1.026.843-1.84.843z" transform="translate(-64.000000, -64.000000) translate(64.000000, 64.000000)"/>
<path fill="#F9F9F9" d="M5.064 14.685l4.822 2.709v5.4L5.064 25.5l-4.82-2.706v-5.4l4.82-2.71zm11.034 0l4.822 2.709v5.4L16.098 25.5l-4.82-2.706v-5.4l4.82-2.71zm42.17-3.332v7.644l1.655 1.432.017-.014-1.187 1.327h-.019l-2.097-1.815-2.505 1.465c-.41.237-.876.362-1.35.357-.709-.005-1.392-.277-1.91-.762-.539-.503-.838-1.213-.824-1.95-.002-.974.52-1.874 1.367-2.356l5.05-2.895v-.644H50.55v-1.79h7.719zm-29.357.085c.802-.392 1.705-.529 2.588-.392.845.17 1.627.57 2.258 1.159.166.145.443.417.632.606l.122.121.08.08-1.178 1.336c.014-.017-.801-.773-.883-.841-.389-.367-.863-.627-1.383-.754-.539-.12-1.104-.022-1.573.27-.323.195-.508.552-.484.928.006.372.213.71.54.885.547.291 1.136.49 1.747.586.387.075.766.172 1.102.261.335.088.662.21.974.359.314.11.602.285.844.515.233.194.416.438.537.715.152.298.232.626.236.96.034.957-.403 1.87-1.17 2.444-.81.602-1.8.911-2.809.876-.379.002-.76-.036-1.131-.114-1.007-.247-1.925-.768-2.652-1.506-.05-.043-.608-.566-.608-.566l1.145-1.378c.246.23.486.462.741.677.313.303.655.574 1.02.813.913.518 2.035.518 2.948 0 .375-.227.596-.638.58-1.074.008-.364-.223-.687-.569-.799-.634-.27-1.298-.47-1.979-.59-.864-.152-1.684-.485-2.408-.978-.602-.433-.895-1.095-.895-2.025-.018-.85.365-1.657 1.03-2.183.186-.152.386-.282.598-.39zm17.705-.135c0-.022.006.006.006.006h1.922l-1.606 10.212h-3.152l-1.15-8.808h-.024l-1.164 8.804H38.3l-1.573-10.214h1.926l1.267 8.822h.024l1.114-8.822h3.157l1.109 8.822h.025zm33.488-.29c.703-.039 1.386.244 1.858.765.435.557.655 1.252.618 1.958v7.76h-1.89V14.08c-.014-.314-.113-.621-.288-.886-.212-.26-.54-.396-.873-.359-.362-.011-.708.145-.936.427-.272.41-.4.9-.357 1.391v6.854h-1.88v-7.337c.018-.353-.097-.7-.317-.976-.206-.241-.508-.373-.824-.359-.376-.022-.74.145-.966.448-.253.366-.379.808-.358 1.252v6.978h-1.876v-10.21h1.876v.946c.153-.354.401-.657.715-.877.38-.245.821-.37 1.272-.358.465-.02.923.124 1.296.404.304.244.535.57.664.937.424-.855 1.312-1.381 2.266-1.341zm-13.503-.011c.899-.011 1.763.349 2.387.996.66.62 1.031 1.488 1.023 2.393v.588h-1.764v-.588c0-.05-.003-.099-.008-.146-.08-.877-.859-1.519-1.735-1.436-.832.111-1.449.829-1.431 1.668v5.205h2.574v1.764H61.56v-1.764h1.687v-5.62l-1.697-1.538 1.178-1.315 1.195 1.096c.626-.846 1.625-1.332 2.677-1.303zm-10.136 4.89L52.32 18.27c-.288.158-.465.46-.46.789.01.496.41.894.905.905.166-.003.328-.052.468-.143h-.022l3.254-1.89v-2.04zM10.544 5.254l1.847 1.102v3.181l.634.354 2.347 1.318v2.15l-4.82 2.707-4.82-2.706v-5.4l4.812-2.706zm5.638-.755l2.568 1.44v2.886l-2.548 1.428-.02-.011-2.553-1.43V5.951l.005-.021L16.182 4.5z" transform="translate(-64.000000, -64.000000) translate(64.000000, 64.000000)"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

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

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

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

Before

Width:  |  Height:  |  Size: 3.7 KiB

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

Before

Width:  |  Height:  |  Size: 5.0 KiB

+38
View File
@@ -0,0 +1,38 @@
import Collapse from '@material-ui/core/Collapse'
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import { Alert, AlertTitle } from '@material-ui/lab'
import { ReactElement } from 'react'
const LIMIT = 100_000_000 // 100 megabytes
interface Props {
file: File
}
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
width: '100%',
marginTop: theme.spacing(2),
marginBottom: theme.spacing(2),
},
}),
)
export default function UploadSizeAlert(props: Props): ReactElement | null {
const classes = useStyles()
const aboveLimit = props.file.size >= LIMIT
return (
<Collapse in={aboveLimit}>
<div className={classes.root}>
<Alert severity="warning">
<AlertTitle>Warning</AlertTitle>
The file you are trying to upload is above the recommended size. The chunks may not be synchronised properly
over the network.
</Alert>
</div>
</Collapse>
)
}
+8 -8
View File
@@ -1,10 +1,10 @@
import { ReactElement, useState } from 'react' import { ReactElement, useState, useContext } from 'react'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import { Alert, AlertTitle } from '@material-ui/lab' import { Alert, AlertTitle } from '@material-ui/lab'
import Collapse from '@material-ui/core/Collapse' import Collapse from '@material-ui/core/Collapse'
import IconButton from '@material-ui/core/IconButton' import IconButton from '@material-ui/core/IconButton'
import CloseIcon from '@material-ui/icons/Close' import CloseIcon from '@material-ui/icons/Close'
import { useStatusNodeVersion } from '../hooks/status' import { Context } from '../providers/Bee'
import { SUPPORTED_BEE_VERSION_EXACT } from '@ethersphere/bee-js' import { SUPPORTED_BEE_VERSION_EXACT } from '@ethersphere/bee-js'
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
@@ -18,12 +18,12 @@ const useStyles = makeStyles((theme: Theme) =>
export default function VersionAlert(): ReactElement | null { export default function VersionAlert(): ReactElement | null {
const classes = useStyles() const classes = useStyles()
const { isLoading, userVersion } = useStatusNodeVersion() const { isLoading, latestUserVersionExact } = useContext(Context)
const [open, setOpen] = useState<boolean>(true) const [open, setOpen] = useState<boolean>(true)
const isExactlySupportedBeeVersion = SUPPORTED_BEE_VERSION_EXACT === userVersion const isExactlySupportedBeeVersion = SUPPORTED_BEE_VERSION_EXACT === latestUserVersionExact
if (isLoading || !userVersion) return null if (isLoading || !latestUserVersionExact) return null
return ( return (
<Collapse in={!isExactlySupportedBeeVersion && open}> <Collapse in={!isExactlySupportedBeeVersion && open}>
@@ -44,9 +44,9 @@ export default function VersionAlert(): ReactElement | null {
} }
> >
<AlertTitle>Warning</AlertTitle> <AlertTitle>Warning</AlertTitle>
Your Bee node version (<code>{userVersion}</code>) does not exactly match the Bee version we tested the Bee Your Bee node version (<code>{latestUserVersionExact}</code>) does not exactly match the Bee version we tested
Dashboard against (<code>{SUPPORTED_BEE_VERSION_EXACT}</code>). Please note that some functionality may not the Bee Dashboard against (<code>{SUPPORTED_BEE_VERSION_EXACT}</code>). Please note that some functionality
work properly. may not work properly.
</Alert> </Alert>
</div> </div>
</Collapse> </Collapse>
+10 -9
View File
@@ -1,15 +1,13 @@
import { ReactElement, useState } from 'react' import { CircularProgress, Container } from '@material-ui/core'
import Button from '@material-ui/core/Button' import Button from '@material-ui/core/Button'
import Dialog from '@material-ui/core/Dialog' import Dialog from '@material-ui/core/Dialog'
import DialogActions from '@material-ui/core/DialogActions' import DialogActions from '@material-ui/core/DialogActions'
import DialogContent from '@material-ui/core/DialogContent' import DialogContent from '@material-ui/core/DialogContent'
import DialogContentText from '@material-ui/core/DialogContentText' import DialogContentText from '@material-ui/core/DialogContentText'
import DialogTitle from '@material-ui/core/DialogTitle' import DialogTitle from '@material-ui/core/DialogTitle'
import { Container, CircularProgress } from '@material-ui/core'
import { useSnackbar } from 'notistack' import { useSnackbar } from 'notistack'
import { ReactElement, useState, useContext } from 'react'
import { beeDebugApi } from '../services/bee' import { Context as SettingsContext } from '../providers/Settings'
import EthereumAddress from './EthereumAddress' import EthereumAddress from './EthereumAddress'
interface Props { interface Props {
@@ -17,10 +15,11 @@ interface Props {
uncashedAmount: string uncashedAmount: string
} }
export default function DepositModal({ peerId, uncashedAmount }: Props): ReactElement { export default function CheckoutModal({ peerId, uncashedAmount }: Props): ReactElement {
const [open, setOpen] = useState<boolean>(false) const [open, setOpen] = useState<boolean>(false)
const [loadingCashout, setLoadingCashout] = useState<boolean>(false) const [loadingCashout, setLoadingCashout] = useState<boolean>(false)
const { enqueueSnackbar } = useSnackbar() const { enqueueSnackbar } = useSnackbar()
const { beeDebugApi } = useContext(SettingsContext)
const handleClickOpen = () => { const handleClickOpen = () => {
setOpen(true) setOpen(true)
@@ -31,16 +30,18 @@ export default function DepositModal({ peerId, uncashedAmount }: Props): ReactEl
} }
const handleCashout = () => { const handleCashout = () => {
if (!beeDebugApi) return
if (peerId) { if (peerId) {
setLoadingCashout(true) setLoadingCashout(true)
beeDebugApi.chequebook beeDebugApi
.peerCashout(peerId) .cashoutLastCheque(peerId)
.then(res => { .then(res => {
setOpen(false) setOpen(false)
enqueueSnackbar( enqueueSnackbar(
<span> <span>
Successfully cashed out cheque. Transaction Successfully cashed out cheque. Transaction
<EthereumAddress hideBlockie transaction address={res} network={'goerli'} /> <EthereumAddress hideBlockie transaction address={res} />
</span>, </span>,
{ variant: 'success' }, { variant: 'success' },
) )
+18 -134
View File
@@ -1,13 +1,7 @@
import React, { ReactElement, useEffect } from 'react' import { ReactElement, useContext } from 'react'
import { withStyles, Theme, createStyles } from '@material-ui/core/styles' import TabsContainer from './TabsContainer'
import { Tabs, Tab, Box, Typography } from '@material-ui/core'
import CodeBlock from './CodeBlock' import CodeBlock from './CodeBlock'
import { Context } from '../providers/Platform'
interface TabPanelProps {
children?: React.ReactNode
index: number
value: number
}
interface Props { interface Props {
linux: string linux: string
@@ -15,133 +9,23 @@ interface Props {
showLineNumbers?: boolean showLineNumbers?: boolean
} }
function a11yProps(index: number) {
return {
id: `simple-tab-${index}`,
'aria-controls': `simple-tabpanel-${index}`,
}
}
function getOS() {
const userAgent = window.navigator.userAgent
const platform = window.navigator.platform
const macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K']
const windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE']
const iosPlatforms = ['iPhone', 'iPad', 'iPod']
if (macosPlatforms.includes(platform)) return 'macOS'
if (iosPlatforms.includes(platform)) return 'iOS'
if (windowsPlatforms.includes(platform)) return 'windows'
if (/Android/.test(userAgent)) return 'android'
if (/Linux/.test(platform)) return 'linux'
return null
}
export default function CodeBlockTabs(props: Props): ReactElement { export default function CodeBlockTabs(props: Props): ReactElement {
const [value, setValue] = React.useState(0) const { platform, setPlatform } = useContext(Context)
const handleChange = (event: React.ChangeEvent<unknown>, newValue: number) => {
setValue(newValue)
}
useEffect(() => {
const os = getOS()
if (os === 'windows') {
setValue(0)
} else if (os === 'linux') {
setValue(0)
} else if (os === 'macOS') {
setValue(1)
}
}, [])
function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<Box style={{ marginTop: '-12px' }}>
<Typography component="div">{children}</Typography>
</Box>
)}
</div>
)
}
const AntTabs = withStyles({
root: {
borderBottom: '1px solid #e8e8e8',
},
indicator: {
backgroundColor: '#3f51b5',
},
})(Tabs)
interface StyledTabProps {
label: string
}
const AntTab = withStyles((theme: Theme) =>
createStyles({
root: {
textTransform: 'none',
minWidth: 72,
backgroundColor: 'transparent',
fontWeight: theme.typography.fontWeightRegular,
marginRight: theme.spacing(4),
fontFamily: [
'-apple-system',
'BlinkMacSystemFont',
'"Segoe UI"',
'Roboto',
'"Helvetica Neue"',
'Arial',
'sans-serif',
'"Apple Color Emoji"',
'"Segoe UI Emoji"',
'"Segoe UI Symbol"',
].join(','),
'&:hover': {
color: '#3f51b5',
opacity: 1,
},
'&$selected': {
color: '#3f51b5',
fontWeight: theme.typography.fontWeightMedium,
},
'&:focus': {
color: '#3f51b5',
},
},
selected: {},
}),
)((props: StyledTabProps) => <Tab disableRipple {...props} />)
return ( return (
<div> <TabsContainer
<AntTabs style={{ marginTop: '12px' }} value={value} onChange={handleChange} aria-label="ant example"> index={platform}
<AntTab label="Linux" {...a11yProps(0)} /> indexChanged={setPlatform}
<AntTab label="MacOS" {...a11yProps(1)} /> values={[
</AntTabs> {
<TabPanel value={value} index={0}> label: 'Linux',
<CodeBlock showLineNumbers={props.showLineNumbers} language="bash" code={props.linux} /> component: <CodeBlock showLineNumbers={props.showLineNumbers} language="bash" code={props.linux} />,
</TabPanel> },
<TabPanel value={value} index={1}> {
<CodeBlock showLineNumbers={props.showLineNumbers} language="bash" code={props.mac} /> label: 'macOS',
</TabPanel> component: <CodeBlock showLineNumbers={props.showLineNumbers} language="bash" code={props.mac} />,
</div> },
]}
/>
) )
} }
+28 -40
View File
@@ -1,62 +1,50 @@
import React, { ReactElement, useState } from 'react' import React, { ReactElement, useState } from 'react'
import { TextField, Button, CircularProgress, Container } from '@material-ui/core' import { TextField, Button } from '@material-ui/core'
interface Props { interface Props {
defaultHost?: string defaultHost?: string
hostName: string setHost: (host: string) => void
} }
export default function ConnectToHost(props: Props): ReactElement { export default function ConnectToHost(props: Props): ReactElement {
const [hostInputVisible, toggleHostInputVisibility] = useState(false) const [hostInputVisible, toggleHostInputVisibility] = useState(false)
const [connectingToHost, setConnectingToHost] = useState(false)
const [host, setHost] = useState('') const [host, setHost] = useState('')
const handleNewHostConnection = () => { const handleNewHostConnection = () => {
if (host) { if (host) {
setConnectingToHost(true) props.setHost(host)
sessionStorage.setItem(props.hostName, host)
toggleHostInputVisibility(!hostInputVisible) toggleHostInputVisibility(!hostInputVisible)
window.location.reload()
} }
} }
return ( return (
<div> <div>
{ {hostInputVisible ? (
// FIXME: this should be broken up <div style={{ display: 'flex' }}>
/* eslint-disable no-nested-ternary */ <TextField
hostInputVisible ? ( defaultValue={props.defaultHost}
<div style={{ display: 'flex' }}> label="Enter host"
<TextField variant="outlined"
defaultValue={props.defaultHost} size="small"
label="Enter host" onChange={e => setHost(e.target.value)}
variant="outlined" style={{ marginRight: '15px', minWidth: '300px' }}
size="small" />
onChange={e => setHost(e.target.value)} <Button onClick={() => handleNewHostConnection()} size="small" variant="outlined">
style={{ marginRight: '15px', minWidth: '300px' }} Connect
/>
<Button onClick={() => handleNewHostConnection()} size="small" variant="outlined">
Connect
</Button>
<Button
style={{ marginLeft: '7px' }}
onClick={() => toggleHostInputVisibility(!hostInputVisible)}
size="small"
>
Cancel
</Button>
</div>
) : connectingToHost ? (
<Container style={{ textAlign: 'center', padding: '0px' }}>
<CircularProgress size={20} />
</Container>
) : (
<Button onClick={() => toggleHostInputVisibility(!hostInputVisible)} size="small" variant="outlined">
Change host
</Button> </Button>
) <Button
/* eslint-enable no-nested-ternary */ style={{ marginLeft: '7px' }}
} onClick={() => toggleHostInputVisibility(!hostInputVisible)}
size="small"
>
Cancel
</Button>
</div>
) : (
<Button onClick={() => toggleHostInputVisibility(!hostInputVisible)} size="small" variant="outlined">
Change host
</Button>
)}
</div> </div>
) )
} }
+3 -4
View File
@@ -7,7 +7,6 @@ import { ReactElement } from 'react'
interface Props { interface Props {
address: string | undefined address: string | undefined
network?: string
hideBlockie?: boolean hideBlockie?: boolean
transaction?: boolean transaction?: boolean
truncate?: boolean truncate?: boolean
@@ -37,9 +36,9 @@ export default function EthereumAddress(props: Props): ReactElement {
} }
: { marginRight: '7px' } : { marginRight: '7px' }
} }
href={`https://${props.network}.${process.env.REACT_APP_ETHERSCAN_HOST}/${ href={`${process.env.REACT_APP_BLOCKCHAIN_EXPLORER_URL}/${props.transaction ? 'tx' : 'address'}/${
props.transaction ? 'tx' : 'address' props.address
}/${props.address}`} }`}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
+18 -34
View File
@@ -1,10 +1,9 @@
import React, { ReactElement } from 'react' import { ReactElement } from 'react'
import { createStyles, makeStyles } from '@material-ui/core/styles' import { createStyles, makeStyles } from '@material-ui/core/styles'
import { Card, CardContent, Typography } from '@material-ui/core/' import { Card, CardContent, Typography } from '@material-ui/core/'
import EthereumAddress from '../components/EthereumAddress' import EthereumAddress from '../components/EthereumAddress'
import { Skeleton } from '@material-ui/lab'
import type { ChequebookAddressResponse, NodeAddresses } from '@ethersphere/bee-js' import type { ChequebookAddressResponse, NodeAddresses } from '@ethersphere/bee-js'
@@ -28,9 +27,7 @@ const useStyles = makeStyles(() =>
interface Props { interface Props {
nodeAddresses: NodeAddresses | null nodeAddresses: NodeAddresses | null
isLoadingNodeAddresses: boolean
chequebookAddress: ChequebookAddressResponse | null chequebookAddress: ChequebookAddressResponse | null
isLoadingChequebookAddress: boolean
} }
function EthereumAddressCard(props: Props): ReactElement { function EthereumAddressCard(props: Props): ReactElement {
@@ -38,36 +35,23 @@ function EthereumAddressCard(props: Props): ReactElement {
return ( return (
<Card className={classes.root}> <Card className={classes.root}>
{props.isLoadingNodeAddresses ? ( <div className={classes.details}>
<div style={{ padding: '16px' }}> <CardContent className={classes.content}>
<Skeleton width={300} height={30} animation="wave" /> <Typography variant="subtitle1" gutterBottom>
<Skeleton width={300} height={50} animation="wave" /> Ethereum Address
</div> </Typography>
) : ( <EthereumAddress address={props.nodeAddresses?.ethereum} />
<div className={classes.details}> </CardContent>
<CardContent className={classes.content}> </div>
<Typography variant="subtitle1" gutterBottom>
Ethereum Address <div className={classes.details}>
</Typography> <CardContent className={classes.content}>
<EthereumAddress address={props.nodeAddresses?.ethereum} network={'goerli'} /> <Typography variant="subtitle1" gutterBottom>
</CardContent> Chequebook Contract Address
</div> </Typography>
)} <EthereumAddress address={props.chequebookAddress?.chequebookAddress} />
{props.isLoadingChequebookAddress ? ( </CardContent>
<div style={{ padding: '16px' }}> </div>
<Skeleton width={300} height={30} animation="wave" />
<Skeleton width={300} height={50} animation="wave" />
</div>
) : (
<div className={classes.details}>
<CardContent className={classes.content}>
<Typography variant="subtitle1" gutterBottom>
Chequebook Contract Address
</Typography>
<EthereumAddress address={props.chequebookAddress?.chequebookAddress} network={'goerli'} />
</CardContent>
</div>
)}
</Card> </Card>
) )
} }
-23
View File
@@ -1,23 +0,0 @@
import { ReactElement, useEffect, useState } from 'react'
interface Props {
date: number | null
}
export default function LastUpdate({ date }: Props): ReactElement {
const [duration, setDuration] = useState<string>('never')
const refresh = () => {
if (!date) setDuration('never')
else setDuration(`${((Date.now() - date) / 1000).toFixed()} seconds ago`)
}
useEffect(() => {
refresh()
const i = setInterval(refresh, 1000)
return () => clearInterval(i)
}, [date])
return <span>Last Update: {duration}</span>
}
-61
View File
@@ -1,61 +0,0 @@
import { useState, ReactElement } from 'react'
import { createStyles, makeStyles } from '@material-ui/core/styles'
import { Toolbar, Chip, 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,
},
network: {},
}),
)
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' }}>
<Chip style={{ marginLeft: '7px' }} size="small" label="Goerli" className={classes.network} />
<div style={{ width: '100%' }}>
<div style={{ float: 'right' }}>
<IconButton style={{ marginRight: '10px' }} aria-label="dark-mode" onClick={() => switchTheme()}>
{props.themeMode === 'dark' ? <Moon /> : <Sun />}
</IconButton>
{/* <Chip
label="Connect Wallet"
color="primary"
/> */}
</div>
</div>
</Toolbar>
</div>
</div>
)
}
+77 -123
View File
@@ -1,51 +1,45 @@
import { ReactElement } from 'react' import type { ReactElement } from 'react'
import { Link, RouteComponentProps } from 'react-router-dom' import { Link } from 'react-router-dom'
import { createStyles, Theme, makeStyles } from '@material-ui/core/styles' import { createStyles, Theme, makeStyles } from '@material-ui/core/styles'
import { ListItemText, ListItemIcon, ListItem, Divider, List, Drawer, Link as MUILink } from '@material-ui/core'
import { OpenInNewSharp } from '@material-ui/icons' import { OpenInNewSharp } from '@material-ui/icons'
import { Activity, FileText, DollarSign, Share2, Settings, Layers } from 'react-feather' import { Divider, List, Drawer, Grid, Link as MUILink } from '@material-ui/core'
import { Home, FileText, DollarSign, Share2, Settings, Layers, BookOpen } from 'react-feather'
import { ROUTES } from '../routes'
import SideBarItem from './SideBarItem'
import SideBarStatus from './SideBarStatus'
import SwarmLogoOrange from '../assets/swarm-logo-orange.svg' import Logo from '../assets/logo.svg'
import { Health } from '@ethersphere/bee-js'
const drawerWidth = 240
const navBarItems = [ const navBarItems = [
{ {
label: 'Status', label: 'Info',
id: 'status', path: ROUTES.INFO,
path: '/', icon: Home,
icon: Activity,
}, },
{ {
label: 'Files', label: 'Files',
id: 'files', path: ROUTES.FILES,
path: '/files/',
icon: FileText, icon: FileText,
}, },
{ {
label: 'Stamps', label: 'Stamps',
id: 'stamps', path: ROUTES.STAMPS,
path: '/stamps/',
icon: Layers, icon: Layers,
}, },
{ {
label: 'Accounting', label: 'Accounting',
id: 'accounting', path: ROUTES.ACCOUNTING,
path: '/accounting/',
icon: DollarSign, icon: DollarSign,
}, },
{ {
label: 'Peers', label: 'Peers',
id: 'peers', path: ROUTES.PEERS,
path: '/peers/',
icon: Share2, icon: Share2,
}, },
{ {
label: 'Settings', label: 'Settings',
id: 'settings', path: ROUTES.SETTINGS,
path: '/settings/',
icon: Settings, icon: Settings,
}, },
] ]
@@ -53,122 +47,82 @@ const navBarItems = [
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
createStyles({ createStyles({
root: { root: {
display: 'flex', flexWrap: 'nowrap',
}, minHeight: '100vh',
appBar: { paddingTop: theme.spacing(8),
width: `calc(100% - ${drawerWidth}px)`, paddingBottom: theme.spacing(8),
marginLeft: drawerWidth,
}, },
logo: { logo: {
padding: 1, marginLeft: theme.spacing(8),
marginTop: 20, marginRight: theme.spacing(8),
}, },
drawer: { icon: {
width: drawerWidth, height: theme.spacing(4),
flexShrink: 0,
}, },
drawerPaper: { iconSmall: {
width: drawerWidth, height: theme.spacing(2),
}, },
activeSideBar: { divider: {
color: '#dd7700', backgroundColor: '#2c2c2c',
marginLeft: theme.spacing(4),
marginRight: theme.spacing(4),
}, },
activeSideBarItem: { link: {
borderLeft: '4px solid #dd7700', color: '#9f9f9f',
backgroundColor: 'inherit !important', textDecoration: 'none',
'&:hover': {
textDecoration: 'none',
// https://github.com/mui-org/material-ui/issues/22543
'@media (hover: none)': {
textDecoration: 'none',
},
},
}, },
toolbar: theme.mixins.toolbar,
}), }),
) )
interface Props extends RouteComponentProps { export default function SideBar(): ReactElement {
themeMode: string
health: boolean
nodeHealth: Health | null
}
export default function SideBar(props: Props): ReactElement {
const classes = useStyles() const classes = useStyles()
return ( return (
<div className={classes.root}> <Drawer variant="permanent">
<Drawer <Grid container direction="column" justifyContent="space-between" className={classes.root}>
className={classes.drawer} <Grid className={classes.logo}>
variant="permanent" <Link to={ROUTES.INFO}>
classes={{ <img alt="swarm" src={Logo} />
paper: classes.drawerPaper,
}}
anchor="left"
>
<div className={classes.toolbar} style={{ textAlign: 'left', marginLeft: 20 }}>
<Link to="/">
<img
alt="swarm"
className={classes.logo}
src={props.themeMode === 'light' ? SwarmLogoOrange : SwarmLogoOrange}
style={{ maxHeight: '30px', alignItems: 'center' }}
/>
</Link> </Link>
</div> </Grid>
<List> <Grid>
{navBarItems.map(item => ( <List>
<Link to={item.path} key={item.id} style={{ color: 'inherit', textDecoration: 'none' }}> {navBarItems.map(p => (
<ListItem <Link to={p.path} key={p.path} className={classes.link}>
button <SideBarItem
selected={props.location.pathname === item.path} key={p.path}
className={props.location.pathname === item.path ? classes.activeSideBarItem : ''} iconStart={<p.icon className={classes.icon} />}
> path={p.path}
<ListItemIcon className={props.location.pathname === item.path ? classes.activeSideBar : ''}> label={p.label}
<item.icon style={{ height: '20px' }} />
</ListItemIcon>
<ListItemText
primary={item.label}
className={props.location.pathname === item.path ? classes.activeSideBar : ''}
/> />
</ListItem> </Link>
</Link> ))}
))} </List>
</List> <Divider className={classes.divider} />
<Divider /> <List>
<List> <MUILink href={process.env.REACT_APP_BEE_DOCS_HOST} target="_blank" className={classes.link}>
<MUILink href={process.env.REACT_APP_BEE_DOCS_HOST} target="_blank" style={{ textDecoration: 'none' }}> <SideBarItem
<ListItem button> iconStart={<BookOpen className={classes.icon} />}
<ListItemText primary={'Docs'} /> iconEnd={<OpenInNewSharp className={classes.iconSmall} />}
<OpenInNewSharp fontSize="small" /> label={<span>Docs</span>}
</ListItem>
</MUILink>
</List>
<div style={{ position: 'fixed', bottom: 0, width: 'inherit', padding: '10px' }}>
<ListItem>
<div style={{ marginRight: '30px' }}>
<div
style={{
backgroundColor: props.health ? '#32c48d' : '#c9201f',
marginRight: '7px',
height: '10px',
width: '10px',
borderRadius: '50%',
display: 'inline-block',
}}
/> />
<span>API</span> </MUILink>
</div> </List>
<div> </Grid>
<div <Grid>
style={{ <Link to={ROUTES.STATUS} className={classes.link}>
backgroundColor: props.nodeHealth?.status === 'ok' ? '#32c48d' : '#c9201f', <SideBarStatus path={ROUTES.STATUS} />
marginRight: '7px', </Link>
height: '10px', </Grid>
width: '10px', </Grid>
borderRadius: '50%', </Drawer>
display: 'inline-block',
}}
/>
<span>Debug API</span>
</div>
</ListItem>
</div>
</Drawer>
</div>
) )
} }
+62
View File
@@ -0,0 +1,62 @@
import type { ReactElement, ReactNode } from 'react'
import { useLocation, matchPath } from 'react-router-dom'
import { createStyles, Theme, makeStyles, withStyles } from '@material-ui/core/styles'
import { ListItemText, ListItemIcon, ListItem } from '@material-ui/core'
const StyledListItem = withStyles((theme: Theme) => ({
root: {
paddingLeft: theme.spacing(4),
paddingRight: theme.spacing(4),
borderLeft: '4px solid rgba(0,0,0,0)',
'&.Mui-selected, &.Mui-selected:hover': {
borderLeft: `4px solid ${theme.palette.primary.main}`,
backgroundColor: '#2c2c2c',
color: '#f9f9f9',
},
},
button: {
'&:hover': {
backgroundColor: '#2c2c2c',
color: '#f9f9f9',
// https://github.com/mui-org/material-ui/issues/22543
'@media (hover: none)': {
backgroundColor: '#2c2c2c',
color: '#f9f9f9',
},
},
},
}))(ListItem)
const useStyles = makeStyles((theme: Theme) =>
createStyles({
icon: {
color: 'inherit',
},
activeIcon: {
color: theme.palette.primary.main,
},
}),
)
interface Props {
iconStart?: ReactNode
iconEnd?: ReactNode
path?: string
label: ReactNode
}
export default function SideBarItem({ iconStart, iconEnd, path, label }: Props): ReactElement {
const classes = useStyles()
const location = useLocation()
const isSelected = Boolean(matchPath(location.pathname, { path, exact: true }))
return (
<StyledListItem button selected={isSelected} disableRipple>
<ListItemIcon className={isSelected ? classes.activeIcon : classes.icon}>{iconStart}</ListItemIcon>
<ListItemText primary={label} />
<ListItemIcon className={isSelected ? classes.activeIcon : classes.icon}>{iconEnd}</ListItemIcon>
</StyledListItem>
)
}
+85
View File
@@ -0,0 +1,85 @@
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'
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',
// https://github.com/mui-org/material-ui/issues/22543
'@media (hover: none)': {
backgroundColor: '#2c2c2c',
},
},
},
smallerText: {
fontSize: '0.9rem',
},
}),
)
interface Props {
path?: string
}
export default function SideBarItem({ path }: Props): ReactElement {
const { status } = 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>
<span
style={{
backgroundColor: status.all ? '#1de600' : '#ff3a52',
height: '14px',
width: '14px',
borderRadius: '50%',
display: 'inline-block',
marginLeft: 30,
}}
/>
</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>
)
}
+6 -13
View File
@@ -1,28 +1,23 @@
import type { ReactElement } from 'react'
import { makeStyles } from '@material-ui/core/styles'
import { Card, CardContent, Typography } from '@material-ui/core/' import { Card, CardContent, Typography } from '@material-ui/core/'
import { makeStyles } from '@material-ui/core/styles'
import { Skeleton } from '@material-ui/lab' import { Skeleton } from '@material-ui/lab'
import type { ReactElement } from 'react'
import { Title } from './Title'
const useStyles = makeStyles({ const useStyles = makeStyles({
root: { root: {
minWidth: 275, minWidth: 275,
}, },
title: {
fontSize: 16,
},
pos: {
marginBottom: 12,
},
}) })
interface Props { interface Props {
label: string label: string
statistic?: string statistic?: string
loading?: boolean loading?: boolean
tooltip?: string
} }
export default function StatCard({ loading, label, statistic }: Props): ReactElement { export default function StatCard({ loading, label, statistic, tooltip }: Props): ReactElement {
const classes = useStyles() const classes = useStyles()
return ( return (
@@ -36,9 +31,7 @@ export default function StatCard({ loading, label, statistic }: Props): ReactEle
)} )}
{!loading && ( {!loading && (
<> <>
<Typography className={classes.title} color="textSecondary" gutterBottom> <Title label={label} tooltip={tooltip} />
{label}
</Typography>
<Typography variant="h5" component="h2"> <Typography variant="h5" component="h2">
{statistic} {statistic}
</Typography> </Typography>
+14 -15
View File
@@ -15,13 +15,7 @@ function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props const { children, value, index, ...other } = props
return ( return (
<div <div role="tabpanel" hidden={value !== index} {...other}>
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && ( {value === index && (
<Box p={3}> <Box p={3}>
<Typography>{children}</Typography> <Typography>{children}</Typography>
@@ -44,25 +38,30 @@ interface TabsValues {
interface Props { interface Props {
values: TabsValues[] values: TabsValues[]
index?: number
indexChanged?: (index: number) => void
} }
export default function SimpleTabs({ values }: Props): ReactElement { export default function SimpleTabs({ values, index, indexChanged }: Props): ReactElement {
const classes = useStyles() const classes = useStyles()
const [value, setValue] = React.useState<number>(0) const [value, setValue] = React.useState<number>(index || 0)
const handleChange = (event: React.ChangeEvent<Record<string, never>>, newValue: number) => { const handleChange = (event: React.ChangeEvent<Record<string, never>>, newValue: number) => {
setValue(newValue) if (indexChanged) indexChanged(newValue)
else setValue(newValue)
} }
const v = index !== undefined ? index : value
return ( return (
<div className={classes.root}> <div className={classes.root}>
<Tabs value={value} onChange={handleChange} aria-label="simple tabs example"> <Tabs value={v} onChange={handleChange}>
{values.map(({ label }, index) => ( {values.map(({ label }, idx) => (
<Tab key={index} label={label} /> <Tab key={idx} label={label} />
))} ))}
</Tabs> </Tabs>
{values.map(({ component }, index) => ( {values.map(({ component }, idx) => (
<TabPanel key={index} value={value} index={index}> <TabPanel key={idx} value={v} index={idx}>
{component} {component}
</TabPanel> </TabPanel>
))} ))}
+41
View File
@@ -0,0 +1,41 @@
import { Grid, Tooltip, Typography } from '@material-ui/core/'
import { makeStyles } from '@material-ui/core/styles'
import { Info } from '@material-ui/icons'
import type { ReactElement } from 'react'
interface TitleProps {
label: string
tooltip?: string
}
const useStyles = makeStyles({
title: {
fontSize: 16,
},
})
export function Title({ label, tooltip }: TitleProps): ReactElement {
const classes = useStyles()
if (!tooltip) {
return (
<Typography className={classes.title} color="textSecondary" gutterBottom>
{label}
</Typography>
)
}
// span is needed as Tooltip expects a non-functional element!
return (
<Tooltip title={tooltip}>
<span>
<Grid container direction="row" justify="space-between">
<Typography className={classes.title} color="textSecondary" gutterBottom>
{label}
</Typography>
<Info />
</Grid>
</span>
</Tooltip>
)
}
+45 -7
View File
@@ -1,25 +1,63 @@
import type { Topology } from '@ethersphere/bee-js' import type { Topology } from '@ethersphere/bee-js'
import { Grid } from '@material-ui/core/' import { Grid } from '@material-ui/core/'
import type { ReactElement } from 'react' import type { ReactElement } from 'react'
import { pickThreshold, ThresholdValues } from '../utils/threshold'
import StatCard from './StatCard' import StatCard from './StatCard'
interface Props { interface RootProps {
isLoading: boolean
topology: Topology | null topology: Topology | null
error: Error | null // FIXME: should display error
} }
const TopologyStats = ({ isLoading, topology }: Props): ReactElement => ( interface Props extends RootProps {
thresholds: ThresholdValues
}
const TopologyStats = (props: RootProps): ReactElement => {
const thresholds: ThresholdValues = {
connectedPeers: pickThreshold('connectedPeers', props.topology?.connected || 0),
population: pickThreshold('population', props.topology?.population || 0),
depth: pickThreshold('depth', props.topology?.depth || 0),
}
return (
<>
<Indicator {...props} thresholds={thresholds} />
<Metrics {...props} thresholds={thresholds} />
</>
)
}
const Indicator = ({ thresholds }: Props): ReactElement => {
const maximumTotalScore = Object.values(thresholds).reduce((sum, item) => sum + item.maximumScore, 0)
const actualTotalScore = Object.values(thresholds).reduce((sum, item) => sum + item.score, 0)
const percentageText = Math.round((actualTotalScore / maximumTotalScore) * 100) + '%'
return (
<div style={{ marginBottom: '20px' }}>
<StatCard label="Overall Health Indicator" statistic={percentageText} />
</div>
)
}
const Metrics = ({ topology, thresholds }: Props): ReactElement => (
<Grid style={{ marginBottom: '20px', flexGrow: 1 }}> <Grid style={{ marginBottom: '20px', flexGrow: 1 }}>
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid key={1} item xs={12} sm={12} md={6} lg={4} xl={4}> <Grid key={1} item xs={12} sm={12} md={6} lg={4} xl={4}>
<StatCard label="Connected Peers" statistic={topology?.connected.toString()} loading={isLoading} /> <StatCard
label="Connected Peers"
statistic={topology?.connected.toString()}
tooltip={thresholds.connectedPeers.explanation}
/>
</Grid> </Grid>
<Grid key={2} item xs={12} sm={12} md={6} lg={4} xl={4}> <Grid key={2} item xs={12} sm={12} md={6} lg={4} xl={4}>
<StatCard label="Population" statistic={topology?.population.toString()} loading={isLoading} /> <StatCard
label="Population"
statistic={topology?.population.toString()}
tooltip={thresholds.population.explanation}
/>
</Grid> </Grid>
<Grid key={3} item xs={12} sm={12} md={6} lg={4} xl={4}> <Grid key={3} item xs={12} sm={12} md={6} lg={4} xl={4}>
<StatCard label="Depth" statistic={topology?.depth.toString()} loading={isLoading} /> <StatCard label="Depth" statistic={topology?.depth.toString()} tooltip={thresholds.depth.explanation} />
</Grid> </Grid>
</Grid> </Grid>
</Grid> </Grid>
@@ -3,6 +3,7 @@ import { Link } from 'react-router-dom'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import { Card, CardContent, Typography } from '@material-ui/core/' import { Card, CardContent, Typography } from '@material-ui/core/'
import { ROUTES } from '../routes'
const useStyles = makeStyles({ const useStyles = makeStyles({
root: { root: {
@@ -26,7 +27,7 @@ export default function TroubleshootConnectionCard(): ReactElement {
</Typography> </Typography>
<div style={{ marginBottom: '20px', textAlign: 'center' }}> <div style={{ marginBottom: '20px', textAlign: 'center' }}>
<strong> <strong>
<Link to="/">Click to run status checks</Link> on your nodes connection or check out the{' '} <Link to={ROUTES.STATUS}>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"> <a href={process.env.REACT_APP_BEE_DOCS_HOST} target="_blank" rel="noreferrer">
Swarm Bee Docs Swarm Bee Docs
</a> </a>
@@ -21,7 +21,7 @@ interface Props {
action: (amount: bigint) => Promise<string> action: (amount: bigint) => Promise<string>
} }
export default function WithdrawModal({ export default function WithdrawDepositModal({
successMessage, successMessage,
errorMessage, errorMessage,
dialogMessage, dialogMessage,
-4
View File
@@ -1,4 +0,0 @@
// These values can for now be constants because their change in the app reloads the page
export const apiHost = sessionStorage.getItem('api_host') || process.env.REACT_APP_BEE_HOST || 'http://localhost:1633'
export const debugApiHost =
sessionStorage.getItem('debug_api_host') || process.env.REACT_APP_BEE_DEBUG_HOST || 'http://localhost:1635'
+11 -5
View File
@@ -1,18 +1,24 @@
import type { ReactElement } from 'react' import { ReactElement, useContext } from 'react'
import { beeDebugApi } from '../services/bee' import { Context as SettingsContext } from '../providers/Settings'
import WDModal from '../components/WDModal' import WithdrawDepositModal from '../components/WithdrawDepositModal'
import { BigNumber } from 'bignumber.js' import { BigNumber } from 'bignumber.js'
export default function DepositModal(): ReactElement { export default function DepositModal(): ReactElement {
const { beeDebugApi } = useContext(SettingsContext)
return ( return (
<WDModal <WithdrawDepositModal
successMessage="Successful deposit." successMessage="Successful deposit."
errorMessage="Error with depositing" errorMessage="Error with depositing"
dialogMessage="Specify the amount of BZZ you would like to withdraw from your node." dialogMessage="Specify the amount of BZZ you would like to withdraw from your node."
label="Deposit" label="Deposit"
min={new BigNumber(0)} min={new BigNumber(0)}
action={beeDebugApi.chequebook.deposit} action={(amount: bigint) => {
if (!beeDebugApi) throw new Error('Bee Debug URL is not valid')
return beeDebugApi.depositTokens(amount.toString())
}}
/> />
) )
} }
+11 -5
View File
@@ -1,18 +1,24 @@
import type { ReactElement } from 'react' import { ReactElement, useContext } from 'react'
import { beeDebugApi } from '../services/bee' import { Context as SettingsContext } from '../providers/Settings'
import WDModal from '../components/WDModal' import WithdrawDepositModal from '../components/WithdrawDepositModal'
import { BigNumber } from 'bignumber.js' import { BigNumber } from 'bignumber.js'
export default function WithdrawModal(): ReactElement { export default function WithdrawModal(): ReactElement {
const { beeDebugApi } = useContext(SettingsContext)
return ( return (
<WDModal <WithdrawDepositModal
successMessage="Successful withdrawl." successMessage="Successful withdrawl."
errorMessage="Error with withdrawing." errorMessage="Error with withdrawing."
dialogMessage="Specify the amount of BZZ you would like to withdraw from your node." dialogMessage="Specify the amount of BZZ you would like to withdraw from your node."
label="Withdraw" label="Withdraw"
min={new BigNumber(0)} min={new BigNumber(0)}
action={beeDebugApi.chequebook.withdraw} action={(amount: bigint) => {
if (!beeDebugApi) throw new Error('Bee Debug URL is not valid')
return beeDebugApi.withdrawTokens(amount.toString())
}}
/> />
) )
} }
+18 -27
View File
@@ -1,15 +1,11 @@
import { LastCashoutActionResponse } from '@ethersphere/bee-js' import { LastCashoutActionResponse, BeeDebug } from '@ethersphere/bee-js'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Token } from '../models/Token' import { Token } from '../models/Token'
import { beeDebugApi } from '../services/bee' import { makeRetriablePromise, unwrapPromiseSettlements } from '../utils'
import { Balance, Settlement, useApiPeerBalances, useApiSettlements } from './apiHooks' import { Balance, Settlements, Settlement } from '../types'
interface UseAccountingHook { interface UseAccountingHook {
isLoading: boolean
isLoadingUncashed: boolean isLoadingUncashed: boolean
error: Error | null
totalsent: Token
totalreceived: Token
accounting: Accounting[] | null accounting: Accounting[] | null
} }
@@ -76,39 +72,34 @@ function mergeAccounting(
) )
} }
export const useAccounting = (): UseAccountingHook => { export const useAccounting = (
const settlements = useApiSettlements() beeDebugApi: BeeDebug | null,
const balances = useApiPeerBalances() settlements: Settlements | null,
balances: Balance[] | null,
const [err, setErr] = useState<Error | null>(null) ): UseAccountingHook => {
const [isLoadingUncashed, setIsloadingUncashed] = useState<boolean>(false) const [isLoadingUncashed, setIsloadingUncashed] = useState<boolean>(false)
const [uncashedAmounts, setUncashedAmounts] = useState<LastCashoutActionResponse[] | undefined>(undefined) const [uncashedAmounts, setUncashedAmounts] = useState<LastCashoutActionResponse[] | undefined>(undefined)
const error = balances.error || settlements.error || err
useEffect(() => { useEffect(() => {
// We don't have any settlements loaded yet or we are already loading/have loaded the uncashed amounts // We don't have any settlements loaded yet or we are already loading/have loaded the uncashed amounts
if (isLoadingUncashed || !settlements.settlements || uncashedAmounts || error) return if (isLoadingUncashed || !beeDebugApi || !settlements || uncashedAmounts) return
setIsloadingUncashed(true) setIsloadingUncashed(true)
const promises = settlements.settlements.settlements const promises = settlements.settlements
.filter(({ received }) => received.toBigNumber.gt('0')) .filter(({ received }) => received.toBigNumber.gt('0'))
.map(({ peer }) => beeDebugApi.chequebook.getPeerLastCashout(peer)) .map(({ peer }) => makeRetriablePromise(() => beeDebugApi.getLastCashoutAction(peer)))
Promise.all(promises) Promise.allSettled(promises).then(settlements => {
.then(setUncashedAmounts) const results = unwrapPromiseSettlements(settlements)
.catch(setErr) setUncashedAmounts(results.fulfilled)
.finally(() => setIsloadingUncashed(false)) setIsloadingUncashed(false)
}, [settlements, isLoadingUncashed, uncashedAmounts, error]) })
}, [settlements, isLoadingUncashed, uncashedAmounts])
const accounting = mergeAccounting(balances.peerBalances, settlements.settlements?.settlements, uncashedAmounts) const accounting = mergeAccounting(balances, settlements?.settlements, uncashedAmounts)
return { return {
isLoading: settlements.isLoadingSettlements || balances.isLoadingPeerBalances,
isLoadingUncashed, isLoadingUncashed,
error,
accounting, accounting,
totalsent: settlements.settlements?.totalSent || new Token('0'),
totalreceived: settlements.settlements?.totalReceived || new Token('0'),
} }
} }
-401
View File
@@ -1,406 +1,5 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import {
NodeAddresses,
ChequebookAddressResponse,
LastChequesResponse,
Health,
Peer,
Topology,
LastChequesForPeerResponse,
} from '@ethersphere/bee-js'
import { beeDebugApi, beeApi } from '../services/bee'
import axios from 'axios' import axios from 'axios'
import { Token } from '../models/Token'
export interface HealthHook {
health: boolean
isLoadingHealth: boolean
error: Error | null
}
export const useApiHealth = (): HealthHook => {
const [health, setHealth] = useState<boolean>(false)
const [isLoadingHealth, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeApi.status
.health()
.then(res => {
setHealth(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { health, isLoadingHealth, error }
}
export interface DebugHealthHook {
nodeHealth: Health | null
isLoadingNodeHealth: boolean
error: Error | null
}
export const useDebugApiHealth = (): DebugHealthHook => {
const [nodeHealth, setNodeHealth] = useState<Health | null>(null)
const [isLoadingNodeHealth, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.status
.nodeHealth()
.then(res => {
setNodeHealth(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { nodeHealth, isLoadingNodeHealth, error }
}
export interface NodeAddressesHook {
nodeAddresses: NodeAddresses | null
isLoadingNodeAddresses: boolean
error: Error | null
}
export const useApiNodeAddresses = (): NodeAddressesHook => {
const [nodeAddresses, setNodeAddresses] = useState<NodeAddresses | null>(null)
const [isLoadingNodeAddresses, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.connectivity
.addresses()
.then(res => {
setNodeAddresses(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { nodeAddresses, isLoadingNodeAddresses, error }
}
export interface NodeTopologyHook {
topology: Topology | null
isLoading: boolean
error: Error | null
}
export const useApiNodeTopology = (): NodeTopologyHook => {
const [topology, setNodeTopology] = useState<Topology | null>(null)
const [isLoading, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.connectivity
.topology()
.then(res => {
setNodeTopology(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { topology, isLoading, error }
}
export interface ChequebookAddressHook {
chequebookAddress: ChequebookAddressResponse | null
isLoadingChequebookAddress: boolean
error: Error | null
}
export const useApiChequebookAddress = (): ChequebookAddressHook => {
const [chequebookAddress, setChequebookAddress] = useState<ChequebookAddressResponse | null>(null)
const [isLoadingChequebookAddress, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.chequebook
.address()
.then(res => {
setChequebookAddress(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { chequebookAddress, isLoadingChequebookAddress, error }
}
export interface NodePeersHook {
peers: Peer[] | null
isLoading: boolean
error: Error | null
}
export const useApiNodePeers = (): NodePeersHook => {
const [peers, setPeers] = useState<Peer[] | null>(null)
const [isLoading, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.connectivity
.listPeers()
.then(res => {
setPeers(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { peers, isLoading, error }
}
export interface ChequebookBalance {
totalBalance: Token
availableBalance: Token
}
export interface ChequebookBalanceHook {
chequebookBalance: ChequebookBalance | null
isLoadingChequebookBalance: boolean
error: Error | null
}
export const useApiChequebookBalance = (): ChequebookBalanceHook => {
const [chequebookBalance, setChequebookBalance] = useState<ChequebookBalance | null>(null)
const [isLoadingChequebookBalance, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.chequebook
.balance()
.then(({ totalBalance, availableBalance }) => {
const balance = {
totalBalance: new Token(totalBalance),
availableBalance: new Token(availableBalance),
}
setChequebookBalance(balance)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { chequebookBalance, isLoadingChequebookBalance, error }
}
export interface Balance {
peer: string
balance: Token
}
export interface PeerBalanceHook {
peerBalances: Balance[] | null
isLoadingPeerBalances: boolean
error: Error | null
}
export const useApiPeerBalances = (): PeerBalanceHook => {
const [peerBalances, setPeerBalances] = useState<Balance[] | null>(null)
const [isLoadingPeerBalances, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.balance
.balances()
.then(res => {
// for some reason sometimes these are numbers and not BigInts
const balances = res.balances.map(({ peer, balance }) => ({ peer, balance: new Token(balance) }))
setPeerBalances(balances)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { peerBalances, isLoadingPeerBalances, error }
}
export interface PeerChequesHook {
peerCheques: LastChequesResponse | null
isLoadingPeerCheques: boolean
error: Error | null
}
export const useApiPeerCheques = (): PeerChequesHook => {
const [peerCheques, setPeerCheques] = useState<LastChequesResponse | null>(null)
const [isLoadingPeerCheques, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.chequebook
.getLastCheques()
.then(res => {
setPeerCheques(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { peerCheques, isLoadingPeerCheques, error }
}
export interface PeerLastChequesHook {
peerCheque: LastChequesForPeerResponse | null
isLoadingPeerCheque: boolean
error: Error | null
}
export const useApiPeerLastCheque = (peerId: string): PeerLastChequesHook => {
const [peerCheque, setPeerCheque] = useState<LastChequesForPeerResponse | null>(null)
const [isLoadingPeerCheque, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.chequebook
.getPeerLastCheques(peerId)
.then(res => {
setPeerCheque(res)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [peerId])
return { peerCheque, isLoadingPeerCheque, error }
}
export interface Settlement {
peer: string
received: Token
sent: Token
}
export interface Settlements {
totalReceived: Token
totalSent: Token
settlements: Settlement[]
}
export interface SettlementsHook {
settlements: Settlements | null
isLoadingSettlements: boolean
error: Error | null
}
export const useApiSettlements = (): SettlementsHook => {
const [settlements, setSettlements] = useState<Settlements | null>(null)
const [isLoadingSettlements, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.settlements
.getSettlements()
.then(({ totalReceived, settlements, totalSent }) => {
const set = {
totalReceived: new Token(totalReceived),
totalSent: new Token(totalSent),
settlements: settlements.map(({ peer, received, sent }) => ({
peer,
received: new Token(received),
sent: new Token(sent),
})),
}
setSettlements(set)
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
return { settlements, isLoadingSettlements, error }
}
export interface LastCashout {
peer: string
uncashedAmount: Token
}
export interface PeerLastCashoutHook {
peerCashout: LastCashout | null
isLoadingPeerCashout: boolean
error: Error | null
}
export const useApiPeerLastCashout = (peerId: string): PeerLastCashoutHook => {
const [peerCashout, setPeerCashout] = useState<LastCashout | null>(null)
const [isLoadingPeerCashout, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
beeDebugApi.chequebook
.getPeerLastCashout(peerId)
.then(({ peer, uncashedAmount }) => {
setPeerCashout({ peer, uncashedAmount: new Token(uncashedAmount.toString()) })
})
.catch(error => {
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [peerId])
return { peerCashout, isLoadingPeerCashout, error }
}
export interface LatestBeeReleaseHook { export interface LatestBeeReleaseHook {
latestBeeRelease: LatestBeeRelease | null latestBeeRelease: LatestBeeRelease | null
-101
View File
@@ -1,101 +0,0 @@
import { ChequebookAddressResponse } from '@ethersphere/bee-js'
import {
ChequebookBalance,
useApiChequebookAddress,
useApiChequebookBalance,
useApiHealth,
useApiNodeAddresses,
useApiNodeTopology,
useDebugApiHealth,
useLatestBeeRelease,
} from './apiHooks'
import semver from 'semver'
import { engines } from '../../package.json'
export interface StatusChequebookHook extends StatusHookCommon {
chequebookBalance: ChequebookBalance | null
chequebookAddress: ChequebookAddressResponse | null
}
export const useStatusNodeVersion = (): StatusNodeVersionHook => {
const { latestBeeRelease, isLoadingLatestBeeRelease } = useLatestBeeRelease()
const { nodeHealth, isLoadingNodeHealth } = useDebugApiHealth()
const latestVersion = semver.coerce(latestBeeRelease?.name)?.version
const latestUserVersion = semver.coerce(nodeHealth?.version)?.version
const isLatestBeeVersion = Boolean(
latestVersion &&
latestUserVersion &&
semver.satisfies(latestVersion, latestUserVersion, {
includePrerelease: true,
}),
)
return {
isLoading: isLoadingNodeHealth || isLoadingLatestBeeRelease,
isOk: Boolean(
nodeHealth &&
semver.satisfies(nodeHealth.version, engines.bee, {
includePrerelease: true,
}),
),
userVersion: nodeHealth?.version,
latestVersion,
latestUrl: latestBeeRelease?.html_url || 'https://github.com/ethersphere/bee/releases/latest',
isLatestBeeVersion,
}
}
export const useStatusEthereumConnection = (): StatusEthereumConnectionHook => {
const { isLoadingNodeAddresses, nodeAddresses } = useApiNodeAddresses()
return {
isLoading: isLoadingNodeAddresses,
isOk: Boolean(nodeAddresses?.ethereum),
nodeAddresses,
}
}
export const useStatusDebugConnection = (): StatusHookCommon => {
const { isLoadingNodeHealth, nodeHealth } = useDebugApiHealth()
return {
isLoading: isLoadingNodeHealth,
isOk: Boolean(nodeHealth?.status === 'ok'),
}
}
export const useStatusConnection = (): StatusHookCommon => {
const { isLoadingHealth, health } = useApiHealth()
return {
isLoading: isLoadingHealth,
isOk: health,
}
}
export const useStatusTopology = (): StatusTopologyHook => {
const { topology, isLoading } = useApiNodeTopology()
return {
isLoading,
isOk: Boolean(topology?.connected && topology?.connected > 0),
topology,
}
}
export const useStatusChequebook = (): StatusChequebookHook => {
const { chequebookAddress, isLoadingChequebookAddress } = useApiChequebookAddress()
const { chequebookBalance, isLoadingChequebookBalance } = useApiChequebookBalance()
return {
isLoading: isLoadingChequebookAddress || isLoadingChequebookBalance,
isOk:
Boolean(chequebookAddress?.chequebookAddress) &&
chequebookBalance !== null &&
chequebookBalance?.totalBalance.toBigNumber.isGreaterThan(0),
chequebookBalance,
chequebookAddress,
}
}
+14 -33
View File
@@ -1,19 +1,18 @@
import { useState, useEffect, ReactElement } from 'react' import { useContext, ReactElement } from 'react'
import ErrorBoundary from '../components/ErrorBoundary' import ErrorBoundary from '../components/ErrorBoundary'
import AlertVersion from '../components/AlertVersion' import AlertVersion from '../components/AlertVersion'
import { Container, CircularProgress } from '@material-ui/core'
import { createStyles, Theme, makeStyles } from '@material-ui/core/styles' import { createStyles, Theme, makeStyles } from '@material-ui/core/styles'
import SideBar from '../components/SideBar' import SideBar from '../components/SideBar'
import NavBar from '../components/NavBar'
import { useApiHealth, useDebugApiHealth } from '../hooks/apiHooks' import { Context } from '../providers/Bee'
import { RouteComponentProps } from 'react-router'
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
createStyles({ createStyles({
content: { content: {
marginLeft: '240px', marginLeft: 300,
flexGrow: 1, flexGrow: 1,
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
padding: theme.spacing(3), padding: theme.spacing(3),
@@ -22,46 +21,28 @@ const useStyles = makeStyles((theme: Theme) =>
}), }),
) )
interface Props extends RouteComponentProps { interface Props {
children?: ReactElement children?: ReactElement
} }
const Dashboard = (props: Props): ReactElement => { const Dashboard = (props: Props): ReactElement => {
const classes = useStyles() const classes = useStyles()
const [themeMode, toggleThemeMode] = useState('light') const { isLoading } = useContext(Context)
// FIXME: handle errrors and loading
const { health } = useApiHealth()
const { nodeHealth } = useDebugApiHealth()
useEffect(() => {
const theme = localStorage.getItem('theme')
if (theme) {
toggleThemeMode(String(localStorage.getItem('theme')))
} else if (window?.matchMedia('(prefers-color-scheme: dark)')?.matches) {
toggleThemeMode('dark')
}
window?.matchMedia('(prefers-color-scheme: dark)')?.addEventListener('change', e => {
toggleThemeMode(e?.matches ? 'dark' : 'light')
})
return () =>
window?.matchMedia('(prefers-color-scheme: dark)')?.removeEventListener('change', e => {
toggleThemeMode(e?.matches ? 'dark' : 'light')
})
}, [])
return ( return (
<div> <div>
<SideBar {...props} themeMode={themeMode} health={health} nodeHealth={nodeHealth} /> <SideBar />
<NavBar themeMode={themeMode} />
<ErrorBoundary> <ErrorBoundary>
<main className={classes.content}> <main className={classes.content}>
<AlertVersion /> <AlertVersion />
{props.children} {isLoading ? (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress />
</Container>
) : (
props.children
)}
</main> </main>
</ErrorBoundary> </ErrorBoundary>
</div> </div>
+24 -35
View File
@@ -2,7 +2,6 @@ import { ReactElement } from 'react'
import { createStyles, makeStyles } from '@material-ui/core/styles' import { createStyles, makeStyles } from '@material-ui/core/styles'
import { Card, CardContent, Typography, Theme } from '@material-ui/core/' import { Card, CardContent, Typography, Theme } from '@material-ui/core/'
import { Skeleton } from '@material-ui/lab'
import WithdrawModal from '../../containers/WithdrawModal' import WithdrawModal from '../../containers/WithdrawModal'
import DepositModal from '../../containers/DepositModal' import DepositModal from '../../containers/DepositModal'
@@ -45,12 +44,11 @@ interface ChequebookBalance {
interface Props { interface Props {
chequebookAddress: ChequebookAddressResponse | null chequebookAddress: ChequebookAddressResponse | null
chequebookBalance: ChequebookBalance | null chequebookBalance: ChequebookBalance | null
totalsent: Token totalsent?: Token
totalreceived: Token totalreceived?: Token
isLoading: boolean
} }
function AccountCard({ totalreceived, totalsent, chequebookBalance, isLoading }: Props): ReactElement { function AccountCard({ totalreceived, totalsent, chequebookBalance }: Props): ReactElement {
const classes = useStyles() const classes = useStyles()
return ( return (
@@ -66,37 +64,28 @@ function AccountCard({ totalreceived, totalsent, chequebookBalance, isLoading }:
</div> </div>
<Card className={classes.root}> <Card className={classes.root}>
{!isLoading && ( <CardContent className={classes.gridContainer}>
<CardContent className={classes.gridContainer}> <div>
<div> <Typography component="h2" variant="h6" color="primary" gutterBottom>
<Typography component="h2" variant="h6" color="primary" gutterBottom> Total Balance
Total Balance </Typography>
</Typography> <Typography variant="h5">{chequebookBalance?.totalBalance.toFixedDecimal()} BZZ</Typography>
<Typography variant="h5">{chequebookBalance?.totalBalance.toFixedDecimal()} BZZ</Typography>
</div>
<div>
<Typography component="h2" variant="h6" color="primary" gutterBottom>
Available Uncommitted Balance
</Typography>
<Typography variant="h5">{chequebookBalance?.availableBalance.toFixedDecimal()} BZZ</Typography>
</div>
<div>
<Typography component="h2" variant="h6" color="primary" gutterBottom>
Total Sent / Received
</Typography>
<Typography variant="h5">
{totalsent.toFixedDecimal()} / {totalreceived.toFixedDecimal()} BZZ
</Typography>
</div>
</CardContent>
)}
{isLoading && (
<div className={classes.gridContainer}>
<Skeleton width={180} height={110} animation="wave" />
<Skeleton width={180} height={110} animation="wave" />
<Skeleton width={180} height={110} animation="wave" />
</div> </div>
)} <div>
<Typography component="h2" variant="h6" color="primary" gutterBottom>
Available Uncommitted Balance
</Typography>
<Typography variant="h5">{chequebookBalance?.availableBalance.toFixedDecimal()} BZZ</Typography>
</div>
<div>
<Typography component="h2" variant="h6" color="primary" gutterBottom>
Total Sent / Received
</Typography>
<Typography variant="h5">
{totalsent?.toFixedDecimal()} / {totalreceived?.toFixedDecimal()} BZZ
</Typography>
</div>
</CardContent>
</Card> </Card>
</div> </div>
) )
+12 -39
View File
@@ -1,19 +1,12 @@
import type { ReactElement } from 'react' import { ReactElement, useContext } from 'react'
import { Theme, createStyles, makeStyles } from '@material-ui/core/styles' import { Theme, createStyles, makeStyles } from '@material-ui/core/styles'
import { Container, CircularProgress } from '@material-ui/core'
import AccountCard from '../accounting/AccountCard' import AccountCard from '../accounting/AccountCard'
import BalancesTable from './BalancesTable' import BalancesTable from './BalancesTable'
import EthereumAddressCard from '../../components/EthereumAddressCard' import EthereumAddressCard from '../../components/EthereumAddressCard'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard' import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { Context as BeeContext } from '../../providers/Bee'
import { import { Context as SettingsContext } from '../../providers/Settings'
useApiNodeAddresses,
useApiChequebookAddress,
useApiChequebookBalance,
useApiHealth,
useDebugApiHealth,
} from '../../hooks/apiHooks'
import { useAccounting } from '../../hooks/accounting' import { useAccounting } from '../../hooks/accounting'
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
@@ -29,44 +22,24 @@ const useStyles = makeStyles((theme: Theme) =>
export default function Accounting(): ReactElement { export default function Accounting(): ReactElement {
const classes = useStyles() const classes = useStyles()
const { chequebookAddress, isLoadingChequebookAddress } = useApiChequebookAddress() const { status, nodeAddresses, chequebookAddress, chequebookBalance, settlements, peerBalances } =
const { chequebookBalance, isLoadingChequebookBalance } = useApiChequebookBalance() useContext(BeeContext)
const { nodeAddresses, isLoadingNodeAddresses } = useApiNodeAddresses() const { beeDebugApi } = useContext(SettingsContext)
const { health, isLoadingHealth } = useApiHealth()
const { nodeHealth, isLoadingNodeHealth } = useDebugApiHealth()
const { isLoading, totalsent, totalreceived, accounting, isLoadingUncashed, error } = useAccounting()
if (isLoadingHealth || isLoadingNodeHealth) { const { accounting, isLoadingUncashed } = useAccounting(beeDebugApi, settlements, peerBalances)
return (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress />
</Container>
)
}
if (nodeHealth?.status !== 'ok' || !health) return <TroubleshootConnectionCard /> if (!status.all) return <TroubleshootConnectionCard />
return ( return (
<div className={classes.root}> <div className={classes.root}>
<AccountCard <AccountCard
chequebookAddress={chequebookAddress} chequebookAddress={chequebookAddress}
isLoading={isLoadingChequebookAddress || isLoading || isLoadingChequebookBalance}
chequebookBalance={chequebookBalance} chequebookBalance={chequebookBalance}
totalsent={totalsent} totalsent={settlements?.totalSent}
totalreceived={totalreceived} totalreceived={settlements?.totalReceived}
/> />
<EthereumAddressCard <EthereumAddressCard nodeAddresses={nodeAddresses} chequebookAddress={chequebookAddress} />
nodeAddresses={nodeAddresses} <BalancesTable accounting={accounting} isLoadingUncashed={isLoadingUncashed} />
isLoadingNodeAddresses={isLoadingNodeAddresses}
chequebookAddress={chequebookAddress}
isLoadingChequebookAddress={isLoadingChequebookAddress}
/>
{error && (
<Container style={{ textAlign: 'center', padding: '50px' }}>
Error loading accounting details: {error.message}
</Container>
)}
{!error && <BalancesTable accounting={accounting} isLoadingUncashed={isLoadingUncashed} />}
</div> </div>
) )
} }
+4 -3
View File
@@ -1,8 +1,8 @@
import { ReactElement, useState } from 'react' import { ReactElement, useState, useContext } from 'react'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import { Paper, InputBase, IconButton, FormHelperText } from '@material-ui/core' import { Paper, InputBase, IconButton, FormHelperText } from '@material-ui/core'
import { Search } from '@material-ui/icons' import { Search } from '@material-ui/icons'
import { apiHost } from '../../constants' import { Context as SettingsContext } from '../../providers/Settings'
import { Utils } from '@ethersphere/bee-js' import { Utils } from '@ethersphere/bee-js'
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
@@ -28,6 +28,7 @@ const useStyles = makeStyles((theme: Theme) =>
export default function Files(): ReactElement { export default function Files(): ReactElement {
const classes = useStyles() const classes = useStyles()
const { apiUrl } = useContext(SettingsContext)
const [referenceInput, setReferenceInput] = useState('') const [referenceInput, setReferenceInput] = useState('')
const [referenceError, setReferenceError] = useState<Error | null>(null) const [referenceError, setReferenceError] = useState<Error | null>(null)
@@ -50,7 +51,7 @@ export default function Files(): ReactElement {
onChange={handleReferenceChange} onChange={handleReferenceChange}
/> />
<IconButton <IconButton
href={`${apiHost}/bzz/${referenceInput}`} href={`${apiUrl}/bzz/${referenceInput}`}
target="_blank" target="_blank"
disabled={referenceError !== null || !referenceInput} disabled={referenceError !== null || !referenceInput}
className={classes.iconButton} className={classes.iconButton}
+7 -7
View File
@@ -1,15 +1,15 @@
import React, { ReactElement } from 'react'
import Button from '@material-ui/core/Button' import Button from '@material-ui/core/Button'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import Menu from '@material-ui/core/Menu' import Menu from '@material-ui/core/Menu'
import MenuItem from '@material-ui/core/MenuItem' import MenuItem from '@material-ui/core/MenuItem'
import ListItemIcon from '@material-ui/core/ListItemIcon' import React, { ReactElement } from 'react'
import { PostageBatch } from '@ethersphere/bee-js'
import PeerDetailDrawer from '../../components/PeerDetail' import PeerDetailDrawer from '../../components/PeerDetail'
import { EnrichedPostageBatch } from '../../providers/Stamps'
interface Props { interface Props {
stamps: PostageBatch[] | null stamps: EnrichedPostageBatch[] | null
selectedStamp: PostageBatch | null selectedStamp: EnrichedPostageBatch | null
setSelected: (stamp: PostageBatch) => void setSelected: (stamp: EnrichedPostageBatch) => void
} }
export default function SimpleMenu({ stamps, selectedStamp, setSelected }: Props): ReactElement | null { export default function SimpleMenu({ stamps, selectedStamp, setSelected }: Props): ReactElement | null {
@@ -38,7 +38,7 @@ export default function SimpleMenu({ stamps, selectedStamp, setSelected }: Props
}} }}
selected={stamp.batchID === selectedStamp?.batchID} selected={stamp.batchID === selectedStamp?.batchID}
> >
<ListItemIcon>{stamp.utilization}</ListItemIcon> <ListItemIcon>{stamp.usageText}</ListItemIcon>
<PeerDetailDrawer peerId={stamp.batchID} /> <PeerDetailDrawer peerId={stamp.batchID} />
</MenuItem> </MenuItem>
))} ))}
+35 -21
View File
@@ -1,33 +1,36 @@
import { ReactElement, useContext, useEffect, useState } from 'react' import { Button, CircularProgress, Container } from '@material-ui/core'
import { beeApi } from '../../services/bee'
import { Button, Container, CircularProgress } from '@material-ui/core'
import { DropzoneArea } from 'material-ui-dropzone'
import ClipboardCopy from '../../components/ClipboardCopy'
import { PostageBatch } from '@ethersphere/bee-js'
import { Context } from '../../providers/Stamps'
import PeerDetailDrawer from '../../components/PeerDetail'
import Chip from '@material-ui/core/Chip'
import Avatar from '@material-ui/core/Avatar' import Avatar from '@material-ui/core/Avatar'
import SelectStamp from './SelectStamp' import Chip from '@material-ui/core/Chip'
import CreatePostageStamp from '../stamps/CreatePostageStampModal' import { DropzoneArea } from 'material-ui-dropzone'
import { useSnackbar } from 'notistack' import { useSnackbar } from 'notistack'
import { ReactElement, useContext, useEffect, useState } from 'react'
import UploadSizeAlert from '../../components/AlertUploadSize'
import ClipboardCopy from '../../components/ClipboardCopy'
import PeerDetailDrawer from '../../components/PeerDetail'
import { Context, EnrichedPostageBatch } from '../../providers/Stamps'
import { Context as SettingsContext } from '../../providers/Settings'
import CreatePostageStamp from '../stamps/CreatePostageStampModal'
import SelectStamp from './SelectStamp'
const MAX_FILE_SIZE = 1_000_000_000 // 1 gigabyte
export default function Files(): ReactElement { export default function Files(): ReactElement {
const [dropzoneKey, setDropzoneKey] = useState(0)
const [file, setFile] = useState<File | null>(null) const [file, setFile] = useState<File | null>(null)
const [uploadReference, setUploadReference] = useState('') const [uploadReference, setUploadReference] = useState('')
const [isUploadingFile, setIsUploadingFile] = useState(false) const [isUploadingFile, setIsUploadingFile] = useState(false)
const [selectedStamp, setSelectedStamp] = useState<PostageBatch | null>(null) const [selectedStamp, setSelectedStamp] = useState<EnrichedPostageBatch | null>(null)
const { isLoading, error, stamps } = useContext(Context) const { isLoading, error, stamps } = useContext(Context)
const { beeApi } = useContext(SettingsContext)
const { enqueueSnackbar } = useSnackbar() const { enqueueSnackbar } = useSnackbar()
// Choose a postage stamp that has the lowest utilization // Choose a postage stamp that has the lowest usage
useEffect(() => { useEffect(() => {
if (!selectedStamp && stamps && stamps.length > 0) { if (!selectedStamp && stamps && stamps.length > 0) {
const stamp = stamps.reduce((prev, curr) => { const stamp = stamps.reduce((prev, curr) => {
if (curr.utilization < prev.utilization) return curr if (curr.usage < prev.usage) return curr
return prev return prev
}, stamps[0]) }, stamps[0])
@@ -38,12 +41,18 @@ export default function Files(): ReactElement {
const uploadFile = () => { const uploadFile = () => {
if (file === null || selectedStamp === null) return if (file === null || selectedStamp === null) return
if (!beeApi) return
setIsUploadingFile(true) setIsUploadingFile(true)
beeApi.files beeApi
.uploadFile(selectedStamp.batchID, file) .uploadFile(selectedStamp.batchID, file)
.then(hash => { .then(hash => {
setUploadReference(hash) window.setTimeout(() => {
setFile(null) setFile(null)
setUploadReference(hash)
setDropzoneKey(dropzoneKey + 1)
}, 0)
}) })
.catch(e => enqueueSnackbar(`Error uploading: ${e.message}`, { variant: 'error' })) .catch(e => enqueueSnackbar(`Error uploading: ${e.message}`, { variant: 'error' }))
.finally(() => { .finally(() => {
@@ -54,21 +63,25 @@ export default function Files(): ReactElement {
const handleChange = (files?: File[]) => { const handleChange = (files?: File[]) => {
if (files) { if (files) {
setFile(files[0]) setFile(files[0])
setUploadReference('')
} }
} }
return ( return (
<div> <div>
<div> <div>
<DropzoneArea onChange={handleChange} filesLimit={1} /> <DropzoneArea
key={'dropzone-' + dropzoneKey}
onChange={handleChange}
filesLimit={1}
maxFileSize={MAX_FILE_SIZE}
/>
<div style={{ marginTop: '15px' }}> <div style={{ marginTop: '15px' }}>
{selectedStamp && ( {selectedStamp && (
<div style={{ display: 'flex' }}> <div style={{ display: 'flex' }}>
<small> <small>
with Postage Stamp{' '} with Postage Stamp{' '}
<Chip <Chip
avatar={<Avatar>{selectedStamp.utilization}</Avatar>} avatar={<Avatar>{selectedStamp.usageText}</Avatar>}
label={<PeerDetailDrawer peerId={selectedStamp.batchID} characterLength={6} />} label={<PeerDetailDrawer peerId={selectedStamp.batchID} characterLength={6} />}
deleteIcon={<ClipboardCopy value={selectedStamp.batchID} />} deleteIcon={<ClipboardCopy value={selectedStamp.batchID} />}
onDelete={() => {} /* eslint-disable-line*/} onDelete={() => {} /* eslint-disable-line*/}
@@ -82,6 +95,7 @@ export default function Files(): ReactElement {
<Button disabled={!file && isUploadingFile && !selectedStamp} onClick={() => uploadFile()}> <Button disabled={!file && isUploadingFile && !selectedStamp} onClick={() => uploadFile()}>
Upload Upload
</Button> </Button>
{file && <UploadSizeAlert file={file} />}
{isUploadingFile && ( {isUploadingFile && (
<Container style={{ textAlign: 'center', padding: '50px' }}> <Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress /> <CircularProgress />
+5 -14
View File
@@ -1,26 +1,17 @@
import { ReactElement } from 'react' import { ReactElement, useContext } from 'react'
import { Container, CircularProgress } from '@material-ui/core' import { Container } from '@material-ui/core'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard' import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { useApiHealth, useDebugApiHealth } from '../../hooks/apiHooks' import { Context } from '../../providers/Bee'
import Download from './Download' import Download from './Download'
import Upload from './Upload' import Upload from './Upload'
import TabsContainer from '../../components/TabsContainer' import TabsContainer from '../../components/TabsContainer'
export default function Files(): ReactElement { export default function Files(): ReactElement {
const { health, isLoadingHealth } = useApiHealth() const { status } = useContext(Context)
const { nodeHealth, isLoadingNodeHealth } = useDebugApiHealth()
if (isLoadingHealth || isLoadingNodeHealth) { if (!status.all) return <TroubleshootConnectionCard />
return (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress />
</Container>
)
}
if (!health || nodeHealth?.status !== 'ok') return <TroubleshootConnectionCard />
return ( return (
<Container maxWidth="sm"> <Container maxWidth="sm">
+113
View File
@@ -0,0 +1,113 @@
import { ReactElement, useState } from 'react'
import { Link } from 'react-router-dom'
import { createStyles, makeStyles } from '@material-ui/core/styles'
import { Card, CardContent, Typography, Chip, Button } from '@material-ui/core/'
import { ArrowRight, ArrowDropUp } from '@material-ui/icons/'
import { NodeAddresses, Topology } from '@ethersphere/bee-js'
import { ROUTES } from '../../routes'
const useStyles = makeStyles(() =>
createStyles({
root: {
display: 'flex',
flex: '1 1 auto',
flexDirection: 'column',
},
status: {
color: '#2145a0',
backgroundColor: '#e1effe',
},
}),
)
interface Props {
nodeAddresses: NodeAddresses | null
nodeTopology: Topology | null
userBeeVersion?: string
isLatestBeeVersion: boolean
isOk: boolean
latestUrl: string
}
function StatusCard({
userBeeVersion,
nodeAddresses,
nodeTopology,
isLatestBeeVersion,
latestUrl,
}: Props): ReactElement | null {
const classes = useStyles()
const [underlayAddressesVisible, setUnderlayAddresessVisible] = useState<boolean>(false)
return (
<Card>
<CardContent className={classes.root}>
<>
<div style={{ marginBottom: '20px' }}>
<span style={{ marginRight: '20px' }}>Discovered Nodes: {nodeTopology?.population}</span>
<span style={{ marginRight: '20px' }}>
<span>Connected Peers: </span>
<Link to={ROUTES.PEERS}>{nodeTopology?.connected}</Link>
</span>
</div>
<div>
<Typography component="div" variant="subtitle2" gutterBottom>
<span>AGENT: </span>
<a href="https://github.com/ethersphere/bee" rel="noreferrer" target="_blank">
Bee
</a>{' '}
<span>{userBeeVersion || '-'}</span>
{isLatestBeeVersion ? (
<Chip
style={{ marginLeft: '7px', color: '#2145a0' }}
size="small"
label="latest"
className={classes.status}
/>
) : (
<Button size="small" variant="outlined" href={latestUrl}>
update
</Button>
)}
</Typography>
<Typography component="div" variant="subtitle2" gutterBottom>
<span>PUBLIC KEY: </span>
<span>{nodeAddresses?.publicKey ? nodeAddresses.publicKey : '-'}</span>
</Typography>
<Typography component="div" variant="subtitle2" gutterBottom>
<span>PSS PUBLIC KEY: </span>
<span>{nodeAddresses?.pssPublicKey ? nodeAddresses.pssPublicKey : '-'}</span>
</Typography>
<Typography component="div" variant="subtitle2" gutterBottom>
<span>OVERLAY ADDRESS (PEER ID): </span>
<span>{nodeAddresses?.overlay ? nodeAddresses.overlay : '-'}</span>
</Typography>
<Typography component="div" variant="subtitle2" gutterBottom>
<Typography component="div" onClick={() => setUnderlayAddresessVisible(!underlayAddressesVisible)}>
<Button color="primary" style={{ padding: 0, marginTop: '6px' }}>
{underlayAddressesVisible ? (
<ArrowDropUp style={{ fontSize: '12px' }} />
) : (
<ArrowRight style={{ fontSize: '12px' }} />
)}
<span>Underlay Addresses</span>
</Button>
</Typography>
{underlayAddressesVisible && (
<div>
{nodeAddresses?.underlay.map(item => (
<li key={item}>{item}</li>
))}
</div>
)}
</Typography>
</div>
</>
</CardContent>
</Card>
)
}
export default StatusCard
+49
View File
@@ -0,0 +1,49 @@
import { ReactElement, useContext } from 'react'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import StatusCard from './StatusCard'
import EthereumAddressCard from '../../components/EthereumAddressCard'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { Context as BeeContext } from '../../providers/Bee'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
width: '100%',
display: 'grid',
rowGap: theme.spacing(3),
},
}),
)
export default function Status(): ReactElement {
const classes = useStyles()
const {
status,
latestUserVersion,
isLatestBeeVersion,
latestBeeVersionUrl,
topology,
nodeAddresses,
chequebookAddress,
} = useContext(BeeContext)
if (!status.all) return <TroubleshootConnectionCard />
return (
<div className={classes.root}>
<StatusCard
userBeeVersion={latestUserVersion}
isLatestBeeVersion={isLatestBeeVersion}
isOk={status.all}
nodeTopology={topology}
latestUrl={latestBeeVersionUrl}
nodeAddresses={nodeAddresses}
/>
{nodeAddresses && chequebookAddress && (
<EthereumAddressCard nodeAddresses={nodeAddresses} chequebookAddress={chequebookAddress} />
)}
</div>
)
}
+27 -45
View File
@@ -1,4 +1,4 @@
import React, { ReactElement, useState } from 'react' import { ReactElement, useState, useContext } from 'react'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import { import {
Table, Table,
@@ -10,12 +10,11 @@ import {
Button, Button,
Paper, Paper,
Tooltip, Tooltip,
Container,
CircularProgress, CircularProgress,
} from '@material-ui/core' } from '@material-ui/core'
import { Autorenew } from '@material-ui/icons' import { Autorenew } from '@material-ui/icons'
import { beeDebugApi } from '../../services/bee' import { Context as SettingsContext } from '../../providers/Settings'
import type { Peer } from '@ethersphere/bee-js' import type { Peer } from '@ethersphere/bee-js'
const useStyles = makeStyles({ const useStyles = makeStyles({
@@ -26,47 +25,43 @@ const useStyles = makeStyles({
interface Props { interface Props {
peers: Peer[] | null peers: Peer[] | null
isLoading: boolean }
error: Error | null
interface PeerLatency {
rtt: string
loading: boolean
}
function getPingState(peerLatency: Record<string, PeerLatency>, peer: Peer): ReactElement {
if (peerLatency[peer.address]?.loading) return <CircularProgress size={20} />
if (peerLatency[peer.address]?.rtt) return <span>{peerLatency[peer.address]?.rtt}</span>
return <Autorenew />
} }
function PeerTable(props: Props): ReactElement { function PeerTable(props: Props): ReactElement {
const classes = useStyles() const classes = useStyles()
const { beeDebugApi } = useContext(SettingsContext)
const [peerLatency, setPeerLatency] = useState([{ peerId: '', rtt: '', loading: false }]) const [peerLatency, setPeerLatency] = useState<Record<string, PeerLatency>>({})
const PingPeer = (peerId: string) => { const pingPeer = (peerId: string) => {
setPeerLatency([...peerLatency, { peerId: peerId, rtt: '', loading: true }]) setPeerLatency(prevPeerLatency => ({ ...prevPeerLatency, [peerId]: { rtt: '', loading: true } }))
beeDebugApi.connectivity beeDebugApi
.ping(peerId) ?.pingPeer(peerId)
.then(res => { .then(res => {
setPeerLatency([...peerLatency, { peerId: peerId, rtt: res.rtt, loading: false }]) setPeerLatency(prevPeerLatency => ({ ...prevPeerLatency, [peerId]: { rtt: res.rtt, loading: false } }))
}) })
.catch(() => { .catch(() => {
setPeerLatency([...peerLatency, { peerId: peerId, rtt: 'error', loading: false }]) setPeerLatency(prevPeerLatency => ({ ...prevPeerLatency, [peerId]: { rtt: 'error', loading: false } }))
}) })
} }
if (props.isLoading) {
return (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress />
</Container>
)
}
if (props.error || props.peers === null) {
return (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<p>Failed to load peers</p>
</Container>
)
}
return ( return (
<div> <div>
<TableContainer component={Paper}> <TableContainer component={Paper}>
<Table className={classes.table} aria-label="simple table"> <Table className={classes.table}>
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell>Index</TableCell> <TableCell>Index</TableCell>
@@ -75,7 +70,7 @@ function PeerTable(props: Props): ReactElement {
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{props.peers.map((peer: Peer, idx: number) => ( {props.peers?.map((peer: Peer, idx: number) => (
<TableRow key={peer.address}> <TableRow key={peer.address}>
<TableCell component="th" scope="row"> <TableCell component="th" scope="row">
{idx + 1} {idx + 1}
@@ -83,21 +78,8 @@ function PeerTable(props: Props): ReactElement {
<TableCell>{peer.address}</TableCell> <TableCell>{peer.address}</TableCell>
<TableCell align="right"> <TableCell align="right">
<Tooltip title="Ping node"> <Tooltip title="Ping node">
<Button color="primary" onClick={() => PingPeer(peer.address)}> <Button color="primary" onClick={() => pingPeer(peer.address)}>
{ {getPingState(peerLatency, peer)}
// FIXME: this should be broken up
/* eslint-disable no-nested-ternary */
peerLatency.find(item => item.peerId === peer.address) ? (
peerLatency.filter(item => item.peerId === peer.address)[0].loading ? (
<CircularProgress size={20} />
) : (
peerLatency.filter(item => item.peerId === peer.address)[0].rtt
)
) : (
<Autorenew />
)
/* eslint-enable no-nested-ternary */
}
</Button> </Button>
</Tooltip> </Tooltip>
</TableCell> </TableCell>
+6 -17
View File
@@ -1,32 +1,21 @@
import { Container, CircularProgress } from '@material-ui/core/'
import PeerTable from './PeerTable' import PeerTable from './PeerTable'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard' import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { useApiNodeTopology, useApiNodePeers, useDebugApiHealth } from '../../hooks/apiHooks' import { Context } from '../../providers/Bee'
import TopologyStats from '../../components/TopologyStats' import TopologyStats from '../../components/TopologyStats'
import { ReactElement } from 'react' import { ReactElement, useContext } from 'react'
export default function Peers(): ReactElement { export default function Peers(): ReactElement {
const topology = useApiNodeTopology() const { topology, peers, status } = useContext(Context)
const debugHealth = useDebugApiHealth()
const peers = useApiNodePeers()
if (debugHealth.isLoadingNodeHealth) { if (!status.all) {
return (
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress />
</Container>
)
}
if (debugHealth.error) {
return <TroubleshootConnectionCard /> return <TroubleshootConnectionCard />
} }
return ( return (
<> <>
<TopologyStats {...topology} /> <TopologyStats topology={topology} />
<PeerTable {...peers} /> <PeerTable peers={peers} />
</> </>
) )
} }
+14 -28
View File
@@ -1,26 +1,20 @@
import React, { ReactElement, useState } from 'react' import React, { ReactElement, useState, useContext } from 'react'
import { Paper, Container, TextField, Typography, Button } from '@material-ui/core' import { Paper, Container, TextField, Typography, Button } from '@material-ui/core'
import { Context as SettingsContext } from '../../providers/Settings'
export default function Settings(): ReactElement { export default function Settings(): ReactElement {
const [refreshVisibility, toggleRefreshVisibility] = useState(false) const { apiUrl, apiDebugUrl, setApiUrl, setDebugApiUrl } = useContext(SettingsContext)
const [host, setHost] = useState('') const [host, setHost] = useState(apiUrl)
const [debugHost, setDebugHost] = useState('') const [debugHost, setDebugHost] = useState(apiDebugUrl)
const handleNewHostConnection = () => { const submit = () => {
if (host) { if (host !== apiUrl) setApiUrl(host)
sessionStorage.setItem('api_host', host)
}
if (debugHost) { if (debugHost !== apiDebugUrl) setDebugApiUrl(debugHost)
sessionStorage.setItem('debug_api_host', debugHost)
}
if (host || debugHost) {
toggleRefreshVisibility(!refreshVisibility)
window.location.reload()
}
} }
const touched = host !== apiUrl || debugHost !== apiDebugUrl
return ( return (
<div> <div>
<Container> <Container>
@@ -34,16 +28,13 @@ export default function Settings(): ReactElement {
placeholder="ex: 127.0.0.0.1:1633" placeholder="ex: 127.0.0.0.1:1633"
helperText="Enter node host override / port" helperText="Enter node host override / port"
fullWidth fullWidth
defaultValue={ defaultValue={apiUrl}
sessionStorage.getItem('api_host') ? sessionStorage.getItem('api_host') : process.env.REACT_APP_BEE_HOST
}
margin="normal" margin="normal"
InputLabelProps={{ InputLabelProps={{
shrink: true, shrink: true,
}} }}
onChange={e => { onChange={e => {
setHost(e.target.value) setHost(e.target.value)
toggleRefreshVisibility(true)
}} }}
variant="filled" variant="filled"
/> />
@@ -55,14 +46,9 @@ export default function Settings(): ReactElement {
placeholder="ex: 127.0.0.0.1:1635" placeholder="ex: 127.0.0.0.1:1635"
helperText="Enter node debug host override / port" helperText="Enter node debug host override / port"
fullWidth fullWidth
defaultValue={ defaultValue={apiDebugUrl}
sessionStorage.getItem('debug_api_host')
? sessionStorage.getItem('debug_api_host')
: process.env.REACT_APP_BEE_DEBUG_HOST
}
onChange={e => { onChange={e => {
setDebugHost(e.target.value) setDebugHost(e.target.value)
toggleRefreshVisibility(true)
}} }}
margin="normal" margin="normal"
InputLabelProps={{ InputLabelProps={{
@@ -71,9 +57,9 @@ export default function Settings(): ReactElement {
variant="filled" variant="filled"
/> />
</Paper> </Paper>
{refreshVisibility ? ( {touched ? (
<div style={{ marginTop: '20px' }}> <div style={{ marginTop: '20px' }}>
<Button variant="outlined" color="primary" onClick={() => handleNewHostConnection()}> <Button variant="outlined" color="primary" onClick={submit}>
Save Save
</Button> </Button>
</div> </div>
+5 -2
View File
@@ -9,7 +9,7 @@ import DialogTitle from '@material-ui/core/DialogTitle'
import BigNumber from 'bignumber.js' import BigNumber from 'bignumber.js'
import { FormikHelpers, Form, Field, Formik } from 'formik' import { FormikHelpers, Form, Field, Formik } from 'formik'
import { TextField } from 'formik-material-ui' import { TextField } from 'formik-material-ui'
import { beeApi } from '../../services/bee' import { Context as SettingsContext } from '../../providers/Settings'
import { Context } from '../../providers/Stamps' import { Context } from '../../providers/Stamps'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import { useSnackbar } from 'notistack' import { useSnackbar } from 'notistack'
@@ -54,6 +54,7 @@ export default function FormDialog({ label }: Props): ReactElement {
const classes = useStyles() const classes = useStyles()
const [open, setOpen] = React.useState(false) const [open, setOpen] = React.useState(false)
const { refresh } = useContext(Context) const { refresh } = useContext(Context)
const { beeApi } = useContext(SettingsContext)
const handleClickOpen = () => setOpen(true) const handleClickOpen = () => setOpen(true)
const handleClose = () => setOpen(false) const handleClose = () => setOpen(false)
const { enqueueSnackbar } = useSnackbar() const { enqueueSnackbar } = useSnackbar()
@@ -66,10 +67,12 @@ export default function FormDialog({ label }: Props): ReactElement {
// This is really just a typeguard, the validation pretty much guarantees these will have the right values // This is really just a typeguard, the validation pretty much guarantees these will have the right values
if (!values.depth || !values.amount) return if (!values.depth || !values.amount) return
if (!beeApi) return
const amount = BigInt(values.amount) const amount = BigInt(values.amount)
const depth = Number.parseInt(values.depth) const depth = Number.parseInt(values.depth)
const options = values.label ? { label: values.label } : undefined const options = values.label ? { label: values.label } : undefined
await beeApi.stamps.buyPostageStamp(amount, depth, options) await beeApi.createPostageBatch(amount.toString(), depth, options)
actions.resetForm() actions.resetForm()
await refresh() await refresh()
handleClose() handleClose()
+7 -8
View File
@@ -1,10 +1,9 @@
import type { ReactElement } from 'react' import { Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@material-ui/core'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import { Table, TableBody, TableCell, TableContainer, TableRow, TableHead, Paper } from '@material-ui/core' import type { ReactElement } from 'react'
import ClipboardCopy from '../../components/ClipboardCopy' import ClipboardCopy from '../../components/ClipboardCopy'
import PeerDetailDrawer from '../../components/PeerDetail' import PeerDetailDrawer from '../../components/PeerDetail'
import { PostageBatch } from '@ethersphere/bee-js' import { EnrichedPostageBatch } from '../../providers/Stamps'
const useStyles = makeStyles({ const useStyles = makeStyles({
table: { table: {
@@ -16,7 +15,7 @@ const useStyles = makeStyles({
}, },
}) })
interface Props { interface Props {
postageStamps: PostageBatch[] | null postageStamps: EnrichedPostageBatch[] | null
} }
function StampsTable({ postageStamps }: Props): ReactElement | null { function StampsTable({ postageStamps }: Props): ReactElement | null {
@@ -29,11 +28,11 @@ function StampsTable({ postageStamps }: Props): ReactElement | null {
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell>Batch ID</TableCell> <TableCell>Batch ID</TableCell>
<TableCell align="right">Utilization</TableCell> <TableCell align="right">Usage</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{postageStamps.map(({ batchID, utilization }) => ( {postageStamps.map(({ batchID, usageText }) => (
<TableRow key={batchID}> <TableRow key={batchID}>
<TableCell> <TableCell>
<div style={{ display: 'flex' }}> <div style={{ display: 'flex' }}>
@@ -43,7 +42,7 @@ function StampsTable({ postageStamps }: Props): ReactElement | null {
<ClipboardCopy value={batchID} /> <ClipboardCopy value={batchID} />
</div> </div>
</TableCell> </TableCell>
<TableCell className={classes.values}>{utilization}</TableCell> <TableCell className={classes.values}>{usageText}</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
+5 -8
View File
@@ -5,10 +5,9 @@ import { Container, CircularProgress } from '@material-ui/core'
import StampsTable from './StampsTable' import StampsTable from './StampsTable'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard' import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import CreatePostageStampModal from './CreatePostageStampModal' import CreatePostageStampModal from './CreatePostageStampModal'
import LastUpdate from '../../components/LastUpdate'
import { useApiHealth, useDebugApiHealth } from '../../hooks/apiHooks'
import { Context } from '../../providers/Stamps' import { Context } from '../../providers/Stamps'
import { Context as BeeContext } from '../../providers/Bee'
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
createStyles({ createStyles({
@@ -32,16 +31,15 @@ const useStyles = makeStyles((theme: Theme) =>
export default function Accounting(): ReactElement { export default function Accounting(): ReactElement {
const classes = useStyles() const classes = useStyles()
const { health, isLoadingHealth } = useApiHealth() const beeContext = useContext(BeeContext)
const { nodeHealth, isLoadingNodeHealth } = useDebugApiHealth() const { stamps, isLoading, error, start, stop } = useContext(Context)
const { stamps, isLoading, error, lastUpdate, start, stop } = useContext(Context)
useEffect(() => { useEffect(() => {
start() start()
return () => stop() return () => stop()
}, []) }, [])
if (isLoadingHealth || isLoadingNodeHealth) { if (beeContext.isLoading) {
return ( return (
<Container style={{ textAlign: 'center', padding: '50px' }}> <Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress /> <CircularProgress />
@@ -49,7 +47,7 @@ export default function Accounting(): ReactElement {
) )
} }
if (nodeHealth?.status !== 'ok' || !health) return <TroubleshootConnectionCard /> if (!beeContext.status.all) return <TroubleshootConnectionCard />
return ( return (
<div className={classes.root}> <div className={classes.root}>
@@ -62,7 +60,6 @@ export default function Accounting(): ReactElement {
<> <>
<div className={classes.actions}> <div className={classes.actions}>
<CreatePostageStampModal /> <CreatePostageStampModal />
<LastUpdate date={lastUpdate} />
<div style={{ height: '5px' }}>{isLoading && <CircularProgress />}</div> <div style={{ height: '5px' }}>{isLoading && <CircularProgress />}</div>
</div> </div>
<StampsTable postageStamps={stamps} /> <StampsTable postageStamps={stamps} />
-178
View File
@@ -1,178 +0,0 @@
import { ReactElement, useEffect, useState } from 'react'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import { Typography, Paper, Button, Step, StepLabel, StepContent, Stepper, StepButton } from '@material-ui/core/'
import { CheckCircle, Error, Sync, ExpandLessSharp, ExpandMoreSharp, Autorenew } from '@material-ui/icons/'
import DebugConnectionCheck from './SetupSteps/DebugConnectionCheck'
import NodeConnectionCheck from './SetupSteps/NodeConnectionCheck'
import VersionCheck from './SetupSteps/VersionCheck'
import EthereumConnectionCheck from './SetupSteps/EthereumConnectionCheck'
import ChequebookDeployFund from './SetupSteps/ChequebookDeployFund'
import PeerConnection from './SetupSteps/PeerConnection'
import { StatusChequebookHook } from '../../hooks/status'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
padding: theme.spacing(2),
width: '100%',
},
button: {
marginTop: theme.spacing(1),
marginRight: theme.spacing(1),
},
actionsContainer: {
margin: theme.spacing(2),
},
}),
)
interface Step {
label: string
isOk: boolean
isLoading: boolean
component: ReactElement
}
interface Props {
nodeVersion: StatusNodeVersionHook
ethereumConnection: StatusEthereumConnectionHook
debugApiConnection: StatusHookCommon
apiConnection: StatusHookCommon
topology: StatusTopologyHook
chequebook: StatusChequebookHook
}
export default function NodeSetupWorkflow({
nodeVersion,
ethereumConnection,
debugApiConnection,
apiConnection,
topology,
chequebook,
}: Props): ReactElement {
const classes = useStyles()
const [activeStep, setActiveStep] = useState(-1)
const steps: Step[] = [
{
label: 'Connected to Node DebugAPI',
isOk: debugApiConnection.isOk,
isLoading: debugApiConnection.isLoading,
component: <DebugConnectionCheck {...debugApiConnection} />,
},
{
label: 'Running latest Bee version',
isOk: nodeVersion.isOk,
isLoading: nodeVersion.isLoading,
component: <VersionCheck {...nodeVersion} />,
},
{
label: 'Connected to Ethereum Blockchain',
isOk: ethereumConnection.isOk,
isLoading: ethereumConnection.isLoading,
component: <EthereumConnectionCheck {...ethereumConnection} />,
},
{
label: 'Deployed and Funded Chequebook',
isOk: chequebook.isOk,
isLoading: chequebook.isLoading,
component: <ChequebookDeployFund ethereumAddress={ethereumConnection.nodeAddresses?.ethereum} {...chequebook} />,
},
{
label: 'Connected to Node API',
isOk: apiConnection.isOk,
isLoading: apiConnection.isLoading,
component: <NodeConnectionCheck {...apiConnection} />,
},
{
label: 'Connected to Peers',
isOk: topology.isOk,
isLoading: topology.isLoading,
component: <PeerConnection {...topology} />,
},
]
useEffect(() => {
// If the user already changed the active step we don't want to overwrite it
if (activeStep >= 0 && activeStep < steps.length) return
// If any step is not fully loaded yet return
if (!steps.every(step => !step.isLoading)) return
// Select first step that is not OK
// This is deliberately a for loop (and not forEach) so that we can terminate the useEffect from within the cycle
for (let i = 0; i < steps.length; ++i) {
if (!steps[i].isOk) {
setActiveStep(i)
return
}
}
}, [steps])
const handleNext = () => {
setActiveStep(prevActiveStep => prevActiveStep + 1)
}
const handleBack = () => {
setActiveStep(prevActiveStep => prevActiveStep - 1)
}
return (
<Paper className={classes.root}>
<Typography variant="h5" gutterBottom>
Node Setup
<span style={{ marginLeft: '25px' }}>
<Button variant="outlined" size="small" onClick={() => window.location.reload()}>
<Sync />
<span style={{ marginLeft: '7px' }}>Refresh Checks</span>
</Button>
</span>
</Typography>
<Stepper nonLinear activeStep={activeStep} orientation="vertical">
{steps.map(({ label, isOk, component, isLoading }, index) => (
<Step key={label}>
<StepLabel
disabled={isLoading}
onClick={() => setActiveStep(index === activeStep ? steps.length : index)}
StepIconComponent={() => {
if (isLoading) return <Autorenew style={{ height: '25px', cursor: 'pointer' }} />
if (isOk) return <CheckCircle style={{ color: '#32c48d', height: '25px', cursor: 'pointer' }} />
return <Error style={{ color: '#c9201f', height: '25px', cursor: 'pointer' }} />
}}
>
<StepButton
disabled={isLoading}
onClick={() => setActiveStep(index === activeStep ? steps.length : index)}
style={{ justifyContent: 'space-between' }}
>
<div style={{ display: 'flex' }}>
<div style={{ marginTop: '5px' }}>{label}</div>
<div style={{ marginLeft: '12px' }}>
{index === activeStep ? <ExpandLessSharp /> : <ExpandMoreSharp />}
</div>
</div>
</StepButton>
</StepLabel>
<StepContent>
<Typography component="div">{component}</Typography>
<div className={classes.actionsContainer}>
<div>
<Button disabled={activeStep === 0} onClick={handleBack} className={classes.button}>
Back
</Button>
<Button variant="contained" color="primary" onClick={handleNext} className={classes.button}>
{index < steps.length - 1 ? 'Next' : 'Finish'}
</Button>
</div>
</div>
</StepContent>
</Step>
))}
</Stepper>
</Paper>
)
}
@@ -2,33 +2,25 @@ import { Typography } from '@material-ui/core/'
import EthereumAddress from '../../../components/EthereumAddress' import EthereumAddress from '../../../components/EthereumAddress'
import DepositModal from '../../../containers/DepositModal' import DepositModal from '../../../containers/DepositModal'
import type { ReactElement } from 'react' import type { ReactElement } from 'react'
import type { StatusChequebookHook } from '../../../hooks/status'
interface Props extends StatusChequebookHook { interface Props extends StatusHookCommon {
ethereumAddress?: string chequebookAddress?: string
} }
const ChequebookDeployFund = ({ const ChequebookDeployFund = ({ chequebookAddress, isOk }: Props): ReactElement | null => {
isLoading,
chequebookAddress,
chequebookBalance,
ethereumAddress,
}: Props): ReactElement | null => {
if (isLoading) return null
return ( return (
<div> <div>
<p style={{ marginBottom: '20px', display: 'flex' }}> <p style={{ marginBottom: '20px', display: 'flex' }}>{chequebookAddress && <DepositModal />}</p>
{chequebookAddress?.chequebookAddress && <DepositModal />}
</p>
<div style={{ marginBottom: '10px' }}> <div style={{ marginBottom: '10px' }}>
{!(chequebookAddress?.chequebookAddress && chequebookBalance?.totalBalance.toBigNumber.isGreaterThan(0)) && ( {!isOk && (
<div> <div>
<span> <span>
Your chequebook is either not deployed or funded. Join{' '} Your chequebook is either not deployed or funded. To run the node you will need xDAI and xBZZ on the xDai
<a href="https://discord.gg/ykCupZMuww">our discord channel</a>, get verified and send a message{' '} network. You may need to aquire BZZ through (e.g. <a href="https://bzz.exchange/">bzz.exchange</a>) and
<pre>sprinkle {ethereumAddress || '<YOUR BEE NODE ETH ADDRESS>'}</pre> in the <pre>#faucet-request</pre>{' '} bridge it to the xDai network through the <a href="https://omni.xdaichain.com/bridge">omni bridge</a>. To
channel to get Goerli ETH and Goerli BZZ token. pay the transaction fees, you will also need xDAI token. You can purchase DAI on the network and bridge it
to xDai network through the <a href="https://bridge.xdaichain.com/">xDai Bridge</a>. See the{' '}
<a href="https://www.xdaichain.com/#xdai-stable-chain">official xDai website</a> for more information.
</span> </span>
</div> </div>
)} )}
@@ -36,7 +28,7 @@ const ChequebookDeployFund = ({
<Typography variant="subtitle1" gutterBottom> <Typography variant="subtitle1" gutterBottom>
Chequebook Address Chequebook Address
</Typography> </Typography>
<EthereumAddress address={chequebookAddress?.chequebookAddress} network={'goerli'} /> <EthereumAddress address={chequebookAddress} />
</div> </div>
) )
} }
@@ -1,23 +1,29 @@
import type { ReactElement } from 'react' import { ReactElement, useContext } from 'react'
import { Typography, Accordion, AccordionSummary, AccordionDetails } from '@material-ui/core/' import { Typography, Accordion, AccordionSummary, AccordionDetails } from '@material-ui/core/'
import MuiAlert from '@material-ui/lab/Alert' import MuiAlert from '@material-ui/lab/Alert'
import { ExpandMoreSharp } from '@material-ui/icons/' import { ExpandMoreSharp } from '@material-ui/icons/'
import ConnectToHost from '../../../components/ConnectToHost' import ConnectToHost from '../../../components/ConnectToHost'
import CodeBlockTabs from '../../../components/CodeBlockTabs' import CodeBlockTabs from '../../../components/CodeBlockTabs'
import { debugApiHost } from '../../../constants' import { Context as SettingsContext } from '../../../providers/Settings'
type Props = StatusHookCommon type Props = StatusHookCommon
export default function NodeConnectionCheck({ isLoading, isOk }: Props): ReactElement | null { export default function NodeConnectionCheck({ isOk }: Props): ReactElement | null {
if (isLoading) return null const { setDebugApiUrl, apiDebugUrl } = useContext(SettingsContext)
const changeDebugApiUrl = ( const changeDebugApiUrl = (
<div style={{ display: 'flex', marginTop: '25px', marginBottom: '25px' }}> <div style={{ display: 'flex', marginTop: '25px', marginBottom: '25px' }}>
<span style={{ marginRight: '15px' }}> <span style={{ marginRight: '15px' }}>
Debug API (<Typography variant="button">{debugApiHost}</Typography>) Debug API (<Typography variant="button">{apiDebugUrl}</Typography>)
</span> </span>
<ConnectToHost hostName={'debug_api_host'} defaultHost={debugApiHost} /> <ConnectToHost
setHost={(host: string) => {
console.log(host) // eslint-disable-line
setDebugApiUrl(host)
}}
defaultHost={apiDebugUrl}
/>
</div> </div>
) )
@@ -31,7 +37,7 @@ export default function NodeConnectionCheck({ isLoading, isOk }: Props): ReactEl
<div> <div>
<Typography component="div" variant="body2" gutterBottom style={{ margin: '15px' }}> <Typography component="div" variant="body2" gutterBottom style={{ margin: '15px' }}>
We cannot connect to your nodes debug API at <Typography variant="button">{debugApiHost}</Typography>. Please We cannot connect to your nodes debug API at <Typography variant="button">{apiDebugUrl}</Typography>. Please
check the following to troubleshoot your issue. check the following to troubleshoot your issue.
<Accordion style={{ marginTop: '20px' }}> <Accordion style={{ marginTop: '20px' }}>
<AccordionSummary expandIcon={<ExpandMoreSharp />} aria-controls="panel1a-content" id="panel1a-header"> <AccordionSummary expandIcon={<ExpandMoreSharp />} aria-controls="panel1a-content" id="panel1a-header">
@@ -4,37 +4,31 @@ import EthereumAddress from '../../../components/EthereumAddress'
type Props = StatusEthereumConnectionHook type Props = StatusEthereumConnectionHook
export default function EthereumConnectionCheck({ isLoading, isOk, nodeAddresses }: Props): ReactElement | null { export default function EthereumConnectionCheck({ isOk, nodeAddresses }: Props): ReactElement | null {
if (isLoading) return null
if (isOk) { if (isOk) {
return ( return (
<div> <div>
<Typography variant="subtitle1" gutterBottom> <Typography variant="subtitle1" gutterBottom>
Node Address Node Address
</Typography> </Typography>
<EthereumAddress address={nodeAddresses?.ethereum} network={'goerli'} /> <EthereumAddress address={nodeAddresses?.ethereum} />
</div> </div>
) )
} }
return ( return (
<p> <p>
Your Bee node must have access to the Ethereum blockchain, so that it can interact and deploy your chequebook Your Bee node must have access to the xDai blockchain, so that it can interact and deploy your chequebook
contract. You can run{' '} contract. You can run{' '}
<a href="https://github.com/goerli/testnet" rel="noreferrer" target="_blank"> <a href="https://www.xdaichain.com/" rel="noreferrer" target="_blank">
your own Goerli node your own xDai node
</a> </a>
, or use a provider such as{' '} , or use a provider instead - we recommend{' '}
<a href="https://rpc.slock.it/goerli" rel="noreferrer" target="_blank"> <a href="https://getblock.io/" rel="noreferrer" target="_blank">
rpc.slock.it/goerli Getblock
</a>{' '}
or{' '}
<a href="https://infura.io/" rel="noreferrer" target="_blank">
Infura
</a> </a>
. By default, Bee expects a local Goerli node at http://localhost:8545. To use a provider instead, simply change . By default, Bee expects a local node at http://localhost:8545. To use a provider instead, simply change the{' '}
your <strong>--swap-endpoint</strong> in your configuration file. <strong>swap-endpoint</strong> in your configuration file.
</p> </p>
) )
} }
@@ -1,28 +1,28 @@
import React, { ReactElement } from 'react' import { ReactElement, useContext } from 'react'
import { Typography, Accordion, AccordionSummary, AccordionDetails } from '@material-ui/core/' import { Typography, Accordion, AccordionSummary, AccordionDetails } from '@material-ui/core/'
import { ExpandMoreSharp } from '@material-ui/icons/' import { ExpandMoreSharp } from '@material-ui/icons/'
import ConnectToHost from '../../../components/ConnectToHost' import ConnectToHost from '../../../components/ConnectToHost'
import CodeBlockTabs from '../../../components/CodeBlockTabs' import CodeBlockTabs from '../../../components/CodeBlockTabs'
import { apiHost } from '../../../constants' import { Context as SettingsContext } from '../../../providers/Settings'
type Props = StatusHookCommon type Props = StatusHookCommon
export default function NodeConnectionCheck({ isLoading, isOk }: Props): ReactElement | null { export default function NodeConnectionCheck({ isOk }: Props): ReactElement | null {
if (isLoading) return null const { setApiUrl, apiUrl } = useContext(SettingsContext)
return ( return (
<div> <div>
<div style={{ display: 'flex', marginBottom: '25px' }}> <div style={{ display: 'flex', marginBottom: '25px' }}>
<span style={{ marginRight: '15px' }}> <span style={{ marginRight: '15px' }}>
Node API (<Typography variant="button">{apiHost}</Typography>) Node API (<Typography variant="button">{apiUrl}</Typography>)
</span> </span>
<ConnectToHost hostName="api_host" defaultHost={apiHost} /> <ConnectToHost setHost={setApiUrl} defaultHost={apiUrl} />
</div> </div>
<div> <div>
{!isOk && ( {!isOk && (
<Typography component="div" variant="body2" gutterBottom style={{ margin: '15px' }}> <Typography component="div" variant="body2" gutterBottom style={{ margin: '15px' }}>
We cannot connect to your nodes API at <Typography variant="button">{apiHost}</Typography>. Please check the We cannot connect to your nodes API at <Typography variant="button">{apiUrl}</Typography>. Please check the
following to troubleshoot your issue. following to troubleshoot your issue.
<Accordion style={{ marginTop: '20px' }}> <Accordion style={{ marginTop: '20px' }}>
<AccordionSummary expandIcon={<ExpandMoreSharp />} aria-controls="panel1a-content" id="panel1a-header"> <AccordionSummary expandIcon={<ExpandMoreSharp />} aria-controls="panel1a-content" id="panel1a-header">
@@ -3,9 +3,7 @@ import { Typography } from '@material-ui/core/'
type Props = StatusTopologyHook type Props = StatusTopologyHook
export default function PeerConnection({ isLoading, isOk, topology }: Props): ReactElement | null { export default function PeerConnection({ isOk, topology }: Props): ReactElement | null {
if (isLoading) return null
const peers = ( const peers = (
<div style={{ display: 'flex', marginTop: '15px' }}> <div style={{ display: 'flex', marginTop: '15px' }}>
<div style={{ marginRight: '30px' }}> <div style={{ marginRight: '30px' }}>
+1 -9
View File
@@ -4,15 +4,7 @@ import CodeBlockTabs from '../../../components/CodeBlockTabs'
type Props = StatusNodeVersionHook type Props = StatusNodeVersionHook
export default function VersionCheck({ export default function VersionCheck({ isOk, userVersion, latestVersion, latestUrl }: Props): ReactElement | null {
isLoading,
isOk,
userVersion,
latestVersion,
latestUrl,
}: Props): ReactElement | null {
if (isLoading) return null
const version = ( const version = (
<div style={{ display: 'flex' }}> <div style={{ display: 'flex' }}>
<div style={{ marginRight: '30px' }}> <div style={{ marginRight: '30px' }}>
-129
View File
@@ -1,129 +0,0 @@
import { ReactElement, useState } from 'react'
import { Link } from 'react-router-dom'
import { createStyles, makeStyles } from '@material-ui/core/styles'
import { Card, CardContent, Typography, Chip, Button } from '@material-ui/core/'
import { CheckCircle, Error, ArrowRight, ArrowDropUp } from '@material-ui/icons/'
import { NodeAddresses, Topology } from '@ethersphere/bee-js'
const useStyles = makeStyles(() =>
createStyles({
root: {
display: 'flex',
flex: '1 1 auto',
flexDirection: 'column',
},
status: {
color: '#2145a0',
backgroundColor: '#e1effe',
},
}),
)
interface Props {
nodeAddresses: NodeAddresses | null
nodeTopology: Topology | null
userBeeVersion?: string
isLatestBeeVersion: boolean
isOk: boolean
latestUrl: string
}
function StatusCard({
userBeeVersion,
nodeAddresses,
nodeTopology,
isOk,
isLatestBeeVersion,
latestUrl,
}: Props): ReactElement | null {
const classes = useStyles()
const [underlayAddressesVisible, setUnderlayAddresessVisible] = useState<boolean>(false)
return (
<Card>
<CardContent className={classes.root}>
<Typography component="h5" variant="h5" style={{ display: 'flex', justifyContent: 'space-between' }}>
{isOk && (
<div>
<CheckCircle style={{ color: '#32c48d', marginRight: '7px' }} />
<span>Your Bee node is running as expected</span>
</div>
)}
{!isOk && (
<div>
<Error style={{ color: '#c9201f', marginRight: '7px' }} />
<span>Could not connect to Bee Node</span>
</div>
)}
</Typography>
{isOk && (
<>
<div style={{ marginBottom: '20px' }}>
<span style={{ marginRight: '20px' }}>Discovered Nodes: {nodeTopology?.population}</span>
<span style={{ marginRight: '20px' }}>
<span>Connected Peers: </span>
<Link to="/peers/">{nodeTopology?.connected}</Link>
</span>
</div>
<div>
<Typography component="div" variant="subtitle2" gutterBottom>
<span>AGENT: </span>
<a href="https://github.com/ethersphere/bee" rel="noreferrer" target="_blank">
Bee
</a>{' '}
<span>{userBeeVersion || '-'}</span>
{isLatestBeeVersion ? (
<Chip
style={{ marginLeft: '7px', color: '#2145a0' }}
size="small"
label="latest"
className={classes.status}
/>
) : (
<Button size="small" variant="outlined" href={latestUrl}>
update
</Button>
)}
</Typography>
<Typography component="div" variant="subtitle2" gutterBottom>
<span>PUBLIC KEY: </span>
<span>{nodeAddresses?.publicKey ? nodeAddresses.publicKey : '-'}</span>
</Typography>
<Typography component="div" variant="subtitle2" gutterBottom>
<span>PSS PUBLIC KEY: </span>
<span>{nodeAddresses?.pssPublicKey ? nodeAddresses.pssPublicKey : '-'}</span>
</Typography>
<Typography component="div" variant="subtitle2" gutterBottom>
<span>OVERLAY ADDRESS (PEER ID): </span>
<span>{nodeAddresses?.overlay ? nodeAddresses.overlay : '-'}</span>
</Typography>
<Typography component="div" variant="subtitle2" gutterBottom>
<Typography component="div" onClick={() => setUnderlayAddresessVisible(!underlayAddressesVisible)}>
<Button color="primary" style={{ padding: 0, marginTop: '6px' }}>
{underlayAddressesVisible ? (
<ArrowDropUp style={{ fontSize: '12px' }} />
) : (
<ArrowRight style={{ fontSize: '12px' }} />
)}
<span>Underlay Addresses</span>
</Button>
</Typography>
{underlayAddressesVisible && (
<div>
{nodeAddresses?.underlay.map(item => (
<li key={item}>{item}</li>
))}
</div>
)}
</Typography>
</div>
</>
)}
</CardContent>
</Card>
)
}
export default StatusCard
+132 -56
View File
@@ -1,76 +1,152 @@
import { ReactElement } from 'react' import { ReactElement, useState, useContext } from 'react'
import { Container, CircularProgress } from '@material-ui/core'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import { Typography, Paper, Button, Step, StepLabel, StepContent, Stepper, StepButton } from '@material-ui/core/'
import { CheckCircle, Error, ExpandLessSharp, ExpandMoreSharp, Autorenew } from '@material-ui/icons/'
import NodeSetupWorkflow from './NodeSetupWorkflow' import DebugConnectionCheck from './SetupSteps/DebugConnectionCheck'
import StatusCard from './StatusCard' import NodeConnectionCheck from './SetupSteps/NodeConnectionCheck'
import EthereumAddressCard from '../../components/EthereumAddressCard' import VersionCheck from './SetupSteps/VersionCheck'
import { import EthereumConnectionCheck from './SetupSteps/EthereumConnectionCheck'
useStatusEthereumConnection, import ChequebookDeployFund from './SetupSteps/ChequebookDeployFund'
useStatusNodeVersion, import PeerConnection from './SetupSteps/PeerConnection'
useStatusDebugConnection, import { Context } from '../../providers/Bee'
useStatusConnection,
useStatusTopology,
useStatusChequebook,
} from '../../hooks/status'
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
createStyles({ createStyles({
root: { root: {
padding: theme.spacing(2),
width: '100%', width: '100%',
display: 'grid', },
rowGap: theme.spacing(3), button: {
marginTop: theme.spacing(1),
marginRight: theme.spacing(1),
},
actionsContainer: {
margin: theme.spacing(2),
}, },
}), }),
) )
export default function Status(): ReactElement { interface Step {
label: string
isOk: boolean
component: ReactElement
}
export default function NodeSetupWorkflow(): ReactElement {
const classes = useStyles() const classes = useStyles()
const [activeStep, setActiveStep] = useState(-1)
const nodeVersion = useStatusNodeVersion() const {
const ethereumConnection = useStatusEthereumConnection() status,
const debugApiConnection = useStatusDebugConnection() isLoading,
const apiConnection = useStatusConnection() latestUserVersion,
const topology = useStatusTopology() latestPublishedVersion,
const chequebook = useStatusChequebook() isLatestBeeVersion,
latestBeeVersionUrl,
topology,
nodeAddresses,
chequebookAddress,
} = useContext(Context)
const checks = [nodeVersion, ethereumConnection, debugApiConnection, apiConnection, topology, chequebook] const steps: Step[] = [
{
label: 'Connected to Node DebugAPI',
isOk: status.debugApiConnection,
component: <DebugConnectionCheck isOk={status.debugApiConnection} />,
},
{
label: 'Running latest Bee version',
isOk: status.version,
component: (
<VersionCheck
isOk={status.version}
isLatestBeeVersion={isLatestBeeVersion}
userVersion={latestUserVersion}
latestVersion={latestPublishedVersion}
latestUrl={latestBeeVersionUrl}
/>
),
},
{
label: 'Connected to xDai Blockchain',
isOk: status.blockchainConnection,
component: <EthereumConnectionCheck isOk={status.blockchainConnection} nodeAddresses={nodeAddresses} />,
},
{
label: 'Deployed and Funded Chequebook',
isOk: status.chequebook,
component: (
<ChequebookDeployFund chequebookAddress={chequebookAddress?.chequebookAddress} isOk={status.chequebook} />
),
},
{
label: 'Connected to Node API',
isOk: status.apiConnection,
component: <NodeConnectionCheck isOk={status.apiConnection} />,
},
{
label: 'Connected to Peers',
isOk: status.topology,
component: <PeerConnection isOk={status.topology} topology={topology} />,
},
]
// If any check data are still loading const handleNext = () => {
if (!checks.every(c => !c.isLoading)) { setActiveStep(prevActiveStep => prevActiveStep + 1)
return ( }
<Container style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress /> const handleBack = () => {
</Container> setActiveStep(prevActiveStep => prevActiveStep - 1)
)
} }
return ( return (
<div className={classes.root}> <Paper className={classes.root}>
<StatusCard <Typography variant="h5" gutterBottom>
userBeeVersion={nodeVersion.userVersion} Node Setup
isLatestBeeVersion={nodeVersion.isLatestBeeVersion} </Typography>
isOk={checks.every(c => c.isOk)} <Stepper nonLinear activeStep={activeStep} orientation="vertical">
nodeTopology={topology.topology} {steps.map(({ label, isOk, component }, index) => (
latestUrl={nodeVersion.latestUrl} <Step key={label}>
nodeAddresses={ethereumConnection.nodeAddresses} <StepLabel
/> onClick={() => setActiveStep(index === activeStep ? steps.length : index)}
{ethereumConnection.nodeAddresses && chequebook.chequebookAddress && ( StepIconComponent={() => {
<EthereumAddressCard if (isLoading) return <Autorenew style={{ height: '25px', cursor: 'pointer' }} />
nodeAddresses={ethereumConnection.nodeAddresses}
isLoadingNodeAddresses={ethereumConnection.isLoading} if (isOk) return <CheckCircle style={{ color: '#32c48d', height: '25px', cursor: 'pointer' }} />
chequebookAddress={chequebook.chequebookAddress}
isLoadingChequebookAddress={chequebook.isLoading} return <Error style={{ color: '#c9201f', height: '25px', cursor: 'pointer' }} />
/> }}
)} >
<NodeSetupWorkflow <StepButton
nodeVersion={nodeVersion} disabled={isLoading}
ethereumConnection={ethereumConnection} onClick={() => setActiveStep(index === activeStep ? steps.length : index)}
debugApiConnection={debugApiConnection} style={{ justifyContent: 'space-between' }}
apiConnection={apiConnection} >
topology={topology} <div style={{ display: 'flex' }}>
chequebook={chequebook} <div style={{ marginTop: '5px' }}>{label}</div>
/> <div style={{ marginLeft: '12px' }}>
</div> {index === activeStep ? <ExpandLessSharp /> : <ExpandMoreSharp />}
</div>
</div>
</StepButton>
</StepLabel>
<StepContent>
<Typography component="div">{component}</Typography>
<div className={classes.actionsContainer}>
<div>
<Button disabled={activeStep === 0} onClick={handleBack} className={classes.button}>
Back
</Button>
<Button variant="contained" color="primary" onClick={handleNext} className={classes.button}>
{index < steps.length - 1 ? 'Next' : 'Finish'}
</Button>
</div>
</div>
</StepContent>
</Step>
))}
</Stepper>
</Paper>
) )
} }
+351
View File
@@ -0,0 +1,351 @@
import type { ChequebookBalance, Balance, Settlements } from '../types'
import { createContext, ReactChild, ReactElement, useEffect, useState, useContext } from 'react'
import { Token } from '../models/Token'
import semver from 'semver'
import { engines } from '../../package.json'
import { Context as SettingsContext } from './Settings'
import type {
NodeAddresses,
ChequebookAddressResponse,
LastChequesResponse,
Health,
Peer,
Topology,
} from '@ethersphere/bee-js'
import { useLatestBeeRelease } from '../hooks/apiHooks'
interface Status {
all: boolean
version: boolean
blockchainConnection: boolean
debugApiConnection: boolean
apiConnection: boolean
topology: boolean
chequebook: boolean
}
interface ContextInterface {
status: Status
latestPublishedVersion?: string
latestUserVersion?: string
latestUserVersionExact?: string
isLatestBeeVersion: boolean
latestBeeVersionUrl: string
error: Error | null
apiHealth: boolean
debugApiHealth: Health | null
nodeAddresses: NodeAddresses | null
topology: Topology | null
chequebookAddress: ChequebookAddressResponse | null
peers: Peer[] | null
chequebookBalance: ChequebookBalance | null
peerBalances: Balance[] | null
peerCheques: LastChequesResponse | null
settlements: Settlements | null
latestBeeRelease: LatestBeeRelease | null
isLoading: boolean
isRefreshing: boolean
lastUpdate: number | null
start: (frequency?: number) => void
stop: () => void
refresh: () => Promise<void>
}
const initialValues: ContextInterface = {
status: {
all: false,
version: false,
blockchainConnection: false,
debugApiConnection: false,
apiConnection: false,
topology: false,
chequebook: false,
},
latestPublishedVersion: undefined,
latestUserVersion: undefined,
latestUserVersionExact: undefined,
isLatestBeeVersion: false,
latestBeeVersionUrl: 'https://github.com/ethersphere/bee/releases/latest',
error: null,
apiHealth: false,
debugApiHealth: null,
nodeAddresses: null,
topology: null,
chequebookAddress: null,
peers: null,
chequebookBalance: null,
peerBalances: null,
peerCheques: null,
settlements: null,
latestBeeRelease: null,
isLoading: true,
isRefreshing: false,
lastUpdate: null,
start: () => {}, // eslint-disable-line
stop: () => {}, // eslint-disable-line
refresh: () => Promise.reject(),
}
export const Context = createContext<ContextInterface>(initialValues)
export const Consumer = Context.Consumer
interface Props {
children: ReactChild
}
function getStatus(
debugApiHealth: Health | null,
nodeAddresses: NodeAddresses | null,
apiHealth: boolean,
topology: Topology | null,
chequebookAddress: ChequebookAddressResponse | null,
chequebookBalance: ChequebookBalance | null,
error: Error | null,
): Status {
const status = {
version: Boolean(
debugApiHealth &&
semver.satisfies(debugApiHealth.version, engines.bee, {
includePrerelease: true,
}),
),
blockchainConnection: Boolean(nodeAddresses?.ethereum),
debugApiConnection: Boolean(debugApiHealth?.status === 'ok'),
apiConnection: apiHealth,
topology: Boolean(topology?.connected && topology?.connected > 0),
chequebook:
Boolean(chequebookAddress?.chequebookAddress) &&
chequebookBalance !== null &&
chequebookBalance?.totalBalance.toBigNumber.isGreaterThan(0),
}
return { ...status, all: !error && Object.values(status).every(v => v) }
}
export function Provider({ children }: Props): ReactElement {
const { beeApi, beeDebugApi } = useContext(SettingsContext)
const [apiHealth, setApiHealth] = useState<boolean>(false)
const [debugApiHealth, setDebugApiHealth] = useState<Health | null>(null)
const [nodeAddresses, setNodeAddresses] = useState<NodeAddresses | null>(null)
const [topology, setNodeTopology] = useState<Topology | null>(null)
const [chequebookAddress, setChequebookAddress] = useState<ChequebookAddressResponse | null>(null)
const [peers, setPeers] = useState<Peer[] | null>(null)
const [chequebookBalance, setChequebookBalance] = useState<ChequebookBalance | null>(null)
const [peerBalances, setPeerBalances] = useState<Balance[] | null>(null)
const [peerCheques, setPeerCheques] = useState<LastChequesResponse | null>(null)
const [settlements, setSettlements] = useState<Settlements | null>(null)
const { latestBeeRelease } = useLatestBeeRelease()
const [error, setError] = useState<Error | null>(initialValues.error)
const [isLoading, setIsLoading] = useState<boolean>(initialValues.isLoading)
const [isRefreshing, setIsRefreshing] = useState<boolean>(initialValues.isRefreshing)
const [lastUpdate, setLastUpdate] = useState<number | null>(initialValues.lastUpdate)
const [frequency, setFrequency] = useState<number | null>(30000)
const latestPublishedVersion = semver.coerce(latestBeeRelease?.name)?.version
const latestUserVersion = semver.coerce(debugApiHealth?.version)?.version
const latestUserVersionExact = debugApiHealth?.version
useEffect(() => {
setIsLoading(true)
setApiHealth(false)
refresh()
}, [beeApi])
useEffect(() => {
setIsLoading(true)
setDebugApiHealth(null)
setNodeAddresses(null)
setNodeTopology(null)
setPeers(null)
setChequebookAddress(null)
setChequebookBalance(null)
setPeerBalances(null)
setPeerCheques(null)
setSettlements(null)
refresh()
}, [beeDebugApi])
const refresh = async () => {
// Don't want to refresh when already refreshing
if (isRefreshing) return
// Not a valid bee api
if (!beeApi || !beeDebugApi) {
setIsLoading(false)
return
}
try {
setIsRefreshing(true)
setError(null)
// Wrap the chequebook balance call to return BZZ values as Token object
const chequeBalanceWrapper = async () => {
const { totalBalance, availableBalance } = await beeDebugApi.getChequebookBalance()
return {
totalBalance: new Token(totalBalance),
availableBalance: new Token(availableBalance),
}
}
// Wrap the balances call to return BZZ values as Token object
const peerBalanceWrapper = async () => {
const { balances } = await beeDebugApi.getAllBalances()
return balances.map(({ peer, balance }) => ({ peer, balance: new Token(balance) }))
}
// Wrap the settlements call to return BZZ values as Token object
const settlementsWrapper = async () => {
const { totalReceived, settlements, totalSent } = await beeDebugApi.getAllSettlements()
return {
totalReceived: new Token(totalReceived),
totalSent: new Token(totalSent),
settlements: settlements.map(({ peer, received, sent }) => ({
peer,
received: new Token(received),
sent: new Token(sent),
})),
}
}
const promises = [
// API health
beeApi
.isConnected()
.then(setApiHealth)
.catch(() => setApiHealth(false)),
// Debug API health
beeDebugApi
.getHealth()
.then(setDebugApiHealth)
.catch(() => setDebugApiHealth(null)),
// Node Addresses
beeDebugApi
.getNodeAddresses()
.then(setNodeAddresses)
.catch(() => setNodeAddresses(null)),
// Network Topology
beeDebugApi
.getTopology()
.then(setNodeTopology)
.catch(() => setNodeTopology(null)),
// Peers
beeDebugApi
.getPeers()
.then(setPeers)
.catch(() => setPeers(null)),
// Chequebook address
beeDebugApi
.getChequebookAddress()
.then(setChequebookAddress)
.catch(() => setChequebookAddress(null)),
// Cheques
beeDebugApi
.getLastCheques()
.then(setPeerCheques)
.catch(() => setPeerCheques(null)),
// Chequebook balance
chequeBalanceWrapper()
.then(setChequebookBalance)
.catch(() => setChequebookBalance(null)),
// Peer balances
peerBalanceWrapper()
.then(setPeerBalances)
.catch(() => setPeerBalances(null)),
// Settlements
settlementsWrapper()
.then(setSettlements)
.catch(() => setSettlements(null)),
]
await Promise.allSettled(promises)
} catch (e) {
setError(e)
} finally {
setIsLoading(false)
setIsRefreshing(false)
setLastUpdate(Date.now())
}
}
const start = (freq = 30000) => setFrequency(freq)
const stop = () => setFrequency(null)
// Start the update loop
useEffect(() => {
refresh()
// Start autorefresh only if the frequency is set
if (frequency) {
const interval = setInterval(refresh, frequency)
return () => clearInterval(interval)
}
}, [frequency, beeDebugApi, beeApi])
return (
<Context.Provider
value={{
status: getStatus(
debugApiHealth,
nodeAddresses,
apiHealth,
topology,
chequebookAddress,
chequebookBalance,
error,
),
latestUserVersion,
latestUserVersionExact,
latestPublishedVersion,
isLatestBeeVersion: Boolean(
latestPublishedVersion &&
latestUserVersion &&
semver.satisfies(latestPublishedVersion, latestUserVersion, {
includePrerelease: true,
}),
),
latestBeeVersionUrl: latestBeeRelease?.html_url || 'https://github.com/ethersphere/bee/releases/latest',
error,
apiHealth,
debugApiHealth,
nodeAddresses,
topology,
chequebookAddress,
peers,
chequebookBalance,
peerBalances,
peerCheques,
settlements,
latestBeeRelease,
isLoading,
isRefreshing,
lastUpdate,
start,
stop,
refresh,
}}
>
{children}
</Context.Provider>
)
}
+69
View File
@@ -0,0 +1,69 @@
import { createContext, ReactChild, ReactElement, useState, useEffect } from 'react'
// These need to be numeric values as they are used as indexes in the TabsContainer
export enum Platforms {
macOS = 0,
Linux,
Windows,
iOS,
Android,
}
export enum SupportedPlatforms {
macOS = Platforms.macOS,
Linux = Platforms.Linux,
}
interface ContextInterface {
platform: SupportedPlatforms
setPlatform: (platform: SupportedPlatforms) => void
}
const initialValues: ContextInterface = {
platform: SupportedPlatforms.macOS,
setPlatform: () => {}, // eslint-disable-line
}
export const Context = createContext<ContextInterface>(initialValues)
export const Consumer = Context.Consumer
interface Props {
children: ReactChild
}
function isSupportedPlatform(platform: unknown): platform is SupportedPlatforms {
return Object.keys(SupportedPlatforms).includes(platform as string)
}
function getOS(): Platforms | null {
const userAgent = window.navigator.userAgent
const platform = window.navigator.platform
const macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K']
const windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE']
const iosPlatforms = ['iPhone', 'iPad', 'iPod']
if (macosPlatforms.includes(platform)) return Platforms.macOS
if (iosPlatforms.includes(platform)) return Platforms.iOS
if (windowsPlatforms.includes(platform)) return Platforms.Windows
if (/Android/.test(userAgent)) return Platforms.Android
if (/Linux/.test(platform)) return Platforms.Linux
return null
}
export function Provider({ children }: Props): ReactElement {
const [platform, setPlatform] = useState<SupportedPlatforms>(SupportedPlatforms.Linux)
// This is in useEffect as it really just needs to run once and not on each re-render
useEffect(() => {
const os = getOS()
setPlatform(isSupportedPlatform(os) ? os : SupportedPlatforms.Linux)
}, [])
return <Context.Provider value={{ platform, setPlatform }}>{children}</Context.Provider>
}
+59
View File
@@ -0,0 +1,59 @@
import { createContext, ReactChild, ReactElement, useState, useEffect } from 'react'
import { Bee, BeeDebug } from '@ethersphere/bee-js'
interface ContextInterface {
apiUrl: string
apiDebugUrl: string
beeApi: Bee | null
beeDebugApi: BeeDebug | null
setApiUrl: (url: string) => void
setDebugApiUrl: (url: string) => void
}
const initialValues: ContextInterface = {
apiUrl: sessionStorage.getItem('api_host') || process.env.REACT_APP_BEE_HOST || 'http://localhost:1633',
apiDebugUrl:
sessionStorage.getItem('debug_api_host') || process.env.REACT_APP_BEE_DEBUG_HOST || 'http://localhost:1635',
beeApi: null,
beeDebugApi: null,
setApiUrl: () => {}, // eslint-disable-line
setDebugApiUrl: () => {}, // eslint-disable-line
}
export const Context = createContext<ContextInterface>(initialValues)
export const Consumer = Context.Consumer
interface Props {
children: ReactChild
}
export function Provider({ children }: Props): ReactElement {
const [apiUrl, setApiUrl] = useState<string>(initialValues.apiUrl)
const [apiDebugUrl, setDebugApiUrl] = useState<string>(initialValues.apiDebugUrl)
const [beeApi, setBeeApi] = useState<Bee | null>(null)
const [beeDebugApi, setBeeDebugApi] = useState<BeeDebug | null>(null)
useEffect(() => {
try {
setBeeApi(new Bee(apiUrl))
sessionStorage.setItem('api_host', apiUrl)
} catch (e) {
setBeeApi(null)
}
}, [apiUrl])
useEffect(() => {
try {
setBeeDebugApi(new BeeDebug(apiDebugUrl))
sessionStorage.setItem('debug_api_host', apiDebugUrl)
} catch (e) {
setBeeDebugApi(null)
}
}, [apiDebugUrl])
return (
<Context.Provider value={{ apiUrl, apiDebugUrl, beeApi, beeDebugApi, setApiUrl, setDebugApiUrl }}>
{children}
</Context.Provider>
)
}
+27 -6
View File
@@ -1,9 +1,14 @@
import { PostageBatch } from '@ethersphere/bee-js' import { PostageBatch } from '@ethersphere/bee-js'
import { createContext, ReactChild, ReactElement, useEffect, useState } from 'react' import { createContext, ReactChild, ReactElement, useEffect, useState, useContext } from 'react'
import { beeApi } from '../services/bee' import { Context as SettingsContext } from './Settings'
export interface EnrichedPostageBatch extends PostageBatch {
usage: number
usageText: string
}
interface ContextInterface { interface ContextInterface {
stamps: PostageBatch[] | null stamps: EnrichedPostageBatch[] | null
error: Error | null error: Error | null
isLoading: boolean isLoading: boolean
lastUpdate: number | null lastUpdate: number | null
@@ -29,8 +34,22 @@ interface Props {
children: ReactChild children: ReactChild
} }
function enrichStamp(postageBatch: PostageBatch): EnrichedPostageBatch {
const { depth, bucketDepth, utilization } = postageBatch
const usage = utilization / Math.pow(2, depth - bucketDepth)
const usageText = `${Math.ceil(usage * 100)}%`
return {
...postageBatch,
usage,
usageText,
}
}
export function Provider({ children }: Props): ReactElement { export function Provider({ children }: Props): ReactElement {
const [stamps, setStamps] = useState<PostageBatch[] | null>(initialValues.stamps) const { beeApi } = useContext(SettingsContext)
const [stamps, setStamps] = useState<EnrichedPostageBatch[] | null>(initialValues.stamps)
const [error, setError] = useState<Error | null>(initialValues.error) const [error, setError] = useState<Error | null>(initialValues.error)
const [isLoading, setIsLoading] = useState<boolean>(initialValues.isLoading) const [isLoading, setIsLoading] = useState<boolean>(initialValues.isLoading)
const [lastUpdate, setLastUpdate] = useState<number | null>(initialValues.lastUpdate) const [lastUpdate, setLastUpdate] = useState<number | null>(initialValues.lastUpdate)
@@ -40,11 +59,13 @@ export function Provider({ children }: Props): ReactElement {
// Don't want to refresh when already refreshing // Don't want to refresh when already refreshing
if (isLoading) return if (isLoading) return
if (!beeApi) return
try { try {
setIsLoading(true) setIsLoading(true)
const stamps = await beeApi.stamps.getPostageStamps() const stamps = await beeApi.getAllPostageBatch()
setStamps(stamps) setStamps(stamps.map(enrichStamp))
setLastUpdate(Date.now()) setLastUpdate(Date.now())
} catch (e) { } catch (e) {
setError(e) setError(e)
-1
View File
@@ -6,7 +6,6 @@ interface LatestBeeRelease {
} }
interface StatusHookCommon { interface StatusHookCommon {
isLoading: boolean
isOk: boolean isOk: boolean
} }
+36
View File
@@ -0,0 +1,36 @@
import type { ReactElement } from 'react'
import { Switch } from 'react-router-dom'
import { Route } from 'react-router-dom'
import Info from './pages/info'
import Status from './pages/status'
import Files from './pages/files'
import Peers from './pages/peers'
import Accounting from './pages/accounting'
import Settings from './pages/settings'
import Stamps from './pages/stamps'
export enum ROUTES {
INFO = '/',
FILES = '/files',
PEERS = '/peers',
ACCOUNTING = '/accounting',
SETTINGS = '/settings',
STAMPS = '/stamps',
STATUS = '/status',
}
const BaseRouter = (): ReactElement => (
<Switch>
<Route exact path={ROUTES.INFO} component={Info} />
<Route exact path={ROUTES.FILES} component={Files} />
<Route exact path={ROUTES.PEERS} component={Peers} />
<Route exact path={ROUTES.ACCOUNTING} component={Accounting} />
<Route exact path={ROUTES.SETTINGS} component={Settings} />
<Route exact path={ROUTES.STAMPS} component={Stamps} />
<Route exact path={ROUTES.STATUS} component={Status} />
</Switch>
)
export default BaseRouter
-23
View File
@@ -1,23 +0,0 @@
import type { JSXElementConstructor, ReactElement } from 'react'
import { Route, RouteComponentProps } from 'react-router-dom'
interface Props {
component: JSXElementConstructor<RouteComponentProps>
layout: JSXElementConstructor<RouteComponentProps>
exact?: boolean
path: string
}
const AppRoute = ({ component: Component, layout: Layout, exact, path }: Props): ReactElement => (
<Route
exact={exact}
path={path}
render={props => (
<Layout {...props}>
<Component {...props} />
</Layout>
)}
/>
)
export default AppRoute
-28
View File
@@ -1,28 +0,0 @@
import type { ReactElement } from 'react'
import { Switch } from 'react-router-dom'
import AppRoute from './AppRoute'
// layouts
import Dashboard from '../layout/Dashboard'
// pages
import Status from '../pages/status'
import Files from '../pages/files'
import Peers from '../pages/peers'
import Accounting from '../pages/accounting'
import Settings from '../pages/settings'
import Stamps from '../pages/stamps'
const BaseRouter = (): ReactElement => (
<Switch>
<AppRoute exact path="/" layout={Dashboard} component={Status} />
<AppRoute exact path="/files/" layout={Dashboard} component={Files} />
<AppRoute exact path="/peers/" layout={Dashboard} component={Peers} />
<AppRoute exact path="/accounting/" layout={Dashboard} component={Accounting} />
<AppRoute exact path="/settings/" layout={Dashboard} component={Settings} />
<AppRoute exact path="/stamps/" layout={Dashboard} component={Stamps} />
</Switch>
)
export default BaseRouter
-109
View File
@@ -1,109 +0,0 @@
import {
Address,
AllSettlements,
BalanceResponse,
Bee,
BeeDebug,
ChequebookAddressResponse,
ChequebookBalanceResponse,
Data,
FileData,
Health,
LastCashoutActionResponse,
LastChequesForPeerResponse,
LastChequesResponse,
NodeAddresses,
Peer,
PingResponse,
PostageBatch,
PostageBatchOptions,
Reference,
Topology,
} from '@ethersphere/bee-js'
import { apiHost, debugApiHost } from '../constants'
const beeJSClient = () => new Bee(apiHost)
const beeJSDebugClient = () => new BeeDebug(debugApiHost)
export const beeApi = {
status: {
health(): Promise<boolean> {
return beeJSClient().isConnected()
},
},
files: {
uploadFile(postageBatchId: Address, file: File): Promise<Reference> {
return beeJSClient().uploadFile(postageBatchId, file)
},
downloadFile(hash: string | Reference): Promise<FileData<Data>> {
return beeJSClient().downloadFile(hash)
},
},
stamps: {
getPostageStamps(): Promise<PostageBatch[]> {
return beeJSClient().getAllPostageBatch()
},
buyPostageStamp(amount: bigint, depth: number, options: PostageBatchOptions = {}): Promise<Address> {
return beeJSClient().createPostageBatch(amount, depth, options)
},
},
}
export const beeDebugApi = {
status: {
nodeHealth(): Promise<Health> {
return beeJSDebugClient().getHealth()
},
},
connectivity: {
addresses(): Promise<NodeAddresses> {
return beeJSDebugClient().getNodeAddresses()
},
listPeers(): Promise<Peer[]> {
return beeJSDebugClient().getPeers()
},
topology(): Promise<Topology> {
return beeJSDebugClient().getTopology()
},
ping(peerId: string): Promise<PingResponse> {
return beeJSDebugClient().pingPeer(peerId)
},
},
balance: {
balances(): Promise<BalanceResponse> {
return beeJSDebugClient().getAllBalances()
},
},
chequebook: {
address(): Promise<ChequebookAddressResponse> {
return beeJSDebugClient().getChequebookAddress()
},
balance(): Promise<ChequebookBalanceResponse> {
return beeJSDebugClient().getChequebookBalance()
},
getLastCheques(): Promise<LastChequesResponse> {
return beeJSDebugClient().getLastCheques()
},
peerCashout(peerId: string): Promise<string> {
return beeJSDebugClient().cashoutLastCheque(peerId)
},
getPeerLastCashout(peerId: string): Promise<LastCashoutActionResponse> {
return beeJSDebugClient().getLastCashoutAction(peerId)
},
getPeerLastCheques(peerId: string): Promise<LastChequesForPeerResponse> {
return beeJSDebugClient().getLastChequesForPeer(peerId)
},
withdraw(amount: bigint): Promise<string> {
return beeJSDebugClient().withdrawTokens(amount)
},
deposit(amount: bigint): Promise<string> {
return beeJSDebugClient().depositTokens(amount)
},
},
settlements: {
getSettlements(): Promise<AllSettlements> {
return beeJSDebugClient().getAllSettlements()
},
},
}
+9 -26
View File
@@ -9,6 +9,12 @@ declare module '@material-ui/core/styles/createPalette' {
// Overwriting default components styles // Overwriting default components styles
const componentsOverrides = (theme: Theme) => ({ const componentsOverrides = (theme: Theme) => ({
MuiDrawer: {
paper: {
width: 300,
backgroundColor: '#212121',
},
},
MuiTab: { MuiTab: {
root: { root: {
backgroundColor: 'transparent', backgroundColor: 'transparent',
@@ -55,7 +61,7 @@ const propsOverrides = {
}, },
} }
export const lightTheme = createMuiTheme({ export const theme = createMuiTheme({
palette: { palette: {
type: 'light', type: 'light',
background: { background: {
@@ -75,28 +81,5 @@ export const lightTheme = createMuiTheme({
}, },
}) })
export const darkTheme = createMuiTheme({ theme.overrides = componentsOverrides(theme)
palette: { theme.props = propsOverrides
type: 'dark',
background: {
default: '#0d1117',
paper: '#161b22',
},
primary: {
light: orange.A200,
main: '#dd7700',
dark: orange[800],
},
secondary: {
main: '#1f2937',
},
},
typography: {
fontFamily: ['Work Sans', 'Montserrat', 'Nunito', 'Roboto', '"Helvetica Neue"', 'Arial', 'sans-serif'].join(','),
},
})
darkTheme.overrides = componentsOverrides(darkTheme)
darkTheme.props = propsOverrides
lightTheme.overrides = componentsOverrides(lightTheme)
lightTheme.props = propsOverrides
+23
View File
@@ -0,0 +1,23 @@
import type { Token } from './models/Token'
export interface ChequebookBalance {
totalBalance: Token
availableBalance: Token
}
export interface Balance {
peer: string
balance: Token
}
export interface Settlement {
peer: string
received: Token
sent: Token
}
export interface Settlements {
totalReceived: Token
totalSent: Token
settlements: Settlement[]
}
+1 -1
View File
@@ -53,7 +53,7 @@ describe('utils', () => {
wrongValues.forEach(v => { wrongValues.forEach(v => {
test(`testing ${v}`, () => { test(`testing ${v}`, () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
expect(() => makeBigNumber((v as unknown) as any)).toThrow() expect(() => makeBigNumber(v as unknown as any)).toThrow()
}) })
}) })
}) })
+74
View File
@@ -32,3 +32,77 @@ export function makeBigNumber(value: BigNumber | BigInt | number | string): BigN
throw new TypeError(`Not a BigNumber or BigNumber convertible value. Type: ${typeof value} value: ${value}`) throw new TypeError(`Not a BigNumber or BigNumber convertible value. Type: ${typeof value} value: ${value}`)
} }
export type PromiseSettlements<T> = {
fulfilled: PromiseFulfilledResult<T>[]
rejected: PromiseRejectedResult[]
}
export type UnwrappedPromiseSettlements<T> = {
fulfilled: T[]
rejected: string[]
}
export async function sleepMs(ms: number): Promise<void> {
await new Promise<void>(resolve =>
setTimeout(() => {
resolve()
}, ms),
)
}
/**
* Maps the returned results of `Promise.allSettled` to an object
* with `fulfilled` and `rejected` arrays for easy access.
*
* The results still need to be unwrapped to get the fulfilled values or rejection reasons.
*/
export function mapPromiseSettlements<T>(promises: PromiseSettledResult<T>[]): PromiseSettlements<T> {
const fulfilled = promises.filter(promise => promise.status === 'fulfilled') as PromiseFulfilledResult<T>[]
const rejected = promises.filter(promise => promise.status === 'rejected') as PromiseRejectedResult[]
return { fulfilled, rejected }
}
/**
* Maps the returned values of `Promise.allSettled` to an object
* with `fulfilled` and `rejected` arrays for easy access.
*
* For rejected promises, the value is the stringified `reason`,
* or `'Unknown error'` string when it is unavailable.
*/
export function unwrapPromiseSettlements<T>(
promiseSettledResults: PromiseSettledResult<T>[],
): UnwrappedPromiseSettlements<T> {
const values = mapPromiseSettlements(promiseSettledResults)
const fulfilled = values.fulfilled.map(x => x.value)
const rejected = values.rejected.map(x => (x.reason ? String(x.reason) : 'Unknown error'))
return { fulfilled, rejected }
}
/**
* Wraps a `Promise<T>` or async function inside a new `Promise<T>`,
* which retries the original function up to `maxRetries` times,
* waiting `delayMs` milliseconds between failed attempts.
*
* If all attempts fail, then this `Promise<T>` also rejects.
*/
export function makeRetriablePromise<T>(fn: () => Promise<T>, maxRetries = 3, delayMs = 1000): Promise<T> {
return new Promise(async (resolve, reject) => {
for (let tries = 0; tries < maxRetries; tries++) {
try {
const results = await fn()
resolve(results)
return
} catch (error) {
if (tries < maxRetries - 1) {
await sleepMs(delayMs)
} else {
reject(error)
}
}
}
})
}
+105
View File
@@ -0,0 +1,105 @@
const OPTIMAL_CONNECTED_PEERS = 200
const OPTIMAL_POPULATION = 100_000
const OPTIMAL_DEPTH = 12
interface Threshold {
minimumValue: number
explanation: string
score: number
}
type Thresholds = {
connectedPeers: Threshold[]
population: Threshold[]
depth: Threshold[]
}
type ThresholdValue = {
score: number
maximumScore: number
explanation: string
}
export type ThresholdValues = {
connectedPeers: ThresholdValue
population: ThresholdValue
depth: ThresholdValue
}
const GENERIC_ERROR = 'There may be issues with your Bee node or connection.'
const THRESHOLDS: Thresholds = {
connectedPeers: [
{
minimumValue: OPTIMAL_CONNECTED_PEERS,
explanation: `Perfect! ${OPTIMAL_CONNECTED_PEERS} or more connected peers indicate a healthy topology.`,
score: 2,
},
{
minimumValue: 1,
explanation: `Your Bee node is connected to peers, but this number should ideally be above ${OPTIMAL_CONNECTED_PEERS}. If you have only started your Bee node, this number may increase quickly.`,
score: 1,
},
{
minimumValue: 0,
explanation: 'Your Bee node has not connected to any peers. ' + GENERIC_ERROR,
score: 0,
},
],
population: [
{
minimumValue: OPTIMAL_POPULATION,
explanation:
'Perfect! Your Bee node seems to have a realistic value for the network size, which means everything is working well on your end.',
score: 2,
},
{
minimumValue: 1,
explanation: `Population is usually above ${OPTIMAL_POPULATION.toLocaleString()}. If the number does not increase within a few hours, there may be issues with your Bee node.`,
score: 1,
},
{
minimumValue: 0,
explanation: 'Your Bee node has no information on the network population. ' + GENERIC_ERROR,
score: 0,
},
],
depth: [
{
minimumValue: OPTIMAL_DEPTH,
explanation: 'Perfect! Your Bee node has the highest available depth.',
score: 2,
},
{
minimumValue: 1,
explanation: `Your Bee node is supposed to reach a depth of ${OPTIMAL_DEPTH} eventually. Stagnation or decrease in this number may indicate problems with your Bee node.`,
score: 1,
},
{
minimumValue: 0,
explanation: 'Your Bee node has not started building its topology yet. ' + GENERIC_ERROR,
score: 0,
},
],
}
export function pickThreshold(key: keyof Thresholds, value: number): ThresholdValue {
const thresholds = THRESHOLDS[key]
const maximumScore = thresholds[0].score
for (const item of thresholds) {
if (value >= item.minimumValue) {
return {
score: item.score,
maximumScore,
explanation: item.explanation,
}
}
}
const last = thresholds[thresholds.length - 1]
return {
score: last.score,
maximumScore,
explanation: last.explanation,
}
}