Convert Backup and Restore to TypeScript

pull/7605/head
Mark McDowall 2 months ago
parent 8a8ea4eb94
commit 9276bd7a16
No known key found for this signature in database

@ -24,7 +24,7 @@ import QualityConnector from 'Settings/Quality/QualityConnector';
import Settings from 'Settings/Settings';
import TagSettings from 'Settings/Tags/TagSettings';
import UISettingsConnector from 'Settings/UI/UISettingsConnector';
import BackupsConnector from 'System/Backup/BackupsConnector';
import Backups from 'System/Backup/Backups';
import LogsTableConnector from 'System/Events/LogsTableConnector';
import Logs from 'System/Logs/Logs';
import Status from 'System/Status/Status';
@ -147,7 +147,7 @@ function AppRoutes() {
<Route path="/system/tasks" component={Tasks} />
<Route path="/system/backup" component={BackupsConnector} />
<Route path="/system/backup" component={Backups} />
<Route path="/system/updates" component={Updates} />

@ -62,6 +62,7 @@ export interface AppSectionState {
isConnected: boolean;
isDisconnected: boolean;
isReconnecting: boolean;
isRestarting: boolean;
isSidebarVisible: boolean;
version: string;
prevVersion?: string;

@ -0,0 +1,9 @@
import Backup from 'typings/Backup';
import AppSectionState, { Error } from './AppSectionState';
interface BackupAppState extends AppSectionState<Backup> {
isRestoring: boolean;
restoreError?: Error;
}
export default BackupAppState;

@ -4,6 +4,7 @@ import SystemStatus from 'typings/SystemStatus';
import Task from 'typings/Task';
import Update from 'typings/Update';
import AppSectionState, { AppSectionItemState } from './AppSectionState';
import BackupAppState from './BackupAppState';
export type DiskSpaceAppState = AppSectionState<DiskSpace>;
export type HealthAppState = AppSectionState<Health>;
@ -12,6 +13,7 @@ export type TaskAppState = AppSectionState<Task>;
export type UpdateAppState = AppSectionState<Update>;
interface SystemAppState {
backups: BackupAppState;
diskSpace: DiskSpaceAppState;
health: HealthAppState;
status: SystemStatusAppState;

@ -24,8 +24,7 @@ function parseValue(
return newValue;
}
interface NumberInputProps
extends Omit<TextInputProps<number | null>, 'value' | 'onChange'> {
interface NumberInputProps extends Omit<TextInputProps, 'value' | 'onChange'> {
value?: number | null;
min?: number;
max?: number;

@ -7,7 +7,7 @@ function onCopy(e: SyntheticEvent) {
e.nativeEvent.stopImmediatePropagation();
}
function PasswordInput(props: TextInputProps<string>) {
function PasswordInput(props: TextInputProps) {
return <TextInput {...props} type="password" onCopy={onCopy} />;
}

@ -7,13 +7,11 @@ import React, {
useEffect,
useRef,
} from 'react';
import { InputType } from 'Helpers/Props/inputTypes';
import { FileInputChanged, InputChanged } from 'typings/inputs';
import styles from './TextInput.css';
export interface TextInputProps<T> {
interface CommonTextInputProps {
className?: string;
type?: InputType;
readOnly?: boolean;
autoFocus?: boolean;
placeholder?: string;
@ -25,14 +23,23 @@ export interface TextInputProps<T> {
step?: number;
min?: number;
max?: number;
onChange: (change: InputChanged<T> | FileInputChanged) => void;
onFocus?: (event: FocusEvent) => void;
onBlur?: (event: SyntheticEvent) => void;
onCopy?: (event: SyntheticEvent) => void;
onSelectionChange?: (start: number | null, end: number | null) => void;
}
function TextInput<T>({
export interface TextInputProps extends CommonTextInputProps {
type?: 'date' | 'number' | 'password' | 'text';
onChange: (change: InputChanged<string>) => void;
}
export interface FileInputProps extends CommonTextInputProps {
type: 'file';
onChange: (change: FileInputChanged) => void;
}
function TextInput({
className = styles.input,
type = 'text',
readOnly = false,
@ -51,7 +58,7 @@ function TextInput<T>({
onCopy,
onChange,
onSelectionChange,
}: TextInputProps<T>) {
}: TextInputProps | FileInputProps): JSX.Element {
const inputRef = useRef<HTMLInputElement>(null);
const selectionTimeout = useRef<ReturnType<typeof setTimeout>>();
const selectionStart = useRef<number | null>();

@ -1,164 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import RestoreBackupModalConnector from './RestoreBackupModalConnector';
import styles from './BackupRow.css';
class BackupRow extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isRestoreModalOpen: false,
isConfirmDeleteModalOpen: false
};
}
//
// Listeners
onRestorePress = () => {
this.setState({ isRestoreModalOpen: true });
};
onRestoreModalClose = () => {
this.setState({ isRestoreModalOpen: false });
};
onDeletePress = () => {
this.setState({ isConfirmDeleteModalOpen: true });
};
onConfirmDeleteModalClose = () => {
this.setState({ isConfirmDeleteModalOpen: false });
};
onConfirmDeletePress = () => {
const {
id,
onDeleteBackupPress
} = this.props;
this.setState({ isConfirmDeleteModalOpen: false }, () => {
onDeleteBackupPress(id);
});
};
//
// Render
render() {
const {
id,
type,
name,
path,
size,
time
} = this.props;
const {
isRestoreModalOpen,
isConfirmDeleteModalOpen
} = this.state;
let iconClassName = icons.SCHEDULED;
let iconTooltip = translate('Scheduled');
if (type === 'manual') {
iconClassName = icons.INTERACTIVE;
iconTooltip = translate('Manual');
} else if (type === 'update') {
iconClassName = icons.UPDATE;
iconTooltip = translate('BeforeUpdate');
}
return (
<TableRow key={id}>
<TableRowCell className={styles.type}>
{
<Icon
name={iconClassName}
title={iconTooltip}
/>
}
</TableRowCell>
<TableRowCell>
<Link
to={`${window.Sonarr.urlBase}${path}`}
noRouter={true}
>
{name}
</Link>
</TableRowCell>
<TableRowCell>
{formatBytes(size)}
</TableRowCell>
<RelativeDateCell
date={time}
/>
<TableRowCell className={styles.actions}>
<IconButton
title={translate('RestoreBackup')}
name={icons.RESTORE}
onPress={this.onRestorePress}
/>
<IconButton
title={translate('DeleteBackup')}
name={icons.DELETE}
onPress={this.onDeletePress}
/>
</TableRowCell>
<RestoreBackupModalConnector
isOpen={isRestoreModalOpen}
id={id}
name={name}
onModalClose={this.onRestoreModalClose}
/>
<ConfirmModal
isOpen={isConfirmDeleteModalOpen}
kind={kinds.DANGER}
title={translate('DeleteBackup')}
message={translate('DeleteBackupMessageText', {
name
})}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeletePress}
onCancel={this.onConfirmDeleteModalClose}
/>
</TableRow>
);
}
}
BackupRow.propTypes = {
id: PropTypes.number.isRequired,
type: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,
time: PropTypes.string.isRequired,
onDeleteBackupPress: PropTypes.func.isRequired
};
export default BackupRow;

@ -0,0 +1,127 @@
import React, { useCallback, useMemo, useState } from 'react';
import { useDispatch } from 'react-redux';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props';
import { deleteBackup } from 'Store/Actions/systemActions';
import { BackupType } from 'typings/Backup';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import RestoreBackupModal from './RestoreBackupModal';
import styles from './BackupRow.css';
interface BackupRowProps {
id: number;
type: BackupType;
name: string;
path: string;
size: number;
time: string;
}
function BackupRow({ id, type, name, path, size, time }: BackupRowProps) {
const dispatch = useDispatch();
const [isRestoreModalOpen, setIsRestoreModalOpen] = useState(false);
const [isConfirmDeleteModalOpen, setIsConfirmDeleteModalOpen] =
useState(false);
const { iconClassName, iconTooltip } = useMemo(() => {
if (type === 'manual') {
return {
iconClassName: icons.INTERACTIVE,
iconTooltip: translate('Manual'),
};
}
if (type === 'update') {
return {
iconClassName: icons.UPDATE,
iconTooltip: translate('BeforeUpdate'),
};
}
return {
iconClassName: icons.SCHEDULED,
iconTooltip: translate('Scheduled'),
};
}, [type]);
const handleRestorePress = useCallback(() => {
setIsRestoreModalOpen(true);
}, []);
const handleRestoreModalClose = useCallback(() => {
setIsRestoreModalOpen(false);
}, []);
const handleDeletePress = useCallback(() => {
setIsConfirmDeleteModalOpen(true);
}, []);
const handleConfirmDeleteModalClose = useCallback(() => {
setIsConfirmDeleteModalOpen(false);
}, []);
const handleConfirmDeletePress = useCallback(() => {
dispatch(deleteBackup({ id }));
setIsConfirmDeleteModalOpen(false);
}, [id, dispatch]);
return (
<TableRow key={id}>
<TableRowCell className={styles.type}>
<Icon name={iconClassName} title={iconTooltip} />
</TableRowCell>
<TableRowCell>
<Link to={`${window.Sonarr.urlBase}${path}`} noRouter={true}>
{name}
</Link>
</TableRowCell>
<TableRowCell>{formatBytes(size)}</TableRowCell>
<RelativeDateCell date={time} />
<TableRowCell className={styles.actions}>
<IconButton
title={translate('RestoreBackup')}
name={icons.RESTORE}
onPress={handleRestorePress}
/>
<IconButton
title={translate('DeleteBackup')}
name={icons.DELETE}
onPress={handleDeletePress}
/>
</TableRowCell>
<RestoreBackupModal
isOpen={isRestoreModalOpen}
id={id}
name={name}
onModalClose={handleRestoreModalClose}
/>
<ConfirmModal
isOpen={isConfirmDeleteModalOpen}
kind={kinds.DANGER}
title={translate('DeleteBackup')}
message={translate('DeleteBackupMessageText', {
name,
})}
confirmLabel={translate('Delete')}
onConfirm={handleConfirmDeletePress}
onCancel={handleConfirmDeleteModalClose}
/>
</TableRow>
);
}
export default BackupRow;

@ -1,179 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import BackupRow from './BackupRow';
import RestoreBackupModalConnector from './RestoreBackupModalConnector';
const columns = [
{
name: 'type',
isVisible: true
},
{
name: 'name',
label: () => translate('Name'),
isVisible: true
},
{
name: 'size',
label: () => translate('Size'),
isVisible: true
},
{
name: 'time',
label: () => translate('Time'),
isVisible: true
},
{
name: 'actions',
isVisible: true
}
];
class Backups extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isRestoreModalOpen: false
};
}
//
// Listeners
onRestorePress = () => {
this.setState({ isRestoreModalOpen: true });
};
onRestoreModalClose = () => {
this.setState({ isRestoreModalOpen: false });
};
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
backupExecuting,
onBackupPress,
onDeleteBackupPress
} = this.props;
const hasBackups = isPopulated && !!items.length;
const noBackups = isPopulated && !items.length;
return (
<PageContent title={translate('Backups')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('BackupNow')}
iconName={icons.BACKUP}
isSpinning={backupExecuting}
onPress={onBackupPress}
/>
<PageToolbarButton
label={translate('RestoreBackup')}
iconName={icons.RESTORE}
onPress={this.onRestorePress}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<Alert kind={kinds.DANGER}>
{translate('BackupsLoadError')}
</Alert>
}
{
noBackups &&
<Alert kind={kinds.INFO}>
{translate('NoBackupsAreAvailable')}
</Alert>
}
{
hasBackups &&
<Table
columns={columns}
>
<TableBody>
{
items.map((item) => {
const {
id,
type,
name,
path,
size,
time
} = item;
return (
<BackupRow
key={id}
id={id}
type={type}
name={name}
path={path}
size={size}
time={time}
onDeleteBackupPress={onDeleteBackupPress}
/>
);
})
}
</TableBody>
</Table>
}
</PageContentBody>
<RestoreBackupModalConnector
isOpen={this.state.isRestoreModalOpen}
onModalClose={this.onRestoreModalClose}
/>
</PageContent>
);
}
}
Backups.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.array.isRequired,
backupExecuting: PropTypes.bool.isRequired,
onBackupPress: PropTypes.func.isRequired,
onDeleteBackupPress: PropTypes.func.isRequired
};
export default Backups;

@ -0,0 +1,156 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import * as commandNames from 'Commands/commandNames';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import Column from 'Components/Table/Column';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import usePrevious from 'Helpers/Hooks/usePrevious';
import { icons, kinds } from 'Helpers/Props';
import { executeCommand } from 'Store/Actions/commandActions';
import { fetchBackups } from 'Store/Actions/systemActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import translate from 'Utilities/String/translate';
import BackupRow from './BackupRow';
import RestoreBackupModal from './RestoreBackupModal';
const columns: Column[] = [
{
name: 'type',
label: '',
isVisible: true,
},
{
name: 'name',
label: () => translate('Name'),
isVisible: true,
},
{
name: 'size',
label: () => translate('Size'),
isVisible: true,
},
{
name: 'time',
label: () => translate('Time'),
isVisible: true,
},
{
name: 'actions',
label: '',
isVisible: true,
},
];
function Backups() {
const dispatch = useDispatch();
const { isFetching, isPopulated, error, items } = useSelector(
(state: AppState) => state.system.backups
);
const isBackupExecuting = useSelector(
createCommandExecutingSelector(commandNames.BACKUP)
);
const [isRestoreModalOpen, setIsRestoreModalOpen] = useState(false);
const wasBackupExecuting = usePrevious(isBackupExecuting);
const hasBackups = isPopulated && !!items.length;
const noBackups = isPopulated && !items.length;
const handleBackupPress = useCallback(() => {
dispatch(
executeCommand({
name: commandNames.BACKUP,
})
);
}, [dispatch]);
const handleRestorePress = useCallback(() => {
setIsRestoreModalOpen(true);
}, []);
const handleRestoreModalClose = useCallback(() => {
setIsRestoreModalOpen(false);
}, []);
useEffect(() => {
dispatch(fetchBackups());
}, [dispatch]);
useEffect(() => {
if (wasBackupExecuting && !isBackupExecuting) {
dispatch(fetchBackups());
}
}, [isBackupExecuting, wasBackupExecuting, dispatch]);
return (
<PageContent title={translate('Backups')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('BackupNow')}
iconName={icons.BACKUP}
isSpinning={isBackupExecuting}
onPress={handleBackupPress}
/>
<PageToolbarButton
label={translate('RestoreBackup')}
iconName={icons.RESTORE}
onPress={handleRestorePress}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
{!isFetching && !!error ? (
<Alert kind={kinds.DANGER}>{translate('BackupsLoadError')}</Alert>
) : null}
{noBackups ? (
<Alert kind={kinds.INFO}>{translate('NoBackupsAreAvailable')}</Alert>
) : null}
{hasBackups ? (
<Table columns={columns}>
<TableBody>
{items.map((item) => {
const { id, type, name, path, size, time } = item;
return (
<BackupRow
key={id}
id={id}
type={type}
name={name}
path={path}
size={size}
time={time}
/>
);
})}
</TableBody>
</Table>
) : null}
</PageContentBody>
<RestoreBackupModal
isOpen={isRestoreModalOpen}
onModalClose={handleRestoreModalClose}
/>
</PageContent>
);
}
export default Backups;

@ -1,84 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import { executeCommand } from 'Store/Actions/commandActions';
import { deleteBackup, fetchBackups } from 'Store/Actions/systemActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import Backups from './Backups';
function createMapStateToProps() {
return createSelector(
(state) => state.system.backups,
createCommandExecutingSelector(commandNames.BACKUP),
(backups, backupExecuting) => {
const {
isFetching,
isPopulated,
error,
items
} = backups;
return {
isFetching,
isPopulated,
error,
items,
backupExecuting
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
dispatchFetchBackups() {
dispatch(fetchBackups());
},
onDeleteBackupPress(id) {
dispatch(deleteBackup({ id }));
},
onBackupPress() {
dispatch(executeCommand({
name: commandNames.BACKUP
}));
}
};
}
class BackupsConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.dispatchFetchBackups();
}
componentDidUpdate(prevProps) {
if (prevProps.backupExecuting && !this.props.backupExecuting) {
this.props.dispatchFetchBackups();
}
}
//
// Render
render() {
return (
<Backups
{...this.props}
/>
);
}
}
BackupsConnector.propTypes = {
backupExecuting: PropTypes.bool.isRequired,
dispatchFetchBackups: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, createMapDispatchToProps)(BackupsConnector);

@ -1,31 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import RestoreBackupModalContentConnector from './RestoreBackupModalContentConnector';
function RestoreBackupModal(props) {
const {
isOpen,
onModalClose,
...otherProps
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<RestoreBackupModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
RestoreBackupModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default RestoreBackupModal;

@ -0,0 +1,36 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import Modal from 'Components/Modal/Modal';
import { clearRestoreBackup } from 'Store/Actions/systemActions';
import RestoreBackupModalContent, {
RestoreBackupModalContentProps,
} from './RestoreBackupModalContent';
interface RestoreBackupModalProps extends RestoreBackupModalContentProps {
isOpen: boolean;
onModalClose: () => void;
}
function RestoreBackupModal({
isOpen,
onModalClose,
...otherProps
}: RestoreBackupModalProps) {
const dispatch = useDispatch();
const handleModalClose = useCallback(() => {
dispatch(clearRestoreBackup());
onModalClose();
}, [dispatch, onModalClose]);
return (
<Modal isOpen={isOpen} onModalClose={handleModalClose}>
<RestoreBackupModalContent
{...otherProps}
onModalClose={handleModalClose}
/>
</Modal>
);
}
export default RestoreBackupModal;

@ -1,15 +0,0 @@
import { connect } from 'react-redux';
import { clearRestoreBackup } from 'Store/Actions/systemActions';
import RestoreBackupModal from './RestoreBackupModal';
function createMapDispatchToProps(dispatch, props) {
return {
onModalClose() {
dispatch(clearRestoreBackup());
props.onModalClose();
}
};
}
export default connect(null, createMapDispatchToProps)(RestoreBackupModal);

@ -9,6 +9,7 @@
.step {
display: flex;
margin-bottom: 10px;
font-size: $largeFontSize;
line-height: 20px;
}

@ -1,241 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import TextInput from 'Components/Form/TextInput';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
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 { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './RestoreBackupModalContent.css';
function getErrorMessage(error) {
if (!error || !error.responseJSON || !error.responseJSON.message) {
return translate('ErrorRestoringBackup');
}
return error.responseJSON.message;
}
function getStepIconProps(isExecuting, hasExecuted, error) {
if (isExecuting) {
return {
name: icons.SPINNER,
isSpinning: true
};
}
if (hasExecuted) {
return {
name: icons.CHECK,
kind: kinds.SUCCESS
};
}
if (error) {
return {
name: icons.FATAL,
kinds: kinds.DANGER,
title: getErrorMessage(error)
};
}
return {
name: icons.PENDING
};
}
class RestoreBackupModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
file: null,
path: '',
isRestored: false,
isRestarted: false,
isReloading: false
};
}
componentDidUpdate(prevProps) {
const {
isRestoring,
restoreError,
isRestarting,
dispatchRestart
} = this.props;
if (prevProps.isRestoring && !isRestoring && !restoreError) {
this.setState({ isRestored: true }, () => {
dispatchRestart();
});
}
if (prevProps.isRestarting && !isRestarting) {
this.setState({
isRestarted: true,
isReloading: true
}, () => {
location.reload();
});
}
}
//
// Listeners
onPathChange = ({ value, files }) => {
this.setState({
file: files[0],
path: value
});
};
onRestorePress = () => {
const {
id,
onRestorePress
} = this.props;
onRestorePress({
id,
file: this.state.file
});
};
//
// Render
render() {
const {
id,
name,
isRestoring,
restoreError,
isRestarting,
onModalClose
} = this.props;
const {
path,
isRestored,
isRestarted,
isReloading
} = this.state;
const isRestoreDisabled = (
(!id && !path) ||
isRestoring ||
isRestarting ||
isReloading
);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Restore Backup
</ModalHeader>
<ModalBody>
{
!!id && translate('WouldYouLikeToRestoreBackup', {
name
})
}
{
!id &&
<TextInput
type="file"
name="path"
value={path}
onChange={this.onPathChange}
/>
}
<div className={styles.steps}>
<div className={styles.step}>
<div className={styles.stepState}>
<Icon
size={20}
{...getStepIconProps(isRestoring, isRestored, restoreError)}
/>
</div>
<div>
{translate('Restore')}
</div>
</div>
<div className={styles.step}>
<div className={styles.stepState}>
<Icon
size={20}
{...getStepIconProps(isRestarting, isRestarted)}
/>
</div>
<div>
{translate('Restart')}
</div>
</div>
<div className={styles.step}>
<div className={styles.stepState}>
<Icon
size={20}
{...getStepIconProps(isReloading, false)}
/>
</div>
<div>
{translate('Reload')}
</div>
</div>
</div>
</ModalBody>
<ModalFooter>
<div className={styles.additionalInfo}>
{translate('RestartReloadNote')}
</div>
<Button onPress={onModalClose}>
{translate('Cancel')}
</Button>
<SpinnerButton
kind={kinds.WARNING}
isDisabled={isRestoreDisabled}
isSpinning={isRestoring}
onPress={this.onRestorePress}
>
{translate('Restore')}
</SpinnerButton>
</ModalFooter>
</ModalContent>
);
}
}
RestoreBackupModalContent.propTypes = {
id: PropTypes.number,
name: PropTypes.string,
path: PropTypes.string,
isRestoring: PropTypes.bool.isRequired,
restoreError: PropTypes.object,
isRestarting: PropTypes.bool.isRequired,
dispatchRestart: PropTypes.func.isRequired,
onRestorePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default RestoreBackupModalContent;

@ -0,0 +1,198 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Error } from 'App/State/AppSectionState';
import AppState from 'App/State/AppState';
import TextInput from 'Components/Form/TextInput';
import Icon, { IconName, IconProps } from 'Components/Icon';
import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
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 usePrevious from 'Helpers/Hooks/usePrevious';
import { icons, kinds } from 'Helpers/Props';
import { restart, restoreBackup } from 'Store/Actions/systemActions';
import { FileInputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
import styles from './RestoreBackupModalContent.css';
function getErrorMessage(error: Error) {
if (
!error ||
!error.responseJSON ||
!('message' in error.responseJSON) ||
!error.responseJSON.message
) {
return translate('ErrorRestoringBackup');
}
return error.responseJSON.message;
}
function getStepIconProps(
isExecuting: boolean,
hasExecuted: boolean,
error?: Error
): {
name: IconName;
kind?: IconProps['kind'];
title?: string;
isSpinning?: boolean;
} {
if (isExecuting) {
return {
name: icons.SPINNER,
isSpinning: true,
};
}
if (hasExecuted) {
return {
name: icons.CHECK,
kind: 'success',
};
}
if (error) {
return {
name: icons.FATAL,
kind: 'danger',
title: getErrorMessage(error),
};
}
return {
name: icons.PENDING,
};
}
export interface RestoreBackupModalContentProps {
id?: number;
name?: string;
onModalClose: () => void;
}
function RestoreBackupModalContent({
id,
name,
onModalClose,
}: RestoreBackupModalContentProps) {
const { isRestoring, restoreError } = useSelector(
(state: AppState) => state.system.backups
);
const { isRestarting } = useSelector((state: AppState) => state.app);
const dispatch = useDispatch();
const [path, setPath] = useState('');
const [file, setFile] = useState<File | null>(null);
const [isRestored, setIsRestored] = useState(false);
const [isRestarted, setIsRestarted] = useState(false);
const [isReloading, setIsReloading] = useState(false);
const wasRestoring = usePrevious(isRestoring);
const wasRestarting = usePrevious(isRestarting);
const isRestoreDisabled =
(!id && !path) || isRestoring || isRestarting || isReloading;
const handlePathChange = useCallback(({ value, files }: FileInputChanged) => {
if (!files?.length) {
return;
}
setPath(value);
setFile(files[0]);
}, []);
const handleRestorePress = useCallback(() => {
dispatch(restoreBackup({ id, file }));
}, [id, file, dispatch]);
useEffect(() => {
if (wasRestoring && !isRestoring && !restoreError) {
setIsRestored(true);
dispatch(restart());
}
}, [isRestoring, wasRestoring, restoreError, dispatch]);
useEffect(() => {
if (wasRestarting && !isRestarting) {
setIsRestarted(true);
setIsReloading(true);
window.location.reload();
}
}, [isRestarting, wasRestarting, dispatch]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>Restore Backup</ModalHeader>
<ModalBody>
{id && name ? (
translate('WouldYouLikeToRestoreBackup', {
name,
})
) : (
<TextInput
type="file"
name="path"
value={path}
onChange={handlePathChange}
/>
)}
<div className={styles.steps}>
<div className={styles.step}>
<div className={styles.stepState}>
<Icon
size={20}
{...getStepIconProps(isRestoring, isRestored, restoreError)}
/>
</div>
<div>{translate('Restore')}</div>
</div>
<div className={styles.step}>
<div className={styles.stepState}>
<Icon
size={20}
{...getStepIconProps(isRestarting, isRestarted)}
/>
</div>
<div>{translate('Restart')}</div>
</div>
<div className={styles.step}>
<div className={styles.stepState}>
<Icon size={20} {...getStepIconProps(isReloading, false)} />
</div>
<div>{translate('Reload')}</div>
</div>
</div>
</ModalBody>
<ModalFooter>
<div className={styles.additionalInfo}>
{translate('RestartReloadNote')}
</div>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<SpinnerButton
kind={kinds.WARNING}
isDisabled={isRestoreDisabled}
isSpinning={isRestoring}
onPress={handleRestorePress}
>
{translate('Restore')}
</SpinnerButton>
</ModalFooter>
</ModalContent>
);
}
export default RestoreBackupModalContent;

@ -1,37 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { restart, restoreBackup } from 'Store/Actions/systemActions';
import RestoreBackupModalContent from './RestoreBackupModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.system.backups,
(state) => state.app.isRestarting,
(backups, isRestarting) => {
const {
isRestoring,
restoreError
} = backups;
return {
isRestoring,
restoreError,
isRestarting
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
onRestorePress(payload) {
dispatch(restoreBackup(payload));
},
dispatchRestart() {
dispatch(restart());
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(RestoreBackupModalContent);

@ -0,0 +1,13 @@
import ModelBase from 'App/ModelBase';
export type BackupType = 'manual' | 'scheduled' | 'update';
interface Backup extends ModelBase {
name: string;
path: string;
type: BackupType;
size: number;
time: string;
}
export default Backup;
Loading…
Cancel
Save