209 lines
9.6 KiB
TypeScript
209 lines
9.6 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { Download, Plus, HardDrive, AlertTriangle } from 'lucide-react';
|
|
import backupService, { Backup } from '../../features/system/services/backupService';
|
|
import { toast } from 'react-toastify';
|
|
import { logger } from '../../shared/utils/logger';
|
|
import Loading from '../../shared/components/Loading';
|
|
import { formatDate } from '../../shared/utils/format';
|
|
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
|
import { isAxiosError } from 'axios';
|
|
|
|
const BackupManagementPage: React.FC = () => {
|
|
const [backups, setBackups] = useState<Backup[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [creating, setCreating] = useState(false);
|
|
const [backupAvailable, setBackupAvailable] = useState<boolean | null>(null);
|
|
|
|
useEffect(() => {
|
|
fetchBackups();
|
|
checkBackupStatus();
|
|
}, []);
|
|
|
|
const checkBackupStatus = async () => {
|
|
try {
|
|
const response = await backupService.checkStatus();
|
|
setBackupAvailable(response.data.available);
|
|
if (!response.data.available) {
|
|
toast.warning(response.data.message || 'Backup service is not available', { autoClose: 8000 });
|
|
}
|
|
} catch (error: unknown) {
|
|
logger.error('Error checking backup status', error);
|
|
setBackupAvailable(false);
|
|
}
|
|
};
|
|
|
|
const fetchBackups = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const response = await backupService.listBackups();
|
|
setBackups(response.data.backups || []);
|
|
} catch (error: unknown) {
|
|
toast.error(getUserFriendlyError(error) || 'Unable to load backups');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleCreateBackup = async () => {
|
|
if (!confirm('Create a new database backup?')) return;
|
|
|
|
try {
|
|
setCreating(true);
|
|
await backupService.createBackup();
|
|
toast.success('Backup created successfully');
|
|
fetchBackups();
|
|
} catch (error: unknown) {
|
|
if (isAxiosError(error) && error.response?.status === 503) {
|
|
const errorDetail = error.response?.data as { requires_installation?: boolean; message?: string } | undefined;
|
|
if (errorDetail?.requires_installation) {
|
|
// Show a more detailed error for missing mysqldump
|
|
toast.error(
|
|
<div>
|
|
<p className="font-semibold mb-1">Backup service unavailable</p>
|
|
<p className="text-sm">{errorDetail.message || getUserFriendlyError(error)}</p>
|
|
</div>,
|
|
{ autoClose: 10000 }
|
|
);
|
|
} else {
|
|
toast.error(getUserFriendlyError(error) || 'Unable to create backup');
|
|
}
|
|
} else {
|
|
toast.error(getUserFriendlyError(error) || 'Unable to create backup');
|
|
}
|
|
} finally {
|
|
setCreating(false);
|
|
}
|
|
};
|
|
|
|
const handleDownload = async (filename: string) => {
|
|
try {
|
|
const blob = await backupService.downloadBackup(filename);
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = filename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
document.body.removeChild(a);
|
|
toast.success('Backup downloaded');
|
|
} catch (error: unknown) {
|
|
toast.error(getUserFriendlyError(error) || 'Unable to download backup');
|
|
}
|
|
};
|
|
|
|
const handleCleanup = async () => {
|
|
if (!confirm('Clean up old backups beyond retention period?')) return;
|
|
|
|
try {
|
|
const response = await backupService.cleanupOldBackups();
|
|
toast.success(`Removed ${response.data.removed_count} old backup(s)`);
|
|
fetchBackups();
|
|
} catch (error: unknown) {
|
|
toast.error(getUserFriendlyError(error) || 'Unable to cleanup backups');
|
|
}
|
|
};
|
|
|
|
if (loading && backups.length === 0) {
|
|
return <Loading fullScreen text="Loading backups..." />;
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-50 px-3 sm:px-4 md:px-6 lg:px-8 py-6 sm:py-8 md:py-10">
|
|
<div className="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-4 sm:gap-6 mb-6 sm:mb-8 md:mb-10 animate-fade-in">
|
|
<div className="w-full lg:w-auto">
|
|
<div className="flex items-center gap-2 sm:gap-3 mb-2 sm:mb-3">
|
|
<div className="h-1 w-12 sm:w-16 md:w-20 bg-gradient-to-r from-slate-400 via-gray-500 to-slate-600 rounded-full"></div>
|
|
<h1 className="text-2xl sm:text-3xl md:text-4xl font-bold bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 bg-clip-text text-transparent tracking-tight">
|
|
Backup Management
|
|
</h1>
|
|
</div>
|
|
<p className="text-slate-600 mt-2 sm:mt-3 text-sm sm:text-base md:text-lg font-light">Manage database backups</p>
|
|
</div>
|
|
<div className="flex flex-col sm:flex-row gap-2 sm:gap-3 w-full lg:w-auto">
|
|
<button
|
|
onClick={handleCleanup}
|
|
className="w-full sm:w-auto px-4 sm:px-6 py-2 sm:py-2.5 bg-gradient-to-r from-slate-600 to-slate-700 text-white rounded-xl font-semibold hover:from-slate-700 hover:to-slate-800 shadow-lg hover:shadow-xl transition-all duration-200 text-xs sm:text-sm"
|
|
>
|
|
Cleanup Old
|
|
</button>
|
|
<button
|
|
onClick={handleCreateBackup}
|
|
disabled={creating || backupAvailable === false}
|
|
className="w-full sm:w-auto px-4 sm:px-6 py-2 sm:py-2.5 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-xl font-semibold hover:from-blue-700 hover:to-blue-800 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 flex items-center justify-center gap-2 text-xs sm:text-sm"
|
|
title={backupAvailable === false ? 'Backup service unavailable - install mysqldump' : ''}
|
|
>
|
|
<Plus className="w-4 h-4 sm:w-5 sm:h-5" />
|
|
{creating ? 'Creating...' : 'Create Backup'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Warning Banner */}
|
|
{backupAvailable === false && (
|
|
<div className="bg-gradient-to-r from-amber-50 to-yellow-50 border-2 border-amber-200 rounded-xl sm:rounded-2xl p-3 sm:p-4 md:p-5 mb-4 sm:mb-6 animate-fade-in shadow-lg">
|
|
<div className="flex items-start gap-3 sm:gap-4">
|
|
<AlertTriangle className="w-5 h-5 sm:w-6 sm:h-6 text-amber-600 flex-shrink-0 mt-0.5" />
|
|
<div className="flex-1">
|
|
<h3 className="font-semibold text-amber-900 text-sm sm:text-base mb-1 sm:mb-2">Backup Service Unavailable</h3>
|
|
<p className="text-xs sm:text-sm text-amber-800 mb-2 sm:mb-3">
|
|
mysqldump is not installed. Please install MySQL client tools to enable backups:
|
|
</p>
|
|
<div className="bg-white/80 rounded-lg sm:rounded-xl p-2 sm:p-3 border border-amber-200">
|
|
<code className="text-xs sm:text-sm text-amber-900 font-mono block">
|
|
<div className="mb-1">Ubuntu/Debian: <span className="font-semibold">sudo apt-get install mysql-client</span></div>
|
|
<div className="mb-1">CentOS/RHEL: <span className="font-semibold">sudo yum install mysql</span></div>
|
|
<div>macOS: <span className="font-semibold">brew install mysql-client</span></div>
|
|
</code>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="bg-white/90 backdrop-blur-md rounded-xl sm:rounded-2xl shadow-xl border border-slate-200/60 p-4 sm:p-5 md:p-6 animate-fade-in hover:shadow-2xl transition-all duration-300">
|
|
{backups.length === 0 ? (
|
|
<div className="text-center py-8 sm:py-12">
|
|
<HardDrive className="w-12 h-12 sm:w-16 sm:h-16 text-slate-400 mx-auto mb-3 sm:mb-4" />
|
|
<p className="text-slate-500 text-sm sm:text-base">No backups available</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3 sm:space-y-4">
|
|
{backups.map((backup) => (
|
|
<div
|
|
key={backup.filename}
|
|
className="bg-gradient-to-r from-slate-50 to-white border border-slate-200 rounded-lg sm:rounded-xl p-3 sm:p-4 hover:from-blue-50 hover:to-indigo-50 hover:border-blue-300 hover:shadow-lg transition-all duration-200"
|
|
>
|
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 sm:gap-4">
|
|
<div className="flex items-start gap-3 sm:gap-4 flex-1 min-w-0">
|
|
<HardDrive className="w-6 h-6 sm:w-8 sm:h-8 text-blue-500 mt-1 flex-shrink-0" />
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="font-semibold text-sm sm:text-base md:text-lg mb-1 truncate">{backup.filename}</h3>
|
|
<p className="text-slate-600 text-xs sm:text-sm mb-1">
|
|
Size: {backup.size_mb} MB • Database: {backup.database}
|
|
</p>
|
|
<p className="text-slate-500 text-xs">
|
|
Created: {formatDate(backup.created_at)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => handleDownload(backup.filename)}
|
|
className="w-full sm:w-auto px-3 sm:px-4 py-2 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-lg sm:rounded-xl hover:from-blue-700 hover:to-blue-800 shadow-lg hover:shadow-xl transition-all duration-200 flex items-center justify-center gap-2 text-xs sm:text-sm font-semibold"
|
|
>
|
|
<Download className="w-4 h-4" />
|
|
Download
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default BackupManagementPage;
|
|
|