Refactor AppUpdateProgress for improved stage tracking and status management
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

- Introduced a new mechanism to track the active update stage using an index, allowing for better handling of out-of-order progress events.
- Simplified status determination functions for download, install, and restart stages, enhancing clarity and maintainability.
- Updated the rendering logic to ensure accurate display of progress and status messages, improving user experience during updates.
- Adjusted modal width in AppUpdateContext for better layout consistency.
This commit is contained in:
Tom Butcher 2026-08-09 14:36:19 +01:00
parent f9d4074d85
commit 3caa62d675
2 changed files with 72 additions and 36 deletions

View File

@ -1,5 +1,5 @@
import PropTypes from 'prop-types'
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { Button, Flex, Modal, Typography, theme, Divider } from 'antd'
import CloudIcon from '../../../Icons/CloudIcon'
@ -66,16 +66,9 @@ const getStageColor = (status, token) => {
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 isInstallComplete = (phase, message) => {
if (phase !== 'installing') return false
const STAGE_INDEX = { download: 0, install: 1, restart: 2 }
const isCompletionMessage = (message) => {
const normalized = String(message || '').toLowerCase()
return (
@ -85,17 +78,33 @@ const isInstallComplete = (phase, message) => {
)
}
const getInstallStageStatus = (phase, isError, message) => {
if (isError && ['downloaded', 'installing'].includes(phase)) return 'error'
if (isInstallComplete(phase, message)) return 'complete'
if (phase === 'installing') return 'active'
return 'pending'
// 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 getRestartStageStatus = (phase, isError, message) => {
if (isError && isInstallComplete(phase, message)) return 'error'
if (isInstallComplete(phase, message)) return 'active'
return 'pending'
const getStageStatus = (stageIndex, activeIndex, isError) => {
if (stageIndex < activeIndex) return 'complete'
if (stageIndex > activeIndex) return 'pending'
return isError ? 'error' : 'active'
}
const getProgressStatus = (stageStatus) => {
@ -110,15 +119,13 @@ const UpdateStage = ({ stage, status, percent, detail }) => {
const StageIcon = config.icon
const resolvedPercent =
typeof percent === 'number' ? Math.min(percent, 100) : undefined
const resolvedStatus =
status !== 'error' && resolvedPercent === 100 ? 'complete' : status
const color = getStageColor(resolvedStatus, token)
const showProgress = resolvedStatus === 'active' && stage !== 'restart'
const color = getStageColor(status, token)
const showProgress = status === 'active' && stage !== 'restart'
const StatusIcon =
resolvedStatus === 'complete'
status === 'complete'
? CheckCircleIcon
: resolvedStatus === 'error'
: status === 'error'
? XMarkCircleIcon
: StageIcon
@ -126,16 +133,20 @@ const UpdateStage = ({ stage, status, percent, detail }) => {
<Flex align='start' gap='middle' style={{ width: '100%' }}>
<StatusIcon style={{ fontSize: 22, color, flexShrink: 0 }} />
<Flex align='start' gap='24px' style={{ flex: 1, minWidth: 0 }}>
<Text style={{ flexShrink: 0 }}>{config.labels[resolvedStatus]}</Text>
<Text style={{ flexShrink: 0 }}>{config.labels[status]}</Text>
{showProgress && (
<Flex vertical gap={2} style={{ flex: 1 }}>
<Flex vertical gap={2} style={{ flex: 1, minWidth: 0 }}>
<HProgress
percent={resolvedPercent}
status={getProgressStatus(resolvedStatus)}
status={getProgressStatus(status)}
showInfo={typeof resolvedPercent === 'number'}
style={{ flex: 1, margin: 0 }}
/>
{detail && <Text type='secondary'>{detail}</Text>}
{detail && (
<Text type='secondary' ellipsis style={{ minWidth: 0 }}>
{detail}
</Text>
)}
</Flex>
)}
</Flex>
@ -164,21 +175,46 @@ const AppUpdateProgress = ({ progress, update, onClose }) => {
const [errorModalOpen, setErrorModalOpen] = useState(true)
const downloadStatus = getDownloadStageStatus(phase, isError)
const installStatus = getInstallStageStatus(phase, isError, message)
const restartStatus = getRestartStageStatus(phase, isError, message)
// 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 === 'preparing' ? 0 : percent) : null
downloadStatus === 'active' && phase === 'downloading' ? percent : 0
const installPercent = installStatus === 'active' ? percent : null
const installPercent =
installStatus === 'active' && phase === 'installing' ? percent : null
const downloadDetail =
downloadStatus === 'active' && downloaded && total
? `${downloaded} of ${total}`
: null
const installDetail = installStatus === 'active' ? message : null
const installDetail =
installStatus === 'active' && phase === 'installing' ? message : null
return (
<Flex vertical gap='middle'>

View File

@ -403,7 +403,7 @@ export const AppUpdateProvider = ({ children }) => {
setNoUpdateOpen(false)
setUpdatePromptOpen(false)
setInstallingUpdate(update)
setModelWidth(550)
setModelWidth(580)
setUpdateProgress({
phase: 'preparing',
percent: 0,