Skip to content

Commit

Permalink
feat: renterd autopilot and not enough contract onboarding
Browse files Browse the repository at this point in the history
  • Loading branch information
alexfreska committed Aug 18, 2023
1 parent 2659481 commit f669717
Show file tree
Hide file tree
Showing 13 changed files with 284 additions and 107 deletions.
5 changes: 5 additions & 0 deletions .changeset/dull-readers-sit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'renterd': minor
---

File upload and directory creation are now disabled until enough contracts are formed.
5 changes: 5 additions & 0 deletions .changeset/odd-pillows-try.md
Original file line number Diff line number Diff line change
@@ -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.
79 changes: 79 additions & 0 deletions apps/renterd/components/Files/EmptyState.tsx
Original file line number Diff line number Diff line change
@@ -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 <StateNoneMatching />
}

if (dataState === 'error') {
return <StateError />
}

// only show on root directory and when there are no files
if (
activeDirectoryPath === '/' &&
dataState === 'noneYet' &&
autopilotNotConfigured.active
) {
return (
<div className="flex flex-col gap-10 justify-center items-center h-[400px] cursor-pointer">
<Text>
<CloudUpload32 className="scale-[200%]" />
</Text>
<div className="flex flex-col gap-6 justify-center items-center">
<Text color="subtle" className="text-center max-w-[500px]">
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.
</Text>
<LinkButton variant="accent" href={routes.autopilot.index}>
Configure autopilot →
</LinkButton>
</div>
</div>
)
}

// only show on root directory and when there are no files
if (
activeDirectoryPath === '/' &&
dataState === 'noneYet' &&
notEnoughContracts.active
) {
return (
<div className="flex flex-col gap-12 justify-center items-center h-[400px] cursor-pointer">
<Text>
<CloudUpload32 className="scale-[200%]" />
</Text>
<div className="flex flex-col gap-4 justify-center items-center">
<Text color="subtle" className="text-center max-w-[500px]">
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.
</Text>
<Text size="30" className="text-center max-w-[500px]">
{notEnoughContracts.count}/{notEnoughContracts.required}
</Text>
</div>
</div>
)
}

if (dataState === 'noneYet') {
return <StateNoneYet />
}

return null
}
7 changes: 6 additions & 1 deletion apps/renterd/components/Files/FilesActionsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ import { useFiles } from '../../contexts/files'
import { useDropzone } from 'react-dropzone'
import { FilesViewDropdownMenu } from './FilesViewDropdownMenu'
import { useDialog } from '../../contexts/dialog'
import { useCanUpload } from './useCanUpload'

export function FilesActionsMenu() {
const { openDialog } = useDialog()
const { uploadFiles } = useFiles()

const canUpload = useCanUpload()

const { getRootProps, getInputProps } = useDropzone({
noDrag: true,
noClick: !canUpload,
onDrop: uploadFiles,
})

Expand All @@ -23,11 +27,12 @@ export function FilesActionsMenu() {
<Button onClick={() => openDialog('filesSearch')} tip="Search files">
<Search16 />
</Button>
<Button {...getRootProps()} tip="Upload files">
<Button {...getRootProps()} tip="Upload files" disabled={!canUpload}>
<input {...getInputProps()} />
<CloudUpload16 />
</Button>
<Button
disabled={!canUpload}
onClick={() => openDialog('filesCreateDirectory')}
tip="Create directory"
>
Expand Down
22 changes: 9 additions & 13 deletions apps/renterd/components/Files/FilesExplorer.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
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 { EmptyState } from './EmptyState'
import { useCanUpload } from './useCanUpload'

export function FilesExplorer() {
const {
Expand All @@ -16,20 +15,17 @@ export function FilesExplorer() {
sortableColumns,
toggleSort,
} = useFiles()
const canUpload = useCanUpload()
return (
<div className="relative">
<Dropzone onDrop={uploadFiles} noClick={pageCount > 0}>
<Dropzone
onDrop={uploadFiles}
noClick={!canUpload || pageCount > 0}
noDrag={!canUpload}
>
<Table
isLoading={dataState === 'loading'}
emptyState={
dataState === 'noneMatchingFilters' ? (
<StateNoneMatching />
) : dataState === 'noneYet' ? (
<StateNoneYet />
) : dataState === 'error' ? (
<StateError />
) : null
}
emptyState={<EmptyState />}
pageSize={10}
data={datasetPage}
columns={columns}
Expand Down
198 changes: 107 additions & 91 deletions apps/renterd/components/Files/FilesStatsMenu/FilesStatsMenuWarnings.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex gap-1">
<Text size="12" font="mono" weight="medium" color="amber">
<Warning16 />
</Text>
<Text size="12" font="mono" weight="medium" color="amber">
Configure a default contract set to get started.{' '}
<Link
underline="hover"
size="12"
font="mono"
weight="medium"
color="amber"
href={routes.config.index}
>
Configuration →
</Link>
</Text>
</div>
)
}

if (autopilot.status === 'on' && apc.error) {
warning = 'setupAutopilot'
}

if (css.data && !css.data?.default) {
warning = 'setupDefaultContractSet'
}

return (
<>
{warning === 'setupDefaultContractSet' && (
<div className="flex gap-1">
<Text size="12" font="mono" weight="medium" color="amber">
<Warning16 />
</Text>
<Text size="12" font="mono" weight="medium" color="amber">
Configure a default contract set to get started.{' '}
<Link
underline="hover"
size="12"
font="mono"
weight="medium"
color="amber"
href={routes.config.index}
>
Configuration →
</Link>
</Text>
</div>
)}
{warning === 'setupAutopilot' && (
// warn about contract set mismatch
if (contractSetMismatch.active) {
return (
<Tooltip
align="start"
content={
<>
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.
</>
}
>
<div className="flex gap-1">
<Text size="12" font="mono" weight="medium" color="amber">
<Warning16 />
</Text>
<Text size="12" font="mono" weight="medium" color="amber">
Configure autopilot to get started.{' '}
<Link
underline="hover"
size="12"
font="mono"
weight="medium"
color="amber"
href={routes.autopilot.index}
>
Autopilot →
</Link>
Uploaded data will not be managed by autopilot.
</Text>
</div>
)}
{warning === 'contractSetMismatch' && (
<Tooltip
align="start"
content={
<>
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.
</>
}
>
<div className="flex gap-1">
<Text size="12" font="mono" weight="medium" color="amber">
<Warning16 />
</Text>
<Text size="12" font="mono" weight="medium" color="amber">
Uploaded data will not be managed by autopilot.
</Text>
</div>
</Tooltip>
)}
</>
)
</Tooltip>
)
}

// 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 (
<div className="flex gap-1">
<Text size="12" font="mono" weight="medium" color="amber">
<Warning16 />
</Text>
<Text size="12" font="mono" weight="medium" color="amber">
Configure autopilot to get started.{' '}
<Link
underline="hover"
size="12"
font="mono"
weight="medium"
color="amber"
href={routes.autopilot.index}
>
Autopilot →
</Link>
</Text>
</div>
)
}

// 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 (
<div className="flex gap-1">
<Text size="12" font="mono" weight="medium" color="amber">
<Warning16 />
</Text>
<Text size="12" font="mono" weight="medium" color="amber">
Not enought contracts to upload files. {notEnoughContracts.count}/
{notEnoughContracts.required}
</Text>
</div>
)
}

return null
}
17 changes: 17 additions & 0 deletions apps/renterd/components/Files/checks/useAutopilotNotConfigured.tsx
Original file line number Diff line number Diff line change
@@ -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,
}
}
Loading

0 comments on commit f669717

Please sign in to comment.