fix: filemanager state handling (#232)

* fix: filemanager state handling
* refactor: fm provider and fm page
* fix: detect bee warmup and wait more for syncing
* refactor: optimize bee provider to avoid rerenders
---------

Co-authored-by: Roland Seres <roland.seres90@gmail.com>
This commit is contained in:
Bálint Ujvári
2026-03-19 16:03:25 +01:00
parent 55e7879849
commit 855a017033
5 changed files with 334 additions and 290 deletions
@@ -1,4 +1,4 @@
import React, { ReactElement } from 'react' import React, { ReactElement, useLayoutEffect, useState } from 'react'
import { createPortal } from 'react-dom' import { createPortal } from 'react-dom'
import { Button } from '../Button/Button' import { Button } from '../Button/Button'
@@ -35,7 +35,11 @@ export function ConfirmModal({
onMinimize, onMinimize,
background = true, background = true,
}: ConfirmModalProps): ReactElement { }: ConfirmModalProps): ReactElement {
const modalRoot = document.querySelector('.fm-main') || document.body const [modalRoot, setModalRoot] = useState<Element>(() => document.querySelector('.fm-main') || document.body)
useLayoutEffect(() => {
setModalRoot(document.querySelector('.fm-main') || document.body)
}, [])
return createPortal( return createPortal(
<div className={`fm-modal-container fm-confirm-modal ${background ? '' : 'fm-modal-no-background'}`}> <div className={`fm-modal-container fm-confirm-modal ${background ? '' : 'fm-modal-no-background'}`}>
@@ -20,7 +20,6 @@ import './InitialModal.scss'
interface InitialModalProps { interface InitialModalProps {
resetState: boolean resetState: boolean
handleVisibility: (isVisible: boolean) => void
handleShowError: (flag: boolean, errorMessage?: string) => void handleShowError: (flag: boolean, errorMessage?: string) => void
setIsCreationInProgress: (isCreating: boolean) => void setIsCreationInProgress: (isCreating: boolean) => void
} }
@@ -65,7 +64,6 @@ const setSecurityLevel = (setter: (value: RedundancyLevel) => void) => {
export function InitialModal({ export function InitialModal({
resetState, resetState,
setIsCreationInProgress, setIsCreationInProgress,
handleVisibility,
handleShowError, handleShowError,
}: InitialModalProps): ReactElement { }: InitialModalProps): ReactElement {
const [isCreateEnabled, setIsCreateEnabled] = useState(false) const [isCreateEnabled, setIsCreateEnabled] = useState(false)
@@ -133,7 +131,6 @@ export function InitialModal({
const createAdminDrive = useCallback(async () => { const createAdminDrive = useCallback(async () => {
setIsCreationInProgress?.(true) setIsCreationInProgress?.(true)
handleVisibility(false)
await handleCreateDrive({ await handleCreateDrive({
beeApi, beeApi,
@@ -148,7 +145,6 @@ export function InitialModal({
resetState, resetState,
existingBatch: selectedBatch, existingBatch: selectedBatch,
onSuccess: () => { onSuccess: () => {
handleVisibility(false)
setIsCreationInProgress(false) setIsCreationInProgress(false)
}, },
onError: err => { onError: err => {
@@ -164,7 +160,6 @@ export function InitialModal({
validityEndDate, validityEndDate,
erasureCodeLevel, erasureCodeLevel,
selectedBatch, selectedBatch,
handleVisibility,
handleShowError, handleShowError,
setIsCreationInProgress, setIsCreationInProgress,
resetState, resetState,
+69 -86
View File
@@ -20,7 +20,6 @@ import { getSignerPk, removeSignerPk } from '@/modules/filemanager/utils/common'
import { CheckState, Context as BeeContext } from '@/providers/Bee' import { CheckState, Context as BeeContext } from '@/providers/Bee'
import { Context as FMContext } from '@/providers/FileManager' import { Context as FMContext } from '@/providers/FileManager'
import { BrowserPlatform, cacheClearUrls, detectBrowser } from '@/providers/Platform' import { BrowserPlatform, cacheClearUrls, detectBrowser } from '@/providers/Platform'
import { Context as SettingsContext } from '@/providers/Settings'
function PrivateKeyModalBlock({ onSaved }: { onSaved: () => void }) { function PrivateKeyModalBlock({ onSaved }: { onSaved: () => void }) {
return ( return (
@@ -74,7 +73,6 @@ function ResetModalBlock({ cacheHelpUrl, onConfirm }: { cacheHelpUrl: string; on
function InitialModalBlock(props: { function InitialModalBlock(props: {
resetState: boolean resetState: boolean
handleVisibility: (isVisible: boolean) => void
handleShowError: (flag: boolean, error?: string) => void handleShowError: (flag: boolean, error?: string) => void
setIsCreationInProgress: (isCreating: boolean) => void setIsCreationInProgress: (isCreating: boolean) => void
}) { }) {
@@ -153,22 +151,29 @@ function FileManagerMainContent(props: {
) )
} }
enum PageState {
Connecting = 'connecting', // still warming up — show nothing / loader
NoPrivateKey = 'no-pk', // private key not set
Loading = 'loading', // bee ready, pk present, FM init in progress
Reset = 'reset', // STATE_INVALID emitted and user has not yet acknowledged
InitError = 'init-error', // FM init completed with an error (non-reset case)
Initial = 'initial', // FM ready but no admin stamp/drive → show InitialModal
AdminError = 'admin-error', // drive creation failed
Ready = 'ready', // fully operational
}
export function FileManagerPage(): ReactElement { export function FileManagerPage(): ReactElement {
const isMountedRef = useRef(true) const isMountedRef = useRef(true)
const [showInitialModal, setShowInitialModal] = useState(false)
const [isLoading, setIsLoading] = useState(true)
const [hasAdminDrive, setHasAdminDrive] = useState(false)
const [hasPk, setHasPk] = useState<boolean>(getSignerPk() !== undefined) const [hasPk, setHasPk] = useState<boolean>(getSignerPk() !== undefined)
const [showErrorModal, setShowErrorModal] = useState<boolean>(false) const [showAdminErrorModal, setAdminShowErrorModal] = useState<boolean>(false)
const [errorMessage, setErrorMessage] = useState<string>('') const [errorMessage, setErrorMessage] = useState<string>('')
const [showResetModal, setShowResetModal] = useState<boolean>(false) const [resetAcknowledged, setResetAcknowledged] = useState<boolean>(false)
const [isCreationInProgress, setIsCreationInProgress] = useState<boolean>(false) const [isCreationInProgress, setIsCreationInProgress] = useState<boolean>(false)
const [showConnectionError, setShowConnectionError] = useState<boolean>(false) const [connectionErrorDismissed, setConnectionErrorDismissed] = useState<boolean>(false)
const [cacheHelpUrl, setCacheHelpUrl] = useState<string>(cacheClearUrls[BrowserPlatform.Chrome]) const [cacheHelpUrl, setCacheHelpUrl] = useState<string>(cacheClearUrls[BrowserPlatform.Chrome])
const { status } = useContext(BeeContext) const { status } = useContext(BeeContext)
const { beeApi } = useContext(SettingsContext) const { fm, initDone, shallReset, adminDrive, initializationError, notifyPkSaved } = useContext(FMContext)
const { fm, shallReset, adminDrive, initializationError, init } = useContext(FMContext)
useEffect(() => { useEffect(() => {
isMountedRef.current = true isMountedRef.current = true
@@ -185,93 +190,76 @@ export function FileManagerPage(): ReactElement {
} }
}, []) }, [])
useEffect(() => { const { isBeeReady, isConnectionError } = useMemo(() => {
const isApiError = status.apiConnection.checkState !== CheckState.OK || !status.apiConnection.isEnabled const isConnecting = status.all === CheckState.CONNECTING
setShowConnectionError(isApiError) const isApiOk = status.apiConnection.isEnabled && status.apiConnection.checkState === CheckState.OK
}, [status.apiConnection])
return {
isBeeReady: !isConnecting && isApiOk,
isConnectionError: !isConnecting && !isApiOk && Boolean(fm),
}
}, [status, fm])
useEffect(() => { useEffect(() => {
if (!beeApi) { if (!isConnectionError) {
return setConnectionErrorDismissed(false)
} }
}, [isConnectionError])
if (!hasPk) { const pageState = useMemo((): PageState => {
setIsLoading(false) if (!isBeeReady && !initDone) return PageState.Connecting
return if (!hasPk) return PageState.NoPrivateKey
}
setShowResetModal(shallReset) if (!initDone) return PageState.Loading
if (shallReset) { if (shallReset && !resetAcknowledged) return PageState.Reset
setShowInitialModal(true)
return if (initializationError && !shallReset) return PageState.InitError
}
if (initializationError) { if (showAdminErrorModal) return PageState.AdminError
setIsLoading(false)
return const hasAdminStamp = Boolean(fm?.adminStamp)
} const hasAdminDrive = Boolean(adminDrive)
if (fm) { if (!hasAdminStamp && !hasAdminDrive && !isCreationInProgress) return PageState.Initial
const hasAdminStamp = Boolean(fm.adminStamp)
const tmpHasAdminDrive = Boolean(adminDrive)
setHasAdminDrive(hasAdminStamp || tmpHasAdminDrive)
setIsLoading(false)
setShowInitialModal(!(hasAdminStamp || tmpHasAdminDrive)) return PageState.Ready
}, [
isBeeReady,
hasPk,
initDone,
shallReset,
resetAcknowledged,
initializationError,
showAdminErrorModal,
fm,
adminDrive,
isCreationInProgress,
])
return const handlePrivateKeySaved = useCallback(() => {
}
setIsLoading(true)
}, [fm, beeApi, hasPk, initializationError, adminDrive, shallReset])
const handlePrivateKeySaved = useCallback(async () => {
if (!isMountedRef.current) return if (!isMountedRef.current) return
setHasPk(true) setHasPk(true)
if (fm) { if (fm) return
if (!isMountedRef.current) return
setIsLoading(false) notifyPkSaved()
}, [fm, notifyPkSaved])
return
}
setIsLoading(true)
const manager = await init()
if (!isMountedRef.current) return
setIsLoading(false)
const hasAdminStamp = Boolean(manager?.adminStamp)
const tmpHasAdminDrive = Boolean(adminDrive)
setShowInitialModal(!(hasAdminStamp || tmpHasAdminDrive))
}, [fm, adminDrive, init])
const isEmptyState = useMemo(() => {
return showInitialModal && !isLoading && !hasAdminDrive && !isCreationInProgress
}, [showInitialModal, isLoading, hasAdminDrive, isCreationInProgress])
const isInvalidState = useMemo(
() => shallReset && fm && !isCreationInProgress,
[shallReset, fm, isCreationInProgress],
)
const loading = !fm?.adminStamp || !adminDrive const loading = !fm?.adminStamp || !adminDrive
const isFormbricksActive = Boolean(fm && fm.adminStamp && adminDrive && !loading)
const isFormbricksActive = Boolean(fm && fm.adminStamp && adminDrive && !showInitialModal && !loading) if (pageState === PageState.Connecting || pageState === PageState.Loading) {
return <LoadingBlock />
}
if (!hasPk) { if (pageState === PageState.NoPrivateKey) {
return <PrivateKeyModalBlock onSaved={handlePrivateKeySaved} /> return <PrivateKeyModalBlock onSaved={handlePrivateKeySaved} />
} }
if (initializationError && !isLoading && !shallReset) { if (pageState === PageState.InitError) {
return ( return (
<InitializationErrorBlock <InitializationErrorBlock
onOk={() => { onOk={() => {
@@ -282,17 +270,16 @@ export function FileManagerPage(): ReactElement {
) )
} }
if (showResetModal) { if (pageState === PageState.Reset) {
return <ResetModalBlock cacheHelpUrl={cacheHelpUrl} onConfirm={() => setShowResetModal(false)} /> return <ResetModalBlock cacheHelpUrl={cacheHelpUrl} onConfirm={() => setResetAcknowledged(true)} />
} }
if (!showErrorModal && (isEmptyState || isInvalidState)) { if (pageState === PageState.Initial) {
return ( return (
<InitialModalBlock <InitialModalBlock
resetState={shallReset} resetState={shallReset}
handleVisibility={(isVisible: boolean) => setShowInitialModal(isVisible)}
handleShowError={(flag: boolean, error?: string) => { handleShowError={(flag: boolean, error?: string) => {
setShowErrorModal(flag) setAdminShowErrorModal(flag)
if (error) setErrorMessage(error) if (error) setErrorMessage(error)
}} }}
@@ -301,19 +288,15 @@ export function FileManagerPage(): ReactElement {
) )
} }
if (!fm) { if (pageState === PageState.AdminError) {
return <LoadingBlock />
}
if (showErrorModal) {
return ( return (
<ErrorModalBlock <ErrorModalBlock
label={ label={
errorMessage ||
'Error creating Admin Drive. Please try again. Possible causes include insufficient xDAI balance or a lost connection to the RPC.' 'Error creating Admin Drive. Please try again. Possible causes include insufficient xDAI balance or a lost connection to the RPC.'
} }
onClick={() => { onClick={() => {
setShowErrorModal(false) setAdminShowErrorModal(false)
setShowInitialModal(true)
setErrorMessage('') setErrorMessage('')
}} }}
/> />
@@ -323,8 +306,8 @@ export function FileManagerPage(): ReactElement {
return ( return (
<FileManagerMainContent <FileManagerMainContent
fm={fm} fm={fm}
showConnectionError={showConnectionError} showConnectionError={isConnectionError && !connectionErrorDismissed}
setShowConnectionError={() => setShowConnectionError(false)} setShowConnectionError={(show: boolean) => setConnectionErrorDismissed(!show)}
isFormbricksActive={isFormbricksActive} isFormbricksActive={isFormbricksActive}
errorMessage={errorMessage} errorMessage={errorMessage}
setErrorMessage={setErrorMessage} setErrorMessage={setErrorMessage}
+113 -114
View File
@@ -29,7 +29,7 @@ import { useLatestBeeRelease } from '../hooks/apiHooks'
import { Context as SettingsContext } from './Settings' import { Context as SettingsContext } from './Settings'
const LAUNCH_GRACE_PERIOD = 15_000 const LAUNCH_GRACE_PERIOD = 35_000
const REFRESH_WHEN_OK = 30_000 const REFRESH_WHEN_OK = 30_000
const REFRESH_WHEN_ERROR = 5_000 const REFRESH_WHEN_ERROR = 5_000
const TIMEOUT = 3_000 const TIMEOUT = 3_000
@@ -116,15 +116,22 @@ interface Props {
children: ReactNode children: ReactNode
} }
function getStatus( interface StatusProps {
nodeInfo: NodeInfo | null, nodeInfo: NodeInfo | null
apiHealth: boolean, apiHealth: boolean
topology: Topology | null, topology: Topology | null
chequebookAddress: ChequebookAddressResponse | null, isWarmingUp: boolean
chequebookBalance: ChequebookBalanceResponse | null, chequebookAddress: ChequebookAddressResponse | null
error: Error | null, chequebookBalance: ChequebookBalanceResponse | null
startedAt: number, error: Error | null
): Status { startedAt: number
}
function getStatus(props: StatusProps): Status {
const { nodeInfo, apiHealth, topology, isWarmingUp, chequebookAddress, chequebookBalance, error, startedAt } = {
...props,
}
const status: Status = { ...initialValues.status } const status: Status = { ...initialValues.status }
// API connection check // API connection check
@@ -143,15 +150,17 @@ function getStatus(
if (chequebookAddress?.chequebookAddress && chequebookBalance !== null) { if (chequebookAddress?.chequebookAddress && chequebookBalance !== null) {
status.chequebook.checkState = CheckState.OK status.chequebook.checkState = CheckState.OK
} else status.chequebook.checkState = CheckState.OK } else {
status.chequebook.checkState = CheckState.WARNING
}
} }
status.all = determineOverallStatus(status, startedAt) status.all = determineOverallStatus(status, isWarmingUp, startedAt)
return status return status
} }
function determineOverallStatus(status: Status, startedAt: number): CheckState { function determineOverallStatus(status: Status, isWarmingUp: boolean, startedAt: number): CheckState {
const hasErrors = Object.values(status).some( const hasErrors = Object.values(status).some(
({ isEnabled, checkState }) => isEnabled && checkState === CheckState.ERROR, ({ isEnabled, checkState }) => isEnabled && checkState === CheckState.ERROR,
) )
@@ -160,15 +169,23 @@ function determineOverallStatus(status: Status, startedAt: number): CheckState {
) )
const isInGracePeriod = Date.now() - startedAt < LAUNCH_GRACE_PERIOD const isInGracePeriod = Date.now() - startedAt < LAUNCH_GRACE_PERIOD
if (hasErrors && isInGracePeriod) { if (isWarmingUp || isInGracePeriod) {
return CheckState.CONNECTING return CheckState.CONNECTING
} else if (hasErrors) {
return CheckState.ERROR
} else if (hasWarnings) {
return CheckState.WARNING
} else {
return CheckState.OK
} }
if (hasErrors) {
return CheckState.ERROR
}
if (hasWarnings) {
return CheckState.WARNING
}
return CheckState.OK
}
function getFulfilledValue<T>(result: PromiseSettledResult<T>): T | null {
return result.status === 'fulfilled' ? result.value : null
} }
// This does not need to be exposed and works much better as variable than state variable which may trigger some unnecessary re-renders // This does not need to be exposed and works much better as variable than state variable which may trigger some unnecessary re-renders
@@ -179,6 +196,7 @@ export function Provider({ children }: Props): ReactElement {
const [beeVersion, setBeeVersion] = useState<string | null>(null) const [beeVersion, setBeeVersion] = useState<string | null>(null)
const [apiHealth, setApiHealth] = useState<boolean>(false) const [apiHealth, setApiHealth] = useState<boolean>(false)
const [isWarmingUp, setIsWarmingUp] = useState<boolean>(true)
const [nodeAddresses, setNodeAddresses] = useState<NodeAddresses | null>(null) const [nodeAddresses, setNodeAddresses] = useState<NodeAddresses | null>(null)
const [nodeInfo, setNodeInfo] = useState<NodeInfo | null>(null) const [nodeInfo, setNodeInfo] = useState<NodeInfo | null>(null)
const [topology, setNodeTopology] = useState<Topology | null>(null) const [topology, setNodeTopology] = useState<Topology | null>(null)
@@ -191,7 +209,7 @@ export function Provider({ children }: Props): ReactElement {
const [settlements, setSettlements] = useState<AllSettlements | null>(null) const [settlements, setSettlements] = useState<AllSettlements | null>(null)
const [chainState, setChainState] = useState<ChainState | null>(null) const [chainState, setChainState] = useState<ChainState | null>(null)
const [walletBalance, setWalletBalance] = useState<WalletBalance | null>(null) const [walletBalance, setWalletBalance] = useState<WalletBalance | null>(null)
const [startedAt] = useState(() => Date.now()) const [startedAt, setStartedAt] = useState(() => Date.now())
const { latestBeeRelease } = useLatestBeeRelease() const { latestBeeRelease } = useLatestBeeRelease()
@@ -202,6 +220,15 @@ export function Provider({ children }: Props): ReactElement {
const frequencyRef = useRef<number | null>(frequency) const frequencyRef = useRef<number | null>(frequency)
useEffect(() => {
if (isWarmingUp) return
setStartedAt(Date.now())
const timer = setTimeout(() => setStartedAt(0), LAUNCH_GRACE_PERIOD)
return () => clearTimeout(timer)
}, [isWarmingUp])
const refresh = useCallback(async () => { const refresh = useCallback(async () => {
// Don't want to refresh when already refreshing // Don't want to refresh when already refreshing
if (isRefreshing) { if (isRefreshing) {
@@ -215,101 +242,63 @@ export function Provider({ children }: Props): ReactElement {
return return
} }
try {
isRefreshing = true isRefreshing = true
const [
healthResult,
statusResult,
nodeAddressesResult,
nodeInfoResult,
topologyResult,
peersResult,
chequebookAddressResult,
peerChequesResult,
chainStateResult,
walletResult,
chequebookBalanceResult,
stakeResult,
peerBalancesResult,
settlementsResult,
] = await Promise.allSettled([
beeApi.getHealth({ timeout: TIMEOUT }),
beeApi.getStatus({ timeout: TIMEOUT }),
beeApi.getNodeAddresses({ timeout: TIMEOUT }),
beeApi.getNodeInfo({ timeout: TIMEOUT }),
beeApi.getTopology({ timeout: TIMEOUT }),
beeApi.getPeers({ timeout: TIMEOUT }),
beeApi.getChequebookAddress({ timeout: TIMEOUT }),
beeApi.getLastCheques({ timeout: TIMEOUT }),
beeApi.getChainState({ timeout: TIMEOUT }),
beeApi.getWalletBalance({ timeout: TIMEOUT }),
beeApi.getChequebookBalance({ timeout: TIMEOUT }),
beeApi.getStake({ timeout: TIMEOUT }),
beeApi.getAllBalances({ timeout: TIMEOUT }),
beeApi.getAllSettlements(),
])
// All setters called synchronously — React 18 batches them into one render.
const health = getFulfilledValue(healthResult)
setBeeVersion(health?.version ?? null)
setApiHealth(Boolean(health))
setIsWarmingUp(getFulfilledValue(statusResult)?.isWarmingUp ?? false)
setNodeAddresses(getFulfilledValue(nodeAddressesResult))
setNodeInfo(getFulfilledValue(nodeInfoResult))
setNodeTopology(getFulfilledValue(topologyResult))
setPeers(getFulfilledValue(peersResult))
setChequebookAddress(getFulfilledValue(chequebookAddressResult))
setPeerCheques(getFulfilledValue(peerChequesResult))
setChainState(getFulfilledValue(chainStateResult))
setWalletBalance(getFulfilledValue(walletResult))
setChequebookBalance(getFulfilledValue(chequebookBalanceResult))
setStake(getFulfilledValue(stakeResult))
setPeerBalances(getFulfilledValue(peerBalancesResult)?.balances ?? null)
setSettlements(getFulfilledValue(settlementsResult))
setError(null) setError(null)
const promises = [
// API health
beeApi
.getHealth({ timeout: TIMEOUT })
.then(response => setBeeVersion(response.version))
.then(() => setApiHealth(true))
.catch(() => {
setBeeVersion(null)
setApiHealth(false)
}),
// Node Addresses
beeApi
.getNodeAddresses({ timeout: TIMEOUT })
.then(setNodeAddresses)
.catch(() => setNodeAddresses(null)),
// NodeInfo
beeApi
.getNodeInfo({ timeout: TIMEOUT })
.then(setNodeInfo)
.catch(() => setNodeInfo(null)),
// Network Topology
beeApi
.getTopology({ timeout: TIMEOUT })
.then(setNodeTopology)
.catch(() => setNodeTopology(null)),
// Peers
beeApi
.getPeers({ timeout: TIMEOUT })
.then(setPeers)
.catch(() => setPeers(null)),
// Chequebook address
beeApi
.getChequebookAddress({ timeout: TIMEOUT })
.then(setChequebookAddress)
.catch(() => setChequebookAddress(null)),
// Cheques
beeApi
.getLastCheques({ timeout: TIMEOUT })
.then(setPeerCheques)
.catch(() => setPeerCheques(null)),
// Chain state
beeApi
.getChainState({ timeout: TIMEOUT })
.then(setChainState)
.catch(() => setChainState(null)),
// Wallet
beeApi
.getWalletBalance({ timeout: TIMEOUT })
.then(setWalletBalance)
.catch(() => setWalletBalance(null)),
// Chequebook balance
beeApi
.getChequebookBalance({ timeout: TIMEOUT })
.then(setChequebookBalance)
.catch(() => setChequebookBalance(null)),
beeApi
.getStake({ timeout: TIMEOUT })
.then(stake => setStake(stake))
.catch(() => setStake(null)),
// Peer balances
beeApi
.getAllBalances({ timeout: TIMEOUT })
.then(x => setPeerBalances(x.balances))
.catch(() => setPeerBalances(null)),
// Settlements
beeApi
.getAllSettlements()
.then(setSettlements)
.catch(() => setSettlements(null)),
]
await Promise.allSettled(promises)
} catch (e) {
setError(e as Error)
}
setIsLoading(false) setIsLoading(false)
isRefreshing = false
setLastUpdate(Date.now()) setLastUpdate(Date.now())
isRefreshing = false
}, [beeApi]) }, [beeApi])
const start = useCallback( const start = useCallback(
@@ -322,8 +311,18 @@ export function Provider({ children }: Props): ReactElement {
const stop = useCallback(() => setFrequency(null), []) const stop = useCallback(() => setFrequency(null), [])
const status = useMemo( const status = useMemo(
() => getStatus(nodeInfo, apiHealth, topology, chequebookAddress, chequebookBalance, error, startedAt), () =>
[nodeInfo, apiHealth, topology, chequebookAddress, chequebookBalance, error, startedAt], getStatus({
nodeInfo,
apiHealth,
topology,
isWarmingUp,
chequebookAddress,
chequebookBalance,
error,
startedAt,
}),
[nodeInfo, apiHealth, topology, chequebookAddress, chequebookBalance, error, startedAt, isWarmingUp],
) )
useEffect(() => { useEffect(() => {
+86 -23
View File
@@ -7,10 +7,12 @@ import { FILE_MANAGER_EVENTS } from '../modules/filemanager/constants/common'
import { getUsableStamps } from '../modules/filemanager/utils/bee' import { getUsableStamps } from '../modules/filemanager/utils/bee'
import { getSignerPk } from '../modules/filemanager/utils/common' import { getSignerPk } from '../modules/filemanager/utils/common'
import { CheckState, Context as BeeContext } from './Bee'
import { Context as SettingsContext } from './Settings' import { Context as SettingsContext } from './Settings'
interface ContextInterface { interface ContextInterface {
fm: FileManagerBase | null fm: FileManagerBase | null
initDone: boolean
files: FileInfo[] files: FileInfo[]
currentDrive?: DriveInfo currentDrive?: DriveInfo
currentStamp?: PostageBatch currentStamp?: PostageBatch
@@ -24,6 +26,7 @@ interface ContextInterface {
setCurrentStamp: (s: PostageBatch | undefined) => void setCurrentStamp: (s: PostageBatch | undefined) => void
resync: () => Promise<void> resync: () => Promise<void>
init: () => Promise<FileManagerBase | null> init: () => Promise<FileManagerBase | null>
notifyPkSaved: () => void
setShowError: (show: boolean) => void setShowError: (show: boolean) => void
syncDrives: () => Promise<void> syncDrives: () => Promise<void>
refreshStamp: (batchId: string) => Promise<PostageBatch | undefined> refreshStamp: (batchId: string) => Promise<PostageBatch | undefined>
@@ -31,6 +34,7 @@ interface ContextInterface {
const initialValues: ContextInterface = { const initialValues: ContextInterface = {
fm: null, fm: null,
initDone: false,
files: [], files: [],
currentDrive: undefined, currentDrive: undefined,
currentStamp: undefined, currentStamp: undefined,
@@ -45,6 +49,7 @@ const initialValues: ContextInterface = {
resync: async () => {}, resync: async () => {},
// eslint-disable-next-line require-await // eslint-disable-next-line require-await
init: async () => null, init: async () => null,
notifyPkSaved: () => {},
setShowError: () => {}, setShowError: () => {},
syncDrives: async () => {}, syncDrives: async () => {},
// eslint-disable-next-line require-await // eslint-disable-next-line require-await
@@ -85,12 +90,18 @@ const findDrives = (
} }
export function Provider({ children }: Props) { export function Provider({ children }: Props) {
const initInProgressRef = useRef(false) const initInProgressRef = useRef<boolean>(false)
const beeInstanceRef = useRef<Bee | null>(null) const isBeeApiInitialized = useRef<boolean>(false)
const { status } = useContext(BeeContext)
const { apiUrl } = useContext(SettingsContext) const { apiUrl } = useContext(SettingsContext)
const apiUrlRef = useRef<string>(apiUrl)
const [pkSaved, setPkSaved] = useState<boolean>(false)
const [beeInstance, setBeeInstance] = useState<Bee | null>(null)
const [fm, setFm] = useState<FileManagerBase | null>(null) const [fm, setFm] = useState<FileManagerBase | null>(null)
const [initDone, setInitDone] = useState<boolean>(false)
const [shallReset, setShallReset] = useState<boolean>(false) const [shallReset, setShallReset] = useState<boolean>(false)
const [files, setFiles] = useState<FileInfo[]>([]) const [files, setFiles] = useState<FileInfo[]>([])
const [drives, setDrives] = useState<DriveInfo[]>([]) const [drives, setDrives] = useState<DriveInfo[]>([])
@@ -102,6 +113,8 @@ export function Provider({ children }: Props) {
const [initializationError, setInitializationError] = useState<boolean>(false) const [initializationError, setInitializationError] = useState<boolean>(false)
const [showError, setShowError] = useState<boolean>(false) const [showError, setShowError] = useState<boolean>(false)
const notifyPkSaved = useCallback(() => setPkSaved(v => !v), [])
const syncFiles = useCallback((manager: FileManagerBase, fi?: FileInfo, remove?: boolean): void => { const syncFiles = useCallback((manager: FileManagerBase, fi?: FileInfo, remove?: boolean): void => {
if (fi) { if (fi) {
if (remove) { if (remove) {
@@ -129,12 +142,13 @@ export function Provider({ children }: Props) {
setFiles([...manager.fileInfoList]) setFiles([...manager.fileInfoList])
}, []) }, [])
const syncDrives = useCallback(async (manager: FileManagerBase, di?: DriveInfo, remove?: boolean): Promise<void> => { const syncDrives = useCallback(
if (!beeInstanceRef.current) { async (manager: FileManagerBase, di?: DriveInfo, remove?: boolean): Promise<void> => {
if (!beeInstance) {
return return
} }
const usableStamps = await getUsableStamps(beeInstanceRef.current) const usableStamps = await getUsableStamps(beeInstance)
if (di) { if (di) {
const isNotExpired = usableStamps.some(s => s.batchID.toString() === di.batchId.toString()) const isNotExpired = usableStamps.some(s => s.batchID.toString() === di.batchId.toString())
@@ -192,7 +206,9 @@ export function Provider({ children }: Props) {
setAdminDrive(tmpAdminDrive) setAdminDrive(tmpAdminDrive)
setDrives(userDrives) setDrives(userDrives)
setExpiredDrives(expiredDrives) setExpiredDrives(expiredDrives)
}, []) },
[beeInstance],
)
const syncDrivesPublic = useCallback(async () => { const syncDrivesPublic = useCallback(async () => {
if (fm) { if (fm) {
@@ -200,12 +216,13 @@ export function Provider({ children }: Props) {
} }
}, [fm, syncDrives]) }, [fm, syncDrives])
const refreshStamp = useCallback(async (batchId: string): Promise<PostageBatch | undefined> => { const refreshStamp = useCallback(
if (!beeInstanceRef.current) { async (batchId: string): Promise<PostageBatch | undefined> => {
if (!beeInstance) {
return return
} }
const usableStamps = await getUsableStamps(beeInstanceRef.current) const usableStamps = await getUsableStamps(beeInstance)
const refreshedStamp = usableStamps.find(s => s.batchID.toString() === batchId) const refreshedStamp = usableStamps.find(s => s.batchID.toString() === batchId)
setCurrentStamp(prev => { setCurrentStamp(prev => {
@@ -217,38 +234,38 @@ export function Provider({ children }: Props) {
}) })
return refreshedStamp return refreshedStamp
}, []) },
[beeInstance],
)
const init = useCallback(async (): Promise<FileManagerBase | null> => { const init = useCallback(async (): Promise<FileManagerBase | null> => {
const pk = getSignerPk() const pk = getSignerPk()
if (!apiUrl || !pk || initInProgressRef.current) return null if (!beeInstance || !pk || initInProgressRef.current) return null
initInProgressRef.current = true initInProgressRef.current = true
setFm(null) setFm(null)
setInitDone(false)
setFiles([]) setFiles([])
setDrives([]) setDrives([])
setAdminDrive(null) setAdminDrive(null)
setInitializationError(false) setInitializationError(false)
setCurrentDrive(undefined) setCurrentDrive(undefined)
setCurrentStamp(undefined) setCurrentStamp(undefined)
setShallReset(false)
if (!beeInstanceRef.current) { const manager = new FileManagerBase(beeInstance)
beeInstanceRef.current = new Bee(apiUrl, { signer: pk })
}
const manager = new FileManagerBase(beeInstanceRef.current)
const handleInitialized = (success: boolean) => { const handleInitialized = (success: boolean) => {
setInitializationError(!success) setInitializationError(!success)
setInitDone(true)
if (success) { if (success) {
if (manager.adminStamp && !manager.adminStamp.usable) { if (manager.adminStamp && !manager.adminStamp.usable) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn('Admin stamp exists but is not usable') console.warn('Admin stamp exists but is not usable')
setShallReset(true) setShallReset(true)
setInitializationError(true)
return return
} }
@@ -310,11 +327,13 @@ export function Provider({ children }: Props) {
return manager return manager
} catch { } catch {
setInitDone(true)
return null return null
} finally { } finally {
initInProgressRef.current = false initInProgressRef.current = false
} }
}, [apiUrl, syncDrives, syncFiles]) }, [beeInstance, syncDrives, syncFiles])
const resync = useCallback(async (): Promise<void> => { const resync = useCallback(async (): Promise<void> => {
const prevDriveId = currentDrive?.id.toString() const prevDriveId = currentDrive?.id.toString()
@@ -322,19 +341,59 @@ export function Provider({ children }: Props) {
const manager = await init() const manager = await init()
if (prevDriveId && manager && beeInstanceRef.current) { if (prevDriveId && manager && beeInstance) {
const refreshedDrive = manager.driveList.find(d => d.id.toString() === prevDriveId) const refreshedDrive = manager.driveList.find(d => d.id.toString() === prevDriveId)
setCurrentDrive(refreshedDrive) setCurrentDrive(refreshedDrive)
const uStamps: PostageBatch[] = await getUsableStamps(beeInstanceRef.current) const uStamps: PostageBatch[] = await getUsableStamps(beeInstance)
const isValidCurrentStamp = uStamps.find(s => s.batchID.toString() === prevStamp?.batchID.toString()) const isValidCurrentStamp = uStamps.find(s => s.batchID.toString() === prevStamp?.batchID.toString())
setCurrentStamp(isValidCurrentStamp) setCurrentStamp(isValidCurrentStamp)
} }
}, [currentDrive?.id, currentStamp, init]) }, [beeInstance, currentDrive?.id, currentStamp, init])
useEffect(() => { useEffect(() => {
if (!apiUrl || initInProgressRef.current) { apiUrlRef.current = apiUrl
}, [apiUrl])
useEffect(() => {
const isConnecting = status.all === CheckState.CONNECTING
const isApiOk = status.apiConnection.isEnabled && status.apiConnection.checkState === CheckState.OK
const currentApiUrl = apiUrlRef.current
const pk = getSignerPk()
if (!currentApiUrl || !pk) {
isBeeApiInitialized.current = false
setBeeInstance(null)
return
}
if (isConnecting) {
return
}
if (isBeeApiInitialized.current) {
return
}
if (!isApiOk) {
return
}
isBeeApiInitialized.current = true
setBeeInstance(new Bee(currentApiUrl, { signer: pk }))
}, [status.all, status.apiConnection, pkSaved])
useEffect(() => {
isBeeApiInitialized.current = false
setBeeInstance(null)
setInitDone(false)
initInProgressRef.current = false
}, [apiUrl])
useEffect(() => {
if (!beeInstance || initInProgressRef.current) {
return return
} }
@@ -343,7 +402,7 @@ export function Provider({ children }: Props) {
} }
initFromLocalState() initFromLocalState()
}, [apiUrl, init]) }, [beeInstance, init])
useEffect(() => { useEffect(() => {
if (fm && drives.length === 0 && !adminDrive) { if (fm && drives.length === 0 && !adminDrive) {
@@ -354,6 +413,7 @@ export function Provider({ children }: Props) {
const contextValue = useMemo( const contextValue = useMemo(
() => ({ () => ({
fm, fm,
initDone,
files, files,
currentDrive, currentDrive,
currentStamp, currentStamp,
@@ -367,12 +427,14 @@ export function Provider({ children }: Props) {
setCurrentStamp, setCurrentStamp,
resync, resync,
init, init,
notifyPkSaved,
setShowError, setShowError,
syncDrives: syncDrivesPublic, syncDrives: syncDrivesPublic,
refreshStamp, refreshStamp,
}), }),
[ [
fm, fm,
initDone,
files, files,
currentDrive, currentDrive,
currentStamp, currentStamp,
@@ -386,6 +448,7 @@ export function Provider({ children }: Props) {
setCurrentStamp, setCurrentStamp,
resync, resync,
init, init,
notifyPkSaved,
setShowError, setShowError,
syncDrivesPublic, syncDrivesPublic,
refreshStamp, refreshStamp,