From 94a55daaab81d4836a738a32db182f70fb98155f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 04:07:30 +0000 Subject: [PATCH] Add Constraint Management UI components and integration Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com> --- src/app/admin/dashboard/page.tsx | 83 +++++++ src/components/admin/ConstraintDialog.tsx | 188 ++++++++++++++++ src/components/admin/ConstraintManagerTab.tsx | 203 ++++++++++++++++++ 3 files changed, 474 insertions(+) create mode 100644 src/components/admin/ConstraintDialog.tsx create mode 100644 src/components/admin/ConstraintManagerTab.tsx diff --git a/src/app/admin/dashboard/page.tsx b/src/app/admin/dashboard/page.tsx index 3661bb4..721e715 100644 --- a/src/app/admin/dashboard/page.tsx +++ b/src/app/admin/dashboard/page.tsx @@ -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 ( @@ -555,6 +622,14 @@ export default function AdminDashboard() { + + setTabValue(4)}> + + + + + + @@ -763,6 +838,14 @@ export default function AdminDashboard() { )} + + + + {successMessage && ( setSuccessMessage('')}> {successMessage} diff --git a/src/components/admin/ConstraintDialog.tsx b/src/components/admin/ConstraintDialog.tsx new file mode 100644 index 0000000..b7c9c27 --- /dev/null +++ b/src/components/admin/ConstraintDialog.tsx @@ -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; +}; + +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 ( + + {getTitle()} + + {mode === 'delete' ? ( + + Are you sure you want to delete the constraint " + {selectedConstraint?.constraint_name}"? This action cannot be undone. + + ) : ( + <> + setConstraintName(e.target.value)} + sx={{ mt: 2, mb: 2 }} + helperText="A unique name for this constraint" + /> + + + + {currentType?.requiresColumn && ( + setColumnName(e.target.value)} + sx={{ mb: 2 }} + helperText="The column to apply this constraint to" + /> + )} + + {currentType?.requiresExpression && ( + setCheckExpression(e.target.value)} + sx={{ mb: 2 }} + multiline + rows={3} + helperText="Boolean expression for the check constraint (e.g., price > 0)" + /> + )} + + )} + + + + + + + ); +} diff --git a/src/components/admin/ConstraintManagerTab.tsx b/src/components/admin/ConstraintManagerTab.tsx new file mode 100644 index 0000000..2635a28 --- /dev/null +++ b/src/components/admin/ConstraintManagerTab.tsx @@ -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; + onDropConstraint: (tableName: string, constraintName: string) => Promise; +}; + +export default function ConstraintManagerTab({ + tables, + onAddConstraint, + onDropConstraint, +}: ConstraintManagerTabProps) { + const [selectedTable, setSelectedTable] = useState(''); + const [constraints, setConstraints] = useState([]); + const [dialogState, setDialogState] = useState<{ + open: boolean; + mode: 'add' | 'delete'; + }>({ open: false, mode: 'add' }); + const [selectedConstraint, setSelectedConstraint] = useState(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 ( + <> + + {feature?.name || 'Constraint Manager'} + + + {feature?.description && ( + + {feature.description} + + )} + + + + + + {selectedTable && ( + <> + + {canAdd && ( + + )} + + + + + + + + Constraint Name + Type + Column + Expression + Actions + + + + {constraints.map((constraint) => ( + + {constraint.constraint_name} + {constraint.constraint_type} + {constraint.column_name || '-'} + {constraint.check_clause || '-'} + + {canDelete && ( + + )} + + + ))} + {constraints.length === 0 && ( + + + No constraints found for this table + + + )} + +
+
+
+ + )} + + + + ); +}