Skip to content

Commit

Permalink
feat(renterd): contract bulk delete
Browse files Browse the repository at this point in the history
  • Loading branch information
alexfreska committed Nov 27, 2024
1 parent 71a0ca5 commit 2dea0c8
Show file tree
Hide file tree
Showing 15 changed files with 182 additions and 34 deletions.
5 changes: 5 additions & 0 deletions .changeset/perfect-houses-approve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@siafoundation/design-system': minor
---

Add handleBatchOperation method.
5 changes: 5 additions & 0 deletions .changeset/shiny-gifts-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'renterd': minor
---

The contracts multi-select menu now supports bulk deletion.
18 changes: 18 additions & 0 deletions apps/renterd-e2e/src/specs/contracts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,21 @@ test('contracts prunable size', async ({ page }) => {
await expect(prunableSize).toBeVisible()
}
})

test('bulk delete contracts', async ({ page }) => {
await navigateToContracts({ page })
const rows = await getContractRows(page)
for (const row of rows) {
await row.click()
}

// Delete selected contracts.
const menu = page.getByLabel('contract multi-select menu')
await menu.getByLabel('delete selected contracts').click()
const dialog = page.getByRole('dialog')
await dialog.getByRole('button', { name: 'Delete' }).click()

await expect(
page.getByText('There are currently no active contracts')
).toBeVisible()
})
4 changes: 2 additions & 2 deletions apps/renterd-e2e/src/specs/files.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ test('shows a new intermediate directory when uploading nested files', async ({
await deleteBucket(page, bucketName)
})

test('batch delete across nested directories', async ({ page }) => {
test('bulk delete across nested directories', async ({ page }) => {
const bucketName = 'bucket1'
await navigateToBuckets({ page })
await createBucket(page, bucketName)
Expand Down Expand Up @@ -267,7 +267,7 @@ test('batch delete across nested directories', async ({ page }) => {
})
})

test('batch delete using the all files explorer mode', async ({ page }) => {
test('bulk delete using the all files explorer mode', async ({ page }) => {
const bucketName = 'bucket1'
await navigateToBuckets({ page })
await createBucket(page, bucketName)
Expand Down
2 changes: 1 addition & 1 deletion apps/renterd-e2e/src/specs/filesMove.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ test('move a file via drag and drop while leaving a separate set of selected fil
})
})

test('move files by selecting and using the docked menu batch action', async ({
test('move files by selecting and using the docked menu bulk action', async ({
page,
}) => {
const bucketName = 'bucket1'
Expand Down
2 changes: 1 addition & 1 deletion apps/renterd-e2e/src/specs/keys.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ test('create and delete a key', async ({ page }) => {
await expect(row).toBeHidden()
})

test('batch delete multiple keys', async ({ page }) => {
test('bulk delete multiple keys', async ({ page }) => {
// Create 3 keys. Note: 1 already exists.
const key1 = await createKey(page)
const key2 = await createKey(page)
Expand Down
6 changes: 3 additions & 3 deletions apps/renterd/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@

- 17b29cf3: Navigating into a directory in the file explorer is now by clicking on the directory name rather than anywhere on the row.
- 17b29cf3: The directory-based file explorer now supports multiselect across any files and directories.
- 6c7e3681: The key management table now supports multiselect and batch deletion.
- 6c7e3681: The key management table now supports multiselect and bulk deletion.
- 17b29cf3: The "all files" file explorer now supports multiselect across any files.
- 17b29cf3: The "all files" file explorer multiselect menu now supports batch deletion of selected files.
- 17b29cf3: The "all files" file explorer multiselect menu now supports bulk deletion of selected files.
- 6c7e3681: The onboarding wizard now animates in and out.
- ed264a0d: The transfers bar now animates in and out.
- 09142864: The keys table now has pagination controls.
- 17b29cf3: The directory-based file explorer multiselect menu now supports batch deletion of selected files and directories.
- 17b29cf3: The directory-based file explorer multiselect menu now supports bulk deletion of selected files and directories.

### Patch Changes

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {
Button,
handleBatchOperation,
Paragraph,
} from '@siafoundation/design-system'
import { Delete16 } from '@siafoundation/react-icons'
import { useContractDelete } from '@siafoundation/renterd-react'
import { useCallback, useMemo } from 'react'
import { useDialog } from '../../../contexts/dialog'
import { useContracts } from '../../../contexts/contracts'
import { pluralize } from '@siafoundation/units'

export function ContractsBatchDelete() {
const { multiSelect } = useContracts()

const ids = useMemo(
() => Object.entries(multiSelect.selectionMap).map(([_, item]) => item.id),
[multiSelect.selectionMap]
)
const { openConfirmDialog } = useDialog()
const deleteContract = useContractDelete()
const deleteAll = useCallback(async () => {
await handleBatchOperation(
ids.map((id) => deleteContract.delete({ params: { id } })),
{
toastError: ({ successCount, errorCount, totalCount }) => ({
title: `${pluralize(successCount, 'contract')} deleted`,
body: `Error deleting ${errorCount}/${totalCount} total contracts.`,
}),
toastSuccess: ({ totalCount }) => ({
title: `${pluralize(totalCount, 'contract')} deleted`,
}),
after: () => {
multiSelect.deselectAll()
},
}
)
}, [multiSelect, ids, deleteContract])

return (
<Button
aria-label="delete selected contracts"
tip="Delete selected contracts"
onClick={() => {
openConfirmDialog({
title: `Delete contracts`,
action: 'Delete',
variant: 'red',
body: (
<div className="flex flex-col gap-1">
<Paragraph size="14">
Are you sure you would like to delete the{' '}
{pluralize(multiSelect.selectionCount, 'selected contract')}?
</Paragraph>
</div>
),
onConfirm: async () => {
deleteAll()
},
})
}}
>
<Delete16 />
</Button>
)
}
13 changes: 13 additions & 0 deletions apps/renterd/components/Contracts/ContractsBatchMenu/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { MultiSelectionMenu } from '@siafoundation/design-system'
import { useContracts } from '../../../contexts/contracts'
import { ContractsBatchDelete } from './ContractsBatchDelete'

export function ContractsBatchMenu() {
const { multiSelect } = useContracts()

return (
<MultiSelectionMenu multiSelect={multiSelect} entityWord="contract">
<ContractsBatchDelete />
</MultiSelectionMenu>
)
}
2 changes: 2 additions & 0 deletions apps/renterd/components/Contracts/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from '../RenterdAuthedLayout'
import { ContractsActionsMenu } from './ContractsActionsMenu'
import { ContractsFilterBar } from './ContractsFilterBar'
import { ContractsBatchMenu } from './ContractsBatchMenu'

export const Layout = RenterdAuthedLayout
export function useLayoutProps(): RenterdAuthedPageLayoutProps {
Expand All @@ -20,5 +21,6 @@ export function useLayoutProps(): RenterdAuthedPageLayoutProps {
stats: <ContractsFilterBar />,
size: 'full',
scroll: false,
dockedControls: <ContractsBatchMenu />,
}
}
50 changes: 25 additions & 25 deletions apps/renterd/components/Files/batchActions/FilesBatchDelete.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import {
Button,
Paragraph,
triggerSuccessToast,
triggerErrorToast,
MultiSelect,
handleBatchOperation,
} from '@siafoundation/design-system'
import { Delete16 } from '@siafoundation/react-icons'
import { useCallback, useMemo } from 'react'
import { useDialog } from '../../../contexts/dialog'
import { useObjectsRemove } from '@siafoundation/renterd-react'
import { ObjectData } from '../../../contexts/filesManager/types'
import { pluralize } from '@siafoundation/units'

export function FilesBatchDelete({
multiSelect,
Expand All @@ -26,29 +26,29 @@ export function FilesBatchDelete({
)
const { openConfirmDialog } = useDialog()
const objectsRemove = useObjectsRemove()
const deleteFiles = useCallback(async () => {
const totalCount = filesToDelete.length
let errorCount = 0
for (const { bucket, prefix } of filesToDelete) {
const response = await objectsRemove.post({
payload: {
bucket,
prefix,
const deleteAll = useCallback(async () => {
await handleBatchOperation(
filesToDelete.map(({ bucket, prefix }) =>
objectsRemove.post({
payload: {
bucket,
prefix,
},
})
),
{
toastError: ({ totalCount, errorCount, successCount }) => ({
title: `${pluralize(successCount, 'file')} deleted`,
body: `Error deleting ${errorCount}/${totalCount} total files.`,
}),
toastSuccess: ({ totalCount }) => ({
title: `${pluralize(totalCount, 'file')} deleted`,
}),
after: () => {
multiSelect.deselectAll()
},
})
if (response.error) {
errorCount++
}
}
if (errorCount > 0) {
triggerErrorToast({
title: `${totalCount - errorCount} files deleted`,
body: `Error deleting ${errorCount}/${totalCount} total files.`,
})
} else {
triggerSuccessToast({ title: `${totalCount} files deleted` })
}
multiSelect.deselectAll()
)
}, [multiSelect, filesToDelete, objectsRemove])

return (
Expand All @@ -64,12 +64,12 @@ export function FilesBatchDelete({
<div className="flex flex-col gap-1">
<Paragraph size="14">
Are you sure you would like to delete the{' '}
{multiSelect.selectionCount.toLocaleString()} selected files?
{pluralize(multiSelect.selectionCount, 'selected file')}?
</Paragraph>
</div>
),
onConfirm: async () => {
deleteFiles()
deleteAll()
},
})
}}
Expand Down
2 changes: 1 addition & 1 deletion libs/design-system/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
- d891861b: Checkbox now supports an indeterminate state.
- d891861b: Added useMultiSelect hook that tracks multiselect state. It supports selection, shift-selecting for ranges, deselection, and works across pagination.
- ed264a0d: Added dockedControls to AppAuthedLayout.
- d891861b: Added MultiSelectMenu. The component can be used along with useMultiSelect for batch menus.
- d891861b: Added MultiSelectMenu. The component can be used along with useMultiSelect for bulk menus.
- 09142864: Table row data now supports an isSelected prop.
- d891861b: Checkbox light mode background color is now white.
- d891861b: Table column sort icons are now chevrons to differentiate from context menus which often use carets.
Expand Down
1 change: 1 addition & 0 deletions libs/design-system/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,4 +191,5 @@ export * from './lib/countryEmoji'
export * from './lib/nodeToImage'
export * from './lib/colors'
export * from './lib/object'
export * from './lib/handleBatchOperation'
export type * from './lib/types'
38 changes: 38 additions & 0 deletions libs/design-system/src/lib/handleBatchOperation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ToastParams, triggerErrorToast, triggerSuccessToast } from './toast'

type Results = {
totalCount: number
errorCount: number
successCount: number
}

type Params = {
toastSuccess: (results: Results) => ToastParams
toastError: (results: Results) => ToastParams
after?: () => void | Promise<void>
}

export async function handleBatchOperation<T>(
operations: Promise<{ data?: T; error?: string }>[],
params: Params
) {
const totalCount = operations.length
let errorCount = 0
const results = await Promise.all(operations)
for (const r of results) {
if (r.error) {
errorCount++
}
}
const successCount = totalCount - errorCount
if (errorCount > 0) {
triggerErrorToast(
params.toastError({ totalCount, errorCount, successCount })
)
} else {
triggerSuccessToast(
params.toastSuccess({ totalCount, errorCount, successCount })
)
}
await params.after?.()
}
2 changes: 1 addition & 1 deletion libs/design-system/src/lib/toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ function ToastLayout({
)
}

type ToastParams = {
export type ToastParams = {
title: React.ReactNode
body?: React.ReactNode
icon?: React.ReactNode
Expand Down

0 comments on commit 2dea0c8

Please sign in to comment.