Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 082a8f52ef | |||
| bcd3d50b42 | |||
| f695ac3a1c | |||
| a6125b3d0b | |||
| e01d9fe3d7 | |||
| 6294bb0a7b | |||
| fbb2ed8a57 | |||
| aef6c07371 | |||
| ed75198528 | |||
| d0c94b7316 | |||
| 63f338075b | |||
| 4cb0bcd3b9 | |||
| 01b1b39c42 | |||
| 8558860f0a | |||
| b4ebfc7c3f |
+1
-2
@@ -9,7 +9,6 @@
|
|||||||
"file-loader",
|
"file-loader",
|
||||||
"ts-node",
|
"ts-node",
|
||||||
"webpack-cli",
|
"webpack-cli",
|
||||||
"assert",
|
|
||||||
"buffer",
|
"buffer",
|
||||||
"crypto*",
|
"crypto*",
|
||||||
"stream*",
|
"stream*",
|
||||||
@@ -17,4 +16,4 @@
|
|||||||
"open",
|
"open",
|
||||||
"base64-inline-loader"
|
"base64-inline-loader"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,13 +56,6 @@ jobs:
|
|||||||
- name: Types check
|
- name: Types check
|
||||||
run: npm run check:types
|
run: npm run check:types
|
||||||
|
|
||||||
- name: Update supported Bee action
|
|
||||||
uses: ethersphere/update-supported-bee-action@v1
|
|
||||||
if: github.ref == 'refs/heads/master'
|
|
||||||
with:
|
|
||||||
updateEngine: true
|
|
||||||
token: ${{ secrets.GHA_PAT_BASIC }}
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
@@ -70,7 +63,7 @@ jobs:
|
|||||||
run: npm run build:component
|
run: npm run build:component
|
||||||
|
|
||||||
- name: Create preview
|
- name: Create preview
|
||||||
uses: ethersphere/swarm-actions/pr-preview@v0
|
uses: ethersphere/swarm-actions/pr-preview@v1
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
bee-url: https://unlimited.gateway.ethswarm.org
|
bee-url: https://unlimited.gateway.ethswarm.org
|
||||||
@@ -79,7 +72,7 @@ jobs:
|
|||||||
headers: '${{ secrets.GATEWAY_AUTHORIZATION_HEADER }}'
|
headers: '${{ secrets.GATEWAY_AUTHORIZATION_HEADER }}'
|
||||||
|
|
||||||
- name: Upload to testnet
|
- name: Upload to testnet
|
||||||
uses: ethersphere/swarm-actions/upload-dir@v0
|
uses: ethersphere/swarm-actions/upload-dir@v1
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
index-document: index.html
|
index-document: index.html
|
||||||
|
|||||||
@@ -1,5 +1,46 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.32.0](https://github.com/ethersphere/bee-dashboard/compare/v0.31.0...v0.32.0) (2025-02-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* vod display ([#686](https://github.com/ethersphere/bee-dashboard/issues/686)) ([bcd3d50](https://github.com/ethersphere/bee-dashboard/commit/bcd3d50b4209a4f66a259b8a3f6ea5ffd908471f))
|
||||||
|
|
||||||
|
## [0.31.0](https://github.com/ethersphere/bee-dashboard/compare/v0.30.0...v0.31.0) (2025-01-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* remove experimental FDP menu item ([#687](https://github.com/ethersphere/bee-dashboard/issues/687)) ([a6125b3](https://github.com/ethersphere/bee-dashboard/commit/a6125b3d0b0b680a9fa61a8edcd75b2ae6c153e0))
|
||||||
|
|
||||||
|
## [0.30.0](https://github.com/ethersphere/bee-dashboard/compare/v0.29.0...v0.30.0) (2024-11-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add experimental fdp ([#681](https://github.com/ethersphere/bee-dashboard/issues/681)) ([d0c94b7](https://github.com/ethersphere/bee-dashboard/commit/d0c94b7316ea2b139bddc5481132ea7de7cb840d))
|
||||||
|
* update map data ([#684](https://github.com/ethersphere/bee-dashboard/issues/684)) ([fbb2ed8](https://github.com/ethersphere/bee-dashboard/commit/fbb2ed8a576f3519883e71382b7f4e8505fbe139))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* allow changing api url ([#676](https://github.com/ethersphere/bee-dashboard/issues/676)) ([6294bb0](https://github.com/ethersphere/bee-dashboard/commit/6294bb0a7be6b9b82354c42da8c84e767fad899e))
|
||||||
|
* explicitly define type 0 transaction ([#674](https://github.com/ethersphere/bee-dashboard/issues/674)) ([63f3380](https://github.com/ethersphere/bee-dashboard/commit/63f338075b919cb70d79665c3d86537f2ac1d2e9))
|
||||||
|
|
||||||
|
## [0.29.0](https://github.com/ethersphere/bee-dashboard/compare/v0.28.0...v0.29.0) (2024-07-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* clarify labels and syncing ([#670](https://github.com/ethersphere/bee-dashboard/issues/670)) ([01b1b39](https://github.com/ethersphere/bee-dashboard/commit/01b1b39c42cc5b68a0132c3696c3c42a27ea2ee4))
|
||||||
|
* polish app ([#669](https://github.com/ethersphere/bee-dashboard/issues/669)) ([8558860](https://github.com/ethersphere/bee-dashboard/commit/8558860f0a3baa82c31c091a44c78bb8e97de70d))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* clarify withdraw and deposit message ([#654](https://github.com/ethersphere/bee-dashboard/issues/654)) ([b4ebfc7](https://github.com/ethersphere/bee-dashboard/commit/b4ebfc7c3fd449807db47fa25763df464cc45618))
|
||||||
|
|
||||||
## [0.28.0](https://github.com/ethersphere/bee-dashboard/compare/v0.27.0...v0.28.0) (2024-06-18)
|
## [0.28.0](https://github.com/ethersphere/bee-dashboard/compare/v0.27.0...v0.28.0) (2024-06-18)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,8 @@
|
|||||||
**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
|
Stay up to date by joining the [official Discord](https://discord.gg/GU22h2utj6) and by keeping an eye on the
|
||||||
|
[releases tab](https://github.com/ethersphere/bee-dashboard/releases).
|
||||||
<!-- SUPPORTED_BEE_START -->1.12.0-88c1d236<!-- SUPPORTED_BEE_END -->**. Using it with older or newer Bee versions is
|
|
||||||
|
|
||||||
not recommended and may not work. Stay up to date by joining the [official Discord](https://discord.gg/GU22h2utj6) and
|
|
||||||
by keeping an eye on the [releases tab](https://github.com/ethersphere/bee-dashboard/releases).
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
Generated
+1576
-1357
File diff suppressed because it is too large
Load Diff
+13
-13
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ethersphere/bee-dashboard",
|
"name": "@ethersphere/bee-dashboard",
|
||||||
"version": "0.28.0",
|
"version": "0.32.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,30 +26,30 @@
|
|||||||
"url": "https://github.com/ethersphere/bee-dashboard.git"
|
"url": "https://github.com/ethersphere/bee-dashboard.git"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ethersphere/bee-js": "^7.0.3",
|
"@ethersphere/bee-js": "^8.3.1",
|
||||||
"@ethersphere/swarm-cid": "^0.1.0",
|
"@ethersphere/swarm-cid": "^0.1.0",
|
||||||
|
"@fairdatasociety/fdp-storage": "^0.19.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",
|
||||||
"assert": "^2.0.0",
|
"axios": "^0.28.1",
|
||||||
"axios": "0.24.0",
|
"bignumber.js": "^9.1.2",
|
||||||
"bignumber.js": "9.0.1",
|
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"crypto": "npm:crypto-browserify",
|
"crypto": "npm:crypto-browserify",
|
||||||
"crypto-browserify": "^3.12.0",
|
"crypto-browserify": "^3.12.0",
|
||||||
"dotted-map": "^2.2.3",
|
"dotted-map": "^2.2.3",
|
||||||
"ethers": "^5.6.4",
|
"ethers": "^5.7.2",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"formik": "2.2.9",
|
"formik": "2.2.9",
|
||||||
"formik-material-ui": "3.0.1",
|
"formik-material-ui": "3.0.1",
|
||||||
"jszip": "^3.7.1",
|
"jszip": "^3.10.1",
|
||||||
"mantaray-js": "^1.0.3",
|
"mantaray-js": "^1.0.3",
|
||||||
"material-ui-dropzone": "3.5.0",
|
"material-ui-dropzone": "3.5.0",
|
||||||
"notistack": "1.0.10",
|
"notistack": "^3.0.1",
|
||||||
"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.4",
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-dom": ">= 17.0.2",
|
"react-dom": ">= 17.0.2",
|
||||||
"react-identicons": "1.2.5",
|
"react-identicons": "1.2.5",
|
||||||
"react-router": "6.2.1",
|
"react-router": "6.2.1",
|
||||||
@@ -77,15 +77,15 @@
|
|||||||
"@types/jest": "27.0.2",
|
"@types/jest": "27.0.2",
|
||||||
"@types/qrcode.react": "1.0.2",
|
"@types/qrcode.react": "1.0.2",
|
||||||
"@types/react": "17.0.34",
|
"@types/react": "17.0.34",
|
||||||
"@types/react-copy-to-clipboard": "5.0.2",
|
"@types/react-copy-to-clipboard": "^5.0.2",
|
||||||
"@types/react-dom": "17.0.11",
|
"@types/react-dom": "17.0.11",
|
||||||
"@types/react-router": "5.1.18",
|
"@types/react-router": "5.1.18",
|
||||||
"@types/react-router-dom": "5.3.2",
|
"@types/react-router-dom": "5.3.2",
|
||||||
"@types/react-syntax-highlighter": "13.5.2",
|
"@types/react-syntax-highlighter": "13.5.2",
|
||||||
"@typescript-eslint/eslint-plugin": "5.28.0",
|
"@typescript-eslint/eslint-plugin": "5.28.0",
|
||||||
"@typescript-eslint/parser": "5.28.0",
|
"@typescript-eslint/parser": "5.28.0",
|
||||||
"babel-eslint": "10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"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",
|
||||||
"base64-inline-loader": "^2.0.1",
|
"base64-inline-loader": "^2.0.1",
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
"ts-node": "^10.8.1",
|
"ts-node": "^10.8.1",
|
||||||
"typescript": "4.8.3",
|
"typescript": "4.8.3",
|
||||||
"web-vitals": "2.1.2",
|
"web-vitals": "2.1.2",
|
||||||
"webpack": "^5.73.0",
|
"webpack": "^5.93.0",
|
||||||
"webpack-cli": "^4.10.0"
|
"webpack-cli": "^4.10.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
+51739
-7115
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
|||||||
|
import { ChainState } from '@ethersphere/bee-js'
|
||||||
|
import { useContext, useEffect, useState } from 'react'
|
||||||
|
import { Context } from '../providers/Settings'
|
||||||
|
import ExpandableListItem from './ExpandableListItem'
|
||||||
|
|
||||||
|
export function ChainSync() {
|
||||||
|
const { beeApi } = useContext(Context)
|
||||||
|
const [chainState, setChainState] = useState<ChainState | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (!beeApi) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
beeApi.getChainState().then(setChainState).catch(console.error) // eslint-disable-line
|
||||||
|
}, 3_000)
|
||||||
|
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ExpandableListItem label="Chain state" value={chainState ? `${chainState.block} / ${chainState.chainTip}` : '-'} />
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import { Utils } from '@ethersphere/bee-js'
|
||||||
import { Typography } from '@material-ui/core/'
|
import { Typography } from '@material-ui/core/'
|
||||||
import { ReactElement } from 'react'
|
import { ReactElement } from 'react'
|
||||||
import Identicon from 'react-identicons'
|
import Identicon from 'react-identicons'
|
||||||
import ClipboardCopy from './ClipboardCopy'
|
|
||||||
import QRCodeModal from './QRCodeModal'
|
|
||||||
import { BLOCKCHAIN_EXPLORER_URL } from '../constants'
|
import { BLOCKCHAIN_EXPLORER_URL } from '../constants'
|
||||||
|
import ClipboardCopy from './ClipboardCopy'
|
||||||
|
import { Flex } from './Flex'
|
||||||
|
import QRCodeModal from './QRCodeModal'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
address: string | undefined
|
address: string | undefined
|
||||||
@@ -16,10 +18,10 @@ export default function EthereumAddress(props: Props): ReactElement {
|
|||||||
return (
|
return (
|
||||||
<Typography component="div" variant="subtitle1">
|
<Typography component="div" variant="subtitle1">
|
||||||
{props.address ? (
|
{props.address ? (
|
||||||
<div style={{ display: 'flex' }}>
|
<Flex>
|
||||||
{props.hideBlockie ? null : (
|
{props.hideBlockie ? null : (
|
||||||
<div style={{ paddingTop: '5px', marginRight: '10px' }}>
|
<div style={{ paddingTop: '5px', marginRight: '10px' }}>
|
||||||
<Identicon size={20} string={props.address} />
|
<Identicon size={20} string={Utils.capitalizeAddressERC55(props.address)} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
@@ -43,9 +45,9 @@ export default function EthereumAddress(props: Props): ReactElement {
|
|||||||
{props.address}
|
{props.address}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<QRCodeModal value={props.address} label={'Ethereum Address'} />
|
<QRCodeModal value={Utils.capitalizeAddressERC55(props.address)} label={'Ethereum Address'} />
|
||||||
<ClipboardCopy value={props.address} />
|
<ClipboardCopy value={Utils.capitalizeAddressERC55(props.address)} />
|
||||||
</div>
|
</Flex>
|
||||||
) : (
|
) : (
|
||||||
'-'
|
'-'
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { ReactElement, ReactNode, useState } from 'react'
|
|
||||||
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
|
|
||||||
import { Collapse, ListItem, ListItemText, Typography } from '@material-ui/core'
|
import { Collapse, ListItem, ListItemText, Typography } from '@material-ui/core'
|
||||||
|
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||||
import { ExpandLess, ExpandMore } from '@material-ui/icons'
|
import { ExpandLess, ExpandMore } from '@material-ui/icons'
|
||||||
|
import { ReactElement, ReactNode, useState } from 'react'
|
||||||
|
import { Flex } from './Flex'
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
createStyles({
|
createStyles({
|
||||||
@@ -65,14 +66,14 @@ export default function ExpandableList({ children, label, level, defaultOpen, in
|
|||||||
<div className={`${classes.root} ${rootLevelClass}`}>
|
<div className={`${classes.root} ${rootLevelClass}`}>
|
||||||
<ListItem button onClick={handleClick} className={classes.header}>
|
<ListItem button onClick={handleClick} className={classes.header}>
|
||||||
<ListItemText primary={<Typography variant={typographyVariant}>{label}</Typography>} />
|
<ListItemText primary={<Typography variant={typographyVariant}>{label}</Typography>} />
|
||||||
<div style={{ display: 'flex' }}>
|
<Flex>
|
||||||
{!open && (
|
{!open && (
|
||||||
<Typography variant="body2" className={classes.infoText}>
|
<Typography variant="body2" className={classes.infoText}>
|
||||||
{info}
|
{info}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
{open ? <ExpandLess /> : <ExpandMore />}
|
{open ? <ExpandLess /> : <ExpandMore />}
|
||||||
</div>
|
</Flex>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||||
<div className={contentLevelClass}>{children}</div>
|
<div className={contentLevelClass}>{children}</div>
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import { Box, Grid, IconButton, InputBase, ListItem, Typography } from '@materia
|
|||||||
import Collapse from '@material-ui/core/Collapse'
|
import Collapse from '@material-ui/core/Collapse'
|
||||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||||
import { ChangeEvent, ReactElement, useState } from 'react'
|
import { ChangeEvent, ReactElement, useState } from 'react'
|
||||||
|
import type { RemixiconReactIconProps } from 'remixicon-react'
|
||||||
import Check from 'remixicon-react/CheckLineIcon'
|
import Check from 'remixicon-react/CheckLineIcon'
|
||||||
|
import X from 'remixicon-react/CloseLineIcon'
|
||||||
import Edit from 'remixicon-react/PencilLineIcon'
|
import Edit from 'remixicon-react/PencilLineIcon'
|
||||||
import Minus from 'remixicon-react/SubtractLineIcon'
|
import Minus from 'remixicon-react/SubtractLineIcon'
|
||||||
import X from 'remixicon-react/CloseLineIcon'
|
|
||||||
import ExpandableListItemActions from './ExpandableListItemActions'
|
import ExpandableListItemActions from './ExpandableListItemActions'
|
||||||
import ExpandableListItemNote from './ExpandableListItemNote'
|
import ExpandableListItemNote from './ExpandableListItemNote'
|
||||||
import { SwarmButton } from './SwarmButton'
|
import { SwarmButton } from './SwarmButton'
|
||||||
import type { RemixiconReactIconProps } from 'remixicon-react'
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
createStyles({
|
createStyles({
|
||||||
@@ -108,12 +108,8 @@ export default function ExpandableListItemKey({
|
|||||||
<div>
|
<div>
|
||||||
{!open && value}
|
{!open && value}
|
||||||
{!expandedOnly && !locked && (
|
{!expandedOnly && !locked && (
|
||||||
<IconButton size="small" className={classes.copyValue}>
|
<IconButton size="small" className={classes.copyValue} onClick={toggleOpen}>
|
||||||
{open ? (
|
{open ? <Minus strokeWidth={1} /> : <Edit strokeWidth={1} />}
|
||||||
<Minus onClick={toggleOpen} strokeWidth={1} />
|
|
||||||
) : (
|
|
||||||
<Edit onClick={toggleOpen} strokeWidth={1} />
|
|
||||||
)}
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -77,20 +77,18 @@ export default function ExpandableListItemKey({ label, value, expanded }: Props)
|
|||||||
<Grid container direction="row" justifyContent="space-between" alignItems="center">
|
<Grid container direction="row" justifyContent="space-between" alignItems="center">
|
||||||
{label && <Typography variant="body1">{label}</Typography>}
|
{label && <Typography variant="body1">{label}</Typography>}
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
<div>
|
{!open && (
|
||||||
{!open && (
|
<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}>{value ? spanText : ''}</span>
|
||||||
<span onClick={tooltipClickHandler}>{value ? spanText : ''}</span>
|
</CopyToClipboard>
|
||||||
</CopyToClipboard>
|
</Tooltip>
|
||||||
</Tooltip>
|
</span>
|
||||||
</span>
|
)}
|
||||||
)}
|
<IconButton size="small" className={classes.copyValue} onClick={toggleOpen}>
|
||||||
<IconButton size="small" className={classes.copyValue}>
|
{open ? <Minus strokeWidth={1} /> : <Eye strokeWidth={1} />}
|
||||||
{open ? <Minus onClick={toggleOpen} strokeWidth={1} /> : <Eye onClick={toggleOpen} strokeWidth={1} />}
|
</IconButton>
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||||
|
|||||||
@@ -82,22 +82,20 @@ export default function ExpandableListItemLink({
|
|||||||
<Grid container direction="row" justifyContent="space-between" alignItems="center">
|
<Grid container direction="row" justifyContent="space-between" alignItems="center">
|
||||||
{label && <Typography variant="body1">{label}</Typography>}
|
{label && <Typography variant="body1">{label}</Typography>}
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
<div>
|
{allowClipboard && (
|
||||||
{allowClipboard && (
|
<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}>{displayValue}</span>
|
||||||
<span onClick={tooltipClickHandler}>{displayValue}</span>
|
</CopyToClipboard>
|
||||||
</CopyToClipboard>
|
</Tooltip>
|
||||||
</Tooltip>
|
</span>
|
||||||
</span>
|
)}
|
||||||
)}
|
{!allowClipboard && <span onClick={onNavigation}>{displayValue}</span>}
|
||||||
{!allowClipboard && <span onClick={onNavigation}>{displayValue}</span>}
|
<IconButton size="small" className={classes.openLinkIcon} onClick={onNavigation}>
|
||||||
<IconButton size="small" className={classes.openLinkIcon}>
|
{navigationType === 'NEW_WINDOW' && <OpenInNewSharp strokeWidth={1} />}
|
||||||
{navigationType === 'NEW_WINDOW' && <OpenInNewSharp onClick={onNavigation} strokeWidth={1} />}
|
{navigationType === 'HISTORY_PUSH' && <ArrowForward strokeWidth={1} />}
|
||||||
{navigationType === 'HISTORY_PUSH' && <ArrowForward onClick={onNavigation} strokeWidth={1} />}
|
</IconButton>
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { createStyles, makeStyles } from '@material-ui/core'
|
||||||
|
import { ReactElement } from 'react'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(() =>
|
||||||
|
createStyles({
|
||||||
|
video: {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'cover',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
interface VideoProps {
|
||||||
|
src: string | undefined
|
||||||
|
maxHeight?: string
|
||||||
|
maxWidth?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FitVideo(props: VideoProps): ReactElement {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const inlineStyles: Record<string, string> = {}
|
||||||
|
|
||||||
|
props.maxHeight && (inlineStyles.maxHeight = props.maxHeight)
|
||||||
|
props.maxWidth && (inlineStyles.maxWidth = props.maxWidth)
|
||||||
|
|
||||||
|
return <video className={classes.video} src={props.src} style={inlineStyles} controls />
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Flex({ children }: Props) {
|
||||||
|
return <div style={{ display: 'flex' }}>{children}</div>
|
||||||
|
}
|
||||||
@@ -133,7 +133,7 @@ export default function SideBar(): ReactElement {
|
|||||||
<SideBarItem
|
<SideBarItem
|
||||||
iconStart={<GithubIcon className={classes.icon} />}
|
iconStart={<GithubIcon className={classes.icon} />}
|
||||||
iconEnd={<ExternalLinkIcon className={classes.icon} color="#595959" />}
|
iconEnd={<ExternalLinkIcon className={classes.icon} color="#595959" />}
|
||||||
label={<span>Github repository</span>}
|
label={<span>GitHub</span>}
|
||||||
/>
|
/>
|
||||||
</MUILink>
|
</MUILink>
|
||||||
</List>
|
</List>
|
||||||
|
|||||||
+1
-2
@@ -1,5 +1,4 @@
|
|||||||
export const META_FILE_NAME = '.swarmgatewaymeta.json'
|
export const META_FILE_NAME = 'metadata'
|
||||||
export const PREVIEW_FILE_NAME = '.swarmgatewaypreview.jpeg'
|
|
||||||
export const PREVIEW_DIMENSIONS = { maxWidth: 250, maxHeight: 175 }
|
export const PREVIEW_DIMENSIONS = { maxWidth: 250, maxHeight: 175 }
|
||||||
export const BZZ_LINK_DOMAIN = 'bzz.link'
|
export const BZZ_LINK_DOMAIN = 'bzz.link'
|
||||||
export const BLOCKCHAIN_EXPLORER_URL = 'https://blockscout.com/xdai/mainnet'
|
export const BLOCKCHAIN_EXPLORER_URL = 'https://blockscout.com/xdai/mainnet'
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default function DepositModal(): ReactElement {
|
|||||||
<WithdrawDepositModal
|
<WithdrawDepositModal
|
||||||
successMessage="Successful deposit."
|
successMessage="Successful deposit."
|
||||||
errorMessage="Error with depositing"
|
errorMessage="Error with depositing"
|
||||||
dialogMessage="Specify the amount of xBZZ you would like to deposit to your node."
|
dialogMessage="Amount of xBZZ to deposit to the checkbook, from your node."
|
||||||
label="Deposit"
|
label="Deposit"
|
||||||
icon={<Download size="1rem" />}
|
icon={<Download size="1rem" />}
|
||||||
min={new BigNumber(0)}
|
min={new BigNumber(0)}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default function WithdrawModal(): ReactElement {
|
|||||||
<WithdrawDepositModal
|
<WithdrawDepositModal
|
||||||
successMessage="Successful withdrawal."
|
successMessage="Successful withdrawal."
|
||||||
errorMessage="Error with withdrawing."
|
errorMessage="Error with withdrawing."
|
||||||
dialogMessage="Specify the amount of xBZZ you would like to withdraw from your node."
|
dialogMessage="Amount of xBZZ to withdraw from the checkbook to your node."
|
||||||
label="Withdraw"
|
label="Withdraw"
|
||||||
icon={<Upload size="1rem" />}
|
icon={<Upload size="1rem" />}
|
||||||
min={new BigNumber(0)}
|
min={new BigNumber(0)}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useSnackbar } from 'notistack'
|
|||||||
import React, { ReactElement, useContext, useEffect } from 'react'
|
import React, { ReactElement, useContext, useEffect } from 'react'
|
||||||
import CloseIcon from 'remixicon-react/CloseCircleLineIcon'
|
import CloseIcon from 'remixicon-react/CloseCircleLineIcon'
|
||||||
import ErrorBoundary from '../components/ErrorBoundary'
|
import ErrorBoundary from '../components/ErrorBoundary'
|
||||||
|
import { Flex } from '../components/Flex'
|
||||||
import SideBar from '../components/SideBar'
|
import SideBar from '../components/SideBar'
|
||||||
import { BEE_DESKTOP_LATEST_RELEASE_PAGE } from '../constants'
|
import { BEE_DESKTOP_LATEST_RELEASE_PAGE } from '../constants'
|
||||||
import { useBeeDesktop, useNewBeeDesktopVersion } from '../hooks/apiHooks'
|
import { useBeeDesktop, useNewBeeDesktopVersion } from '../hooks/apiHooks'
|
||||||
@@ -81,13 +82,13 @@ const Dashboard = (props: Props): ReactElement => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex' }}>
|
<Flex>
|
||||||
<SideBar />
|
<SideBar />
|
||||||
<Container className={classes.content}>
|
<Container className={classes.content}>
|
||||||
{' '}
|
{' '}
|
||||||
<ErrorBoundary errorReporting={props.errorReporting}>{content}</ErrorBoundary>
|
<ErrorBoundary errorReporting={props.errorReporting}>{content}</ErrorBoundary>
|
||||||
</Container>
|
</Container>
|
||||||
</div>
|
</Flex>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+8
-7
@@ -81,17 +81,18 @@ export class Token {
|
|||||||
return asString.slice(0, indexOfSignificantDigit + digits)
|
return asString.slice(0, indexOfSignificantDigit + digits)
|
||||||
}
|
}
|
||||||
|
|
||||||
minusBaseUnits(amount: string): Token {
|
minusBaseUnits(amount: string | BigNumber | bigint): Token {
|
||||||
|
const baseUnits = makeBigNumber(amount)
|
||||||
|
|
||||||
return new Token(
|
return new Token(
|
||||||
this.toBigNumber.minus(new BigNumber(amount).multipliedBy(new BigNumber(10).pow(this.decimals))),
|
this.toBigNumber.minus(baseUnits.multipliedBy(new BigNumber(10).pow(this.decimals))),
|
||||||
this.decimals,
|
this.decimals,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
plusBaseUnits(amount: string): Token {
|
plusBaseUnits(amount: string | BigNumber | bigint): Token {
|
||||||
return new Token(
|
const baseUnits = makeBigNumber(amount)
|
||||||
this.toBigNumber.plus(new BigNumber(amount).multipliedBy(new BigNumber(10).pow(this.decimals))),
|
|
||||||
this.decimals,
|
return new Token(this.toBigNumber.plus(baseUnits.multipliedBy(new BigNumber(10).pow(this.decimals))), this.decimals)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Utils } from '@ethersphere/bee-js'
|
||||||
import { Box } from '@material-ui/core'
|
import { Box } from '@material-ui/core'
|
||||||
import { ReactElement, useContext } from 'react'
|
import { ReactElement, useContext } from 'react'
|
||||||
import ExpandableList from '../../../components/ExpandableList'
|
import ExpandableList from '../../../components/ExpandableList'
|
||||||
@@ -57,7 +58,10 @@ export function AccountChequebook(): ReactElement {
|
|||||||
</ExpandableList>
|
</ExpandableList>
|
||||||
)}
|
)}
|
||||||
<ExpandableList label="Blockchain" defaultOpen>
|
<ExpandableList label="Blockchain" defaultOpen>
|
||||||
<ExpandableListItemKey label="Ethereum address" value={nodeAddresses?.ethereum || ''} />
|
<ExpandableListItemKey
|
||||||
|
label="Ethereum address"
|
||||||
|
value={nodeAddresses?.ethereum ? Utils.capitalizeAddressERC55(nodeAddresses.ethereum) : ''}
|
||||||
|
/>
|
||||||
<ExpandableListItemKey
|
<ExpandableListItemKey
|
||||||
label="Chequebook contract address"
|
label="Chequebook contract address"
|
||||||
value={chequebookAddress?.chequebookAddress || ''}
|
value={chequebookAddress?.chequebookAddress || ''}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { CircularProgress, Container, createStyles, makeStyles } from '@material
|
|||||||
import { ReactElement, useContext, useEffect } from 'react'
|
import { ReactElement, useContext, useEffect } from 'react'
|
||||||
import { useNavigate } from 'react-router'
|
import { useNavigate } from 'react-router'
|
||||||
import PlusSquare from 'remixicon-react/AddBoxLineIcon'
|
import PlusSquare from 'remixicon-react/AddBoxLineIcon'
|
||||||
|
import { ChainSync } from '../../../components/ChainSync'
|
||||||
import { Loading } from '../../../components/Loading'
|
import { Loading } from '../../../components/Loading'
|
||||||
import { SwarmButton } from '../../../components/SwarmButton'
|
import { SwarmButton } from '../../../components/SwarmButton'
|
||||||
import TroubleshootConnectionCard from '../../../components/TroubleshootConnectionCard'
|
import TroubleshootConnectionCard from '../../../components/TroubleshootConnectionCard'
|
||||||
@@ -57,7 +58,7 @@ export function AccountStamps(): ReactElement {
|
|||||||
{error && (
|
{error && (
|
||||||
<Container style={{ textAlign: 'center', padding: '50px' }}>
|
<Container style={{ textAlign: 'center', padding: '50px' }}>
|
||||||
<Loading />
|
<Loading />
|
||||||
{error.message}
|
<ChainSync />
|
||||||
</Container>
|
</Container>
|
||||||
)}
|
)}
|
||||||
{!error && (
|
{!error && (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { BeeModes } from '@ethersphere/bee-js'
|
import { BeeModes, Utils } from '@ethersphere/bee-js'
|
||||||
import { Box, Grid, Typography } from '@material-ui/core'
|
import { Box, Grid, Typography } from '@material-ui/core'
|
||||||
import { ReactElement, useContext } from 'react'
|
import { ReactElement, useContext } from 'react'
|
||||||
import { useNavigate } from 'react-router'
|
import { useNavigate } from 'react-router'
|
||||||
@@ -26,7 +26,7 @@ export function AccountWallet(): ReactElement {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
function onCheckTransactions() {
|
function onCheckTransactions() {
|
||||||
window.open(`https://blockscout.com/xdai/mainnet/address/${nodeAddresses?.ethereum}/transactions`, '_blank')
|
window.open(`https://gnosisscan.io/address/${nodeAddresses?.ethereum}`, '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
function onInvite() {
|
function onInvite() {
|
||||||
@@ -56,7 +56,11 @@ export function AccountWallet(): ReactElement {
|
|||||||
{balance && nodeAddresses ? (
|
{balance && nodeAddresses ? (
|
||||||
<>
|
<>
|
||||||
<Box mb={0.25}>
|
<Box mb={0.25}>
|
||||||
<ExpandableListItemKey label="Node wallet address" value={nodeAddresses.ethereum} expanded />
|
<ExpandableListItemKey
|
||||||
|
label="Node wallet address"
|
||||||
|
value={Utils.capitalizeAddressERC55(nodeAddresses.ethereum)}
|
||||||
|
expanded
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box mb={0.25}>
|
<Box mb={0.25}>
|
||||||
<ExpandableListItem label="xDAI balance" value={`${balance.dai.toSignificantDigits(4)} xDAI`} />
|
<ExpandableListItem label="xDAI balance" value={`${balance.dai.toSignificantDigits(4)} xDAI`} />
|
||||||
@@ -72,7 +76,7 @@ export function AccountWallet(): ReactElement {
|
|||||||
)}
|
)}
|
||||||
<ExpandableListItemActions>
|
<ExpandableListItemActions>
|
||||||
<SwarmButton onClick={onCheckTransactions} iconType={Link}>
|
<SwarmButton onClick={onCheckTransactions} iconType={Link}>
|
||||||
Check transactions on Blockscout
|
Check transactions
|
||||||
</SwarmButton>
|
</SwarmButton>
|
||||||
{isDesktop && (
|
{isDesktop && (
|
||||||
<SwarmButton onClick={onInvite} iconType={Gift}>
|
<SwarmButton onClick={onInvite} iconType={Gift}>
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import { FdpStorage } from '@fairdatasociety/fdp-storage'
|
||||||
|
import { Checkbox, InputBase, Typography } from '@material-ui/core'
|
||||||
|
import { useSnackbar } from 'notistack'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import RegisterIcon from 'remixicon-react/AddBoxLineIcon'
|
||||||
|
import LoginIcon from 'remixicon-react/LoginBoxLineIcon'
|
||||||
|
import { SwarmButton } from '../../components/SwarmButton'
|
||||||
|
import { Horizontal } from './Horizontal'
|
||||||
|
import { Vertical } from './Vertical'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
fdp: FdpStorage
|
||||||
|
onSuccessfulLogin: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FdpLogin({ fdp, onSuccessfulLogin }: Props) {
|
||||||
|
const [username, setUsername] = useState<string>('')
|
||||||
|
const [password, setPassword] = useState<string>('')
|
||||||
|
const [remember, setRemember] = useState<boolean>(false)
|
||||||
|
const [sepolia, setSepolia] = useState<string>('https://sepolia.drpc.org')
|
||||||
|
const { enqueueSnackbar } = useSnackbar()
|
||||||
|
|
||||||
|
const inputStyle = { background: 'white', padding: '2px 8px', width: '100%' }
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const storedSepolia = localStorage.getItem('sepolia')
|
||||||
|
|
||||||
|
if (storedSepolia) {
|
||||||
|
setSepolia(storedSepolia)
|
||||||
|
}
|
||||||
|
const fdpCredentials = localStorage.getItem('fdpCredentials')
|
||||||
|
|
||||||
|
if (fdpCredentials) {
|
||||||
|
const { username, password } = JSON.parse(fdpCredentials)
|
||||||
|
setUsername(username)
|
||||||
|
setPassword(password)
|
||||||
|
setRemember(true)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
async function onLogin() {
|
||||||
|
localStorage.setItem('sepolia', sepolia)
|
||||||
|
|
||||||
|
if (remember) {
|
||||||
|
localStorage.setItem('fdpCredentials', JSON.stringify({ username, password }))
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('fdpCredentials')
|
||||||
|
}
|
||||||
|
enqueueSnackbar('Logging in...', { variant: 'info' })
|
||||||
|
try {
|
||||||
|
await fdp.account.login(username, password)
|
||||||
|
enqueueSnackbar('Logged in successfully', { variant: 'success' })
|
||||||
|
onSuccessfulLogin()
|
||||||
|
} catch {
|
||||||
|
enqueueSnackbar('Login failed', { variant: 'error' })
|
||||||
|
} finally {
|
||||||
|
setUsername('')
|
||||||
|
setPassword('')
|
||||||
|
setRemember(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRegister() {
|
||||||
|
window.open('https://create.fairdatasociety.org/', '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
maxWidth: '500px',
|
||||||
|
margin: 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Vertical gap={16} full>
|
||||||
|
<Vertical gap={8} left full>
|
||||||
|
<Typography variant="body2">Sepolia JSON RPC</Typography>
|
||||||
|
<InputBase value={sepolia} onChange={e => setSepolia(e.target.value)} style={inputStyle} />
|
||||||
|
</Vertical>
|
||||||
|
<Vertical gap={8} left full>
|
||||||
|
<Typography variant="body2">Username</Typography>
|
||||||
|
<InputBase value={username} onChange={e => setUsername(e.target.value)} style={inputStyle} />
|
||||||
|
</Vertical>
|
||||||
|
<Vertical gap={8} left full>
|
||||||
|
<Typography variant="body2">Password</Typography>
|
||||||
|
<InputBase value={password} onChange={e => setPassword(e.target.value)} style={inputStyle} type="password" />
|
||||||
|
</Vertical>
|
||||||
|
<Vertical gap={8} left full>
|
||||||
|
<Horizontal>
|
||||||
|
<Checkbox checked={remember} onChange={e => setRemember(e.target.checked)} />
|
||||||
|
<Typography variant="body2">Remember me</Typography>
|
||||||
|
</Horizontal>
|
||||||
|
</Vertical>
|
||||||
|
<Vertical left full>
|
||||||
|
<Horizontal gap={4}>
|
||||||
|
<SwarmButton iconType={LoginIcon} onClick={onLogin}>
|
||||||
|
Login
|
||||||
|
</SwarmButton>
|
||||||
|
<SwarmButton iconType={RegisterIcon} onClick={onRegister}>
|
||||||
|
Registration
|
||||||
|
</SwarmButton>
|
||||||
|
</Horizontal>
|
||||||
|
</Vertical>
|
||||||
|
</Vertical>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { FdpStorage } from '@fairdatasociety/fdp-storage'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { CafeReactFs } from '../../react-fs/CafeReactFs'
|
||||||
|
import { FsItem, FsItemType } from '../../react-fs/CafeReactType'
|
||||||
|
import { joinUrl } from '../../react-fs/Utility'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
fdp: FdpStorage
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FdpPod({ fdp, name }: Props) {
|
||||||
|
const [reloader, setReloader] = useState(0)
|
||||||
|
|
||||||
|
function reload() {
|
||||||
|
setReloader(reloader + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CafeReactFs
|
||||||
|
rootAlias={`/${name}`}
|
||||||
|
backgroundColor="#ffffff"
|
||||||
|
reloader={reloader}
|
||||||
|
onDeleteFile={async (path: string) => {
|
||||||
|
await fdp.file.delete(name, path)
|
||||||
|
reload()
|
||||||
|
}}
|
||||||
|
onDeleteDirectory={async (path: string) => {
|
||||||
|
await fdp.directory.delete(name, path)
|
||||||
|
reload()
|
||||||
|
}}
|
||||||
|
onUpload={(path: string) => {
|
||||||
|
const input = document.createElement('input')
|
||||||
|
input.type = 'file'
|
||||||
|
input.multiple = true
|
||||||
|
input.click()
|
||||||
|
|
||||||
|
return new Promise<void>(resolve => {
|
||||||
|
input.onchange = async () => {
|
||||||
|
if (!input.files || !input.files.length) {
|
||||||
|
resolve()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (const file of Array.from(input.files)) {
|
||||||
|
const data = await file.arrayBuffer()
|
||||||
|
await fdp.file.uploadData(name, joinUrl(path, file.name), new Uint8Array(data))
|
||||||
|
}
|
||||||
|
reload()
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
onCreateDirectory={async (path: string) => {
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
const newDirectoryName = prompt('Directory name')
|
||||||
|
|
||||||
|
if (!newDirectoryName) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await fdp.directory.create(name, joinUrl(path, newDirectoryName))
|
||||||
|
reload()
|
||||||
|
}}
|
||||||
|
// eslint-disable-next-line require-await
|
||||||
|
onSync={async () => {
|
||||||
|
setReloader(reloader + 1)
|
||||||
|
}}
|
||||||
|
download={async (path: string) => {
|
||||||
|
const data = await fdp.file.downloadData(name, path)
|
||||||
|
const url = URL.createObjectURL(new Blob([data]))
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = path.split('/').pop() || 'Untitled'
|
||||||
|
a.click()
|
||||||
|
}}
|
||||||
|
list={async (path: string) => {
|
||||||
|
const fdpResponse = await fdp.directory.read(name, path)
|
||||||
|
const items: FsItem[] = []
|
||||||
|
for (const directory of fdpResponse.directories) {
|
||||||
|
items.push({
|
||||||
|
name: directory.name,
|
||||||
|
$type: FsItemType.DIRECTORY,
|
||||||
|
id: directory.name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for (const file of fdpResponse.files) {
|
||||||
|
items.push({
|
||||||
|
name: file.name,
|
||||||
|
$type: FsItemType.FILE,
|
||||||
|
id: file.name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { FdpStorage } from '@fairdatasociety/fdp-storage'
|
||||||
|
import { Pod } from '@fairdatasociety/fdp-storage/dist/pod/types'
|
||||||
|
import { CircularProgress, Typography } from '@material-ui/core'
|
||||||
|
import { FdpPod } from './FdpPod'
|
||||||
|
import { Vertical } from './Vertical'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
fdp: FdpStorage
|
||||||
|
pods: Pod[]
|
||||||
|
loadingPods: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FdpPods({ fdp, pods, loadingPods }: Props) {
|
||||||
|
if (loadingPods) {
|
||||||
|
return (
|
||||||
|
<Vertical gap={32} full>
|
||||||
|
<CircularProgress />
|
||||||
|
<Typography>Loading your pods...</Typography>
|
||||||
|
</Vertical>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Vertical gap={16} full left>
|
||||||
|
{pods.map(pod => (
|
||||||
|
<FdpPod key={pod.index} fdp={fdp} name={pod.name} />
|
||||||
|
))}
|
||||||
|
</Vertical>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
interface Props {
|
||||||
|
children: React.ReactNode
|
||||||
|
p?: string
|
||||||
|
gap?: number
|
||||||
|
between?: boolean
|
||||||
|
background?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Horizontal({ children, p = '0', gap = 8, between, background }: Props) {
|
||||||
|
const style = {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row' as 'row', //eslint-disable-line
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: between ? 'space-between' : 'flex-start',
|
||||||
|
gap: `${gap}px`,
|
||||||
|
padding: p,
|
||||||
|
background,
|
||||||
|
width: between ? '100%' : 'auto',
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div style={style}>{children}</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
interface Props {
|
||||||
|
children: React.ReactNode
|
||||||
|
p?: number
|
||||||
|
gap?: number
|
||||||
|
left?: boolean
|
||||||
|
full?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Vertical({ children, p = 0, gap = 0, left = false, full = false }: Props) {
|
||||||
|
const style = {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column' as 'column', //eslint-disable-line
|
||||||
|
alignItems: left ? 'flex-start' : 'center',
|
||||||
|
gap: `${gap}px`,
|
||||||
|
width: full ? '100%' : 'auto',
|
||||||
|
padding: `${p}px`,
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div style={style}>{children}</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import { Bee } from '@ethersphere/bee-js'
|
||||||
|
import { FdpStorage } from '@fairdatasociety/fdp-storage'
|
||||||
|
import { Pod } from '@fairdatasociety/fdp-storage/dist/pod/types'
|
||||||
|
import { CircularProgress, Typography } from '@material-ui/core'
|
||||||
|
import { useSnackbar } from 'notistack'
|
||||||
|
import { ReactElement, useEffect, useState } from 'react'
|
||||||
|
import ImportIcon from 'remixicon-react/AddBoxLineIcon'
|
||||||
|
import PlusCircle from 'remixicon-react/AddCircleLineIcon'
|
||||||
|
import { SwarmButton } from '../../components/SwarmButton'
|
||||||
|
import { joinUrl } from '../../react-fs/Utility'
|
||||||
|
import { ManifestJs } from '../../utils/manifest'
|
||||||
|
import { FdpLogin } from './FdpLogin'
|
||||||
|
import { FdpPods } from './FdpPods'
|
||||||
|
import { Horizontal } from './Horizontal'
|
||||||
|
import { Vertical } from './Vertical'
|
||||||
|
|
||||||
|
async function makeFdp(): Promise<FdpStorage | null> {
|
||||||
|
const bee = new Bee('http://localhost:1633')
|
||||||
|
const sepolia = localStorage.getItem('sepolia') ?? 'https://sepolia.drpc.org'
|
||||||
|
const postageBatches = await bee.getAllPostageBatch()
|
||||||
|
const usableBatches = postageBatches.filter(batch => batch.usable)
|
||||||
|
const highestCapacityBatch = usableBatches.length ? usableBatches.reduce((a, b) => (a.depth > b.depth ? a : b)) : null
|
||||||
|
|
||||||
|
if (!highestCapacityBatch) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FdpStorage('http://localhost:1633', highestCapacityBatch.batchID, {
|
||||||
|
ensOptions: {
|
||||||
|
rpcUrl: sepolia,
|
||||||
|
contractAddresses: {
|
||||||
|
ensRegistry: '0x42a96D45d787685ac4b36292d218B106Fb39be7F',
|
||||||
|
fdsRegistrar: '0xFBF00389140C00384d88d458239833E3231a7414',
|
||||||
|
nameResolver: '0xE20ECe6Ea93c4edE41e4d3B973f6679F1E89986A',
|
||||||
|
publicResolver: '0xC904989B579c2B216A75723688C784038AA99B56',
|
||||||
|
reverseResolver: '0xbDC8D98d3cbFd68EA9c165E1f15Df6e77A2ae0C5',
|
||||||
|
},
|
||||||
|
gasEstimation: 1,
|
||||||
|
performChecks: true,
|
||||||
|
},
|
||||||
|
providerOptions: {
|
||||||
|
url: sepolia,
|
||||||
|
},
|
||||||
|
ensDomain: 'fds',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FDP(): ReactElement {
|
||||||
|
const [fdp, setFdp] = useState<FdpStorage | null>(null)
|
||||||
|
const [pods, setPods] = useState<Pod[]>([])
|
||||||
|
const [loggedIn, setLoggedIn] = useState<boolean>(false)
|
||||||
|
const [loadingPods, setLoadingPods] = useState<boolean>(false)
|
||||||
|
const [creatingPod, setCreatingPod] = useState<boolean>(false)
|
||||||
|
const { enqueueSnackbar } = useSnackbar()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
makeFdp().then(fdp => {
|
||||||
|
if (!fdp) {
|
||||||
|
enqueueSnackbar('FDP could not be initialized. Do you have a postage batch?', { variant: 'error' })
|
||||||
|
}
|
||||||
|
setFdp(fdp)
|
||||||
|
})
|
||||||
|
}, [enqueueSnackbar])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fdp && loggedIn) {
|
||||||
|
setLoadingPods(true)
|
||||||
|
fdp.personalStorage.list().then(pods => {
|
||||||
|
setPods(pods.pods)
|
||||||
|
setLoadingPods(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [fdp, loggedIn])
|
||||||
|
|
||||||
|
function onSuccessfulLogin() {
|
||||||
|
setLoggedIn(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCreatePod() {
|
||||||
|
if (!fdp) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadingPods || creatingPod) {
|
||||||
|
enqueueSnackbar('Please wait until the pods are loaded', { variant: 'info' })
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
const name = prompt('Enter a name for the new pod')
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
setCreatingPod(true)
|
||||||
|
fdp.personalStorage.create(name).then(() => {
|
||||||
|
fdp.personalStorage.list().then(pods => {
|
||||||
|
setPods(pods.pods)
|
||||||
|
setCreatingPod(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onImportPod() {
|
||||||
|
if (!fdp) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadingPods || creatingPod) {
|
||||||
|
enqueueSnackbar('Please wait until the pods are loaded', { variant: 'info' })
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
const name = prompt('Enter a name for the new pod')
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
const importHash = prompt('Enter the Swarm reference')
|
||||||
|
|
||||||
|
if (!name || !importHash) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setCreatingPod(true)
|
||||||
|
const bee = new Bee('http://localhost:1633')
|
||||||
|
const manifestJs = new ManifestJs(bee)
|
||||||
|
const entries = await manifestJs.getHashes(importHash)
|
||||||
|
await fdp.personalStorage.create(name)
|
||||||
|
for (const [path, hash] of Object.entries(entries)) {
|
||||||
|
await fdp.file.uploadData(name, joinUrl('/', path), await bee.downloadData(hash))
|
||||||
|
}
|
||||||
|
const pods = await fdp.personalStorage.list()
|
||||||
|
setPods(pods.pods)
|
||||||
|
setCreatingPod(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fdp) {
|
||||||
|
return <CircularProgress />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Vertical gap={32} full left>
|
||||||
|
<Horizontal between>
|
||||||
|
<Typography variant="h1">Files</Typography>
|
||||||
|
{loggedIn && (
|
||||||
|
<Horizontal gap={4}>
|
||||||
|
<SwarmButton onClick={onCreatePod} iconType={PlusCircle}>
|
||||||
|
Create
|
||||||
|
</SwarmButton>
|
||||||
|
<SwarmButton onClick={onImportPod} iconType={ImportIcon}>
|
||||||
|
Import
|
||||||
|
</SwarmButton>
|
||||||
|
</Horizontal>
|
||||||
|
)}
|
||||||
|
</Horizontal>
|
||||||
|
{!loggedIn && <FdpLogin fdp={fdp} onSuccessfulLogin={onSuccessfulLogin} />}
|
||||||
|
{loggedIn && <FdpPods fdp={fdp} pods={pods} loadingPods={loadingPods || creatingPod} />}
|
||||||
|
{loggedIn && !loadingPods && !creatingPod && pods.length === 0 && (
|
||||||
|
<Typography>
|
||||||
|
<strong>You do not have any pods yet.</strong> Get started by clicking the Create or Import button on the top
|
||||||
|
right.
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Vertical>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
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 } from 'react'
|
import { ReactElement, useMemo } from 'react'
|
||||||
import File from 'remixicon-react/FileLineIcon'
|
import File from 'remixicon-react/FileLineIcon'
|
||||||
import Folder from 'remixicon-react/FolderLineIcon'
|
import Folder from 'remixicon-react/FolderLineIcon'
|
||||||
import { FitImage } from '../../components/FitImage'
|
import { FitImage } from '../../components/FitImage'
|
||||||
@@ -8,35 +8,52 @@ import { shortenText } from '../../utils'
|
|||||||
import { getHumanReadableFileSize } from '../../utils/file'
|
import { getHumanReadableFileSize } from '../../utils/file'
|
||||||
import { shortenHash } from '../../utils/hash'
|
import { shortenHash } from '../../utils/hash'
|
||||||
import { AssetIcon } from './AssetIcon'
|
import { AssetIcon } from './AssetIcon'
|
||||||
|
import { FitVideo } from '../../components/FitVideo'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
previewUri?: string
|
previewUri?: string
|
||||||
metadata?: Metadata
|
metadata?: Metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: add optional prop for indexDocument when it is already known (e.g. downloading a manifest)
|
/* eslint-disable react/display-name */
|
||||||
|
const getPreviewComponent = (previewUri?: string, metadata?: Metadata) => {
|
||||||
|
if (metadata?.isVideo) {
|
||||||
|
return () => <FitVideo src={previewUri} maxWidth="250px" maxHeight="175px" />
|
||||||
|
}
|
||||||
|
|
||||||
export function AssetPreview({ metadata, previewUri }: Props): ReactElement | null {
|
if (metadata?.isImage) {
|
||||||
let previewComponent = <File />
|
return () => <FitImage maxWidth="250px" maxHeight="175px" alt="Upload Preview" src={previewUri} />
|
||||||
let type = metadata?.type
|
}
|
||||||
|
|
||||||
if (metadata?.isWebsite) {
|
if (metadata?.isWebsite) {
|
||||||
previewComponent = <Web />
|
return () => <AssetIcon icon={<Web />} />
|
||||||
type = 'Website'
|
|
||||||
} else if (metadata?.type === 'folder') {
|
|
||||||
previewComponent = <Folder />
|
|
||||||
type = 'Folder'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (metadata?.type === 'folder') {
|
||||||
|
return () => <AssetIcon icon={<Folder />} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => <AssetIcon icon={<File />} />
|
||||||
|
}
|
||||||
|
|
||||||
|
const getType = (metadata?: Metadata) => {
|
||||||
|
if (metadata?.isWebsite) return 'Website'
|
||||||
|
|
||||||
|
if (metadata?.type === 'folder') return 'Folder'
|
||||||
|
|
||||||
|
return metadata?.type
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add optional prop for indexDocument when it is already known (e.g. downloading a manifest)
|
||||||
|
export function AssetPreview({ metadata, previewUri }: Props): ReactElement | null {
|
||||||
|
const PreviewAssetComponent = useMemo(() => getPreviewComponent(previewUri, metadata), [metadata, previewUri])
|
||||||
|
const type = useMemo(() => getType(metadata), [metadata])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box mb={4}>
|
<Box mb={4}>
|
||||||
<Box bgcolor="background.paper">
|
<Box bgcolor="background.paper">
|
||||||
<Grid container direction="row">
|
<Grid container direction="row">
|
||||||
{previewUri ? (
|
<PreviewAssetComponent />
|
||||||
<FitImage maxWidth="250px" maxHeight="175px" alt="Upload Preview" src={previewUri} />
|
|
||||||
) : (
|
|
||||||
<AssetIcon icon={previewComponent} />
|
|
||||||
)}
|
|
||||||
<Box p={2}>
|
<Box p={2}>
|
||||||
{metadata?.hash && <Typography>Swarm Hash: {shortenHash(metadata.hash)}</Typography>}
|
{metadata?.hash && <Typography>Swarm Hash: {shortenHash(metadata.hash)}</Typography>}
|
||||||
{metadata?.name && metadata?.name !== metadata?.hash && (
|
{metadata?.name && metadata?.name !== metadata?.hash && (
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Box } from '@material-ui/core'
|
|||||||
import { ReactElement, useContext, useEffect, useRef, useState } from 'react'
|
import { ReactElement, useContext, useEffect, useRef, useState } from 'react'
|
||||||
import { DocumentationText } from '../../components/DocumentationText'
|
import { DocumentationText } from '../../components/DocumentationText'
|
||||||
import { LinearProgressWithLabel } from '../../components/ProgressBar'
|
import { LinearProgressWithLabel } from '../../components/ProgressBar'
|
||||||
|
import { Tag } from '@ethersphere/bee-js'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
reference: string
|
reference: string
|
||||||
@@ -16,12 +17,20 @@ export function AssetSyncing({ reference }: Props): ReactElement {
|
|||||||
const [syncProgress, setSyncProgress] = useState<number>(0)
|
const [syncProgress, setSyncProgress] = useState<number>(0)
|
||||||
|
|
||||||
const syncCheck = async () => {
|
const syncCheck = async () => {
|
||||||
if (!beeApi) {
|
if (!beeApi) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const tags = await beeApi.getAllTags()
|
let allTags: Tag[] = []
|
||||||
const tag = tags.find(t => t.address === reference)
|
let offset = 0
|
||||||
|
const limit = 1000
|
||||||
|
let tagsBatch
|
||||||
|
|
||||||
|
do {
|
||||||
|
tagsBatch = await beeApi.getAllTags({ limit, offset })
|
||||||
|
allTags = allTags.concat(tagsBatch)
|
||||||
|
offset += limit
|
||||||
|
} while (tagsBatch.length === limit) // Continue if the batch is full, stop if fewer than the limit
|
||||||
|
|
||||||
|
const tag = allTags.find(t => t.address === reference)
|
||||||
|
|
||||||
if (tag) {
|
if (tag) {
|
||||||
const progress = ((tag.seen + tag.synced) / tag.split) * 100
|
const progress = ((tag.seen + tag.synced) / tag.split) * 100
|
||||||
@@ -51,8 +60,6 @@ export function AssetSyncing({ reference }: Props): ReactElement {
|
|||||||
There are instances when it seems that the content isn't synchronized, despite being already available.
|
There are instances when it seems that the content isn't synchronized, despite being already available.
|
||||||
To ensure it's not due to invalid synchronization data,
|
To ensure it's not due to invalid synchronization data,
|
||||||
verify availability from at least 70% using one of the stewardship endpoints.
|
verify availability from at least 70% using one of the stewardship endpoints.
|
||||||
|
|
||||||
TODO: is 70 a good number?
|
|
||||||
*/
|
*/
|
||||||
if (beeApi && !isRetrieveChecking && syncProgress > 10 && syncProgress < 100) {
|
if (beeApi && !isRetrieveChecking && syncProgress > 10 && syncProgress < 100) {
|
||||||
// It's a long running task make sure only one run occurs at a time.
|
// It's a long running task make sure only one run occurs at a time.
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export function Download(): ReactElement {
|
|||||||
<>
|
<>
|
||||||
{nodeInfo?.beeMode !== BeeModes.ULTRA_LIGHT && <FileNavigation active="DOWNLOAD" />}
|
{nodeInfo?.beeMode !== BeeModes.ULTRA_LIGHT && <FileNavigation active="DOWNLOAD" />}
|
||||||
<ExpandableListItemInput
|
<ExpandableListItemInput
|
||||||
label="Swarm Hash"
|
label="Swarm Hash or ENS"
|
||||||
onConfirm={value => onSwarmIdentifier(value)}
|
onConfirm={value => onSwarmIdentifier(value)}
|
||||||
onChange={validateChange}
|
onChange={validateChange}
|
||||||
helperText={referenceError}
|
helperText={referenceError}
|
||||||
|
|||||||
+24
-27
@@ -7,7 +7,7 @@ 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 { META_FILE_NAME, PREVIEW_FILE_NAME } from '../../constants'
|
import { META_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'
|
||||||
@@ -50,38 +50,35 @@ export function Share(): ReactElement {
|
|||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const entries = await manifestJs.getHashes(reference)
|
|
||||||
|
const entries = await manifestJs.getHashes(reference, { exclude: [META_FILE_NAME] })
|
||||||
|
setSwarmEntries(entries)
|
||||||
|
|
||||||
const indexDocument = await manifestJs.getIndexDocumentPath(reference)
|
const indexDocument = await manifestJs.getIndexDocumentPath(reference)
|
||||||
setIndexDocument(indexDocument)
|
setIndexDocument(indexDocument)
|
||||||
|
|
||||||
const previewFile = entries[PREVIEW_FILE_NAME]
|
|
||||||
|
|
||||||
delete entries[META_FILE_NAME]
|
|
||||||
delete entries[PREVIEW_FILE_NAME]
|
|
||||||
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 {
|
try {
|
||||||
const mtdt = await beeApi.downloadFile(reference, META_FILE_NAME)
|
const remoteMetadata = await beeApi.downloadFile(reference, META_FILE_NAME)
|
||||||
const remoteMetadata = mtdt.data.text()
|
const formattedMetadata = JSON.parse(remoteMetadata.data.text()) as Metadata
|
||||||
metadata = { ...metadata, ...(JSON.parse(remoteMetadata) as Metadata) }
|
|
||||||
} catch (e) {} // eslint-disable-line no-empty
|
|
||||||
|
|
||||||
if (previewFile) {
|
if (formattedMetadata.isVideo || formattedMetadata.isImage) {
|
||||||
setPreview(`${apiUrl}/bzz/${reference}/${PREVIEW_FILE_NAME}`)
|
setPreview(`${apiUrl}/bzz/${reference}`)
|
||||||
|
}
|
||||||
|
setMetadata({ ...formattedMetadata, hash })
|
||||||
|
} catch (e) {
|
||||||
|
// if metadata is not available or invalid go with the default one
|
||||||
|
const count = Object.keys(entries).length
|
||||||
|
setMetadata({
|
||||||
|
hash,
|
||||||
|
type: count > 1 ? 'folder' : 'unknown',
|
||||||
|
name: reference,
|
||||||
|
count,
|
||||||
|
isWebsite: Boolean(indexDocument && /.*\.html?$/i.test(indexDocument)),
|
||||||
|
isVideo: Boolean(indexDocument && /.*\.(mp4|webm|ogg|mp3|ogg|wav)$/i.test(indexDocument)),
|
||||||
|
isImage: Boolean(indexDocument && /.*\.(jpg|jpeg|png|gif|webp|svg)$/i.test(indexDocument)),
|
||||||
|
// naive assumption based on indexDocument, we don't want to donwload the whole manifest
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
setMetadata(metadata)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onOpen() {
|
function onOpen() {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ 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 { META_FILE_NAME, PREVIEW_FILE_NAME } from '../../constants'
|
import { META_FILE_NAME } from '../../constants'
|
||||||
import { Context as BeeContext, CheckState } from '../../providers/Bee'
|
import { Context as BeeContext, CheckState } from '../../providers/Bee'
|
||||||
import { Identity, Context as IdentityContext } from '../../providers/Feeds'
|
import { Identity, Context as IdentityContext } from '../../providers/Feeds'
|
||||||
import { Context as FileContext } from '../../providers/File'
|
import { Context as FileContext } from '../../providers/File'
|
||||||
@@ -33,7 +33,7 @@ export function Upload(): ReactElement {
|
|||||||
|
|
||||||
const { stamps, refresh } = useContext(StampsContext)
|
const { stamps, refresh } = useContext(StampsContext)
|
||||||
const { beeApi } = useContext(SettingsContext)
|
const { beeApi } = useContext(SettingsContext)
|
||||||
const { files, setFiles, uploadOrigin, metadata, previewUri, previewBlob } = useContext(FileContext)
|
const { files, setFiles, uploadOrigin, metadata, previewUri } = useContext(FileContext)
|
||||||
const { identities, setIdentities } = useContext(IdentityContext)
|
const { identities, setIdentities } = useContext(IdentityContext)
|
||||||
const { status } = useContext(BeeContext)
|
const { status } = useContext(BeeContext)
|
||||||
|
|
||||||
@@ -98,31 +98,15 @@ export function Upload(): ReactElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastModified = files[0].lastModified
|
const lastModified = files[0].lastModified
|
||||||
|
|
||||||
// We want to store only some metadata
|
const metafile = new File([JSON.stringify(metadata)], META_FILE_NAME, {
|
||||||
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',
|
type: 'application/json',
|
||||||
lastModified,
|
lastModified,
|
||||||
})
|
})
|
||||||
fls.push(packageFile(metafile))
|
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)
|
||||||
|
|
||||||
await waitUntilStampUsable(stamp.batchID, beeApi)
|
await waitUntilStampUsable(stamp.batchID, beeApi)
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ export function ChequebookInfoCard() {
|
|||||||
<Card
|
<Card
|
||||||
buttonProps={{
|
buttonProps={{
|
||||||
iconType: ExchangeFunds,
|
iconType: ExchangeFunds,
|
||||||
children: 'View chequebook',
|
children: 'Manage chequebook',
|
||||||
onClick: () => navigate(ROUTES.ACCOUNT_CHEQUEBOOK),
|
onClick: () => navigate(ROUTES.ACCOUNT_CHEQUEBOOK),
|
||||||
}}
|
}}
|
||||||
icon={<ExchangeFunds />}
|
icon={<ExchangeFunds />}
|
||||||
title={`${chequebookBalance?.availableBalance.toSignificantDigits(4)} xBZZ`}
|
title={`${chequebookBalance?.availableBalance.toSignificantDigits(4)} xBZZ`}
|
||||||
subtitle="Current chequebook balance."
|
subtitle="Network transfer balance."
|
||||||
status="ok"
|
status="ok"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -32,7 +32,7 @@ export function ChequebookInfoCard() {
|
|||||||
<Card
|
<Card
|
||||||
buttonProps={{
|
buttonProps={{
|
||||||
iconType: ExchangeFunds,
|
iconType: ExchangeFunds,
|
||||||
children: 'View chequebook',
|
children: 'Manage chequebook',
|
||||||
onClick: () => navigate(ROUTES.ACCOUNT_CHEQUEBOOK),
|
onClick: () => navigate(ROUTES.ACCOUNT_CHEQUEBOOK),
|
||||||
}}
|
}}
|
||||||
icon={<ExchangeFunds />}
|
icon={<ExchangeFunds />}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Button } from '@material-ui/core'
|
import { Button } from '@material-ui/core'
|
||||||
import { ReactElement, useContext } from 'react'
|
import { ReactElement, useContext } from 'react'
|
||||||
|
import { ChainSync } from '../../components/ChainSync'
|
||||||
import ExpandableListItem from '../../components/ExpandableListItem'
|
import ExpandableListItem from '../../components/ExpandableListItem'
|
||||||
import Map from '../../components/Map'
|
import Map from '../../components/Map'
|
||||||
import { BEE_DESKTOP_LATEST_RELEASE_PAGE } from '../../constants'
|
import { BEE_DESKTOP_LATEST_RELEASE_PAGE } from '../../constants'
|
||||||
@@ -19,11 +20,17 @@ export default function Status(): ReactElement {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'stretch', alignContent: 'stretch' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'stretch',
|
||||||
|
alignContent: 'stretch',
|
||||||
|
gap: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<NodeInfoCard />
|
<NodeInfoCard />
|
||||||
<div style={{ width: '8px' }}></div>
|
|
||||||
<WalletInfoCard />
|
<WalletInfoCard />
|
||||||
<div style={{ width: '8px' }}></div>
|
|
||||||
<ChequebookInfoCard />
|
<ChequebookInfoCard />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ height: '16px' }} />
|
<div style={{ height: '16px' }} />
|
||||||
@@ -31,6 +38,9 @@ export default function Status(): ReactElement {
|
|||||||
<div style={{ height: '2px' }} />
|
<div style={{ height: '2px' }} />
|
||||||
<ExpandableListItem label="Connected peers" value={topology?.connected ?? '-'} />
|
<ExpandableListItem label="Connected peers" value={topology?.connected ?? '-'} />
|
||||||
<ExpandableListItem label="Population" value={topology?.population ?? '-'} />
|
<ExpandableListItem label="Population" value={topology?.population ?? '-'} />
|
||||||
|
<ExpandableListItem label="Depth" value={topology?.depth ?? '-'} />
|
||||||
|
<ChainSync />
|
||||||
|
|
||||||
<div style={{ height: '16px' }} />
|
<div style={{ height: '16px' }} />
|
||||||
{isDesktop && (
|
{isDesktop && (
|
||||||
<ExpandableListItem
|
<ExpandableListItem
|
||||||
|
|||||||
@@ -1,35 +1,37 @@
|
|||||||
import { BeeModes } from '@ethersphere/bee-js'
|
import { BeeModes } from '@ethersphere/bee-js'
|
||||||
import { Box, Grid, Typography } from '@material-ui/core'
|
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 } from 'react'
|
||||||
import { useNavigate } from 'react-router'
|
import { useNavigate } from 'react-router'
|
||||||
|
import { ChainSync } from '../../components/ChainSync'
|
||||||
import { Waiting } from '../../components/Waiting'
|
import { Waiting } from '../../components/Waiting'
|
||||||
import { Context } from '../../providers/Bee'
|
import { Context } from '../../providers/Settings'
|
||||||
import { ROUTES } from '../../routes'
|
import { ROUTES } from '../../routes'
|
||||||
|
|
||||||
const STARTED_UPGRADE_AT = 'started-upgrade-at'
|
|
||||||
|
|
||||||
export default function LightModeRestart(): ReactElement {
|
export default function LightModeRestart(): ReactElement {
|
||||||
const [startedAt] = useState(Number.parseInt(localStorage.getItem(STARTED_UPGRADE_AT) ?? Date.now().toFixed()))
|
|
||||||
const { apiHealth, nodeInfo } = useContext(Context)
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { enqueueSnackbar } = useSnackbar()
|
const { enqueueSnackbar } = useSnackbar()
|
||||||
|
const { beeApi } = useContext(Context)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem(STARTED_UPGRADE_AT, startedAt.toFixed())
|
if (!beeApi) {
|
||||||
}, [startedAt])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (Date.now() - startedAt < 45_000) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (apiHealth && nodeInfo?.beeMode === BeeModes.LIGHT) {
|
const interval = setInterval(() => {
|
||||||
enqueueSnackbar('Upgraded to light node', { variant: 'success' })
|
beeApi
|
||||||
localStorage.removeItem(STARTED_UPGRADE_AT)
|
.getNodeInfo()
|
||||||
navigate(ROUTES.INFO)
|
.then(nodeInfo => {
|
||||||
}
|
if (nodeInfo.beeMode === BeeModes.LIGHT) {
|
||||||
}, [startedAt, navigate, nodeInfo, apiHealth, enqueueSnackbar])
|
enqueueSnackbar('Upgraded to light node', { variant: 'success' })
|
||||||
|
navigate(ROUTES.INFO)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(console.error) // eslint-disable-line
|
||||||
|
}, 3_000)
|
||||||
|
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [beeApi, enqueueSnackbar, navigate])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid container direction="column" justifyContent="center" alignItems="center">
|
<Grid container direction="column" justifyContent="center" alignItems="center">
|
||||||
@@ -41,9 +43,12 @@ export default function LightModeRestart(): ReactElement {
|
|||||||
<strong>Upgrading Bee</strong>
|
<strong>Upgrading Bee</strong>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Typography>
|
<Box mb={1}>
|
||||||
You will be redirected automatically once your node is up and running. This may take up to 10 minutes.
|
<Typography>
|
||||||
</Typography>
|
You will be redirected automatically once your node is up and running. This may take up to 10 minutes.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<ChainSync />
|
||||||
</Grid>
|
</Grid>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export function PostageStampAdvancedCreation({ onFinished }: Props): ReactElemen
|
|||||||
|
|
||||||
return `${secondsToTimeString(
|
return `${secondsToTimeString(
|
||||||
convertAmountToSeconds(amount, pricePerBlock),
|
convertAmountToSeconds(amount, pricePerBlock),
|
||||||
)} (with price of ${pricePerBlock.toFixed(0)} per block)`
|
)} (with price of ${pricePerBlock.toFixed(0)} PLUR per block)`
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPrice(depth: number, amount: bigint): string {
|
function getPrice(depth: number, amount: bigint): string {
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export function PostageStampStandardCreation({ onFinished }: Props): ReactElemen
|
|||||||
|
|
||||||
return `${secondsToTimeString(
|
return `${secondsToTimeString(
|
||||||
convertAmountToSeconds(parseInt(amount, 10), pricePerBlock),
|
convertAmountToSeconds(parseInt(amount, 10), pricePerBlock),
|
||||||
)} (with price of ${pricePerBlock.toFixed(0)} per block)`
|
)} (with price of ${pricePerBlock.toFixed(0)} PLUR per block)`
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPrice(depth: number, amount: bigint): string {
|
function getPrice(depth: number, amount: bigint): string {
|
||||||
@@ -196,7 +196,7 @@ export function PostageStampStandardCreation({ onFinished }: Props): ReactElemen
|
|||||||
</Box>
|
</Box>
|
||||||
<Box display="flex" justifyContent={'right'} mt={0.5}>
|
<Box display="flex" justifyContent={'right'} mt={0.5}>
|
||||||
<Typography style={{ fontSize: '10px', color: 'rgba(0, 0, 0, 0.26)' }}>
|
<Typography style={{ fontSize: '10px', color: 'rgba(0, 0, 0, 0.26)' }}>
|
||||||
Current price of 24000 per block
|
Current price of 24000 PLUR per block
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
+16
-6
@@ -41,7 +41,8 @@ export function Provider({ children }: Props): ReactElement {
|
|||||||
const [previewBlob, setPreviewBlob] = useState<Blob | undefined>(undefined)
|
const [previewBlob, setPreviewBlob] = useState<Blob | undefined>(undefined)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMetadata(getMetadata(files))
|
const metadata = getMetadata(files)
|
||||||
|
setMetadata(metadata)
|
||||||
|
|
||||||
if (previewUri) {
|
if (previewUri) {
|
||||||
URL.revokeObjectURL(previewUri) // Clear the preview from memory
|
URL.revokeObjectURL(previewUri) // Clear the preview from memory
|
||||||
@@ -49,12 +50,21 @@ export function Provider({ children }: Props): ReactElement {
|
|||||||
setPreviewBlob(undefined)
|
setPreviewBlob(undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (files.length !== 1 || !files[0].type.startsWith('image')) return
|
if (files.length !== 1) return
|
||||||
|
|
||||||
resize(files[0], PREVIEW_DIMENSIONS.maxWidth, PREVIEW_DIMENSIONS.maxHeight).then(blob => {
|
if (metadata.isVideo) {
|
||||||
setPreviewUri(URL.createObjectURL(blob)) // NOTE: Until it is cleared with URL.revokeObjectURL, the file stays allocated in memory
|
const videoFile = files[0]
|
||||||
setPreviewBlob(blob)
|
const videoBlob = new Blob([videoFile], { type: videoFile.type })
|
||||||
})
|
setPreviewUri(URL.createObjectURL(videoBlob))
|
||||||
|
setPreviewBlob(videoBlob)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.isImage) {
|
||||||
|
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 () => {
|
return () => {
|
||||||
if (previewUri) {
|
if (previewUri) {
|
||||||
|
|||||||
@@ -65,17 +65,15 @@ export function Provider({ children, ...propsSettings }: Props): ReactElement {
|
|||||||
const propsProviderUrl =
|
const propsProviderUrl =
|
||||||
localStorage.getItem(LocalStorageKeys.providerUrl) || propsSettings.defaultRpcUrl || DEFAULT_RPC_URL
|
localStorage.getItem(LocalStorageKeys.providerUrl) || propsSettings.defaultRpcUrl || DEFAULT_RPC_URL
|
||||||
|
|
||||||
const [apiUrl, setApiUrl] = useState<string>(initialValues.apiUrl)
|
const [apiUrl, setApiUrl] = useState<string>(
|
||||||
|
sessionStorage.getItem('api_host') ?? propsSettings.beeApiUrl ?? initialValues.apiUrl,
|
||||||
|
)
|
||||||
const [beeApi, setBeeApi] = useState<Bee | null>(null)
|
const [beeApi, setBeeApi] = useState<Bee | null>(null)
|
||||||
const [desktopApiKey, setDesktopApiKey] = useState<string>(initialValues.desktopApiKey)
|
const [desktopApiKey, setDesktopApiKey] = useState<string>(initialValues.desktopApiKey)
|
||||||
const [rpcProviderUrl, setRpcProviderUrl] = useState(propsProviderUrl)
|
const [rpcProviderUrl, setRpcProviderUrl] = useState(propsProviderUrl)
|
||||||
const [rpcProvider, setRpcProvider] = useState(new providers.JsonRpcProvider(propsProviderUrl))
|
const [rpcProvider, setRpcProvider] = useState(new providers.JsonRpcProvider(propsProviderUrl))
|
||||||
const { config, isLoading, error } = useGetBeeConfig(desktopUrl)
|
const { config, isLoading, error } = useGetBeeConfig(desktopUrl)
|
||||||
|
|
||||||
const url = makeHttpUrl(
|
|
||||||
config?.['api-addr'] ?? sessionStorage.getItem('api_host') ?? propsSettings.beeApiUrl ?? apiUrl,
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const urlSearchParams = new URLSearchParams(window.location.search)
|
const urlSearchParams = new URLSearchParams(window.location.search)
|
||||||
const newApiKey = urlSearchParams.get('v')
|
const newApiKey = urlSearchParams.get('v')
|
||||||
@@ -88,18 +86,19 @@ export function Provider({ children, ...propsSettings }: Props): ReactElement {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const url = makeHttpUrl(config?.['api-addr'] ?? apiUrl)
|
||||||
try {
|
try {
|
||||||
setBeeApi(new Bee(url))
|
setBeeApi(new Bee(url))
|
||||||
sessionStorage.setItem('api_host', url)
|
sessionStorage.setItem('api_host', url)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setBeeApi(null)
|
setBeeApi(null)
|
||||||
}
|
}
|
||||||
}, [url])
|
}, [config, apiUrl])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Context.Provider
|
<Context.Provider
|
||||||
value={{
|
value={{
|
||||||
apiUrl: url,
|
apiUrl,
|
||||||
beeApi,
|
beeApi,
|
||||||
setApiUrl,
|
setApiUrl,
|
||||||
lockedApiSettings: Boolean(propsSettings.lockedApiSettings),
|
lockedApiSettings: Boolean(propsSettings.lockedApiSettings),
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export function Provider({ children }: Props): ReactElement {
|
|||||||
|
|
||||||
setStamps(stamps.filter(x => x.exists).map(enrichStamp))
|
setStamps(stamps.filter(x => x.exists).map(enrichStamp))
|
||||||
setLastUpdate(Date.now())
|
setLastUpdate(Date.now())
|
||||||
|
setError(null)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e as Error)
|
setError(e as Error)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Vendored
+4
-2
@@ -6,14 +6,16 @@ interface LatestBeeRelease {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface SwarmMetadata {
|
interface SwarmMetadata {
|
||||||
size: number
|
size?: number
|
||||||
name: string
|
name: string
|
||||||
type?: string
|
type?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Metadata extends SwarmMetadata {
|
interface Metadata extends SwarmMetadata {
|
||||||
type: string
|
type: string
|
||||||
isWebsite: boolean
|
isWebsite?: boolean
|
||||||
|
isVideo?: boolean
|
||||||
|
isImage?: boolean
|
||||||
count?: number
|
count?: number
|
||||||
hash?: string
|
hash?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { CafeReactFsCreate } from './CafeReactFsCreate'
|
||||||
|
import { CafeReactFsItem } from './CafeReactFsItem'
|
||||||
|
import { CafeReactFsLoading } from './CafeReactFsLoading'
|
||||||
|
import { CafeReactFsPath } from './CafeReactFsPath'
|
||||||
|
import { CafeReactFsSync } from './CafeReactFsSync'
|
||||||
|
import { CafeReactFsUpload } from './CafeReactFsUpload'
|
||||||
|
import { FsItem } from './CafeReactType'
|
||||||
|
|
||||||
|
const DEFAULT_BACKGROUND_COLOR = '#f0f0f0'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
download: (path: string) => Promise<void>
|
||||||
|
list: (path: string) => Promise<FsItem[]>
|
||||||
|
onUpload: (path: string) => Promise<void>
|
||||||
|
onCreateDirectory: (path: string) => Promise<void>
|
||||||
|
onDeleteFile: (path: string) => Promise<void>
|
||||||
|
onDeleteDirectory: (path: string) => Promise<void>
|
||||||
|
onSync: () => Promise<void>
|
||||||
|
reloader: number
|
||||||
|
backgroundColor?: string
|
||||||
|
rootAlias?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CafeReactFs({
|
||||||
|
download,
|
||||||
|
list,
|
||||||
|
onUpload,
|
||||||
|
onCreateDirectory,
|
||||||
|
onDeleteFile,
|
||||||
|
onDeleteDirectory,
|
||||||
|
onSync,
|
||||||
|
reloader,
|
||||||
|
backgroundColor,
|
||||||
|
rootAlias,
|
||||||
|
}: Props) {
|
||||||
|
const [path, setPath] = useState('/')
|
||||||
|
const [items, setItems] = useState<FsItem[]>([])
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
function setItemsSorted(items: FsItem[]) {
|
||||||
|
// directories first, all alphabetically
|
||||||
|
const sortedItems = items.slice().sort((a, b) => {
|
||||||
|
if (a.$type === b.$type) {
|
||||||
|
return a.name.localeCompare(b.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.$type === 'directory' ? -1 : 1
|
||||||
|
})
|
||||||
|
setItems(sortedItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true)
|
||||||
|
list(path)
|
||||||
|
.then(setItemsSorted)
|
||||||
|
.finally(() => setLoading(false))
|
||||||
|
}, [reloader, list, path])
|
||||||
|
|
||||||
|
const pathParts = ['/', ...path.split('/').filter(x => x)]
|
||||||
|
|
||||||
|
function jumpToDirectory(fullPath: string) {
|
||||||
|
setPath(fullPath)
|
||||||
|
setLoading(true)
|
||||||
|
list(fullPath)
|
||||||
|
.then(setItemsSorted)
|
||||||
|
.finally(() => setLoading(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
function enterDirectory(name: string) {
|
||||||
|
const newPath = path.endsWith('/') ? `${path}${name}` : `${path}/${name}`
|
||||||
|
setPath(newPath)
|
||||||
|
setLoading(true)
|
||||||
|
list(newPath)
|
||||||
|
.then(setItemsSorted)
|
||||||
|
.finally(() => setLoading(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||||
|
<CafeReactFsPath
|
||||||
|
pathParts={pathParts}
|
||||||
|
jumpToDirectory={jumpToDirectory}
|
||||||
|
backgroundColor={backgroundColor ?? DEFAULT_BACKGROUND_COLOR}
|
||||||
|
rootAlias={rootAlias}
|
||||||
|
/>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'row', flexWrap: 'wrap', gap: '4px' }}>
|
||||||
|
{loading && <CafeReactFsLoading backgroundColor={backgroundColor ?? DEFAULT_BACKGROUND_COLOR} />}
|
||||||
|
{!loading &&
|
||||||
|
items.map(item => (
|
||||||
|
<CafeReactFsItem
|
||||||
|
key={item.id}
|
||||||
|
path={path}
|
||||||
|
item={item}
|
||||||
|
enterDirectory={enterDirectory}
|
||||||
|
onDeleteFile={onDeleteFile}
|
||||||
|
onDeleteDirectory={onDeleteDirectory}
|
||||||
|
download={download}
|
||||||
|
backgroundColor={backgroundColor ?? DEFAULT_BACKGROUND_COLOR}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{!loading && (
|
||||||
|
<>
|
||||||
|
<CafeReactFsUpload
|
||||||
|
onUpload={() => onUpload(path)}
|
||||||
|
backgroundColor={backgroundColor ?? DEFAULT_BACKGROUND_COLOR}
|
||||||
|
/>
|
||||||
|
<CafeReactFsCreate
|
||||||
|
onCreateDirectory={() => onCreateDirectory(path)}
|
||||||
|
backgroundColor={backgroundColor ?? DEFAULT_BACKGROUND_COLOR}
|
||||||
|
/>
|
||||||
|
<CafeReactFsSync backgroundColor={backgroundColor ?? DEFAULT_BACKGROUND_COLOR} onSync={onSync} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { CafeReactFsLoading } from './CafeReactFsLoading'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
backgroundColor: string
|
||||||
|
onCreateDirectory: () => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CafeReactFsCreate({ backgroundColor, onCreateDirectory }: Props) {
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
function proxyUpload() {
|
||||||
|
setLoading(true)
|
||||||
|
onCreateDirectory().finally(() => setLoading(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <CafeReactFsLoading backgroundColor={backgroundColor} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '80px',
|
||||||
|
height: '80px',
|
||||||
|
position: 'relative',
|
||||||
|
background: backgroundColor,
|
||||||
|
borderRadius: '2px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
onClick={proxyUpload}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="Create"
|
||||||
|
style={{ width: '64px', height: '64px', position: 'absolute', left: '8px', top: 0 }}
|
||||||
|
src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%3F%3E%3Csvg%20width%3D%22512%22%20height%3D%22512%22%20viewBox%3D%220%200%20512%20512%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M%20106%20131%20l%20100%200%20l%2025%2025%20l%20175%200%20l%200%20200%20l%20-300%200%20z%22%20stroke%3D%22%231F2D3D%22%20stroke-width%3D%2220%22%20fill%3D%22none%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%20%2F%3E%3Cpath%20d%3D%22M%20206%20256%20l%20100%200%22%20stroke%3D%22%23B83B5E%22%20stroke-width%3D%2220%22%20fill%3D%22none%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%20%2F%3E%3Cpath%20d%3D%22M%20256%20206%20l%200%20100%22%20stroke%3D%22%23B83B5E%22%20stroke-width%3D%2220%22%20fill%3D%22none%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%20%2F%3E%3C%2Fsvg%3E"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
fontFamily: 'sans-serif',
|
||||||
|
fontSize: '12px',
|
||||||
|
textAlign: 'center',
|
||||||
|
width: '80px',
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '5px',
|
||||||
|
left: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
New Folder
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
interface Props {
|
||||||
|
onDelete: () => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CafeReactFsDelete({ onDelete }: Props) {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
alt="Delete"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '2px',
|
||||||
|
right: '2px',
|
||||||
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
zIndex: 1,
|
||||||
|
}}
|
||||||
|
src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%3F%3E%3Csvg%20width%3D%22128%22%20height%3D%22128%22%20viewBox%3D%220%200%20128%20128%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ccircle%20cx%3D%2264%22%20cy%3D%2264%22%20r%3D%2264%22%20fill%3D%22%231F2D3D%22%20stroke%3D%22none%22%20stroke-width%3D%220%22%20%20%2F%3E%3Cpath%20d%3D%22M%2032%2064%20l%2064%200%22%20stroke%3D%22%23B83B5E%22%20stroke-width%3D%2220%22%20fill%3D%22none%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%20%2F%3E%3C%2Fsvg%3E"
|
||||||
|
onClick={event => {
|
||||||
|
onDelete()
|
||||||
|
event.stopPropagation()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { CafeReactFsDelete } from './CafeReactFsDelete'
|
||||||
|
import { CafeReactFsLoading } from './CafeReactFsLoading'
|
||||||
|
import { CafeReactFsName } from './CafeReactFsName'
|
||||||
|
import { VirtualDirectory } from './CafeReactType'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
directory: VirtualDirectory
|
||||||
|
enterDirectory: (name: string) => void
|
||||||
|
deleteDirectory: (name: string) => Promise<void>
|
||||||
|
backgroundColor: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CafeReactFsDirectory({ directory, enterDirectory, deleteDirectory, backgroundColor }: Props) {
|
||||||
|
const [hovered, setHovered] = useState(false)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
function proxyDelete() {
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
return deleteDirectory(directory.name).finally(() => setLoading(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <CafeReactFsLoading backgroundColor={backgroundColor} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '80px',
|
||||||
|
height: '80px',
|
||||||
|
position: 'relative',
|
||||||
|
background: backgroundColor,
|
||||||
|
borderRadius: '2px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
onClick={() => enterDirectory(directory.name)}
|
||||||
|
onMouseEnter={() => setHovered(true)}
|
||||||
|
onMouseLeave={() => setHovered(false)}
|
||||||
|
>
|
||||||
|
{hovered && <CafeReactFsDelete onDelete={proxyDelete} />}
|
||||||
|
<img
|
||||||
|
alt="Directory"
|
||||||
|
src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%3F%3E%3Csvg%20width%3D%22512%22%20height%3D%22512%22%20viewBox%3D%220%200%20512%20512%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M%20106%20131%20l%20100%200%20l%2025%2025%20l%20175%200%20l%200%20200%20l%20-300%200%20z%22%20stroke%3D%22%231F2D3D%22%20stroke-width%3D%2220%22%20fill%3D%22none%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%20%2F%3E%3C%2Fsvg%3E"
|
||||||
|
style={{ width: '64px', height: '64px', position: 'absolute', left: '8px', top: 0 }}
|
||||||
|
/>
|
||||||
|
<CafeReactFsName name={directory.name} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { CafeReactFsDelete } from './CafeReactFsDelete'
|
||||||
|
import { CafeReactFsLoading } from './CafeReactFsLoading'
|
||||||
|
import { CafeReactFsName } from './CafeReactFsName'
|
||||||
|
import { VirtualFile } from './CafeReactType'
|
||||||
|
import { joinUrl } from './Utility'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
path: string
|
||||||
|
file: VirtualFile
|
||||||
|
download: (path: string) => Promise<void>
|
||||||
|
deleteFile: (path: string) => Promise<void>
|
||||||
|
backgroundColor: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CafeReactFsFile({ path, file, download, deleteFile, backgroundColor }: Props) {
|
||||||
|
const [hovered, setHovered] = useState(false)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <CafeReactFsLoading backgroundColor={backgroundColor} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function proxyDelete() {
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
return deleteFile(joinUrl(path, file.name)).finally(() => setLoading(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={() => download(joinUrl(path, file.name))}
|
||||||
|
onMouseEnter={() => setHovered(true)}
|
||||||
|
onMouseLeave={() => setHovered(false)}
|
||||||
|
style={{
|
||||||
|
width: '80px',
|
||||||
|
height: '80px',
|
||||||
|
position: 'relative',
|
||||||
|
background: backgroundColor,
|
||||||
|
borderRadius: '2px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{hovered && <CafeReactFsDelete onDelete={proxyDelete} />}
|
||||||
|
<img
|
||||||
|
alt="File"
|
||||||
|
style={{ width: '64px', height: '64px', position: 'absolute', left: '8px', top: 0 }}
|
||||||
|
src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%3F%3E%3Csvg%20width%3D%22512%22%20height%3D%22512%22%20viewBox%3D%220%200%20512%20512%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M%20156%20131%20l%20150%200%20l%2050%2050%20l%200%20200%20l%20-200%200%20z%22%20stroke%3D%22%231F2D3D%22%20stroke-width%3D%2220%22%20fill%3D%22none%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%20%2F%3E%3Cpath%20d%3D%22M%20306%20131%20l%200%2050%20l%2050%200%22%20stroke%3D%22%231F2D3D%22%20stroke-width%3D%2220%22%20fill%3D%22none%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%20%2F%3E%3C%2Fsvg%3E"
|
||||||
|
/>
|
||||||
|
<CafeReactFsName name={file.name} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { CafeReactFsDirectory } from './CafeReactFsDirectory'
|
||||||
|
import { CafeReactFsFile } from './CafeReactFsFile'
|
||||||
|
import { FsItem, isVirtualDirectory, isVirtualFile } from './CafeReactType'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
path: string
|
||||||
|
item: FsItem
|
||||||
|
download: (path: string) => Promise<void>
|
||||||
|
enterDirectory: (name: string) => void
|
||||||
|
onDeleteFile: (path: string) => Promise<void>
|
||||||
|
onDeleteDirectory: (path: string) => Promise<void>
|
||||||
|
backgroundColor: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CafeReactFsItem({
|
||||||
|
path,
|
||||||
|
item,
|
||||||
|
download,
|
||||||
|
enterDirectory,
|
||||||
|
onDeleteFile,
|
||||||
|
onDeleteDirectory,
|
||||||
|
backgroundColor,
|
||||||
|
}: Props) {
|
||||||
|
if (isVirtualFile(item)) {
|
||||||
|
return (
|
||||||
|
<CafeReactFsFile
|
||||||
|
path={path}
|
||||||
|
file={item}
|
||||||
|
download={download}
|
||||||
|
deleteFile={onDeleteFile}
|
||||||
|
backgroundColor={backgroundColor}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVirtualDirectory(item)) {
|
||||||
|
return (
|
||||||
|
<CafeReactFsDirectory
|
||||||
|
directory={item}
|
||||||
|
enterDirectory={enterDirectory}
|
||||||
|
deleteDirectory={onDeleteDirectory}
|
||||||
|
backgroundColor={backgroundColor}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { CSSProperties } from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
backgroundColor: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CafeReactFsLoading({ backgroundColor }: Props) {
|
||||||
|
const spinnerStyle = {
|
||||||
|
width: '80px',
|
||||||
|
height: '80px',
|
||||||
|
borderRadius: '2px',
|
||||||
|
position: 'relative',
|
||||||
|
background: backgroundColor,
|
||||||
|
} as CSSProperties
|
||||||
|
|
||||||
|
const bounceStyle = {
|
||||||
|
width: '32px',
|
||||||
|
height: '32px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: '#333',
|
||||||
|
top: '24px',
|
||||||
|
left: '24px',
|
||||||
|
opacity: 0.6,
|
||||||
|
position: 'absolute',
|
||||||
|
animation: 'bounce 2.0s infinite ease-in-out',
|
||||||
|
} as CSSProperties
|
||||||
|
|
||||||
|
const bounceStyle2 = {
|
||||||
|
...bounceStyle,
|
||||||
|
animationDelay: '-1.0s',
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyframes = `
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(0.0);
|
||||||
|
} 50% {
|
||||||
|
transform: scale(1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<style>{keyframes}</style>
|
||||||
|
<div style={spinnerStyle}>
|
||||||
|
<div style={bounceStyle}></div>
|
||||||
|
<div style={bounceStyle2}></div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
interface Props {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CafeReactFsName({ name }: Props) {
|
||||||
|
const shortName = name.length > 10 ? name.slice(0, 10) + '...' : name
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
title={name}
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
fontFamily: 'sans-serif',
|
||||||
|
fontSize: '12px',
|
||||||
|
textAlign: 'center',
|
||||||
|
width: '80px',
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '5px',
|
||||||
|
left: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{shortName}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { joinUrl } from './Utility'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
pathParts: string[]
|
||||||
|
jumpToDirectory: (fullPath: string) => void
|
||||||
|
backgroundColor: string
|
||||||
|
rootAlias?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CafeReactFsPath({ pathParts, jumpToDirectory, backgroundColor, rootAlias }: Props) {
|
||||||
|
const absolutePaths: string[] = []
|
||||||
|
|
||||||
|
for (const pathPart of pathParts) {
|
||||||
|
if (absolutePaths.length === 0) {
|
||||||
|
absolutePaths.push(pathPart)
|
||||||
|
} else {
|
||||||
|
absolutePaths.push(joinUrl(absolutePaths[absolutePaths.length - 1], pathPart))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'row', flexWrap: 'wrap', gap: '4px' }}>
|
||||||
|
{pathParts.map((part, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
style={{
|
||||||
|
background: backgroundColor,
|
||||||
|
borderRadius: '2px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
outline: 'none',
|
||||||
|
border: 'none',
|
||||||
|
fontSize: '20px',
|
||||||
|
padding: '4px 16px',
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
jumpToDirectory(absolutePaths[index])
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{rootAlias && part === '/' ? rootAlias : part}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
interface Props {
|
||||||
|
backgroundColor: string
|
||||||
|
onSync: () => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CafeReactFsSync({ backgroundColor, onSync }: Props) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '80px',
|
||||||
|
height: '80px',
|
||||||
|
position: 'relative',
|
||||||
|
background: backgroundColor,
|
||||||
|
borderRadius: '2px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
onClick={onSync}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="Sync"
|
||||||
|
style={{ width: '64px', height: '64px', position: 'absolute', left: '8px', top: 0 }}
|
||||||
|
src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%3F%3E%3Csvg%20width%3D%22512%22%20height%3D%22512%22%20viewBox%3D%220%200%20512%20512%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M%20156%20156%20l%20250%200%20l%200%20150%22%20stroke%3D%22%231F2D3D%22%20stroke-width%3D%2220%22%20fill%3D%22none%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%20%2F%3E%3Cpath%20d%3D%22M%20356%20256%20l%2050%2050%20l%2050%20-50%22%20stroke%3D%22%231F2D3D%22%20stroke-width%3D%2220%22%20fill%3D%22none%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%20%2F%3E%3Cpath%20d%3D%22M%20356%20356%20l%20-250%200%20l%200%20-150%22%20stroke%3D%22%231F2D3D%22%20stroke-width%3D%2220%22%20fill%3D%22none%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%20%2F%3E%3Cpath%20d%3D%22M%20156%20256%20l%20-50%20-50%20l%20-50%2050%22%20stroke%3D%22%231F2D3D%22%20stroke-width%3D%2220%22%20fill%3D%22none%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%20%2F%3E%3C%2Fsvg%3E"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
fontFamily: 'sans-serif',
|
||||||
|
fontSize: '12px',
|
||||||
|
textAlign: 'center',
|
||||||
|
width: '80px',
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '5px',
|
||||||
|
left: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Sync
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { CafeReactFsLoading } from './CafeReactFsLoading'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onUpload: () => Promise<void>
|
||||||
|
backgroundColor: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CafeReactFsUpload({ onUpload, backgroundColor }: Props) {
|
||||||
|
const [uploading, setUploading] = useState(false)
|
||||||
|
|
||||||
|
function proxyUpload() {
|
||||||
|
setUploading(true)
|
||||||
|
onUpload().finally(() => setUploading(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploading) {
|
||||||
|
return <CafeReactFsLoading backgroundColor={backgroundColor} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '80px',
|
||||||
|
height: '80px',
|
||||||
|
position: 'relative',
|
||||||
|
background: backgroundColor,
|
||||||
|
borderRadius: '2px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
onClick={proxyUpload}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="Upload"
|
||||||
|
style={{ width: '64px', height: '64px', position: 'absolute', left: '8px', top: 0 }}
|
||||||
|
src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%3F%3E%3Csvg%20width%3D%22512%22%20height%3D%22512%22%20viewBox%3D%220%200%20512%20512%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M%20156%20131%20l%20150%200%20l%2050%2050%20l%200%20200%20l%20-200%200%20z%22%20stroke%3D%22%231F2D3D%22%20stroke-width%3D%2220%22%20fill%3D%22none%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%20%2F%3E%3Cpath%20d%3D%22M%20306%20131%20l%200%2050%20l%2050%200%22%20stroke%3D%22%231F2D3D%22%20stroke-width%3D%2220%22%20fill%3D%22none%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%20%2F%3E%3Cpath%20d%3D%22M%20206%20256%20l%20100%200%22%20stroke%3D%22%23B83B5E%22%20stroke-width%3D%2220%22%20fill%3D%22none%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%20%2F%3E%3Cpath%20d%3D%22M%20256%20206%20l%200%20100%22%20stroke%3D%22%23B83B5E%22%20stroke-width%3D%2220%22%20fill%3D%22none%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%20%2F%3E%3C%2Fsvg%3E"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
fontFamily: 'sans-serif',
|
||||||
|
fontSize: '12px',
|
||||||
|
textAlign: 'center',
|
||||||
|
width: '80px',
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '5px',
|
||||||
|
left: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Upload
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
export enum FsItemType {
|
||||||
|
FILE = 'file',
|
||||||
|
DIRECTORY = 'directory',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VirtualFile {
|
||||||
|
id: string | number
|
||||||
|
name: string
|
||||||
|
$type: FsItemType.FILE
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VirtualDirectory {
|
||||||
|
id: string | number
|
||||||
|
name: string
|
||||||
|
$type: FsItemType.DIRECTORY
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FsItem = VirtualFile | VirtualDirectory
|
||||||
|
|
||||||
|
export function isVirtualFile(item: FsItem): item is VirtualFile {
|
||||||
|
return item.$type === FsItemType.FILE
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isVirtualDirectory(item: FsItem): item is VirtualDirectory {
|
||||||
|
return item.$type === FsItemType.DIRECTORY
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export function joinUrl(...parts: unknown[]): string {
|
||||||
|
return parts
|
||||||
|
.filter(x => x)
|
||||||
|
.join('/')
|
||||||
|
.replace(/(?<!:)\/+/g, '/')
|
||||||
|
}
|
||||||
+4
-1
@@ -5,6 +5,7 @@ import { AccountFeeds } from './pages/account/feeds/AccountFeeds'
|
|||||||
import { AccountStaking } from './pages/account/staking/AccountStaking'
|
import { AccountStaking } from './pages/account/staking/AccountStaking'
|
||||||
import { AccountStamps } from './pages/account/stamps/AccountStamps'
|
import { AccountStamps } from './pages/account/stamps/AccountStamps'
|
||||||
import { AccountWallet } from './pages/account/wallet/AccountWallet'
|
import { AccountWallet } from './pages/account/wallet/AccountWallet'
|
||||||
|
import FDP from './pages/fdp'
|
||||||
import CreateNewFeed from './pages/feeds/CreateNewFeed'
|
import CreateNewFeed from './pages/feeds/CreateNewFeed'
|
||||||
import { FeedSubpage } from './pages/feeds/FeedSubpage'
|
import { FeedSubpage } from './pages/feeds/FeedSubpage'
|
||||||
import UpdateFeed from './pages/feeds/UpdateFeed'
|
import UpdateFeed from './pages/feeds/UpdateFeed'
|
||||||
@@ -17,6 +18,7 @@ import Info from './pages/info'
|
|||||||
import LightModeRestart from './pages/restart/LightModeRestart'
|
import LightModeRestart from './pages/restart/LightModeRestart'
|
||||||
import Settings from './pages/settings'
|
import Settings from './pages/settings'
|
||||||
import { CreatePostageStampPage } from './pages/stamps/CreatePostageStampAdvancedPage'
|
import { CreatePostageStampPage } from './pages/stamps/CreatePostageStampAdvancedPage'
|
||||||
|
import { CreatePostageStampBasicPage } from './pages/stamps/CreatePostageStampStandardPage'
|
||||||
import Status from './pages/status'
|
import Status from './pages/status'
|
||||||
import TopUp from './pages/top-up'
|
import TopUp from './pages/top-up'
|
||||||
import { BankCardTopUpIndex } from './pages/top-up/BankCardTopUpIndex'
|
import { BankCardTopUpIndex } from './pages/top-up/BankCardTopUpIndex'
|
||||||
@@ -25,7 +27,6 @@ import { GiftCardFund } from './pages/top-up/GiftCardFund'
|
|||||||
import { GiftCardTopUpIndex } from './pages/top-up/GiftCardTopUpIndex'
|
import { GiftCardTopUpIndex } from './pages/top-up/GiftCardTopUpIndex'
|
||||||
import { Swap } from './pages/top-up/Swap'
|
import { Swap } from './pages/top-up/Swap'
|
||||||
import { Context as SettingsContext } from './providers/Settings'
|
import { Context as SettingsContext } from './providers/Settings'
|
||||||
import { CreatePostageStampBasicPage } from './pages/stamps/CreatePostageStampStandardPage'
|
|
||||||
|
|
||||||
export enum ROUTES {
|
export enum ROUTES {
|
||||||
INFO = '/',
|
INFO = '/',
|
||||||
@@ -55,6 +56,7 @@ export enum ROUTES {
|
|||||||
ACCOUNT_FEEDS_VIEW = '/account/feeds/:uuid',
|
ACCOUNT_FEEDS_VIEW = '/account/feeds/:uuid',
|
||||||
ACCOUNT_INVITATIONS = '/account/invitations',
|
ACCOUNT_INVITATIONS = '/account/invitations',
|
||||||
ACCOUNT_STAKING = '/account/staking',
|
ACCOUNT_STAKING = '/account/staking',
|
||||||
|
FDP = '/fdp',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ACCOUNT_TABS = [
|
export const ACCOUNT_TABS = [
|
||||||
@@ -95,6 +97,7 @@ const BaseRouter = (): ReactElement => {
|
|||||||
<Route path={ROUTES.ACCOUNT_FEEDS_UPDATE} element={<UpdateFeed />} />
|
<Route path={ROUTES.ACCOUNT_FEEDS_UPDATE} element={<UpdateFeed />} />
|
||||||
<Route path={ROUTES.ACCOUNT_FEEDS_VIEW} element={<FeedSubpage />} />
|
<Route path={ROUTES.ACCOUNT_FEEDS_VIEW} element={<FeedSubpage />} />
|
||||||
<Route path={ROUTES.ACCOUNT_STAKING} element={<AccountStaking />} />
|
<Route path={ROUTES.ACCOUNT_STAKING} element={<AccountStaking />} />
|
||||||
|
<Route path={ROUTES.FDP} element={<FDP />} />
|
||||||
{isDesktop && <Route path={ROUTES.ACCOUNT_INVITATIONS} element={<GiftCards />} />}
|
{isDesktop && <Route path={ROUTES.ACCOUNT_INVITATIONS} element={<GiftCards />} />}
|
||||||
</Routes>
|
</Routes>
|
||||||
)
|
)
|
||||||
|
|||||||
+7
-2
@@ -1,3 +1,6 @@
|
|||||||
|
import { isSupportedImageType } from './image'
|
||||||
|
import { isSupportedVideoType } from './video'
|
||||||
|
|
||||||
const indexHtmls = ['index.html', 'index.htm']
|
const indexHtmls = ['index.html', 'index.htm']
|
||||||
|
|
||||||
interface DetectedIndex {
|
interface DetectedIndex {
|
||||||
@@ -83,12 +86,14 @@ export function getAssetNameFromFiles(files: FilePath[]): string {
|
|||||||
|
|
||||||
export function getMetadata(files: FilePath[]): Metadata {
|
export function getMetadata(files: FilePath[]): Metadata {
|
||||||
const size = files.reduce((total, item) => total + item.size, 0)
|
const size = files.reduce((total, item) => total + item.size, 0)
|
||||||
const isWebsite = Boolean(detectIndexHtml(files))
|
|
||||||
const name = getAssetNameFromFiles(files)
|
const name = getAssetNameFromFiles(files)
|
||||||
const type = files.length === 1 ? files[0].type : 'folder'
|
const type = files.length === 1 ? files[0].type : 'folder'
|
||||||
const count = files.length
|
const count = files.length
|
||||||
|
const isWebsite = Boolean(detectIndexHtml(files))
|
||||||
|
const isVideo = isSupportedVideoType(type)
|
||||||
|
const isImage = isSupportedImageType(type)
|
||||||
|
|
||||||
return { size, name, type, isWebsite, count }
|
return { size, name, type, isWebsite, count, isVideo, isImage }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPath(file: FilePath): string {
|
export function getPath(file: FilePath): string {
|
||||||
|
|||||||
+23
-10
@@ -25,6 +25,28 @@ export function getDimensions(imgWidth: number, imgHeight: number, maxWidth?: nu
|
|||||||
return { width: imgWidth / ratio, height: imgHeight / ratio }
|
return { width: imgWidth / ratio, height: imgHeight / ratio }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAllowedTypes(): string[] {
|
||||||
|
return [
|
||||||
|
'image/bmp',
|
||||||
|
'image/gif',
|
||||||
|
'image/vnd.microsoft.icon',
|
||||||
|
'image/jpeg',
|
||||||
|
'image/png',
|
||||||
|
'image/svg+xml',
|
||||||
|
'image/tiff',
|
||||||
|
'image/webp',
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the image type is supported
|
||||||
|
*
|
||||||
|
* @param type Image MIME type
|
||||||
|
*
|
||||||
|
* @returns True if the type is supported, false otherwise
|
||||||
|
*/
|
||||||
|
export const isSupportedImageType = (type: string): boolean => getAllowedTypes().includes(type)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resize image passed to fit in the bounding box defined with maxWidth and maxHeight.
|
* 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
|
* Note that one or both of the bounding box dimensions may be omitted
|
||||||
@@ -37,16 +59,7 @@ export function getDimensions(imgWidth: number, imgHeight: number, maxWidth?: nu
|
|||||||
*/
|
*/
|
||||||
export function resize(file: File, maxWidth?: number, maxHeight?: number): Promise<Blob> {
|
export function resize(file: File, maxWidth?: number, maxHeight?: number): Promise<Blob> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const allowedTypes = [
|
const allowedTypes = getAllowedTypes()
|
||||||
'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!')
|
if (!file.size || !file.type || !allowedTypes.includes(file.type)) return reject('File not supported!')
|
||||||
|
|
||||||
|
|||||||
+1
-2
@@ -31,8 +31,7 @@ export function makeBigNumber(value: BigNumber | bigint | number | string): BigN
|
|||||||
|
|
||||||
if (typeof value === 'bigint') return new BigNumber(value.toString())
|
if (typeof value === 'bigint') return new BigNumber(value.toString())
|
||||||
|
|
||||||
// FIXME: bee-js still returns some values as numbers and even outside of SAFE INTEGER bounds
|
if (typeof value === 'number') return new BigNumber(value)
|
||||||
if (typeof value === 'number' /* && Number.isSafeInteger(value)*/) return new BigNumber(value)
|
|
||||||
|
|
||||||
throw new TypeError(`Not a BigNumber or BigNumber convertible value. Type: ${typeof value} value: ${value}`)
|
throw new TypeError(`Not a BigNumber or BigNumber convertible value. Type: ${typeof value} value: ${value}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,14 +50,20 @@ export class ManifestJs {
|
|||||||
/**
|
/**
|
||||||
* Retrieves all paths with the associated hashes from a Swarm manifest
|
* Retrieves all paths with the associated hashes from a Swarm manifest
|
||||||
*/
|
*/
|
||||||
public async getHashes(hash: string): Promise<Record<string, string>> {
|
public async getHashes(hash: string, options?: { exclude: string[] }): Promise<Record<string, string>> {
|
||||||
const data = await this.bee.downloadData(hash)
|
const data = await this.bee.downloadData(hash)
|
||||||
const node = new MantarayNode()
|
const node = new MantarayNode()
|
||||||
node.deserialize(data)
|
node.deserialize(data)
|
||||||
await loadAllNodes(this.load.bind(this), node)
|
await loadAllNodes(this.load.bind(this), node)
|
||||||
const result = {}
|
const result: Record<string, string> = {}
|
||||||
this.extractHashes(result, node)
|
this.extractHashes(result, node)
|
||||||
|
|
||||||
|
if (options?.exclude) {
|
||||||
|
for (const path of options.exclude) {
|
||||||
|
delete result[path]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ export async function sendRequest(
|
|||||||
const authorization = localStorage.getItem('apiKey')
|
const authorization = localStorage.getItem('apiKey')
|
||||||
|
|
||||||
if (!authorization) {
|
if (!authorization) {
|
||||||
throw Error('API key not found in local storage')
|
throw Error('API key not found in local storage. Please reopen via Swarm Desktop > Open Web UI.')
|
||||||
}
|
}
|
||||||
const headers = {
|
const headers = {
|
||||||
authorization,
|
authorization,
|
||||||
|
|||||||
+7
-1
@@ -66,7 +66,13 @@ export async function sendNativeTransaction(
|
|||||||
): Promise<TransferResponse> {
|
): Promise<TransferResponse> {
|
||||||
const signer = await makeReadySigner(privateKey, jsonRpcProvider)
|
const signer = await makeReadySigner(privateKey, jsonRpcProvider)
|
||||||
const gasPrice = externalGasPrice ?? (await signer.getGasPrice())
|
const gasPrice = externalGasPrice ?? (await signer.getGasPrice())
|
||||||
const transaction = await signer.sendTransaction({ to, value, gasPrice })
|
const transaction = await signer.sendTransaction({
|
||||||
|
to,
|
||||||
|
value: BN.from(value),
|
||||||
|
gasPrice,
|
||||||
|
gasLimit: BN.from(21000),
|
||||||
|
type: 0,
|
||||||
|
})
|
||||||
const receipt = await transaction.wait(1)
|
const receipt = await transaction.wait(1)
|
||||||
|
|
||||||
return { transaction, receipt }
|
return { transaction, receipt }
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export function isSupportedVideoType(type: string) {
|
||||||
|
const video = document.createElement('video')
|
||||||
|
|
||||||
|
const result = video.canPlayType(type)
|
||||||
|
const isDefinitelySupported = result && result !== 'maybe'
|
||||||
|
|
||||||
|
return Boolean(isDefinitelySupported)
|
||||||
|
}
|
||||||
@@ -18,7 +18,9 @@ async function getData(url) {
|
|||||||
function processData(data) {
|
function processData(data) {
|
||||||
const db = new Map()
|
const db = new Map()
|
||||||
data.nodes.forEach(node => {
|
data.nodes.forEach(node => {
|
||||||
db.set(node.overlay, { lat: node.location.latitude, lng: node.location.longitude })
|
if (node.location) {
|
||||||
|
db.set(node.overlay, { lat: node.location.latitude, lng: node.location.longitude })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return Object.fromEntries([...db.entries()].sort())
|
return Object.fromEntries([...db.entries()].sort())
|
||||||
|
|||||||
Reference in New Issue
Block a user