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/.changeset/plenty-tomatoes-teach.md b/.changeset/plenty-tomatoes-teach.md new file mode 100644 index 000000000..4a1592620 --- /dev/null +++ b/.changeset/plenty-tomatoes-teach.md @@ -0,0 +1,5 @@ +--- +'renterd': minor +--- + +The hosts graphs no longer include a count metric. diff --git a/.changeset/shaggy-dolls-smash.md b/.changeset/shaggy-dolls-smash.md deleted file mode 100644 index bdd025472..000000000 --- a/.changeset/shaggy-dolls-smash.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'renterd': minor ---- - -Add overall contract count metrics graph. diff --git a/.changeset/shiny-gifts-hope.md b/.changeset/shiny-gifts-hope.md new file mode 100644 index 000000000..71a8a5523 --- /dev/null +++ b/.changeset/shiny-gifts-hope.md @@ -0,0 +1,5 @@ +--- +'renterd': minor +--- + +The contracts multi-select menu now supports batch deletion. diff --git a/.changeset/two-seas-judge.md b/.changeset/two-seas-judge.md deleted file mode 100644 index 1387dd6a9..000000000 --- a/.changeset/two-seas-judge.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@siafoundation/renterd-js': minor -'@siafoundation/renterd-react': minor -'@siafoundation/renterd-types': minor ---- - -Add overall contract churn metrics API. diff --git a/apps/renterd-e2e/src/specs/contracts.spec.ts b/apps/renterd-e2e/src/specs/contracts.spec.ts index 4c7a93f7f..676b674d0 100644 --- a/apps/renterd-e2e/src/specs/contracts.spec.ts +++ b/apps/renterd-e2e/src/specs/contracts.spec.ts @@ -61,3 +61,21 @@ test('contracts prunable size', async ({ page }) => { await expect(prunableSize).toBeVisible() } }) + +test('batch delete contracts', async ({ page }) => { + await navigateToContracts({ page }) + const rows = await getContractRows(page) + for (const row of rows) { + await row.click() + } + + // Delete selected contracts. + const menu = page.getByLabel('contract multi-select menu') + await menu.getByLabel('delete selected contracts').click() + const dialog = page.getByRole('dialog') + await dialog.getByRole('button', { name: 'Delete' }).click() + + await expect( + page.getByText('There are currently no active contracts') + ).toBeVisible() +}) 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/ContractMetrics.tsx b/apps/renterd/components/Contracts/ContractMetrics.tsx index 33fe55fd9..b2f6cecb4 100644 --- a/apps/renterd/components/Contracts/ContractMetrics.tsx +++ b/apps/renterd/components/Contracts/ContractMetrics.tsx @@ -7,7 +7,6 @@ export function ContractMetrics() { selectedContract, allContractsSpendingMetrics, selectedContractSpendingMetrics, - contractsCountMetrics, graphMode, setGraphMode, } = useContracts() @@ -31,14 +30,6 @@ export function ContractMetrics() { {stripPrefix(selectedContract.id).slice(0, 6)} )} - {!selectedContract && ( - - )} ) @@ -66,17 +57,6 @@ export function ContractMetrics() { emptyState={} /> )} - {graphMode === 'count' && !selectedContract && ( - } - /> - )} ) } diff --git a/apps/renterd/components/Contracts/ContractsBatchMenu/ContractsBatchDelete.tsx b/apps/renterd/components/Contracts/ContractsBatchMenu/ContractsBatchDelete.tsx new file mode 100644 index 000000000..cf12076c2 --- /dev/null +++ b/apps/renterd/components/Contracts/ContractsBatchMenu/ContractsBatchDelete.tsx @@ -0,0 +1,63 @@ +import { Button, Paragraph } from '@siafoundation/design-system' +import { Delete16 } from '@siafoundation/react-icons' +import { useContractDelete } from '@siafoundation/renterd-react' +import { useCallback, useMemo } from 'react' +import { useDialog } from '../../../contexts/dialog' +import { useContracts } from '../../../contexts/contracts' +import { handleBatchOperation } from '../../../lib/handleBatchOperation' +import { pluralize } from '@siafoundation/units' + +export function ContractsBatchDelete() { + const { multiSelect } = useContracts() + + const ids = useMemo( + () => Object.entries(multiSelect.selectionMap).map(([_, item]) => item.id), + [multiSelect.selectionMap] + ) + const { openConfirmDialog } = useDialog() + const deleteContract = useContractDelete() + const deleteAll = useCallback(async () => { + await handleBatchOperation( + ids.map((id) => deleteContract.delete({ params: { id } })), + { + toastError: ({ successCount, errorCount, totalCount }) => ({ + title: `${pluralize(successCount, 'contract')} deleted`, + body: `Error deleting ${errorCount}/${totalCount} total contracts.`, + }), + toastSuccess: ({ totalCount }) => ({ + title: `${pluralize(totalCount, 'contract')} deleted`, + }), + after: () => { + multiSelect.deselectAll() + }, + } + ) + }, [multiSelect, ids, deleteContract]) + + return ( + + ) +} diff --git a/apps/renterd/components/Contracts/ContractsBatchMenu/index.tsx b/apps/renterd/components/Contracts/ContractsBatchMenu/index.tsx new file mode 100644 index 000000000..e45343bf8 --- /dev/null +++ b/apps/renterd/components/Contracts/ContractsBatchMenu/index.tsx @@ -0,0 +1,13 @@ +import { MultiSelectionMenu } from '@siafoundation/design-system' +import { useContracts } from '../../../contexts/contracts' +import { ContractsBatchDelete } from './ContractsBatchDelete' + +export function ContractsBatchMenu() { + const { multiSelect } = useContracts() + + return ( + + + + ) +} diff --git a/apps/renterd/components/Contracts/Layout.tsx b/apps/renterd/components/Contracts/Layout.tsx index 381364aa2..5a86058f2 100644 --- a/apps/renterd/components/Contracts/Layout.tsx +++ b/apps/renterd/components/Contracts/Layout.tsx @@ -7,6 +7,7 @@ import { } from '../RenterdAuthedLayout' import { ContractsActionsMenu } from './ContractsActionsMenu' import { ContractsFilterBar } from './ContractsFilterBar' +import { ContractsBatchMenu } from './ContractsBatchMenu' export const Layout = RenterdAuthedLayout export function useLayoutProps(): RenterdAuthedPageLayoutProps { @@ -20,5 +21,6 @@ export function useLayoutProps(): RenterdAuthedPageLayoutProps { stats: , size: 'full', scroll: false, + dockedControls: , } } 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/components/Files/batchActions/FilesBatchDelete.tsx b/apps/renterd/components/Files/batchActions/FilesBatchDelete.tsx index a0a8def17..2224b51b1 100644 --- a/apps/renterd/components/Files/batchActions/FilesBatchDelete.tsx +++ b/apps/renterd/components/Files/batchActions/FilesBatchDelete.tsx @@ -1,15 +1,11 @@ -import { - Button, - Paragraph, - triggerSuccessToast, - triggerErrorToast, - MultiSelect, -} from '@siafoundation/design-system' +import { Button, Paragraph, MultiSelect } from '@siafoundation/design-system' import { Delete16 } from '@siafoundation/react-icons' import { useCallback, useMemo } from 'react' import { useDialog } from '../../../contexts/dialog' import { useObjectsRemove } from '@siafoundation/renterd-react' import { ObjectData } from '../../../contexts/filesManager/types' +import { handleBatchOperation } from '../../../lib/handleBatchOperation' +import { pluralize } from '@siafoundation/units' export function FilesBatchDelete({ multiSelect, @@ -26,29 +22,29 @@ export function FilesBatchDelete({ ) const { openConfirmDialog } = useDialog() const objectsRemove = useObjectsRemove() - const deleteFiles = useCallback(async () => { - const totalCount = filesToDelete.length - let errorCount = 0 - for (const { bucket, prefix } of filesToDelete) { - const response = await objectsRemove.post({ - payload: { - bucket, - prefix, + const deleteAll = useCallback(async () => { + await handleBatchOperation( + filesToDelete.map(({ bucket, prefix }) => + objectsRemove.post({ + payload: { + bucket, + prefix, + }, + }) + ), + { + toastError: ({ totalCount, errorCount, successCount }) => ({ + title: `${pluralize(successCount, 'file')} deleted`, + body: `Error deleting ${errorCount}/${totalCount} total files.`, + }), + toastSuccess: ({ totalCount }) => ({ + title: `${pluralize(totalCount, 'file')} deleted`, + }), + after: () => { + multiSelect.deselectAll() }, - }) - if (response.error) { - errorCount++ } - } - if (errorCount > 0) { - triggerErrorToast({ - title: `${totalCount - errorCount} files deleted`, - body: `Error deleting ${errorCount}/${totalCount} total files.`, - }) - } else { - triggerSuccessToast({ title: `${totalCount} files deleted` }) - } - multiSelect.deselectAll() + ) }, [multiSelect, filesToDelete, objectsRemove]) return ( @@ -64,12 +60,12 @@ export function FilesBatchDelete({
Are you sure you would like to delete the{' '} - {multiSelect.selectionCount.toLocaleString()} selected files? + {pluralize(multiSelect.selectionCount, 'selected file')}?
), onConfirm: async () => { - deleteFiles() + deleteAll() }, }) }} 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 5eda4256b..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, @@ -31,7 +27,6 @@ import { defaultDatasetRefreshInterval } from '../../config/swr' import { useDataset } from './dataset' import { useFilteredStats } from './useFilteredStats' import { daysInMilliseconds } from '@siafoundation/units' -import { useContractsMetrics } from './useContractsMetrics' const defaultLimit = 50 @@ -54,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, @@ -75,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() @@ -112,7 +88,7 @@ function useContractsMain() { sortDirection, }) - const datasetPage = useMemo(() => { + const _datasetPage = useMemo>(() => { if (!datasetFiltered) { return undefined } @@ -120,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( @@ -143,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, @@ -152,6 +144,7 @@ function useContractsMain() { isFetchingPrunableSizeAll, fetchPrunableSizeAll, filteredStats, + multiSelect, } return context }, [ @@ -162,21 +155,27 @@ 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, }) - const { contractsMetrics: contractsCountMetrics } = useContractsMetrics() - return { dataState, limit, @@ -212,14 +211,13 @@ function useContractsMain() { graphMode, setGraphMode, selectedContract, - selectContract, allContractsSpendingMetrics, selectedContractSpendingMetrics, - contractsCountMetrics, isFetchingPrunableSizeAll, isFetchingPrunableSizeById, fetchPrunableSize, fetchPrunableSizeAll, + multiSelect, } } diff --git a/apps/renterd/contexts/contracts/types.ts b/apps/renterd/contexts/contracts/types.ts index 9b1d0ee6f..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' @@ -181,7 +193,7 @@ export const sortOptions: { ] export type ViewMode = 'list' | 'detail' -export type GraphMode = 'spending' | 'count' +export type GraphMode = 'spending' export type ChartContractKey = | 'uploadSpending' @@ -196,7 +208,3 @@ export type ChartContractCategory = 'funding' | 'spending' export type ChartContractsKey = 'contracts' export type ChartContractsCategory = never - -export type ChartContractsChurnKey = 'contracts' - -export type ChartContractsChurnCategory = never diff --git a/apps/renterd/contexts/contracts/useContractsMetrics.tsx b/apps/renterd/contexts/contracts/useContractsMetrics.tsx deleted file mode 100644 index 7fe824a59..000000000 --- a/apps/renterd/contexts/contracts/useContractsMetrics.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { - Chart, - formatChartData, - computeChartStats, - colors, - getDataIntervalLabelFormatter, -} from '@siafoundation/design-system' -import { useMetricsContracts } from '@siafoundation/renterd-react' -import { useMemo } from 'react' -import { ChartContractsCategory, ChartContractsKey } from './types' -import { getTimeClampedToNearest5min } from './utils' -import { daysInMilliseconds } from '@siafoundation/units' -import { ContractsMetricsParams } from '@siafoundation/renterd-types' - -export function useContractsMetrics() { - // don't use exact times, round to 5 minutes so that swr can cache - // if the user flips back and forth between contracts. - const start = getTimeClampedToNearest5min( - new Date().getTime() - daysInMilliseconds(30) - ) - const interval = daysInMilliseconds(1) - const periods = useMemo(() => { - const now = new Date().getTime() - const today = getTimeClampedToNearest5min(now) - const span = today - start - return Math.round(span / interval) - }, [start, interval]) - const response = useMetricsContracts({ - params: { - start: new Date(start).toISOString(), - interval, - n: periods, - } as ContractsMetricsParams, - }) - const contractsMetrics = useMemo< - Chart - >(() => { - const data = formatChartData( - response.data?.map((m) => ({ - contracts: Number(m.contracts), - timestamp: new Date(m.timestamp).getTime(), - })), - 'none' - ) - const stats = computeChartStats(data) - return { - data, - stats, - config: { - enabledGraph: ['contracts'], - enabledTip: ['contracts'], - data: { - contracts: { - label: 'contracts', - color: colors.emerald[600], - }, - }, - // formatComponent: function ({ value }) { - // return - // }, - formatTimestamp: - interval === daysInMilliseconds(1) - ? getDataIntervalLabelFormatter('daily') - : undefined, - // formatTickY: (v) => `${v} contracts`, - disableAnimations: true, - chartType: 'line', - curveType: 'linear', - stackOffset: 'none', - }, - isLoading: response.isValidating && !response.data, - } - }, [response.data, response.isValidating, interval]) - return { - contractsMetrics, - } -} diff --git a/apps/renterd/contexts/hosts/dataset.ts b/apps/renterd/contexts/hosts/dataset.ts index a56da4705..9803604ae 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, @@ -23,7 +23,7 @@ export function useDataset({ }: { response: ReturnType autopilotID?: string - allContracts: ContractData[] + allContracts: Maybe allowlist: ReturnType blocklist: ReturnType isAllowlistActive: boolean @@ -66,7 +66,7 @@ export function useDataset({ ]) } -function getHostFields(host: Host, allContracts: ContractData[]) { +function getHostFields(host: Host, allContracts: Maybe) { return { id: host.publicKey, netAddress: host.netAddress, diff --git a/apps/renterd/lib/handleBatchOperation.ts b/apps/renterd/lib/handleBatchOperation.ts new file mode 100644 index 000000000..9930dc5a5 --- /dev/null +++ b/apps/renterd/lib/handleBatchOperation.ts @@ -0,0 +1,42 @@ +import { + ToastParams, + triggerErrorToast, + triggerSuccessToast, +} from '@siafoundation/design-system' + +type Results = { + totalCount: number + errorCount: number + successCount: number +} + +type Params = { + toastSuccess: (results: Results) => ToastParams + toastError: (results: Results) => ToastParams + after?: () => void | Promise +} + +export async function handleBatchOperation( + operations: Promise<{ data?: T; error?: string }>[], + params: Params +) { + const totalCount = operations.length + let errorCount = 0 + const results = await Promise.all(operations) + for (const r of results) { + if (r.error) { + errorCount++ + } + } + const successCount = totalCount - errorCount + if (errorCount > 0) { + triggerErrorToast( + params.toastError({ totalCount, errorCount, successCount }) + ) + } else { + triggerSuccessToast( + params.toastSuccess({ totalCount, errorCount, successCount }) + ) + } + await params.after?.() +} diff --git a/libs/design-system/src/lib/toast.tsx b/libs/design-system/src/lib/toast.tsx index 63de5f85f..d5ae57762 100644 --- a/libs/design-system/src/lib/toast.tsx +++ b/libs/design-system/src/lib/toast.tsx @@ -69,7 +69,7 @@ function ToastLayout({ ) } -type ToastParams = { +export type ToastParams = { title: React.ReactNode body?: React.ReactNode icon?: React.ReactNode diff --git a/libs/renterd-js/src/bus.ts b/libs/renterd-js/src/bus.ts index 0f5a65406..9da3ec8fb 100644 --- a/libs/renterd-js/src/bus.ts +++ b/libs/renterd-js/src/bus.ts @@ -44,12 +44,6 @@ import { ContractRenewedPayload, ContractRenewedResponse, ContractResponse, - ContractsMetricsParams, - ContractsMetricsPayload, - ContractsMetricsResponse, - ContractsChurnMetricsParams, - ContractsChurnMetricsPayload, - ContractsChurnMetricsResponse, ContractSizeParams, ContractSizePayload, ContractSizeResponse, @@ -191,8 +185,6 @@ import { busHostsBlocklistRoute, busHostsHostKeyRoute, busMetricContractRoute, - busMetricContractsRoute, - busMetricChurnRoute, busMetricWalletRoute, busMultipartAbortRoute, busMultipartCompleteRoute, @@ -553,16 +545,6 @@ export function Bus({ api, password }: { api: string; password?: string }) { ContractMetricsPayload, ContractMetricsResponse >(axios, 'get', busMetricContractRoute), - contractsMetrics: buildRequestHandler< - ContractsMetricsParams, - ContractsMetricsPayload, - ContractsMetricsResponse - >(axios, 'get', busMetricContractsRoute), - contractsChurnMetrics: buildRequestHandler< - ContractsChurnMetricsParams, - ContractsChurnMetricsPayload, - ContractsChurnMetricsResponse - >(axios, 'get', busMetricChurnRoute), walletMetrics: buildRequestHandler< WalletMetricsParams, WalletMetricsPayload, diff --git a/libs/renterd-react/src/bus.ts b/libs/renterd-react/src/bus.ts index bf1bdcff5..d86407343 100644 --- a/libs/renterd-react/src/bus.ts +++ b/libs/renterd-react/src/bus.ts @@ -54,10 +54,6 @@ import { ContractRenewedPayload, ContractRenewedResponse, ContractResponse, - ContractsMetricsParams, - ContractsMetricsResponse, - ContractsChurnMetricsParams, - ContractsChurnMetricsResponse, ContractsAddParams, ContractsAddPayload, ContractsAddResponse, @@ -190,8 +186,6 @@ import { busAlertsDismissRoute, busSlabKeyObjectsRoute, busMetricContractRoute, - busMetricContractsRoute, - busMetricChurnRoute, busMetricWalletRoute, busMultipartCreateRoute, busMultipartRoute, @@ -912,17 +906,6 @@ export function useMetricsContract( return useGetSwr({ ...args, route: busMetricContractRoute }) } -export function useMetricsContracts( - args: HookArgsSwr -) { - return useGetSwr({ ...args, route: busMetricContractsRoute }) -} -export function useMetricsContractsChurn( - args: HookArgsSwr -) { - return useGetSwr({ ...args, route: busMetricChurnRoute }) -} - export function useMetricsWallet( args: HookArgsSwr ) { diff --git a/libs/renterd-types/src/bus.ts b/libs/renterd-types/src/bus.ts index d888f5b88..3bf8fb9e1 100644 --- a/libs/renterd-types/src/bus.ts +++ b/libs/renterd-types/src/bus.ts @@ -77,8 +77,6 @@ export const busAlertsRoute = '/bus/alerts' export const busAlertsDismissRoute = '/bus/alerts/dismiss' export const busSlabKeyObjectsRoute = '/bus/slab/:key/objects' export const busMetricContractRoute = '/bus/metric/contract' -export const busMetricContractsRoute = '/bus/metric/contracts' -export const busMetricChurnRoute = '/bus/metric/churn' export const busMetricWalletRoute = '/bus/metric/wallet' export const busMultipartRoute = '/bus/multipart' export const busMultipartCreateRoute = '/bus/multipart/create' @@ -524,26 +522,6 @@ export type ContractMetricsParams = MetricsParams & { export type ContractMetricsPayload = void export type ContractMetricsResponse = ContractMetric[] -export type ContractsMetric = { - contracts: number - timestamp: string -} -export type ContractsMetricsParams = MetricsParams -export type ContractsMetricsPayload = void -export type ContractsMetricsResponse = ContractsMetric[] -export type ContractsChurnMetric = { - direction: string - contractID: string - reason: string - timestamp: string -} -export type ContractsChurnMetricsParams = MetricsParams & { - direction?: string - reason?: string -} -export type ContractsChurnMetricsPayload = void -export type ContractsChurnMetricsResponse = ContractsChurnMetric[] - export type WalletMetric = { timestamp: string confirmed: string @@ -555,20 +533,6 @@ export type WalletMetricsParams = MetricsParams export type WalletMetricsPayload = void export type WalletMetricsResponse = WalletMetric[] -// export type PerformanceMetric = { -// action: string -// hostKey: string -// origin: string -// duration: number -// timestamp: string -// } - -// export type PerformanceMetricsParams = MetricsParams & { -// action: string -// hostKey: string -// origin: string -// } - // multipart export type MultipartUploadCreateParams = void