From 3caa62d67502d9b938a3e11bd4071021a87e8d41 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sun, 9 Aug 2026 14:36:19 +0100 Subject: [PATCH] Refactor AppUpdateProgress for improved stage tracking and status management - 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. --- .../AppUpdates/AppUpdateProgress.jsx | 106 ++++++++++++------ .../Dashboard/context/AppUpdateContext.jsx | 2 +- 2 files changed, 72 insertions(+), 36 deletions(-) diff --git a/src/components/Dashboard/Management/AppUpdates/AppUpdateProgress.jsx b/src/components/Dashboard/Management/AppUpdates/AppUpdateProgress.jsx index 508c545..65b5c3d 100644 --- a/src/components/Dashboard/Management/AppUpdates/AppUpdateProgress.jsx +++ b/src/components/Dashboard/Management/AppUpdates/AppUpdateProgress.jsx @@ -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 }) => { - {config.labels[resolvedStatus]} + {config.labels[status]} {showProgress && ( - + - {detail && {detail}} + {detail && ( + + {detail} + + )} )} @@ -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 ( diff --git a/src/components/Dashboard/context/AppUpdateContext.jsx b/src/components/Dashboard/context/AppUpdateContext.jsx index 6a0c18d..bf949d6 100644 --- a/src/components/Dashboard/context/AppUpdateContext.jsx +++ b/src/components/Dashboard/context/AppUpdateContext.jsx @@ -403,7 +403,7 @@ export const AppUpdateProvider = ({ children }) => { setNoUpdateOpen(false) setUpdatePromptOpen(false) setInstallingUpdate(update) - setModelWidth(550) + setModelWidth(580) setUpdateProgress({ phase: 'preparing', percent: 0,