diff --git a/apps/hostd/components/Contracts/index.tsx b/apps/hostd/components/Contracts/index.tsx index dee309951..c8c33f140 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' @@ -24,13 +24,12 @@ export function Contracts() { context={cellContext} isLoading={dataState === 'loading'} emptyState={ - dataState === 'noneMatchingFilters' ? ( - - ) : dataState === 'noneYet' ? ( - - ) : dataState === 'error' ? ( - - ) : null + } + noneYet={} + error={} + /> } pageSize={limit} data={datasetPage} diff --git a/apps/hostd/components/Volumes/index.tsx b/apps/hostd/components/Volumes/index.tsx index ec0358316..44c3047df 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, dataState, isLoading, columns } = useVolumes() return (
} + emptyState={ + } /> + } /> ) diff --git a/apps/hostd/components/Wallet/index.tsx b/apps/hostd/components/Wallet/index.tsx index 72b6b0d75..558e81810 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,7 +12,7 @@ export function Wallet() { const { balances, metrics, - dataset, + datasetPage, dataState, columns, cellContext, @@ -31,16 +35,15 @@ export function Wallet() { testId="eventsTable" isLoading={dataState === 'loading'} emptyState={ - dataState === 'noneMatchingFilters' ? ( - - ) : 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..e4b99c312 100644 --- a/apps/hostd/contexts/contracts/index.tsx +++ b/apps/hostd/contexts/contracts/index.tsx @@ -1,6 +1,6 @@ import { useTableState, - useDatasetEmptyState, + useDataEmptyState, useServerFilters, getContractsTimeRangeBlockHeight, useMultiSelect, @@ -82,15 +82,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 +106,13 @@ function useContractsMain() { }) }, [_datasetPage, multiSelect]) + const isValidating = response.isValidating + const error = response.error + const dataState = useDataEmptyState(datasetPage, isValidating, error, { + filters, + offset, + }) + const siascanUrl = useSiascanUrl() const cellContext = useMemo( diff --git a/apps/hostd/contexts/transactions/index.tsx b/apps/hostd/contexts/transactions/index.tsx index 0751fb8a4..9d09c8a5d 100644 --- a/apps/hostd/contexts/transactions/index.tsx +++ b/apps/hostd/contexts/transactions/index.tsx @@ -1,5 +1,5 @@ import { - useDatasetEmptyState, + useDataEmptyState, 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,22 @@ function useTransactionsMain() { [metrics.data] ) + const isValidating = events.isValidating || pending.isValidating + const error = events.error || pending.error + const dataState = useDataEmptyState(datasetPage, isValidating, error, { + filters, + offset, + }) + return { balances, metrics, - dataset, + datasetPage, error, dataState, offset, limit, - pageCount: dataset?.length || 0, + pageCount: 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..d6a5a6534 100644 --- a/apps/hostd/contexts/volumes/index.tsx +++ b/apps/hostd/contexts/volumes/index.tsx @@ -1,7 +1,4 @@ -import { - useTableState, - useDatasetEmptyState, -} from '@siafoundation/design-system' +import { useTableState, useDataEmptyState } from '@siafoundation/design-system' import { VolumeMeta } from '@siafoundation/hostd-types' import { useVolumes as useVolumesData } from '@siafoundation/hostd-react' import { createContext, useContext, useMemo } from 'react' @@ -51,7 +48,7 @@ function useVolumesMain() { const isValidating = response.isValidating const error = response.error - const dataState = useDatasetEmptyState(dataset, isValidating, error, []) + const dataState = useDataEmptyState(dataset, isValidating, error) return { dataState, diff --git a/apps/renterd/components/Contracts/index.tsx b/apps/renterd/components/Contracts/index.tsx index 1c651bd43..88d305a4d 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' @@ -66,13 +66,12 @@ export function Contracts() { context={cellContext} isLoading={dataState === 'loading'} emptyState={ - dataState === 'noneMatchingFilters' ? ( - - ) : dataState === 'noneYet' ? ( - - ) : dataState === 'error' ? ( - - ) : null + } + noneYet={} + error={} + /> } sortableColumns={sortableColumns} pageSize={limit} 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/FilesDirectory/EmptyState/index.tsx b/apps/renterd/components/FilesDirectory/EmptyState/index.tsx index 14722c89a..4262854a3 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' @@ -17,6 +22,10 @@ export function EmptyState() { const autopilotNotEnabled = useAutopilotNotEnabled() const notEnoughContracts = useNotEnoughContracts() + if (dataState === 'noneOnPage') { + return + } + if (dataState === 'noneMatchingFilters') { return } diff --git a/apps/renterd/components/FilesDirectory/FilesStatsMenu/index.tsx b/apps/renterd/components/FilesDirectory/FilesDirectoryStatsMenu/index.tsx similarity index 83% rename from apps/renterd/components/FilesDirectory/FilesStatsMenu/index.tsx rename to apps/renterd/components/FilesDirectory/FilesDirectoryStatsMenu/index.tsx index ebad019c3..05677f357 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, nextMarker, isMore, pageCount, dataState } = + useFilesDirectory() return (
{isViewingBuckets ? ( @@ -18,7 +19,7 @@ export function FilesStatsMenu() { {isViewingABucket && ( + } + if (dataState === 'noneMatchingFilters') { return } diff --git a/apps/renterd/components/FilesFlat/FilesStatsMenu/index.tsx b/apps/renterd/components/FilesFlat/FilesFlatStatsMenu/index.tsx similarity index 90% rename from apps/renterd/components/FilesFlat/FilesStatsMenu/index.tsx rename to apps/renterd/components/FilesFlat/FilesFlatStatsMenu/index.tsx index ae573d840..87a902565 100644 --- a/apps/renterd/components/FilesFlat/FilesStatsMenu/index.tsx +++ b/apps/renterd/components/FilesFlat/FilesFlatStatsMenu/index.tsx @@ -3,14 +3,14 @@ import { useFilesFlat } from '../../../contexts/filesFlat' import { FilesFilterDirectoryMenu } from '../../Files/FilesFilterDirectoryMenu' import { FilesStatsMenuShared } from '../../Files/FilesStatsMenuShared' -export function FilesStatsMenu() { +export function FilesFlatStatsMenu() { const { limit, pageCount, dataState, nextMarker, isMore } = useFilesFlat() return (
+ } + if (dataState === 'error') { return (
diff --git a/apps/renterd/components/Keys/index.tsx b/apps/renterd/components/Keys/index.tsx index e4dcc0758..a814f2982 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' @@ -23,13 +23,12 @@ export function Keys() { testId="keysTable" isLoading={dataState === 'loading'} emptyState={ - dataState === 'noneMatchingFilters' ? ( - - ) : dataState === 'noneYet' ? ( - - ) : dataState === 'error' ? ( - - ) : null + } + noneYet={} + error={} + /> } sortableColumns={sortableColumns} pageSize={limit} diff --git a/apps/renterd/components/Uploads/EmptyState/index.tsx b/apps/renterd/components/Uploads/EmptyState/index.tsx index a6ccb146e..01f17d1df 100644 --- a/apps/renterd/components/Uploads/EmptyState/index.tsx +++ b/apps/renterd/components/Uploads/EmptyState/index.tsx @@ -2,10 +2,15 @@ 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() + if (dataState === 'noneOnPage') { + return + } + if (dataState === 'noneMatchingFilters') { return } diff --git a/apps/renterd/components/Uploads/UploadsStatsMenu/index.tsx b/apps/renterd/components/Uploads/UploadsStatsMenu/index.tsx index 651b121c7..e84dc78d0 100644 --- a/apps/renterd/components/Uploads/UploadsStatsMenu/index.tsx +++ b/apps/renterd/components/Uploads/UploadsStatsMenu/index.tsx @@ -9,7 +9,7 @@ export function UploadsStatsMenu() {
{pageCount > 0 && } - ) : 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..517c8ecc1 100644 --- a/apps/renterd/contexts/alerts/index.tsx +++ b/apps/renterd/contexts/alerts/index.tsx @@ -1,6 +1,6 @@ import { useTableState, - useDatasetEmptyState, + useDataEmptyState, useServerFilters, } from '@siafoundation/design-system' import { useRouter } from 'next/router' @@ -23,6 +23,7 @@ import { useAlertsDismiss, } from '@siafoundation/renterd-react' import { useCallback } from 'react' +import { Maybe } from '@siafoundation/types' const defaultLimit = 50 @@ -106,7 +107,7 @@ function useAlertsMain() { [dismiss] ) - const datasetPage = useMemo(() => { + const datasetPage = useMemo>(() => { if (!response.data) { return undefined } @@ -150,11 +151,14 @@ function useAlertsMain() { [enabledColumns] ) - const dataState = useDatasetEmptyState( + const dataState = useDataEmptyState( datasetPage, response.isValidating, response.error, - filters + { + filters, + offset, + } ) const totals = useMemo( diff --git a/apps/renterd/contexts/contracts/dataset.tsx b/apps/renterd/contexts/contracts/dataset.tsx index 51ecb51ef..f66b24a49 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' @@ -70,7 +72,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..80fb4f97b 100644 --- a/apps/renterd/contexts/contracts/index.tsx +++ b/apps/renterd/contexts/contracts/index.tsx @@ -1,7 +1,7 @@ import { useTableState, getContractsTimeRangeBlockHeight, - useDatasetEmptyState, + useDataEmptyState, useClientFilters, useClientFilteredDataset, useMultiSelect, @@ -108,13 +108,6 @@ function useContractsMain() { [enabledColumns] ) - const dataState = useDatasetEmptyState( - datasetFiltered, - response.isValidating, - response.error, - filters - ) - const siascanUrl = useSiascanUrl() const filteredStats = useFilteredStats({ datasetFiltered }) @@ -135,6 +128,16 @@ function useContractsMain() { }) }, [_datasetPage, multiSelect]) + const dataState = useDataEmptyState( + datasetPage, + response.isValidating, + response.error, + { + offset, + filters, + } + ) + const cellContext = useMemo(() => { const context: ContractTableContext = { currentHeight: syncStatus.estimatedBlockHeight, diff --git a/apps/renterd/contexts/filesDirectory/dataset.tsx b/apps/renterd/contexts/filesDirectory/dataset.tsx index f1355702e..c9838b7d0 100644 --- a/apps/renterd/contexts/filesDirectory/dataset.tsx +++ b/apps/renterd/contexts/filesDirectory/dataset.tsx @@ -1,7 +1,7 @@ import { useObjects } from '@siafoundation/renterd-react' import { useDataset as useDatasetGeneric } from '../filesManager/dataset' import { bucketAndKeyParamsFromPath } from '../../lib/paths' -import { useRouter } from 'next/router' +import { useSearchParams } from '@siafoundation/next' import { useMemo } from 'react' import { useFilesManager } from '../filesManager' import { defaultDatasetRefreshInterval } from '../../config/swr' @@ -17,9 +17,9 @@ export function useDataset() { sortDirection, sortField, } = useFilesManager() - const router = useRouter() - const limit = Number(router.query.limit || defaultLimit) - const marker = router.query.marker as string + const searchParams = useSearchParams() + const limit = Number(searchParams.get('limit') || defaultLimit) + const marker = searchParams.get('marker') const pathParams = bucketAndKeyParamsFromPath(activeDirectoryPath) @@ -77,6 +77,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 3401c8877..19165981a 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 { useDataEmptyState, 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() // Add parent directory to the dataset. const _datasetPage = useMemo(() => { @@ -125,11 +123,14 @@ function useFilesDirectoryMain() { }) }, [datasetPageWithOnClick, draggingObjects]) - const dataState = useDatasetEmptyState( - dataset, + const dataState = useDataEmptyState( + datasetPage, response.isValidating, response.error, - filters + { + marker, + filters, + } ) const filteredTableColumns = useMemo( @@ -156,7 +157,7 @@ function useFilesDirectoryMain() { cellContext, refresh, limit, - marker, + nextMarker, isMore, datasetPage, pageCount: dataset?.length || 0, diff --git a/apps/renterd/contexts/filesFlat/dataset.tsx b/apps/renterd/contexts/filesFlat/dataset.tsx index 6f6badf71..5287983a7 100644 --- a/apps/renterd/contexts/filesFlat/dataset.tsx +++ b/apps/renterd/contexts/filesFlat/dataset.tsx @@ -1,7 +1,7 @@ import { useObjects } from '@siafoundation/renterd-react' import { SortField } from '../filesManager/types' import { useDataset as useDatasetGeneric } from '../filesManager/dataset' -import { useRouter } from 'next/router' +import { useSearchParams } from '@siafoundation/next' import { useMemo } from 'react' import { useFilesManager } from '../filesManager' import { defaultDatasetRefreshInterval } from '../../config/swr' @@ -16,9 +16,9 @@ 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 searchParams = useSearchParams() + const limit = Number(searchParams.get('limit') || defaultLimit) + const marker = searchParams.get('marker') const params = useMemo(() => { let prefix = '' @@ -74,6 +74,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..251e3b199 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 { useDataEmptyState, 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,16 @@ function useFilesFlatMain() { }) }, [_datasetPage, multiSelect]) + const dataState = useDataEmptyState( + datasetPage, + response.isValidating, + response.error, + { + marker, + filters, + } + ) + const cellContext = useMemo( () => ({ 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..9f811469e 100644 --- a/apps/renterd/contexts/hosts/index.tsx +++ b/apps/renterd/contexts/hosts/index.tsx @@ -1,7 +1,7 @@ import { triggerToast, truncate, - useDatasetEmptyState, + useDataEmptyState, useMultiSelect, useServerFilters, useTableState, @@ -166,7 +166,10 @@ function useHostsMain() { const isValidating = response.isValidating const error = response.error - const dataState = useDatasetEmptyState(dataset, isValidating, error, filters) + const dataState = useDataEmptyState(dataset, isValidating, error, { + offset, + filters, + }) const hostsWithLocation = useMemo( () => dataset?.filter((h) => h.location) as HostDataWithLocation[], diff --git a/apps/renterd/contexts/keys/index.tsx b/apps/renterd/contexts/keys/index.tsx index 4e0bc55f1..11a2287a2 100644 --- a/apps/renterd/contexts/keys/index.tsx +++ b/apps/renterd/contexts/keys/index.tsx @@ -1,6 +1,6 @@ import { useTableState, - useDatasetEmptyState, + useDataEmptyState, useClientFilters, useClientFilteredDataset, useMultiSelect, @@ -17,6 +17,7 @@ import { import { columns } from './columns' import { defaultDatasetRefreshInterval } from '../../config/swr' import { useSettingsS3 } from '@siafoundation/renterd-react' +import { Maybe } from '@siafoundation/types' const defaultLimit = 50 @@ -32,20 +33,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]) @@ -79,7 +79,7 @@ function useKeysMain() { sortDirection, }) - const _datasetPage = useMemo(() => { + const _datasetPage = useMemo(() => { if (!datasetFiltered) { return undefined } @@ -110,11 +110,14 @@ function useKeysMain() { [enabledColumns] ) - const dataState = useDatasetEmptyState( + const dataState = useDataEmptyState( datasetPage, response.isValidating, response.error, - filters + { + offset, + filters, + } ) const cellContext = useMemo( diff --git a/apps/renterd/contexts/transactions/index.tsx b/apps/renterd/contexts/transactions/index.tsx index a9030b468..683355062 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 { useDataEmptyState, 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,10 @@ function useTransactionsMain() { const isValidating = events.isValidating || pending.isValidating const error = events.error || pending.error - const dataState = useDatasetEmptyState(dataset, isValidating, error, filters) + const dataState = useDataEmptyState(datasetPage, isValidating, error, { + offset, + filters, + }) const siascanUrl = useSiascanUrl() const cellContext = useMemo( @@ -177,12 +182,12 @@ function useTransactionsMain() { return { balances, metrics, - dataset, + datasetPage, error, dataState, offset, limit, - pageCount: dataset?.length || 0, + pageCount: datasetPage?.length || 0, defaultPageSize, cellContext, configurableColumns, diff --git a/apps/renterd/contexts/uploads/index.tsx b/apps/renterd/contexts/uploads/index.tsx index 2edf51281..ea69dbbbc 100644 --- a/apps/renterd/contexts/uploads/index.tsx +++ b/apps/renterd/contexts/uploads/index.tsx @@ -1,6 +1,6 @@ import { useTableState, - useDatasetEmptyState, + useDataEmptyState, useServerFilters, } from '@siafoundation/design-system' import { useSearchParams } from '@siafoundation/next' @@ -15,6 +15,8 @@ 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 } from '@siafoundation/types' const defaultLimit = 50 @@ -69,11 +71,15 @@ function useUploadsMain() { ) }, [response.data, apiBusUploadAbort, activeBucket, uploadsMap]) - const dataset: ObjectUploadData[] = useMemo(() => { - if (!response.data?.uploads || !activeBucket?.name) { - return [] + const dataset = useMemo>(() => { + const uploads = maybeFromNullishArrayResponse( + response.data?.uploads, + response.isValidating + ) + 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 +112,7 @@ function useUploadsMain() { }, } }) - }, [uploadsMap, activeBucket, response.data, apiBusUploadAbort]) + }, [uploadsMap, activeBucket, response, apiBusUploadAbort]) const { configurableColumns, @@ -136,18 +142,21 @@ function useUploadsMain() { [enabledColumns] ) - const dataState = useDatasetEmptyState( + const dataState = useDataEmptyState( dataset, response.isValidating, response.error, - filters + { + marker, + filters, + } ) return { abortAll, dataState, limit, - nextMarker: response.data?.nextUploadIDMarker, + nextMarker: response.data?.nextUploadIDMarker || null, hasMore: !!response.data?.hasMore, isLoading: response.isLoading, error: response.error, diff --git a/apps/walletd/components/Wallet/index.tsx b/apps/walletd/components/Wallet/index.tsx index 37b35e13a..75b41b87b 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' @@ -22,13 +22,12 @@ export function Wallet() { testId="eventsTable" isLoading={dataState === 'loading'} emptyState={ - dataState === 'noneMatchingFilters' ? ( - - ) : dataState === 'noneYet' ? ( - - ) : dataState === 'error' ? ( - - ) : null + } + noneYet={} + error={} + /> } pageSize={6} data={dataset} diff --git a/apps/walletd/components/WalletAddresses/index.tsx b/apps/walletd/components/WalletAddresses/index.tsx index 0bea96982..a8bb18d83 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' @@ -21,13 +21,12 @@ export function WalletAddresses() {
- ) : dataState === 'noneYet' ? ( - - ) : dataState === 'error' ? ( - - ) : null + } + noneYet={} + error={} + /> } pageSize={6} data={dataset} diff --git a/apps/walletd/components/WalletsList/WalletsFiltersBar.tsx b/apps/walletd/components/WalletsList/WalletsFiltersBar.tsx index 121ca2944..cbfcb6a12 100644 --- a/apps/walletd/components/WalletsList/WalletsFiltersBar.tsx +++ b/apps/walletd/components/WalletsList/WalletsFiltersBar.tsx @@ -4,7 +4,7 @@ import { useWallets } from '../../contexts/wallets' import { pluralize } from '@siafoundation/units' export function WalletsFiltersBar() { - const { datasetCount, unlockedCount } = useWallets() + const { pageCount: datasetCount, unlockedCount } = useWallets() return (
diff --git a/apps/walletd/components/WalletsList/index.tsx b/apps/walletd/components/WalletsList/index.tsx index 16b0cb4b7..03defdad1 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,7 +6,7 @@ import { StateError } from './StateError' export function WalletsList() { const { - dataset, + datasetPage, dataState, context, columns, @@ -24,14 +24,14 @@ export function WalletsList() { testId="walletsTable" isLoading={dataState === 'loading'} emptyState={ - dataState === 'noneMatchingFilters' ? ( - - ) : 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..65c087779 100644 --- a/apps/walletd/contexts/addresses/dataset.tsx +++ b/apps/walletd/contexts/addresses/dataset.tsx @@ -1,5 +1,5 @@ import { - useDatasetEmptyState, + useDataEmptyState, ClientFilterItem, } from '@siafoundation/design-system' import { @@ -10,6 +10,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,18 +48,20 @@ 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( + const dataState = useDataEmptyState( dataset, response.isValidating, response.error, - filters + { + filters, + } ) const lastIndex = (dataset || []).reduce( diff --git a/apps/walletd/contexts/events/index.tsx b/apps/walletd/contexts/events/index.tsx index 64d5df722..357cc509f 100644 --- a/apps/walletd/contexts/events/index.tsx +++ b/apps/walletd/contexts/events/index.tsx @@ -1,7 +1,7 @@ import { useTableState, - useDatasetEmptyState, useServerFilters, + useDataEmptyState, } from '@siafoundation/design-system' import { useWalletEvents, @@ -27,6 +27,7 @@ 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 @@ -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,10 @@ export function useEventsMain() { responseEvents.isValidating || responseTxPool.isValidating const error = responseEvents.error || responseTxPool.error - const dataState = useDatasetEmptyState(dataset, isValidating, error, filters) + const dataState = useDataEmptyState(datasetPage, isValidating, error, { + filters, + offset, + }) const siascanUrl = useSiascanUrl() const cellContext = useMemo( @@ -154,9 +167,9 @@ export function useEventsMain() { return { dataState, error: responseEvents.error, - pageCount: dataset?.length || 0, + pageCount: datasetPage?.length || 0, columns: filteredTableColumns, - dataset, + dataset: datasetPage, cellContext, configurableColumns, enabledColumns, diff --git a/apps/walletd/contexts/wallets/index.tsx b/apps/walletd/contexts/wallets/index.tsx index 2c450c906..c67ddf144 100644 --- a/apps/walletd/contexts/wallets/index.tsx +++ b/apps/walletd/contexts/wallets/index.tsx @@ -1,6 +1,6 @@ import { useTableState, - useDatasetEmptyState, + useDataEmptyState, useClientFilters, useClientFilteredDataset, } from '@siafoundation/design-system' @@ -20,6 +20,7 @@ import { useWalletSeedCache } from './useWalletSeedCache' import { useDialog } from '../dialog' import { useAppSettings } from '@siafoundation/react-core' import { defaultDatasetRefreshInterval } from '../../config/swr' +import { Maybe } from '@siafoundation/types' function useWalletsMain() { const response = useWalletsData({ @@ -51,9 +52,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,7 +127,7 @@ function useWalletsMain() { defaultSortField, }) - const datasetFiltered = useClientFilteredDataset({ + const datasetPage = useClientFilteredDataset({ dataset, filters, sortField, @@ -141,11 +142,13 @@ function useWalletsMain() { [enabledColumns] ) - const dataState = useDatasetEmptyState( - dataset, + const dataState = useDataEmptyState( + datasetPage, response.isValidating, response.error, - filters + { + filters, + } ) const context = useMemo( @@ -159,10 +162,10 @@ function useWalletsMain() { return { dataState, error: response.error, - datasetCount: datasetFiltered?.length || 0, + pageCount: datasetPage?.length || 0, unlockedCount: cachedMnemonicCount, columns: filteredTableColumns, - dataset: datasetFiltered, + datasetPage, context, wallet, configurableColumns, diff --git a/libs/design-system/src/app/AlertsDialog/index.tsx b/libs/design-system/src/app/AlertsDialog/index.tsx index 762b45f5c..8ac03c20d 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 { useDataEmptyState } from '../../hooks/useDataEmptyState' import { humanDate } from '@siafoundation/units' import { cx } from 'class-variance-authority' import { times } from '@technically/lodash' @@ -47,7 +47,7 @@ export function AlertsDialog({ dataFieldOrder, dataFields, }: Props) { - const loadingState = useDatasetEmptyState( + const loadingState = useDataEmptyState( alerts.data, alerts.isValidating, alerts.error, 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..7f35eb623 --- /dev/null +++ b/libs/design-system/src/components/EmptyState/StateError.tsx @@ -0,0 +1,20 @@ +import { Text } from '@siafoundation/design-system' +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..80c6aa8b2 --- /dev/null +++ b/libs/design-system/src/components/EmptyState/StateNoData.tsx @@ -0,0 +1,15 @@ +import { Text } from '@siafoundation/design-system' +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..28451c2d2 --- /dev/null +++ b/libs/design-system/src/components/EmptyState/StateNoneMatching.tsx @@ -0,0 +1,15 @@ +import { Text } from '@siafoundation/design-system' +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..bccd71c2e --- /dev/null +++ b/libs/design-system/src/components/EmptyState/StateNoneOnPage.tsx @@ -0,0 +1,28 @@ +import { Button, Text } from '@siafoundation/design-system' +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..14a9970fa --- /dev/null +++ b/libs/design-system/src/components/EmptyState/StateNoneYet.tsx @@ -0,0 +1,15 @@ +import { Text } from '@siafoundation/design-system' +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..047e2d603 --- /dev/null +++ b/libs/design-system/src/components/EmptyState/index.tsx @@ -0,0 +1,30 @@ +import { DataEmptyState } from '@siafoundation/design-system' +import { StateNoneMatching } from './StateNoneMatching' +import { StateNoneYet } from './StateNoneYet' +import { StateError } from './StateError' +import { StateNoneOnPage } from './StateNoneOnPage' +import React from 'react' + +export function EmptyState({ + datasetState, + noneOnPage, + noneMatching, + noneYet, + error, +}: { + datasetState: DataEmptyState + 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/PaginatorMarker.tsx b/libs/design-system/src/components/PaginatorMarker.tsx index 63fb1b6b3..f6995331e 100644 --- a/libs/design-system/src/components/PaginatorMarker.tsx +++ b/libs/design-system/src/components/PaginatorMarker.tsx @@ -7,7 +7,7 @@ import { usePagesRouter } from '@siafoundation/next' import { LoadingDots } from './LoadingDots' type Props = { - marker?: string + nextMarker: string | null isMore: boolean limit: number pageTotal: number @@ -15,7 +15,7 @@ type Props = { } export function PaginatorMarker({ - marker, + nextMarker, isMore, pageTotal, isLoading, @@ -28,7 +28,6 @@ export function PaginatorMarker({ size="small" variant="gray" className="rounded-r-none" - disabled={!marker} onClick={() => router.push({ query: { @@ -61,14 +60,17 @@ export function PaginatorMarker({ size="small" variant="gray" className="rounded-none" - onClick={() => + onClick={() => { + console.log('xxx', { + marker: nextMarker, + }) router.push({ query: { ...router.query, - marker, + marker: nextMarker, }, }) - } + }} > diff --git a/libs/design-system/src/hooks/useClientFilteredDataset.ts b/libs/design-system/src/hooks/useClientFilteredDataset.ts index b69d7c63d..f6a28443f 100644 --- a/libs/design-system/src/hooks/useClientFilteredDataset.ts +++ b/libs/design-system/src/hooks/useClientFilteredDataset.ts @@ -1,6 +1,7 @@ import BigNumber from 'bignumber.js' import { useMemo } from 'react' import { ClientFilterItem } from './useClientFilters' +import { Maybe } from '@siafoundation/types' type DatumValue = | BigNumber @@ -12,7 +13,7 @@ type DatumValue = | object type Props> = { - dataset: Datum[] | undefined + dataset: Maybe filters: ClientFilterItem[] sortField: string sortDirection: 'asc' | 'desc' @@ -21,7 +22,7 @@ type Props> = { export function useClientFilteredDataset< Datum extends Record >({ dataset, filters, sortField, sortDirection }: Props) { - return useMemo(() => { + return useMemo>(() => { if (!dataset) { return undefined } diff --git a/libs/design-system/src/hooks/useDataEmptyState.tsx b/libs/design-system/src/hooks/useDataEmptyState.tsx new file mode 100644 index 000000000..9925013e1 --- /dev/null +++ b/libs/design-system/src/hooks/useDataEmptyState.tsx @@ -0,0 +1,91 @@ +'use client' + +import { useEffect, useMemo, useState } from 'react' + +export type DataEmptyState = + | 'loading' + | 'noneYet' + | 'noneMatchingFilters' + | 'noneOnPage' + | 'error' + | undefined + +type Pagination = { + offset?: number + marker?: string | null + filters?: unknown[] +} + +/** + * Returns the current sate of the dataset. Note that an empty dataset should + * be an empty array. An undefined value represents data that has not finished + * initial fetch or fetch after a key change. + **/ +export function useDataEmptyState( + datasetPage: unknown[] | undefined, + isFetching: boolean, + error: Error | undefined, + pagination?: Pagination +): DataEmptyState { + const { filters } = pagination || {} + const isOnFirstPage = getIsOnFirstPage(pagination || {}) + const [lastDatasetSize, setLastDatasetSize] = useState() + useEffect(() => { + // Update last dataset size every time refetching completes. + if (!isFetching && datasetPage) { + setLastDatasetSize(datasetPage.length) + } + }, [isFetching, datasetPage, setLastDatasetSize]) + + return useMemo(() => { + if (error) { + return 'error' + } + // No previous dataset, initialize in loading state. + if (lastDatasetSize === undefined) { + return 'loading' + } + // Previous dataset not empty and loading. + // If a loading state between dataset is not desired, turn on + // swr keepPreviousData on the dataset. + // Note that dataset will be defined if revalidating same key. + if (lastDatasetSize > 0 && !datasetPage) { + return 'loading' + } + // Previous dataset was empty, show none state until results. + // This sticks to none state even when new data is loading to avoid a + // flickering skeleton loader. + if (lastDatasetSize === 0) { + if (!isOnFirstPage) { + return 'noneOnPage' + } + return !filters || filters.length === 0 + ? 'noneYet' + : 'noneMatchingFilters' + } + return undefined + }, [datasetPage, lastDatasetSize, error, filters, isOnFirstPage]) +} + +function getIsOnFirstPage({ offset, marker }: Pagination): boolean { + // If marker is undefined it is not in use. + if (marker !== undefined) { + // Page marker. + if (marker) { + return false + } + // If marker is null, its the first page. + if (marker === null) { + return true + } + } + // If both marker and offset are undefined, there is no paging. + if (offset === undefined) { + return true + } + // Offset based pagination. + if (offset > 0) { + return false + } + return true +} diff --git a/libs/design-system/src/hooks/useDatasetEmptyState.tsx b/libs/design-system/src/hooks/useDatasetEmptyState.tsx deleted file mode 100644 index ee86e61e8..000000000 --- a/libs/design-system/src/hooks/useDatasetEmptyState.tsx +++ /dev/null @@ -1,49 +0,0 @@ -'use client' - -import { useEffect, useMemo, useState } from 'react' - -type EmptyState = - | 'loading' - | 'noneYet' - | 'noneMatchingFilters' - | 'error' - | undefined - -export function useDatasetEmptyState( - dataset: unknown[] | undefined, - isFetching: boolean, - error: Error | undefined, - filters: unknown[] -): EmptyState { - const [lastDatasetSize, setLastDatasetSize] = useState() - useEffect(() => { - // Update last dataset size every time refetching completes - if (!isFetching && dataset) { - setLastDatasetSize(dataset.length) - } - }, [isFetching, dataset, setLastDatasetSize]) - - return useMemo(() => { - if (error) { - return 'error' - } - // No previous dataset, initialize in loading state. - if (lastDatasetSize === undefined) { - return 'loading' - } - // Previous dataset not empty and loading. - // If a loading state between dataset is not desired, turn on - // swr keepPreviousData on the dataset. - // Note that dataset will be defined if revalidating same key. - if (lastDatasetSize > 0 && !dataset) { - return 'loading' - } - // Previous dataset was empty, show none state until results. - // This sticks to none state even when new data is loading to avoid a - // flickering skeleton loader. - if (lastDatasetSize === 0) { - return filters.length === 0 ? 'noneYet' : 'noneMatchingFilters' - } - return undefined - }, [dataset, lastDatasetSize, error, filters]) -} diff --git a/libs/design-system/src/index.ts b/libs/design-system/src/index.ts index 7e7926b99..7fd5cdcd7 100644 --- a/libs/design-system/src/index.ts +++ b/libs/design-system/src/index.ts @@ -77,6 +77,12 @@ export * from './components/PaginatorUnknownTotal' export * from './components/PaginatorMarker' export * from './components/ListWithSeparators' export * from './components/ClientSideOnly' +export * from './components/EmptyState' +export * from './components/EmptyState/StateNoneOnPage' +export * from './components/EmptyState/StateNoneYet' +export * from './components/EmptyState/StateNoData' +export * from './components/EmptyState/StateNoneMatching' +export * from './components/EmptyState/StateError' // app export * from './app/AppPublicLayout' @@ -163,7 +169,7 @@ export * from './hooks/useClientFilters' export * from './hooks/useClientFilteredDataset' export * from './hooks/useServerFilters' export * from './hooks/useFormChanged' -export * from './hooks/useDatasetEmptyState' +export * from './hooks/useDataEmptyState' export * from './hooks/useSiacoinFiat' export * from './hooks/useOS' diff --git a/libs/react-core/src/arrayResponse.tsx b/libs/react-core/src/arrayResponse.tsx new file mode 100644 index 000000000..39bb8f49d --- /dev/null +++ b/libs/react-core/src/arrayResponse.tsx @@ -0,0 +1,20 @@ +import { Maybe, Nullish } from '@siafoundation/types' + +/** + * Ensure the data is an empty array if the response returns null. + * This makes responses consistent and allows other methods to distinguish + * between a loading state and an empty state. + * @param data Nullish + * @returns Maybe + */ +export function maybeFromNullishArrayResponse( + data: Nullish +): Maybe { + if (data) { + return data + } + if (data === null) { + return [] + } + return undefined +} diff --git a/libs/react-core/src/index.ts b/libs/react-core/src/index.ts index abae5f095..aa2e9b4a2 100644 --- a/libs/react-core/src/index.ts +++ b/libs/react-core/src/index.ts @@ -13,6 +13,7 @@ export * from './useExchangeRate' export * from './useTryUntil' export * from './userPrefersReducedMotion' export * from './mutate' +export * from './arrayResponse' export * from './workflows' export * from './coreProvider' diff --git a/libs/renterd-types/src/bus.ts b/libs/renterd-types/src/bus.ts index 9614b7578..c9c5b6a96 100644 --- a/libs/renterd-types/src/bus.ts +++ b/libs/renterd-types/src/bus.ts @@ -9,7 +9,7 @@ import { Transaction, TransactionID, WalletEvent, - Maybe, + Nullable, } from '@siafoundation/types' import { ConsensusState, @@ -232,7 +232,7 @@ export type HostsPayload = { limit?: number maxLastScan?: string } -export type HostsResponse = Maybe +export type HostsResponse = Nullable export type HostParams = { hostkey: string } export type HostPayload = Host @@ -291,7 +291,7 @@ export type HostScanResponse = { export type ContractsParams = void export type ContractsPayload = void -export type ContractsResponse = Maybe +export type ContractsResponse = Nullable export type ContractAcquireParams = { id: string diff --git a/libs/types/src/utils.ts b/libs/types/src/utils.ts index 4ada1062b..44d529936 100644 --- a/libs/types/src/utils.ts +++ b/libs/types/src/utils.ts @@ -1,4 +1,6 @@ export type Maybe = T | undefined +export type Nullable = T | null +export type Nullish = T | null | undefined export type NoUndefined = { [K in keyof T]: Exclude