Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(renterd): contracts bulk manage blocklist and allowlist #828

Open
wants to merge 1 commit into
base: feat_renterd_contract_bulk_delete
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/shy-dingos-roll.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 adding and removing to both the allowlist and blocklists.
20 changes: 19 additions & 1 deletion apps/renterd-e2e/src/fixtures/hosts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { Locator, Page, expect } from '@playwright/test'
import { maybeExpectAndReturn, step } from '@siafoundation/e2e'
import {
fillTextInputByName,
maybeExpectAndReturn,
openCmdkMenu,
step,
} from '@siafoundation/e2e'

export const getHostRowById = step(
'get host row by ID',
Expand Down Expand Up @@ -57,3 +62,16 @@ export const openRowHostContextMenu = step(
return menu.click()
}
)

export const openManageListsDialog = step(
'open manage lists dialog',
async (page: Page) => {
const dialog = await openCmdkMenu(page)
await fillTextInputByName(page, 'cmdk-input', 'manage filter lists')
await expect(dialog.locator('div[cmdk-item]')).toHaveCount(1)
await dialog
.locator('div[cmdk-item]')
.getByText('manage filter lists')
.click()
}
)
75 changes: 74 additions & 1 deletion apps/renterd-e2e/src/specs/contracts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
getContractRows,
getContractsSummaryRow,
} from '../fixtures/contracts'
import { openManageListsDialog } from '../fixtures/hosts'

