From f9d4074d8535e857f57a7e4ba2b82ab284fce040 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sun, 9 Aug 2026 14:26:06 +0100 Subject: [PATCH] Add HProgress component for enhanced progress display in Dashboard - Introduced a new `HProgress` component to replace the existing `Progress` component, providing a customizable and visually appealing progress indicator. - Updated `AppUpdateProgress` and `StateDisplay` components to utilize `HProgress`, improving consistency in progress representation across the application. - Added CSS animations and styles for a smoother user experience during progress updates. --- assets/stylesheets/App.css | 102 +++++++ .../AppUpdates/AppUpdateProgress.jsx | 5 +- src/components/Dashboard/common/HProgress.jsx | 284 ++++++++++++++++++ .../Dashboard/common/StateDisplay.jsx | 5 +- 4 files changed, 392 insertions(+), 4 deletions(-) create mode 100644 src/components/Dashboard/common/HProgress.jsx diff --git a/assets/stylesheets/App.css b/assets/stylesheets/App.css index f4ab48f..9c4466b 100644 --- a/assets/stylesheets/App.css +++ b/assets/stylesheets/App.css @@ -971,3 +971,105 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton { .electron-body .ant-modal-mask { top: 41px; } + +@keyframes h-progress-macos-wave { + 0% { + transform: translateX(-120%); + } + 100% { + transform: translateX(120%); + } +} + +.h-progress { + display: inline-block; + width: 100%; + font-size: 14px; + line-height: 1; +} + +.h-progress-outer { + display: inline-flex; + align-items: center; + width: 100%; +} + +.h-progress-inner { + position: relative; + display: inline-block; + width: 100%; + flex: 1; + overflow: hidden; + vertical-align: middle; +} + +.h-progress-bg, +.h-progress-success-bg { + position: relative; + transition: all 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86); +} + +.h-progress-success-bg { + position: absolute; + inset-block-start: 0; + inset-inline-start: 0; +} + +.h-progress-bg-active::after { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + pointer-events: none; + background: linear-gradient( + 90deg, + rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 0.08) 18%, + rgba(255, 255, 255, 0.42) 50%, + rgba(255, 255, 255, 0.08) 82%, + rgba(255, 255, 255, 0) 100% + ); + width: 55%; + animation: h-progress-macos-wave 1.6s ease-in-out infinite; +} + +.h-progress-text { + display: inline-block; + margin-inline-start: 8px; + line-height: 1; + width: 2em; + white-space: nowrap; + text-align: start; + vertical-align: middle; + word-break: normal; +} + +.h-progress-text-start { + width: max-content; + margin-inline-start: 0; + margin-inline-end: 8px; +} + +.h-progress-text-inner { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + margin-inline-start: 0; + padding: 0 4px; + color: #fff; +} + +.h-progress-layout-bottom { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.h-progress-layout-bottom .h-progress-text { + width: max-content; + margin-inline-start: 0; + margin-top: 4px; +} diff --git a/src/components/Dashboard/Management/AppUpdates/AppUpdateProgress.jsx b/src/components/Dashboard/Management/AppUpdates/AppUpdateProgress.jsx index 482c3f8..508c545 100644 --- a/src/components/Dashboard/Management/AppUpdates/AppUpdateProgress.jsx +++ b/src/components/Dashboard/Management/AppUpdates/AppUpdateProgress.jsx @@ -1,10 +1,11 @@ import PropTypes from 'prop-types' import { useState } from 'react' -import { Button, Flex, Modal, Progress, Typography, theme, Divider } from 'antd' +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' @@ -128,7 +129,7 @@ const UpdateStage = ({ stage, status, percent, detail }) => { {config.labels[resolvedStatus]} {showProgress && ( - { + if (!progress || progress < 0) return 0 + if (progress > 100) return 100 + return progress +} + +const getSuccessPercent = ({ success, successPercent }) => { + if (success && 'progress' in success) return success.progress + if (success && 'percent' in success) return success.percent + return successPercent +} + +const sortGradient = (gradients) => { + const entries = Object.keys(gradients) + .map((key) => ({ + key: Number.parseFloat(key.replace(/%/g, '')), + value: gradients[key] + })) + .filter(({ key }) => !Number.isNaN(key)) + .sort((a, b) => a.key - b.key) + + return entries.map(({ key, value }) => `${value} ${key}%`).join(', ') +} + +const resolveStrokeBackground = (strokeColor, direction = 'to right') => { + if (!strokeColor) return undefined + + if (typeof strokeColor === 'string') { + return strokeColor + } + + if (Array.isArray(strokeColor)) { + return strokeColor[0] + } + + const { from, to, direction: gradientDirection, ...rest } = strokeColor + + if (Object.keys(rest).length > 0) { + return `linear-gradient(${gradientDirection || direction}, ${sortGradient(rest)})` + } + + return `linear-gradient(${gradientDirection || direction}, ${from}, ${to})` +} + +const getBarHeight = ({ size, strokeWidth }) => { + if (typeof strokeWidth === 'number') return strokeWidth + + if (size === 'small') return 6 + if (typeof size === 'number') return size + if (Array.isArray(size)) return size[1] ?? 8 + if (typeof size === 'object' && size?.height != null) return size.height + + return 8 +} + +const getOuterWidth = ({ size, strokeWidth }) => { + if (typeof size === 'number') return size + if (Array.isArray(size)) return size[0] ?? -1 + if (typeof size === 'object' && size?.width != null) return size.width + if (strokeWidth) return -1 + return -1 +} + +const HProgress = forwardRef(function HProgress( + { + className, + rootClassName, + style, + percent = 0, + success, + successPercent, + status, + showInfo = true, + strokeColor, + trailColor, + strokeLinecap = 'round', + strokeWidth, + size = 'default', + format, + percentPosition = {} + }, + ref +) { + const { token } = theme.useToken() + + const { align: infoAlign = 'end', type: infoPosition = 'outer' } = + percentPosition + + const resolvedPercent = validProgress(percent) + const resolvedSuccessPercent = getSuccessPercent({ success, successPercent }) + const successValue = + resolvedSuccessPercent == null + ? undefined + : validProgress(resolvedSuccessPercent) + + const percentNumber = Number.parseInt( + String(successValue ?? resolvedPercent), + 10 + ) + + const progressStatus = useMemo(() => { + if (status === 'exception' || status === 'success' || status === 'active') { + return status + } + if (percentNumber >= 100) return 'success' + return status || 'normal' + }, [status, percentNumber]) + + const barHeight = getBarHeight({ size, strokeWidth }) + const outerWidth = getOuterWidth({ size, strokeWidth }) + const borderRadius = + strokeLinecap === 'square' || strokeLinecap === 'butt' ? 0 : barHeight + + const trailBackground = trailColor || token.colorFillSecondary + + const defaultFillColor = + progressStatus === 'exception' + ? token.colorError + : progressStatus === 'success' + ? token.colorSuccess + : token.colorPrimary + + const fillBackground = + resolveStrokeBackground(strokeColor) || defaultFillColor + + const successBackground = + success?.strokeColor || token.colorSuccess + + const textFormatter = format || ((value) => `${value}%`) + const progressTextValue = textFormatter( + resolvedPercent, + successValue ?? undefined + ) + + const progressInfo = !showInfo ? null : ( + + {progressStatus === 'exception' ? ( + + ) : progressStatus === 'success' ? ( + + ) : ( + progressTextValue + )} + + ) + + const innerInfo = + infoPosition === 'inner' && showInfo ? ( + {progressTextValue} + ) : null + + const lineInner = ( +
+
+ {innerInfo} +
+ {successValue != null && ( +
+ )} +
+ ) + + const isOuterStart = infoPosition === 'outer' && infoAlign === 'start' + const isOuterEnd = infoPosition === 'outer' && infoAlign === 'end' + const isLayoutBottom = + infoPosition === 'outer' && infoAlign === 'center' + + const content = isLayoutBottom ? ( +
+ {lineInner} + {progressInfo} +
+ ) : ( +
+ {isOuterStart && progressInfo} + {lineInner} + {isOuterEnd && progressInfo} +
+ ) + + return ( +
+ {content} +
+ ) +}) + +HProgress.propTypes = { + className: PropTypes.string, + rootClassName: PropTypes.string, + style: PropTypes.object, + percent: PropTypes.number, + success: PropTypes.shape({ + percent: PropTypes.number, + progress: PropTypes.number, + strokeColor: PropTypes.string + }), + successPercent: PropTypes.number, + status: PropTypes.oneOf(['normal', 'exception', 'active', 'success']), + showInfo: PropTypes.bool, + strokeColor: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.arrayOf(PropTypes.string), + PropTypes.object + ]), + trailColor: PropTypes.string, + strokeLinecap: PropTypes.oneOf(['butt', 'square', 'round']), + strokeWidth: PropTypes.number, + size: PropTypes.oneOfType([ + PropTypes.oneOf(['default', 'small']), + PropTypes.number, + PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])), + PropTypes.shape({ + width: PropTypes.number, + height: PropTypes.number + }) + ]), + format: PropTypes.func, + percentPosition: PropTypes.shape({ + align: PropTypes.oneOf(['start', 'center', 'end']), + type: PropTypes.oneOf(['inner', 'outer']) + }) +} + +export default HProgress diff --git a/src/components/Dashboard/common/StateDisplay.jsx b/src/components/Dashboard/common/StateDisplay.jsx index 8aaa091..881e25a 100644 --- a/src/components/Dashboard/common/StateDisplay.jsx +++ b/src/components/Dashboard/common/StateDisplay.jsx @@ -1,7 +1,8 @@ // PrinterSelect.js import PropTypes from 'prop-types' -import { Progress, Flex, Space, Modal, Button, Typography } from 'antd' +import { Flex, Space, Modal, Button, Typography } from 'antd' import StateTag from './StateTag' +import HProgress from './HProgress' import InfoCircleIcon from '../../Icons/InfoCircleIcon' import { useState } from 'react' @@ -43,7 +44,7 @@ const StateDisplay = ({ currentState?.progress && progressValue !== 100 && currentState?.progress > 0 ? ( -