feat: add identity and feed management (#272)
* feat(wip): add basic feed operations * ci: bump checks * ci: bump checks * feat: rework stamps and add feed functionalities * refactor: polish and fixes * feat(wip): add formulas * feat: show bzz.link for websites * feat: add stamp empty states and formatBzz * feat: add feed download * chore: update manifest-js version * feat: dev mode support with bee-js 3.1.0 (#273) * feat: dev mode support with bee-js 3.1.0 * fix: added missing package-lock.json file * build: remove PR preview * style: work on design * feat: add TroubleshootConnectionCard * build: remove depcheck Co-authored-by: Attila Gazso <agazso@gmail.com>
This commit is contained in:
@@ -58,9 +58,6 @@ jobs:
|
|||||||
- name: Types build
|
- name: Types build
|
||||||
run: npm run compile:types
|
run: npm run compile:types
|
||||||
|
|
||||||
- name: Dependency check
|
|
||||||
run: npm run depcheck
|
|
||||||
|
|
||||||
- name: Update supported Bee action
|
- name: Update supported Bee action
|
||||||
uses: ethersphere/update-supported-bee-action@v1
|
uses: ethersphere/update-supported-bee-action@v1
|
||||||
if: github.ref == 'refs/heads/master'
|
if: github.ref == 'refs/heads/master'
|
||||||
@@ -69,16 +66,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Build Component
|
- name: Build Component
|
||||||
run: npm run build:component
|
run: npm run build:component
|
||||||
|
|
||||||
- name: Create preview
|
|
||||||
uses: ethersphere/beeload-action@v1
|
|
||||||
with:
|
|
||||||
preview: 'true'
|
|
||||||
|
|
||||||
- name: Upload to testnet
|
|
||||||
uses: ethersphere/beeload-action@v1
|
|
||||||
with:
|
|
||||||
bee-url: https://api.gateway.testnet.ethswarm.org
|
|
||||||
|
|||||||
Generated
+2352
-6016
File diff suppressed because it is too large
Load Diff
+4
-2
@@ -26,13 +26,15 @@
|
|||||||
"url": "https://github.com/ethersphere/bee-dashboard.git"
|
"url": "https://github.com/ethersphere/bee-dashboard.git"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ethersphere/bee-js": "3.0.0",
|
"@ethersphere/bee-js": "3.1.0",
|
||||||
"@ethersphere/manifest-js": "^1.0.0",
|
"@ethersphere/manifest-js": "1.1.0",
|
||||||
|
"@ethersphere/swarm-cid": "^0.1.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",
|
||||||
"axios": "0.24.0",
|
"axios": "0.24.0",
|
||||||
"bignumber.js": "9.0.1",
|
"bignumber.js": "9.0.1",
|
||||||
|
"ethereumjs-wallet": "^1.0.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",
|
||||||
|
|||||||
+15
-12
@@ -6,6 +6,7 @@ import { BrowserRouter as Router } from 'react-router-dom'
|
|||||||
import './App.css'
|
import './App.css'
|
||||||
import Dashboard from './layout/Dashboard'
|
import Dashboard from './layout/Dashboard'
|
||||||
import { Provider as BeeProvider } from './providers/Bee'
|
import { Provider as BeeProvider } from './providers/Bee'
|
||||||
|
import { Provider as FeedsProvider } from './providers/Feeds'
|
||||||
import { Provider as FileProvider } from './providers/File'
|
import { Provider as FileProvider } from './providers/File'
|
||||||
import { Provider as PlatformProvider } from './providers/Platform'
|
import { Provider as PlatformProvider } from './providers/Platform'
|
||||||
import { Provider as SettingsProvider } from './providers/Settings'
|
import { Provider as SettingsProvider } from './providers/Settings'
|
||||||
@@ -26,18 +27,20 @@ const App = ({ beeApiUrl, beeDebugApiUrl, lockedApiSettings }: Props): ReactElem
|
|||||||
<BeeProvider>
|
<BeeProvider>
|
||||||
<StampsProvider>
|
<StampsProvider>
|
||||||
<FileProvider>
|
<FileProvider>
|
||||||
<PlatformProvider>
|
<FeedsProvider>
|
||||||
<SnackbarProvider>
|
<PlatformProvider>
|
||||||
<Router>
|
<SnackbarProvider>
|
||||||
<>
|
<Router>
|
||||||
<CssBaseline />
|
<>
|
||||||
<Dashboard>
|
<CssBaseline />
|
||||||
<BaseRouter />
|
<Dashboard>
|
||||||
</Dashboard>
|
<BaseRouter />
|
||||||
</>
|
</Dashboard>
|
||||||
</Router>
|
</>
|
||||||
</SnackbarProvider>
|
</Router>
|
||||||
</PlatformProvider>
|
</SnackbarProvider>
|
||||||
|
</PlatformProvider>
|
||||||
|
</FeedsProvider>
|
||||||
</FileProvider>
|
</FileProvider>
|
||||||
</StampsProvider>
|
</StampsProvider>
|
||||||
</BeeProvider>
|
</BeeProvider>
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { createStyles, makeStyles, Theme } from '@material-ui/core'
|
||||||
|
import { Close } from '@material-ui/icons'
|
||||||
|
import { ReactElement } from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
wrapper: {
|
||||||
|
padding: theme.spacing(1),
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export function CloseButton({ onClose }: Props): ReactElement {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.wrapper} onClick={onClose}>
|
||||||
|
<Close />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { createStyles, makeStyles, Theme } from '@material-ui/core'
|
||||||
|
import { ReactElement } from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: string
|
||||||
|
prettify?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
wrapper: {
|
||||||
|
overflow: 'scroll',
|
||||||
|
background: '#ffffff',
|
||||||
|
},
|
||||||
|
pre: {
|
||||||
|
maxHeight: '6em',
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
function prettifyString(string: string): string {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(JSON.parse(string), null, 4)
|
||||||
|
} catch {
|
||||||
|
return string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Code({ children, prettify }: Props): ReactElement {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.wrapper}>
|
||||||
|
<pre className={classes.pre}>{prettify ? prettifyString(children) : children}</pre>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { createStyles, makeStyles, Typography } from '@material-ui/core'
|
||||||
|
import { ReactElement } from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: (string | ReactElement)[] | (string | ReactElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles(() =>
|
||||||
|
createStyles({
|
||||||
|
text: {
|
||||||
|
color: '#606060',
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export function DocumentationText({ children }: Props): ReactElement {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
return <Typography className={classes.text}>{children}</Typography>
|
||||||
|
}
|
||||||
@@ -4,8 +4,12 @@ import { ReactElement, ReactNode } from 'react'
|
|||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
createStyles({
|
createStyles({
|
||||||
|
wrapper: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
},
|
||||||
action: {
|
action: {
|
||||||
marginTop: theme.spacing(0.75),
|
|
||||||
marginBottom: theme.spacing(1),
|
marginBottom: theme.spacing(1),
|
||||||
marginRight: theme.spacing(1),
|
marginRight: theme.spacing(1),
|
||||||
},
|
},
|
||||||
@@ -21,16 +25,16 @@ export default function ExpandableListItemActions({ children }: Props): ReactEle
|
|||||||
|
|
||||||
if (Array.isArray(children)) {
|
if (Array.isArray(children)) {
|
||||||
return (
|
return (
|
||||||
<Grid container direction="row">
|
<div className={classes.wrapper}>
|
||||||
{children
|
{children
|
||||||
// Exclude falsy values to allow conditional rendering
|
// Exclude falsy values to allow conditional rendering
|
||||||
.filter(x => x)
|
.filter(x => x)
|
||||||
.map((a, i) => (
|
.map((a, i) => (
|
||||||
<Grid key={i} className={classes.action}>
|
<div key={i} className={classes.action}>
|
||||||
{a}
|
{a}
|
||||||
</Grid>
|
</div>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Button, Grid, IconButton, InputBase, ListItem, Typography } from '@material-ui/core'
|
import { Grid, IconButton, InputBase, ListItem, Typography } from '@material-ui/core'
|
||||||
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 { Check, Edit, Minus, RotateCcw } from 'react-feather'
|
import { Edit, Minus, Search, X } from 'react-feather'
|
||||||
import ExpandableListItemActions from './ExpandableListItemActions'
|
import ExpandableListItemActions from './ExpandableListItemActions'
|
||||||
import ExpandableListItemNote from './ExpandableListItemNote'
|
import ExpandableListItemNote from './ExpandableListItemNote'
|
||||||
|
import { SwarmButton } from './SwarmButton'
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
createStyles({
|
createStyles({
|
||||||
@@ -52,6 +53,7 @@ interface Props {
|
|||||||
expandedOnly?: boolean
|
expandedOnly?: boolean
|
||||||
confirmLabel?: string
|
confirmLabel?: string
|
||||||
confirmLabelDisabled?: boolean
|
confirmLabelDisabled?: boolean
|
||||||
|
loading?: boolean
|
||||||
onChange?: (value: string) => void
|
onChange?: (value: string) => void
|
||||||
onConfirm: (value: string) => void
|
onConfirm: (value: string) => void
|
||||||
mapperFn?: (value: string) => string
|
mapperFn?: (value: string) => string
|
||||||
@@ -68,6 +70,7 @@ export default function ExpandableListItemKey({
|
|||||||
expandedOnly,
|
expandedOnly,
|
||||||
helperText,
|
helperText,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
loading,
|
||||||
mapperFn,
|
mapperFn,
|
||||||
locked,
|
locked,
|
||||||
}: Props): ReactElement | null {
|
}: Props): ReactElement | null {
|
||||||
@@ -126,26 +129,27 @@ export default function ExpandableListItemKey({
|
|||||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||||
{helperText && <ExpandableListItemNote>{helperText}</ExpandableListItemNote>}
|
{helperText && <ExpandableListItemNote>{helperText}</ExpandableListItemNote>}
|
||||||
<ExpandableListItemActions>
|
<ExpandableListItemActions>
|
||||||
<Button
|
<SwarmButton
|
||||||
variant="contained"
|
|
||||||
disabled={
|
disabled={
|
||||||
|
loading ||
|
||||||
inputValue === value ||
|
inputValue === value ||
|
||||||
Boolean(confirmLabelDisabled) || // Disable if external validation is provided
|
Boolean(confirmLabelDisabled) || // Disable if external validation is provided
|
||||||
(inputValue === '' && value === undefined) // Disable if no initial value was not provided and the field is empty. The undefined check is improtant so that it is possible to submit with empty input in other cases
|
(inputValue === '' && value === undefined) // Disable if no initial value was not provided and the field is empty. The undefined check is improtant so that it is possible to submit with empty input in other cases
|
||||||
}
|
}
|
||||||
startIcon={<Check size="1rem" />}
|
loading={loading}
|
||||||
|
iconType={Search}
|
||||||
onClick={() => onConfirm(inputValue)}
|
onClick={() => onConfirm(inputValue)}
|
||||||
>
|
>
|
||||||
{confirmLabel || 'Save'}
|
{confirmLabel || 'Save'}
|
||||||
</Button>
|
</SwarmButton>
|
||||||
<Button
|
<SwarmButton
|
||||||
variant="contained"
|
disabled={loading || inputValue === value || inputValue === ''}
|
||||||
disabled={inputValue === value || inputValue === ''}
|
iconType={X}
|
||||||
startIcon={<RotateCcw size="1rem" />}
|
|
||||||
onClick={() => setInputValue(value || '')}
|
onClick={() => setInputValue(value || '')}
|
||||||
|
cancel
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</SwarmButton>
|
||||||
</ExpandableListItemActions>
|
</ExpandableListItemActions>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { createStyles, Grid, makeStyles, Typography } from '@material-ui/core'
|
||||||
|
import { ReactElement } from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
steps: string[]
|
||||||
|
index: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles(() =>
|
||||||
|
createStyles({
|
||||||
|
wrapper: {
|
||||||
|
height: '52px',
|
||||||
|
display: 'flex',
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
todo: {
|
||||||
|
background: '#f7f7f7',
|
||||||
|
color: '#c9c9c9',
|
||||||
|
},
|
||||||
|
inProgress: {
|
||||||
|
background: '#ffffff',
|
||||||
|
color: '#242424',
|
||||||
|
height: '52px',
|
||||||
|
},
|
||||||
|
done: {
|
||||||
|
background: '#f7f7f7',
|
||||||
|
color: '#606060',
|
||||||
|
height: '52px',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export function ProgressIndicator({ steps, index }: Props): ReactElement {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
function pickClass(i: number): string {
|
||||||
|
if (i === index) {
|
||||||
|
return classes.inProgress
|
||||||
|
}
|
||||||
|
|
||||||
|
return i < index ? classes.done : classes.todo
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid container justifyContent="space-between">
|
||||||
|
{steps.map((x, i) => (
|
||||||
|
<div key={i} className={`${classes.wrapper} ${pickClass(i)}`}>
|
||||||
|
<Typography>{x}</Typography>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import { Divider, Drawer, Grid, Link as MUILink, List } from '@material-ui/core'
|
|||||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
||||||
import { OpenInNewSharp } from '@material-ui/icons'
|
import { OpenInNewSharp } from '@material-ui/icons'
|
||||||
import type { ReactElement } from 'react'
|
import type { ReactElement } from 'react'
|
||||||
import { BookOpen, DollarSign, FileText, Home, Layers, Settings } from 'react-feather'
|
import { Bookmark, BookOpen, DollarSign, FileText, Home, Layers, Settings } from 'react-feather'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import Logo from '../assets/logo.svg'
|
import Logo from '../assets/logo.svg'
|
||||||
import { config } from '../config'
|
import { config } from '../config'
|
||||||
@@ -21,6 +21,11 @@ const navBarItems = [
|
|||||||
path: ROUTES.UPLOAD,
|
path: ROUTES.UPLOAD,
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Feeds',
|
||||||
|
path: ROUTES.FEEDS,
|
||||||
|
icon: Bookmark,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Stamps',
|
label: 'Stamps',
|
||||||
path: ROUTES.STAMPS,
|
path: ROUTES.STAMPS,
|
||||||
|
|||||||
@@ -9,13 +9,16 @@ interface Props {
|
|||||||
className?: string
|
className?: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
|
cancel?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles(() =>
|
const useStyles = makeStyles(() =>
|
||||||
createStyles({
|
createStyles({
|
||||||
button: {
|
button: {
|
||||||
|
height: '52px',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
|
color: '#242424',
|
||||||
'&:hover, &:focus': {
|
'&:hover, &:focus': {
|
||||||
'& svg': {
|
'& svg': {
|
||||||
stroke: '#fff',
|
stroke: '#fff',
|
||||||
@@ -23,6 +26,10 @@ const useStyles = makeStyles(() =>
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
cancelButton: {
|
||||||
|
background: '#f7f7f7',
|
||||||
|
color: '#606060',
|
||||||
|
},
|
||||||
spinnerWrapper: {
|
spinnerWrapper: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: '50%',
|
left: '50%',
|
||||||
@@ -34,19 +41,37 @@ const useStyles = makeStyles(() =>
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export function SwarmButton({ children, onClick, iconType, className, disabled, loading }: Props): ReactElement {
|
export function SwarmButton({
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
iconType,
|
||||||
|
className,
|
||||||
|
disabled,
|
||||||
|
loading,
|
||||||
|
cancel,
|
||||||
|
}: Props): ReactElement {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
|
function getIconColor() {
|
||||||
|
if (loading || disabled) {
|
||||||
|
return 'rgba(0, 0, 0, 0.26)'
|
||||||
|
}
|
||||||
|
|
||||||
|
return cancel ? '#606060' : '#dd7700'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getButtonClassName() {
|
||||||
|
return [className, classes.button, cancel && classes.cancelButton].filter(x => x).join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
const icon = React.createElement(iconType, {
|
const icon = React.createElement(iconType, {
|
||||||
size: '1.25rem',
|
size: '1.25rem',
|
||||||
color: disabled ? 'rgba(0, 0, 0, 0.26)' : '#dd7700',
|
color: getIconColor(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const classNames = className ? [className, classes.button].join(' ') : classes.button
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className={classNames}
|
className={getButtonClassName()}
|
||||||
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
|
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
onClick()
|
onClick()
|
||||||
event.currentTarget.blur()
|
event.currentTarget.blur()
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Box, Dialog, Grid } from '@material-ui/core'
|
||||||
|
import { ReactElement } from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactElement | ReactElement[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SwarmDialog({ children }: Props): ReactElement {
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={true}
|
||||||
|
PaperProps={{
|
||||||
|
style: { borderRadius: 0, background: '#efefef' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box p={4} sx={{ maxWidth: '100%', width: '650px' }}>
|
||||||
|
<Grid container direction="column">
|
||||||
|
{children}
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { createStyles, FormHelperText, makeStyles, MenuItem, Select as SimpleSelect, Theme } from '@material-ui/core'
|
||||||
|
import { Field } from 'formik'
|
||||||
|
import { Select } from 'formik-material-ui'
|
||||||
|
import { ReactElement } from 'react'
|
||||||
|
|
||||||
|
export type SelectEvent = React.ChangeEvent<{
|
||||||
|
name?: string | undefined
|
||||||
|
value: unknown
|
||||||
|
}>
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
label?: string
|
||||||
|
name?: string
|
||||||
|
options: { value: string; label: string }[]
|
||||||
|
onChange?: (event: SelectEvent) => void
|
||||||
|
formik?: boolean
|
||||||
|
defaultValue?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
select: {
|
||||||
|
borderRadius: 0,
|
||||||
|
background: theme.palette.background.paper,
|
||||||
|
'& fieldset': {
|
||||||
|
border: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
option: {
|
||||||
|
height: '52px',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export function SwarmSelect({ defaultValue, formik, name, options, onChange, label }: Props): ReactElement {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
if (formik) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{label && <FormHelperText>{label}</FormHelperText>}
|
||||||
|
<Field
|
||||||
|
required
|
||||||
|
component={Select}
|
||||||
|
name={name}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
defaultValue={defaultValue || ''}
|
||||||
|
className={classes.select}
|
||||||
|
placeholder={label}
|
||||||
|
>
|
||||||
|
{options.map((x, i) => (
|
||||||
|
<MenuItem key={i} value={x.value} className={classes.option}>
|
||||||
|
{x.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Field>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{label && <FormHelperText>{label}</FormHelperText>}
|
||||||
|
<SimpleSelect
|
||||||
|
required
|
||||||
|
name={name}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
className={classes.select}
|
||||||
|
defaultValue={defaultValue || ''}
|
||||||
|
onChange={onChange}
|
||||||
|
placeholder={label}
|
||||||
|
>
|
||||||
|
{options.map((x, i) => (
|
||||||
|
<MenuItem key={i} value={x.value} className={classes.option}>
|
||||||
|
{x.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</SimpleSelect>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { createStyles, makeStyles, TextField as SimpleTextField, Theme } from '@material-ui/core'
|
||||||
|
import { Field } from 'formik'
|
||||||
|
import { TextField } from 'formik-material-ui'
|
||||||
|
import { ChangeEvent, ReactElement } from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
name: string
|
||||||
|
label: string
|
||||||
|
password?: boolean
|
||||||
|
formik?: boolean
|
||||||
|
optional?: boolean
|
||||||
|
onChange?: (event: ChangeEvent<HTMLTextAreaElement>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
field: {
|
||||||
|
background: theme.palette.background.paper,
|
||||||
|
height: '52px',
|
||||||
|
'& fieldset': {
|
||||||
|
border: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export function SwarmTextInput({ name, label, password, optional, formik, onChange }: Props): ReactElement {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
if (formik) {
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
component={TextField}
|
||||||
|
type={password ? 'password' : undefined}
|
||||||
|
required={!optional}
|
||||||
|
name={name}
|
||||||
|
label={label}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
className={classes.field}
|
||||||
|
defaultValue=""
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SimpleTextField
|
||||||
|
type={password ? 'password' : undefined}
|
||||||
|
required
|
||||||
|
label={label}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
className={classes.field}
|
||||||
|
defaultValue=""
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { createStyles, Grid, makeStyles, Typography } from '@material-ui/core'
|
||||||
|
import { ReactElement } from 'react'
|
||||||
|
import { CloseButton } from './CloseButton'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: string
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles(() =>
|
||||||
|
createStyles({
|
||||||
|
text: {
|
||||||
|
color: '#606060',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export function TitleWithClose({ children, onClose }: Props): ReactElement {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid container justifyContent="space-between" alignItems="center">
|
||||||
|
<span> </span>
|
||||||
|
<Typography className={classes.text} align="center">
|
||||||
|
{children}
|
||||||
|
</Typography>
|
||||||
|
<CloseButton onClose={onClose} />
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { Box, Grid, Typography } from '@material-ui/core'
|
||||||
|
import { Form, Formik } from 'formik'
|
||||||
|
import { useSnackbar } from 'notistack'
|
||||||
|
import { ReactElement, useContext, useState } from 'react'
|
||||||
|
import { Check, X } from 'react-feather'
|
||||||
|
import { useHistory } from 'react-router'
|
||||||
|
import { DocumentationText } from '../../components/DocumentationText'
|
||||||
|
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
|
||||||
|
import ExpandableListItemKey from '../../components/ExpandableListItemKey'
|
||||||
|
import { HistoryHeader } from '../../components/HistoryHeader'
|
||||||
|
import { SwarmButton } from '../../components/SwarmButton'
|
||||||
|
import { SwarmSelect } from '../../components/SwarmSelect'
|
||||||
|
import { SwarmTextInput } from '../../components/SwarmTextInput'
|
||||||
|
import { Context as FeedsContext, IdentityType } from '../../providers/Feeds'
|
||||||
|
import { Context as SettingsContext } from '../../providers/Settings'
|
||||||
|
import { ROUTES } from '../../routes'
|
||||||
|
import { convertWalletToIdentity, generateWallet, persistIdentity } from '../../utils/identity'
|
||||||
|
|
||||||
|
interface FormValues {
|
||||||
|
identityName?: string
|
||||||
|
type?: IdentityType
|
||||||
|
password?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialValues: FormValues = {
|
||||||
|
identityName: '',
|
||||||
|
type: 'PRIVATE_KEY',
|
||||||
|
password: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CreateNewFeed(): ReactElement {
|
||||||
|
const { beeApi, beeDebugApi } = useContext(SettingsContext)
|
||||||
|
const { identities, setIdentities } = useContext(FeedsContext)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const { enqueueSnackbar } = useSnackbar()
|
||||||
|
|
||||||
|
const history = useHistory()
|
||||||
|
|
||||||
|
async function onSubmit(values: FormValues) {
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
if (!beeApi) {
|
||||||
|
enqueueSnackbar(<span>Bee API unavailabe</span>, { variant: 'error' })
|
||||||
|
setLoading(false)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const wallet = generateWallet()
|
||||||
|
const stamps = await beeDebugApi?.getAllPostageBatch()
|
||||||
|
|
||||||
|
if (!stamps || !stamps.length) {
|
||||||
|
enqueueSnackbar(<span>No stamp available</span>, { variant: 'error' })
|
||||||
|
setLoading(false)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!values.identityName || !values.type) {
|
||||||
|
enqueueSnackbar(<span>Form is unfinished</span>, { variant: 'error' })
|
||||||
|
setLoading(false)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const identity = await convertWalletToIdentity(wallet, values.type, values.identityName, values.password)
|
||||||
|
persistIdentity(identities, identity)
|
||||||
|
setIdentities(identities)
|
||||||
|
history.push(ROUTES.FEEDS)
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
history.goBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<HistoryHeader>Create new feed</HistoryHeader>
|
||||||
|
<Box mb={4}>
|
||||||
|
<DocumentationText>
|
||||||
|
To create a feed you will need to create an identity. Please refer to the{' '}
|
||||||
|
<a
|
||||||
|
href="https://docs.ethswarm.org/api/#tag/Feed/paths/~1feeds~1{owner}~1{topic}/post"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
official Bee documentation
|
||||||
|
</a>{' '}
|
||||||
|
to understand how feeds work.
|
||||||
|
</DocumentationText>
|
||||||
|
</Box>
|
||||||
|
<Formik initialValues={initialValues} onSubmit={onSubmit}>
|
||||||
|
{({ submitForm, values }) => (
|
||||||
|
<Form>
|
||||||
|
<Box mb={0.25}>
|
||||||
|
<SwarmTextInput name="identityName" label="Identity name" formik />
|
||||||
|
</Box>
|
||||||
|
<Box mb={0.25}>
|
||||||
|
<SwarmSelect
|
||||||
|
formik
|
||||||
|
name="type"
|
||||||
|
options={[
|
||||||
|
{ label: 'Keypair Only', value: 'PRIVATE_KEY' },
|
||||||
|
{ label: 'Password Protected', value: 'V3' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
{values.type === 'V3' && <SwarmTextInput name="password" label="Password" password formik />}
|
||||||
|
<Box mt={2}>
|
||||||
|
<ExpandableListItemKey label="Topic" value={'00'.repeat(32)} />
|
||||||
|
</Box>
|
||||||
|
<Box mt={2} sx={{ bgcolor: '#fcf2e8' }} p={2}>
|
||||||
|
<Grid container justifyContent="space-between">
|
||||||
|
<Typography>Feeds name</Typography>
|
||||||
|
<Typography>{values.identityName} Website</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
<Box mt={1.25}>
|
||||||
|
<ExpandableListItemActions>
|
||||||
|
<SwarmButton onClick={submitForm} iconType={Check} disabled={loading} loading={loading}>
|
||||||
|
Create Feed
|
||||||
|
</SwarmButton>
|
||||||
|
<SwarmButton onClick={cancel} iconType={X} disabled={loading} cancel>
|
||||||
|
Cancel
|
||||||
|
</SwarmButton>
|
||||||
|
</ExpandableListItemActions>
|
||||||
|
</Box>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Box, Typography } from '@material-ui/core'
|
||||||
|
import { ReactElement } from 'react'
|
||||||
|
import { Trash, X } from 'react-feather'
|
||||||
|
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
|
||||||
|
import { SwarmButton } from '../../components/SwarmButton'
|
||||||
|
import { SwarmDialog } from '../../components/SwarmDialog'
|
||||||
|
import { TitleWithClose } from '../../components/TitleWithClose'
|
||||||
|
import { Identity } from '../../providers/Feeds'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
identity: Identity
|
||||||
|
onConfirm: (identity: Identity) => void
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeleteFeedDialog({ identity, onConfirm, onClose }: Props): ReactElement {
|
||||||
|
return (
|
||||||
|
<SwarmDialog>
|
||||||
|
<Box mb={4}>
|
||||||
|
<TitleWithClose onClose={onClose}>Delete</TitleWithClose>
|
||||||
|
</Box>
|
||||||
|
<Box mb={2}>
|
||||||
|
<Typography align="center">{`You are about to delete feed ${identity.name} Website. It is strongly advised to export this feed first.`}</Typography>
|
||||||
|
</Box>
|
||||||
|
<ExpandableListItemActions>
|
||||||
|
<SwarmButton iconType={Trash} onClick={() => onConfirm(identity)}>
|
||||||
|
Delete
|
||||||
|
</SwarmButton>
|
||||||
|
<SwarmButton iconType={X} onClick={onClose} cancel>
|
||||||
|
Cancel
|
||||||
|
</SwarmButton>
|
||||||
|
</ExpandableListItemActions>
|
||||||
|
</SwarmDialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { Box, createStyles, makeStyles, Typography } from '@material-ui/core'
|
||||||
|
import { saveAs } from 'file-saver'
|
||||||
|
import { useSnackbar } from 'notistack'
|
||||||
|
import { ReactElement } from 'react'
|
||||||
|
import { Clipboard, Download } from 'react-feather'
|
||||||
|
import { Code } from '../../components/Code'
|
||||||
|
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
|
||||||
|
import { SwarmButton } from '../../components/SwarmButton'
|
||||||
|
import { SwarmDialog } from '../../components/SwarmDialog'
|
||||||
|
import { TitleWithClose } from '../../components/TitleWithClose'
|
||||||
|
import { Identity } from '../../providers/Feeds'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
identity: Identity
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles(() =>
|
||||||
|
createStyles({
|
||||||
|
wrapper: {
|
||||||
|
maxWidth: '100%',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export function ExportFeedDialog({ identity, onClose }: Props): ReactElement {
|
||||||
|
const { enqueueSnackbar } = useSnackbar()
|
||||||
|
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
function onDownload() {
|
||||||
|
saveAs(
|
||||||
|
new Blob([identity.identity], {
|
||||||
|
type: 'application/json',
|
||||||
|
}),
|
||||||
|
identity.name + '.json',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExportText() {
|
||||||
|
return identity.type === 'V3' ? 'JSON file' : 'the private key string'
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCopy() {
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(identity.identity)
|
||||||
|
.then(() => enqueueSnackbar('Copied to Clipboard', { variant: 'success' }))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SwarmDialog>
|
||||||
|
<Box mb={4}>
|
||||||
|
<TitleWithClose onClose={onClose}>Export</TitleWithClose>
|
||||||
|
</Box>
|
||||||
|
<Box mb={2}>
|
||||||
|
<Typography align="center">{`We exported the identity associated with this feed as ${getExportText()}.`}</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box mb={4} className={classes.wrapper}>
|
||||||
|
<Code prettify>{identity.identity}</Code>
|
||||||
|
</Box>
|
||||||
|
<ExpandableListItemActions>
|
||||||
|
<SwarmButton iconType={Download} onClick={onDownload}>
|
||||||
|
Download JSON File
|
||||||
|
</SwarmButton>
|
||||||
|
<SwarmButton iconType={Clipboard} onClick={onCopy}>
|
||||||
|
Copy To Clipboard
|
||||||
|
</SwarmButton>
|
||||||
|
</ExpandableListItemActions>
|
||||||
|
</SwarmDialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { Box, Typography } from '@material-ui/core'
|
||||||
|
import { ReactElement, useState } from 'react'
|
||||||
|
import { Check, X } from 'react-feather'
|
||||||
|
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
|
||||||
|
import { SwarmButton } from '../../components/SwarmButton'
|
||||||
|
import { SwarmDialog } from '../../components/SwarmDialog'
|
||||||
|
import { SwarmTextInput } from '../../components/SwarmTextInput'
|
||||||
|
import { TitleWithClose } from '../../components/TitleWithClose'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
feedName: string
|
||||||
|
onProceed: (password: string) => void
|
||||||
|
onCancel: () => void
|
||||||
|
loading: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FeedPasswordDialog({ feedName, onProceed, onCancel, loading }: Props): ReactElement {
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
|
||||||
|
function onProceedClick() {
|
||||||
|
return onProceed(password)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SwarmDialog>
|
||||||
|
<Box mb={4}>
|
||||||
|
<TitleWithClose onClose={onCancel}>Update Feed</TitleWithClose>
|
||||||
|
</Box>
|
||||||
|
<Box mb={2}>
|
||||||
|
<Typography>Please enter the password for “{feedName}”:</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box mb={4}>
|
||||||
|
<SwarmTextInput
|
||||||
|
label="Password"
|
||||||
|
name="password"
|
||||||
|
onChange={event => {
|
||||||
|
setPassword(event.target.value)
|
||||||
|
}}
|
||||||
|
password
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<ExpandableListItemActions>
|
||||||
|
<SwarmButton iconType={Check} onClick={onProceedClick} disabled={loading} loading={loading}>
|
||||||
|
Proceed
|
||||||
|
</SwarmButton>
|
||||||
|
<SwarmButton iconType={X} onClick={onCancel} cancel disabled={loading}>
|
||||||
|
Cancel
|
||||||
|
</SwarmButton>
|
||||||
|
</ExpandableListItemActions>
|
||||||
|
</SwarmDialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import * as swarmCid from '@ethersphere/swarm-cid'
|
||||||
|
import { Box } from '@material-ui/core'
|
||||||
|
import { ReactElement, useContext, useEffect, useState } from 'react'
|
||||||
|
import { X } from 'react-feather'
|
||||||
|
import { RouteComponentProps, useHistory } from 'react-router-dom'
|
||||||
|
import { DocumentationText } from '../../components/DocumentationText'
|
||||||
|
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
|
||||||
|
import ExpandableListItemKey from '../../components/ExpandableListItemKey'
|
||||||
|
import ExpandableListItemLink from '../../components/ExpandableListItemLink'
|
||||||
|
import { HistoryHeader } from '../../components/HistoryHeader'
|
||||||
|
import { SwarmButton } from '../../components/SwarmButton'
|
||||||
|
import { Context as BeeContext } from '../../providers/Bee'
|
||||||
|
import { Context as IdentityContext } from '../../providers/Feeds'
|
||||||
|
import { Context as SettingsContext } from '../../providers/Settings'
|
||||||
|
import { ROUTES } from '../../routes'
|
||||||
|
import { UploadArea } from '../files/UploadArea'
|
||||||
|
|
||||||
|
interface MatchParams {
|
||||||
|
uuid: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FeedSubpage(props: RouteComponentProps<MatchParams>): ReactElement {
|
||||||
|
const { identities } = useContext(IdentityContext)
|
||||||
|
const { beeApi } = useContext(SettingsContext)
|
||||||
|
const { status } = useContext(BeeContext)
|
||||||
|
|
||||||
|
const history = useHistory()
|
||||||
|
|
||||||
|
const [available, setAvailable] = useState(false)
|
||||||
|
|
||||||
|
const uuid = props.match.params.uuid
|
||||||
|
const identity = identities.find(x => x.uuid === uuid)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!identity || !identity.feedHash) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
beeApi?.downloadData(identity.feedHash).then(() => setAvailable(true))
|
||||||
|
} catch {
|
||||||
|
setAvailable(false)
|
||||||
|
}
|
||||||
|
}, [beeApi, uuid, identity])
|
||||||
|
|
||||||
|
if (!identity || !status.all) {
|
||||||
|
history.replace(ROUTES.FEEDS)
|
||||||
|
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
history.push(ROUTES.FEEDS)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<HistoryHeader>{`${identity.name} Website`}</HistoryHeader>
|
||||||
|
<UploadArea showHelp={false} uploadOrigin={{ origin: 'FEED', uuid }} />
|
||||||
|
{available && identity.feedHash ? (
|
||||||
|
<>
|
||||||
|
<Box mb={0.25}>
|
||||||
|
<ExpandableListItemKey label="Feed hash" value={identity.feedHash} />
|
||||||
|
</Box>
|
||||||
|
<Box mb={4}>
|
||||||
|
<ExpandableListItemLink
|
||||||
|
label="BZZ Link"
|
||||||
|
value={`https://${swarmCid.encodeFeedReference(identity.feedHash)}.bzz.link`}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Box mb={4}>
|
||||||
|
<DocumentationText>
|
||||||
|
This feed is curently not pointing anywhere, you can update the feed to fix this. Please refer to the{' '}
|
||||||
|
<a
|
||||||
|
href="https://docs.ethswarm.org/api/#tag/Feed/paths/~1feeds~1{owner}~1{topic}/post"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
official Bee documentation
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</DocumentationText>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<ExpandableListItemActions>
|
||||||
|
<SwarmButton iconType={X} onClick={onClose} cancel>
|
||||||
|
Close
|
||||||
|
</SwarmButton>
|
||||||
|
</ExpandableListItemActions>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { Box, createStyles, makeStyles, TextareaAutosize, Theme } from '@material-ui/core'
|
||||||
|
import { useSnackbar } from 'notistack'
|
||||||
|
import React, { ReactElement, useContext, useRef, useState } from 'react'
|
||||||
|
import { Check, Upload } from 'react-feather'
|
||||||
|
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
|
||||||
|
import { SwarmButton } from '../../components/SwarmButton'
|
||||||
|
import { SwarmDialog } from '../../components/SwarmDialog'
|
||||||
|
import { SwarmTextInput } from '../../components/SwarmTextInput'
|
||||||
|
import { TitleWithClose } from '../../components/TitleWithClose'
|
||||||
|
import { Context, Identity } from '../../providers/Feeds'
|
||||||
|
import { importIdentity, persistIdentity } from '../../utils/identity'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
textarea: {
|
||||||
|
width: '100%',
|
||||||
|
border: 0,
|
||||||
|
padding: theme.spacing(1),
|
||||||
|
},
|
||||||
|
displayNone: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export function ImportFeedDialog({ onClose }: Props): ReactElement {
|
||||||
|
const [textareaValue, setTextareaValue] = useState('')
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
const fileInputRef = useRef(null)
|
||||||
|
|
||||||
|
const { identities, setIdentities } = useContext(Context)
|
||||||
|
|
||||||
|
const { enqueueSnackbar } = useSnackbar()
|
||||||
|
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
async function onImport() {
|
||||||
|
const feed = await importIdentity(name, textareaValue)
|
||||||
|
|
||||||
|
if (feed) {
|
||||||
|
onFeedReady(feed)
|
||||||
|
} else {
|
||||||
|
enqueueSnackbar('Feed is not valid', { variant: 'error' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUploadIdentityFile() {
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
const input = fileInputRef.current as HTMLInputElement
|
||||||
|
input.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onIdentityFileSelected(event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
const fileReader = new FileReader()
|
||||||
|
const file = event.target?.files?.[0]
|
||||||
|
fileReader.onload = async event => {
|
||||||
|
const string = event.target?.result
|
||||||
|
|
||||||
|
if (string) {
|
||||||
|
const feed = await importIdentity(name, string as string)
|
||||||
|
|
||||||
|
if (feed) {
|
||||||
|
onFeedReady(feed)
|
||||||
|
} else {
|
||||||
|
enqueueSnackbar('Feed is not valid', { variant: 'error' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
fileReader.readAsText(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFeedReady(identity: Identity) {
|
||||||
|
persistIdentity(identities, identity)
|
||||||
|
setIdentities(identities)
|
||||||
|
enqueueSnackbar('Feed imported successfully', { variant: 'success' })
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SwarmDialog>
|
||||||
|
<input onChange={onIdentityFileSelected} ref={fileInputRef} className={classes.displayNone} type="file" />
|
||||||
|
<Box mb={4}>
|
||||||
|
<TitleWithClose onClose={onClose}>Import</TitleWithClose>
|
||||||
|
</Box>
|
||||||
|
<Box mb={2}>
|
||||||
|
<SwarmTextInput label="Identity Name" name="name" onChange={event => setName(event.target.value)} />
|
||||||
|
</Box>
|
||||||
|
<Box mb={4}>
|
||||||
|
<TextareaAutosize
|
||||||
|
className={classes.textarea}
|
||||||
|
minRows={5}
|
||||||
|
onChange={event => setTextareaValue(event.target.value)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<ExpandableListItemActions>
|
||||||
|
<SwarmButton iconType={Upload} onClick={onUploadIdentityFile}>
|
||||||
|
Upload Json File
|
||||||
|
</SwarmButton>
|
||||||
|
<SwarmButton iconType={Check} onClick={onImport}>
|
||||||
|
Use Pasted Text
|
||||||
|
</SwarmButton>
|
||||||
|
</ExpandableListItemActions>
|
||||||
|
</SwarmDialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import { Box, Grid, Typography } from '@material-ui/core'
|
||||||
|
import { useSnackbar } from 'notistack'
|
||||||
|
import { ReactElement, useContext, useEffect, useState } from 'react'
|
||||||
|
import { Bookmark, X } from 'react-feather'
|
||||||
|
import { RouteComponentProps, useHistory } from 'react-router'
|
||||||
|
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
|
||||||
|
import { HistoryHeader } from '../../components/HistoryHeader'
|
||||||
|
import { SwarmButton } from '../../components/SwarmButton'
|
||||||
|
import { SelectEvent, SwarmSelect } from '../../components/SwarmSelect'
|
||||||
|
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
|
||||||
|
import { Context as BeeContext } from '../../providers/Bee'
|
||||||
|
import { Context as IdentityContext, Identity } from '../../providers/Feeds'
|
||||||
|
import { Context as SettingsContext } from '../../providers/Settings'
|
||||||
|
import { Context as StampContext } from '../../providers/Stamps'
|
||||||
|
import { ROUTES } from '../../routes'
|
||||||
|
import { persistIdentity, updateFeed } from '../../utils/identity'
|
||||||
|
import { FeedPasswordDialog } from './FeedPasswordDialog'
|
||||||
|
|
||||||
|
interface MatchParams {
|
||||||
|
hash: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UpdateFeed(props: RouteComponentProps<MatchParams>): ReactElement {
|
||||||
|
const { identities, setIdentities } = useContext(IdentityContext)
|
||||||
|
const { beeApi, beeDebugApi } = useContext(SettingsContext)
|
||||||
|
const { stamps, refresh } = useContext(StampContext)
|
||||||
|
const { status } = useContext(BeeContext)
|
||||||
|
|
||||||
|
const [selectedStamp, setSelectedStamp] = useState<string | null>(null)
|
||||||
|
const [selectedIdentity, setSelectedIdentity] = useState<Identity | null>(null)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const { enqueueSnackbar } = useSnackbar()
|
||||||
|
const [showPasswordPrompt, setShowPasswordPrompt] = useState(false)
|
||||||
|
|
||||||
|
const history = useHistory()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refresh()
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
function onFeedChange(event: SelectEvent) {
|
||||||
|
const uuid = event.target.value
|
||||||
|
setSelectedIdentity(identities.find(x => x.uuid === uuid) || null)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onStampChange(event: SelectEvent) {
|
||||||
|
const batchId = event.target.value as string
|
||||||
|
setSelectedStamp(batchId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCancel() {
|
||||||
|
history.goBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBeginUpdatingFeed() {
|
||||||
|
if (!selectedIdentity) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedIdentity.type === 'V3') {
|
||||||
|
setShowPasswordPrompt(true)
|
||||||
|
} else {
|
||||||
|
onFeedUpdate(selectedIdentity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onFeedUpdate(identity: Identity, password?: string) {
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
if (!beeApi || !beeDebugApi || !selectedStamp) {
|
||||||
|
enqueueSnackbar(<span>Bee API unavailabe</span>, { variant: 'error' })
|
||||||
|
setLoading(false)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateFeed(beeApi, identity, props.match.params.hash, selectedStamp, password as string)
|
||||||
|
persistIdentity(identities, identity)
|
||||||
|
setIdentities([...identities])
|
||||||
|
history.push(ROUTES.FEEDS_PAGE.replace(':uuid', identity.uuid))
|
||||||
|
} catch (error: unknown) {
|
||||||
|
setLoading(false)
|
||||||
|
|
||||||
|
const message = (typeof error === 'object' && error !== null && Reflect.get(error, 'message')) || ''
|
||||||
|
|
||||||
|
if (message.includes('possibly wrong passphrase')) {
|
||||||
|
enqueueSnackbar('Wrong password, please try again', { variant: 'error' })
|
||||||
|
} else {
|
||||||
|
enqueueSnackbar('Could not update feed at this time, please try again later', { variant: 'error' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!status.all) return <TroubleshootConnectionCard />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{showPasswordPrompt && selectedIdentity && (
|
||||||
|
<FeedPasswordDialog
|
||||||
|
feedName={selectedIdentity.name + ' Website'}
|
||||||
|
onCancel={() => {
|
||||||
|
setShowPasswordPrompt(false)
|
||||||
|
}}
|
||||||
|
onProceed={(password: string) => {
|
||||||
|
onFeedUpdate(selectedIdentity, password)
|
||||||
|
}}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<HistoryHeader>Update feed</HistoryHeader>
|
||||||
|
<Box mb={2}>
|
||||||
|
<Grid container>
|
||||||
|
<SwarmSelect
|
||||||
|
options={identities.map(x => ({ value: x.uuid, label: `${x.name} Website` }))}
|
||||||
|
onChange={onFeedChange}
|
||||||
|
label="Feed"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box mb={4}>
|
||||||
|
<Grid container>
|
||||||
|
{stamps ? (
|
||||||
|
<SwarmSelect
|
||||||
|
options={stamps.map(x => ({ value: x.batchID, label: x.batchID.slice(0, 8) }))}
|
||||||
|
onChange={onStampChange}
|
||||||
|
label="Stamp"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Typography>You need to buy a stamp first to be able to update a feed.</Typography>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
<ExpandableListItemActions>
|
||||||
|
<SwarmButton
|
||||||
|
onClick={onBeginUpdatingFeed}
|
||||||
|
iconType={Bookmark}
|
||||||
|
loading={!showPasswordPrompt && loading}
|
||||||
|
disabled={loading || !selectedStamp || !selectedIdentity}
|
||||||
|
>
|
||||||
|
Update Selected Feed
|
||||||
|
</SwarmButton>
|
||||||
|
<SwarmButton onClick={onCancel} iconType={X} disabled={loading} cancel>
|
||||||
|
Close
|
||||||
|
</SwarmButton>
|
||||||
|
</ExpandableListItemActions>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import { Box, Typography } from '@material-ui/core'
|
||||||
|
import { ReactElement, useContext, useState } from 'react'
|
||||||
|
import { Download, Info, PlusSquare, Trash } from 'react-feather'
|
||||||
|
import { useHistory } from 'react-router'
|
||||||
|
import ExpandableList from '../../components/ExpandableList'
|
||||||
|
import ExpandableListItem from '../../components/ExpandableListItem'
|
||||||
|
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
|
||||||
|
import ExpandableListItemKey from '../../components/ExpandableListItemKey'
|
||||||
|
import { SwarmButton } from '../../components/SwarmButton'
|
||||||
|
import { Context as BeeContext } from '../../providers/Bee'
|
||||||
|
import { Context as IdentityContext, Identity } from '../../providers/Feeds'
|
||||||
|
import { ROUTES } from '../../routes'
|
||||||
|
import { formatEnum } from '../../utils'
|
||||||
|
import { persistIdentitiesWithoutUpdate } from '../../utils/identity'
|
||||||
|
import { DeleteFeedDialog } from './DeleteFeedDialog'
|
||||||
|
import { ExportFeedDialog } from './ExportFeedDialog'
|
||||||
|
import { ImportFeedDialog } from './ImportFeedDialog'
|
||||||
|
|
||||||
|
export default function Feeds(): ReactElement {
|
||||||
|
const { identities, setIdentities } = useContext(IdentityContext)
|
||||||
|
const { status } = useContext(BeeContext)
|
||||||
|
|
||||||
|
const history = useHistory()
|
||||||
|
|
||||||
|
const [selectedIdentity, setSelectedIdentity] = useState<Identity | null>(null)
|
||||||
|
const [showImport, setShowImport] = useState(false)
|
||||||
|
const [showExport, setShowExport] = useState(false)
|
||||||
|
const [showDelete, setShowDelete] = useState(false)
|
||||||
|
|
||||||
|
function createNewFeed() {
|
||||||
|
return history.push(ROUTES.FEEDS_NEW)
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewFeed(uuid: string) {
|
||||||
|
history.push(ROUTES.FEEDS_PAGE.replace(':uuid', uuid))
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDialogClose() {
|
||||||
|
setShowDelete(false)
|
||||||
|
setShowExport(false)
|
||||||
|
setShowImport(false)
|
||||||
|
setSelectedIdentity(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDelete(identity: Identity) {
|
||||||
|
onDialogClose()
|
||||||
|
const updatedFeeds = identities.filter(x => x.uuid !== identity.uuid)
|
||||||
|
setIdentities(updatedFeeds)
|
||||||
|
persistIdentitiesWithoutUpdate(updatedFeeds)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onShowExport(identity: Identity) {
|
||||||
|
setSelectedIdentity(identity)
|
||||||
|
setShowExport(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onShowDelete(identity: Identity) {
|
||||||
|
setSelectedIdentity(identity)
|
||||||
|
setShowDelete(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{showImport && <ImportFeedDialog onClose={() => setShowImport(false)} />}
|
||||||
|
{showExport && selectedIdentity && <ExportFeedDialog identity={selectedIdentity} onClose={onDialogClose} />}
|
||||||
|
{showDelete && selectedIdentity && (
|
||||||
|
<DeleteFeedDialog
|
||||||
|
identity={selectedIdentity}
|
||||||
|
onClose={onDialogClose}
|
||||||
|
onConfirm={(identity: Identity) => onDelete(identity)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Box mb={4}>
|
||||||
|
<Typography variant="h1">Feeds</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box mb={4}>
|
||||||
|
<ExpandableListItemActions>
|
||||||
|
<SwarmButton iconType={PlusSquare} onClick={createNewFeed}>
|
||||||
|
Create New Feed
|
||||||
|
</SwarmButton>
|
||||||
|
<SwarmButton iconType={PlusSquare} onClick={() => setShowImport(true)}>
|
||||||
|
Import Feed
|
||||||
|
</SwarmButton>
|
||||||
|
</ExpandableListItemActions>
|
||||||
|
</Box>
|
||||||
|
{identities.map((x, i) => (
|
||||||
|
<ExpandableList key={i} label={`${x.name} Website`} defaultOpen>
|
||||||
|
<Box mb={0.5}>
|
||||||
|
<ExpandableList label={x.name} level={1}>
|
||||||
|
<ExpandableListItemKey label="Identity address" value={x.address} />
|
||||||
|
<ExpandableListItem label="Identity type" value={formatEnum(x.type)} />
|
||||||
|
</ExpandableList>
|
||||||
|
</Box>
|
||||||
|
<ExpandableListItemKey label="Topic" value={'00'.repeat(32)} />
|
||||||
|
{x.feedHash && <ExpandableListItemKey label="Feed hash" value={x.feedHash} />}
|
||||||
|
<Box mt={0.75}>
|
||||||
|
<ExpandableListItemActions>
|
||||||
|
{status.all && (
|
||||||
|
<SwarmButton onClick={() => viewFeed(x.uuid)} iconType={Info}>
|
||||||
|
View Feed Page
|
||||||
|
</SwarmButton>
|
||||||
|
)}
|
||||||
|
<SwarmButton onClick={() => onShowExport(x)} iconType={Download}>
|
||||||
|
Export...
|
||||||
|
</SwarmButton>
|
||||||
|
<SwarmButton onClick={() => onShowDelete(x)} iconType={Trash}>
|
||||||
|
Delete...
|
||||||
|
</SwarmButton>
|
||||||
|
</ExpandableListItemActions>
|
||||||
|
</Box>
|
||||||
|
</ExpandableList>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,24 +1,35 @@
|
|||||||
import { Box, Typography } from '@material-ui/core'
|
import * as swarmCid from '@ethersphere/swarm-cid'
|
||||||
|
import { Box } from '@material-ui/core'
|
||||||
import { ReactElement } from 'react'
|
import { ReactElement } from 'react'
|
||||||
|
import { DocumentationText } from '../../components/DocumentationText'
|
||||||
import ExpandableListItemKey from '../../components/ExpandableListItemKey'
|
import ExpandableListItemKey from '../../components/ExpandableListItemKey'
|
||||||
import ExpandableListItemLink from '../../components/ExpandableListItemLink'
|
import ExpandableListItemLink from '../../components/ExpandableListItemLink'
|
||||||
|
import { detectIndexHtml } from '../../utils/file'
|
||||||
|
import { SwarmFile } from '../../utils/SwarmFile'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
files: SwarmFile[]
|
||||||
hash: string
|
hash: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AssetSummary({ hash }: Props): ReactElement {
|
export function AssetSummary({ files, hash }: Props): ReactElement {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box mb={4}>
|
<Box mb={4}>
|
||||||
<ExpandableListItemKey label="Swarm hash" value={hash} />
|
<ExpandableListItemKey label="Swarm hash" value={hash} />
|
||||||
<ExpandableListItemLink label="Share on Swarm Gateway" value={`https://gateway.ethswarm.org/access/${hash}`} />
|
<ExpandableListItemLink label="Share on Swarm Gateway" value={`https://gateway.ethswarm.org/access/${hash}`} />
|
||||||
|
{detectIndexHtml(files) && (
|
||||||
|
<ExpandableListItemLink
|
||||||
|
label="BZZ Link"
|
||||||
|
value={`https://${swarmCid.encodeManifestReference(hash).toString()}.bzz.link`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Typography>
|
<DocumentationText>
|
||||||
The Swarm Gateway is graciously provided by the Swarm Foundation. This service is under development and provided
|
The Swarm Gateway is graciously provided by the Swarm Foundation. This service is under development and provided
|
||||||
for testing purposes only. Learn more at{' '}
|
for testing purposes only. Learn more at{' '}
|
||||||
<a href="https://gateway.ethswarm.org/">https://gateway.ethswarm.org/</a>.
|
<a href="https://gateway.ethswarm.org/">https://gateway.ethswarm.org/</a>.
|
||||||
</Typography>
|
</DocumentationText>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ReactElement, useContext, useState } from 'react'
|
|||||||
import { useHistory } from 'react-router-dom'
|
import { useHistory } from 'react-router-dom'
|
||||||
import ExpandableListItemInput from '../../components/ExpandableListItemInput'
|
import ExpandableListItemInput from '../../components/ExpandableListItemInput'
|
||||||
import { History } from '../../components/History'
|
import { History } from '../../components/History'
|
||||||
|
import { Context, defaultUploadOrigin } from '../../providers/File'
|
||||||
import { Context as SettingsContext } from '../../providers/Settings'
|
import { Context as SettingsContext } from '../../providers/Settings'
|
||||||
import { ROUTES } from '../../routes'
|
import { ROUTES } from '../../routes'
|
||||||
import { extractSwarmHash } from '../../utils'
|
import { extractSwarmHash } from '../../utils'
|
||||||
@@ -16,6 +17,8 @@ export function Download(): ReactElement {
|
|||||||
const { beeApi } = useContext(SettingsContext)
|
const { beeApi } = useContext(SettingsContext)
|
||||||
const [referenceError, setReferenceError] = useState<string | undefined>(undefined)
|
const [referenceError, setReferenceError] = useState<string | undefined>(undefined)
|
||||||
|
|
||||||
|
const { setUploadOrigin } = useContext(Context)
|
||||||
|
|
||||||
const { enqueueSnackbar } = useSnackbar()
|
const { enqueueSnackbar } = useSnackbar()
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
|
|
||||||
@@ -28,12 +31,21 @@ export function Download(): ReactElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onSwarmIdentifier(identifier: string) {
|
async function onSwarmIdentifier(identifier: string) {
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
if (!beeApi) {
|
if (!beeApi) {
|
||||||
|
setLoading(false)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const manifestJs = new ManifestJs(beeApi)
|
const manifestJs = new ManifestJs(beeApi)
|
||||||
|
const feedIdentifier = await manifestJs.resolveFeedManifest(identifier)
|
||||||
|
|
||||||
|
if (feedIdentifier) {
|
||||||
|
identifier = feedIdentifier
|
||||||
|
}
|
||||||
const isManifest = await manifestJs.isManifest(identifier)
|
const isManifest = await manifestJs.isManifest(identifier)
|
||||||
|
|
||||||
if (!isManifest) {
|
if (!isManifest) {
|
||||||
@@ -41,6 +53,7 @@ export function Download(): ReactElement {
|
|||||||
}
|
}
|
||||||
const indexDocument = await manifestJs.getIndexDocumentPath(identifier)
|
const indexDocument = await manifestJs.getIndexDocumentPath(identifier)
|
||||||
putHistory(HISTORY_KEYS.DOWNLOAD_HISTORY, identifier, determineHistoryName(identifier, indexDocument))
|
putHistory(HISTORY_KEYS.DOWNLOAD_HISTORY, identifier, determineHistoryName(identifier, indexDocument))
|
||||||
|
setUploadOrigin(defaultUploadOrigin)
|
||||||
history.push(ROUTES.HASH.replace(':hash', identifier))
|
history.push(ROUTES.HASH.replace(':hash', identifier))
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
let message = typeof error === 'object' && error !== null && Reflect.get(error, 'message')
|
let message = typeof error === 'object' && error !== null && Reflect.get(error, 'message')
|
||||||
@@ -80,11 +93,12 @@ export function Download(): ReactElement {
|
|||||||
onConfirm={value => onSwarmIdentifier(value)}
|
onConfirm={value => onSwarmIdentifier(value)}
|
||||||
onChange={validateChange}
|
onChange={validateChange}
|
||||||
helperText={referenceError}
|
helperText={referenceError}
|
||||||
confirmLabel={'Search'}
|
confirmLabel={'Find'}
|
||||||
confirmLabelDisabled={Boolean(referenceError) || loading}
|
confirmLabelDisabled={Boolean(referenceError) || loading}
|
||||||
placeholder="e.g. 31fb0362b1a42536134c86bc58b97ac0244e5c6630beec3e27c2d1cecb38c605"
|
placeholder="e.g. 31fb0362b1a42536134c86bc58b97ac0244e5c6630beec3e27c2d1cecb38c605"
|
||||||
expandedOnly
|
expandedOnly
|
||||||
mapperFn={value => recognizeSwarmHash(value)}
|
mapperFn={value => recognizeSwarmHash(value)}
|
||||||
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
<History title="Download History" localStorageKey={HISTORY_KEYS.DOWNLOAD_HISTORY} />
|
<History title="Download History" localStorageKey={HISTORY_KEYS.DOWNLOAD_HISTORY} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,32 +1,46 @@
|
|||||||
import { Button } from '@material-ui/core'
|
import { Box, Grid } from '@material-ui/core'
|
||||||
import { Clear } from '@material-ui/icons'
|
|
||||||
import { ReactElement } from 'react'
|
import { ReactElement } from 'react'
|
||||||
import { Download, Link } from 'react-feather'
|
import { Bookmark, Download, Link, X } from 'react-feather'
|
||||||
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
|
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
|
||||||
import { SwarmButton } from '../../components/SwarmButton'
|
import { SwarmButton } from '../../components/SwarmButton'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onOpen: () => void
|
onOpen: () => void
|
||||||
onDownload: () => void
|
|
||||||
onCancel: () => void
|
onCancel: () => void
|
||||||
|
onDownload: () => void
|
||||||
|
onUpdateFeed: () => void
|
||||||
hasIndexDocument: boolean
|
hasIndexDocument: boolean
|
||||||
loading: boolean
|
loading: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DownloadActionBar({ onOpen, onDownload, onCancel, hasIndexDocument, loading }: Props): ReactElement {
|
export function DownloadActionBar({
|
||||||
|
onOpen,
|
||||||
|
onCancel,
|
||||||
|
onDownload,
|
||||||
|
onUpdateFeed,
|
||||||
|
hasIndexDocument,
|
||||||
|
loading,
|
||||||
|
}: Props): ReactElement {
|
||||||
return (
|
return (
|
||||||
<ExpandableListItemActions>
|
<Grid container justifyContent="space-between">
|
||||||
{hasIndexDocument && (
|
<ExpandableListItemActions>
|
||||||
<SwarmButton onClick={onOpen} iconType={Link} disabled={loading}>
|
{hasIndexDocument && (
|
||||||
View Website
|
<SwarmButton onClick={onOpen} iconType={Link} disabled={loading}>
|
||||||
|
View Website
|
||||||
|
</SwarmButton>
|
||||||
|
)}
|
||||||
|
<SwarmButton onClick={onDownload} iconType={Download} disabled={loading} loading={loading}>
|
||||||
|
Download
|
||||||
</SwarmButton>
|
</SwarmButton>
|
||||||
)}
|
<SwarmButton onClick={onCancel} iconType={X} disabled={loading} loading={loading} cancel>
|
||||||
<SwarmButton onClick={onDownload} iconType={Download} disabled={loading} loading={loading}>
|
Close
|
||||||
Download
|
</SwarmButton>
|
||||||
</SwarmButton>
|
</ExpandableListItemActions>
|
||||||
<Button onClick={onCancel} variant="contained" startIcon={<Clear />} disabled={loading}>
|
<Box mb={1} mr={1}>
|
||||||
Close
|
<SwarmButton onClick={onUpdateFeed} iconType={Bookmark}>
|
||||||
</Button>
|
Update Feed
|
||||||
</ExpandableListItemActions>
|
</SwarmButton>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { ManifestJs } from '@ethersphere/manifest-js'
|
import { ManifestJs } from '@ethersphere/manifest-js'
|
||||||
import { Box } from '@material-ui/core'
|
import { Box, Typography } from '@material-ui/core'
|
||||||
import { saveAs } from 'file-saver'
|
import { saveAs } from 'file-saver'
|
||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
|
import { useSnackbar } from 'notistack'
|
||||||
import { ReactElement, useContext, useEffect, useState } from 'react'
|
import { ReactElement, useContext, useEffect, useState } from 'react'
|
||||||
import { RouteComponentProps, useHistory } from 'react-router-dom'
|
import { RouteComponentProps, useHistory } from 'react-router-dom'
|
||||||
|
import { HistoryHeader } from '../../components/HistoryHeader'
|
||||||
import { Loading } from '../../components/Loading'
|
import { Loading } from '../../components/Loading'
|
||||||
|
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
|
||||||
|
import { Context as BeeContext } from '../../providers/Bee'
|
||||||
import { Context as SettingsContext } from '../../providers/Settings'
|
import { Context as SettingsContext } from '../../providers/Settings'
|
||||||
import { ROUTES } from '../../routes'
|
import { ROUTES } from '../../routes'
|
||||||
import { convertBeeFileToBrowserFile, convertManifestToFiles } from '../../utils/file'
|
import { convertBeeFileToBrowserFile, convertManifestToFiles } from '../../utils/file'
|
||||||
@@ -21,17 +25,22 @@ interface MatchParams {
|
|||||||
|
|
||||||
export function Share(props: RouteComponentProps<MatchParams>): ReactElement {
|
export function Share(props: RouteComponentProps<MatchParams>): ReactElement {
|
||||||
const { apiUrl, beeApi } = useContext(SettingsContext)
|
const { apiUrl, beeApi } = useContext(SettingsContext)
|
||||||
|
const { status } = useContext(BeeContext)
|
||||||
|
|
||||||
const reference = props.match.params.hash
|
const reference = props.match.params.hash
|
||||||
|
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
|
const { enqueueSnackbar } = useSnackbar()
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [downloading, setDownloading] = useState(false)
|
const [downloading, setDownloading] = useState(false)
|
||||||
const [files, setFiles] = useState<SwarmFile[]>([])
|
const [files, setFiles] = useState<SwarmFile[]>([])
|
||||||
const [swarmEntries, setSwarmEntries] = useState<Record<string, string>>({})
|
const [swarmEntries, setSwarmEntries] = useState<Record<string, string>>({})
|
||||||
const [indexDocument, setIndexDocument] = useState<string | null>(null)
|
const [indexDocument, setIndexDocument] = useState<string | null>(null)
|
||||||
|
const [notFound, setNotFound] = useState(false)
|
||||||
|
|
||||||
async function prepare() {
|
async function prepare() {
|
||||||
if (!beeApi) {
|
if (!beeApi || !status.all) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +48,10 @@ export function Share(props: RouteComponentProps<MatchParams>): ReactElement {
|
|||||||
const isManifest = await manifestJs.isManifest(reference)
|
const isManifest = await manifestJs.isManifest(reference)
|
||||||
|
|
||||||
if (!isManifest) {
|
if (!isManifest) {
|
||||||
throw Error('The specified hash does not contain valid content.')
|
setNotFound(true)
|
||||||
|
enqueueSnackbar('The specified hash does not contain valid content.', { variant: 'error' })
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
const entries = await manifestJs.getHashes(reference)
|
const entries = await manifestJs.getHashes(reference)
|
||||||
setSwarmEntries(entries)
|
setSwarmEntries(entries)
|
||||||
@@ -67,9 +79,13 @@ export function Share(props: RouteComponentProps<MatchParams>): ReactElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onUpdateFeed() {
|
||||||
|
history.push(ROUTES.FEEDS_UPDATE.replace(':hash', reference))
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
prepare().then(() => {
|
prepare().finally(() => {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@@ -97,22 +113,34 @@ export function Share(props: RouteComponentProps<MatchParams>): ReactElement {
|
|||||||
|
|
||||||
const assetName = shortenHash(reference)
|
const assetName = shortenHash(reference)
|
||||||
|
|
||||||
|
if (!status.all) return <TroubleshootConnectionCard />
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <Loading />
|
return <Loading />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (notFound) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<HistoryHeader>Not Found</HistoryHeader>
|
||||||
|
<Typography>The specified hash is not found.</Typography>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box mb={4}>
|
<Box mb={4}>
|
||||||
<AssetPreview files={files} assetName={assetName} />
|
<AssetPreview files={files} assetName={assetName} />
|
||||||
</Box>
|
</Box>
|
||||||
<Box mb={4}>
|
<Box mb={4}>
|
||||||
<AssetSummary hash={reference} />
|
<AssetSummary files={files} hash={reference} />
|
||||||
</Box>
|
</Box>
|
||||||
<DownloadActionBar
|
<DownloadActionBar
|
||||||
onOpen={onOpen}
|
onOpen={onOpen}
|
||||||
onCancel={onClose}
|
onCancel={onClose}
|
||||||
onDownload={onDownload}
|
onDownload={onDownload}
|
||||||
|
onUpdateFeed={onUpdateFeed}
|
||||||
hasIndexDocument={Boolean(indexDocument && files.length > 1)}
|
hasIndexDocument={Boolean(indexDocument && files.length > 1)}
|
||||||
loading={downloading}
|
loading={downloading}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+107
-35
@@ -1,41 +1,71 @@
|
|||||||
|
import { Box } from '@material-ui/core'
|
||||||
import { useSnackbar } from 'notistack'
|
import { useSnackbar } from 'notistack'
|
||||||
import { ReactElement, useContext, useEffect, useState } from 'react'
|
import { ReactElement, useContext, useEffect, useState } from 'react'
|
||||||
import { useHistory } from 'react-router-dom'
|
import { useHistory } from 'react-router-dom'
|
||||||
|
import { DocumentationText } from '../../components/DocumentationText'
|
||||||
import { HistoryHeader } from '../../components/HistoryHeader'
|
import { HistoryHeader } from '../../components/HistoryHeader'
|
||||||
|
import { ProgressIndicator } from '../../components/ProgressIndicator'
|
||||||
|
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
|
||||||
|
import { Context as BeeContext } from '../../providers/Bee'
|
||||||
|
import { Context as IdentityContext, Identity } from '../../providers/Feeds'
|
||||||
import { Context as FileContext } from '../../providers/File'
|
import { Context as FileContext } from '../../providers/File'
|
||||||
import { Context as SettingsContext } from '../../providers/Settings'
|
import { Context as SettingsContext } from '../../providers/Settings'
|
||||||
import { Context, EnrichedPostageBatch } from '../../providers/Stamps'
|
import { Context as StampsContext, EnrichedPostageBatch } from '../../providers/Stamps'
|
||||||
import { ROUTES } from '../../routes'
|
import { ROUTES } from '../../routes'
|
||||||
import { detectIndexHtml, getAssetNameFromFiles } from '../../utils/file'
|
import { detectIndexHtml, getAssetNameFromFiles } from '../../utils/file'
|
||||||
|
import { persistIdentity, updateFeed } from '../../utils/identity'
|
||||||
import { HISTORY_KEYS, putHistory } from '../../utils/local-storage'
|
import { HISTORY_KEYS, putHistory } from '../../utils/local-storage'
|
||||||
import { CreatePostageStampModal } from '../stamps/CreatePostageStampModal'
|
import { FeedPasswordDialog } from '../feeds/FeedPasswordDialog'
|
||||||
import { SelectPostageStampModal } from '../stamps/SelectPostageStampModal'
|
import { PostageStampCreation } from '../stamps/PostageStampCreation'
|
||||||
|
import { PostageStampSelector } from '../stamps/PostageStampSelector'
|
||||||
import { AssetPreview } from './AssetPreview'
|
import { AssetPreview } from './AssetPreview'
|
||||||
import { StampPreview } from './StampPreview'
|
import { StampPreview } from './StampPreview'
|
||||||
import { UploadActionBar } from './UploadActionBar'
|
import { UploadActionBar } from './UploadActionBar'
|
||||||
|
|
||||||
export function Upload(): ReactElement {
|
export function Upload(): ReactElement {
|
||||||
const [isBuyingStamp, setBuyingStamp] = useState(false)
|
const [step, setStep] = useState(0)
|
||||||
const [isSelectingStamp, setSelectingStamp] = useState(false)
|
const [stampMode, setStampMode] = useState<'SELECT' | 'BUY'>('SELECT')
|
||||||
const [stamp, setStamp] = useState<EnrichedPostageBatch | null>(null)
|
const [stamp, setStamp] = useState<EnrichedPostageBatch | null>(null)
|
||||||
const [isUploading, setUploading] = useState(false)
|
const [isUploading, setUploading] = useState(false)
|
||||||
|
const [showPasswordPrompt, setShowPasswordPrompt] = useState(false)
|
||||||
|
|
||||||
const { stamps, refresh } = useContext(Context)
|
const { refresh } = useContext(StampsContext)
|
||||||
const { beeApi } = useContext(SettingsContext)
|
const { beeApi } = useContext(SettingsContext)
|
||||||
const { files, setFiles } = useContext(FileContext)
|
const { files, setFiles, uploadOrigin } = useContext(FileContext)
|
||||||
|
const { identities, setIdentities } = useContext(IdentityContext)
|
||||||
|
const { status } = useContext(BeeContext)
|
||||||
|
|
||||||
const { enqueueSnackbar } = useSnackbar()
|
const { enqueueSnackbar } = useSnackbar()
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
|
|
||||||
if (!files.length) {
|
|
||||||
setFiles([])
|
|
||||||
history.replace(ROUTES.UPLOAD)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refresh()
|
refresh()
|
||||||
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const uploadFiles = () => {
|
if (!status.all) return <TroubleshootConnectionCard />
|
||||||
|
|
||||||
|
if (!files.length) {
|
||||||
|
setFiles([])
|
||||||
|
history.replace(ROUTES.UPLOAD)
|
||||||
|
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
|
||||||
|
const identity = uploadOrigin.uuid ? identities.find(x => x.uuid === uploadOrigin.uuid) : null
|
||||||
|
|
||||||
|
const onUpload = () => {
|
||||||
|
if (uploadOrigin.origin === 'UPLOAD') {
|
||||||
|
uploadFiles()
|
||||||
|
} else {
|
||||||
|
if ((identity as Identity).type === 'PRIVATE_KEY') {
|
||||||
|
uploadFiles()
|
||||||
|
} else {
|
||||||
|
setShowPasswordPrompt(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadFiles = (password?: string) => {
|
||||||
if (!beeApi || !files.length || !stamp) {
|
if (!beeApi || !files.length || !stamp) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -48,7 +78,16 @@ export function Upload(): ReactElement {
|
|||||||
.uploadFiles(stamp.batchID, files as unknown as File[], { indexDocument })
|
.uploadFiles(stamp.batchID, files as unknown as File[], { indexDocument })
|
||||||
.then(hash => {
|
.then(hash => {
|
||||||
putHistory(HISTORY_KEYS.UPLOAD_HISTORY, hash.reference, getAssetNameFromFiles(files))
|
putHistory(HISTORY_KEYS.UPLOAD_HISTORY, hash.reference, getAssetNameFromFiles(files))
|
||||||
history.replace(ROUTES.HASH.replace(':hash', hash.reference))
|
|
||||||
|
if (uploadOrigin.origin === 'UPLOAD') {
|
||||||
|
history.replace(ROUTES.HASH.replace(':hash', hash.reference))
|
||||||
|
} else {
|
||||||
|
updateFeed(beeApi, identity as Identity, hash.reference, stamp.batchID, password as string).then(() => {
|
||||||
|
persistIdentity(identities, identity as Identity)
|
||||||
|
setIdentities([...identities])
|
||||||
|
history.replace(ROUTES.FEEDS_PAGE.replace(':uuid', uploadOrigin.uuid as string))
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
enqueueSnackbar(`Error uploading: ${e.message}`, { variant: 'error' })
|
enqueueSnackbar(`Error uploading: ${e.message}`, { variant: 'error' })
|
||||||
@@ -57,36 +96,69 @@ export function Upload(): ReactElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
|
setStep(0)
|
||||||
setFiles([])
|
setFiles([])
|
||||||
setStamp(null)
|
setStamp(null)
|
||||||
setUploading(false)
|
setUploading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onFeedPasswordGiven = (password: string) => {
|
||||||
|
uploadFiles(password)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HistoryHeader>Upload</HistoryHeader>
|
{showPasswordPrompt && (
|
||||||
{files.length && <AssetPreview files={files} />}
|
<FeedPasswordDialog
|
||||||
{stamp !== null ? <StampPreview stamp={stamp} /> : null}
|
loading={isUploading}
|
||||||
{files.length && (
|
feedName={(identity as Identity).name}
|
||||||
<UploadActionBar
|
onCancel={() => setShowPasswordPrompt(false)}
|
||||||
canSelectStamp={stamps !== null && stamps.length > 0}
|
onProceed={onFeedPasswordGiven}
|
||||||
hasSelectedStamp={stamp !== null}
|
|
||||||
onCancel={reset}
|
|
||||||
onBuy={() => setBuyingStamp(true)}
|
|
||||||
onSelect={() => setSelectingStamp(true)}
|
|
||||||
onUpload={uploadFiles}
|
|
||||||
onClearStamp={() => setStamp(null)}
|
|
||||||
isUploading={isUploading}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isBuyingStamp ? <CreatePostageStampModal onClose={() => setBuyingStamp(false)} /> : null}
|
{identity && <HistoryHeader>{`Update "${identity.name}"`}</HistoryHeader>}
|
||||||
{stamps && isSelectingStamp ? (
|
{!identity && <HistoryHeader>Upload</HistoryHeader>}
|
||||||
<SelectPostageStampModal
|
<Box mb={4}>
|
||||||
stamps={stamps}
|
<ProgressIndicator steps={['Preview', 'Add postage stamp', 'Upload to node']} index={step} />
|
||||||
onClose={() => setSelectingStamp(false)}
|
</Box>
|
||||||
onSelect={stamp => setStamp(stamp)}
|
{(step === 0 || step === 2) && <AssetPreview files={files} />}
|
||||||
/>
|
{step === 1 && (
|
||||||
) : null}
|
<>
|
||||||
|
<Box mb={2}>
|
||||||
|
{stampMode === 'SELECT' ? (
|
||||||
|
<PostageStampSelector onSelect={stamp => setStamp(stamp)} defaultValue={stamp?.batchID} />
|
||||||
|
) : (
|
||||||
|
<PostageStampCreation onFinished={() => setStampMode('SELECT')} />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Box mb={4}>
|
||||||
|
<DocumentationText>
|
||||||
|
Please refer to the{' '}
|
||||||
|
<a
|
||||||
|
href="https://docs.ethswarm.org/debug-api/#tag/Postage-Stamps/paths/~1stamps~1{amount}~1{depth}/post"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
official Bee documentation
|
||||||
|
</a>{' '}
|
||||||
|
to understand these values.
|
||||||
|
</DocumentationText>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{step === 2 && stamp && <StampPreview stamp={stamp} />}
|
||||||
|
<UploadActionBar
|
||||||
|
step={step}
|
||||||
|
onCancel={reset}
|
||||||
|
onGoBack={() => setStep(step => step - 1)}
|
||||||
|
onProceed={() => setStep(step => step + 1)}
|
||||||
|
onUpload={onUpload}
|
||||||
|
isUploading={isUploading}
|
||||||
|
hasStamp={Boolean(stamp)}
|
||||||
|
uploadLabel={identity ? 'Update Feed' : 'Upload To Your Node'}
|
||||||
|
stampMode={stampMode}
|
||||||
|
setStampMode={setStampMode}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +1,88 @@
|
|||||||
import { Button, Typography } from '@material-ui/core'
|
import { Box, Grid } from '@material-ui/core'
|
||||||
import { Clear } from '@material-ui/icons'
|
|
||||||
import { ReactElement } from 'react'
|
import { ReactElement } from 'react'
|
||||||
import { Check, Layers, PlusSquare, RefreshCcw } from 'react-feather'
|
import { ArrowLeft, Check, Layers, PlusSquare, X } from 'react-feather'
|
||||||
|
import { DocumentationText } from '../../components/DocumentationText'
|
||||||
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
|
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
|
||||||
import { SwarmButton } from '../../components/SwarmButton'
|
import { SwarmButton } from '../../components/SwarmButton'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
canSelectStamp: boolean
|
step: number
|
||||||
hasSelectedStamp: boolean
|
|
||||||
onUpload: () => void
|
onUpload: () => void
|
||||||
onBuy: () => void
|
|
||||||
onSelect: () => void
|
|
||||||
onCancel: () => void
|
onCancel: () => void
|
||||||
onClearStamp: () => void
|
onGoBack: () => void
|
||||||
|
onProceed: () => void
|
||||||
isUploading: boolean
|
isUploading: boolean
|
||||||
|
hasStamp: boolean
|
||||||
|
uploadLabel: string
|
||||||
|
stampMode: 'BUY' | 'SELECT'
|
||||||
|
setStampMode: (mode: 'BUY' | 'SELECT') => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UploadActionBar({
|
export function UploadActionBar({
|
||||||
canSelectStamp,
|
step,
|
||||||
hasSelectedStamp,
|
|
||||||
onUpload,
|
onUpload,
|
||||||
onBuy,
|
|
||||||
onSelect,
|
|
||||||
onCancel,
|
onCancel,
|
||||||
onClearStamp,
|
onGoBack,
|
||||||
|
onProceed,
|
||||||
isUploading,
|
isUploading,
|
||||||
|
hasStamp,
|
||||||
|
uploadLabel,
|
||||||
|
stampMode,
|
||||||
|
setStampMode,
|
||||||
}: Props): ReactElement {
|
}: Props): ReactElement {
|
||||||
const showBuy = !hasSelectedStamp
|
if (step === 0) {
|
||||||
const showSelect = canSelectStamp && !hasSelectedStamp
|
return (
|
||||||
const showUpload = hasSelectedStamp
|
<>
|
||||||
const showChange = canSelectStamp && hasSelectedStamp
|
<Box mb={1}>
|
||||||
|
<ExpandableListItemActions>
|
||||||
|
<SwarmButton onClick={onProceed} iconType={Layers}>
|
||||||
|
Add Postage Stamp
|
||||||
|
</SwarmButton>
|
||||||
|
<SwarmButton onClick={onCancel} iconType={X} cancel>
|
||||||
|
Cancel
|
||||||
|
</SwarmButton>
|
||||||
|
</ExpandableListItemActions>
|
||||||
|
</Box>
|
||||||
|
<DocumentationText>You need a postage stamp to upload.</DocumentationText>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
if (step === 1) {
|
||||||
<>
|
return (
|
||||||
|
<Grid container direction="row" justifyContent="space-between">
|
||||||
|
<ExpandableListItemActions>
|
||||||
|
{stampMode === 'SELECT' && (
|
||||||
|
<SwarmButton onClick={onProceed} iconType={Check} disabled={!hasStamp}>
|
||||||
|
Proceed With Selected Stamp
|
||||||
|
</SwarmButton>
|
||||||
|
)}
|
||||||
|
<SwarmButton onClick={onGoBack} iconType={ArrowLeft} cancel>
|
||||||
|
Back To Preview
|
||||||
|
</SwarmButton>
|
||||||
|
</ExpandableListItemActions>
|
||||||
|
<SwarmButton
|
||||||
|
onClick={() => setStampMode(stampMode === 'BUY' ? 'SELECT' : 'BUY')}
|
||||||
|
iconType={stampMode === 'BUY' ? Layers : PlusSquare}
|
||||||
|
>
|
||||||
|
{stampMode === 'BUY' ? 'Use Existing Stamp' : 'Buy New Stamp'}
|
||||||
|
</SwarmButton>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step === 2) {
|
||||||
|
return (
|
||||||
<ExpandableListItemActions>
|
<ExpandableListItemActions>
|
||||||
{showBuy ? (
|
<SwarmButton onClick={onUpload} iconType={Check} disabled={isUploading} loading={isUploading}>
|
||||||
<SwarmButton onClick={onBuy} iconType={PlusSquare}>
|
{uploadLabel}
|
||||||
Buy New Postage Stamp
|
</SwarmButton>
|
||||||
</SwarmButton>
|
<SwarmButton onClick={onGoBack} iconType={ArrowLeft} disabled={isUploading} cancel>
|
||||||
) : null}
|
Change Postage Stamp
|
||||||
{showSelect ? (
|
</SwarmButton>
|
||||||
<SwarmButton onClick={onSelect} iconType={Layers}>
|
|
||||||
Use Existing Postage Stamp
|
|
||||||
</SwarmButton>
|
|
||||||
) : null}
|
|
||||||
{showUpload ? (
|
|
||||||
<SwarmButton onClick={onUpload} iconType={Check} disabled={isUploading} loading={isUploading}>
|
|
||||||
Upload To Your Node
|
|
||||||
</SwarmButton>
|
|
||||||
) : null}
|
|
||||||
{showChange ? (
|
|
||||||
<SwarmButton onClick={onClearStamp} iconType={RefreshCcw} disabled={isUploading}>
|
|
||||||
Change Postage Stamp
|
|
||||||
</SwarmButton>
|
|
||||||
) : null}
|
|
||||||
<Button onClick={onCancel} variant="contained" startIcon={<Clear />}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</ExpandableListItemActions>
|
</ExpandableListItemActions>
|
||||||
{showSelect ? (
|
)
|
||||||
<Typography>
|
}
|
||||||
You need a postage stamp to upload. Please refer to the official Bee documentation to understand how postage
|
|
||||||
stamps work.
|
return <></>
|
||||||
</Typography>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
import { createStyles, makeStyles, Theme, Typography } from '@material-ui/core'
|
import { createStyles, makeStyles, Theme } from '@material-ui/core'
|
||||||
import { DropzoneArea } from 'material-ui-dropzone'
|
import { DropzoneArea } from 'material-ui-dropzone'
|
||||||
import { useSnackbar } from 'notistack'
|
import { useSnackbar } from 'notistack'
|
||||||
import { ReactElement, useContext, useState } from 'react'
|
import { ReactElement, useContext, useState } from 'react'
|
||||||
import { FilePlus, FolderPlus, PlusCircle } from 'react-feather'
|
import { FilePlus, FolderPlus, PlusCircle } from 'react-feather'
|
||||||
import { useHistory } from 'react-router-dom'
|
import { useHistory } from 'react-router-dom'
|
||||||
|
import { DocumentationText } from '../../components/DocumentationText'
|
||||||
import { SwarmButton } from '../../components/SwarmButton'
|
import { SwarmButton } from '../../components/SwarmButton'
|
||||||
import { Context } from '../../providers/File'
|
import { Context, UploadOrigin } from '../../providers/File'
|
||||||
import { ROUTES } from '../../routes'
|
import { ROUTES } from '../../routes'
|
||||||
import { detectIndexHtml } from '../../utils/file'
|
import { detectIndexHtml } from '../../utils/file'
|
||||||
import { SwarmFile } from '../../utils/SwarmFile'
|
import { SwarmFile } from '../../utils/SwarmFile'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
maximumSizeInBytes: number
|
uploadOrigin: UploadOrigin
|
||||||
|
showHelp: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_FILE_SIZE = 1_000_000_000 // 1 gigabyte
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
createStyles({
|
createStyles({
|
||||||
areaWrapper: { position: 'relative', marginBottom: theme.spacing(2) },
|
areaWrapper: { position: 'relative', marginBottom: theme.spacing(2) },
|
||||||
@@ -44,8 +48,8 @@ const useStyles = makeStyles((theme: Theme) =>
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export function UploadArea({ maximumSizeInBytes }: Props): ReactElement {
|
export function UploadArea({ uploadOrigin, showHelp }: Props): ReactElement {
|
||||||
const { setFiles } = useContext(Context)
|
const { setFiles, setUploadOrigin } = useContext(Context)
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const { enqueueSnackbar } = useSnackbar()
|
const { enqueueSnackbar } = useSnackbar()
|
||||||
@@ -110,6 +114,7 @@ export function UploadArea({ maximumSizeInBytes }: Props): ReactElement {
|
|||||||
setFiles(swarmFiles)
|
setFiles(swarmFiles)
|
||||||
|
|
||||||
if (files.length) {
|
if (files.length) {
|
||||||
|
setUploadOrigin(uploadOrigin)
|
||||||
history.push(ROUTES.UPLOAD_IN_PROGRESS)
|
history.push(ROUTES.UPLOAD_IN_PROGRESS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,7 +128,7 @@ export function UploadArea({ maximumSizeInBytes }: Props): ReactElement {
|
|||||||
dropzoneClass={classes.dropzone}
|
dropzoneClass={classes.dropzone}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
filesLimit={1e9}
|
filesLimit={1e9}
|
||||||
maxFileSize={maximumSizeInBytes}
|
maxFileSize={MAX_FILE_SIZE}
|
||||||
showPreviews={false}
|
showPreviews={false}
|
||||||
/>
|
/>
|
||||||
<div className={classes.buttonWrapper}>
|
<div className={classes.buttonWrapper}>
|
||||||
@@ -138,10 +143,12 @@ export function UploadArea({ maximumSizeInBytes }: Props): ReactElement {
|
|||||||
</SwarmButton>
|
</SwarmButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Typography>
|
{showHelp && (
|
||||||
You can click the buttons above or simply drag and drop to add a file or folder. To upload a website to Swarm,
|
<DocumentationText>
|
||||||
make sure that your folder contains an “index.html” file.
|
You can click the buttons above or simply drag and drop to add a file or folder. To upload a website to Swarm,
|
||||||
</Typography>
|
make sure that your folder contains an “index.html” file.
|
||||||
|
</DocumentationText>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
import { ReactElement } from 'react'
|
import { ReactElement, useContext } from 'react'
|
||||||
import { History } from '../../components/History'
|
import { History } from '../../components/History'
|
||||||
|
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
|
||||||
|
import { Context as BeeContext } from '../../providers/Bee'
|
||||||
|
import { defaultUploadOrigin } from '../../providers/File'
|
||||||
import { HISTORY_KEYS } from '../../utils/local-storage'
|
import { HISTORY_KEYS } from '../../utils/local-storage'
|
||||||
import { FileNavigation } from './FileNavigation'
|
import { FileNavigation } from './FileNavigation'
|
||||||
import { UploadArea } from './UploadArea'
|
import { UploadArea } from './UploadArea'
|
||||||
|
|
||||||
const MAX_FILE_SIZE = 1_000_000_000 // 1 gigabyte
|
|
||||||
|
|
||||||
export function UploadLander(): ReactElement {
|
export function UploadLander(): ReactElement {
|
||||||
|
const { status } = useContext(BeeContext)
|
||||||
|
|
||||||
|
if (!status.all) return <TroubleshootConnectionCard />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FileNavigation active="UPLOAD" />
|
<FileNavigation active="UPLOAD" />
|
||||||
<UploadArea maximumSizeInBytes={MAX_FILE_SIZE} />
|
<UploadArea showHelp={true} uploadOrigin={defaultUploadOrigin} />
|
||||||
<History title="Upload History" localStorageKey={HISTORY_KEYS.UPLOAD_HISTORY} />
|
<History title="Upload History" localStorageKey={HISTORY_KEYS.UPLOAD_HISTORY} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,152 +0,0 @@
|
|||||||
import Button from '@material-ui/core/Button'
|
|
||||||
import CircularProgress from '@material-ui/core/CircularProgress'
|
|
||||||
import Dialog from '@material-ui/core/Dialog'
|
|
||||||
import DialogActions from '@material-ui/core/DialogActions'
|
|
||||||
import DialogContent from '@material-ui/core/DialogContent'
|
|
||||||
import DialogContentText from '@material-ui/core/DialogContentText'
|
|
||||||
import DialogTitle from '@material-ui/core/DialogTitle'
|
|
||||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
|
|
||||||
import BigNumber from 'bignumber.js'
|
|
||||||
import { Field, Form, Formik, FormikHelpers } from 'formik'
|
|
||||||
import { TextField } from 'formik-material-ui'
|
|
||||||
import { useSnackbar } from 'notistack'
|
|
||||||
import React, { ReactElement, useContext } from 'react'
|
|
||||||
import { Context as SettingsContext } from '../../providers/Settings'
|
|
||||||
import { Context } from '../../providers/Stamps'
|
|
||||||
|
|
||||||
interface FormValues {
|
|
||||||
depth?: string
|
|
||||||
amount?: string
|
|
||||||
label?: string
|
|
||||||
}
|
|
||||||
type FormErrors = Partial<FormValues>
|
|
||||||
const initialFormValues: FormValues = {
|
|
||||||
depth: '',
|
|
||||||
amount: '',
|
|
||||||
label: '',
|
|
||||||
}
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
|
||||||
createStyles({
|
|
||||||
wrapper: {
|
|
||||||
margin: theme.spacing(1),
|
|
||||||
position: 'relative',
|
|
||||||
},
|
|
||||||
field: {
|
|
||||||
marginTop: theme.spacing(1),
|
|
||||||
marginBottom: theme.spacing(1),
|
|
||||||
},
|
|
||||||
buttonProgress: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: '50%',
|
|
||||||
left: '50%',
|
|
||||||
marginTop: -12,
|
|
||||||
marginBottom: -12,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onClose: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CreatePostageStampModal({ onClose }: Props): ReactElement {
|
|
||||||
const classes = useStyles()
|
|
||||||
const { refresh } = useContext(Context)
|
|
||||||
const { beeDebugApi } = useContext(SettingsContext)
|
|
||||||
const { enqueueSnackbar } = useSnackbar()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Formik
|
|
||||||
initialValues={initialFormValues}
|
|
||||||
onSubmit={async (values: FormValues, actions: FormikHelpers<FormValues>) => {
|
|
||||||
try {
|
|
||||||
// This is really just a typeguard, the validation pretty much guarantees these will have the right values
|
|
||||||
if (!values.depth || !values.amount) return
|
|
||||||
|
|
||||||
if (!beeDebugApi) return
|
|
||||||
|
|
||||||
const amount = BigInt(values.amount)
|
|
||||||
const depth = Number.parseInt(values.depth)
|
|
||||||
const options = values.label ? { label: values.label } : undefined
|
|
||||||
await beeDebugApi.createPostageBatch(amount.toString(), depth, options)
|
|
||||||
actions.resetForm()
|
|
||||||
await refresh()
|
|
||||||
onClose()
|
|
||||||
} catch (e) {
|
|
||||||
enqueueSnackbar(`Error: ${(e as Error).message}`, { variant: 'error' })
|
|
||||||
actions.setSubmitting(false)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
validate={(values: FormValues) => {
|
|
||||||
const errors: FormErrors = {}
|
|
||||||
|
|
||||||
// Depth
|
|
||||||
if (!values.depth) errors.depth = 'Required field'
|
|
||||||
else {
|
|
||||||
const depth = new BigNumber(values.depth)
|
|
||||||
|
|
||||||
if (!depth.isInteger()) errors.depth = 'Depth must be an integer'
|
|
||||||
else if (depth.isLessThan(16)) errors.depth = 'Minimal depth is 16'
|
|
||||||
else if (depth.isGreaterThan(255)) errors.depth = 'Depth has to be at most 255'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Amount
|
|
||||||
if (!values.amount) errors.amount = 'Required field'
|
|
||||||
else {
|
|
||||||
const amount = new BigNumber(values.amount)
|
|
||||||
|
|
||||||
if (!amount.isInteger()) errors.amount = 'Amount must be an integer'
|
|
||||||
else if (amount.isLessThanOrEqualTo(0)) errors.amount = 'Amount must be greater than 0'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Label
|
|
||||||
if (values.label && !/^[0-9a-z]*$/i.test(values.label)) errors.label = 'Label must be an alphanumeric string'
|
|
||||||
|
|
||||||
return errors
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{({ submitForm, isValid, isSubmitting, values }) => (
|
|
||||||
<Form>
|
|
||||||
<Dialog open={true} onClose={onClose} aria-labelledby="form-dialog-title">
|
|
||||||
<DialogTitle id="form-dialog-title">Buy new postage stamp</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<Field
|
|
||||||
component={TextField}
|
|
||||||
required
|
|
||||||
name="depth"
|
|
||||||
autoFocus
|
|
||||||
label="Depth"
|
|
||||||
fullWidth
|
|
||||||
className={classes.field}
|
|
||||||
/>
|
|
||||||
<Field component={TextField} required name="amount" label="Amount" fullWidth className={classes.field} />
|
|
||||||
<Field component={TextField} name="label" label="Label" fullWidth className={classes.field} />
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={onClose} variant="contained">
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<div className={classes.wrapper}>
|
|
||||||
<Button
|
|
||||||
disabled={isSubmitting || !isValid || !values.amount || !values.depth}
|
|
||||||
type="submit"
|
|
||||||
variant="contained"
|
|
||||||
onClick={submitForm}
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
{isSubmitting && <CircularProgress size={24} className={classes.buttonProgress} />}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</DialogActions>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogContentText>
|
|
||||||
Please refer to the official Bee documentation to understand these values.
|
|
||||||
</DialogContentText>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
</Formik>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { ReactElement } from 'react'
|
||||||
|
import { useHistory } from 'react-router'
|
||||||
|
import { HistoryHeader } from '../../components/HistoryHeader'
|
||||||
|
import { ROUTES } from '../../routes'
|
||||||
|
import { PostageStampCreation } from './PostageStampCreation'
|
||||||
|
|
||||||
|
export function CreatePostageStampPage(): ReactElement {
|
||||||
|
const history = useHistory()
|
||||||
|
|
||||||
|
function onFinished() {
|
||||||
|
history.push(ROUTES.STAMPS)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<HistoryHeader>Buy new postage stamp</HistoryHeader>
|
||||||
|
<PostageStampCreation onFinished={onFinished} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import { Box, Grid, Typography } from '@material-ui/core'
|
||||||
|
import BigNumber from 'bignumber.js'
|
||||||
|
import { Form, Formik, FormikHelpers } from 'formik'
|
||||||
|
import { useSnackbar } from 'notistack'
|
||||||
|
import React, { ReactElement, useContext } from 'react'
|
||||||
|
import { Check } from 'react-feather'
|
||||||
|
import { SwarmButton } from '../../components/SwarmButton'
|
||||||
|
import { SwarmTextInput } from '../../components/SwarmTextInput'
|
||||||
|
import { Context as SettingsContext } from '../../providers/Settings'
|
||||||
|
import { Context } from '../../providers/Stamps'
|
||||||
|
import {
|
||||||
|
calculateStampPrice,
|
||||||
|
convertAmountToSeconds,
|
||||||
|
convertDepthToBytes,
|
||||||
|
formatBzz,
|
||||||
|
secondsToTimeString,
|
||||||
|
} from '../../utils'
|
||||||
|
import { getHumanReadableFileSize } from '../../utils/file'
|
||||||
|
|
||||||
|
interface FormValues {
|
||||||
|
depth?: string
|
||||||
|
amount?: string
|
||||||
|
label?: string
|
||||||
|
}
|
||||||
|
type FormErrors = Partial<FormValues>
|
||||||
|
const initialFormValues: FormValues = {
|
||||||
|
depth: '',
|
||||||
|
amount: '',
|
||||||
|
label: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onFinished: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PostageStampCreation({ onFinished }: Props): ReactElement {
|
||||||
|
const { refresh } = useContext(Context)
|
||||||
|
const { beeDebugApi } = useContext(SettingsContext)
|
||||||
|
const { enqueueSnackbar } = useSnackbar()
|
||||||
|
|
||||||
|
function getFileSize(depth: number): string {
|
||||||
|
if (isNaN(depth) || depth < 17 || depth > 255) {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
return `~${getHumanReadableFileSize(convertDepthToBytes(depth))}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTtl(amount: number): string {
|
||||||
|
if (isNaN(amount) || amount <= 0) {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
return secondsToTimeString(convertAmountToSeconds(amount))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPrice(depth: number, amount: number): string {
|
||||||
|
if (isNaN(amount) || amount <= 0 || isNaN(depth) || depth < 17 || depth > 255) {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
const price = calculateStampPrice(depth, amount)
|
||||||
|
|
||||||
|
return `${formatBzz(price)} BZZ`
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
initialValues={initialFormValues}
|
||||||
|
onSubmit={async (values: FormValues, actions: FormikHelpers<FormValues>) => {
|
||||||
|
try {
|
||||||
|
// This is really just a typeguard, the validation pretty much guarantees these will have the right values
|
||||||
|
if (!values.depth || !values.amount) return
|
||||||
|
|
||||||
|
if (!beeDebugApi) return
|
||||||
|
|
||||||
|
const amount = BigInt(values.amount)
|
||||||
|
const depth = Number.parseInt(values.depth)
|
||||||
|
const options = values.label ? { label: values.label } : undefined
|
||||||
|
await beeDebugApi.createPostageBatch(amount.toString(), depth, options)
|
||||||
|
actions.resetForm()
|
||||||
|
await refresh()
|
||||||
|
onFinished()
|
||||||
|
} catch (e) {
|
||||||
|
enqueueSnackbar(`Error: ${(e as Error).message}`, { variant: 'error' })
|
||||||
|
actions.setSubmitting(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
validate={(values: FormValues) => {
|
||||||
|
const errors: FormErrors = {}
|
||||||
|
|
||||||
|
// Depth
|
||||||
|
if (!values.depth) errors.depth = 'Required field'
|
||||||
|
else {
|
||||||
|
const depth = new BigNumber(values.depth)
|
||||||
|
|
||||||
|
if (!depth.isInteger()) errors.depth = 'Depth must be an integer'
|
||||||
|
else if (depth.isLessThan(16)) errors.depth = 'Minimal depth is 16'
|
||||||
|
else if (depth.isGreaterThan(255)) errors.depth = 'Depth has to be at most 255'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Amount
|
||||||
|
if (!values.amount) errors.amount = 'Required field'
|
||||||
|
else {
|
||||||
|
const amount = new BigNumber(values.amount)
|
||||||
|
|
||||||
|
if (!amount.isInteger()) errors.amount = 'Amount must be an integer'
|
||||||
|
else if (amount.isLessThanOrEqualTo(0)) errors.amount = 'Amount must be greater than 0'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Label
|
||||||
|
if (values.label && !/^[0-9a-z]*$/i.test(values.label)) errors.label = 'Label must be an alphanumeric string'
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ submitForm, isValid, isSubmitting, values }) => (
|
||||||
|
<Form>
|
||||||
|
<Box mb={2}>
|
||||||
|
<SwarmTextInput name="depth" label="Depth" formik />
|
||||||
|
<Box mt={0.25} sx={{ bgcolor: '#f6f6f6' }} p={2}>
|
||||||
|
<Grid container justifyContent="space-between">
|
||||||
|
<Typography>Corresponding file size</Typography>
|
||||||
|
<Typography>{getFileSize(parseInt(values.depth || '0', 10))}</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box mb={2}>
|
||||||
|
<SwarmTextInput name="amount" label="Amount" formik />
|
||||||
|
<Box mt={0.25} sx={{ bgcolor: '#f6f6f6' }} p={2}>
|
||||||
|
<Grid container justifyContent="space-between">
|
||||||
|
<Typography>Corresponding TTL (Time to live)</Typography>
|
||||||
|
<Typography>{getTtl(parseInt(values.amount || '0', 10))}</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box mb={2}>
|
||||||
|
<SwarmTextInput name="label" label="Label" optional formik />
|
||||||
|
</Box>
|
||||||
|
<Box mb={4} sx={{ bgcolor: '#fcf2e8' }} p={2}>
|
||||||
|
<Grid container justifyContent="space-between">
|
||||||
|
<Typography>Indicative Price</Typography>
|
||||||
|
<Typography>{getPrice(parseInt(values.depth || '0', 10), parseInt(values.amount || '0', 10))}</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
<SwarmButton
|
||||||
|
disabled={isSubmitting || !isValid || !values.amount || !values.depth}
|
||||||
|
onClick={submitForm}
|
||||||
|
iconType={Check}
|
||||||
|
loading={isSubmitting}
|
||||||
|
>
|
||||||
|
Buy New Stamp
|
||||||
|
</SwarmButton>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import React, { ReactElement, useContext } from 'react'
|
||||||
|
import { SwarmSelect } from '../../components/SwarmSelect'
|
||||||
|
import { Context, EnrichedPostageBatch } from '../../providers/Stamps'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onSelect: (stamp: EnrichedPostageBatch) => void
|
||||||
|
defaultValue?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PostageStampSelector({ onSelect, defaultValue }: Props): ReactElement {
|
||||||
|
const { stamps } = useContext(Context)
|
||||||
|
|
||||||
|
function onChange(stampId: string) {
|
||||||
|
if (!stamps) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const stamp = stamps.find(x => x.batchID === stampId)
|
||||||
|
|
||||||
|
if (stamp) {
|
||||||
|
onSelect(stamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SwarmSelect
|
||||||
|
options={(stamps || []).map(x => ({ label: x.batchID.slice(0, 8), value: x.batchID }))}
|
||||||
|
onChange={event => onChange(event.target.value as string)}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createStyles, FormControl, makeStyles, MenuItem, Select, Theme } from '@material-ui/core'
|
import { createStyles, makeStyles, Theme } from '@material-ui/core'
|
||||||
import Button from '@material-ui/core/Button'
|
import Button from '@material-ui/core/Button'
|
||||||
import Dialog from '@material-ui/core/Dialog'
|
import Dialog from '@material-ui/core/Dialog'
|
||||||
import DialogContent from '@material-ui/core/DialogContent'
|
import DialogContent from '@material-ui/core/DialogContent'
|
||||||
@@ -6,6 +6,7 @@ import DialogTitle from '@material-ui/core/DialogTitle'
|
|||||||
import { Check, Clear } from '@material-ui/icons'
|
import { Check, Clear } from '@material-ui/icons'
|
||||||
import React, { ReactElement, useState } from 'react'
|
import React, { ReactElement, useState } from 'react'
|
||||||
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
|
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
|
||||||
|
import { SwarmSelect } from '../../components/SwarmSelect'
|
||||||
import { EnrichedPostageBatch } from '../../providers/Stamps'
|
import { EnrichedPostageBatch } from '../../providers/Stamps'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -26,14 +27,6 @@ const useStyles = makeStyles((theme: Theme) =>
|
|||||||
color: '#606060',
|
color: '#606060',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
select: {
|
|
||||||
background: theme.palette.background.paper,
|
|
||||||
borderRadius: 0,
|
|
||||||
border: 0,
|
|
||||||
},
|
|
||||||
option: {
|
|
||||||
height: '52px',
|
|
||||||
},
|
|
||||||
hint: {
|
hint: {
|
||||||
marginBottom: '16px',
|
marginBottom: '16px',
|
||||||
},
|
},
|
||||||
@@ -72,21 +65,10 @@ export function SelectPostageStampModal({ stamps, onSelect, onClose }: Props): R
|
|||||||
Select postage stamp
|
Select postage stamp
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<FormControl fullWidth>
|
<SwarmSelect
|
||||||
<Select
|
options={stamps.map(x => ({ label: x.batchID, value: x.batchID }))}
|
||||||
onChange={event => onChange(event.target.value as string)}
|
onChange={event => onChange(event.target.value as string)}
|
||||||
fullWidth
|
/>
|
||||||
variant="outlined"
|
|
||||||
className={classes.select}
|
|
||||||
defaultValue=""
|
|
||||||
>
|
|
||||||
{stamps.map(x => (
|
|
||||||
<MenuItem key={x.batchID} value={x.batchID} className={classes.option}>
|
|
||||||
{x.batchID.slice(0, 8)}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<ExpandableListItemActions>
|
<ExpandableListItemActions>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import type { ReactElement } from 'react'
|
import type { ReactElement } from 'react'
|
||||||
import ExpandableElement from '../../components/ExpandableElement'
|
import ExpandableElement from '../../components/ExpandableElement'
|
||||||
import ExpandableList from '../../components/ExpandableList'
|
import ExpandableList from '../../components/ExpandableList'
|
||||||
|
import ExpandableListItem from '../../components/ExpandableListItem'
|
||||||
import ExpandableListItemKey from '../../components/ExpandableListItemKey'
|
import ExpandableListItemKey from '../../components/ExpandableListItemKey'
|
||||||
import { EnrichedPostageBatch } from '../../providers/Stamps'
|
import { EnrichedPostageBatch } from '../../providers/Stamps'
|
||||||
|
import { getHumanReadableFileSize } from '../../utils/file'
|
||||||
import { PostageStamp } from './PostageStamp'
|
import { PostageStamp } from './PostageStamp'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -17,7 +19,19 @@ function StampsTable({ postageStamps }: Props): ReactElement | null {
|
|||||||
{postageStamps.map(stamp => (
|
{postageStamps.map(stamp => (
|
||||||
<ExpandableElement
|
<ExpandableElement
|
||||||
key={stamp.batchID}
|
key={stamp.batchID}
|
||||||
expandable={<ExpandableListItemKey label="Batch ID" value={stamp.batchID} />}
|
expandable={
|
||||||
|
<>
|
||||||
|
<ExpandableListItemKey label="Batch ID" value={stamp.batchID} />
|
||||||
|
<ExpandableListItem label="Depth" value={String(stamp.depth)} />
|
||||||
|
<ExpandableListItem
|
||||||
|
label="Capacity"
|
||||||
|
value={`${getHumanReadableFileSize(2 ** stamp.depth * 4096 * stamp.usage)} / ${getHumanReadableFileSize(
|
||||||
|
2 ** stamp.depth * 4096,
|
||||||
|
)}`}
|
||||||
|
/>
|
||||||
|
<ExpandableListItem label="Amount" value={parseInt(stamp.amount, 10).toLocaleString()} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<PostageStamp stamp={stamp} shorten={true} />
|
<PostageStamp stamp={stamp} shorten={true} />
|
||||||
</ExpandableElement>
|
</ExpandableElement>
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { CircularProgress, Container } from '@material-ui/core'
|
import { CircularProgress, Container } from '@material-ui/core'
|
||||||
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
||||||
import { ReactElement, useContext, useEffect, useState } from 'react'
|
import { ReactElement, useContext, useEffect } from 'react'
|
||||||
import { PlusSquare } from 'react-feather'
|
import { PlusSquare } from 'react-feather'
|
||||||
|
import { useHistory } from 'react-router'
|
||||||
import { SwarmButton } from '../../components/SwarmButton'
|
import { SwarmButton } from '../../components/SwarmButton'
|
||||||
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
|
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
|
||||||
import { Context as BeeContext } from '../../providers/Bee'
|
import { Context as BeeContext } from '../../providers/Bee'
|
||||||
import { Context as StampsContext } from '../../providers/Stamps'
|
import { Context as StampsContext } from '../../providers/Stamps'
|
||||||
import { CreatePostageStampModal } from './CreatePostageStampModal'
|
import { ROUTES } from '../../routes'
|
||||||
import StampsTable from './StampsTable'
|
import StampsTable from './StampsTable'
|
||||||
|
|
||||||
const useStyles = makeStyles(() =>
|
const useStyles = makeStyles(() =>
|
||||||
@@ -28,7 +29,7 @@ const useStyles = makeStyles(() =>
|
|||||||
export default function Stamp(): ReactElement {
|
export default function Stamp(): ReactElement {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
const [isBuyingStamp, setBuyingStamp] = useState(false)
|
const history = useHistory()
|
||||||
|
|
||||||
const { stamps, isLoading, error, start, stop } = useContext(StampsContext)
|
const { stamps, isLoading, error, start, stop } = useContext(StampsContext)
|
||||||
const { status } = useContext(BeeContext)
|
const { status } = useContext(BeeContext)
|
||||||
@@ -42,6 +43,10 @@ export default function Stamp(): ReactElement {
|
|||||||
|
|
||||||
if (!status.all) return <TroubleshootConnectionCard />
|
if (!status.all) return <TroubleshootConnectionCard />
|
||||||
|
|
||||||
|
function navigateToNewStamp() {
|
||||||
|
history.push(ROUTES.STAMPS_NEW)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.root}>
|
<div className={classes.root}>
|
||||||
{error && (
|
{error && (
|
||||||
@@ -52,9 +57,7 @@ export default function Stamp(): ReactElement {
|
|||||||
{!error && (
|
{!error && (
|
||||||
<>
|
<>
|
||||||
<div className={classes.actions}>
|
<div className={classes.actions}>
|
||||||
{isBuyingStamp ? <CreatePostageStampModal onClose={() => setBuyingStamp(false)} /> : null}
|
<SwarmButton onClick={navigateToNewStamp} iconType={PlusSquare}>
|
||||||
|
|
||||||
<SwarmButton onClick={() => setBuyingStamp(true)} iconType={PlusSquare}>
|
|
||||||
Buy New Postage Stamp
|
Buy New Postage Stamp
|
||||||
</SwarmButton>
|
</SwarmButton>
|
||||||
<div style={{ height: '5px' }}>{isLoading && <CircularProgress />}</div>
|
<div style={{ height: '5px' }}>{isLoading && <CircularProgress />}</div>
|
||||||
|
|||||||
+15
-1
@@ -3,6 +3,7 @@ import type {
|
|||||||
Health,
|
Health,
|
||||||
LastChequesResponse,
|
LastChequesResponse,
|
||||||
NodeAddresses,
|
NodeAddresses,
|
||||||
|
NodesInfo,
|
||||||
Peer,
|
Peer,
|
||||||
Topology,
|
Topology,
|
||||||
} from '@ethersphere/bee-js'
|
} from '@ethersphere/bee-js'
|
||||||
@@ -35,6 +36,7 @@ interface ContextInterface {
|
|||||||
apiHealth: boolean
|
apiHealth: boolean
|
||||||
debugApiHealth: Health | null
|
debugApiHealth: Health | null
|
||||||
nodeAddresses: NodeAddresses | null
|
nodeAddresses: NodeAddresses | null
|
||||||
|
nodeInfo: NodesInfo | null
|
||||||
topology: Topology | null
|
topology: Topology | null
|
||||||
chequebookAddress: ChequebookAddressResponse | null
|
chequebookAddress: ChequebookAddressResponse | null
|
||||||
peers: Peer[] | null
|
peers: Peer[] | null
|
||||||
@@ -72,6 +74,7 @@ const initialValues: ContextInterface = {
|
|||||||
apiHealth: false,
|
apiHealth: false,
|
||||||
debugApiHealth: null,
|
debugApiHealth: null,
|
||||||
nodeAddresses: null,
|
nodeAddresses: null,
|
||||||
|
nodeInfo: null,
|
||||||
topology: null,
|
topology: null,
|
||||||
chequebookAddress: null,
|
chequebookAddress: null,
|
||||||
peers: null,
|
peers: null,
|
||||||
@@ -98,6 +101,7 @@ interface Props {
|
|||||||
function getStatus(
|
function getStatus(
|
||||||
debugApiHealth: Health | null,
|
debugApiHealth: Health | null,
|
||||||
nodeAddresses: NodeAddresses | null,
|
nodeAddresses: NodeAddresses | null,
|
||||||
|
nodeInfo: NodesInfo | null,
|
||||||
apiHealth: boolean,
|
apiHealth: boolean,
|
||||||
topology: Topology | null,
|
topology: Topology | null,
|
||||||
chequebookAddress: ChequebookAddressResponse | null,
|
chequebookAddress: ChequebookAddressResponse | null,
|
||||||
@@ -105,7 +109,7 @@ function getStatus(
|
|||||||
error: Error | null,
|
error: Error | null,
|
||||||
): Status {
|
): Status {
|
||||||
// FIXME: `devMode` is a temporary workaround to be able to develop with only one node
|
// FIXME: `devMode` is a temporary workaround to be able to develop with only one node
|
||||||
const devMode = startedInDevMode || Boolean(process.env.REACT_APP_DEV_MODE)
|
const devMode = startedInDevMode || Boolean(process.env.REACT_APP_DEV_MODE) || nodeInfo?.beeMode === 'dev'
|
||||||
const status = {
|
const status = {
|
||||||
version: Boolean(
|
version: Boolean(
|
||||||
debugApiHealth &&
|
debugApiHealth &&
|
||||||
@@ -132,6 +136,7 @@ export function Provider({ children }: Props): ReactElement {
|
|||||||
const [apiHealth, setApiHealth] = useState<boolean>(false)
|
const [apiHealth, setApiHealth] = useState<boolean>(false)
|
||||||
const [debugApiHealth, setDebugApiHealth] = useState<Health | null>(null)
|
const [debugApiHealth, setDebugApiHealth] = useState<Health | null>(null)
|
||||||
const [nodeAddresses, setNodeAddresses] = useState<NodeAddresses | null>(null)
|
const [nodeAddresses, setNodeAddresses] = useState<NodeAddresses | null>(null)
|
||||||
|
const [nodeInfo, setNodeInfo] = useState<NodesInfo | null>(null)
|
||||||
const [topology, setNodeTopology] = useState<Topology | null>(null)
|
const [topology, setNodeTopology] = useState<Topology | null>(null)
|
||||||
const [chequebookAddress, setChequebookAddress] = useState<ChequebookAddressResponse | null>(null)
|
const [chequebookAddress, setChequebookAddress] = useState<ChequebookAddressResponse | null>(null)
|
||||||
const [peers, setPeers] = useState<Peer[] | null>(null)
|
const [peers, setPeers] = useState<Peer[] | null>(null)
|
||||||
@@ -165,6 +170,7 @@ export function Provider({ children }: Props): ReactElement {
|
|||||||
setDebugApiHealth(null)
|
setDebugApiHealth(null)
|
||||||
setNodeAddresses(null)
|
setNodeAddresses(null)
|
||||||
setNodeTopology(null)
|
setNodeTopology(null)
|
||||||
|
setNodeInfo(null)
|
||||||
setPeers(null)
|
setPeers(null)
|
||||||
setChequebookAddress(null)
|
setChequebookAddress(null)
|
||||||
setChequebookBalance(null)
|
setChequebookBalance(null)
|
||||||
@@ -241,6 +247,12 @@ export function Provider({ children }: Props): ReactElement {
|
|||||||
.then(setNodeAddresses)
|
.then(setNodeAddresses)
|
||||||
.catch(() => setNodeAddresses(null)),
|
.catch(() => setNodeAddresses(null)),
|
||||||
|
|
||||||
|
// NodeInfo
|
||||||
|
beeDebugApi
|
||||||
|
.getNodeInfo()
|
||||||
|
.then(setNodeInfo)
|
||||||
|
.catch(() => setNodeInfo(null)),
|
||||||
|
|
||||||
// Network Topology
|
// Network Topology
|
||||||
beeDebugApi
|
beeDebugApi
|
||||||
.getTopology()
|
.getTopology()
|
||||||
@@ -312,6 +324,7 @@ export function Provider({ children }: Props): ReactElement {
|
|||||||
status: getStatus(
|
status: getStatus(
|
||||||
debugApiHealth,
|
debugApiHealth,
|
||||||
nodeAddresses,
|
nodeAddresses,
|
||||||
|
nodeInfo,
|
||||||
apiHealth,
|
apiHealth,
|
||||||
topology,
|
topology,
|
||||||
chequebookAddress,
|
chequebookAddress,
|
||||||
@@ -333,6 +346,7 @@ export function Provider({ children }: Props): ReactElement {
|
|||||||
apiHealth,
|
apiHealth,
|
||||||
debugApiHealth,
|
debugApiHealth,
|
||||||
nodeAddresses,
|
nodeAddresses,
|
||||||
|
nodeInfo,
|
||||||
topology,
|
topology,
|
||||||
chequebookAddress,
|
chequebookAddress,
|
||||||
peers,
|
peers,
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { createContext, ReactChild, ReactElement, useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
export type IdentityType = 'V3' | 'PRIVATE_KEY'
|
||||||
|
|
||||||
|
export interface Identity {
|
||||||
|
uuid: string
|
||||||
|
name: string
|
||||||
|
feedHash?: string
|
||||||
|
identity: string
|
||||||
|
address: string
|
||||||
|
type: IdentityType
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ContextInterface {
|
||||||
|
identities: Identity[]
|
||||||
|
setIdentities: (identities: Identity[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialValues: ContextInterface = {
|
||||||
|
identities: [],
|
||||||
|
setIdentities: () => {}, // eslint-disable-line
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Context = createContext<ContextInterface>(initialValues)
|
||||||
|
export const Consumer = Context.Consumer
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactChild
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Provider({ children }: Props): ReactElement {
|
||||||
|
const [identities, setIdentities] = useState<Identity[]>(initialValues.identities)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
setIdentities(JSON.parse(localStorage.getItem('feeds') || '[]'))
|
||||||
|
} catch {
|
||||||
|
setIdentities([])
|
||||||
|
}
|
||||||
|
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
return <Context.Provider value={{ identities, setIdentities }}>{children}</Context.Provider>
|
||||||
|
}
|
||||||
+13
-2
@@ -1,14 +1,24 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||||
|
|
||||||
import { createContext, ReactChild, ReactElement, useState } from 'react'
|
import { createContext, ReactChild, ReactElement, useState } from 'react'
|
||||||
import { SwarmFile } from '../utils/SwarmFile'
|
import { SwarmFile } from '../utils/SwarmFile'
|
||||||
|
|
||||||
|
export type UploadOrigin = { origin: 'UPLOAD' | 'FEED'; uuid?: string }
|
||||||
|
|
||||||
|
export const defaultUploadOrigin: UploadOrigin = { origin: 'UPLOAD' }
|
||||||
|
|
||||||
interface ContextInterface {
|
interface ContextInterface {
|
||||||
files: SwarmFile[]
|
files: SwarmFile[]
|
||||||
setFiles: (files: SwarmFile[]) => void
|
setFiles: (files: SwarmFile[]) => void
|
||||||
|
uploadOrigin: UploadOrigin
|
||||||
|
setUploadOrigin: (uploadOrigin: UploadOrigin) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialValues: ContextInterface = {
|
const initialValues: ContextInterface = {
|
||||||
files: [],
|
files: [],
|
||||||
setFiles: () => {}, // eslint-disable-line
|
setFiles: () => {},
|
||||||
|
uploadOrigin: defaultUploadOrigin,
|
||||||
|
setUploadOrigin: () => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Context = createContext<ContextInterface>(initialValues)
|
export const Context = createContext<ContextInterface>(initialValues)
|
||||||
@@ -20,6 +30,7 @@ interface Props {
|
|||||||
|
|
||||||
export function Provider({ children }: Props): ReactElement {
|
export function Provider({ children }: Props): ReactElement {
|
||||||
const [files, setFiles] = useState<SwarmFile[]>(initialValues.files)
|
const [files, setFiles] = useState<SwarmFile[]>(initialValues.files)
|
||||||
|
const [uploadOrigin, setUploadOrigin] = useState<UploadOrigin>(initialValues.uploadOrigin)
|
||||||
|
|
||||||
return <Context.Provider value={{ files, setFiles }}>{children}</Context.Provider>
|
return <Context.Provider value={{ files, setFiles, uploadOrigin, setUploadOrigin }}>{children}</Context.Provider>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import type { ReactElement } from 'react'
|
import type { ReactElement } from 'react'
|
||||||
import { Route, Switch } from 'react-router-dom'
|
import { Route, Switch } from 'react-router-dom'
|
||||||
import Accounting from './pages/accounting'
|
import Accounting from './pages/accounting'
|
||||||
|
import Feeds from './pages/feeds'
|
||||||
|
import CreateNewFeed from './pages/feeds/CreateNewFeed'
|
||||||
|
import { FeedSubpage } from './pages/feeds/FeedSubpage'
|
||||||
|
import UpdateFeed from './pages/feeds/UpdateFeed'
|
||||||
import { Download } from './pages/files/Download'
|
import { Download } from './pages/files/Download'
|
||||||
import { Share } from './pages/files/Share'
|
import { Share } from './pages/files/Share'
|
||||||
import { Upload } from './pages/files/Upload'
|
import { Upload } from './pages/files/Upload'
|
||||||
@@ -8,6 +12,7 @@ import { UploadLander } from './pages/files/UploadLander'
|
|||||||
import Info from './pages/info'
|
import Info from './pages/info'
|
||||||
import Settings from './pages/settings'
|
import Settings from './pages/settings'
|
||||||
import Stamps from './pages/stamps'
|
import Stamps from './pages/stamps'
|
||||||
|
import { CreatePostageStampPage } from './pages/stamps/CreatePostageStampPage'
|
||||||
import Status from './pages/status'
|
import Status from './pages/status'
|
||||||
|
|
||||||
export enum ROUTES {
|
export enum ROUTES {
|
||||||
@@ -20,7 +25,12 @@ export enum ROUTES {
|
|||||||
ACCOUNTING = '/accounting',
|
ACCOUNTING = '/accounting',
|
||||||
SETTINGS = '/settings',
|
SETTINGS = '/settings',
|
||||||
STAMPS = '/stamps',
|
STAMPS = '/stamps',
|
||||||
|
STAMPS_NEW = '/stamps/new',
|
||||||
STATUS = '/status',
|
STATUS = '/status',
|
||||||
|
FEEDS = '/feeds',
|
||||||
|
FEEDS_NEW = '/feeds/new',
|
||||||
|
FEEDS_UPDATE = '/feeds/update/:hash',
|
||||||
|
FEEDS_PAGE = '/feeds/:uuid',
|
||||||
}
|
}
|
||||||
|
|
||||||
const BaseRouter = (): ReactElement => (
|
const BaseRouter = (): ReactElement => (
|
||||||
@@ -32,7 +42,12 @@ const BaseRouter = (): ReactElement => (
|
|||||||
<Route exact path={ROUTES.ACCOUNTING} component={Accounting} />
|
<Route exact path={ROUTES.ACCOUNTING} component={Accounting} />
|
||||||
<Route exact path={ROUTES.SETTINGS} component={Settings} />
|
<Route exact path={ROUTES.SETTINGS} component={Settings} />
|
||||||
<Route exact path={ROUTES.STAMPS} component={Stamps} />
|
<Route exact path={ROUTES.STAMPS} component={Stamps} />
|
||||||
|
<Route exact path={ROUTES.STAMPS_NEW} component={CreatePostageStampPage} />
|
||||||
<Route exact path={ROUTES.STATUS} component={Status} />
|
<Route exact path={ROUTES.STATUS} component={Status} />
|
||||||
|
<Route exact path={ROUTES.FEEDS} component={Feeds} />
|
||||||
|
<Route exact path={ROUTES.FEEDS_NEW} component={CreateNewFeed} />
|
||||||
|
<Route exact path={ROUTES.FEEDS_UPDATE} component={UpdateFeed} />
|
||||||
|
<Route exact path={ROUTES.FEEDS_PAGE} component={FeedSubpage} />
|
||||||
<Route path={ROUTES.INFO} component={Info} />
|
<Route path={ROUTES.INFO} component={Info} />
|
||||||
</Switch>
|
</Switch>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -30,6 +30,18 @@ export function detectIndexHtml(files: SwarmFile[]): string | false {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getHumanReadableFileSize(bytes: number): string {
|
export function getHumanReadableFileSize(bytes: number): string {
|
||||||
|
if (bytes >= 1e15) {
|
||||||
|
return (bytes / 1e15).toFixed(2) + ' PB'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes >= 1e12) {
|
||||||
|
return (bytes / 1e12).toFixed(2) + ' TB'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes >= 1e9) {
|
||||||
|
return (bytes / 1e9).toFixed(2) + ' GB'
|
||||||
|
}
|
||||||
|
|
||||||
if (bytes >= 1e6) {
|
if (bytes >= 1e6) {
|
||||||
return (bytes / 1e6).toFixed(2) + ' MB'
|
return (bytes / 1e6).toFixed(2) + ' MB'
|
||||||
}
|
}
|
||||||
@@ -65,6 +77,10 @@ export function convertManifestToFiles(files: Record<string, string>): SwarmFile
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getAssetNameFromFiles(files: SwarmFile[]): string {
|
export function getAssetNameFromFiles(files: SwarmFile[]): string {
|
||||||
|
if (!files.length) {
|
||||||
|
return 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
if (files.length === 1) {
|
if (files.length === 1) {
|
||||||
return files[0].name
|
return files[0].name
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { Bee, Reference } from '@ethersphere/bee-js'
|
||||||
|
import Wallet from 'ethereumjs-wallet'
|
||||||
|
import { uuidV4 } from '.'
|
||||||
|
import { Identity, IdentityType } from '../providers/Feeds'
|
||||||
|
|
||||||
|
export function generateWallet(): Wallet {
|
||||||
|
const buffer = new Uint8Array(32)
|
||||||
|
crypto.getRandomValues(buffer)
|
||||||
|
const wallet = new Wallet(Buffer.from(buffer))
|
||||||
|
|
||||||
|
return wallet
|
||||||
|
}
|
||||||
|
|
||||||
|
export function persistIdentity(identities: Identity[], identity: Identity): void {
|
||||||
|
const existingIndex = identities.findIndex(x => x.uuid === identity.uuid)
|
||||||
|
|
||||||
|
if (existingIndex !== -1) {
|
||||||
|
identities.splice(existingIndex, 1)
|
||||||
|
}
|
||||||
|
identities.unshift(identity)
|
||||||
|
localStorage.setItem('feeds', JSON.stringify(identities))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function persistIdentitiesWithoutUpdate(identities: Identity[]): void {
|
||||||
|
localStorage.setItem('feeds', JSON.stringify(identities))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function convertWalletToIdentity(
|
||||||
|
identity: Wallet,
|
||||||
|
type: IdentityType,
|
||||||
|
name: string,
|
||||||
|
password?: string,
|
||||||
|
): Promise<Identity> {
|
||||||
|
if (type === 'V3' && !password) {
|
||||||
|
throw Error('V3 passwords require password')
|
||||||
|
}
|
||||||
|
|
||||||
|
const identityString =
|
||||||
|
type === 'PRIVATE_KEY' ? identity.getPrivateKeyString() : await identity.toV3String(password as string)
|
||||||
|
|
||||||
|
return {
|
||||||
|
uuid: uuidV4(),
|
||||||
|
name,
|
||||||
|
type: password ? 'V3' : 'PRIVATE_KEY',
|
||||||
|
address: identity.getAddressString(),
|
||||||
|
identity: identityString,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function importIdentity(name: string, data: string): Promise<Identity | null> {
|
||||||
|
if (data.length === 64) {
|
||||||
|
const wallet = await getWallet('PRIVATE_KEY', data)
|
||||||
|
|
||||||
|
return {
|
||||||
|
uuid: uuidV4(),
|
||||||
|
name,
|
||||||
|
type: 'PRIVATE_KEY',
|
||||||
|
identity: data,
|
||||||
|
address: wallet.getAddressString(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length === 66 && data.toLowerCase().startsWith('0x')) {
|
||||||
|
const wallet = await getWallet('PRIVATE_KEY', data.slice(2))
|
||||||
|
|
||||||
|
return { uuid: uuidV4(), name, type: 'PRIVATE_KEY', identity: data, address: wallet.getAddressString() }
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { address } = JSON.parse(data)
|
||||||
|
|
||||||
|
return { uuid: uuidV4(), name, type: 'V3', identity: data, address }
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWalletFromIdentity(identity: Identity, password?: string): Promise<Wallet> {
|
||||||
|
return getWallet(identity.type, identity.identity, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getWallet(type: IdentityType, data: string, password?: string): Promise<Wallet> {
|
||||||
|
return type === 'PRIVATE_KEY'
|
||||||
|
? Wallet.fromPrivateKey(Buffer.from(trimHexString(data), 'hex'))
|
||||||
|
: await Wallet.fromV3(data, password as string)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateFeed(
|
||||||
|
beeApi: Bee,
|
||||||
|
identity: Identity,
|
||||||
|
hash: string,
|
||||||
|
stamp: string,
|
||||||
|
password?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const wallet = await getWalletFromIdentity(identity, password)
|
||||||
|
|
||||||
|
if (!identity.feedHash) {
|
||||||
|
identity.feedHash = await beeApi.createFeedManifest(stamp, 'sequence', '00'.repeat(32), wallet.getAddressString())
|
||||||
|
}
|
||||||
|
|
||||||
|
const writer = beeApi.makeFeedWriter('sequence', '00'.repeat(32), wallet.getPrivateKeyString())
|
||||||
|
await writer.upload(stamp, hash as Reference)
|
||||||
|
}
|
||||||
|
|
||||||
|
function trimHexString(string: string): string {
|
||||||
|
if (string.toLowerCase().startsWith('0x')) {
|
||||||
|
return string.slice(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string
|
||||||
|
}
|
||||||
@@ -112,3 +112,80 @@ export function extractSwarmHash(string: string): string | null {
|
|||||||
|
|
||||||
return (matches && matches[0]) || null
|
return (matches && matches[0]) || null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function uuidV4(): string {
|
||||||
|
const pattern = '10000000-1000-4000-8000-100000000000'
|
||||||
|
|
||||||
|
return pattern.replace(/[018]/g, (s: string) => {
|
||||||
|
const c = parseInt(s, 10)
|
||||||
|
|
||||||
|
return (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatEnum(string: string): string {
|
||||||
|
return (string.charAt(0).toUpperCase() + string.slice(1).toLowerCase()).replaceAll('_', ' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function secondsToTimeString(seconds: number): string {
|
||||||
|
let unit = seconds
|
||||||
|
|
||||||
|
if (unit < 120) {
|
||||||
|
return `${seconds} seconds`
|
||||||
|
}
|
||||||
|
unit /= 60
|
||||||
|
|
||||||
|
if (unit < 120) {
|
||||||
|
return `${Math.round(unit)} minutes`
|
||||||
|
}
|
||||||
|
unit /= 60
|
||||||
|
|
||||||
|
if (unit < 48) {
|
||||||
|
return `${Math.round(unit)} hours`
|
||||||
|
}
|
||||||
|
unit /= 24
|
||||||
|
|
||||||
|
if (unit < 14) {
|
||||||
|
return `${Math.round(unit)} days`
|
||||||
|
}
|
||||||
|
unit /= 7
|
||||||
|
|
||||||
|
if (unit < 52) {
|
||||||
|
return `${Math.round(unit)} weeks`
|
||||||
|
}
|
||||||
|
unit /= 52
|
||||||
|
|
||||||
|
return `${unit.toFixed(1)} years`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatBzz(amount: number): string {
|
||||||
|
const asString = amount.toFixed(16)
|
||||||
|
|
||||||
|
let indexOfSignificantDigit = -1
|
||||||
|
let reachedDecimalPoint = false
|
||||||
|
|
||||||
|
for (let i = 0; i < asString.length; i++) {
|
||||||
|
const char = asString[i]
|
||||||
|
|
||||||
|
if (char === '.') {
|
||||||
|
reachedDecimalPoint = true
|
||||||
|
} else if (reachedDecimalPoint && char !== '0') {
|
||||||
|
indexOfSignificantDigit = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return asString.slice(0, indexOfSignificantDigit + 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertDepthToBytes(depth: number): number {
|
||||||
|
return 2 ** depth * 4096
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertAmountToSeconds(amount: number): number {
|
||||||
|
return amount / 10 / 1
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateStampPrice(depth: number, amount: number): number {
|
||||||
|
return (amount * 2 ** (depth - 16) * 2) / 1e16
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user