mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 13:31:12 -05:00
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:
@@ -70,6 +70,7 @@ const AdminUsers = lazy(() => import("./pages/AdminUsers"));
|
|||||||
const AdminBlog = lazy(() => import("./pages/AdminBlog"));
|
const AdminBlog = lazy(() => import("./pages/AdminBlog"));
|
||||||
const AdminSettings = lazy(() => import("./pages/AdminSettings"));
|
const AdminSettings = lazy(() => import("./pages/AdminSettings"));
|
||||||
const AdminDatabaseStats = lazy(() => import("./pages/AdminDatabaseStats"));
|
const AdminDatabaseStats = lazy(() => import("./pages/AdminDatabaseStats"));
|
||||||
|
const DatabaseMaintenance = lazy(() => import("./pages/admin/DatabaseMaintenance"));
|
||||||
const AdminContact = lazy(() => import("./pages/admin/AdminContact"));
|
const AdminContact = lazy(() => import("./pages/admin/AdminContact"));
|
||||||
const AdminEmailSettings = lazy(() => import("./pages/admin/AdminEmailSettings"));
|
const AdminEmailSettings = lazy(() => import("./pages/admin/AdminEmailSettings"));
|
||||||
const ErrorMonitoring = lazy(() => import("./pages/admin/ErrorMonitoring"));
|
const ErrorMonitoring = lazy(() => import("./pages/admin/ErrorMonitoring"));
|
||||||
@@ -423,6 +424,14 @@ function AppContent(): React.JSX.Element {
|
|||||||
</AdminErrorBoundary>
|
</AdminErrorBoundary>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/admin/database-maintenance"
|
||||||
|
element={
|
||||||
|
<AdminErrorBoundary section="Database Maintenance">
|
||||||
|
<DatabaseMaintenance />
|
||||||
|
</AdminErrorBoundary>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Utility routes - lazy loaded */}
|
{/* Utility routes - lazy loaded */}
|
||||||
<Route path="/force-logout" element={<ForceLogout />} />
|
<Route path="/force-logout" element={<ForceLogout />} />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LayoutDashboard, FileText, Flag, Users, Settings, ArrowLeft, ScrollText, BookOpen, Inbox, Mail, AlertTriangle, Shield, Activity, BarChart } from 'lucide-react';
|
import { LayoutDashboard, FileText, Flag, Users, Settings, ArrowLeft, ScrollText, BookOpen, Inbox, Mail, AlertTriangle, Shield, Activity, BarChart, Database } from 'lucide-react';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import { useUserRole } from '@/hooks/useUserRole';
|
import { useUserRole } from '@/hooks/useUserRole';
|
||||||
import { useSidebar } from '@/hooks/useSidebar';
|
import { useSidebar } from '@/hooks/useSidebar';
|
||||||
@@ -73,6 +73,12 @@ export function AdminSidebar() {
|
|||||||
url: '/admin/database-stats',
|
url: '/admin/database-stats',
|
||||||
icon: BarChart,
|
icon: BarChart,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Database Maintenance',
|
||||||
|
url: '/admin/database-maintenance',
|
||||||
|
icon: Database,
|
||||||
|
visible: isSuperuser, // Only superusers can access
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Users',
|
title: 'Users',
|
||||||
url: '/admin/users',
|
url: '/admin/users',
|
||||||
@@ -134,7 +140,7 @@ export function AdminSidebar() {
|
|||||||
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
|
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
|
||||||
<SidebarGroupContent>
|
<SidebarGroupContent>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{navItems.map((item) => (
|
{navItems.filter(item => item.visible !== false).map((item) => (
|
||||||
<SidebarMenuItem key={item.url}>
|
<SidebarMenuItem key={item.url}>
|
||||||
<SidebarMenuButton asChild tooltip={collapsed ? item.title : undefined}>
|
<SidebarMenuButton asChild tooltip={collapsed ? item.title : undefined}>
|
||||||
<NavLink
|
<NavLink
|
||||||
|
|||||||
135
src/hooks/admin/useDatabaseMaintenance.ts
Normal file
135
src/hooks/admin/useDatabaseMaintenance.ts
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
|
import { queryKeys } from '@/lib/queryKeys';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export interface MaintenanceTable {
|
||||||
|
table_name: string;
|
||||||
|
row_count: number;
|
||||||
|
table_size: string;
|
||||||
|
indexes_size: string;
|
||||||
|
total_size: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MaintenanceResult {
|
||||||
|
table_name: string;
|
||||||
|
operation: string;
|
||||||
|
started_at: string;
|
||||||
|
completed_at: string;
|
||||||
|
duration_ms: number;
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMaintenanceTables() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: queryKeys.admin.maintenanceTables(),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data, error } = await supabase.rpc('get_maintenance_tables');
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return data as unknown as MaintenanceTable[];
|
||||||
|
},
|
||||||
|
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useVacuumTable() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (tableName: string) => {
|
||||||
|
const { data, error } = await supabase.rpc('run_vacuum_table', {
|
||||||
|
table_name: tableName,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return data as unknown as MaintenanceResult;
|
||||||
|
},
|
||||||
|
onSuccess: (result) => {
|
||||||
|
if (result.success) {
|
||||||
|
toast.success(`Vacuum completed on ${result.table_name}`, {
|
||||||
|
description: `Duration: ${Math.round(result.duration_ms)}ms`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.error(`Vacuum failed on ${result.table_name}`, {
|
||||||
|
description: result.error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
queryClient.invalidateQueries({ queryKey: queryKeys.admin.maintenanceTables() });
|
||||||
|
},
|
||||||
|
onError: (error: Error) => {
|
||||||
|
toast.error('Vacuum operation failed', {
|
||||||
|
description: error.message,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAnalyzeTable() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (tableName: string) => {
|
||||||
|
const { data, error } = await supabase.rpc('run_analyze_table', {
|
||||||
|
table_name: tableName,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return data as unknown as MaintenanceResult;
|
||||||
|
},
|
||||||
|
onSuccess: (result) => {
|
||||||
|
if (result.success) {
|
||||||
|
toast.success(`Analyze completed on ${result.table_name}`, {
|
||||||
|
description: `Duration: ${Math.round(result.duration_ms)}ms`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.error(`Analyze failed on ${result.table_name}`, {
|
||||||
|
description: result.error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
queryClient.invalidateQueries({ queryKey: queryKeys.admin.maintenanceTables() });
|
||||||
|
},
|
||||||
|
onError: (error: Error) => {
|
||||||
|
toast.error('Analyze operation failed', {
|
||||||
|
description: error.message,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useReindexTable() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (tableName: string) => {
|
||||||
|
const { data, error } = await supabase.rpc('run_reindex_table', {
|
||||||
|
table_name: tableName,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return data as unknown as MaintenanceResult;
|
||||||
|
},
|
||||||
|
onSuccess: (result) => {
|
||||||
|
if (result.success) {
|
||||||
|
toast.success(`Reindex completed on ${result.table_name}`, {
|
||||||
|
description: `Duration: ${Math.round(result.duration_ms)}ms`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.error(`Reindex failed on ${result.table_name}`, {
|
||||||
|
description: result.error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
queryClient.invalidateQueries({ queryKey: queryKeys.admin.maintenanceTables() });
|
||||||
|
},
|
||||||
|
onError: (error: Error) => {
|
||||||
|
toast.error('Reindex operation failed', {
|
||||||
|
description: error.message,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -6853,6 +6853,16 @@ export type Database = {
|
|||||||
Args: { _profile_user_id: string; _viewer_id?: string }
|
Args: { _profile_user_id: string; _viewer_id?: string }
|
||||||
Returns: Json
|
Returns: Json
|
||||||
}
|
}
|
||||||
|
get_maintenance_tables: {
|
||||||
|
Args: never
|
||||||
|
Returns: {
|
||||||
|
indexes_size: string
|
||||||
|
row_count: number
|
||||||
|
table_name: string
|
||||||
|
table_size: string
|
||||||
|
total_size: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
get_my_sessions: {
|
get_my_sessions: {
|
||||||
Args: never
|
Args: never
|
||||||
Returns: {
|
Returns: {
|
||||||
@@ -7115,6 +7125,7 @@ export type Database = {
|
|||||||
Returns: string
|
Returns: string
|
||||||
}
|
}
|
||||||
run_all_cleanup_jobs: { Args: never; Returns: Json }
|
run_all_cleanup_jobs: { Args: never; Returns: Json }
|
||||||
|
run_analyze_table: { Args: { table_name: string }; Returns: Json }
|
||||||
run_data_retention_cleanup: { Args: never; Returns: Json }
|
run_data_retention_cleanup: { Args: never; Returns: Json }
|
||||||
run_pipeline_monitoring: {
|
run_pipeline_monitoring: {
|
||||||
Args: never
|
Args: never
|
||||||
@@ -7124,6 +7135,7 @@ export type Database = {
|
|||||||
status: string
|
status: string
|
||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
|
run_reindex_table: { Args: { table_name: string }; Returns: Json }
|
||||||
run_system_maintenance: {
|
run_system_maintenance: {
|
||||||
Args: never
|
Args: never
|
||||||
Returns: {
|
Returns: {
|
||||||
@@ -7132,6 +7144,7 @@ export type Database = {
|
|||||||
task: string
|
task: string
|
||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
|
run_vacuum_table: { Args: { table_name: string }; Returns: Json }
|
||||||
set_config_value: {
|
set_config_value: {
|
||||||
Args: {
|
Args: {
|
||||||
is_local?: boolean
|
is_local?: boolean
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ export const queryKeys = {
|
|||||||
admin: {
|
admin: {
|
||||||
databaseStats: () => ['admin', 'database-stats'] as const,
|
databaseStats: () => ['admin', 'database-stats'] as const,
|
||||||
recentAdditions: (limit: number) => ['admin', 'recent-additions', limit] as const,
|
recentAdditions: (limit: number) => ['admin', 'recent-additions', limit] as const,
|
||||||
|
maintenanceTables: () => ['admin', 'maintenance-tables'] as const,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Analytics queries
|
// Analytics queries
|
||||||
|
|||||||
224
src/pages/admin/DatabaseMaintenance.tsx
Normal file
224
src/pages/admin/DatabaseMaintenance.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
-- Database Maintenance Functions
|
||||||
|
-- These functions allow authorized users to perform database maintenance operations
|
||||||
|
|
||||||
|
-- Function to run VACUUM on a specific table
|
||||||
|
CREATE OR REPLACE FUNCTION run_vacuum_table(table_name text)
|
||||||
|
RETURNS jsonb
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
result jsonb;
|
||||||
|
start_time timestamp;
|
||||||
|
end_time timestamp;
|
||||||
|
BEGIN
|
||||||
|
-- Only allow superusers to run this
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM profiles
|
||||||
|
WHERE id = auth.uid()
|
||||||
|
AND role = 'superuser'
|
||||||
|
) THEN
|
||||||
|
RAISE EXCEPTION 'Only superusers can perform database maintenance';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
start_time := clock_timestamp();
|
||||||
|
|
||||||
|
-- Execute VACUUM on the specified table
|
||||||
|
EXECUTE format('VACUUM ANALYZE %I', table_name);
|
||||||
|
|
||||||
|
end_time := clock_timestamp();
|
||||||
|
|
||||||
|
result := jsonb_build_object(
|
||||||
|
'table_name', table_name,
|
||||||
|
'operation', 'VACUUM ANALYZE',
|
||||||
|
'started_at', start_time,
|
||||||
|
'completed_at', end_time,
|
||||||
|
'duration_ms', EXTRACT(MILLISECONDS FROM (end_time - start_time)),
|
||||||
|
'success', true
|
||||||
|
);
|
||||||
|
|
||||||
|
RETURN result;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
RETURN jsonb_build_object(
|
||||||
|
'table_name', table_name,
|
||||||
|
'operation', 'VACUUM ANALYZE',
|
||||||
|
'success', false,
|
||||||
|
'error', SQLERRM
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Function to run ANALYZE on a specific table
|
||||||
|
CREATE OR REPLACE FUNCTION run_analyze_table(table_name text)
|
||||||
|
RETURNS jsonb
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
result jsonb;
|
||||||
|
start_time timestamp;
|
||||||
|
end_time timestamp;
|
||||||
|
BEGIN
|
||||||
|
-- Only allow superusers to run this
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM profiles
|
||||||
|
WHERE id = auth.uid()
|
||||||
|
AND role = 'superuser'
|
||||||
|
) THEN
|
||||||
|
RAISE EXCEPTION 'Only superusers can perform database maintenance';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
start_time := clock_timestamp();
|
||||||
|
|
||||||
|
-- Execute ANALYZE on the specified table
|
||||||
|
EXECUTE format('ANALYZE %I', table_name);
|
||||||
|
|
||||||
|
end_time := clock_timestamp();
|
||||||
|
|
||||||
|
result := jsonb_build_object(
|
||||||
|
'table_name', table_name,
|
||||||
|
'operation', 'ANALYZE',
|
||||||
|
'started_at', start_time,
|
||||||
|
'completed_at', end_time,
|
||||||
|
'duration_ms', EXTRACT(MILLISECONDS FROM (end_time - start_time)),
|
||||||
|
'success', true
|
||||||
|
);
|
||||||
|
|
||||||
|
RETURN result;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
RETURN jsonb_build_object(
|
||||||
|
'table_name', table_name,
|
||||||
|
'operation', 'ANALYZE',
|
||||||
|
'success', false,
|
||||||
|
'error', SQLERRM
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Function to run REINDEX on a specific table
|
||||||
|
CREATE OR REPLACE FUNCTION run_reindex_table(table_name text)
|
||||||
|
RETURNS jsonb
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
result jsonb;
|
||||||
|
start_time timestamp;
|
||||||
|
end_time timestamp;
|
||||||
|
BEGIN
|
||||||
|
-- Only allow superusers to run this
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM profiles
|
||||||
|
WHERE id = auth.uid()
|
||||||
|
AND role = 'superuser'
|
||||||
|
) THEN
|
||||||
|
RAISE EXCEPTION 'Only superusers can perform database maintenance';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
start_time := clock_timestamp();
|
||||||
|
|
||||||
|
-- Execute REINDEX on the specified table
|
||||||
|
EXECUTE format('REINDEX TABLE %I', table_name);
|
||||||
|
|
||||||
|
end_time := clock_timestamp();
|
||||||
|
|
||||||
|
result := jsonb_build_object(
|
||||||
|
'table_name', table_name,
|
||||||
|
'operation', 'REINDEX',
|
||||||
|
'started_at', start_time,
|
||||||
|
'completed_at', end_time,
|
||||||
|
'duration_ms', EXTRACT(MILLISECONDS FROM (end_time - start_time)),
|
||||||
|
'success', true
|
||||||
|
);
|
||||||
|
|
||||||
|
RETURN result;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
RETURN jsonb_build_object(
|
||||||
|
'table_name', table_name,
|
||||||
|
'operation', 'REINDEX',
|
||||||
|
'success', false,
|
||||||
|
'error', SQLERRM
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Function to get list of tables that can be maintained
|
||||||
|
CREATE OR REPLACE FUNCTION get_maintenance_tables()
|
||||||
|
RETURNS TABLE (
|
||||||
|
table_name text,
|
||||||
|
row_count bigint,
|
||||||
|
table_size text,
|
||||||
|
indexes_size text,
|
||||||
|
total_size text
|
||||||
|
)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
-- Only allow superusers to view this
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM profiles
|
||||||
|
WHERE id = auth.uid()
|
||||||
|
AND role = 'superuser'
|
||||||
|
) THEN
|
||||||
|
RAISE EXCEPTION 'Only superusers can view database maintenance information';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT
|
||||||
|
t.tablename::text,
|
||||||
|
(xpath('/row/cnt/text()', xml_count))[1]::text::bigint as row_count,
|
||||||
|
pg_size_pretty(pg_total_relation_size(quote_ident(t.tablename)::regclass) - pg_indexes_size(quote_ident(t.tablename)::regclass)) as table_size,
|
||||||
|
pg_size_pretty(pg_indexes_size(quote_ident(t.tablename)::regclass)) as indexes_size,
|
||||||
|
pg_size_pretty(pg_total_relation_size(quote_ident(t.tablename)::regclass)) as total_size
|
||||||
|
FROM pg_tables t
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT table_to_xml(t.tablename::regclass, false, true, '') as xml_count
|
||||||
|
) x ON true
|
||||||
|
WHERE t.schemaname = 'public'
|
||||||
|
ORDER BY pg_total_relation_size(quote_ident(t.tablename)::regclass) DESC;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
Reference in New Issue
Block a user