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'