Feature/show backup restore progress (#431)
* Extract "Progress" component * Show backup restore progress
This commit is contained in:
@@ -9,28 +9,11 @@
|
||||
import { useMemo } from 'react';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import { Box } from '@mui/material';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { makeToast } from '@/components/util/Toast';
|
||||
import { UpdaterSubscription } from '@/lib/graphql/generated/graphql.ts';
|
||||
|
||||
interface IProgressProps {
|
||||
progress: number;
|
||||
}
|
||||
|
||||
function Progress({ progress }: IProgressProps) {
|
||||
return (
|
||||
<Box sx={{ display: 'grid', placeItems: 'center', position: 'relative' }}>
|
||||
<CircularProgress variant="determinate" value={progress} />
|
||||
<Box sx={{ position: 'absolute' }}>
|
||||
<Typography fontSize="0.8rem">{`${Math.round(progress)}%`}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
import { Progress } from '@/components/util/Progress';
|
||||
|
||||
const calcProgress = (status: UpdaterSubscription['updateStatusChanged'] | undefined) => {
|
||||
if (!status) {
|
||||
|
||||
20
src/components/util/Progress.tsx
Normal file
20
src/components/util/Progress.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Box } from '@mui/material';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
export const Progress = ({ progress }: { progress: number }) => (
|
||||
<Box sx={{ display: 'grid', placeItems: 'center', position: 'relative' }}>
|
||||
<CircularProgress variant="determinate" value={progress} />
|
||||
<Box sx={{ position: 'absolute' }}>
|
||||
<Typography fontSize="0.8rem">{`${Math.round(progress)}%`}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
@@ -83,6 +83,8 @@ import {
|
||||
GetMangaQueryVariables,
|
||||
GetMangasQuery,
|
||||
GetMangasQueryVariables,
|
||||
GetRestoreStatusQuery,
|
||||
GetRestoreStatusQueryVariables,
|
||||
GetServerSettingsQuery,
|
||||
GetSourceMangasFetchMutation,
|
||||
GetSourceMangasFetchMutationVariables,
|
||||
@@ -199,7 +201,7 @@ import {
|
||||
import { GET_UPDATE_STATUS } from '@/lib/graphql/queries/UpdaterQuery.ts';
|
||||
import { CustomCache } from '@/lib/requests/CustomCache.ts';
|
||||
import { RESTORE_BACKUP } from '@/lib/graphql/mutations/BackupMutation.ts';
|
||||
import { VALIDATE_BACKUP } from '@/lib/graphql/queries/BackupQuery.ts';
|
||||
import { GET_RESTORE_STATUS, VALIDATE_BACKUP } from '@/lib/graphql/queries/BackupQuery.ts';
|
||||
import { DOWNLOAD_STATUS_SUBSCRIPTION } from '@/lib/graphql/subscriptions/DownloaderSubscription.ts';
|
||||
import { UPDATER_SUBSCRIPTION } from '@/lib/graphql/subscriptions/UpdaterSubscription.ts';
|
||||
import { GET_SERVER_SETTINGS } from '@/lib/graphql/queries/SettingsQuery.ts';
|
||||
@@ -1591,6 +1593,12 @@ export class RequestManager {
|
||||
return this.doRequest(GQLMethod.QUERY, VALIDATE_BACKUP, { backup: file }, options);
|
||||
}
|
||||
|
||||
public useGetBackupRestoreStatus(
|
||||
options?: QueryHookOptions<GetRestoreStatusQuery, GetRestoreStatusQueryVariables>,
|
||||
): AbortableApolloUseQueryResponse<GetRestoreStatusQuery, GetRestoreStatusQueryVariables> {
|
||||
return this.doRequest(GQLMethod.USE_QUERY, GET_RESTORE_STATUS, undefined, options);
|
||||
}
|
||||
|
||||
public getExportBackupUrl(): string {
|
||||
return this.getValidUrlFor('backup/export/file');
|
||||
}
|
||||
|
||||
@@ -6,16 +6,19 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { useContext, useEffect, useRef, useState } from 'react';
|
||||
import List from '@mui/material/List';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import { fromEvent } from 'file-selector';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ListItemButton } from '@mui/material';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { makeToast } from '@/components/util/Toast';
|
||||
import { ListItemLink } from '@/components/util/ListItemLink';
|
||||
import { NavBarContext, useSetDefaultBackTo } from '@/components/context/NavbarContext';
|
||||
import { BackupRestoreState, GetRestoreStatusQuery } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { Progress } from '@/components/util/Progress.tsx';
|
||||
|
||||
export function Backup() {
|
||||
const { t } = useTranslation();
|
||||
@@ -27,13 +30,57 @@ export function Backup() {
|
||||
|
||||
useSetDefaultBackTo('settings');
|
||||
|
||||
const submitBackup = (file: File) => {
|
||||
const [isRestoring, setIsRestoring] = useState<boolean | null>(null);
|
||||
const { data } = requestManager.useGetBackupRestoreStatus({
|
||||
skip: isRestoring !== null ? !isRestoring : false,
|
||||
pollInterval: 1000,
|
||||
});
|
||||
const prevRestoreStatusRef = useRef<GetRestoreStatusQuery['restoreStatus'] | null>(null);
|
||||
|
||||
const restoreProgress = (() => {
|
||||
if (!isRestoring || !data) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const progress = 100 * (data.restoreStatus.mangaProgress / data.restoreStatus.totalManga);
|
||||
return Number.isNaN(progress) ? 0 : progress;
|
||||
})();
|
||||
|
||||
useEffect(() => {
|
||||
if (!data || isRestoring !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isRestoreInProgress = data?.restoreStatus.state !== BackupRestoreState.Idle;
|
||||
setIsRestoring(isRestoreInProgress);
|
||||
}, [data?.restoreStatus.state]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isRestoreFinished =
|
||||
isRestoring &&
|
||||
data.restoreStatus.mangaProgress === data.restoreStatus.totalManga &&
|
||||
prevRestoreStatusRef.current?.state !== BackupRestoreState.Idle;
|
||||
if (isRestoreFinished) {
|
||||
setIsRestoring(false);
|
||||
makeToast(t('settings.backup.label.restored_backup'), 'success');
|
||||
}
|
||||
prevRestoreStatusRef.current = data.restoreStatus;
|
||||
}, [data?.restoreStatus.state]);
|
||||
|
||||
const submitBackup = async (file: File) => {
|
||||
if (file.name.toLowerCase().match(/proto\.gz$|tachibk$/g)) {
|
||||
makeToast(t('settings.backup.label.restoring_backup'), 'info');
|
||||
requestManager
|
||||
.restoreBackupFile(file)
|
||||
.response.then(() => makeToast(t('settings.backup.label.restored_backup'), 'success'))
|
||||
.catch(() => makeToast(t('settings.backup.label.backup_restore_failed'), 'error'));
|
||||
|
||||
try {
|
||||
await requestManager.restoreBackupFile(file).response;
|
||||
setIsRestoring(true);
|
||||
} catch (e) {
|
||||
makeToast(t('settings.backup.label.backup_restore_failed'), 'error');
|
||||
}
|
||||
} else if (file.name.toLowerCase().endsWith('json')) {
|
||||
makeToast(t('settings.backup.label.legacy_backup_unsupported'), 'error');
|
||||
} else {
|
||||
@@ -56,15 +103,18 @@ export function Backup() {
|
||||
document.addEventListener('drop', dropHandler);
|
||||
document.addEventListener('dragover', dragOverHandler);
|
||||
|
||||
const input = document.getElementById('backup-file');
|
||||
input?.addEventListener('change', async (evt) => {
|
||||
const files = await fromEvent(evt);
|
||||
const handleFileSelection = async (event: Event) => {
|
||||
const files = await fromEvent(event);
|
||||
submitBackup(files[0] as File);
|
||||
});
|
||||
};
|
||||
|
||||
const input = document.getElementById('backup-file');
|
||||
input?.addEventListener('change', handleFileSelection);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('drop', dropHandler);
|
||||
document.removeEventListener('dragover', dragOverHandler);
|
||||
input?.removeEventListener('change', handleFileSelection);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -77,11 +127,19 @@ export function Backup() {
|
||||
secondary={t('settings.backup.label.create_backup_info')}
|
||||
/>
|
||||
</ListItemLink>
|
||||
<ListItemButton onClick={() => document.getElementById('backup-file')?.click()}>
|
||||
<ListItemButton
|
||||
onClick={() => document.getElementById('backup-file')?.click()}
|
||||
disabled={!!isRestoring}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t('settings.backup.label.restore_backup')}
|
||||
secondary={t('settings.backup.label.restore_backup_info')}
|
||||
/>
|
||||
{isRestoring ? (
|
||||
<ListItemIcon>
|
||||
<Progress progress={restoreProgress} />
|
||||
</ListItemIcon>
|
||||
) : null}
|
||||
</ListItemButton>
|
||||
</List>
|
||||
<input type="file" id="backup-file" style={{ display: 'none' }} />
|
||||
|
||||
Reference in New Issue
Block a user