diff --git a/frontend/src/Activity/Queue/QueueStatus.tsx b/frontend/src/Activity/Queue/QueueStatus.tsx
index 31a28f35c2..8a9f973d05 100644
--- a/frontend/src/Activity/Queue/QueueStatus.tsx
+++ b/frontend/src/Activity/Queue/QueueStatus.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import Icon, { IconProps } from 'Components/Icon';
+import Icon, { IconKind } from 'Components/Icon';
import Popover from 'Components/Tooltip/Popover';
import { icons, kinds } from 'Helpers/Props';
import { TooltipPosition } from 'Helpers/Props/tooltipPositions';
@@ -61,7 +61,7 @@ function QueueStatus(props: QueueStatusProps) {
// status === 'downloading'
let iconName = icons.DOWNLOADING;
- let iconKind: IconProps['kind'] = kinds.DEFAULT;
+ let iconKind: IconKind = kinds.DEFAULT;
let title = translate('Downloading');
if (status === 'paused') {
diff --git a/frontend/src/Components/Form/FormInputButton.tsx b/frontend/src/Components/Form/FormInputButton.tsx
index e5149ab14e..1235010ebb 100644
--- a/frontend/src/Components/Form/FormInputButton.tsx
+++ b/frontend/src/Components/Form/FormInputButton.tsx
@@ -8,12 +8,14 @@ import styles from './FormInputButton.css';
export interface FormInputButtonProps extends ButtonProps {
canSpin?: boolean;
isLastButton?: boolean;
+ isSpinning?: boolean;
}
function FormInputButton({
className = styles.button,
canSpin = false,
isLastButton = true,
+ isSpinning = false,
kind = kinds.PRIMARY,
...otherProps
}: FormInputButtonProps) {
@@ -22,6 +24,7 @@ function FormInputButton({
);
diff --git a/frontend/src/Components/Link/SpinnerButton.js b/frontend/src/Components/Link/SpinnerButton.js
deleted file mode 100644
index 09f11499b5..0000000000
--- a/frontend/src/Components/Link/SpinnerButton.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import classNames from 'classnames';
-import PropTypes from 'prop-types';
-import React from 'react';
-import Icon from 'Components/Icon';
-import { icons } from 'Helpers/Props';
-import Button from './Button';
-import styles from './SpinnerButton.css';
-
-function SpinnerButton(props) {
- const {
- className,
- isSpinning,
- isDisabled,
- spinnerIcon,
- children,
- ...otherProps
- } = props;
-
- return (
-
- );
-}
-
-SpinnerButton.propTypes = {
- ...Button.Props,
- className: PropTypes.string.isRequired,
- isSpinning: PropTypes.bool.isRequired,
- isDisabled: PropTypes.bool,
- spinnerIcon: PropTypes.object.isRequired,
- children: PropTypes.node
-};
-
-SpinnerButton.defaultProps = {
- className: styles.button,
- spinnerIcon: icons.SPINNER
-};
-
-export default SpinnerButton;
diff --git a/frontend/src/Components/Link/SpinnerButton.tsx b/frontend/src/Components/Link/SpinnerButton.tsx
new file mode 100644
index 0000000000..3a5ab2b325
--- /dev/null
+++ b/frontend/src/Components/Link/SpinnerButton.tsx
@@ -0,0 +1,41 @@
+import classNames from 'classnames';
+import React from 'react';
+import Icon, { IconName } from 'Components/Icon';
+import { icons } from 'Helpers/Props';
+import Button, { ButtonProps } from './Button';
+import styles from './SpinnerButton.css';
+
+export interface SpinnerButtonProps extends ButtonProps {
+ isSpinning: boolean;
+ isDisabled?: boolean;
+ spinnerIcon?: IconName;
+}
+
+function SpinnerButton({
+ className = styles.button,
+ isSpinning,
+ isDisabled,
+ spinnerIcon = icons.SPINNER,
+ children,
+ ...otherProps
+}: SpinnerButtonProps) {
+ return (
+
+ );
+}
+
+export default SpinnerButton;
diff --git a/frontend/src/Components/Link/SpinnerErrorButton.js b/frontend/src/Components/Link/SpinnerErrorButton.js
deleted file mode 100644
index b0f39bc267..0000000000
--- a/frontend/src/Components/Link/SpinnerErrorButton.js
+++ /dev/null
@@ -1,165 +0,0 @@
-import _ from 'lodash';
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import Icon from 'Components/Icon';
-import SpinnerButton from 'Components/Link/SpinnerButton';
-import { icons, kinds } from 'Helpers/Props';
-import styles from './SpinnerErrorButton.css';
-
-function getTestResult(error) {
- if (!error) {
- return {
- wasSuccessful: true,
- hasWarning: false,
- hasError: false
- };
- }
-
- if (error.status !== 400) {
- return {
- wasSuccessful: false,
- hasWarning: false,
- hasError: true
- };
- }
-
- const failures = error.responseJSON;
-
- const hasWarning = _.some(failures, { isWarning: true });
- const hasError = _.some(failures, (failure) => !failure.isWarning);
-
- return {
- wasSuccessful: false,
- hasWarning,
- hasError
- };
-}
-
-class SpinnerErrorButton extends Component {
-
- //
- // Lifecycle
-
- constructor(props, context) {
- super(props, context);
-
- this._testResultTimeout = null;
-
- this.state = {
- wasSuccessful: false,
- hasWarning: false,
- hasError: false
- };
- }
-
- componentDidUpdate(prevProps) {
- const {
- isSpinning,
- error
- } = this.props;
-
- if (prevProps.isSpinning && !isSpinning) {
- const testResult = getTestResult(error);
-
- this.setState(testResult, () => {
- const {
- wasSuccessful,
- hasWarning,
- hasError
- } = testResult;
-
- if (wasSuccessful || hasWarning || hasError) {
- this._testResultTimeout = setTimeout(this.resetState, 3000);
- }
- });
- }
- }
-
- componentWillUnmount() {
- if (this._testResultTimeout) {
- clearTimeout(this._testResultTimeout);
- }
- }
-
- //
- // Control
-
- resetState = () => {
- this.setState({
- wasSuccessful: false,
- hasWarning: false,
- hasError: false
- });
- };
-
- //
- // Render
-
- render() {
- const {
- kind,
- isSpinning,
- error,
- children,
- ...otherProps
- } = this.props;
-
- const {
- wasSuccessful,
- hasWarning,
- hasError
- } = this.state;
-
- const showIcon = wasSuccessful || hasWarning || hasError;
-
- let iconName = icons.CHECK;
- let iconKind = kind === kinds.PRIMARY ? kinds.DEFAULT : kinds.SUCCESS;
-
- if (hasWarning) {
- iconName = icons.WARNING;
- iconKind = kinds.WARNING;
- }
-
- if (hasError) {
- iconName = icons.DANGER;
- iconKind = kinds.DANGER;
- }
-
- return (
-
-
- {
- showIcon &&
-
-
-
- }
-
- {
-
- {
- children
- }
-
- }
-
-
- );
- }
-}
-
-SpinnerErrorButton.propTypes = {
- kind: PropTypes.oneOf(kinds.all),
- isSpinning: PropTypes.bool.isRequired,
- error: PropTypes.object,
- children: PropTypes.node.isRequired
-};
-
-export default SpinnerErrorButton;
diff --git a/frontend/src/Components/Link/SpinnerErrorButton.tsx b/frontend/src/Components/Link/SpinnerErrorButton.tsx
new file mode 100644
index 0000000000..221a12b610
--- /dev/null
+++ b/frontend/src/Components/Link/SpinnerErrorButton.tsx
@@ -0,0 +1,143 @@
+import React, { useEffect, useMemo, useRef, useState } from 'react';
+import { Error } from 'App/State/AppSectionState';
+import Icon, { IconKind, IconName } from 'Components/Icon';
+import SpinnerButton, {
+ SpinnerButtonProps,
+} from 'Components/Link/SpinnerButton';
+import usePrevious from 'Helpers/Hooks/usePrevious';
+import { icons } from 'Helpers/Props';
+import { ValidationFailure } from 'typings/pending';
+import styles from './SpinnerErrorButton.css';
+
+function getTestResult(error: Error | string | undefined) {
+ if (!error) {
+ return {
+ wasSuccessful: true,
+ hasWarning: false,
+ hasError: false,
+ };
+ }
+
+ if (typeof error === 'string' || error.status !== 400) {
+ return {
+ wasSuccessful: false,
+ hasWarning: false,
+ hasError: true,
+ };
+ }
+
+ const failures = error.responseJSON as ValidationFailure[];
+
+ const { hasError, hasWarning } = failures.reduce(
+ (acc, failure) => {
+ if (failure.isWarning) {
+ acc.hasWarning = true;
+ } else {
+ acc.hasError = true;
+ }
+
+ return acc;
+ },
+ { hasWarning: false, hasError: false }
+ );
+
+ return {
+ wasSuccessful: false,
+ hasWarning,
+ hasError,
+ };
+}
+
+interface SpinnerErrorButtonProps extends SpinnerButtonProps {
+ isSpinning: boolean;
+ error?: Error | string;
+ children: React.ReactNode;
+}
+
+function SpinnerErrorButton({
+ kind,
+ isSpinning,
+ error,
+ children,
+ ...otherProps
+}: SpinnerErrorButtonProps) {
+ const wasSpinning = usePrevious(isSpinning);
+ const updateTimeout = useRef>();
+
+ const [result, setResult] = useState({
+ wasSuccessful: false,
+ hasWarning: false,
+ hasError: false,
+ });
+ const { wasSuccessful, hasWarning, hasError } = result;
+
+ const showIcon = wasSuccessful || hasWarning || hasError;
+
+ const { iconName, iconKind } = useMemo<{
+ iconName: IconName;
+ iconKind: IconKind;
+ }>(() => {
+ if (hasWarning) {
+ return {
+ iconName: icons.WARNING,
+ iconKind: 'warning',
+ };
+ }
+
+ if (hasError) {
+ return {
+ iconName: icons.DANGER,
+ iconKind: 'danger',
+ };
+ }
+
+ return {
+ iconName: icons.CHECK,
+ iconKind: kind === 'primary' ? 'default' : 'success',
+ };
+ }, [kind, hasError, hasWarning]);
+
+ useEffect(() => {
+ if (wasSpinning && !isSpinning) {
+ const testResult = getTestResult(error);
+
+ setResult(testResult);
+
+ const { wasSuccessful, hasWarning, hasError } = testResult;
+
+ if (wasSuccessful || hasWarning || hasError) {
+ updateTimeout.current = setTimeout(() => {
+ setResult({
+ wasSuccessful: false,
+ hasWarning: false,
+ hasError: false,
+ });
+ }, 3000);
+ }
+ }
+ }, [isSpinning, wasSpinning, error]);
+
+ useEffect(() => {
+ return () => {
+ if (updateTimeout.current) {
+ clearTimeout(updateTimeout.current);
+ }
+ };
+ }, []);
+
+ return (
+
+
+ {showIcon && (
+
+
+
+ )}
+
+ {children}
+
+
+ );
+}
+
+export default SpinnerErrorButton;
diff --git a/frontend/src/Components/Link/SpinnerIconButton.js b/frontend/src/Components/Link/SpinnerIconButton.js
deleted file mode 100644
index d36ebb24d2..0000000000
--- a/frontend/src/Components/Link/SpinnerIconButton.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import { icons } from 'Helpers/Props';
-import IconButton from './IconButton';
-
-function SpinnerIconButton(props) {
- const {
- name,
- spinningName,
- isDisabled,
- isSpinning,
- ...otherProps
- } = props;
-
- return (
-
- );
-}
-
-SpinnerIconButton.propTypes = {
- ...IconButton.propTypes,
- className: PropTypes.string,
- name: PropTypes.object.isRequired,
- spinningName: PropTypes.object.isRequired,
- isDisabled: PropTypes.bool.isRequired,
- isSpinning: PropTypes.bool.isRequired
-};
-
-SpinnerIconButton.defaultProps = {
- spinningName: icons.SPINNER,
- isDisabled: false,
- isSpinning: false
-};
-
-export default SpinnerIconButton;
diff --git a/frontend/src/Components/Link/SpinnerIconButton.tsx b/frontend/src/Components/Link/SpinnerIconButton.tsx
new file mode 100644
index 0000000000..e0df823048
--- /dev/null
+++ b/frontend/src/Components/Link/SpinnerIconButton.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import { IconName } from 'Components/Icon';
+import { icons } from 'Helpers/Props';
+import IconButton, { IconButtonProps } from './IconButton';
+
+interface SpinnerIconButtonProps extends IconButtonProps {
+ spinningName?: IconName;
+}
+
+function SpinnerIconButton({
+ name,
+ spinningName = icons.SPINNER,
+ isDisabled = false,
+ isSpinning = false,
+ ...otherProps
+}: SpinnerIconButtonProps) {
+ return (
+
+ );
+}
+
+export default SpinnerIconButton;
diff --git a/frontend/src/Components/Modal/ConfirmModal.tsx b/frontend/src/Components/Modal/ConfirmModal.tsx
index 2adf99a3a5..259d753bfe 100644
--- a/frontend/src/Components/Modal/ConfirmModal.tsx
+++ b/frontend/src/Components/Modal/ConfirmModal.tsx
@@ -1,20 +1,17 @@
import React, { useEffect } from 'react';
import Button from 'Components/Link/Button';
-import SpinnerButton from 'Components/Link/SpinnerButton';
-import Modal from 'Components/Modal/Modal';
+import SpinnerButton, {
+ SpinnerButtonProps,
+} from 'Components/Link/SpinnerButton';
+import Modal, { ModalProps } from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import useKeyboardShortcuts from 'Helpers/Hooks/useKeyboardShortcuts';
-import { Kind } from 'Helpers/Props/kinds';
-import { Size } from 'Helpers/Props/sizes';
-interface ConfirmModalProps {
- className?: string;
- isOpen: boolean;
- kind?: Kind;
- size?: Size;
+interface ConfirmModalProps extends Omit {
+ kind?: SpinnerButtonProps['kind'];
title: string;
message: React.ReactNode;
confirmLabel?: string;
diff --git a/frontend/src/System/Status/Health/Health.tsx b/frontend/src/System/Status/Health/Health.tsx
index 087146740a..b921c4d964 100644
--- a/frontend/src/System/Status/Health/Health.tsx
+++ b/frontend/src/System/Status/Health/Health.tsx
@@ -3,7 +3,7 @@ import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
-import Icon, { IconProps } from 'Components/Icon';
+import Icon, { IconKind } from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@@ -97,7 +97,7 @@ function Health() {
{items.map((item) => {
const source = item.source;
- let kind: IconProps['kind'] = kinds.WARNING;
+ let kind: IconKind = kinds.WARNING;
switch (item.type.toLowerCase()) {
case 'error':
kind = kinds.DANGER;