test.beforeEach(async ({ page }) => {
await beforeTest(page, {
Expand Down Expand Up @@ -62,7 +63,7 @@ test('contracts prunable size', async ({ page }) => {
}
})

test('bulk delete contracts', async ({ page }) => {
test('contracts bulk delete', async ({ page }) => {
await navigateToContracts({ page })
const rows = await getContractRows(page)
for (const row of rows) {
Expand All @@ -79,3 +80,75 @@ test('bulk delete contracts', async ({ page }) => {
page.getByText('There are currently no active contracts')
).toBeVisible()
})

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

const menu = page.getByLabel('contract multi-select menu')
const dialog = page.getByRole('dialog')

// Add selected contract hosts to the allowlist.
await menu.getByLabel('add host public keys to allowlist').click()
await dialog.getByRole('button', { name: 'Add to allowlist' }).click()

await openManageListsDialog(page)
await expect(dialog.getByText('The blocklist is empty')).toBeVisible()
await dialog.getByLabel('view allowlist').click()
await expect(
dialog.getByTestId('allowlistPublicKeys').getByTestId('item')
).toHaveCount(3)
await dialog.getByLabel('close').click()

for (const row of rows) {
await row.click()
}

// Remove selected contract hosts from the allowlist.
await menu.getByLabel('remove host public keys from allowlist').click()
await dialog.getByRole('button', { name: 'Remove from allowlist' }).click()

await openManageListsDialog(page)
await expect(dialog.getByText('The blocklist is empty')).toBeVisible()
await dialog.getByLabel('view allowlist').click()
await expect(dialog.getByText('The allowlist is empty')).toBeVisible()
})

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

const menu = page.getByLabel('contract multi-select menu')
const dialog = page.getByRole('dialog')

// Add selected contract hosts to the allowlist.
await menu.getByLabel('add host addresses to blocklist').click()
await dialog.getByRole('button', { name: 'Add to blocklist' }).click()

await openManageListsDialog(page)
await expect(
dialog.getByTestId('blocklistAddresses').getByTestId('item')
).toHaveCount(3)
await dialog.getByLabel('view allowlist').click()
await expect(dialog.getByText('The allowlist is empty')).toBeVisible()
await dialog.getByLabel('close').click()

for (const row of rows) {
await row.click()
}

// Remove selected contract hosts from the blocklist.
await menu.getByLabel('remove host addresses from blocklist').click()
await dialog.getByRole('button', { name: 'Remove from blocklist' }).click()

await openManageListsDialog(page)
await expect(dialog.getByText('The blocklist is empty')).toBeVisible()
await dialog.getByLabel('view allowlist').click()
await expect(dialog.getByText('The allowlist is empty')).toBeVisible()
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Button, Paragraph } from '@siafoundation/design-system'
import { ListChecked16 } from '@siafoundation/react-icons'
import { useCallback, useMemo } from 'react'
import { useDialog } from '../../../contexts/dialog'
import { useContracts } from '../../../contexts/contracts'
import { pluralize } from '@siafoundation/units'
import { useAllowlistUpdate } from '../../../hooks/useAllowlistUpdate'

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

const publicKeys = useMemo(
() =>
Object.entries(multiSelect.selectionMap).map(([_, item]) => item.hostKey),
[multiSelect.selectionMap]
)
const { openConfirmDialog } = useDialog()
const allowlistUpdate = useAllowlistUpdate()

const add = useCallback(async () => {
allowlistUpdate(publicKeys, [])
multiSelect.deselectAll()
}, [allowlistUpdate, multiSelect, publicKeys])

return (
<Button
aria-label="add host public keys to allowlist"
tip="Add host public keys to allowlist"
onClick={() => {
openConfirmDialog({
title: `Add ${pluralize(
multiSelect.selectionCount,
'host'
)} to allowlist`,
action: 'Add to allowlist',
variant: 'accent',
body: (
<div className="flex flex-col gap-1">
<Paragraph size="14">
Are you sure you would like to add{' '}
{pluralize(multiSelect.selectionCount, 'host public key')} to
the allowlist?
</Paragraph>
</div>
),
onConfirm: add,
})
}}
>
<ListChecked16 />
Add to allowlist
</Button>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Button, Paragraph } from '@siafoundation/design-system'
import { ListChecked16 } from '@siafoundation/react-icons'
import { useCallback, useMemo } from 'react'
import { useDialog } from '../../../contexts/dialog'
import { useContracts } from '../../../contexts/contracts'
import { pluralize } from '@siafoundation/units'
import { useBlocklistUpdate } from '../../../hooks/useBlocklistUpdate'

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

const hostAddresses = useMemo(
() =>
Object.entries(multiSelect.selectionMap).map(([_, item]) => item.hostIp),
[multiSelect.selectionMap]
)
const { openConfirmDialog } = useDialog()
const blocklistUpdate = useBlocklistUpdate()

const add = useCallback(async () => {
blocklistUpdate(hostAddresses, [])
multiSelect.deselectAll()
}, [blocklistUpdate, multiSelect, hostAddresses])

return (
<Button
aria-label="add host addresses to blocklist"
tip="Add host addresses to blocklist"
onClick={() => {
openConfirmDialog({
title: `Add ${pluralize(
multiSelect.selectionCount,
'host'
)} to blocklist`,
action: 'Add to blocklist',
variant: 'red',
body: (
<div className="flex flex-col gap-1">
<Paragraph size="14">
Are you sure you would like to add{' '}
{pluralize(
multiSelect.selectionCount,
'host address',
'host addresses'
)}{' '}
to the blocklist?
</Paragraph>
</div>
),
onConfirm: add,
})
}}
>
<ListChecked16 />
Add to blocklist
</Button>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Button, Paragraph } from '@siafoundation/design-system'
import { ListChecked16 } from '@siafoundation/react-icons'
import { useCallback, useMemo } from 'react'
import { useDialog } from '../../../contexts/dialog'
import { useContracts } from '../../../contexts/contracts'
import { pluralize } from '@siafoundation/units'
import { useAllowlistUpdate } from '../../../hooks/useAllowlistUpdate'

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

const publicKeys = useMemo(
() =>
Object.entries(multiSelect.selectionMap).map(([_, item]) => item.hostKey),
[multiSelect.selectionMap]
)
const { openConfirmDialog } = useDialog()
const allowlistUpdate = useAllowlistUpdate()

const remove = useCallback(async () => {
await allowlistUpdate([], publicKeys)
multiSelect.deselectAll()
}, [allowlistUpdate, multiSelect, publicKeys])

return (
<Button
aria-label="remove host public keys from allowlist"
tip="Remove host public keys from allowlist"
onClick={() => {
openConfirmDialog({
title: `Remove ${pluralize(
multiSelect.selectionCount,
'host'
)} from allowlist`,
action: 'Remove from allowlist',
variant: 'accent',
body: (
<div className="flex flex-col gap-1">
<Paragraph size="14">
Are you sure you would like to remove{' '}
{pluralize(multiSelect.selectionCount, 'host public key')} from
the allowlist?
</Paragraph>
</div>
),
onConfirm: remove,
})
}}
>
<ListChecked16 />
Remove from allowlist
</Button>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Button, Paragraph } from '@siafoundation/design-system'
import { ListChecked16 } from '@siafoundation/react-icons'
import { useCallback, useMemo } from 'react'
import { useDialog } from '../../../contexts/dialog'
import { useContracts } from '../../../contexts/contracts'
import { pluralize } from '@siafoundation/units'
import { useBlocklistUpdate } from '../../../hooks/useBlocklistUpdate'

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

const hostAddresses = useMemo(
() =>
Object.entries(multiSelect.selectionMap).map(([_, item]) => item.hostIp),
[multiSelect.selectionMap]
)
const { openConfirmDialog } = useDialog()
const blocklistUpdate = useBlocklistUpdate()

const remove = useCallback(async () => {
blocklistUpdate([], hostAddresses)
multiSelect.deselectAll()
}, [blocklistUpdate, multiSelect, hostAddresses])

return (
<Button
aria-label="remove host addresses from blocklist"
tip="Remove host addresses from blocklist"
onClick={() => {
openConfirmDialog({
title: `Remove ${pluralize(
multiSelect.selectionCount,
'host'
)} from blocklist`,
action: 'Remove from blocklist',
variant: 'red',
body: (
<div className="flex flex-col gap-1">
<Paragraph size="14">
Are you sure you would like to remove{' '}
{pluralize(
multiSelect.selectionCount,
'host address',
'host addresses'
)}{' '}
from the blocklist?
</Paragraph>
</div>
),
onConfirm: remove,
})
}}
>
<ListChecked16 />
Remove from blocklist
</Button>
)
}
12 changes: 12 additions & 0 deletions apps/renterd/components/Contracts/ContractsBatchMenu/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import { MultiSelectionMenu } from '@siafoundation/design-system'
import { useContracts } from '../../../contexts/contracts'
import { ContractsBatchDelete } from './ContractsBatchDelete'
import { ContractsAddBlocklist } from './ContractsAddBlocklist'
import { ContractsAddAllowlist } from './ContractsAddAllowlist'
import { ContractsRemoveBlocklist } from './ContractsRemoveBlocklist'
import { ContractsRemoveAllowlist } from './ContractsRemoveAllowlist'

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

return (
<MultiSelectionMenu multiSelect={multiSelect} entityWord="contract">
<div className="flex flex-col gap-1">
<ContractsAddAllowlist />
<ContractsAddBlocklist />
</div>
<div className="flex flex-col gap-1">
<ContractsRemoveAllowlist />
<ContractsRemoveBlocklist />
</div>
<ContractsBatchDelete />
</MultiSelectionMenu>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export function AllowlistForm() {
<div className="flex-1 overflow-hidden !-m-2">
{filtered.length ? (
<ScrollArea>
<div className="p-2">
<div className="p-2" data-testid="allowlistPublicKeys">
<PoolSelected
options={
filtered.map((publicKey) => ({
Expand Down
Loading
Loading