Files
chat_room/src/app/(dashboard)/backups/page.tsx
T
mobiniqow 3480eac2b0 Add server monitoring and database backup pages
New sidebar sections wired to the backend's new admin APIs:
- Monitoring: live request/error-rate/latency stats plus a recent-requests
  table, polling every 8s.
- Backups: create/list/download database backups (auth'd blob download
  since the endpoint requires a Bearer token, not a plain link).
2026-09-19 22:17:44 +03:30

130 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
"use client";
import { useCallback, useEffect, useState } from "react";
import { Database, Download, HardDrive, RefreshCw } from "lucide-react";
import { Card } from "@/components/ui/Card";
import { Button } from "@/components/ui/Button";
import { ErrorBanner } from "@/components/ui/ErrorBanner";
import { FullScreenLoader } from "@/components/ui/FullScreenLoader";
import { useToast } from "@/components/ui/Toast";
import { formatApiError } from "@/lib/hooks";
import { BackupInfo, listBackups, createBackup, downloadBackup } from "@/lib/api";
function formatSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
export default function BackupsPage() {
const toast = useToast();
const [backups, setBackups] = useState<BackupInfo[] | null>(null);
const [error, setError] = useState<string | null>(null);
const [creating, setCreating] = useState(false);
const [downloadingFile, setDownloadingFile] = useState<string | null>(null);
const load = useCallback(async () => {
try {
setBackups(await listBackups());
setError(null);
} catch (err) {
setError(formatApiError(err));
}
}, []);
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
load();
}, [load]);
async function handleCreate() {
setCreating(true);
try {
await createBackup();
toast.showSuccess("بک‌آپ جدید با موفقیت ساخته شد");
await load();
} catch (err) {
toast.showError(formatApiError(err));
} finally {
setCreating(false);
}
}
async function handleDownload(filename: string) {
setDownloadingFile(filename);
try {
await downloadBackup(filename);
} catch (err) {
toast.showError(formatApiError(err));
} finally {
setDownloadingFile(null);
}
}
if (!backups && !error) return <FullScreenLoader label="لیست بک‌آپ‌ها رو میاریم..." />;
return (
<div className="flex flex-col gap-6 max-w-3xl">
<div className="flex items-center justify-between flex-wrap gap-3">
<div>
<h1 className="font-[var(--font-display)] font-extrabold text-[26px]">
بک‌آپ دیتابیس 💾
</h1>
<p className="text-[14px] text-(--color-content-secondary) mt-1">
یک نسخه‌ی کامل و فشرده از دیتابیس بگیر یا نسخه‌های قبلی رو دانلود کن.
</p>
</div>
<div className="flex items-center gap-2">
<Button variant="tertiary" size="sm" icon={<RefreshCw size={14} />} onClick={load}>
تازه‌سازی
</Button>
<Button size="sm" icon={<Database size={15} />} isLoading={creating} onClick={handleCreate}>
ساخت بک‌آپ جدید
</Button>
</div>
</div>
{error && <ErrorBanner message={error} />}
<Card padded={false} className="overflow-hidden">
{!backups || backups.length === 0 ? (
<div className="flex flex-col items-center gap-2 py-14 text-center">
<HardDrive size={28} className="text-(--color-content-tertiary)" />
<p className="text-[14px] font-bold text-(--color-content-primary)">
هنوز هیچ بک‌آپی گرفته نشده
</p>
<p className="text-[13px] text-(--color-content-tertiary)">
با دکمه‌ی «ساخت بک‌آپ جدید» اولین نسخه رو بگیر.
</p>
</div>
) : (
<div className="divide-y divide-(--color-border-subtle)">
{backups.map((b) => (
<div key={b.filename} className="flex items-center justify-between px-5 py-4 gap-3">
<div className="min-w-0">
<p dir="ltr" className="font-bold text-[13px] text-(--color-content-primary) truncate">
{b.filename}
</p>
<p className="text-[12px] text-(--color-content-tertiary) mt-0.5">
{new Date(b.created_at).toLocaleString("fa-IR")} · {formatSize(b.size_bytes)}
</p>
</div>
<Button
variant="tertiary"
size="sm"
icon={<Download size={14} />}
isLoading={downloadingFile === b.filename}
onClick={() => handleDownload(b.filename)}
className="shrink-0"
>
دانلود
</Button>
</div>
))}
</div>
)}
</Card>
</div>
);
}