Feature/show backup restore progress (#431)

* Extract "Progress" component

* Show backup restore progress
This commit is contained in:
schroda
2023-10-29 01:49:27 +02:00
committed by GitHub
parent 30fd8b0fca
commit 4c6d507405
4 changed files with 99 additions and 30 deletions

View File

@@ -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) {

View 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>
);

View File

@@ -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');
}

View 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' }} />