Merge pull request #14 from johndoe6345789/copilot/implement-roadmap-features

Implement Constraint Management UI
This commit is contained in:
2026-01-08 04:20:36 +00:00
committed by GitHub
7 changed files with 539 additions and 8 deletions

View File

@@ -54,7 +54,7 @@ This project is a full-stack web application featuring:
- 🛠️ **Admin Panel** - Manage tables, columns, and data through a beautiful UI
- 📊 **Table Manager** - Create and drop tables with visual column definition
- 🔧 **Column Manager** - Add, modify, and drop columns from existing tables
- 🔒 **Constraint Manager** - Add and manage UNIQUE and CHECK constraints (API ready, UI in progress)
- 🔒 **Constraint Manager** - Add and manage UNIQUE and CHECK constraints (fully implemented)
- 📊 **SQL Query Interface** - Execute custom queries with safety validation
- 🔒 **JWT Authentication** with secure session management
- 📦 **DrizzleORM** - Support for PostgreSQL, MySQL, and SQLite
@@ -767,10 +767,9 @@ See [ROADMAP.md](ROADMAP.md) for planned features and improvements.
- ✅ Table Manager - Create and drop tables with visual column builder
- ✅ Column Manager - Add, modify, and drop columns from existing tables
- ✅ Schema management interface for table and column operations
- 🔄 Constraint Manager - Add and manage UNIQUE and CHECK constraints (API complete, UI in progress)
- Constraint Manager - Add and manage UNIQUE and CHECK constraints (fully implemented)
**Upcoming features:**
- Complete constraint management UI
- Visual database designer
- Multi-database server connections
- Advanced query builder

View File

@@ -60,11 +60,11 @@ See `src/config/features.json` for the complete feature configuration.
- [x] ✅ Create schema management interface
- [x] ✅ Implement table creation/editing UI (API ready, UI implemented)
- [x] ✅ Add column type management UI (API ready, UI implemented)
- [ ] Add data validation and constraints management 🏗️ **IN PROGRESS**
- [x] Add data validation and constraints management **COMPLETED**
- [x] ✅ Implement constraints API (UNIQUE, CHECK constraints)
- [x] ✅ Add constraint listing endpoint
- [x] ✅ Add constraint creation/deletion endpoints
- [ ] Build constraints management UI
- [x] Build constraints management UI
- [ ] Add PRIMARY KEY constraint support
- [ ] Add DEFAULT value management
- [ ] Add NOT NULL constraint management

View File

@@ -149,9 +149,9 @@ All tests verify that:
| Feature Config | - | - | - | 40 | 40 |
| Table Manager | 7 | 2 (2 skipped) | 3 | - | 12 |
| Column Manager | 9 | 2 (2 skipped) | 3 | - | 14 |
| Constraint Manager | 14 | 0 (UI pending) | 3 | 4 | 21 |
| Constraint Manager | 14 | 3 (3 skipped) | 4 | 4 | 25 |
| Admin Dashboard | - | 3 | 3 | - | 6 |
| **Total** | **30** | **7** | **12** | **44** | **93** |
| **Total** | **30** | **10** | **16** | **44** | **100** |
## Feature: Constraint Management Tests
@@ -186,6 +186,25 @@ Tests for the Constraint Management API endpoints (`/api/admin/constraints`):
- Error handling for all CRUD operations
- Support for UNIQUE and CHECK constraints
### End-to-End Tests (Playwright UI Tests)
#### 2. `tests/e2e/AdminDashboard.e2e.ts` - Constraints Manager UI
**UI Tests:**
- 🔄 Display Constraints tab (requires auth - skipped)
- 🔄 Show table selector in Constraints Manager (requires auth - skipped)
- 🔄 Open add constraint dialog (requires auth - skipped)
**Security Tests:**
- ✅ Blocks constraint API access without authentication
**Note:** UI tests are skipped because they require an authenticated session. These can be enabled when a test authentication mechanism is implemented.
**Components Implemented:**
-`ConstraintManagerTab.tsx` - Main UI component for managing constraints
-`ConstraintDialog.tsx` - Reusable dialog for add/delete constraint operations
- ✅ Integration with admin dashboard navigation and handlers
### Unit Tests
#### 2. `src/utils/featureConfig.test.ts`
@@ -269,4 +288,4 @@ When adding new features:
**Last Updated:** January 2026
**Test Framework:** Playwright + Vitest
**Coverage Status:** ✅ API Validation | 🔄 UI Tests (partial - needs auth)
**Coverage Status:** ✅ API Validation | 🔄 UI Tests (partial - needs auth) | ✅ Constraint Manager UI Complete

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 { useCallback, 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
const fetchConstraints = useCallback(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);
}
}, [selectedTable]);
useEffect(() => {
if (selectedTable) {
fetchConstraints();
} else {
setConstraints([]);
}
}, [selectedTable, fetchConstraints]);
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}
/>
</>
);
}

View File

@@ -95,5 +95,44 @@ test.describe('Admin Dashboard', () => {
expect(response.status()).toBe(401);
});
test('should not allow constraint management without auth', async ({ page }) => {
const response = await page.request.get('/api/admin/constraints?tableName=test');
expect(response.status()).toBe(401);
});
});
test.describe('Constraints Manager UI', () => {
test.skip('should display Constraints tab after login', async ({ page }) => {
// This test would require actual authentication
// Skipping for now as it needs a real admin user
// await page.goto('/admin/dashboard');
// await expect(page.getByText('Constraints')).toBeVisible();
});
test.skip('should show table selector in Constraints Manager', async ({ page }) => {
// This test would require authentication
// Skipping for now
// await page.goto('/admin/dashboard');
// await page.getByText('Constraints').click();
// await expect(page.getByText(/select a table/i)).toBeVisible();
});
test.skip('should open add constraint dialog', async ({ page }) => {
// This test would require authentication
// Skipping for now
// await page.goto('/admin/dashboard');
// await page.getByText('Constraints').click();
// Select a table first
// await page.getByRole('button', { name: /add constraint/i }).click();
// await expect(page.getByText('Add Constraint')).toBeVisible();
// await expect(page.getByLabel(/constraint name/i)).toBeVisible();
});
});
});