Compare commits

...

27 Commits

Author SHA1 Message Date
bee-worker 9b5b2973cb chore: release 0.14.0 (#319) 2022-04-14 16:03:11 +05:00
bee-worker 36da804ca4 docs: update supported bee (#337) 2022-04-14 15:42:02 +05:00
Vojtech Simetka 8f51aa9e89 ci: migrate to swarm-actions for PR previews (#310) 2022-04-14 15:31:09 +05:00
Vojtech Simetka 0a31a04148 chore(deps): update bee-js to 3.3.4 (#336) 2022-04-14 15:30:46 +05:00
Vojtech Simetka eb9e309c8b feat: add hook that detects if the bee-dashboard is run within bee-desktop (#334)
* feat: add hook that detects if the bee-dashboard is run withing bee-desktop

* chore: make the URL configurable

* feat: remove error and instead return false

* test: add testing with mockserver
2022-04-13 19:00:37 +05:00
Vojtech Simetka 5d0fbf705d feat: optional status checks (e.g. connected peers > 0 or funded chequebook) (#331)
* feat: make some check optional (e.g. connected peers > 0 or funded chequebook)

* fix: alter setup step text to better describe what needs to be done

* refactor: rename isOk from boolean value to checkState enum

* fix: add checking for any error
2022-04-13 18:09:30 +05:00
Ivan Vandot cd332c4dfd chore: replace REPO_GHA_PAT with GHA_PAT_BASIC (#330) 2022-04-08 22:23:50 +02:00
Cafe137 224fe4ce25 refactor: add missing props to generic components (#325)
* refactor: add missing props to generic components

* fix: remove undefined from variant

Co-authored-by: Vojtech Simetka <vojtech@simetka.cz>

Co-authored-by: Vojtech Simetka <vojtech@simetka.cz>
2022-04-05 22:54:16 +02:00
Vojtech Simetka 4736e82da5 ci: enable depcheck (#320) 2022-04-01 11:22:27 +02:00
Vojtech Simetka 8baecb783f feat: detect bee mode and enable/disable status checks accordingly (#318) 2022-03-29 15:37:40 +02:00
bee-worker bf24d61584 docs: update supported bee (#316) 2022-03-27 23:11:31 +02:00
Vojtech Simetka 01351a0380 chore(deps): update to bee-js 3.3.3 (#315)
* chore(deps): update to bee-js 3.3.3-pre.0

* chore: update to bee-js 3.3.3
2022-03-27 22:40:49 +02:00
Vojtech Simetka d0b3f1abee fix: postage stamp price and TTL calculation (#305)
* fix: postage stamp price and TTL calculation

* chore: removed logs and fixed linter issues
2022-03-10 17:49:09 +01:00
bee-worker d9e7560117 chore: release 0.13.0 (#289) 2022-01-31 13:04:20 +01:00
Cafe137 3a30ee59d4 ci: add swarm-cli extra flags (#299)
* ci: add swarm-cli extra flags

* ci: rename SWARM_CLI_EXTRA_FLAGS to GATEWAY_AUTHORIZATION_HEADER

* ci: change bee-url
2022-01-28 14:02:15 +01:00
Cafe137 7880c802ae fix: do not print size and name when meta is unknown (#297)
* fix: do not print zero when size is unknown

* fix: do not print name if it is the same as the hash

* feat: shorten asset name
2022-01-27 16:22:27 +01:00
Vojtech Simetka f4013142af feat: add metadata and preview (#292)
* chore: upload flow uses metadata object and has preview

* chore: remove SwarmFile

* feat: upload metadata and file preview

* feat: add metadata and preview on download

* fix: package the meta and preview files

* fix: upload websites that are inside a folder (#296)

* fix: upload websites that are inside a folder

* docs: few comments to clarify what is going on

* refactor: decrease local variables and fix state order to detect websites properly

Co-authored-by: Cafe137 <aron@aronsoos.com>
2022-01-26 18:29:09 +01:00
Cafe137 57bff96c99 style: make select and text input style consistent (#295) 2022-01-25 18:03:40 +01:00
Cafe137 a406e0fc01 fix: clean up spinner and disabled state on download page (#294) 2022-01-25 18:02:57 +01:00
Cafe137 1310deb17a fix: disable feeds page when disconnected (#293) 2022-01-25 18:02:20 +01:00
Vojtech Simetka d8787476ac fix: correct folder name when uploading multiple files or mix of files & directories (#291) 2022-01-24 18:03:06 +01:00
Cafe137 bc82e67561 fix: get current price from chain state (#286)
* fix: get current price from chain state

* refactor: do not allow optional currentPrice
2022-01-20 15:49:41 +01:00
Cafe137 63e79ae2aa ci: enable beeload action (#290)
* ci: enable beeload action

* ci: remove testnet

* ci: add continue-on-error to testnet beeload-action
2022-01-20 15:49:30 +01:00
Cafe137 48ce9ba659 refactor: remove google fonts dependency (#285)
* refactor: remove google fonts dependency

* fix: change montserrat to work sans

* refactor: omit unicode ranges
2022-01-20 14:51:46 +01:00
Cafe137 9ee1c9107b feat: add hash based routing (#287) 2022-01-20 14:51:27 +01:00
Vojtech Simetka a90b4c439b chore(deps): update react router from v5 to v6 (#280)
* chore(deps): update react router from v5 to v6

* fix: correctly choose navigate target if there is no history
2022-01-17 14:47:26 +01:00
Vojtech Simetka 2187b9001c refactor: settings to use less useEffects (and therefore less re-renders) (#277) 2022-01-17 12:31:07 +01:00
59 changed files with 1550 additions and 901 deletions
+21 -1
View File
@@ -52,6 +52,9 @@ jobs:
env: env:
CI: true CI: true
- name: Dependency check
run: npm run depcheck
- name: Types check - name: Types check
run: npm run check:types run: npm run check:types
@@ -62,10 +65,27 @@ jobs:
uses: ethersphere/update-supported-bee-action@v1 uses: ethersphere/update-supported-bee-action@v1
if: github.ref == 'refs/heads/master' if: github.ref == 'refs/heads/master'
with: with:
token: ${{ secrets.REPO_GHA_PAT }} token: ${{ secrets.GHA_PAT_BASIC }}
- name: Build - name: Build
run: npm run build run: npm run build
- name: Build Component - name: Build Component
run: npm run build:component run: npm run build:component
- name: Create preview
uses: ethersphere/swarm-actions/pr-preview@v0
with:
bee-url: https://unlimited.gateway.ethswarm.org
token: ${{ secrets.GHA_PAT_BASIC }}
error-document: index.html
headers: "${{ secrets.GATEWAY_AUTHORIZATION_HEADER }}"
- name: Upload to testnet
uses: ethersphere/swarm-actions/upload-dir@v0
continue-on-error: true
with:
index-document: index.html
error-document: index.html
dir: ./build
bee-url: https://api.gateway.testnet.ethswarm.org
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
- uses: GoogleCloudPlatform/release-please-action@v2 - uses: GoogleCloudPlatform/release-please-action@v2
id: release id: release
with: with:
token: ${{ secrets.REPO_GHA_PAT }} token: ${{ secrets.GHA_PAT_BASIC }}
release-type: node release-type: node
package-name: bee-dashboard package-name: bee-dashboard
bump-minor-pre-major: true bump-minor-pre-major: true
+31
View File
@@ -1,5 +1,36 @@
# Changelog # Changelog
## [0.14.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.13.0...v0.14.0) (2022-04-14)
### Features
* add hook that detects if the bee-dashboard is run within bee-desktop ([#334](https://www.github.com/ethersphere/bee-dashboard/issues/334)) ([eb9e309](https://www.github.com/ethersphere/bee-dashboard/commit/eb9e309c8bc0327d137f190d6873618cb215fece))
* detect bee mode and enable/disable status checks accordingly ([#318](https://www.github.com/ethersphere/bee-dashboard/issues/318)) ([8baecb7](https://www.github.com/ethersphere/bee-dashboard/commit/8baecb783f1574af1cd1f17738efae4b0ac9f0c8))
* optional status checks (e.g. connected peers > 0 or funded chequebook) ([#331](https://www.github.com/ethersphere/bee-dashboard/issues/331)) ([5d0fbf7](https://www.github.com/ethersphere/bee-dashboard/commit/5d0fbf705dfed6738980c751a9654199d60a3787))
### Bug Fixes
* postage stamp price and TTL calculation ([#305](https://www.github.com/ethersphere/bee-dashboard/issues/305)) ([d0b3f1a](https://www.github.com/ethersphere/bee-dashboard/commit/d0b3f1abee7ea017bdd05954d5fadafb67365efd))
## [0.13.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.12.0...v0.13.0) (2022-01-28)
### Features
* add hash based routing ([#287](https://www.github.com/ethersphere/bee-dashboard/issues/287)) ([9ee1c91](https://www.github.com/ethersphere/bee-dashboard/commit/9ee1c9107bb08d1838044f39e4d0dd5817c8f283))
* add metadata and preview ([#292](https://www.github.com/ethersphere/bee-dashboard/issues/292)) ([f401314](https://www.github.com/ethersphere/bee-dashboard/commit/f4013142afdb407e699eff9587921e23c971f1db))
### Bug Fixes
* clean up spinner and disabled state on download page ([#294](https://www.github.com/ethersphere/bee-dashboard/issues/294)) ([a406e0f](https://www.github.com/ethersphere/bee-dashboard/commit/a406e0fc014991fcbaca230f27f41cd071d8a863))
* correct folder name when uploading multiple files or mix of files & directories ([#291](https://www.github.com/ethersphere/bee-dashboard/issues/291)) ([d878747](https://www.github.com/ethersphere/bee-dashboard/commit/d8787476acf068be6609a77b1fadb2f61d0fd502))
* disable feeds page when disconnected ([#293](https://www.github.com/ethersphere/bee-dashboard/issues/293)) ([1310deb](https://www.github.com/ethersphere/bee-dashboard/commit/1310deb17aec91f368f99974aaa245abb0a3e201))
* do not print size and name when meta is unknown ([#297](https://www.github.com/ethersphere/bee-dashboard/issues/297)) ([7880c80](https://www.github.com/ethersphere/bee-dashboard/commit/7880c802aea6b0830ca52b47b88540b8df5888cc))
* get current price from chain state ([#286](https://www.github.com/ethersphere/bee-dashboard/issues/286)) ([bc82e67](https://www.github.com/ethersphere/bee-dashboard/commit/bc82e6756154b33d01796a6e66e51dcfa1495338))
## [0.12.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.11.2...v0.12.0) (2021-12-21) ## [0.12.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.11.2...v0.12.0) (2021-12-21)
+1 -1
View File
@@ -13,7 +13,7 @@
**Warning: This project is in alpha state. There might (and most probably will) be changes in the future to its API and **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.** 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 <!-- SUPPORTED_BEE_START -->1.4.1-238867f1<!-- SUPPORTED_BEE_END -->**. This project is intended to be used with **Bee version <!-- SUPPORTED_BEE_START -->1.5.1-d0a77598<!-- SUPPORTED_BEE_END -->**.
Using it with older or newer Bee versions is not recommended and may not work. Stay up to date by joining the 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 [official Discord](https://discord.gg/GU22h2utj6) and by keeping an eye on the
[releases tab](https://github.com/ethersphere/bee-dashboard/releases). [releases tab](https://github.com/ethersphere/bee-dashboard/releases).
+580 -436
View File
File diff suppressed because it is too large Load Diff
+10 -5
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ethersphere/bee-dashboard", "name": "@ethersphere/bee-dashboard",
"version": "0.12.0", "version": "0.14.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",
@@ -26,7 +26,7 @@
"url": "https://github.com/ethersphere/bee-dashboard.git" "url": "https://github.com/ethersphere/bee-dashboard.git"
}, },
"dependencies": { "dependencies": {
"@ethersphere/bee-js": "3.1.0", "@ethersphere/bee-js": "^3.3.4",
"@ethersphere/manifest-js": "1.1.0", "@ethersphere/manifest-js": "1.1.0",
"@ethersphere/swarm-cid": "^0.1.0", "@ethersphere/swarm-cid": "^0.1.0",
"@material-ui/core": "4.12.3", "@material-ui/core": "4.12.3",
@@ -48,8 +48,8 @@
"react-dom": ">= 17.0.2", "react-dom": ">= 17.0.2",
"react-feather": "2.0.9", "react-feather": "2.0.9",
"react-identicons": "1.2.5", "react-identicons": "1.2.5",
"react-router": "5.2.0", "react-router": "6.2.1",
"react-router-dom": "5.2.0", "react-router-dom": "6.2.1",
"react-syntax-highlighter": "15.4.4", "react-syntax-highlighter": "15.4.4",
"semver": "7.3.5", "semver": "7.3.5",
"serve-handler": "6.1.3" "serve-handler": "6.1.3"
@@ -64,6 +64,9 @@
"@commitlint/config-conventional": "14.1.0", "@commitlint/config-conventional": "14.1.0",
"@testing-library/jest-dom": "5.15.0", "@testing-library/jest-dom": "5.15.0",
"@testing-library/react": "12.1.2", "@testing-library/react": "12.1.2",
"@testing-library/react-hooks": "^8.0.0",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.13",
"@types/file-saver": "2.0.4", "@types/file-saver": "2.0.4",
"@types/jest": "27.0.2", "@types/jest": "27.0.2",
"@types/qrcode.react": "1.0.2", "@types/qrcode.react": "1.0.2",
@@ -80,7 +83,8 @@
"babel-loader": "8.1.0", "babel-loader": "8.1.0",
"babel-plugin-syntax-dynamic-import": "6.18.0", "babel-plugin-syntax-dynamic-import": "6.18.0",
"babel-plugin-tsconfig-paths": "1.0.2", "babel-plugin-tsconfig-paths": "1.0.2",
"depcheck": "1.4.2", "cors": "^2.8.5",
"depcheck": "^1.4.3",
"eslint": "7.24.0", "eslint": "7.24.0",
"eslint-config-prettier": "8.2.0", "eslint-config-prettier": "8.2.0",
"eslint-config-react-app": "6.0.0", "eslint-config-react-app": "6.0.0",
@@ -92,6 +96,7 @@
"eslint-plugin-react": "7.23.2", "eslint-plugin-react": "7.23.2",
"eslint-plugin-react-hooks": "4.2.0", "eslint-plugin-react-hooks": "4.2.0",
"eslint-plugin-testing-library": "3.10.2", "eslint-plugin-testing-library": "3.10.2",
"express": "^4.17.3",
"file-loader": "6.2.0", "file-loader": "6.2.0",
"prettier": "2.4.1", "prettier": "2.4.1",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
+1 -7
View File
@@ -6,13 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<link rel="preconnect" href="https://fonts.gstatic.com"> <meta name="description" content="Bee Dashboard" />
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600&display=swap" rel="stylesheet">
<meta
name="description"
content="Bee Dashboard"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!-- <!--
manifest.json provides metadata used when your web app is installed on a manifest.json provides metadata used when your web app is installed on a
+37 -8
View File
@@ -1,34 +1,63 @@
@font-face { @font-face {
font-family: "IBMPlexMono500"; font-family: 'Work Sans';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(assets/fonts/WorkSans/WorkSans-Light.ttf) format('truetype');
}
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(assets/fonts/WorkSans/WorkSans-Regular.ttf) format('truetype');
}
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(assets/fonts/WorkSans/WorkSans-Medium.ttf) format('truetype');
}
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(assets/fonts/WorkSans/WorkSans-SemiBold.ttf) format('truetype');
}
@font-face {
font-family: 'IBMPlexMono500';
src: url(assets/fonts/IBMPlexMono500.ttf) format('truetype'); src: url(assets/fonts/IBMPlexMono500.ttf) format('truetype');
font-weight: 500; font-weight: 500;
} }
@font-face { @font-face {
font-family: "IBMPlexMono600"; font-family: 'IBMPlexMono600';
src: url(assets/fonts/IBMPlexMono600.ttf) format('truetype'); src: url(assets/fonts/IBMPlexMono600.ttf) format('truetype');
font-weight: 600; font-weight: 600;
} }
@font-face { @font-face {
font-family: "IBMPlexMonoregular"; font-family: 'IBMPlexMonoregular';
src: url(assets/fonts/IBMPlexMonoregular.ttf) format('truetype'); src: url(assets/fonts/IBMPlexMonoregular.ttf) format('truetype');
font-weight: 300; font-weight: 300;
} }
@font-face { @font-face {
font-family: "WorkSans-Italic-VariableFont_wght"; font-family: 'WorkSans-Italic-VariableFont_wght';
src: url(assets/fonts/WorkSans-Italic-VariableFont_wght.ttf) format('truetype'); src: url(assets/fonts/WorkSans-Italic-VariableFont_wght.ttf) format('truetype');
font-weight: 700; font-weight: 700;
} }
@font-face { @font-face {
font-family: "WorkSans-VariableFont_wght"; font-family: 'WorkSans-VariableFont_wght';
src: url(assets/fonts/WorkSans-VariableFont_wght.ttf) format('truetype'); src: url(assets/fonts/WorkSans-VariableFont_wght.ttf) format('truetype');
font-weight: 400; font-weight: 400;
} }
.App { .App {
font-family: "Helvetica Neue", HelveticaNeue, Helvetica, Arial, sans-serif; font-family: 'Work Sans', 'Helvetica Neue', HelveticaNeue, Helvetica, Arial, sans-serif;
} }
a, button { a,
font-family: "IBMPlexMono500" !important; button {
font-family: 'IBMPlexMono500' !important;
color: #dd7700; color: #dd7700;
} }
+1 -1
View File
@@ -2,7 +2,7 @@ import CssBaseline from '@material-ui/core/CssBaseline'
import { ThemeProvider } from '@material-ui/core/styles' import { ThemeProvider } from '@material-ui/core/styles'
import { SnackbarProvider } from 'notistack' import { SnackbarProvider } from 'notistack'
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { BrowserRouter as Router } from 'react-router-dom' import { HashRouter as Router } from 'react-router-dom'
import './App.css' import './App.css'
import Dashboard from './layout/Dashboard' import Dashboard from './layout/Dashboard'
import { Provider as BeeProvider } from './providers/Bee' import { Provider as BeeProvider } from './providers/Bee'
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3 -3
View File
@@ -3,7 +3,7 @@ import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import { ArrowForward, OpenInNewSharp } from '@material-ui/icons' import { ArrowForward, OpenInNewSharp } from '@material-ui/icons'
import { ReactElement, useState } from 'react' import { ReactElement, useState } from 'react'
import CopyToClipboard from 'react-copy-to-clipboard' import CopyToClipboard from 'react-copy-to-clipboard'
import { useHistory } from 'react-router' import { useNavigate } from 'react-router'
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
createStyles({ createStyles({
@@ -61,7 +61,7 @@ export default function ExpandableListItemLink({
}: Props): ReactElement | null { }: Props): ReactElement | null {
const classes = useStyles() const classes = useStyles()
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState(false)
const history = useHistory() const navigate = useNavigate()
const tooltipClickHandler = () => setCopied(true) const tooltipClickHandler = () => setCopied(true)
const tooltipCloseHandler = () => setCopied(false) const tooltipCloseHandler = () => setCopied(false)
@@ -72,7 +72,7 @@ export default function ExpandableListItemLink({
if (navigationType === 'NEW_WINDOW') { if (navigationType === 'NEW_WINDOW') {
window.open(link || value) window.open(link || value)
} else { } else {
history.push(link || value) navigate(link || value)
} }
} }
+3 -3
View File
@@ -1,7 +1,7 @@
import { Box, createStyles, Grid, makeStyles, Typography } from '@material-ui/core' import { Box, createStyles, Grid, makeStyles, Typography } from '@material-ui/core'
import { ArrowBack } from '@material-ui/icons' import { ArrowBack } from '@material-ui/icons'
import { ReactElement } from 'react' import { ReactElement } from 'react'
import { useHistory } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
interface Props { interface Props {
children: string children: string
@@ -20,10 +20,10 @@ const useStyles = makeStyles(() =>
export function HistoryHeader({ children }: Props): ReactElement { export function HistoryHeader({ children }: Props): ReactElement {
const classes = useStyles() const classes = useStyles()
const history = useHistory() const navigate = useNavigate()
function goBack() { function goBack() {
history.goBack() navigate(-1)
} }
return ( return (
+1 -1
View File
@@ -50,7 +50,7 @@ interface Props {
export default function SideBarItem({ iconStart, iconEnd, path, label }: Props): ReactElement { export default function SideBarItem({ iconStart, iconEnd, path, label }: Props): ReactElement {
const classes = useStyles() const classes = useStyles()
const location = useLocation() const location = useLocation()
const isSelected = Boolean(matchPath(location.pathname, { path, exact: true })) const isSelected = Boolean(path && matchPath(location.pathname, path))
return ( return (
<StyledListItem button selected={isSelected} disableRipple> <StyledListItem button selected={isSelected} disableRipple>
+3 -5
View File
@@ -56,7 +56,7 @@ export default function SideBarItem({ path }: Props): ReactElement {
const { status, isLoading } = useContext(Context) const { status, isLoading } = useContext(Context)
const classes = useStyles() const classes = useStyles()
const location = useLocation() const location = useLocation()
const isSelected = Boolean(matchPath(location.pathname, { path, exact: true })) const isSelected = Boolean(path && matchPath(location.pathname, path))
return ( return (
<ListItem <ListItem
@@ -66,11 +66,9 @@ export default function SideBarItem({ path }: Props): ReactElement {
disableRipple disableRipple
> >
<ListItemIcon style={{ marginLeft: '30px' }}> <ListItemIcon style={{ marginLeft: '30px' }}>
<StatusIcon isOk={status.all} isLoading={isLoading} /> <StatusIcon checkState={status.all} isLoading={isLoading} />
</ListItemIcon> </ListItemIcon>
<ListItemText <ListItemText primary={<Typography className={classes.smallerText}>{`Node ${status.all}`}</Typography>} />
primary={<Typography className={classes.smallerText}>{`Node ${status.all ? 'OK' : 'Error'}`}</Typography>}
/>
<ListItemIcon className={classes.icon}> <ListItemIcon className={classes.icon}>
{status.all ? null : <ArrowRight className={classes.iconSmall} />} {status.all ? null : <ArrowRight className={classes.iconSmall} />}
</ListItemIcon> </ListItemIcon>
+20 -3
View File
@@ -1,23 +1,40 @@
import type { ReactElement } from 'react' import type { ReactElement } from 'react'
import { CircularProgress } from '@material-ui/core' import { CircularProgress } from '@material-ui/core'
import { CheckState } from '../providers/Bee'
interface Props { interface Props {
isOk: boolean checkState: CheckState
isLoading?: boolean isLoading?: boolean
size?: number | string size?: number | string
className?: string className?: string
} }
export default function StatusIcon({ isOk, size, className, isLoading }: Props): ReactElement { export default function StatusIcon({ checkState, size, className, isLoading }: Props): ReactElement {
const s = size || '1rem' const s = size || '1rem'
if (isLoading) return <CircularProgress size={s} className={className} /> if (isLoading) return <CircularProgress size={s} className={className} />
let backgroundColor: string
switch (checkState) {
case CheckState.OK:
backgroundColor = '#1de600'
break
case CheckState.WARNING:
backgroundColor = 'orange'
break
case CheckState.ERROR:
backgroundColor = '#ff3a52'
break
default:
// Default is error
backgroundColor = '#ff3a52'
}
return ( return (
<span <span
className={className} className={className}
style={{ style={{
backgroundColor: isOk ? '#1de600' : '#ff3a52', backgroundColor,
height: s, height: s,
width: s, width: s,
borderRadius: '50%', borderRadius: '50%',
+3 -1
View File
@@ -10,6 +10,7 @@ interface Props {
disabled?: boolean disabled?: boolean
loading?: boolean loading?: boolean
cancel?: boolean cancel?: boolean
variant?: 'text' | 'contained' | 'outlined'
} }
const useStyles = makeStyles(() => const useStyles = makeStyles(() =>
@@ -49,6 +50,7 @@ export function SwarmButton({
disabled, disabled,
loading, loading,
cancel, cancel,
variant = 'contained',
}: Props): ReactElement { }: Props): ReactElement {
const classes = useStyles() const classes = useStyles()
@@ -76,7 +78,7 @@ export function SwarmButton({
onClick() onClick()
event.currentTarget.blur() event.currentTarget.blur()
}} }}
variant="contained" variant={variant}
startIcon={icon} startIcon={icon}
disabled={disabled} disabled={disabled}
> >
+7
View File
@@ -25,6 +25,11 @@ const useStyles = makeStyles((theme: Theme) =>
'& fieldset': { '& fieldset': {
border: 0, border: 0,
}, },
'& .MuiSelect-select': {
'&:focus': {
background: theme.palette.background.paper,
},
},
}, },
option: { option: {
height: '52px', height: '52px',
@@ -48,6 +53,7 @@ export function SwarmSelect({ defaultValue, formik, name, options, onChange, lab
defaultValue={defaultValue || ''} defaultValue={defaultValue || ''}
className={classes.select} className={classes.select}
placeholder={label} placeholder={label}
MenuProps={{ MenuListProps: { disablePadding: true }, PaperProps: { square: true } }}
> >
{options.map((x, i) => ( {options.map((x, i) => (
<MenuItem key={i} value={x.value} className={classes.option}> <MenuItem key={i} value={x.value} className={classes.option}>
@@ -71,6 +77,7 @@ export function SwarmSelect({ defaultValue, formik, name, options, onChange, lab
defaultValue={defaultValue || ''} defaultValue={defaultValue || ''}
onChange={onChange} onChange={onChange}
placeholder={label} placeholder={label}
MenuProps={{ MenuListProps: { disablePadding: true }, PaperProps: { square: true } }}
> >
{options.map((x, i) => ( {options.map((x, i) => (
<MenuItem key={i} value={x.value} className={classes.option}> <MenuItem key={i} value={x.value} className={classes.option}>
+25 -6
View File
@@ -9,6 +9,7 @@ interface Props {
password?: boolean password?: boolean
formik?: boolean formik?: boolean
optional?: boolean optional?: boolean
defaultValue?: string
onChange?: (event: ChangeEvent<HTMLTextAreaElement>) => void onChange?: (event: ChangeEvent<HTMLTextAreaElement>) => void
} }
@@ -16,15 +17,31 @@ const useStyles = makeStyles((theme: Theme) =>
createStyles({ createStyles({
field: { field: {
background: theme.palette.background.paper, background: theme.palette.background.paper,
height: '52px',
'& fieldset': { '& fieldset': {
border: 0, border: 0,
}, },
'& .Mui-focused': {
background: theme.palette.background.paper,
},
'& .MuiInputBase-root': {
background: theme.palette.background.paper,
},
'& .MuiFilledInput-root': {
borderRadius: 0,
},
}, },
}), }),
) )
export function SwarmTextInput({ name, label, password, optional, formik, onChange }: Props): ReactElement { export function SwarmTextInput({
name,
label,
password,
optional,
formik,
onChange,
defaultValue,
}: Props): ReactElement {
const classes = useStyles() const classes = useStyles()
if (formik) { if (formik) {
@@ -36,9 +53,10 @@ export function SwarmTextInput({ name, label, password, optional, formik, onChan
name={name} name={name}
label={label} label={label}
fullWidth fullWidth
variant="outlined" variant="filled"
className={classes.field} className={classes.field}
defaultValue="" defaultValue={defaultValue || ''}
InputProps={{ disableUnderline: true }}
/> />
) )
} }
@@ -49,10 +67,11 @@ export function SwarmTextInput({ name, label, password, optional, formik, onChan
required required
label={label} label={label}
fullWidth fullWidth
variant="outlined" variant="filled"
className={classes.field} className={classes.field}
defaultValue="" defaultValue={defaultValue || ''}
onChange={onChange} onChange={onChange}
InputProps={{ disableUnderline: true }}
/> />
) )
} }
+2
View File
@@ -9,6 +9,7 @@ class Config {
public readonly BEE_DOCS_HOST: string public readonly BEE_DOCS_HOST: string
public readonly BEE_DISCORD_HOST: string public readonly BEE_DISCORD_HOST: string
public readonly GITHUB_REPO_URL: string public readonly GITHUB_REPO_URL: string
public readonly BEE_DESKTOP_URL: string
constructor() { constructor() {
this.BEE_API_HOST = this.BEE_API_HOST =
@@ -21,6 +22,7 @@ class Config {
this.BEE_DISCORD_HOST = getProcessEnv('REACT_APP_BEE_DISCORD_HOST') || 'https://discord.gg/eKr9XPv7' this.BEE_DISCORD_HOST = getProcessEnv('REACT_APP_BEE_DISCORD_HOST') || 'https://discord.gg/eKr9XPv7'
this.GITHUB_REPO_URL = this.GITHUB_REPO_URL =
getProcessEnv('REACT_APP_BEE_GITHUB_REPO_URL') || 'https://api.github.com/repos/ethersphere/bee' getProcessEnv('REACT_APP_BEE_GITHUB_REPO_URL') || 'https://api.github.com/repos/ethersphere/bee'
this.BEE_DESKTOP_URL = getProcessEnv('REACT_APP_BEE_DESKTOP_URL') || window.location.origin
} }
} }
+3
View File
@@ -0,0 +1,3 @@
export const META_FILE_NAME = '.swarmgatewaymeta.json'
export const PREVIEW_FILE_NAME = '.swarmgatewaypreview.jpeg'
export const PREVIEW_DIMENSIONS = { maxWidth: 250, maxHeight: 175 }
+73
View File
@@ -0,0 +1,73 @@
import { renderHook } from '@testing-library/react-hooks'
import express from 'express'
import cors from 'cors'
import type { Server } from 'http'
import { useIsBeeDesktop } from './apiHooks'
interface AddressInfo {
address: string
family: string
port: number
}
export function mockServer(data: Record<string | number | symbol, string>): Promise<Server> {
const app = express()
app.use(cors())
app.get('/info', (req, res) => {
res.send(data)
})
return new Promise(resolve => {
const server = app.listen(() => {
resolve(server)
})
})
}
let serverCorrect: Server
let serverWrong: Server
let serverCorrectURL: string
let serverWrongURL: string
beforeAll(async () => {
serverCorrect = await mockServer({ name: 'bee-desktop' })
const portServerCorrect = (serverCorrect.address() as AddressInfo).port
serverCorrectURL = `http://localhost:${portServerCorrect}`
serverWrong = await mockServer({ foo: 'bar' })
const portServerWrong = (serverWrong.address() as AddressInfo).port
serverWrongURL = `http://localhost:${portServerWrong}`
})
afterAll(async () => {
await new Promise(resolve => serverCorrect.close(resolve))
await new Promise(resolve => serverWrong.close(resolve))
})
describe('useIsBeeDesktop', () => {
it('should fail when connected to wrong server', async () => {
const { result, waitFor } = renderHook(() => useIsBeeDesktop({ BEE_DESKTOP_URL: serverWrongURL }))
expect(result.current.isLoading).toBe(true)
expect(result.current.isBeeDesktop).toBe(false)
await waitFor(() => {
expect(result.current.isLoading).toBe(false)
})
expect(result.current.isBeeDesktop).toBe(false)
})
it('should return isBeeDesktop true when connected to bee-desktop', async () => {
const { result, waitFor } = renderHook(() => useIsBeeDesktop({ BEE_DESKTOP_URL: serverCorrectURL }))
expect(result.current.isLoading).toBe(true)
expect(result.current.isBeeDesktop).toBe(false)
await waitFor(() => {
expect(result.current.isLoading).toBe(false)
})
expect(result.current.isBeeDesktop).toBe(true)
})
})
+36
View File
@@ -8,6 +8,42 @@ export interface LatestBeeReleaseHook {
error: Error | null error: Error | null
} }
export interface IsBeeDesktopHook {
isBeeDesktop: boolean
isLoading: boolean
}
interface Config {
BEE_DESKTOP_URL: string
}
/**
* Detect if the dashboard is run within bee-desktop
*
* @returns isBeeDesktop true if this is run within bee-desktop
*/
export const useIsBeeDesktop = (conf: Config = config): IsBeeDesktopHook => {
const [isBeeDesktop, setIsBeeDesktop] = useState<boolean>(false)
const [isLoading, setLoading] = useState<boolean>(true)
useEffect(() => {
axios
.get(`${conf.BEE_DESKTOP_URL}/info`)
.then(res => {
if (res.data?.name === 'bee-desktop') setIsBeeDesktop(true)
else setIsBeeDesktop(false)
})
.catch(() => {
setIsBeeDesktop(false)
})
.finally(() => {
setLoading(false)
})
}, [conf])
return { isBeeDesktop, isLoading }
}
export const useLatestBeeRelease = (): LatestBeeReleaseHook => { export const useLatestBeeRelease = (): LatestBeeReleaseHook => {
const [latestBeeRelease, setLatestBeeRelease] = useState<LatestBeeRelease | null>(null) const [latestBeeRelease, setLatestBeeRelease] = useState<LatestBeeRelease | null>(null)
const [isLoadingLatestBeeRelease, setLoading] = useState<boolean>(false) const [isLoadingLatestBeeRelease, setLoading] = useState<boolean>(false)
+21
View File
@@ -57,4 +57,25 @@ export class Token {
toFixedDecimal(digits = 7): string { toFixedDecimal(digits = 7): string {
return this.toDecimal.toFixed(digits) return this.toDecimal.toFixed(digits)
} }
toSignificantDigits(digits = 4): string {
const asString = this.toDecimal.toFixed(16)
let indexOfSignificantDigit = -1
let reachedDecimalPoint = false
for (let i = 0; i < asString.length; i++) {
const char = asString[i]
if (char === '.') {
reachedDecimalPoint = true
indexOfSignificantDigit = i + 1
} else if (reachedDecimalPoint && char !== '0') {
indexOfSignificantDigit = i
break
}
}
return asString.slice(0, indexOfSignificantDigit + digits)
}
} }
+2 -2
View File
@@ -2,7 +2,7 @@ import { ReactElement, useContext } from 'react'
import PeerBalances from './PeerBalances' import PeerBalances from './PeerBalances'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard' import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { Context as BeeContext } from '../../providers/Bee' import { CheckState, Context as BeeContext } from '../../providers/Bee'
import { Context as SettingsContext } from '../../providers/Settings' import { Context as SettingsContext } from '../../providers/Settings'
import { useAccounting } from '../../hooks/accounting' import { useAccounting } from '../../hooks/accounting'
import ExpandableList from '../../components/ExpandableList' import ExpandableList from '../../components/ExpandableList'
@@ -19,7 +19,7 @@ export default function Accounting(): ReactElement {
const { accounting, totalUncashed, isLoadingUncashed } = useAccounting(beeDebugApi, settlements, peerBalances) const { accounting, totalUncashed, isLoadingUncashed } = useAccounting(beeDebugApi, settlements, peerBalances)
if (!status.all) return <TroubleshootConnectionCard /> if (status.all === CheckState.ERROR) return <TroubleshootConnectionCard />
return ( return (
<div> <div>
+4 -4
View File
@@ -3,7 +3,7 @@ import { Form, Formik } from 'formik'
import { useSnackbar } from 'notistack' import { useSnackbar } from 'notistack'
import { ReactElement, useContext, useState } from 'react' import { ReactElement, useContext, useState } from 'react'
import { Check, X } from 'react-feather' import { Check, X } from 'react-feather'
import { useHistory } from 'react-router' import { useNavigate } from 'react-router'
import { DocumentationText } from '../../components/DocumentationText' import { DocumentationText } from '../../components/DocumentationText'
import ExpandableListItemActions from '../../components/ExpandableListItemActions' import ExpandableListItemActions from '../../components/ExpandableListItemActions'
import ExpandableListItemKey from '../../components/ExpandableListItemKey' import ExpandableListItemKey from '../../components/ExpandableListItemKey'
@@ -34,7 +34,7 @@ export default function CreateNewFeed(): ReactElement {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const { enqueueSnackbar } = useSnackbar() const { enqueueSnackbar } = useSnackbar()
const history = useHistory() const navigate = useNavigate()
async function onSubmit(values: FormValues) { async function onSubmit(values: FormValues) {
setLoading(true) setLoading(true)
@@ -65,12 +65,12 @@ export default function CreateNewFeed(): ReactElement {
const identity = await convertWalletToIdentity(wallet, values.type, values.identityName, values.password) const identity = await convertWalletToIdentity(wallet, values.type, values.identityName, values.password)
persistIdentity(identities, identity) persistIdentity(identities, identity)
setIdentities(identities) setIdentities(identities)
history.push(ROUTES.FEEDS) navigate(ROUTES.FEEDS)
setLoading(false) setLoading(false)
} }
function cancel() { function cancel() {
history.goBack() navigate(-1)
} }
return ( return (
+6 -10
View File
@@ -2,7 +2,7 @@ import * as swarmCid from '@ethersphere/swarm-cid'
import { Box } from '@material-ui/core' import { Box } from '@material-ui/core'
import { ReactElement, useContext, useEffect, useState } from 'react' import { ReactElement, useContext, useEffect, useState } from 'react'
import { X } from 'react-feather' import { X } from 'react-feather'
import { RouteComponentProps, useHistory } from 'react-router-dom' import { useParams, useNavigate } from 'react-router-dom'
import { DocumentationText } from '../../components/DocumentationText' import { DocumentationText } from '../../components/DocumentationText'
import ExpandableListItemActions from '../../components/ExpandableListItemActions' import ExpandableListItemActions from '../../components/ExpandableListItemActions'
import ExpandableListItemKey from '../../components/ExpandableListItemKey' import ExpandableListItemKey from '../../components/ExpandableListItemKey'
@@ -15,20 +15,16 @@ import { Context as SettingsContext } from '../../providers/Settings'
import { ROUTES } from '../../routes' import { ROUTES } from '../../routes'
import { UploadArea } from '../files/UploadArea' import { UploadArea } from '../files/UploadArea'
interface MatchParams { export function FeedSubpage(): ReactElement {
uuid: string
}
export function FeedSubpage(props: RouteComponentProps<MatchParams>): ReactElement {
const { identities } = useContext(IdentityContext) const { identities } = useContext(IdentityContext)
const { uuid } = useParams()
const { beeApi } = useContext(SettingsContext) const { beeApi } = useContext(SettingsContext)
const { status } = useContext(BeeContext) const { status } = useContext(BeeContext)
const history = useHistory() const navigate = useNavigate()
const [available, setAvailable] = useState(false) const [available, setAvailable] = useState(false)
const uuid = props.match.params.uuid
const identity = identities.find(x => x.uuid === uuid) const identity = identities.find(x => x.uuid === uuid)
useEffect(() => { useEffect(() => {
@@ -44,13 +40,13 @@ export function FeedSubpage(props: RouteComponentProps<MatchParams>): ReactEleme
}, [beeApi, uuid, identity]) }, [beeApi, uuid, identity])
if (!identity || !status.all) { if (!identity || !status.all) {
history.replace(ROUTES.FEEDS) navigate(ROUTES.FEEDS, { replace: true })
return <></> return <></>
} }
function onClose() { function onClose() {
history.push(ROUTES.FEEDS) navigate(ROUTES.FEEDS)
} }
return ( return (
+7 -10
View File
@@ -2,7 +2,7 @@ import { Box, Grid, Typography } from '@material-ui/core'
import { useSnackbar } from 'notistack' import { useSnackbar } from 'notistack'
import { ReactElement, useContext, useEffect, useState } from 'react' import { ReactElement, useContext, useEffect, useState } from 'react'
import { Bookmark, X } from 'react-feather' import { Bookmark, X } from 'react-feather'
import { RouteComponentProps, useHistory } from 'react-router' import { useParams, useNavigate } from 'react-router'
import ExpandableListItemActions from '../../components/ExpandableListItemActions' import ExpandableListItemActions from '../../components/ExpandableListItemActions'
import { HistoryHeader } from '../../components/HistoryHeader' import { HistoryHeader } from '../../components/HistoryHeader'
import { SwarmButton } from '../../components/SwarmButton' import { SwarmButton } from '../../components/SwarmButton'
@@ -16,15 +16,12 @@ import { ROUTES } from '../../routes'
import { persistIdentity, updateFeed } from '../../utils/identity' import { persistIdentity, updateFeed } from '../../utils/identity'
import { FeedPasswordDialog } from './FeedPasswordDialog' import { FeedPasswordDialog } from './FeedPasswordDialog'
interface MatchParams { export default function UpdateFeed(): ReactElement {
hash: string
}
export default function UpdateFeed(props: RouteComponentProps<MatchParams>): ReactElement {
const { identities, setIdentities } = useContext(IdentityContext) const { identities, setIdentities } = useContext(IdentityContext)
const { beeApi, beeDebugApi } = useContext(SettingsContext) const { beeApi, beeDebugApi } = useContext(SettingsContext)
const { stamps, refresh } = useContext(StampContext) const { stamps, refresh } = useContext(StampContext)
const { status } = useContext(BeeContext) const { status } = useContext(BeeContext)
const { hash } = useParams()
const [selectedStamp, setSelectedStamp] = useState<string | null>(null) const [selectedStamp, setSelectedStamp] = useState<string | null>(null)
const [selectedIdentity, setSelectedIdentity] = useState<Identity | null>(null) const [selectedIdentity, setSelectedIdentity] = useState<Identity | null>(null)
@@ -32,7 +29,7 @@ export default function UpdateFeed(props: RouteComponentProps<MatchParams>): Rea
const { enqueueSnackbar } = useSnackbar() const { enqueueSnackbar } = useSnackbar()
const [showPasswordPrompt, setShowPasswordPrompt] = useState(false) const [showPasswordPrompt, setShowPasswordPrompt] = useState(false)
const history = useHistory() const navigate = useNavigate()
useEffect(() => { useEffect(() => {
refresh() refresh()
@@ -50,7 +47,7 @@ export default function UpdateFeed(props: RouteComponentProps<MatchParams>): Rea
} }
function onCancel() { function onCancel() {
history.goBack() navigate(-1)
} }
function onBeginUpdatingFeed() { function onBeginUpdatingFeed() {
@@ -76,10 +73,10 @@ export default function UpdateFeed(props: RouteComponentProps<MatchParams>): Rea
} }
try { try {
await updateFeed(beeApi, identity, props.match.params.hash, selectedStamp, password as string) await updateFeed(beeApi, identity, hash!, selectedStamp, password as string) // eslint-disable-line
persistIdentity(identities, identity) persistIdentity(identities, identity)
setIdentities([...identities]) setIdentities([...identities])
history.push(ROUTES.FEEDS_PAGE.replace(':uuid', identity.uuid)) navigate(ROUTES.FEEDS_PAGE.replace(':uuid', identity.uuid))
} catch (error: unknown) { } catch (error: unknown) {
setLoading(false) setLoading(false)
+11 -10
View File
@@ -1,13 +1,14 @@
import { Box, Typography } from '@material-ui/core' import { Box, Typography } from '@material-ui/core'
import { ReactElement, useContext, useState } from 'react' import { ReactElement, useContext, useState } from 'react'
import { Download, Info, PlusSquare, Trash } from 'react-feather' import { Download, Info, PlusSquare, Trash } from 'react-feather'
import { useHistory } from 'react-router' import { useNavigate } from 'react-router'
import ExpandableList from '../../components/ExpandableList' import ExpandableList from '../../components/ExpandableList'
import ExpandableListItem from '../../components/ExpandableListItem' import ExpandableListItem from '../../components/ExpandableListItem'
import ExpandableListItemActions from '../../components/ExpandableListItemActions' import ExpandableListItemActions from '../../components/ExpandableListItemActions'
import ExpandableListItemKey from '../../components/ExpandableListItemKey' import ExpandableListItemKey from '../../components/ExpandableListItemKey'
import { SwarmButton } from '../../components/SwarmButton' import { SwarmButton } from '../../components/SwarmButton'
import { Context as BeeContext } from '../../providers/Bee' import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { CheckState, Context as BeeContext } from '../../providers/Bee'
import { Context as IdentityContext, Identity } from '../../providers/Feeds' import { Context as IdentityContext, Identity } from '../../providers/Feeds'
import { ROUTES } from '../../routes' import { ROUTES } from '../../routes'
import { formatEnum } from '../../utils' import { formatEnum } from '../../utils'
@@ -20,7 +21,7 @@ export default function Feeds(): ReactElement {
const { identities, setIdentities } = useContext(IdentityContext) const { identities, setIdentities } = useContext(IdentityContext)
const { status } = useContext(BeeContext) const { status } = useContext(BeeContext)
const history = useHistory() const navigate = useNavigate()
const [selectedIdentity, setSelectedIdentity] = useState<Identity | null>(null) const [selectedIdentity, setSelectedIdentity] = useState<Identity | null>(null)
const [showImport, setShowImport] = useState(false) const [showImport, setShowImport] = useState(false)
@@ -28,11 +29,11 @@ export default function Feeds(): ReactElement {
const [showDelete, setShowDelete] = useState(false) const [showDelete, setShowDelete] = useState(false)
function createNewFeed() { function createNewFeed() {
return history.push(ROUTES.FEEDS_NEW) return navigate(ROUTES.FEEDS_NEW)
} }
function viewFeed(uuid: string) { function viewFeed(uuid: string) {
history.push(ROUTES.FEEDS_PAGE.replace(':uuid', uuid)) navigate(ROUTES.FEEDS_PAGE.replace(':uuid', uuid))
} }
function onDialogClose() { function onDialogClose() {
@@ -59,6 +60,8 @@ export default function Feeds(): ReactElement {
setShowDelete(true) setShowDelete(true)
} }
if (status.all === CheckState.ERROR) return <TroubleshootConnectionCard />
return ( return (
<div> <div>
{showImport && <ImportFeedDialog onClose={() => setShowImport(false)} />} {showImport && <ImportFeedDialog onClose={() => setShowImport(false)} />}
@@ -95,11 +98,9 @@ export default function Feeds(): ReactElement {
{x.feedHash && <ExpandableListItemKey label="Feed hash" value={x.feedHash} />} {x.feedHash && <ExpandableListItemKey label="Feed hash" value={x.feedHash} />}
<Box mt={0.75}> <Box mt={0.75}>
<ExpandableListItemActions> <ExpandableListItemActions>
{status.all && ( <SwarmButton onClick={() => viewFeed(x.uuid)} iconType={Info}>
<SwarmButton onClick={() => viewFeed(x.uuid)} iconType={Info}> View Feed Page
View Feed Page </SwarmButton>
</SwarmButton>
)}
<SwarmButton onClick={() => onShowExport(x)} iconType={Download}> <SwarmButton onClick={() => onShowExport(x)} iconType={Download}>
Export... Export...
</SwarmButton> </SwarmButton>
+28 -69
View File
@@ -1,99 +1,58 @@
import { Box, Grid, Typography } from '@material-ui/core' import { Box, Grid, Typography } from '@material-ui/core'
import { Web } from '@material-ui/icons' import { Web } from '@material-ui/icons'
import { ReactElement, useEffect, useState } from 'react' import { ReactElement } from 'react'
import { File, Folder } from 'react-feather' import { File, Folder } from 'react-feather'
import { FitImage } from '../../components/FitImage' import { FitImage } from '../../components/FitImage'
import { detectIndexHtml, getAssetNameFromFiles, getHumanReadableFileSize } from '../../utils/file' import { shortenText } from '../../utils'
import { SwarmFile } from '../../utils/SwarmFile' import { getHumanReadableFileSize } from '../../utils/file'
import { shortenHash } from '../../utils/hash'
import { AssetIcon } from './AssetIcon' import { AssetIcon } from './AssetIcon'
interface Props { interface Props {
assetName?: string previewUri?: string
files: SwarmFile[] metadata?: Metadata
} }
// TODO: add optional prop for indexDocument when it is already known (e.g. downloading a manifest) // TODO: add optional prop for indexDocument when it is already known (e.g. downloading a manifest)
export function AssetPreview({ assetName, files }: Props): ReactElement { export function AssetPreview({ metadata, previewUri }: Props): ReactElement | null {
const [previewComponent, setPreviewComponent] = useState<ReactElement | undefined>(undefined) let previewComponent = <File />
const [previewUri, setPreviewUri] = useState<string | undefined>(undefined) let type = metadata?.type
useEffect(() => { if (metadata?.isWebsite) {
if (files.length === 1) { previewComponent = <Web />
// single image type = 'Website'
if (files[0].type.startsWith('image/')) { } else if (metadata?.type === 'folder') {
files[0].arrayBuffer().then(value => { previewComponent = <Folder />
const blob = new Blob([value]) type = 'Folder'
setPreviewUri(URL.createObjectURL(blob))
})
// single non-image
} else {
setPreviewUri(undefined)
setPreviewComponent(<AssetIcon icon={<File />} />)
}
// collection
} else if (detectIndexHtml(files)) {
setPreviewUri(undefined)
setPreviewComponent(<AssetIcon icon={<Web />} />)
} else {
setPreviewUri(undefined)
setPreviewComponent(<AssetIcon icon={<Folder />} />)
}
}, [files])
const getPrimaryText = () => {
const name = getAssetNameFromFiles(files)
if (files.length === 1) {
return 'Filename: ' + (assetName || name)
}
return 'Folder name: ' + (assetName || name)
} }
const getKind = () => {
if (files.length === 1) {
return files[0].type
}
if (detectIndexHtml(files)) {
return 'Website'
}
return 'Folder'
}
const isFolder = () => ['Folder', 'Website'].includes(getKind())
const getSize = () => {
const bytes = files.reduce((total, item) => total + item.size, 0)
return getHumanReadableFileSize(bytes)
}
const size = getSize()
return ( return (
<Box mb={4}> <Box mb={4}>
<Box bgcolor="background.paper"> <Box bgcolor="background.paper">
<Grid container direction="row"> <Grid container direction="row">
{previewComponent ? ( {previewUri ? (
previewComponent
) : (
<FitImage maxWidth="250px" maxHeight="175px" alt="Upload Preview" src={previewUri} /> <FitImage maxWidth="250px" maxHeight="175px" alt="Upload Preview" src={previewUri} />
) : (
<AssetIcon icon={previewComponent} />
)} )}
<Box p={2}> <Box p={2}>
<Typography>{getPrimaryText()}</Typography> {metadata?.hash && <Typography>Swarm Hash: {shortenHash(metadata.hash)}</Typography>}
<Typography>Kind: {getKind()}</Typography> {metadata?.name && metadata?.name !== metadata?.hash && (
{size !== '0 bytes' && <Typography>Size: {size}</Typography>} <Typography>
{metadata?.type === 'folder' ? 'Folder Name' : 'Filename'}: {shortenText(metadata?.name)}
</Typography>
)}
<Typography>Kind: {type}</Typography>
{metadata?.size ? <Typography>Size: {getHumanReadableFileSize(metadata.size)}</Typography> : null}
</Box> </Box>
</Grid> </Grid>
</Box> </Box>
{isFolder() && ( {metadata?.type === 'folder' && metadata.count && (
<Box mt={0.25} p={2} bgcolor="background.paper"> <Box mt={0.25} p={2} bgcolor="background.paper">
<Grid container justifyContent="space-between" alignItems="center" direction="row"> <Grid container justifyContent="space-between" alignItems="center" direction="row">
<Typography variant="subtitle2">Folder content</Typography> <Typography variant="subtitle2">Folder content</Typography>
<Typography variant="subtitle2">{files.length} items</Typography> <Typography variant="subtitle2">{metadata.count} items</Typography>
</Grid> </Grid>
</Box> </Box>
)} )}
+3 -5
View File
@@ -4,21 +4,19 @@ import { ReactElement } from 'react'
import { DocumentationText } from '../../components/DocumentationText' import { DocumentationText } from '../../components/DocumentationText'
import ExpandableListItemKey from '../../components/ExpandableListItemKey' import ExpandableListItemKey from '../../components/ExpandableListItemKey'
import ExpandableListItemLink from '../../components/ExpandableListItemLink' import ExpandableListItemLink from '../../components/ExpandableListItemLink'
import { detectIndexHtml } from '../../utils/file'
import { SwarmFile } from '../../utils/SwarmFile'
interface Props { interface Props {
files: SwarmFile[] isWebsite?: boolean
hash: string hash: string
} }
export function AssetSummary({ files, hash }: Props): ReactElement { export function AssetSummary({ isWebsite, hash }: Props): ReactElement {
return ( return (
<> <>
<Box mb={4}> <Box mb={4}>
<ExpandableListItemKey label="Swarm hash" value={hash} /> <ExpandableListItemKey label="Swarm hash" value={hash} />
<ExpandableListItemLink label="Share on Swarm Gateway" value={`https://gateway.ethswarm.org/access/${hash}`} /> <ExpandableListItemLink label="Share on Swarm Gateway" value={`https://gateway.ethswarm.org/access/${hash}`} />
{detectIndexHtml(files) && ( {isWebsite && (
<ExpandableListItemLink <ExpandableListItemLink
label="BZZ Link" label="BZZ Link"
value={`https://${swarmCid.encodeManifestReference(hash).toString()}.bzz.link`} value={`https://${swarmCid.encodeManifestReference(hash).toString()}.bzz.link`}
+3 -3
View File
@@ -2,7 +2,7 @@ import { Utils } from '@ethersphere/bee-js'
import { ManifestJs } from '@ethersphere/manifest-js' import { ManifestJs } from '@ethersphere/manifest-js'
import { useSnackbar } from 'notistack' import { useSnackbar } from 'notistack'
import { ReactElement, useContext, useState } from 'react' import { ReactElement, useContext, useState } from 'react'
import { useHistory } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import ExpandableListItemInput from '../../components/ExpandableListItemInput' import ExpandableListItemInput from '../../components/ExpandableListItemInput'
import { History } from '../../components/History' import { History } from '../../components/History'
import { Context, defaultUploadOrigin } from '../../providers/File' import { Context, defaultUploadOrigin } from '../../providers/File'
@@ -20,7 +20,7 @@ export function Download(): ReactElement {
const { setUploadOrigin } = useContext(Context) const { setUploadOrigin } = useContext(Context)
const { enqueueSnackbar } = useSnackbar() const { enqueueSnackbar } = useSnackbar()
const history = useHistory() const navigate = useNavigate()
const validateChange = (value: string) => { const validateChange = (value: string) => {
if (Utils.isHexString(value, 64) || Utils.isHexString(value, 128) || !value.trim().length) { if (Utils.isHexString(value, 64) || Utils.isHexString(value, 128) || !value.trim().length) {
@@ -54,7 +54,7 @@ export function Download(): ReactElement {
const indexDocument = await manifestJs.getIndexDocumentPath(identifier) const indexDocument = await manifestJs.getIndexDocumentPath(identifier)
putHistory(HISTORY_KEYS.DOWNLOAD_HISTORY, identifier, determineHistoryName(identifier, indexDocument)) putHistory(HISTORY_KEYS.DOWNLOAD_HISTORY, identifier, determineHistoryName(identifier, indexDocument))
setUploadOrigin(defaultUploadOrigin) setUploadOrigin(defaultUploadOrigin)
history.push(ROUTES.HASH.replace(':hash', identifier)) navigate(ROUTES.HASH.replace(':hash', identifier))
} catch (error: unknown) { } catch (error: unknown) {
let message = typeof error === 'object' && error !== null && Reflect.get(error, 'message') let message = typeof error === 'object' && error !== null && Reflect.get(error, 'message')
+2 -2
View File
@@ -32,12 +32,12 @@ export function DownloadActionBar({
<SwarmButton onClick={onDownload} iconType={Download} disabled={loading} loading={loading}> <SwarmButton onClick={onDownload} iconType={Download} disabled={loading} loading={loading}>
Download Download
</SwarmButton> </SwarmButton>
<SwarmButton onClick={onCancel} iconType={X} disabled={loading} loading={loading} cancel> <SwarmButton onClick={onCancel} iconType={X} disabled={loading} cancel>
Close Close
</SwarmButton> </SwarmButton>
</ExpandableListItemActions> </ExpandableListItemActions>
<Box mb={1} mr={1}> <Box mb={1} mr={1}>
<SwarmButton onClick={onUpdateFeed} iconType={Bookmark}> <SwarmButton onClick={onUpdateFeed} iconType={Bookmark} disabled={loading}>
Update Feed Update Feed
</SwarmButton> </SwarmButton>
</Box> </Box>
+3 -3
View File
@@ -1,6 +1,6 @@
import { createStyles, makeStyles, Tab, Tabs, Theme } from '@material-ui/core' import { createStyles, makeStyles, Tab, Tabs, Theme } from '@material-ui/core'
import { ReactElement } from 'react' import { ReactElement } from 'react'
import { useHistory } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { ROUTES } from '../../routes' import { ROUTES } from '../../routes'
interface Props { interface Props {
@@ -24,10 +24,10 @@ const useStyles = makeStyles((theme: Theme) =>
export function FileNavigation({ active }: Props): ReactElement { export function FileNavigation({ active }: Props): ReactElement {
const classes = useStyles() const classes = useStyles()
const history = useHistory() const navigate = useNavigate()
function onChange(event: React.ChangeEvent<Record<string, never>>, newValue: number) { function onChange(event: React.ChangeEvent<Record<string, never>>, newValue: number) {
history.push(newValue === 1 ? ROUTES.DOWNLOAD : ROUTES.UPLOAD) navigate(newValue === 1 ? ROUTES.DOWNLOAD : ROUTES.UPLOAD)
} }
return ( return (
+45 -28
View File
@@ -4,40 +4,37 @@ import { saveAs } from 'file-saver'
import JSZip from 'jszip' import JSZip from 'jszip'
import { useSnackbar } from 'notistack' import { useSnackbar } from 'notistack'
import { ReactElement, useContext, useEffect, useState } from 'react' import { ReactElement, useContext, useEffect, useState } from 'react'
import { RouteComponentProps, useHistory } from 'react-router-dom' import { useNavigate, useParams } from 'react-router-dom'
import { HistoryHeader } from '../../components/HistoryHeader' import { HistoryHeader } from '../../components/HistoryHeader'
import { Loading } from '../../components/Loading' import { Loading } from '../../components/Loading'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard' import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import config from '../../config'
import { META_FILE_NAME, PREVIEW_FILE_NAME } from '../../constants'
import { Context as BeeContext } from '../../providers/Bee' import { Context as BeeContext } from '../../providers/Bee'
import { Context as SettingsContext } from '../../providers/Settings' import { Context as SettingsContext } from '../../providers/Settings'
import { ROUTES } from '../../routes' import { ROUTES } from '../../routes'
import { convertBeeFileToBrowserFile, convertManifestToFiles } from '../../utils/file'
import { shortenHash } from '../../utils/hash'
import { determineHistoryName, HISTORY_KEYS, putHistory } from '../../utils/local-storage' import { determineHistoryName, HISTORY_KEYS, putHistory } from '../../utils/local-storage'
import { SwarmFile } from '../../utils/SwarmFile'
import { AssetPreview } from './AssetPreview' import { AssetPreview } from './AssetPreview'
import { AssetSummary } from './AssetSummary' import { AssetSummary } from './AssetSummary'
import { DownloadActionBar } from './DownloadActionBar' import { DownloadActionBar } from './DownloadActionBar'
interface MatchParams { export function Share(): ReactElement {
hash: string
}
export function Share(props: RouteComponentProps<MatchParams>): ReactElement {
const { apiUrl, beeApi } = useContext(SettingsContext) const { apiUrl, beeApi } = useContext(SettingsContext)
const { status } = useContext(BeeContext) const { status } = useContext(BeeContext)
const reference = props.match.params.hash const { hash } = useParams()
const reference = hash! // eslint-disable-line
const history = useHistory() const navigate = useNavigate()
const { enqueueSnackbar } = useSnackbar() const { enqueueSnackbar } = useSnackbar()
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [downloading, setDownloading] = useState(false) const [downloading, setDownloading] = useState(false)
const [files, setFiles] = useState<SwarmFile[]>([])
const [swarmEntries, setSwarmEntries] = useState<Record<string, string>>({}) const [swarmEntries, setSwarmEntries] = useState<Record<string, string>>({})
const [indexDocument, setIndexDocument] = useState<string | null>(null) const [indexDocument, setIndexDocument] = useState<string | null>(null)
const [notFound, setNotFound] = useState(false) const [notFound, setNotFound] = useState(false)
const [preview, setPreview] = useState<string | undefined>(undefined)
const [metadata, setMetadata] = useState<Metadata | undefined>()
async function prepare() { async function prepare() {
if (!beeApi || !status.all) { if (!beeApi || !status.all) {
@@ -54,16 +51,37 @@ export function Share(props: RouteComponentProps<MatchParams>): ReactElement {
return return
} }
const entries = await manifestJs.getHashes(reference) const entries = await manifestJs.getHashes(reference)
setSwarmEntries(entries)
const indexDocument = await manifestJs.getIndexDocumentPath(reference) const indexDocument = await manifestJs.getIndexDocumentPath(reference)
setIndexDocument(indexDocument) setIndexDocument(indexDocument)
if (Object.keys(entries).length === 1) { const previewFile = entries[PREVIEW_FILE_NAME]
const response = await beeApi.downloadFile(reference)
setFiles([new SwarmFile(convertBeeFileToBrowserFile(response) as File)]) delete entries[META_FILE_NAME]
} else { delete entries[PREVIEW_FILE_NAME]
setFiles(convertManifestToFiles(entries)) setSwarmEntries(entries)
const count = Object.keys(entries).length
let metadata: Metadata | undefined = {
hash,
size: 0,
type: count > 1 ? 'folder' : 'unknown',
name: reference,
isWebsite: Boolean(indexDocument) && count > 1,
count,
} }
try {
const mtdt = await beeApi.downloadFile(reference, META_FILE_NAME)
const remoteMetadata = mtdt.data.text()
metadata = { ...metadata, ...(JSON.parse(remoteMetadata) as Metadata) }
} catch (e) {} // eslint-disable-line no-empty
if (previewFile) {
setPreview(`${config.BEE_API_HOST}/bzz/${reference}/${PREVIEW_FILE_NAME}`)
}
setMetadata(metadata)
} }
function onOpen() { function onOpen() {
@@ -71,16 +89,17 @@ export function Share(props: RouteComponentProps<MatchParams>): ReactElement {
} }
function onClose() { function onClose() {
// POP means there is no history - nowhere to go back yet if (navigate.length > 0) {
if (history.action === 'POP') { // There is at least one different route in browser history that we can return to
history.push(ROUTES.UPLOAD) navigate(-1)
} else { } else {
history.goBack() // This is the first page user opened, navigate to upload page instead of going back
navigate(ROUTES.UPLOAD)
} }
} }
function onUpdateFeed() { function onUpdateFeed() {
history.push(ROUTES.FEEDS_UPDATE.replace(':hash', reference)) navigate(ROUTES.FEEDS_UPDATE.replace(':hash', reference))
} }
useEffect(() => { useEffect(() => {
@@ -111,8 +130,6 @@ export function Share(props: RouteComponentProps<MatchParams>): ReactElement {
setDownloading(false) setDownloading(false)
} }
const assetName = shortenHash(reference)
if (!status.all) return <TroubleshootConnectionCard /> if (!status.all) return <TroubleshootConnectionCard />
if (loading) { if (loading) {
@@ -131,17 +148,17 @@ export function Share(props: RouteComponentProps<MatchParams>): ReactElement {
return ( return (
<> <>
<Box mb={4}> <Box mb={4}>
<AssetPreview files={files} assetName={assetName} /> <AssetPreview metadata={metadata} previewUri={preview} />
</Box> </Box>
<Box mb={4}> <Box mb={4}>
<AssetSummary files={files} hash={reference} /> <AssetSummary isWebsite={metadata?.isWebsite} hash={reference} />
</Box> </Box>
<DownloadActionBar <DownloadActionBar
onOpen={onOpen} onOpen={onOpen}
onCancel={onClose} onCancel={onClose}
onDownload={onDownload} onDownload={onDownload}
onUpdateFeed={onUpdateFeed} onUpdateFeed={onUpdateFeed}
hasIndexDocument={Boolean(indexDocument && files.length > 1)} hasIndexDocument={Boolean(metadata?.isWebsite)}
loading={downloading} loading={downloading}
/> />
</> </>
+61 -13
View File
@@ -1,18 +1,18 @@
import { Box } from '@material-ui/core' import { Box } from '@material-ui/core'
import { useSnackbar } from 'notistack' import { useSnackbar } from 'notistack'
import { ReactElement, useContext, useEffect, useState } from 'react' import { ReactElement, useContext, useEffect, useState } from 'react'
import { useHistory } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { DocumentationText } from '../../components/DocumentationText' import { DocumentationText } from '../../components/DocumentationText'
import { HistoryHeader } from '../../components/HistoryHeader' import { HistoryHeader } from '../../components/HistoryHeader'
import { ProgressIndicator } from '../../components/ProgressIndicator' import { ProgressIndicator } from '../../components/ProgressIndicator'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard' import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { Context as BeeContext } from '../../providers/Bee' import { CheckState, Context as BeeContext } from '../../providers/Bee'
import { Context as IdentityContext, Identity } from '../../providers/Feeds' import { Context as IdentityContext, Identity } from '../../providers/Feeds'
import { Context as FileContext } from '../../providers/File' import { Context as FileContext } from '../../providers/File'
import { Context as SettingsContext } from '../../providers/Settings' import { Context as SettingsContext } from '../../providers/Settings'
import { Context as StampsContext, EnrichedPostageBatch } from '../../providers/Stamps' import { Context as StampsContext, EnrichedPostageBatch } from '../../providers/Stamps'
import { ROUTES } from '../../routes' import { ROUTES } from '../../routes'
import { detectIndexHtml, getAssetNameFromFiles } from '../../utils/file' import { detectIndexHtml, getAssetNameFromFiles, packageFile } from '../../utils/file'
import { persistIdentity, updateFeed } from '../../utils/identity' import { persistIdentity, updateFeed } from '../../utils/identity'
import { HISTORY_KEYS, putHistory } from '../../utils/local-storage' import { HISTORY_KEYS, putHistory } from '../../utils/local-storage'
import { FeedPasswordDialog } from '../feeds/FeedPasswordDialog' import { FeedPasswordDialog } from '../feeds/FeedPasswordDialog'
@@ -21,6 +21,7 @@ import { PostageStampSelector } from '../stamps/PostageStampSelector'
import { AssetPreview } from './AssetPreview' import { AssetPreview } from './AssetPreview'
import { StampPreview } from './StampPreview' import { StampPreview } from './StampPreview'
import { UploadActionBar } from './UploadActionBar' import { UploadActionBar } from './UploadActionBar'
import { META_FILE_NAME, PREVIEW_FILE_NAME } from '../../constants'
export function Upload(): ReactElement { export function Upload(): ReactElement {
const [step, setStep] = useState(0) const [step, setStep] = useState(0)
@@ -31,22 +32,22 @@ export function Upload(): ReactElement {
const { refresh } = useContext(StampsContext) const { refresh } = useContext(StampsContext)
const { beeApi } = useContext(SettingsContext) const { beeApi } = useContext(SettingsContext)
const { files, setFiles, uploadOrigin } = useContext(FileContext) const { files, setFiles, uploadOrigin, metadata, previewUri, previewBlob } = useContext(FileContext)
const { identities, setIdentities } = useContext(IdentityContext) const { identities, setIdentities } = useContext(IdentityContext)
const { status } = useContext(BeeContext) const { status } = useContext(BeeContext)
const { enqueueSnackbar } = useSnackbar() const { enqueueSnackbar } = useSnackbar()
const history = useHistory() const navigate = useNavigate()
useEffect(() => { useEffect(() => {
refresh() refresh()
}, []) // eslint-disable-line react-hooks/exhaustive-deps }, []) // eslint-disable-line react-hooks/exhaustive-deps
if (!status.all) return <TroubleshootConnectionCard /> if (status.all === CheckState.ERROR) return <TroubleshootConnectionCard />
if (!files.length) { if (!files.length) {
setFiles([]) setFiles([])
history.replace(ROUTES.UPLOAD) navigate(ROUTES.UPLOAD, { replace: true })
return <></> return <></>
} }
@@ -66,26 +67,73 @@ export function Upload(): ReactElement {
} }
const uploadFiles = (password?: string) => { const uploadFiles = (password?: string) => {
if (!beeApi || !files.length || !stamp) { if (!beeApi || !files.length || !stamp || !metadata) {
return return
} }
const indexDocument = files.length === 1 ? files[0].name : detectIndexHtml(files) || undefined let fls = files.map(packageFile) // Apart from packaging, this is needed to not modify the original files array as it can trigger effects
let indexDocument: string | undefined = undefined // This means we assume it's folder
if (files.length === 1) indexDocument = files[0].name
else if (files.length > 1) {
const idx = detectIndexHtml(files)
// This is a website
if (idx) {
// The website is in some directory, remove it
if (idx.commonPrefix) {
const substrStart = idx.commonPrefix.length
indexDocument = idx.indexPath.substr(substrStart)
fls = fls.map(f => {
const path = (f.path as string).substr(substrStart)
return { ...f, path, webkitRelativePath: path, fullPath: path }
})
} else {
// The website is not packed in a directory
indexDocument = idx.indexPath
}
}
}
const lastModified = files[0].lastModified
// We want to store only some metadata
const mtd: SwarmMetadata = {
name: metadata.name,
size: metadata.size,
}
// Type of the file only makes sense for a single file
if (files.length === 1) mtd.type = metadata.type
const metafile = new File([JSON.stringify(mtd)], META_FILE_NAME, {
type: 'application/json',
lastModified,
})
fls.push(packageFile(metafile))
if (previewBlob) {
const previewFile = new File([previewBlob], PREVIEW_FILE_NAME, {
type: 'image/jpeg',
lastModified,
})
fls.push(packageFile(previewFile))
}
setUploading(true) setUploading(true)
beeApi beeApi
.uploadFiles(stamp.batchID, files as unknown as File[], { indexDocument }) .uploadFiles(stamp.batchID, fls, { indexDocument })
.then(hash => { .then(hash => {
putHistory(HISTORY_KEYS.UPLOAD_HISTORY, hash.reference, getAssetNameFromFiles(files)) putHistory(HISTORY_KEYS.UPLOAD_HISTORY, hash.reference, getAssetNameFromFiles(files))
if (uploadOrigin.origin === 'UPLOAD') { if (uploadOrigin.origin === 'UPLOAD') {
history.replace(ROUTES.HASH.replace(':hash', hash.reference)) navigate(ROUTES.HASH.replace(':hash', hash.reference), { replace: true })
} else { } else {
updateFeed(beeApi, identity as Identity, hash.reference, stamp.batchID, password as string).then(() => { updateFeed(beeApi, identity as Identity, hash.reference, stamp.batchID, password as string).then(() => {
persistIdentity(identities, identity as Identity) persistIdentity(identities, identity as Identity)
setIdentities([...identities]) setIdentities([...identities])
history.replace(ROUTES.FEEDS_PAGE.replace(':uuid', uploadOrigin.uuid as string)) navigate(ROUTES.FEEDS_PAGE.replace(':uuid', uploadOrigin.uuid as string), { replace: true })
}) })
} }
}) })
@@ -121,7 +169,7 @@ export function Upload(): ReactElement {
<Box mb={4}> <Box mb={4}>
<ProgressIndicator steps={['Preview', 'Add postage stamp', 'Upload to node']} index={step} /> <ProgressIndicator steps={['Preview', 'Add postage stamp', 'Upload to node']} index={step} />
</Box> </Box>
{(step === 0 || step === 2) && <AssetPreview files={files} />} {(step === 0 || step === 2) && <AssetPreview metadata={metadata} previewUri={previewUri} />}
{step === 1 && ( {step === 1 && (
<> <>
<Box mb={2}> <Box mb={2}>
+6 -7
View File
@@ -3,13 +3,12 @@ import { DropzoneArea } from 'material-ui-dropzone'
import { useSnackbar } from 'notistack' import { useSnackbar } from 'notistack'
import { ReactElement, useContext, useState } from 'react' import { ReactElement, useContext, useState } from 'react'
import { FilePlus, FolderPlus, PlusCircle } from 'react-feather' import { FilePlus, FolderPlus, PlusCircle } from 'react-feather'
import { useHistory } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { DocumentationText } from '../../components/DocumentationText' import { DocumentationText } from '../../components/DocumentationText'
import { SwarmButton } from '../../components/SwarmButton' import { SwarmButton } from '../../components/SwarmButton'
import { Context, UploadOrigin } from '../../providers/File' import { Context, UploadOrigin } from '../../providers/File'
import { ROUTES } from '../../routes' import { ROUTES } from '../../routes'
import { detectIndexHtml } from '../../utils/file' import { detectIndexHtml } from '../../utils/file'
import { SwarmFile } from '../../utils/SwarmFile'
interface Props { interface Props {
uploadOrigin: UploadOrigin uploadOrigin: UploadOrigin
@@ -51,7 +50,7 @@ const useStyles = makeStyles((theme: Theme) =>
export function UploadArea({ uploadOrigin, showHelp }: Props): ReactElement { export function UploadArea({ uploadOrigin, showHelp }: Props): ReactElement {
const { setFiles, setUploadOrigin } = useContext(Context) const { setFiles, setUploadOrigin } = useContext(Context)
const classes = useStyles() const classes = useStyles()
const history = useHistory() const navigate = useNavigate()
const { enqueueSnackbar } = useSnackbar() const { enqueueSnackbar } = useSnackbar()
const [strictWebsiteMode, setStrictWebsiteMode] = useState(false) const [strictWebsiteMode, setStrictWebsiteMode] = useState(false)
const [version, setVersion] = useState(0) const [version, setVersion] = useState(0)
@@ -99,8 +98,8 @@ export function UploadArea({ uploadOrigin, showHelp }: Props): ReactElement {
const handleChange = (files?: File[]) => { const handleChange = (files?: File[]) => {
if (files) { if (files) {
const swarmFiles = files.map(x => new SwarmFile(x)) const FilePaths = files as FilePath[]
const indexDocument = files.length === 1 ? files[0].name : detectIndexHtml(swarmFiles) || undefined const indexDocument = files.length === 1 ? files[0].name : detectIndexHtml(FilePaths) || undefined
if (files.length && strictWebsiteMode && !indexDocument) { if (files.length && strictWebsiteMode && !indexDocument) {
enqueueSnackbar('To upload a website, there must be an index.html or index.htm in the root of the folder.', { enqueueSnackbar('To upload a website, there must be an index.html or index.htm in the root of the folder.', {
@@ -111,11 +110,11 @@ export function UploadArea({ uploadOrigin, showHelp }: Props): ReactElement {
return return
} }
setFiles(swarmFiles) setFiles(FilePaths)
if (files.length) { if (files.length) {
setUploadOrigin(uploadOrigin) setUploadOrigin(uploadOrigin)
history.push(ROUTES.UPLOAD_IN_PROGRESS) navigate(ROUTES.UPLOAD_IN_PROGRESS)
} }
} }
} }
+4 -2
View File
@@ -2,7 +2,7 @@ import { ReactElement, useContext } from 'react'
import { Button } from '@material-ui/core' import { Button } from '@material-ui/core'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard' import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { Context as BeeContext } from '../../providers/Bee' import { CheckState, Context as BeeContext } from '../../providers/Bee'
import ExpandableList from '../../components/ExpandableList' import ExpandableList from '../../components/ExpandableList'
import ExpandableListItem from '../../components/ExpandableListItem' import ExpandableListItem from '../../components/ExpandableListItem'
import ExpandableListItemKey from '../../components/ExpandableListItemKey' import ExpandableListItemKey from '../../components/ExpandableListItemKey'
@@ -17,13 +17,15 @@ export default function Status(): ReactElement {
topology, topology,
nodeAddresses, nodeAddresses,
chequebookAddress, chequebookAddress,
nodeInfo,
} = useContext(BeeContext) } = useContext(BeeContext)
if (!status.all) return <TroubleshootConnectionCard /> if (status.all === CheckState.ERROR) return <TroubleshootConnectionCard />
return ( return (
<div> <div>
<ExpandableList label="Bee Node" defaultOpen> <ExpandableList label="Bee Node" defaultOpen>
<ExpandableListItem label="Mode" value={nodeInfo?.beeMode} />
<ExpandableListItem <ExpandableListItem
label="Agent" label="Agent"
value={ value={
+3 -3
View File
@@ -1,14 +1,14 @@
import { ReactElement } from 'react' import { ReactElement } from 'react'
import { useHistory } from 'react-router' import { useNavigate } from 'react-router'
import { HistoryHeader } from '../../components/HistoryHeader' import { HistoryHeader } from '../../components/HistoryHeader'
import { ROUTES } from '../../routes' import { ROUTES } from '../../routes'
import { PostageStampCreation } from './PostageStampCreation' import { PostageStampCreation } from './PostageStampCreation'
export function CreatePostageStampPage(): ReactElement { export function CreatePostageStampPage(): ReactElement {
const history = useHistory() const navigate = useNavigate()
function onFinished() { function onFinished() {
history.push(ROUTES.STAMPS) navigate(ROUTES.STAMPS)
} }
return ( return (
+21 -17
View File
@@ -2,19 +2,14 @@ import { Box, Grid, Typography } from '@material-ui/core'
import BigNumber from 'bignumber.js' import BigNumber from 'bignumber.js'
import { Form, Formik, FormikHelpers } from 'formik' import { Form, Formik, FormikHelpers } from 'formik'
import { useSnackbar } from 'notistack' import { useSnackbar } from 'notistack'
import React, { ReactElement, useContext } from 'react' import { ReactElement, useContext } from 'react'
import { Check } from 'react-feather' import { Check } from 'react-feather'
import { SwarmButton } from '../../components/SwarmButton' import { SwarmButton } from '../../components/SwarmButton'
import { SwarmTextInput } from '../../components/SwarmTextInput' import { SwarmTextInput } from '../../components/SwarmTextInput'
import { Context as BeeContext } from '../../providers/Bee'
import { Context as SettingsContext } from '../../providers/Settings' import { Context as SettingsContext } from '../../providers/Settings'
import { Context } from '../../providers/Stamps' import { Context as StampsContext } from '../../providers/Stamps'
import { import { calculateStampPrice, convertAmountToSeconds, convertDepthToBytes, secondsToTimeString } from '../../utils'
calculateStampPrice,
convertAmountToSeconds,
convertDepthToBytes,
formatBzz,
secondsToTimeString,
} from '../../utils'
import { getHumanReadableFileSize } from '../../utils/file' import { getHumanReadableFileSize } from '../../utils/file'
interface FormValues { interface FormValues {
@@ -34,8 +29,10 @@ interface Props {
} }
export function PostageStampCreation({ onFinished }: Props): ReactElement { export function PostageStampCreation({ onFinished }: Props): ReactElement {
const { refresh } = useContext(Context) const { chainState } = useContext(BeeContext)
const { refresh } = useContext(StampsContext)
const { beeDebugApi } = useContext(SettingsContext) const { beeDebugApi } = useContext(SettingsContext)
const { enqueueSnackbar } = useSnackbar() const { enqueueSnackbar } = useSnackbar()
function getFileSize(depth: number): string { function getFileSize(depth: number): string {
@@ -47,20 +44,27 @@ export function PostageStampCreation({ onFinished }: Props): ReactElement {
} }
function getTtl(amount: number): string { function getTtl(amount: number): string {
if (isNaN(amount) || amount <= 0) { const isCurrentPriceAvailable = chainState && chainState.currentPrice
if (amount <= 0 || !isCurrentPriceAvailable) {
return '-' return '-'
} }
return secondsToTimeString(convertAmountToSeconds(amount)) const pricePerBlock = Number.parseInt(chainState.currentPrice, 10)
return `${secondsToTimeString(convertAmountToSeconds(amount, pricePerBlock))} (with price of 0 per block)`
} }
function getPrice(depth: number, amount: number): string { function getPrice(depth: number, amount: bigint): string {
if (isNaN(amount) || amount <= 0 || isNaN(depth) || depth < 17 || depth > 255) { const hasInvalidInput = amount <= 0 || isNaN(depth) || depth < 17 || depth > 255
if (hasInvalidInput) {
return '-' return '-'
} }
const price = calculateStampPrice(depth, amount) const price = calculateStampPrice(depth, amount)
return `${formatBzz(price)} BZZ` return `${price.toSignificantDigits()} BZZ`
} }
return ( return (
@@ -129,7 +133,7 @@ export function PostageStampCreation({ onFinished }: Props): ReactElement {
<Box mt={0.25} sx={{ bgcolor: '#f6f6f6' }} p={2}> <Box mt={0.25} sx={{ bgcolor: '#f6f6f6' }} p={2}>
<Grid container justifyContent="space-between"> <Grid container justifyContent="space-between">
<Typography>Corresponding TTL (Time to live)</Typography> <Typography>Corresponding TTL (Time to live)</Typography>
<Typography>{getTtl(parseInt(values.amount || '0', 10))}</Typography> <Typography>{getTtl(Number.parseInt(values.amount || '0', 10))}</Typography>
</Grid> </Grid>
</Box> </Box>
</Box> </Box>
@@ -139,7 +143,7 @@ export function PostageStampCreation({ onFinished }: Props): ReactElement {
<Box mb={4} sx={{ bgcolor: '#fcf2e8' }} p={2}> <Box mb={4} sx={{ bgcolor: '#fcf2e8' }} p={2}>
<Grid container justifyContent="space-between"> <Grid container justifyContent="space-between">
<Typography>Indicative Price</Typography> <Typography>Indicative Price</Typography>
<Typography>{getPrice(parseInt(values.depth || '0', 10), parseInt(values.amount || '0', 10))}</Typography> <Typography>{getPrice(parseInt(values.depth || '0', 10), BigInt(values.amount || '0'))}</Typography>
</Grid> </Grid>
</Box> </Box>
<SwarmButton <SwarmButton
+5 -5
View File
@@ -2,10 +2,10 @@ import { CircularProgress, Container } from '@material-ui/core'
import { createStyles, makeStyles } from '@material-ui/core/styles' import { createStyles, makeStyles } from '@material-ui/core/styles'
import { ReactElement, useContext, useEffect } from 'react' import { ReactElement, useContext, useEffect } from 'react'
import { PlusSquare } from 'react-feather' import { PlusSquare } from 'react-feather'
import { useHistory } from 'react-router' import { useNavigate } from 'react-router'
import { SwarmButton } from '../../components/SwarmButton' import { SwarmButton } from '../../components/SwarmButton'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard' import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { Context as BeeContext } from '../../providers/Bee' import { CheckState, Context as BeeContext } from '../../providers/Bee'
import { Context as StampsContext } from '../../providers/Stamps' import { Context as StampsContext } from '../../providers/Stamps'
import { ROUTES } from '../../routes' import { ROUTES } from '../../routes'
import StampsTable from './StampsTable' import StampsTable from './StampsTable'
@@ -29,7 +29,7 @@ const useStyles = makeStyles(() =>
export default function Stamp(): ReactElement { export default function Stamp(): ReactElement {
const classes = useStyles() const classes = useStyles()
const history = useHistory() const navigate = useNavigate()
const { stamps, isLoading, error, start, stop } = useContext(StampsContext) const { stamps, isLoading, error, start, stop } = useContext(StampsContext)
const { status } = useContext(BeeContext) const { status } = useContext(BeeContext)
@@ -41,10 +41,10 @@ export default function Stamp(): ReactElement {
return () => stop() return () => stop()
}, [status]) // eslint-disable-line react-hooks/exhaustive-deps }, [status]) // eslint-disable-line react-hooks/exhaustive-deps
if (!status.all) return <TroubleshootConnectionCard /> if (status.all === CheckState.ERROR) return <TroubleshootConnectionCard />
function navigateToNewStamp() { function navigateToNewStamp() {
history.push(ROUTES.STAMPS_NEW) navigate(ROUTES.STAMPS_NEW)
} }
return ( return (
@@ -1,39 +1,59 @@
import { useContext } from 'react' import { useContext } from 'react'
import DepositModal from '../../../containers/DepositModal' import DepositModal from '../../../containers/DepositModal'
import type { ReactElement } from 'react' import type { ReactElement, ReactNode } from 'react'
import ExpandableList from '../../../components/ExpandableList' import ExpandableList from '../../../components/ExpandableList'
import ExpandableListItemKey from '../../../components/ExpandableListItemKey' import ExpandableListItemKey from '../../../components/ExpandableListItemKey'
import ExpandableListItemActions from '../../../components/ExpandableListItemActions' import ExpandableListItemActions from '../../../components/ExpandableListItemActions'
import ExpandableListItemNote from '../../../components/ExpandableListItemNote' import ExpandableListItemNote from '../../../components/ExpandableListItemNote'
import StatusIcon from '../../../components/StatusIcon' import StatusIcon from '../../../components/StatusIcon'
import { Context } from '../../../providers/Bee' import { CheckState, Context } from '../../../providers/Bee'
const ChequebookDeployFund = (): ReactElement | null => { const ChequebookDeployFund = (): ReactElement | null => {
const { status, isLoading, chequebookAddress } = useContext(Context) const { status, isLoading, chequebookAddress } = useContext(Context)
const isOk = status.chequebook const { checkState, isEnabled } = status.chequebook
if (!isEnabled) return null
let text: ReactNode
switch (checkState) {
case CheckState.OK:
text = 'Your chequebook is deployed and funded'
break
case CheckState.WARNING:
text = (
<>
Your chequebook is not funded. Please deposit some xBZZ to your chequebook address. You may need to aquire BZZ
(e.g. <a href="https://bzz.exchange/">bzz.exchange</a>) and bridge it to the xDai network through the{' '}
<a href="https://omni.xdaichain.com/bridge">omni bridge</a>. To pay the transaction fees, you will also need
xDAI token. You can purchase DAI on the network and bridge it to xDai network through the{' '}
<a href="https://bridge.xdaichain.com/">xDai Bridge</a>. See the{' '}
<a href="https://www.xdaichain.com/#xdai-stable-chain">official xDai website</a> for more information.
</>
)
break
default:
text = (
<>
Your chequebook is either not deployed nor funded. To run the node you will need xDAI and xBZZ on the xDai
network. You may need to aquire BZZ (e.g. <a href="https://bzz.exchange/">bzz.exchange</a>) and bridge it to
the xDai network through the <a href="https://omni.xdaichain.com/bridge">omni bridge</a>. To pay the
transaction fees, you will also need xDAI token. You can purchase DAI on the network and bridge it to xDai
network through the <a href="https://bridge.xdaichain.com/">xDai Bridge</a>. See the{' '}
<a href="https://www.xdaichain.com/#xdai-stable-chain">official xDai website</a> for more information.
</>
)
}
return ( return (
<ExpandableList <ExpandableList
label={ label={
<> <>
<StatusIcon isOk={isOk} isLoading={isLoading} /> Chequebook Deployment & Funding <StatusIcon checkState={checkState} isLoading={isLoading} /> Chequebook Deployment & Funding
</> </>
} }
> >
<ExpandableListItemNote> <ExpandableListItemNote>{text}</ExpandableListItemNote>
{isOk ? (
'Your chequebook is deployed and funded'
) : (
<>
Your chequebook is either not deployed or funded. To run the node you will need xDAI and xBZZ on the xDai
network. You may need to aquire BZZ (e.g. <a href="https://bzz.exchange/">bzz.exchange</a>) and bridge it to
the xDai network through the <a href="https://omni.xdaichain.com/bridge">omni bridge</a>. To pay the
transaction fees, you will also need xDAI token. You can purchase DAI on the network and bridge it to xDai
network through the <a href="https://bridge.xdaichain.com/">xDai Bridge</a>. See the{' '}
<a href="https://www.xdaichain.com/#xdai-stable-chain">official xDai website</a> for more information.
</>
)}
</ExpandableListItemNote>
{chequebookAddress && ( {chequebookAddress && (
<> <>
<ExpandableListItemKey label="Chequebook Address" value={chequebookAddress.chequebookAddress} /> <ExpandableListItemKey label="Chequebook Address" value={chequebookAddress.chequebookAddress} />
@@ -6,30 +6,32 @@ import ExpandableListItem from '../../../components/ExpandableListItem'
import ExpandableListItemInput from '../../../components/ExpandableListItemInput' import ExpandableListItemInput from '../../../components/ExpandableListItemInput'
import ExpandableListItemNote from '../../../components/ExpandableListItemNote' import ExpandableListItemNote from '../../../components/ExpandableListItemNote'
import StatusIcon from '../../../components/StatusIcon' import StatusIcon from '../../../components/StatusIcon'
import { Context } from '../../../providers/Bee' import { CheckState, Context } from '../../../providers/Bee'
import { Context as SettingsContext } from '../../../providers/Settings' import { Context as SettingsContext } from '../../../providers/Settings'
export default function NodeConnectionCheck(): ReactElement | null { export default function NodeConnectionCheck(): ReactElement | null {
const { status, isLoading } = useContext(Context) const { status, isLoading } = useContext(Context)
const { setDebugApiUrl, apiDebugUrl } = useContext(SettingsContext) const { setDebugApiUrl, apiDebugUrl } = useContext(SettingsContext)
const isOk = status.debugApiConnection const { checkState, isEnabled } = status.debugApiConnection
if (!isEnabled) return null
return ( return (
<ExpandableList <ExpandableList
label={ label={
<> <>
<StatusIcon isOk={isOk} isLoading={isLoading} /> Connection to Bee Debug API <StatusIcon checkState={checkState} isLoading={isLoading} /> Connection to Bee Debug API
</> </>
} }
> >
<ExpandableListItemNote> <ExpandableListItemNote>
{isOk {checkState === CheckState.OK
? 'The connection to the Bee nodes debug API has been successful' ? 'The connection to the Bee nodes debug API has been successful'
: 'We cannot connect to your nodes debug API. Please check the following to troubleshoot your issue.'} : 'We cannot connect to your nodes debug API. Please check the following to troubleshoot your issue.'}
</ExpandableListItemNote> </ExpandableListItemNote>
<ExpandableListItemInput label="Bee Debug API" value={apiDebugUrl} onConfirm={setDebugApiUrl} /> <ExpandableListItemInput label="Bee Debug API" value={apiDebugUrl} onConfirm={setDebugApiUrl} />
{!isOk && ( {checkState === CheckState.ERROR && (
<ExpandableList level={1} label="Troubleshoot"> <ExpandableList level={1} label="Troubleshoot">
<ExpandableListItem <ExpandableListItem
label={ label={
@@ -3,22 +3,24 @@ import ExpandableList from '../../../components/ExpandableList'
import ExpandableListItemKey from '../../../components/ExpandableListItemKey' import ExpandableListItemKey from '../../../components/ExpandableListItemKey'
import ExpandableListItemNote from '../../../components/ExpandableListItemNote' import ExpandableListItemNote from '../../../components/ExpandableListItemNote'
import StatusIcon from '../../../components/StatusIcon' import StatusIcon from '../../../components/StatusIcon'
import { Context } from '../../../providers/Bee' import { CheckState, Context } from '../../../providers/Bee'
export default function EthereumConnectionCheck(): ReactElement | null { export default function EthereumConnectionCheck(): ReactElement | null {
const { status, isLoading, nodeAddresses } = useContext(Context) const { status, isLoading, nodeAddresses } = useContext(Context)
const isOk = status.blockchainConnection const { checkState, isEnabled } = status.blockchainConnection
if (!isEnabled) return null
return ( return (
<ExpandableList <ExpandableList
label={ label={
<> <>
<StatusIcon isOk={isOk} isLoading={isLoading} /> Connection to Blockchain <StatusIcon checkState={checkState} isLoading={isLoading} /> Connection to Blockchain
</> </>
} }
> >
<ExpandableListItemNote> <ExpandableListItemNote>
{isOk ? ( {checkState === CheckState.OK ? (
'Your node is connected to the xDai blockchain' 'Your node is connected to the xDai blockchain'
) : ( ) : (
<> <>
@@ -7,28 +7,30 @@ import ExpandableListItem from '../../../components/ExpandableListItem'
import ExpandableListItemNote from '../../../components/ExpandableListItemNote' import ExpandableListItemNote from '../../../components/ExpandableListItemNote'
import ExpandableListItemInput from '../../../components/ExpandableListItemInput' import ExpandableListItemInput from '../../../components/ExpandableListItemInput'
import StatusIcon from '../../../components/StatusIcon' import StatusIcon from '../../../components/StatusIcon'
import { Context } from '../../../providers/Bee' import { CheckState, Context } from '../../../providers/Bee'
export default function NodeConnectionCheck(): ReactElement | null { export default function NodeConnectionCheck(): ReactElement | null {
const { setApiUrl, apiUrl } = useContext(SettingsContext) const { setApiUrl, apiUrl } = useContext(SettingsContext)
const { status, isLoading } = useContext(Context) const { status, isLoading } = useContext(Context)
const isOk = status.apiConnection const { isEnabled, checkState } = status.apiConnection
if (!isEnabled) return null
return ( return (
<ExpandableList <ExpandableList
label={ label={
<> <>
<StatusIcon isOk={isOk} isLoading={isLoading} /> Connection to Bee API <StatusIcon checkState={checkState} isLoading={isLoading} /> Connection to Bee API
</> </>
} }
> >
<ExpandableListItemNote> <ExpandableListItemNote>
{isOk {checkState === CheckState.OK
? 'The connection to the Bee nodes API has been successful' ? 'The connection to the Bee nodes API has been successful'
: 'Could not connect to your Bee nodes API. Please check the troubleshoot below on how you may resolve it.'} : 'Could not connect to your Bee nodes API. Please check the troubleshoot below on how you may resolve it.'}
</ExpandableListItemNote> </ExpandableListItemNote>
<ExpandableListItemInput label="Bee API" value={apiUrl} onConfirm={setApiUrl} /> <ExpandableListItemInput label="Bee API" value={apiUrl} onConfirm={setApiUrl} />
{!isOk && ( {checkState === CheckState.ERROR && (
<ExpandableList level={1} label="Troubleshoot"> <ExpandableList level={1} label="Troubleshoot">
<ExpandableListItem <ExpandableListItem
label={ label={
+19 -9
View File
@@ -1,27 +1,37 @@
import { ReactElement, useContext } from 'react' import { ReactElement, ReactNode, useContext } from 'react'
import ExpandableList from '../../../components/ExpandableList' import ExpandableList from '../../../components/ExpandableList'
import ExpandableListItemNote from '../../../components/ExpandableListItemNote' import ExpandableListItemNote from '../../../components/ExpandableListItemNote'
import TopologyStats from '../../../components/TopologyStats' import TopologyStats from '../../../components/TopologyStats'
import StatusIcon from '../../../components/StatusIcon' import StatusIcon from '../../../components/StatusIcon'
import { Context } from '../../../providers/Bee' import { CheckState, Context } from '../../../providers/Bee'
export default function PeerConnection(): ReactElement | null { export default function PeerConnection(): ReactElement | null {
const { status, isLoading, topology } = useContext(Context) const { status, isLoading, topology } = useContext(Context)
const isOk = status.topology const { isEnabled, checkState } = status.topology
if (!isEnabled) return null
let text: ReactNode
switch (checkState) {
case CheckState.OK:
text = 'You are connected to other Bee nodes'
break
// Both error state and warning state
default:
text =
'Your node is not connected to any peers. Please wait a bit if you just started the node, otherwise review your configuration file.'
}
return ( return (
<ExpandableList <ExpandableList
label={ label={
<> <>
<StatusIcon isOk={isOk} isLoading={isLoading} /> Connection to Peers <StatusIcon checkState={checkState} isLoading={isLoading} /> Connection to Peers
</> </>
} }
> >
<ExpandableListItemNote> <ExpandableListItemNote>{text}</ExpandableListItemNote>
{isOk
? 'You are connected to other Bee nodes'
: 'Your node is not connected to any peers. Please wait a bit if you just started the node, otherwise review your configuration file.'}
</ExpandableListItemNote>
<TopologyStats topology={topology} /> <TopologyStats topology={topology} />
</ExpandableList> </ExpandableList>
+6 -4
View File
@@ -4,22 +4,24 @@ import ExpandableList from '../../../components/ExpandableList'
import ExpandableListItem from '../../../components/ExpandableListItem' import ExpandableListItem from '../../../components/ExpandableListItem'
import ExpandableListItemNote from '../../../components/ExpandableListItemNote' import ExpandableListItemNote from '../../../components/ExpandableListItemNote'
import StatusIcon from '../../../components/StatusIcon' import StatusIcon from '../../../components/StatusIcon'
import { Context } from '../../../providers/Bee' import { CheckState, Context } from '../../../providers/Bee'
export default function VersionCheck(): ReactElement | null { export default function VersionCheck(): ReactElement | null {
const { status, isLoading, latestUserVersion, latestPublishedVersion, latestBeeVersionUrl } = useContext(Context) const { status, isLoading, latestUserVersion, latestPublishedVersion, latestBeeVersionUrl } = useContext(Context)
const isOk = status.version const { isEnabled, checkState } = status.version
if (!isEnabled) return null
return ( return (
<ExpandableList <ExpandableList
label={ label={
<> <>
<StatusIcon isOk={isOk} isLoading={isLoading} /> Bee Version <StatusIcon checkState={checkState} isLoading={isLoading} /> Bee Version
</> </>
} }
> >
<ExpandableListItemNote> <ExpandableListItemNote>
{isOk ? ( {checkState === CheckState.OK ? (
'You are running the latest version of Bee.' 'You are running the latest version of Bee.'
) : ( ) : (
<> <>
+98 -40
View File
@@ -1,11 +1,13 @@
import type { import {
ChainState,
ChequebookAddressResponse, ChequebookAddressResponse,
Health, Health,
LastChequesResponse, LastChequesResponse,
NodeAddresses, NodeAddresses,
NodesInfo, NodeInfo,
Peer, Peer,
Topology, Topology,
BeeModes,
} from '@ethersphere/bee-js' } from '@ethersphere/bee-js'
import { createContext, ReactChild, ReactElement, useContext, useEffect, useState } from 'react' import { createContext, ReactChild, ReactElement, useContext, useEffect, useState } from 'react'
import semver from 'semver' import semver from 'semver'
@@ -15,14 +17,25 @@ import { Token } from '../models/Token'
import type { Balance, ChequebookBalance, Settlements } from '../types' import type { Balance, ChequebookBalance, Settlements } from '../types'
import { Context as SettingsContext } from './Settings' import { Context as SettingsContext } from './Settings'
export enum CheckState {
OK = 'OK',
WARNING = 'Warning',
ERROR = 'Error',
}
interface StatusItem {
isEnabled: boolean
checkState: CheckState
}
interface Status { interface Status {
all: boolean all: CheckState
version: boolean version: StatusItem
blockchainConnection: boolean blockchainConnection: StatusItem
debugApiConnection: boolean debugApiConnection: StatusItem
apiConnection: boolean apiConnection: StatusItem
topology: boolean topology: StatusItem
chequebook: boolean chequebook: StatusItem
} }
interface ContextInterface { interface ContextInterface {
@@ -36,7 +49,7 @@ interface ContextInterface {
apiHealth: boolean apiHealth: boolean
debugApiHealth: Health | null debugApiHealth: Health | null
nodeAddresses: NodeAddresses | null nodeAddresses: NodeAddresses | null
nodeInfo: NodesInfo | null nodeInfo: NodeInfo | null
topology: Topology | null topology: Topology | null
chequebookAddress: ChequebookAddressResponse | null chequebookAddress: ChequebookAddressResponse | null
peers: Peer[] | null peers: Peer[] | null
@@ -44,6 +57,7 @@ interface ContextInterface {
peerBalances: Balance[] | null peerBalances: Balance[] | null
peerCheques: LastChequesResponse | null peerCheques: LastChequesResponse | null
settlements: Settlements | null settlements: Settlements | null
chainState: ChainState | null
latestBeeRelease: LatestBeeRelease | null latestBeeRelease: LatestBeeRelease | null
isLoading: boolean isLoading: boolean
isRefreshing: boolean isRefreshing: boolean
@@ -53,17 +67,15 @@ interface ContextInterface {
refresh: () => Promise<void> refresh: () => Promise<void>
} }
const startedInDevMode = window.location.search.includes('devMode=1')
const initialValues: ContextInterface = { const initialValues: ContextInterface = {
status: { status: {
all: false, all: CheckState.ERROR,
version: false, version: { isEnabled: false, checkState: CheckState.ERROR },
blockchainConnection: false, blockchainConnection: { isEnabled: false, checkState: CheckState.ERROR },
debugApiConnection: false, debugApiConnection: { isEnabled: false, checkState: CheckState.ERROR },
apiConnection: false, apiConnection: { isEnabled: false, checkState: CheckState.ERROR },
topology: false, topology: { isEnabled: false, checkState: CheckState.ERROR },
chequebook: false, chequebook: { isEnabled: false, checkState: CheckState.ERROR },
}, },
latestPublishedVersion: undefined, latestPublishedVersion: undefined,
latestUserVersion: undefined, latestUserVersion: undefined,
@@ -82,6 +94,7 @@ const initialValues: ContextInterface = {
peerBalances: null, peerBalances: null,
peerCheques: null, peerCheques: null,
settlements: null, settlements: null,
chainState: null,
latestBeeRelease: null, latestBeeRelease: null,
isLoading: true, isLoading: true,
isRefreshing: false, isRefreshing: false,
@@ -101,34 +114,69 @@ interface Props {
function getStatus( function getStatus(
debugApiHealth: Health | null, debugApiHealth: Health | null,
nodeAddresses: NodeAddresses | null, nodeAddresses: NodeAddresses | null,
nodeInfo: NodesInfo | null, nodeInfo: NodeInfo | null,
apiHealth: boolean, apiHealth: boolean,
topology: Topology | null, topology: Topology | null,
chequebookAddress: ChequebookAddressResponse | null, chequebookAddress: ChequebookAddressResponse | null,
chequebookBalance: ChequebookBalance | null, chequebookBalance: ChequebookBalance | null,
error: Error | null, error: Error | null,
): Status { ): Status {
// FIXME: `devMode` is a temporary workaround to be able to develop with only one node const status: Status = { ...initialValues.status }
const devMode = startedInDevMode || Boolean(process.env.REACT_APP_DEV_MODE) || nodeInfo?.beeMode === 'dev'
const status = { // Version check
version: Boolean( status.version.isEnabled = true
debugApiHealth && status.version.checkState =
semver.satisfies(debugApiHealth.version, engines.bee, { debugApiHealth &&
includePrerelease: true, semver.satisfies(debugApiHealth.version, engines.bee, {
}), includePrerelease: true,
), })
blockchainConnection: Boolean(nodeAddresses?.ethereum), ? CheckState.OK
debugApiConnection: Boolean(debugApiHealth?.status === 'ok'), : CheckState.ERROR
apiConnection: apiHealth,
topology: Boolean(topology?.connected && topology?.connected > 0) || devMode, // Blockchain connection check
chequebook: status.blockchainConnection.isEnabled = true
(Boolean(chequebookAddress?.chequebookAddress) && status.blockchainConnection.checkState = Boolean(debugApiHealth?.status === 'ok') ? CheckState.OK : CheckState.ERROR
chequebookBalance !== null &&
chequebookBalance?.totalBalance.toBigNumber.isGreaterThan(0)) || // Debug API connection check
devMode, status.debugApiConnection.isEnabled = true
status.debugApiConnection.checkState = Boolean(debugApiHealth?.status === 'ok') ? CheckState.OK : CheckState.ERROR
// API connection check
status.apiConnection.isEnabled = true
status.apiConnection.checkState = apiHealth ? CheckState.OK : CheckState.ERROR
// Topology check
if (nodeInfo && [BeeModes.FULL, BeeModes.LIGHT, BeeModes.ULTRA_LIGHT].includes(nodeInfo.beeMode)) {
status.topology.isEnabled = true
status.topology.checkState = topology?.connected && topology?.connected > 0 ? CheckState.OK : CheckState.WARNING
} }
return { ...status, all: !error && Object.values(status).every(v => v) } // Chequebook check
if (error || (nodeInfo && [BeeModes.FULL, BeeModes.LIGHT].includes(nodeInfo.beeMode))) {
status.chequebook.isEnabled = true
if (
chequebookAddress?.chequebookAddress &&
chequebookBalance !== null &&
chequebookBalance?.totalBalance.toBigNumber.isGreaterThan(0)
) {
status.chequebook.checkState = CheckState.OK
} else if (chequebookAddress?.chequebookAddress) status.chequebook.checkState = CheckState.WARNING
else status.chequebook.checkState = CheckState.OK
}
// Determine overall status
if (Object.values(status).some(({ isEnabled, checkState }) => isEnabled && checkState === CheckState.ERROR)) {
status.all = CheckState.ERROR
} else if (
Object.values(status).some(({ isEnabled, checkState }) => isEnabled && checkState === CheckState.WARNING)
) {
status.all = CheckState.WARNING
} else {
status.all = CheckState.OK
}
return status
} }
export function Provider({ children }: Props): ReactElement { export function Provider({ children }: Props): ReactElement {
@@ -136,7 +184,7 @@ export function Provider({ children }: Props): ReactElement {
const [apiHealth, setApiHealth] = useState<boolean>(false) const [apiHealth, setApiHealth] = useState<boolean>(false)
const [debugApiHealth, setDebugApiHealth] = useState<Health | null>(null) const [debugApiHealth, setDebugApiHealth] = useState<Health | null>(null)
const [nodeAddresses, setNodeAddresses] = useState<NodeAddresses | null>(null) const [nodeAddresses, setNodeAddresses] = useState<NodeAddresses | null>(null)
const [nodeInfo, setNodeInfo] = useState<NodesInfo | null>(null) const [nodeInfo, setNodeInfo] = useState<NodeInfo | null>(null)
const [topology, setNodeTopology] = useState<Topology | null>(null) const [topology, setNodeTopology] = useState<Topology | null>(null)
const [chequebookAddress, setChequebookAddress] = useState<ChequebookAddressResponse | null>(null) const [chequebookAddress, setChequebookAddress] = useState<ChequebookAddressResponse | null>(null)
const [peers, setPeers] = useState<Peer[] | null>(null) const [peers, setPeers] = useState<Peer[] | null>(null)
@@ -144,6 +192,8 @@ export function Provider({ children }: Props): ReactElement {
const [peerBalances, setPeerBalances] = useState<Balance[] | null>(null) const [peerBalances, setPeerBalances] = useState<Balance[] | null>(null)
const [peerCheques, setPeerCheques] = useState<LastChequesResponse | null>(null) const [peerCheques, setPeerCheques] = useState<LastChequesResponse | null>(null)
const [settlements, setSettlements] = useState<Settlements | null>(null) const [settlements, setSettlements] = useState<Settlements | null>(null)
const [chainState, setChainState] = useState<ChainState | null>(null)
const { latestBeeRelease } = useLatestBeeRelease() const { latestBeeRelease } = useLatestBeeRelease()
const [error, setError] = useState<Error | null>(initialValues.error) const [error, setError] = useState<Error | null>(initialValues.error)
@@ -177,6 +227,7 @@ export function Provider({ children }: Props): ReactElement {
setPeerBalances(null) setPeerBalances(null)
setPeerCheques(null) setPeerCheques(null)
setSettlements(null) setSettlements(null)
setChainState(null)
refresh() refresh()
}, [beeDebugApi]) // eslint-disable-line react-hooks/exhaustive-deps }, [beeDebugApi]) // eslint-disable-line react-hooks/exhaustive-deps
@@ -277,6 +328,12 @@ export function Provider({ children }: Props): ReactElement {
.then(setPeerCheques) .then(setPeerCheques)
.catch(() => setPeerCheques(null)), .catch(() => setPeerCheques(null)),
// Chain state
beeDebugApi
.getChainState()
.then(setChainState)
.catch(() => setChainState(null)),
// Chequebook balance // Chequebook balance
chequeBalanceWrapper() chequeBalanceWrapper()
.then(setChequebookBalance) .then(setChequebookBalance)
@@ -354,6 +411,7 @@ export function Provider({ children }: Props): ReactElement {
peerBalances, peerBalances,
peerCheques, peerCheques,
settlements, settlements,
chainState,
latestBeeRelease, latestBeeRelease,
isLoading, isLoading,
isRefreshing, isRefreshing,
+41 -6
View File
@@ -1,17 +1,22 @@
/* eslint-disable @typescript-eslint/no-empty-function */ /* eslint-disable @typescript-eslint/no-empty-function */
import { createContext, ReactChild, ReactElement, useState } from 'react' import { createContext, ReactChild, ReactElement, useState, useEffect } from 'react'
import { SwarmFile } from '../utils/SwarmFile' import { getMetadata } from '../utils/file'
import { resize } from '../utils/image'
import { PREVIEW_DIMENSIONS } from '../constants'
export type UploadOrigin = { origin: 'UPLOAD' | 'FEED'; uuid?: string } export type UploadOrigin = { origin: 'UPLOAD' | 'FEED'; uuid?: string }
export const defaultUploadOrigin: UploadOrigin = { origin: 'UPLOAD' } export const defaultUploadOrigin: UploadOrigin = { origin: 'UPLOAD' }
interface ContextInterface { interface ContextInterface {
files: SwarmFile[] files: FilePath[]
setFiles: (files: SwarmFile[]) => void setFiles: (files: FilePath[]) => void
uploadOrigin: UploadOrigin uploadOrigin: UploadOrigin
setUploadOrigin: (uploadOrigin: UploadOrigin) => void setUploadOrigin: (uploadOrigin: UploadOrigin) => void
metadata?: Metadata
previewUri?: string
previewBlob?: Blob
} }
const initialValues: ContextInterface = { const initialValues: ContextInterface = {
@@ -29,8 +34,38 @@ interface Props {
} }
export function Provider({ children }: Props): ReactElement { export function Provider({ children }: Props): ReactElement {
const [files, setFiles] = useState<SwarmFile[]>(initialValues.files) const [files, setFiles] = useState<FilePath[]>(initialValues.files)
const [uploadOrigin, setUploadOrigin] = useState<UploadOrigin>(initialValues.uploadOrigin) const [uploadOrigin, setUploadOrigin] = useState<UploadOrigin>(initialValues.uploadOrigin)
const [metadata, setMetadata] = useState<Metadata | undefined>(undefined)
const [previewUri, setPreviewUri] = useState<string | undefined>(undefined)
const [previewBlob, setPreviewBlob] = useState<Blob | undefined>(undefined)
return <Context.Provider value={{ files, setFiles, uploadOrigin, setUploadOrigin }}>{children}</Context.Provider> useEffect(() => {
setMetadata(getMetadata(files))
if (previewUri) {
URL.revokeObjectURL(previewUri) // Clear the preview from memory
setPreviewUri(undefined)
setPreviewBlob(undefined)
}
if (files.length !== 1 || !files[0].type.startsWith('image')) return
resize(files[0], PREVIEW_DIMENSIONS.maxWidth, PREVIEW_DIMENSIONS.maxHeight).then(blob => {
setPreviewUri(URL.createObjectURL(blob)) // NOTE: Until it is cleared with URL.revokeObjectURL, the file stays allocated in memory
setPreviewBlob(blob)
})
return () => {
if (previewUri) {
URL.revokeObjectURL(previewUri)
}
}
}, [files]) // eslint-disable-line react-hooks/exhaustive-deps
return (
<Context.Provider value={{ files, setFiles, uploadOrigin, setUploadOrigin, metadata, previewUri, previewBlob }}>
{children}
</Context.Provider>
)
} }
+18 -15
View File
@@ -44,35 +44,38 @@ export function Provider({
const [beeDebugApi, setBeeDebugApi] = useState<BeeDebug | null>(null) const [beeDebugApi, setBeeDebugApi] = useState<BeeDebug | null>(null)
const [lockedApiSettings] = useState<boolean>(Boolean(extLockedApiSettings)) const [lockedApiSettings] = useState<boolean>(Boolean(extLockedApiSettings))
const url = beeApiUrl || apiUrl
const debugUrl = beeDebugApiUrl || apiDebugUrl
useEffect(() => { useEffect(() => {
try { try {
setBeeApi(new Bee(apiUrl)) setBeeApi(new Bee(url))
sessionStorage.setItem('api_host', apiUrl) sessionStorage.setItem('api_host', url)
} catch (e) { } catch (e) {
setBeeApi(null) setBeeApi(null)
} }
}, [apiUrl]) }, [url])
useEffect(() => {
if (beeApiUrl) setApiUrl(beeApiUrl)
}, [beeApiUrl])
useEffect(() => {
if (beeDebugApiUrl) setDebugApiUrl(beeDebugApiUrl)
}, [beeDebugApiUrl])
useEffect(() => { useEffect(() => {
try { try {
setBeeDebugApi(new BeeDebug(apiDebugUrl)) setBeeDebugApi(new BeeDebug(debugUrl))
sessionStorage.setItem('debug_api_host', apiDebugUrl) sessionStorage.setItem('debug_api_host', debugUrl)
} catch (e) { } catch (e) {
setBeeDebugApi(null) setBeeDebugApi(null)
} }
}, [apiDebugUrl]) }, [debugUrl])
return ( return (
<Context.Provider <Context.Provider
value={{ apiUrl, apiDebugUrl, beeApi, beeDebugApi, setApiUrl, setDebugApiUrl, lockedApiSettings }} value={{
apiUrl: url,
apiDebugUrl: debugUrl,
beeApi,
beeDebugApi,
setApiUrl,
setDebugApiUrl,
lockedApiSettings,
}}
> >
{children} {children}
</Context.Provider> </Context.Provider>
+11 -13
View File
@@ -5,19 +5,17 @@ interface LatestBeeRelease {
html_url: string html_url: string
} }
interface StatusHookCommon { interface SwarmMetadata {
isOk: boolean size: number
name: string
type?: string
} }
interface StatusNodeVersionHook extends StatusHookCommon { interface Metadata extends SwarmMetadata {
userVersion?: string type: string
latestVersion?: string isWebsite: boolean
latestUrl: string count?: number
isLatestBeeVersion: boolean hash?: string
}
interface StatusEthereumConnectionHook extends StatusHookCommon {
nodeAddresses: NodeAddresses | null
}
interface StatusTopologyHook extends StatusHookCommon {
topology: Topology | null
} }
type FilePath = File & { path?: string; fullPath?: string }
+17 -17
View File
@@ -1,5 +1,5 @@
import type { ReactElement } from 'react' import type { ReactElement } from 'react'
import { Route, Switch } from 'react-router-dom' import { Route, Routes } from 'react-router-dom'
import Accounting from './pages/accounting' import Accounting from './pages/accounting'
import Feeds from './pages/feeds' import Feeds from './pages/feeds'
import CreateNewFeed from './pages/feeds/CreateNewFeed' import CreateNewFeed from './pages/feeds/CreateNewFeed'
@@ -34,22 +34,22 @@ export enum ROUTES {
} }
const BaseRouter = (): ReactElement => ( const BaseRouter = (): ReactElement => (
<Switch> <Routes>
<Route exact path={ROUTES.UPLOAD_IN_PROGRESS} component={Upload} /> <Route path={ROUTES.UPLOAD_IN_PROGRESS} element={<Upload />} />
<Route exact path={ROUTES.UPLOAD} component={UploadLander} /> <Route path={ROUTES.UPLOAD} element={<UploadLander />} />
<Route exact path={ROUTES.DOWNLOAD} component={Download} /> <Route path={ROUTES.DOWNLOAD} element={<Download />} />
<Route exact path={ROUTES.HASH} component={Share} /> <Route path={ROUTES.HASH} element={<Share />} />
<Route exact path={ROUTES.ACCOUNTING} component={Accounting} /> <Route path={ROUTES.ACCOUNTING} element={<Accounting />} />
<Route exact path={ROUTES.SETTINGS} component={Settings} /> <Route path={ROUTES.SETTINGS} element={<Settings />} />
<Route exact path={ROUTES.STAMPS} component={Stamps} /> <Route path={ROUTES.STAMPS} element={<Stamps />} />
<Route exact path={ROUTES.STAMPS_NEW} component={CreatePostageStampPage} /> <Route path={ROUTES.STAMPS_NEW} element={<CreatePostageStampPage />} />
<Route exact path={ROUTES.STATUS} component={Status} /> <Route path={ROUTES.STATUS} element={<Status />} />
<Route exact path={ROUTES.FEEDS} component={Feeds} /> <Route path={ROUTES.FEEDS} element={<Feeds />} />
<Route exact path={ROUTES.FEEDS_NEW} component={CreateNewFeed} /> <Route path={ROUTES.FEEDS_NEW} element={<CreateNewFeed />} />
<Route exact path={ROUTES.FEEDS_UPDATE} component={UpdateFeed} /> <Route path={ROUTES.FEEDS_UPDATE} element={<UpdateFeed />} />
<Route exact path={ROUTES.FEEDS_PAGE} component={FeedSubpage} /> <Route path={ROUTES.FEEDS_PAGE} element={<FeedSubpage />} />
<Route path={ROUTES.INFO} component={Info} /> <Route path={ROUTES.INFO} element={<Info />} />
</Switch> </Routes>
) )
export default BaseRouter export default BaseRouter
+19
View File
@@ -1,4 +1,23 @@
import type { NodeAddresses, Topology } from '@ethersphere/bee-js'
import type { Token } from './models/Token' import type { Token } from './models/Token'
import { CheckState } from './providers/Bee'
export interface StatusHookCommon {
checkState: CheckState
}
export interface StatusNodeVersionHook extends StatusHookCommon {
userVersion?: string
latestVersion?: string
latestUrl: string
isLatestBeeVersion: boolean
}
export interface StatusEthereumConnectionHook extends StatusHookCommon {
nodeAddresses: NodeAddresses | null
}
export interface StatusTopologyHook extends StatusHookCommon {
topology: Topology | null
}
export interface ChequebookBalance { export interface ChequebookBalance {
totalBalance: Token totalBalance: Token
-24
View File
@@ -1,24 +0,0 @@
export class SwarmFile {
public name: string
public path: string
public type: string
public size: number
public webkitRelativePath: string
public arrayBuffer: () => Promise<ArrayBuffer>
private data: Promise<ArrayBuffer>
constructor(file: File) {
const path = Reflect.get(file, 'path') || file.webkitRelativePath || file.name
this.path = path.startsWith('/') ? path.slice(1) : path
this.webkitRelativePath = this.path
this.name = file.name
this.type = file.type
this.size = file.size
this.data = file.arrayBuffer()
this.arrayBuffer = async () => {
const data = await this.data
return data
}
}
}
+58 -41
View File
@@ -1,28 +1,32 @@
import { FileData } from '@ethersphere/bee-js'
import { SwarmFile } from './SwarmFile'
const indexHtmls = ['index.html', 'index.htm'] const indexHtmls = ['index.html', 'index.htm']
export function detectIndexHtml(files: SwarmFile[]): string | false { interface DetectedIndex {
if (!files.length) { indexPath: string
commonPrefix?: string
}
export function detectIndexHtml(files: FilePath[]): DetectedIndex | false {
const paths = files.map(getPath)
if (!paths.length) {
return false return false
} }
const exactMatch = files.find(x => indexHtmls.includes(x.path)) const exactMatch = paths.find(x => indexHtmls.includes(x))
if (exactMatch) { if (exactMatch) {
return exactMatch.name return { indexPath: exactMatch }
} }
const prefix = files[0].path.split('/')[0] + '/' const prefix = paths[0].split('/')[0] + '/'
const allStartWithSamePrefix = files.every(x => x.path.startsWith(prefix)) const allStartWithSamePrefix = paths.every(x => x.startsWith(prefix))
if (allStartWithSamePrefix) { if (allStartWithSamePrefix) {
const match = files.find(x => indexHtmls.map(y => prefix + y).includes(x.path)) const match = paths.find(x => indexHtmls.map(y => prefix + y).includes(x))
if (match) { if (match) {
return match.name return { indexPath: match, commonPrefix: prefix }
} }
} }
@@ -53,37 +57,50 @@ export function getHumanReadableFileSize(bytes: number): string {
return bytes + ' bytes' return bytes + ' bytes'
} }
export function convertBeeFileToBrowserFile(file: FileData<ArrayBuffer>): Partial<File> { export function getAssetNameFromFiles(files: FilePath[]): string {
if (files.length === 1) return files[0].name
if (files.length > 0) {
const prefix = getPath(files[0]).split('/')[0]
// Only if all files have a common prefix we can use it as a folder name
if (files.every(f => getPath(f).split('/')[0] === prefix)) return prefix
}
return 'unknown'
}
export function getMetadata(files: FilePath[]): Metadata {
const size = files.reduce((total, item) => total + item.size, 0)
const isWebsite = Boolean(detectIndexHtml(files))
const name = getAssetNameFromFiles(files)
const type = files.length === 1 ? files[0].type : 'folder'
const count = files.length
return { size, name, type, isWebsite, count }
}
export function getPath(file: FilePath): string {
return (file.path || file.webkitRelativePath || file.name).replace(/^\//g, '') // remove the starting slash
}
/**
* Utility function that is needed to have correct directory structure as webkitRelativePath is read only
*/
export function packageFile(file: FilePath): FilePath {
const path = getPath(file)
return { return {
path: path,
fullPath: path,
webkitRelativePath: path,
lastModified: file.lastModified,
name: file.name, name: file.name,
size: file.data.byteLength, size: file.size,
type: file.contentType, type: file.type,
arrayBuffer: () => new Promise(resolve => resolve(file.data)), stream: file.stream,
slice: file.slice,
text: file.text,
arrayBuffer: async () => await file.arrayBuffer(), // This is needed for successful upload and can not simply be { arrayBuffer: file.arrayBuffer }
} }
} }
export function convertManifestToFiles(files: Record<string, string>): SwarmFile[] {
return Object.entries(files).map(
x =>
({
name: x[0],
path: x[0],
type: 'n/a',
size: 0,
webkitRelativePath: x[0],
arrayBuffer: () => new Promise(resolve => resolve(new ArrayBuffer(0))),
} as SwarmFile),
)
}
export function getAssetNameFromFiles(files: SwarmFile[]): string {
if (!files.length) {
return 'Unknown'
}
if (files.length === 1) {
return files[0].name
}
return files[0].path.split('/')[0]
}
+89
View File
@@ -0,0 +1,89 @@
interface Dimensions {
width: number
height: number
}
/**
* Get the dimensions of the image after resize
*
* @param imgWidth Current image width
* @param imgHeight Current image height
* @param maxWidth Desired max width
* @param maxHeight Desired max height
*
* @returns Downscaled dimensions of the image to fit in the bounding box
*/
export function getDimensions(imgWidth: number, imgHeight: number, maxWidth?: number, maxHeight?: number): Dimensions {
const ratioWidth = maxWidth ? imgWidth / maxWidth : 1
const ratioHeight = maxHeight ? imgHeight / maxHeight : 1
const ratio = Math.max(ratioWidth, ratioHeight)
// No need to resize
if (ratio <= 1) return { width: imgWidth, height: imgHeight }
return { width: imgWidth / ratio, height: imgHeight / ratio }
}
/**
* Resize image passed to fit in the bounding box defined with maxWidth and maxHeight.
* Note that one or both of the bounding box dimensions may be omitted
*
* @param file Image file to be resized
* @param maxWidth Maximal image width
* @param maxHeight Maximal image height
*
* @returns Promise that resolves into the resized image blob
*/
export function resize(file: File, maxWidth?: number, maxHeight?: number): Promise<Blob> {
return new Promise((resolve, reject) => {
const allowedTypes = [
'image/bmp',
'image/gif',
'image/vnd.microsoft.icon',
'image/jpeg',
'image/png',
'image/svg+xml',
'image/tiff',
'image/webp',
]
if (!file.size || !file.type || !allowedTypes.includes(file.type)) return reject('File not supported!')
try {
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = event => {
const src = event?.target?.result
if (!src || typeof src !== 'string') throw new Error('Failed to load the image source')
const img = new Image()
img.src = src
img.onload = () => {
const dimensions = getDimensions(img.width, img.height, maxWidth, maxHeight)
const elem = document.createElement('canvas')
elem.width = dimensions.width
elem.height = dimensions.height
const ctx = elem.getContext('2d')
if (!ctx) throw new Error('Failed to create canvas context')
ctx.drawImage(img, 0, 0, elem.width, elem.height)
ctx.canvas.toBlob(
blob => {
if (!blob) throw new Error('Failed to extract the blob from canvas')
resolve(blob)
},
'image/jpeg',
1,
)
}
}
reader.onerror = error => reject(error)
} catch (error) {
reject(error)
}
})
}
+18 -24
View File
@@ -1,4 +1,5 @@
import { BigNumber } from 'bignumber.js' import { BigNumber } from 'bignumber.js'
import { Token } from '../models/Token'
/** /**
* Test if value is an integer * Test if value is an integer
@@ -158,34 +159,27 @@ export function secondsToTimeString(seconds: number): string {
return `${unit.toFixed(1)} years` return `${unit.toFixed(1)} years`
} }
export function formatBzz(amount: number): string {
const asString = amount.toFixed(16)
let indexOfSignificantDigit = -1
let reachedDecimalPoint = false
for (let i = 0; i < asString.length; i++) {
const char = asString[i]
if (char === '.') {
reachedDecimalPoint = true
} else if (reachedDecimalPoint && char !== '0') {
indexOfSignificantDigit = i
break
}
}
return asString.slice(0, indexOfSignificantDigit + 4)
}
export function convertDepthToBytes(depth: number): number { export function convertDepthToBytes(depth: number): number {
return 2 ** depth * 4096 return 2 ** depth * 4096
} }
export function convertAmountToSeconds(amount: number): number { export function convertAmountToSeconds(amount: number, pricePerBlock: number): number {
return amount / 10 / 1 // TODO: blocktime should come directly from the blockchain as it may differ between different networks
const blockTime = 5 // On mainnet there is 5 seconds between blocks
// See https://github.com/ethersphere/bee/blob/66f079930d739182c4c79eb6008784afeeba1096/pkg/debugapi/postage.go#L410-L413
return (amount * blockTime) / pricePerBlock
} }
export function calculateStampPrice(depth: number, amount: number): number { export function calculateStampPrice(depth: number, amount: bigint): Token {
return (amount * 2 ** (depth - 16) * 2) / 1e16 // See https://github.com/ethersphere/bee/blob/66f079930d739182c4c79eb6008784afeeba1096/pkg/debugapi/postage.go#L410-L413
return new Token(amount * BigInt(2 ** depth)) // FIXME: the 2 ** depth should be performed on bigint already
}
export function shortenText(text: string, length = 20, separator = '[…]'): string {
if (text.length <= length * 2 + separator.length) {
return text
}
return `${text.slice(0, length)}${separator}${text.slice(-length)}`
} }