Enhance FilePreview Component with Download Progress Indication
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

- Integrated download progress tracking in the FilePreview component to provide users with real-time feedback during file loading.
- Updated fetchFileContent function in ApiServerContext to support progress callbacks, enhancing user experience during file downloads.
- Refactored rendering logic in FilePreview to display a progress overlay while files are being fetched, improving visual feedback and interaction.
This commit is contained in:
Tom Butcher 2026-09-13 23:21:15 +01:00
parent 49e23299da
commit 5f776f896f
2 changed files with 147 additions and 49 deletions

View File

@ -1,4 +1,5 @@
import PropTypes from 'prop-types'
import { Card, Flex } from 'antd'
import { ApiServerContext } from '../context/ApiServerContext'
import {
useCallback,
@ -8,10 +9,10 @@ import {
memo,
useRef
} from 'react'
import LoadingPlaceholder from './LoadingPlaceholder'
import GCodePreview from './GCodePreview'
import ThreeDPreview from './ThreeDPreview'
import PDFPreview from './PDFPreview'
import ProgressDisplay from './ProgressDisplay'
import { AuthContext } from '../context/AuthContext'
const hasExplicitPreviewHeight = (height) =>
@ -25,6 +26,10 @@ const FilePreview = ({ file, style = {} }) => {
const [fileObjectUrl, setFileObjectUrl] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [downloadProgress, setDownloadProgress] = useState({
progress: 0,
message: 'Loading file preview...'
})
const currentId = useRef(null)
@ -39,78 +44,147 @@ const FilePreview = ({ file, style = {} }) => {
if (error != null) {
return
}
const requestId = file._id
setLoading(true)
const objectUrl = await fetchFileContent(file, false)
setDownloadProgress({ progress: 0, message: 'Starting download...' })
const objectUrl = await fetchFileContent(file, false, (progress) => {
if (currentId.current !== requestId) {
return
}
setDownloadProgress(progress)
})
if (currentId.current !== requestId) {
return
}
if (objectUrl == null) {
setLoading(false)
setDownloadProgress(null)
console.error('Failed to fetch file content', file)
setError('Failed to fetch file content')
return
}
setFileObjectUrl(objectUrl)
setDownloadProgress(null)
setLoading(false)
}, [file, fetchFileContent, error])
useEffect(() => {
if (file._id !== currentId.current) {
setFileObjectUrl(null)
setError(null)
setLoading(true)
setDownloadProgress({
progress: 0,
message: 'Loading file preview...'
})
}
if (file?.type && token != null && file._id !== currentId.current) {
currentId.current = file._id
fetchPreview()
}
}, [file._id, file?.type, fetchPreview, token])
if (isPdf) {
if (error != null) {
return <div style={{ color: 'red' }}>{error}</div>
const showProgressOverlay = downloadProgress != null
const wrapperHeight = hasExplicitPreviewHeight(style.height)
? style.height
: isPdf
? '72vh'
: style.height
const renderPreview = () => {
if (isPdf) {
return (
<PDFPreview
file={fileObjectUrl}
loading={loading}
style={{
...style,
height: '100%'
}}
/>
)
}
return (
<PDFPreview
file={fileObjectUrl}
loading={loading}
style={{
...style,
height: hasExplicitPreviewHeight(style.height)
? style.height
: '72vh'
}}
/>
)
}
if (loading == true || !file?.type) {
return null
}
if (loading == true || !file?.type) {
return <LoadingPlaceholder message={'Loading file preview...'} />
if (isGcode && fileObjectUrl) {
return (
<GCodePreview
src={fileObjectUrl}
startLayer={0}
endLayer={undefined}
lineWidth={1}
style={style}
/>
)
}
if (is3DModel && fileObjectUrl) {
return (
<ThreeDPreview
src={fileObjectUrl}
style={style}
extension={file.extension}
/>
)
}
if (isImage && fileObjectUrl) {
return <img src={fileObjectUrl} style={style}></img>
}
return null
}
if (error != null) {
return <div style={{ color: 'red' }}>{error}</div>
}
if (isGcode && fileObjectUrl) {
return (
<GCodePreview
src={fileObjectUrl}
startLayer={0}
endLayer={undefined}
lineWidth={1}
style={style}
/>
)
}
if (is3DModel && fileObjectUrl) {
return (
<ThreeDPreview
src={fileObjectUrl}
style={style}
extension={file.extension}
/>
)
}
if (isImage && fileObjectUrl) {
return <img src={fileObjectUrl} style={style}></img>
}
return null
return (
<div
style={{
...style,
position: 'relative',
height: wrapperHeight,
minHeight:
showProgressOverlay && !wrapperHeight ? 240 : style.minHeight
}}
>
<div
className={`previewProgressOverlay ${showProgressOverlay ? 'visible' : 'hidden'}`}
>
{showProgressOverlay ? (
<Flex
style={{ width: '100%', height: '100%' }}
align='center'
justify='center'
>
<Card
style={{
maxWidth: 360,
minWidth: 100,
width: '100%',
margin: 24
}}
styles={{ body: { padding: 18 } }}
>
<ProgressDisplay
percent={Math.round(
(Number(downloadProgress.progress) || 0) * 100
)}
status='active'
>
{downloadProgress.message || 'Downloading...'}
</ProgressDisplay>
</Card>
</Flex>
) : null}
</div>
{renderPreview()}
</div>
)
}
FilePreview.propTypes = {
@ -122,6 +196,8 @@ FilePreview.propTypes = {
const areEqual = (prevProps, nextProps) => {
return (
prevProps.file?._id === nextProps.file?._id &&
prevProps.file?.type === nextProps.file?.type &&
prevProps.file?.size === nextProps.file?.size &&
JSON.stringify(prevProps.style) === JSON.stringify(nextProps.style)
)
}

View File

@ -1829,8 +1829,11 @@ const ApiServerProvider = ({ children }) => {
}
// Download GCode file content
const fetchFileContent = async (file, download = false) => {
const fetchFileContent = async (file, download = false, onProgress = null) => {
try {
if (typeof onProgress === 'function') {
onProgress({ progress: 0, message: 'Starting download...' })
}
const response = await axios.get(
`${config.backendUrl}/files/${file._id}/content`,
{
@ -1838,13 +1841,32 @@ const ApiServerProvider = ({ children }) => {
Accept: '*/*',
Authorization: `Bearer ${token}`
},
responseType: 'blob'
responseType: 'blob',
onDownloadProgress: (progressEvent) => {
if (typeof onProgress !== 'function') {
return
}
const total =
progressEvent.total > 0
? progressEvent.total
: typeof file.size === 'number' && file.size > 0
? file.size
: 0
const progress = total > 0 ? progressEvent.loaded / total : 0
onProgress({
progress: Math.min(progress, 1),
message: 'Downloading...'
})
}
}
)
const blob = new Blob([response.data], {
type: response.headers['content-type']
})
const fileURL = window.URL.createObjectURL(blob)
if (typeof onProgress === 'function') {
onProgress({ progress: 1, message: 'Download complete' })
}
if (download == true) {
const fileLink = document.createElement('a')
fileLink.href = fileURL
@ -1858,7 +1880,7 @@ const ApiServerProvider = ({ children }) => {
} catch (err) {
console.error(err)
showError(err, () => {
fetchFileContent(file, download)
fetchFileContent(file, download, onProgress)
})
}
}