diff --git a/.changeset/hot-pots-prove.md b/.changeset/hot-pots-prove.md
new file mode 100644
index 000000000..c030eec51
--- /dev/null
+++ b/.changeset/hot-pots-prove.md
@@ -0,0 +1,5 @@
+---
+'hostd': minor
+---
+
+Metrics intervals for 1Y and ALL are now weekly and monthly.
diff --git a/.changeset/lovely-moose-fix.md b/.changeset/lovely-moose-fix.md
new file mode 100644
index 000000000..2cdcbbe3d
--- /dev/null
+++ b/.changeset/lovely-moose-fix.md
@@ -0,0 +1,5 @@
+---
+'@siafoundation/design-system': minor
+---
+
+Refactor hooks used in server synced configuration features.
diff --git a/apps/hostd/components/Config/StateConnError.tsx b/apps/hostd/components/Config/StateConnError.tsx
new file mode 100644
index 000000000..77890740c
--- /dev/null
+++ b/apps/hostd/components/Config/StateConnError.tsx
@@ -0,0 +1,21 @@
+import { Button, Text } from '@siafoundation/design-system'
+import { Warning32 } from '@siafoundation/react-icons'
+import { useConfig } from '../../contexts/config'
+
+export function StateConnError() {
+ const { revalidateAndResetForm } = useConfig()
+ return (
+
+
+
+
+
+
+ Error retrieving settings from daemon. Please check your connection
+ and try again.
+
+
+
+
+ )
+}
diff --git a/apps/hostd/components/Config/index.tsx b/apps/hostd/components/Config/index.tsx
index f2f8b20e6..a33b3133d 100644
--- a/apps/hostd/components/Config/index.tsx
+++ b/apps/hostd/components/Config/index.tsx
@@ -12,6 +12,7 @@ import { HostdAuthedLayout } from '../../components/HostdAuthedLayout'
import { AnnounceButton } from './AnnounceButton'
import { useConfig } from '../../contexts/config'
import { ConfigNav } from './ConfigNav'
+import { StateConnError } from './StateConnError'
export function Config() {
const { openDialog } = useDialog()
@@ -20,9 +21,10 @@ export function Config() {
settings,
dynDNSCheck,
changeCount,
- revalidateAndResetFormData,
+ revalidateAndResetForm,
form,
onSubmit,
+ remoteError,
} = useConfig()
return (
@@ -81,44 +83,48 @@ export function Config() {
}
openSettings={() => openDialog('settings')}
>
-
-
-
-
-
-
-
-
+ {remoteError ? (
+
+ ) : (
+
+
+
+
+
+
+
+
+ )}
)
}
diff --git a/apps/hostd/contexts/config/index.tsx b/apps/hostd/contexts/config/index.tsx
index 16e302ed7..7d4db3b18 100644
--- a/apps/hostd/contexts/config/index.tsx
+++ b/apps/hostd/contexts/config/index.tsx
@@ -1,153 +1,82 @@
import { createContext, useContext } from 'react'
import {
- triggerSuccessToast,
triggerErrorToast,
useOnInvalid,
- minutesInMilliseconds,
+ useFormInit,
+ useFormServerSynced,
+ useFormChangeCount,
} from '@siafoundation/design-system'
-import { useCallback, useEffect, useMemo, useState } from 'react'
+import { useCallback, useMemo } from 'react'
+import { transformDown } from './transform'
+import { useResources } from './useResources'
+import { useForm } from './useForm'
import {
- HostSettings,
- useSettings,
- useSettingsDdns,
- useSettingsUpdate,
-} from '@siafoundation/react-hostd'
-import { SettingsData, initialValues } from './types'
-import { getFields } from './fields'
-import { calculateMaxCollateral, transformDown, transformUp } from './transform'
-import { useForm } from 'react-hook-form'
-import useLocalStorageState from 'use-local-storage-state'
-import { useAppSettings } from '@siafoundation/react-core'
-import { useSiaCentralExchangeRates } from '@siafoundation/react-sia-central'
+ checkIfAllResourcesLoaded,
+ checkIfAnyResourcesErrored,
+} from './resources'
+import { useOnValid } from './useOnValid'
export function useConfigMain() {
- const settings = useSettings({
- standalone: 'configSettingsForm',
- config: {
- swr: {
- refreshInterval: minutesInMilliseconds(1),
- },
- },
- })
- const settingsUpdate = useSettingsUpdate()
- const dynDNSCheck = useSettingsDdns({
- disabled: !settings.data || !settings.data.ddns.provider,
- config: {
- swr: {
- revalidateOnFocus: false,
- errorRetryCount: 0,
- },
- },
- })
- const [showAdvanced, setShowAdvanced] = useLocalStorageState(
- 'v0/config/showAdvanced',
- {
- defaultValue: false,
- }
- )
+ const { settings, dynDNSCheck } = useResources()
- const form = useForm({
- mode: 'all',
- defaultValues: initialValues,
- })
- const storageTBMonth = form.watch('storagePrice')
- const collateralMultiplier = form.watch('collateralMultiplier')
+ const { form, fields, setShowAdvanced, showAdvanced } = useForm()
- const resetFormData = useCallback(
- (data: HostSettings) => {
- const settingsData = transformDown(data)
- form.reset(settingsData)
- return settingsData
- },
- [form]
+ // resources required to intialize form
+ const resources = useMemo(
+ () => ({
+ settings: {
+ data: settings.data,
+ error: settings.error,
+ },
+ }),
+ [settings.data, settings.error]
)
- const didDataRevalidate = useMemo(() => [settings.data], [settings.data])
-
- const resetFormDataIfAllDataFetched = useCallback((): SettingsData | null => {
- if (settings.data) {
- return resetFormData(settings.data)
+ const remoteValues = useMemo(() => {
+ if (!checkIfAllResourcesLoaded(resources)) {
+ return null
}
- return null
- }, [resetFormData, settings.data])
-
- // init - when new config is fetched, set the form
- const [hasInit, setHasInit] = useState(false)
- useEffect(() => {
- if (!hasInit) {
- const didReset = resetFormDataIfAllDataFetched()
- if (didReset) {
- setHasInit(true)
- }
- }
- }, [hasInit, resetFormDataIfAllDataFetched])
+ return transformDown({
+ settings: resources.settings.data,
+ })
+ }, [resources])
+
+ const remoteError = useMemo(
+ () => checkIfAnyResourcesErrored(resources),
+ [resources]
+ )
- const revalidateAndResetFormData = useCallback(async () => {
- const data = await settings.mutate()
- if (!data) {
+ const revalidateAndResetForm = useCallback(async () => {
+ const _settings = await settings.mutate()
+ if (!_settings) {
triggerErrorToast('Error fetching settings.')
} else {
- resetFormData(data)
// also recheck dynamic dns
await dynDNSCheck.mutate()
+ return form.reset(
+ transformDown({
+ settings: _settings,
+ })
+ )
}
- }, [settings, resetFormData, dynDNSCheck])
+ }, [form, settings, dynDNSCheck])
- const onValid = useCallback(
- async (values: typeof initialValues) => {
- if (!settings.data) {
- return
- }
- try {
- const calculatedValues: Partial = {}
- if (!showAdvanced) {
- calculatedValues.maxCollateral = calculateMaxCollateral(
- values.storagePrice,
- values.collateralMultiplier
- )
- }
-
- const finalValues = {
- ...values,
- ...calculatedValues,
- }
-
- const response = await settingsUpdate.patch({
- payload: transformUp(finalValues, settings.data),
- })
- if (response.error) {
- throw Error(response.error)
- }
- if (form.formState.dirtyFields.netAddress) {
- triggerSuccessToast(
- 'Settings have been saved. Address has changed, make sure to re-announce the host.',
- {
- duration: 20_000,
- }
- )
- } else {
- triggerSuccessToast('Settings have been saved.')
- }
- await revalidateAndResetFormData()
- } catch (e) {
- triggerErrorToast((e as Error).message)
- console.log(e)
- }
- },
- [form, showAdvanced, settings, settingsUpdate, revalidateAndResetFormData]
- )
+ useFormInit({
+ form,
+ remoteValues,
+ })
+ useFormServerSynced({
+ form,
+ remoteValues,
+ })
+ const { changeCount } = useFormChangeCount({ form })
- const rates = useSiaCentralExchangeRates()
- const fields = useMemo(
- () =>
- getFields({
- showAdvanced,
- storageTBMonth,
- collateralMultiplier,
- rates: rates.data?.rates,
- }),
- [showAdvanced, storageTBMonth, collateralMultiplier, rates.data]
- )
+ const onValid = useOnValid({
+ resources,
+ dirtyFields: form.formState.dirtyFields,
+ showAdvanced,
+ revalidateAndResetForm,
+ })
const onInvalid = useOnInvalid(fields)
@@ -156,59 +85,17 @@ export function useConfigMain() {
[form, onValid, onInvalid]
)
- // Resets so that stale values that are no longer in sync with what is on
- // the daemon will show up as changed.
- const resetWithUserChanges = useCallback(() => {
- const currentFormValues = form.getValues()
- const serverFormValues = resetFormDataIfAllDataFetched()
- if (!serverFormValues) {
- return
- }
- form.reset(serverFormValues)
- for (const [key, value] of Object.entries(currentFormValues)) {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- form.setValue(key as any, value, {
- shouldDirty: true,
- })
- }
- }, [form, resetFormDataIfAllDataFetched])
-
- const { isUnlockedAndAuthedRoute } = useAppSettings()
- useEffect(() => {
- if (isUnlockedAndAuthedRoute) {
- revalidateAndResetFormData()
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [isUnlockedAndAuthedRoute])
-
- useEffect(() => {
- if (form.formState.isSubmitting) {
- return
- }
- resetWithUserChanges()
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [
- form,
- // if form mode is toggled reset
- showAdvanced,
- // if any of the settings are revalidated reset
- didDataRevalidate,
- ])
-
- const changeCount = Object.entries(form.formState.dirtyFields).filter(
- ([_, val]) => !!val
- ).length
-
return {
fields,
settings,
dynDNSCheck,
changeCount,
- revalidateAndResetFormData,
+ revalidateAndResetForm,
form,
onSubmit,
showAdvanced,
setShowAdvanced,
+ remoteError,
}
}
diff --git a/apps/hostd/contexts/config/resources.ts b/apps/hostd/contexts/config/resources.ts
new file mode 100644
index 000000000..ea28ae0c1
--- /dev/null
+++ b/apps/hostd/contexts/config/resources.ts
@@ -0,0 +1,23 @@
+import { SWRError } from '@siafoundation/react-core'
+import { HostSettings } from '@siafoundation/react-hostd'
+
+export type Resources = {
+ settings: {
+ data?: HostSettings
+ error?: SWRError
+ }
+}
+
+export function checkIfAllResourcesLoaded({ settings }: Resources) {
+ return !!(
+ // has initial daemon values
+ settings.data
+ )
+}
+
+export function checkIfAnyResourcesErrored({ settings }: Resources) {
+ return !!(
+ // settings has initial daemon values
+ settings.error
+ )
+}
diff --git a/apps/hostd/contexts/config/transform.spec.ts b/apps/hostd/contexts/config/transform.spec.ts
index 297c50990..0ac1272c7 100644
--- a/apps/hostd/contexts/config/transform.spec.ts
+++ b/apps/hostd/contexts/config/transform.spec.ts
@@ -5,34 +5,36 @@ describe('data transforms', () => {
it('down', () => {
expect(
transformDown({
- acceptingContracts: true,
- netAddress: 'tabo.zen.sia.tech:9882',
- maxContractDuration: 25920,
- contractPrice: '200000000000000000000000',
- baseRPCPrice: '100000000000000000',
- sectorAccessPrice: '100000000000000000',
- collateralMultiplier: 2,
- maxCollateral: '1000000000000000000000000000',
- storagePrice: '10526559048',
- egressPrice: '227373675443232',
- ingressPrice: '9094947017729',
- priceTableValidity: 1800000000000,
- maxRegistryEntries: 100000,
- accountExpiry: 2592000000000000,
- maxAccountBalance: '10000000000000000000000000',
- ingressLimit: 0,
- egressLimit: 0,
- ddns: {
- provider: 'route53',
- ipv4: false,
- ipv6: false,
- options: {
- id: 'ID',
- secret: 'secret',
- zoneID: 'zone',
+ settings: {
+ acceptingContracts: true,
+ netAddress: 'tabo.zen.sia.tech:9882',
+ maxContractDuration: 25920,
+ contractPrice: '200000000000000000000000',
+ baseRPCPrice: '100000000000000000',
+ sectorAccessPrice: '100000000000000000',
+ collateralMultiplier: 2,
+ maxCollateral: '1000000000000000000000000000',
+ storagePrice: '10526559048',
+ egressPrice: '227373675443232',
+ ingressPrice: '9094947017729',
+ priceTableValidity: 1800000000000,
+ maxRegistryEntries: 100000,
+ accountExpiry: 2592000000000000,
+ maxAccountBalance: '10000000000000000000000000',
+ ingressLimit: 0,
+ egressLimit: 0,
+ ddns: {
+ provider: 'route53',
+ ipv4: false,
+ ipv6: false,
+ options: {
+ id: 'ID',
+ secret: 'secret',
+ zoneID: 'zone',
+ },
},
+ revision: 0,
},
- revision: 0,
})
).toEqual({
acceptingContracts: true,
diff --git a/apps/hostd/contexts/config/transform.ts b/apps/hostd/contexts/config/transform.ts
index 8a90a6a6f..f34e43ba8 100644
--- a/apps/hostd/contexts/config/transform.ts
+++ b/apps/hostd/contexts/config/transform.ts
@@ -125,93 +125,100 @@ export function transformUp(
}
}
-export function transformDown(s: HostSettings): SettingsData {
+export function transformDown({
+ settings,
+}: {
+ settings: HostSettings
+}): SettingsData {
let dnsOptions = null
// DNS DuckDNS
- if (s.ddns.provider === 'duckdns') {
+ if (settings.ddns.provider === 'duckdns') {
dnsOptions = {
- dnsDuckDnsToken: s.ddns.options['token'],
+ dnsDuckDnsToken: settings.ddns.options['token'],
}
}
// DNS No-IP
- if (s.ddns.provider === 'noip') {
+ if (settings.ddns.provider === 'noip') {
dnsOptions = {
- dnsNoIpEmail: s.ddns.options['email'],
- dnsNoIpPassword: s.ddns.options['password'],
+ dnsNoIpEmail: settings.ddns.options['email'],
+ dnsNoIpPassword: settings.ddns.options['password'],
}
}
// DNS AWS
- if (s.ddns.provider === 'route53') {
+ if (settings.ddns.provider === 'route53') {
dnsOptions = {
- dnsAwsId: s.ddns.options['id'],
- dnsAwsSecret: s.ddns.options['secret'],
- dnsAwsZoneId: s.ddns.options['zoneID'],
+ dnsAwsId: settings.ddns.options['id'],
+ dnsAwsSecret: settings.ddns.options['secret'],
+ dnsAwsZoneId: settings.ddns.options['zoneID'],
}
}
// DNS Cloudflare
- if (s.ddns.provider === 'cloudflare') {
+ if (settings.ddns.provider === 'cloudflare') {
dnsOptions = {
- dnsCloudflareToken: s.ddns.options['token'],
- dnsCloudflareZoneId: s.ddns.options['zoneID'],
+ dnsCloudflareToken: settings.ddns.options['token'],
+ dnsCloudflareZoneId: settings.ddns.options['zoneID'],
}
}
return {
// Host settings
- acceptingContracts: s.acceptingContracts,
- netAddress: s.netAddress,
- maxContractDuration: new BigNumber(s.maxContractDuration).div(
+ acceptingContracts: settings.acceptingContracts,
+ netAddress: settings.netAddress,
+ maxContractDuration: new BigNumber(settings.maxContractDuration).div(
monthsToBlocks(1)
),
// Pricing
- contractPrice: toSiacoins(s.contractPrice, scDecimalPlaces),
+ contractPrice: toSiacoins(settings.contractPrice, scDecimalPlaces),
baseRPCPrice: toSiacoins(
- humanBaseRpcPrice(s.baseRPCPrice),
+ humanBaseRpcPrice(settings.baseRPCPrice),
scDecimalPlaces
),
sectorAccessPrice: toSiacoins(
- humanSectorAccessPrice(s.sectorAccessPrice),
+ humanSectorAccessPrice(settings.sectorAccessPrice),
scDecimalPlaces
),
- collateralMultiplier: new BigNumber(s.collateralMultiplier),
- maxCollateral: toSiacoins(s.maxCollateral, scDecimalPlaces),
+ collateralMultiplier: new BigNumber(settings.collateralMultiplier),
+ maxCollateral: toSiacoins(settings.maxCollateral, scDecimalPlaces),
storagePrice: toSiacoins(
- humanStoragePrice(s.storagePrice),
+ humanStoragePrice(settings.storagePrice),
+ scDecimalPlaces
+ ),
+ egressPrice: toSiacoins(
+ humanEgressPrice(settings.egressPrice),
scDecimalPlaces
),
- egressPrice: toSiacoins(humanEgressPrice(s.egressPrice), scDecimalPlaces),
ingressPrice: toSiacoins(
- humanIngressPrice(s.ingressPrice),
+ humanIngressPrice(settings.ingressPrice),
scDecimalPlaces
),
- priceTableValidity: new BigNumber(s.priceTableValidity)
+ priceTableValidity: new BigNumber(settings.priceTableValidity)
.div(1_000_000_000) // nanoseconds to seconds
.div(60), // seconds to minutes
// Registry settings
- maxRegistryEntries: new BigNumber(s.maxRegistryEntries),
+ maxRegistryEntries: new BigNumber(settings.maxRegistryEntries),
// RHP3 settings
- accountExpiry: new BigNumber(s.accountExpiry)
+ accountExpiry: new BigNumber(settings.accountExpiry)
.div(1_000_000_000) // nanoseconds to seconds
.div(60 * 60 * 24), // seconds to days
- maxAccountBalance: toSiacoins(s.maxAccountBalance, scDecimalPlaces),
+ maxAccountBalance: toSiacoins(settings.maxAccountBalance, scDecimalPlaces),
// Bandwidth limiter settings
- ingressLimit: bytesToMB(new BigNumber(s.ingressLimit)),
- egressLimit: bytesToMB(new BigNumber(s.egressLimit)),
+ ingressLimit: bytesToMB(new BigNumber(settings.ingressLimit)),
+ egressLimit: bytesToMB(new BigNumber(settings.egressLimit)),
// DNS settings
- dnsProvider: s.ddns.provider,
- dnsIpv4: s.ddns.ipv4,
- dnsIpv6: s.ddns.ipv6,
+ dnsProvider: settings.ddns.provider,
+ dnsIpv4: settings.ddns.ipv4,
+ dnsIpv6: settings.ddns.ipv6,
// DNS options
...dnsOptions,
diff --git a/apps/hostd/contexts/config/useForm.tsx b/apps/hostd/contexts/config/useForm.tsx
new file mode 100644
index 000000000..77d676124
--- /dev/null
+++ b/apps/hostd/contexts/config/useForm.tsx
@@ -0,0 +1,44 @@
+import { useForm as useHookForm } from 'react-hook-form'
+import { initialValues } from './types'
+import { useMemo } from 'react'
+import { getFields } from './fields'
+import useLocalStorageState from 'use-local-storage-state'
+import { useSiaCentralExchangeRates } from '@siafoundation/react-sia-central'
+
+export function useForm() {
+ const form = useHookForm({
+ mode: 'all',
+ defaultValues: initialValues,
+ })
+ const storageTBMonth = form.watch('storagePrice')
+ const collateralMultiplier = form.watch('collateralMultiplier')
+
+ const [showAdvanced, setShowAdvanced] = useLocalStorageState(
+ 'v0/config/showAdvanced',
+ {
+ defaultValue: false,
+ }
+ )
+ // const mode: 'simple' | 'advanced' = showAdvanced ? 'advanced' : 'simple'
+
+ const rates = useSiaCentralExchangeRates()
+ const fields = useMemo(
+ () =>
+ getFields({
+ showAdvanced,
+ storageTBMonth,
+ collateralMultiplier,
+ rates: rates.data?.rates,
+ }),
+ [showAdvanced, storageTBMonth, collateralMultiplier, rates.data]
+ )
+
+ return {
+ form,
+ fields,
+ storageTBMonth,
+ collateralMultiplier,
+ showAdvanced,
+ setShowAdvanced,
+ }
+}
diff --git a/apps/hostd/contexts/config/useOnValid.tsx b/apps/hostd/contexts/config/useOnValid.tsx
new file mode 100644
index 000000000..5f6c61d7f
--- /dev/null
+++ b/apps/hostd/contexts/config/useOnValid.tsx
@@ -0,0 +1,74 @@
+import {
+ triggerSuccessToast,
+ triggerErrorToast,
+} from '@siafoundation/design-system'
+import { useCallback } from 'react'
+import { SettingsData, initialValues } from './types'
+import { calculateMaxCollateral, transformUp } from './transform'
+import { UseFormReturn } from 'react-hook-form'
+import { Resources } from './resources'
+import { useSettingsUpdate } from '@siafoundation/react-hostd'
+
+export function useOnValid({
+ resources,
+ dirtyFields,
+ showAdvanced,
+ revalidateAndResetForm,
+}: {
+ dirtyFields: UseFormReturn['formState']['dirtyFields']
+ resources: Resources
+ showAdvanced: boolean
+ revalidateAndResetForm: () => Promise
+}) {
+ const settingsUpdate = useSettingsUpdate()
+ const onValid = useCallback(
+ async (values: typeof initialValues) => {
+ if (!resources) {
+ return
+ }
+ try {
+ const calculatedValues: Partial = {}
+ if (!showAdvanced) {
+ calculatedValues.maxCollateral = calculateMaxCollateral(
+ values.storagePrice,
+ values.collateralMultiplier
+ )
+ }
+
+ const finalValues = {
+ ...values,
+ ...calculatedValues,
+ }
+
+ const response = await settingsUpdate.patch({
+ payload: transformUp(finalValues, resources.settings.data),
+ })
+ if (response.error) {
+ throw Error(response.error)
+ }
+ if (dirtyFields.netAddress) {
+ triggerSuccessToast(
+ 'Settings have been saved. Address has changed, make sure to re-announce the host.',
+ {
+ duration: 20_000,
+ }
+ )
+ } else {
+ triggerSuccessToast('Settings have been saved.')
+ }
+ await revalidateAndResetForm()
+ } catch (e) {
+ triggerErrorToast((e as Error).message)
+ console.log(e)
+ }
+ },
+ [
+ showAdvanced,
+ resources,
+ dirtyFields.netAddress,
+ settingsUpdate,
+ revalidateAndResetForm,
+ ]
+ )
+ return onValid
+}
diff --git a/apps/hostd/contexts/config/useResources.tsx b/apps/hostd/contexts/config/useResources.tsx
new file mode 100644
index 000000000..ec5e9f64b
--- /dev/null
+++ b/apps/hostd/contexts/config/useResources.tsx
@@ -0,0 +1,26 @@
+import { minutesInMilliseconds } from '@siafoundation/design-system'
+import { useSettings, useSettingsDdns } from '@siafoundation/react-hostd'
+
+export function useResources() {
+ const settings = useSettings({
+ config: {
+ swr: {
+ refreshInterval: minutesInMilliseconds(1),
+ },
+ },
+ })
+ const dynDNSCheck = useSettingsDdns({
+ disabled: !settings.data || !settings.data.ddns.provider,
+ config: {
+ swr: {
+ revalidateOnFocus: false,
+ errorRetryCount: 0,
+ },
+ },
+ })
+
+ return {
+ settings,
+ dynDNSCheck,
+ }
+}
diff --git a/apps/hostd/contexts/metrics/types.tsx b/apps/hostd/contexts/metrics/types.tsx
index 076d907a6..e47b143f6 100644
--- a/apps/hostd/contexts/metrics/types.tsx
+++ b/apps/hostd/contexts/metrics/types.tsx
@@ -124,12 +124,12 @@ export const dataTimeSpanOptions: {
},
{
label: '1Y',
- interval: 'daily',
+ interval: 'weekly',
value: '365',
},
{
label: 'ALL',
- interval: 'weekly',
+ interval: 'monthly',
value: 'all',
},
]
diff --git a/apps/renterd/components/Config/ConfigActions.tsx b/apps/renterd/components/Config/ConfigActions.tsx
index aa842ea7d..59e6c1dbb 100644
--- a/apps/renterd/components/Config/ConfigActions.tsx
+++ b/apps/renterd/components/Config/ConfigActions.tsx
@@ -16,7 +16,7 @@ export function ConfigActions() {
changeCount,
shouldSyncDefaultContractSet,
setShouldSyncDefaultContractSet,
- revalidateAndResetFormData,
+ revalidateAndResetForm,
form,
} = useConfig()
@@ -31,7 +31,7 @@ export function ConfigActions() {
tip="Reset all changes"
icon="contrast"
disabled={!changeCount}
- onClick={revalidateAndResetFormData}
+ onClick={revalidateAndResetForm}
>
diff --git a/apps/renterd/components/Config/StateConnError.tsx b/apps/renterd/components/Config/StateConnError.tsx
new file mode 100644
index 000000000..77890740c
--- /dev/null
+++ b/apps/renterd/components/Config/StateConnError.tsx
@@ -0,0 +1,21 @@
+import { Button, Text } from '@siafoundation/design-system'
+import { Warning32 } from '@siafoundation/react-icons'
+import { useConfig } from '../../contexts/config'
+
+export function StateConnError() {
+ const { revalidateAndResetForm } = useConfig()
+ return (
+
+
+
+
+
+
+ Error retrieving settings from daemon. Please check your connection
+ and try again.
+
+
+
+
+ )
+}
diff --git a/apps/renterd/components/Config/index.tsx b/apps/renterd/components/Config/index.tsx
index 68be946ed..9f796a0f0 100644
--- a/apps/renterd/components/Config/index.tsx
+++ b/apps/renterd/components/Config/index.tsx
@@ -7,10 +7,11 @@ import { useConfig } from '../../contexts/config'
import { ConfigStats } from './ConfigStats'
import { ConfigActions } from './ConfigActions'
import { ConfigNav } from './ConfigNav'
+import { StateConnError } from './StateConnError'
export function Config() {
const { openDialog } = useDialog()
- const { form, fields } = useConfig()
+ const { form, fields, remoteError } = useConfig()
return (
}
openSettings={() => openDialog('settings')}
>
-
-
-
-
-
-
-
-
-
+ {remoteError ? (
+
+ ) : (
+
+
+
+
+
+
+
+
+
+ )}
)
}
diff --git a/apps/renterd/contexts/config/index.tsx b/apps/renterd/contexts/config/index.tsx
index 8dbbcf716..19a019c31 100644
--- a/apps/renterd/contexts/config/index.tsx
+++ b/apps/renterd/contexts/config/index.tsx
@@ -1,43 +1,36 @@
import React, { createContext, useContext } from 'react'
import {
triggerErrorToast,
+ useFormChangeCount,
+ useFormInit,
+ useFormServerSynced,
useOnInvalid,
- useServerSyncedForm,
} from '@siafoundation/design-system'
-import BigNumber from 'bignumber.js'
import { useCallback, useMemo } from 'react'
-import { useBusState, GougingSettings } from '@siafoundation/react-renterd'
-import { getFields } from './fields'
-import { SettingsData, getAdvancedDefaults } from './types'
+import { SettingsData } from './types'
import { transformDown } from './transform'
-import { useAppSettings } from '@siafoundation/react-core'
-import { TBToBytes } from '@siafoundation/units'
import { useResources } from './useResources'
import { useOnValid } from './useOnValid'
import { useEstimates } from './useEstimates'
import { useForm } from './useForm'
-import { useAverages } from './useAverages'
+import {
+ checkIfAllResourcesLoaded,
+ checkIfAnyResourcesErrored,
+} from './resources'
export function useConfigMain() {
const {
- app,
- isAutopilotEnabled,
autopilot,
contractSet,
display,
gouging,
redundancy,
uploadPacking,
- settingUpdate,
averages,
- showAdvanced,
- setShowAdvanced,
- mode,
shouldSyncDefaultContractSet,
setShouldSyncDefaultContractSet,
- syncDefaultContractSet,
- autopilotUpdate,
appSettings,
+ isAutopilotEnabled,
} = useResources()
const {
@@ -48,185 +41,134 @@ export function useConfigMain() {
storageTB,
downloadTBMonth,
uploadTBMonth,
- minShards,
- totalShards,
includeRedundancyMaxStoragePrice,
includeRedundancyMaxUploadPrice,
redundancyMultiplier,
+ fields,
+ showAdvanced,
+ setShowAdvanced,
} = useForm()
- const { storageAverage, uploadAverage, downloadAverage, contractAverage } =
- useAverages({
- minShards,
- totalShards,
- includeRedundancyMaxStoragePrice,
- includeRedundancyMaxUploadPrice,
- })
-
- // Override gouging data defaults with sia central averages.
- // We override the remote data so that the values appear as the defaults.
- // If we just changed the form values they would appear as a user change.
- const buildGougingData = useCallback(
- (gougingData: GougingSettings): GougingSettings => {
- // wait for gouging data and for ap to be initialized
- if (!gougingData || app.autopilot.status === 'init') {
- return null
- }
- // if sia central is disabled, we cant override with averages
- if (!appSettings.settings.siaCentral) {
- return gougingData
- }
- // already configured, the user has changed the defaults
- if (app.autopilot.state.data?.configured) {
- return gougingData
- }
- if (averages.isLoading) {
- return null
- }
- // first time user, override defaults
- if (averages.data) {
- return {
- ...gougingData,
- maxStoragePrice: averages.data?.settings.storage_price,
- maxDownloadPrice: new BigNumber(
- averages.data?.settings.download_price
- )
- .times(TBToBytes(1))
- .toString(),
- maxUploadPrice: new BigNumber(averages.data?.settings.upload_price)
- .times(TBToBytes(1))
- .toString(),
- }
- }
- return gougingData
- },
+ // resources required to intialize form
+ const resources = useMemo(
+ () => ({
+ autopilot: {
+ data: autopilot.data,
+ error: autopilot.error,
+ },
+ contractSet: {
+ data: contractSet.data,
+ error: contractSet.error,
+ },
+ uploadPacking: {
+ data: uploadPacking.data,
+ error: uploadPacking.error,
+ },
+ gouging: {
+ data: gouging.data,
+ error: gouging.error,
+ },
+ redundancy: {
+ data: redundancy.data,
+ error: redundancy.error,
+ },
+ display: {
+ data: display.data,
+ error: display.error,
+ },
+ averages: {
+ data: averages.data,
+ error: averages.error,
+ },
+ appSettings: {
+ settings: {
+ siaCentral: appSettings.settings.siaCentral,
+ },
+ },
+ }),
[
+ autopilot.data,
+ autopilot.error,
+ contractSet.data,
+ contractSet.error,
+ uploadPacking.data,
+ uploadPacking.error,
+ gouging.data,
+ gouging.error,
+ redundancy.data,
+ redundancy.error,
+ display.data,
+ display.error,
averages.data,
- averages.isLoading,
+ averages.error,
appSettings.settings.siaCentral,
- app.autopilot.status,
- app.autopilot.state.data?.configured,
]
)
const remoteValues: SettingsData = useMemo(() => {
- const g = buildGougingData(gouging.data)
- if (
- (!isAutopilotEnabled || autopilot.data || autopilot.error) &&
- g &&
- redundancy.data &&
- uploadPacking.data &&
- (contractSet.data || contractSet.error) &&
- (display.data || display.error)
- ) {
- return transformDown({
- autopilot: autopilot.data,
- contractSet: contractSet.data,
- uploadPacking: uploadPacking.data,
- gouging: g,
- redundancy: redundancy.data,
- display: display.data,
- })
+ if (!checkIfAllResourcesLoaded(resources)) {
+ return null
}
- return null
- }, [
- isAutopilotEnabled,
- autopilot.data,
- autopilot.error,
- contractSet.data,
- contractSet.error,
- uploadPacking.data,
- gouging.data,
- buildGougingData,
- redundancy.data,
- display.data,
- display.error,
- ])
+ return transformDown({
+ autopilot: resources.autopilot.data,
+ contractSet: resources.contractSet.data,
+ uploadPacking: resources.uploadPacking.data,
+ gouging: resources.gouging.data,
+ averages: resources.averages.data,
+ redundancy: resources.redundancy.data,
+ display: resources.display.data,
+ })
+ }, [resources])
+
+ const remoteError = useMemo(
+ () => checkIfAnyResourcesErrored(resources),
+ [resources]
+ )
- const revalidate = useCallback(async (): Promise => {
- const a = isAutopilotEnabled ? await autopilot.mutate() : undefined
- const cs = await contractSet.mutate()
- const _g = await gouging.mutate()
- const r = await redundancy.mutate()
- const up = await uploadPacking.mutate()
- const d = await display.mutate()
- if (!_g || !r) {
+ const revalidateAndResetForm = useCallback(async () => {
+ // these do not seem to throw on errors, just return undefined
+ const _autopilot = isAutopilotEnabled ? await autopilot.mutate() : undefined
+ const _contractSet = await contractSet.mutate()
+ const _gouging = await gouging.mutate()
+ const _redundancy = await redundancy.mutate()
+ const _uploadPacking = await uploadPacking.mutate()
+ const _display = await display.mutate()
+ if (!gouging || !redundancy) {
triggerErrorToast('Error fetching settings.')
return null
- } else {
- const g = buildGougingData(_g)
- if (!g) {
- return null
- }
- return transformDown({
- autopilot: a,
- contractSet: cs,
- gouging: g,
- redundancy: r,
- uploadPacking: up,
- display: d,
- })
}
+ form.reset(
+ transformDown({
+ autopilot: _autopilot,
+ contractSet: _contractSet,
+ uploadPacking: _uploadPacking,
+ gouging: _gouging,
+ averages: averages.data,
+ redundancy: _redundancy,
+ display: _display,
+ })
+ )
}, [
+ form,
isAutopilotEnabled,
autopilot,
contractSet,
gouging,
- buildGougingData,
uploadPacking,
redundancy,
display,
+ averages.data,
])
- const { isUnlockedAndAuthedRoute } = useAppSettings()
- const { revalidateAndResetFormData, changeCount } = useServerSyncedForm({
+ useFormInit({
form,
remoteValues,
- revalidate,
- initialized: isUnlockedAndAuthedRoute,
- mode,
})
-
- const renterdState = useBusState()
- const fields = useMemo(() => {
- const advancedDefaults = renterdState.data
- ? getAdvancedDefaults(renterdState.data.network)
- : undefined
- if (averages.data) {
- return getFields({
- advancedDefaults,
- isAutopilotEnabled,
- showAdvanced,
- redundancyMultiplier,
- includeRedundancyMaxStoragePrice,
- includeRedundancyMaxUploadPrice,
- storageAverage,
- uploadAverage,
- downloadAverage,
- contractAverage,
- })
- }
- return getFields({
- advancedDefaults,
- isAutopilotEnabled,
- showAdvanced,
- redundancyMultiplier,
- includeRedundancyMaxStoragePrice,
- includeRedundancyMaxUploadPrice,
- })
- }, [
- renterdState.data,
- isAutopilotEnabled,
- showAdvanced,
- averages.data,
- storageAverage,
- uploadAverage,
- downloadAverage,
- contractAverage,
- redundancyMultiplier,
- includeRedundancyMaxStoragePrice,
- includeRedundancyMaxUploadPrice,
- ])
+ useFormServerSynced({
+ form,
+ remoteValues,
+ })
+ const { changeCount } = useFormChangeCount({ form })
const { canEstimate, estimatedSpendingPerMonth, estimatedSpendingPerTB } =
useEstimates({
@@ -243,20 +185,11 @@ export function useConfigMain() {
})
const onValid = useOnValid({
- renterdState,
+ resources,
estimatedSpendingPerMonth,
showAdvanced,
isAutopilotEnabled,
- autopilot,
- autopilotUpdate,
- revalidateAndResetFormData,
- syncDefaultContractSet,
- settingUpdate,
- contractSet,
- uploadPacking,
- redundancy,
- gouging,
- display,
+ revalidateAndResetForm,
})
const onInvalid = useOnInvalid(fields)
@@ -268,7 +201,7 @@ export function useConfigMain() {
return {
onSubmit,
- revalidateAndResetFormData,
+ revalidateAndResetForm,
form,
fields,
changeCount,
@@ -281,6 +214,7 @@ export function useConfigMain() {
setShouldSyncDefaultContractSet,
showAdvanced,
setShowAdvanced,
+ remoteError,
}
}
diff --git a/apps/renterd/contexts/config/resources.ts b/apps/renterd/contexts/config/resources.ts
new file mode 100644
index 000000000..3573ee79d
--- /dev/null
+++ b/apps/renterd/contexts/config/resources.ts
@@ -0,0 +1,121 @@
+import { SWRError } from '@siafoundation/react-core'
+import {
+ AutopilotConfig,
+ ContractSetSettings,
+ GougingSettings,
+ RedundancySettings,
+ UploadPackingSettings,
+} from '@siafoundation/react-renterd'
+import { ConfigDisplaySettings } from '../../hooks/useConfigDisplaySettings'
+import { SiaCentralHostsNetworkAveragesResponse } from '@siafoundation/sia-central'
+import BigNumber from 'bignumber.js'
+import { TBToBytes } from '@siafoundation/units'
+
+export type Resources = {
+ autopilot: {
+ data?: AutopilotConfig
+ error?: SWRError
+ }
+ contractSet: {
+ data?: ContractSetSettings
+ error?: SWRError
+ }
+ uploadPacking: {
+ data?: UploadPackingSettings
+ error?: SWRError
+ }
+ gouging: {
+ data?: GougingSettings
+ error?: SWRError
+ }
+ redundancy: {
+ data?: RedundancySettings
+ error?: SWRError
+ }
+ display: {
+ data?: ConfigDisplaySettings
+ error?: SWRError
+ }
+ averages: {
+ data?: SiaCentralHostsNetworkAveragesResponse
+ error?: SWRError
+ }
+ appSettings: {
+ settings: {
+ siaCentral: boolean
+ }
+ }
+}
+
+export function checkIfAllResourcesLoaded({
+ autopilot,
+ contractSet,
+ uploadPacking,
+ gouging,
+ redundancy,
+ display,
+ averages,
+ appSettings,
+}: Resources) {
+ return !!(
+ // these settings have initial daemon values
+ (
+ redundancy.data &&
+ uploadPacking.data &&
+ gouging.data &&
+ // these settings are undefined and will error
+ // until the user sets them
+ (autopilot.data || autopilot.error) &&
+ (contractSet.data || contractSet.error) &&
+ (display.data || display.error) &&
+ // other data dependencies
+ (!appSettings.settings.siaCentral || averages.data)
+ )
+ )
+}
+
+export function checkIfAnyResourcesErrored({
+ uploadPacking,
+ gouging,
+ redundancy,
+}: Resources) {
+ return !!(
+ // these settings have initial daemon values
+ (redundancy.error || uploadPacking.error || gouging.error)
+ )
+}
+
+export function firstTimeGougingData({
+ gouging,
+ averages,
+ hasBeenConfigured,
+}: {
+ gouging: GougingSettings
+ averages?: {
+ settings: {
+ download_price: string
+ storage_price: string
+ upload_price: string
+ }
+ }
+ hasBeenConfigured: boolean
+}): GougingSettings {
+ // already configured, the user has changed the defaults
+ if (hasBeenConfigured) {
+ return gouging
+ }
+ // if sia central is disabled, we cant override with averages
+ if (!averages) {
+ return gouging
+ }
+ return {
+ ...gouging,
+ maxStoragePrice: averages.settings.storage_price,
+ maxDownloadPrice: new BigNumber(averages.settings.download_price)
+ .times(TBToBytes(1))
+ .toString(),
+ maxUploadPrice: new BigNumber(averages.settings.upload_price)
+ .times(TBToBytes(1))
+ .toString(),
+ }
+}
diff --git a/apps/renterd/contexts/config/transform.spec.ts b/apps/renterd/contexts/config/transform.spec.ts
index 20109f0ae..f1dd685fd 100644
--- a/apps/renterd/contexts/config/transform.spec.ts
+++ b/apps/renterd/contexts/config/transform.spec.ts
@@ -101,6 +101,46 @@ describe('tansforms', () => {
} as SettingsData)
})
+ it('default works with first time user overrides', () => {
+ const values = transformDown({
+ autopilot: undefined,
+ contractSet: undefined,
+ uploadPacking: {
+ enabled: false,
+ },
+ gouging: {
+ hostBlockHeightLeeway: 4,
+ maxContractPrice: '20000000000000000000000000',
+ maxDownloadPrice: '1004310000000000000000000000',
+ maxRPCPrice: '99970619000000000000000000',
+ maxStoragePrice: '210531181019',
+ maxUploadPrice: '1000232323000000000000000000',
+ minAccountExpiry: 86400000000000,
+ minMaxCollateral: '10000000000000000000000000',
+ minMaxEphemeralAccountBalance: '1000000000000000000000000',
+ minPriceTableValidity: 300000000000,
+ migrationSurchargeMultiplier: 10,
+ },
+ redundancy: {
+ minShards: 10,
+ totalShards: 30,
+ },
+ display: undefined,
+ averages: {
+ settings: {
+ download_price: (4e24).toString(),
+ storage_price: (4e24).toString(),
+ upload_price: (4e24).toString(),
+ },
+ },
+ })
+ expect(values.maxUploadPriceTB).toEqual(new BigNumber('12000000000000'))
+ expect(values.maxDownloadPriceTB).toEqual(new BigNumber('4000000000000'))
+ expect(values.maxStoragePriceTBMonth).toEqual(
+ new BigNumber('51840000000000000')
+ )
+ })
+
it('with include redundancy for storage and upload', () => {
expect(
transformDown({
diff --git a/apps/renterd/contexts/config/transform.ts b/apps/renterd/contexts/config/transform.ts
index 69c8ce786..c28d74dc6 100644
--- a/apps/renterd/contexts/config/transform.ts
+++ b/apps/renterd/contexts/config/transform.ts
@@ -37,7 +37,8 @@ import {
defaultAutopilot,
advancedDefaultContractSet,
} from './types'
-import { ConfigDisplayOptions } from '../../hooks/useConfigDisplayOptions'
+import { ConfigDisplaySettings } from '../../hooks/useConfigDisplaySettings'
+import { firstTimeGougingData } from './resources'
const filterUndefinedKeys = (obj: Record) => {
return Object.fromEntries(
@@ -187,7 +188,7 @@ export function transformUpRedundancy(
export function transformUpDisplay(
values: DisplayData,
existingValues: Record | undefined
-): ConfigDisplayOptions {
+): ConfigDisplaySettings {
return {
...existingValues,
includeRedundancyMaxStoragePrice: values.includeRedundancyMaxStoragePrice,
@@ -276,61 +277,84 @@ export function transformDownUploadPacking(
}
}
-export function transformDownConfigApp(ca?: ConfigDisplayOptions): DisplayData {
- if (!ca) {
+export function transformDownDisplay(d?: ConfigDisplaySettings): DisplayData {
+ if (!d) {
return defaultDisplay
}
return {
- includeRedundancyMaxStoragePrice: ca.includeRedundancyMaxStoragePrice,
- includeRedundancyMaxUploadPrice: ca.includeRedundancyMaxUploadPrice,
+ includeRedundancyMaxStoragePrice: d.includeRedundancyMaxStoragePrice,
+ includeRedundancyMaxUploadPrice: d.includeRedundancyMaxUploadPrice,
}
}
-export function transformDownGouging(
- g: GougingSettings,
- r: RedundancyData,
- ca: DisplayData
-): GougingData {
+export function transformDownGouging({
+ gouging: _gouging,
+ redundancy,
+ display,
+ averages,
+ hasBeenConfigured,
+}: {
+ gouging: GougingSettings
+ redundancy: RedundancyData
+ display: DisplayData
+ averages?: {
+ settings: {
+ download_price: string
+ storage_price: string
+ upload_price: string
+ }
+ }
+ hasBeenConfigured: boolean
+}): GougingData {
+ const gouging = firstTimeGougingData({
+ gouging: _gouging,
+ averages,
+ hasBeenConfigured,
+ })
return {
maxStoragePriceTBMonth: toSiacoins(
- new BigNumber(g.maxStoragePrice) // bytes/block
+ new BigNumber(gouging.maxStoragePrice) // bytes/block
.times(monthsToBlocks(1)) // bytes/month
.times(TBToBytes(1)) // tb/month
.times(
getRedundancyMultiplierIfIncluded(
- r.minShards,
- r.totalShards,
- ca.includeRedundancyMaxStoragePrice
+ redundancy.minShards,
+ redundancy.totalShards,
+ display.includeRedundancyMaxStoragePrice
)
),
scDecimalPlaces
), // TB/month
maxUploadPriceTB: toSiacoins(
- new BigNumber(g.maxUploadPrice).times(
+ new BigNumber(gouging.maxUploadPrice).times(
getRedundancyMultiplierIfIncluded(
- r.minShards,
- r.totalShards,
- ca.includeRedundancyMaxUploadPrice
+ redundancy.minShards,
+ redundancy.totalShards,
+ display.includeRedundancyMaxUploadPrice
)
),
scDecimalPlaces
),
- maxDownloadPriceTB: toSiacoins(g.maxDownloadPrice, scDecimalPlaces),
- maxContractPrice: toSiacoins(g.maxContractPrice, scDecimalPlaces),
- maxRpcPriceMillion: toSiacoins(g.maxRPCPrice, scDecimalPlaces).times(
+ maxDownloadPriceTB: toSiacoins(gouging.maxDownloadPrice, scDecimalPlaces),
+ maxContractPrice: toSiacoins(gouging.maxContractPrice, scDecimalPlaces),
+ maxRpcPriceMillion: toSiacoins(gouging.maxRPCPrice, scDecimalPlaces).times(
1_000_000
),
- minMaxCollateral: toSiacoins(g.minMaxCollateral, scDecimalPlaces),
- hostBlockHeightLeeway: new BigNumber(g.hostBlockHeightLeeway),
+ minMaxCollateral: toSiacoins(gouging.minMaxCollateral, scDecimalPlaces),
+ hostBlockHeightLeeway: new BigNumber(gouging.hostBlockHeightLeeway),
minPriceTableValidityMinutes: new BigNumber(
- nanosecondsInMinutes(g.minPriceTableValidity)
+ nanosecondsInMinutes(gouging.minPriceTableValidity)
+ ),
+ minAccountExpiryDays: new BigNumber(
+ nanosecondsInDays(gouging.minAccountExpiry)
),
- minAccountExpiryDays: new BigNumber(nanosecondsInDays(g.minAccountExpiry)),
minMaxEphemeralAccountBalance: toSiacoins(
- g.minMaxEphemeralAccountBalance,
+ gouging.minMaxEphemeralAccountBalance,
scDecimalPlaces
),
- migrationSurchargeMultiplier: new BigNumber(g.migrationSurchargeMultiplier),
+ migrationSurchargeMultiplier: new BigNumber(
+ gouging.migrationSurchargeMultiplier
+ ),
}
}
@@ -347,7 +371,14 @@ export type RemoteData = {
uploadPacking: UploadPackingSettings
gouging: GougingSettings
redundancy: RedundancySettings
- display: ConfigDisplayOptions | undefined
+ display: ConfigDisplaySettings | undefined
+ averages?: {
+ settings: {
+ download_price: string
+ storage_price: string
+ upload_price: string
+ }
+ }
}
export function transformDown({
@@ -355,11 +386,14 @@ export function transformDown({
contractSet,
uploadPacking,
gouging,
- redundancy,
- display,
+ redundancy: _redundancy,
+ display: _display,
+ averages,
}: RemoteData): SettingsData {
- const d = transformDownConfigApp(display)
- const r = transformDownRedundancy(redundancy)
+ // display will be undefined if its the first time the user is configuring
+ const hasBeenConfigured = !!_display
+ const display = transformDownDisplay(_display)
+ const redundancy = transformDownRedundancy(_redundancy)
return {
// autopilot
...transformDownAutopilot(autopilot),
@@ -368,11 +402,17 @@ export function transformDown({
// uploadpacking
...transformDownUploadPacking(uploadPacking),
// gouging
- ...transformDownGouging(gouging, r, d),
+ ...transformDownGouging({
+ gouging,
+ averages,
+ redundancy,
+ display,
+ hasBeenConfigured,
+ }),
// redundancy
- ...r,
+ ...redundancy,
// config app
- ...d,
+ ...display,
}
}
diff --git a/apps/renterd/contexts/config/useAverages.tsx b/apps/renterd/contexts/config/useAverages.tsx
index 9fafbf0e0..0615cf48d 100644
--- a/apps/renterd/contexts/config/useAverages.tsx
+++ b/apps/renterd/contexts/config/useAverages.tsx
@@ -82,6 +82,7 @@ export function useAverages({
)
return {
+ averages,
storageAverage,
uploadAverage,
downloadAverage,
diff --git a/apps/renterd/contexts/config/useEstimates.tsx b/apps/renterd/contexts/config/useEstimates.tsx
index 613ac3c46..afb21fc4e 100644
--- a/apps/renterd/contexts/config/useEstimates.tsx
+++ b/apps/renterd/contexts/config/useEstimates.tsx
@@ -12,6 +12,17 @@ export function useEstimates({
downloadTBMonth,
maxUploadPriceTB,
uploadTBMonth,
+}: {
+ isAutopilotEnabled: boolean
+ includeRedundancyMaxStoragePrice: boolean
+ includeRedundancyMaxUploadPrice: boolean
+ redundancyMultiplier: BigNumber
+ maxStoragePriceTBMonth: BigNumber
+ storageTB: BigNumber
+ maxDownloadPriceTB: BigNumber
+ downloadTBMonth: BigNumber
+ maxUploadPriceTB: BigNumber
+ uploadTBMonth: BigNumber
}) {
const canEstimate = useMemo(() => {
if (!isAutopilotEnabled) {
diff --git a/apps/renterd/contexts/config/useForm.tsx b/apps/renterd/contexts/config/useForm.tsx
index 6b4057a62..182a8264b 100644
--- a/apps/renterd/contexts/config/useForm.tsx
+++ b/apps/renterd/contexts/config/useForm.tsx
@@ -1,7 +1,12 @@
import { useMemo } from 'react'
-import { defaultValues } from './types'
+import { defaultValues, getAdvancedDefaults } from './types'
import { getRedundancyMultiplier } from './transform'
import { useForm as useHookForm } from 'react-hook-form'
+import { useAverages } from './useAverages'
+import { useBusState } from '@siafoundation/react-renterd'
+import { getFields } from './fields'
+import { useApp } from '../app'
+import useLocalStorageState from 'use-local-storage-state'
export function useForm() {
const form = useHookForm({
@@ -27,8 +32,72 @@ export function useForm() {
[minShards, totalShards]
)
+ const {
+ averages,
+ storageAverage,
+ uploadAverage,
+ downloadAverage,
+ contractAverage,
+ } = useAverages({
+ minShards,
+ totalShards,
+ includeRedundancyMaxStoragePrice,
+ includeRedundancyMaxUploadPrice,
+ })
+
+ const app = useApp()
+ const isAutopilotEnabled = app.autopilot.status === 'on'
+ const [showAdvanced, setShowAdvanced] = useLocalStorageState(
+ 'v0/config/showAdvanced',
+ {
+ defaultValue: false,
+ }
+ )
+
+ const renterdState = useBusState()
+ const fields = useMemo(() => {
+ const advancedDefaults = renterdState.data
+ ? getAdvancedDefaults(renterdState.data.network)
+ : undefined
+ if (averages.data) {
+ return getFields({
+ advancedDefaults,
+ isAutopilotEnabled,
+ showAdvanced,
+ redundancyMultiplier,
+ includeRedundancyMaxStoragePrice,
+ includeRedundancyMaxUploadPrice,
+ storageAverage,
+ uploadAverage,
+ downloadAverage,
+ contractAverage,
+ })
+ }
+ return getFields({
+ advancedDefaults,
+ isAutopilotEnabled,
+ showAdvanced,
+ redundancyMultiplier,
+ includeRedundancyMaxStoragePrice,
+ includeRedundancyMaxUploadPrice,
+ })
+ }, [
+ renterdState.data,
+ isAutopilotEnabled,
+ showAdvanced,
+ averages.data,
+ storageAverage,
+ uploadAverage,
+ downloadAverage,
+ contractAverage,
+ redundancyMultiplier,
+ includeRedundancyMaxStoragePrice,
+ includeRedundancyMaxUploadPrice,
+ ])
+
return {
form,
+ fields,
maxStoragePriceTBMonth,
maxDownloadPriceTB,
maxUploadPriceTB,
@@ -40,5 +109,7 @@ export function useForm() {
includeRedundancyMaxStoragePrice,
includeRedundancyMaxUploadPrice,
redundancyMultiplier,
+ showAdvanced,
+ setShowAdvanced,
}
}
diff --git a/apps/renterd/contexts/config/useOnValid.tsx b/apps/renterd/contexts/config/useOnValid.tsx
index b0c2d40ea..3c2026e22 100644
--- a/apps/renterd/contexts/config/useOnValid.tsx
+++ b/apps/renterd/contexts/config/useOnValid.tsx
@@ -5,7 +5,10 @@ import {
import { useCallback } from 'react'
import {
autopilotHostsKey,
+ useAutopilotConfigUpdate,
useAutopilotTrigger,
+ useBusState,
+ useSettingUpdate,
} from '@siafoundation/react-renterd'
import { SettingsData, defaultValues } from './types'
import {
@@ -17,29 +20,37 @@ import {
transformUpUploadPacking,
} from './transform'
import { delay, useMutate } from '@siafoundation/react-core'
-import { configDisplayOptionsKey } from '../../hooks/useConfigDisplayOptions'
+import { configDisplaySettingsKey } from '../../hooks/useConfigDisplaySettings'
+import { Resources } from './resources'
+import { useSyncContractSet } from './useSyncContractSet'
+import BigNumber from 'bignumber.js'
export function useOnValid({
- gouging,
- redundancy,
- renterdState,
+ resources,
estimatedSpendingPerMonth,
isAutopilotEnabled,
showAdvanced,
- revalidateAndResetFormData,
- syncDefaultContractSet,
- settingUpdate,
- contractSet,
- uploadPacking,
- display,
- autopilot,
- autopilotUpdate,
+ revalidateAndResetForm,
+}: {
+ resources: Resources
+ estimatedSpendingPerMonth: BigNumber
+ isAutopilotEnabled: boolean
+ showAdvanced: boolean
+ revalidateAndResetForm: () => Promise
}) {
const autopilotTrigger = useAutopilotTrigger()
+ const autopilotUpdate = useAutopilotConfigUpdate()
+ const settingUpdate = useSettingUpdate()
+ const renterdState = useBusState()
+ const { syncDefaultContractSet } = useSyncContractSet()
const mutate = useMutate()
const onValid = useCallback(
async (values: typeof defaultValues) => {
- if (!gouging.data || !redundancy.data || !renterdState.data) {
+ if (
+ !resources.gouging.data ||
+ !resources.redundancy.data ||
+ !renterdState.data
+ ) {
return
}
try {
@@ -53,13 +64,14 @@ export function useOnValid({
...calculatedValues,
}
- const firstTimeSettingConfig = isAutopilotEnabled && !autopilot.data
+ const firstTimeSettingConfig =
+ isAutopilotEnabled && !resources.autopilot.data
const autopilotResponse = isAutopilotEnabled
? await autopilotUpdate.put({
payload: transformUpAutopilot(
renterdState.data.network,
finalValues,
- autopilot.data
+ resources.autopilot.data
),
})
: undefined
@@ -75,31 +87,40 @@ export function useOnValid({
params: {
key: 'contractset',
},
- payload: transformUpContractSet(finalValues, contractSet.data),
+ payload: transformUpContractSet(
+ finalValues,
+ resources.contractSet.data
+ ),
}),
settingUpdate.put({
params: {
key: 'uploadpacking',
},
- payload: transformUpUploadPacking(finalValues, uploadPacking.data),
+ payload: transformUpUploadPacking(
+ finalValues,
+ resources.uploadPacking.data
+ ),
}),
settingUpdate.put({
params: {
key: 'gouging',
},
- payload: transformUpGouging(finalValues, gouging.data),
+ payload: transformUpGouging(finalValues, resources.gouging.data),
}),
settingUpdate.put({
params: {
key: 'redundancy',
},
- payload: transformUpRedundancy(finalValues, redundancy.data),
+ payload: transformUpRedundancy(
+ finalValues,
+ resources.redundancy.data
+ ),
}),
settingUpdate.put({
params: {
- key: configDisplayOptionsKey,
+ key: configDisplaySettingsKey,
},
- payload: transformUpDisplay(finalValues, display.data),
+ payload: transformUpDisplay(finalValues, resources.display.data),
}),
])
@@ -153,7 +174,7 @@ export function useOnValid({
refreshHostsAfterDelay()
}
- await revalidateAndResetFormData()
+ await revalidateAndResetForm()
} catch (e) {
triggerErrorToast((e as Error).message)
console.log(e)
@@ -164,17 +185,12 @@ export function useOnValid({
estimatedSpendingPerMonth,
showAdvanced,
isAutopilotEnabled,
- autopilot,
autopilotUpdate,
- revalidateAndResetFormData,
+ revalidateAndResetForm,
syncDefaultContractSet,
mutate,
settingUpdate,
- contractSet,
- uploadPacking,
- redundancy,
- gouging,
- display,
+ resources,
autopilotTrigger,
]
)
diff --git a/apps/renterd/contexts/config/useResources.tsx b/apps/renterd/contexts/config/useResources.tsx
index 517b71823..5d6974db6 100644
--- a/apps/renterd/contexts/config/useResources.tsx
+++ b/apps/renterd/contexts/config/useResources.tsx
@@ -1,28 +1,20 @@
import { minutesInMilliseconds } from '@siafoundation/design-system'
-import {
- useAutopilotConfig,
- useAutopilotConfigUpdate,
- useSettingUpdate,
-} from '@siafoundation/react-renterd'
+import { useAutopilotConfig } from '@siafoundation/react-renterd'
import { useSyncContractSet } from './useSyncContractSet'
import { useAppSettings } from '@siafoundation/react-core'
import { useContractSetSettings } from '../../hooks/useContractSetSettings'
-import { useConfigDisplayOptions } from '../../hooks/useConfigDisplayOptions'
+import { useConfigDisplaySettings } from '../../hooks/useConfigDisplaySettings'
import { useGougingSettings } from '../../hooks/useGougingSettings'
import { useRedundancySettings } from '../../hooks/useRedundancySettings'
import { useUploadPackingSettings } from '../../hooks/useUploadPackingSettings'
import { useSiaCentralHostsNetworkAverages } from '@siafoundation/react-sia-central'
-import useLocalStorageState from 'use-local-storage-state'
import { useApp } from '../app'
export function useResources() {
const app = useApp()
- const isAutopilotConfigured = app.autopilot.state.data?.configured
const isAutopilotEnabled = app.autopilot.status === 'on'
// settings that 404 when empty
const autopilot = useAutopilotConfig({
- disabled: !isAutopilotEnabled,
- standalone: 'configFormAutopilot',
config: {
swr: {
errorRetryCount: 0,
@@ -31,7 +23,6 @@ export function useResources() {
},
})
const contractSet = useContractSetSettings({
- standalone: 'configFormContractSet',
config: {
swr: {
errorRetryCount: 0,
@@ -39,8 +30,7 @@ export function useResources() {
},
},
})
- const display = useConfigDisplayOptions({
- standalone: 'configFormConfigApp',
+ const display = useConfigDisplaySettings({
config: {
swr: {
errorRetryCount: 0,
@@ -50,7 +40,6 @@ export function useResources() {
})
// settings with initial defaults
const gouging = useGougingSettings({
- standalone: 'configFormGouging',
config: {
swr: {
refreshInterval: minutesInMilliseconds(1),
@@ -58,7 +47,6 @@ export function useResources() {
},
})
const redundancy = useRedundancySettings({
- standalone: 'configFormRedundancy',
config: {
swr: {
refreshInterval: minutesInMilliseconds(1),
@@ -66,7 +54,6 @@ export function useResources() {
},
})
const uploadPacking = useUploadPackingSettings({
- standalone: 'configFormUploadPacking',
config: {
swr: {
refreshInterval: minutesInMilliseconds(1),
@@ -74,8 +61,6 @@ export function useResources() {
},
})
- const settingUpdate = useSettingUpdate()
-
const averages = useSiaCentralHostsNetworkAverages({
config: {
swr: {
@@ -83,41 +68,26 @@ export function useResources() {
},
},
})
- const [showAdvanced, setShowAdvanced] = useLocalStorageState(
- 'v0/config/showAdvanced',
- {
- defaultValue: false,
- }
- )
- const mode: 'advanced' | 'simple' = showAdvanced ? 'advanced' : 'simple'
const {
shouldSyncDefaultContractSet,
setShouldSyncDefaultContractSet,
syncDefaultContractSet,
} = useSyncContractSet()
- const autopilotUpdate = useAutopilotConfigUpdate()
const appSettings = useAppSettings()
return {
- app,
- isAutopilotConfigured,
- isAutopilotEnabled,
autopilot,
contractSet,
display,
gouging,
redundancy,
uploadPacking,
- settingUpdate,
averages,
- showAdvanced,
- setShowAdvanced,
- mode,
shouldSyncDefaultContractSet,
setShouldSyncDefaultContractSet,
syncDefaultContractSet,
- autopilotUpdate,
appSettings,
+ isAutopilotEnabled,
}
}
diff --git a/apps/renterd/hooks/useConfigDisplayOptions.tsx b/apps/renterd/hooks/useConfigDisplayOptions.tsx
deleted file mode 100644
index b9572b72d..000000000
--- a/apps/renterd/hooks/useConfigDisplayOptions.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { HookArgsSwr } from '@siafoundation/react-core'
-import { useSetting } from '@siafoundation/react-renterd'
-
-export const configDisplayOptionsKey = 'v0-config-display-options'
-
-export type ConfigDisplayOptions = {
- includeRedundancyMaxStoragePrice: boolean
- includeRedundancyMaxUploadPrice: boolean
-}
-
-export function useConfigDisplayOptions(
- args?: HookArgsSwr
-) {
- return useSetting({
- ...args,
- params: { key: configDisplayOptionsKey },
- })
-}
diff --git a/apps/renterd/hooks/useConfigDisplaySettings.tsx b/apps/renterd/hooks/useConfigDisplaySettings.tsx
new file mode 100644
index 000000000..288b97dec
--- /dev/null
+++ b/apps/renterd/hooks/useConfigDisplaySettings.tsx
@@ -0,0 +1,18 @@
+import { HookArgsSwr } from '@siafoundation/react-core'
+import { useSetting } from '@siafoundation/react-renterd'
+
+export const configDisplaySettingsKey = 'v0-config-display-options'
+
+export type ConfigDisplaySettings = {
+ includeRedundancyMaxStoragePrice: boolean
+ includeRedundancyMaxUploadPrice: boolean
+}
+
+export function useConfigDisplaySettings(
+ args?: HookArgsSwr
+) {
+ return useSetting({
+ ...args,
+ params: { key: configDisplaySettingsKey },
+ })
+}
diff --git a/libs/design-system/src/form/useFormChangeCount.tsx b/libs/design-system/src/form/useFormChangeCount.tsx
new file mode 100644
index 000000000..8b3a0494c
--- /dev/null
+++ b/libs/design-system/src/form/useFormChangeCount.tsx
@@ -0,0 +1,19 @@
+'use client'
+
+import { FieldValues, UseFormReturn } from 'react-hook-form'
+
+type Props = {
+ form: UseFormReturn
+}
+
+export function useFormChangeCount({
+ form,
+}: Props) {
+ const changeCount = Object.entries(form.formState.dirtyFields).filter(
+ ([_, val]) => !!val
+ ).length
+
+ return {
+ changeCount,
+ }
+}
diff --git a/libs/design-system/src/form/useFormInit.tsx b/libs/design-system/src/form/useFormInit.tsx
new file mode 100644
index 000000000..de8b90a06
--- /dev/null
+++ b/libs/design-system/src/form/useFormInit.tsx
@@ -0,0 +1,37 @@
+'use client'
+
+import { useAppSettings } from '@siafoundation/react-core'
+import { useEffect, useState } from 'react'
+import { FieldValues, UseFormReturn } from 'react-hook-form'
+
+type Props = {
+ form: UseFormReturn
+ remoteValues?: DataForm
+}
+
+// initializes form when the rmoteValues first become available
+// and resets init when the user locks the app
+export function useFormInit({
+ form,
+ remoteValues,
+}: Props) {
+ const [init, setInit] = useState(false)
+
+ // reset init when the user locks the app
+ const { isUnlockedAndAuthedRoute } = useAppSettings()
+ useEffect(() => {
+ if (!isUnlockedAndAuthedRoute) {
+ setInit(false)
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isUnlockedAndAuthedRoute])
+
+ // reset form when needs init and the remoteValues become available
+ useEffect(() => {
+ if (!init && remoteValues) {
+ setInit(true)
+ form.reset(remoteValues)
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [remoteValues])
+}
diff --git a/libs/design-system/src/form/useFormServerSynced.tsx b/libs/design-system/src/form/useFormServerSynced.tsx
new file mode 100644
index 000000000..8d5201883
--- /dev/null
+++ b/libs/design-system/src/form/useFormServerSynced.tsx
@@ -0,0 +1,37 @@
+'use client'
+
+import { useCallback, useEffect } from 'react'
+import { FieldValues, UseFormReturn } from 'react-hook-form'
+
+type Props = {
+ form: UseFormReturn
+ remoteValues?: DataForm
+}
+
+export function useFormServerSynced({
+ form,
+ remoteValues,
+}: Props) {
+ // Syncs updated remote data and re-applies user changes to the form.
+ const syncRemoteDataWithUserChanges = useCallback(() => {
+ if (form.formState.isSubmitting || !remoteValues) {
+ return
+ }
+ const localValues = form.getValues()
+ form.reset(remoteValues)
+ for (const [key, value] of Object.entries(localValues)) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ form.setValue(key as any, value, {
+ shouldDirty: true,
+ })
+ }
+ }, [form, remoteValues])
+
+ useEffect(() => {
+ syncRemoteDataWithUserChanges()
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [
+ // if any of the remote data changes, trigger a sync
+ remoteValues,
+ ])
+}
diff --git a/libs/design-system/src/form/useServerSyncedForm.tsx b/libs/design-system/src/form/useServerSyncedForm.tsx
deleted file mode 100644
index fe63e75b9..000000000
--- a/libs/design-system/src/form/useServerSyncedForm.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-'use client'
-
-import { useCallback, useEffect, useState } from 'react'
-import { FieldValues, UseFormReturn } from 'react-hook-form'
-
-type Props = {
- form: UseFormReturn
- remoteValues?: DataForm
- revalidate: () => Promise
- mode?: 'simple' | 'advanced'
- // used to reset the init process
- initialized?: boolean
-}
-
-export function useServerSyncedForm({
- form,
- remoteValues,
- revalidate,
- mode,
- initialized,
-}: Props) {
- const changeCount = Object.entries(form.formState.dirtyFields).filter(
- ([_, val]) => !!val
- ).length
-
- const revalidateAndResetFormData = useCallback(async () => {
- const remoteData = await revalidate()
- if (remoteData) {
- form.reset(remoteData)
- }
- }, [form, revalidate])
-
- // init - when new config is fetched, set the form
- const [hasInit, setHasInit] = useState(false)
- useEffect(() => {
- if (!initialized) {
- setHasInit(false)
- }
- }, [initialized])
-
- useEffect(() => {
- if (!hasInit && remoteValues) {
- form.reset(remoteValues)
- setHasInit(true)
- }
- // Try to initialize whenever remoteData changes from null to a value
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [remoteValues])
-
- // Syncs updated remote data and re-applies user changes to the form.
- const syncRemoteDataWithUserChanges = useCallback(() => {
- if (!hasInit || form.formState.isSubmitting || !remoteValues) {
- return
- }
- const localValues = form.getValues()
- form.reset(remoteValues)
- for (const [key, value] of Object.entries(localValues)) {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- form.setValue(key as any, value, {
- shouldDirty: true,
- })
- }
- }, [form, hasInit, remoteValues])
-
- useEffect(() => {
- syncRemoteDataWithUserChanges()
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [
- // if form mode is toggled reset
- mode,
- // if any of the remote data changes, trigger a sync
- remoteValues,
- ])
-
- return {
- changeCount,
- revalidateAndResetFormData,
- }
-}
diff --git a/libs/design-system/src/index.ts b/libs/design-system/src/index.ts
index 1f01d62b9..c78b86a80 100644
--- a/libs/design-system/src/index.ts
+++ b/libs/design-system/src/index.ts
@@ -123,7 +123,9 @@ export * from './form/FieldText'
export * from './form/FieldTextArea'
export * from './form/FieldSwitch'
export * from './form/FieldSelect'
-export * from './form/useServerSyncedForm'
+export * from './form/useFormServerSynced'
+export * from './form/useFormChangeCount'
+export * from './form/useFormInit'
// site
export * from './site/SiteHeading'