Skip to content

Commit

Permalink
feat: remove upload (#90)
Browse files Browse the repository at this point in the history
Adds a remove button and confirmation modal to make sure folks _really_
want to remove the upload.

### Button on upload info page

<img width="1503" alt="Screenshot 2024-02-08 at 13 47 55"
src="https://github.com/web3-storage/console/assets/152863/225f1232-a101-4365-95ea-4c367fa72344">

### Button hover state (red so you know it is dangerous)

<img width="1503" alt="Screenshot 2024-02-08 at 13 48 56"
src="https://github.com/web3-storage/console/assets/152863/1b64d96b-b1bd-4b8e-ab25-16b0137bb2cb">

### Confirmation modal

<img width="1503" alt="Screenshot 2024-02-08 at 13 49 00"
src="https://github.com/web3-storage/console/assets/152863/12fe9ee5-5e8c-49db-a78c-63c2661db563">

resolves storacha/w3up#1298
  • Loading branch information
Alan Shaw authored Feb 9, 2024
1 parent 0009431 commit 92aaabf
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 6 deletions.
3 changes: 2 additions & 1 deletion src/app/space/[did]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { UploadsList } from '@/components/UploadsList'
import { useW3, UnknownLink, UploadListSuccess } from '@w3ui/react'
import useSWR from 'swr'
import { useRouter, usePathname } from 'next/navigation'
import { createUploadsListKey } from '@/cache'

const pageSize = 20

Expand All @@ -22,7 +23,7 @@ export default function Page ({ params, searchParams }: PageProps): JSX.Element
const spaceDID = decodeURIComponent(params.did)
const space = spaces.find(s => s.did() === spaceDID)

const key = `/space/${spaceDID}/uploads?cursor=${searchParams.cursor ?? ''}&pre=${searchParams.pre ?? 'false'}`
const key = space ? createUploadsListKey(space.did(), searchParams.cursor, searchParams.pre === 'true') : ''
const { data: uploads, isLoading, isValidating, mutate } = useSWR<UploadListSuccess|undefined>(key, {
fetcher: async () => {
if (!client || !space) return
Expand Down
86 changes: 81 additions & 5 deletions src/app/space/[did]/root/[cid]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
'use client'

import { MouseEventHandler, useState } from 'react'
import { Dialog } from '@headlessui/react'
import { TrashIcon, ExclamationTriangleIcon } from '@heroicons/react/24/outline'
import { H2 } from '@/components/Text'
import { useW3, UploadGetSuccess, FilecoinInfoSuccess, SpaceDID, CARLink } from '@w3ui/react'
import useSWR from 'swr'
import { useW3, UploadGetSuccess, SpaceDID, CARLink } from '@w3ui/react'
import useSWR, { useSWRConfig } from 'swr'
import { UnknownLink, parse as parseLink } from 'multiformats/link'
import DefaultLoader from '@/components/Loader'
import * as Claims from '@web3-storage/content-claims/client'
import { Piece, PieceLink } from '@web3-storage/data-segment'
import Link from 'next/link'
import CopyIcon from '@/components/CopyIcon'
import BackIcon from '@/components/BackIcon'
import { Breadcrumbs } from '@/components/Breadcrumbs'
import { useRouter } from 'next/navigation'
import { createUploadsListKey } from '@/cache'

interface PageProps {
params: {
Expand Down Expand Up @@ -39,9 +41,23 @@ export default function ItemPage ({ params }: PageProps): JSX.Element {
onError: err => console.error(err.message, err.cause)
})

const [isRemoveConfirmModalOpen, setRemoveConfirmModalOpen] = useState(false)
const router = useRouter()
const { mutate } = useSWRConfig()

if (!space) {
return <h1>Space not found</h1>
}

const handleRemove = async () => {
await client?.remove(root, { shards: true })
setRemoveConfirmModalOpen(false)
// ensure list data is fresh
mutate(createUploadsListKey(space.did()))
// navigate to list (this page no longer exists)
router.replace(`/space/${spaceDID}`)
}

const url = `https://${root}.ipfs.w3s.link`
return (
<div>
Expand All @@ -62,6 +78,17 @@ export default function ItemPage ({ params }: PageProps): JSX.Element {
? <DefaultLoader className='w-5 h-5 inline-block' />
: upload.data?.shards?.map(link => <Shard space={space.did()} root={root} shard={link} key={link.toString()} />)}
</div>

<button onClick={e => { e.preventDefault(); setRemoveConfirmModalOpen(true) }} className={`inline-block bg-zinc-950 text-white font-bold text-sm pl-4 pr-6 py-2 rounded-full whitespace-nowrap hover:bg-red-700 hover:outline`}>
<TrashIcon className='h-5 w-5 inline-block mr-1 align-middle' style={{marginTop: -4}} /> Remove
</button>
<RemoveConfirmModal
isOpen={isRemoveConfirmModalOpen}
root={root}
shards={upload.data?.shards ?? []}
onConfirm={handleRemove}
onCancel={() => setRemoveConfirmModalOpen(false)}
/>
</div>
)
}
Expand All @@ -74,3 +101,52 @@ function Shard ({ space, root, shard }: { space: SpaceDID, root: UnknownLink, sh
</div>
)
}

interface RemoveConfirmModalProps {
isOpen: boolean
root: UnknownLink
shards: UnknownLink[]
onConfirm: () => void
onCancel: () => void
}

function RemoveConfirmModal ({ isOpen, root, shards, onConfirm, onCancel }: RemoveConfirmModalProps) {
const [confirmed, setConfirmed] = useState(false)
const displayShards = shards.slice(0, 10)
return (
<Dialog open={isOpen} onClose={() => { setConfirmed(false); onCancel() }} className='relative z-50'>
<div className='fixed inset-0 flex w-screen items-center justify-center bg-black/70' aria-hidden='true'>
<Dialog.Panel className='bg-grad p-4 shadow-lg rounded-lg'>
<Dialog.Title className='text-lg font-semibold leading-5 text-black text-center my-3'>
<ExclamationTriangleIcon className='h-10 w-10 inline-block' /><br/>
Confirm remove
</Dialog.Title>
<Dialog.Description className='py-2'>
Are you sure you want to remove <span className='font-mono font-bold text-sm'>{root.toString()}</span>?
</Dialog.Description>

<p className='py-2'>The following shards will be removed:</p>

<ul className='py-2 list-disc pl-6'>
{displayShards.map(s => <li key={s.toString()} className='font-mono text-sm'>{s.toString()}</li>)}
</ul>

{displayShards.length < shards.length ? <p className='py-2'>...and {shards.length - displayShards.length} more.</p> : null}

<p className='py-2'>
Any uploads using the same shards as those listed above <em>will</em> be corrputed. This cannot be undone.
</p>

<div className='py-2 text-center'>
<button onClick={e => { e.preventDefault(); setConfirmed(true); onConfirm() }} className={`inline-block bg-red-700 text-white font-bold text-sm pl-4 pr-6 py-2 mr-3 rounded-full whitespace-nowrap ${confirmed ? 'opacity-50' : 'hover:outline'}`} disabled={confirmed}>
<TrashIcon className='h-5 w-5 inline-block mr-1 align-middle' style={{marginTop: -4}} /> {confirmed ? 'Removing...' : 'Remove'}
</button>
<button onClick={e => { e.preventDefault(); setConfirmed(false); onCancel() }} className={`inline-block bg-zinc-950 text-white font-bold text-sm px-8 py-2 rounded-full whitespace-nowrap ${confirmed ? 'opacity-50' : 'hover:outline'}`} disabled={confirmed}>
Cancel
</button>
</div>
</Dialog.Panel>
</div>
</Dialog>
)
}
7 changes: 7 additions & 0 deletions src/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { DID } from '@ucanto/interface'

export const createUploadsListKey = (
space: DID<'key'>,
cursor?: string,
pre?: boolean
) => `/space/${space}/uploads?cursor=${cursor ?? ''}&pre=${pre ?? false}`

0 comments on commit 92aaabf

Please sign in to comment.