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): hosts multi-select and batch manage blocklist and allowlist #829

Merged
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/metal-numbers-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'renterd': minor
---

The hosts multi-select menu now supports bulk adding and removing to both the allowlist and blocklists.
5 changes: 5 additions & 0 deletions .changeset/pink-buses-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'renterd': minor
---

- The hosts table now supports multi-select.
5 changes: 5 additions & 0 deletions .changeset/sour-eagles-tickle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'renterd': minor
---

The host map must now be explicitly toggled open with the action button in the navbar.
17 changes: 10 additions & 7 deletions apps/renterd-e2e/src/fixtures/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,13 @@ export const getContractRowByIndex = step(
}
)

export const getContractRows = step('get contract rows', async (page: Page) => {
return page
.getByTestId('contractsTable')
.locator('tbody')
.getByRole('row')
.all()
})
export function getContractRows(page: Page) {
return page.getByTestId('contractsTable').locator('tbody').getByRole('row')
}

export const getContractRowsAll = step(
'get contract rows',
async (page: Page) => {
return getContractRows(page).all()
}
)
8 changes: 8 additions & 0 deletions apps/renterd-e2e/src/fixtures/hosts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ export const openRowHostContextMenu = step(
}
)

export function getHostRows(page: Page) {
return page.getByTestId('hostsTable').locator('tbody').getByRole('row')
}

export const getHostRowsAll = step('get host rows', async (page: Page) => {
return getHostRows(page).all()
})

