mirror of
https://github.com/Sonarr/Sonarr
synced 2025-12-06 08:28:37 +01:00
Use react-query for backups
This commit is contained in:
parent
29170f17d2
commit
c295e24fc6
12 changed files with 183 additions and 228 deletions
|
|
@ -22,7 +22,6 @@ import ReleasesAppState from './ReleasesAppState';
|
|||
import RootFolderAppState from './RootFolderAppState';
|
||||
import SeriesAppState, { SeriesIndexAppState } from './SeriesAppState';
|
||||
import SettingsAppState from './SettingsAppState';
|
||||
import SystemAppState from './SystemAppState';
|
||||
import TagsAppState from './TagsAppState';
|
||||
import WantedAppState from './WantedAppState';
|
||||
|
||||
|
|
@ -103,7 +102,6 @@ interface AppState {
|
|||
seriesHistory: SeriesHistoryAppState;
|
||||
seriesIndex: SeriesIndexAppState;
|
||||
settings: SettingsAppState;
|
||||
system: SystemAppState;
|
||||
tags: TagsAppState;
|
||||
wanted: WantedAppState;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
import BackupAppState from './BackupAppState';
|
||||
|
||||
interface SystemAppState {
|
||||
backups: BackupAppState;
|
||||
}
|
||||
|
||||
export default SystemAppState;
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Icon from 'Components/Icon';
|
||||
import Menu from 'Components/Menu/Menu';
|
||||
import MenuButton from 'Components/Menu/MenuButton';
|
||||
|
|
@ -7,8 +6,8 @@ import MenuContent from 'Components/Menu/MenuContent';
|
|||
import MenuItem from 'Components/Menu/MenuItem';
|
||||
import MenuItemSeparator from 'Components/Menu/MenuItemSeparator';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import { restart, shutdown } from 'Store/Actions/systemActions';
|
||||
import { useSystemStatusData } from 'System/Status/useSystemStatus';
|
||||
import { useRestart, useShutdown } from 'System/useSystem';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './PageHeaderActionsMenu.css';
|
||||
|
||||
|
|
@ -19,18 +18,19 @@ interface PageHeaderActionsMenuProps {
|
|||
function PageHeaderActionsMenu(props: PageHeaderActionsMenuProps) {
|
||||
const { onKeyboardShortcutsPress } = props;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const { authentication, isDocker } = useSystemStatusData();
|
||||
const { mutate: restart } = useRestart();
|
||||
const { mutate: shutdown } = useShutdown();
|
||||
|
||||
const formsAuth = authentication === 'forms';
|
||||
|
||||
const handleRestartPress = useCallback(() => {
|
||||
dispatch(restart());
|
||||
}, [dispatch]);
|
||||
restart();
|
||||
}, [restart]);
|
||||
|
||||
const handleShutdownPress = useCallback(() => {
|
||||
dispatch(shutdown());
|
||||
}, [dispatch]);
|
||||
shutdown();
|
||||
}, [shutdown]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -16,10 +16,10 @@ import {
|
|||
saveGeneralSettings,
|
||||
setGeneralSettingsValue,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import { restart } from 'Store/Actions/systemActions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
|
||||
import { useIsWindowsService } from 'System/Status/useSystemStatus';
|
||||
import { useRestart } from 'System/useSystem';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AnalyticSettings from './AnalyticSettings';
|
||||
|
|
@ -46,6 +46,7 @@ const requiresRestartKeys = [
|
|||
function GeneralSettings() {
|
||||
const dispatch = useDispatch();
|
||||
const isWindowsService = useIsWindowsService();
|
||||
const { mutate: restart } = useRestart();
|
||||
const isResettingApiKey = useSelector(
|
||||
createCommandExecutingSelector(commandNames.RESET_API_KEY)
|
||||
);
|
||||
|
|
@ -85,8 +86,8 @@ function GeneralSettings() {
|
|||
|
||||
const handleConfirmRestart = useCallback(() => {
|
||||
setIsRestartRequiredModalOpen(false);
|
||||
dispatch(restart());
|
||||
}, [dispatch]);
|
||||
restart();
|
||||
}, [restart]);
|
||||
|
||||
const handleCloseRestartRequiredModalOpen = useCallback(() => {
|
||||
setIsRestartRequiredModalOpen(false);
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import * as series from './seriesActions';
|
|||
import * as seriesHistory from './seriesHistoryActions';
|
||||
import * as seriesIndex from './seriesIndexActions';
|
||||
import * as settings from './settingsActions';
|
||||
import * as system from './systemActions';
|
||||
import * as tags from './tagActions';
|
||||
import * as wanted from './wantedActions';
|
||||
|
||||
|
|
@ -47,7 +46,6 @@ export default [
|
|||
seriesHistory,
|
||||
seriesIndex,
|
||||
settings,
|
||||
system,
|
||||
tags,
|
||||
wanted
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,160 +0,0 @@
|
|||
import { createAction } from 'redux-actions';
|
||||
import { setAppValue } from 'Store/Actions/appActions';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import { pingServer } from './appActions';
|
||||
import { set } from './baseActions';
|
||||
import createFetchHandler from './Creators/createFetchHandler';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import createRemoveItemHandler from './Creators/createRemoveItemHandler';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
export const section = 'system';
|
||||
const backupsSection = 'system.backups';
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
export const defaultState = {
|
||||
backups: {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
isRestoring: false,
|
||||
restoreError: null,
|
||||
isDeleting: false,
|
||||
deleteError: null,
|
||||
items: []
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_BACKUPS = 'system/backups/fetchBackups';
|
||||
export const RESTORE_BACKUP = 'system/backups/restoreBackup';
|
||||
export const CLEAR_RESTORE_BACKUP = 'system/backups/clearRestoreBackup';
|
||||
export const DELETE_BACKUP = 'system/backups/deleteBackup';
|
||||
|
||||
export const RESTART = 'system/restart';
|
||||
export const SHUTDOWN = 'system/shutdown';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchBackups = createThunk(FETCH_BACKUPS);
|
||||
export const restoreBackup = createThunk(RESTORE_BACKUP);
|
||||
export const clearRestoreBackup = createAction(CLEAR_RESTORE_BACKUP);
|
||||
export const deleteBackup = createThunk(DELETE_BACKUP);
|
||||
|
||||
export const restart = createThunk(RESTART);
|
||||
export const shutdown = createThunk(SHUTDOWN);
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
[FETCH_BACKUPS]: createFetchHandler(backupsSection, '/system/backup'),
|
||||
|
||||
[RESTORE_BACKUP]: function(getState, payload, dispatch) {
|
||||
const {
|
||||
id,
|
||||
file
|
||||
} = payload;
|
||||
|
||||
dispatch(set({
|
||||
section: backupsSection,
|
||||
isRestoring: true
|
||||
}));
|
||||
|
||||
let ajaxOptions = null;
|
||||
|
||||
if (id) {
|
||||
ajaxOptions = {
|
||||
url: `/system/backup/restore/${id}`,
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
data: JSON.stringify({
|
||||
id
|
||||
})
|
||||
};
|
||||
} else if (file) {
|
||||
const formData = new FormData();
|
||||
formData.append('restore', file);
|
||||
|
||||
ajaxOptions = {
|
||||
url: '/system/backup/restore/upload',
|
||||
method: 'POST',
|
||||
processData: false,
|
||||
contentType: false,
|
||||
data: formData
|
||||
};
|
||||
} else {
|
||||
dispatch(set({
|
||||
section: backupsSection,
|
||||
isRestoring: false,
|
||||
restoreError: 'Error restoring backup'
|
||||
}));
|
||||
}
|
||||
|
||||
const promise = createAjaxRequest(ajaxOptions).request;
|
||||
|
||||
promise.done((data) => {
|
||||
dispatch(set({
|
||||
section: backupsSection,
|
||||
isRestoring: false,
|
||||
restoreError: null
|
||||
}));
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
dispatch(set({
|
||||
section: backupsSection,
|
||||
isRestoring: false,
|
||||
restoreError: xhr
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
[DELETE_BACKUP]: createRemoveItemHandler(backupsSection, '/system/backup'),
|
||||
|
||||
[RESTART]: function(getState, payload, dispatch) {
|
||||
const promise = createAjaxRequest({
|
||||
url: '/system/restart',
|
||||
method: 'POST'
|
||||
}).request;
|
||||
|
||||
promise.done(() => {
|
||||
dispatch(setAppValue({ isRestarting: true }));
|
||||
dispatch(pingServer());
|
||||
});
|
||||
},
|
||||
|
||||
[SHUTDOWN]: function() {
|
||||
createAjaxRequest({
|
||||
url: '/system/shutdown',
|
||||
method: 'POST'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
export const reducers = createHandleActions({
|
||||
|
||||
[CLEAR_RESTORE_BACKUP]: function(state, { payload }) {
|
||||
return {
|
||||
...state,
|
||||
backups: {
|
||||
...state.backups,
|
||||
isRestoring: false,
|
||||
restoreError: null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}, defaultState, section);
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
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';
|
||||
|
|
@ -8,11 +7,11 @@ 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 { useDeleteBackup } from './useBackups';
|
||||
import styles from './BackupRow.css';
|
||||
|
||||
interface BackupRowProps {
|
||||
|
|
@ -25,7 +24,7 @@ interface BackupRowProps {
|
|||
}
|
||||
|
||||
function BackupRow({ id, type, name, path, size, time }: BackupRowProps) {
|
||||
const dispatch = useDispatch();
|
||||
const deleteBackupMutation = useDeleteBackup(id);
|
||||
const [isRestoreModalOpen, setIsRestoreModalOpen] = useState(false);
|
||||
const [isConfirmDeleteModalOpen, setIsConfirmDeleteModalOpen] =
|
||||
useState(false);
|
||||
|
|
@ -68,9 +67,15 @@ function BackupRow({ id, type, name, path, size, time }: BackupRowProps) {
|
|||
}, []);
|
||||
|
||||
const handleConfirmDeletePress = useCallback(() => {
|
||||
dispatch(deleteBackup({ id }));
|
||||
setIsConfirmDeleteModalOpen(false);
|
||||
}, [id, dispatch]);
|
||||
deleteBackupMutation.mutate(undefined, {
|
||||
onSuccess: () => {
|
||||
setIsConfirmDeleteModalOpen(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Failed to delete backup:', error);
|
||||
},
|
||||
});
|
||||
}, [deleteBackupMutation]);
|
||||
|
||||
return (
|
||||
<TableRow key={id}>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
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';
|
||||
|
|
@ -15,11 +14,11 @@ 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';
|
||||
import useBackups from './useBackups';
|
||||
|
||||
const columns: Column[] = [
|
||||
{
|
||||
|
|
@ -51,10 +50,7 @@ const columns: Column[] = [
|
|||
|
||||
function Backups() {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { isFetching, isPopulated, error, items } = useSelector(
|
||||
(state: AppState) => state.system.backups
|
||||
);
|
||||
const { data: items, isLoading: isFetching, error, refetch } = useBackups();
|
||||
|
||||
const isBackupExecuting = useSelector(
|
||||
createCommandExecutingSelector(commandNames.BACKUP)
|
||||
|
|
@ -63,8 +59,8 @@ function Backups() {
|
|||
const [isRestoreModalOpen, setIsRestoreModalOpen] = useState(false);
|
||||
|
||||
const wasBackupExecuting = usePrevious(isBackupExecuting);
|
||||
const hasBackups = isPopulated && !!items.length;
|
||||
const noBackups = isPopulated && !items.length;
|
||||
const hasBackups = !!items.length;
|
||||
const noBackups = !items.length && !isFetching && !error;
|
||||
|
||||
const handleBackupPress = useCallback(() => {
|
||||
dispatch(
|
||||
|
|
@ -82,15 +78,11 @@ function Backups() {
|
|||
setIsRestoreModalOpen(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchBackups());
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (wasBackupExecuting && !isBackupExecuting) {
|
||||
dispatch(fetchBackups());
|
||||
refetch();
|
||||
}
|
||||
}, [isBackupExecuting, wasBackupExecuting, dispatch]);
|
||||
}, [isBackupExecuting, wasBackupExecuting, refetch]);
|
||||
|
||||
return (
|
||||
<PageContent title={translate('Backups')}>
|
||||
|
|
@ -112,7 +104,7 @@ function Backups() {
|
|||
</PageToolbar>
|
||||
|
||||
<PageContentBody>
|
||||
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && !!error ? (
|
||||
<Alert kind={kinds.DANGER}>{translate('BackupsLoadError')}</Alert>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { clearRestoreBackup } from 'Store/Actions/systemActions';
|
||||
import RestoreBackupModalContent, {
|
||||
RestoreBackupModalContentProps,
|
||||
} from './RestoreBackupModalContent';
|
||||
|
|
@ -16,19 +14,9 @@ function RestoreBackupModal({
|
|||
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 isOpen={isOpen} onModalClose={onModalClose}>
|
||||
<RestoreBackupModalContent {...otherProps} onModalClose={onModalClose} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,10 @@ 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 { useRestart } from 'System/useSystem';
|
||||
import { FileInputChanged } from 'typings/inputs';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import { useRestoreBackup, useRestoreBackupUpload } from './useBackups';
|
||||
import styles from './RestoreBackupModalContent.css';
|
||||
|
||||
function getErrorMessage(error: Error) {
|
||||
|
|
@ -78,18 +79,29 @@ function RestoreBackupModalContent({
|
|||
name,
|
||||
onModalClose,
|
||||
}: RestoreBackupModalContentProps) {
|
||||
const { isRestoring, restoreError } = useSelector(
|
||||
(state: AppState) => state.system.backups
|
||||
);
|
||||
|
||||
const { isRestarting } = useSelector((state: AppState) => state.app);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {
|
||||
mutate: restoreBackupById,
|
||||
isPending: isRestoreBackupPending,
|
||||
error: restoreBackupError,
|
||||
} = useRestoreBackup(id || 0);
|
||||
const {
|
||||
mutate: uploadBackupFile,
|
||||
isPending: isUploadBackupPending,
|
||||
error: uploadBackupError,
|
||||
} = useRestoreBackupUpload();
|
||||
const { mutate: restart } = useRestart();
|
||||
|
||||
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 isRestoring = isRestoreBackupPending || isUploadBackupPending;
|
||||
const restoreError = restoreBackupError || uploadBackupError;
|
||||
const wasRestoring = usePrevious(isRestoring);
|
||||
const wasRestarting = usePrevious(isRestarting);
|
||||
|
||||
|
|
@ -106,15 +118,21 @@ function RestoreBackupModalContent({
|
|||
}, []);
|
||||
|
||||
const handleRestorePress = useCallback(() => {
|
||||
dispatch(restoreBackup({ id, file }));
|
||||
}, [id, file, dispatch]);
|
||||
if (id) {
|
||||
restoreBackupById();
|
||||
} else if (file) {
|
||||
const formData = new FormData();
|
||||
formData.append('restore', file);
|
||||
uploadBackupFile(formData);
|
||||
}
|
||||
}, [id, file, restoreBackupById, uploadBackupFile]);
|
||||
|
||||
useEffect(() => {
|
||||
if (wasRestoring && !isRestoring && !restoreError) {
|
||||
setIsRestored(true);
|
||||
dispatch(restart());
|
||||
restart();
|
||||
}
|
||||
}, [isRestoring, wasRestoring, restoreError, dispatch]);
|
||||
}, [isRestoring, wasRestoring, restoreError, restart]);
|
||||
|
||||
useEffect(() => {
|
||||
if (wasRestarting && !isRestarting) {
|
||||
|
|
@ -147,7 +165,7 @@ function RestoreBackupModalContent({
|
|||
<div className={styles.stepState}>
|
||||
<Icon
|
||||
size={20}
|
||||
{...getStepIconProps(isRestoring, isRestored, restoreError)}
|
||||
{...getStepIconProps(isRestoring, isRestored, undefined)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
82
frontend/src/System/Backup/useBackups.ts
Normal file
82
frontend/src/System/Backup/useBackups.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import useApiMutation from 'Helpers/Hooks/useApiMutation';
|
||||
import useApiQuery from 'Helpers/Hooks/useApiQuery';
|
||||
import Backup from 'typings/Backup';
|
||||
|
||||
const useBackups = () => {
|
||||
const result = useApiQuery<Backup[]>({
|
||||
path: '/system/backup',
|
||||
queryOptions: {
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
data: result.data ?? [],
|
||||
};
|
||||
};
|
||||
|
||||
export default useBackups;
|
||||
|
||||
export const useDeleteBackup = (id: number) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useApiMutation<object, void>({
|
||||
path: `/system/backup/${id}`,
|
||||
method: 'DELETE',
|
||||
mutationOptions: {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/system/backup'] });
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
interface RestoreBackupResponse {
|
||||
restartRequired: boolean;
|
||||
}
|
||||
|
||||
export const useRestoreBackup = (id: number) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useApiMutation<RestoreBackupResponse, void>({
|
||||
path: `/system/backup/restore/${id}`,
|
||||
method: 'POST',
|
||||
mutationOptions: {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/system/backup'] });
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useRestoreBackupUpload = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<RestoreBackupResponse, Error, FormData>({
|
||||
mutationFn: async (formData: FormData) => {
|
||||
const response = await fetch(
|
||||
`${window.Sonarr.urlBase}/api/v5/system/backup/restore/upload`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Api-Key': window.Sonarr.apiKey,
|
||||
'X-Sonarr-Client': 'Sonarr',
|
||||
// Don't set Content-Type, let browser set it with boundary for multipart/form-data
|
||||
},
|
||||
body: formData,
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to restore backup: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/system/backup'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
40
frontend/src/System/useSystem.ts
Normal file
40
frontend/src/System/useSystem.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { pingServer, setAppValue } from 'Store/Actions/appActions';
|
||||
|
||||
const createSystemMutationFn = (endpoint: string) => {
|
||||
return async () => {
|
||||
const response = await fetch(
|
||||
`${window.Sonarr.urlBase}/system/${endpoint}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Api-Key': window.Sonarr.apiKey,
|
||||
'X-Sonarr-Client': 'Sonarr',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to ${endpoint}: ${response.statusText}`);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const useRestart = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
return useMutation<void, Error, void>({
|
||||
mutationFn: createSystemMutationFn('restart'),
|
||||
onSuccess: () => {
|
||||
dispatch(setAppValue({ isRestarting: true }));
|
||||
dispatch(pingServer());
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useShutdown = () => {
|
||||
return useMutation<void, Error, void>({
|
||||
mutationFn: createSystemMutationFn('shutdown'),
|
||||
});
|
||||
};
|
||||
Loading…
Reference in a new issue