Skip to content

Commit

Permalink
feat: upload progress (storacha#499)
Browse files Browse the repository at this point in the history
Use the new `onUploadProgress` callback in `@web3-storage/upload-client`
to add upload progress bars to w3console and the example apps.

This PR also includes a decent amount of bugfixing in the example apps
to get them working.


https://user-images.githubusercontent.com/1113/230191386-f0f635f2-1008-4e1f-91e2-517a65b66e31.mov

---------

Co-authored-by: Benjamin Goering <171782+gobengo@users.noreply.github.com>
  • Loading branch information
Travis Vachon and gobengo authored May 3, 2023
1 parent 12fd5f6 commit 6c9de97
Show file tree
Hide file tree
Showing 28 changed files with 480 additions and 91 deletions.
11 changes: 6 additions & 5 deletions examples/react/file-upload/src/ContentPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import React, { useState } from 'react'
import { useUploader } from '@w3ui/react-uploader'
import { withIdentity } from './components/Authenticator'
import './spinner.css'
import Loader from './components/Loader'

export function ContentPage () {
const [{ storedDAGShards }, uploader] = useUploader()
const [file, setFile] = useState(null)
const [{ storedDAGShards, uploadProgress }, uploader] = useUploader()
const [file, setFile] = useState({})
const [dataCid, setDataCid] = useState('')
const [status, setStatus] = useState('')
const [error, setError] = useState(null)
Expand All @@ -27,7 +28,7 @@ export function ContentPage () {
}

if (status === 'uploading') {
return <Uploading file={file} storedDAGShards={storedDAGShards} />
return <Uploading file={file} storedDAGShards={storedDAGShards} uploadProgress={uploadProgress} />
}

if (status === 'done') {
Expand All @@ -45,9 +46,9 @@ export function ContentPage () {
)
}

const Uploading = ({ file, storedDAGShards }) => (
const Uploading = ({ file, storedDAGShards, uploadProgress }) => (
<div className='flex items-center'>
<div className='spinner mr3 flex-none' />
<Loader className='mr3' uploadProgress={uploadProgress} />
<div className='flex-auto'>
<p className='truncate'>Uploading DAG for {file.name}</p>
{storedDAGShards.map(({ cid, size }) => (
Expand Down
24 changes: 24 additions & 0 deletions examples/react/file-upload/src/components/Loader.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
function StatusLoader ({ progressStatus }) {
const { total, loaded, lengthComputable } = progressStatus
if (lengthComputable) {
const percentComplete = Math.floor((loaded / total) * 100)
return (
<div className='relative w2 h5 ba b--white flex flex-column justify-end'>
<div className='bg-white w100' style={{ height: `${percentComplete}%` }}>
</div>
</div>
)
} else {
return <ArrowPathIcon className='animate-spin h-4 w-4' />
}
}

export default function Loader ({ uploadProgress, className = '' }) {
return (
<div className={`${className} flex flex-row`}>
{Object.values(uploadProgress).map(
status => <StatusLoader progressStatus={status} key={status.url} />
)}
</div>
)
}
11 changes: 6 additions & 5 deletions examples/react/multi-file-upload/src/ContentPage.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { useState } from 'react'
import { useUploader } from '@w3ui/react-uploader'
import { withIdentity } from './components/Authenticator'
import Loader from './components/Loader'
import './spinner.css'

export function ContentPage () {
const [{ storedDAGShards }, uploader] = useUploader()
const [{ storedDAGShards, uploadProgress }, uploader] = useUploader()
const [files, setFiles] = useState([])
const [allowDirectory, setAllowDirectory] = useState(false)
const [wrapInDirectory, setWrapInDirectory] = useState(false)
Expand Down Expand Up @@ -33,7 +34,7 @@ export function ContentPage () {
}

if (status === 'uploading') {
return <Uploading files={files} storedDAGShards={storedDAGShards} />
return <Uploading files={files} storedDAGShards={storedDAGShards} uploadProgress={uploadProgress} />
}

if (status === 'done') {
Expand All @@ -60,16 +61,16 @@ export function ContentPage () {
<input type='checkbox' value={wrapInDirectory} onChange={e => setWrapInDirectory(e.target.checked)} /> Wrap file in a directory
</label>
</div>
)
)
: null}
<button type='submit' className='ph3 pv2'>Upload</button>
</form>
)
}

const Uploading = ({ files, storedDAGShards }) => (
const Uploading = ({ files, storedDAGShards, uploadProgress }) => (
<div className='flex items-center'>
<div className='spinner mr3 flex-none' />
<Loader className='mr3' uploadProgress={uploadProgress} />
<div className='flex-auto'>
<p className='truncate'>Uploading DAG for {files.length > 1 ? `${files.length} files` : files[0].name}</p>
{storedDAGShards.map(({ cid, size }) => (
Expand Down
24 changes: 24 additions & 0 deletions examples/react/multi-file-upload/src/components/Loader.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
function StatusLoader ({ progressStatus }) {
const { total, loaded, lengthComputable } = progressStatus
if (lengthComputable) {
const percentComplete = Math.floor((loaded / total) * 100)
return (
<div className='relative w2 h5 ba b--white flex flex-column justify-end'>
<div className='bg-white w100' style={{ height: `${percentComplete}%` }}>
</div>
</div>
)
} else {
return <ArrowPathIcon className='animate-spin h-4 w-4' />
}
}

export default function Loader ({ uploadProgress, className = '' }) {
return (
<div className={`${className} flex flex-row`}>
{Object.values(uploadProgress).map(
status => <StatusLoader progressStatus={status} key={status.url} />
)}
</div>
)
}
2 changes: 1 addition & 1 deletion examples/react/w3console/src/components/Authenticator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function AuthenticationSubmitted (): JSX.Element {
<p className='pt-2 pb-4'>
Click the link in the email we sent to <span className='font-semibold tracking-wide'>{email}</span> to authorize this agent.
</p>
<AuthCore.CancelButton className='w3ui-button hidden' >
<AuthCore.CancelButton className='w3ui-button' >
Cancel
</AuthCore.CancelButton>
</div>
Expand Down
39 changes: 35 additions & 4 deletions examples/react/w3console/src/components/Uploader.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,56 @@
import type {
OnUploadComplete,
ProgressStatus,
UploadProgress,
CARMetadata,
CID
} from '@w3ui/react-uploader'

import { CloudArrowUpIcon } from '@heroicons/react/24/outline'
import { CloudArrowUpIcon, ArrowPathIcon } from '@heroicons/react/24/outline'
import {
Status,
Uploader as UploaderCore,
useUploaderComponent
} from '@w3ui/react-uploader'
import { gatewayHost } from '../components/services'

function StatusLoader ({ progressStatus }: { progressStatus: ProgressStatus }) {
const { total, loaded, lengthComputable } = progressStatus
if (lengthComputable) {
const percentComplete = Math.floor((loaded / total) * 100)
return (
<div className='relative w-80 h-4 border border-solid border-white'>
<div className='bg-white h-full' style={{ width: `${percentComplete}%` }}>
</div>
</div>
)
} else {
return <ArrowPathIcon className='animate-spin h-4 w-4' />
}
}

function Loader ({ uploadProgress }: { uploadProgress: UploadProgress }): JSX.Element {
return (
<div className='flex flex-col'>
{Object.values(uploadProgress).map(
status => <StatusLoader progressStatus={status} key={status.url} />
)}
</div>
)
}

export const Uploading = ({
file,
storedDAGShards
storedDAGShards,
uploadProgress
}: {
file?: File
storedDAGShards?: CARMetadata[]
uploadProgress: UploadProgress
}): JSX.Element => (
<div className='flex flex-col items-center w-full'>
<h1 className='font-bold text-sm uppercase text-gray-400'>Uploading {file?.name}</h1>
<Loader uploadProgress={uploadProgress} />
{storedDAGShards?.map(({ cid, size }) => (
<p className='text-xs max-w-full overflow-hidden text-ellipsis' key={cid.toString()}>
shard {cid.toString()} ({humanFileSize(size)}) uploaded
Expand Down Expand Up @@ -165,11 +195,12 @@ const UploaderContents = (): JSX.Element => {
}

const UploaderConsole = (): JSX.Element => {
const [{ status, file, error, dataCID, storedDAGShards }] =
const [{ status, file, error, dataCID, storedDAGShards, uploadProgress }] =
useUploaderComponent()

switch (status) {
case Status.Uploading: {
return <Uploading file={file} storedDAGShards={storedDAGShards} />
return <Uploading file={file} storedDAGShards={storedDAGShards} uploadProgress={uploadProgress} />
}
case Status.Succeeded: {
return (
Expand Down
7 changes: 4 additions & 3 deletions examples/solid/file-upload/src/ContentPage.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createSignal, Switch, Match } from 'solid-js'
import { useUploader } from '@w3ui/solid-uploader'
import { withIdentity } from './components/Authenticator'
import Loader from './components/Loader'
import './spinner.css'

export function ContentPage () {
Expand Down Expand Up @@ -36,7 +37,7 @@ export function ContentPage () {
</form>
</Match>
<Match when={status() === 'uploading'}>
<Uploading file={file()} storedDAGShards={progress.storedDAGShards} />
<Uploading file={file()} progress={progress} />
</Match>
<Match when={status() === 'done'}>
{error() ? <Errored error={error()} /> : <Done file={file()} dataCid={dataCid()} storedDAGShards={progress.storedDAGShards} />}
Expand All @@ -47,10 +48,10 @@ export function ContentPage () {

const Uploading = props => (
<div className='flex items-center'>
<div className='spinner mr3 flex-none' />
<Loader className='mr3' progress={props.progress} />
<div className='flex-auto'>
<p className='truncate'>Uploading DAG for {props.file.name}</p>
{props.storedDAGShards.map(({ cid, size }) => (
{props.progress.storedDAGShards.map(({ cid, size }) => (
<p key={cid.toString()} className='f7 truncate'>
{cid.toString()} ({size} bytes)
</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function Authenticator ({ children }) {

return (
<Switch>
<Match when={keyring.space?.registered()}>
<Match when={keyring.account}>
{children}
</Match>
<Match when={submitted()}>
Expand Down
25 changes: 25 additions & 0 deletions examples/solid/file-upload/src/components/Loader.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { For, Show } from "solid-js"

function StatusLoader ({ progressStatus }) {
const { total, loaded, lengthComputable } = progressStatus
return (
<Show
when={lengthComputable}
fallback={<div className='spinner mr3 flex-none' />}>
<div className='relative w2 h5 ba b--white flex flex-column justify-end'>
<div className='bg-white w100' style={{ height: `${Math.floor((loaded / total) * 100)}%` }}>
</div>
</div>
</Show>
)
}

export default function Loader ({ progress, className = '' }) {
return (
<div className={`${className} flex flex-row`}>
<For each={Object.values(progress.uploadProgress)}>
{(status) => <StatusLoader progressStatus={status} key={status.url} />}
</For>
</div>
)
}
7 changes: 4 additions & 3 deletions examples/solid/multi-file-upload/src/ContentPage.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createSignal, Switch, Match } from 'solid-js'
import { useUploader } from '@w3ui/solid-uploader'
import { withIdentity } from './components/Authenticator'
import Loader from './components/Loader'
import './spinner.css'

export function ContentPage () {
Expand Down Expand Up @@ -58,7 +59,7 @@ export function ContentPage () {
</form>
</Match>
<Match when={status() === 'uploading'}>
<Uploading files={files()} storedDAGShards={progress.storedDAGShards} />
<Uploading files={files()} progress={progress} />
</Match>
<Match when={status() === 'done'}>
{error() ? <Errored error={error()} /> : <Done files={files()} dataCid={dataCid()} storedDAGShards={progress.storedDAGShards} />}
Expand All @@ -69,10 +70,10 @@ export function ContentPage () {

const Uploading = props => (
<div className='flex items-center'>
<div className='spinner mr3 flex-none' />
<Loader className='mr3' progress={props.progress} />
<div className='flex-auto'>
<p className='truncate'>Uploading DAG for {props.files.length > 1 ? `${props.files.length} files` : props.files[0].name}</p>
{props.storedDAGShards.map(({ cid, size }) => (
{props.progress.storedDAGShards.map(({ cid, size }) => (
<p key={cid.toString()} className='f7 truncate'>
{cid.toString()} ({size} bytes)
</p>
Expand Down
25 changes: 25 additions & 0 deletions examples/solid/multi-file-upload/src/components/Loader.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { For, Show } from "solid-js"

function StatusLoader ({ progressStatus }) {
const { total, loaded, lengthComputable } = progressStatus
return (
<Show
when={lengthComputable}
fallback={<div className='spinner mr3 flex-none' />}>
<div className='relative w2 h5 ba b--white flex flex-column justify-end'>
<div className='bg-white w100' style={{ height: `${Math.floor((loaded / total) * 100)}%` }}>
</div>
</div>
</Show>
)
}

export default function Loader ({ progress, className = '' }) {
return (
<div className={`${className} flex flex-row`}>
<For each={Object.values(progress.uploadProgress)}>
{(status) => <StatusLoader progressStatus={status} key={status.url} />}
</For>
</div>
)
}
10 changes: 7 additions & 3 deletions examples/vue/file-upload/src/ContentPage.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
<script>
import { UploaderProviderInjectionKey } from '@w3ui/vue-uploader'
import Loader from './components/Loader.vue'
export default {
inject: {
uploadFile: { from: UploaderProviderInjectionKey.uploadFile },
storedDAGShards: { from: UploaderProviderInjectionKey.storedDAGShards }
storedDAGShards: { from: UploaderProviderInjectionKey.storedDAGShards },
uploadProgress: { from: UploaderProviderInjectionKey.uploadProgress }
},
data () {
return {
Expand Down Expand Up @@ -32,13 +35,14 @@ export default {
e.preventDefault()
this.file = e.target.files[0]
}
}
},
components: { Loader }
}
</script>

<template>
<div v-if="status === 'uploading'" className="flex items-center">
<div className="spinner mr3 flex-none"></div>
<Loader className="mr3 flex-none" :uploadProgress="uploadProgress" />
<div className="flex-auto">
<p className="truncate">Uploading DAG for {{file.name}}</p>
<p className="f6 code truncate">{{dataCid}}</p>
Expand Down
17 changes: 17 additions & 0 deletions examples/vue/file-upload/src/components/Loader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script>
export default {
props: ['uploadProgress']
}
</script>

<template>
<div>
<div v-for="status in Object.values(uploadProgress)">
<div v-if="status.lengthComputable" className='relative w2 h5 ba b--white flex flex-column justify-end'>
<div className='bg-white w100' :style='{ height: Math.floor((status.loaded / status.total) * 100) + "%" }'>
</div>
</div>
<div v-else className='spinner' />
</div>
</div>
</template>
2 changes: 1 addition & 1 deletion packages/keyring-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"dependencies": {
"@ucanto/interface": "^6.2.0",
"@ucanto/principal": "^5.1.0",
"@web3-storage/access": "^12.0.0"
"@web3-storage/access": "^12.0.2"
},
"eslintConfig": {
"extends": [
Expand Down
Loading

0 comments on commit 6c9de97

Please sign in to comment.