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:
Cafe137
2021-12-21 10:58:54 +01:00
committed by GitHub
parent d7c59a1495
commit 25b65c3fb7
46 changed files with 4354 additions and 6378 deletions
+15 -4
View File
@@ -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 { DocumentationText } from '../../components/DocumentationText'
import ExpandableListItemKey from '../../components/ExpandableListItemKey'
import ExpandableListItemLink from '../../components/ExpandableListItemLink'
import { detectIndexHtml } from '../../utils/file'
import { SwarmFile } from '../../utils/SwarmFile'
interface Props {
files: SwarmFile[]
hash: string
}
export function AssetSummary({ hash }: Props): ReactElement {
export function AssetSummary({ files, hash }: Props): ReactElement {
return (
<>
<Box mb={4}>
<ExpandableListItemKey label="Swarm hash" value={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>
<Typography>
<DocumentationText>
The Swarm Gateway is graciously provided by the Swarm Foundation. This service is under development and provided
for testing purposes only. Learn more at{' '}
<a href="https://gateway.ethswarm.org/">https://gateway.ethswarm.org/</a>.
</Typography>
</DocumentationText>
</>
)
}
+15 -1
View File
@@ -5,6 +5,7 @@ import { ReactElement, useContext, useState } from 'react'
import { useHistory } from 'react-router-dom'
import ExpandableListItemInput from '../../components/ExpandableListItemInput'
import { History } from '../../components/History'
import { Context, defaultUploadOrigin } from '../../providers/File'
import { Context as SettingsContext } from '../../providers/Settings'
import { ROUTES } from '../../routes'
import { extractSwarmHash } from '../../utils'
@@ -16,6 +17,8 @@ export function Download(): ReactElement {
const { beeApi } = useContext(SettingsContext)
const [referenceError, setReferenceError] = useState<string | undefined>(undefined)
const { setUploadOrigin } = useContext(Context)
const { enqueueSnackbar } = useSnackbar()
const history = useHistory()
@@ -28,12 +31,21 @@ export function Download(): ReactElement {
}
async function onSwarmIdentifier(identifier: string) {
setLoading(true)
if (!beeApi) {
setLoading(false)
return
}
try {
const manifestJs = new ManifestJs(beeApi)
const feedIdentifier = await manifestJs.resolveFeedManifest(identifier)
if (feedIdentifier) {
identifier = feedIdentifier
}
const isManifest = await manifestJs.isManifest(identifier)
if (!isManifest) {
@@ -41,6 +53,7 @@ export function Download(): ReactElement {
}
const indexDocument = await manifestJs.getIndexDocumentPath(identifier)
putHistory(HISTORY_KEYS.DOWNLOAD_HISTORY, identifier, determineHistoryName(identifier, indexDocument))
setUploadOrigin(defaultUploadOrigin)
history.push(ROUTES.HASH.replace(':hash', identifier))
} catch (error: unknown) {
let message = typeof error === 'object' && error !== null && Reflect.get(error, 'message')
@@ -80,11 +93,12 @@ export function Download(): ReactElement {
onConfirm={value => onSwarmIdentifier(value)}
onChange={validateChange}
helperText={referenceError}
confirmLabel={'Search'}
confirmLabel={'Find'}
confirmLabelDisabled={Boolean(referenceError) || loading}
placeholder="e.g. 31fb0362b1a42536134c86bc58b97ac0244e5c6630beec3e27c2d1cecb38c605"
expandedOnly
mapperFn={value => recognizeSwarmHash(value)}
loading={loading}
/>
<History title="Download History" localStorageKey={HISTORY_KEYS.DOWNLOAD_HISTORY} />
</>
+31 -17
View File
@@ -1,32 +1,46 @@
import { Button } from '@material-ui/core'
import { Clear } from '@material-ui/icons'
import { Box, Grid } from '@material-ui/core'
import { ReactElement } from 'react'
import { Download, Link } from 'react-feather'
import { Bookmark, Download, Link, X } from 'react-feather'
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
import { SwarmButton } from '../../components/SwarmButton'
interface Props {
onOpen: () => void
onDownload: () => void
onCancel: () => void
onDownload: () => void
onUpdateFeed: () => void
hasIndexDocument: 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 (
<ExpandableListItemActions>
{hasIndexDocument && (
<SwarmButton onClick={onOpen} iconType={Link} disabled={loading}>
View Website
<Grid container justifyContent="space-between">
<ExpandableListItemActions>
{hasIndexDocument && (
<SwarmButton onClick={onOpen} iconType={Link} disabled={loading}>
View Website
</SwarmButton>
)}
<SwarmButton onClick={onDownload} iconType={Download} disabled={loading} loading={loading}>
Download
</SwarmButton>
)}
<SwarmButton onClick={onDownload} iconType={Download} disabled={loading} loading={loading}>
Download
</SwarmButton>
<Button onClick={onCancel} variant="contained" startIcon={<Clear />} disabled={loading}>
Close
</Button>
</ExpandableListItemActions>
<SwarmButton onClick={onCancel} iconType={X} disabled={loading} loading={loading} cancel>
Close
</SwarmButton>
</ExpandableListItemActions>
<Box mb={1} mr={1}>
<SwarmButton onClick={onUpdateFeed} iconType={Bookmark}>
Update Feed
</SwarmButton>
</Box>
</Grid>
)
}
+33 -5
View File
@@ -1,10 +1,14 @@
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 JSZip from 'jszip'
import { useSnackbar } from 'notistack'
import { ReactElement, useContext, useEffect, useState } from 'react'
import { RouteComponentProps, useHistory } from 'react-router-dom'
import { HistoryHeader } from '../../components/HistoryHeader'
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 { ROUTES } from '../../routes'
import { convertBeeFileToBrowserFile, convertManifestToFiles } from '../../utils/file'
@@ -21,17 +25,22 @@ interface MatchParams {
export function Share(props: RouteComponentProps<MatchParams>): ReactElement {
const { apiUrl, beeApi } = useContext(SettingsContext)
const { status } = useContext(BeeContext)
const reference = props.match.params.hash
const history = useHistory()
const { enqueueSnackbar } = useSnackbar()
const [loading, setLoading] = useState(true)
const [downloading, setDownloading] = useState(false)
const [files, setFiles] = useState<SwarmFile[]>([])
const [swarmEntries, setSwarmEntries] = useState<Record<string, string>>({})
const [indexDocument, setIndexDocument] = useState<string | null>(null)
const [notFound, setNotFound] = useState(false)
async function prepare() {
if (!beeApi) {
if (!beeApi || !status.all) {
return
}
@@ -39,7 +48,10 @@ export function Share(props: RouteComponentProps<MatchParams>): ReactElement {
const isManifest = await manifestJs.isManifest(reference)
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)
setSwarmEntries(entries)
@@ -67,9 +79,13 @@ export function Share(props: RouteComponentProps<MatchParams>): ReactElement {
}
}
function onUpdateFeed() {
history.push(ROUTES.FEEDS_UPDATE.replace(':hash', reference))
}
useEffect(() => {
setLoading(true)
prepare().then(() => {
prepare().finally(() => {
setLoading(false)
})
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -97,22 +113,34 @@ export function Share(props: RouteComponentProps<MatchParams>): ReactElement {
const assetName = shortenHash(reference)
if (!status.all) return <TroubleshootConnectionCard />
if (loading) {
return <Loading />
}
if (notFound) {
return (
<>
<HistoryHeader>Not Found</HistoryHeader>
<Typography>The specified hash is not found.</Typography>
</>
)
}
return (
<>
<Box mb={4}>
<AssetPreview files={files} assetName={assetName} />
</Box>
<Box mb={4}>
<AssetSummary hash={reference} />
<AssetSummary files={files} hash={reference} />
</Box>
<DownloadActionBar
onOpen={onOpen}
onCancel={onClose}
onDownload={onDownload}
onUpdateFeed={onUpdateFeed}
hasIndexDocument={Boolean(indexDocument && files.length > 1)}
loading={downloading}
/>
+107 -35
View File
@@ -1,41 +1,71 @@
import { Box } from '@material-ui/core'
import { useSnackbar } from 'notistack'
import { ReactElement, useContext, useEffect, useState } from 'react'
import { useHistory } from 'react-router-dom'
import { DocumentationText } from '../../components/DocumentationText'
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 SettingsContext } from '../../providers/Settings'
import { Context, EnrichedPostageBatch } from '../../providers/Stamps'
import { Context as StampsContext, EnrichedPostageBatch } from '../../providers/Stamps'
import { ROUTES } from '../../routes'
import { detectIndexHtml, getAssetNameFromFiles } from '../../utils/file'
import { persistIdentity, updateFeed } from '../../utils/identity'
import { HISTORY_KEYS, putHistory } from '../../utils/local-storage'
import { CreatePostageStampModal } from '../stamps/CreatePostageStampModal'
import { SelectPostageStampModal } from '../stamps/SelectPostageStampModal'
import { FeedPasswordDialog } from '../feeds/FeedPasswordDialog'
import { PostageStampCreation } from '../stamps/PostageStampCreation'
import { PostageStampSelector } from '../stamps/PostageStampSelector'
import { AssetPreview } from './AssetPreview'
import { StampPreview } from './StampPreview'
import { UploadActionBar } from './UploadActionBar'
export function Upload(): ReactElement {
const [isBuyingStamp, setBuyingStamp] = useState(false)
const [isSelectingStamp, setSelectingStamp] = useState(false)
const [step, setStep] = useState(0)
const [stampMode, setStampMode] = useState<'SELECT' | 'BUY'>('SELECT')
const [stamp, setStamp] = useState<EnrichedPostageBatch | null>(null)
const [isUploading, setUploading] = useState(false)
const [showPasswordPrompt, setShowPasswordPrompt] = useState(false)
const { stamps, refresh } = useContext(Context)
const { refresh } = useContext(StampsContext)
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 history = useHistory()
if (!files.length) {
setFiles([])
history.replace(ROUTES.UPLOAD)
}
useEffect(() => {
refresh()
}, []) // 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) {
return
}
@@ -48,7 +78,16 @@ export function Upload(): ReactElement {
.uploadFiles(stamp.batchID, files as unknown as File[], { indexDocument })
.then(hash => {
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 => {
enqueueSnackbar(`Error uploading: ${e.message}`, { variant: 'error' })
@@ -57,36 +96,69 @@ export function Upload(): ReactElement {
}
const reset = () => {
setStep(0)
setFiles([])
setStamp(null)
setUploading(false)
}
const onFeedPasswordGiven = (password: string) => {
uploadFiles(password)
}
return (
<>
<HistoryHeader>Upload</HistoryHeader>
{files.length && <AssetPreview files={files} />}
{stamp !== null ? <StampPreview stamp={stamp} /> : null}
{files.length && (
<UploadActionBar
canSelectStamp={stamps !== null && stamps.length > 0}
hasSelectedStamp={stamp !== null}
onCancel={reset}
onBuy={() => setBuyingStamp(true)}
onSelect={() => setSelectingStamp(true)}
onUpload={uploadFiles}
onClearStamp={() => setStamp(null)}
isUploading={isUploading}
{showPasswordPrompt && (
<FeedPasswordDialog
loading={isUploading}
feedName={(identity as Identity).name}
onCancel={() => setShowPasswordPrompt(false)}
onProceed={onFeedPasswordGiven}
/>
)}
{isBuyingStamp ? <CreatePostageStampModal onClose={() => setBuyingStamp(false)} /> : null}
{stamps && isSelectingStamp ? (
<SelectPostageStampModal
stamps={stamps}
onClose={() => setSelectingStamp(false)}
onSelect={stamp => setStamp(stamp)}
/>
) : null}
{identity && <HistoryHeader>{`Update "${identity.name}"`}</HistoryHeader>}
{!identity && <HistoryHeader>Upload</HistoryHeader>}
<Box mb={4}>
<ProgressIndicator steps={['Preview', 'Add postage stamp', 'Upload to node']} index={step} />
</Box>
{(step === 0 || step === 2) && <AssetPreview files={files} />}
{step === 1 && (
<>
<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}
/>
</>
)
}
+69 -50
View File
@@ -1,69 +1,88 @@
import { Button, Typography } from '@material-ui/core'
import { Clear } from '@material-ui/icons'
import { Box, Grid } from '@material-ui/core'
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 { SwarmButton } from '../../components/SwarmButton'
interface Props {
canSelectStamp: boolean
hasSelectedStamp: boolean
step: number
onUpload: () => void
onBuy: () => void
onSelect: () => void
onCancel: () => void
onClearStamp: () => void
onGoBack: () => void
onProceed: () => void
isUploading: boolean
hasStamp: boolean
uploadLabel: string
stampMode: 'BUY' | 'SELECT'
setStampMode: (mode: 'BUY' | 'SELECT') => void
}
export function UploadActionBar({
canSelectStamp,
hasSelectedStamp,
step,
onUpload,
onBuy,
onSelect,
onCancel,
onClearStamp,
onGoBack,
onProceed,
isUploading,
hasStamp,
uploadLabel,
stampMode,
setStampMode,
}: Props): ReactElement {
const showBuy = !hasSelectedStamp
const showSelect = canSelectStamp && !hasSelectedStamp
const showUpload = hasSelectedStamp
const showChange = canSelectStamp && hasSelectedStamp
if (step === 0) {
return (
<>
<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>
{showBuy ? (
<SwarmButton onClick={onBuy} iconType={PlusSquare}>
Buy New Postage Stamp
</SwarmButton>
) : null}
{showSelect ? (
<SwarmButton onClick={onSelect} iconType={Layers}>
Use Existing Postage Stamp
</SwarmButton>
) : null}
{showUpload ? (
<SwarmButton onClick={onUpload} iconType={Check} disabled={isUploading} loading={isUploading}>
Upload To Your Node
</SwarmButton>
) : null}
{showChange ? (
<SwarmButton onClick={onClearStamp} iconType={RefreshCcw} disabled={isUploading}>
Change Postage Stamp
</SwarmButton>
) : null}
<Button onClick={onCancel} variant="contained" startIcon={<Clear />}>
Cancel
</Button>
<SwarmButton onClick={onUpload} iconType={Check} disabled={isUploading} loading={isUploading}>
{uploadLabel}
</SwarmButton>
<SwarmButton onClick={onGoBack} iconType={ArrowLeft} disabled={isUploading} cancel>
Change Postage Stamp
</SwarmButton>
</ExpandableListItemActions>
{showSelect ? (
<Typography>
You need a postage stamp to upload. Please refer to the official Bee documentation to understand how postage
stamps work.
</Typography>
) : null}
</>
)
)
}
return <></>
}
+17 -10
View File
@@ -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 { useSnackbar } from 'notistack'
import { ReactElement, useContext, useState } from 'react'
import { FilePlus, FolderPlus, PlusCircle } from 'react-feather'
import { useHistory } from 'react-router-dom'
import { DocumentationText } from '../../components/DocumentationText'
import { SwarmButton } from '../../components/SwarmButton'
import { Context } from '../../providers/File'
import { Context, UploadOrigin } from '../../providers/File'
import { ROUTES } from '../../routes'
import { detectIndexHtml } from '../../utils/file'
import { SwarmFile } from '../../utils/SwarmFile'
interface Props {
maximumSizeInBytes: number
uploadOrigin: UploadOrigin
showHelp: boolean
}
const MAX_FILE_SIZE = 1_000_000_000 // 1 gigabyte
const useStyles = makeStyles((theme: Theme) =>
createStyles({
areaWrapper: { position: 'relative', marginBottom: theme.spacing(2) },
@@ -44,8 +48,8 @@ const useStyles = makeStyles((theme: Theme) =>
}),
)
export function UploadArea({ maximumSizeInBytes }: Props): ReactElement {
const { setFiles } = useContext(Context)
export function UploadArea({ uploadOrigin, showHelp }: Props): ReactElement {
const { setFiles, setUploadOrigin } = useContext(Context)
const classes = useStyles()
const history = useHistory()
const { enqueueSnackbar } = useSnackbar()
@@ -110,6 +114,7 @@ export function UploadArea({ maximumSizeInBytes }: Props): ReactElement {
setFiles(swarmFiles)
if (files.length) {
setUploadOrigin(uploadOrigin)
history.push(ROUTES.UPLOAD_IN_PROGRESS)
}
}
@@ -123,7 +128,7 @@ export function UploadArea({ maximumSizeInBytes }: Props): ReactElement {
dropzoneClass={classes.dropzone}
onChange={handleChange}
filesLimit={1e9}
maxFileSize={maximumSizeInBytes}
maxFileSize={MAX_FILE_SIZE}
showPreviews={false}
/>
<div className={classes.buttonWrapper}>
@@ -138,10 +143,12 @@ export function UploadArea({ maximumSizeInBytes }: Props): ReactElement {
</SwarmButton>
</div>
</div>
<Typography>
You can click the buttons above or simply drag and drop to add a file or folder. To upload a website to Swarm,
make sure that your folder contains an index.html file.
</Typography>
{showHelp && (
<DocumentationText>
You can click the buttons above or simply drag and drop to add a file or folder. To upload a website to Swarm,
make sure that your folder contains an index.html file.
</DocumentationText>
)}
</>
)
}
+9 -4
View File
@@ -1,16 +1,21 @@
import { ReactElement } from 'react'
import { ReactElement, useContext } from 'react'
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 { FileNavigation } from './FileNavigation'
import { UploadArea } from './UploadArea'
const MAX_FILE_SIZE = 1_000_000_000 // 1 gigabyte
export function UploadLander(): ReactElement {
const { status } = useContext(BeeContext)
if (!status.all) return <TroubleshootConnectionCard />
return (
<>
<FileNavigation active="UPLOAD" />
<UploadArea maximumSizeInBytes={MAX_FILE_SIZE} />
<UploadArea showHelp={true} uploadOrigin={defaultUploadOrigin} />
<History title="Upload History" localStorageKey={HISTORY_KEYS.UPLOAD_HISTORY} />
</>
)