Files
postgres/src/components/admin/IndexManagerTab.tsx
copilot-swe-agent[bot] da1f968e3f refactor: Use configuration for operators and index types
- Replace hardcoded OPERATORS array with getQueryOperators()
- Replace hardcoded INDEX_TYPES array with getIndexTypes()
- Maintain single source of truth in features.json
- Address code review feedback for better maintainability

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
2026-01-08 13:01:31 +00:00

435 lines
12 KiB
TypeScript

'use client';
import AddIcon from '@mui/icons-material/Add';
import DeleteIcon from '@mui/icons-material/Delete';
import SpeedIcon from '@mui/icons-material/Speed';
import {
Box,
Button,
Checkbox,
Chip,
FormControl,
FormControlLabel,
IconButton,
InputLabel,
List,
ListItem,
ListItemIcon,
ListItemText,
MenuItem,
Paper,
Select,
TextField,
Tooltip,
Typography,
} from '@mui/material';
import { useState } from 'react';
import { getFeatureById, getIndexTypes } from '@/utils/featureConfig';
import ConfirmDialog from './ConfirmDialog';
type IndexManagerTabProps = {
tables: Array<{ table_name: string }>;
onRefresh: () => void;
};
export default function IndexManagerTab({
tables,
onRefresh,
}: IndexManagerTabProps) {
const [selectedTable, setSelectedTable] = useState('');
const [indexes, setIndexes] = useState<any[]>([]);
const [availableColumns, setAvailableColumns] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
// Create index form state
const [openCreateDialog, setOpenCreateDialog] = useState(false);
const [indexName, setIndexName] = useState('');
const [selectedColumns, setSelectedColumns] = useState<string[]>([]);
const [indexType, setIndexType] = useState('BTREE');
const [isUnique, setIsUnique] = useState(false);
// Delete confirmation
const [deleteIndex, setDeleteIndex] = useState<string | null>(null);
const feature = getFeatureById('index-management');
const INDEX_TYPES = getIndexTypes();
// Fetch indexes for selected table
const fetchIndexes = async (tableName: string) => {
try {
setLoading(true);
setError('');
const response = await fetch(`/api/admin/indexes?tableName=${tableName}`);
const data = await response.json();
if (response.ok) {
setIndexes(data.indexes || []);
}
else {
setError(data.error || 'Failed to fetch indexes');
}
}
catch (err: any) {
setError(err.message || 'Failed to fetch indexes');
}
finally {
setLoading(false);
}
};
// Fetch columns for selected table
const fetchColumns = async (tableName: string) => {
try {
const response = await fetch('/api/admin/table-schema', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tableName }),
});
if (response.ok) {
const data = await response.json();
const cols = data.columns.map((col: any) => col.column_name);
setAvailableColumns(cols);
}
}
catch (err) {
console.error('Failed to fetch columns:', err);
}
};
// Handle table selection
const handleTableChange = async (tableName: string) => {
setSelectedTable(tableName);
setIndexes([]);
setError('');
setSuccess('');
if (tableName) {
await Promise.all([
fetchIndexes(tableName),
fetchColumns(tableName),
]);
}
};
// Handle create index
const handleCreateIndex = async () => {
if (!indexName || selectedColumns.length === 0) {
setError('Index name and at least one column are required');
return;
}
try {
setLoading(true);
setError('');
const response = await fetch('/api/admin/indexes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tableName: selectedTable,
indexName,
columns: selectedColumns,
indexType,
unique: isUnique,
}),
});
const data = await response.json();
if (response.ok) {
setSuccess(`Index "${indexName}" created successfully`);
setOpenCreateDialog(false);
setIndexName('');
setSelectedColumns([]);
setIndexType('BTREE');
setIsUnique(false);
await fetchIndexes(selectedTable);
onRefresh();
}
else {
setError(data.error || 'Failed to create index');
}
}
catch (err: any) {
setError(err.message || 'Failed to create index');
}
finally {
setLoading(false);
}
};
// Handle delete index
const handleDeleteIndex = async () => {
if (!deleteIndex)
return;
try {
setLoading(true);
setError('');
const response = await fetch('/api/admin/indexes', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ indexName: deleteIndex }),
});
const data = await response.json();
if (response.ok) {
setSuccess(`Index "${deleteIndex}" dropped successfully`);
setDeleteIndex(null);
await fetchIndexes(selectedTable);
onRefresh();
}
else {
setError(data.error || 'Failed to drop index');
}
}
catch (err: any) {
setError(err.message || 'Failed to drop index');
}
finally {
setLoading(false);
}
};
return (
<>
<Typography variant="h5" gutterBottom>
{feature?.name || 'Index Management'}
</Typography>
{feature?.description && (
<Typography variant="body2" color="text.secondary" gutterBottom>
{feature.description}
</Typography>
)}
{/* Success/Error Messages */}
{success && (
<Paper sx={{ p: 2, mt: 2, bgcolor: 'success.light' }}>
<Typography color="success.dark">{success}</Typography>
</Paper>
)}
{error && (
<Paper sx={{ p: 2, mt: 2, bgcolor: 'error.light' }}>
<Typography color="error">{error}</Typography>
</Paper>
)}
{/* Table Selection */}
<Paper sx={{ p: 2, mt: 2 }}>
<FormControl fullWidth>
<InputLabel>Select Table</InputLabel>
<Select
value={selectedTable}
label="Select Table"
onChange={e => handleTableChange(e.target.value)}
>
{tables.map(table => (
<MenuItem key={table.table_name} value={table.table_name}>
{table.table_name}
</MenuItem>
))}
</Select>
</FormControl>
{selectedTable && (
<Box sx={{ mt: 2 }}>
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={() => setOpenCreateDialog(true)}
disabled={loading}
>
Create Index
</Button>
</Box>
)}
</Paper>
{/* Indexes List */}
{selectedTable && indexes.length > 0 && (
<Paper sx={{ p: 2, mt: 2 }}>
<Typography variant="h6" gutterBottom>
Indexes on {selectedTable}
</Typography>
<List>
{indexes.map(index => (
<ListItem
key={index.index_name}
secondaryAction={(
!index.is_primary && (
<Tooltip title="Drop Index">
<IconButton
edge="end"
color="error"
onClick={() => setDeleteIndex(index.index_name)}
>
<DeleteIcon />
</IconButton>
</Tooltip>
)
)}
>
<ListItemIcon>
<SpeedIcon />
</ListItemIcon>
<ListItemText
primary={(
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body1">{index.index_name}</Typography>
{index.is_primary && <Chip label="PRIMARY KEY" size="small" color="primary" />}
{index.is_unique && !index.is_primary && <Chip label="UNIQUE" size="small" color="secondary" />}
<Chip label={index.index_type.toUpperCase()} size="small" />
</Box>
)}
secondary={`Columns: ${index.columns.join(', ')}`}
/>
</ListItem>
))}
</List>
</Paper>
)}
{selectedTable && indexes.length === 0 && !loading && (
<Paper sx={{ p: 2, mt: 2 }}>
<Typography color="text.secondary">
No indexes found for table &quot;{selectedTable}&quot;
</Typography>
</Paper>
)}
{/* Create Index Dialog */}
{openCreateDialog && (
<Paper
sx={{
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
p: 3,
zIndex: 1300,
minWidth: 400,
maxWidth: 600,
}}
>
<Typography variant="h6" gutterBottom>
Create Index on {selectedTable}
</Typography>
<TextField
fullWidth
label="Index Name"
value={indexName}
onChange={e => setIndexName(e.target.value)}
sx={{ mt: 2 }}
placeholder="e.g., idx_users_email"
/>
<FormControl fullWidth sx={{ mt: 2 }}>
<InputLabel>Columns</InputLabel>
<Select
multiple
value={selectedColumns}
label="Columns"
onChange={e => setSelectedColumns(e.target.value as string[])}
renderValue={selected => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{(selected as string[]).map(value => (
<Chip key={value} label={value} size="small" />
))}
</Box>
)}
>
{availableColumns.map(col => (
<MenuItem key={col} value={col}>
{col}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl fullWidth sx={{ mt: 2 }}>
<InputLabel>Index Type</InputLabel>
<Select
value={indexType}
label="Index Type"
onChange={e => setIndexType(e.target.value)}
>
{INDEX_TYPES.map(type => (
<MenuItem key={type.value} value={type.value}>
<Box>
<Typography variant="body1">{type.label}</Typography>
<Typography variant="caption" color="text.secondary">
{type.description}
</Typography>
</Box>
</MenuItem>
))}
</Select>
</FormControl>
<FormControlLabel
control={
<Checkbox checked={isUnique} onChange={e => setIsUnique(e.target.checked)} />
}
label="Unique Index"
sx={{ mt: 2 }}
/>
<Box sx={{ mt: 3, display: 'flex', gap: 1 }}>
<Button
variant="contained"
onClick={handleCreateIndex}
disabled={loading}
>
Create
</Button>
<Button
variant="outlined"
onClick={() => {
setOpenCreateDialog(false);
setIndexName('');
setSelectedColumns([]);
setIndexType('BTREE');
setIsUnique(false);
}}
>
Cancel
</Button>
</Box>
</Paper>
)}
{/* Overlay for create dialog */}
{openCreateDialog && (
<Box
sx={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
bgcolor: 'rgba(0, 0, 0, 0.5)',
zIndex: 1299,
}}
onClick={() => setOpenCreateDialog(false)}
/>
)}
{/* Delete Confirmation Dialog */}
<ConfirmDialog
open={!!deleteIndex}
title="Drop Index"
message={`Are you sure you want to drop the index "${deleteIndex}"? This action cannot be undone.`}
onConfirm={handleDeleteIndex}
onCancel={() => setDeleteIndex(null)}
/>
</>
);
}