mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-26 06:44:58 +00:00
Move 48 package-specific documentation files from /docs/packages/ to individual package /docs/ directories. This follows the proximity principle: documentation lives close to the code it describes. Breakdown by package: - admin: 5 files - audit_log: 3 files - dashboard: 5 files - data_table: 5 files - forum_forge: 5 files - irc_webchat: 5 files - media_center: 4 files - notification_center: 4 files - stream_cast: 8 files - user_manager: 4 files Files remaining in /docs/packages/: - PACKAGES_INVENTORY.md (cross-project reference) - PACKAGE_MIGRATION_ROADMAP.md (cross-project reference) - EXPORT_IMPORT_* (3 files - no package exists yet) - PACKAGEREPO_* (3 files - no package exists yet) Benefits: - Package maintainers can find related docs with package code - Easier to keep docs in sync with package changes - Reduces /docs/ directory to project-wide content only Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
27 KiB
27 KiB
/admin/database Page - Code Examples & Patterns
Example 1: Basic Handler Implementation
Stats Refresh Handler
// In page.tsx
async function onRefreshStats() {
setStatsLoading(true)
setStatsError(null)
try {
const response = await fetch('/api/admin/database/stats', {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Failed to fetch stats: ${response.statusText} - ${errorText}`)
}
const data = (await response.json()) as DatabaseStats
setStats(data)
// Optional: Show success toast
console.log('[AdminDatabasePage] Stats refreshed successfully')
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
setStatsError(message)
console.error('[AdminDatabasePage] Stats refresh failed:', error)
} finally {
setStatsLoading(false)
}
}
Auto-Refresh Effect
// In page.tsx useEffect
useEffect(() => {
if (statsRefreshInterval === 'off') return
const intervals: Record<string, number> = {
'10s': 10000,
'30s': 30000,
'60s': 60000,
}
const timer = setInterval(() => {
console.log('[AutoRefresh] Refreshing stats...')
onRefreshStats()
}, intervals[statsRefreshInterval])
return () => {
clearInterval(timer)
console.log('[AutoRefresh] Cleared interval')
}
}, [statsRefreshInterval, onRefreshStats])
Example 2: Entity CRUD Operations
Load Entities with Pagination/Sorting/Filtering
async function loadEntityRecords(
entityType: string,
page: number,
pageSize: number,
filters: Record<string, string>,
sort: { column: string; order: 'asc' | 'desc' }
) {
setEntityLoading(true)
setEntityError(null)
try {
// Build query parameters
const params = new URLSearchParams({
entityType,
page: page.toString(),
pageSize: pageSize.toString(),
sort: `${sort.column}:${sort.order}`,
// Add all filters
...filters,
})
console.log(`[EntityBrowser] Loading ${entityType} records:`, {
page,
pageSize,
sort: `${sort.column}:${sort.order}`,
filters,
})
const response = await fetch(`/api/admin/database/entities?${params}`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
})
if (!response.ok) {
throw new Error(`Failed to fetch entities: ${response.statusText}`)
}
const data = (await response.json()) as {
records: EntityRecord[]
total: number
}
console.log(`[EntityBrowser] Loaded ${data.records.length} records, total: ${data.total}`)
setEntityRecords(data.records)
setEntityPagination({ page, pageSize, total: data.total })
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
setEntityError(message)
console.error('[EntityBrowser] Load failed:', error)
} finally {
setEntityLoading(false)
}
}
Handle Sorting
async function onEntitySort(column: string, order: 'asc' | 'desc') {
console.log(`[EntitySort] Sorting by ${column} ${order}`)
setEntitySort({ column, order })
// Reset to first page when sorting changes
await loadEntityRecords(
selectedEntityType,
1, // Reset to page 1
entityPagination.pageSize,
entityFilters,
{ column, order }
)
}
Handle Filtering
async function onEntityFilter(field: string, value: string) {
console.log(`[EntityFilter] Filtering ${field} = "${value}"`)
const updatedFilters = { ...entityFilters, [field]: value }
setEntityFilters(updatedFilters)
// Reset to first page when filter changes
await loadEntityRecords(
selectedEntityType,
1, // Reset to page 1
entityPagination.pageSize,
updatedFilters,
entitySort
)
}
Delete with Confirmation
async function onEntityDelete(id: string) {
// Show confirmation dialog
const confirmed = window.confirm(
`Are you sure you want to delete this ${selectedEntityType} record (ID: ${id})?\n\nThis action cannot be undone.`
)
if (!confirmed) {
console.log('[EntityDelete] User cancelled deletion')
return
}
try {
console.log(`[EntityDelete] Deleting ${selectedEntityType}/${id}`)
const response = await fetch(
`/api/admin/database/entities/${selectedEntityType}/${id}`,
{
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
}
)
if (!response.ok) {
throw new Error(`Failed to delete entity: ${response.statusText}`)
}
console.log('[EntityDelete] Successfully deleted, reloading records...')
// Reload records after successful deletion
await loadEntityRecords(
selectedEntityType,
entityPagination.page,
entityPagination.pageSize,
entityFilters,
entitySort
)
// Show success notification
// toast.success(`${selectedEntityType} record deleted`)
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
console.error('[EntityDelete] Failed:', error)
// Show error notification
// toast.error(`Failed to delete: ${message}`)
}
}
Example 3: Export Implementation
Export Handler
async function onExport() {
setExportLoading(true)
setExportError(null)
try {
console.log('[Export] Starting export:', {
format: exportFormat,
entityTypes: exportEntityTypes,
})
const response = await fetch('/api/admin/database/export', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
entityTypes: exportEntityTypes,
format: exportFormat,
}),
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Export failed: ${response.statusText} - ${errorText}`)
}
// Get filename from Content-Disposition header
const contentDisposition = response.headers.get('Content-Disposition')
let fileName = `export.${exportFormat}`
if (contentDisposition) {
const match = contentDisposition.match(/filename="([^"]+)"/)
if (match && match[1]) {
fileName = match[1]
}
}
console.log(`[Export] File name: ${fileName}`)
// Download the blob
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = fileName
link.click()
// Cleanup
window.URL.revokeObjectURL(url)
console.log('[Export] Successfully downloaded', {
fileName,
size: blob.size,
})
// Show success notification
// toast.success(`Exported ${blob.size} bytes to ${fileName}`)
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
setExportError(message)
console.error('[Export] Failed:', error)
// toast.error(`Export failed: ${message}`)
} finally {
setExportLoading(false)
}
}
Example 4: Import Implementation
Import Handler with FormData
async function onImport() {
// Validate file selection
if (!importFile) {
setImportError('Please select a file to import')
return
}
setImportLoading(true)
setImportError(null)
try {
console.log('[Import] Starting import:', {
fileName: importFile.name,
fileSize: importFile.size,
format: importFormat,
mode: importMode,
dryRun: importDryRun,
})
// Create FormData
const formData = new FormData()
formData.append('file', importFile)
formData.append('format', importFormat)
formData.append('mode', importMode)
formData.append('dryRun', importDryRun.toString())
const response = await fetch('/api/admin/database/import', {
method: 'POST',
body: formData, // Don't set Content-Type header, browser will set it with boundary
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Import failed: ${response.statusText} - ${errorText}`)
}
const results = (await response.json()) as ImportResults
console.log('[Import] Completed:', {
imported: results.imported,
skipped: results.skipped,
errors: results.errors.length,
dryRun: results.dryRun,
duration: results.duration,
})
setImportResults(results)
setShowImportResults(true)
// If not dry-run and no errors, reload entities
if (!importDryRun && results.errors.length === 0) {
console.log('[Import] Reloading entity records...')
await loadEntityRecords(
selectedEntityType,
entityPagination.page,
entityPagination.pageSize,
entityFilters,
entitySort
)
}
// Show notification
if (importDryRun) {
// toast.info(`Dry run: ${results.imported} records would be imported`)
} else {
// toast.success(`Imported ${results.imported} records`)
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
setImportError(message)
console.error('[Import] Failed:', error)
// toast.error(`Import failed: ${message}`)
} finally {
setImportLoading(false)
}
}
Example 5: Component Implementation
StatsTab Component Full Example
'use client'
import React from 'react'
import { LoadingIndicator } from '@/components/LoadingIndicator'
import { ErrorState } from '@/components/EmptyState'
import type { DatabaseStats } from '@/lib/api/admin-database-client'
interface StatsTabProps {
stats: DatabaseStats | null
loading: boolean
error: string | null
refreshInterval: 'off' | '60s' | '30s' | '10s'
onRefresh: () => Promise<void>
onRefreshIntervalChange: (interval: 'off' | '60s' | '30s' | '10s') => void
}
export function StatsTab({
stats,
loading,
error,
refreshInterval,
onRefresh,
onRefreshIntervalChange,
}: StatsTabProps) {
const healthColor = {
good: 'bg-green-100 text-green-800 border-green-300',
warning: 'bg-yellow-100 text-yellow-800 border-yellow-300',
critical: 'bg-red-100 text-red-800 border-red-300',
}[stats?.health ?? 'good']
// Initial loading state
if (loading && !stats) {
return <LoadingIndicator />
}
// Error state
if (error) {
return (
<ErrorState
title="Failed to load database statistics"
description={error}
action={{
label: 'Retry',
onClick: onRefresh,
}}
/>
)
}
// No data
if (!stats) {
return (
<ErrorState
title="No statistics available"
description="Unable to retrieve database statistics"
/>
)
}
return (
<div className="space-y-6">
{/* Control Bar */}
<div className="flex flex-wrap gap-4 items-center">
<button
onClick={onRefresh}
disabled={loading}
className={`px-4 py-2 bg-blue-500 text-white rounded font-medium transition-colors ${
loading
? 'opacity-50 cursor-not-allowed'
: 'hover:bg-blue-600 active:bg-blue-700'
}`}
>
{loading ? (
<>
<span className="inline-block mr-2">⟳</span>
Refreshing...
</>
) : (
<>
<span className="inline-block mr-2">↻</span>
Refresh
</>
)}
</button>
<select
value={refreshInterval}
onChange={(e) =>
onRefreshIntervalChange(
e.target.value as 'off' | '60s' | '30s' | '10s'
)
}
className="px-4 py-2 border border-gray-300 rounded font-medium bg-white text-gray-700 hover:border-gray-400"
>
<option value="off">Auto-refresh: Off</option>
<option value="60s">Auto-refresh: Every 60s</option>
<option value="30s">Auto-refresh: Every 30s</option>
<option value="10s">Auto-refresh: Every 10s</option>
</select>
{refreshInterval !== 'off' && (
<span className="px-3 py-1 bg-blue-100 text-blue-700 rounded text-sm font-medium">
🔄 Auto-refreshing
</span>
)}
</div>
{/* Stats Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Table Count */}
<div className="p-6 bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition-shadow">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600 font-medium">Tables</p>
<p className="text-3xl font-bold text-gray-900 mt-2">
{stats.tableCount}
</p>
</div>
<div className="text-4xl text-blue-300">📊</div>
</div>
</div>
{/* Total Records */}
<div className="p-6 bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition-shadow">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600 font-medium">Total Records</p>
<p className="text-3xl font-bold text-gray-900 mt-2">
{stats.totalRecords.toLocaleString()}
</p>
</div>
<div className="text-4xl text-green-300">📈</div>
</div>
</div>
{/* Storage Size */}
<div className="p-6 bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition-shadow">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600 font-medium">Storage Size</p>
<p className="text-3xl font-bold text-gray-900 mt-2">
{(stats.storageSize / 1024 / 1024).toFixed(2)} MB
</p>
</div>
<div className="text-4xl text-purple-300">💾</div>
</div>
</div>
{/* Last Vacuum */}
<div className="p-6 bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition-shadow">
<p className="text-sm text-gray-600 font-medium">Last Vacuum</p>
<p className="text-lg font-bold text-gray-900 mt-2 font-mono">
{stats.lastVacuum
? new Date(stats.lastVacuum).toLocaleDateString('en-US', {
weekday: 'short',
month: 'short',
day: 'numeric',
year: 'numeric',
})
: 'Never'}
</p>
<p className="text-xs text-gray-500 mt-1">
{stats.lastVacuum
? new Date(stats.lastVacuum).toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
})
: '—'}
</p>
</div>
{/* Active Connections */}
<div className="p-6 bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition-shadow">
<p className="text-sm text-gray-600 font-medium">Active Connections</p>
<p className="text-3xl font-bold text-gray-900 mt-2">
{stats.activeConnections}
</p>
</div>
{/* Health Status */}
<div className="p-6 bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition-shadow">
<p className="text-sm text-gray-600 font-medium mb-2">Health</p>
<span
className={`inline-block px-3 py-1 rounded-full text-sm font-bold border ${healthColor}`}
>
{stats.health.toUpperCase()}
</span>
</div>
</div>
{/* Health Alert */}
{stats.health !== 'good' && (
<div className={`p-4 rounded-lg border-l-4 ${healthColor}`}>
<p className="font-semibold text-sm mb-2">⚠️ Health Alert</p>
<p className="text-sm mb-3">{stats.healthDetails.reason}</p>
<div className="bg-white bg-opacity-50 p-3 rounded text-sm">
<p className="font-semibold mb-1">💡 Recommended Action:</p>
<p>{stats.healthDetails.recommendation}</p>
</div>
</div>
)}
{/* Metadata */}
<div className="flex justify-between items-center pt-4 border-t border-gray-200">
<p className="text-xs text-gray-500">
Last updated: {new Date(stats.timestamp).toLocaleTimeString()}
</p>
{loading && <span className="text-xs text-gray-400">Updating...</span>}
</div>
</div>
)
}
Example 6: Hook Implementation
useDatabaseStats Hook
'use client'
import { useEffect, useState, useCallback } from 'react'
import { fetchDatabaseStats } from '@/lib/api/admin-database-client'
import type { DatabaseStats } from '@/lib/api/admin-database-client'
export interface UseDatabaseStatsResult {
stats: DatabaseStats | null
loading: boolean
error: string | null
refresh: () => Promise<void>
}
/**
* Hook for managing database statistics
* Handles fetching, error handling, and automatic refresh
*/
export function useDatabaseStats(autoRefresh: boolean = false): UseDatabaseStatsResult {
const [stats, setStats] = useState<DatabaseStats | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
// Memoized refresh function
const refresh = useCallback(async () => {
console.log('[useDatabaseStats] Refreshing statistics...')
setLoading(true)
setError(null)
try {
const data = await fetchDatabaseStats()
setStats(data)
console.log('[useDatabaseStats] Statistics refreshed successfully')
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error'
setError(message)
console.error('[useDatabaseStats] Failed to fetch:', err)
} finally {
setLoading(false)
}
}, [])
// Initial fetch on mount
useEffect(() => {
console.log('[useDatabaseStats] Mounted, performing initial fetch')
refresh()
}, [refresh])
// Auto-refresh effect
useEffect(() => {
if (!autoRefresh) {
console.log('[useDatabaseStats] Auto-refresh disabled')
return
}
console.log('[useDatabaseStats] Auto-refresh enabled, setting interval to 60s')
const timer = setInterval(() => {
console.log('[useDatabaseStats] Auto-refresh triggered')
refresh()
}, 60000)
return () => {
console.log('[useDatabaseStats] Clearing auto-refresh interval')
clearInterval(timer)
}
}, [autoRefresh, refresh])
return { stats, loading, error, refresh }
}
Example 7: API Client Implementation
Type-Safe API Client
/**
* Type-safe API client for admin database endpoints
* All functions include proper error handling and logging
*/
export interface DatabaseStats {
tableCount: number
totalRecords: number
storageSize: number
lastVacuum: string | null
activeConnections: number
health: 'good' | 'warning' | 'critical'
healthDetails: { reason: string; recommendation: string }
timestamp: string
}
export interface EntityRecord {
id: string
[key: string]: unknown
}
export interface ImportResults {
imported: number
skipped: number
errors: Array<{ row: number; error: string }>
warnings: string[]
dryRun: boolean
duration: number
}
/**
* Fetch database statistics
* @throws Error if the request fails
*/
export async function fetchDatabaseStats(): Promise<DatabaseStats> {
console.log('[API] GET /api/admin/database/stats')
const response = await fetch('/api/admin/database/stats', {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
})
if (!response.ok) {
const text = await response.text()
throw new Error(
`API Error: ${response.status} ${response.statusText} - ${text}`
)
}
const data = (await response.json()) as DatabaseStats
console.log('[API] Response received:', data)
return data
}
/**
* Fetch entity records with pagination, sorting, and filtering
* @throws Error if the request fails
*/
export async function fetchEntityRecords(
entityType: string,
page: number = 1,
pageSize: number = 25,
filters: Record<string, string> = {},
sort: { column: string; order: 'asc' | 'desc' } = { column: 'id', order: 'asc' }
): Promise<{ records: EntityRecord[]; total: number }> {
const params = new URLSearchParams({
entityType,
page: page.toString(),
pageSize: pageSize.toString(),
sort: `${sort.column}:${sort.order}`,
...filters,
})
console.log(`[API] GET /api/admin/database/entities?${params}`)
const response = await fetch(`/api/admin/database/entities?${params}`)
if (!response.ok) {
const text = await response.text()
throw new Error(
`API Error: ${response.status} ${response.statusText} - ${text}`
)
}
const data = (await response.json()) as { records: EntityRecord[]; total: number }
console.log('[API] Response received:', {
recordsCount: data.records.length,
total: data.total,
})
return data
}
/**
* Update entity record
* @throws Error if the request fails
*/
export async function updateEntity(
entityType: string,
id: string,
updates: Record<string, unknown>
): Promise<EntityRecord> {
console.log(`[API] PATCH /api/admin/database/entities/${entityType}/${id}`, updates)
const response = await fetch(
`/api/admin/database/entities/${entityType}/${id}`,
{
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates),
}
)
if (!response.ok) {
const text = await response.text()
throw new Error(
`API Error: ${response.status} ${response.statusText} - ${text}`
)
}
const data = (await response.json()) as EntityRecord
console.log('[API] Entity updated successfully:', data)
return data
}
/**
* Delete entity record
* @throws Error if the request fails
*/
export async function deleteEntity(
entityType: string,
id: string
): Promise<void> {
console.log(`[API] DELETE /api/admin/database/entities/${entityType}/${id}`)
const response = await fetch(
`/api/admin/database/entities/${entityType}/${id}`,
{ method: 'DELETE' }
)
if (!response.ok) {
const text = await response.text()
throw new Error(
`API Error: ${response.status} ${response.statusText} - ${text}`
)
}
console.log('[API] Entity deleted successfully')
}
/**
* Export database data
* @throws Error if the request fails
*/
export async function exportDatabase(options: {
entityTypes: string[]
format: 'json' | 'yaml' | 'sql'
}): Promise<Blob> {
console.log('[API] POST /api/admin/database/export', options)
const response = await fetch('/api/admin/database/export', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(options),
})
if (!response.ok) {
const text = await response.text()
throw new Error(
`API Error: ${response.status} ${response.statusText} - ${text}`
)
}
const blob = await response.blob()
console.log('[API] Export successful, blob size:', blob.size)
return blob
}
/**
* Import database data
* @throws Error if the request fails
*/
export async function importDatabase(
file: File,
options: {
format: 'json' | 'yaml' | 'sql' | 'auto'
mode: 'append' | 'upsert' | 'replace'
dryRun: boolean
}
): Promise<ImportResults> {
console.log('[API] POST /api/admin/database/import', {
fileName: file.name,
fileSize: file.size,
...options,
})
const formData = new FormData()
formData.append('file', file)
formData.append('format', options.format)
formData.append('mode', options.mode)
formData.append('dryRun', options.dryRun.toString())
const response = await fetch('/api/admin/database/import', {
method: 'POST',
body: formData,
})
if (!response.ok) {
const text = await response.text()
throw new Error(
`API Error: ${response.status} ${response.statusText} - ${text}`
)
}
const data = (await response.json()) as ImportResults
console.log('[API] Import completed:', {
imported: data.imported,
skipped: data.skipped,
errors: data.errors.length,
})
return data
}
Example 8: Error Handling Patterns
Try-Catch Pattern
try {
// Operation that might fail
const data = await fetchDatabaseStats()
setStats(data)
} catch (error) {
// Type-safe error handling
const message = error instanceof Error ? error.message : 'Unknown error'
setError(message)
console.error('[Component] Error:', error)
} finally {
// Cleanup
setLoading(false)
}
Async/Await with Type Guard
async function handleAsyncOperation() {
try {
const response = await fetch('/api/endpoint')
if (!response.ok) {
const errorText = await response.text()
throw new Error(`HTTP ${response.status}: ${errorText}`)
}
const data = (await response.json()) as ExpectedType
return data
} catch (error) {
if (error instanceof TypeError) {
console.error('Network error:', error)
} else if (error instanceof SyntaxError) {
console.error('JSON parse error:', error)
} else if (error instanceof Error) {
console.error('Application error:', error.message)
} else {
console.error('Unknown error:', error)
}
throw error
}
}
Chained Promises with Error Recovery
loadEntityRecords(type, 1, 25, {}, { column: 'id', order: 'asc' })
.then(() => {
console.log('Records loaded successfully')
// Show success notification
})
.catch((error) => {
console.error('Failed to load records:', error)
// Show error notification
// Optionally provide retry option
})
Example 9: State Updates Best Practices
Batch Updates
// ❌ Bad: Multiple setState calls trigger re-renders
setEntityRecords(data.records)
setEntityPagination({ page, pageSize, total })
setEntityLoading(false)
// ✅ Good: Combine related state
const [entityState, setEntityState] = useState({
records: [],
pagination: { page: 1, pageSize: 25, total: 0 },
loading: false,
})
setEntityState({
records: data.records,
pagination: { page, pageSize, total },
loading: false,
})
Conditional Updates
// Only update if the current entityType matches
if (currentEntityType === entityType) {
setEntityRecords(data.records)
}
Preserving Previous State
// Preserve existing filters when adding a new one
const updatedFilters = { ...entityFilters, [field]: value }
setEntityFilters(updatedFilters)
// Preserve pagination state when sorting
const newSort = { column, order }
// Reset to page 1 but keep other pagination values
await loadRecords(..., 1, pagination.pageSize, ...)
Example 10: Loading State Patterns
Skeleton Loader
{loading && !data ? (
<LoadingSkeleton
lines={5}
className="space-y-4"
/>
) : null}
Disabled State
<button
onClick={onRefresh}
disabled={loading}
className={loading ? 'opacity-50 cursor-not-allowed' : '...'}
>
{loading ? 'Loading...' : 'Refresh'}
</button>
Conditional Rendering
{loading && !stats ? (
<LoadingIndicator />
) : error ? (
<ErrorState error={error} onRetry={refresh} />
) : stats ? (
<StatsDisplay stats={stats} />
) : null}
These examples demonstrate production-ready patterns for the /admin/database page implementation.