import { useState } from 'react'; import { useAuthStore } from '../../auth/auth-store'; import { StatusBadge } from '../../components/admin/StatusBadge'; import { ConfirmDeleteDialog } from '../../components/admin/ConfirmDeleteDialog'; import { useDatabaseStatus, useDatabasePool, useDatabaseTables, useDatabaseQueries, useKillQuery, } from '../../api/queries/admin/database'; import { useThresholds, useSaveThresholds, type ThresholdConfig } from '../../api/queries/admin/thresholds'; import layout from '../../styles/AdminLayout.module.css'; import styles from './DatabaseAdminPage.module.css'; type Section = 'pool' | 'tables' | 'queries' | 'maintenance' | 'thresholds'; interface SectionDef { id: Section; label: string; icon: string; } const SECTIONS: SectionDef[] = [ { id: 'pool', label: 'Connection Pool', icon: 'CP' }, { id: 'tables', label: 'Table Sizes', icon: 'TS' }, { id: 'queries', label: 'Active Queries', icon: 'AQ' }, { id: 'maintenance', label: 'Maintenance', icon: 'MN' }, { id: 'thresholds', label: 'Thresholds', icon: 'TH' }, ]; export function DatabaseAdminPage() { const roles = useAuthStore((s) => s.roles); if (!roles.includes('ADMIN')) { return (
Access Denied -- this page requires the ADMIN role.
); } return ; } function DatabaseAdminContent() { const [selectedSection, setSelectedSection] = useState
('pool'); const status = useDatabaseStatus(); const pool = useDatabasePool(); const tables = useDatabaseTables(); const queries = useDatabaseQueries(); const thresholds = useThresholds(); if (status.isLoading) { return (
Loading...
); } const db = status.data; function getMiniStatus(section: Section): string { switch (section) { case 'pool': { const d = pool.data; if (!d) return '--'; const pct = d.maxPoolSize > 0 ? Math.round((d.activeConnections / d.maxPoolSize) * 100) : 0; return `${pct}%`; } case 'tables': return tables.data ? `${tables.data.length}` : '--'; case 'queries': return queries.data ? `${queries.data.length}` : '--'; case 'maintenance': return 'Coming soon'; case 'thresholds': return thresholds.data ? 'Configured' : '--'; } } return (
Database
{db?.version && {db.version}} {db?.host && {db.host}} {db?.schema && Schema: {db.schema}}
{SECTIONS.map((sec) => (
setSelectedSection(sec.id)} >
{sec.icon}
{sec.label}
{getMiniStatus(sec.id)}
))}
{selectedSection === 'pool' && ( )} {selectedSection === 'tables' && } {selectedSection === 'queries' && ( )} {selectedSection === 'maintenance' && } {selectedSection === 'thresholds' && ( )}
); } function PoolSection({ pool, warningPct, criticalPct, }: { pool: ReturnType; warningPct?: number; criticalPct?: number; }) { const data = pool.data; if (!data) return null; const usagePct = data.maxPoolSize > 0 ? Math.round((data.activeConnections / data.maxPoolSize) * 100) : 0; const barColor = criticalPct && usagePct >= criticalPct ? '#ef4444' : warningPct && usagePct >= warningPct ? '#eab308' : '#22c55e'; return ( <>
Connection Pool
{data.activeConnections} / {data.maxPoolSize} connections {usagePct}%
{data.activeConnections} Active
{data.idleConnections} Idle
{data.pendingThreads} Pending
{data.maxWaitMs}ms Max Wait
); } function TablesSection({ tables }: { tables: ReturnType }) { const data = tables.data; return ( <>
Table Sizes
{!data ? (
Loading...
) : (
{data.map((t) => ( ))}
Table Rows Data Size Index Size
{t.tableName} {t.rowCount.toLocaleString()} {t.dataSize} {t.indexSize}
)} ); } function QueriesSection({ queries, warningSeconds, }: { queries: ReturnType; warningSeconds?: number; }) { const [killTarget, setKillTarget] = useState(null); const killMutation = useKillQuery(); const data = queries.data; const warningSec = warningSeconds ?? 30; return ( <>
Active Queries
{!data || data.length === 0 ? (
No active queries
) : (
{data.map((q) => ( warningSec ? styles.rowWarning : undefined} > ))}
PID Duration State Query
{q.pid} {formatDuration(q.durationSeconds)} {q.state} {q.query.length > 100 ? `${q.query.slice(0, 100)}...` : q.query}
)} setKillTarget(null)} onConfirm={() => { if (killTarget !== null) { killMutation.mutate(killTarget); setKillTarget(null); } }} resourceName={String(killTarget ?? '')} resourceType="query (PID)" /> ); } function MaintenanceSection() { return ( <>
Maintenance
); } function ThresholdsSection({ thresholds }: { thresholds?: ThresholdConfig }) { const [form, setForm] = useState(null); const saveMutation = useSaveThresholds(); const [status, setStatus] = useState<{ type: 'success' | 'error'; msg: string } | null>(null); const current = form ?? thresholds; if (!current) return null; function updateDb(key: keyof ThresholdConfig['database'], value: number) { setForm((prev) => { const base = prev ?? thresholds!; return { ...base, database: { ...base.database, [key]: value } }; }); } async function handleSave() { if (!form && !thresholds) return; const data = form ?? thresholds!; try { await saveMutation.mutateAsync(data); setStatus({ type: 'success', msg: 'Thresholds saved.' }); setTimeout(() => setStatus(null), 3000); } catch { setStatus({ type: 'error', msg: 'Failed to save thresholds.' }); } } return ( <>
Thresholds
updateDb('connectionPoolWarning', Number(e.target.value))} />
updateDb('connectionPoolCritical', Number(e.target.value))} />
updateDb('queryDurationWarning', Number(e.target.value))} />
updateDb('queryDurationCritical', Number(e.target.value))} />
{status && ( {status.msg} )}
); } function formatDuration(seconds: number): string { if (seconds < 1) return `${Math.round(seconds * 1000)}ms`; const s = Math.floor(seconds); if (s < 60) return `${s}s`; const m = Math.floor(s / 60); return `${m}m ${s % 60}s`; }