From a2a53d60f3b97a5757391ec3b6b80ae913647971 Mon Sep 17 00:00:00 2001 From: Alex Freska Date: Mon, 16 Oct 2023 15:04:17 -0400 Subject: [PATCH 1/3] feat: explorer outputs address --- .changeset/quiet-ways-pay.md | 5 ++ .../components/Transaction/OutputListItem.tsx | 52 +++++++++++++++++++ .../explorer/components/Transaction/index.tsx | 42 ++++++++++++--- 3 files changed, 91 insertions(+), 8 deletions(-) create mode 100644 .changeset/quiet-ways-pay.md create mode 100644 apps/explorer/components/Transaction/OutputListItem.tsx diff --git a/.changeset/quiet-ways-pay.md b/.changeset/quiet-ways-pay.md new file mode 100644 index 000000000..5f4982981 --- /dev/null +++ b/.changeset/quiet-ways-pay.md @@ -0,0 +1,5 @@ +--- +'explorer': minor +--- + +Outputs on the transaction view now display and link to their associated address. diff --git a/apps/explorer/components/Transaction/OutputListItem.tsx b/apps/explorer/components/Transaction/OutputListItem.tsx new file mode 100644 index 000000000..1dbae42ed --- /dev/null +++ b/apps/explorer/components/Transaction/OutputListItem.tsx @@ -0,0 +1,52 @@ +'use client' + +import { + EntityListItemLayout, + Link, + ValueCopyable, + ValueSc, + ValueSf, +} from '@siafoundation/design-system' +import BigNumber from 'bignumber.js' + +type Props = { + label: string + address: string + addressHref: string + outputId: string + sc?: BigNumber + sf?: number +} + +export function OutputListItem(props: Props) { + const { label, outputId, address, addressHref, sc, sf } = props + return ( + +
+
+ + {label} + +
+ {sc && } + {sf && } +
+
+ + +
+
+ + ) +} diff --git a/apps/explorer/components/Transaction/index.tsx b/apps/explorer/components/Transaction/index.tsx index cd6e67dd3..bd10b8536 100644 --- a/apps/explorer/components/Transaction/index.tsx +++ b/apps/explorer/components/Transaction/index.tsx @@ -7,33 +7,47 @@ import { EntityList, EntityListItemProps } from '@siafoundation/design-system' import { routes } from '../../config/routes' import { ContentLayout } from '../ContentLayout' import { TransactionHeader } from './TransactionHeader' +import { OutputListItem } from './OutputListItem' type Props = { transaction: SiaCentralTransaction title?: string } +type OutputItem = { + label: string + addressHref: string + address: string + sc?: BigNumber + sf?: number + outputId: string +} + export function Transaction({ title, transaction }: Props) { const inputs = useMemo(() => { if (!transaction) { return [] } - const list: EntityListItemProps[] = [] + const list: OutputItem[] = [] transaction.siacoin_inputs?.forEach((o) => { list.push({ label: o.source === 'transaction' ? 'siacoin output' : o.source.replace(/_/g, ' '), + addressHref: routes.address.view.replace(':id', o.unlock_hash), + address: o.unlock_hash, sc: new BigNumber(o.value), - hash: o.output_id, + outputId: o.output_id, }) }) transaction.siafund_inputs?.forEach((o) => { list.push({ label: 'siafund output', + addressHref: routes.address.view.replace(':id', o.unlock_hash), + address: o.unlock_hash, sc: new BigNumber(o.value), - hash: o.output_id, + outputId: o.output_id, }) }) return list @@ -43,22 +57,26 @@ export function Transaction({ title, transaction }: Props) { if (!transaction) { return [] } - const list: EntityListItemProps[] = [] + const list: OutputItem[] = [] transaction.siacoin_outputs?.forEach((o) => { list.push({ label: o.source === 'transaction' ? 'siacoin output' : o.source.replace(/_/g, ' '), + addressHref: routes.address.view.replace(':id', o.unlock_hash), + address: o.unlock_hash, sc: new BigNumber(o.value), - hash: o.output_id, + outputId: o.output_id, }) }) transaction.siafund_outputs?.forEach((o) => { list.push({ label: 'siafund output', + addressHref: routes.address.view.replace(':id', o.unlock_hash), + address: o.unlock_hash, sf: Number(o.value), - hash: o.output_id, + outputId: o.output_id, }) }) return list @@ -112,13 +130,21 @@ export function Transaction({ title, transaction }: Props) {
- + + {inputs?.map((i) => ( + + ))} +
+ > + {outputs?.map((o) => ( + + ))} +
{!!operations?.length && ( From ac8d3c782d701baac2b7b1092da933fe386075f2 Mon Sep 17 00:00:00 2001 From: Alex Freska Date: Tue, 17 Oct 2023 09:20:47 -0400 Subject: [PATCH 2/3] feat: data revalidation refresh intervals --- .changeset/metal-crews-unite.md | 6 +++++ apps/hostd/contexts/config/index.tsx | 6 +++++ apps/hostd/contexts/contracts/index.tsx | 22 +++++++--------- apps/hostd/contexts/metrics/index.tsx | 2 ++ apps/hostd/contexts/volumes/index.tsx | 21 ++++++++++++++-- apps/renterd/contexts/app/useAutopilot.tsx | 6 +++-- apps/renterd/contexts/config/index.tsx | 19 ++++++++++++++ apps/renterd/contexts/contracts/index.tsx | 29 +++++++++------------- apps/renterd/contexts/files/dataset.tsx | 6 +++++ apps/renterd/contexts/hosts/index.tsx | 7 ++++++ apps/renterd/hooks/useSyncStatus.ts | 8 ++++-- 11 files changed, 96 insertions(+), 36 deletions(-) create mode 100644 .changeset/metal-crews-unite.md diff --git a/.changeset/metal-crews-unite.md b/.changeset/metal-crews-unite.md new file mode 100644 index 000000000..c78cff711 --- /dev/null +++ b/.changeset/metal-crews-unite.md @@ -0,0 +1,6 @@ +--- +'hostd': minor +'renterd': minor +--- + +Data tables now refresh themselves without user interaction or refocusing the app, even more frequently if a long running operation is in progress. diff --git a/apps/hostd/contexts/config/index.tsx b/apps/hostd/contexts/config/index.tsx index 984769e0f..98d477a91 100644 --- a/apps/hostd/contexts/config/index.tsx +++ b/apps/hostd/contexts/config/index.tsx @@ -3,6 +3,7 @@ import { triggerSuccessToast, triggerErrorToast, useOnInvalid, + minutesInMilliseconds, } from '@siafoundation/design-system' import { useCallback, useEffect, useMemo, useState } from 'react' import { @@ -20,6 +21,11 @@ import useLocalStorageState from 'use-local-storage-state' export function useConfigMain() { const settings = useSettings({ standalone: 'configSettingsForm', + config: { + swr: { + refreshInterval: minutesInMilliseconds(1), + }, + }, }) const settingsUpdate = useSettingsUpdate() const dynDNSCheck = useSettingsDdns({ diff --git a/apps/hostd/contexts/contracts/index.tsx b/apps/hostd/contexts/contracts/index.tsx index d5eb9aaac..4404ec7b6 100644 --- a/apps/hostd/contexts/contracts/index.tsx +++ b/apps/hostd/contexts/contracts/index.tsx @@ -3,13 +3,12 @@ import { useDatasetEmptyState, useServerFilters, getContractsTimeRangeBlockHeight, + secondsInMilliseconds, } from '@siafoundation/design-system' import { useRouter } from 'next/router' import { ContractStatus, useContracts as useContractsData, - useEstimatedNetworkBlockHeight, - useStateConsensus, } from '@siafoundation/react-hostd' import { createContext, useContext, useMemo } from 'react' import { @@ -21,6 +20,7 @@ import { } from './types' import { columns } from './columns' import { useDataset } from './dataset' +import { useSyncStatus } from '../../hooks/useSyncStatus' const defaultLimit = 50 @@ -64,6 +64,11 @@ function useContractsMain() { .filter((f) => f.id.startsWith('filterStatus')) .map((f) => f.value) as ContractStatus[], }, + config: { + swr: { + refreshInterval: secondsInMilliseconds(60), + }, + }, }) const dataset = useDataset({ @@ -79,17 +84,8 @@ function useContractsMain() { const error = response.error const dataState = useDatasetEmptyState(dataset, isValidating, error, filters) - const estimatedNetworkHeight = useEstimatedNetworkBlockHeight() - const state = useStateConsensus({ - config: { - swr: { - refreshInterval: 60_000, - }, - }, - }) - const currentHeight = state.data?.synced - ? state.data.chainIndex.height - : estimatedNetworkHeight + const { estimatedBlockHeight, isSynced, nodeBlockHeight } = useSyncStatus() + const currentHeight = isSynced ? nodeBlockHeight : estimatedBlockHeight const { range: contractsTimeRange } = useMemo( () => getContractsTimeRangeBlockHeight(currentHeight, dataset || []), diff --git a/apps/hostd/contexts/metrics/index.tsx b/apps/hostd/contexts/metrics/index.tsx index 368b88e33..259abf549 100644 --- a/apps/hostd/contexts/metrics/index.tsx +++ b/apps/hostd/contexts/metrics/index.tsx @@ -6,6 +6,7 @@ import { getDataIntervalLabelFormatter, getTimeRange, MiBToBytes, + minutesInMilliseconds, } from '@siafoundation/design-system' import { humanBytes, humanNumber, humanSiacoin } from '@siafoundation/sia-js' import { useCallback, useMemo } from 'react' @@ -108,6 +109,7 @@ function useMetricsMain() { config: { swr: { revalidateOnFocus: false, + refreshInterval: minutesInMilliseconds(5), }, }, }) diff --git a/apps/hostd/contexts/volumes/index.tsx b/apps/hostd/contexts/volumes/index.tsx index f9bcfa8ef..017212b82 100644 --- a/apps/hostd/contexts/volumes/index.tsx +++ b/apps/hostd/contexts/volumes/index.tsx @@ -1,8 +1,12 @@ import { useTableState, useDatasetEmptyState, + secondsInMilliseconds, } from '@siafoundation/design-system' -import { useVolumes as useVolumesData } from '@siafoundation/react-hostd' +import { + VolumeMeta, + useVolumes as useVolumesData, +} from '@siafoundation/react-hostd' import { createContext, useContext, useMemo } from 'react' import { columnsDefaultVisible, TableColumnId } from './types' import { columns } from './columns' @@ -26,7 +30,16 @@ function useVolumesMain() { columnsDefaultVisible, }) - const response = useVolumesData() + const response = useVolumesData({ + config: { + swr: { + refreshInterval: (data) => + data.find((v) => isOperationInProgress(v)) + ? secondsInMilliseconds(5) + : secondsInMilliseconds(60), + }, + }, + }) const dataset = useDataset({ response, @@ -76,3 +89,7 @@ export function VolumesProvider({ children }: Props) { {children} ) } + +function isOperationInProgress(volume: VolumeMeta) { + return !['ready', 'unavailable'].includes(volume.status) +} diff --git a/apps/renterd/contexts/app/useAutopilot.tsx b/apps/renterd/contexts/app/useAutopilot.tsx index b2cbc97b2..29d4183d6 100644 --- a/apps/renterd/contexts/app/useAutopilot.tsx +++ b/apps/renterd/contexts/app/useAutopilot.tsx @@ -1,3 +1,4 @@ +import { secondsInMilliseconds } from '@siafoundation/design-system' import { useAutopilotState } from '@siafoundation/react-renterd' import { useEffect, useState } from 'react' @@ -5,9 +6,10 @@ export function useAutopilot() { const state = useAutopilotState({ config: { swr: { - dedupingInterval: 5_000, + dedupingInterval: secondsInMilliseconds(5), revalidateOnFocus: false, - refreshInterval: (data) => (!data ? 1_000 : 60_000), + refreshInterval: (data) => + !data ? secondsInMilliseconds(1) : secondsInMilliseconds(60), }, }, }) diff --git a/apps/renterd/contexts/config/index.tsx b/apps/renterd/contexts/config/index.tsx index 54018e641..0a202a666 100644 --- a/apps/renterd/contexts/config/index.tsx +++ b/apps/renterd/contexts/config/index.tsx @@ -5,6 +5,7 @@ import { useOnInvalid, monthsToBlocks, TBToBytes, + minutesInMilliseconds, } from '@siafoundation/design-system' import BigNumber from 'bignumber.js' import { useCallback, useEffect, useMemo, useState } from 'react' @@ -59,6 +60,7 @@ export function useConfigMain() { config: { swr: { errorRetryCount: 0, + refreshInterval: minutesInMilliseconds(1), }, }, }) @@ -67,6 +69,7 @@ export function useConfigMain() { config: { swr: { errorRetryCount: 0, + refreshInterval: minutesInMilliseconds(1), }, }, }) @@ -75,18 +78,34 @@ export function useConfigMain() { config: { swr: { errorRetryCount: 0, + refreshInterval: minutesInMilliseconds(1), }, }, }) // settings with initial defaults const gouging = useGougingSettings({ standalone: 'configFormGouging', + config: { + swr: { + refreshInterval: minutesInMilliseconds(1), + }, + }, }) const redundancy = useRedundancySettings({ standalone: 'configFormRedundancy', + config: { + swr: { + refreshInterval: minutesInMilliseconds(1), + }, + }, }) const uploadPacking = useUploadPackingSettings({ standalone: 'configFormUploadPacking', + config: { + swr: { + refreshInterval: minutesInMilliseconds(1), + }, + }, }) const settingUpdate = useSettingUpdate() diff --git a/apps/renterd/contexts/contracts/index.tsx b/apps/renterd/contexts/contracts/index.tsx index 1c98b6426..dc96205a8 100644 --- a/apps/renterd/contexts/contracts/index.tsx +++ b/apps/renterd/contexts/contracts/index.tsx @@ -5,13 +5,10 @@ import { useDatasetEmptyState, useClientFilters, useClientFilteredDataset, + minutesInMilliseconds, } from '@siafoundation/design-system' import { useRouter } from 'next/router' -import { - useConsensusState, - useContracts as useContractsData, - useEstimatedNetworkBlockHeight, -} from '@siafoundation/react-renterd' +import { useContracts as useContractsData } from '@siafoundation/react-renterd' import { createContext, useContext, useMemo } from 'react' import BigNumber from 'bignumber.js' import { @@ -30,21 +27,20 @@ function useContractsMain() { const router = useRouter() const limit = Number(router.query.limit || defaultLimit) const offset = Number(router.query.offset || 0) - const response = useContractsData() - const geo = useSiaCentralHosts() - const geoHosts = useMemo(() => geo.data?.hosts || [], [geo.data]) - - const estimatedNetworkHeight = useEstimatedNetworkBlockHeight() - const network = useConsensusState({ + const response = useContractsData({ config: { swr: { - refreshInterval: 60_000, + refreshInterval: minutesInMilliseconds(1), }, }, }) - const currentHeight = network.data?.synced - ? network.data.blockHeight - : estimatedNetworkHeight + const geo = useSiaCentralHosts() + const geoHosts = useMemo(() => geo.data?.hosts || [], [geo.data]) + + const syncStatus = useSyncStatus() + const currentHeight = syncStatus.isSynced + ? syncStatus.nodeBlockHeight + : syncStatus.estimatedBlockHeight const dataset = useMemo(() => { if (!response.data) { @@ -142,7 +138,6 @@ function useContractsMain() { filters ) - const syncStatus = useSyncStatus() const datasetConfirmedCount = useMemo(() => { if (!dataset) { return 0 @@ -165,7 +160,7 @@ function useContractsMain() { dataset, datasetPage, cellContext: { - currentHeight: estimatedNetworkHeight, + currentHeight: syncStatus.estimatedBlockHeight, contractsTimeRange, }, configurableColumns, diff --git a/apps/renterd/contexts/files/dataset.tsx b/apps/renterd/contexts/files/dataset.tsx index 88953e144..162e18cc9 100644 --- a/apps/renterd/contexts/files/dataset.tsx +++ b/apps/renterd/contexts/files/dataset.tsx @@ -12,6 +12,7 @@ import { getFilePath, isDirectory, } from './paths' +import { minutesInMilliseconds } from '@siafoundation/design-system' type Props = { activeDirectoryPath: string @@ -25,6 +26,11 @@ export function useDataset({ activeDirectoryPath, uploadsList }: Props) { const response = useObjectDirectory({ disabled: !bucket, params: bucketAndKeyParamsFromPath(activeDirectoryPath), + config: { + swr: { + refreshInterval: minutesInMilliseconds(1), + }, + }, }) const { dataset: allContracts } = useContracts() diff --git a/apps/renterd/contexts/hosts/index.tsx b/apps/renterd/contexts/hosts/index.tsx index 724bd3de9..bfc6116f7 100644 --- a/apps/renterd/contexts/hosts/index.tsx +++ b/apps/renterd/contexts/hosts/index.tsx @@ -4,6 +4,7 @@ import { useServerFilters, triggerErrorToast, truncate, + minutesInMilliseconds, } from '@siafoundation/design-system' import { HostsSearchFilterMode, @@ -81,6 +82,7 @@ function useHostsMain() { swr: { // before autopilot is configured this will repeatedly 500 errorRetryInterval: 20_000, + refreshInterval: minutesInMilliseconds(1), }, }, }) @@ -98,6 +100,11 @@ function useHostsMain() { ? allContracts.map((c) => c.hostKey) : undefined, }, + config: { + swr: { + refreshInterval: minutesInMilliseconds(1), + }, + }, }) const allowlist = useHostsAllowlist() diff --git a/apps/renterd/hooks/useSyncStatus.ts b/apps/renterd/hooks/useSyncStatus.ts index 4ded740fc..ffe9d42e8 100644 --- a/apps/renterd/hooks/useSyncStatus.ts +++ b/apps/renterd/hooks/useSyncStatus.ts @@ -1,3 +1,4 @@ +import { secondsInMilliseconds } from '@siafoundation/design-system' import { useAppSettings } from '@siafoundation/react-core' import { useConsensusState, @@ -10,7 +11,8 @@ export function useSyncStatus() { const state = useConsensusState({ config: { swr: { - refreshInterval: (data) => (data?.synced ? 60_000 : 10_000), + refreshInterval: (data) => + data?.synced ? secondsInMilliseconds(60) : secondsInMilliseconds(10), }, }, }) @@ -20,7 +22,9 @@ export function useSyncStatus() { config: { swr: { refreshInterval: (data) => - data?.scanHeight >= nodeBlockHeight ? 60_000 : 10_000, + data?.scanHeight >= nodeBlockHeight + ? secondsInMilliseconds(60) + : secondsInMilliseconds(10), }, }, }) From a7a8f62ec0235275ad4127f2311271c84e663781 Mon Sep 17 00:00:00 2001 From: Alex Freska Date: Tue, 17 Oct 2023 09:33:18 -0400 Subject: [PATCH 3/3] fix: files encode uri --- apps/renterd/contexts/files/index.tsx | 3 ++- .../src/components/LoadingDots.tsx | 21 +++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/apps/renterd/contexts/files/index.tsx b/apps/renterd/contexts/files/index.tsx index 797139a5a..5906096a7 100644 --- a/apps/renterd/contexts/files/index.tsx +++ b/apps/renterd/contexts/files/index.tsx @@ -25,7 +25,8 @@ function useFilesMain() { // [bucket, key, directory] const activeDirectory = useMemo( - () => (router.query.path as FullPathSegments).map(decodeURIComponent) || [], + () => + ((router.query.path || []) as FullPathSegments).map(decodeURIComponent), [router.query.path] ) diff --git a/libs/design-system/src/components/LoadingDots.tsx b/libs/design-system/src/components/LoadingDots.tsx index 68a27031d..ef28b5946 100644 --- a/libs/design-system/src/components/LoadingDots.tsx +++ b/libs/design-system/src/components/LoadingDots.tsx @@ -1,14 +1,17 @@ import { cx } from 'class-variance-authority' +import { forwardRef } from 'react' -export function LoadingDots({ className }: { className?: string }) { - return ( -
- - - -
- ) -} +export const LoadingDots = forwardRef( + ({ className }, ref) => { + return ( +
+ + + +
+ ) + } +) function Dot() { return (