Files
bee-dashboard/src/modules/filemanager/components/UpgradeDriveModal/UpgradeDriveModal.tsx
T
rolandlor a7e42053ae fix: spdv-1007 cost values correcting (#234)
* fix: spdv-1007

* refactor: spdv-1007

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

382 lines
14 KiB
TypeScript

import {
BatchId,
BeeRequestOptions,
BZZ,
capacityBreakpoints,
Duration,
PostageBatch,
RedundancyLevel,
Size,
Utils,
} from '@ethersphere/bee-js'
import { Warning } from '@mui/icons-material'
import { DriveInfo } from '@solarpunkltd/file-manager-lib'
import { ReactElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import CalendarIcon from 'remixicon-react/CalendarLineIcon'
import DatabaseIcon from 'remixicon-react/Database2LineIcon'
import ExternalLinkIcon from 'remixicon-react/ExternalLinkLineIcon'
import DriveIcon from 'remixicon-react/HardDrive2LineIcon'
import WalletIcon from 'remixicon-react/Wallet3LineIcon'
import { Context as BeeContext } from '../../../../providers/Bee'
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 { desiredLifetimeOptions } from '../../constants/stamps'
import { useStampPolling } from '../../hooks/useStampPolling'
import { validateStampStillExists } from '../../utils/bee'
import { fromBytesConversion, getExpiryDateByLifetime, truncateNameMiddle } from '../../utils/common'
import { Button } from '../Button/Button'
import { CustomDropdown } from '../CustomDropdown/CustomDropdown'
import './UpgradeDriveModal.scss'
import '../../styles/global.scss'
interface UpgradeDriveModalProps {
stamp: PostageBatch
drive: DriveInfo
onCancelClick: () => void
containerColor?: string
setErrorMessage?: (error: string) => void
}
const defaultErasureCodeLevel = RedundancyLevel.OFF
const encryption_off = 'ENCRYPTION_OFF'
export function UpgradeDriveModal({
stamp,
onCancelClick,
containerColor,
drive,
setErrorMessage,
}: UpgradeDriveModalProps): ReactElement {
const { nodeAddresses, walletBalance } = useContext(BeeContext)
const { beeApi } = useContext(SettingsContext)
const { refreshStamp, setShowError } = useContext(FMContext)
const [isBalanceSufficient, setIsBalanceSufficient] = useState(true)
const [capacity, setCapacity] = useState(stamp.size)
const [capacityExtensionCost, setCapacityExtensionCost] = useState('')
const [capacityIndex, setCapacityIndex] = useState(0)
const [durationExtensionCost, setDurationExtensionCost] = useState('')
const [lifetimeIndex, setLifetimeIndex] = useState(0)
const validityEndDate = useMemo(
() => getExpiryDateByLifetime(lifetimeIndex, stamp.duration.toEndDate()),
[lifetimeIndex, stamp.duration],
)
const [sizeMarks, setSizeMarks] = useState<{ value: number; label: string }[]>([])
const [extensionCost, setExtensionCost] = useState('0')
const [isSubmitting, setIsSubmitting] = useState(false)
const modalRoot = document.querySelector('.fm-main') || document.body
const isMountedRef = useRef(true)
const { startPolling } = useStampPolling({
refreshStamp,
onStampUpdated: (updatedStamp: PostageBatch) => {
window.dispatchEvent(
new CustomEvent(FILE_MANAGER_EVENTS.DRIVE_UPGRADE_END, {
detail: {
driveId: drive.id.toString(),
success: true,
updatedStamp,
},
}),
)
},
onTimeout: (finalStamp: PostageBatch | null) => {
window.dispatchEvent(
new CustomEvent(FILE_MANAGER_EVENTS.DRIVE_UPGRADE_TIMEOUT, {
detail: {
driveId: drive.id.toString(),
finalStamp: finalStamp || null,
},
}),
)
},
onPollingStateChange: () => {
// no-op
},
timeout: POLLING_TIMEOUT_MS,
})
const handleCapacityChange = (value: number, index: number) => {
setCapacity(value === -1 ? stamp.size : Size.fromBytes(value))
setCapacityIndex(index)
}
const handleCostCalculation = useCallback(
async (
batchId: BatchId,
capacity: Size,
duration: Duration,
options: BeeRequestOptions | undefined,
encryption: boolean,
erasureCodeLevel: RedundancyLevel,
isCapacityExtensionSet: boolean,
) => {
let cost: BZZ | undefined
try {
cost = await beeApi?.getExtensionCost(batchId, capacity, duration, options, encryption, erasureCodeLevel)
} catch {
setErrorMessage?.('Failed to calculate extension cost')
setShowError(true)
return
}
const costText = cost ? cost.toSignificantDigits(2) : '0'
if (!isMountedRef.current) return
if ((walletBalance && cost && cost.gte(walletBalance.bzzBalance)) || !walletBalance) {
setIsBalanceSufficient(false)
} else {
setIsBalanceSufficient(true)
}
if (isCapacityExtensionSet) {
setCapacityExtensionCost('')
setDurationExtensionCost('')
} else {
setCapacityExtensionCost('0')
setDurationExtensionCost(costText)
}
setExtensionCost(costText)
},
[beeApi, walletBalance, isMountedRef, setErrorMessage, setShowError],
)
useEffect(() => {
isMountedRef.current = true
return () => {
isMountedRef.current = false
}
}, [])
useEffect(() => {
const fetchSizes = () => {
const sizes = Array.from(Utils.getStampEffectiveBytesBreakpoints(false, defaultErasureCodeLevel).values())
const capacityValues = capacityBreakpoints[encryption_off][defaultErasureCodeLevel]
const fromIndex = capacityValues.findIndex(item => item.batchDepth === stamp.depth)
const newSizes = sizes.slice(fromIndex + 1)
const updatedSizes = [
{ value: -1, label: 'No additional storage (0 GB)' },
...newSizes.map(size => ({
value: size,
label: getHumanReadableFileSize(size - stamp.size.toBytes()),
})),
]
setSizeMarks(updatedSizes)
}
fetchSizes()
}, [stamp.depth, stamp.size])
useEffect(() => {
const fetchExtensionCost = () => {
const isCapacitySet = capacityIndex > 0
const extendDuration = Duration.fromEndDate(validityEndDate, stamp.duration.toEndDate())
handleCostCalculation(
stamp.batchID,
capacity,
extendDuration,
undefined,
false,
defaultErasureCodeLevel,
isCapacitySet,
)
}
fetchExtensionCost()
}, [capacity, validityEndDate, capacityIndex, handleCostCalculation, stamp.batchID, stamp.duration])
const batchIdStr = stamp.batchID.toString()
const shortBatchId = batchIdStr.length > 12 ? `${batchIdStr.slice(0, 4)}...${batchIdStr.slice(-4)}` : batchIdStr
return createPortal(
<div
className={`fm-modal-container fm-upgrade-drive-modal-container${
containerColor === 'none' ? ' fm-modal-container-no-bg' : ''
}`}
>
<div className="fm-modal-window fm-upgrade-drive-modal">
<div className="fm-modal-window-header">
<DriveIcon size="18px" /> Upgrade {truncateNameMiddle(drive.name || stamp.label || shortBatchId, 35)}
</div>
<div>Choose extension period and additional storage for your drive.</div>
<div className="fm-modal-window-scrollable">
<div className="fm-modal-window-body">
<div className="fm-upgrade-drive-modal-wallet">
<div className="fm-upgrade-drive-modal-wallet-header fm-emphasized-text">
<WalletIcon size="14px" color="rgb(237, 129, 49)" /> Wallet information
</div>
{walletBalance && nodeAddresses ? (
<div className="fm-upgrade-drive-modal-wallet-info-container">
<div className="fm-upgrade-drive-modal-wallet-info">
<div>Balance</div>
<div>{`${walletBalance.bzzBalance.toSignificantDigits(4)} xBZZ`}</div>
</div>
<div className="fm-upgrade-drive-modal-wallet-info">
<div>Wallet address:</div>
<div className="fm-value-snippet">{`${walletBalance.walletAddress.slice(
0,
4,
)}...${walletBalance.walletAddress.slice(-4)}`}</div>
</div>
</div>
) : (
<div>Wallet information is not available</div>
)}
<div className="fm-upgrade-drive-modal-info fm-swarm-orange-font">
<a
className="fm-upgrade-drive-modal-info-link fm-pointer"
href="https://www.ethswarm.org/get-bzz#how-to-get-bzz"
target="_blank"
rel="noopener noreferrer"
>
<ExternalLinkIcon size="14px" />
Need help topping up?
</a>
</div>
</div>
</div>
<div className="fm-modal-window-body">
<div className="fm-upgrade-drive-modal-input-row">
<div className="fm-modal-window-input-container">
<CustomDropdown
id="drive-type"
label="Additional storage"
icon={<DatabaseIcon size="14px" color="rgb(237, 129, 49)" />}
options={sizeMarks}
value={capacityIndex === 0 ? -1 : capacity.toBytes()}
onChange={handleCapacityChange}
/>
</div>
<div className="fm-modal-window-input-container">
<CustomDropdown
id="drive-type"
label="Duration"
icon={<CalendarIcon size="14px" color="rgb(237, 129, 49)" />}
options={desiredLifetimeOptions}
value={lifetimeIndex}
onChange={(value, _) => {
setLifetimeIndex(value)
}}
/>
</div>
</div>
<div className="fm-modal-white-section">
<div className="fm-emphasized-text">Summary</div>
<div>
Drive: {truncateNameMiddle(drive.name)} {drive.isAdmin && <Warning style={{ fontSize: '16px' }} />}
</div>
<div>
BatchId: {truncateNameMiddle(stamp.label, 25)} ({shortBatchId})
</div>
<div>Expiry: {stamp.duration.toEndDate().toLocaleDateString()}</div>
<div>
Additional storage:{' '}
{(() => {
if (capacityIndex === 0) return '0 GB'
return `${
fromBytesConversion(Math.max(capacity.toBytes() - stamp.size.toBytes(), 0), 'GB').toFixed(3) + ' GB'
} ${durationExtensionCost === '' ? '' : '(' + extensionCost + ' xBZZ)'}`
})()}
</div>
<div>
Extension period:{' '}
{`${desiredLifetimeOptions[lifetimeIndex]?.label} ${
capacityExtensionCost === '' ? '' : '(' + extensionCost + ' xBZZ)'
}`}
</div>
<div className="fm-upgrade-drive-modal-info fm-emphasized-text">
Total:{' '}
<span className="fm-swarm-orange-font">
{extensionCost} xBZZ {isBalanceSufficient ? '' : '(Insufficient balance)'}
</span>
</div>
</div>
</div>
</div>
<div className="fm-modal-window-footer">
<Button
label={isSubmitting ? 'Confirming…' : 'Confirm upgrade'}
variant="primary"
disabled={isSubmitting || !isBalanceSufficient || !walletBalance || !beeApi}
onClick={async () => {
if (!beeApi || !walletBalance) return
const isStampValid = await validateStampStillExists(beeApi, stamp.batchID)
if (!isStampValid) {
setErrorMessage?.('The admin drive has expired. Please clear the browser cache and reload the page.')
setShowError(true)
onCancelClick()
return
}
try {
setIsSubmitting(true)
window.dispatchEvent(
new CustomEvent(FILE_MANAGER_EVENTS.DRIVE_UPGRADE_START, {
detail: { driveId: drive.id.toString() },
}),
)
onCancelClick()
await beeApi.extendStorage(
stamp.batchID,
capacity,
lifetimeIndex >= 0
? Duration.fromEndDate(validityEndDate, stamp.duration.toEndDate())
: Duration.ZERO,
undefined,
false,
defaultErasureCodeLevel,
)
startPolling(stamp, capacityIndex > 0)
} catch (e) {
const msg = e instanceof Error ? e.message : 'Upgrade failed'
window.dispatchEvent(
new CustomEvent(FILE_MANAGER_EVENTS.DRIVE_UPGRADE_END, {
detail: {
driveId: drive.id.toString(),
success: false,
error: msg + ' (drive: ' + drive.name + ')',
},
}),
)
}
}}
/>
<Button label="Cancel" variant="secondary" disabled={isSubmitting} onClick={onCancelClick} />
</div>
{isSubmitting && (
<div className="fm-drive-item-creating-overlay">
<div className="fm-mini-spinner" />
<span>Please wait</span>
</div>
)}
</div>
</div>,
modalRoot,
)
}