}
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.
+
+ Back to first page
+
+ )
+}
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({
- {datasetTotal > 0 ? (
- `${offset + 1} - ${Math.min(offset + limit, datasetTotal)} of ${
- datasetTotal ? datasetTotal.toLocaleString() : ''
+ {total > 0 ? (
+ `${offset + 1} - ${Math.min(offset + limit, total)} of ${
+ total ? total.toLocaleString() : ''
}`
) : isLoading ? (
@@ -77,7 +76,7 @@ export function PaginatorKnownTotal({
= datasetTotal}
+ disabled={offset + limit >= total}
size="small"
variant="gray"
className="rounded-none"
@@ -85,7 +84,7 @@ export function PaginatorKnownTotal({
router.push({
query: {
...router.query,
- offset: Math.min(offset + limit, datasetTotal),
+ offset: Math.min(offset + limit, total),
},
})
}
@@ -94,7 +93,7 @@ export function PaginatorKnownTotal({
= datasetTotal}
+ disabled={offset + limit >= total}
size="small"
variant="gray"
className="rounded-l-none"
@@ -102,7 +101,7 @@ export function PaginatorKnownTotal({
router.push({
query: {
...router.query,
- offset: Math.floor(datasetTotal / limit) * limit,
+ offset: Math.floor(total / limit) * limit,
},
})
}
diff --git a/libs/design-system/src/components/PaginatorMarker.tsx b/libs/design-system/src/components/PaginatorMarker.tsx
index 63fb1b6b3..8f256cd97 100644
--- a/libs/design-system/src/components/PaginatorMarker.tsx
+++ b/libs/design-system/src/components/PaginatorMarker.tsx
@@ -7,7 +7,8 @@ import { usePagesRouter } from '@siafoundation/next'
import { LoadingDots } from './LoadingDots'
type Props = {
- marker?: string
+ marker?: string | null
+ nextMarker: string | null
isMore: boolean
limit: number
pageTotal: number
@@ -16,19 +17,23 @@ type Props = {
export function PaginatorMarker({
marker,
+ nextMarker,
isMore,
pageTotal,
isLoading,
}: Props) {
const router = usePagesRouter()
+ // If no marker is provided, we do not know if we are on the first or any other page
+ // so leave the control enabled. If the marker is null, we are on the first page.
+ const previousDisabled = marker === undefined ? false : marker === null
return (
router.push({
query: {
@@ -48,7 +53,7 @@ export function PaginatorMarker({
) : pageTotal ? (
- {pageTotal}
+ {pageTotal} on page
) : (
@@ -65,7 +70,7 @@ export function PaginatorMarker({
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..4b6a5026f 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,16 +13,18 @@ type DatumValue =
| object
type Props> = {
- dataset: Datum[] | undefined
+ dataset: Maybe
filters: ClientFilterItem[]
sortField: string
sortDirection: 'asc' | 'desc'
+ limit: number
+ offset: number
}
export function useClientFilteredDataset<
Datum extends Record
->({ dataset, filters, sortField, sortDirection }: Props) {
- return useMemo(() => {
+>({ dataset, filters, sortField, sortDirection, limit, offset }: Props) {
+ const datasetFiltered = useMemo>(() => {
if (!dataset) {
return undefined
}
@@ -64,4 +67,16 @@ export function useClientFilteredDataset<
})
return [...data]
}, [dataset, filters, sortField, sortDirection])
+
+ const datasetPage = useMemo(() => {
+ if (!datasetFiltered) {
+ return undefined
+ }
+ return datasetFiltered.slice(offset, offset + limit)
+ }, [datasetFiltered, offset, limit])
+
+ return {
+ datasetFiltered,
+ datasetPage,
+ }
}
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/hooks/useDatasetState.tsx b/libs/design-system/src/hooks/useDatasetState.tsx
new file mode 100644
index 000000000..c41c2e235
--- /dev/null
+++ b/libs/design-system/src/hooks/useDatasetState.tsx
@@ -0,0 +1,99 @@
+'use client'
+
+import { useEffect, useMemo, useState } from 'react'
+
+export type DatasetState =
+ | 'loading'
+ | 'noneYet'
+ | 'noneMatchingFilters'
+ | 'noneOnPage'
+ | 'error'
+ | 'loaded'
+
+/**
+ * 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 useDatasetState({
+ datasetPage,
+ isValidating,
+ error,
+ filters,
+ offset,
+ marker,
+}: {
+ datasetPage: unknown[] | undefined
+ isValidating: boolean
+ error: Error | undefined
+ offset?: number
+ marker?: string | null
+ filters?: unknown[]
+}): DatasetState {
+ const isOnFirstPage = getIsOnFirstPage({ offset, marker })
+ const [lastDatasetSize, setLastDatasetSize] = useState()
+ useEffect(() => {
+ // Update last dataset size every time refetching completes.
+ if (!isValidating && datasetPage) {
+ setLastDatasetSize(datasetPage.length)
+ }
+ }, [isValidating, 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 'loaded'
+ }, [datasetPage, lastDatasetSize, error, filters, isOnFirstPage])
+}
+
+function getIsOnFirstPage({
+ offset,
+ marker,
+}: {
+ offset?: number
+ marker?: string | null
+}): 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/usePaginationMarker.ts b/libs/design-system/src/hooks/usePaginationMarker.ts
new file mode 100644
index 000000000..454e534f9
--- /dev/null
+++ b/libs/design-system/src/hooks/usePaginationMarker.ts
@@ -0,0 +1,10 @@
+'use client'
+
+import { useSearchParams } from '@siafoundation/next'
+
+export function usePaginationMarker(defaultLimit: number) {
+ const searchParams = useSearchParams()
+ const limit = Number(searchParams.get('limit') || defaultLimit)
+ const marker = searchParams.get('marker') || null
+ return { limit, marker }
+}
diff --git a/libs/design-system/src/hooks/usePaginationOffset.ts b/libs/design-system/src/hooks/usePaginationOffset.ts
new file mode 100644
index 000000000..2c14c2c71
--- /dev/null
+++ b/libs/design-system/src/hooks/usePaginationOffset.ts
@@ -0,0 +1,10 @@
+'use client'
+
+import { useSearchParams } from '@siafoundation/next'
+
+export function usePaginationOffset(defaultLimit: number) {
+ const searchParams = useSearchParams()
+ const limit = Number(searchParams.get('limit') || defaultLimit)
+ const offset = Number(searchParams.get('offset') || 0)
+ return { limit, offset }
+}
diff --git a/libs/design-system/src/index.ts b/libs/design-system/src/index.ts
index 7e7926b99..21ef7c2bf 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,9 +169,11 @@ export * from './hooks/useClientFilters'
export * from './hooks/useClientFilteredDataset'
export * from './hooks/useServerFilters'
export * from './hooks/useFormChanged'
-export * from './hooks/useDatasetEmptyState'
+export * from './hooks/useDatasetState'
export * from './hooks/useSiacoinFiat'
export * from './hooks/useOS'
+export * from './hooks/usePaginationOffset'
+export * from './hooks/usePaginationMarker'
// multi
export * from './multi/useMultiSelect'
diff --git a/libs/react-core/package.json b/libs/react-core/package.json
index 42e57367e..c741a0089 100644
--- a/libs/react-core/package.json
+++ b/libs/react-core/package.json
@@ -4,7 +4,8 @@
"version": "1.5.0",
"license": "MIT",
"peerDependencies": {
- "react": "^18.2.0"
+ "react": "^18.2.0",
+ "@siafoundation/types": "0.7.0"
},
"dependencies": {
"@siafoundation/next": "^0.1.3",
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 74d9eb644..c4b6daece 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