922 lines
30 KiB
TypeScript
922 lines
30 KiB
TypeScript
/**
|
|
* @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>
|
|
);
|
|
};
|