Skip to content

Commit

Permalink
feat(hostd): contracts multi-select and bulk integrity check
Browse files Browse the repository at this point in the history
  • Loading branch information
alexfreska committed Dec 3, 2024
1 parent b591ebf commit 0171d92
Show file tree
Hide file tree
Showing 18 changed files with 288 additions and 19 deletions.
5 changes: 5 additions & 0 deletions .changeset/sour-ants-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@siafoundation/clusterd': minor
---

Added distinct renterdWaitForContracts and hostdWaitForContracts methods.
5 changes: 5 additions & 0 deletions .changeset/tame-files-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'hostd': minor
---

The contracts table now supports bulk integrity checks.
5 changes: 5 additions & 0 deletions .changeset/wicked-cycles-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'hostd': minor
---

The contracts table now support multi-select.
6 changes: 4 additions & 2 deletions apps/hostd-e2e/src/fixtures/beforeTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { login } from './login'
import { Page } from 'playwright'
import {
clusterd,
hostdWaitForContracts,
setupCluster,
teardownCluster,
} from '@siafoundation/clusterd'
Expand All @@ -12,11 +13,12 @@ import {
mockApiSiaScanExchangeRates,
} from '@siafoundation/e2e'

export async function beforeTest(page: Page) {
export async function beforeTest(page: Page, { renterdCount = 0 }) {
await mockApiSiaScanExchangeRates({ page })
await mockApiSiaCentralHostsNetworkAverages({ page })
await setupCluster({ hostdCount: 1 })
await setupCluster({ hostdCount: 1, renterdCount })
const hostdNode = clusterd.nodes.find((n) => n.type === 'hostd')
await hostdWaitForContracts({ hostdNode, renterdCount })
await login({
page,
address: hostdNode.apiAddress,
Expand Down
45 changes: 45 additions & 0 deletions apps/hostd-e2e/src/fixtures/contracts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Page } from '@playwright/test'
import { maybeExpectAndReturn, step } from '@siafoundation/e2e'

export const getContractRowById = step(
'get contract row by ID',
async (page: Page, id: string) => {
return page.getByTestId('contractsTable').getByTestId(id)
}
)

export const getContractsSummaryRow = step(
'get contracts summary row',
async (page: Page) => {
return page
.getByTestId('contractsTable')
.locator('thead')
.getByRole('row')
.nth(1)
}
)

export const getContractRowByIndex = step(
'get contract row by index',
async (page: Page, index: number, shouldExpect?: boolean) => {
return maybeExpectAndReturn(
page
.getByTestId('contractsTable')
.locator('tbody')
.getByRole('row')
.nth(index),
shouldExpect
)
}
)

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()
}
)
10 changes: 10 additions & 0 deletions apps/hostd-e2e/src/fixtures/navigate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,13 @@ export const navigateToWallet = step(
await expect(page.getByTestId('navbar').getByText('Wallet')).toBeVisible()
}
)

export const navigateToContracts = step(
'navigate to contracts',
async (page: Page) => {
await page.getByTestId('sidenav').getByLabel('Contracts').click()
await expect(
page.getByTestId('navbar').getByText('Contracts')
).toBeVisible()
}
)
30 changes: 30 additions & 0 deletions apps/hostd-e2e/src/specs/contracts.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { test, expect } from '@playwright/test'
import { navigateToContracts } from '../fixtures/navigate'
import { afterTest, beforeTest } from '../fixtures/beforeTest'
import { getContractRowsAll } from '../fixtures/contracts'

test.beforeEach(async ({ page }) => {
await beforeTest(page, {
renterdCount: 2,
})
})

test.afterEach(async () => {
await afterTest()
})

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

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

