Merge pull request #6917 from viown/backup-feature
Add backup dashboard page
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import Article from '@mui/icons-material/Article';
|
||||
import Backup from '@mui/icons-material/Backup';
|
||||
import Lan from '@mui/icons-material/Lan';
|
||||
import Schedule from '@mui/icons-material/Schedule';
|
||||
import VpnKey from '@mui/icons-material/VpnKey';
|
||||
@@ -38,6 +39,14 @@ const AdvancedDrawerSection = () => {
|
||||
<ListItemText primary={globalize.translate('HeaderApiKeys')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard/backups'>
|
||||
<ListItemIcon>
|
||||
<Backup />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('HeaderBackups')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard/logs'>
|
||||
<ListItemIcon>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Api } from '@jellyfin/sdk';
|
||||
import { BackupApi } from '@jellyfin/sdk/lib/generated-client/api/backup-api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
|
||||
export const QUERY_KEY = 'Backups';
|
||||
|
||||
const fetchBackups = async (api: Api, options?: AxiosRequestConfig) => {
|
||||
// FIXME: Replace with getBackupApi when available in SDK
|
||||
const backupApi = new BackupApi(api.configuration, undefined, api.axiosInstance);
|
||||
|
||||
const response = await backupApi.listBackups(options);
|
||||
|
||||
const backups = response.data;
|
||||
|
||||
backups.sort((a, b) => {
|
||||
if (a.DateCreated && b.DateCreated) {
|
||||
return new Date(b.DateCreated).getTime() - new Date(a.DateCreated).getTime();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
return backups;
|
||||
};
|
||||
|
||||
export const useBackups = () => {
|
||||
const { api } = useApi();
|
||||
|
||||
return useQuery({
|
||||
queryKey: [ QUERY_KEY ],
|
||||
queryFn: ({ signal }) =>
|
||||
fetchBackups(api!, { signal }),
|
||||
enabled: !!api
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { BackupOptionsDto } from '@jellyfin/sdk/lib/generated-client/models/backup-options-dto';
|
||||
import { BackupApi } from '@jellyfin/sdk/lib/generated-client/api/backup-api';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { QUERY_KEY } from './useBackups';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
|
||||
export const useCreateBackup = () => {
|
||||
const { api } = useApi();
|
||||
// FIXME: Replace with getBackupApi when available in SDK
|
||||
const backupApi = new BackupApi(api?.configuration, undefined, api?.axiosInstance);
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (backupOptions: BackupOptionsDto) => (
|
||||
backupApi.createBackup({
|
||||
backupOptionsDto: backupOptions
|
||||
})
|
||||
),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ QUERY_KEY ]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { BackupApi } from '@jellyfin/sdk/lib/generated-client/api/backup-api';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
|
||||
export const useRestoreBackup = () => {
|
||||
const { api } = useApi();
|
||||
// FIXME: Replace with getBackupApi when available in SDK
|
||||
const backupApi = new BackupApi(api?.configuration, undefined, api?.axiosInstance);
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (fileName: string) => (
|
||||
backupApi.startRestoreBackup({
|
||||
backupRestoreRequestDto: {
|
||||
ArchiveFileName: fileName
|
||||
}
|
||||
})
|
||||
)
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import React, { FunctionComponent, useCallback, useState } from 'react';
|
||||
import type { BackupManifestDto } from '@jellyfin/sdk/lib/generated-client/models/backup-manifest-dto';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import Restore from '@mui/icons-material/Restore';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import globalize from 'lib/globalize';
|
||||
import BackupInfoDialog from './BackupInfoDialog';
|
||||
|
||||
type BackupProps = {
|
||||
backup: BackupManifestDto;
|
||||
onRestore: (backup: BackupManifestDto) => void;
|
||||
};
|
||||
|
||||
const Backup: FunctionComponent<BackupProps> = ({ backup, onRestore }) => {
|
||||
const [ isInfoDialogOpen, setIsInfoDialogOpen ] = useState(false);
|
||||
|
||||
const onDialogClose = useCallback(() => {
|
||||
setIsInfoDialogOpen(false);
|
||||
}, []);
|
||||
|
||||
const openDialog = useCallback(() => {
|
||||
setIsInfoDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
const restore = useCallback(() => {
|
||||
onRestore(backup);
|
||||
}, [ backup, onRestore ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<BackupInfoDialog
|
||||
backup={backup}
|
||||
onClose={onDialogClose}
|
||||
open={isInfoDialogOpen}
|
||||
/>
|
||||
<ListItem
|
||||
disablePadding
|
||||
secondaryAction={
|
||||
<Tooltip disableInteractive title={globalize.translate('LabelRestore')}>
|
||||
<IconButton onClick={restore}>
|
||||
<Restore />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<ListItemButton onClick={openDialog}>
|
||||
<ListItemText
|
||||
primary={backup.DateCreated}
|
||||
secondary={backup.Path}
|
||||
slotProps={{
|
||||
primary: {
|
||||
variant: 'h3'
|
||||
},
|
||||
secondary: {
|
||||
variant: 'body1'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Backup;
|
||||
@@ -0,0 +1,135 @@
|
||||
import type { BackupManifestDto } from '@jellyfin/sdk/lib/generated-client/models/backup-manifest-dto';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Button from '@mui/material/Button';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import Box from '@mui/material/Box';
|
||||
import globalize from 'lib/globalize';
|
||||
import React, { FunctionComponent, useCallback } from 'react';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import FormGroup from '@mui/material/FormGroup';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
import ContentCopy from '@mui/icons-material/ContentCopy';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { copy } from 'scripts/clipboard';
|
||||
import toast from 'components/toast/toast';
|
||||
|
||||
type IProps = {
|
||||
backup: BackupManifestDto;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const BackupInfoDialog: FunctionComponent<IProps> = ({ backup, open, onClose }: IProps) => {
|
||||
const copyPath = useCallback(async () => {
|
||||
if (backup.Path) {
|
||||
await copy(backup.Path);
|
||||
toast({ text: globalize.translate('Copied') });
|
||||
}
|
||||
}, [ backup.Path ]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
onClose={onClose}
|
||||
open={open}
|
||||
maxWidth={'sm'}
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>
|
||||
{backup.DateCreated}
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<Stack gap={2}>
|
||||
<Box>
|
||||
<Stack
|
||||
direction='row'
|
||||
gap={2}
|
||||
>
|
||||
<Typography fontWeight='bold'>{globalize.translate('LabelPath')}</Typography>
|
||||
<Stack direction='row'>
|
||||
<Typography color='text.secondary'>{backup.Path}</Typography>
|
||||
<IconButton size='small' onClick={copyPath}>
|
||||
<ContentCopy fontSize='small' />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction='row'
|
||||
gap={2}
|
||||
>
|
||||
<Typography fontWeight='bold'>{globalize.translate('LabelVersion')}</Typography>
|
||||
<Typography color='text.secondary'>{backup.ServerVersion}</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<FormGroup>
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name='Database'
|
||||
defaultChecked={true}
|
||||
disabled
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('LabelDatabase')}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name='Metadata'
|
||||
defaultChecked={backup.Options?.Metadata}
|
||||
disabled
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('LabelMetadata')}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name='Subtitles'
|
||||
defaultChecked={backup.Options?.Subtitles}
|
||||
disabled
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('Subtitles')}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name='Trickplay'
|
||||
defaultChecked={backup.Options?.Trickplay}
|
||||
disabled
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('Trickplay')}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>
|
||||
{globalize.translate('ButtonOk')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default BackupInfoDialog;
|
||||
@@ -0,0 +1,27 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import globalize from 'lib/globalize';
|
||||
|
||||
type IProps = {
|
||||
open: boolean
|
||||
};
|
||||
|
||||
const BackupProgressDialog: FunctionComponent<IProps> = ({ open }) => {
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
maxWidth={'xs'}
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>{globalize.translate('MessageBackupInProgress')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<LinearProgress />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default BackupProgressDialog;
|
||||
@@ -0,0 +1,123 @@
|
||||
import React, { FunctionComponent, useCallback } from 'react';
|
||||
import globalize from 'lib/globalize';
|
||||
import type { BackupOptionsDto } from '@jellyfin/sdk/lib/generated-client/models/backup-options-dto';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import Button from '@mui/material/Button';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
import FormGroup from '@mui/material/FormGroup';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
|
||||
type IProps = {
|
||||
open: boolean,
|
||||
onClose?: () => void,
|
||||
onCreate: (backupOptions: BackupOptionsDto) => void
|
||||
};
|
||||
|
||||
const CreateBackupForm: FunctionComponent<IProps> = ({ open, onClose, onCreate }) => {
|
||||
const onSubmit = useCallback((e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.currentTarget);
|
||||
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
|
||||
const backupOptions: BackupOptionsDto = {
|
||||
'Metadata': data.Metadata?.toString() === 'on',
|
||||
'Trickplay': data.Trickplay?.toString() === 'on',
|
||||
'Subtitles': data.Subtitles?.toString() === 'on'
|
||||
};
|
||||
|
||||
onCreate(backupOptions);
|
||||
}, [ onCreate ]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
maxWidth={'xs'}
|
||||
fullWidth
|
||||
onClose={onClose}
|
||||
slotProps={{
|
||||
paper: {
|
||||
component: 'form',
|
||||
onSubmit
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTitle>{globalize.translate('ButtonCreateBackup')}</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<Stack spacing={2}>
|
||||
<DialogContentText>
|
||||
{globalize.translate('MessageBackupDisclaimer')}
|
||||
</DialogContentText>
|
||||
<FormGroup>
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name='Database'
|
||||
defaultChecked={true}
|
||||
disabled
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('LabelDatabase')}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name='Metadata'
|
||||
defaultChecked={false}
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('LabelMetadata')}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name='Subtitles'
|
||||
defaultChecked={false}
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('Subtitles')}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name='Trickplay'
|
||||
defaultChecked={false}
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('Trickplay')}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
variant='text'
|
||||
>{globalize.translate('ButtonCancel')}</Button>
|
||||
<Button type='submit'>{globalize.translate('Create')}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateBackupForm;
|
||||
@@ -0,0 +1,46 @@
|
||||
import Button from '@mui/material/Button';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import globalize from 'lib/globalize';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
|
||||
type IProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
};
|
||||
|
||||
const RestoreConfirmationDialog: FunctionComponent<IProps> = ({ open, onClose, onConfirm }: IProps) => {
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
maxWidth={'xs'}
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>
|
||||
{globalize.translate('LabelRestore')}
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{globalize.translate('MessageRestoreDisclaimer')}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} color='error'>
|
||||
{globalize.translate('ButtonCancel')}
|
||||
</Button>
|
||||
<Button onClick={onConfirm}>
|
||||
{globalize.translate('LabelRestore')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default RestoreConfirmationDialog;
|
||||
@@ -0,0 +1,32 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import globalize from 'lib/globalize';
|
||||
|
||||
type IProps = {
|
||||
open: boolean
|
||||
};
|
||||
|
||||
const RestoreProgressDialog: FunctionComponent<IProps> = ({ open }) => {
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
maxWidth={'xs'}
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>{globalize.translate('MessageRestoreInProgress')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Stack spacing={2}>
|
||||
<DialogContentText>{globalize.translate('MessageWaitingForServer')}</DialogContentText>
|
||||
<LinearProgress />
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default RestoreProgressDialog;
|
||||
@@ -3,6 +3,7 @@ import { AppType } from 'constants/appType';
|
||||
|
||||
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
|
||||
{ path: 'activity', type: AppType.Dashboard },
|
||||
{ path: 'backups', type: AppType.Dashboard },
|
||||
{ path: 'branding', type: AppType.Dashboard },
|
||||
{ path: 'devices', type: AppType.Dashboard },
|
||||
{ path: 'settings', type: AppType.Dashboard },
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import { useBackups } from 'apps/dashboard/features/backups/api/useBackups';
|
||||
import Page from 'components/Page';
|
||||
import globalize from 'lib/globalize';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import Loading from 'components/loading/LoadingComponent';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import List from '@mui/material/List';
|
||||
import CreateBackupForm from 'apps/dashboard/features/backups/components/CreateBackupForm';
|
||||
import type { BackupOptionsDto } from '@jellyfin/sdk/lib/generated-client/models/backup-options-dto';
|
||||
import type { BackupManifestDto } from '@jellyfin/sdk/lib/generated-client/models/backup-manifest-dto';
|
||||
import { useCreateBackup } from 'apps/dashboard/features/backups/api/useCreateBackup';
|
||||
import BackupProgressDialog from 'apps/dashboard/features/backups/components/BackupProgressDialog';
|
||||
import Backup from 'apps/dashboard/features/backups/components/Backup';
|
||||
import SimpleAlert from 'components/SimpleAlert';
|
||||
import RestoreConfirmationDialog from 'apps/dashboard/features/backups/components/RestoreConfirmationDialog';
|
||||
import { useRestoreBackup } from 'apps/dashboard/features/backups/api/useRestoreBackup';
|
||||
import RestoreProgressDialog from 'apps/dashboard/features/backups/components/RestoreProgressDialog';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
|
||||
|
||||
export const Component = () => {
|
||||
const { api } = useApi();
|
||||
const { data: backups, isPending, isError } = useBackups();
|
||||
const [ isCreateFormOpen, setIsCreateFormOpen ] = useState(false);
|
||||
const [ backupInProgress, setBackupInProgress ] = useState(false);
|
||||
const [ restoreInProgress, setRestoreInProgress ] = useState(false);
|
||||
const [ isRestoreSuccess, setIsRestoreSuccess ] = useState(false);
|
||||
const [ isErrorOccurred, setIsErrorOccurred ] = useState(false);
|
||||
const [ isRestoreDialogOpen, setIsRestoreDialogOpen ] = useState(false);
|
||||
const [ backupToRestore, setBackupToRestore ] = useState<BackupManifestDto | null>(null);
|
||||
const createBackup = useCreateBackup();
|
||||
const restoreBackup = useRestoreBackup();
|
||||
|
||||
const onCreateClick = useCallback(() => {
|
||||
setIsCreateFormOpen(true);
|
||||
}, []);
|
||||
|
||||
const onCreateFormClose = useCallback(() => {
|
||||
setIsCreateFormOpen(false);
|
||||
}, []);
|
||||
|
||||
const onRestoreDialogClose = useCallback(() => {
|
||||
setIsRestoreDialogOpen(false);
|
||||
}, []);
|
||||
|
||||
const onErrorAlertClose = useCallback(() => {
|
||||
setIsErrorOccurred(false);
|
||||
}, []);
|
||||
|
||||
const onRestoreSuccessAlertClose = useCallback(() => {
|
||||
setIsRestoreSuccess(false);
|
||||
}, []);
|
||||
|
||||
const onBackupCreate = useCallback((backupOptions: BackupOptionsDto) => {
|
||||
setBackupInProgress(true);
|
||||
setIsCreateFormOpen(false);
|
||||
createBackup.mutate(backupOptions, {
|
||||
onError: () => {
|
||||
setIsErrorOccurred(true);
|
||||
},
|
||||
onSettled: () => {
|
||||
setBackupInProgress(false);
|
||||
}
|
||||
});
|
||||
}, [ createBackup ]);
|
||||
|
||||
const promptRestore = useCallback((backup: BackupManifestDto) => {
|
||||
setIsRestoreDialogOpen(true);
|
||||
setBackupToRestore(backup);
|
||||
}, []);
|
||||
|
||||
const onRestoreConfirm = useCallback(() => {
|
||||
if (backupToRestore?.Path) {
|
||||
restoreBackup.mutate(backupToRestore?.Path, {
|
||||
onSuccess: () => {
|
||||
setRestoreInProgress(true);
|
||||
},
|
||||
onError: () => {
|
||||
setIsErrorOccurred(true);
|
||||
},
|
||||
onSettled: () => {
|
||||
setIsRestoreDialogOpen(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [backupToRestore, restoreBackup]);
|
||||
|
||||
useEffect(() => {
|
||||
if (restoreInProgress) {
|
||||
const serverCheckInterval = setInterval(() => {
|
||||
void getSystemApi(api!)
|
||||
.getPublicSystemInfo()
|
||||
.then(() => {
|
||||
setRestoreInProgress(false);
|
||||
setIsRestoreSuccess(true);
|
||||
clearInterval(serverCheckInterval);
|
||||
}).catch(() => {
|
||||
// Server is still down
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
return () => {
|
||||
clearInterval(serverCheckInterval);
|
||||
};
|
||||
}
|
||||
}, [api, restoreInProgress]);
|
||||
|
||||
if (isPending) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page
|
||||
id='backupsPage'
|
||||
title={globalize.translate('HeaderBackups')}
|
||||
className='mainAnimatedPage type-interior'
|
||||
>
|
||||
<BackupProgressDialog open={backupInProgress} />
|
||||
<RestoreProgressDialog open={restoreInProgress} />
|
||||
<CreateBackupForm
|
||||
open={isCreateFormOpen}
|
||||
onClose={onCreateFormClose}
|
||||
onCreate={onBackupCreate}
|
||||
/>
|
||||
<SimpleAlert
|
||||
open={isErrorOccurred}
|
||||
text={globalize.translate('UnknownError')}
|
||||
onClose={onErrorAlertClose}
|
||||
/>
|
||||
<SimpleAlert
|
||||
open={isRestoreSuccess}
|
||||
title={globalize.translate('Success')}
|
||||
text={globalize.translate('MessageRestoreSuccess')}
|
||||
onClose={onRestoreSuccessAlertClose}
|
||||
/>
|
||||
<RestoreConfirmationDialog
|
||||
open={isRestoreDialogOpen}
|
||||
onClose={onRestoreDialogClose}
|
||||
onConfirm={onRestoreConfirm}
|
||||
/>
|
||||
<Box className='content-primary'>
|
||||
{isError ? (
|
||||
<Alert severity='error'>{globalize.translate('BackupsPageLoadError')}</Alert>
|
||||
) : (
|
||||
<Stack spacing={3}>
|
||||
<Typography variant='h1'>
|
||||
{globalize.translate('HeaderBackups')}
|
||||
</Typography>
|
||||
<Typography>
|
||||
{globalize.translate('HeaderBackupsHelp')}
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
sx={{ alignSelf: 'flex-start' }}
|
||||
startIcon={<AddIcon />}
|
||||
onClick={onCreateClick}
|
||||
>
|
||||
{globalize.translate('ButtonCreateBackup')}
|
||||
</Button>
|
||||
|
||||
<Box className='readOnlyContent'>
|
||||
{backups.length > 0 && (
|
||||
<List sx={{ bgcolor: 'background.paper' }}>
|
||||
{backups.map(backup => {
|
||||
return <Backup
|
||||
key={backup.Path}
|
||||
backup={backup}
|
||||
onRestore={promptRestore}
|
||||
/>;
|
||||
})}
|
||||
</List>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -76,6 +76,7 @@
|
||||
"Backdrop": "Backdrop",
|
||||
"Backdrops": "Backdrops",
|
||||
"BackdropScreensaver": "Backdrop Screensaver",
|
||||
"BackupsPageLoadError": "Failed to load backups page",
|
||||
"Banner": "Banner",
|
||||
"BirthDateValue": "Born: {0}",
|
||||
"BirthLocation": "Birth location",
|
||||
@@ -104,6 +105,7 @@
|
||||
"ButtonCast": "Cast to Device",
|
||||
"ButtonChangeServer": "Change Server",
|
||||
"ButtonClose": "Close",
|
||||
"ButtonCreateBackup": "Create Backup",
|
||||
"ButtonEditOtherUserPreferences": "Edit this user's profile, image and personal preferences.",
|
||||
"ButtonEditUser": "Edit user",
|
||||
"ButtonForgotPassword": "Forgot Password",
|
||||
@@ -190,6 +192,7 @@
|
||||
"CopyStreamURLSuccess": "URL copied successfully.",
|
||||
"CopyLogSuccess": "Log contents copied successfully.",
|
||||
"CoverArtist": "Cover artist",
|
||||
"Create": "Create",
|
||||
"Creator": "Creator",
|
||||
"CriticRating": "Critics rating",
|
||||
"Cursive": "Cursive",
|
||||
@@ -388,6 +391,7 @@
|
||||
"HeaderAudioSettings": "Audio Settings",
|
||||
"HeaderAutoDiscovery": "Network Discovery",
|
||||
"HeaderBlockItemsWithNoRating": "Block items with no or unrecognized rating information",
|
||||
"HeaderBackupsHelp": "Create and restore snapshots of your server which can include the database, metadata, subtitles and trickplay.",
|
||||
"HeaderBranding": "Branding",
|
||||
"HeaderCancelRecording": "Cancel Recording",
|
||||
"HeaderCancelSeries": "Cancel Series",
|
||||
@@ -617,6 +621,7 @@
|
||||
"LabelAutomaticallyRefreshInternetMetadataEvery": "Automatically refresh metadata from the internet",
|
||||
"LabelAutomaticDiscovery": "Enable Auto Discovery",
|
||||
"LabelAutomaticDiscoveryHelp": "Allow applications to automatically detect Jellyfin by using UDP port 7359.",
|
||||
"LabelBackupsUnavailable": "No backups available",
|
||||
"LabelBaseUrl": "Base URL",
|
||||
"LabelBaseUrlHelp": "Add a custom subdirectory to the server URL. For example: <code>http://example.com/<b><baseurl></b></code>",
|
||||
"LabelBackdropScreensaverInterval": "Backdrop Screensaver Interval",
|
||||
@@ -661,6 +666,7 @@
|
||||
"LabelCustomTagDelimiters": "Custom Tag Delimiter",
|
||||
"LabelCustomTagDelimitersHelp": "Characters to be treated as delimiters to separate tags.",
|
||||
"LabelDashboardTheme": "Server Dashboard theme",
|
||||
"LabelDatabase": "Database",
|
||||
"LabelDate": "Date",
|
||||
"LabelDateAdded": "Date added",
|
||||
"LabelDateAddedBehavior": "Date added behavior for new content",
|
||||
@@ -881,6 +887,7 @@
|
||||
"LabelRepositoryUrlHelp": "The location of the repository manifest you want to include.",
|
||||
"LabelRequireHttps": "Require HTTPS",
|
||||
"LabelRequireHttpsHelp": "If checked, the server will automatically redirect all requests over HTTP to HTTPS. This has no effect if the server is not listening on HTTPS.",
|
||||
"LabelRestore": "Restore",
|
||||
"LabelRuntimeMinutes": "Runtime",
|
||||
"LabelSaveLocalMetadata": "Save artwork into media folders",
|
||||
"LabelSaveLocalMetadataHelp": "Saving artwork into media folders will put them in a place where they can be easily edited.",
|
||||
@@ -1109,6 +1116,8 @@
|
||||
"MessageAddRepository": "If you wish to add a repository, click the button next to the header and fill out the requested information.",
|
||||
"MessageAreYouSureDeleteSubtitles": "Are you sure you wish to delete this subtitle file?",
|
||||
"MessageAreYouSureYouWishToRemoveMediaFolder": "Are you sure you wish to remove this media folder?",
|
||||
"MessageBackupDisclaimer": "Depending on the size of your library, the backup process may take a while.",
|
||||
"MessageBackupInProgress": "Backup in progress",
|
||||
"MessageBrowsePluginCatalog": "Browse our plugin catalog to view available plugins.",
|
||||
"MessageCancelSeriesTimerError": "An error occurred while canceling the series timer",
|
||||
"MessageCancelTimerError": "An error occurred while canceling the timer",
|
||||
@@ -1163,6 +1172,9 @@
|
||||
"MessageReenableUser": "See below to reenable",
|
||||
"MessageRenameMediaFolder": "Renaming a media library will cause all metadata to be lost, proceed with caution.",
|
||||
"MessageRepositoryInstallDisclaimer": "WARNING: Installing a third party plugin repository carries risks. It may contain unstable or malicious code, and may change at any time. Only install repositories from authors that you trust.",
|
||||
"MessageRestoreDisclaimer": "WARNING: You are about to restore from a previous backup. Your server will be rendered unusable for the duration of the process and any new data created since the backup will be lost.",
|
||||
"MessageRestoreInProgress": "Restore in progress",
|
||||
"MessageRestoreSuccess": "Restore successfully completed",
|
||||
"MessageSent": "Message sent.",
|
||||
"MessageSplitVersionsError": "An error occurred while splitting versions",
|
||||
"MessageSyncPlayCreateGroupDenied": "Permission required to create a group.",
|
||||
@@ -1185,6 +1197,7 @@
|
||||
"MessageUnableToConnectToServer": "We're unable to connect to the selected server right now. Please ensure it is running and try again.",
|
||||
"MessageUnauthorizedUser": "You are not authorized to access the server at this time. Please contact your server administrator for more information.",
|
||||
"MessageUnsetContentHelp": "Content will be displayed as plain folders. For best results use the metadata manager to set the content types of sub-folders.",
|
||||
"MessageWaitingForServer": "Waiting for server...",
|
||||
"MetadataImagesLoadError": "Failed to load metadata settings",
|
||||
"MetadataManager": "Metadata Manager",
|
||||
"MetadataNfoLoadError": "Failed to load metadata NFO settings",
|
||||
@@ -1565,12 +1578,14 @@
|
||||
"SubtitleVerticalPositionHelp": "Line number where text appears. Positive numbers indicate top down. Negative numbers indicate bottom up.",
|
||||
"SubtitleWhite": "White",
|
||||
"SubtitleYellow": "Yellow",
|
||||
"Success": "Success",
|
||||
"Suggestions": "Suggestions",
|
||||
"Sunday": "Sunday",
|
||||
"SyncPlayAccessHelp": "The SyncPlay feature enables to sync playback with other devices. Select the level of access this user has to the SyncPlay.",
|
||||
"SyncPlayGroupDefaultTitle": "{0}'s group",
|
||||
"TabAccess": "Access",
|
||||
"TabAdvanced": "Advanced",
|
||||
"HeaderBackups": "Backups",
|
||||
"TabCatalog": "Catalog",
|
||||
"TabDashboard": "Dashboard",
|
||||
"TabLatest": "Recently Added",
|
||||
|
||||
Reference in New Issue
Block a user