import PropTypes from 'prop-types' import { useEffect, useState } from 'react' import { Button, Flex, Modal, Typography, theme, Divider } from 'antd' import CloudIcon from '../../../Icons/CloudIcon' import HostIcon from '../../../Icons/HostIcon' import ReloadIcon from '../../../Icons/ReloadIcon' import HProgress from '../../common/HProgress' import CheckCircleIcon from '../../../Icons/CheckCircleIcon' import XMarkCircleIcon from '../../../Icons/XMarkCircleIcon' const { Text } = Typography const formatBytes = (bytes) => { if (!Number.isFinite(bytes) || bytes <= 0) return null const units = ['B', 'KB', 'MB', 'GB'] let value = bytes let unitIndex = 0 while (value >= 1024 && unitIndex < units.length - 1) { value /= 1024 unitIndex += 1 } return `${value.toFixed(value >= 10 || unitIndex === 0 ? 0 : 1)} ${ units[unitIndex] }` } 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' } }, restart: { icon: ReloadIcon, labels: { pending: 'Restart', active: 'Restarting...', complete: 'Restarted', error: 'Restart 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 STAGE_INDEX = { download: 0, install: 1, restart: 2 } const isCompletionMessage = (message) => { const normalized = String(message || '').toLowerCase() return ( normalized.includes('complete') || normalized.includes('successful') || normalized.includes('restarting') ) } // Maps a progress event to the stage that should currently be active. // Returns null when the event carries no stage information (e.g. errors), // so the previously reached stage is kept. const getStageIndexFromProgress = (phase, percent, message) => { if (phase === 'restarting') return STAGE_INDEX.restart if (phase === 'installing') { return percent >= 100 || isCompletionMessage(message) ? STAGE_INDEX.restart : STAGE_INDEX.install } if (phase === 'downloaded') return STAGE_INDEX.install if (phase === 'preparing' || phase === 'downloading') { return phase === 'downloading' && percent >= 100 ? STAGE_INDEX.install : STAGE_INDEX.download } return null } const getStageStatus = (stageIndex, activeIndex, isError) => { if (stageIndex < activeIndex) return 'complete' if (stageIndex > activeIndex) return 'pending' return isError ? 'error' : 'active' } const getProgressStatus = (stageStatus) => { if (stageStatus === 'error') return 'exception' if (stageStatus === 'complete') return 'success' return 'active' } const UpdateStage = ({ stage, status, percent, detail }) => { const { token } = theme.useToken() const config = STAGE_CONFIG[stage] const StageIcon = config.icon const resolvedPercent = typeof percent === 'number' ? Math.min(percent, 100) : undefined const color = getStageColor(status, token) const showProgress = status === 'active' && stage !== 'restart' const StatusIcon = status === 'complete' ? CheckCircleIcon : status === 'error' ? XMarkCircleIcon : StageIcon return ( {config.labels[status]} {showProgress && ( {detail && ( {detail} )} )} ) } UpdateStage.propTypes = { stage: PropTypes.oneOf(['download', 'install', 'restart']).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) : null const downloaded = formatBytes(progress?.downloadedBytes) const total = formatBytes(progress?.totalBytes) const message = progress?.message || 'Preparing update' const isError = phase === 'error' const [errorModalOpen, setErrorModalOpen] = useState(true) // Track the furthest stage reached so out-of-order or skipped progress // events can never move the steps backwards. const [activeStageIndex, setActiveStageIndex] = useState(STAGE_INDEX.download) useEffect(() => { const stageIndex = getStageIndexFromProgress(phase, percent, message) if (stageIndex !== null) { setActiveStageIndex((previous) => Math.max(previous, stageIndex)) } }, [phase, percent, message]) const downloadStatus = getStageStatus( STAGE_INDEX.download, activeStageIndex, isError ) const installStatus = getStageStatus( STAGE_INDEX.install, activeStageIndex, isError ) const restartStatus = getStageStatus( STAGE_INDEX.restart, activeStageIndex, isError ) const downloadPercent = downloadStatus === 'active' && phase === 'downloading' ? percent : 0 const installPercent = installStatus === 'active' && phase === 'installing' ? percent : null const downloadDetail = downloadStatus === 'active' && downloaded && total ? `${downloaded} of ${total}` : null const installDetail = installStatus === 'active' && phase === 'installing' ? message : null return ( Updating Farm Control to version{' '} {update?.version ? `${update.version}` : 'unknown'} build{' '} {update?.buildNumber ? `${update.buildNumber}` : 'unknown'} from branch{' '} {update?.branch ? `${update.branch}` : 'unknown'}... { setErrorModalOpen(false) onClose() }} footer={[ ]} > {message} ) } AppUpdateProgress.propTypes = { progress: PropTypes.shape({ phase: PropTypes.string, percent: PropTypes.number, downloadedBytes: PropTypes.number, totalBytes: PropTypes.number, message: PropTypes.string, artifact: PropTypes.object }), update: PropTypes.object, onClose: PropTypes.func } export default AppUpdateProgress