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:
@@ -1,4 +1,4 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import React, { ReactElement, useLayoutEffect, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
import { Button } from '../Button/Button'
|
||||
@@ -35,7 +35,11 @@ export function ConfirmModal({
|
||||
onMinimize,
|
||||
background = true,
|
||||
}: 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(
|
||||
<div className={`fm-modal-container fm-confirm-modal ${background ? '' : 'fm-modal-no-background'}`}>
|
||||
|
||||
@@ -20,7 +20,6 @@ import './InitialModal.scss'
|
||||
|
||||
interface InitialModalProps {
|
||||
resetState: boolean
|
||||
handleVisibility: (isVisible: boolean) => void
|
||||
handleShowError: (flag: boolean, errorMessage?: string) => void
|
||||
setIsCreationInProgress: (isCreating: boolean) => void
|
||||
}
|
||||
@@ -65,7 +64,6 @@ const setSecurityLevel = (setter: (value: RedundancyLevel) => void) => {
|
||||
export function InitialModal({
|
||||
resetState,
|
||||
setIsCreationInProgress,
|
||||
handleVisibility,
|
||||
handleShowError,
|
||||
}: InitialModalProps): ReactElement {
|
||||
const [isCreateEnabled, setIsCreateEnabled] = useState(false)
|
||||
@@ -133,7 +131,6 @@ export function InitialModal({
|
||||
|
||||
const createAdminDrive = useCallback(async () => {
|
||||
setIsCreationInProgress?.(true)
|
||||
handleVisibility(false)
|
||||
|
||||
await handleCreateDrive({
|
||||
beeApi,
|
||||
@@ -148,7 +145,6 @@ export function InitialModal({
|
||||
resetState,
|
||||
existingBatch: selectedBatch,
|
||||
onSuccess: () => {
|
||||
handleVisibility(false)
|
||||
setIsCreationInProgress(false)
|
||||
},
|
||||
onError: err => {
|
||||
@@ -164,7 +160,6 @@ export function InitialModal({
|
||||
validityEndDate,
|
||||
erasureCodeLevel,
|
||||
selectedBatch,
|
||||
handleVisibility,
|
||||
handleShowError,
|
||||
setIsCreationInProgress,
|
||||
resetState,
|
||||
|
||||
@@ -20,7 +20,6 @@ import { getSignerPk, removeSignerPk } from '@/modules/filemanager/utils/common'
|
||||
import { CheckState, Context as BeeContext } from '@/providers/Bee'
|
||||
import { Context as FMContext } from '@/providers/FileManager'
|
||||
import { BrowserPlatform, cacheClearUrls, detectBrowser } from '@/providers/Platform'
|
||||
import { Context as SettingsContext } from '@/providers/Settings'
|
||||
|
||||
function PrivateKeyModalBlock({ onSaved }: { onSaved: () => void }) {
|
||||
return (
|
||||
@@ -74,7 +73,6 @@ function ResetModalBlock({ cacheHelpUrl, onConfirm }: { cacheHelpUrl: string; on
|
||||
|
||||
function InitialModalBlock(props: {
|
||||
resetState: boolean
|
||||
handleVisibility: (isVisible: boolean) => void
|
||||
handleShowError: (flag: boolean, error?: string) => 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 {
|
||||
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 [showErrorModal, setShowErrorModal] = useState<boolean>(false)
|
||||
const [showAdminErrorModal, setAdminShowErrorModal] = useState<boolean>(false)
|
||||
const [errorMessage, setErrorMessage] = useState<string>('')
|
||||
const [showResetModal, setShowResetModal] = useState<boolean>(false)
|
||||
const [resetAcknowledged, setResetAcknowledged] = 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 { status } = useContext(BeeContext)
|
||||
const { beeApi } = useContext(SettingsContext)
|
||||
const { fm, shallReset, adminDrive, initializationError, init } = useContext(FMContext)
|
||||
const { fm, initDone, shallReset, adminDrive, initializationError, notifyPkSaved } = useContext(FMContext)
|
||||
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true
|
||||
@@ -185,93 +190,76 @@ export function FileManagerPage(): ReactElement {
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const isApiError = status.apiConnection.checkState !== CheckState.OK || !status.apiConnection.isEnabled
|
||||
setShowConnectionError(isApiError)
|
||||
}, [status.apiConnection])
|
||||
const { isBeeReady, isConnectionError } = useMemo(() => {
|
||||
const isConnecting = status.all === CheckState.CONNECTING
|
||||
const isApiOk = status.apiConnection.isEnabled && status.apiConnection.checkState === CheckState.OK
|
||||
|
||||
return {
|
||||
isBeeReady: !isConnecting && isApiOk,
|
||||
isConnectionError: !isConnecting && !isApiOk && Boolean(fm),
|
||||
}
|
||||
}, [status, fm])
|
||||
|
||||
useEffect(() => {
|
||||
if (!beeApi) {
|
||||
return
|
||||
if (!isConnectionError) {
|
||||
setConnectionErrorDismissed(false)
|
||||
}
|
||||
}, [isConnectionError])
|
||||
|
||||
if (!hasPk) {
|
||||
setIsLoading(false)
|
||||
const pageState = useMemo((): PageState => {
|
||||
if (!isBeeReady && !initDone) return PageState.Connecting
|
||||
|
||||
return
|
||||
}
|
||||
if (!hasPk) return PageState.NoPrivateKey
|
||||
|
||||
setShowResetModal(shallReset)
|
||||
if (!initDone) return PageState.Loading
|
||||
|
||||
if (shallReset) {
|
||||
setShowInitialModal(true)
|
||||
if (shallReset && !resetAcknowledged) return PageState.Reset
|
||||
|
||||
return
|
||||
}
|
||||
if (initializationError && !shallReset) return PageState.InitError
|
||||
|
||||
if (initializationError) {
|
||||
setIsLoading(false)
|
||||
if (showAdminErrorModal) return PageState.AdminError
|
||||
|
||||
return
|
||||
}
|
||||
const hasAdminStamp = Boolean(fm?.adminStamp)
|
||||
const hasAdminDrive = Boolean(adminDrive)
|
||||
|
||||
if (fm) {
|
||||
const hasAdminStamp = Boolean(fm.adminStamp)
|
||||
const tmpHasAdminDrive = Boolean(adminDrive)
|
||||
setHasAdminDrive(hasAdminStamp || tmpHasAdminDrive)
|
||||
setIsLoading(false)
|
||||
if (!hasAdminStamp && !hasAdminDrive && !isCreationInProgress) return PageState.Initial
|
||||
|
||||
setShowInitialModal(!(hasAdminStamp || tmpHasAdminDrive))
|
||||
return PageState.Ready
|
||||
}, [
|
||||
isBeeReady,
|
||||
hasPk,
|
||||
initDone,
|
||||
shallReset,
|
||||
resetAcknowledged,
|
||||
initializationError,
|
||||
showAdminErrorModal,
|
||||
fm,
|
||||
adminDrive,
|
||||
isCreationInProgress,
|
||||
])
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
}, [fm, beeApi, hasPk, initializationError, adminDrive, shallReset])
|
||||
|
||||
const handlePrivateKeySaved = useCallback(async () => {
|
||||
const handlePrivateKeySaved = useCallback(() => {
|
||||
if (!isMountedRef.current) return
|
||||
|
||||
setHasPk(true)
|
||||
|
||||
if (fm) {
|
||||
if (!isMountedRef.current) return
|
||||
if (fm) return
|
||||
|
||||
setIsLoading(false)
|
||||
|
||||
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],
|
||||
)
|
||||
notifyPkSaved()
|
||||
}, [fm, notifyPkSaved])
|
||||
|
||||
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} />
|
||||
}
|
||||
|
||||
if (initializationError && !isLoading && !shallReset) {
|
||||
if (pageState === PageState.InitError) {
|
||||
return (
|
||||
<InitializationErrorBlock
|
||||
onOk={() => {
|
||||
@@ -282,17 +270,16 @@ export function FileManagerPage(): ReactElement {
|
||||
)
|
||||
}
|
||||
|
||||
if (showResetModal) {
|
||||
return <ResetModalBlock cacheHelpUrl={cacheHelpUrl} onConfirm={() => setShowResetModal(false)} />
|
||||
if (pageState === PageState.Reset) {
|
||||
return <ResetModalBlock cacheHelpUrl={cacheHelpUrl} onConfirm={() => setResetAcknowledged(true)} />
|
||||
}
|
||||
|
||||
if (!showErrorModal && (isEmptyState || isInvalidState)) {
|
||||
if (pageState === PageState.Initial) {
|
||||
return (
|
||||
<InitialModalBlock
|
||||
resetState={shallReset}
|
||||
handleVisibility={(isVisible: boolean) => setShowInitialModal(isVisible)}
|
||||
handleShowError={(flag: boolean, error?: string) => {
|
||||
setShowErrorModal(flag)
|
||||
setAdminShowErrorModal(flag)
|
||||
|
||||
if (error) setErrorMessage(error)
|
||||
}}
|
||||
@@ -301,19 +288,15 @@ export function FileManagerPage(): ReactElement {
|
||||
)
|
||||
}
|
||||
|
||||
if (!fm) {
|
||||
return <LoadingBlock />
|
||||
}
|
||||
|
||||
if (showErrorModal) {
|
||||
if (pageState === PageState.AdminError) {
|
||||
return (
|
||||
<ErrorModalBlock
|
||||
label={
|
||||
errorMessage ||
|
||||
'Error creating Admin Drive. Please try again. Possible causes include insufficient xDAI balance or a lost connection to the RPC.'
|
||||
}
|
||||
onClick={() => {
|
||||
setShowErrorModal(false)
|
||||
setShowInitialModal(true)
|
||||
setAdminShowErrorModal(false)
|
||||
setErrorMessage('')
|
||||
}}
|
||||
/>
|
||||
@@ -323,8 +306,8 @@ export function FileManagerPage(): ReactElement {
|
||||
return (
|
||||
<FileManagerMainContent
|
||||
fm={fm}
|
||||
showConnectionError={showConnectionError}
|
||||
setShowConnectionError={() => setShowConnectionError(false)}
|
||||
showConnectionError={isConnectionError && !connectionErrorDismissed}
|
||||
setShowConnectionError={(show: boolean) => setConnectionErrorDismissed(!show)}
|
||||
isFormbricksActive={isFormbricksActive}
|
||||
errorMessage={errorMessage}
|
||||
setErrorMessage={setErrorMessage}
|
||||
|
||||
+112
-113
@@ -29,7 +29,7 @@ import { useLatestBeeRelease } from '../hooks/apiHooks'
|
||||
|
||||
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_ERROR = 5_000
|
||||
const TIMEOUT = 3_000
|
||||
@@ -116,15 +116,22 @@ interface Props {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
function getStatus(
|
||||
nodeInfo: NodeInfo | null,
|
||||
apiHealth: boolean,
|
||||
topology: Topology | null,
|
||||
chequebookAddress: ChequebookAddressResponse | null,
|
||||
chequebookBalance: ChequebookBalanceResponse | null,
|
||||
error: Error | null,
|
||||
startedAt: number,
|
||||
): Status {
|
||||
interface StatusProps {
|
||||
nodeInfo: NodeInfo | null
|
||||
apiHealth: boolean
|
||||
topology: Topology | null
|
||||
isWarmingUp: boolean
|
||||
chequebookAddress: ChequebookAddressResponse | null
|
||||
chequebookBalance: ChequebookBalanceResponse | null
|
||||
error: Error | null
|
||||
startedAt: number
|
||||
}
|
||||
|
||||
function getStatus(props: StatusProps): Status {
|
||||
const { nodeInfo, apiHealth, topology, isWarmingUp, chequebookAddress, chequebookBalance, error, startedAt } = {
|
||||
...props,
|
||||
}
|
||||
|
||||
const status: Status = { ...initialValues.status }
|
||||
|
||||
// API connection check
|
||||
@@ -143,15 +150,17 @@ function getStatus(
|
||||
|
||||
if (chequebookAddress?.chequebookAddress && chequebookBalance !== null) {
|
||||
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
|
||||
}
|
||||
|
||||
function determineOverallStatus(status: Status, startedAt: number): CheckState {
|
||||
function determineOverallStatus(status: Status, isWarmingUp: boolean, startedAt: number): CheckState {
|
||||
const hasErrors = Object.values(status).some(
|
||||
({ 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
|
||||
|
||||
if (hasErrors && isInGracePeriod) {
|
||||
if (isWarmingUp || isInGracePeriod) {
|
||||
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
|
||||
@@ -179,6 +196,7 @@ export function Provider({ children }: Props): ReactElement {
|
||||
|
||||
const [beeVersion, setBeeVersion] = useState<string | null>(null)
|
||||
const [apiHealth, setApiHealth] = useState<boolean>(false)
|
||||
const [isWarmingUp, setIsWarmingUp] = useState<boolean>(true)
|
||||
const [nodeAddresses, setNodeAddresses] = useState<NodeAddresses | null>(null)
|
||||
const [nodeInfo, setNodeInfo] = useState<NodeInfo | 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 [chainState, setChainState] = useState<ChainState | null>(null)
|
||||
const [walletBalance, setWalletBalance] = useState<WalletBalance | null>(null)
|
||||
const [startedAt] = useState(() => Date.now())
|
||||
const [startedAt, setStartedAt] = useState(() => Date.now())
|
||||
|
||||
const { latestBeeRelease } = useLatestBeeRelease()
|
||||
|
||||
@@ -202,6 +220,15 @@ export function Provider({ children }: Props): ReactElement {
|
||||
|
||||
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 () => {
|
||||
// Don't want to refresh when already refreshing
|
||||
if (isRefreshing) {
|
||||
@@ -215,101 +242,63 @@ export function Provider({ children }: Props): ReactElement {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isRefreshing = true
|
||||
setError(null)
|
||||
isRefreshing = true
|
||||
|
||||
const promises = [
|
||||
// API health
|
||||
beeApi
|
||||
.getHealth({ timeout: TIMEOUT })
|
||||
.then(response => setBeeVersion(response.version))
|
||||
.then(() => setApiHealth(true))
|
||||
.catch(() => {
|
||||
setBeeVersion(null)
|
||||
setApiHealth(false)
|
||||
}),
|
||||
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(),
|
||||
])
|
||||
|
||||
// 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)
|
||||
}
|
||||
// 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)
|
||||
setIsLoading(false)
|
||||
isRefreshing = false
|
||||
setLastUpdate(Date.now())
|
||||
|
||||
isRefreshing = false
|
||||
}, [beeApi])
|
||||
|
||||
const start = useCallback(
|
||||
@@ -322,8 +311,18 @@ export function Provider({ children }: Props): ReactElement {
|
||||
const stop = useCallback(() => setFrequency(null), [])
|
||||
|
||||
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(() => {
|
||||
|
||||
+147
-84
@@ -7,10 +7,12 @@ import { FILE_MANAGER_EVENTS } from '../modules/filemanager/constants/common'
|
||||
import { getUsableStamps } from '../modules/filemanager/utils/bee'
|
||||
import { getSignerPk } from '../modules/filemanager/utils/common'
|
||||
|
||||
import { CheckState, Context as BeeContext } from './Bee'
|
||||
import { Context as SettingsContext } from './Settings'
|
||||
|
||||
interface ContextInterface {
|
||||
fm: FileManagerBase | null
|
||||
initDone: boolean
|
||||
files: FileInfo[]
|
||||
currentDrive?: DriveInfo
|
||||
currentStamp?: PostageBatch
|
||||
@@ -24,6 +26,7 @@ interface ContextInterface {
|
||||
setCurrentStamp: (s: PostageBatch | undefined) => void
|
||||
resync: () => Promise<void>
|
||||
init: () => Promise<FileManagerBase | null>
|
||||
notifyPkSaved: () => void
|
||||
setShowError: (show: boolean) => void
|
||||
syncDrives: () => Promise<void>
|
||||
refreshStamp: (batchId: string) => Promise<PostageBatch | undefined>
|
||||
@@ -31,6 +34,7 @@ interface ContextInterface {
|
||||
|
||||
const initialValues: ContextInterface = {
|
||||
fm: null,
|
||||
initDone: false,
|
||||
files: [],
|
||||
currentDrive: undefined,
|
||||
currentStamp: undefined,
|
||||
@@ -45,6 +49,7 @@ const initialValues: ContextInterface = {
|
||||
resync: async () => {},
|
||||
// eslint-disable-next-line require-await
|
||||
init: async () => null,
|
||||
notifyPkSaved: () => {},
|
||||
setShowError: () => {},
|
||||
syncDrives: async () => {},
|
||||
// eslint-disable-next-line require-await
|
||||
@@ -85,12 +90,18 @@ const findDrives = (
|
||||
}
|
||||
|
||||
export function Provider({ children }: Props) {
|
||||
const initInProgressRef = useRef(false)
|
||||
const beeInstanceRef = useRef<Bee | null>(null)
|
||||
const initInProgressRef = useRef<boolean>(false)
|
||||
const isBeeApiInitialized = useRef<boolean>(false)
|
||||
|
||||
const { status } = useContext(BeeContext)
|
||||
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 [initDone, setInitDone] = useState<boolean>(false)
|
||||
const [shallReset, setShallReset] = useState<boolean>(false)
|
||||
const [files, setFiles] = useState<FileInfo[]>([])
|
||||
const [drives, setDrives] = useState<DriveInfo[]>([])
|
||||
@@ -102,6 +113,8 @@ export function Provider({ children }: Props) {
|
||||
const [initializationError, setInitializationError] = 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 => {
|
||||
if (fi) {
|
||||
if (remove) {
|
||||
@@ -129,70 +142,73 @@ export function Provider({ children }: Props) {
|
||||
setFiles([...manager.fileInfoList])
|
||||
}, [])
|
||||
|
||||
const syncDrives = useCallback(async (manager: FileManagerBase, di?: DriveInfo, remove?: boolean): Promise<void> => {
|
||||
if (!beeInstanceRef.current) {
|
||||
return
|
||||
}
|
||||
const syncDrives = useCallback(
|
||||
async (manager: FileManagerBase, di?: DriveInfo, remove?: boolean): Promise<void> => {
|
||||
if (!beeInstance) {
|
||||
return
|
||||
}
|
||||
|
||||
const usableStamps = await getUsableStamps(beeInstanceRef.current)
|
||||
const usableStamps = await getUsableStamps(beeInstance)
|
||||
|
||||
if (di) {
|
||||
const isNotExpired = usableStamps.some(s => s.batchID.toString() === di.batchId.toString())
|
||||
if (di) {
|
||||
const isNotExpired = usableStamps.some(s => s.batchID.toString() === di.batchId.toString())
|
||||
|
||||
if (isNotExpired) {
|
||||
if (remove) {
|
||||
setDrives(prev => prev.filter(d => d.id.toString() !== di.id.toString()))
|
||||
if (isNotExpired) {
|
||||
if (remove) {
|
||||
setDrives(prev => prev.filter(d => d.id.toString() !== di.id.toString()))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (di.isAdmin) {
|
||||
setAdminDrive(di)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
setDrives(prev => {
|
||||
const existingIndex = prev.findIndex(d => d.id.toString() === di.id.toString())
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
const updated = [...prev]
|
||||
updated[existingIndex] = di
|
||||
|
||||
return updated
|
||||
return
|
||||
}
|
||||
|
||||
return [...prev, di]
|
||||
})
|
||||
if (di.isAdmin) {
|
||||
setAdminDrive(di)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
setDrives(prev => {
|
||||
const existingIndex = prev.findIndex(d => d.id.toString() === di.id.toString())
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
const updated = [...prev]
|
||||
updated[existingIndex] = di
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
return [...prev, di]
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (remove) {
|
||||
setExpiredDrives(prev => prev.filter(d => d.id.toString() !== di.id.toString()))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (!di.isAdmin) {
|
||||
setExpiredDrives(prev => {
|
||||
const exists = prev.some(d => d.id.toString() === di.id.toString())
|
||||
|
||||
return exists ? prev : [...prev, di]
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: handle admin drive expiration!
|
||||
return
|
||||
}
|
||||
|
||||
if (remove) {
|
||||
setExpiredDrives(prev => prev.filter(d => d.id.toString() !== di.id.toString()))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (!di.isAdmin) {
|
||||
setExpiredDrives(prev => {
|
||||
const exists = prev.some(d => d.id.toString() === di.id.toString())
|
||||
|
||||
return exists ? prev : [...prev, di]
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: handle admin drive expiration!
|
||||
return
|
||||
}
|
||||
|
||||
const { adminDrive: tmpAdminDrive, userDrives, expiredDrives } = findDrives(manager.driveList, usableStamps)
|
||||
setAdminDrive(tmpAdminDrive)
|
||||
setDrives(userDrives)
|
||||
setExpiredDrives(expiredDrives)
|
||||
}, [])
|
||||
const { adminDrive: tmpAdminDrive, userDrives, expiredDrives } = findDrives(manager.driveList, usableStamps)
|
||||
setAdminDrive(tmpAdminDrive)
|
||||
setDrives(userDrives)
|
||||
setExpiredDrives(expiredDrives)
|
||||
},
|
||||
[beeInstance],
|
||||
)
|
||||
|
||||
const syncDrivesPublic = useCallback(async () => {
|
||||
if (fm) {
|
||||
@@ -200,55 +216,56 @@ export function Provider({ children }: Props) {
|
||||
}
|
||||
}, [fm, syncDrives])
|
||||
|
||||
const refreshStamp = useCallback(async (batchId: string): Promise<PostageBatch | undefined> => {
|
||||
if (!beeInstanceRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
const usableStamps = await getUsableStamps(beeInstanceRef.current)
|
||||
const refreshedStamp = usableStamps.find(s => s.batchID.toString() === batchId)
|
||||
|
||||
setCurrentStamp(prev => {
|
||||
if (prev && prev.batchID.toString() === batchId && refreshedStamp) {
|
||||
return refreshedStamp
|
||||
const refreshStamp = useCallback(
|
||||
async (batchId: string): Promise<PostageBatch | undefined> => {
|
||||
if (!beeInstance) {
|
||||
return
|
||||
}
|
||||
|
||||
return prev
|
||||
})
|
||||
const usableStamps = await getUsableStamps(beeInstance)
|
||||
const refreshedStamp = usableStamps.find(s => s.batchID.toString() === batchId)
|
||||
|
||||
return refreshedStamp
|
||||
}, [])
|
||||
setCurrentStamp(prev => {
|
||||
if (prev && prev.batchID.toString() === batchId && refreshedStamp) {
|
||||
return refreshedStamp
|
||||
}
|
||||
|
||||
return prev
|
||||
})
|
||||
|
||||
return refreshedStamp
|
||||
},
|
||||
[beeInstance],
|
||||
)
|
||||
|
||||
const init = useCallback(async (): Promise<FileManagerBase | null> => {
|
||||
const pk = getSignerPk()
|
||||
|
||||
if (!apiUrl || !pk || initInProgressRef.current) return null
|
||||
if (!beeInstance || !pk || initInProgressRef.current) return null
|
||||
|
||||
initInProgressRef.current = true
|
||||
|
||||
setFm(null)
|
||||
setInitDone(false)
|
||||
setFiles([])
|
||||
setDrives([])
|
||||
setAdminDrive(null)
|
||||
setInitializationError(false)
|
||||
setCurrentDrive(undefined)
|
||||
setCurrentStamp(undefined)
|
||||
setShallReset(false)
|
||||
|
||||
if (!beeInstanceRef.current) {
|
||||
beeInstanceRef.current = new Bee(apiUrl, { signer: pk })
|
||||
}
|
||||
|
||||
const manager = new FileManagerBase(beeInstanceRef.current)
|
||||
const manager = new FileManagerBase(beeInstance)
|
||||
|
||||
const handleInitialized = (success: boolean) => {
|
||||
setInitializationError(!success)
|
||||
setInitDone(true)
|
||||
|
||||
if (success) {
|
||||
if (manager.adminStamp && !manager.adminStamp.usable) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('Admin stamp exists but is not usable')
|
||||
setShallReset(true)
|
||||
setInitializationError(true)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -310,11 +327,13 @@ export function Provider({ children }: Props) {
|
||||
|
||||
return manager
|
||||
} catch {
|
||||
setInitDone(true)
|
||||
|
||||
return null
|
||||
} finally {
|
||||
initInProgressRef.current = false
|
||||
}
|
||||
}, [apiUrl, syncDrives, syncFiles])
|
||||
}, [beeInstance, syncDrives, syncFiles])
|
||||
|
||||
const resync = useCallback(async (): Promise<void> => {
|
||||
const prevDriveId = currentDrive?.id.toString()
|
||||
@@ -322,19 +341,59 @@ export function Provider({ children }: Props) {
|
||||
|
||||
const manager = await init()
|
||||
|
||||
if (prevDriveId && manager && beeInstanceRef.current) {
|
||||
if (prevDriveId && manager && beeInstance) {
|
||||
const refreshedDrive = manager.driveList.find(d => d.id.toString() === prevDriveId)
|
||||
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())
|
||||
|
||||
setCurrentStamp(isValidCurrentStamp)
|
||||
}
|
||||
}, [currentDrive?.id, currentStamp, init])
|
||||
}, [beeInstance, currentDrive?.id, currentStamp, init])
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -343,7 +402,7 @@ export function Provider({ children }: Props) {
|
||||
}
|
||||
|
||||
initFromLocalState()
|
||||
}, [apiUrl, init])
|
||||
}, [beeInstance, init])
|
||||
|
||||
useEffect(() => {
|
||||
if (fm && drives.length === 0 && !adminDrive) {
|
||||
@@ -354,6 +413,7 @@ export function Provider({ children }: Props) {
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
fm,
|
||||
initDone,
|
||||
files,
|
||||
currentDrive,
|
||||
currentStamp,
|
||||
@@ -367,12 +427,14 @@ export function Provider({ children }: Props) {
|
||||
setCurrentStamp,
|
||||
resync,
|
||||
init,
|
||||
notifyPkSaved,
|
||||
setShowError,
|
||||
syncDrives: syncDrivesPublic,
|
||||
refreshStamp,
|
||||
}),
|
||||
[
|
||||
fm,
|
||||
initDone,
|
||||
files,
|
||||
currentDrive,
|
||||
currentStamp,
|
||||
@@ -386,6 +448,7 @@ export function Provider({ children }: Props) {
|
||||
setCurrentStamp,
|
||||
resync,
|
||||
init,
|
||||
notifyPkSaved,
|
||||
setShowError,
|
||||
syncDrivesPublic,
|
||||
refreshStamp,
|
||||
|
||||
Reference in New Issue
Block a user