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