Files
bee-dashboard/src/modules/filemanager/components/AdminStatusBar/AdminStatusBar.tsx
T
rolandlor e1fdd52676 fix: spdv-963 - Expired admin drive upgrading handling (#230)
* fix: spdv-963

* refactor: spdv-963
2026-03-20 16:02:17 +01:00

270 lines
8.8 KiB
TypeScript

import { PostageBatch } from '@ethersphere/bee-js'
import { DriveInfo, estimateDriveListMetadataSize } from '@solarpunkltd/file-manager-lib'
import { ReactElement, useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { Context as FMContext } from '../../../../providers/FileManager'
import { Context as SettingsContext } from '../../../../providers/Settings'
import { getHumanReadableFileSize } from '../../../../utils/file'
import { FILE_MANAGER_EVENTS, POLLING_TIMEOUT_MS } from '../../constants/common'
import { TOOLTIPS } from '../../constants/tooltips'
import { useStampPolling } from '../../hooks/useStampPolling'
import { calculateStampCapacityMetrics, validateStampStillExists } from '../../utils/bee'
import { ConfirmModal } from '../ConfirmModal/ConfirmModal'
import { ProgressBar } from '../ProgressBar/ProgressBar'
import { Tooltip } from '../Tooltip/Tooltip'
import { UpgradeDriveModal } from '../UpgradeDriveModal/UpgradeDriveModal'
import { UpgradeTimeoutModal } from '../UpgradeTimeoutModal/UpgradeTimeoutModal'
import './AdminStatusBar.scss'
interface AdminStatusBarProps {
adminStamp: PostageBatch | null
adminDrive: DriveInfo | null
loading: boolean
isCreationInProgress: boolean
setErrorMessage?: (error: string) => void
}
export function AdminStatusBar({
adminStamp,
adminDrive,
loading,
isCreationInProgress,
setErrorMessage,
}: AdminStatusBarProps): ReactElement {
const { drives, setShowError, refreshStamp } = useContext(FMContext)
const { beeApi } = useContext(SettingsContext)
const [isUpgradeDriveModalOpen, setIsUpgradeDriveModalOpen] = useState(false)
const [isUpgradeTimeoutModalOpen, setIsUpgradeTimeoutModalOpen] = useState(false)
const [isUpgrading, setIsUpgrading] = useState(false)
const [actualStamp, setActualStamp] = useState<PostageBatch | null>(adminStamp)
const [showProgressModal, setShowProgressModal] = useState(true)
const handleStampUpdated = useCallback((updatedStamp: PostageBatch) => {
setActualStamp(updatedStamp)
}, [])
const handlePollingStateChange = useCallback((_isPolling: boolean) => {
// no-op
}, [])
const { startPolling } = useStampPolling({
onStampUpdated: handleStampUpdated,
onPollingStateChange: handlePollingStateChange,
refreshStamp,
timeout: POLLING_TIMEOUT_MS,
})
useEffect(() => {
setShowProgressModal(isCreationInProgress || loading)
}, [isCreationInProgress, loading, setShowProgressModal])
useEffect(() => {
if (!adminStamp || !actualStamp) {
setActualStamp(adminStamp)
return
}
if (actualStamp.batchID.toString() !== adminStamp.batchID.toString()) {
setActualStamp(adminStamp)
return
}
const incomingSize = adminStamp.size.toBytes()
const currentSize = actualStamp.size.toBytes()
const incomingExpiry = adminStamp.duration.toEndDate().getTime()
const currentExpiry = actualStamp.duration.toEndDate().getTime()
if (incomingSize > currentSize || incomingExpiry > currentExpiry) {
setActualStamp(adminStamp)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [adminStamp])
useEffect(() => {
if (!adminDrive) return
const id = adminDrive.id.toString()
const onStart = (e: Event) => {
const { driveId } = (e as CustomEvent).detail || {}
if (driveId === id) {
setIsUpgrading(true)
}
}
const onEnd = (e: Event) => {
const { driveId, success, error, updatedStamp } = (e as CustomEvent).detail || {}
if (driveId !== id) return
if (!success && error) {
setIsUpgrading(false)
setErrorMessage?.(error)
setShowError(true)
return
}
if (updatedStamp) {
setActualStamp(updatedStamp)
}
setIsUpgrading(false)
}
const onTimeout = (e: Event) => {
const { driveId } = (e as CustomEvent).detail || {}
if (driveId === id) {
setIsUpgradeTimeoutModalOpen(true)
}
}
window.addEventListener(FILE_MANAGER_EVENTS.DRIVE_UPGRADE_START, onStart as EventListener)
window.addEventListener(FILE_MANAGER_EVENTS.DRIVE_UPGRADE_END, onEnd as EventListener)
window.addEventListener(FILE_MANAGER_EVENTS.DRIVE_UPGRADE_TIMEOUT, onTimeout as EventListener)
return () => {
window.removeEventListener(FILE_MANAGER_EVENTS.DRIVE_UPGRADE_START, onStart as EventListener)
window.removeEventListener(FILE_MANAGER_EVENTS.DRIVE_UPGRADE_END, onEnd as EventListener)
window.removeEventListener(FILE_MANAGER_EVENTS.DRIVE_UPGRADE_TIMEOUT, onTimeout as EventListener)
}
}, [adminDrive, setErrorMessage, setShowError])
const handleTimeoutCancel = useCallback(() => {
setIsUpgrading(false)
setIsUpgradeTimeoutModalOpen(false)
// Restart polling to continue checking for capacity updates
if (actualStamp) {
startPolling(actualStamp)
}
}, [actualStamp, startPolling])
const { capacityPct, usedSize, totalSize } = useMemo(() => {
if (!actualStamp) {
return {
capacityPct: 0,
usedSize: '—',
totalSize: '—',
}
}
const estimatedDlSizeBytes = estimateDriveListMetadataSize(drives) * drives.length
const {
capacityPct: reportedPct,
usedBytes: reportedUsedBytes,
stampSizeBytes,
} = calculateStampCapacityMetrics(actualStamp, [], adminDrive?.redundancyLevel)
const actualUsedSizeBytes = Math.max(reportedUsedBytes, estimatedDlSizeBytes)
const actualPct = Math.max(reportedPct, (actualUsedSizeBytes / stampSizeBytes) * 100)
return {
capacityPct: actualPct,
usedSize: getHumanReadableFileSize(actualUsedSizeBytes),
totalSize: getHumanReadableFileSize(stampSizeBytes),
}
}, [actualStamp, adminDrive, drives])
const expiresAt = useMemo(
() => (actualStamp ? actualStamp.duration.toEndDate().toLocaleDateString() : '—'),
[actualStamp],
)
const isBusy = loading || isUpgrading || isCreationInProgress
const blurCls = isBusy ? ' is-loading' : ''
const statusVerb = isCreationInProgress ? 'Creating' : 'Loading'
const statusText = statusVerb + ' admin drive, please do not reload'
const renderModalsAndOverlays = () => {
return (
<>
{isUpgradeDriveModalOpen && actualStamp && adminDrive && (
<UpgradeDriveModal
stamp={actualStamp}
drive={adminDrive}
onCancelClick={() => setIsUpgradeDriveModalOpen(false)}
setErrorMessage={setErrorMessage}
/>
)}
{isUpgradeTimeoutModalOpen && adminDrive && actualStamp && (
<UpgradeTimeoutModal driveName={adminDrive.name} onOk={handleTimeoutCancel} />
)}
{isUpgrading && (
<div className="fm-drive-item-creating-overlay" aria-live="polite">
<div className="fm-mini-spinner" />
<span>Upgrading admin drive</span>
</div>
)}
{showProgressModal && (
<ConfirmModal
title="Admin Drive Creation"
isProgress
spinnerMessage={statusText}
showFooter={false}
showMinimize={true}
onMinimize={() => setShowProgressModal(false)}
/>
)}
</>
)
}
return (
<div>
<div className={`fm-admin-status-bar-container${blurCls}`} aria-busy={isBusy ? 'true' : 'false'}>
<div className="fm-admin-status-bar-left">
<div
className={`fm-drive-item-capacity ${isUpgrading ? 'fm-drive-item-capacity-updating' : ''}`}
title={isUpgrading ? 'Capacity is updating... This may take a few moments.' : ''}
>
Capacity <ProgressBar value={capacityPct} width="150px" /> {usedSize} / {totalSize}
</div>
<div>File Manager Available: Until: {expiresAt}</div>
<Tooltip label={TOOLTIPS.ADMIN_STATUS_WARNING} />
</div>
{renderModalsAndOverlays()}
<div
className="fm-admin-status-bar-upgrade-button"
onClick={async () => {
if (!isBusy && actualStamp && adminDrive && beeApi) {
const isStampValid = await validateStampStillExists(beeApi, actualStamp.batchID)
if (!isStampValid) {
setErrorMessage?.('The admin drive has expired. Please clear the browser cache and reload the page.')
setShowError(true)
return
}
setIsUpgradeDriveModalOpen(true)
}
}}
aria-disabled={isBusy ? 'true' : 'false'}
>
{isBusy ? 'Working…' : 'Manage'}
</div>
</div>
{!showProgressModal && (loading || isCreationInProgress) && (
<div className="fm-admin-status-bar-progress-pill-container">
<div className="fm-admin-status-progress-pill" onClick={() => setShowProgressModal(true)}>
{statusText}
</div>
</div>
)}
</div>
)
}