feat: add experimental fdp (#681)
* feat: add experimental fdp * ci: update swarm-actions to v1 * fix: fix eslint violations * refactor: decaf
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import { FdpStorage } from '@fairdatasociety/fdp-storage'
|
||||
import { Checkbox, InputBase, Typography } from '@material-ui/core'
|
||||
import { useSnackbar } from 'notistack'
|
||||
import { useEffect, useState } from 'react'
|
||||
import RegisterIcon from 'remixicon-react/AddBoxLineIcon'
|
||||
import LoginIcon from 'remixicon-react/LoginBoxLineIcon'
|
||||
import { SwarmButton } from '../../components/SwarmButton'
|
||||
import { Horizontal } from './Horizontal'
|
||||
import { Vertical } from './Vertical'
|
||||
|
||||
interface Props {
|
||||
fdp: FdpStorage
|
||||
onSuccessfulLogin: () => void
|
||||
}
|
||||
|
||||
export function FdpLogin({ fdp, onSuccessfulLogin }: Props) {
|
||||
const [username, setUsername] = useState<string>('')
|
||||
const [password, setPassword] = useState<string>('')
|
||||
const [remember, setRemember] = useState<boolean>(false)
|
||||
const [sepolia, setSepolia] = useState<string>('https://sepolia.drpc.org')
|
||||
const { enqueueSnackbar } = useSnackbar()
|
||||
|
||||
const inputStyle = { background: 'white', padding: '2px 8px', width: '100%' }
|
||||
|
||||
useEffect(() => {
|
||||
const storedSepolia = localStorage.getItem('sepolia')
|
||||
|
||||
if (storedSepolia) {
|
||||
setSepolia(storedSepolia)
|
||||
}
|
||||
const fdpCredentials = localStorage.getItem('fdpCredentials')
|
||||
|
||||
if (fdpCredentials) {
|
||||
const { username, password } = JSON.parse(fdpCredentials)
|
||||
setUsername(username)
|
||||
setPassword(password)
|
||||
setRemember(true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
async function onLogin() {
|
||||
localStorage.setItem('sepolia', sepolia)
|
||||
|
||||
if (remember) {
|
||||
localStorage.setItem('fdpCredentials', JSON.stringify({ username, password }))
|
||||
} else {
|
||||
localStorage.removeItem('fdpCredentials')
|
||||
}
|
||||
enqueueSnackbar('Logging in...', { variant: 'info' })
|
||||
try {
|
||||
await fdp.account.login(username, password)
|
||||
enqueueSnackbar('Logged in successfully', { variant: 'success' })
|
||||
onSuccessfulLogin()
|
||||
} catch {
|
||||
enqueueSnackbar('Login failed', { variant: 'error' })
|
||||
} finally {
|
||||
setUsername('')
|
||||
setPassword('')
|
||||
setRemember(false)
|
||||
}
|
||||
}
|
||||
|
||||
function onRegister() {
|
||||
window.open('https://create.fairdatasociety.org/', '_blank')
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
maxWidth: '500px',
|
||||
margin: 'auto',
|
||||
}}
|
||||
>
|
||||
<Vertical gap={16} full>
|
||||
<Vertical gap={8} left full>
|
||||
<Typography variant="body2">Sepolia JSON RPC</Typography>
|
||||
<InputBase value={sepolia} onChange={e => setSepolia(e.target.value)} style={inputStyle} />
|
||||
</Vertical>
|
||||
<Vertical gap={8} left full>
|
||||
<Typography variant="body2">Username</Typography>
|
||||
<InputBase value={username} onChange={e => setUsername(e.target.value)} style={inputStyle} />
|
||||
</Vertical>
|
||||
<Vertical gap={8} left full>
|
||||
<Typography variant="body2">Password</Typography>
|
||||
<InputBase value={password} onChange={e => setPassword(e.target.value)} style={inputStyle} type="password" />
|
||||
</Vertical>
|
||||
<Vertical gap={8} left full>
|
||||
<Horizontal>
|
||||
<Checkbox checked={remember} onChange={e => setRemember(e.target.checked)} />
|
||||
<Typography variant="body2">Remember me</Typography>
|
||||
</Horizontal>
|
||||
</Vertical>
|
||||
<Vertical left full>
|
||||
<Horizontal gap={4}>
|
||||
<SwarmButton iconType={LoginIcon} onClick={onLogin}>
|
||||
Login
|
||||
</SwarmButton>
|
||||
<SwarmButton iconType={RegisterIcon} onClick={onRegister}>
|
||||
Registration
|
||||
</SwarmButton>
|
||||
</Horizontal>
|
||||
</Vertical>
|
||||
</Vertical>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { FdpStorage } from '@fairdatasociety/fdp-storage'
|
||||
import { useState } from 'react'
|
||||
import { CafeReactFs } from '../../react-fs/CafeReactFs'
|
||||
import { FsItem, FsItemType } from '../../react-fs/CafeReactType'
|
||||
import { joinUrl } from '../../react-fs/Utility'
|
||||
|
||||
interface Props {
|
||||
fdp: FdpStorage
|
||||
name: string
|
||||
}
|
||||
|
||||
export function FdpPod({ fdp, name }: Props) {
|
||||
const [reloader, setReloader] = useState(0)
|
||||
|
||||
function reload() {
|
||||
setReloader(reloader + 1)
|
||||
}
|
||||
|
||||
return (
|
||||
<CafeReactFs
|
||||
rootAlias={`/${name}`}
|
||||
backgroundColor="#ffffff"
|
||||
reloader={reloader}
|
||||
onDeleteFile={async (path: string) => {
|
||||
await fdp.file.delete(name, path)
|
||||
reload()
|
||||
}}
|
||||
onDeleteDirectory={async (path: string) => {
|
||||
await fdp.directory.delete(name, path)
|
||||
reload()
|
||||
}}
|
||||
onUpload={(path: string) => {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.multiple = true
|
||||
input.click()
|
||||
|
||||
return new Promise<void>(resolve => {
|
||||
input.onchange = async () => {
|
||||
if (!input.files || !input.files.length) {
|
||||
resolve()
|
||||
|
||||
return
|
||||
}
|
||||
for (const file of Array.from(input.files)) {
|
||||
const data = await file.arrayBuffer()
|
||||
await fdp.file.uploadData(name, joinUrl(path, file.name), new Uint8Array(data))
|
||||
}
|
||||
reload()
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}}
|
||||
onCreateDirectory={async (path: string) => {
|
||||
// eslint-disable-next-line no-alert
|
||||
const newDirectoryName = prompt('Directory name')
|
||||
|
||||
if (!newDirectoryName) {
|
||||
return
|
||||
}
|
||||
await fdp.directory.create(name, joinUrl(path, newDirectoryName))
|
||||
reload()
|
||||
}}
|
||||
// eslint-disable-next-line require-await
|
||||
onSync={async () => {
|
||||
setReloader(reloader + 1)
|
||||
}}
|
||||
download={async (path: string) => {
|
||||
const data = await fdp.file.downloadData(name, path)
|
||||
const url = URL.createObjectURL(new Blob([data]))
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = path.split('/').pop() || 'Untitled'
|
||||
a.click()
|
||||
}}
|
||||
list={async (path: string) => {
|
||||
const fdpResponse = await fdp.directory.read(name, path)
|
||||
const items: FsItem[] = []
|
||||
for (const directory of fdpResponse.directories) {
|
||||
items.push({
|
||||
name: directory.name,
|
||||
$type: FsItemType.DIRECTORY,
|
||||
id: directory.name,
|
||||
})
|
||||
}
|
||||
for (const file of fdpResponse.files) {
|
||||
items.push({
|
||||
name: file.name,
|
||||
$type: FsItemType.FILE,
|
||||
id: file.name,
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { FdpStorage } from '@fairdatasociety/fdp-storage'
|
||||
import { Pod } from '@fairdatasociety/fdp-storage/dist/pod/types'
|
||||
import { CircularProgress, Typography } from '@material-ui/core'
|
||||
import { FdpPod } from './FdpPod'
|
||||
import { Vertical } from './Vertical'
|
||||
|
||||
interface Props {
|
||||
fdp: FdpStorage
|
||||
pods: Pod[]
|
||||
loadingPods: boolean
|
||||
}
|
||||
|
||||
export function FdpPods({ fdp, pods, loadingPods }: Props) {
|
||||
if (loadingPods) {
|
||||
return (
|
||||
<Vertical gap={32} full>
|
||||
<CircularProgress />
|
||||
<Typography>Loading your pods...</Typography>
|
||||
</Vertical>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Vertical gap={16} full left>
|
||||
{pods.map(pod => (
|
||||
<FdpPod key={pod.index} fdp={fdp} name={pod.name} />
|
||||
))}
|
||||
</Vertical>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
interface Props {
|
||||
children: React.ReactNode
|
||||
p?: string
|
||||
gap?: number
|
||||
between?: boolean
|
||||
background?: string
|
||||
}
|
||||
|
||||
export function Horizontal({ children, p = '0', gap = 8, between, background }: Props) {
|
||||
const style = {
|
||||
display: 'flex',
|
||||
flexDirection: 'row' as 'row', //eslint-disable-line
|
||||
alignItems: 'center',
|
||||
justifyContent: between ? 'space-between' : 'flex-start',
|
||||
gap: `${gap}px`,
|
||||
padding: p,
|
||||
background,
|
||||
width: between ? '100%' : 'auto',
|
||||
}
|
||||
|
||||
return <div style={style}>{children}</div>
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
interface Props {
|
||||
children: React.ReactNode
|
||||
p?: number
|
||||
gap?: number
|
||||
left?: boolean
|
||||
full?: boolean
|
||||
}
|
||||
|
||||
export function Vertical({ children, p = 0, gap = 0, left = false, full = false }: Props) {
|
||||
const style = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as 'column', //eslint-disable-line
|
||||
alignItems: left ? 'flex-start' : 'center',
|
||||
gap: `${gap}px`,
|
||||
width: full ? '100%' : 'auto',
|
||||
padding: `${p}px`,
|
||||
}
|
||||
|
||||
return <div style={style}>{children}</div>
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import { Bee } from '@ethersphere/bee-js'
|
||||
import { FdpStorage } from '@fairdatasociety/fdp-storage'
|
||||
import { Pod } from '@fairdatasociety/fdp-storage/dist/pod/types'
|
||||
import { CircularProgress, Typography } from '@material-ui/core'
|
||||
import { useSnackbar } from 'notistack'
|
||||
import { ReactElement, useEffect, useState } from 'react'
|
||||
import ImportIcon from 'remixicon-react/AddBoxLineIcon'
|
||||
import PlusCircle from 'remixicon-react/AddCircleLineIcon'
|
||||
import { SwarmButton } from '../../components/SwarmButton'
|
||||
import { joinUrl } from '../../react-fs/Utility'
|
||||
import { ManifestJs } from '../../utils/manifest'
|
||||
import { FdpLogin } from './FdpLogin'
|
||||
import { FdpPods } from './FdpPods'
|
||||
import { Horizontal } from './Horizontal'
|
||||
import { Vertical } from './Vertical'
|
||||
|
||||
async function makeFdp(): Promise<FdpStorage | null> {
|
||||
const bee = new Bee('http://localhost:1633')
|
||||
const sepolia = localStorage.getItem('sepolia') ?? 'https://sepolia.drpc.org'
|
||||
const postageBatches = await bee.getAllPostageBatch()
|
||||
const usableBatches = postageBatches.filter(batch => batch.usable)
|
||||
const highestCapacityBatch = usableBatches.length ? usableBatches.reduce((a, b) => (a.depth > b.depth ? a : b)) : null
|
||||
|
||||
if (!highestCapacityBatch) {
|
||||
return null
|
||||
}
|
||||
|
||||
return new FdpStorage('http://localhost:1633', highestCapacityBatch.batchID, {
|
||||
ensOptions: {
|
||||
rpcUrl: sepolia,
|
||||
contractAddresses: {
|
||||
ensRegistry: '0x42a96D45d787685ac4b36292d218B106Fb39be7F',
|
||||
fdsRegistrar: '0xFBF00389140C00384d88d458239833E3231a7414',
|
||||
nameResolver: '0xE20ECe6Ea93c4edE41e4d3B973f6679F1E89986A',
|
||||
publicResolver: '0xC904989B579c2B216A75723688C784038AA99B56',
|
||||
reverseResolver: '0xbDC8D98d3cbFd68EA9c165E1f15Df6e77A2ae0C5',
|
||||
},
|
||||
gasEstimation: 1,
|
||||
performChecks: true,
|
||||
},
|
||||
providerOptions: {
|
||||
url: sepolia,
|
||||
},
|
||||
ensDomain: 'fds',
|
||||
})
|
||||
}
|
||||
|
||||
export default function FDP(): ReactElement {
|
||||
const [fdp, setFdp] = useState<FdpStorage | null>(null)
|
||||
const [pods, setPods] = useState<Pod[]>([])
|
||||
const [loggedIn, setLoggedIn] = useState<boolean>(false)
|
||||
const [loadingPods, setLoadingPods] = useState<boolean>(false)
|
||||
const [creatingPod, setCreatingPod] = useState<boolean>(false)
|
||||
const { enqueueSnackbar } = useSnackbar()
|
||||
|
||||
useEffect(() => {
|
||||
makeFdp().then(fdp => {
|
||||
if (!fdp) {
|
||||
enqueueSnackbar('FDP could not be initialized. Do you have a postage batch?', { variant: 'error' })
|
||||
}
|
||||
setFdp(fdp)
|
||||
})
|
||||
}, [enqueueSnackbar])
|
||||
|
||||
useEffect(() => {
|
||||
if (fdp && loggedIn) {
|
||||
setLoadingPods(true)
|
||||
fdp.personalStorage.list().then(pods => {
|
||||
setPods(pods.pods)
|
||||
setLoadingPods(false)
|
||||
})
|
||||
}
|
||||
}, [fdp, loggedIn])
|
||||
|
||||
function onSuccessfulLogin() {
|
||||
setLoggedIn(true)
|
||||
}
|
||||
|
||||
function onCreatePod() {
|
||||
if (!fdp) {
|
||||
return
|
||||
}
|
||||
|
||||
if (loadingPods || creatingPod) {
|
||||
enqueueSnackbar('Please wait until the pods are loaded', { variant: 'info' })
|
||||
|
||||
return
|
||||
}
|
||||
// eslint-disable-next-line no-alert
|
||||
const name = prompt('Enter a name for the new pod')
|
||||
|
||||
if (name) {
|
||||
setCreatingPod(true)
|
||||
fdp.personalStorage.create(name).then(() => {
|
||||
fdp.personalStorage.list().then(pods => {
|
||||
setPods(pods.pods)
|
||||
setCreatingPod(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function onImportPod() {
|
||||
if (!fdp) {
|
||||
return
|
||||
}
|
||||
|
||||
if (loadingPods || creatingPod) {
|
||||
enqueueSnackbar('Please wait until the pods are loaded', { variant: 'info' })
|
||||
|
||||
return
|
||||
}
|
||||
// eslint-disable-next-line no-alert
|
||||
const name = prompt('Enter a name for the new pod')
|
||||
// eslint-disable-next-line no-alert
|
||||
const importHash = prompt('Enter the Swarm reference')
|
||||
|
||||
if (!name || !importHash) {
|
||||
return
|
||||
}
|
||||
setCreatingPod(true)
|
||||
const bee = new Bee('http://localhost:1633')
|
||||
const manifestJs = new ManifestJs(bee)
|
||||
const entries = await manifestJs.getHashes(importHash)
|
||||
await fdp.personalStorage.create(name)
|
||||
for (const [path, hash] of Object.entries(entries)) {
|
||||
await fdp.file.uploadData(name, joinUrl('/', path), await bee.downloadData(hash))
|
||||
}
|
||||
const pods = await fdp.personalStorage.list()
|
||||
setPods(pods.pods)
|
||||
setCreatingPod(false)
|
||||
}
|
||||
|
||||
if (!fdp) {
|
||||
return <CircularProgress />
|
||||
}
|
||||
|
||||
return (
|
||||
<Vertical gap={32} full left>
|
||||
<Horizontal between>
|
||||
<Typography variant="h1">Files</Typography>
|
||||
{loggedIn && (
|
||||
<Horizontal gap={4}>
|
||||
<SwarmButton onClick={onCreatePod} iconType={PlusCircle}>
|
||||
Create
|
||||
</SwarmButton>
|
||||
<SwarmButton onClick={onImportPod} iconType={ImportIcon}>
|
||||
Import
|
||||
</SwarmButton>
|
||||
</Horizontal>
|
||||
)}
|
||||
</Horizontal>
|
||||
{!loggedIn && <FdpLogin fdp={fdp} onSuccessfulLogin={onSuccessfulLogin} />}
|
||||
{loggedIn && <FdpPods fdp={fdp} pods={pods} loadingPods={loadingPods || creatingPod} />}
|
||||
{loggedIn && !loadingPods && !creatingPod && pods.length === 0 && (
|
||||
<Typography>
|
||||
<strong>You do not have any pods yet.</strong> Get started by clicking the Create or Import button on the top
|
||||
right.
|
||||
</Typography>
|
||||
)}
|
||||
</Vertical>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user