mirror of
https://github.com/johndoe6345789/postgres.git
synced 2026-04-25 06:15:02 +00:00
- 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>
435 lines
12 KiB
TypeScript
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 "{selectedTable}"
|
|
</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)}
|
|
/>
|
|
</>
|
|
);
|
|
}
|