128 lines
4.7 KiB
TypeScript
128 lines
4.7 KiB
TypeScript
|
|
import React, { useEffect, useState } from 'react';
|
|||
|
|
import { backendApi } from '../../services/apiClient';
|
|||
|
|
import { DatabaseBackup, Loader2, Download } from 'lucide-react';
|
|||
|
|
|
|||
|
|
type BackupItem = { filename: string; createdAt: string };
|
|||
|
|
|
|||
|
|
export const BackupsSection: React.FC = () => {
|
|||
|
|
const [backups, setBackups] = useState<BackupItem[]>([]);
|
|||
|
|
const [loading, setLoading] = useState(true);
|
|||
|
|
const [creating, setCreating] = useState(false);
|
|||
|
|
const [error, setError] = useState<string | null>(null);
|
|||
|
|
|
|||
|
|
const load = async () => {
|
|||
|
|
setLoading(true);
|
|||
|
|
setError(null);
|
|||
|
|
try {
|
|||
|
|
const list = await backendApi.getBackups();
|
|||
|
|
setBackups(list);
|
|||
|
|
} catch (e: any) {
|
|||
|
|
setError(e?.message || 'Ошибка загрузки списка');
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
load();
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
const handleCreate = async () => {
|
|||
|
|
setCreating(true);
|
|||
|
|
setError(null);
|
|||
|
|
try {
|
|||
|
|
await backendApi.createBackup();
|
|||
|
|
await load();
|
|||
|
|
} catch (e: any) {
|
|||
|
|
setError(e?.message || 'Ошибка создания резервной копии');
|
|||
|
|
} finally {
|
|||
|
|
setCreating(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const formatDate = (iso: string) => {
|
|||
|
|
try {
|
|||
|
|
const d = new Date(iso);
|
|||
|
|
return d.toLocaleString('ru-RU', {
|
|||
|
|
day: '2-digit',
|
|||
|
|
month: '2-digit',
|
|||
|
|
year: 'numeric',
|
|||
|
|
hour: '2-digit',
|
|||
|
|
minute: '2-digit',
|
|||
|
|
});
|
|||
|
|
} catch {
|
|||
|
|
return iso;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="space-y-6">
|
|||
|
|
<div className="flex justify-between items-center">
|
|||
|
|
<h3 className="text-lg font-bold text-slate-800">Резервные копии БД</h3>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={handleCreate}
|
|||
|
|
disabled={creating}
|
|||
|
|
className="flex items-center gap-2 px-4 py-2 bg-primary-600 text-white text-sm font-bold rounded-xl hover:bg-primary-700 disabled:opacity-50"
|
|||
|
|
>
|
|||
|
|
{creating ? <Loader2 className="w-4 h-4 animate-spin" /> : <DatabaseBackup className="w-4 h-4" />}
|
|||
|
|
Создать резервную копию
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<p className="text-sm text-slate-500">
|
|||
|
|
Создаётся дамп PostgreSQL (pg_dump). Убедитесь, что pg_dump доступен на сервере (PostgreSQL bin в PATH).
|
|||
|
|
</p>
|
|||
|
|
|
|||
|
|
{error && (
|
|||
|
|
<div className="rounded-xl border border-red-200 bg-red-50 p-4 text-red-700 text-sm">{error}</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{loading ? (
|
|||
|
|
<div className="flex items-center justify-center py-12">
|
|||
|
|
<Loader2 className="w-8 h-8 animate-spin text-primary-500" />
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
<div className="rounded-xl border border-slate-200 overflow-hidden">
|
|||
|
|
<div className="overflow-x-auto">
|
|||
|
|
<table className="w-full text-sm">
|
|||
|
|
<thead className="bg-slate-50 border-b border-slate-200">
|
|||
|
|
<tr>
|
|||
|
|
<th className="text-left py-3 px-4 font-bold text-slate-700">Файл</th>
|
|||
|
|
<th className="text-left py-3 px-4 font-bold text-slate-700">Дата создания</th>
|
|||
|
|
<th className="text-right py-3 px-4 font-bold text-slate-700">Действие</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody>
|
|||
|
|
{backups.length === 0 ? (
|
|||
|
|
<tr>
|
|||
|
|
<td colSpan={3} className="py-8 px-4 text-center text-slate-500">
|
|||
|
|
Резервных копий пока нет. Нажмите «Создать резервную копию».
|
|||
|
|
</td>
|
|||
|
|
</tr>
|
|||
|
|
) : (
|
|||
|
|
backups.map((b) => (
|
|||
|
|
<tr key={b.filename} className="border-b border-slate-100 hover:bg-slate-50/50">
|
|||
|
|
<td className="py-3 px-4 font-medium text-slate-800">{b.filename}</td>
|
|||
|
|
<td className="py-3 px-4 text-slate-600">{formatDate(b.createdAt)}</td>
|
|||
|
|
<td className="py-3 px-4 text-right">
|
|||
|
|
<a
|
|||
|
|
href={backendApi.getBackupDownloadUrl(b.filename)}
|
|||
|
|
target="_blank"
|
|||
|
|
rel="noopener noreferrer"
|
|||
|
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 bg-slate-100 hover:bg-slate-200 text-slate-700 text-xs font-medium rounded-lg"
|
|||
|
|
>
|
|||
|
|
<Download className="w-3.5 h-3.5" /> Скачать
|
|||
|
|
</a>
|
|||
|
|
</td>
|
|||
|
|
</tr>
|
|||
|
|
))
|
|||
|
|
)}
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|