From 8a0bc22124484836bf09e4e1d67d048971b41e7d Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sun, 21 Jun 2026 19:08:18 +0100 Subject: [PATCH] Enhance AppUpdateProgress component with detailed download and installation stages; introduce modal for error handling and improve progress status management. --- .../AppUpdates/AppUpdateProgress.jsx | 207 ++++++++++++++---- .../Dashboard/context/AppUpdateContext.jsx | 1 + 2 files changed, 168 insertions(+), 40 deletions(-) diff --git a/src/components/Dashboard/Management/AppUpdates/AppUpdateProgress.jsx b/src/components/Dashboard/Management/AppUpdates/AppUpdateProgress.jsx index c098f7d..06571b2 100644 --- a/src/components/Dashboard/Management/AppUpdates/AppUpdateProgress.jsx +++ b/src/components/Dashboard/Management/AppUpdates/AppUpdateProgress.jsx @@ -1,6 +1,12 @@ import PropTypes from 'prop-types' -import { Alert, Flex, Progress, Typography } from 'antd' -import { LoadingOutlined } from '@ant-design/icons' +import { useState } from 'react' +import { Button, Flex, Modal, Progress, Typography, theme } from 'antd' + +import CloudIcon from '../../../Icons/CloudIcon' +import HostIcon from '../../../Icons/HostIcon' + +import CheckCircleIcon from '../../../Icons/CheckCircleIcon' +import XMarkCircleIcon from '../../../Icons/XMarkCircleIcon' const { Text } = Typography @@ -21,27 +27,137 @@ const formatBytes = (bytes) => { }` } -const getProgressStatus = (phase) => { - if (phase === 'error') return 'exception' - if (phase === 'downloaded' || phase === 'installing') return 'success' +const STAGE_CONFIG = { + download: { + icon: CloudIcon, + labels: { + pending: 'Download', + active: 'Downloading', + complete: 'Downloaded', + error: 'Download failed' + } + }, + install: { + icon: HostIcon, + labels: { + pending: 'Install', + active: 'Installing', + complete: 'Installed', + error: 'Install failed' + } + } +} + +const getStageColor = (status, token) => { + if (status === 'complete') return token.colorSuccess + if (status === 'active') return token.colorPrimary + if (status === 'error') return token.colorError + return token.colorTextQuaternary +} + +const getDownloadStageStatus = (phase, isError) => { + if (isError && ['preparing', 'downloading'].includes(phase)) return 'error' + if (['downloaded', 'installing'].includes(phase)) return 'complete' + if (['preparing', 'downloading'].includes(phase)) return 'active' + return 'pending' +} + +const getInstallStageStatus = (phase, isError, message) => { + if (isError && ['downloaded', 'installing'].includes(phase)) return 'error' + if ( + phase === 'installing' && + String(message || '') + .toLowerCase() + .includes('complete') + ) { + return 'complete' + } + if (phase === 'installing') return 'active' + return 'pending' +} + +const getProgressStatus = (stageStatus) => { + if (stageStatus === 'error') return 'exception' + if (stageStatus === 'complete') return 'success' return 'active' } -const AppUpdateProgress = ({ progress, update }) => { +const UpdateStage = ({ stage, status, percent, detail }) => { + const { token } = theme.useToken() + const config = STAGE_CONFIG[stage] + const StageIcon = config.icon + const color = getStageColor(status, token) + const showProgress = status === 'active' + const resolvedPercent = + typeof percent === 'number' ? Math.min(percent, 100) : undefined + + const StatusIcon = + status === 'complete' + ? CheckCircleIcon + : status === 'error' + ? XMarkCircleIcon + : StageIcon + + return ( + + + + + + {config.labels[status]} + + {showProgress && ( + + )} + + + {detail && ( + + {detail} + + )} + + ) +} + +UpdateStage.propTypes = { + stage: PropTypes.oneOf(['download', 'install']).isRequired, + status: PropTypes.oneOf(['pending', 'active', 'complete', 'error']) + .isRequired, + percent: PropTypes.number, + detail: PropTypes.string +} + +const AppUpdateProgress = ({ progress, update, onClose }) => { const phase = progress?.phase || 'preparing' const percent = - typeof progress?.percent === 'number' ? Math.min(progress.percent, 100) : 0 + typeof progress?.percent === 'number' + ? Math.min(progress.percent, 100) + : null const downloaded = formatBytes(progress?.downloadedBytes) const total = formatBytes(progress?.totalBytes) - const artifactName = - progress?.artifact?.fileName || - progress?.artifact?.relativePath || - 'Selected installer' const message = progress?.message || 'Preparing update' - const isInstalling = phase === 'installing' const isError = phase === 'error' - const showProgress = - !isError && (!isInstalling || typeof progress?.percent === 'number') + + const [errorModalOpen, setErrorModalOpen] = useState(true) + + const downloadStatus = getDownloadStageStatus(phase, isError) + const installStatus = getInstallStageStatus(phase, isError, message) + + const downloadPercent = + downloadStatus === 'active' ? (phase === 'preparing' ? 0 : percent) : null + + const installPercent = installStatus === 'active' ? percent : null + + const downloadDetail = + downloadStatus === 'active' && downloaded && total + ? `${downloaded} of ${total}` + : null return ( @@ -52,34 +168,44 @@ const AppUpdateProgress = ({ progress, update }) => { {update?.branch ? `${update.branch}` : 'unknown'}... - {isError ? ( - - ) : ( - } - message={ - isInstalling - ? message - : `Downloading ${artifactName}` - } + + - )} - - {showProgress && ( - - )} + - {downloaded && total && ( - - {downloaded} of {total} downloaded - - )} + { + setErrorModalOpen(false) + onClose() + }} + footer={[ + + ]} + > + {message} + ) } @@ -93,7 +219,8 @@ AppUpdateProgress.propTypes = { message: PropTypes.string, artifact: PropTypes.object }), - update: PropTypes.object + update: PropTypes.object, + onClose: PropTypes.func } export default AppUpdateProgress diff --git a/src/components/Dashboard/context/AppUpdateContext.jsx b/src/components/Dashboard/context/AppUpdateContext.jsx index bd84155..6de63ef 100644 --- a/src/components/Dashboard/context/AppUpdateContext.jsx +++ b/src/components/Dashboard/context/AppUpdateContext.jsx @@ -269,6 +269,7 @@ export const AppUpdateProvider = ({ children }) => { ) : (