From 30fb33f3bff8a4b9ee4ba4b806f437d05ee0f92b Mon Sep 17 00:00:00 2001 From: Alex Freska Date: Fri, 18 Aug 2023 15:28:56 -0400 Subject: [PATCH] feat: renterd autopilot and not enough contract onboarding --- .changeset/odd-pillows-try.md | 5 + apps/renterd/components/Files/EmptyState.tsx | 79 +++++++ .../components/Files/FilesExplorer.tsx | 26 +-- .../FilesStatsMenu/FilesStatsMenuWarnings.tsx | 198 ++++++++++-------- .../checks/useAutopilotNotConfigured.tsx | 17 ++ .../Files/checks/useContractSetMismatch.tsx | 15 ++ .../checks/useDefaultContractSetNotSet.tsx | 9 + .../Files/checks/useNotEnoughContracts.tsx | 22 ++ libs/react-renterd/src/worker.ts | 2 +- 9 files changed, 268 insertions(+), 105 deletions(-) create mode 100644 .changeset/odd-pillows-try.md create mode 100644 apps/renterd/components/Files/EmptyState.tsx create mode 100644 apps/renterd/components/Files/checks/useAutopilotNotConfigured.tsx create mode 100644 apps/renterd/components/Files/checks/useContractSetMismatch.tsx create mode 100644 apps/renterd/components/Files/checks/useDefaultContractSetNotSet.tsx create mode 100644 apps/renterd/components/Files/checks/useNotEnoughContracts.tsx diff --git a/.changeset/odd-pillows-try.md b/.changeset/odd-pillows-try.md new file mode 100644 index 000000000..dc077238c --- /dev/null +++ b/.changeset/odd-pillows-try.md @@ -0,0 +1,5 @@ +--- +'renterd': minor +--- + +New users are now more clearly instructed to configure autopilot and to wait for enough contracts before files can be uploaded. diff --git a/apps/renterd/components/Files/EmptyState.tsx b/apps/renterd/components/Files/EmptyState.tsx new file mode 100644 index 000000000..18175ca67 --- /dev/null +++ b/apps/renterd/components/Files/EmptyState.tsx @@ -0,0 +1,79 @@ +import { CloudUpload32, LinkButton, Text } from '@siafoundation/design-system' +import { routes } from '../../config/routes' +import { useFiles } from '../../contexts/files' +import { useAutopilotNotConfigured } from './checks/useAutopilotNotConfigured' +import { useNotEnoughContracts } from './checks/useNotEnoughContracts' +import { StateError } from './StateError' +import { StateNoneMatching } from './StateNoneMatching' +import { StateNoneYet } from './StateNoneYet' + +export function EmptyState() { + const { dataState, activeDirectoryPath } = useFiles() + + const autopilotNotConfigured = useAutopilotNotConfigured() + const notEnoughContracts = useNotEnoughContracts() + + if (dataState === 'noneMatchingFilters') { + return + } + + if (dataState === 'error') { + return + } + + // only show on root directory and when there are no files + if ( + activeDirectoryPath === '/' && + dataState === 'noneYet' && + autopilotNotConfigured.active + ) { + return ( +
+ + + +
+ + Before you can upload files you must configure autopilot. Autopilot + finds contracts with hosts based on the settings you choose. + Autopilot also repairs your data as hosts come and go. + + + Configure autopilot → + +
+
+ ) + } + + // only show on root directory and when there are no files + if ( + activeDirectoryPath === '/' && + dataState === 'noneYet' && + notEnoughContracts.active + ) { + return ( +
+ + + +
+ + There are not enough contracts to upload data yet. Redundancy is + configured to use {notEnoughContracts.required} shards which means + at least that many contracts are required. + + + {notEnoughContracts.count}/{notEnoughContracts.required} + +
+
+ ) + } + + if (dataState === 'noneYet') { + return + } + + return null +} diff --git a/apps/renterd/components/Files/FilesExplorer.tsx b/apps/renterd/components/Files/FilesExplorer.tsx index 1cff5a056..a2be66864 100644 --- a/apps/renterd/components/Files/FilesExplorer.tsx +++ b/apps/renterd/components/Files/FilesExplorer.tsx @@ -1,8 +1,8 @@ import { Table, Dropzone } from '@siafoundation/design-system' import { useFiles } from '../../contexts/files' -import { StateError } from './StateError' -import { StateNoneMatching } from './StateNoneMatching' -import { StateNoneYet } from './StateNoneYet' +import { useAutopilotNotConfigured } from './checks/useAutopilotNotConfigured' +import { useNotEnoughContracts } from './checks/useNotEnoughContracts' +import { EmptyState } from './EmptyState' export function FilesExplorer() { const { @@ -16,20 +16,20 @@ export function FilesExplorer() { sortableColumns, toggleSort, } = useFiles() + const autopilotNotConfigured = useAutopilotNotConfigured() + const notEnoughContracts = useNotEnoughContracts() + const uploadsDisabled = + autopilotNotConfigured.active || notEnoughContracts.active return (
- 0}> + 0} + noDrag={uploadsDisabled} + > - ) : dataState === 'noneYet' ? ( - - ) : dataState === 'error' ? ( - - ) : null - } + emptyState={} pageSize={10} data={datasetPage} columns={columns} diff --git a/apps/renterd/components/Files/FilesStatsMenu/FilesStatsMenuWarnings.tsx b/apps/renterd/components/Files/FilesStatsMenu/FilesStatsMenuWarnings.tsx index 7a7dc7f72..1e7356287 100644 --- a/apps/renterd/components/Files/FilesStatsMenu/FilesStatsMenuWarnings.tsx +++ b/apps/renterd/components/Files/FilesStatsMenu/FilesStatsMenuWarnings.tsx @@ -1,106 +1,122 @@ import { Link, Text, Tooltip, Warning16 } from '@siafoundation/design-system' -import { useAutopilotConfig } from '@siafoundation/react-renterd' -import { useIsApcsEqDcs } from '../../../hooks/useIsApcsEqDcs' +import { useFiles } from '../../../contexts/files' import { routes } from '../../../config/routes' -import { useContractSetSettings } from '../../../hooks/useContractSetSettings' -import { useApp } from '../../../contexts/app' +import { useContractSetMismatch } from '../checks/useContractSetMismatch' +import { useDefaultContractSetNotSet } from '../checks/useDefaultContractSetNotSet' +import { useAutopilotNotConfigured } from '../checks/useAutopilotNotConfigured' +import { useNotEnoughContracts } from '../checks/useNotEnoughContracts' export function FilesStatsMenuWarnings() { - const { autopilot } = useApp() - const apc = useAutopilotConfig({ - config: { - swr: { - errorRetryCount: 0, - }, - }, - }) - const css = useContractSetSettings() - const isApcsEqDcs = useIsApcsEqDcs() + const { dataState, activeDirectoryPath } = useFiles() + const contractSetMismatch = useContractSetMismatch() + const defaultContractSetNotSet = useDefaultContractSetNotSet() + const autopilotNotConfigured = useAutopilotNotConfigured() + const notEnoughContracts = useNotEnoughContracts() - let warning = 'none' - - if ( - autopilot.status === 'on' && - !isApcsEqDcs.isValidating && - !isApcsEqDcs.data - ) { - warning = 'contractSetMismatch' + // onboard/warn about default contract set + if (defaultContractSetNotSet.active) { + return ( +
+ + + + + Configure a default contract set to get started.{' '} + + Configuration → + + +
+ ) } - if (autopilot.status === 'on' && apc.error) { - warning = 'setupAutopilot' - } - - if (css.data && !css.data?.default) { - warning = 'setupDefaultContractSet' - } - - return ( - <> - {warning === 'setupDefaultContractSet' && ( -
- - - - - Configure a default contract set to get started.{' '} - - Configuration → - - -
- )} - {warning === 'setupAutopilot' && ( + // warn about contract set mismatch + if (contractSetMismatch.active) { + return ( + + The autopilot contract set does not match the default contract set. + This means that by default workers will not upload data to contracts + that autopilot manages. Unless these contract are being manually + maintained, this will result in data loss. Continue with caution or + update the autopilot contract set to match the default contract set. + + } + >
- Configure autopilot to get started.{' '} - - Autopilot → - + Uploaded data will not be managed by autopilot.
- )} - {warning === 'contractSetMismatch' && ( - - The autopilot contract set does not match the default contract - set. This means that by default workers will not upload data to - contracts that autopilot manages. Unless these contract are being - manually maintained, this will result in data loss. Continue with - caution or update the autopilot contract set to match the default - contract set. - - } - > -
- - - - - Uploaded data will not be managed by autopilot. - -
-
- )} - - ) +
+ ) + } + + // only show if not on the root directory because the explorer empty state shows the same info + const notEnoughContractsRootDirectory = + notEnoughContracts.active && + activeDirectoryPath === '/' && + dataState !== 'noneYet' + const notEnoughContractsNotRootDirectory = + notEnoughContracts.active && activeDirectoryPath !== '/' + if (notEnoughContractsRootDirectory || notEnoughContractsNotRootDirectory) { + return ( +
+ + + + + Not enought contracts to upload files. {notEnoughContracts.count}/ + {notEnoughContracts.required} + +
+ ) + } + + // only show if not on the root directory because the explorer empty state shows the same info + const autopilotNotConfiguredRootDirectory = + autopilotNotConfigured.active && + activeDirectoryPath === '/' && + dataState !== 'noneYet' + const autopilotNotConfiguredNotRootDirectory = + autopilotNotConfigured.active && activeDirectoryPath !== '/' + if ( + autopilotNotConfiguredRootDirectory || + autopilotNotConfiguredNotRootDirectory + ) { + return ( +
+ + + + + Configure autopilot to get started.{' '} + + Autopilot → + + +
+ ) + } + + return null } diff --git a/apps/renterd/components/Files/checks/useAutopilotNotConfigured.tsx b/apps/renterd/components/Files/checks/useAutopilotNotConfigured.tsx new file mode 100644 index 000000000..316c60ed6 --- /dev/null +++ b/apps/renterd/components/Files/checks/useAutopilotNotConfigured.tsx @@ -0,0 +1,17 @@ +import { useAutopilotConfig } from '@siafoundation/react-renterd' +import { useApp } from '../../../contexts/app' + +export function useAutopilotNotConfigured() { + const { autopilot } = useApp() + const apc = useAutopilotConfig({ + config: { + swr: { + errorRetryCount: 0, + }, + }, + }) + + return { + active: autopilot.status === 'on' && !!apc.error, + } +} diff --git a/apps/renterd/components/Files/checks/useContractSetMismatch.tsx b/apps/renterd/components/Files/checks/useContractSetMismatch.tsx new file mode 100644 index 000000000..3fa0fe3a7 --- /dev/null +++ b/apps/renterd/components/Files/checks/useContractSetMismatch.tsx @@ -0,0 +1,15 @@ +import { useIsApcsEqDcs } from '../../../hooks/useIsApcsEqDcs' +import { useApp } from '../../../contexts/app' + +export function useContractSetMismatch() { + const { autopilot } = useApp() + const isApcsEqDcs = useIsApcsEqDcs() + + // warn about contract set mismatch + const active = + autopilot.status === 'on' && !isApcsEqDcs.isValidating && !isApcsEqDcs.data + + return { + active, + } +} diff --git a/apps/renterd/components/Files/checks/useDefaultContractSetNotSet.tsx b/apps/renterd/components/Files/checks/useDefaultContractSetNotSet.tsx new file mode 100644 index 000000000..628e3a860 --- /dev/null +++ b/apps/renterd/components/Files/checks/useDefaultContractSetNotSet.tsx @@ -0,0 +1,9 @@ +import { useContractSetSettings } from '../../../hooks/useContractSetSettings' + +export function useDefaultContractSetNotSet() { + const css = useContractSetSettings() + + return { + active: css.data && !css.data?.default, + } +} diff --git a/apps/renterd/components/Files/checks/useNotEnoughContracts.tsx b/apps/renterd/components/Files/checks/useNotEnoughContracts.tsx new file mode 100644 index 000000000..5e1dd44bc --- /dev/null +++ b/apps/renterd/components/Files/checks/useNotEnoughContracts.tsx @@ -0,0 +1,22 @@ +import { useContracts } from '../../../contexts/contracts' +import { useRedundancySettings } from '../../../hooks/useRedundancySettings' + +export function useNotEnoughContracts() { + const redundancy = useRedundancySettings({ + config: { + swr: { + // Do not automatically refetch + revalidateOnFocus: false, + }, + }, + }) + const { datasetCount } = useContracts() + + const active = redundancy.data && datasetCount < redundancy.data.totalShards + + return { + active, + count: datasetCount, + required: redundancy.data?.totalShards || 0, + } +} diff --git a/libs/react-renterd/src/worker.ts b/libs/react-renterd/src/worker.ts index 291ab5aeb..497e00732 100644 --- a/libs/react-renterd/src/worker.ts +++ b/libs/react-renterd/src/worker.ts @@ -19,7 +19,7 @@ type WorkerState = StateResponse & { const workerStateKey = '/worker/state' -export function useBusState(args?: HookArgsSwr) { +export function useWorkerState(args?: HookArgsSwr) { return useGetSwr({ ...args, route: workerStateKey,