From f0019d622ad0b79ec83a78b32a490698b40c3076 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 4 Sep 2024 13:40:15 +0300 Subject: [PATCH] Convert Progress Bars to TypeScript (cherry picked from commit a9072ac460f971d3da737de6446153d8cbf1e1c2) Closes #10404 --- .../src/Components/CircularProgressBar.js | 138 ------------------ .../src/Components/CircularProgressBar.tsx | 99 +++++++++++++ frontend/src/Components/Icon.tsx | 3 +- frontend/src/Components/Label.tsx | 6 +- frontend/src/Components/Link/Button.tsx | 6 +- frontend/src/Components/ProgressBar.js | 114 --------------- frontend/src/Components/ProgressBar.tsx | 94 ++++++++++++ frontend/src/Helpers/Props/kinds.ts | 12 ++ frontend/src/Helpers/Props/sizes.ts | 8 + .../src/System/Status/DiskSpace/DiskSpace.tsx | 3 +- 10 files changed, 225 insertions(+), 258 deletions(-) delete mode 100644 frontend/src/Components/CircularProgressBar.js create mode 100644 frontend/src/Components/CircularProgressBar.tsx delete mode 100644 frontend/src/Components/ProgressBar.js create mode 100644 frontend/src/Components/ProgressBar.tsx diff --git a/frontend/src/Components/CircularProgressBar.js b/frontend/src/Components/CircularProgressBar.js deleted file mode 100644 index b6b841c71..000000000 --- a/frontend/src/Components/CircularProgressBar.js +++ /dev/null @@ -1,138 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import styles from './CircularProgressBar.css'; - -class CircularProgressBar extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - progress: 0 - }; - } - - componentDidMount() { - this._progressStep(); - } - - componentDidUpdate(prevProps) { - const progress = this.props.progress; - - if (prevProps.progress !== progress) { - this._cancelProgressStep(); - this._progressStep(); - } - } - - componentWillUnmount() { - this._cancelProgressStep(); - } - - // - // Control - - _progressStep() { - this.requestAnimationFrame = window.requestAnimationFrame(() => { - this.setState({ - progress: this.state.progress + 1 - }, () => { - if (this.state.progress < this.props.progress) { - this._progressStep(); - } - }); - }); - } - - _cancelProgressStep() { - if (this.requestAnimationFrame) { - window.cancelAnimationFrame(this.requestAnimationFrame); - } - } - - // - // Render - - render() { - const { - className, - containerClassName, - size, - strokeWidth, - strokeColor, - showProgressText - } = this.props; - - const progress = this.state.progress; - - const center = size / 2; - const radius = center - strokeWidth; - const circumference = Math.PI * (radius * 2); - const sizeInPixels = `${size}px`; - const strokeDashoffset = ((100 - progress) / 100) * circumference; - const progressText = `${Math.round(progress)}%`; - - return ( -
- - - - - { - showProgressText && -
- {progressText} -
- } -
- ); - } -} - -CircularProgressBar.propTypes = { - className: PropTypes.string, - containerClassName: PropTypes.string, - size: PropTypes.number, - progress: PropTypes.number.isRequired, - strokeWidth: PropTypes.number, - strokeColor: PropTypes.string, - showProgressText: PropTypes.bool -}; - -CircularProgressBar.defaultProps = { - className: styles.circularProgressBar, - containerClassName: styles.circularProgressBarContainer, - size: 60, - strokeWidth: 5, - strokeColor: '#ffc230', - showProgressText: false -}; - -export default CircularProgressBar; diff --git a/frontend/src/Components/CircularProgressBar.tsx b/frontend/src/Components/CircularProgressBar.tsx new file mode 100644 index 000000000..bad48f83e --- /dev/null +++ b/frontend/src/Components/CircularProgressBar.tsx @@ -0,0 +1,99 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import styles from './CircularProgressBar.css'; + +interface CircularProgressBarProps { + className?: string; + containerClassName?: string; + size?: number; + progress: number; + strokeWidth?: number; + strokeColor?: string; + showProgressText?: boolean; +} + +function CircularProgressBar({ + className = styles.circularProgressBar, + containerClassName = styles.circularProgressBarContainer, + size = 60, + strokeWidth = 5, + strokeColor = '#ffc230', + showProgressText = false, + progress, +}: CircularProgressBarProps) { + const [currentProgress, setCurrentProgress] = useState(0); + const raf = React.useRef(0); + const center = size / 2; + const radius = center - strokeWidth; + const circumference = Math.PI * (radius * 2); + const sizeInPixels = `${size}px`; + const strokeDashoffset = ((100 - currentProgress) / 100) * circumference; + const progressText = `${Math.round(currentProgress)}%`; + + const handleAnimation = useCallback( + (p: number) => { + setCurrentProgress((prevProgress) => { + if (prevProgress < p) { + return prevProgress + Math.min(1, p - prevProgress); + } + + return prevProgress; + }); + }, + [setCurrentProgress] + ); + + useEffect(() => { + if (progress > currentProgress) { + cancelAnimationFrame(raf.current); + + raf.current = requestAnimationFrame(() => handleAnimation(progress)); + } + }, [progress, currentProgress, handleAnimation]); + + useEffect( + () => { + return () => cancelAnimationFrame(raf.current); + }, + // We only want to run this effect once + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + return ( +
+ + + + + {showProgressText && ( +
{progressText}
+ )} +
+ ); +} + +export default CircularProgressBar; diff --git a/frontend/src/Components/Icon.tsx b/frontend/src/Components/Icon.tsx index 86ff57a30..ea5279840 100644 --- a/frontend/src/Components/Icon.tsx +++ b/frontend/src/Components/Icon.tsx @@ -5,6 +5,7 @@ import { import classNames from 'classnames'; import React, { ComponentProps } from 'react'; import { kinds } from 'Helpers/Props'; +import { Kind } from 'Helpers/Props/kinds'; import styles from './Icon.css'; export interface IconProps @@ -14,7 +15,7 @@ export interface IconProps > { containerClassName?: ComponentProps<'span'>['className']; name: FontAwesomeIconProps['icon']; - kind?: Extract<(typeof kinds.all)[number], keyof typeof styles>; + kind?: Extract; size?: number; isSpinning?: FontAwesomeIconProps['spin']; title?: string | (() => string); diff --git a/frontend/src/Components/Label.tsx b/frontend/src/Components/Label.tsx index 411cefddf..9ab360f42 100644 --- a/frontend/src/Components/Label.tsx +++ b/frontend/src/Components/Label.tsx @@ -1,11 +1,13 @@ import classNames from 'classnames'; import React, { ComponentProps, ReactNode } from 'react'; import { kinds, sizes } from 'Helpers/Props'; +import { Kind } from 'Helpers/Props/kinds'; +import { Size } from 'Helpers/Props/sizes'; import styles from './Label.css'; export interface LabelProps extends ComponentProps<'span'> { - kind?: Extract<(typeof kinds.all)[number], keyof typeof styles>; - size?: Extract<(typeof sizes.all)[number], keyof typeof styles>; + kind?: Extract; + size?: Extract; outline?: boolean; children: ReactNode; } diff --git a/frontend/src/Components/Link/Button.tsx b/frontend/src/Components/Link/Button.tsx index c512b3a90..cf2293f59 100644 --- a/frontend/src/Components/Link/Button.tsx +++ b/frontend/src/Components/Link/Button.tsx @@ -1,6 +1,8 @@ import classNames from 'classnames'; import React from 'react'; import { align, kinds, sizes } from 'Helpers/Props'; +import { Kind } from 'Helpers/Props/kinds'; +import { Size } from 'Helpers/Props/sizes'; import Link, { LinkProps } from './Link'; import styles from './Button.css'; @@ -9,8 +11,8 @@ export interface ButtonProps extends Omit { (typeof align.all)[number], keyof typeof styles >; - kind?: Extract<(typeof kinds.all)[number], keyof typeof styles>; - size?: Extract<(typeof sizes.all)[number], keyof typeof styles>; + kind?: Extract; + size?: Extract; children: Required; } diff --git a/frontend/src/Components/ProgressBar.js b/frontend/src/Components/ProgressBar.js deleted file mode 100644 index 171b4c0fa..000000000 --- a/frontend/src/Components/ProgressBar.js +++ /dev/null @@ -1,114 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { ColorImpairedConsumer } from 'App/ColorImpairedContext'; -import { kinds, sizes } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import styles from './ProgressBar.css'; - -function ProgressBar(props) { - const { - className, - containerClassName, - title, - progress, - precision, - showText, - text, - kind, - size, - width - } = props; - - const progressPercent = `${progress.toFixed(precision)}%`; - const progressText = text || progressPercent; - const actualWidth = width ? `${width}px` : '100%'; - - return ( - - {(enableColorImpairedMode) => { - return ( -
- { - showText && width ? -
-
-
- {progressText} -
-
-
: - null - } - -
- - { - showText ? -
-
-
- {progressText} -
-
-
: - null - } -
- ); - }} - - ); -} - -ProgressBar.propTypes = { - className: PropTypes.string, - containerClassName: PropTypes.string, - title: PropTypes.string, - progress: PropTypes.number.isRequired, - precision: PropTypes.number.isRequired, - showText: PropTypes.bool.isRequired, - text: PropTypes.string, - kind: PropTypes.oneOf(kinds.all).isRequired, - size: PropTypes.oneOf(sizes.all).isRequired, - width: PropTypes.number -}; - -ProgressBar.defaultProps = { - className: styles.progressBar, - containerClassName: styles.container, - precision: 1, - showText: false, - kind: kinds.PRIMARY, - size: sizes.MEDIUM -}; - -export default ProgressBar; diff --git a/frontend/src/Components/ProgressBar.tsx b/frontend/src/Components/ProgressBar.tsx new file mode 100644 index 000000000..07b20d8a4 --- /dev/null +++ b/frontend/src/Components/ProgressBar.tsx @@ -0,0 +1,94 @@ +import classNames from 'classnames'; +import React from 'react'; +import { ColorImpairedConsumer } from 'App/ColorImpairedContext'; +import { Kind } from 'Helpers/Props/kinds'; +import { Size } from 'Helpers/Props/sizes'; +import translate from 'Utilities/String/translate'; +import styles from './ProgressBar.css'; + +interface ProgressBarProps { + className?: string; + containerClassName?: string; + title?: string; + progress: number; + precision?: number; + showText?: boolean; + text?: string; + kind?: Extract; + size?: Extract; + width?: number; +} + +function ProgressBar({ + className = styles.progressBar, + containerClassName = styles.container, + title, + progress, + precision = 1, + showText = false, + text, + kind = 'primary', + size = 'medium', + width, +}: ProgressBarProps) { + const progressPercent = `${progress.toFixed(precision)}%`; + const progressText = text || progressPercent; + const actualWidth = width ? `${width}px` : '100%'; + + return ( + + {(enableColorImpairedMode) => { + return ( +
+ {showText && width ? ( +
+
+
{progressText}
+
+
+ ) : null} + +
+ + {showText ? ( +
+
+
{progressText}
+
+
+ ) : null} +
+ ); + }} + + ); +} + +export default ProgressBar; diff --git a/frontend/src/Helpers/Props/kinds.ts b/frontend/src/Helpers/Props/kinds.ts index 9f205dd91..fc94defbb 100644 --- a/frontend/src/Helpers/Props/kinds.ts +++ b/frontend/src/Helpers/Props/kinds.ts @@ -25,3 +25,15 @@ export const all = [ WARNING, QUEUE, ] as const; + +export type Kind = + | 'danger' + | 'default' + | 'disabled' + | 'info' + | 'inverse' + | 'pink' + | 'primary' + | 'purple' + | 'success' + | 'warning'; diff --git a/frontend/src/Helpers/Props/sizes.ts b/frontend/src/Helpers/Props/sizes.ts index 809f0397a..526f49f8c 100644 --- a/frontend/src/Helpers/Props/sizes.ts +++ b/frontend/src/Helpers/Props/sizes.ts @@ -13,3 +13,11 @@ export const all = [ EXTRA_LARGE, EXTRA_EXTRA_LARGE, ] as const; + +export type Size = + | 'extraSmall' + | 'small' + | 'medium' + | 'large' + | 'extraLarge' + | 'extraExtraLarge'; diff --git a/frontend/src/System/Status/DiskSpace/DiskSpace.tsx b/frontend/src/System/Status/DiskSpace/DiskSpace.tsx index 2174e5b1e..f4f8e84d6 100644 --- a/frontend/src/System/Status/DiskSpace/DiskSpace.tsx +++ b/frontend/src/System/Status/DiskSpace/DiskSpace.tsx @@ -11,6 +11,7 @@ import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; import TableRow from 'Components/Table/TableRow'; import { kinds, sizes } from 'Helpers/Props'; +import { Kind } from 'Helpers/Props/kinds'; import { fetchDiskSpace } from 'Store/Actions/systemActions'; import formatBytes from 'Utilities/Number/formatBytes'; import translate from 'Utilities/String/translate'; @@ -67,7 +68,7 @@ function DiskSpace() { const { freeSpace, totalSpace } = item; const diskUsage = 100 - (freeSpace / totalSpace) * 100; - let diskUsageKind: (typeof kinds.all)[number] = kinds.PRIMARY; + let diskUsageKind: Kind = kinds.PRIMARY; if (diskUsage > 90) { diskUsageKind = kinds.DANGER;