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.
287 lines
7.8 KiB
JavaScript
287 lines
7.8 KiB
JavaScript
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 (
|
|
<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[status]}</Text>
|
|
{showProgress && (
|
|
<Flex vertical gap={2} style={{ flex: 1, minWidth: 0 }}>
|
|
<HProgress
|
|
percent={resolvedPercent}
|
|
status={getProgressStatus(status)}
|
|
showInfo={typeof resolvedPercent === 'number'}
|
|
style={{ flex: 1, margin: 0 }}
|
|
/>
|
|
{detail && (
|
|
<Text type='secondary' ellipsis style={{ minWidth: 0 }}>
|
|
{detail}
|
|
</Text>
|
|
)}
|
|
</Flex>
|
|
)}
|
|
</Flex>
|
|
</Flex>
|
|
)
|
|
}
|
|
|
|
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 (
|
|
<Flex vertical gap='middle'>
|
|
<Text>
|
|
Updating Farm Control to version{' '}
|
|
{update?.version ? `${update.version}` : 'unknown'} build{' '}
|
|
{update?.buildNumber ? `${update.buildNumber}` : 'unknown'} from branch{' '}
|
|
{update?.branch ? `${update.branch}` : 'unknown'}...
|
|
</Text>
|
|
|
|
<Divider style={{ margin: '4px 0' }} />
|
|
<Flex vertical gap='middle'>
|
|
<UpdateStage
|
|
stage='download'
|
|
status={downloadStatus}
|
|
percent={downloadPercent}
|
|
detail={downloadDetail}
|
|
/>
|
|
<UpdateStage
|
|
stage='install'
|
|
status={installStatus}
|
|
percent={installPercent}
|
|
detail={installDetail}
|
|
/>
|
|
<UpdateStage stage='restart' status={restartStatus} />
|
|
</Flex>
|
|
|
|
<Modal
|
|
title='Update Failed'
|
|
open={isError && errorModalOpen == true}
|
|
centered
|
|
closable={false}
|
|
maskClosable={false}
|
|
onCancel={() => {
|
|
setErrorModalOpen(false)
|
|
onClose()
|
|
}}
|
|
footer={[
|
|
<Button
|
|
key='close'
|
|
onClick={() => {
|
|
setErrorModalOpen(false)
|
|
onClose()
|
|
}}
|
|
>
|
|
Close
|
|
</Button>
|
|
]}
|
|
>
|
|
<Text>{message}</Text>
|
|
</Modal>
|
|
</Flex>
|
|
)
|
|
}
|
|
|
|
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
|