diff --git a/.changeset/eighty-papayas-notice.md b/.changeset/eighty-papayas-notice.md new file mode 100644 index 000000000..ecc09afa3 --- /dev/null +++ b/.changeset/eighty-papayas-notice.md @@ -0,0 +1,5 @@ +--- +'renterd': minor +--- + +The contract graphs are now explicitly toggled open with the action button in the navbar. diff --git a/.changeset/light-pens-turn.md b/.changeset/light-pens-turn.md new file mode 100644 index 000000000..6a63ede97 --- /dev/null +++ b/.changeset/light-pens-turn.md @@ -0,0 +1,5 @@ +--- +'renterd': minor +--- + +The contracts table now supports multi-select. diff --git a/apps/renterd/components/Contracts/ContractContextMenu.tsx b/apps/renterd/components/Contracts/ContractContextMenu.tsx index 74a175385..5c6cb5bec 100644 --- a/apps/renterd/components/Contracts/ContractContextMenu.tsx +++ b/apps/renterd/components/Contracts/ContractContextMenu.tsx @@ -49,7 +49,12 @@ export function ContractContextMenu({ + ) diff --git a/apps/renterd/components/Contracts/index.tsx b/apps/renterd/components/Contracts/index.tsx index af3978c82..1c651bd43 100644 --- a/apps/renterd/components/Contracts/index.tsx +++ b/apps/renterd/components/Contracts/index.tsx @@ -81,7 +81,6 @@ export function Contracts() { sortDirection={sortDirection} sortField={sortField} toggleSort={toggleSort} - focusId={selectedContract?.id} rowSize="default" /> diff --git a/apps/renterd/contexts/contracts/columns.tsx b/apps/renterd/contexts/contracts/columns.tsx index a13ff4c98..249184c72 100644 --- a/apps/renterd/contexts/contracts/columns.tsx +++ b/apps/renterd/contexts/contracts/columns.tsx @@ -11,6 +11,7 @@ import { Separator, Button, LoadingDots, + Checkbox, } from '@siafoundation/design-system' import { ArrowUpLeft16, @@ -40,7 +41,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, hostIp, hostKey } }) => ( ), diff --git a/apps/renterd/contexts/contracts/dataset.tsx b/apps/renterd/contexts/contracts/dataset.tsx index 729afc9c9..16d4fbf42 100644 --- a/apps/renterd/contexts/contracts/dataset.tsx +++ b/apps/renterd/contexts/contracts/dataset.tsx @@ -7,12 +7,9 @@ import { useSyncStatus } from '../../hooks/useSyncStatus' import { blockHeightToTime } from '@siafoundation/units' import { defaultDatasetRefreshInterval } from '../../config/swr' import { usePrunableContractSizes } from './usePrunableContractSizes' +import { Maybe } from '@siafoundation/design-system' -export function useDataset({ - selectContract, -}: { - selectContract: (id: string) => void -}) { +export function useDataset() { const response = useContractsData({ config: { swr: { @@ -29,10 +26,10 @@ export function useDataset({ : syncStatus.estimatedBlockHeight const datasetWithoutPrunable = useMemo< - ContractDataWithoutPrunable[] | null + Maybe >(() => { if (!response.data) { - return null + return undefined } const datums = response.data?.map((c) => { @@ -44,7 +41,6 @@ export function useDataset({ const endTime = blockHeightToTime(currentHeight, endHeight) const datum: ContractDataWithoutPrunable = { id: c.id, - onClick: () => selectContract(c.id), state: c.state, hostIp: c.hostIP, hostKey: c.hostKey, @@ -67,11 +63,14 @@ export function useDataset({ spendingSectorRoots: new BigNumber(c.spending.sectorRoots), spendingFundAccount: new BigNumber(c.spending.fundAccount), size: new BigNumber(c.size), + // selectable + onClick: () => null, + isSelected: false, } return datum }) || [] return datums - }, [response.data, geoHosts, currentHeight, selectContract]) + }, [response.data, geoHosts, currentHeight]) const { prunableSizes, @@ -81,7 +80,7 @@ export function useDataset({ fetchPrunableSizeAll, } = usePrunableContractSizes() - const dataset = useMemo( + const dataset = useMemo>( () => datasetWithoutPrunable?.map((d) => { const datum: ContractData = { @@ -105,7 +104,7 @@ export function useDataset({ ) const hasFetchedAllPrunableSize = useMemo( - () => dataset?.every((d) => d.hasFetchedPrunableSize), + () => !!dataset?.every((d) => d.hasFetchedPrunableSize), [dataset] ) diff --git a/apps/renterd/contexts/contracts/index.tsx b/apps/renterd/contexts/contracts/index.tsx index b097f1e18..41efbd1ff 100644 --- a/apps/renterd/contexts/contracts/index.tsx +++ b/apps/renterd/contexts/contracts/index.tsx @@ -4,16 +4,12 @@ import { useDatasetEmptyState, useClientFilters, useClientFilteredDataset, + useMultiSelect, + Maybe, } from '@siafoundation/design-system' import { useRouter } from 'next/router' import { useContracts as useContractsData } from '@siafoundation/renterd-react' -import { - createContext, - useCallback, - useContext, - useMemo, - useState, -} from 'react' +import { createContext, useContext, useMemo, useState } from 'react' import { ContractData, ContractTableContext, @@ -53,20 +49,6 @@ function useContractsMain() { ? syncStatus.nodeBlockHeight : syncStatus.estimatedBlockHeight - const [selectedContractId, setSelectedContractId] = useState() - const selectContract = useCallback( - (id: string) => { - if (selectedContractId === id) { - setSelectedContractId(undefined) - return - } - setSelectedContractId(id) - setViewMode('detail') - setGraphMode('spending') - }, - [selectedContractId, setSelectedContractId, setViewMode] - ) - const { dataset, isFetchingPrunableSizeAll, @@ -74,12 +56,7 @@ function useContractsMain() { fetchPrunableSize, fetchPrunableSizeAll, hasFetchedAllPrunableSize, - } = useDataset({ selectContract }) - - const selectedContract = useMemo( - () => dataset?.find((d) => d.id === selectedContractId), - [dataset, selectedContractId] - ) + } = useDataset() const { filters, setFilter, removeFilter, removeLastFilter, resetFilters } = useClientFilters() @@ -111,7 +88,7 @@ function useContractsMain() { sortDirection, }) - const datasetPage = useMemo(() => { + const _datasetPage = useMemo>(() => { if (!datasetFiltered) { return undefined } @@ -119,8 +96,8 @@ function useContractsMain() { }, [datasetFiltered, offset, limit]) const { range: contractsTimeRange } = useMemo( - () => getContractsTimeRangeBlockHeight(currentHeight, datasetPage || []), - [currentHeight, datasetPage] + () => getContractsTimeRangeBlockHeight(currentHeight, _datasetPage || []), + [currentHeight, _datasetPage] ) const filteredTableColumns = useMemo( @@ -142,6 +119,22 @@ function useContractsMain() { const filteredStats = useFilteredStats({ datasetFiltered }) + 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 cellContext = useMemo(() => { const context: ContractTableContext = { currentHeight: syncStatus.estimatedBlockHeight, @@ -151,6 +144,7 @@ function useContractsMain() { isFetchingPrunableSizeAll, fetchPrunableSizeAll, filteredStats, + multiSelect, } return context }, [ @@ -161,15 +155,23 @@ function useContractsMain() { isFetchingPrunableSizeAll, fetchPrunableSizeAll, filteredStats, + multiSelect, ]) + const selectedContract = useMemo(() => { + if (multiSelect.selectedIds.length === 1) { + const selectedContractId = multiSelect.selectedIds[0] + return dataset?.find((d) => d.id === selectedContractId) + } + }, [dataset, multiSelect.selectedIds]) + const thirtyDaysAgo = new Date().getTime() - daysInMilliseconds(30) const { contractMetrics: allContractsSpendingMetrics } = useContractMetrics({ start: thirtyDaysAgo, }) const { contractMetrics: selectedContractSpendingMetrics } = useContractMetrics({ - contractId: selectedContractId, + contractId: selectedContract?.id, start: selectedContract?.startTime || 0, disabled: !selectedContract, }) @@ -209,13 +211,13 @@ function useContractsMain() { graphMode, setGraphMode, selectedContract, - selectContract, allContractsSpendingMetrics, selectedContractSpendingMetrics, isFetchingPrunableSizeAll, isFetchingPrunableSizeById, fetchPrunableSize, fetchPrunableSizeAll, + multiSelect, } } diff --git a/apps/renterd/contexts/contracts/types.ts b/apps/renterd/contexts/contracts/types.ts index 0b4ad7aa6..377183885 100644 --- a/apps/renterd/contexts/contracts/types.ts +++ b/apps/renterd/contexts/contracts/types.ts @@ -1,6 +1,7 @@ import { ContractState, ContractUsability } from '@siafoundation/renterd-types' import BigNumber from 'bignumber.js' import { useFilteredStats } from './useFilteredStats' +import { MultiSelect } from '@siafoundation/design-system' export type ContractTableContext = { currentHeight: number @@ -15,11 +16,11 @@ export type ContractTableContext = { isFetchingPrunableSizeAll: boolean // totals filteredStats: ReturnType + multiSelect: MultiSelect } -export type ContractDataWithoutPrunable = { +export type ContractData = { id: string - onClick: () => void hostIp: string hostKey: string state: ContractState @@ -42,15 +43,26 @@ export type ContractDataWithoutPrunable = { spendingSectorRoots: BigNumber spendingFundAccount: BigNumber size: BigNumber -} -export type ContractData = ContractDataWithoutPrunable & { + // selectable + onClick: (e: React.MouseEvent) => void + isSelected: boolean + + // prunable prunableSize?: BigNumber isFetchingPrunableSize: boolean hasFetchedPrunableSize: boolean fetchPrunableSize: () => void } +export type ContractDataWithoutPrunable = Omit< + ContractData, + | 'prunableSize' + | 'isFetchingPrunableSize' + | 'hasFetchedPrunableSize' + | 'fetchPrunableSize' +> + export type TableColumnId = | 'actions' | 'contractId' diff --git a/apps/renterd/contexts/hosts/dataset.ts b/apps/renterd/contexts/hosts/dataset.ts index d57aaa37f..7b32d9fc2 100644 --- a/apps/renterd/contexts/hosts/dataset.ts +++ b/apps/renterd/contexts/hosts/dataset.ts @@ -9,7 +9,7 @@ import { } from '@siafoundation/renterd-react' import { ContractData } from '../contracts/types' import { SiaCentralHost } from '@siafoundation/sia-central-types' -import { objectEntries } from '@siafoundation/design-system' +import { Maybe, objectEntries } from '@siafoundation/design-system' export function useDataset({ response, @@ -21,7 +21,7 @@ export function useDataset({ onHostSelect, }: { response: ReturnType - allContracts: ContractData[] + allContracts: Maybe allowlist: ReturnType blocklist: ReturnType isAllowlistActive: boolean @@ -61,7 +61,7 @@ export function useDataset({ ]) } -function getHostFields(host: Host, allContracts: ContractData[]) { +function getHostFields(host: Host, allContracts: Maybe) { return { id: host.publicKey, netAddress: host.netAddress,