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:
@@ -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 Dialog from '@material-ui/core/Dialog'
|
||||
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 React, { ReactElement, useState } from 'react'
|
||||
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
|
||||
import { SwarmSelect } from '../../components/SwarmSelect'
|
||||
import { EnrichedPostageBatch } from '../../providers/Stamps'
|
||||
|
||||
interface Props {
|
||||
@@ -26,14 +27,6 @@ const useStyles = makeStyles((theme: Theme) =>
|
||||
color: '#606060',
|
||||
textAlign: 'center',
|
||||
},
|
||||
select: {
|
||||
background: theme.palette.background.paper,
|
||||
borderRadius: 0,
|
||||
border: 0,
|
||||
},
|
||||
option: {
|
||||
height: '52px',
|
||||
},
|
||||
hint: {
|
||||
marginBottom: '16px',
|
||||
},
|
||||
@@ -72,21 +65,10 @@ export function SelectPostageStampModal({ stamps, onSelect, onClose }: Props): R
|
||||
Select postage stamp
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<FormControl fullWidth>
|
||||
<Select
|
||||
onChange={event => onChange(event.target.value as string)}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
className={classes.select}
|
||||
defaultValue=""
|
||||
>
|
||||
{stamps.map(x => (
|
||||
<MenuItem key={x.batchID} value={x.batchID} className={classes.option}>
|
||||
{x.batchID.slice(0, 8)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<SwarmSelect
|
||||
options={stamps.map(x => ({ label: x.batchID, value: x.batchID }))}
|
||||
onChange={event => onChange(event.target.value as string)}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogContent>
|
||||
<ExpandableListItemActions>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import ExpandableElement from '../../components/ExpandableElement'
|
||||
import ExpandableList from '../../components/ExpandableList'
|
||||
import ExpandableListItem from '../../components/ExpandableListItem'
|
||||
import ExpandableListItemKey from '../../components/ExpandableListItemKey'
|
||||
import { EnrichedPostageBatch } from '../../providers/Stamps'
|
||||
import { getHumanReadableFileSize } from '../../utils/file'
|
||||
import { PostageStamp } from './PostageStamp'
|
||||
|
||||
interface Props {
|
||||
@@ -17,7 +19,19 @@ function StampsTable({ postageStamps }: Props): ReactElement | null {
|
||||
{postageStamps.map(stamp => (
|
||||
<ExpandableElement
|
||||
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} />
|
||||
</ExpandableElement>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { CircularProgress, Container } from '@material-ui/core'
|
||||
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 { useHistory } from 'react-router'
|
||||
import { SwarmButton } from '../../components/SwarmButton'
|
||||
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
|
||||
import { Context as BeeContext } from '../../providers/Bee'
|
||||
import { Context as StampsContext } from '../../providers/Stamps'
|
||||
import { CreatePostageStampModal } from './CreatePostageStampModal'
|
||||
import { ROUTES } from '../../routes'
|
||||
import StampsTable from './StampsTable'
|
||||
|
||||
const useStyles = makeStyles(() =>
|
||||
@@ -28,7 +29,7 @@ const useStyles = makeStyles(() =>
|
||||
export default function Stamp(): ReactElement {
|
||||
const classes = useStyles()
|
||||
|
||||
const [isBuyingStamp, setBuyingStamp] = useState(false)
|
||||
const history = useHistory()
|
||||
|
||||
const { stamps, isLoading, error, start, stop } = useContext(StampsContext)
|
||||
const { status } = useContext(BeeContext)
|
||||
@@ -42,6 +43,10 @@ export default function Stamp(): ReactElement {
|
||||
|
||||
if (!status.all) return <TroubleshootConnectionCard />
|
||||
|
||||
function navigateToNewStamp() {
|
||||
history.push(ROUTES.STAMPS_NEW)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
{error && (
|
||||
@@ -52,9 +57,7 @@ export default function Stamp(): ReactElement {
|
||||
{!error && (
|
||||
<>
|
||||
<div className={classes.actions}>
|
||||
{isBuyingStamp ? <CreatePostageStampModal onClose={() => setBuyingStamp(false)} /> : null}
|
||||
|
||||
<SwarmButton onClick={() => setBuyingStamp(true)} iconType={PlusSquare}>
|
||||
<SwarmButton onClick={navigateToNewStamp} iconType={PlusSquare}>
|
||||
Buy New Postage Stamp
|
||||
</SwarmButton>
|
||||
<div style={{ height: '5px' }}>{isLoading && <CircularProgress />}</div>
|
||||
|
||||
Reference in New Issue
Block a user