diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 4f46b523..58c2fdca 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -70,7 +70,7 @@ jobs: run: npm run build:component - name: Create preview - uses: ethersphere/swarm-actions/pr-preview@v0 + uses: ethersphere/swarm-actions/pr-preview@v1 continue-on-error: true with: bee-url: https://unlimited.gateway.ethswarm.org @@ -79,7 +79,7 @@ jobs: headers: '${{ secrets.GATEWAY_AUTHORIZATION_HEADER }}' - name: Upload to testnet - uses: ethersphere/swarm-actions/upload-dir@v0 + uses: ethersphere/swarm-actions/upload-dir@v1 continue-on-error: true with: index-document: index.html diff --git a/package-lock.json b/package-lock.json index dc6fa874..bc67b9d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@ethersphere/bee-js": "^7.1.0", "@ethersphere/swarm-cid": "^0.1.0", + "@fairdatasociety/fdp-storage": "^0.19.0", "@material-ui/core": "4.12.3", "@material-ui/icons": "4.11.2", "@material-ui/lab": "4.0.0-alpha.57", @@ -3164,6 +3165,106 @@ "@ethersproject/strings": "^5.7.0" } }, + "node_modules/@fairdatasociety/fdp-contracts-js": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@fairdatasociety/fdp-contracts-js/-/fdp-contracts-js-3.12.0.tgz", + "integrity": "sha512-pfmRucv40GMGAMfXB8hFDRvdxkY5nX172dQFnWh4vGCS2iRKbz6p78cqnF8Xyu9lYSjtSVEWAnXOk9Yug6X5OQ==", + "license": "MIT", + "peerDependencies": { + "ethers": ">=5.6.4" + } + }, + "node_modules/@fairdatasociety/fdp-storage": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@fairdatasociety/fdp-storage/-/fdp-storage-0.19.0.tgz", + "integrity": "sha512-tN1mosanu4nAxLx+uxuRSdbSV+/HSgAIsakR1jsgJ2Lneycc4i9Yd7but2LFyOQ5okNh6TM5P6S4hlPUpWk/jQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@ethersphere/bee-js": "^6.2.0", + "@fairdatasociety/fdp-contracts-js": "^3.11.0", + "crypto-js": "^4.2.0", + "elliptic": "^6.5.4", + "ethers": "^5.5.2", + "js-sha3": "^0.9.2", + "pako": "^2.1.0" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=9.0.0" + } + }, + "node_modules/@fairdatasociety/fdp-storage/node_modules/@ethersphere/bee-js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@ethersphere/bee-js/-/bee-js-6.9.1.tgz", + "integrity": "sha512-aZaoD4Q9BH2jOPU049vIsN3N/RID/CTrXdpMb593PREU9S9PksZkxVPS95sgLWnUsOK1IzW9a8WtkOIjcRZswA==", + "license": "BSD-3-Clause", + "dependencies": { + "@ethersphere/swarm-cid": "^0.1.0", + "@types/readable-stream": "^2.3.13", + "axios": "^0.28.0", + "cafe-utility": "^15.0.2", + "elliptic": "^6.5.4", + "fetch-blob": "2.1.2", + "isomorphic-ws": "^4.0.1", + "js-sha3": "^0.8.0", + "semver": "^7.3.5", + "tar-js": "^0.3.0", + "web-streams-polyfill": "^4.0.0-beta.3", + "ws": "^8.7.0" + }, + "engines": { + "bee": "1.18.2-759f56f", + "beeApiVersion": "4.0.0", + "beeDebugApiVersion": "4.0.0", + "node": ">=14.0.0", + "npm": ">=6.0.0" + } + }, + "node_modules/@fairdatasociety/fdp-storage/node_modules/@ethersphere/bee-js/node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", + "license": "MIT" + }, + "node_modules/@fairdatasociety/fdp-storage/node_modules/cafe-utility": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/cafe-utility/-/cafe-utility-15.0.2.tgz", + "integrity": "sha512-TNKSfA/q/XRd86NwYtF5QImQB8U5n/hKZuWblYFgYW4aveHcSg2RGOfR3+xquXRXF7BCeNoAXe2/snWFKviPzw==", + "license": "MIT" + }, + "node_modules/@fairdatasociety/fdp-storage/node_modules/js-sha3": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.9.3.tgz", + "integrity": "sha512-BcJPCQeLg6WjEx3FE591wVAevlli8lxsxm9/FzV4HXkV49TmBH38Yvrpce6fjbADGMKFrBMGTqrVz3qPIZ88Gg==", + "license": "MIT" + }, + "node_modules/@fairdatasociety/fdp-storage/node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, + "node_modules/@fairdatasociety/fdp-storage/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.9.5", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", @@ -4910,8 +5011,7 @@ "node_modules/@types/node": { "version": "16.11.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.6.tgz", - "integrity": "sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w==", - "dev": true + "integrity": "sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w==" }, "node_modules/@types/parse-json": { "version": "4.0.0", @@ -5028,6 +5128,16 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz", "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==" }, + "node_modules/@types/readable-stream": { + "version": "2.3.15", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-2.3.15.tgz", + "integrity": "sha512-oM5JSKQCcICF1wvGgmecmHldZ48OZamtMxcGGVICOJA8o8cahXC1zEVAif8iwoc5j8etxFaRFnf095+CDsuoFQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "safe-buffer": "~5.1.1" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -7758,6 +7868,12 @@ "node": "*" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, "node_modules/crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", @@ -10179,6 +10295,20 @@ "pend": "~1.2.0" } }, + "node_modules/fetch-blob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-2.1.2.tgz", + "integrity": "sha512-YKqtUDwqLyfyMnmbw8XD6Q8j9i/HggKtPEI+pZ1+8bvheBu78biSmNaXWusx1TauGqtUUGx/cBb1mKdq2rLYow==", + "license": "MIT", + "engines": { + "node": "^10.17.0 || >=12.3.0" + }, + "peerDependenciesMeta": { + "domexception": { + "optional": true + } + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -18754,6 +18884,14 @@ "tar-stream": "^2.1.4" } }, + "node_modules/tar-js": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/tar-js/-/tar-js-0.3.0.tgz", + "integrity": "sha512-9uqP2hJUZNKRkwPDe5nXxXdzo6w+BFBPq9x/tyi5/U/DneuSesO/HMb0y5TeWpfcv49YDJTs7SrrZeeu8ZHWDA==", + "engines": { + "node": "*" + } + }, "node_modules/tar-stream": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", @@ -19603,6 +19741,15 @@ "minimalistic-assert": "^1.0.0" } }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0.tgz", + "integrity": "sha512-0zJXHRAYEjM2tUfZ2DiSOHAa2aw1tisnnhU3ufD57R8iefL+DcdJyRBRyJpG+NUimDgbTI/lH+gAE1PAvV3Cgw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/web-vitals": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.2.tgz", @@ -22639,6 +22786,75 @@ "@ethersproject/strings": "^5.7.0" } }, + "@fairdatasociety/fdp-contracts-js": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@fairdatasociety/fdp-contracts-js/-/fdp-contracts-js-3.12.0.tgz", + "integrity": "sha512-pfmRucv40GMGAMfXB8hFDRvdxkY5nX172dQFnWh4vGCS2iRKbz6p78cqnF8Xyu9lYSjtSVEWAnXOk9Yug6X5OQ==", + "requires": {} + }, + "@fairdatasociety/fdp-storage": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@fairdatasociety/fdp-storage/-/fdp-storage-0.19.0.tgz", + "integrity": "sha512-tN1mosanu4nAxLx+uxuRSdbSV+/HSgAIsakR1jsgJ2Lneycc4i9Yd7but2LFyOQ5okNh6TM5P6S4hlPUpWk/jQ==", + "requires": { + "@ethersphere/bee-js": "^6.2.0", + "@fairdatasociety/fdp-contracts-js": "^3.11.0", + "crypto-js": "^4.2.0", + "elliptic": "^6.5.4", + "ethers": "^5.5.2", + "js-sha3": "^0.9.2", + "pako": "^2.1.0" + }, + "dependencies": { + "@ethersphere/bee-js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@ethersphere/bee-js/-/bee-js-6.9.1.tgz", + "integrity": "sha512-aZaoD4Q9BH2jOPU049vIsN3N/RID/CTrXdpMb593PREU9S9PksZkxVPS95sgLWnUsOK1IzW9a8WtkOIjcRZswA==", + "requires": { + "@ethersphere/swarm-cid": "^0.1.0", + "@types/readable-stream": "^2.3.13", + "axios": "^0.28.0", + "cafe-utility": "^15.0.2", + "elliptic": "^6.5.4", + "fetch-blob": "2.1.2", + "isomorphic-ws": "^4.0.1", + "js-sha3": "^0.8.0", + "semver": "^7.3.5", + "tar-js": "^0.3.0", + "web-streams-polyfill": "^4.0.0-beta.3", + "ws": "^8.7.0" + }, + "dependencies": { + "js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + } + } + }, + "cafe-utility": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/cafe-utility/-/cafe-utility-15.0.2.tgz", + "integrity": "sha512-TNKSfA/q/XRd86NwYtF5QImQB8U5n/hKZuWblYFgYW4aveHcSg2RGOfR3+xquXRXF7BCeNoAXe2/snWFKviPzw==" + }, + "js-sha3": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.9.3.tgz", + "integrity": "sha512-BcJPCQeLg6WjEx3FE591wVAevlli8lxsxm9/FzV4HXkV49TmBH38Yvrpce6fjbADGMKFrBMGTqrVz3qPIZ88Gg==" + }, + "pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" + }, + "ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "requires": {} + } + } + }, "@humanwhocodes/config-array": { "version": "0.9.5", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", @@ -23913,8 +24129,7 @@ "@types/node": { "version": "16.11.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.6.tgz", - "integrity": "sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w==", - "dev": true + "integrity": "sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w==" }, "@types/parse-json": { "version": "4.0.0", @@ -24033,6 +24248,15 @@ "@types/react": "*" } }, + "@types/readable-stream": { + "version": "2.3.15", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-2.3.15.tgz", + "integrity": "sha512-oM5JSKQCcICF1wvGgmecmHldZ48OZamtMxcGGVICOJA8o8cahXC1zEVAif8iwoc5j8etxFaRFnf095+CDsuoFQ==", + "requires": { + "@types/node": "*", + "safe-buffer": "~5.1.1" + } + }, "@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -26165,6 +26389,11 @@ "randomfill": "^1.0.3" } }, + "crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, "crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", @@ -27989,6 +28218,11 @@ "pend": "~1.2.0" } }, + "fetch-blob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-2.1.2.tgz", + "integrity": "sha512-YKqtUDwqLyfyMnmbw8XD6Q8j9i/HggKtPEI+pZ1+8bvheBu78biSmNaXWusx1TauGqtUUGx/cBb1mKdq2rLYow==" + }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -34331,6 +34565,11 @@ "tar-stream": "^2.1.4" } }, + "tar-js": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/tar-js/-/tar-js-0.3.0.tgz", + "integrity": "sha512-9uqP2hJUZNKRkwPDe5nXxXdzo6w+BFBPq9x/tyi5/U/DneuSesO/HMb0y5TeWpfcv49YDJTs7SrrZeeu8ZHWDA==" + }, "tar-stream": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", @@ -34953,6 +35192,11 @@ "minimalistic-assert": "^1.0.0" } }, + "web-streams-polyfill": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0.tgz", + "integrity": "sha512-0zJXHRAYEjM2tUfZ2DiSOHAa2aw1tisnnhU3ufD57R8iefL+DcdJyRBRyJpG+NUimDgbTI/lH+gAE1PAvV3Cgw==" + }, "web-vitals": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.2.tgz", diff --git a/package.json b/package.json index 0dc5c22b..8ba6f221 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "dependencies": { "@ethersphere/bee-js": "^7.1.0", "@ethersphere/swarm-cid": "^0.1.0", + "@fairdatasociety/fdp-storage": "^0.19.0", "@material-ui/core": "4.12.3", "@material-ui/icons": "4.11.2", "@material-ui/lab": "4.0.0-alpha.57", diff --git a/src/components/SideBar.tsx b/src/components/SideBar.tsx index 7b41529c..bffb1cd7 100644 --- a/src/components/SideBar.tsx +++ b/src/components/SideBar.tsx @@ -7,6 +7,7 @@ import FilesIcon from 'remixicon-react/ArrowUpDownLineIcon' import DocsIcon from 'remixicon-react/BookOpenLineIcon' import ExternalLinkIcon from 'remixicon-react/ExternalLinkLineIcon' import GithubIcon from 'remixicon-react/GithubFillIcon' +import FdpIcon from 'remixicon-react/HardDrive2LineIcon' import HomeIcon from 'remixicon-react/Home3LineIcon' import SettingsIcon from 'remixicon-react/Settings2LineIcon' import AccountIcon from 'remixicon-react/Wallet3LineIcon' @@ -76,6 +77,12 @@ export default function SideBar(): ReactElement { path: ROUTES.INFO, icon: HomeIcon, }, + { + label: 'FDP', + path: ROUTES.FDP, + icon: FdpIcon, + pathMatcherSubstring: '/fdp', + }, { label: 'Files', path: nodeInfo?.beeMode === BeeModes.ULTRA_LIGHT ? ROUTES.DOWNLOAD : ROUTES.UPLOAD, diff --git a/src/pages/fdp/FdpLogin.tsx b/src/pages/fdp/FdpLogin.tsx new file mode 100644 index 00000000..11da2f09 --- /dev/null +++ b/src/pages/fdp/FdpLogin.tsx @@ -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('') + const [password, setPassword] = useState('') + const [remember, setRemember] = useState(false) + const [sepolia, setSepolia] = useState('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 ( +
+ + + Sepolia JSON RPC + setSepolia(e.target.value)} style={inputStyle} /> + + + Username + setUsername(e.target.value)} style={inputStyle} /> + + + Password + setPassword(e.target.value)} style={inputStyle} type="password" /> + + + + setRemember(e.target.checked)} /> + Remember me + + + + + + Login + + + Registration + + + + +
+ ) +} diff --git a/src/pages/fdp/FdpPod.tsx b/src/pages/fdp/FdpPod.tsx new file mode 100644 index 00000000..b47797eb --- /dev/null +++ b/src/pages/fdp/FdpPod.tsx @@ -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 ( + { + 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(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 + }} + /> + ) +} diff --git a/src/pages/fdp/FdpPods.tsx b/src/pages/fdp/FdpPods.tsx new file mode 100644 index 00000000..f93dc54e --- /dev/null +++ b/src/pages/fdp/FdpPods.tsx @@ -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 ( + + + Loading your pods... + + ) + } + + return ( + + {pods.map(pod => ( + + ))} + + ) +} diff --git a/src/pages/fdp/Horizontal.tsx b/src/pages/fdp/Horizontal.tsx new file mode 100644 index 00000000..44562f44 --- /dev/null +++ b/src/pages/fdp/Horizontal.tsx @@ -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
{children}
+} diff --git a/src/pages/fdp/Vertical.tsx b/src/pages/fdp/Vertical.tsx new file mode 100644 index 00000000..19a7bb69 --- /dev/null +++ b/src/pages/fdp/Vertical.tsx @@ -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
{children}
+} diff --git a/src/pages/fdp/index.tsx b/src/pages/fdp/index.tsx new file mode 100644 index 00000000..c388ddc2 --- /dev/null +++ b/src/pages/fdp/index.tsx @@ -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 { + 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(null) + const [pods, setPods] = useState([]) + const [loggedIn, setLoggedIn] = useState(false) + const [loadingPods, setLoadingPods] = useState(false) + const [creatingPod, setCreatingPod] = useState(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 + } + + return ( + + + Files + {loggedIn && ( + + + Create + + + Import + + + )} + + {!loggedIn && } + {loggedIn && } + {loggedIn && !loadingPods && !creatingPod && pods.length === 0 && ( + + You do not have any pods yet. Get started by clicking the Create or Import button on the top + right. + + )} + + ) +} diff --git a/src/react-fs/CafeReactFs.tsx b/src/react-fs/CafeReactFs.tsx new file mode 100644 index 00000000..46ef655d --- /dev/null +++ b/src/react-fs/CafeReactFs.tsx @@ -0,0 +1,118 @@ +import { useEffect, useState } from 'react' +import { CafeReactFsCreate } from './CafeReactFsCreate' +import { CafeReactFsItem } from './CafeReactFsItem' +import { CafeReactFsLoading } from './CafeReactFsLoading' +import { CafeReactFsPath } from './CafeReactFsPath' +import { CafeReactFsSync } from './CafeReactFsSync' +import { CafeReactFsUpload } from './CafeReactFsUpload' +import { FsItem } from './CafeReactType' + +const DEFAULT_BACKGROUND_COLOR = '#f0f0f0' + +interface Props { + download: (path: string) => Promise + list: (path: string) => Promise + onUpload: (path: string) => Promise + onCreateDirectory: (path: string) => Promise + onDeleteFile: (path: string) => Promise + onDeleteDirectory: (path: string) => Promise + onSync: () => Promise + reloader: number + backgroundColor?: string + rootAlias?: string +} + +export function CafeReactFs({ + download, + list, + onUpload, + onCreateDirectory, + onDeleteFile, + onDeleteDirectory, + onSync, + reloader, + backgroundColor, + rootAlias, +}: Props) { + const [path, setPath] = useState('/') + const [items, setItems] = useState([]) + const [loading, setLoading] = useState(false) + + function setItemsSorted(items: FsItem[]) { + // directories first, all alphabetically + const sortedItems = items.slice().sort((a, b) => { + if (a.$type === b.$type) { + return a.name.localeCompare(b.name) + } + + return a.$type === 'directory' ? -1 : 1 + }) + setItems(sortedItems) + } + + useEffect(() => { + setLoading(true) + list(path) + .then(setItemsSorted) + .finally(() => setLoading(false)) + }, [reloader, list, path]) + + const pathParts = ['/', ...path.split('/').filter(x => x)] + + function jumpToDirectory(fullPath: string) { + setPath(fullPath) + setLoading(true) + list(fullPath) + .then(setItemsSorted) + .finally(() => setLoading(false)) + } + + function enterDirectory(name: string) { + const newPath = path.endsWith('/') ? `${path}${name}` : `${path}/${name}` + setPath(newPath) + setLoading(true) + list(newPath) + .then(setItemsSorted) + .finally(() => setLoading(false)) + } + + return ( +
+ +
+ {loading && } + {!loading && + items.map(item => ( + + ))} + {!loading && ( + <> + onUpload(path)} + backgroundColor={backgroundColor ?? DEFAULT_BACKGROUND_COLOR} + /> + onCreateDirectory(path)} + backgroundColor={backgroundColor ?? DEFAULT_BACKGROUND_COLOR} + /> + + + )} +
+
+ ) +} diff --git a/src/react-fs/CafeReactFsCreate.tsx b/src/react-fs/CafeReactFsCreate.tsx new file mode 100644 index 00000000..33a395db --- /dev/null +++ b/src/react-fs/CafeReactFsCreate.tsx @@ -0,0 +1,54 @@ +import { useState } from 'react' +import { CafeReactFsLoading } from './CafeReactFsLoading' + +interface Props { + backgroundColor: string + onCreateDirectory: () => Promise +} + +export function CafeReactFsCreate({ backgroundColor, onCreateDirectory }: Props) { + const [loading, setLoading] = useState(false) + + function proxyUpload() { + setLoading(true) + onCreateDirectory().finally(() => setLoading(false)) + } + + if (loading) { + return + } + + return ( +
+ Create +

+ New Folder +

+
+ ) +} diff --git a/src/react-fs/CafeReactFsDelete.tsx b/src/react-fs/CafeReactFsDelete.tsx new file mode 100644 index 00000000..281e11e3 --- /dev/null +++ b/src/react-fs/CafeReactFsDelete.tsx @@ -0,0 +1,25 @@ +interface Props { + onDelete: () => Promise +} + +export function CafeReactFsDelete({ onDelete }: Props) { + return ( + Delete { + onDelete() + event.stopPropagation() + }} + /> + ) +} diff --git a/src/react-fs/CafeReactFsDirectory.tsx b/src/react-fs/CafeReactFsDirectory.tsx new file mode 100644 index 00000000..4b573beb --- /dev/null +++ b/src/react-fs/CafeReactFsDirectory.tsx @@ -0,0 +1,51 @@ +import { useState } from 'react' +import { CafeReactFsDelete } from './CafeReactFsDelete' +import { CafeReactFsLoading } from './CafeReactFsLoading' +import { CafeReactFsName } from './CafeReactFsName' +import { VirtualDirectory } from './CafeReactType' + +interface Props { + directory: VirtualDirectory + enterDirectory: (name: string) => void + deleteDirectory: (name: string) => Promise + backgroundColor: string +} + +export function CafeReactFsDirectory({ directory, enterDirectory, deleteDirectory, backgroundColor }: Props) { + const [hovered, setHovered] = useState(false) + const [loading, setLoading] = useState(false) + + function proxyDelete() { + setLoading(true) + + return deleteDirectory(directory.name).finally(() => setLoading(false)) + } + + if (loading) { + return + } + + return ( +
enterDirectory(directory.name)} + onMouseEnter={() => setHovered(true)} + onMouseLeave={() => setHovered(false)} + > + {hovered && } + Directory + +
+ ) +} diff --git a/src/react-fs/CafeReactFsFile.tsx b/src/react-fs/CafeReactFsFile.tsx new file mode 100644 index 00000000..212033a8 --- /dev/null +++ b/src/react-fs/CafeReactFsFile.tsx @@ -0,0 +1,53 @@ +import { useState } from 'react' +import { CafeReactFsDelete } from './CafeReactFsDelete' +import { CafeReactFsLoading } from './CafeReactFsLoading' +import { CafeReactFsName } from './CafeReactFsName' +import { VirtualFile } from './CafeReactType' +import { joinUrl } from './Utility' + +interface Props { + path: string + file: VirtualFile + download: (path: string) => Promise + deleteFile: (path: string) => Promise + backgroundColor: string +} + +export function CafeReactFsFile({ path, file, download, deleteFile, backgroundColor }: Props) { + const [hovered, setHovered] = useState(false) + const [loading, setLoading] = useState(false) + + if (loading) { + return + } + + function proxyDelete() { + setLoading(true) + + return deleteFile(joinUrl(path, file.name)).finally(() => setLoading(false)) + } + + return ( +
download(joinUrl(path, file.name))} + onMouseEnter={() => setHovered(true)} + onMouseLeave={() => setHovered(false)} + style={{ + width: '80px', + height: '80px', + position: 'relative', + background: backgroundColor, + borderRadius: '2px', + cursor: 'pointer', + }} + > + {hovered && } + File + +
+ ) +} diff --git a/src/react-fs/CafeReactFsItem.tsx b/src/react-fs/CafeReactFsItem.tsx new file mode 100644 index 00000000..9081e833 --- /dev/null +++ b/src/react-fs/CafeReactFsItem.tsx @@ -0,0 +1,48 @@ +import { CafeReactFsDirectory } from './CafeReactFsDirectory' +import { CafeReactFsFile } from './CafeReactFsFile' +import { FsItem, isVirtualDirectory, isVirtualFile } from './CafeReactType' + +interface Props { + path: string + item: FsItem + download: (path: string) => Promise + enterDirectory: (name: string) => void + onDeleteFile: (path: string) => Promise + onDeleteDirectory: (path: string) => Promise + backgroundColor: string +} + +export function CafeReactFsItem({ + path, + item, + download, + enterDirectory, + onDeleteFile, + onDeleteDirectory, + backgroundColor, +}: Props) { + if (isVirtualFile(item)) { + return ( + + ) + } + + if (isVirtualDirectory(item)) { + return ( + + ) + } + + return null +} diff --git a/src/react-fs/CafeReactFsLoading.tsx b/src/react-fs/CafeReactFsLoading.tsx new file mode 100644 index 00000000..871c871c --- /dev/null +++ b/src/react-fs/CafeReactFsLoading.tsx @@ -0,0 +1,52 @@ +import { CSSProperties } from 'react' + +interface Props { + backgroundColor: string +} + +export function CafeReactFsLoading({ backgroundColor }: Props) { + const spinnerStyle = { + width: '80px', + height: '80px', + borderRadius: '2px', + position: 'relative', + background: backgroundColor, + } as CSSProperties + + const bounceStyle = { + width: '32px', + height: '32px', + borderRadius: '50%', + backgroundColor: '#333', + top: '24px', + left: '24px', + opacity: 0.6, + position: 'absolute', + animation: 'bounce 2.0s infinite ease-in-out', + } as CSSProperties + + const bounceStyle2 = { + ...bounceStyle, + animationDelay: '-1.0s', + } + + const keyframes = ` + @keyframes bounce { + 0%, 100% { + transform: scale(0.0); + } 50% { + transform: scale(1.0); + } + } + ` + + return ( + <> + +
+
+
+
+ + ) +} diff --git a/src/react-fs/CafeReactFsName.tsx b/src/react-fs/CafeReactFsName.tsx new file mode 100644 index 00000000..6aa5579b --- /dev/null +++ b/src/react-fs/CafeReactFsName.tsx @@ -0,0 +1,25 @@ +interface Props { + name: string +} + +export function CafeReactFsName({ name }: Props) { + const shortName = name.length > 10 ? name.slice(0, 10) + '...' : name + + return ( +

+ {shortName} +

+ ) +} diff --git a/src/react-fs/CafeReactFsPath.tsx b/src/react-fs/CafeReactFsPath.tsx new file mode 100644 index 00000000..eef7a811 --- /dev/null +++ b/src/react-fs/CafeReactFsPath.tsx @@ -0,0 +1,44 @@ +import { joinUrl } from './Utility' + +interface Props { + pathParts: string[] + jumpToDirectory: (fullPath: string) => void + backgroundColor: string + rootAlias?: string +} + +export function CafeReactFsPath({ pathParts, jumpToDirectory, backgroundColor, rootAlias }: Props) { + const absolutePaths: string[] = [] + + for (const pathPart of pathParts) { + if (absolutePaths.length === 0) { + absolutePaths.push(pathPart) + } else { + absolutePaths.push(joinUrl(absolutePaths[absolutePaths.length - 1], pathPart)) + } + } + + return ( +
+ {pathParts.map((part, index) => ( + + ))} +
+ ) +} diff --git a/src/react-fs/CafeReactFsSync.tsx b/src/react-fs/CafeReactFsSync.tsx new file mode 100644 index 00000000..2e15d9f5 --- /dev/null +++ b/src/react-fs/CafeReactFsSync.tsx @@ -0,0 +1,40 @@ +interface Props { + backgroundColor: string + onSync: () => Promise +} + +export function CafeReactFsSync({ backgroundColor, onSync }: Props) { + return ( +
+ Sync +

+ Sync +

+
+ ) +} diff --git a/src/react-fs/CafeReactFsUpload.tsx b/src/react-fs/CafeReactFsUpload.tsx new file mode 100644 index 00000000..63cedd89 --- /dev/null +++ b/src/react-fs/CafeReactFsUpload.tsx @@ -0,0 +1,54 @@ +import { useState } from 'react' +import { CafeReactFsLoading } from './CafeReactFsLoading' + +interface Props { + onUpload: () => Promise + backgroundColor: string +} + +export function CafeReactFsUpload({ onUpload, backgroundColor }: Props) { + const [uploading, setUploading] = useState(false) + + function proxyUpload() { + setUploading(true) + onUpload().finally(() => setUploading(false)) + } + + if (uploading) { + return + } + + return ( +
+ Upload +

+ Upload +

+
+ ) +} diff --git a/src/react-fs/CafeReactType.ts b/src/react-fs/CafeReactType.ts new file mode 100644 index 00000000..87c57a64 --- /dev/null +++ b/src/react-fs/CafeReactType.ts @@ -0,0 +1,26 @@ +export enum FsItemType { + FILE = 'file', + DIRECTORY = 'directory', +} + +export interface VirtualFile { + id: string | number + name: string + $type: FsItemType.FILE +} + +export interface VirtualDirectory { + id: string | number + name: string + $type: FsItemType.DIRECTORY +} + +export type FsItem = VirtualFile | VirtualDirectory + +export function isVirtualFile(item: FsItem): item is VirtualFile { + return item.$type === FsItemType.FILE +} + +export function isVirtualDirectory(item: FsItem): item is VirtualDirectory { + return item.$type === FsItemType.DIRECTORY +} diff --git a/src/react-fs/Utility.ts b/src/react-fs/Utility.ts new file mode 100644 index 00000000..6d0fd4bd --- /dev/null +++ b/src/react-fs/Utility.ts @@ -0,0 +1,6 @@ +export function joinUrl(...parts: unknown[]): string { + return parts + .filter(x => x) + .join('/') + .replace(/(? { } /> } /> } /> + } /> {isDesktop && } />} )