Fix: file-manager and swarm-desktop bugs (#714)
- drive capacity display with stamp polling - download/upload progress handling - overlay and tooltip issues - FileMaganger readme - ultra-light mode handling - account feed view page - download media files - remove not found syncing link - fix ultra light node wallet page - tooltip issues --------- Co-authored-by: Andrei Mitrea <andrei.mitrea.hq@gmail.com> Co-authored-by: nidishk <nidishkrishnan45@gmail.com> Co-authored-by: Ferenc Sárai <sarai.ferenc@gmail.com> Co-authored-by: Nándor Komlódi <nandor.komlodi@gmail.com> Co-authored-by: rolandlor <33499567+rolandlor@users.noreply.github.com>
This commit is contained in:
@@ -5,6 +5,8 @@
|
||||
color: rgb(31, 41, 55);
|
||||
font-weight: 500;
|
||||
transition: background-color 0.3s;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.fm-drive-item-icon {
|
||||
display: flex;
|
||||
@@ -26,6 +28,7 @@
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.fm-drive-item-container {
|
||||
@@ -48,7 +51,41 @@
|
||||
.fm-drive-item-capacity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 4px;
|
||||
position: relative;
|
||||
|
||||
& > span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.fm-tooltip-wrapper {
|
||||
flex-shrink: 0;
|
||||
margin-left: 0;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
filter: none !important;
|
||||
opacity: 1 !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
&.fm-drive-item-capacity-updating {
|
||||
& > span {
|
||||
filter: blur(2px);
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// Keep tooltip interactive even when capacity is updating
|
||||
.fm-tooltip-wrapper {
|
||||
filter: none;
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.fm-drive-item-actions {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ReactElement, useState, useContext, useEffect, useRef, useMemo } from 'react'
|
||||
import { ReactElement, useState, useContext, useEffect, useMemo, useCallback, useRef, memo } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import Drive from 'remixicon-react/HardDrive2LineIcon'
|
||||
import DriveFill from 'remixicon-react/HardDrive2FillIcon'
|
||||
@@ -8,15 +8,150 @@ 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 { DestroyDriveModal, ProgressDestroyModal } from '../../DestroyDriveModal/DestroyDriveModal'
|
||||
import { UpgradeDriveModal } from '../../UpgradeDriveModal/UpgradeDriveModal'
|
||||
import { UpgradeTimeoutModal } from '../../UpgradeTimeoutModal/UpgradeTimeoutModal'
|
||||
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 { calculateStampCapacityMetrics, handleDestroyAndForgetDrive } from '../../../utils/bee'
|
||||
import { Context as SettingsContext } from '../../../../../providers/Settings'
|
||||
import { truncateNameMiddle } from '../../../utils/common'
|
||||
import { Tooltip } from '../../Tooltip/Tooltip'
|
||||
import { TOOLTIPS } from '../../../constants/tooltips'
|
||||
import { FILE_MANAGER_EVENTS, UPLOAD_POLLING_TIMEOUT_MS } from '../../../constants/common'
|
||||
import { useStampPolling } from '../../../hooks/useStampPolling'
|
||||
|
||||
function useDriveEventListeners(
|
||||
driveId: string,
|
||||
handleUpgradeStart: (eventDriveId: string, id: string) => void,
|
||||
handleUpgradeEnd: (
|
||||
eventDriveId: string,
|
||||
id: string,
|
||||
success: boolean,
|
||||
error: string | undefined,
|
||||
updatedStamp?: PostageBatch,
|
||||
) => void,
|
||||
handleUpgradeTimeout: (eventDriveId: string, id: string) => void,
|
||||
handleFileUploaded: (e: Event) => void,
|
||||
) {
|
||||
useEffect(() => {
|
||||
const onStart = (e: Event) => {
|
||||
const { driveId: eventDriveId } = (e as CustomEvent).detail || {}
|
||||
handleUpgradeStart(eventDriveId, driveId)
|
||||
}
|
||||
|
||||
const onEnd = (e: Event) => {
|
||||
const { driveId: eventDriveId, success, error, updatedStamp } = (e as CustomEvent).detail || {}
|
||||
handleUpgradeEnd(eventDriveId, driveId, success, error, updatedStamp)
|
||||
}
|
||||
|
||||
const onTimeout = (e: Event) => {
|
||||
const { driveId: eventDriveId } = (e as CustomEvent).detail || {}
|
||||
handleUpgradeTimeout(eventDriveId, driveId)
|
||||
}
|
||||
|
||||
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)
|
||||
window.addEventListener(FILE_MANAGER_EVENTS.FILE_UPLOADED, handleFileUploaded 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)
|
||||
window.removeEventListener(FILE_MANAGER_EVENTS.FILE_UPLOADED, handleFileUploaded as EventListener)
|
||||
}
|
||||
}, [driveId, handleUpgradeStart, handleUpgradeEnd, handleUpgradeTimeout, handleFileUploaded])
|
||||
}
|
||||
|
||||
interface DriveModalsProps {
|
||||
isUpgradeDriveModalOpen: boolean
|
||||
setIsUpgradeDriveModalOpen: (open: boolean) => void
|
||||
isUpgradeTimeoutModalOpen: boolean
|
||||
actualStamp: PostageBatch
|
||||
drive: DriveInfo
|
||||
setErrorMessage?: (error: string) => void
|
||||
isUpgrading: boolean
|
||||
isCapacityUpdating: boolean
|
||||
isDestroying: boolean
|
||||
setIsProgressModalOpen: (open: boolean) => void
|
||||
isProgressModalOpen: boolean
|
||||
isDestroyDriveModalOpen: boolean
|
||||
setIsDestroyDriveModalOpen: (open: boolean) => void
|
||||
doDestroy: () => Promise<void>
|
||||
onCancelTimeout: () => void
|
||||
}
|
||||
|
||||
function DriveModals({
|
||||
isUpgradeDriveModalOpen,
|
||||
setIsUpgradeDriveModalOpen,
|
||||
isUpgradeTimeoutModalOpen,
|
||||
actualStamp,
|
||||
drive,
|
||||
setErrorMessage,
|
||||
isUpgrading,
|
||||
isCapacityUpdating,
|
||||
isDestroying,
|
||||
setIsProgressModalOpen,
|
||||
isProgressModalOpen,
|
||||
isDestroyDriveModalOpen,
|
||||
setIsDestroyDriveModalOpen,
|
||||
doDestroy,
|
||||
onCancelTimeout,
|
||||
}: DriveModalsProps): ReactElement | null {
|
||||
return (
|
||||
<>
|
||||
{isUpgradeDriveModalOpen && (
|
||||
<UpgradeDriveModal
|
||||
stamp={actualStamp}
|
||||
drive={drive}
|
||||
onCancelClick={() => setIsUpgradeDriveModalOpen(false)}
|
||||
setErrorMessage={setErrorMessage}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isUpgradeTimeoutModalOpen && <UpgradeTimeoutModal driveName={drive.name} onOk={onCancelTimeout} />}
|
||||
|
||||
{isUpgrading && (
|
||||
<div className="fm-drive-item-creating-overlay" aria-live="polite">
|
||||
<div className="fm-mini-spinner" />
|
||||
<span>Upgrading drive…</span>
|
||||
</div>
|
||||
)}
|
||||
{isCapacityUpdating && !isUpgrading && (
|
||||
<div className="fm-drive-item-creating-overlay" aria-live="polite">
|
||||
<div className="fm-mini-spinner" />
|
||||
<span>Updating capacity…</span>
|
||||
</div>
|
||||
)}
|
||||
{isDestroying && (
|
||||
<div
|
||||
className="fm-drive-item-creating-overlay"
|
||||
aria-live="polite"
|
||||
onClick={() => setIsProgressModalOpen(true)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
title="Click to show progress modal"
|
||||
>
|
||||
<div className="fm-mini-spinner" />
|
||||
<span>Destroying drive…</span>
|
||||
</div>
|
||||
)}
|
||||
{isProgressModalOpen && isDestroying && (
|
||||
<ProgressDestroyModal drive={drive} onMinimize={() => setIsProgressModalOpen(false)} />
|
||||
)}
|
||||
{isDestroyDriveModalOpen && (
|
||||
<DestroyDriveModal
|
||||
drive={drive}
|
||||
onCancelClick={() => setIsDestroyDriveModalOpen(false)}
|
||||
doDestroy={doDestroy}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
interface DriveItemProps {
|
||||
drive: DriveInfo
|
||||
@@ -25,122 +160,247 @@ interface DriveItemProps {
|
||||
setErrorMessage?: (error: string) => void
|
||||
}
|
||||
|
||||
export function DriveItem({ drive, stamp, isSelected, setErrorMessage }: DriveItemProps): ReactElement {
|
||||
const { fm, setShowError, refreshStamp } = useContext(FMContext)
|
||||
function DriveItemComponent({ drive, stamp, isSelected, setErrorMessage }: DriveItemProps): ReactElement {
|
||||
const { fm, adminDrive, files, setShowError, refreshStamp } = useContext(FMContext)
|
||||
const { beeApi } = useContext(SettingsContext)
|
||||
|
||||
const driveId = drive.id.toString()
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [isDestroyDriveModalOpen, setIsDestroyDriveModalOpen] = useState(false)
|
||||
const [isProgressModalOpen, setIsProgressModalOpen] = useState(false)
|
||||
const [isUpgradeDriveModalOpen, setIsUpgradeDriveModalOpen] = useState(false)
|
||||
const isMountedRef = useRef(true)
|
||||
const [isUpgradeTimeoutModalOpen, setIsUpgradeTimeoutModalOpen] = useState(false)
|
||||
const [isUpgrading, setIsUpgrading] = useState(false)
|
||||
const [isCapacityUpdating, setIsCapacityUpdating] = useState(false)
|
||||
const [isDestroying, setIsDestroying] = useState(false)
|
||||
const [actualStamp, setActualStamp] = useState<PostageBatch>(stamp)
|
||||
const batchIDRef = useRef(stamp.batchID)
|
||||
const isUpgradingRef = useRef(false)
|
||||
const actualStampRef = useRef(actualStamp)
|
||||
const startPollingRef = useRef<((stamp: PostageBatch) => void) | null>(null)
|
||||
const stopPollingRef = useRef<(() => void) | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
actualStampRef.current = actualStamp
|
||||
}, [actualStamp])
|
||||
|
||||
const handleStampUpdated = useCallback((updatedStamp: PostageBatch) => {
|
||||
setActualStamp(updatedStamp)
|
||||
batchIDRef.current = updatedStamp.batchID
|
||||
}, [])
|
||||
|
||||
const handlePollingStateChange = useCallback((isPolling: boolean) => {
|
||||
setIsCapacityUpdating(isPolling)
|
||||
}, [])
|
||||
|
||||
const { startPolling, stopPolling } = useStampPolling({
|
||||
onStampUpdated: handleStampUpdated,
|
||||
onPollingStateChange: handlePollingStateChange,
|
||||
refreshStamp,
|
||||
timeout: UPLOAD_POLLING_TIMEOUT_MS,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
startPollingRef.current = startPolling
|
||||
}, [startPolling])
|
||||
|
||||
useEffect(() => {
|
||||
stopPollingRef.current = stopPolling
|
||||
}, [stopPolling])
|
||||
|
||||
const { showContext, pos, contextRef, setPos, setShowContext } = useContextMenu<HTMLDivElement>()
|
||||
|
||||
const { setView, setActualItemView } = useView()
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
isMountedRef.current = false
|
||||
if (isUpgradingRef.current) {
|
||||
return
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (actualStamp.batchID.toString() !== stamp.batchID.toString()) {
|
||||
setActualStamp(stamp)
|
||||
batchIDRef.current = stamp.batchID
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const incomingSize = stamp.size.toBytes()
|
||||
const currentSize = actualStamp.size.toBytes()
|
||||
const incomingExpiry = stamp.duration.toEndDate().getTime()
|
||||
const currentExpiry = actualStamp.duration.toEndDate().getTime()
|
||||
|
||||
if (incomingSize > currentSize || incomingExpiry > currentExpiry) {
|
||||
setActualStamp(stamp)
|
||||
batchIDRef.current = stamp.batchID
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [stamp])
|
||||
|
||||
useEffect(() => {
|
||||
setActualStamp(stamp)
|
||||
}, [stamp])
|
||||
return () => {
|
||||
if (stopPollingRef.current) {
|
||||
stopPollingRef.current()
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
function handleMenuClick(e: React.MouseEvent) {
|
||||
setShowContext(true)
|
||||
setPos({ x: e.clientX, y: e.clientY })
|
||||
}
|
||||
|
||||
function handleDestroyDriveClick() {
|
||||
setShowContext(false)
|
||||
}
|
||||
const handleUpgradeStart = useCallback((driveId: string, id: string) => {
|
||||
if (driveId !== id) return
|
||||
|
||||
useEffect(() => {
|
||||
const id = drive.id.toString()
|
||||
const batchId = stamp.batchID.toString()
|
||||
isUpgradingRef.current = true
|
||||
setIsUpgrading(true)
|
||||
}, [])
|
||||
|
||||
const onStart = (e: Event) => {
|
||||
const { driveId } = (e as CustomEvent).detail || {}
|
||||
const handleUpgradeEnd = useCallback(
|
||||
(driveId: string, id: string, success: boolean, error: string | undefined, updatedStamp?: PostageBatch) => {
|
||||
if (driveId !== id) return
|
||||
|
||||
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) {
|
||||
const resetUpgrading = () => {
|
||||
setIsUpgrading(false)
|
||||
|
||||
const upgradedStamp = await refreshStamp(batchId)
|
||||
|
||||
if (!isMountedRef.current) return
|
||||
|
||||
if (upgradedStamp) {
|
||||
setActualStamp(upgradedStamp)
|
||||
}
|
||||
isUpgradingRef.current = false
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('fm:drive-upgrade-start', onStart as EventListener)
|
||||
window.addEventListener('fm:drive-upgrade-end', onEnd as EventListener)
|
||||
if (!success && error) {
|
||||
resetUpgrading()
|
||||
setErrorMessage?.(error)
|
||||
setShowError(true)
|
||||
|
||||
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])
|
||||
return
|
||||
}
|
||||
|
||||
const { capacityPct, usedSize, totalSize } = useMemo(
|
||||
() => calculateStampCapacityMetrics(actualStamp, drive),
|
||||
[actualStamp, drive],
|
||||
if (updatedStamp) {
|
||||
setActualStamp(updatedStamp)
|
||||
batchIDRef.current = updatedStamp.batchID
|
||||
setTimeout(resetUpgrading, 300)
|
||||
} else {
|
||||
resetUpgrading()
|
||||
}
|
||||
},
|
||||
[setErrorMessage, setShowError],
|
||||
)
|
||||
|
||||
const doDestroy = useCallback(async () => {
|
||||
const closeModals = () => {
|
||||
setIsDestroyDriveModalOpen(false)
|
||||
setIsDestroying(false)
|
||||
setIsProgressModalOpen(false)
|
||||
}
|
||||
|
||||
setIsDestroyDriveModalOpen(false)
|
||||
setIsProgressModalOpen(true)
|
||||
setIsDestroying(true)
|
||||
|
||||
await handleDestroyAndForgetDrive({
|
||||
beeApi,
|
||||
fm,
|
||||
drive,
|
||||
isDestroy: true,
|
||||
adminDrive,
|
||||
onSuccess: closeModals,
|
||||
onError: e => {
|
||||
closeModals()
|
||||
setErrorMessage?.(`Error destroying drive: ${drive.name}: ${e}`)
|
||||
setShowError(true)
|
||||
},
|
||||
})
|
||||
}, [beeApi, fm, drive, adminDrive, setErrorMessage, setShowError])
|
||||
|
||||
const handleUpgradeTimeout = useCallback(
|
||||
(eventDriveId: string, id: string) => {
|
||||
if (eventDriveId !== id) return
|
||||
setIsUpgradeTimeoutModalOpen(true)
|
||||
},
|
||||
[setIsUpgradeTimeoutModalOpen],
|
||||
)
|
||||
|
||||
const handleCancelTimeout = useCallback(() => {
|
||||
setIsUpgrading(false)
|
||||
isUpgradingRef.current = false
|
||||
setIsUpgradeTimeoutModalOpen(false)
|
||||
|
||||
if (startPollingRef.current && actualStampRef.current) {
|
||||
startPollingRef.current(actualStampRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleFileUploaded = useCallback(
|
||||
(e: Event) => {
|
||||
const { fileInfo } = (e as CustomEvent).detail || {}
|
||||
|
||||
if (!fileInfo || fileInfo.driveId !== driveId || !startPollingRef.current) return
|
||||
|
||||
startPollingRef.current(actualStampRef.current)
|
||||
},
|
||||
[driveId],
|
||||
)
|
||||
|
||||
useDriveEventListeners(driveId, handleUpgradeStart, handleUpgradeEnd, handleUpgradeTimeout, handleFileUploaded)
|
||||
|
||||
const { capacityPct, usedSize, stampSize } = useMemo(() => {
|
||||
const filesPerDrive = files.filter(fi => fi.driveId === drive.id.toString())
|
||||
|
||||
return calculateStampCapacityMetrics(actualStamp, filesPerDrive, drive.redundancyLevel, isCapacityUpdating)
|
||||
}, [actualStamp, drive, files, isCapacityUpdating])
|
||||
|
||||
const handleDriveClick = useCallback(() => {
|
||||
setView(ViewType.File)
|
||||
setActualItemView?.(drive.name)
|
||||
}, [setView, setActualItemView, drive.name])
|
||||
|
||||
const handleDestroyClick = useCallback(() => {
|
||||
setShowContext(false)
|
||||
setIsDestroyDriveModalOpen(true)
|
||||
}, [setShowContext, setIsDestroyDriveModalOpen])
|
||||
|
||||
const selectedClass = isSelected ? ' fm-drive-item-container-selected' : ''
|
||||
const containerClassName = `fm-drive-item-container${selectedClass}`
|
||||
|
||||
const updatingClass = isUpgrading || isCapacityUpdating ? ' fm-drive-item-capacity-updating' : ''
|
||||
const capacityClassName = `fm-drive-item-capacity${updatingClass}`
|
||||
|
||||
const driveIcon = isHovered ? <DriveFill size="16px" /> : <Drive size="16px" />
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fm-drive-item-container${isSelected ? ' fm-drive-item-container-selected' : ''}`}
|
||||
onClick={() => {
|
||||
setView(ViewType.File)
|
||||
setActualItemView?.(drive.name)
|
||||
}}
|
||||
>
|
||||
<div className={containerClassName} onClick={handleDriveClick}>
|
||||
<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 className="fm-drive-item-icon">{driveIcon}</div>
|
||||
<div>{truncateNameMiddle(drive.name, 35, 8, 8)}</div>
|
||||
</div>
|
||||
<div className="fm-drive-item-content">
|
||||
<div className="fm-drive-item-capacity">
|
||||
Capacity <ProgressBar value={capacityPct} width="64px" /> {usedSize} / {totalSize}
|
||||
<div className={capacityClassName}>
|
||||
<span>
|
||||
Capacity <ProgressBar value={capacityPct} width="64px" /> {usedSize} / {stampSize}
|
||||
</span>
|
||||
<Tooltip
|
||||
label={
|
||||
isUpgrading || isCapacityUpdating ? TOOLTIPS.DRIVE_CAPACITY_UPDATING : TOOLTIPS.DRIVE_CAPACITY_INFO
|
||||
}
|
||||
iconSize="12px"
|
||||
disableMargin={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="fm-drive-item-capacity">
|
||||
Expiry date: {actualStamp.duration.toEndDate().toLocaleDateString()}
|
||||
<div className={capacityClassName}>
|
||||
<span>Expiry date: {actualStamp.duration.toEndDate().toLocaleDateString()}</span>
|
||||
</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'}
|
||||
className={`fm-pointer${isUpgrading || isDestroying ? ' fm-disabled' : ''}`}
|
||||
onClick={!isUpgrading && !isDestroying ? handleMenuClick : undefined}
|
||||
aria-disabled={isUpgrading || isDestroying ? 'true' : 'false'}
|
||||
/>
|
||||
{showContext &&
|
||||
createPortal(
|
||||
@@ -153,13 +413,7 @@ export function DriveItem({ drive, stamp, isSelected, setErrorMessage }: DriveIt
|
||||
}}
|
||||
>
|
||||
<ContextMenu>
|
||||
<div
|
||||
className="fm-context-item red"
|
||||
onClick={() => {
|
||||
handleDestroyDriveClick()
|
||||
setIsDestroyDriveModalOpen(true)
|
||||
}}
|
||||
>
|
||||
<div className="fm-context-item red" onClick={handleDestroyClick}>
|
||||
Destroy entire drive
|
||||
</div>
|
||||
</ContextMenu>
|
||||
@@ -171,48 +425,38 @@ export function DriveItem({ drive, stamp, isSelected, setErrorMessage }: DriveIt
|
||||
label="Upgrade"
|
||||
variant="primary"
|
||||
size="small"
|
||||
disabled={isUpgrading}
|
||||
disabled={isUpgrading || isDestroying}
|
||||
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)
|
||||
},
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<DriveModals
|
||||
isUpgradeDriveModalOpen={isUpgradeDriveModalOpen}
|
||||
setIsUpgradeDriveModalOpen={setIsUpgradeDriveModalOpen}
|
||||
isUpgradeTimeoutModalOpen={isUpgradeTimeoutModalOpen}
|
||||
actualStamp={actualStamp}
|
||||
drive={drive}
|
||||
setErrorMessage={setErrorMessage}
|
||||
isUpgrading={isUpgrading}
|
||||
isCapacityUpdating={isCapacityUpdating}
|
||||
isDestroying={isDestroying}
|
||||
setIsProgressModalOpen={setIsProgressModalOpen}
|
||||
isProgressModalOpen={isProgressModalOpen}
|
||||
isDestroyDriveModalOpen={isDestroyDriveModalOpen}
|
||||
setIsDestroyDriveModalOpen={setIsDestroyDriveModalOpen}
|
||||
doDestroy={doDestroy}
|
||||
onCancelTimeout={handleCancelTimeout}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function arePropsEqual(prevProps: DriveItemProps, nextProps: DriveItemProps) {
|
||||
const driveIdEqual = prevProps.drive.id.toString() === nextProps.drive.id.toString()
|
||||
const stampIdEqual = prevProps.stamp.batchID.toString() === nextProps.stamp.batchID.toString()
|
||||
const isSelectedEqual = prevProps.isSelected === nextProps.isSelected
|
||||
|
||||
return driveIdEqual && stampIdEqual && isSelectedEqual
|
||||
}
|
||||
|
||||
export const MemoizedDriveItem = memo(DriveItemComponent, arePropsEqual)
|
||||
export const DriveItem = MemoizedDriveItem
|
||||
|
||||
@@ -7,9 +7,10 @@ import { ContextMenu } from '../../ContextMenu/ContextMenu'
|
||||
import { useContextMenu } from '../../../hooks/useContextMenu'
|
||||
import { DriveInfo } from '@solarpunkltd/file-manager-lib'
|
||||
import { Context as FMContext } from '../../../../../providers/FileManager'
|
||||
import { handleForgetDrive } from '../../../utils/bee'
|
||||
import { handleDestroyAndForgetDrive } from '../../../utils/bee'
|
||||
import { ConfirmModal } from '../../ConfirmModal/ConfirmModal'
|
||||
import './DriveItem.scss'
|
||||
import { truncateNameMiddle } from '../../../utils/common'
|
||||
|
||||
interface Props {
|
||||
drive: DriveInfo
|
||||
@@ -18,7 +19,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export function ExpiredDriveItem({ drive, onForgot, setErrorMessage }: Props): ReactElement {
|
||||
const { fm, setShowError } = useContext(FMContext)
|
||||
const { fm, adminDrive, setShowError } = useContext(FMContext)
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [showForgetConfirm, setShowForgetConfirm] = useState(false)
|
||||
const { showContext, pos, contextRef, setPos, setShowContext } = useContextMenu<HTMLDivElement>()
|
||||
@@ -37,7 +38,7 @@ export function ExpiredDriveItem({ drive, onForgot, setErrorMessage }: Props): R
|
||||
<div className="fm-drive-item-info">
|
||||
<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>{truncateNameMiddle(drive.name, 35, 8, 8)}</div>
|
||||
</div>
|
||||
<div className="fm-drive-item-content">
|
||||
<div className="fm-drive-item-capacity">Stamp expired — files unavailable</div>
|
||||
@@ -89,21 +90,21 @@ export function ExpiredDriveItem({ drive, onForgot, setErrorMessage }: Props): R
|
||||
cancelLabel="Keep"
|
||||
onCancel={() => setShowForgetConfirm(false)}
|
||||
onConfirm={async () => {
|
||||
if (!fm) return
|
||||
|
||||
await handleForgetDrive(
|
||||
await handleDestroyAndForgetDrive({
|
||||
fm,
|
||||
drive,
|
||||
async () => {
|
||||
isDestroy: false,
|
||||
adminDrive,
|
||||
onSuccess: async () => {
|
||||
setShowForgetConfirm(false)
|
||||
await onForgot?.()
|
||||
},
|
||||
() => {
|
||||
onError: () => {
|
||||
setShowForgetConfirm(false)
|
||||
setErrorMessage?.(`Failed to forget drive ${drive.name}`)
|
||||
setShowError(true)
|
||||
},
|
||||
)
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -40,6 +40,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.fm-sidebar-item-description {
|
||||
padding: 8px 12px;
|
||||
text-align: center;
|
||||
color: rgb(75, 85, 99);
|
||||
font-size: 14px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.fm-sidebar-drive-creation {
|
||||
padding: 12px;
|
||||
border-top: 1px solid rgb(146, 146, 146);
|
||||
|
||||
@@ -20,6 +20,8 @@ import { useView } from '../../../../pages/filemanager/ViewContext'
|
||||
import { Context as FMContext } from '../../../../providers/FileManager'
|
||||
import { getUsableStamps } from '../../utils/bee'
|
||||
import { DriveInfo } from '@solarpunkltd/file-manager-lib'
|
||||
import { truncateNameMiddle } from '../../utils/common'
|
||||
import { FILE_MANAGER_EVENTS } from '../../constants/common'
|
||||
|
||||
interface SidebarProps {
|
||||
loading: boolean
|
||||
@@ -34,6 +36,7 @@ export function Sidebar({ setErrorMessage, loading }: SidebarProps): ReactElemen
|
||||
const [isCreateDriveOpen, setIsCreateDriveOpen] = useState(false)
|
||||
const [usableStamps, setUsableStamps] = useState<PostageBatch[]>([])
|
||||
const [isDriveCreationInProgress, setIsDriveCreationInProgress] = useState(false)
|
||||
const [creatingDriveName, setCreatingDriveName] = useState<string | null>(null)
|
||||
const [isExpiredOpen, setIsExpiredOpen] = useState(false)
|
||||
|
||||
const { beeApi } = useContext(SettingsContext)
|
||||
@@ -65,8 +68,17 @@ export function Sidebar({ setErrorMessage, loading }: SidebarProps): ReactElemen
|
||||
getStamps()
|
||||
}
|
||||
|
||||
const handleUpgradeEnd = async () => {
|
||||
if (isMounted && beeApi) {
|
||||
await getStamps()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener(FILE_MANAGER_EVENTS.DRIVE_UPGRADE_END, handleUpgradeEnd as EventListener)
|
||||
|
||||
return () => {
|
||||
isMounted = false
|
||||
window.removeEventListener(FILE_MANAGER_EVENTS.DRIVE_UPGRADE_END, handleUpgradeEnd as EventListener)
|
||||
}
|
||||
}, [beeApi, drives])
|
||||
|
||||
@@ -81,14 +93,22 @@ export function Sidebar({ setErrorMessage, loading }: SidebarProps): ReactElemen
|
||||
setView(ViewType.File)
|
||||
}
|
||||
|
||||
if (currentDrive && !currentStamp && usableStamps.length > 0) {
|
||||
if (currentDrive && usableStamps.length > 0) {
|
||||
const correspondingStamp = usableStamps.find(s => s.batchID.toString() === currentDrive.batchId.toString())
|
||||
|
||||
if (correspondingStamp) {
|
||||
setCurrentStamp(correspondingStamp)
|
||||
}
|
||||
}
|
||||
}, [fm, drives, currentDrive, currentStamp, usableStamps, setCurrentDrive, setCurrentStamp, setView])
|
||||
}, [fm, drives, currentDrive, usableStamps, setCurrentDrive, setCurrentStamp, setView, beeApi])
|
||||
|
||||
const handleCreateNewDrive = () => {
|
||||
if (isDriveCreationInProgress) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsCreateDriveOpen(true)
|
||||
}
|
||||
|
||||
const isCurrent = (di: DriveInfo) => currentDrive?.id.toString() === di.id.toString()
|
||||
|
||||
@@ -96,12 +116,22 @@ export function Sidebar({ setErrorMessage, loading }: SidebarProps): ReactElemen
|
||||
<div className="fm-sidebar">
|
||||
<div className="fm-sidebar-content">
|
||||
{!loading && (
|
||||
<div className="fm-sidebar-item" onClick={() => setIsCreateDriveOpen(true)}>
|
||||
<div className="fm-sidebar-item-icon">
|
||||
<Add size="16px" />
|
||||
<>
|
||||
<div
|
||||
className={`fm-sidebar-item ${isDriveCreationInProgress ? 'disabled' : ''}`}
|
||||
onClick={() => handleCreateNewDrive()}
|
||||
>
|
||||
<div className="fm-sidebar-item-icon">
|
||||
<Add size="16px" />
|
||||
</div>
|
||||
<div>Create new drive</div>
|
||||
</div>
|
||||
<div>Create new drive</div>
|
||||
</div>
|
||||
{isDriveCreationInProgress && (
|
||||
<div className="fm-sidebar-item-description">
|
||||
{truncateNameMiddle(creatingDriveName || 'Your Drive', 35, 8, 8)} is currently being created.
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{isCreateDriveOpen && (
|
||||
@@ -110,12 +140,19 @@ export function Sidebar({ setErrorMessage, loading }: SidebarProps): ReactElemen
|
||||
onDriveCreated={() => {
|
||||
setIsCreateDriveOpen(false)
|
||||
setIsDriveCreationInProgress(false)
|
||||
setCreatingDriveName(null)
|
||||
}}
|
||||
onCreationStarted={(driveName: string) => {
|
||||
setIsDriveCreationInProgress(true)
|
||||
setCreatingDriveName(driveName)
|
||||
}}
|
||||
onCreationStarted={() => setIsDriveCreationInProgress(true)}
|
||||
onCreationError={(name: string) => {
|
||||
setIsDriveCreationInProgress(false)
|
||||
setErrorMessage?.(`Error creating drive: ${name}`)
|
||||
setErrorMessage?.(
|
||||
`Error creating drive ${name}. Please try again. Possible causes include insufficient xDAI balance or a lost connection to the RPC.`,
|
||||
)
|
||||
setShowError(true)
|
||||
setCreatingDriveName(null)
|
||||
|
||||
return
|
||||
}}
|
||||
@@ -253,7 +290,7 @@ export function Sidebar({ setErrorMessage, loading }: SidebarProps): ReactElemen
|
||||
}}
|
||||
title={`${d.name} Trash`}
|
||||
>
|
||||
{d.name} Trash
|
||||
{truncateNameMiddle(d.name, 35, 8, 8)} Trash
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user