Add Constraint Management UI components and integration

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-01-08 04:07:30 +00:00
parent cdcea9c1eb
commit 94a55daaab
3 changed files with 474 additions and 0 deletions

View File

@@ -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}

View 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>
);
}

View 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}
/>
</>
);
}