Compare commits

..

12 Commits

Author SHA1 Message Date
bee-worker d399a5c556 chore: release 0.9.0 (#249) 2021-11-25 15:20:26 +01:00
Cafe137 59dd1a3c81 chore: bump bee-js to 3.0.0 (#258) 2021-11-25 15:10:36 +01:00
Cafe137 635621b04a feat: improve upload flow (#240)
* feat: separate flow for folder and file uploads

* feat: add basic index document detection

* feat(wip): separate preview step

* fix: fix kb and mb units

* feat: add post upload summary, add some styling

* feat: upload flow

* fix: change element order and add conditional rendering

* refactor: remove unused variables for now

* fix: put back stamp creation to stamp page

* refactor: rework postage stamps and grid

* feat: add website and folder icons

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

* feat: add basic design to postage stamp selection dialog

* feat: add web icon, shorten stamp in preview

* feat: extract swarm hash in download flow

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

* fix: always show buy button on stamp page

* refactor: downgrade

* refactor: speed up icon transition

* style: improve download buttons

* style: change [back to upload] icon

* style: add spacing before swarm gateway text

* style: post upload summary spacing

* refactor: drop verticalspacing and use box

* refactor: merge icons to one component

* refactor: use conditions instead of weird assignment

* docs: explain filter(x => x)

* refactor: generalize capacity

* refactor: avoid passing arrow functions

* refactor: get rid of PaperGridContainer and Container

* fix: fix hover color for postage stamps

* feat: add disabled and loading state to buttons

* fix: make drag and drop work for websites

* feat: handle folders and non existing hashes

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

* style: remove body2 font variants

* fix: remove typo

* feat: disable folder upload, add website upload

* fix: disable showPreviews to avoid flickering

* feat(temp): remove folder upload

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

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

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

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

* docs: update dev mode docs

* docs: move dev mode comment

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

* docs: add REACT_APP_DEV_MODE fixme comment

* feat: also ignore chequebook status in dev mode

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

* docs: add dev mode to readme

* docs: revert autoformat

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

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

* chore: lock dependency versions

* chore: update dependencies to latest working ones

* chore: fix deprecation createMuiTheme

* chore: revert notistack to v1
2021-11-08 13:33:12 +01:00
Vojtech Simetka 15b4b0e561 ci: check supported bee version (#231) 2021-11-07 14:43:54 +01:00
fossabot c1a219c2e2 chore: add license scan report and status (#232)
Signed off by: fossabot <badges@fossa.com>
2021-11-05 13:56:10 +01:00
43 changed files with 11879 additions and 5666 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"ignores": ["@types/jest", "@commitlint/config-conventional", "@types/react-router"]
}
+3 -1
View File
@@ -8,7 +8,9 @@
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",
"prettier", "prettier",
"plugin:prettier/recommended", "plugin:prettier/recommended",
"plugin:react/recommended" "plugin:react/recommended",
"react-app",
"react-app/jest"
], ],
"env": { "env": {
"browser": true, "browser": true,
+19
View File
@@ -16,6 +16,11 @@ jobs:
matrix: matrix:
node-version: [14.x] node-version: [14.x]
env:
REACT_APP_BEE_HOST: https://api.test-node.staging.ethswarm.org/
REACT_APP_BEE_DEBUG_HOST: https://debug.test-node.staging.ethswarm.org/
REACT_APP_DEV_MODE: 1
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with: with:
@@ -47,5 +52,19 @@ jobs:
env: env:
CI: true CI: true
- name: Dependency check
run: npm run depcheck
- name: Update supported Bee action
uses: ethersphere/update-supported-bee-action@v1
if: github.ref == 'refs/heads/master'
with:
token: ${{ secrets.REPO_GHA_PAT }}
- name: Build - name: Build
run: npm run build run: npm run build
- name: Create preview
uses: ethersphere/beeload-action@v1
with:
preview: 'true'
+10
View File
@@ -1,5 +1,15 @@
# Changelog # Changelog
## [0.9.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.8.0...v0.9.0) (2021-11-25)
### Features
* add dev mode flag ([#246](https://www.github.com/ethersphere/bee-dashboard/issues/246)) ([49350b0](https://www.github.com/ethersphere/bee-dashboard/commit/49350b05709053ecfbc4fc98f8b1df1aa0345e95))
* enable setting devMode from queryParams ([#254](https://www.github.com/ethersphere/bee-dashboard/issues/254)) ([844383b](https://www.github.com/ethersphere/bee-dashboard/commit/844383bea7b2118232a74ac23c9e9a38fc47d3fd))
* improve upload flow ([#240](https://www.github.com/ethersphere/bee-dashboard/issues/240)) ([635621b](https://www.github.com/ethersphere/bee-dashboard/commit/635621b04aea7124a99d00f9e31a86983063f5ce))
* move postage stamp operations to bee debug api ([#256](https://www.github.com/ethersphere/bee-dashboard/issues/256)) ([3bb0077](https://www.github.com/ethersphere/bee-dashboard/commit/3bb00771d684ad93fd7acd921b648574013aec5c))
## [0.8.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.7.0...v0.8.0) (2021-10-20) ## [0.8.0](https://www.github.com/ethersphere/bee-dashboard/compare/v0.7.0...v0.8.0) (2021-10-20)
In this version we are adding support for the bee release 1.2.0. The app also went through a graphical redesign. More to come soon! In this version we are adding support for the bee release 1.2.0. The app also went through a graphical redesign. More to come soon!
+10 -3
View File
@@ -3,6 +3,7 @@
[![](https://img.shields.io/badge/made%20by-Swarm-blue.svg?style=flat-square)](https://swarm.ethereum.org/) [![](https://img.shields.io/badge/made%20by-Swarm-blue.svg?style=flat-square)](https://swarm.ethereum.org/)
[![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme) [![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme)
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fethersphere%2Fbee-dashboard.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fethersphere%2Fbee-dashboard?ref=badge_shield)
![](https://img.shields.io/badge/npm-%3E%3D6.0.0-orange.svg?style=flat-square) ![](https://img.shields.io/badge/npm-%3E%3D6.0.0-orange.svg?style=flat-square)
![](https://img.shields.io/badge/Node.js-%3E%3D10.0.0-orange.svg?style=flat-square) ![](https://img.shields.io/badge/Node.js-%3E%3D10.0.0-orange.svg?style=flat-square)
@@ -12,9 +13,10 @@
**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 the latest released version of Bee. Using it with older Bee versions is not This project is intended to be used with **Bee version <!-- SUPPORTED_BEE_START -->1.4.0-8fa696a8<!-- SUPPORTED_BEE_END -->**.
recommended and may not work. Stay up to date by joining the [official Discord](https://discord.gg/GU22h2utj6) and by Using it with older or newer Bee versions is not recommended and may not work. Stay up to date by joining the
keeping an eye on the [releases tab](https://github.com/ethersphere/bee-dashboard/releases). [official Discord](https://discord.gg/GU22h2utj6) and by keeping an eye on the
[releases tab](https://github.com/ethersphere/bee-dashboard/releases).
![Status page](/ui_samples/info.png) ![Status page](/ui_samples/info.png)
@@ -83,6 +85,8 @@ npm start
The Bee Dashboard runs in development mode on [http://localhost:3031/](http://localhost:3031/) The Bee Dashboard runs in development mode on [http://localhost:3031/](http://localhost:3031/)
> Setting the `REACT_APP_DEV_MODE=1` environment variable, or opening Bee Dashboard with the query string `?devMode=1` loosens some checks. This makes it possible to develop Bee Dashboard without having connected peers and chequebook properly set up, effectively supporting the dev mode of Bee itself.
## Contribute ## Contribute
There are some ways you can make this module better: There are some ways you can make this module better:
@@ -102,3 +106,6 @@ See what "Maintainer" means [here](https://github.com/ethersphere/repo-maintaine
## License ## License
[BSD-3-Clause](./LICENSE) [BSD-3-Clause](./LICENSE)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fethersphere%2Fbee-dashboard.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fethersphere%2Fbee-dashboard?ref=badge_large)
+10557 -5398
View File
File diff suppressed because it is too large Load Diff
+39 -36
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ethersphere/bee-dashboard", "name": "@ethersphere/bee-dashboard",
"version": "0.8.0", "version": "0.9.0",
"description": "An app which helps users to setup their Bee node and do actions like cash out cheques", "description": "An app which helps users to setup their Bee node and do actions like cash out cheques",
"keywords": [ "keywords": [
"bee", "bee",
@@ -24,52 +24,60 @@
"url": "https://github.com/ethersphere/bee-dashboard.git" "url": "https://github.com/ethersphere/bee-dashboard.git"
}, },
"dependencies": { "dependencies": {
"@ethersphere/bee-js": "2.1.0", "@ethersphere/bee-js": "3.0.0",
"@material-ui/core": "4.12.3", "@material-ui/core": "4.12.3",
"@material-ui/icons": "4.11.2", "@material-ui/icons": "4.11.2",
"@material-ui/lab": "4.0.0-alpha.57", "@material-ui/lab": "4.0.0-alpha.57",
"@types/react-router": "5.1.13", "axios": "0.24.0",
"@types/react-router-dom": "5.1.7",
"axios": "0.21.1",
"bignumber.js": "9.0.1", "bignumber.js": "9.0.1",
"feather-icons": "4.28.0", "formik": "2.2.9",
"formik": "2.2.8",
"formik-material-ui": "3.0.1", "formik-material-ui": "3.0.1",
"material-ui-dropzone": "3.5.0", "material-ui-dropzone": "3.5.0",
"notistack": "1.0.9", "notistack": "1.0.10",
"opener": "1.5.2", "opener": "1.5.2",
"qrcode.react": "1.0.1", "qrcode.react": "1.0.1",
"react": "17.0.2", "react": "17.0.2",
"react-copy-to-clipboard": "5.0.3", "react-copy-to-clipboard": "5.0.4",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-feather": "2.0.9", "react-feather": "2.0.9",
"react-identicons": "1.2.5", "react-identicons": "1.2.5",
"react-router-dom": "5.2.0", "react-router-dom": "5.2.0",
"react-syntax-highlighter": "15.4.3", "react-syntax-highlighter": "15.4.4",
"semver": "7.3.2", "semver": "7.3.5",
"serve-handler": "6.1.3" "serve-handler": "6.1.3"
}, },
"devDependencies": { "devDependencies": {
"@testing-library/jest-dom": "5.12.0", "@commitlint/config-conventional": "14.1.0",
"@testing-library/react": "11.2.6", "@testing-library/jest-dom": "5.15.0",
"@testing-library/user-event": "13.1.5", "@testing-library/react": "12.1.2",
"@types/jest": "26.0.22", "@types/jest": "27.0.2",
"@types/node": "14.14.41", "@types/qrcode.react": "1.0.2",
"@types/qrcode.react": "1.0.1", "@types/react": "17.0.34",
"@types/react": "17.0.3", "@types/react-copy-to-clipboard": "5.0.2",
"@types/react-copy-to-clipboard": "5.0.0", "@types/react-dom": "17.0.11",
"@types/react-dom": "17.0.3", "@types/react-router": "5.1.17",
"@types/react-syntax-highlighter": "13.5.0", "@types/react-router-dom": "5.3.2",
"@types/semver": "7.3.6", "@types/react-syntax-highlighter": "13.5.2",
"eslint": "7.32.0", "@types/semver": "7.3.9",
"eslint-config-prettier": "8.3.0", "@typescript-eslint/eslint-plugin": "4.33.0",
"eslint-plugin-jest": "24.4.0", "@typescript-eslint/parser": "4.33.0",
"eslint-plugin-prettier": "3.4.1", "babel-eslint": "10.1.0",
"eslint-plugin-react": "7.24.0", "depcheck": "1.4.2",
"prettier": "2.3.2", "eslint": "7.24.0",
"eslint-config-prettier": "8.2.0",
"eslint-config-react-app": "6.0.0",
"eslint-plugin-flowtype": "5.10.0",
"eslint-plugin-import": "2.25.2",
"eslint-plugin-jest": "24.3.5",
"eslint-plugin-jsx-a11y": "6.4.1",
"eslint-plugin-prettier": "3.4.0",
"eslint-plugin-react": "7.23.2",
"eslint-plugin-react-hooks": "4.2.0",
"eslint-plugin-testing-library": "3.10.2",
"prettier": "2.4.1",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"typescript": "4.2.4", "typescript": "4.4.4",
"web-vitals": "1.1.1" "web-vitals": "2.1.2"
}, },
"scripts": { "scripts": {
"prepare": "npm run build", "prepare": "npm run build",
@@ -77,6 +85,7 @@
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test", "test": "react-scripts test",
"serve": "node ./serve.js", "serve": "node ./serve.js",
"depcheck": "depcheck .",
"lint": "eslint --fix \"src/**/*.ts\" \"src/**/*.tsx\" && prettier --write \"src/**/*.ts\" \"src/**/*.tsx\"", "lint": "eslint --fix \"src/**/*.ts\" \"src/**/*.tsx\" && prettier --write \"src/**/*.ts\" \"src/**/*.tsx\"",
"lint:check": "eslint \"src/**/*.ts\" \"src/**/*.tsx\" && prettier --check \"src/**/*.ts\" \"src/**/*.tsx\"" "lint:check": "eslint \"src/**/*.ts\" \"src/**/*.tsx\" && prettier --check \"src/**/*.ts\" \"src/**/*.tsx\""
}, },
@@ -84,12 +93,6 @@
"build", "build",
"serve.js" "serve.js"
], ],
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": { "browserslist": {
"production": [ "production": [
">0.2%", ">0.2%",
+5 -3
View File
@@ -6,7 +6,7 @@ import { ReactElement } from 'react'
const LIMIT = 100_000_000 // 100 megabytes const LIMIT = 100_000_000 // 100 megabytes
interface Props { interface Props {
file: File files: File[]
} }
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
@@ -22,14 +22,16 @@ const useStyles = makeStyles((theme: Theme) =>
export default function UploadSizeAlert(props: Props): ReactElement | null { export default function UploadSizeAlert(props: Props): ReactElement | null {
const classes = useStyles() const classes = useStyles()
const aboveLimit = props.file.size >= LIMIT const totalSize = props.files.reduce((previous, current) => previous + current.size, 0)
const aboveLimit = totalSize >= LIMIT
return ( return (
<Collapse in={aboveLimit}> <Collapse in={aboveLimit}>
<div className={classes.root}> <div className={classes.root}>
<Alert severity="warning"> <Alert severity="warning">
<AlertTitle>Warning</AlertTitle> <AlertTitle>Warning</AlertTitle>
The file you are trying to upload is above the recommended size. The chunks may not be synchronised properly The files you are trying to upload are above the recommended size. The chunks may not be synchronised properly
over the network. over the network.
</Alert> </Alert>
</div> </div>
+22
View File
@@ -0,0 +1,22 @@
import { ReactElement } from 'react'
interface Props {
width: string
usage: number
}
export function Capacity({ width, usage }: Props): ReactElement {
const integerUsage = Math.round(usage * 100)
const used = integerUsage + '%'
const free = 100 - 2 - integerUsage + '%'
return (
<div style={{ display: 'flex', alignItems: 'center', height: '100%', width }}>
<div style={{ display: 'flex', height: '4px', width: '100%' }}>
<div style={{ width: used, background: '#dd7200' }} />
<div style={{ width: '2%' }} />
<div style={{ width: free, background: '#c9c9c9' }} />
</div>
</div>
)
}
+59
View File
@@ -0,0 +1,59 @@
import { Collapse, ListItem } from '@material-ui/core'
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import { ExpandLess, ExpandMore } from '@material-ui/icons'
import { ReactElement, ReactNode, useState } from 'react'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
width: '100%',
padding: 0,
margin: 0,
marginTop: theme.spacing(4),
'&:first-child': {
marginTop: 0,
},
},
rootLevel1: { marginTop: theme.spacing(1) },
rootLevel2: { marginTop: theme.spacing(0.5) },
header: {
backgroundColor: theme.palette.background.paper,
},
contentLevel0: {
marginTop: theme.spacing(1),
},
contentLevel12: {
marginTop: theme.spacing(0.25),
},
infoText: {
color: '#c9c9c9',
},
}),
)
interface Props {
children: ReactNode
expandable: ReactNode
defaultOpen?: boolean
}
export default function ExpandableElement({ children, expandable, defaultOpen }: Props): ReactElement | null {
const classes = useStyles()
const [open, setOpen] = useState<boolean>(Boolean(defaultOpen))
const handleClick = () => {
setOpen(!open)
}
return (
<div className={`${classes.root} ${classes.rootLevel2}`}>
<ListItem button onClick={handleClick} className={classes.header}>
{children}
{open ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
<div className={classes.contentLevel12}>{expandable}</div>
</Collapse>
</div>
)
}
+10 -7
View File
@@ -1,6 +1,6 @@
import { ReactElement, ReactNode } from 'react'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import { Grid } from '@material-ui/core' import { Grid } from '@material-ui/core'
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import { ReactElement, ReactNode } from 'react'
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
createStyles({ createStyles({
@@ -22,11 +22,14 @@ export default function ExpandableListItemActions({ children }: Props): ReactEle
if (Array.isArray(children)) { if (Array.isArray(children)) {
return ( return (
<Grid container direction="row"> <Grid container direction="row">
{children.map((a, i) => ( {children
<Grid key={i} className={classes.action}> // Exclude falsy values to allow conditional rendering
{a} .filter(x => x)
</Grid> .map((a, i) => (
))} <Grid key={i} className={classes.action}>
{a}
</Grid>
))}
</Grid> </Grid>
) )
} }
+10 -5
View File
@@ -1,9 +1,8 @@
import { ReactElement, ChangeEvent, useState } from 'react' import { Button, Grid, IconButton, InputBase, ListItem, Typography } from '@material-ui/core'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import Collapse from '@material-ui/core/Collapse' import Collapse from '@material-ui/core/Collapse'
import { ListItem, Typography, Grid, IconButton, InputBase, Button } from '@material-ui/core' import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import { Edit, Minus, RotateCcw, Check } from 'react-feather' import { ChangeEvent, ReactElement, useState } from 'react'
import { Check, Edit, Minus, RotateCcw } from 'react-feather'
import ExpandableListItemActions from './ExpandableListItemActions' import ExpandableListItemActions from './ExpandableListItemActions'
import ExpandableListItemNote from './ExpandableListItemNote' import ExpandableListItemNote from './ExpandableListItemNote'
@@ -55,6 +54,7 @@ interface Props {
confirmLabelDisabled?: boolean confirmLabelDisabled?: boolean
onChange?: (value: string) => void onChange?: (value: string) => void
onConfirm: (value: string) => void onConfirm: (value: string) => void
mapperFn?: (value: string) => string
} }
export default function ExpandableListItemKey({ export default function ExpandableListItemKey({
@@ -67,12 +67,17 @@ export default function ExpandableListItemKey({
expandedOnly, expandedOnly,
helperText, helperText,
placeholder, placeholder,
mapperFn,
}: Props): ReactElement | null { }: Props): ReactElement | null {
const classes = useStyles() const classes = useStyles()
const [open, setOpen] = useState(Boolean(expandedOnly)) const [open, setOpen] = useState(Boolean(expandedOnly))
const [inputValue, setInputValue] = useState<string>(value || '') const [inputValue, setInputValue] = useState<string>(value || '')
const toggleOpen = () => setOpen(!open) const toggleOpen = () => setOpen(!open)
const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => { const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
if (mapperFn) {
e.target.value = mapperFn(e.target.value)
}
setInputValue(e.target.value) setInputValue(e.target.value)
if (onChange) onChange(e.target.value) if (onChange) onChange(e.target.value)
+8 -7
View File
@@ -1,9 +1,9 @@
import { ReactElement, useState } from 'react' import { Grid, IconButton, ListItem, Tooltip, Typography } from '@material-ui/core'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import Collapse from '@material-ui/core/Collapse' import Collapse from '@material-ui/core/Collapse'
import { ListItem, Typography, Grid, IconButton, Tooltip } from '@material-ui/core' import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import { Eye, Minus } from 'react-feather' import { ReactElement, useState } from 'react'
import { CopyToClipboard } from 'react-copy-to-clipboard' import { CopyToClipboard } from 'react-copy-to-clipboard'
import { Eye, Minus } from 'react-feather'
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
createStyles({ createStyles({
@@ -65,6 +65,9 @@ export default function ExpandableListItemKey({ label, value }: Props): ReactEle
const splitValues = split(value) const splitValues = split(value)
const hasPrefix = isPrefixedHexString(value) const hasPrefix = isPrefixedHexString(value)
const spanText = `${hasPrefix ? `${splitValues[0]} ${splitValues[1]}` : splitValues[0]}[…]${
splitValues[splitValues.length - 1]
}`
return ( return (
<ListItem className={`${classes.header} ${open ? classes.headerOpen : ''}`}> <ListItem className={`${classes.header} ${open ? classes.headerOpen : ''}`}>
@@ -77,9 +80,7 @@ export default function ExpandableListItemKey({ label, value }: Props): ReactEle
<span className={classes.copyValue}> <span className={classes.copyValue}>
<Tooltip title={copied ? 'Copied' : 'Copy'} placement="top" arrow onClose={tooltipCloseHandler}> <Tooltip title={copied ? 'Copied' : 'Copy'} placement="top" arrow onClose={tooltipCloseHandler}>
<CopyToClipboard text={value}> <CopyToClipboard text={value}>
<span onClick={tooltipClickHandler}>{`${ <span onClick={tooltipClickHandler}>{value ? spanText : ''}</span>
hasPrefix ? `${splitValues[0]} ${splitValues[1]}` : splitValues[0]
}[…]${splitValues[splitValues.length - 1]}`}</span>
</CopyToClipboard> </CopyToClipboard>
</Tooltip> </Tooltip>
</span> </span>
+81
View File
@@ -0,0 +1,81 @@
import { Grid, IconButton, ListItem, Tooltip, Typography } from '@material-ui/core'
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import { OpenInNewSharp } from '@material-ui/icons'
import { ReactElement, useState } from 'react'
import CopyToClipboard from 'react-copy-to-clipboard'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
header: {
backgroundColor: theme.palette.background.paper,
marginBottom: theme.spacing(0.25),
borderLeft: `${theme.spacing(0.25)}px solid rgba(0,0,0,0)`,
wordBreak: 'break-word',
},
headerOpen: {
borderLeft: `${theme.spacing(0.25)}px solid ${theme.palette.primary.main}`,
},
openLinkIcon: {
cursor: 'pointer',
padding: theme.spacing(1),
borderRadius: 0,
'&:hover': {
backgroundColor: '#fcf2e8',
color: theme.palette.primary.main,
},
},
content: {
marginTop: theme.spacing(2),
marginBottom: theme.spacing(2),
},
keyMargin: {
marginRight: theme.spacing(1),
},
copyValue: {
cursor: 'pointer',
padding: theme.spacing(1),
borderRadius: 0,
'&:hover': {
backgroundColor: '#fcf2e8',
color: theme.palette.primary.main,
},
},
}),
)
interface Props {
label: string
value: string
}
export default function ExpandableListItemLink({ label, value }: Props): ReactElement | null {
const classes = useStyles()
const [copied, setCopied] = useState(false)
const tooltipClickHandler = () => setCopied(true)
const tooltipCloseHandler = () => setCopied(false)
return (
<ListItem className={classes.header}>
<Grid container direction="column" justifyContent="space-between" alignItems="stretch">
<Grid container direction="row" justifyContent="space-between" alignItems="center">
{label && <Typography variant="body1">{label}</Typography>}
<Typography variant="body2">
<div>
<span className={classes.copyValue}>
<Tooltip title={copied ? 'Copied' : 'Copy'} placement="top" arrow onClose={tooltipCloseHandler}>
<CopyToClipboard text={value}>
<span onClick={tooltipClickHandler}>{value.slice(0, 19)}...</span>
</CopyToClipboard>
</Tooltip>
</span>
<IconButton size="small" className={classes.openLinkIcon}>
<OpenInNewSharp onClick={() => window.open(value)} strokeWidth={1} />
</IconButton>
</div>
</Typography>
</Grid>
</Grid>
</ListItem>
)
}
+30
View File
@@ -0,0 +1,30 @@
import { createStyles, makeStyles } from '@material-ui/core'
import { ReactElement } from 'react'
const useStyles = makeStyles(() =>
createStyles({
image: {
width: '100%',
height: '100%',
objectFit: 'cover',
},
}),
)
interface Props {
alt: string
src: string | undefined
maxHeight?: string
maxWidth?: string
}
export function FitImage(props: Props): ReactElement {
const classes = useStyles()
const inlineStyles: Record<string, string> = {}
props.maxHeight && (inlineStyles.maxHeight = props.maxHeight)
props.maxWidth && (inlineStyles.maxWidth = props.maxWidth)
return <img className={classes.image} alt={props.alt} src={props.src} style={inlineStyles} />
}
+31
View File
@@ -0,0 +1,31 @@
import { createStyles, makeStyles } from '@material-ui/core'
import { ReactElement } from 'react'
interface Props {
children: ReactElement | ReactElement[]
}
const useStyles = makeStyles(() =>
createStyles({
wrapper: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: '175px',
height: '175px',
background: `repeating-linear-gradient(
45deg,
#efefef,
#efefef 4px,
#ffffff 4px,
#ffffff 8px
)`,
},
}),
)
export function StripedWrapper({ children }: Props): ReactElement {
const classes = useStyles()
return <div className={classes.wrapper}>{children}</div>
}
+66
View File
@@ -0,0 +1,66 @@
import { Button, CircularProgress, createStyles, makeStyles } from '@material-ui/core'
import React, { ReactElement } from 'react'
import { IconProps } from 'react-feather'
interface Props {
onClick: () => void
iconType: React.ComponentType<IconProps>
children: string
className?: string
disabled?: boolean
loading?: boolean
}
const useStyles = makeStyles(() =>
createStyles({
button: {
position: 'relative',
whiteSpace: 'nowrap',
'&:hover, &:focus': {
'& svg': {
stroke: '#fff',
transition: '0.1s',
},
},
},
spinnerWrapper: {
position: 'absolute',
left: '50%',
top: '50%',
width: '40px',
height: '40px',
transform: 'translate(-50%, -50%)',
},
}),
)
export function SwarmButton({ children, onClick, iconType, className, disabled, loading }: Props): ReactElement {
const classes = useStyles()
const icon = React.createElement(iconType, {
size: '1.25rem',
color: disabled ? 'rgba(0, 0, 0, 0.26)' : '#dd7700',
})
const classNames = className ? [className, classes.button].join(' ') : classes.button
return (
<Button
className={classNames}
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
onClick()
event.currentTarget.blur()
}}
variant="contained"
startIcon={icon}
disabled={disabled}
>
{children}
{loading && (
<div className={classes.spinnerWrapper}>
<CircularProgress />
</div>
)}
</Button>
)
}
+2 -2
View File
@@ -55,7 +55,7 @@ export default function WithdrawDepositModal({
setOpen(false) setOpen(false)
enqueueSnackbar(`${successMessage} Transaction ${transactionHash}`, { variant: 'success' }) enqueueSnackbar(`${successMessage} Transaction ${transactionHash}`, { variant: 'success' })
} catch (e) { } catch (e) {
enqueueSnackbar(`${errorMessage} Error: ${e.message}`, { variant: 'error' }) enqueueSnackbar(`${errorMessage} Error: ${(e as Error).message}`, { variant: 'error' })
} }
} }
@@ -71,7 +71,7 @@ export default function WithdrawDepositModal({
if (max && t.toDecimal.isGreaterThan(max)) setAmountError(new Error(`Needs to be less than ${max}`)) if (max && t.toDecimal.isGreaterThan(max)) setAmountError(new Error(`Needs to be less than ${max}`))
} catch (e) { } catch (e) {
setAmountError(e) setAmountError(e as Error)
} }
} }
+3 -4
View File
@@ -1,16 +1,15 @@
import { BigNumber } from 'bignumber.js'
import { ReactElement, useContext } from 'react' import { ReactElement, useContext } from 'react'
import { Upload } from 'react-feather' import { Upload } from 'react-feather'
import { Context as SettingsContext } from '../providers/Settings'
import WithdrawDepositModal from '../components/WithdrawDepositModal' import WithdrawDepositModal from '../components/WithdrawDepositModal'
import { BigNumber } from 'bignumber.js' import { Context as SettingsContext } from '../providers/Settings'
export default function WithdrawModal(): ReactElement { export default function WithdrawModal(): ReactElement {
const { beeDebugApi } = useContext(SettingsContext) const { beeDebugApi } = useContext(SettingsContext)
return ( return (
<WithdrawDepositModal <WithdrawDepositModal
successMessage="Successful withdrawl." successMessage="Successful withdrawal."
errorMessage="Error with withdrawing." errorMessage="Error with withdrawing."
dialogMessage="Specify the amount of BZZ you would like to withdraw from your node." dialogMessage="Specify the amount of BZZ you would like to withdraw from your node."
label="Withdraw" label="Withdraw"
+1 -1
View File
@@ -100,7 +100,7 @@ export const useAccounting = (
setUncashedAmounts(results.fulfilled) setUncashedAmounts(results.fulfilled)
setIsloadingUncashed(false) setIsloadingUncashed(false)
}) })
}, [settlements, isLoadingUncashed, uncashedAmounts]) }, [settlements, isLoadingUncashed, uncashedAmounts, beeDebugApi])
const accounting = mergeAccounting(balances, settlements?.settlements, uncashedAmounts) const accounting = mergeAccounting(balances, settlements?.settlements, uncashedAmounts)
+10
View File
@@ -0,0 +1,10 @@
import { ReactElement } from 'react'
import { StripedWrapper } from '../../components/StripedWrapper'
interface Props {
icon: ReactElement
}
export function AssetIcon({ icon }: Props): ReactElement {
return <StripedWrapper>{icon}</StripedWrapper>
}
+95
View File
@@ -0,0 +1,95 @@
import { Box, Grid, Typography } from '@material-ui/core'
import { Web } from '@material-ui/icons'
import { ReactElement, useEffect, useState } from 'react'
import { File, Folder } from 'react-feather'
import { FitImage } from '../../components/FitImage'
import { detectIndexHtml, getHumanReadableFileSize } from '../../utils/file'
import { SwarmFile } from '../../utils/SwarmFile'
import { AssetIcon } from './AssetIcon'
interface Props {
files: SwarmFile[]
}
export function AssetPreview({ files }: Props): ReactElement {
const [previewComponent, setPreviewComponent] = useState<ReactElement | undefined>(undefined)
const [previewUri, setPreviewUri] = useState<string | undefined>(undefined)
useEffect(() => {
if (files.length === 1) {
// single image
if (files[0].type.startsWith('image/')) {
files[0].arrayBuffer().then(value => {
const blob = new Blob([value])
setPreviewUri(URL.createObjectURL(blob))
})
// single non-image
} else {
setPreviewUri(undefined)
setPreviewComponent(<AssetIcon icon={<File />} />)
}
// collection
} else if (detectIndexHtml(files)) {
setPreviewUri(undefined)
setPreviewComponent(<AssetIcon icon={<Web />} />)
} else {
setPreviewUri(undefined)
setPreviewComponent(<AssetIcon icon={<Folder />} />)
}
}, [files])
const getPrimaryText = () => {
if (files.length === 1) {
return 'Filename: ' + files[0].name
}
return 'Folder name: ' + files[0].path.split('/')[0]
}
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)
}
return (
<Box mb={4}>
<Box bgcolor="background.paper">
<Grid container direction="row">
{previewComponent ? (
previewComponent
) : (
<FitImage maxWidth="250px" maxHeight="175px" alt="Upload Preview" src={previewUri} />
)}
<Box p={2}>
<Typography>{getPrimaryText()}</Typography>
<Typography>Kind: {getKind()}</Typography>
<Typography>Size: {getSize()}</Typography>
</Box>
</Grid>
</Box>
{isFolder() && (
<Box mt={0.25} p={2} bgcolor="background.paper">
<Grid container justifyContent="space-between" alignItems="center" direction="row">
<Typography variant="subtitle2">Folder content</Typography>
<Typography variant="subtitle2">{files.length} items</Typography>
</Grid>
</Box>
)}
</Box>
)
}
+69 -6
View File
@@ -1,28 +1,91 @@
import { ReactElement, useState, useContext } from 'react'
import { Context as SettingsContext } from '../../providers/Settings'
import ExpandableListItemInput from '../../components/ExpandableListItemInput'
import { Utils } from '@ethersphere/bee-js' import { Utils } from '@ethersphere/bee-js'
import { Box } from '@material-ui/core'
import { useSnackbar } from 'notistack'
import { ReactElement, useContext, useState } from 'react'
import ExpandableListItemInput from '../../components/ExpandableListItemInput'
import { Context as SettingsContext } from '../../providers/Settings'
import { extractSwarmHash } from '../../utils'
import { convertBeeFileToBrowserFile } from '../../utils/file'
import { SwarmFile } from '../../utils/SwarmFile'
import { AssetPreview } from './AssetPreview'
import { DownloadActionBar } from './DownloadActionBar'
export default function Files(): ReactElement { export default function Files(): ReactElement {
const { apiUrl } = useContext(SettingsContext) const { apiUrl, beeApi } = useContext(SettingsContext)
const [reference, setReference] = useState('')
const [referenceError, setReferenceError] = useState<string | undefined>(undefined) const [referenceError, setReferenceError] = useState<string | undefined>(undefined)
const [downloadedFile, setDownloadedFile] = useState<Partial<File> | null>(null)
const { enqueueSnackbar } = useSnackbar()
const validateChange = (value: string) => { const validateChange = (value: string) => {
if (Utils.isHexString(value, 64) || Utils.isHexString(value, 128)) setReferenceError(undefined) if (Utils.isHexString(value, 64) || Utils.isHexString(value, 128)) setReferenceError(undefined)
else setReferenceError('Incorrect format of swarm hash. Expected 64 or 128 hexstring characters.') else setReferenceError('Incorrect format of swarm hash. Expected 64 or 128 hexstring characters.')
} }
function onDownload() {
window.open(`${apiUrl}/bzz/${reference}/`, '_blank')
}
async function onSwarmIdentifier(identifier: string) {
if (!beeApi) {
return
}
setReference(identifier)
try {
const response = await beeApi.downloadFile(identifier)
setDownloadedFile(convertBeeFileToBrowserFile(response))
} catch (error: unknown) {
let message = typeof error === 'object' && error !== null && Reflect.get(error, 'message')
if (message.includes('path address not found')) {
message = 'The specified hash does not have an index document set.'
}
if (message.includes('Not Found: Not Found')) {
message = 'The specified hash was not found.'
}
enqueueSnackbar(<span>Error: {message || 'Unknown'}</span>, { variant: 'error' })
}
}
if (downloadedFile) {
return (
<>
<Box mb={4}>
<AssetPreview files={[new SwarmFile(downloadedFile as File)]} />
</Box>
<DownloadActionBar onCancel={() => setDownloadedFile(null)} onDownload={onDownload} />
</>
)
}
function recognizeSwarmHash(value: string) {
if (value.length < 64) {
return value
}
const hash = extractSwarmHash(value)
if (hash) {
return hash
}
return value
}
return ( return (
<ExpandableListItemInput <ExpandableListItemInput
label="Swarm Hash" label="Swarm Hash"
onConfirm={value => window.open(`${apiUrl}/bzz/${value}`, '_blank')} onConfirm={value => onSwarmIdentifier(value)}
onChange={validateChange} onChange={validateChange}
helperText={referenceError} helperText={referenceError}
confirmLabel={'Download'} confirmLabel={'Search'}
confirmLabelDisabled={Boolean(referenceError)} confirmLabelDisabled={Boolean(referenceError)}
placeholder="e.g. 31fb0362b1a42536134c86bc58b97ac0244e5c6630beec3e27c2d1cecb38c605" placeholder="e.g. 31fb0362b1a42536134c86bc58b97ac0244e5c6630beec3e27c2d1cecb38c605"
expandedOnly expandedOnly
mapperFn={value => recognizeSwarmHash(value)}
/> />
) )
} }
+24
View File
@@ -0,0 +1,24 @@
import { Button } from '@material-ui/core'
import { Clear } from '@material-ui/icons'
import { ReactElement } from 'react'
import { Download } from 'react-feather'
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
import { SwarmButton } from '../../components/SwarmButton'
interface Props {
onDownload: () => void
onCancel: () => void
}
export function DownloadActionBar({ onDownload, onCancel }: Props): ReactElement {
return (
<ExpandableListItemActions>
<SwarmButton onClick={onDownload} iconType={Download}>
Download This File
</SwarmButton>
<Button onClick={onCancel} variant="contained" startIcon={<Clear />}>
Cancel
</Button>
</ExpandableListItemActions>
)
}
+38
View File
@@ -0,0 +1,38 @@
import { Box, Typography } from '@material-ui/core'
import { ReactElement } from 'react'
import { CornerUpLeft } from 'react-feather'
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
import ExpandableListItemKey from '../../components/ExpandableListItemKey'
import ExpandableListItemLink from '../../components/ExpandableListItemLink'
import { SwarmButton } from '../../components/SwarmButton'
interface Props {
uploadReference: string
onUploadNewClick: () => void
}
export function PostUploadSummary({ uploadReference, onUploadNewClick }: Props): ReactElement {
return (
<>
<Box mb={4}>
<ExpandableListItemKey label="Swarm hash" value={uploadReference} />
<ExpandableListItemLink
label="Share on Swarm Gateway"
value={`https://gateway.ethswarm.org/access/${uploadReference}`}
/>
</Box>
<Box mb={2}>
<ExpandableListItemActions>
<SwarmButton onClick={onUploadNewClick} iconType={CornerUpLeft}>
Back to Upload
</SwarmButton>
</ExpandableListItemActions>
</Box>
<Typography>
The Swarm Gateway is graciously provided by the Swarm Foundation. This service is under development and provided
for testing purposes only. Learn more at{' '}
<a href="https://gateway.ethswarm.org/">https://gateway.ethswarm.org/</a>.
</Typography>
</>
)
}
+21
View File
@@ -0,0 +1,21 @@
import { Box, Typography } from '@material-ui/core'
import { ReactElement } from 'react'
import { EnrichedPostageBatch } from '../../providers/Stamps'
import { PostageStamp } from '../stamps/PostageStamp'
interface Props {
stamp: EnrichedPostageBatch
}
export function StampPreview({ stamp }: Props): ReactElement {
return (
<Box mb={4}>
<Box mb={0.25} p={2} bgcolor="background.paper">
<Typography variant="subtitle2">Associated postage stamp:</Typography>
</Box>
<Box bgcolor="background.paper">
<PostageStamp stamp={stamp} shorten={true} />
</Box>
</Box>
)
}
+59 -114
View File
@@ -1,19 +1,17 @@
import { Button, CircularProgress, Container, Avatar, Chip, Typography } from '@material-ui/core' import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import { DropzoneArea } from 'material-ui-dropzone'
import { useSnackbar } from 'notistack' import { useSnackbar } from 'notistack'
import { RotateCcw, Check } from 'react-feather'
import { ReactElement, useContext, useEffect, useState } from 'react' import { ReactElement, useContext, useEffect, useState } from 'react'
import UploadSizeAlert from '../../components/AlertUploadSize'
import ClipboardCopy from '../../components/ClipboardCopy'
import { Context, EnrichedPostageBatch } from '../../providers/Stamps'
import { Context as SettingsContext } from '../../providers/Settings' import { Context as SettingsContext } from '../../providers/Settings'
import CreatePostageStamp from '../stamps/CreatePostageStampModal' import { Context, EnrichedPostageBatch } from '../../providers/Stamps'
import SelectStamp from './SelectStamp' import { detectIndexHtml } from '../../utils/file'
import ExpandableListItem from '../../components/ExpandableListItem' import { SwarmFile } from '../../utils/SwarmFile'
import ExpandableListItemKey from '../../components/ExpandableListItemKey' import { CreatePostageStampModal } from '../stamps/CreatePostageStampModal'
import ExpandableListItemNote from '../../components/ExpandableListItemNote' import { SelectPostageStampModal } from '../stamps/SelectPostageStampModal'
import ExpandableListItemActions from '../../components/ExpandableListItemActions' import { AssetPreview } from './AssetPreview'
import { PostUploadSummary } from './PostUploadSummary'
import { StampPreview } from './StampPreview'
import { UploadActionBar } from './UploadActionBar'
import { UploadArea } from './UploadArea'
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
createStyles({ createStyles({
@@ -27,137 +25,84 @@ const MAX_FILE_SIZE = 1_000_000_000 // 1 gigabyte
export default function Files(): ReactElement { export default function Files(): ReactElement {
const classes = useStyles() const classes = useStyles()
const [dropzoneKey, setDropzoneKey] = useState(0) const [dropzoneKey, setDropzoneKey] = useState(0)
const [file, setFile] = useState<File | null>(null) const [files, setFiles] = useState<SwarmFile[]>([])
const [uploadReference, setUploadReference] = useState('') const [uploadReference, setUploadReference] = useState('')
const [isUploadingFile, setIsUploadingFile] = useState(false) const [isBuyingStamp, setBuyingStamp] = useState(false)
const [isSelectingStamp, setSelectingStamp] = useState(false)
const [stamp, setStamp] = useState<EnrichedPostageBatch | null>(null)
const [isUploading, setUploading] = useState(false)
const [selectedStamp, setSelectedStamp] = useState<EnrichedPostageBatch | null>(null) const { stamps, refresh } = useContext(Context)
const { isLoading, error, stamps, refresh } = useContext(Context)
const { beeApi } = useContext(SettingsContext) const { beeApi } = useContext(SettingsContext)
const { enqueueSnackbar } = useSnackbar() const { enqueueSnackbar } = useSnackbar()
useEffect(() => { useEffect(() => {
refresh() refresh()
}, []) }, []) // eslint-disable-line react-hooks/exhaustive-deps
// Choose a postage stamp that has the lowest usage const uploadFiles = () => {
useEffect(() => { if (!beeApi || !files.length || !stamp) {
if (!selectedStamp && stamps && stamps.length > 0) { return
const stamp = stamps.reduce((prev, curr) => {
if (curr.usage < prev.usage) return curr
return prev
}, stamps[0])
setSelectedStamp(stamp)
} }
}, [isLoading, error, stamps, selectedStamp])
const uploadFile = () => { const indexDocument = files.length === 1 ? files[0].name : detectIndexHtml(files) || undefined
if (file === null || selectedStamp === null) return
if (!beeApi) return setUploading(true)
setIsUploadingFile(true)
beeApi beeApi
.uploadFile(selectedStamp.batchID, file) .uploadFiles(stamp.batchID, files as unknown as File[], { indexDocument })
.then(hash => setUploadReference(hash.reference)) .then(hash => setUploadReference(hash.reference))
.catch(e => enqueueSnackbar(`Error uploading: ${e.message}`, { variant: 'error' })) .catch(e => enqueueSnackbar(`Error uploading: ${e.message}`, { variant: 'error' }))
.finally(() => setIsUploadingFile(false)) .finally(() => setUploading(false))
}
const reset = () => {
setFiles([])
setStamp(null)
setUploading(false)
} }
const uploadNew = () => { const uploadNew = () => {
setTimeout(() => { setTimeout(() => {
setFile(null) reset()
setDropzoneKey(dropzoneKey + 1) setDropzoneKey(dropzoneKey + 1)
setUploadReference('') setUploadReference('')
}, 0) }, 0)
} }
const handleChange = (files?: File[]) => {
setUploadReference('')
if (files) {
setFile(files[0])
}
}
return ( return (
<> <>
<DropzoneArea {files.length ? (
key={'dropzone-' + dropzoneKey} <AssetPreview files={files} />
onChange={handleChange} ) : (
filesLimit={1} <UploadArea maximumSizeInBytes={MAX_FILE_SIZE} setFiles={setFiles} />
maxFileSize={MAX_FILE_SIZE} )}
/> {stamp !== null && !uploadReference ? <StampPreview stamp={stamp} /> : null}
{files.length && !uploadReference ? (
<UploadActionBar
canSelectStamp={stamps !== null && stamps.length > 0}
hasSelectedStamp={stamp !== null}
onCancel={reset}
onBuy={() => setBuyingStamp(true)}
onSelect={() => setSelectingStamp(true)}
onUpload={uploadFiles}
onClearStamp={() => setStamp(null)}
isUploading={isUploading}
/>
) : null}
<div className={classes.content}> <div className={classes.content}>
{/* We have file and can upload display stamp selection */}
{file && !isUploadingFile && !uploadReference && (
<>
<ExpandableListItemNote>
To upload this file to your node, you need a postage stamp. You can buy a new one or you can use an
existing stamp (providing its sufficient for this file).
</ExpandableListItemNote>
{selectedStamp && (
<ExpandableListItem
label={
<>
Upload with Postage Stamp{' '}
<Chip
avatar={<Avatar>{selectedStamp.usageText}</Avatar>}
label={<Typography variant="body2">{selectedStamp.batchID.substr(0, 8)}[]</Typography>}
deleteIcon={<ClipboardCopy value={selectedStamp.batchID} />}
onDelete={() => {} /* eslint-disable-line*/}
variant="outlined"
/>
</>
}
value={<SelectStamp stamps={stamps} selectedStamp={selectedStamp} setSelected={setSelectedStamp} />}
/>
)}
{!selectedStamp && (
<ExpandableListItemActions>
<CreatePostageStamp />
</ExpandableListItemActions>
)}
</>
)}
{/* We have file and can upload display upload button */}
{file && !uploadReference && (
<>
<ExpandableListItemActions>
<Button
variant="contained"
disabled={!file && isUploadingFile && !selectedStamp}
onClick={() => uploadFile()}
startIcon={<Check size="1rem" />}
>
Upload
</Button>
{isUploadingFile && (
<Container className={classes.loadingProgress}>
<CircularProgress />
</Container>
)}
</ExpandableListItemActions>
<UploadSizeAlert file={file} />
</>
)}
{/* File has already been uploaded */}
{uploadReference && ( {uploadReference && (
<> <PostUploadSummary onUploadNewClick={() => uploadNew()} uploadReference={uploadReference} />
<ExpandableListItemKey label="Swarm Reference" value={uploadReference} />
<ExpandableListItemActions>
<Button variant="contained" onClick={uploadNew} startIcon={<RotateCcw size="1rem" />}>
Upload New File
</Button>
</ExpandableListItemActions>
</>
)} )}
</div> </div>
{isBuyingStamp ? <CreatePostageStampModal onClose={() => setBuyingStamp(false)} /> : null}
{stamps && isSelectingStamp ? (
<SelectPostageStampModal
stamps={stamps}
onClose={() => setSelectingStamp(false)}
onSelect={stamp => setStamp(stamp)}
/>
) : null}
</> </>
) )
} }
+69
View File
@@ -0,0 +1,69 @@
import { Button, Typography } from '@material-ui/core'
import { Clear } from '@material-ui/icons'
import { ReactElement } from 'react'
import { Check, Layers, PlusSquare, RefreshCcw } from 'react-feather'
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
import { SwarmButton } from '../../components/SwarmButton'
interface Props {
canSelectStamp: boolean
hasSelectedStamp: boolean
onUpload: () => void
onBuy: () => void
onSelect: () => void
onCancel: () => void
onClearStamp: () => void
isUploading: boolean
}
export function UploadActionBar({
canSelectStamp,
hasSelectedStamp,
onUpload,
onBuy,
onSelect,
onCancel,
onClearStamp,
isUploading,
}: Props): ReactElement {
const showBuy = !hasSelectedStamp
const showSelect = canSelectStamp && !hasSelectedStamp
const showUpload = hasSelectedStamp
const showChange = canSelectStamp && hasSelectedStamp
return (
<>
<ExpandableListItemActions>
{showBuy ? (
<SwarmButton onClick={onBuy} iconType={PlusSquare}>
Buy New Postage Stamp
</SwarmButton>
) : null}
{showSelect ? (
<SwarmButton onClick={onSelect} iconType={Layers}>
Use Existing Postage Stamp
</SwarmButton>
) : null}
{showUpload ? (
<SwarmButton onClick={onUpload} iconType={Check} disabled={isUploading} loading={isUploading}>
Upload To Your Node
</SwarmButton>
) : null}
{showChange ? (
<SwarmButton onClick={onClearStamp} iconType={RefreshCcw} disabled={isUploading}>
Change Postage Stamp
</SwarmButton>
) : null}
<Button onClick={onCancel} variant="contained" startIcon={<Clear />}>
Cancel
</Button>
</ExpandableListItemActions>
{showSelect ? (
<Typography>
You need a postage stamp to upload. Please refer to the official Bee documentation to understand how postage
stamps work.
</Typography>
) : null}
</>
)
}
+119
View File
@@ -0,0 +1,119 @@
import { createStyles, makeStyles, Theme, Typography } from '@material-ui/core'
import { DropzoneArea } from 'material-ui-dropzone'
import { useSnackbar } from 'notistack'
import { ReactElement } from 'react'
import { FilePlus } from 'react-feather'
import { SwarmButton } from '../../components/SwarmButton'
import { detectIndexHtml } from '../../utils/file'
import { SwarmFile } from '../../utils/SwarmFile'
interface Props {
setFiles: (files: SwarmFile[]) => void
maximumSizeInBytes: number
}
const useStyles = makeStyles((theme: Theme) =>
createStyles({
areaWrapper: { position: 'relative', marginBottom: theme.spacing(2) },
dropzone: {
background: theme.palette.background.default,
outline: 'none',
color: 'transparent',
zIndex: 1,
'& svg': {
opacity: 0,
},
},
buttonWrapper: {
top: '0',
left: '0',
position: 'absolute',
display: 'flex',
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center',
},
button: {
marginLeft: theme.spacing(0.5),
marginRight: theme.spacing(0.5),
zIndex: 2,
},
}),
)
export function UploadArea({ setFiles, maximumSizeInBytes }: Props): ReactElement {
const classes = useStyles()
const { enqueueSnackbar } = useSnackbar()
const getDropzoneInputDomElement = () => document.querySelector('.MuiDropzoneArea-root input') as HTMLInputElement
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const onUploadFolderClick = () => {
const element = getDropzoneInputDomElement()
if (element) {
element.setAttribute('directory', '')
element.setAttribute('webkitdirectory', '')
element.setAttribute('mozdirectory', '')
element.click()
}
}
const onUploadFileClick = () => {
const element = getDropzoneInputDomElement()
if (element) {
element.removeAttribute('directory')
element.removeAttribute('webkitdirectory')
element.removeAttribute('mozdirectory')
element.click()
}
}
const resetComponentOnAddingInvalidContent = (files: SwarmFile[]) => {
setFiles(files)
setTimeout(() => {
setFiles([])
}, 0)
}
const handleChange = (files?: File[]) => {
if (files) {
const swarmFiles = files.map(x => new SwarmFile(x))
const indexDocument = files.length === 1 ? files[0].name : detectIndexHtml(swarmFiles) || undefined
if (files.length && !indexDocument) {
enqueueSnackbar('To upload a website, there must be an index.html or index.htm in the root of the folder.', {
variant: 'error',
})
resetComponentOnAddingInvalidContent(swarmFiles)
return
}
setFiles(swarmFiles)
}
}
return (
<>
<div className={classes.areaWrapper}>
<DropzoneArea
dropzoneClass={classes.dropzone}
onChange={handleChange}
filesLimit={1}
maxFileSize={maximumSizeInBytes}
showPreviews={false}
/>
<div className={classes.buttonWrapper}>
<SwarmButton className={classes.button} onClick={onUploadFileClick} iconType={FilePlus}>
Add File
</SwarmButton>
</div>
</div>
<Typography>You can click the button above or simply drag and drop to add a file.</Typography>
</>
)
}
+20 -29
View File
@@ -1,18 +1,18 @@
import React, { ReactElement, useContext } from 'react'
import Button from '@material-ui/core/Button' import Button from '@material-ui/core/Button'
import CircularProgress from '@material-ui/core/CircularProgress'
import Dialog from '@material-ui/core/Dialog' import Dialog from '@material-ui/core/Dialog'
import DialogActions from '@material-ui/core/DialogActions' import DialogActions from '@material-ui/core/DialogActions'
import DialogContent from '@material-ui/core/DialogContent' import DialogContent from '@material-ui/core/DialogContent'
import DialogContentText from '@material-ui/core/DialogContentText' import DialogContentText from '@material-ui/core/DialogContentText'
import CircularProgress from '@material-ui/core/CircularProgress'
import DialogTitle from '@material-ui/core/DialogTitle' import DialogTitle from '@material-ui/core/DialogTitle'
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import BigNumber from 'bignumber.js' import BigNumber from 'bignumber.js'
import { FormikHelpers, Form, Field, Formik } from 'formik' import { Field, Form, Formik, FormikHelpers } from 'formik'
import { TextField } from 'formik-material-ui' import { TextField } from 'formik-material-ui'
import { useSnackbar } from 'notistack'
import React, { ReactElement, useContext } from 'react'
import { Context as SettingsContext } from '../../providers/Settings' import { Context as SettingsContext } from '../../providers/Settings'
import { Context } from '../../providers/Stamps' import { Context } from '../../providers/Stamps'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import { useSnackbar } from 'notistack'
interface FormValues { interface FormValues {
depth?: string depth?: string
@@ -47,16 +47,13 @@ const useStyles = makeStyles((theme: Theme) =>
) )
interface Props { interface Props {
label?: string onClose: () => void
} }
export default function FormDialog({ label }: Props): ReactElement { export function CreatePostageStampModal({ onClose }: Props): ReactElement {
const classes = useStyles() const classes = useStyles()
const [open, setOpen] = React.useState(false)
const { refresh } = useContext(Context) const { refresh } = useContext(Context)
const { beeApi } = useContext(SettingsContext) const { beeDebugApi } = useContext(SettingsContext)
const handleClickOpen = () => setOpen(true)
const handleClose = () => setOpen(false)
const { enqueueSnackbar } = useSnackbar() const { enqueueSnackbar } = useSnackbar()
return ( return (
@@ -67,17 +64,17 @@ export default function FormDialog({ label }: Props): ReactElement {
// This is really just a typeguard, the validation pretty much guarantees these will have the right values // This is really just a typeguard, the validation pretty much guarantees these will have the right values
if (!values.depth || !values.amount) return if (!values.depth || !values.amount) return
if (!beeApi) return if (!beeDebugApi) return
const amount = BigInt(values.amount) const amount = BigInt(values.amount)
const depth = Number.parseInt(values.depth) const depth = Number.parseInt(values.depth)
const options = values.label ? { label: values.label } : undefined const options = values.label ? { label: values.label } : undefined
await beeApi.createPostageBatch(amount.toString(), depth, options) await beeDebugApi.createPostageBatch(amount.toString(), depth, options)
actions.resetForm() actions.resetForm()
await refresh() await refresh()
handleClose() onClose()
} catch (e) { } catch (e) {
enqueueSnackbar(`Error: ${e.message}`, { variant: 'error' }) enqueueSnackbar(`Error: ${(e as Error).message}`, { variant: 'error' })
actions.setSubmitting(false) actions.setSubmitting(false)
} }
}} }}
@@ -111,20 +108,9 @@ export default function FormDialog({ label }: Props): ReactElement {
> >
{({ submitForm, isValid, isSubmitting, values }) => ( {({ submitForm, isValid, isSubmitting, values }) => (
<Form> <Form>
<Button variant="contained" onClick={handleClickOpen}> <Dialog open={true} onClose={onClose} aria-labelledby="form-dialog-title">
{label || 'Buy Postage Stamp'} <DialogTitle id="form-dialog-title">Buy new postage stamp</DialogTitle>
{isSubmitting && <CircularProgress size={24} className={classes.buttonProgress} />}
</Button>
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">Purchase new postage stamp</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText>
Provide the depth, amount and optionally the label of the postage stamp. Please refer to the{' '}
<a href="https://docs.ethswarm.org/docs/access-the-swarm/keep-your-data-alive" target="blank">
official bee docs
</a>{' '}
to understand these values.
</DialogContentText>
<Field <Field
component={TextField} component={TextField}
required required
@@ -138,7 +124,7 @@ export default function FormDialog({ label }: Props): ReactElement {
<Field component={TextField} name="label" label="Label" fullWidth className={classes.field} /> <Field component={TextField} name="label" label="Label" fullWidth className={classes.field} />
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={handleClose} variant="contained"> <Button onClick={onClose} variant="contained">
Cancel Cancel
</Button> </Button>
<div className={classes.wrapper}> <div className={classes.wrapper}>
@@ -153,6 +139,11 @@ export default function FormDialog({ label }: Props): ReactElement {
</Button> </Button>
</div> </div>
</DialogActions> </DialogActions>
<DialogContent>
<DialogContentText>
Please refer to the official Bee documentation to understand these values.
</DialogContentText>
</DialogContent>
</Dialog> </Dialog>
</Form> </Form>
)} )}
+20
View File
@@ -0,0 +1,20 @@
import { Box, Grid, Typography } from '@material-ui/core'
import { ReactElement } from 'react'
import { Capacity } from '../../components/Capacity'
import { EnrichedPostageBatch } from '../../providers/Stamps'
interface Props {
stamp: EnrichedPostageBatch
shorten?: boolean
}
export function PostageStamp({ stamp, shorten }: Props): ReactElement {
return (
<Box p={2} width="100%">
<Grid container justifyContent="space-between" alignItems="center" direction="row">
<Typography variant="subtitle2">{shorten ? stamp.batchID.slice(0, 8) : stamp.batchID}</Typography>
<Capacity width="100px" usage={stamp.usage} />
</Grid>
</Box>
)
}
@@ -0,0 +1,118 @@
import { Box, createStyles, FormControl, makeStyles, MenuItem, Select, Theme, Typography } from '@material-ui/core'
import Button from '@material-ui/core/Button'
import Dialog from '@material-ui/core/Dialog'
import DialogContent from '@material-ui/core/DialogContent'
import DialogTitle from '@material-ui/core/DialogTitle'
import { Check, Clear } from '@material-ui/icons'
import React, { ReactElement, useState } from 'react'
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
import { EnrichedPostageBatch } from '../../providers/Stamps'
interface Props {
stamps: EnrichedPostageBatch[]
onSelect: (stamp: EnrichedPostageBatch) => void
onClose: () => void
}
const useStyles = makeStyles((theme: Theme) =>
createStyles({
dialog: {
background: theme.palette.background.default,
borderRadius: 0,
width: '100%',
maxWidth: '890px',
},
title: {
color: '#606060',
textAlign: 'center',
},
select: {
background: theme.palette.background.paper,
borderRadius: 0,
border: 0,
},
option: {
height: '52px',
},
hint: {
marginBottom: '16px',
},
}),
)
export function SelectPostageStampModal({ stamps, onSelect, onClose }: Props): ReactElement {
const [selectedStamp, setSelectedStamp] = useState<EnrichedPostageBatch | null>(null)
const classes = useStyles()
function onChange(stampId: string) {
const stamp = stamps.find(x => x.batchID === stampId)
if (stamp) {
setSelectedStamp(stamp)
}
}
function onFinish() {
if (selectedStamp) {
onSelect(selectedStamp)
onClose()
}
}
return (
<Dialog
open={true}
onClose={onClose}
aria-labelledby="form-dialog-title"
fullWidth
PaperProps={{ className: classes.dialog }}
>
<DialogTitle id="form-dialog-title" className={classes.title}>
Select postage stamp
</DialogTitle>
<DialogContent>
<FormControl fullWidth>
<Select
onChange={event => onChange(event.target.value as string)}
fullWidth
variant="outlined"
className={classes.select}
defaultValue=""
>
{stamps.map(x => (
<MenuItem key={x.batchID} value={x.batchID} className={classes.option}>
{x.batchID.slice(0, 8)}
</MenuItem>
))}
</Select>
</FormControl>
</DialogContent>
<Box mb={2}>
<DialogContent>
<ExpandableListItemActions>
<Button disabled={!selectedStamp} onClick={onFinish} variant="contained" startIcon={<Check />}>
Select
</Button>
<Button onClick={onClose} variant="contained" startIcon={<Clear />}>
Cancel
</Button>
</ExpandableListItemActions>
</DialogContent>
</Box>
<DialogContent>
<Typography className={classes.hint}>
Please refer to the{' '}
<a
href="https://docs.ethswarm.org/docs/access-the-swarm/keep-your-data-alive#purchase-a-batch-of-stamps"
target="_blank"
rel="noreferrer"
>
official Bee documentation
</a>{' '}
to understand these values.
</Typography>
</DialogContent>
</Dialog>
)
}
+10 -7
View File
@@ -1,8 +1,9 @@
import type { ReactElement } from 'react' import type { ReactElement } from 'react'
import { EnrichedPostageBatch } from '../../providers/Stamps' import ExpandableElement from '../../components/ExpandableElement'
import ExpandableList from '../../components/ExpandableList' import ExpandableList from '../../components/ExpandableList'
import ExpandableListItem from '../../components/ExpandableListItem'
import ExpandableListItemKey from '../../components/ExpandableListItemKey' import ExpandableListItemKey from '../../components/ExpandableListItemKey'
import { EnrichedPostageBatch } from '../../providers/Stamps'
import { PostageStamp } from './PostageStamp'
interface Props { interface Props {
postageStamps: EnrichedPostageBatch[] | null postageStamps: EnrichedPostageBatch[] | null
@@ -13,11 +14,13 @@ function StampsTable({ postageStamps }: Props): ReactElement | null {
return ( return (
<ExpandableList label="Postage Stamps" defaultOpen> <ExpandableList label="Postage Stamps" defaultOpen>
{postageStamps.map(({ batchID, usageText }) => ( {postageStamps.map(stamp => (
<ExpandableList key={batchID} label={`${batchID.substr(0, 8)}[…]`} level={1} info={`${usageText} used`}> <ExpandableElement
<ExpandableListItemKey label="Batch ID" value={batchID} /> key={stamp.batchID}
<ExpandableListItem label="Usage" value={usageText} /> expandable={<ExpandableListItemKey label="Batch ID" value={stamp.batchID} />}
</ExpandableList> >
<PostageStamp stamp={stamp} shorten={true} />
</ExpandableElement>
))} ))}
</ExpandableList> </ExpandableList>
) )
+20 -12
View File
@@ -1,13 +1,13 @@
import { ReactElement, useContext, useEffect } from 'react' import { CircularProgress, Container } from '@material-ui/core'
import { createStyles, makeStyles } from '@material-ui/core/styles' import { createStyles, makeStyles } from '@material-ui/core/styles'
import { Container, CircularProgress } from '@material-ui/core' import { ReactElement, useContext, useEffect, useState } from 'react'
import { PlusSquare } from 'react-feather'
import StampsTable from './StampsTable' import { SwarmButton } from '../../components/SwarmButton'
import CreatePostageStampModal from './CreatePostageStampModal'
import { Context as StampsContext } from '../../providers/Stamps'
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard' import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
import { Context as BeeContext } from '../../providers/Bee' import { Context as BeeContext } from '../../providers/Bee'
import { Context as StampsContext } from '../../providers/Stamps'
import { CreatePostageStampModal } from './CreatePostageStampModal'
import StampsTable from './StampsTable'
const useStyles = makeStyles(() => const useStyles = makeStyles(() =>
createStyles({ createStyles({
@@ -25,18 +25,22 @@ const useStyles = makeStyles(() =>
}), }),
) )
export default function Accounting(): ReactElement { export default function Stamp(): ReactElement {
const classes = useStyles() const classes = useStyles()
const [isBuyingStamp, setBuyingStamp] = useState(false)
const { stamps, isLoading, error, start, stop } = useContext(StampsContext) const { stamps, isLoading, error, start, stop } = useContext(StampsContext)
const { status } = useContext(BeeContext) const { status } = useContext(BeeContext)
if (!status.all) return <TroubleshootConnectionCard />
useEffect(() => { useEffect(() => {
if (!status.all) return
start() start()
return () => stop() return () => stop()
}, []) }, [status]) // eslint-disable-line react-hooks/exhaustive-deps
if (!status.all) return <TroubleshootConnectionCard />
return ( return (
<div className={classes.root}> <div className={classes.root}>
@@ -48,7 +52,11 @@ export default function Accounting(): ReactElement {
{!error && ( {!error && (
<> <>
<div className={classes.actions}> <div className={classes.actions}>
<CreatePostageStampModal /> {isBuyingStamp ? <CreatePostageStampModal onClose={() => setBuyingStamp(false)} /> : null}
<SwarmButton onClick={() => setBuyingStamp(true)} iconType={PlusSquare}>
Buy New Postage Stamp
</SwarmButton>
<div style={{ height: '5px' }}>{isLoading && <CircularProgress />}</div> <div style={{ height: '5px' }}>{isLoading && <CircularProgress />}</div>
</div> </div>
<StampsTable postageStamps={stamps} /> <StampsTable postageStamps={stamps} />
@@ -1,14 +1,13 @@
import { ReactElement, useContext } from 'react'
import MuiAlert from '@material-ui/lab/Alert' import MuiAlert from '@material-ui/lab/Alert'
import { ReactElement, useContext } from 'react'
import CodeBlockTabs from '../../../components/CodeBlockTabs' import CodeBlockTabs from '../../../components/CodeBlockTabs'
import ExpandableList from '../../../components/ExpandableList' import ExpandableList from '../../../components/ExpandableList'
import ExpandableListItem from '../../../components/ExpandableListItem' 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 as SettingsContext } from '../../../providers/Settings'
import { Context } from '../../../providers/Bee' import { Context } from '../../../providers/Bee'
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)
@@ -25,7 +24,7 @@ export default function NodeConnectionCheck(): ReactElement | null {
> >
<ExpandableListItemNote> <ExpandableListItemNote>
{isOk {isOk
? 'The connection to the Bee nodes deug 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} />
+21 -17
View File
@@ -1,19 +1,18 @@
import type { ChequebookBalance, Balance, Settlements } from '../types'
import { createContext, ReactChild, ReactElement, useEffect, useState, useContext } from 'react'
import { Token } from '../models/Token'
import semver from 'semver'
import { engines } from '../../package.json'
import { Context as SettingsContext } from './Settings'
import type { import type {
NodeAddresses,
ChequebookAddressResponse, ChequebookAddressResponse,
LastChequesResponse,
Health, Health,
LastChequesResponse,
NodeAddresses,
Peer, Peer,
Topology, Topology,
} from '@ethersphere/bee-js' } from '@ethersphere/bee-js'
import { createContext, ReactChild, ReactElement, useContext, useEffect, useState } from 'react'
import semver from 'semver'
import { engines } from '../../package.json'
import { useLatestBeeRelease } from '../hooks/apiHooks' import { useLatestBeeRelease } from '../hooks/apiHooks'
import { Token } from '../models/Token'
import type { Balance, ChequebookBalance, Settlements } from '../types'
import { Context as SettingsContext } from './Settings'
interface Status { interface Status {
all: boolean all: boolean
@@ -52,6 +51,8 @@ interface ContextInterface {
refresh: () => Promise<void> refresh: () => Promise<void>
} }
const startedInDevMode = window.location.search.includes('devMode=1')
const initialValues: ContextInterface = { const initialValues: ContextInterface = {
status: { status: {
all: false, all: false,
@@ -103,6 +104,8 @@ function getStatus(
chequebookBalance: ChequebookBalance | null, chequebookBalance: ChequebookBalance | null,
error: Error | null, error: Error | null,
): Status { ): Status {
// FIXME: `devMode` is a temporary workaround to be able to develop with only one node
const devMode = startedInDevMode || Boolean(process.env.REACT_APP_DEV_MODE)
const status = { const status = {
version: Boolean( version: Boolean(
debugApiHealth && debugApiHealth &&
@@ -113,11 +116,12 @@ function getStatus(
blockchainConnection: Boolean(nodeAddresses?.ethereum), blockchainConnection: Boolean(nodeAddresses?.ethereum),
debugApiConnection: Boolean(debugApiHealth?.status === 'ok'), debugApiConnection: Boolean(debugApiHealth?.status === 'ok'),
apiConnection: apiHealth, apiConnection: apiHealth,
topology: Boolean(topology?.connected && topology?.connected > 0), topology: Boolean(topology?.connected && topology?.connected > 0) || devMode,
chequebook: chequebook:
Boolean(chequebookAddress?.chequebookAddress) && (Boolean(chequebookAddress?.chequebookAddress) &&
chequebookBalance !== null && chequebookBalance !== null &&
chequebookBalance?.totalBalance.toBigNumber.isGreaterThan(0), chequebookBalance?.totalBalance.toBigNumber.isGreaterThan(0)) ||
devMode,
} }
return { ...status, all: !error && Object.values(status).every(v => v) } return { ...status, all: !error && Object.values(status).every(v => v) }
@@ -153,7 +157,7 @@ export function Provider({ children }: Props): ReactElement {
setApiHealth(false) setApiHealth(false)
refresh() refresh()
}, [beeApi]) }, [beeApi]) // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => { useEffect(() => {
setIsLoading(true) setIsLoading(true)
@@ -169,7 +173,7 @@ export function Provider({ children }: Props): ReactElement {
setSettlements(null) setSettlements(null)
refresh() refresh()
}, [beeDebugApi]) }, [beeDebugApi]) // eslint-disable-line react-hooks/exhaustive-deps
const refresh = async () => { const refresh = async () => {
// Don't want to refresh when already refreshing // Don't want to refresh when already refreshing
@@ -279,7 +283,7 @@ export function Provider({ children }: Props): ReactElement {
await Promise.allSettled(promises) await Promise.allSettled(promises)
} catch (e) { } catch (e) {
setError(e) setError(e as Error)
} finally { } finally {
setIsLoading(false) setIsLoading(false)
setIsRefreshing(false) setIsRefreshing(false)
@@ -300,7 +304,7 @@ export function Provider({ children }: Props): ReactElement {
return () => clearInterval(interval) return () => clearInterval(interval)
} }
}, [frequency, beeDebugApi, beeApi]) }, [frequency, beeDebugApi, beeApi]) // eslint-disable-line react-hooks/exhaustive-deps
return ( return (
<Context.Provider <Context.Provider
+6 -6
View File
@@ -1,5 +1,5 @@
import { PostageBatch } from '@ethersphere/bee-js' import { PostageBatch } from '@ethersphere/bee-js'
import { createContext, ReactChild, ReactElement, useEffect, useState, useContext } from 'react' import { createContext, ReactChild, ReactElement, useContext, useEffect, useState } from 'react'
import { Context as SettingsContext } from './Settings' import { Context as SettingsContext } from './Settings'
export interface EnrichedPostageBatch extends PostageBatch { export interface EnrichedPostageBatch extends PostageBatch {
@@ -48,7 +48,7 @@ function enrichStamp(postageBatch: PostageBatch): EnrichedPostageBatch {
} }
export function Provider({ children }: Props): ReactElement { export function Provider({ children }: Props): ReactElement {
const { beeApi } = useContext(SettingsContext) const { beeDebugApi } = useContext(SettingsContext)
const [stamps, setStamps] = useState<EnrichedPostageBatch[] | null>(initialValues.stamps) const [stamps, setStamps] = useState<EnrichedPostageBatch[] | null>(initialValues.stamps)
const [error, setError] = useState<Error | null>(initialValues.error) const [error, setError] = useState<Error | null>(initialValues.error)
const [isLoading, setIsLoading] = useState<boolean>(initialValues.isLoading) const [isLoading, setIsLoading] = useState<boolean>(initialValues.isLoading)
@@ -59,16 +59,16 @@ export function Provider({ children }: Props): ReactElement {
// Don't want to refresh when already refreshing // Don't want to refresh when already refreshing
if (isLoading) return if (isLoading) return
if (!beeApi) return if (!beeDebugApi) return
try { try {
setIsLoading(true) setIsLoading(true)
const stamps = await beeApi.getAllPostageBatch() const stamps = await beeDebugApi.getAllPostageBatch()
setStamps(stamps.map(enrichStamp)) setStamps(stamps.map(enrichStamp))
setLastUpdate(Date.now()) setLastUpdate(Date.now())
} catch (e) { } catch (e) {
setError(e) setError(e as Error)
} finally { } finally {
setIsLoading(false) setIsLoading(false)
} }
@@ -87,7 +87,7 @@ export function Provider({ children }: Props): ReactElement {
return () => clearInterval(interval) return () => clearInterval(interval)
} }
}, [frequency]) }, [frequency]) // eslint-disable-line react-hooks/exhaustive-deps
return ( return (
<Context.Provider value={{ stamps, error, isLoading, lastUpdate, start, stop, refresh }}> <Context.Provider value={{ stamps, error, isLoading, lastUpdate, start, stop, refresh }}>
+2 -2
View File
@@ -1,4 +1,4 @@
import { createMuiTheme, Theme } from '@material-ui/core/styles' import { createTheme, Theme } from '@material-ui/core/styles'
import { orange } from '@material-ui/core/colors' import { orange } from '@material-ui/core/colors'
declare module '@material-ui/core/styles/createPalette' { declare module '@material-ui/core/styles/createPalette' {
@@ -170,7 +170,7 @@ const propsOverrides = {
}, },
} }
export const theme = createMuiTheme({ export const theme = createTheme({
palette: { palette: {
type: 'light', type: 'light',
background: { background: {
+24
View File
@@ -0,0 +1,24 @@
export class SwarmFile {
public name: string
public path: string
public type: string
public size: number
public webkitRelativePath: string
public arrayBuffer: () => Promise<ArrayBuffer>
private data: Promise<ArrayBuffer>
constructor(file: File) {
const path = Reflect.get(file, 'path') || file.webkitRelativePath || file.name
this.path = path.startsWith('/') ? path.slice(1) : path
this.webkitRelativePath = this.path
this.name = file.name
this.type = file.type
this.size = file.size
this.data = file.arrayBuffer()
this.arrayBuffer = async () => {
const data = await this.data
return data
}
}
}
+57
View File
@@ -0,0 +1,57 @@
import { detectIndexHtml } from './file'
describe('file utils', () => {
it('detectIndexHtml should find index.html', () => {
expect(
detectIndexHtml([
{ name: 'swarm.png', path: 'swarm.png' },
{ name: 'index.html', path: 'index.html' },
]),
).toBe('index.html')
})
it('detectIndexHtml should find index.htm', () => {
expect(
detectIndexHtml([
{ name: 'index.htm', path: 'index.htm' },
{ name: 'swarm.png', path: 'swarm.png' },
]),
).toBe('index.htm')
})
it('detectIndexHtml should find nested index.html', () => {
expect(
detectIndexHtml([
{ name: 'swarm.png', path: 'sample-folder/swarm.png' },
{ name: 'index.html', path: 'sample-folder/index.html' },
]),
).toBe('index.html')
})
it('detectIndexHtml should not find nested index.htm when ambigous', () => {
expect(
detectIndexHtml([
{ name: 'index.htm', path: 'sample-folder/index.htm' },
{ name: 'swarm.png', path: 'other-folder/swarm.png' },
]),
).toBe(false)
})
it('detectIndexHtml should not find deep index.html', () => {
expect(
detectIndexHtml([
{ name: 'index.html', path: 'sample-folder/index.html' },
{ name: 'swarm.png', path: 'swarm.png' },
]),
).toBe(false)
})
it('detectIndexHtml should return false when no matches appear', () => {
expect(
detectIndexHtml([
{ name: 'swarm.png', path: 'swarm.png' },
{ name: 'swarm.jpg', path: 'swarm.jpg' },
]),
).toBe(false)
})
})
+51
View File
@@ -0,0 +1,51 @@
import { FileData } from '@ethersphere/bee-js'
import { SwarmFile } from './SwarmFile'
const indexHtmls = ['index.html', 'index.htm']
export function detectIndexHtml(files: SwarmFile[]): string | false {
if (!files.length) {
return false
}
const exactMatch = files.find(x => indexHtmls.includes(x.path))
if (exactMatch) {
return exactMatch.name
}
const prefix = files[0].path.split('/')[0] + '/'
const allStartWithSamePrefix = files.every(x => x.path.startsWith(prefix))
if (allStartWithSamePrefix) {
const match = files.find(x => indexHtmls.map(y => prefix + y).includes(x.path))
if (match) {
return match.name
}
}
return false
}
export function getHumanReadableFileSize(bytes: number): string {
if (bytes >= 1e6) {
return (bytes / 1e6).toFixed(2) + ' MB'
}
if (bytes >= 1e3) {
return (bytes / 1e3).toFixed(2) + ' kB'
}
return bytes + ' bytes'
}
export function convertBeeFileToBrowserFile(file: FileData<ArrayBuffer>): Partial<File> {
return {
name: file.name,
size: file.data.byteLength,
type: file.contentType,
arrayBuffer: () => new Promise(resolve => resolve(file.data)),
}
}
+48 -2
View File
@@ -1,5 +1,5 @@
import BigNumber from 'bignumber.js' import BigNumber from 'bignumber.js'
import { isInteger, makeBigNumber } from './index' import { extractSwarmHash, isInteger, makeBigNumber } from './index'
describe('utils', () => { describe('utils', () => {
describe('isInteger', () => { describe('isInteger', () => {
@@ -42,7 +42,7 @@ describe('utils', () => {
1, 1,
-1, -1,
] ]
const wrongValues = [new Function()] const wrongValues = [new Function()] // eslint-disable-line no-new-func
correctValues.forEach(v => { correctValues.forEach(v => {
test(`testing ${v}`, () => { test(`testing ${v}`, () => {
@@ -57,4 +57,50 @@ describe('utils', () => {
}) })
}) })
}) })
describe('extractSwarmHash', () => {
test('should return 64 hash', () => {
expect(extractSwarmHash('7f0fe712cdd78bdea52d040369eb32b6af5ecd01fa5ae49b7506412abdd81ac3')).toBe(
'7f0fe712cdd78bdea52d040369eb32b6af5ecd01fa5ae49b7506412abdd81ac3',
)
})
test('should return 128 hash', () => {
expect(
extractSwarmHash(
'd1829242c4d08e9f914fedfb1f68aacd62826d75370a9a57a80c9e6e6a49983c767c013be9aa4319e34fd8323ef0f2a57426b30e66c87e219f6f6359e2595e7f',
),
).toBe(
'd1829242c4d08e9f914fedfb1f68aacd62826d75370a9a57a80c9e6e6a49983c767c013be9aa4319e34fd8323ef0f2a57426b30e66c87e219f6f6359e2595e7f',
)
})
test('should return 64 hash from url', () => {
expect(
extractSwarmHash('http://localhost:1633/bzz/7f0fe712cdd78bdea52d040369eb32b6af5ecd01fa5ae49b7506412abdd81ac3/'),
).toBe('7f0fe712cdd78bdea52d040369eb32b6af5ecd01fa5ae49b7506412abdd81ac3')
})
test('should return 128 hash from url', () => {
expect(
extractSwarmHash(
'http://localhost:1633/bzz/d1829242c4d08e9f914fedfb1f68aacd62826d75370a9a57a80c9e6e6a49983c767c013be9aa4319e34fd8323ef0f2a57426b30e66c87e219f6f6359e2595e7f/',
),
).toBe(
'd1829242c4d08e9f914fedfb1f68aacd62826d75370a9a57a80c9e6e6a49983c767c013be9aa4319e34fd8323ef0f2a57426b30e66c87e219f6f6359e2595e7f',
)
})
test('should return null when nothing is found', () => {
expect(extractSwarmHash('Bee Dashboard')).toBe(null)
})
test('should return null when length is incorrect', () => {
expect(extractSwarmHash('7f0fe712cdd78bdea52d040369eb32b6af5ecd01fa5ae49b7506412abdd81a')).toBe(null)
})
test('should return null when alphanumeric', () => {
expect(extractSwarmHash('gkQ6duo5iHJ099g908P0t17ZWFf8Ke2klrywLP5BGtLkcaEC5W0kLEfbe4wUnDI6')).toBe(null)
})
})
}) })
+6
View File
@@ -106,3 +106,9 @@ export function makeRetriablePromise<T>(fn: () => Promise<T>, maxRetries = 3, de
} }
}) })
} }
export function extractSwarmHash(string: string): string | null {
const matches = string.match(/[a-fA-F0-9]{64,128}/)
return (matches && matches[0]) || null
}