chore(release): snapshot 1.4.2
This commit is contained in:
385
front-end-dash.aggios.app/app/superadmin/backup/page.tsx
Normal file
385
front-end-dash.aggios.app/app/superadmin/backup/page.tsx
Normal file
@@ -0,0 +1,385 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
ArrowDownTrayIcon,
|
||||
ArrowUpTrayIcon,
|
||||
ClockIcon,
|
||||
ServerIcon,
|
||||
CheckCircleIcon,
|
||||
ExclamationTriangleIcon,
|
||||
BoltIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
|
||||
interface Backup {
|
||||
filename: string;
|
||||
size: string;
|
||||
date: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export default function BackupPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [backups, setBackups] = useState<Backup[]>([]);
|
||||
const [selectedBackup, setSelectedBackup] = useState<string>("");
|
||||
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
|
||||
const [autoBackupEnabled, setAutoBackupEnabled] = useState(false);
|
||||
const [autoBackupInterval, setAutoBackupInterval] = useState<number>(6);
|
||||
|
||||
useEffect(() => {
|
||||
loadBackups();
|
||||
loadAutoBackupSettings();
|
||||
}, []);
|
||||
|
||||
const loadAutoBackupSettings = () => {
|
||||
const enabled = localStorage.getItem('autoBackupEnabled') === 'true';
|
||||
const interval = parseInt(localStorage.getItem('autoBackupInterval') || '6');
|
||||
setAutoBackupEnabled(enabled);
|
||||
setAutoBackupInterval(interval);
|
||||
};
|
||||
|
||||
const toggleAutoBackup = () => {
|
||||
const newValue = !autoBackupEnabled;
|
||||
setAutoBackupEnabled(newValue);
|
||||
localStorage.setItem('autoBackupEnabled', newValue.toString());
|
||||
|
||||
if (newValue) {
|
||||
startAutoBackup();
|
||||
setMessage({ type: 'success', text: `Backup automático ativado (a cada ${autoBackupInterval}h)` });
|
||||
} else {
|
||||
stopAutoBackup();
|
||||
setMessage({ type: 'success', text: 'Backup automático desativado' });
|
||||
}
|
||||
};
|
||||
|
||||
const startAutoBackup = () => {
|
||||
const intervalMs = autoBackupInterval * 60 * 60 * 1000; // horas para ms
|
||||
const intervalId = setInterval(() => {
|
||||
createBackup();
|
||||
}, intervalMs);
|
||||
|
||||
localStorage.setItem('autoBackupIntervalId', intervalId.toString());
|
||||
};
|
||||
|
||||
const stopAutoBackup = () => {
|
||||
const intervalId = localStorage.getItem('autoBackupIntervalId');
|
||||
if (intervalId) {
|
||||
clearInterval(parseInt(intervalId));
|
||||
localStorage.removeItem('autoBackupIntervalId');
|
||||
}
|
||||
};
|
||||
|
||||
const changeInterval = (hours: number) => {
|
||||
setAutoBackupInterval(hours);
|
||||
localStorage.setItem('autoBackupInterval', hours.toString());
|
||||
|
||||
if (autoBackupEnabled) {
|
||||
stopAutoBackup();
|
||||
startAutoBackup();
|
||||
setMessage({ type: 'success', text: `Intervalo alterado para ${hours}h` });
|
||||
}
|
||||
};
|
||||
|
||||
const loadBackups = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await fetch('http://localhost:8085/api/superadmin/backups', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setBackups(data.backups || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar backups:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const createBackup = async () => {
|
||||
setLoading(true);
|
||||
setMessage(null);
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await fetch('http://localhost:8085/api/superadmin/backup/create', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
setMessage({ type: 'success', text: `Backup criado: ${data.filename} (${data.size})` });
|
||||
await loadBackups();
|
||||
} else {
|
||||
setMessage({ type: 'error', text: data.error || 'Erro ao criar backup' });
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({ type: 'error', text: 'Erro ao criar backup' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const restoreBackup = async () => {
|
||||
if (!selectedBackup) {
|
||||
setMessage({ type: 'error', text: 'Selecione um backup para restaurar' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`⚠️ ATENÇÃO: Isso irá SOBRESCREVER todos os dados atuais!\n\nDeseja restaurar o backup:\n${selectedBackup}?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setMessage(null);
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await fetch('http://localhost:8085/api/superadmin/backup/restore', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ filename: selectedBackup })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
setMessage({ type: 'success', text: 'Backup restaurado com sucesso! Recarregando...' });
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
setMessage({ type: 'error', text: data.error || 'Erro ao restaurar backup' });
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({ type: 'error', text: 'Erro ao restaurar backup' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const downloadBackup = async (filename: string) => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await fetch(`http://localhost:8085/api/superadmin/backup/download/${filename}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
} else {
|
||||
setMessage({ type: 'error', text: 'Erro ao baixar backup' });
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({ type: 'error', text: 'Erro ao baixar backup' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
Backup & Restore
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
Gerencie backups do banco de dados PostgreSQL
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{message && (
|
||||
<div className={`mb-6 p-4 rounded-lg flex items-center gap-3 ${message.type === 'success'
|
||||
? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200'
|
||||
: 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200'
|
||||
}`}>
|
||||
{message.type === 'success' ? (
|
||||
<CheckCircleIcon className="h-5 w-5" />
|
||||
) : (
|
||||
<ExclamationTriangleIcon className="h-5 w-5" />
|
||||
)}
|
||||
<span>{message.text}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-3 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
|
||||
<ArrowDownTrayIcon className="h-6 w-6 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Criar Backup
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Exportar dados atuais
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={createBackup}
|
||||
disabled={loading}
|
||||
className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{loading ? 'Criando...' : 'Criar Novo Backup'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-3 bg-green-100 dark:bg-green-900/30 rounded-lg">
|
||||
<BoltIcon className="h-6 w-6 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Backup Automático
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{autoBackupEnabled ? `Ativo (${autoBackupInterval}h)` : 'Desativado'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<select
|
||||
value={autoBackupInterval}
|
||||
onChange={(e) => changeInterval(parseInt(e.target.value))}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
>
|
||||
<option value={1}>A cada 1 hora</option>
|
||||
<option value={3}>A cada 3 horas</option>
|
||||
<option value={6}>A cada 6 horas</option>
|
||||
<option value={12}>A cada 12 horas</option>
|
||||
<option value={24}>A cada 24 horas</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={toggleAutoBackup}
|
||||
className={`w-full px-4 py-2 rounded-lg transition-colors ${
|
||||
autoBackupEnabled
|
||||
? 'bg-red-600 hover:bg-red-700 text-white'
|
||||
: 'bg-green-600 hover:bg-green-700 text-white'
|
||||
}`}
|
||||
>
|
||||
{autoBackupEnabled ? 'Desativar' : 'Ativar'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-3 bg-amber-100 dark:bg-amber-900/30 rounded-lg">
|
||||
<ArrowUpTrayIcon className="h-6 w-6 text-amber-600 dark:text-amber-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Restaurar Backup
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
⚠️ Sobrescreve dados atuais
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<select
|
||||
value={selectedBackup}
|
||||
onChange={(e) => setSelectedBackup(e.target.value)}
|
||||
className="w-full mb-3 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
>
|
||||
<option value="">Selecione um backup...</option>
|
||||
{backups.map((backup) => (
|
||||
<option key={backup.filename} value={backup.filename}>
|
||||
{backup.filename} - {backup.size}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
onClick={restoreBackup}
|
||||
disabled={loading || !selectedBackup}
|
||||
className="w-full px-4 py-2 bg-amber-600 text-white rounded-lg hover:bg-amber-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{loading ? 'Restaurando...' : 'Restaurar Backup'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow">
|
||||
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-3">
|
||||
<ServerIcon className="h-6 w-6 text-gray-600 dark:text-gray-400" />
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Backups Disponíveis
|
||||
</h2>
|
||||
<span className="ml-auto text-sm text-gray-500 dark:text-gray-400">
|
||||
{backups.length} arquivo(s)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
{backups.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<ServerIcon className="mx-auto h-12 w-12 text-gray-400" />
|
||||
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
Nenhum backup encontrado
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{backups.map((backup) => (
|
||||
<div
|
||||
key={backup.filename}
|
||||
className={`p-4 rounded-lg border transition-colors cursor-pointer ${selectedBackup === backup.filename
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||||
: 'border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50'
|
||||
}`}
|
||||
onClick={() => setSelectedBackup(backup.filename)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<ClockIcon className="h-5 w-5 text-gray-400" />
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-white">
|
||||
{backup.filename}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{backup.date} • {backup.size}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
downloadBackup(backup.filename);
|
||||
}}
|
||||
className="px-3 py-1 text-sm text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900/30 rounded"
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user