* feat: add file manager module - Complete file manager implementation with UI/UX - Add drive management functionality - Add file upload/download with progress tracking - Add stamp integration and handling - Add bulk operations and context menus Co-authored-by: Roland Seres <roland.seres90@gmail.com> Co-authored-by: nidishk <nidishkrishnan45@gmail.com>
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
import { ReactElement, useCallback, useContext, useEffect, useRef, useState } from 'react'
|
||||
import './UpgradeDriveModal.scss'
|
||||
import '../../styles/global.scss'
|
||||
import { CustomDropdown } from '../CustomDropdown/CustomDropdown'
|
||||
import { Button } from '../Button/Button'
|
||||
import { createPortal } from 'react-dom'
|
||||
import DriveIcon from 'remixicon-react/HardDrive2LineIcon'
|
||||
import DatabaseIcon from 'remixicon-react/Database2LineIcon'
|
||||
import WalletIcon from 'remixicon-react/Wallet3LineIcon'
|
||||
import ExternalLinkIcon from 'remixicon-react/ExternalLinkLineIcon'
|
||||
import CalendarIcon from 'remixicon-react/CalendarLineIcon'
|
||||
import { desiredLifetimeOptions } from '../../constants/stamps'
|
||||
import { Context as BeeContext } from '../../../../providers/Bee'
|
||||
import { fromBytesConversion, getExpiryDateByLifetime } from '../../utils/common'
|
||||
import { Context as SettingsContext } from '../../../../providers/Settings'
|
||||
import { Context as FMContext } from '../../../../providers/FileManager'
|
||||
|
||||
import {
|
||||
BatchId,
|
||||
BeeRequestOptions,
|
||||
BZZ,
|
||||
capacityBreakpoints,
|
||||
Duration,
|
||||
PostageBatch,
|
||||
RedundancyLevel,
|
||||
Size,
|
||||
Utils,
|
||||
} from '@ethersphere/bee-js'
|
||||
import { DriveInfo } from '@solarpunkltd/file-manager-lib'
|
||||
import { getHumanReadableFileSize } from '../../../../utils/file'
|
||||
import { Warning } from '@material-ui/icons'
|
||||
|
||||
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 { setShowError } = useContext(FMContext)
|
||||
|
||||
const [isBalanceSufficient, setIsBalanceSufficient] = useState(true)
|
||||
const [capacity, setCapacity] = useState(Size.fromBytes(0))
|
||||
const [capacityExtensionCost, setCapacityExtensionCost] = useState('')
|
||||
const [capacityIndex, setCapacityIndex] = useState(0)
|
||||
const [durationExtensionCost, setDurationExtensionCost] = useState('')
|
||||
const [lifetimeIndex, setLifetimeIndex] = useState(0)
|
||||
const [validityEndDate, setValidityEndDate] = useState(new Date())
|
||||
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)
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
isMountedRef.current = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleCapacityChange = (value: number, index: number) => {
|
||||
setCapacity(Size.fromBytes(value === -1 ? 0 : value))
|
||||
setCapacityIndex(index)
|
||||
}
|
||||
|
||||
const handleCostCalculation = useCallback(
|
||||
async (
|
||||
batchId: BatchId,
|
||||
capacity: Size,
|
||||
duration: Duration,
|
||||
options: BeeRequestOptions | undefined,
|
||||
encryption: boolean,
|
||||
erasureCodeLevel: RedundancyLevel,
|
||||
isCapacityExtensionSet: boolean,
|
||||
isDurationExtensionSet: boolean,
|
||||
) => {
|
||||
setIsBalanceSufficient(true)
|
||||
|
||||
let cost: BZZ | undefined
|
||||
|
||||
try {
|
||||
cost = await beeApi?.getExtensionCost(batchId, capacity, duration, options, encryption, erasureCodeLevel)
|
||||
} catch (e) {
|
||||
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)
|
||||
}
|
||||
|
||||
const bothExtensions = isCapacityExtensionSet && isDurationExtensionSet
|
||||
const capacityOnly = isCapacityExtensionSet && !isDurationExtensionSet
|
||||
const durationOnly = !isCapacityExtensionSet && isDurationExtensionSet
|
||||
const noExtensions = !isCapacityExtensionSet && !isDurationExtensionSet
|
||||
|
||||
if (bothExtensions) {
|
||||
setCapacityExtensionCost('')
|
||||
setDurationExtensionCost('')
|
||||
} else if (capacityOnly) {
|
||||
setCapacityExtensionCost(costText)
|
||||
setDurationExtensionCost('0')
|
||||
} else if (durationOnly) {
|
||||
setCapacityExtensionCost('0')
|
||||
setDurationExtensionCost(costText)
|
||||
} else {
|
||||
setCapacityExtensionCost('0')
|
||||
setDurationExtensionCost('0')
|
||||
}
|
||||
|
||||
setExtensionCost(noExtensions ? '0' : costText)
|
||||
},
|
||||
[beeApi, walletBalance, setErrorMessage, setShowError],
|
||||
)
|
||||
|
||||
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 isDurationSet = true
|
||||
const duration = Duration.fromEndDate(validityEndDate)
|
||||
|
||||
handleCostCalculation(
|
||||
stamp.batchID,
|
||||
capacity,
|
||||
duration,
|
||||
undefined,
|
||||
false,
|
||||
defaultErasureCodeLevel,
|
||||
isCapacitySet,
|
||||
isDurationSet,
|
||||
)
|
||||
}
|
||||
|
||||
fetchExtensionCost()
|
||||
}, [capacity, validityEndDate, capacityIndex, handleCostCalculation, lifetimeIndex, stamp.batchID])
|
||||
|
||||
useEffect(() => {
|
||||
setValidityEndDate(getExpiryDateByLifetime(lifetimeIndex, stamp.duration.toEndDate()))
|
||||
}, [lifetimeIndex, 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${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 {drive.name || stamp.label || shortBatchId}
|
||||
</div>
|
||||
<div>Choose extension period and additional storage for your drive.</div>
|
||||
<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, index) => {
|
||||
setLifetimeIndex(value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="fm-modal-white-section">
|
||||
<div className="fm-emphasized-text">Summary</div>
|
||||
<div>
|
||||
Drive: {drive.name} {drive.isAdmin && <Warning style={{ fontSize: '16px' }} />}
|
||||
</div>
|
||||
<div>
|
||||
BatchId: {stamp.label} ({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 className="fm-modal-window-footer">
|
||||
<Button
|
||||
label={isSubmitting ? 'Confirming…' : 'Confirm upgrade'}
|
||||
variant="primary"
|
||||
disabled={isSubmitting || !isBalanceSufficient || !walletBalance || !beeApi}
|
||||
onClick={async () => {
|
||||
if (!beeApi || !walletBalance) return
|
||||
|
||||
try {
|
||||
setIsSubmitting(true)
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('fm:drive-upgrade-start', {
|
||||
detail: { driveId: drive.id.toString() },
|
||||
}),
|
||||
)
|
||||
|
||||
onCancelClick()
|
||||
|
||||
await beeApi.extendStorage(
|
||||
stamp.batchID,
|
||||
capacity,
|
||||
durationExtensionCost === '0'
|
||||
? Duration.ZERO
|
||||
: Duration.fromEndDate(validityEndDate, stamp.duration.toEndDate()),
|
||||
undefined,
|
||||
false,
|
||||
defaultErasureCodeLevel,
|
||||
)
|
||||
|
||||
// TODO: replace eventlisteners with a better maintainable solution
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('fm:drive-upgrade-end', {
|
||||
detail: { driveId: drive.id.toString(), success: true },
|
||||
}),
|
||||
)
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : 'Upgrade failed'
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('fm: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,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user