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 Nov 27, 2024
1 parent 01ff510 commit ab257dc
Show file tree
Hide file tree
Showing 10 changed files with 150 additions and 13 deletions.
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.
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 resetAll = 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,
'host'
)}`,
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={resetAll}
>
<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 />,
}
}
4 changes: 2 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 @@ -32,7 +32,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
40 changes: 33 additions & 7 deletions apps/hostd/contexts/contracts/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import {
useDatasetEmptyState,
useServerFilters,
getContractsTimeRangeBlockHeight,
Maybe,
useMultiSelect,
} from '@siafoundation/design-system'
import { useRouter } from 'next/router'
import { ContractStatus } from '@siafoundation/hostd-types'
import { useContracts as useContractsData } from '@siafoundation/hostd-react'
import { createContext, useContext, useMemo } from 'react'
import {
columnsDefaultVisible,
ContractData,
defaultSortField,
SortField,
sortOptions,
Expand Down Expand Up @@ -70,7 +73,7 @@ function useContractsMain() {
},
})

const dataset = useDataset({
const _datasetPage = useDataset({
response,
})

Expand All @@ -81,36 +84,58 @@ function useContractsMain() {

const isValidating = response.isValidating
const error = response.error
const dataState = useDatasetEmptyState(dataset, isValidating, error, filters)
const dataState = useDatasetEmptyState(
_datasetPage,
isValidating,
error,
filters
)

const { estimatedBlockHeight, isSynced, nodeBlockHeight } = useSyncStatus()
const currentHeight = isSynced ? nodeBlockHeight : estimatedBlockHeight

const { range: contractsTimeRange } = useMemo(
() => getContractsTimeRangeBlockHeight(currentHeight, dataset || []),
[currentHeight, dataset]
() => getContractsTimeRangeBlockHeight(currentHeight, _datasetPage || []),
[currentHeight, _datasetPage]
)

const multiSelect = useMultiSelect(_datasetPage)

const datasetPage = useMemo<Maybe<ContractData[]>>(() => {
if (!_datasetPage) {
return undefined
}
return _datasetPage.map((datum) => {
return {
...datum,
onClick: (e: React.MouseEvent<HTMLTableRowElement>) =>
multiSelect.onSelect(datum.id, e),
isSelected: !!multiSelect.selectionMap[datum.id],
}
})
}, [_datasetPage, multiSelect])

const siascanUrl = useSiascanUrl()

const cellContext = useMemo(
() => ({
contractsTimeRange,
currentHeight,
siascanUrl,
multiSelect,
}),
[contractsTimeRange, currentHeight, siascanUrl]
[contractsTimeRange, currentHeight, siascanUrl, multiSelect]
)

return {
dataState,
offset,
limit,
cellContext,
pageCount: dataset?.length || 0,
pageCount: datasetPage?.length || 0,
totalCount: response.data?.count,
columns: filteredTableColumns,
dataset,
datasetPage,
configurableColumns,
enabledColumns,
sortableColumns,
Expand All @@ -128,6 +153,7 @@ function useContractsMain() {
removeFilter,
removeLastFilter,
resetFilters,
multiSelect,
}
}

Expand Down

0 comments on commit ab257dc

Please sign in to comment.