Feat: FileManager (#98) (#703)

* 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:
Bálint Ujvári
2025-11-12 11:26:00 +01:00
committed by GitHub
parent 1249c0df71
commit 5bfe2a0331
107 changed files with 21529 additions and 5578 deletions
@@ -0,0 +1,218 @@
import { ReactElement, useState, useContext, useEffect, useRef, useMemo } from 'react'
import { createPortal } from 'react-dom'
import Drive from 'remixicon-react/HardDrive2LineIcon'
import DriveFill from 'remixicon-react/HardDrive2FillIcon'
import MoreFill from 'remixicon-react/MoreFillIcon'
import './DriveItem.scss'
import { ProgressBar } from '../../ProgressBar/ProgressBar'
import { ContextMenu } from '../../ContextMenu/ContextMenu'
import { useContextMenu } from '../../../hooks/useContextMenu'
import { Button } from '../../Button/Button'
import { DestroyDriveModal } from '../../DestroyDriveModal/DestroyDriveModal'
import { UpgradeDriveModal } from '../../UpgradeDriveModal/UpgradeDriveModal'
import { ViewType } from '../../../constants/transfers'
import { useView } from '../../../../../pages/filemanager/ViewContext'
import { Context as FMContext } from '../../../../../providers/FileManager'
import { PostageBatch } from '@ethersphere/bee-js'
import { DriveInfo } from '@solarpunkltd/file-manager-lib'
import { calculateStampCapacityMetrics, handleDestroyDrive } from '../../../utils/bee'
import { Context as SettingsContext } from '../../../../../providers/Settings'
interface DriveItemProps {
drive: DriveInfo
stamp: PostageBatch
isSelected: boolean
setErrorMessage?: (error: string) => void
}
export function DriveItem({ drive, stamp, isSelected, setErrorMessage }: DriveItemProps): ReactElement {
const { fm, setShowError, refreshStamp } = useContext(FMContext)
const { beeApi } = useContext(SettingsContext)
const [isHovered, setIsHovered] = useState(false)
const [isDestroyDriveModalOpen, setIsDestroyDriveModalOpen] = useState(false)
const [isUpgradeDriveModalOpen, setIsUpgradeDriveModalOpen] = useState(false)
const isMountedRef = useRef(true)
const [isUpgrading, setIsUpgrading] = useState(false)
const [actualStamp, setActualStamp] = useState<PostageBatch>(stamp)
const { showContext, pos, contextRef, setPos, setShowContext } = useContextMenu<HTMLDivElement>()
const { setView, setActualItemView } = useView()
useEffect(() => {
return () => {
isMountedRef.current = false
}
}, [])
useEffect(() => {
setActualStamp(stamp)
}, [stamp])
function handleMenuClick(e: React.MouseEvent) {
setShowContext(true)
setPos({ x: e.clientX, y: e.clientY })
}
function handleDestroyDriveClick() {
setShowContext(false)
}
useEffect(() => {
const id = drive.id.toString()
const batchId = stamp.batchID.toString()
const onStart = (e: Event) => {
const { driveId } = (e as CustomEvent).detail || {}
if (driveId === id) {
setIsUpgrading(true)
}
}
const onEnd = async (e: Event) => {
const { driveId, success, error } = (e as CustomEvent).detail || {}
if (!success) {
if (error) {
setErrorMessage?.(error)
}
setShowError(true)
}
if (driveId === id) {
setIsUpgrading(false)
const upgradedStamp = await refreshStamp(batchId)
if (!isMountedRef.current) return
if (upgradedStamp) {
setActualStamp(upgradedStamp)
}
}
}
window.addEventListener('fm:drive-upgrade-start', onStart as EventListener)
window.addEventListener('fm:drive-upgrade-end', onEnd as EventListener)
return () => {
window.removeEventListener('fm:drive-upgrade-start', onStart as EventListener)
window.removeEventListener('fm:drive-upgrade-end', onEnd as EventListener)
}
}, [drive.id, setShowError, setErrorMessage, stamp.batchID, refreshStamp])
const { capacityPct, usedSize, totalSize } = useMemo(
() => calculateStampCapacityMetrics(actualStamp, drive),
[actualStamp, drive],
)
return (
<div
className={`fm-drive-item-container${isSelected ? ' fm-drive-item-container-selected' : ''}`}
onClick={() => {
setView(ViewType.File)
setActualItemView?.(drive.name)
}}
>
<div
className="fm-drive-item-info"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div className="fm-drive-item-header">
<div className="fm-drive-item-icon">{isHovered ? <DriveFill size="16px" /> : <Drive size="16px" />}</div>
<div>{drive.name}</div>
</div>
<div className="fm-drive-item-content">
<div className="fm-drive-item-capacity">
Capacity <ProgressBar value={capacityPct} width="64px" /> {usedSize} / {totalSize}
</div>
<div className="fm-drive-item-capacity">
Expiry date: {actualStamp.duration.toEndDate().toLocaleDateString()}
</div>
</div>
</div>
<div className="fm-drive-item-actions">
<MoreFill
size="13"
className={`fm-pointer${isUpgrading ? ' fm-disabled' : ''}`}
onClick={!isUpgrading ? handleMenuClick : undefined}
aria-disabled={isUpgrading ? 'true' : 'false'}
/>
{showContext &&
createPortal(
<div
ref={contextRef}
className="fm-drive-item-context-menu"
style={{
top: pos.y,
left: pos.x,
}}
>
<ContextMenu>
<div
className="fm-context-item red"
onClick={() => {
handleDestroyDriveClick()
setIsDestroyDriveModalOpen(true)
}}
>
Destroy entire drive
</div>
</ContextMenu>
</div>,
document.body,
)}
<Button
label="Upgrade"
variant="primary"
size="small"
disabled={isUpgrading}
onClick={() => setIsUpgradeDriveModalOpen(true)}
/>
</div>
{isUpgradeDriveModalOpen && (
<UpgradeDriveModal
stamp={actualStamp}
drive={drive}
onCancelClick={() => setIsUpgradeDriveModalOpen(false)}
setErrorMessage={setErrorMessage}
/>
)}
{isUpgrading && (
<div className="fm-drive-item-creating-overlay" aria-live="polite">
<div className="fm-mini-spinner" />
<span>Upgrading drive</span>
</div>
)}
{isDestroyDriveModalOpen && (
<DestroyDriveModal
drive={drive}
onCancelClick={() => setIsDestroyDriveModalOpen(false)}
doDestroy={async () => {
setIsDestroyDriveModalOpen(false)
await handleDestroyDrive(
beeApi,
fm,
drive,
() => {
setIsDestroyDriveModalOpen(false)
},
e => {
setIsDestroyDriveModalOpen(false)
setErrorMessage?.(`Error destroying drive: ${drive.name}: ${e}`)
setShowError(true)
},
)
}}
/>
)}
</div>
)
}