Skip to content

Commit

Permalink
feat(renterd): contracts batch manage blocklist and allowlist
Browse files Browse the repository at this point in the history
  • Loading branch information
alexfreska committed Nov 27, 2024
1 parent 35910f1 commit e335641
Show file tree
Hide file tree
Showing 14 changed files with 378 additions and 28 deletions.
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

0 comments on commit e335641

Please sign in to comment.