export const openManageListsDialog = step(
'open manage lists dialog',
async (page: Page) => {
Expand Down
10 changes: 5 additions & 5 deletions apps/renterd-e2e/src/specs/contracts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { navigateToContracts } from '../fixtures/navigate'
import { afterTest, beforeTest } from '../fixtures/beforeTest'
import {
getContractRowByIndex,
getContractRows,
getContractRowsAll,
getContractsSummaryRow,
} from '../fixtures/contracts'
import { openManageListsDialog } from '../fixtures/hosts'
Expand Down Expand Up @@ -56,7 +56,7 @@ test('contracts prunable size', async ({ page }) => {
await expect(summarySize).toBeVisible()

// Check that the prunable size is visible for all contracts.
const rows = await getContractRows(page)
const rows = await getContractRowsAll(page)
for (const row of rows) {
const prunableSize = row.getByLabel('prunable size')
await expect(prunableSize).toBeVisible()
Expand All @@ -65,7 +65,7 @@ test('contracts prunable size', async ({ page }) => {

test('contracts bulk delete', async ({ page }) => {
await navigateToContracts({ page })
const rows = await getContractRows(page)
const rows = await getContractRowsAll(page)
for (const row of rows) {
await row.click()
}
Expand All @@ -83,7 +83,7 @@ test('contracts bulk delete', async ({ page }) => {

test('contracts bulk allowlist', async ({ page }) => {
await navigateToContracts({ page })
const rows = await getContractRows(page)
const rows = await getContractRowsAll(page)
for (const row of rows) {
await row.click()
}
Expand Down Expand Up @@ -119,7 +119,7 @@ test('contracts bulk allowlist', async ({ page }) => {

test('contracts bulk blocklist', async ({ page }) => {
await navigateToContracts({ page })
const rows = await getContractRows(page)
const rows = await getContractRowsAll(page)
for (const row of rows) {
await row.click()
}
Expand Down
105 changes: 104 additions & 1 deletion apps/renterd-e2e/src/specs/hosts.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { test, expect } from '@playwright/test'
import { navigateToHosts } from '../fixtures/navigate'
import { afterTest, beforeTest } from '../fixtures/beforeTest'
import { getHostRowByIndex } from '../fixtures/hosts'
import {
getHostRowByIndex,
getHostRows,
getHostRowsAll,
openManageListsDialog,
} from '../fixtures/hosts'

test.beforeEach(async ({ page }) => {
await beforeTest(page, {
Expand All @@ -23,3 +28,101 @@ test('hosts explorer shows all hosts', async ({ page }) => {
await expect(row2).toBeVisible()
await expect(row3).toBeVisible()
})

test('hosts bulk allowlist', async ({ page }) => {
await navigateToHosts({ page })
const rows = await getHostRowsAll(page)
for (const row of rows) {
await row.click()
}

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

// Add selected 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()
await expect(
getHostRows(page).getByTestId('allow').getByTestId('blocked')
).toHaveCount(0)
await expect(
getHostRows(page).getByTestId('allow').getByTestId('allowed')
).toHaveCount(3)

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

// Remove selected 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()
await dialog.getByLabel('close').click()
await expect(
getHostRows(page).getByTestId('allow').getByTestId('blocked')
).toHaveCount(0)
await expect(
getHostRows(page).getByTestId('allow').getByTestId('allowed')
).toHaveCount(3)
})

test('hosts bulk blocklist', async ({ page }) => {
await navigateToHosts({ page })
const rows = await getHostRowsAll(page)
for (const row of rows) {
await row.click()
}

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

// Add selected 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()
await expect(
getHostRows(page).getByTestId('allow').getByTestId('blocked')
).toHaveCount(3)
await expect(
getHostRows(page).getByTestId('allow').getByTestId('allowed')
).toHaveCount(0)

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

// Remove selected 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()
await dialog.getByLabel('close').click()
await expect(
getHostRows(page).getByTestId('allow').getByTestId('blocked')
).toHaveCount(0)
await expect(
getHostRows(page).getByTestId('allow').getByTestId('allowed')
).toHaveCount(3)
})
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { Button, Paragraph } from '@siafoundation/design-system'
import { ListChecked16 } from '@siafoundation/react-icons'
import { useCallback, useMemo } from 'react'
import { useDialog } from '../../../contexts/dialog'
import { useMemo } from 'react'
import { useContracts } from '../../../contexts/contracts'
import { pluralize } from '@siafoundation/units'
import { useAllowlistUpdate } from '../../../hooks/useAllowlistUpdate'
import { BulkAddAllowlist } from '../../bulkActions/BulkAddAllowlist'

export function ContractsAddAllowlist() {
const { multiSelect } = useContracts()
Expand All @@ -14,41 +10,6 @@ export function ContractsAddAllowlist() {
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>
)
return <BulkAddAllowlist multiSelect={multiSelect} publicKeys={publicKeys} />
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { Button, Paragraph } from '@siafoundation/design-system'
import { ListChecked16 } from '@siafoundation/react-icons'
import { useCallback, useMemo } from 'react'
import { useDialog } from '../../../contexts/dialog'
import { useMemo } from 'react'
import { useContracts } from '../../../contexts/contracts'
import { pluralize } from '@siafoundation/units'
import { useBlocklistUpdate } from '../../../hooks/useBlocklistUpdate'
import { BulkAddBlocklist } from '../../bulkActions/BulkAddBlocklist'

export function ContractsAddBlocklist() {
const { multiSelect } = useContracts()
Expand All @@ -14,45 +10,7 @@ export function ContractsAddBlocklist() {
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>
<BulkAddBlocklist multiSelect={multiSelect} hostAddresses={hostAddresses} />
)
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { Button, Paragraph } from '@siafoundation/design-system'
import { ListChecked16 } from '@siafoundation/react-icons'
import { useCallback, useMemo } from 'react'
import { useDialog } from '../../../contexts/dialog'
import { useMemo } from 'react'
import { useContracts } from '../../../contexts/contracts'
import { pluralize } from '@siafoundation/units'
import { useAllowlistUpdate } from '../../../hooks/useAllowlistUpdate'
import { BulkRemoveAllowlist } from '../../bulkActions/BulkRemoveAllowlist'

export function ContractsRemoveAllowlist() {
const { multiSelect } = useContracts()
Expand All @@ -14,41 +10,8 @@ export function ContractsRemoveAllowlist() {
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>
<BulkRemoveAllowlist multiSelect={multiSelect} publicKeys={publicKeys} />
)
}
Loading
Loading