diff --git a/.changeset/cold-keys-wash.md b/.changeset/cold-keys-wash.md new file mode 100644 index 000000000..eba16a12e --- /dev/null +++ b/.changeset/cold-keys-wash.md @@ -0,0 +1,5 @@ +--- +'@siafoundation/renterd-types': patch +--- + +Host and contract responses are now Nullable rather than Maybe, since empty responses return null. diff --git a/.changeset/cuddly-balloons-raise.md b/.changeset/cuddly-balloons-raise.md new file mode 100644 index 000000000..9c497de96 --- /dev/null +++ b/.changeset/cuddly-balloons-raise.md @@ -0,0 +1,7 @@ +--- +'hostd': patch +'renterd': patch +'walletd': patch +--- + +Fixed a bug where the transaction list would show pending transactions when viewing pages other than the first page. diff --git a/.changeset/fluffy-owls-tan.md b/.changeset/fluffy-owls-tan.md new file mode 100644 index 000000000..d3bd76835 --- /dev/null +++ b/.changeset/fluffy-owls-tan.md @@ -0,0 +1,5 @@ +--- +'renterd': patch +--- + +Fixed a bug where pagination did now work on the file uploads list. diff --git a/.changeset/fluffy-snails-hope.md b/.changeset/fluffy-snails-hope.md new file mode 100644 index 000000000..56d75597b --- /dev/null +++ b/.changeset/fluffy-snails-hope.md @@ -0,0 +1,5 @@ +--- +'@siafoundation/react-core': minor +--- + +Added maybeFromNullishArrayResponse for casting null empty array responses to []. diff --git a/.changeset/hip-years-sin.md b/.changeset/hip-years-sin.md new file mode 100644 index 000000000..876c3fb4c --- /dev/null +++ b/.changeset/hip-years-sin.md @@ -0,0 +1,5 @@ +--- +'@siafoundation/design-system': minor +--- + +Refactored useDatasetState to include a noneOnPage state, a renamed loaded state, and a more explicit API. diff --git a/.changeset/nice-maps-dress.md b/.changeset/nice-maps-dress.md new file mode 100644 index 000000000..63fca2494 --- /dev/null +++ b/.changeset/nice-maps-dress.md @@ -0,0 +1,5 @@ +--- +'@siafoundation/design-system': minor +--- + +PaginationMarker now takes an explicit nextMarker and also an optional marker. If the current marker is not passed at all, previous page navigation is always enabled. diff --git a/.changeset/old-planes-happen.md b/.changeset/old-planes-happen.md new file mode 100644 index 000000000..1f2efd2c3 --- /dev/null +++ b/.changeset/old-planes-happen.md @@ -0,0 +1,5 @@ +--- +'renterd': patch +--- + +Fixed a bug where pagination did not work on the file explorer. diff --git a/.changeset/slow-tools-cheat.md b/.changeset/slow-tools-cheat.md new file mode 100644 index 000000000..4b0f94c94 --- /dev/null +++ b/.changeset/slow-tools-cheat.md @@ -0,0 +1,5 @@ +--- +'@siafoundation/design-system': minor +--- + +Added EmptyState component for handling all dataset empty states with custom or default components. diff --git a/.changeset/spotty-fans-shave.md b/.changeset/spotty-fans-shave.md new file mode 100644 index 000000000..92f5eaf72 --- /dev/null +++ b/.changeset/spotty-fans-shave.md @@ -0,0 +1,7 @@ +--- +'hostd': minor +'renterd': minor +'walletd': minor +--- + +Data tables now show an empty state when viewing a page greater than the first page with no data. diff --git a/apps/hostd-e2e/src/specs/contracts.spec.ts b/apps/hostd-e2e/src/specs/contracts.spec.ts index 6e79fa3ef..3c4b828c1 100644 --- a/apps/hostd-e2e/src/specs/contracts.spec.ts +++ b/apps/hostd-e2e/src/specs/contracts.spec.ts @@ -1,7 +1,11 @@ import { test, expect } from '@playwright/test' import { navigateToContracts } from '../fixtures/navigate' import { afterTest, beforeTest } from '../fixtures/beforeTest' -import { getContractRows, getContractRowsAll } from '../fixtures/contracts' +import { + getContractRowByIndex, + getContractRows, + getContractRowsAll, +} from '../fixtures/contracts' test.beforeEach(async ({ page }) => { await beforeTest(page, { @@ -35,3 +39,17 @@ test('new contracts do not show a renewed from or to contract', async ({ await expect(getContractRows(page).getByTestId('renewedFrom')).toBeHidden() await expect(getContractRows(page).getByTestId('renewedTo')).toBeHidden() }) + +test('viewing a page with no data shows the correct empty state', async ({ + page, +}) => { + await page.goto('/contracts?offset=100') + // Check that the empty state is correct. + await expect( + page.getByText('No data on this page, reset pagination to continue.') + ).toBeVisible() + await expect(page.getByText('Back to first page')).toBeVisible() + await page.getByText('Back to first page').click() + // Ensure we are now seeing rows of data. + await getContractRowByIndex(page, 0, true) +}) diff --git a/apps/hostd/components/Contracts/ContractsFiltersBar.tsx b/apps/hostd/components/Contracts/ContractsFiltersBar.tsx index 4f8bed918..1567aea16 100644 --- a/apps/hostd/components/Contracts/ContractsFiltersBar.tsx +++ b/apps/hostd/components/Contracts/ContractsFiltersBar.tsx @@ -3,7 +3,7 @@ import { useContracts } from '../../contexts/contracts' import { ContractsFilterMenu } from './ContractsFilterMenu' export function ContractsFiltersBar() { - const { offset, limit, totalCount, pageCount, dataState } = useContracts() + const { offset, limit, datasetFilteredTotal, datasetState } = useContracts() return (
@@ -11,9 +11,8 @@ export function ContractsFiltersBar() {
) diff --git a/apps/hostd/components/Contracts/index.tsx b/apps/hostd/components/Contracts/index.tsx index dee309951..5c331603f 100644 --- a/apps/hostd/components/Contracts/index.tsx +++ b/apps/hostd/components/Contracts/index.tsx @@ -1,4 +1,4 @@ -import { Table } from '@siafoundation/design-system' +import { EmptyState, Table } from '@siafoundation/design-system' import { useContracts } from '../../contexts/contracts' import { StateNoneMatching } from './StateNoneMatching' import { StateNoneYet } from './StateNoneYet' @@ -13,7 +13,7 @@ export function Contracts() { sortableColumns, toggleSort, limit, - dataState, + datasetState, cellContext, } = useContracts() @@ -22,15 +22,14 @@ export function Contracts() { - ) : dataState === 'noneYet' ? ( - - ) : dataState === 'error' ? ( - - ) : null + } + noneYet={} + error={} + /> } pageSize={limit} data={datasetPage} diff --git a/apps/hostd/components/Volumes/Layout.tsx b/apps/hostd/components/Volumes/Layout.tsx index a8fc56562..879fd7ae4 100644 --- a/apps/hostd/components/Volumes/Layout.tsx +++ b/apps/hostd/components/Volumes/Layout.tsx @@ -1,6 +1,5 @@ -import { Button, Text, Separator } from '@siafoundation/design-system' +import { Button } from '@siafoundation/design-system' import { Add20 } from '@siafoundation/react-icons' -import { humanBytes } from '@siafoundation/units' import { HostdAuthedLayout, HostdAuthedPageLayoutProps, @@ -8,18 +7,12 @@ import { import { useDialog } from '../../contexts/dialog' import { HostdSidenav } from '../HostdSidenav' import { routes } from '../../config/routes' -import { useVolumes } from '../../contexts/volumes' import { VolumesViewDropdownMenu } from './VolumesViewDropdownMenu' +import { VolumesFiltersBar } from './VolumesFiltersBar' export const Layout = HostdAuthedLayout export function useLayoutProps(): HostdAuthedPageLayoutProps { const { openDialog } = useDialog() - - const { dataset } = useVolumes() - - const total = dataset?.reduce((acc, i) => acc + i.totalBytes, 0) - const used = dataset?.reduce((acc, i) => acc + i.usedBytes, 0) - const free = total - used return { title: 'Volumes', routes, @@ -34,20 +27,6 @@ export function useLayoutProps(): HostdAuthedPageLayoutProps { ), - stats: ( -
- {`${humanBytes( - used - )} used`} - - {`${humanBytes( - free - )} free`} - - {`${humanBytes( - total - )} total`} -
- ), + stats: , } } diff --git a/apps/hostd/components/Volumes/VolumesFiltersBar.tsx b/apps/hostd/components/Volumes/VolumesFiltersBar.tsx new file mode 100644 index 000000000..09cd52f00 --- /dev/null +++ b/apps/hostd/components/Volumes/VolumesFiltersBar.tsx @@ -0,0 +1,37 @@ +import { PaginatorKnownTotal } from '@siafoundation/design-system' +import { Text, Separator } from '@siafoundation/design-system' +import { humanBytes } from '@siafoundation/units' +import { useVolumes } from '../../contexts/volumes' + +export function VolumesFiltersBar() { + const { dataset, datasetState, datasetFilteredTotal, offset, limit } = + useVolumes() + + const total = dataset?.reduce((acc, i) => acc + i.totalBytes, 0) + const used = dataset?.reduce((acc, i) => acc + i.usedBytes, 0) + const free = total - used + + return ( +
+
+ {`${humanBytes( + used + )} used`} + + {`${humanBytes( + free + )} free`} + + {`${humanBytes( + total + )} total`} +
+ +
+ ) +} diff --git a/apps/hostd/components/Volumes/index.tsx b/apps/hostd/components/Volumes/index.tsx index ec0358316..160cf61f8 100644 --- a/apps/hostd/components/Volumes/index.tsx +++ b/apps/hostd/components/Volumes/index.tsx @@ -1,9 +1,9 @@ -import { Table } from '@siafoundation/design-system' +import { EmptyState, Table } from '@siafoundation/design-system' import { useVolumes } from '../../contexts/volumes' import { StateNoneYet } from './StateNoneYet' export function Volumes() { - const { dataset, isLoading, columns } = useVolumes() + const { dataset, datasetState, isLoading, columns } = useVolumes() return (
} + emptyState={ + } /> + } /> ) diff --git a/apps/hostd/components/Wallet/WalletFilterBar.tsx b/apps/hostd/components/Wallet/WalletFilterBar.tsx index bef137535..1de4bb78e 100644 --- a/apps/hostd/components/Wallet/WalletFilterBar.tsx +++ b/apps/hostd/components/Wallet/WalletFilterBar.tsx @@ -8,7 +8,7 @@ import { useTransactions } from '../../contexts/transactions' export function WalletFilterBar() { const { isSynced, syncPercent, isWalletSynced, walletScanPercent } = useSyncStatus() - const { offset, limit, pageCount, dataState } = useTransactions() + const { offset, limit, datasetPageTotal, datasetState } = useTransactions() return (
) diff --git a/apps/hostd/components/Wallet/index.tsx b/apps/hostd/components/Wallet/index.tsx index 72b6b0d75..1a7391345 100644 --- a/apps/hostd/components/Wallet/index.tsx +++ b/apps/hostd/components/Wallet/index.tsx @@ -1,4 +1,8 @@ -import { BalanceEvolution, Table } from '@siafoundation/design-system' +import { + BalanceEvolution, + EmptyState, + Table, +} from '@siafoundation/design-system' import { useTransactions } from '../../contexts/transactions' import { StateNoneMatching } from './StateNoneMatching' import { StateNoneYet } from './StateNoneYet' @@ -8,8 +12,8 @@ export function Wallet() { const { balances, metrics, - dataset, - dataState, + datasetPage, + datasetState, columns, cellContext, sortableColumns, @@ -29,18 +33,17 @@ export function Wallet() { ) : null}
- ) : dataState === 'noneYet' ? ( - - ) : dataState === 'error' ? ( - - ) : null + } + noneYet={} + error={} + /> } pageSize={defaultPageSize} - data={dataset} + data={datasetPage} context={cellContext} columns={columns} sortableColumns={sortableColumns} diff --git a/apps/hostd/contexts/contracts/index.tsx b/apps/hostd/contexts/contracts/index.tsx index a8881ecd3..071de9d94 100644 --- a/apps/hostd/contexts/contracts/index.tsx +++ b/apps/hostd/contexts/contracts/index.tsx @@ -1,12 +1,12 @@ import { useTableState, - useDatasetEmptyState, + useDatasetState, useServerFilters, getContractsTimeRangeBlockHeight, useMultiSelect, + usePaginationOffset, } from '@siafoundation/design-system' import { Maybe } from '@siafoundation/types' -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' @@ -27,9 +27,7 @@ import { defaultDatasetRefreshInterval } from '../../config/swr' const defaultLimit = 50 function useContractsMain() { - const router = useRouter() - const limit = Number(router.query.limit || defaultLimit) - const offset = Number(router.query.offset || 0) + const { limit, offset } = usePaginationOffset(defaultLimit) const { filters, setFilter, removeFilter, removeLastFilter, resetFilters } = useServerFilters() @@ -82,15 +80,6 @@ function useContractsMain() { [enabledColumns] ) - const isValidating = response.isValidating - const error = response.error - const dataState = useDatasetEmptyState( - _datasetPage, - isValidating, - error, - filters - ) - const { estimatedBlockHeight, isSynced, nodeBlockHeight } = useSyncStatus() const currentHeight = isSynced ? nodeBlockHeight : estimatedBlockHeight @@ -115,6 +104,16 @@ function useContractsMain() { }) }, [_datasetPage, multiSelect]) + const isValidating = response.isValidating + const error = response.error + const datasetState = useDatasetState({ + datasetPage, + isValidating, + error, + filters, + offset, + }) + const siascanUrl = useSiascanUrl() const cellContext = useMemo( @@ -128,12 +127,12 @@ function useContractsMain() { ) return { - dataState, + datasetState, offset, limit, cellContext, - pageCount: datasetPage?.length || 0, - totalCount: response.data?.count, + datasetPageTotal: datasetPage?.length || 0, + datasetFilteredTotal: response.data?.count, columns: filteredTableColumns, datasetPage, configurableColumns, diff --git a/apps/hostd/contexts/transactions/index.tsx b/apps/hostd/contexts/transactions/index.tsx index 0751fb8a4..459d61758 100644 --- a/apps/hostd/contexts/transactions/index.tsx +++ b/apps/hostd/contexts/transactions/index.tsx @@ -1,5 +1,5 @@ import { - useDatasetEmptyState, + useDatasetState, useServerFilters, useTableState, } from '@siafoundation/design-system' @@ -28,6 +28,7 @@ import { defaultSortField, sortOptions, } from './types' +import { Maybe } from '@siafoundation/types' const defaultPageSize = 50 @@ -58,7 +59,7 @@ function useTransactionsMain() { useServerFilters() const syncStatus = useSyncStatus() - const dataset = useMemo(() => { + const datasetPage = useMemo>(() => { if (!events.data || !pending.data) { return null } @@ -97,8 +98,12 @@ function useTransactionsMain() { } return res }) - return [...dataPending.reverse(), ...dataEvents] - }, [events.data, pending.data, syncStatus.nodeBlockHeight]) + if (offset === 0) { + return [...dataPending.reverse(), ...dataEvents] + } else { + return [...dataEvents] + } + }, [events.data, pending.data, syncStatus.nodeBlockHeight, offset]) const { configurableColumns, @@ -128,11 +133,6 @@ function useTransactionsMain() { [enabledColumns] ) - const isValidating = events.isValidating || pending.isValidating - const error = events.error || pending.error - - const dataState = useDatasetEmptyState(dataset, isValidating, error, filters) - const siascanUrl = useSiascanUrl() const cellContext = useMemo( () => ({ @@ -168,15 +168,25 @@ function useTransactionsMain() { [metrics.data] ) + const isValidating = events.isValidating || pending.isValidating + const error = events.error || pending.error + const datasetState = useDatasetState({ + datasetPage, + isValidating, + error, + filters, + offset, + }) + return { balances, metrics, - dataset, + datasetPage, error, - dataState, + datasetState, offset, limit, - pageCount: dataset?.length || 0, + datasetPageTotal: datasetPage?.length || 0, defaultPageSize, cellContext, configurableColumns, diff --git a/apps/hostd/contexts/volumes/dataset.ts b/apps/hostd/contexts/volumes/dataset.ts index 7481f9ed6..21b9b4832 100644 --- a/apps/hostd/contexts/volumes/dataset.ts +++ b/apps/hostd/contexts/volumes/dataset.ts @@ -4,15 +4,16 @@ import { useVolumes } from '@siafoundation/hostd-react' import { VolumeData } from './types' import BigNumber from 'bignumber.js' import { MiBToBytes } from '@siafoundation/units' +import { Maybe } from '@siafoundation/types' export function useDataset({ response, }: { response: ReturnType }) { - return useMemo(() => { + return useMemo>(() => { if (!response.data) { - return null + return undefined } return ( response.data?.map((contract) => { diff --git a/apps/hostd/contexts/volumes/index.tsx b/apps/hostd/contexts/volumes/index.tsx index ce21b86e0..07a61e649 100644 --- a/apps/hostd/contexts/volumes/index.tsx +++ b/apps/hostd/contexts/volumes/index.tsx @@ -1,17 +1,25 @@ import { useTableState, - useDatasetEmptyState, + useDatasetState, + useClientFilteredDataset, + useClientFilters, + usePaginationOffset, } from '@siafoundation/design-system' import { VolumeMeta } from '@siafoundation/hostd-types' import { useVolumes as useVolumesData } from '@siafoundation/hostd-react' import { createContext, useContext, useMemo } from 'react' -import { columnsDefaultVisible, TableColumnId } from './types' +import { columnsDefaultVisible, TableColumnId, VolumeData } from './types' import { columns } from './columns' import { useDataset } from './dataset' import { defaultDatasetRefreshInterval } from '../../config/swr' import { secondsInMilliseconds } from '@siafoundation/units' +const defaultLimit = 50 + function useVolumesMain() { + const { limit, offset } = usePaginationOffset(defaultLimit) + const { filters } = useClientFilters() + const { configurableColumns, enabledColumns, @@ -44,6 +52,15 @@ function useVolumesMain() { response, }) + const { datasetFiltered, datasetPage } = useClientFilteredDataset({ + dataset, + filters, + sortField, + sortDirection, + offset, + limit, + }) + const filteredTableColumns = useMemo( () => columns.filter((column) => enabledColumns.includes(column.id)), [enabledColumns] @@ -51,14 +68,23 @@ function useVolumesMain() { const isValidating = response.isValidating const error = response.error - const dataState = useDatasetEmptyState(dataset, isValidating, error, []) + const datasetState = useDatasetState({ + datasetPage, + isValidating, + error, + }) return { - dataState, - totalCount: dataset?.length || 0, + datasetState, + datasetTotal: dataset?.length || 0, + datasetFilteredTotal: datasetFiltered?.length || 0, + datasetPageTotal: datasetPage?.length || 0, isLoading: response.isValidating, columns: filteredTableColumns, dataset, + datasetPage, + offset, + limit, configurableColumns, enabledColumns, toggleColumnVisibility, diff --git a/apps/hostd/dialogs/HostdTransactionDetailsDialog.tsx b/apps/hostd/dialogs/HostdTransactionDetailsDialog.tsx index c381297f1..7cb6988b6 100644 --- a/apps/hostd/dialogs/HostdTransactionDetailsDialog.tsx +++ b/apps/hostd/dialogs/HostdTransactionDetailsDialog.tsx @@ -5,11 +5,12 @@ import { useTransactions } from '../contexts/transactions' export function HostdTransactionDetailsDialog() { const { id, dialog, onOpenChange } = useDialog() - const { dataset } = useTransactions() + // TODO: fetch transaction so that not dependent on datasetPage. + const { datasetPage } = useTransactions() const transaction = useMemo(() => { - return dataset?.find((t) => t.id === id) - }, [dataset, id]) + return datasetPage?.find((t) => t.id === id) + }, [datasetPage, id]) return (
- {!dataState && !!pageCount && ( + {datasetState === 'loaded' && !!datasetPageTotal && ( )}
) diff --git a/apps/renterd/components/Alerts/index.tsx b/apps/renterd/components/Alerts/index.tsx index 45fee41db..c4f3aa853 100644 --- a/apps/renterd/components/Alerts/index.tsx +++ b/apps/renterd/components/Alerts/index.tsx @@ -13,19 +13,19 @@ export function Alerts() { sortableColumns, toggleSort, limit, - dataState, + datasetState, } = useAlerts() return (
- ) : dataState === 'noneYet' ? ( + ) : datasetState === 'noneYet' ? ( - ) : dataState === 'error' ? ( + ) : datasetState === 'error' ? ( ) : null } diff --git a/apps/renterd/components/Contracts/ContractsFilterBar.tsx b/apps/renterd/components/Contracts/ContractsFilterBar.tsx index 7a0cfd01d..35929a1f9 100644 --- a/apps/renterd/components/Contracts/ContractsFilterBar.tsx +++ b/apps/renterd/components/Contracts/ContractsFilterBar.tsx @@ -3,19 +3,17 @@ import { useContracts } from '../../contexts/contracts' import { ContractsFilterMenu } from './ContractsFilterMenu' export function ContractsFilterBar() { - const { dataState, offset, limit, datasetFilteredCount, pageCount } = - useContracts() + const { datasetState, offset, limit, datasetFilteredTotal } = useContracts() return (
) diff --git a/apps/renterd/components/Contracts/index.tsx b/apps/renterd/components/Contracts/index.tsx index 1c651bd43..29d59662b 100644 --- a/apps/renterd/components/Contracts/index.tsx +++ b/apps/renterd/components/Contracts/index.tsx @@ -1,4 +1,4 @@ -import { ScrollArea, Table } from '@siafoundation/design-system' +import { EmptyState, ScrollArea, Table } from '@siafoundation/design-system' import { useContracts } from '../../contexts/contracts' import { StateNoneMatching } from './StateNoneMatching' import { StateNoneYet } from './StateNoneYet' @@ -15,7 +15,7 @@ export function Contracts() { sortableColumns, toggleSort, limit, - dataState, + datasetState, cellContext, error, viewMode, @@ -64,15 +64,14 @@ export function Contracts() {
- ) : dataState === 'noneYet' ? ( - - ) : dataState === 'error' ? ( - - ) : null + } + noneYet={} + error={} + /> } sortableColumns={sortableColumns} pageSize={limit} diff --git a/apps/renterd/components/Files/FilesStatsMenuShared/FilesStatsMenuCount.tsx b/apps/renterd/components/Files/FilesStatsMenuShared/FilesStatsMenuCount.tsx index a2d07b4dc..4cb79de84 100644 --- a/apps/renterd/components/Files/FilesStatsMenuShared/FilesStatsMenuCount.tsx +++ b/apps/renterd/components/Files/FilesStatsMenuShared/FilesStatsMenuCount.tsx @@ -7,10 +7,10 @@ import { useFilesFlat } from '../../../contexts/filesFlat' export function FilesStatsMenuCount() { const { isViewingABucket, uploadsList, activeExplorerMode } = useFilesManager() - const { pageCount: directoryPageCount } = useFilesDirectory() - const { pageCount: flatPageCount } = useFilesFlat() - const pageCount = - activeExplorerMode === 'flat' ? flatPageCount : directoryPageCount + const { datasetPageTotal: directoryPageTotal } = useFilesDirectory() + const { datasetPageTotal: flatPageTotal } = useFilesFlat() + const datasetPageTotal = + activeExplorerMode === 'flat' ? flatPageTotal : directoryPageTotal const stats = useObjectStats({ config: { @@ -35,7 +35,7 @@ export function FilesStatsMenuCount() { content="Number of files in page of current directory" > - {pageCount.toLocaleString()} + {datasetPageTotal.toLocaleString()} diff --git a/apps/renterd/components/Files/Layout.tsx b/apps/renterd/components/Files/Layout.tsx index 4c226c745..11dae035e 100644 --- a/apps/renterd/components/Files/Layout.tsx +++ b/apps/renterd/components/Files/Layout.tsx @@ -8,7 +8,8 @@ import { RenterdAuthedPageLayoutProps, } from '../RenterdAuthedLayout' import { FilesActionsMenu } from '../FilesDirectory/FilesActionsMenu' -import { FilesStatsMenu } from '../FilesDirectory/FilesStatsMenu' +import { FilesDirectoryStatsMenu } from '../FilesDirectory/FilesDirectoryStatsMenu' +import { FilesFlatStatsMenu } from '../FilesFlat/FilesFlatStatsMenu' import { FilesDirectoryBulkMenu } from '../FilesDirectory/FilesDirectoryBulkMenu' import { useFilesManager } from '../../contexts/filesManager' import { FilesFlatBulkMenu } from '../FilesFlat/FilesFlatBulkMenu' @@ -29,7 +30,7 @@ export function useLayoutProps(): RenterdAuthedPageLayoutProps { sidenav: , openSettings: () => openDialog('settings'), nav: , - stats: , + stats: , actions: , dockedControls: , } @@ -42,7 +43,7 @@ export function useLayoutProps(): RenterdAuthedPageLayoutProps { sidenav: , openSettings: () => openDialog('settings'), nav: , - stats: , + stats: , actions: , dockedControls: , } diff --git a/apps/renterd/components/Files/checks/useNotEnoughContracts.tsx b/apps/renterd/components/Files/checks/useNotEnoughContracts.tsx index 7f161729f..1daf78ba0 100644 --- a/apps/renterd/components/Files/checks/useNotEnoughContracts.tsx +++ b/apps/renterd/components/Files/checks/useNotEnoughContracts.tsx @@ -3,16 +3,16 @@ import { useContracts } from '../../../contexts/contracts' export function useNotEnoughContracts() { const settingsUpload = useSettingsUpload() - const { datasetCount, isLoading: isContractsLoading } = useContracts() + const { datasetTotal, isLoading: isContractsLoading } = useContracts() const active = settingsUpload.data && !isContractsLoading && - datasetCount < settingsUpload.data.redundancy.totalShards + datasetTotal < settingsUpload.data.redundancy.totalShards return { active, - count: datasetCount, + count: datasetTotal, required: settingsUpload.data?.redundancy.totalShards || 0, } } diff --git a/apps/renterd/components/FilesDirectory/EmptyState/index.tsx b/apps/renterd/components/FilesDirectory/EmptyState/index.tsx index 14722c89a..370c9bab7 100644 --- a/apps/renterd/components/FilesDirectory/EmptyState/index.tsx +++ b/apps/renterd/components/FilesDirectory/EmptyState/index.tsx @@ -1,4 +1,9 @@ -import { Code, LinkButton, Text } from '@siafoundation/design-system' +import { + Code, + LinkButton, + StateNoneOnPage, + Text, +} from '@siafoundation/design-system' import { CloudUpload32 } from '@siafoundation/react-icons' import { routes } from '../../../config/routes' import { useFilesDirectory } from '../../../contexts/filesDirectory' @@ -12,23 +17,27 @@ import { StateNoneYetBuckets } from './StateNoneYetBuckets' export function EmptyState() { const { isViewingRootOfABucket, isViewingBuckets } = useFilesManager() - const { dataState } = useFilesDirectory() + const { datasetState } = useFilesDirectory() const autopilotNotEnabled = useAutopilotNotEnabled() const notEnoughContracts = useNotEnoughContracts() - if (dataState === 'noneMatchingFilters') { + if (datasetState === 'noneOnPage') { + return + } + + if (datasetState === 'noneMatchingFilters') { return } - if (dataState === 'error') { + if (datasetState === 'error') { return } // Only show on root directory and when there are no files. if ( isViewingRootOfABucket && - dataState === 'noneYet' && + datasetState === 'noneYet' && autopilotNotEnabled.active ) { return ( @@ -54,7 +63,7 @@ export function EmptyState() { // Only show on root directory and when there are no files. if ( isViewingRootOfABucket && - dataState === 'noneYet' && + datasetState === 'noneYet' && notEnoughContracts.active ) { return ( @@ -76,7 +85,7 @@ export function EmptyState() { ) } - if (dataState === 'noneYet') { + if (datasetState === 'noneYet') { if (isViewingBuckets) { return } diff --git a/apps/renterd/components/FilesDirectory/FilesStatsMenu/index.tsx b/apps/renterd/components/FilesDirectory/FilesDirectoryStatsMenu/index.tsx similarity index 74% rename from apps/renterd/components/FilesDirectory/FilesStatsMenu/index.tsx rename to apps/renterd/components/FilesDirectory/FilesDirectoryStatsMenu/index.tsx index ebad019c3..f33d3ceee 100644 --- a/apps/renterd/components/FilesDirectory/FilesStatsMenu/index.tsx +++ b/apps/renterd/components/FilesDirectory/FilesDirectoryStatsMenu/index.tsx @@ -4,9 +4,10 @@ import { FilesStatsMenuShared } from '../../Files/FilesStatsMenuShared' import { FilesFilterDirectoryMenu } from '../../Files/FilesFilterDirectoryMenu' import { useFilesManager } from '../../../contexts/filesManager' -export function FilesStatsMenu() { +export function FilesDirectoryStatsMenu() { const { isViewingABucket, isViewingBuckets } = useFilesManager() - const { limit, marker, isMore, pageCount, dataState } = useFilesDirectory() + const { limit, marker, nextMarker, isMore, datasetPageTotal, datasetState } = + useFilesDirectory() return (
{isViewingBuckets ? ( @@ -19,9 +20,10 @@ export function FilesStatsMenu() { )}
diff --git a/apps/renterd/components/FilesDirectory/FilesExplorer.tsx b/apps/renterd/components/FilesDirectory/FilesExplorer.tsx index f896c78cb..e159d97e1 100644 --- a/apps/renterd/components/FilesDirectory/FilesExplorer.tsx +++ b/apps/renterd/components/FilesDirectory/FilesExplorer.tsx @@ -17,8 +17,8 @@ export function FilesExplorer() { } = useFilesManager() const { datasetPage, - pageCount, - dataState, + datasetPageTotal, + datasetState, cellContext, onDragEnd, onDragOver, @@ -33,12 +33,12 @@ export function FilesExplorer() { 0} + noClick={!canUpload || datasetPageTotal > 0} noDrag={!canUpload} >
} pageSize={10} data={datasetPage} diff --git a/apps/renterd/components/FilesFlat/EmptyState/index.tsx b/apps/renterd/components/FilesFlat/EmptyState/index.tsx index 21789cf4e..8c1a8b7ae 100644 --- a/apps/renterd/components/FilesFlat/EmptyState/index.tsx +++ b/apps/renterd/components/FilesFlat/EmptyState/index.tsx @@ -2,19 +2,24 @@ import { StateError } from './StateError' import { StateNoneMatching } from './StateNoneMatching' import { StateNoneYet } from './StateNoneYet' import { useFilesFlat } from '../../../contexts/filesFlat' +import { StateNoneOnPage } from '@siafoundation/design-system' export function EmptyState() { - const { dataState } = useFilesFlat() + const { datasetState } = useFilesFlat() - if (dataState === 'noneMatchingFilters') { + if (datasetState === 'noneOnPage') { + return + } + + if (datasetState === 'noneMatchingFilters') { return } - if (dataState === 'error') { + if (datasetState === 'error') { return } - if (dataState === 'noneYet') { + if (datasetState === 'noneYet') { return } diff --git a/apps/renterd/components/FilesFlat/FilesExplorer.tsx b/apps/renterd/components/FilesFlat/FilesExplorer.tsx index 06053f043..0f453e32d 100644 --- a/apps/renterd/components/FilesFlat/FilesExplorer.tsx +++ b/apps/renterd/components/FilesFlat/FilesExplorer.tsx @@ -6,13 +6,13 @@ import { columns } from '../../contexts/filesFlat/columns' export function FilesExplorer() { const { sortableColumns, toggleSort } = useFilesManager() - const { datasetPage, dataState, cellContext, sortField, sortDirection } = + const { datasetPage, datasetState, cellContext, sortField, sortDirection } = useFilesFlat() return (
} pageSize={10} data={datasetPage} diff --git a/apps/renterd/components/FilesFlat/FilesStatsMenu/index.tsx b/apps/renterd/components/FilesFlat/FilesFlatStatsMenu/index.tsx similarity index 65% rename from apps/renterd/components/FilesFlat/FilesStatsMenu/index.tsx rename to apps/renterd/components/FilesFlat/FilesFlatStatsMenu/index.tsx index ae573d840..45a1602e5 100644 --- a/apps/renterd/components/FilesFlat/FilesStatsMenu/index.tsx +++ b/apps/renterd/components/FilesFlat/FilesFlatStatsMenu/index.tsx @@ -3,18 +3,20 @@ import { useFilesFlat } from '../../../contexts/filesFlat' import { FilesFilterDirectoryMenu } from '../../Files/FilesFilterDirectoryMenu' import { FilesStatsMenuShared } from '../../Files/FilesStatsMenuShared' -export function FilesStatsMenu() { - const { limit, pageCount, dataState, nextMarker, isMore } = useFilesFlat() +export function FilesFlatStatsMenu() { + const { limit, datasetPageTotal, datasetState, marker, nextMarker, isMore } = + useFilesFlat() return (
) diff --git a/apps/renterd/components/Hosts/HostsFilterBar.tsx b/apps/renterd/components/Hosts/HostsFilterBar.tsx index 94c7880b8..755b9b872 100644 --- a/apps/renterd/components/Hosts/HostsFilterBar.tsx +++ b/apps/renterd/components/Hosts/HostsFilterBar.tsx @@ -3,7 +3,7 @@ import { useHosts } from '../../contexts/hosts' import { HostsFilterMenu } from './HostsFilterMenu' export function HostsFilterBar() { - const { offset, limit, pageCount, dataState } = useHosts() + const { offset, limit, datasetPageTotal, datasetState } = useHosts() return (
@@ -11,8 +11,8 @@ export function HostsFilterBar() {
) diff --git a/apps/renterd/components/Hosts/StateEmpty.tsx b/apps/renterd/components/Hosts/StateEmpty.tsx index 304aadd2d..9cc2b04d8 100644 --- a/apps/renterd/components/Hosts/StateEmpty.tsx +++ b/apps/renterd/components/Hosts/StateEmpty.tsx @@ -1,4 +1,9 @@ -import { Code, LinkButton, Text } from '@siafoundation/design-system' +import { + Code, + LinkButton, + StateNoneOnPage, + Text, +} from '@siafoundation/design-system' import { Filter32, HardDriveIcon, @@ -8,9 +13,13 @@ import { routes } from '../../config/routes' import { useHosts } from '../../contexts/hosts' export function StateEmpty() { - const { dataState } = useHosts() + const { datasetState } = useHosts() + + if (datasetState === 'noneOnPage') { + return + } - if (dataState === 'error') { + if (datasetState === 'error') { return (
@@ -23,7 +32,7 @@ export function StateEmpty() { ) } - if (dataState === 'noneMatchingFilters') { + if (datasetState === 'noneMatchingFilters') { return (
@@ -35,7 +44,7 @@ export function StateEmpty() {
) } - if (dataState === 'noneYet') { + if (datasetState === 'noneYet') { return (
diff --git a/apps/renterd/components/Hosts/index.tsx b/apps/renterd/components/Hosts/index.tsx index 063fd18ad..1fd7e2cdb 100644 --- a/apps/renterd/components/Hosts/index.tsx +++ b/apps/renterd/components/Hosts/index.tsx @@ -11,7 +11,7 @@ export function Hosts() { activeHost, columns, limit, - dataState, + datasetState, tableContext, viewMode, } = useHosts() @@ -56,7 +56,7 @@ export function Hosts() { focusColor={ activeHost ? getHostStatus(activeHost).colorName : undefined } - isLoading={dataState === 'loading'} + isLoading={datasetState === 'loading'} emptyState={} context={tableContext} pageSize={limit} diff --git a/apps/renterd/components/Keys/KeysStatsMenu/index.tsx b/apps/renterd/components/Keys/KeysStatsMenu/index.tsx index 7e5d4de82..c305be8be 100644 --- a/apps/renterd/components/Keys/KeysStatsMenu/index.tsx +++ b/apps/renterd/components/Keys/KeysStatsMenu/index.tsx @@ -2,16 +2,15 @@ import { PaginatorKnownTotal } from '@siafoundation/design-system' import { useKeys } from '../../../contexts/keys' export function KeysStatsMenu() { - const { limit, offset, datasetCount, pageCount, dataState } = useKeys() + const { limit, offset, datasetTotal, datasetState } = useKeys() return (
) diff --git a/apps/renterd/components/Keys/index.tsx b/apps/renterd/components/Keys/index.tsx index e4dcc0758..483602752 100644 --- a/apps/renterd/components/Keys/index.tsx +++ b/apps/renterd/components/Keys/index.tsx @@ -1,4 +1,4 @@ -import { Table } from '@siafoundation/design-system' +import { EmptyState, Table } from '@siafoundation/design-system' import { StateNoneMatching } from './StateNoneMatching' import { StateNoneYet } from './StateNoneYet' import { StateError } from './StateError' @@ -13,7 +13,7 @@ export function Keys() { sortableColumns, toggleSort, limit, - dataState, + datasetState, cellContext, } = useKeys() @@ -21,15 +21,14 @@ export function Keys() {
- ) : dataState === 'noneYet' ? ( - - ) : dataState === 'error' ? ( - - ) : null + } + noneYet={} + error={} + /> } sortableColumns={sortableColumns} pageSize={limit} diff --git a/apps/renterd/components/TransfersBar.tsx b/apps/renterd/components/TransfersBar.tsx index 3341458ce..69acc0eb5 100644 --- a/apps/renterd/components/TransfersBar.tsx +++ b/apps/renterd/components/TransfersBar.tsx @@ -16,10 +16,10 @@ export function TransfersBar() { const { isUnlockedAndAuthedRoute } = useAppSettings() const { downloadsList, downloadCancel, isViewingUploads, navigateToUploads } = useFilesManager() - const { pageCount: uploadsPageCount } = useUploads() + const { datasetPageTotal: uploadsPageTotal } = useUploads() const [maximized, setMaximized] = useState(true) - const isActiveUploads = !!uploadsPageCount + const isActiveUploads = !!uploadsPageTotal const downloadCount = downloadsList.length const isActiveDownloads = !!downloadCount diff --git a/apps/renterd/components/Uploads/EmptyState/index.tsx b/apps/renterd/components/Uploads/EmptyState/index.tsx index a6ccb146e..58047f6f1 100644 --- a/apps/renterd/components/Uploads/EmptyState/index.tsx +++ b/apps/renterd/components/Uploads/EmptyState/index.tsx @@ -2,19 +2,24 @@ import { StateError } from './StateError' import { StateNoneMatching } from './StateNoneMatching' import { StateNoneYet } from './StateNoneYet' import { useUploads } from '../../../contexts/uploads' +import { StateNoneOnPage } from '@siafoundation/design-system' export function EmptyState() { - const { dataState } = useUploads() + const { datasetState } = useUploads() - if (dataState === 'noneMatchingFilters') { + if (datasetState === 'noneOnPage') { + return + } + + if (datasetState === 'noneMatchingFilters') { return } - if (dataState === 'error') { + if (datasetState === 'error') { return } - if (dataState === 'noneYet') { + if (datasetState === 'noneYet') { return } diff --git a/apps/renterd/components/Uploads/UploadsStatsMenu/index.tsx b/apps/renterd/components/Uploads/UploadsStatsMenu/index.tsx index 651b121c7..9f7b8616b 100644 --- a/apps/renterd/components/Uploads/UploadsStatsMenu/index.tsx +++ b/apps/renterd/components/Uploads/UploadsStatsMenu/index.tsx @@ -2,18 +2,28 @@ import { Button, PaginatorMarker } from '@siafoundation/design-system' import { useUploads } from '../../../contexts/uploads' export function UploadsStatsMenu() { - const { abortAll, limit, pageCount, dataState, nextMarker, hasMore } = - useUploads() + const { + abortAll, + limit, + datasetPageTotal, + datasetState, + marker, + nextMarker, + hasMore, + } = useUploads() return (
- {pageCount > 0 && } + {datasetPageTotal > 0 && ( + + )}
) diff --git a/apps/renterd/components/Uploads/UploadsTable.tsx b/apps/renterd/components/Uploads/UploadsTable.tsx index 65365434c..13cd0121c 100644 --- a/apps/renterd/components/Uploads/UploadsTable.tsx +++ b/apps/renterd/components/Uploads/UploadsTable.tsx @@ -8,14 +8,14 @@ export function UploadsTable() { sortableColumns, toggleSort, datasetPage, - dataState, + datasetState, sortField, sortDirection, } = useUploads() return (
} pageSize={10} data={datasetPage} diff --git a/apps/renterd/components/Wallet/WalletFilterBar.tsx b/apps/renterd/components/Wallet/WalletFilterBar.tsx index bef137535..1de4bb78e 100644 --- a/apps/renterd/components/Wallet/WalletFilterBar.tsx +++ b/apps/renterd/components/Wallet/WalletFilterBar.tsx @@ -8,7 +8,7 @@ import { useTransactions } from '../../contexts/transactions' export function WalletFilterBar() { const { isSynced, syncPercent, isWalletSynced, walletScanPercent } = useSyncStatus() - const { offset, limit, pageCount, dataState } = useTransactions() + const { offset, limit, datasetPageTotal, datasetState } = useTransactions() return (
) diff --git a/apps/renterd/components/Wallet/index.tsx b/apps/renterd/components/Wallet/index.tsx index 921d636b6..dd109880c 100644 --- a/apps/renterd/components/Wallet/index.tsx +++ b/apps/renterd/components/Wallet/index.tsx @@ -1,4 +1,8 @@ -import { BalanceEvolution, Table } from '@siafoundation/design-system' +import { + BalanceEvolution, + EmptyState, + Table, +} from '@siafoundation/design-system' import { useTransactions } from '../../contexts/transactions' import { StateNoneMatching } from './StateNoneMatching' import { StateNoneYet } from './StateNoneYet' @@ -8,8 +12,8 @@ export function Wallet() { const { balances, metrics, - dataset, - dataState, + datasetPage, + datasetState, columns, cellContext, sortableColumns, @@ -31,18 +35,17 @@ export function Wallet() { ) : null}
- ) : dataState === 'noneYet' ? ( - - ) : dataState === 'error' ? ( - - ) : null + } + noneYet={} + error={} + /> } pageSize={defaultPageSize} - data={dataset} + data={datasetPage} context={cellContext} columns={columns} sortableColumns={sortableColumns} diff --git a/apps/renterd/contexts/alerts/index.tsx b/apps/renterd/contexts/alerts/index.tsx index edc7e7845..0e58d31e1 100644 --- a/apps/renterd/contexts/alerts/index.tsx +++ b/apps/renterd/contexts/alerts/index.tsx @@ -1,9 +1,9 @@ import { useTableState, - useDatasetEmptyState, + useDatasetState, useServerFilters, + usePaginationOffset, } from '@siafoundation/design-system' -import { useRouter } from 'next/router' import { createContext, useContext, useMemo } from 'react' import { AlertData, @@ -23,13 +23,12 @@ import { useAlertsDismiss, } from '@siafoundation/renterd-react' import { useCallback } from 'react' +import { Maybe } from '@siafoundation/types' const defaultLimit = 50 function useAlertsMain() { - const router = useRouter() - const limit = Number(router.query.limit || defaultLimit) - const offset = Number(router.query.offset || 0) + const { limit, offset } = usePaginationOffset(defaultLimit) const { filters, setFilter, removeFilter, removeLastFilter, resetFilters } = useServerFilters() @@ -106,7 +105,7 @@ function useAlertsMain() { [dismiss] ) - const datasetPage = useMemo(() => { + const datasetPage = useMemo>(() => { if (!response.data) { return undefined } @@ -150,12 +149,13 @@ function useAlertsMain() { [enabledColumns] ) - const dataState = useDatasetEmptyState( + const datasetState = useDatasetState({ datasetPage, - response.isValidating, - response.error, - filters - ) + isValidating: response.isValidating, + error: response.error, + filters, + offset, + }) const totals = useMemo( () => ({ @@ -171,12 +171,12 @@ function useAlertsMain() { ) return { - dataState, + datasetState, limit, offset, isLoading: response.isLoading, error: response.error, - pageCount: datasetPage?.length || 0, + datasetPageTotal: datasetPage?.length || 0, totals, columns: filteredTableColumns, datasetPage, diff --git a/apps/renterd/contexts/contracts/dataset.tsx b/apps/renterd/contexts/contracts/dataset.tsx index 4b0621815..67985c436 100644 --- a/apps/renterd/contexts/contracts/dataset.tsx +++ b/apps/renterd/contexts/contracts/dataset.tsx @@ -8,6 +8,7 @@ import { blockHeightToTime } from '@siafoundation/units' import { defaultDatasetRefreshInterval } from '../../config/swr' import { usePrunableContractSizes } from './usePrunableContractSizes' import { Maybe } from '@siafoundation/types' +import { maybeFromNullishArrayResponse } from '@siafoundation/react-core' export function useDataset() { const response = useContractsData({ @@ -28,11 +29,12 @@ export function useDataset() { const datasetWithoutPrunable = useMemo< Maybe >(() => { - if (!response.data) { + const data = maybeFromNullishArrayResponse(response.data) + if (!data) { return undefined } const datums = - response.data?.map((c) => { + data.map((c) => { const isRenewed = c.renewedFrom !== '0000000000000000000000000000000000000000000000000000000000000000' @@ -69,7 +71,7 @@ export function useDataset() { return datum }) || [] return datums - }, [response.data, geoHosts, currentHeight]) + }, [response, geoHosts, currentHeight]) const { prunableSizes, diff --git a/apps/renterd/contexts/contracts/index.tsx b/apps/renterd/contexts/contracts/index.tsx index bcf732558..19e309598 100644 --- a/apps/renterd/contexts/contracts/index.tsx +++ b/apps/renterd/contexts/contracts/index.tsx @@ -1,13 +1,13 @@ import { useTableState, getContractsTimeRangeBlockHeight, - useDatasetEmptyState, + useDatasetState, useClientFilters, useClientFilteredDataset, useMultiSelect, + usePaginationOffset, } from '@siafoundation/design-system' import { Maybe } from '@siafoundation/types' -import { useRouter } from 'next/router' import { useContracts as useContractsData } from '@siafoundation/renterd-react' import { createContext, useContext, useMemo, useState } from 'react' import { @@ -33,9 +33,7 @@ const defaultLimit = 50 function useContractsMain() { const [viewMode, setViewMode] = useState('list') const [graphMode, setGraphMode] = useState('spending') - const router = useRouter() - const limit = Number(router.query.limit || defaultLimit) - const offset = Number(router.query.offset || 0) + const { limit, offset } = usePaginationOffset(defaultLimit) const response = useContractsData({ config: { swr: { @@ -81,19 +79,15 @@ function useContractsMain() { defaultSortField, }) - const datasetFiltered = useClientFilteredDataset({ - dataset, - filters, - sortField, - sortDirection, - }) - - const _datasetPage = useMemo>(() => { - if (!datasetFiltered) { - return undefined - } - return datasetFiltered.slice(offset, offset + limit) - }, [datasetFiltered, offset, limit]) + const { datasetFiltered, datasetPage: _datasetPage } = + useClientFilteredDataset({ + dataset, + filters, + sortField, + sortDirection, + offset, + limit, + }) const { range: contractsTimeRange } = useMemo( () => getContractsTimeRangeBlockHeight(currentHeight, _datasetPage || []), @@ -108,13 +102,6 @@ function useContractsMain() { [enabledColumns] ) - const dataState = useDatasetEmptyState( - datasetFiltered, - response.isValidating, - response.error, - filters - ) - const siascanUrl = useSiascanUrl() const filteredStats = useFilteredStats({ datasetFiltered }) @@ -135,6 +122,14 @@ function useContractsMain() { }) }, [_datasetPage, multiSelect]) + const datasetState = useDatasetState({ + datasetPage, + isValidating: response.isValidating, + error: response.error, + offset, + filters, + }) + const cellContext = useMemo(() => { const context: ContractTableContext = { currentHeight: syncStatus.estimatedBlockHeight, @@ -177,14 +172,14 @@ function useContractsMain() { }) return { - dataState, + datasetState, limit, offset, isLoading: response.isLoading, error: response.error, - pageCount: datasetPage?.length || 0, - datasetCount: dataset?.length || 0, - datasetFilteredCount: datasetFiltered?.length || 0, + datasetTotal: dataset?.length || 0, + datasetFilteredTotal: datasetFiltered?.length || 0, + datasetPageTotal: datasetPage?.length || 0, columns: filteredTableColumns, dataset, cellContext, diff --git a/apps/renterd/contexts/filesDirectory/dataset.tsx b/apps/renterd/contexts/filesDirectory/dataset.tsx index f1355702e..d15f661d1 100644 --- a/apps/renterd/contexts/filesDirectory/dataset.tsx +++ b/apps/renterd/contexts/filesDirectory/dataset.tsx @@ -1,11 +1,11 @@ import { useObjects } from '@siafoundation/renterd-react' import { useDataset as useDatasetGeneric } from '../filesManager/dataset' import { bucketAndKeyParamsFromPath } from '../../lib/paths' -import { useRouter } from 'next/router' import { useMemo } from 'react' import { useFilesManager } from '../filesManager' import { defaultDatasetRefreshInterval } from '../../config/swr' import { ObjectsParams } from '@siafoundation/renterd-types' +import { usePaginationMarker } from '@siafoundation/design-system' const defaultLimit = 50 @@ -17,10 +17,7 @@ export function useDataset() { sortDirection, sortField, } = useFilesManager() - const router = useRouter() - const limit = Number(router.query.limit || defaultLimit) - const marker = router.query.marker as string - + const { limit, marker } = usePaginationMarker(defaultLimit) const pathParams = bucketAndKeyParamsFromPath(activeDirectoryPath) const params = useMemo(() => { @@ -77,6 +74,7 @@ export function useDataset() { return { limit, marker, + nextMarker: response.data?.nextMarker || null, isMore: !!response.data?.hasMore, response, dataset: d.data, diff --git a/apps/renterd/contexts/filesDirectory/index.tsx b/apps/renterd/contexts/filesDirectory/index.tsx index e7ded0adc..90b70f6dc 100644 --- a/apps/renterd/contexts/filesDirectory/index.tsx +++ b/apps/renterd/contexts/filesDirectory/index.tsx @@ -1,7 +1,4 @@ -import { - useDatasetEmptyState, - useMultiSelect, -} from '@siafoundation/design-system' +import { useDatasetState, useMultiSelect } from '@siafoundation/design-system' import { createContext, MouseEvent, @@ -25,7 +22,8 @@ function useFilesDirectoryMain() { isViewingBuckets, } = useFilesManager() - const { limit, marker, isMore, response, refresh, dataset } = useDataset() + const { limit, marker, nextMarker, isMore, response, refresh, dataset } = + useDataset() const multiSelect = useMultiSelect(dataset) @@ -125,12 +123,13 @@ function useFilesDirectoryMain() { }) }, [datasetPageWithOnClick, draggingObjects]) - const dataState = useDatasetEmptyState( - dataset, - response.isValidating, - response.error, - filters - ) + const datasetState = useDatasetState({ + datasetPage, + isValidating: response.isValidating, + error: response.error, + marker, + filters, + }) const filteredTableColumns = useMemo( () => @@ -150,16 +149,17 @@ function useFilesDirectoryMain() { ) return { - dataState, + datasetState, columns: filteredTableColumns, multiSelect, cellContext, refresh, limit, marker, + nextMarker, isMore, datasetPage, - pageCount: dataset?.length || 0, + datasetPageTotal: dataset?.length || 0, onDragStart, onDragEnd, onDragMove, diff --git a/apps/renterd/contexts/filesFlat/dataset.tsx b/apps/renterd/contexts/filesFlat/dataset.tsx index 6f6badf71..aa01e7515 100644 --- a/apps/renterd/contexts/filesFlat/dataset.tsx +++ b/apps/renterd/contexts/filesFlat/dataset.tsx @@ -1,11 +1,11 @@ import { useObjects } from '@siafoundation/renterd-react' import { SortField } from '../filesManager/types' import { useDataset as useDatasetGeneric } from '../filesManager/dataset' -import { useRouter } from 'next/router' import { useMemo } from 'react' import { useFilesManager } from '../filesManager' import { defaultDatasetRefreshInterval } from '../../config/swr' import { ObjectsParams } from '@siafoundation/renterd-types' +import { usePaginationMarker } from '@siafoundation/design-system' type Props = { sortDirection: 'asc' | 'desc' @@ -16,9 +16,7 @@ const defaultLimit = 50 export function useDataset({ sortDirection, sortField }: Props) { const { activeBucketName, fileNamePrefixFilter } = useFilesManager() - const router = useRouter() - const limit = Number(router.query.limit || defaultLimit) - const marker = router.query.marker as string + const { limit, marker } = usePaginationMarker(defaultLimit) const params = useMemo(() => { let prefix = '' @@ -74,6 +72,7 @@ export function useDataset({ sortDirection, sortField }: Props) { return { limit, marker, + nextMarker: response.data?.nextMarker || null, response, isMore: !!response.data?.hasMore, dataset: d.data, diff --git a/apps/renterd/contexts/filesFlat/index.tsx b/apps/renterd/contexts/filesFlat/index.tsx index dcc0bfaa8..fa5265d87 100644 --- a/apps/renterd/contexts/filesFlat/index.tsx +++ b/apps/renterd/contexts/filesFlat/index.tsx @@ -1,7 +1,4 @@ -import { - useDatasetEmptyState, - useMultiSelect, -} from '@siafoundation/design-system' +import { useDatasetState, useMultiSelect } from '@siafoundation/design-system' import { createContext, MouseEvent, @@ -23,23 +20,16 @@ function useFilesFlatMain() { enabledColumns, isViewingBuckets, } = useFilesManager() - const { limit, response, isMore, refresh, dataset } = useDataset({ - sortField, - sortDirection, - }) - const nextMarker = response.data?.nextMarker + const { limit, marker, nextMarker, response, isMore, refresh, dataset } = + useDataset({ + sortField, + sortDirection, + }) const _datasetPage = useMemo(() => { return dataset }, [dataset]) - const dataState = useDatasetEmptyState( - dataset, - response.isValidating, - response.error, - filters - ) - const filteredTableColumns = useMemo( () => columns.filter( @@ -72,6 +62,14 @@ function useFilesFlatMain() { }) }, [_datasetPage, multiSelect]) + const datasetState = useDatasetState({ + datasetPage, + isValidating: response.isValidating, + error: response.error, + marker, + filters, + }) + const cellContext = useMemo( () => ({ @@ -82,16 +80,17 @@ function useFilesFlatMain() { ) return { - dataState, + datasetState, multiSelect, cellContext, refresh, limit, datasetPage, columns: filteredTableColumns, + marker, nextMarker, isMore, - pageCount: dataset?.length || 0, + datasetPageTotal: dataset?.length || 0, sortField, filters, sortDirection, diff --git a/apps/renterd/contexts/filesManager/dataset.tsx b/apps/renterd/contexts/filesManager/dataset.tsx index fec8a14c3..f2aac5915 100644 --- a/apps/renterd/contexts/filesManager/dataset.tsx +++ b/apps/renterd/contexts/filesManager/dataset.tsx @@ -12,6 +12,7 @@ import { } from '../../lib/paths' import { useFilesManager } from '.' import { useEffect } from 'react' +import { Maybe } from '@siafoundation/types' type Props = { id: string @@ -34,7 +35,7 @@ export function useDataset({ id, objects }: Props) { setActiveDirectory, } = useFilesManager() const { dataset: allContracts } = useContracts() - const response = useSWR( + const response = useSWR>( objects.isValidating || buckets.isValidating ? undefined : [id, activeBucketName, activeDirectoryPath], diff --git a/apps/renterd/contexts/hosts/dataset.ts b/apps/renterd/contexts/hosts/dataset.ts index efad1db4b..041448210 100644 --- a/apps/renterd/contexts/hosts/dataset.ts +++ b/apps/renterd/contexts/hosts/dataset.ts @@ -11,6 +11,7 @@ import { ContractData } from '../contracts/types' import { SiaCentralHost } from '@siafoundation/sia-central-types' import { Maybe } from '@siafoundation/types' import { objectEntries } from '@siafoundation/design-system' +import { maybeFromNullishArrayResponse } from '@siafoundation/react-core' export function useDataset({ response, @@ -28,12 +29,13 @@ export function useDataset({ geoHosts: SiaCentralHost[] }) { return useMemo>(() => { - const allow = allowlist.data - const block = blocklist.data - if (!response.data || !allow || !block) { + const data = maybeFromNullishArrayResponse(response.data) + const allow = maybeFromNullishArrayResponse(allowlist.data) + const block = maybeFromNullishArrayResponse(blocklist.data) + if (!data || !allow || !block) { return undefined } - return response.data.map((host) => { + return data.map((host) => { const sch = geoHosts.find((gh) => gh.public_key === host.publicKey) return { ...getHostFields(host, allContracts), @@ -52,10 +54,10 @@ export function useDataset({ } }) }, [ - response.data, + response, allContracts, - allowlist.data, - blocklist.data, + allowlist, + blocklist, isAllowlistActive, geoHosts, ]) diff --git a/apps/renterd/contexts/hosts/index.tsx b/apps/renterd/contexts/hosts/index.tsx index 46657eab9..2c1c43a9c 100644 --- a/apps/renterd/contexts/hosts/index.tsx +++ b/apps/renterd/contexts/hosts/index.tsx @@ -1,8 +1,9 @@ import { triggerToast, truncate, - useDatasetEmptyState, + useDatasetState, useMultiSelect, + usePaginationOffset, useServerFilters, useTableState, } from '@siafoundation/design-system' @@ -18,7 +19,6 @@ import { HostsUsabilityMode, } from '@siafoundation/renterd-types' import { useSiaCentralHosts } from '@siafoundation/sia-central-react' -import { useRouter } from 'next/router' import { createContext, useCallback, @@ -45,10 +45,8 @@ import { const defaultLimit = 50 function useHostsMain() { - const router = useRouter() const [viewMode, setViewMode] = useState('list') - const limit = Number(router.query.limit || defaultLimit) - const offset = Number(router.query.offset || 0) + const { limit, offset } = usePaginationOffset(defaultLimit) const { filters, setFilter, removeFilter, removeLastFilter, resetFilters } = useServerFilters() @@ -164,10 +162,6 @@ function useHostsMain() { [enabledColumns] ) - const isValidating = response.isValidating - const error = response.error - const dataState = useDatasetEmptyState(dataset, isValidating, error, filters) - const hostsWithLocation = useMemo( () => dataset?.filter((h) => h.location) as HostDataWithLocation[], [dataset] @@ -239,6 +233,16 @@ function useHostsMain() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeHost]) + const isValidating = response.isValidating + const error = response.error + const datasetState = useDatasetState({ + datasetPage, + isValidating, + error, + offset, + filters, + }) + return { setCmd, viewMode, @@ -247,10 +251,10 @@ function useHostsMain() { setViewMode, hostsWithLocation, error, - dataState, + datasetState, offset, limit, - pageCount: datasetPage?.length || 0, + datasetPageTotal: datasetPage?.length || 0, columns: filteredTableColumns, datasetPage, tableContext, diff --git a/apps/renterd/contexts/keys/index.tsx b/apps/renterd/contexts/keys/index.tsx index 4e0bc55f1..07b9e3193 100644 --- a/apps/renterd/contexts/keys/index.tsx +++ b/apps/renterd/contexts/keys/index.tsx @@ -1,11 +1,11 @@ import { useTableState, - useDatasetEmptyState, + useDatasetState, useClientFilters, useClientFilteredDataset, useMultiSelect, + usePaginationOffset, } from '@siafoundation/design-system' -import { useRouter } from 'next/router' import { createContext, useContext, useMemo } from 'react' import { CellContext, @@ -17,13 +17,12 @@ import { import { columns } from './columns' import { defaultDatasetRefreshInterval } from '../../config/swr' import { useSettingsS3 } from '@siafoundation/renterd-react' +import { Maybe } from '@siafoundation/types' const defaultLimit = 50 function useKeysMain() { - const router = useRouter() - const limit = Number(router.query.limit || defaultLimit) - const offset = Number(router.query.offset || 0) + const { limit, offset } = usePaginationOffset(defaultLimit) const response = useSettingsS3({ config: { swr: { @@ -32,20 +31,19 @@ function useKeysMain() { }, }) - const dataset = useMemo(() => { + const dataset = useMemo>(() => { if (!response.data) { return undefined } - const data: KeyData[] = - Object.entries(response.data?.authentication.v4Keypairs || {}).map( - ([key, secret]) => { - return { - id: key, - key, - secret, - } - } - ) || [] + const data: KeyData[] = Object.entries( + response.data?.authentication.v4Keypairs || {} + ).map(([key, secret]) => { + return { + id: key, + key, + secret, + } + }) return data }, [response.data]) @@ -72,19 +70,15 @@ function useKeysMain() { defaultSortField, }) - const datasetFiltered = useClientFilteredDataset({ - dataset, - filters, - sortField, - sortDirection, - }) - - const _datasetPage = useMemo(() => { - if (!datasetFiltered) { - return undefined - } - return datasetFiltered.slice(offset, offset + limit) - }, [datasetFiltered, offset, limit]) + const { datasetFiltered, datasetPage: _datasetPage } = + useClientFilteredDataset({ + dataset, + filters, + sortField, + sortDirection, + offset, + limit, + }) const multiSelect = useMultiSelect(_datasetPage) @@ -110,12 +104,13 @@ function useKeysMain() { [enabledColumns] ) - const dataState = useDatasetEmptyState( + const datasetState = useDatasetState({ datasetPage, - response.isValidating, - response.error, - filters - ) + isValidating: response.isValidating, + error: response.error, + offset, + filters, + }) const cellContext = useMemo( () => @@ -126,14 +121,14 @@ function useKeysMain() { ) return { - dataState, + datasetState, limit, offset, isLoading: response.isLoading, error: response.error, - pageCount: datasetPage?.length || 0, - datasetCount: dataset?.length || 0, - datasetFilteredCount: datasetFiltered?.length || 0, + datasetTotal: dataset?.length || 0, + datasetFilteredTotal: datasetFiltered?.length || 0, + datasetPageTotal: datasetPage?.length || 0, columns: filteredTableColumns, multiSelect, cellContext, diff --git a/apps/renterd/contexts/transactions/index.tsx b/apps/renterd/contexts/transactions/index.tsx index a9030b468..c6501593c 100644 --- a/apps/renterd/contexts/transactions/index.tsx +++ b/apps/renterd/contexts/transactions/index.tsx @@ -1,7 +1,4 @@ -import { - useDatasetEmptyState, - useTableState, -} from '@siafoundation/design-system' +import { useDatasetState, useTableState } from '@siafoundation/design-system' import { useMetricsWallet, useWalletEvents, @@ -28,6 +25,7 @@ import { sortOptions, } from './types' import BigNumber from 'bignumber.js' +import { Maybe } from '@siafoundation/types' const defaultPageSize = 50 const filters = [] as string[] @@ -56,7 +54,7 @@ function useTransactionsMain() { }) const syncStatus = useSyncStatus() - const dataset = useMemo(() => { + const datasetPage = useMemo>(() => { if (!events.data || !pending.data) { return undefined } @@ -95,8 +93,12 @@ function useTransactionsMain() { } return res }) - return [...dataPending.reverse(), ...dataEvents] - }, [events.data, pending.data, syncStatus.nodeBlockHeight]) + if (offset === 0) { + return [...dataPending.reverse(), ...dataEvents] + } else { + return [...dataEvents] + } + }, [events.data, pending.data, syncStatus.nodeBlockHeight, offset]) const { configurableColumns, @@ -129,7 +131,13 @@ function useTransactionsMain() { const isValidating = events.isValidating || pending.isValidating const error = events.error || pending.error - const dataState = useDatasetEmptyState(dataset, isValidating, error, filters) + const datasetState = useDatasetState({ + datasetPage, + isValidating, + error, + offset, + filters, + }) const siascanUrl = useSiascanUrl() const cellContext = useMemo( @@ -177,12 +185,12 @@ function useTransactionsMain() { return { balances, metrics, - dataset, + datasetPage, error, - dataState, + datasetState, offset, limit, - pageCount: dataset?.length || 0, + datasetPageTotal: datasetPage?.length || 0, defaultPageSize, cellContext, configurableColumns, diff --git a/apps/renterd/contexts/uploads/index.tsx b/apps/renterd/contexts/uploads/index.tsx index 2edf51281..ee7f2088b 100644 --- a/apps/renterd/contexts/uploads/index.tsx +++ b/apps/renterd/contexts/uploads/index.tsx @@ -1,9 +1,8 @@ import { useTableState, - useDatasetEmptyState, + useDatasetState, useServerFilters, } from '@siafoundation/design-system' -import { useSearchParams } from '@siafoundation/next' import { useMultipartUploadAbort, useMultipartUploadListUploads, @@ -15,30 +14,38 @@ import { join, getFilename } from '../../lib/paths' import { useFilesManager } from '../filesManager' import { ObjectUploadData } from '../filesManager/types' import { MultipartUploadListUploadsPayload } from '@siafoundation/renterd-types' +import { maybeFromNullishArrayResponse } from '@siafoundation/react-core' +import { Maybe, Nullable } from '@siafoundation/types' +import { useSearchParams } from '@siafoundation/next' const defaultLimit = 50 function useUploadsMain() { const { uploadsMap, activeBucket } = useFilesManager() - const params = useSearchParams() - const limit = Number(params.get('limit') || defaultLimit) - const marker = params.get('marker') + const searchParams = useSearchParams() + const limit = Number(searchParams.get('limit') || defaultLimit) + const marker = searchParams.get('marker') || null + const markers = useMarkersFromParam(marker) const { filters, setFilter, removeFilter, removeLastFilter, resetFilters } = useServerFilters() const apiBusUploadAbort = useMultipartUploadAbort() - const payload = useMemo(() => { + const payload = useMemo>(() => { + if (!activeBucket?.name) { + return undefined + } return { bucket: activeBucket?.name, - uploadIDMarker: marker, + uploadIDMarker: markers?.uploadIDMarker || undefined, + keyMarker: markers?.keyMarker || undefined, limit, } - }, [activeBucket, marker, limit]) + }, [activeBucket, limit, markers]) const response = useMultipartUploadListUploads({ - disabled: !activeBucket, + disabled: !payload, payload: payload as MultipartUploadListUploadsPayload, config: { swr: { @@ -69,11 +76,12 @@ function useUploadsMain() { ) }, [response.data, apiBusUploadAbort, activeBucket, uploadsMap]) - const dataset: ObjectUploadData[] = useMemo(() => { - if (!response.data?.uploads || !activeBucket?.name) { - return [] + const datasetPage = useMemo>(() => { + const uploads = maybeFromNullishArrayResponse(response.data?.uploads) + if (!uploads || !activeBucket?.name) { + return undefined } - return response.data.uploads.map((upload) => { + return uploads.map((upload) => { const id = upload.uploadID const key = upload.key const name = getFilename(key) @@ -106,7 +114,7 @@ function useUploadsMain() { }, } }) - }, [uploadsMap, activeBucket, response.data, apiBusUploadAbort]) + }, [uploadsMap, activeBucket, response, apiBusUploadAbort]) const { configurableColumns, @@ -136,24 +144,31 @@ function useUploadsMain() { [enabledColumns] ) - const dataState = useDatasetEmptyState( - dataset, - response.isValidating, - response.error, - filters - ) + const datasetState = useDatasetState({ + datasetPage, + isValidating: response.isValidating, + error: response.error, + marker, + filters, + }) + + const nextMarker = useBuildMarkerParam({ + uploadIDMarker: response.data?.nextUploadIDMarker || '', + keyMarker: response.data?.nextMarker || '', + }) return { abortAll, - dataState, + datasetState, limit, - nextMarker: response.data?.nextUploadIDMarker, + marker, + nextMarker, hasMore: !!response.data?.hasMore, isLoading: response.isLoading, error: response.error, - pageCount: dataset?.length || 0, + datasetPageTotal: datasetPage?.length || 0, columns: filteredTableColumns, - datasetPage: dataset, + datasetPage, configurableColumns, enabledColumns, sortableColumns, @@ -189,3 +204,31 @@ export function UploadsProvider({ children }: Props) { {children} ) } + +function useMarkersFromParam(marker: Nullable) { + return useMemo>(() => { + if (marker) { + const [uploadIDMarker, keyMarker] = marker.split('') + if (uploadIDMarker && keyMarker) { + return { keyMarker, uploadIDMarker } + } + return undefined + } + return undefined + }, [marker]) +} + +function useBuildMarkerParam({ + uploadIDMarker, + keyMarker, +}: { + uploadIDMarker: string + keyMarker: string +}): Nullable { + return useMemo(() => { + if (!uploadIDMarker || !keyMarker) { + return null + } + return `${uploadIDMarker}${keyMarker}` + }, [uploadIDMarker, keyMarker]) +} diff --git a/apps/renterd/dialogs/RenterdTransactionDetailsDialog.tsx b/apps/renterd/dialogs/RenterdTransactionDetailsDialog.tsx index 06d9d48eb..93fc72709 100644 --- a/apps/renterd/dialogs/RenterdTransactionDetailsDialog.tsx +++ b/apps/renterd/dialogs/RenterdTransactionDetailsDialog.tsx @@ -6,11 +6,11 @@ import { useTransactions } from '../contexts/transactions' export function RenterdTransactionDetailsDialog() { const { id, dialog, onOpenChange } = useDialog() - // TODO: add transaction endpoint - const { dataset } = useTransactions() + // TODO: fetch transaction so that not dependent on datasetPage. + const { datasetPage } = useTransactions() const transaction = useMemo(() => { - return dataset?.find((t) => t.id === id) - }, [dataset, id]) + return datasetPage?.find((t) => t.id === id) + }, [datasetPage, id]) return (
) diff --git a/apps/walletd/components/Wallet/index.tsx b/apps/walletd/components/Wallet/index.tsx index 37b35e13a..12c1cfc52 100644 --- a/apps/walletd/components/Wallet/index.tsx +++ b/apps/walletd/components/Wallet/index.tsx @@ -1,4 +1,4 @@ -import { Table } from '@siafoundation/design-system' +import { EmptyState, Table } from '@siafoundation/design-system' import { useEvents } from '../../contexts/events' import { StateNoneMatching } from './StateNoneMatching' import { StateNoneYet } from './StateNoneYet' @@ -6,8 +6,8 @@ import { StateError } from './StateError' export function Wallet() { const { - dataset, - dataState, + datasetPage, + datasetState, columns, cellContext, sortableColumns, @@ -20,18 +20,17 @@ export function Wallet() {
- ) : dataState === 'noneYet' ? ( - - ) : dataState === 'error' ? ( - - ) : null + } + noneYet={} + error={} + /> } pageSize={6} - data={dataset} + data={datasetPage} context={cellContext} columns={columns} sortableColumns={sortableColumns} diff --git a/apps/walletd/components/WalletAddresses/AddressesFilterBar.tsx b/apps/walletd/components/WalletAddresses/AddressesFilterBar.tsx index 10a749bb6..2d1079c51 100644 --- a/apps/walletd/components/WalletAddresses/AddressesFilterBar.tsx +++ b/apps/walletd/components/WalletAddresses/AddressesFilterBar.tsx @@ -1,15 +1,17 @@ -import { Text } from '@siafoundation/design-system' +import { PaginatorKnownTotal } from '@siafoundation/design-system' import { useAddresses } from '../../contexts/addresses' -import { pluralize } from '@siafoundation/units' export function AddressesFiltersBar() { - const { datasetCount } = useAddresses() + const { datasetTotal, offset, limit, datasetState } = useAddresses() return (
- - {pluralize(datasetCount, 'address', 'addresses')} - +
) } diff --git a/apps/walletd/components/WalletAddresses/index.tsx b/apps/walletd/components/WalletAddresses/index.tsx index 0bea96982..b8336a049 100644 --- a/apps/walletd/components/WalletAddresses/index.tsx +++ b/apps/walletd/components/WalletAddresses/index.tsx @@ -1,4 +1,4 @@ -import { Table } from '@siafoundation/design-system' +import { EmptyState, Table } from '@siafoundation/design-system' import { useAddresses } from '../../contexts/addresses' import { StateNoneMatching } from './StateNoneMatching' import { StateNoneYet } from './StateNoneYet' @@ -6,8 +6,8 @@ import { StateError } from './StateError' export function WalletAddresses() { const { - dataset, - dataState, + datasetPage, + datasetState, columns, cellContext, sortableColumns, @@ -19,18 +19,17 @@ export function WalletAddresses() { return (
- ) : dataState === 'noneYet' ? ( - - ) : dataState === 'error' ? ( - - ) : null + } + noneYet={} + error={} + /> } pageSize={6} - data={dataset} + data={datasetPage} context={cellContext} columns={columns} sortableColumns={sortableColumns} diff --git a/apps/walletd/components/WalletsList/WalletsFiltersBar.tsx b/apps/walletd/components/WalletsList/WalletsFiltersBar.tsx index 121ca2944..da6713802 100644 --- a/apps/walletd/components/WalletsList/WalletsFiltersBar.tsx +++ b/apps/walletd/components/WalletsList/WalletsFiltersBar.tsx @@ -1,10 +1,11 @@ -import { Text } from '@siafoundation/design-system' +import { PaginatorKnownTotal, Text } from '@siafoundation/design-system' import { Unlocked16 } from '@siafoundation/react-icons' import { useWallets } from '../../contexts/wallets' import { pluralize } from '@siafoundation/units' export function WalletsFiltersBar() { - const { datasetCount, unlockedCount } = useWallets() + const { datasetTotal, unlockedCount, offset, limit, datasetState } = + useWallets() return (
@@ -20,8 +21,14 @@ export function WalletsFiltersBar() { )}
- {pluralize(datasetCount, 'wallet')} + {pluralize(datasetTotal, 'wallet')} +
) } diff --git a/apps/walletd/components/WalletsList/index.tsx b/apps/walletd/components/WalletsList/index.tsx index 16b0cb4b7..d93bfeea6 100644 --- a/apps/walletd/components/WalletsList/index.tsx +++ b/apps/walletd/components/WalletsList/index.tsx @@ -1,4 +1,4 @@ -import { Table } from '@siafoundation/design-system' +import { EmptyState, Table } from '@siafoundation/design-system' import { useWallets } from '../../contexts/wallets' import { StateNoneYet } from './StateNoneYet' import { StateNoneMatching } from './StateNoneMatching' @@ -6,8 +6,8 @@ import { StateError } from './StateError' export function WalletsList() { const { - dataset, - dataState, + datasetPage, + datasetState, context, columns, sortableColumns, @@ -18,20 +18,20 @@ export function WalletsList() { return (
- {dataState === 'noneYet' && } - {dataState !== 'noneYet' && ( + {datasetState === 'noneYet' && } + {datasetState !== 'noneYet' && (
- ) : dataState === 'error' ? ( - - ) : null + } + error={} + /> } pageSize={6} - data={dataset} + data={datasetPage} context={context} columns={columns} sortableColumns={sortableColumns} diff --git a/apps/walletd/contexts/addresses/dataset.tsx b/apps/walletd/contexts/addresses/dataset.tsx index 2e2a4fa4a..d9d95f1ff 100644 --- a/apps/walletd/contexts/addresses/dataset.tsx +++ b/apps/walletd/contexts/addresses/dataset.tsx @@ -1,7 +1,4 @@ -import { - useDatasetEmptyState, - ClientFilterItem, -} from '@siafoundation/design-system' +import { useDatasetState, ClientFilterItem } from '@siafoundation/design-system' import { WalletAddressMetadata, WalletAddressesResponse, @@ -10,6 +7,7 @@ import { useWalletAddresses } from '@siafoundation/walletd-react' import { useMemo } from 'react' import { AddressData } from './types' import { OpenDialog, useDialog } from '../dialog' +import { Maybe } from '@siafoundation/types' export function transformAddressesResponse( response: WalletAddressesResponse, @@ -47,19 +45,19 @@ export function useDataset({ filters: ClientFilterItem[] }) { const { openDialog } = useDialog() - const dataset = useMemo(() => { + const dataset = useMemo>(() => { if (!response.data) { - return null + return undefined } return transformAddressesResponse(response.data, walletId, openDialog) }, [response.data, openDialog, walletId]) - const dataState = useDatasetEmptyState( - dataset, - response.isValidating, - response.error, - filters - ) + const datasetState = useDatasetState({ + datasetPage: dataset, + isValidating: response.isValidating, + error: response.error, + filters, + }) const lastIndex = (dataset || []).reduce( (highest, { metadata }) => @@ -69,7 +67,7 @@ export function useDataset({ return { dataset, - dataState, + datasetState, error: response.error, lastIndex, filters, diff --git a/apps/walletd/contexts/addresses/index.tsx b/apps/walletd/contexts/addresses/index.tsx index 13d776cc5..d5a6aef19 100644 --- a/apps/walletd/contexts/addresses/index.tsx +++ b/apps/walletd/contexts/addresses/index.tsx @@ -2,6 +2,7 @@ import { useTableState, useClientFilters, useClientFilteredDataset, + usePaginationOffset, } from '@siafoundation/design-system' import { useWalletAddresses } from '@siafoundation/walletd-react' import { createContext, useContext, useMemo } from 'react' @@ -18,9 +19,12 @@ import { useSiascanUrl } from '../../hooks/useSiascanUrl' import { defaultDatasetRefreshInterval } from '../../config/swr' import { useDataset } from './dataset' +const defaultLimit = 50 + export function useAddressesMain() { const router = useRouter() const walletId = router.query.id as string + const { limit, offset } = usePaginationOffset(defaultLimit) const response = useWalletAddresses({ disabled: !walletId, @@ -37,7 +41,7 @@ export function useAddressesMain() { const { filters, setFilter, removeFilter, removeLastFilter, resetFilters } = useClientFilters() - const { dataset, dataState, lastIndex } = useDataset({ + const { dataset, datasetState, lastIndex } = useDataset({ walletId, response, filters, @@ -63,11 +67,13 @@ export function useAddressesMain() { defaultSortField, }) - const datasetFiltered = useClientFilteredDataset({ + const { datasetFiltered, datasetPage } = useClientFilteredDataset({ dataset, filters, sortField, sortDirection, + limit, + offset, }) const filteredTableColumns = useMemo( @@ -87,11 +93,16 @@ export function useAddressesMain() { ) return { - dataState, + datasetState, error: response.error, - datasetCount: datasetFiltered?.length || 0, + datasetTotal: dataset?.length || 0, + datasetFilteredTotal: datasetFiltered?.length || 0, + datasetPageTotal: datasetPage?.length || 0, columns: filteredTableColumns, - dataset: datasetFiltered, + dataset, + datasetPage, + offset, + limit, cellContext, lastIndex, configurableColumns, diff --git a/apps/walletd/contexts/events/index.tsx b/apps/walletd/contexts/events/index.tsx index 64d5df722..419b5e5d1 100644 --- a/apps/walletd/contexts/events/index.tsx +++ b/apps/walletd/contexts/events/index.tsx @@ -1,7 +1,8 @@ import { useTableState, - useDatasetEmptyState, useServerFilters, + useDatasetState, + usePaginationOffset, } from '@siafoundation/design-system' import { useWalletEvents, @@ -27,14 +28,14 @@ import { useRouter } from 'next/router' import { useSiascanUrl } from '../../hooks/useSiascanUrl' import { defaultDatasetRefreshInterval } from '../../config/swr' import { useSyncStatus } from '../../hooks/useSyncStatus' +import { Maybe } from '@siafoundation/types' const defaultLimit = 100 export function useEventsMain() { const router = useRouter() const id = router.query.id as string - const limit = Number(router.query.limit || defaultLimit) - const offset = Number(router.query.offset || 0) + const { limit, offset } = usePaginationOffset(defaultLimit) const { filters, setFilter, removeFilter, removeLastFilter, resetFilters } = useServerFilters() const responseTxPool = useWalletEventsUnconfirmed({ @@ -63,9 +64,9 @@ export function useEventsMain() { }) const syncStatus = useSyncStatus() - const dataset = useMemo(() => { + const datasetPage = useMemo>(() => { if (!responseEvents.data || !responseTxPool.data) { - return null + return undefined } const dataTxPool: EventData[] = responseTxPool.data.map((e) => { const amountSc = calculateScValue(e) @@ -106,8 +107,17 @@ export function useEventsMain() { } return res }) - return [...dataTxPool.reverse(), ...dataEvents] - }, [responseEvents.data, responseTxPool.data, syncStatus.nodeBlockHeight]) + if (offset === 0) { + return [...dataTxPool.reverse(), ...dataEvents] + } else { + return [...dataEvents] + } + }, [ + responseEvents.data, + responseTxPool.data, + syncStatus.nodeBlockHeight, + offset, + ]) const { configurableColumns, @@ -141,7 +151,13 @@ export function useEventsMain() { responseEvents.isValidating || responseTxPool.isValidating const error = responseEvents.error || responseTxPool.error - const dataState = useDatasetEmptyState(dataset, isValidating, error, filters) + const datasetState = useDatasetState({ + datasetPage, + isValidating, + error, + filters, + offset, + }) const siascanUrl = useSiascanUrl() const cellContext = useMemo( @@ -152,11 +168,11 @@ export function useEventsMain() { ) return { - dataState, + datasetState, error: responseEvents.error, - pageCount: dataset?.length || 0, + datasetPageTotal: datasetPage?.length || 0, columns: filteredTableColumns, - dataset, + datasetPage, cellContext, configurableColumns, enabledColumns, diff --git a/apps/walletd/contexts/wallets/index.tsx b/apps/walletd/contexts/wallets/index.tsx index 2c450c906..a96d9b240 100644 --- a/apps/walletd/contexts/wallets/index.tsx +++ b/apps/walletd/contexts/wallets/index.tsx @@ -1,8 +1,9 @@ import { useTableState, - useDatasetEmptyState, + useDatasetState, useClientFilters, useClientFilteredDataset, + usePaginationOffset, } from '@siafoundation/design-system' import { WalletMetadata } from '@siafoundation/walletd-types' import { useWallets as useWalletsData } from '@siafoundation/walletd-react' @@ -20,6 +21,9 @@ import { useWalletSeedCache } from './useWalletSeedCache' import { useDialog } from '../dialog' import { useAppSettings } from '@siafoundation/react-core' import { defaultDatasetRefreshInterval } from '../../config/swr' +import { Maybe } from '@siafoundation/types' + +const defaultLimit = 50 function useWalletsMain() { const response = useWalletsData({ @@ -30,6 +34,7 @@ function useWalletsMain() { }, }) const router = useRouter() + const { limit, offset } = usePaginationOffset(defaultLimit) const { openDialog } = useDialog() const { setOnLockCallback } = useAppSettings() const { @@ -51,9 +56,9 @@ function useWalletsMain() { // eslint-disable-next-line react-hooks/exhaustive-deps }, []) - const dataset = useMemo(() => { + const dataset = useMemo>(() => { if (!response.data) { - return null + return undefined } const data: WalletData[] = response.data.map((wallet) => { const { id, name, description, dateCreated, lastUpdated, metadata } = @@ -126,11 +131,13 @@ function useWalletsMain() { defaultSortField, }) - const datasetFiltered = useClientFilteredDataset({ + const { datasetFiltered, datasetPage } = useClientFilteredDataset({ dataset, filters, sortField, sortDirection, + offset, + limit, }) const filteredTableColumns = useMemo( @@ -141,12 +148,12 @@ function useWalletsMain() { [enabledColumns] ) - const dataState = useDatasetEmptyState( - dataset, - response.isValidating, - response.error, - filters - ) + const datasetState = useDatasetState({ + datasetPage: datasetFiltered, + isValidating: response.isValidating, + error: response.error, + filters, + }) const context = useMemo( () => ({ @@ -157,14 +164,19 @@ function useWalletsMain() { ) return { - dataState, + datasetState, error: response.error, - datasetCount: datasetFiltered?.length || 0, + datasetTotal: dataset?.length || 0, + datasetFilteredTotal: datasetFiltered?.length || 0, + datasetPageTotal: datasetFiltered?.length || 0, unlockedCount: cachedMnemonicCount, columns: filteredTableColumns, - dataset: datasetFiltered, + datasetPage, + dataset, context, wallet, + limit, + offset, configurableColumns, enabledColumns, sortableColumns, diff --git a/apps/walletd/dialogs/AddressUpdateDialog/index.tsx b/apps/walletd/dialogs/AddressUpdateDialog/index.tsx index 2340fb934..bf6e6dd28 100644 --- a/apps/walletd/dialogs/AddressUpdateDialog/index.tsx +++ b/apps/walletd/dialogs/AddressUpdateDialog/index.tsx @@ -56,7 +56,7 @@ export function AddressUpdateDialog({ }: Props) { const { walletId, address: addr } = params || {} const { openDialog } = useDialog() - const { dataset, dataState } = useWalletAddresses({ id: walletId }) + const { dataset, datasetState } = useWalletAddresses({ id: walletId }) const address = dataset?.find((d) => d.id === addr) const addressAdd = useWalletAddressAdd() const defaultValues = getDefaultValues({ @@ -74,7 +74,7 @@ export function AddressUpdateDialog({ // Resets form with latest default values after elements change and are // all thruthy // This is used because address data is async and can be intially undefined - initKey: [params, dataState === undefined], + initKey: [params, datasetState === 'loaded'], }) const fields = getFields() diff --git a/apps/walletd/dialogs/WalletAddressesGenerateLedgerDialog/index.tsx b/apps/walletd/dialogs/WalletAddressesGenerateLedgerDialog/index.tsx index 3749ad913..71dae62ad 100644 --- a/apps/walletd/dialogs/WalletAddressesGenerateLedgerDialog/index.tsx +++ b/apps/walletd/dialogs/WalletAddressesGenerateLedgerDialog/index.tsx @@ -121,7 +121,7 @@ export function WalletAddressesGenerateLedgerDialog({ const { dataset: existingAddresses, lastIndex, - datasetCount, + datasetTotal, } = useWalletAddresses({ id: walletId }) const syncStatus = useSyncStatus() const { dataset } = useWallets() @@ -364,7 +364,7 @@ export function WalletAddressesGenerateLedgerDialog({ >
- Wallet currently has {pluralize(datasetCount, 'address', 'addresses')}{' '} + Wallet currently has {pluralize(datasetTotal, 'address', 'addresses')}{' '} with a highest index of {lastIndex}. Select a start index and the number of sequential addresses you would like to generate. diff --git a/apps/walletd/hooks/useWalletAddresses.tsx b/apps/walletd/hooks/useWalletAddresses.tsx index bedb3693f..644582464 100644 --- a/apps/walletd/hooks/useWalletAddresses.tsx +++ b/apps/walletd/hooks/useWalletAddresses.tsx @@ -20,16 +20,16 @@ export function useWalletAddresses({ id }: { id: string }) { }, }) - const { dataset, dataState, lastIndex } = useDataset({ + const { dataset, datasetState, lastIndex } = useDataset({ walletId: id, response, filters, }) return { - dataState, + datasetState, error: response.error, - datasetCount: dataset?.length || 0, + datasetTotal: dataset?.length || 0, dataset: dataset, lastIndex, } diff --git a/libs/design-system/src/app/AlertsDialog/index.tsx b/libs/design-system/src/app/AlertsDialog/index.tsx index 762b45f5c..c066bf7f3 100644 --- a/libs/design-system/src/app/AlertsDialog/index.tsx +++ b/libs/design-system/src/app/AlertsDialog/index.tsx @@ -6,7 +6,7 @@ import { Heading } from '../../core/Heading' import { Checkmark16 } from '@siafoundation/react-icons' import { Skeleton } from '../../core/Skeleton' import { Text } from '../../core/Text' -import { useDatasetEmptyState } from '../../hooks/useDatasetEmptyState' +import { useDatasetState } from '../../hooks/useDatasetState' import { humanDate } from '@siafoundation/units' import { cx } from 'class-variance-authority' import { times } from '@technically/lodash' @@ -36,8 +36,6 @@ type Props = { > } -const stubFilters: unknown[] = [] - export function AlertsDialog({ open, onOpenChange, @@ -47,12 +45,11 @@ export function AlertsDialog({ dataFieldOrder, dataFields, }: Props) { - const loadingState = useDatasetEmptyState( - alerts.data, - alerts.isValidating, - alerts.error, - stubFilters - ) + const loadingState = useDatasetState({ + datasetPage: alerts.data, + isValidating: alerts.isValidating, + error: alerts.error, + }) const [filter, setFilter] = useState() const dataset = useMemo( diff --git a/libs/design-system/src/components/EmptyState/StateError.tsx b/libs/design-system/src/components/EmptyState/StateError.tsx new file mode 100644 index 000000000..25adda91e --- /dev/null +++ b/libs/design-system/src/components/EmptyState/StateError.tsx @@ -0,0 +1,20 @@ +import { Text } from '../../core/Text' +import { MisuseOutline32 } from '@siafoundation/react-icons' +import { SWRError } from '@siafoundation/react-core' + +type Props = { + error?: SWRError +} + +export function StateError({ error }: Props) { + return ( +
+ + + + + Error loading data. Please try again later. + +
+ ) +} diff --git a/libs/design-system/src/components/EmptyState/StateNoData.tsx b/libs/design-system/src/components/EmptyState/StateNoData.tsx new file mode 100644 index 000000000..95475811f --- /dev/null +++ b/libs/design-system/src/components/EmptyState/StateNoData.tsx @@ -0,0 +1,15 @@ +import { Text } from '../../core/Text' +import { ChartArea32 } from '@siafoundation/react-icons' + +export function StateNoData() { + return ( +
+ + + + + No data available. + +
+ ) +} diff --git a/libs/design-system/src/components/EmptyState/StateNoneMatching.tsx b/libs/design-system/src/components/EmptyState/StateNoneMatching.tsx new file mode 100644 index 000000000..ddbd19f49 --- /dev/null +++ b/libs/design-system/src/components/EmptyState/StateNoneMatching.tsx @@ -0,0 +1,15 @@ +import { Text } from '../../core/Text' +import { Filter32 } from '@siafoundation/react-icons' + +export function StateNoneMatching() { + return ( +
+ + + + + No data matching filters. + +
+ ) +} diff --git a/libs/design-system/src/components/EmptyState/StateNoneOnPage.tsx b/libs/design-system/src/components/EmptyState/StateNoneOnPage.tsx new file mode 100644 index 000000000..a6c7a9b9d --- /dev/null +++ b/libs/design-system/src/components/EmptyState/StateNoneOnPage.tsx @@ -0,0 +1,29 @@ +import { Button } from '../../core/Button' +import { Text } from '../../core/Text' +import { usePagesRouter } from '@siafoundation/next' +import { Reset32 } from '@siafoundation/react-icons' +import { useCallback } from 'react' + +export function StateNoneOnPage() { + const router = usePagesRouter() + const back = useCallback(() => { + router.push({ + query: { + ...router.query, + offset: 0, + marker: undefined, + }, + }) + }, [router]) + return ( +
+ + + + + No data on this page, reset pagination to continue. + + +
+ ) +} diff --git a/libs/design-system/src/components/EmptyState/StateNoneYet.tsx b/libs/design-system/src/components/EmptyState/StateNoneYet.tsx new file mode 100644 index 000000000..ca6a75b6d --- /dev/null +++ b/libs/design-system/src/components/EmptyState/StateNoneYet.tsx @@ -0,0 +1,15 @@ +import { Text } from '../../core/Text' +import { DataBase32 } from '@siafoundation/react-icons' + +export function StateNoneYet() { + return ( +
+ + + + + There is no data yet. + +
+ ) +} diff --git a/libs/design-system/src/components/EmptyState/index.tsx b/libs/design-system/src/components/EmptyState/index.tsx new file mode 100644 index 000000000..e643187a5 --- /dev/null +++ b/libs/design-system/src/components/EmptyState/index.tsx @@ -0,0 +1,30 @@ +import { StateNoneMatching } from './StateNoneMatching' +import { StateNoneYet } from './StateNoneYet' +import { StateError } from './StateError' +import { StateNoneOnPage } from './StateNoneOnPage' +import React from 'react' +import { DatasetState } from '../../hooks/useDatasetState' + +export function EmptyState({ + datasetState, + noneOnPage, + noneMatching, + noneYet, + error, +}: { + datasetState: DatasetState + noneOnPage?: React.ReactNode + noneMatching?: React.ReactNode + noneYet?: React.ReactNode + error?: React.ReactNode +}) { + return datasetState === 'noneOnPage' + ? noneOnPage || + : datasetState === 'noneMatchingFilters' + ? noneMatching || + : datasetState === 'noneYet' + ? noneYet || + : datasetState === 'error' + ? error || + : null +} diff --git a/libs/design-system/src/components/PaginatorKnownTotal.tsx b/libs/design-system/src/components/PaginatorKnownTotal.tsx index 009d012bf..f20b63825 100644 --- a/libs/design-system/src/components/PaginatorKnownTotal.tsx +++ b/libs/design-system/src/components/PaginatorKnownTotal.tsx @@ -15,14 +15,13 @@ type Props = { offset: number limit: number isLoading: boolean - datasetTotal: number - pageTotal: number + total: number } export function PaginatorKnownTotal({ offset, limit, - datasetTotal, + total, isLoading, }: Props) { const router = usePagesRouter() @@ -65,9 +64,9 @@ export function PaginatorKnownTotal({ ) : pageTotal ? ( ) : (