Files
mkd/components/admin/BackupsSection.tsx
2026-02-04 00:17:04 +05:00

128 lines
4.7 KiB
TypeScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
};