// Run check for each contract.
await menu.getByLabel('run integrity check for each contract').click()
await expect(
page.getByText('Integrity checks started for 2 contracts')
).toBeVisible()
})
7 changes: 6 additions & 1 deletion apps/hostd/components/Contracts/ContractContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,12 @@ export function ContractContextMenu({
return (
<DropdownMenu
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,70 @@
import {
Button,
Code,
handleBatchOperation,
Link,
} from '@siafoundation/design-system'
import { DataCheck16 } from '@siafoundation/react-icons'
import { useCallback, useMemo } from 'react'
import { pluralize } from '@siafoundation/units'
import { useContracts } from '../../../contexts/contracts'
import { useContractsIntegrityCheck } from '@siafoundation/hostd-react'
import { useDialog } from '../../../contexts/dialog'

export function ContractsBulkIntegrityCheck() {
const { openDialog } = useDialog()
const { multiSelect } = useContracts()
const integrityCheck = useContractsIntegrityCheck()

const ids = useMemo(
() => Object.entries(multiSelect.selectionMap).map(([_, item]) => item.id),
[multiSelect.selectionMap]
)
const checkAll = useCallback(async () => {
await handleBatchOperation(
ids.map((id) =>
integrityCheck.put({
params: {
id,
},
})
),
{
toastError: ({ successCount, errorCount, totalCount }) => ({
title: `Integrity checks started for ${pluralize(
successCount,
'contract'
)}`,
body: `Error starting integrity checks for ${errorCount}/${totalCount} total contracts.`,
}),
toastSuccess: ({ totalCount }) => ({
title: `Integrity checks started for ${pluralize(
totalCount,
'contract'
)}`,
body: (
<>
Depending on contract data size this operation can take a while.
Check <Code>hostd</Code>{' '}
<Link onClick={() => openDialog('alerts')}>alerts</Link> for
status updates.
</>
),
}),
after: () => {
multiSelect.deselectAll()
},
}
)
}, [multiSelect, ids, integrityCheck, openDialog])

return (
<Button
aria-label="run integrity check for each contract"
tip="Run integrity check for each contract"
onClick={checkAll}
>
<DataCheck16 />
</Button>
)
}
13 changes: 13 additions & 0 deletions apps/hostd/components/Contracts/ContractsBulkMenu/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 { ContractsBulkIntegrityCheck } from './ContractsBulkIntegrityCheck'

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

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

export const Layout = HostdAuthedLayout
export function useLayoutProps(): HostdAuthedPageLayoutProps {
Expand All @@ -19,5 +20,6 @@ export function useLayoutProps(): HostdAuthedPageLayoutProps {
actions: <ContractsActionsMenu />,
stats: <ContractsFiltersBar />,
size: 'full',
dockedControls: <ContractsBulkMenu />,
}
}
5 changes: 3 additions & 2 deletions apps/hostd/components/Contracts/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { StateError } from './StateError'
export function Contracts() {
const {
columns,
dataset,
datasetPage,
sortField,
sortDirection,
sortableColumns,
Expand All @@ -20,6 +20,7 @@ export function Contracts() {
return (
<div className="p-6 min-w-fit">
<Table
testId="contractsTable"
context={cellContext}
isLoading={dataState === 'loading'}
emptyState={
Expand All @@ -32,7 +33,7 @@ export function Contracts() {
) : null
}
pageSize={limit}
data={dataset}
data={datasetPage}
columns={columns}
sortableColumns={sortableColumns}
sortDirection={sortDirection}
Expand Down
12 changes: 11 additions & 1 deletion apps/hostd/contexts/contracts/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
ContractTimeline,
ValueNum,
ValueScFiat,
Checkbox,
MultiSelect,
} from '@siafoundation/design-system'
import {
ArrowUpLeft16,
Expand All @@ -26,6 +28,7 @@ type Context = {
endHeight: number
}
siascanUrl: string
multiSelect: MultiSelect<ContractData>
}

type ContractsTableColumn = TableColumn<
Expand All @@ -43,7 +46,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, status } }) => (
<ContractContextMenu id={id} status={status} />
),
Expand Down
5 changes: 3 additions & 2 deletions apps/hostd/contexts/contracts/dataset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ import { Contract } from '@siafoundation/hostd-types'
import { useContracts } from '@siafoundation/hostd-react'
import { ContractData } from './types'
import BigNumber from 'bignumber.js'
import { Maybe } from '@siafoundation/design-system'

export function useDataset({
response,
}: {
response: ReturnType<typeof useContracts>
}) {
return useMemo<ContractData[] | null>(() => {
return useMemo<Maybe<ContractData[]>>(() => {
if (!response.data) {
return null
return undefined
}
return (
response.data.contracts?.map((contract) => {
Expand Down
Loading

0 comments on commit 0171d92

Please sign in to comment.