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 (
);
}
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}
{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...
) : (
| Table |
Rows |
Data Size |
Index Size |
{data.map((t) => (
| {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
) : (
| PID |
Duration |
State |
Query |
|
{data.map((q) => (
warningSec ? styles.rowWarning : undefined}
>
| {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
{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`;
}