feat: Add database stats tab to admin dashboard with PostgreSQL support

This commit is contained in:
Your Name
2026-03-31 22:58:50 +02:00
parent d4236b0757
commit 33ccb4fde5
2 changed files with 102 additions and 1 deletions

View File

@@ -42,10 +42,12 @@ import {
type InternalAdminContact, type InternalAdminContact,
type InternalAdminOverview, type InternalAdminOverview,
type InternalAdminUser, type InternalAdminUser,
getDatabaseStats,
type DatabaseStats,
} from '@/services/api'; } from '@/services/api';
import { useAuthStore } from '@/stores/authStore'; import { useAuthStore } from '@/stores/authStore';
type AdminTab = 'overview' | 'users' | 'tools' | 'ratings' | 'contacts' | 'system'; type AdminTab = 'overview' | 'users' | 'tools' | 'ratings' | 'contacts' | 'system' | 'database';
type Lang = 'ar' | 'en'; type Lang = 'ar' | 'en';
const TRANSLATIONS: Record<Lang, Record<string, string>> = { const TRANSLATIONS: Record<Lang, Record<string, string>> = {
@@ -75,6 +77,7 @@ const TRANSLATIONS: Record<Lang, Record<string, string>> = {
tabRatings: 'Ratings & Reviews', tabRatings: 'Ratings & Reviews',
tabContacts: 'Inbox', tabContacts: 'Inbox',
tabSystem: 'System Health', tabSystem: 'System Health',
tabDatabase: 'Database',
// Overview cards // Overview cards
totalUsers: 'Total users', totalUsers: 'Total users',
filesProcessed: 'Files processed', filesProcessed: 'Files processed',
@@ -216,6 +219,7 @@ const TRANSLATIONS: Record<Lang, Record<string, string>> = {
tabRatings: 'التقييمات والمراجعات', tabRatings: 'التقييمات والمراجعات',
tabContacts: 'صندوق الوارد', tabContacts: 'صندوق الوارد',
tabSystem: 'صحة النظام', tabSystem: 'صحة النظام',
tabDatabase: 'قاعدة البيانات',
// Overview cards // Overview cards
totalUsers: 'إجمالي المستخدمين', totalUsers: 'إجمالي المستخدمين',
filesProcessed: 'الملفات المعالجة', filesProcessed: 'الملفات المعالجة',
@@ -396,6 +400,9 @@ export default function InternalAdminPage() {
// Plan interest state // Plan interest state
const [planInterest, setPlanInterest] = useState<AdminPlanInterest | null>(null); const [planInterest, setPlanInterest] = useState<AdminPlanInterest | null>(null);
// Database state
const [databaseStats, setDatabaseStats] = useState<DatabaseStats | null>(null);
// Language // Language
const [lang, setLang] = useState<Lang>(() => (localStorage.getItem('admin-lang') as Lang) ?? 'en'); const [lang, setLang] = useState<Lang>(() => (localStorage.getItem('admin-lang') as Lang) ?? 'en');
const isRtl = lang === 'ar'; const isRtl = lang === 'ar';
@@ -423,6 +430,7 @@ export default function InternalAdminPage() {
{ key: 'ratings', label: t('tabRatings'), icon: Star }, { key: 'ratings', label: t('tabRatings'), icon: Star },
{ key: 'contacts', label: t('tabContacts'), icon: Inbox }, { key: 'contacts', label: t('tabContacts'), icon: Inbox },
{ key: 'system', label: t('tabSystem'), icon: ShieldCheck }, { key: 'system', label: t('tabSystem'), icon: ShieldCheck },
{ key: 'database', label: t('tabDatabase'), icon: Database },
]; ];
useEffect(() => { useEffect(() => {
@@ -486,6 +494,11 @@ export default function InternalAdminPage() {
setPlanInterest(pi); setPlanInterest(pi);
break; break;
} }
case 'database': {
const dbStats = await getDatabaseStats();
setDatabaseStats(dbStats);
break;
}
} }
} catch (e) { } catch (e) {
const msg = e instanceof Error ? e.message : t('loadError'); const msg = e instanceof Error ? e.message : t('loadError');
@@ -1398,6 +1411,76 @@ export default function InternalAdminPage() {
); );
} }
// ====================== DATABASE TAB ======================
function renderDatabaseTab() {
if (!databaseStats) return null;
return (
<>
<section className="grid gap-4 md:grid-cols-3">
<article className="rounded-3xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-sm font-medium text-slate-500 dark:text-slate-400">Database Type</p>
<p className="mt-3 text-2xl font-bold capitalize text-slate-900 dark:text-white">{databaseStats.database_type}</p>
</div>
<Database className="h-5 w-5 text-slate-400" />
</div>
</article>
<article className="rounded-3xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-sm font-medium text-slate-500 dark:text-slate-400">Total Tables</p>
<p className="mt-3 text-2xl font-bold text-slate-900 dark:text-white">{databaseStats.table_count}</p>
</div>
<Database className="h-5 w-5 text-slate-400" />
</div>
</article>
<article className="rounded-3xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-sm font-medium text-slate-500 dark:text-slate-400">Total Rows</p>
<p className="mt-3 text-2xl font-bold text-slate-900 dark:text-white">
{databaseStats.tables.reduce((sum, t) => sum + t.row_count, 0).toLocaleString()}
</p>
</div>
<Database className="h-5 w-5 text-slate-400" />
</div>
</article>
</section>
<article className="rounded-3xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
<h2 className="text-xl font-bold text-slate-900 dark:text-white">Tables</h2>
<div className="mt-4 overflow-x-auto">
<table className="min-w-full divide-y divide-slate-200 text-sm dark:divide-slate-700">
<thead>
<tr className="text-left text-slate-500 dark:text-slate-400">
<th className="py-2 pe-3 font-medium">Table Name</th>
<th className="py-2 pe-3 font-medium">Row Count</th>
{databaseStats.tables[0]?.total_size_kb !== undefined && (
<th className="py-2 pe-3 font-medium">Size (KB)</th>
)}
</tr>
</thead>
<tbody className="divide-y divide-slate-100 dark:divide-slate-800">
{databaseStats.tables.map((table) => (
<tr key={table.table_name} className="text-slate-700 dark:text-slate-200">
<td className="py-2 pe-3 font-mono text-xs">{table.table_name}</td>
<td className="py-2 pe-3">{table.row_count.toLocaleString()}</td>
{table.total_size_kb !== undefined && (
<td className="py-2 pe-3">{table.total_size_kb.toLocaleString()} KB</td>
)}
</tr>
))}
</tbody>
</table>
</div>
</article>
</>
);
}
// ====================== MAIN RENDER ====================== // ====================== MAIN RENDER ======================
return ( return (
@@ -1566,6 +1649,7 @@ export default function InternalAdminPage() {
{activeTab === 'ratings' && renderRatingsTab()} {activeTab === 'ratings' && renderRatingsTab()}
{activeTab === 'contacts' && renderContactsTab()} {activeTab === 'contacts' && renderContactsTab()}
{activeTab === 'system' && renderSystemTab()} {activeTab === 'system' && renderSystemTab()}
{activeTab === 'database' && renderDatabaseTab()}
</div> </div>
</> </>
)} )}

View File

@@ -876,6 +876,18 @@ export interface AdminSystemHealth {
tasks_last_1h: number; tasks_last_1h: number;
failures_last_1h: number; failures_last_1h: number;
database_size_mb: number; database_size_mb: number;
database_type: string;
}
export interface DatabaseStats {
database_type: string;
tables: Array<{
table_name: string;
row_count: number;
total_size_kb?: number;
data_size_kb?: number;
}>;
table_count: number;
} }
export async function getAdminRatingsDetail(page = 1, perPage = 20, tool = ''): Promise<AdminRatingsDetail> { export async function getAdminRatingsDetail(page = 1, perPage = 20, tool = ''): Promise<AdminRatingsDetail> {
@@ -905,6 +917,11 @@ export async function getAdminSystemHealth(): Promise<AdminSystemHealth> {
return response.data; return response.data;
} }
export async function getDatabaseStats(): Promise<DatabaseStats> {
const response = await api.get<DatabaseStats>('/internal/admin/database-stats');
return response.data;
}
// --- Account / Usage / API Keys --- // --- Account / Usage / API Keys ---
export interface UsageSummary { export interface UsageSummary {