Add database maintenance tooling

- Implement maintenance hooks (useMaintenanceTables, useVacuumTable, useAnalyzeTable, useReindexTable)
- Add DatabaseMaintenance page and UI for vacuum/analyze/reindex
- Wire new route / admin/database-maintenance and sidebar entry
- Remove DatabaseMaintenance icon usage on page and align with AdminLayout props
This commit is contained in:
gpt-engineer-app[bot]
2025-11-12 01:39:05 +00:00
parent 2468d3cc18
commit e2ee11b9f5
7 changed files with 578 additions and 2 deletions

View File

@@ -0,0 +1,224 @@
import { useState } from 'react';
import { AdminLayout } from '@/components/layout/AdminLayout';
import { AdminPageLayout } from '@/components/admin';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import {
useMaintenanceTables,
useVacuumTable,
useAnalyzeTable,
useReindexTable,
} from '@/hooks/admin/useDatabaseMaintenance';
import { Database, RefreshCw, Zap, Settings, AlertTriangle } from 'lucide-react';
import { Skeleton } from '@/components/ui/skeleton';
export default function DatabaseMaintenance() {
const { data: tables, isLoading, refetch } = useMaintenanceTables();
const vacuumMutation = useVacuumTable();
const analyzeMutation = useAnalyzeTable();
const reindexMutation = useReindexTable();
const [selectedTable, setSelectedTable] = useState<string | null>(null);
const handleVacuum = (tableName: string) => {
setSelectedTable(tableName);
vacuumMutation.mutate(tableName);
};
const handleAnalyze = (tableName: string) => {
setSelectedTable(tableName);
analyzeMutation.mutate(tableName);
};
const handleReindex = (tableName: string) => {
setSelectedTable(tableName);
reindexMutation.mutate(tableName);
};
const isOperationPending = (tableName: string) => {
return (
selectedTable === tableName &&
(vacuumMutation.isPending || analyzeMutation.isPending || reindexMutation.isPending)
);
};
return (
<AdminLayout>
<AdminPageLayout
title="Database Maintenance"
description="Run vacuum, analyze, and reindex operations on database tables"
>
<Alert variant="default" className="mb-6">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>Superuser Access Required</AlertTitle>
<AlertDescription>
These operations require superuser privileges. They can help improve database
performance by reclaiming storage, updating statistics, and rebuilding indexes.
</AlertDescription>
</Alert>
<div className="grid gap-6 mb-6 md:grid-cols-3">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">VACUUM</CardTitle>
<Database className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<CardDescription className="text-xs">
Reclaims storage occupied by dead tuples and makes space available for reuse
</CardDescription>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">ANALYZE</CardTitle>
<Zap className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<CardDescription className="text-xs">
Updates statistics used by the query planner for optimal query execution plans
</CardDescription>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">REINDEX</CardTitle>
<Settings className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<CardDescription className="text-xs">
Rebuilds indexes to eliminate bloat and restore optimal index performance
</CardDescription>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Database Tables</CardTitle>
<CardDescription>
Select maintenance operations to perform on each table
</CardDescription>
</div>
<Button
variant="outline"
size="sm"
onClick={() => refetch()}
disabled={isLoading}
>
<RefreshCw className={`mr-2 h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
Refresh
</Button>
</div>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="space-y-4">
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} className="h-12 w-full" />
))}
</div>
) : tables && tables.length > 0 ? (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Table Name</TableHead>
<TableHead className="text-right">Rows</TableHead>
<TableHead className="text-right">Table Size</TableHead>
<TableHead className="text-right">Indexes Size</TableHead>
<TableHead className="text-right">Total Size</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{tables.map((table) => (
<TableRow key={table.table_name}>
<TableCell className="font-medium">
<code className="text-sm">{table.table_name}</code>
</TableCell>
<TableCell className="text-right">
{table.row_count?.toLocaleString() || 'N/A'}
</TableCell>
<TableCell className="text-right">
<Badge variant="secondary">{table.table_size}</Badge>
</TableCell>
<TableCell className="text-right">
<Badge variant="secondary">{table.indexes_size}</Badge>
</TableCell>
<TableCell className="text-right">
<Badge variant="outline">{table.total_size}</Badge>
</TableCell>
<TableCell className="text-right">
<div className="flex gap-2 justify-end">
<Button
variant="outline"
size="sm"
onClick={() => handleVacuum(table.table_name)}
disabled={isOperationPending(table.table_name)}
>
{isOperationPending(table.table_name) &&
vacuumMutation.isPending ? (
<RefreshCw className="h-3 w-3 animate-spin" />
) : (
'Vacuum'
)}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleAnalyze(table.table_name)}
disabled={isOperationPending(table.table_name)}
>
{isOperationPending(table.table_name) &&
analyzeMutation.isPending ? (
<RefreshCw className="h-3 w-3 animate-spin" />
) : (
'Analyze'
)}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleReindex(table.table_name)}
disabled={isOperationPending(table.table_name)}
>
{isOperationPending(table.table_name) &&
reindexMutation.isPending ? (
<RefreshCw className="h-3 w-3 animate-spin" />
) : (
'Reindex'
)}
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
) : (
<div className="text-center py-12 text-muted-foreground">
No tables available
</div>
)}
</CardContent>
</Card>
</AdminPageLayout>
</AdminLayout>
);
}