Skip to content

Commit

Permalink
feat(renterd): contract multiselect and batch delete
Browse files Browse the repository at this point in the history
  • Loading branch information
alexfreska committed Nov 25, 2024
1 parent afc1830 commit 1521ba8
Show file tree
Hide file tree
Showing 16 changed files with 253 additions and 84 deletions.
5 changes: 5 additions & 0 deletions .changeset/light-pens-turn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'renterd': minor
---

The contracts table now supports multi-select.
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 batch 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('batch 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()
})
7 changes: 6 additions & 1 deletion apps/renterd/components/Contracts/ContractContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,12 @@ export function ContractContextMenu({
<DropdownMenu
trigger={
trigger || (
<Button variant="ghost" icon="hover" {...buttonProps}>
<Button
aria-label="contract context menu"
icon="hover"
size="none"
{...buttonProps}
>
<CaretDown16 />
</Button>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Button, 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 { handleBatchOperation } from '../../../lib/handleBatchOperation'
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 />,
}
}
1 change: 0 additions & 1 deletion apps/renterd/components/Contracts/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ export function Contracts() {
sortDirection={sortDirection}
sortField={sortField}
toggleSort={toggleSort}
focusId={selectedContract?.id}
rowSize="default"
/>
</div>
Expand Down
56 changes: 26 additions & 30 deletions apps/renterd/components/Files/batchActions/FilesBatchDelete.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import {
Button,
Paragraph,
triggerSuccessToast,
triggerErrorToast,
MultiSelect,
} from '@siafoundation/design-system'
import { Button, Paragraph, MultiSelect } 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 { handleBatchOperation } from '../../../lib/handleBatchOperation'
import { pluralize } from '@siafoundation/units'

export function FilesBatchDelete({
multiSelect,
Expand All @@ -26,29 +22,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 +60,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
10 changes: 9 additions & 1 deletion apps/renterd/contexts/contracts/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Separator,
Button,
LoadingDots,
Checkbox,
} from '@siafoundation/design-system'
import {
ArrowUpLeft16,
Expand Down Expand Up @@ -40,7 +41,14 @@ export const columns: ContractsTableColumn[] = [
id: 'actions',
label: '',
fixed: true,
cellClassName: 'w-[50px] !pl-2 !pr-4 [&+*]:!pl-0',
contentClassName: '!pl-3 !pr-4',
cellClassName: 'w-[20px] !pl-0 !pr-0',
heading: ({ context: { multiSelect } }) => (
<Checkbox
onClick={multiSelect.onSelectPage}
checked={multiSelect.isPageAllSelected}
/>
),
render: ({ data: { id, hostIp, hostKey } }) => (
<ContractContextMenu id={id} hostAddress={hostIp} hostKey={hostKey} />
),
Expand Down
21 changes: 10 additions & 11 deletions apps/renterd/contexts/contracts/dataset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,9 @@ import { useSyncStatus } from '../../hooks/useSyncStatus'
import { blockHeightToTime } from '@siafoundation/units'
import { defaultDatasetRefreshInterval } from '../../config/swr'
import { usePrunableContractSizes } from './usePrunableContractSizes'
import { Maybe } from '@siafoundation/design-system'

export function useDataset({
selectContract,
}: {
selectContract: (id: string) => void
}) {
export function useDataset() {
const response = useContractsData({
config: {
swr: {
Expand All @@ -29,10 +26,10 @@ export function useDataset({
: syncStatus.estimatedBlockHeight

const datasetWithoutPrunable = useMemo<
ContractDataWithoutPrunable[] | null
Maybe<ContractDataWithoutPrunable[]>
>(() => {
if (!response.data) {
return null
return undefined
}
const datums =
response.data?.map((c) => {
Expand All @@ -44,7 +41,6 @@ export function useDataset({
const endTime = blockHeightToTime(currentHeight, endHeight)
const datum: ContractDataWithoutPrunable = {
id: c.id,
onClick: () => selectContract(c.id),
state: c.state,
hostIp: c.hostIP,
hostKey: c.hostKey,
Expand All @@ -67,11 +63,14 @@ export function useDataset({
spendingSectorRoots: new BigNumber(c.spending.sectorRoots),
spendingFundAccount: new BigNumber(c.spending.fundAccount),
size: new BigNumber(c.size),
// selectable
onClick: () => null,
isSelected: false,
}
return datum
}) || []
return datums
}, [response.data, geoHosts, currentHeight, selectContract])
}, [response.data, geoHosts, currentHeight])

const {
prunableSizes,
Expand All @@ -81,7 +80,7 @@ export function useDataset({
fetchPrunableSizeAll,
} = usePrunableContractSizes()

const dataset = useMemo(
const dataset = useMemo<Maybe<ContractData[]>>(
() =>
datasetWithoutPrunable?.map((d) => {
const datum: ContractData = {
Expand All @@ -105,7 +104,7 @@ export function useDataset({
)

const hasFetchedAllPrunableSize = useMemo(
() => dataset?.every((d) => d.hasFetchedPrunableSize),
() => !!dataset?.every((d) => d.hasFetchedPrunableSize),
[dataset]
)

Expand Down
Loading

0 comments on commit 1521ba8

Please sign in to comment.