a7e42053ae
* fix: spdv-1007 * refactor: spdv-1007 * refactor: spdv-1007
382 lines
14 KiB
TypeScript
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,
|
|
)
|
|
}
|