mirror of
https://github.com/johndoe6345789/postgres.git
synced 2026-04-24 22:04:58 +00:00
Add Constraint Management UI components and integration
Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import CodeIcon from '@mui/icons-material/Code';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import LogoutIcon from '@mui/icons-material/Logout';
|
||||
import RuleIcon from '@mui/icons-material/Rule';
|
||||
import StorageIcon from '@mui/icons-material/Storage';
|
||||
import TableChartIcon from '@mui/icons-material/TableChart';
|
||||
import ViewColumnIcon from '@mui/icons-material/ViewColumn';
|
||||
@@ -43,6 +44,7 @@ import {
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import ConstraintManagerTab from '@/components/admin/ConstraintManagerTab';
|
||||
import { theme } from '@/utils/theme';
|
||||
|
||||
const DRAWER_WIDTH = 240;
|
||||
@@ -491,6 +493,71 @@ export default function AdminDashboard() {
|
||||
}
|
||||
};
|
||||
|
||||
// Constraint Management Handlers
|
||||
const handleAddConstraint = async (tableName: string, data: any) => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
setSuccessMessage('');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/constraints', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tableName,
|
||||
...data,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.error || 'Failed to add constraint');
|
||||
}
|
||||
|
||||
setSuccessMessage(result.message || 'Constraint added successfully');
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDropConstraint = async (tableName: string, constraintName: string) => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
setSuccessMessage('');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/constraints', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tableName,
|
||||
constraintName,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.error || 'Failed to drop constraint');
|
||||
}
|
||||
|
||||
setSuccessMessage(result.message || 'Constraint dropped successfully');
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
@@ -555,6 +622,14 @@ export default function AdminDashboard() {
|
||||
<ListItemText primary="Column Manager" />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton onClick={() => setTabValue(4)}>
|
||||
<ListItemIcon>
|
||||
<RuleIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Constraints" />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Box>
|
||||
</Drawer>
|
||||
@@ -763,6 +838,14 @@ export default function AdminDashboard() {
|
||||
)}
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tabValue} index={4}>
|
||||
<ConstraintManagerTab
|
||||
tables={tables}
|
||||
onAddConstraint={handleAddConstraint}
|
||||
onDropConstraint={handleDropConstraint}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
{successMessage && (
|
||||
<Alert severity="success" sx={{ mt: 2 }} onClose={() => setSuccessMessage('')}>
|
||||
{successMessage}
|
||||
|
||||
188
src/components/admin/ConstraintDialog.tsx
Normal file
188
src/components/admin/ConstraintDialog.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
MenuItem,
|
||||
Select,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
type ConstraintDialogProps = {
|
||||
open: boolean;
|
||||
mode: 'add' | 'delete';
|
||||
constraintTypes: Array<{
|
||||
name: string;
|
||||
description: string;
|
||||
requiresColumn: boolean;
|
||||
requiresExpression: boolean;
|
||||
}>;
|
||||
selectedConstraint?: any;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: any) => Promise<void>;
|
||||
};
|
||||
|
||||
export default function ConstraintDialog({
|
||||
open,
|
||||
mode,
|
||||
constraintTypes,
|
||||
selectedConstraint,
|
||||
onClose,
|
||||
onSubmit,
|
||||
}: ConstraintDialogProps) {
|
||||
const [constraintName, setConstraintName] = useState('');
|
||||
const [constraintType, setConstraintType] = useState('UNIQUE');
|
||||
const [columnName, setColumnName] = useState('');
|
||||
const [checkExpression, setCheckExpression] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
// Reset form when dialog closes
|
||||
setConstraintName('');
|
||||
setConstraintType('UNIQUE');
|
||||
setColumnName('');
|
||||
setCheckExpression('');
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
if (mode === 'add') {
|
||||
const data: any = {
|
||||
constraintName,
|
||||
constraintType,
|
||||
};
|
||||
|
||||
// Get the current constraint type config
|
||||
const currentType = constraintTypes.find(ct => ct.name === constraintType);
|
||||
|
||||
if (currentType?.requiresColumn) {
|
||||
data.columnName = columnName;
|
||||
}
|
||||
|
||||
if (currentType?.requiresExpression) {
|
||||
data.checkExpression = checkExpression;
|
||||
}
|
||||
|
||||
await onSubmit(data);
|
||||
} else if (mode === 'delete') {
|
||||
// For delete, we just need to confirm
|
||||
await onSubmit({});
|
||||
}
|
||||
onClose();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getTitle = () => {
|
||||
if (mode === 'add') {
|
||||
return 'Add Constraint';
|
||||
}
|
||||
return `Delete Constraint: ${selectedConstraint?.constraint_name}`;
|
||||
};
|
||||
|
||||
const isFormValid = () => {
|
||||
if (mode === 'delete') {
|
||||
return true; // Always valid for delete
|
||||
}
|
||||
|
||||
if (!constraintName.trim() || !constraintType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentType = constraintTypes.find(ct => ct.name === constraintType);
|
||||
|
||||
if (currentType?.requiresColumn && !columnName.trim()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (currentType?.requiresExpression && !checkExpression.trim()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const currentType = constraintTypes.find(ct => ct.name === constraintType);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>{getTitle()}</DialogTitle>
|
||||
<DialogContent>
|
||||
{mode === 'delete' ? (
|
||||
<Typography variant="body2" color="error" gutterBottom>
|
||||
Are you sure you want to delete the constraint "
|
||||
{selectedConstraint?.constraint_name}"? This action cannot be undone.
|
||||
</Typography>
|
||||
) : (
|
||||
<>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Constraint Name"
|
||||
value={constraintName}
|
||||
onChange={e => setConstraintName(e.target.value)}
|
||||
sx={{ mt: 2, mb: 2 }}
|
||||
helperText="A unique name for this constraint"
|
||||
/>
|
||||
|
||||
<Select
|
||||
fullWidth
|
||||
value={constraintType}
|
||||
onChange={e => setConstraintType(e.target.value)}
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
{constraintTypes.map((type) => (
|
||||
<MenuItem key={type.name} value={type.name}>
|
||||
{type.name} - {type.description}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
{currentType?.requiresColumn && (
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Column Name"
|
||||
value={columnName}
|
||||
onChange={e => setColumnName(e.target.value)}
|
||||
sx={{ mb: 2 }}
|
||||
helperText="The column to apply this constraint to"
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentType?.requiresExpression && (
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Check Expression"
|
||||
value={checkExpression}
|
||||
onChange={e => setCheckExpression(e.target.value)}
|
||||
sx={{ mb: 2 }}
|
||||
multiline
|
||||
rows={3}
|
||||
helperText="Boolean expression for the check constraint (e.g., price > 0)"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
variant="contained"
|
||||
color={mode === 'delete' ? 'error' : 'primary'}
|
||||
disabled={loading || !isFormValid()}
|
||||
>
|
||||
{mode === 'add' ? 'Add Constraint' : 'Delete Constraint'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
203
src/components/admin/ConstraintManagerTab.tsx
Normal file
203
src/components/admin/ConstraintManagerTab.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
'use client';
|
||||
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Select,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getConstraintTypes, getFeatureById } from '@/utils/featureConfig';
|
||||
import ConstraintDialog from './ConstraintDialog';
|
||||
|
||||
type ConstraintManagerTabProps = {
|
||||
tables: Array<{ table_name: string }>;
|
||||
onAddConstraint: (tableName: string, data: any) => Promise<void>;
|
||||
onDropConstraint: (tableName: string, constraintName: string) => Promise<void>;
|
||||
};
|
||||
|
||||
export default function ConstraintManagerTab({
|
||||
tables,
|
||||
onAddConstraint,
|
||||
onDropConstraint,
|
||||
}: ConstraintManagerTabProps) {
|
||||
const [selectedTable, setSelectedTable] = useState('');
|
||||
const [constraints, setConstraints] = useState<any[]>([]);
|
||||
const [dialogState, setDialogState] = useState<{
|
||||
open: boolean;
|
||||
mode: 'add' | 'delete';
|
||||
}>({ open: false, mode: 'add' });
|
||||
const [selectedConstraint, setSelectedConstraint] = useState<any>(null);
|
||||
|
||||
// Get feature configuration from JSON
|
||||
const feature = getFeatureById('constraint-management');
|
||||
const constraintTypes = getConstraintTypes();
|
||||
|
||||
// Check if actions are enabled from config
|
||||
const canAdd = feature?.ui.actions.includes('add');
|
||||
const canDelete = feature?.ui.actions.includes('delete');
|
||||
|
||||
// Fetch constraints when table is selected
|
||||
useEffect(() => {
|
||||
if (selectedTable) {
|
||||
fetchConstraints();
|
||||
} else {
|
||||
setConstraints([]);
|
||||
}
|
||||
}, [selectedTable]);
|
||||
|
||||
const fetchConstraints = async () => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/admin/constraints?tableName=${selectedTable}`,
|
||||
{
|
||||
method: 'GET',
|
||||
},
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setConstraints(data.constraints || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch constraints:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConstraintOperation = async (data: any) => {
|
||||
if (dialogState.mode === 'add') {
|
||||
await onAddConstraint(selectedTable, data);
|
||||
} else if (dialogState.mode === 'delete' && selectedConstraint) {
|
||||
await onDropConstraint(selectedTable, selectedConstraint.constraint_name);
|
||||
}
|
||||
await fetchConstraints(); // Refresh constraints list
|
||||
};
|
||||
|
||||
const openAddDialog = () => {
|
||||
setSelectedConstraint(null);
|
||||
setDialogState({ open: true, mode: 'add' });
|
||||
};
|
||||
|
||||
const openDeleteDialog = (constraint: any) => {
|
||||
setSelectedConstraint(constraint);
|
||||
setDialogState({ open: true, mode: 'delete' });
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
setDialogState({ ...dialogState, open: false });
|
||||
setSelectedConstraint(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
{feature?.name || 'Constraint Manager'}
|
||||
</Typography>
|
||||
|
||||
{feature?.description && (
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
{feature.description}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Box sx={{ mt: 2, mb: 2 }}>
|
||||
<Select
|
||||
value={selectedTable}
|
||||
onChange={e => setSelectedTable(e.target.value)}
|
||||
displayEmpty
|
||||
fullWidth
|
||||
sx={{ maxWidth: 400 }}
|
||||
>
|
||||
<MenuItem value="">
|
||||
<em>Select a table</em>
|
||||
</MenuItem>
|
||||
{tables.map(table => (
|
||||
<MenuItem key={table.table_name} value={table.table_name}>
|
||||
{table.table_name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Box>
|
||||
|
||||
{selectedTable && (
|
||||
<>
|
||||
<Box sx={{ mt: 2, mb: 2 }}>
|
||||
{canAdd && (
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={openAddDialog}
|
||||
>
|
||||
Add Constraint
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Paper sx={{ mt: 2 }}>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Constraint Name</TableCell>
|
||||
<TableCell>Type</TableCell>
|
||||
<TableCell>Column</TableCell>
|
||||
<TableCell>Expression</TableCell>
|
||||
<TableCell align="right">Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{constraints.map((constraint) => (
|
||||
<TableRow key={constraint.constraint_name}>
|
||||
<TableCell>{constraint.constraint_name}</TableCell>
|
||||
<TableCell>{constraint.constraint_type}</TableCell>
|
||||
<TableCell>{constraint.column_name || '-'}</TableCell>
|
||||
<TableCell>{constraint.check_clause || '-'}</TableCell>
|
||||
<TableCell align="right">
|
||||
{canDelete && (
|
||||
<Button
|
||||
size="small"
|
||||
color="error"
|
||||
startIcon={<DeleteIcon />}
|
||||
onClick={() => openDeleteDialog(constraint)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{constraints.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} align="center">
|
||||
No constraints found for this table
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Paper>
|
||||
</>
|
||||
)}
|
||||
|
||||
<ConstraintDialog
|
||||
open={dialogState.open}
|
||||
mode={dialogState.mode}
|
||||
constraintTypes={constraintTypes}
|
||||
selectedConstraint={selectedConstraint}
|
||||
onSubmit={handleConstraintOperation}
|
||||
onClose={closeDialog}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user