feat: Backup & Restore - Manual backup tested complete.
This commit is contained in:
921
frontend/src/pages/admin/AdminBackupPage.tsx
Normal file
921
frontend/src/pages/admin/AdminBackupPage.tsx
Normal file
@@ -0,0 +1,921 @@
|
||||
/**
|
||||
* @ai-summary Admin Backup & Restore page for managing backups, schedules, and settings
|
||||
* @ai-context Desktop version with tabbed interface for backup management
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
Chip,
|
||||
CircularProgress,
|
||||
Container,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
FormControlLabel,
|
||||
Switch,
|
||||
Tab,
|
||||
Tabs,
|
||||
TextField,
|
||||
Typography,
|
||||
Alert,
|
||||
Divider,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
IconButton,
|
||||
MenuItem,
|
||||
Select,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Paper,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Backup,
|
||||
Download,
|
||||
Delete,
|
||||
Schedule,
|
||||
Settings,
|
||||
Add,
|
||||
Upload,
|
||||
RestorePage,
|
||||
Edit,
|
||||
} from '@mui/icons-material';
|
||||
import { useAdminAccess } from '../../core/auth/useAdminAccess';
|
||||
import {
|
||||
useBackups,
|
||||
useBackupSchedules,
|
||||
useBackupSettings,
|
||||
useCreateBackup,
|
||||
useDeleteBackup,
|
||||
useDownloadBackup,
|
||||
useUploadBackup,
|
||||
useRestorePreview,
|
||||
useExecuteRestore,
|
||||
useCreateSchedule,
|
||||
useUpdateSchedule,
|
||||
useDeleteSchedule,
|
||||
useToggleSchedule,
|
||||
useUpdateSettings,
|
||||
} from '../../features/admin/hooks/useBackups';
|
||||
import {
|
||||
BackupSchedule,
|
||||
BackupSettings,
|
||||
CreateScheduleRequest,
|
||||
UpdateScheduleRequest,
|
||||
BackupFrequency,
|
||||
BackupHistory,
|
||||
} from '../../features/admin/types/admin.types';
|
||||
|
||||
// Helper to format file size
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
// Helper to format date
|
||||
const formatDate = (dateString: string): string => {
|
||||
return dayjs(dateString).format('MMM DD, YYYY HH:mm');
|
||||
};
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
index: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
const TabPanel: React.FC<TabPanelProps> = ({ children, value, index }) => {
|
||||
return (
|
||||
<div role="tabpanel" hidden={value !== index}>
|
||||
{value === index && <Box sx={{ py: 3 }}>{children}</Box>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AdminBackupPage: React.FC = () => {
|
||||
const { loading: authLoading, isAdmin } = useAdminAccess();
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// State for dialogs
|
||||
const [createBackupDialogOpen, setCreateBackupDialogOpen] = useState(false);
|
||||
const [createScheduleDialogOpen, setCreateScheduleDialogOpen] = useState(false);
|
||||
const [editScheduleDialogOpen, setEditScheduleDialogOpen] = useState(false);
|
||||
const [deleteScheduleDialogOpen, setDeleteScheduleDialogOpen] = useState(false);
|
||||
const [deleteBackupDialogOpen, setDeleteBackupDialogOpen] = useState(false);
|
||||
const [restoreDialogOpen, setRestoreDialogOpen] = useState(false);
|
||||
const [restorePreviewDialogOpen, setRestorePreviewDialogOpen] = useState(false);
|
||||
|
||||
// State for forms
|
||||
const [selectedSchedule, setSelectedSchedule] = useState<BackupSchedule | null>(null);
|
||||
const [selectedBackup, setSelectedBackup] = useState<BackupHistory | null>(null);
|
||||
const [backupName, setBackupName] = useState('');
|
||||
const [includeDocuments, setIncludeDocuments] = useState(true);
|
||||
const [scheduleName, setScheduleName] = useState('');
|
||||
const [scheduleFrequency, setScheduleFrequency] = useState<BackupFrequency>('daily');
|
||||
const [scheduleRetention, setScheduleRetention] = useState(7);
|
||||
const [scheduleEnabled, setScheduleEnabled] = useState(true);
|
||||
|
||||
// Queries
|
||||
const { data: backupsData, isLoading: backupsLoading } = useBackups();
|
||||
const { data: schedules, isLoading: schedulesLoading } = useBackupSchedules();
|
||||
const { data: settings, isLoading: settingsLoading } = useBackupSettings();
|
||||
|
||||
// Mutations
|
||||
const createBackupMutation = useCreateBackup();
|
||||
const deleteBackupMutation = useDeleteBackup();
|
||||
const downloadBackupMutation = useDownloadBackup();
|
||||
const uploadBackupMutation = useUploadBackup();
|
||||
const restorePreviewMutation = useRestorePreview();
|
||||
const executeRestoreMutation = useExecuteRestore();
|
||||
const createScheduleMutation = useCreateSchedule();
|
||||
const updateScheduleMutation = useUpdateSchedule();
|
||||
const deleteScheduleMutation = useDeleteSchedule();
|
||||
const toggleScheduleMutation = useToggleSchedule();
|
||||
const updateSettingsMutation = useUpdateSettings();
|
||||
|
||||
// Handlers for backups
|
||||
const handleCreateBackup = useCallback(() => {
|
||||
createBackupMutation.mutate(
|
||||
{
|
||||
name: backupName || undefined,
|
||||
includeDocuments,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setCreateBackupDialogOpen(false);
|
||||
setBackupName('');
|
||||
setIncludeDocuments(true);
|
||||
},
|
||||
}
|
||||
);
|
||||
}, [backupName, includeDocuments, createBackupMutation]);
|
||||
|
||||
const handleDownloadBackup = useCallback(
|
||||
(backup: BackupHistory) => {
|
||||
downloadBackupMutation.mutate({
|
||||
id: backup.id,
|
||||
filename: backup.filename,
|
||||
});
|
||||
},
|
||||
[downloadBackupMutation]
|
||||
);
|
||||
|
||||
const handleDeleteBackup = useCallback(() => {
|
||||
if (!selectedBackup) return;
|
||||
deleteBackupMutation.mutate(selectedBackup.id, {
|
||||
onSuccess: () => {
|
||||
setDeleteBackupDialogOpen(false);
|
||||
setSelectedBackup(null);
|
||||
},
|
||||
});
|
||||
}, [selectedBackup, deleteBackupMutation]);
|
||||
|
||||
const handleUploadBackup = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
uploadBackupMutation.mutate(file);
|
||||
}
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
},
|
||||
[uploadBackupMutation]
|
||||
);
|
||||
|
||||
const handleRestorePreview = useCallback(
|
||||
(backup: BackupHistory) => {
|
||||
setSelectedBackup(backup);
|
||||
restorePreviewMutation.mutate(backup.id, {
|
||||
onSuccess: () => {
|
||||
setRestorePreviewDialogOpen(true);
|
||||
},
|
||||
});
|
||||
},
|
||||
[restorePreviewMutation]
|
||||
);
|
||||
|
||||
const handleExecuteRestore = useCallback(() => {
|
||||
if (!selectedBackup) return;
|
||||
executeRestoreMutation.mutate(selectedBackup.id, {
|
||||
onSuccess: () => {
|
||||
setRestoreDialogOpen(false);
|
||||
setRestorePreviewDialogOpen(false);
|
||||
setSelectedBackup(null);
|
||||
},
|
||||
});
|
||||
}, [selectedBackup, executeRestoreMutation]);
|
||||
|
||||
// Handlers for schedules
|
||||
const handleCreateSchedule = useCallback(() => {
|
||||
const data: CreateScheduleRequest = {
|
||||
name: scheduleName,
|
||||
frequency: scheduleFrequency,
|
||||
retentionCount: scheduleRetention,
|
||||
isEnabled: scheduleEnabled,
|
||||
};
|
||||
createScheduleMutation.mutate(data, {
|
||||
onSuccess: () => {
|
||||
setCreateScheduleDialogOpen(false);
|
||||
setScheduleName('');
|
||||
setScheduleFrequency('daily');
|
||||
setScheduleRetention(7);
|
||||
setScheduleEnabled(true);
|
||||
},
|
||||
});
|
||||
}, [scheduleName, scheduleFrequency, scheduleRetention, scheduleEnabled, createScheduleMutation]);
|
||||
|
||||
const handleEditSchedule = useCallback(
|
||||
(schedule: BackupSchedule) => {
|
||||
setSelectedSchedule(schedule);
|
||||
setScheduleName(schedule.name);
|
||||
setScheduleFrequency(schedule.frequency);
|
||||
setScheduleRetention(schedule.retentionCount);
|
||||
setScheduleEnabled(schedule.isEnabled);
|
||||
setEditScheduleDialogOpen(true);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleUpdateSchedule = useCallback(() => {
|
||||
if (!selectedSchedule) return;
|
||||
const data: UpdateScheduleRequest = {
|
||||
name: scheduleName !== selectedSchedule.name ? scheduleName : undefined,
|
||||
frequency: scheduleFrequency !== selectedSchedule.frequency ? scheduleFrequency : undefined,
|
||||
retentionCount:
|
||||
scheduleRetention !== selectedSchedule.retentionCount ? scheduleRetention : undefined,
|
||||
isEnabled: scheduleEnabled !== selectedSchedule.isEnabled ? scheduleEnabled : undefined,
|
||||
};
|
||||
updateScheduleMutation.mutate(
|
||||
{ id: selectedSchedule.id, data },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setEditScheduleDialogOpen(false);
|
||||
setSelectedSchedule(null);
|
||||
},
|
||||
}
|
||||
);
|
||||
}, [
|
||||
selectedSchedule,
|
||||
scheduleName,
|
||||
scheduleFrequency,
|
||||
scheduleRetention,
|
||||
scheduleEnabled,
|
||||
updateScheduleMutation,
|
||||
]);
|
||||
|
||||
const handleDeleteSchedule = useCallback(() => {
|
||||
if (!selectedSchedule) return;
|
||||
deleteScheduleMutation.mutate(selectedSchedule.id, {
|
||||
onSuccess: () => {
|
||||
setDeleteScheduleDialogOpen(false);
|
||||
setSelectedSchedule(null);
|
||||
},
|
||||
});
|
||||
}, [selectedSchedule, deleteScheduleMutation]);
|
||||
|
||||
const handleToggleSchedule = useCallback(
|
||||
(schedule: BackupSchedule) => {
|
||||
toggleScheduleMutation.mutate(schedule.id);
|
||||
},
|
||||
[toggleScheduleMutation]
|
||||
);
|
||||
|
||||
// Handlers for settings
|
||||
const handleUpdateSettings = useCallback(
|
||||
(field: keyof BackupSettings, value: boolean | string | number) => {
|
||||
if (!settings) return;
|
||||
updateSettingsMutation.mutate({ [field]: value });
|
||||
},
|
||||
[settings, updateSettingsMutation]
|
||||
);
|
||||
|
||||
// Auth loading
|
||||
if (authLoading) {
|
||||
return (
|
||||
<Container maxWidth="lg" sx={{ py: 4 }}>
|
||||
<Box display="flex" justifyContent="center" alignItems="center" minHeight="400px">
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
// Not admin
|
||||
if (!isAdmin) {
|
||||
return <Navigate to="/garage/settings" replace />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container maxWidth="lg" sx={{ py: 4 }}>
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="h4" component="h1" sx={{ mb: 1, fontWeight: 600 }}>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Backup />
|
||||
Backup & Restore
|
||||
</Box>
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
Manage database backups, schedules, and restore operations
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}>
|
||||
<Tabs value={tabValue} onChange={(_, v) => setTabValue(v)}>
|
||||
<Tab label="Backups" icon={<Backup />} iconPosition="start" />
|
||||
<Tab label="Schedules" icon={<Schedule />} iconPosition="start" />
|
||||
<Tab label="Settings" icon={<Settings />} iconPosition="start" />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
{/* Backups Tab */}
|
||||
<TabPanel value={tabValue} index={0}>
|
||||
<Box sx={{ mb: 3, display: 'flex', gap: 2 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<Add />}
|
||||
onClick={() => setCreateBackupDialogOpen(true)}
|
||||
>
|
||||
Create Backup
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<Upload />}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
Upload Backup
|
||||
</Button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".tar.gz,.tar"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleUploadBackup}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{backupsLoading ? (
|
||||
<Box display="flex" justifyContent="center" py={8}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : (
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Filename</TableCell>
|
||||
<TableCell>Type</TableCell>
|
||||
<TableCell>Size</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Created</TableCell>
|
||||
<TableCell align="right">Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{backupsData?.items.map((backup) => (
|
||||
<TableRow key={backup.id}>
|
||||
<TableCell>{backup.filename}</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={backup.backupType}
|
||||
size="small"
|
||||
color={backup.backupType === 'scheduled' ? 'primary' : 'default'}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{formatFileSize(backup.fileSizeBytes)}</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={backup.status}
|
||||
size="small"
|
||||
color={
|
||||
backup.status === 'completed'
|
||||
? 'success'
|
||||
: backup.status === 'failed'
|
||||
? 'error'
|
||||
: 'warning'
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{formatDate(backup.startedAt)}</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleDownloadBackup(backup)}
|
||||
title="Download"
|
||||
>
|
||||
<Download />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleRestorePreview(backup)}
|
||||
title="Restore"
|
||||
disabled={backup.status !== 'completed'}
|
||||
>
|
||||
<RestorePage />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setSelectedBackup(backup);
|
||||
setDeleteBackupDialogOpen(true);
|
||||
}}
|
||||
title="Delete"
|
||||
>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</TabPanel>
|
||||
|
||||
{/* Schedules Tab */}
|
||||
<TabPanel value={tabValue} index={1}>
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<Add />}
|
||||
onClick={() => setCreateScheduleDialogOpen(true)}
|
||||
>
|
||||
Create Schedule
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{schedulesLoading ? (
|
||||
<Box display="flex" justifyContent="center" py={8}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : (
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Frequency</TableCell>
|
||||
<TableCell>Retention</TableCell>
|
||||
<TableCell>Last Run</TableCell>
|
||||
<TableCell>Next Run</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell align="right">Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{schedules?.map((schedule) => (
|
||||
<TableRow key={schedule.id}>
|
||||
<TableCell>{schedule.name}</TableCell>
|
||||
<TableCell>
|
||||
<Chip label={schedule.frequency} size="small" />
|
||||
</TableCell>
|
||||
<TableCell>{schedule.retentionCount} backups</TableCell>
|
||||
<TableCell>
|
||||
{schedule.lastRunAt ? formatDate(schedule.lastRunAt) : 'Never'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{schedule.nextRunAt ? formatDate(schedule.nextRunAt) : 'N/A'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Switch
|
||||
checked={schedule.isEnabled}
|
||||
onChange={() => handleToggleSchedule(schedule)}
|
||||
color="primary"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleEditSchedule(schedule)}
|
||||
title="Edit"
|
||||
>
|
||||
<Edit />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setSelectedSchedule(schedule);
|
||||
setDeleteScheduleDialogOpen(true);
|
||||
}}
|
||||
title="Delete"
|
||||
>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</TabPanel>
|
||||
|
||||
{/* Settings Tab */}
|
||||
<TabPanel value={tabValue} index={2}>
|
||||
{settingsLoading ? (
|
||||
<Box display="flex" justifyContent="center" py={8}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : settings ? (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Backup Settings
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={settings.emailOnSuccess}
|
||||
onChange={(e) => handleUpdateSettings('emailOnSuccess', e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label="Email on backup success"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={settings.emailOnFailure}
|
||||
onChange={(e) => handleUpdateSettings('emailOnFailure', e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label="Email on backup failure"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<TextField
|
||||
label="Admin Email"
|
||||
value={settings.adminEmail}
|
||||
onChange={(e) => handleUpdateSettings('adminEmail', e.target.value)}
|
||||
fullWidth
|
||||
helperText="Email address to receive backup notifications"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<TextField
|
||||
label="Max Backup Size (MB)"
|
||||
type="number"
|
||||
value={settings.maxBackupSizeMb}
|
||||
onChange={(e) =>
|
||||
handleUpdateSettings('maxBackupSizeMb', parseInt(e.target.value, 10))
|
||||
}
|
||||
fullWidth
|
||||
helperText="Maximum backup file size in megabytes"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={settings.compressionEnabled}
|
||||
onChange={(e) => handleUpdateSettings('compressionEnabled', e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label="Enable compression"
|
||||
/>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
</TabPanel>
|
||||
|
||||
{/* Create Backup Dialog */}
|
||||
<Dialog
|
||||
open={createBackupDialogOpen}
|
||||
onClose={() => setCreateBackupDialogOpen(false)}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>Create Manual Backup</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ mt: 2, display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
<TextField
|
||||
label="Backup Name (optional)"
|
||||
fullWidth
|
||||
value={backupName}
|
||||
onChange={(e) => setBackupName(e.target.value)}
|
||||
placeholder="e.g., Pre-upgrade backup"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={includeDocuments}
|
||||
onChange={(e) => setIncludeDocuments(e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label="Include document files"
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setCreateBackupDialogOpen(false)}>Cancel</Button>
|
||||
<Button
|
||||
onClick={handleCreateBackup}
|
||||
variant="contained"
|
||||
disabled={createBackupMutation.isPending}
|
||||
>
|
||||
{createBackupMutation.isPending ? 'Creating...' : 'Create Backup'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* Create Schedule Dialog */}
|
||||
<Dialog
|
||||
open={createScheduleDialogOpen}
|
||||
onClose={() => setCreateScheduleDialogOpen(false)}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>Create Backup Schedule</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ mt: 2, display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
<TextField
|
||||
label="Schedule Name"
|
||||
fullWidth
|
||||
value={scheduleName}
|
||||
onChange={(e) => setScheduleName(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Frequency</InputLabel>
|
||||
<Select
|
||||
value={scheduleFrequency}
|
||||
onChange={(e) => setScheduleFrequency(e.target.value as BackupFrequency)}
|
||||
label="Frequency"
|
||||
>
|
||||
<MenuItem value="hourly">Hourly</MenuItem>
|
||||
<MenuItem value="daily">Daily</MenuItem>
|
||||
<MenuItem value="weekly">Weekly</MenuItem>
|
||||
<MenuItem value="monthly">Monthly</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<TextField
|
||||
label="Retention Count"
|
||||
type="number"
|
||||
fullWidth
|
||||
value={scheduleRetention}
|
||||
onChange={(e) => setScheduleRetention(parseInt(e.target.value, 10))}
|
||||
helperText="Number of backups to keep"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={scheduleEnabled}
|
||||
onChange={(e) => setScheduleEnabled(e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label="Enabled"
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setCreateScheduleDialogOpen(false)}>Cancel</Button>
|
||||
<Button
|
||||
onClick={handleCreateSchedule}
|
||||
variant="contained"
|
||||
disabled={!scheduleName || createScheduleMutation.isPending}
|
||||
>
|
||||
{createScheduleMutation.isPending ? 'Creating...' : 'Create Schedule'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* Edit Schedule Dialog */}
|
||||
<Dialog
|
||||
open={editScheduleDialogOpen}
|
||||
onClose={() => setEditScheduleDialogOpen(false)}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>Edit Schedule</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ mt: 2, display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
<TextField
|
||||
label="Schedule Name"
|
||||
fullWidth
|
||||
value={scheduleName}
|
||||
onChange={(e) => setScheduleName(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Frequency</InputLabel>
|
||||
<Select
|
||||
value={scheduleFrequency}
|
||||
onChange={(e) => setScheduleFrequency(e.target.value as BackupFrequency)}
|
||||
label="Frequency"
|
||||
>
|
||||
<MenuItem value="hourly">Hourly</MenuItem>
|
||||
<MenuItem value="daily">Daily</MenuItem>
|
||||
<MenuItem value="weekly">Weekly</MenuItem>
|
||||
<MenuItem value="monthly">Monthly</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<TextField
|
||||
label="Retention Count"
|
||||
type="number"
|
||||
fullWidth
|
||||
value={scheduleRetention}
|
||||
onChange={(e) => setScheduleRetention(parseInt(e.target.value, 10))}
|
||||
helperText="Number of backups to keep"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={scheduleEnabled}
|
||||
onChange={(e) => setScheduleEnabled(e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label="Enabled"
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setEditScheduleDialogOpen(false)}>Cancel</Button>
|
||||
<Button
|
||||
onClick={handleUpdateSchedule}
|
||||
variant="contained"
|
||||
disabled={!scheduleName || updateScheduleMutation.isPending}
|
||||
>
|
||||
{updateScheduleMutation.isPending ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Schedule Dialog */}
|
||||
<Dialog open={deleteScheduleDialogOpen} onClose={() => setDeleteScheduleDialogOpen(false)}>
|
||||
<DialogTitle>Delete Schedule</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>
|
||||
Are you sure you want to delete the schedule "{selectedSchedule?.name}"? This action
|
||||
cannot be undone.
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setDeleteScheduleDialogOpen(false)}>Cancel</Button>
|
||||
<Button
|
||||
onClick={handleDeleteSchedule}
|
||||
variant="contained"
|
||||
color="error"
|
||||
disabled={deleteScheduleMutation.isPending}
|
||||
>
|
||||
{deleteScheduleMutation.isPending ? 'Deleting...' : 'Delete'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Backup Dialog */}
|
||||
<Dialog open={deleteBackupDialogOpen} onClose={() => setDeleteBackupDialogOpen(false)}>
|
||||
<DialogTitle>Delete Backup</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>
|
||||
Are you sure you want to delete the backup "{selectedBackup?.filename}"? This action
|
||||
cannot be undone.
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setDeleteBackupDialogOpen(false)}>Cancel</Button>
|
||||
<Button
|
||||
onClick={handleDeleteBackup}
|
||||
variant="contained"
|
||||
color="error"
|
||||
disabled={deleteBackupMutation.isPending}
|
||||
>
|
||||
{deleteBackupMutation.isPending ? 'Deleting...' : 'Delete'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* Restore Preview Dialog */}
|
||||
<Dialog
|
||||
open={restorePreviewDialogOpen}
|
||||
onClose={() => setRestorePreviewDialogOpen(false)}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>Restore Preview</DialogTitle>
|
||||
<DialogContent>
|
||||
{restorePreviewMutation.isPending ? (
|
||||
<Box display="flex" justifyContent="center" py={4}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : restorePreviewMutation.data ? (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Alert severity="warning" sx={{ mb: 3 }}>
|
||||
A safety backup will be created automatically before restoring.
|
||||
</Alert>
|
||||
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Backup Information
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
Version: {restorePreviewMutation.data.manifest.version}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
Created: {formatDate(restorePreviewMutation.data.manifest.createdAt)}
|
||||
</Typography>
|
||||
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Contents
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Database Tables: {restorePreviewMutation.data.manifest.contents.database.tablesCount}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Database Size:{' '}
|
||||
{formatFileSize(restorePreviewMutation.data.manifest.contents.database.sizeBytes)}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Documents: {restorePreviewMutation.data.manifest.contents.documents.totalFiles}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Documents Size:{' '}
|
||||
{formatFileSize(
|
||||
restorePreviewMutation.data.manifest.contents.documents.totalSizeBytes
|
||||
)}
|
||||
</Typography>
|
||||
|
||||
{restorePreviewMutation.data.warnings.length > 0 && (
|
||||
<>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Typography variant="h6" gutterBottom color="warning.main">
|
||||
Warnings
|
||||
</Typography>
|
||||
{restorePreviewMutation.data.warnings.map((warning, index) => (
|
||||
<Alert key={index} severity="warning" sx={{ mb: 1 }}>
|
||||
{warning}
|
||||
</Alert>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Estimated Duration: {restorePreviewMutation.data.estimatedDuration}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : null}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setRestorePreviewDialogOpen(false)}>Cancel</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setRestorePreviewDialogOpen(false);
|
||||
setRestoreDialogOpen(true);
|
||||
}}
|
||||
variant="contained"
|
||||
color="warning"
|
||||
>
|
||||
Proceed to Restore
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* Restore Confirmation Dialog */}
|
||||
<Dialog open={restoreDialogOpen} onClose={() => setRestoreDialogOpen(false)}>
|
||||
<DialogTitle>Confirm Restore</DialogTitle>
|
||||
<DialogContent>
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
This action will replace all current data with the backup data. A safety backup will be
|
||||
created first.
|
||||
</Alert>
|
||||
<Typography>
|
||||
Are you sure you want to restore from "{selectedBackup?.filename}"?
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setRestoreDialogOpen(false)}>Cancel</Button>
|
||||
<Button
|
||||
onClick={handleExecuteRestore}
|
||||
variant="contained"
|
||||
color="error"
|
||||
disabled={executeRestoreMutation.isPending}
|
||||
>
|
||||
{executeRestoreMutation.isPending ? 'Restoring...' : 'Restore'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user