From ab257dc0dbcb08c098d53725000a7e4af2bfa7db Mon Sep 17 00:00:00 2001 From: Alex Freska Date: Wed, 27 Nov 2024 16:47:46 -0500 Subject: [PATCH] feat(hostd): contracts multi-select and bulk integrity check --- .changeset/tame-files-dress.md | 5 ++ .changeset/wicked-cycles-dress.md | 5 ++ .../Contracts/ContractContextMenu.tsx | 7 +- .../ContractsBulkIntegrityCheck.tsx | 70 +++++++++++++++++++ .../Contracts/ContractsBulkMenu/index.tsx | 13 ++++ apps/hostd/components/Contracts/Layout.tsx | 2 + apps/hostd/components/Contracts/index.tsx | 4 +- apps/hostd/contexts/contracts/columns.tsx | 12 +++- apps/hostd/contexts/contracts/dataset.ts | 5 +- apps/hostd/contexts/contracts/index.tsx | 40 +++++++++-- 10 files changed, 150 insertions(+), 13 deletions(-) create mode 100644 .changeset/tame-files-dress.md create mode 100644 .changeset/wicked-cycles-dress.md create mode 100644 apps/hostd/components/Contracts/ContractsBulkMenu/ContractsBulkIntegrityCheck.tsx create mode 100644 apps/hostd/components/Contracts/ContractsBulkMenu/index.tsx diff --git a/.changeset/tame-files-dress.md b/.changeset/tame-files-dress.md new file mode 100644 index 000000000..3cdaa3652 --- /dev/null +++ b/.changeset/tame-files-dress.md @@ -0,0 +1,5 @@ +--- +'hostd': minor +--- + +The contracts table now supports bulk integrity checks. diff --git a/.changeset/wicked-cycles-dress.md b/.changeset/wicked-cycles-dress.md new file mode 100644 index 000000000..0797ab236 --- /dev/null +++ b/.changeset/wicked-cycles-dress.md @@ -0,0 +1,5 @@ +--- +'hostd': minor +--- + +The contracts table now support multi-select. diff --git a/apps/hostd/components/Contracts/ContractContextMenu.tsx b/apps/hostd/components/Contracts/ContractContextMenu.tsx index 9d4b80fc4..1340fac68 100644 --- a/apps/hostd/components/Contracts/ContractContextMenu.tsx +++ b/apps/hostd/components/Contracts/ContractContextMenu.tsx @@ -65,7 +65,12 @@ export function ContractContextMenu({ return ( + } diff --git a/apps/hostd/components/Contracts/ContractsBulkMenu/ContractsBulkIntegrityCheck.tsx b/apps/hostd/components/Contracts/ContractsBulkMenu/ContractsBulkIntegrityCheck.tsx new file mode 100644 index 000000000..3fddeda21 --- /dev/null +++ b/apps/hostd/components/Contracts/ContractsBulkMenu/ContractsBulkIntegrityCheck.tsx @@ -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 hostd{' '} + openDialog('alerts')}>alerts for + status updates. + + ), + }), + after: () => { + multiSelect.deselectAll() + }, + } + ) + }, [multiSelect, ids, integrityCheck, openDialog]) + + return ( + + ) +} diff --git a/apps/hostd/components/Contracts/ContractsBulkMenu/index.tsx b/apps/hostd/components/Contracts/ContractsBulkMenu/index.tsx new file mode 100644 index 000000000..0b73ecb6b --- /dev/null +++ b/apps/hostd/components/Contracts/ContractsBulkMenu/index.tsx @@ -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 ( + + + + ) +} diff --git a/apps/hostd/components/Contracts/Layout.tsx b/apps/hostd/components/Contracts/Layout.tsx index 1dfd2c3b4..59c6d3ff0 100644 --- a/apps/hostd/components/Contracts/Layout.tsx +++ b/apps/hostd/components/Contracts/Layout.tsx @@ -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 { @@ -19,5 +20,6 @@ export function useLayoutProps(): HostdAuthedPageLayoutProps { actions: , stats: , size: 'full', + dockedControls: , } } diff --git a/apps/hostd/components/Contracts/index.tsx b/apps/hostd/components/Contracts/index.tsx index a25e017d4..e585139fa 100644 --- a/apps/hostd/components/Contracts/index.tsx +++ b/apps/hostd/components/Contracts/index.tsx @@ -7,7 +7,7 @@ import { StateError } from './StateError' export function Contracts() { const { columns, - dataset, + datasetPage, sortField, sortDirection, sortableColumns, @@ -32,7 +32,7 @@ export function Contracts() { ) : null } pageSize={limit} - data={dataset} + data={datasetPage} columns={columns} sortableColumns={sortableColumns} sortDirection={sortDirection} diff --git a/apps/hostd/contexts/contracts/columns.tsx b/apps/hostd/contexts/contracts/columns.tsx index 2caf7a2f9..b7192883e 100644 --- a/apps/hostd/contexts/contracts/columns.tsx +++ b/apps/hostd/contexts/contracts/columns.tsx @@ -8,6 +8,8 @@ import { ContractTimeline, ValueNum, ValueScFiat, + Checkbox, + MultiSelect, } from '@siafoundation/design-system' import { ArrowUpLeft16, @@ -26,6 +28,7 @@ type Context = { endHeight: number } siascanUrl: string + multiSelect: MultiSelect } type ContractsTableColumn = TableColumn< @@ -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 } }) => ( + + ), render: ({ data: { id, status } }) => ( ), diff --git a/apps/hostd/contexts/contracts/dataset.ts b/apps/hostd/contexts/contracts/dataset.ts index d7b72ab0c..2f259dc82 100644 --- a/apps/hostd/contexts/contracts/dataset.ts +++ b/apps/hostd/contexts/contracts/dataset.ts @@ -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 }) { - return useMemo(() => { + return useMemo>(() => { if (!response.data) { - return null + return undefined } return ( response.data.contracts?.map((contract) => { diff --git a/apps/hostd/contexts/contracts/index.tsx b/apps/hostd/contexts/contracts/index.tsx index eee424d10..aee79989a 100644 --- a/apps/hostd/contexts/contracts/index.tsx +++ b/apps/hostd/contexts/contracts/index.tsx @@ -3,6 +3,8 @@ import { useDatasetEmptyState, useServerFilters, getContractsTimeRangeBlockHeight, + Maybe, + useMultiSelect, } from '@siafoundation/design-system' import { useRouter } from 'next/router' import { ContractStatus } from '@siafoundation/hostd-types' @@ -10,6 +12,7 @@ import { useContracts as useContractsData } from '@siafoundation/hostd-react' import { createContext, useContext, useMemo } from 'react' import { columnsDefaultVisible, + ContractData, defaultSortField, SortField, sortOptions, @@ -70,7 +73,7 @@ function useContractsMain() { }, }) - const dataset = useDataset({ + const _datasetPage = useDataset({ response, }) @@ -81,16 +84,37 @@ 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>(() => { + if (!_datasetPage) { + return undefined + } + return _datasetPage.map((datum) => { + return { + ...datum, + onClick: (e: React.MouseEvent) => + multiSelect.onSelect(datum.id, e), + isSelected: !!multiSelect.selectionMap[datum.id], + } + }) + }, [_datasetPage, multiSelect]) + const siascanUrl = useSiascanUrl() const cellContext = useMemo( @@ -98,8 +122,9 @@ function useContractsMain() { contractsTimeRange, currentHeight, siascanUrl, + multiSelect, }), - [contractsTimeRange, currentHeight, siascanUrl] + [contractsTimeRange, currentHeight, siascanUrl, multiSelect] ) return { @@ -107,10 +132,10 @@ function useContractsMain() { offset, limit, cellContext, - pageCount: dataset?.length || 0, + pageCount: datasetPage?.length || 0, totalCount: response.data?.count, columns: filteredTableColumns, - dataset, + datasetPage, configurableColumns, enabledColumns, sortableColumns, @@ -128,6 +153,7 @@ function useContractsMain() { removeFilter, removeLastFilter, resetFilters, + multiSelect, } }