From 190757d0ab7c88fc1f1a86a59447790bed1f1e55 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 03:01:16 +0000 Subject: [PATCH] Add reusable components and code style guide following config-driven architecture Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com> --- CODE_STYLE.md | 542 +++++++++++++++++++++ TESTING.md | 226 +++++++++ src/components/admin/ColumnDialog.tsx | 202 ++++++++ src/components/admin/ColumnManagerTab.tsx | 215 ++++++++ src/components/admin/CreateTableDialog.tsx | 162 ++++++ src/components/admin/DropTableDialog.tsx | 85 ++++ src/components/admin/TableManagerTab.tsx | 116 +++++ src/utils/featureConfig.test.ts | 337 +++++++++++++ 8 files changed, 1885 insertions(+) create mode 100644 CODE_STYLE.md create mode 100644 TESTING.md create mode 100644 src/components/admin/ColumnDialog.tsx create mode 100644 src/components/admin/ColumnManagerTab.tsx create mode 100644 src/components/admin/CreateTableDialog.tsx create mode 100644 src/components/admin/DropTableDialog.tsx create mode 100644 src/components/admin/TableManagerTab.tsx create mode 100644 src/utils/featureConfig.test.ts diff --git a/CODE_STYLE.md b/CODE_STYLE.md new file mode 100644 index 0000000..86f8e31 --- /dev/null +++ b/CODE_STYLE.md @@ -0,0 +1,542 @@ +# Code Style Guide + +This document outlines the coding standards and best practices for the PostgreSQL Admin Panel project. + +## General Principles + +### 1. Keep Components Small and Reusable +- **Maximum component size**: ~200 lines of code +- **Single Responsibility**: Each component should do one thing well +- **Reusability**: Extract common patterns into shared components +- **Example**: Instead of a 1000+ line dashboard, break it into: + - `TableManagerTab.tsx` (table management UI) + - `ColumnManagerTab.tsx` (column management UI) + - `CreateTableDialog.tsx` (reusable dialog) + - `ColumnDialog.tsx` (reusable for add/modify/drop) + +### 2. Configuration-Driven Architecture +- **Use JSON configuration**: Define features in `src/config/features.json` +- **Don't hardcode**: Pull data types, actions, and UI settings from config +- **Example**: +```typescript +// ❌ Bad - Hardcoded +const dataTypes = ['INTEGER', 'VARCHAR', 'TEXT']; + +// ✅ Good - Config-driven +import { getDataTypes } from '@/utils/featureConfig'; +const dataTypes = getDataTypes().map(dt => dt.name); +``` + +### 3. Leverage Existing Utilities +- Use `src/utils/featureConfig.ts` functions: + - `getFeatures()` - Get all enabled features + - `getFeatureById(id)` - Get specific feature config + - `getDataTypes()` - Get database data types + - `getNavItems()` - Get navigation items + +## TypeScript Standards + +### Type Definitions +```typescript +// ✅ Good - Explicit types +type TableManagerTabProps = { + tables: Array<{ table_name: string }>; + onCreateTable: (tableName: string, columns: any[]) => Promise; + onDropTable: (tableName: string) => Promise; +}; + +// ❌ Bad - Using 'any' without reason +function handleData(data: any) { } + +// ✅ Good - Proper typing +function handleData(data: { id: number; name: string }) { } +``` + +### Avoid Type Assertions +```typescript +// ❌ Bad +const value = response as SomeType; + +// ✅ Good - Validate first +if (isValidType(response)) { + const value = response; +} +``` + +## React/Next.js Standards + +### Component Structure +```typescript +'use client'; // Only if component uses client-side features + +import { useState } from 'react'; // React imports first +import { Button } from '@mui/material'; // Third-party imports +import { getFeatures } from '@/utils/featureConfig'; // Local imports + +type ComponentProps = { + // Props type definition +}; + +export default function ComponentName({ prop1, prop2 }: ComponentProps) { + // 1. Hooks + const [state, setState] = useState(); + + // 2. Derived state + const derivedValue = useMemo(() => compute(), [deps]); + + // 3. Handlers + const handleClick = () => { }; + + // 4. Effects + useEffect(() => { }, []); + + // 5. Render + return
...
; +} +``` + +### Client vs Server Components +```typescript +// ✅ Server Component (default) - No 'use client' +export default function ServerComponent() { + // Can fetch data, use async/await + // Cannot use hooks, events, or browser APIs + return
Static content
; +} + +// ✅ Client Component - Add 'use client' +'use client'; +export default function ClientComponent() { + const [state, setState] = useState(); + return ; +} +``` + +### Prop Naming +```typescript +// ✅ Good - Clear and consistent +type DialogProps = { + open: boolean; // State boolean + onClose: () => void; // Event handler (on*) + onCreate: (data) => Promise; // Async handler + tables: Table[]; // Plural for arrays + selectedTable: string; // Singular for single value +}; + +// ❌ Bad - Unclear naming +type DialogProps = { + isOpen: boolean; // Don't use 'is' prefix unnecessarily + close: () => void; // Missing 'on' prefix + data: any; // Too generic +}; +``` + +## File Organization + +### Directory Structure +``` +src/ +├── app/ # Next.js pages and routes +│ ├── admin/ # Admin pages +│ └── api/ # API routes +├── components/ # Reusable React components +│ └── admin/ # Admin-specific components +├── config/ # Configuration files +│ └── features.json # Feature definitions (USE THIS!) +├── utils/ # Utility functions +│ └── featureConfig.ts # Config helpers (USE THIS!) +├── models/ # Database models +└── types/ # TypeScript type definitions +``` + +### File Naming +- **Components**: PascalCase - `TableManagerTab.tsx` +- **Utilities**: camelCase - `featureConfig.ts` +- **Tests**: Same as source + `.test.ts` - `featureConfig.test.ts` +- **Types**: PascalCase - `UserTypes.ts` + +## Component Patterns + +### Small, Focused Components +```typescript +// ✅ Good - Small, single purpose +export default function CreateTableDialog({ open, onClose, onCreate }) { + // Only handles table creation dialog + return ...; +} + +// ❌ Bad - Too many responsibilities +export default function AdminDashboard() { + // 1000+ lines handling: + // - Navigation + // - Table management + // - Column management + // - Query execution + // - All dialogs inline +} +``` + +### Reusable Dialog Pattern +```typescript +// ✅ Good - Reusable for multiple operations +export default function ColumnDialog({ + open, + mode, // 'add' | 'modify' | 'drop' + onSubmit, +}) { + // Single dialog component, multiple use cases +} + +// Usage: + + + +``` + +## State Management + +### Local State +```typescript +// ✅ Good - Related state grouped +const [dialog, setDialog] = useState({ open: false, mode: 'add' }); + +// ❌ Bad - Too many separate states +const [openAddDialog, setOpenAddDialog] = useState(false); +const [openModifyDialog, setOpenModifyDialog] = useState(false); +const [openDropDialog, setOpenDropDialog] = useState(false); +``` + +### Async Operations +```typescript +// ✅ Good - Proper error handling +const handleSubmit = async () => { + setLoading(true); + try { + await apiCall(); + setSuccess('Operation completed'); + } catch (error) { + setError(error.message); + } finally { + setLoading(false); + } +}; + +// ❌ Bad - No error handling +const handleSubmit = async () => { + await apiCall(); + setSuccess('Done'); +}; +``` + +## API Route Standards + +### Validation Pattern +```typescript +// ✅ Good - Validate inputs +export async function POST(request: Request) { + const session = await getSession(); + if (!session) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { tableName, columns } = await request.json(); + + if (!tableName || !columns || columns.length === 0) { + return NextResponse.json( + { error: 'Table name and columns are required' }, + { status: 400 } + ); + } + + if (!isValidIdentifier(tableName)) { + return NextResponse.json( + { error: 'Invalid table name format' }, + { status: 400 } + ); + } + + // Process request... +} +``` + +### SQL Injection Prevention +```typescript +// ✅ Good - Validate identifiers +function isValidIdentifier(name: string): boolean { + return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name); +} + +// ✅ Good - Use parameterized queries +await db.execute(sql`SELECT * FROM ${sql.identifier(tableName)}`); + +// ❌ Bad - String concatenation +await db.execute(`SELECT * FROM ${tableName}`); // SQL injection risk! +``` + +## Testing Standards + +### Test File Naming +- Unit tests: `ComponentName.test.tsx` or `utilityName.test.ts` +- Integration tests: `tests/integration/FeatureName.spec.ts` +- E2E tests: `tests/e2e/FeatureName.e2e.ts` + +### Test Structure +```typescript +import { describe, expect, it } from 'vitest'; + +describe('FeatureName', () => { + describe('Specific functionality', () => { + it('should do something specific', () => { + // Arrange + const input = 'test'; + + // Act + const result = functionUnderTest(input); + + // Assert + expect(result).toBe('expected'); + }); + }); +}); +``` + +### Playwright Test Pattern +```typescript +test.describe('Feature Name', () => { + test('should validate API endpoint', async ({ page }) => { + const response = await page.request.post('/api/endpoint', { + data: { field: 'value' }, + }); + + expect(response.status()).toBe(200); + }); +}); +``` + +## Material-UI Standards + +### Component Usage +```typescript +// ✅ Good - Consistent spacing + + + + +// ❌ Bad - Inconsistent styling +
+ +
+``` + +### Dialog Pattern +```typescript +// ✅ Good - Complete dialog structure + + Title + + {/* Content */} + + + + + + +``` + +## Error Handling + +### User-Facing Errors +```typescript +// ✅ Good - Clear, actionable messages +setError('Table name must contain only letters, numbers, and underscores'); + +// ❌ Bad - Technical jargon +setError('RegExp validation failed on identifier'); +``` + +### API Errors +```typescript +// ✅ Good - Structured error responses +return NextResponse.json( + { + error: 'Invalid table name format', + details: 'Table names must start with a letter or underscore' + }, + { status: 400 } +); +``` + +## Documentation + +### Component Documentation +```typescript +/** + * Dialog for creating a new database table + * + * Features: + * - Dynamic column builder + * - Type selection from config + * - Validation for table/column names + * + * @example + * + */ +export default function CreateTableDialog(props) { } +``` + +### Function Documentation +```typescript +/** + * Validates if a string is a safe SQL identifier + * Prevents SQL injection by ensuring only alphanumeric and underscore + * + * @param name - The identifier to validate + * @returns true if valid, false otherwise + * + * @example + * isValidIdentifier('my_table') // true + * isValidIdentifier('my-table!') // false + */ +function isValidIdentifier(name: string): boolean { } +``` + +## Git Commit Standards + +### Commit Message Format +``` +type(scope): Short description + +Longer description if needed + +- List changes +- One per line +``` + +### Commit Types +- `feat`: New feature +- `fix`: Bug fix +- `refactor`: Code refactoring +- `test`: Adding tests +- `docs`: Documentation changes +- `style`: Code style changes (formatting) +- `chore`: Maintenance tasks + +### Examples +``` +feat(admin): Add table manager UI component + +- Create TableManagerTab component +- Extract CreateTableDialog to separate file +- Use features.json for configuration +- Add validation for table names + +fix(api): Prevent SQL injection in table creation + +- Add identifier validation +- Use parameterized queries +- Add security tests +``` + +## Performance Best Practices + +### Avoid Unnecessary Re-renders +```typescript +// ✅ Good - Memoize callbacks +const handleClick = useCallback(() => { + doSomething(); +}, [dependency]); + +// ✅ Good - Memoize expensive computations +const derivedData = useMemo(() => { + return expensiveComputation(data); +}, [data]); +``` + +### Optimize Bundle Size +```typescript +// ✅ Good - Named imports +import { Button, TextField } from '@mui/material'; + +// ❌ Bad - Default imports (larger bundle) +import Button from '@mui/material/Button'; +import TextField from '@mui/material/TextField'; +``` + +## Security Best Practices + +### Authentication +```typescript +// ✅ Good - Always check session first +const session = await getSession(); +if (!session) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); +} +``` + +### Input Validation +```typescript +// ✅ Good - Validate all inputs +if (!isValidIdentifier(tableName)) { + return NextResponse.json({ error: 'Invalid format' }, { status: 400 }); +} + +// ✅ Good - Sanitize user input +const sanitized = tableName.trim().toLowerCase(); +``` + +## ESLint & Prettier + +This project uses ESLint and Prettier for code quality: + +```bash +# Check code style +npm run lint + +# Fix auto-fixable issues +npm run lint:fix + +# Check TypeScript types +npm run check:types +``` + +### Key Rules +- **No unused variables**: Remove or prefix with `_` +- **Consistent quotes**: Single quotes for strings +- **Semicolons**: Required at end of statements +- **Indentation**: 2 spaces +- **Line length**: Max 100 characters (soft limit) +- **Trailing commas**: Required in multiline + +## Quick Reference + +### Component Checklist +- [ ] Less than 200 lines +- [ ] Uses feature config from JSON +- [ ] Has proper TypeScript types +- [ ] Includes error handling +- [ ] Has tests (if logic-heavy) +- [ ] Follows naming conventions +- [ ] Documented if complex + +### PR Checklist +- [ ] Code follows style guide +- [ ] Components are small and reusable +- [ ] Uses configuration from features.json +- [ ] Tests added/updated +- [ ] Documentation updated +- [ ] Linter passes +- [ ] Type checking passes +- [ ] No console.log statements +- [ ] Error handling implemented + +--- + +**Last Updated**: January 2026 +**Maintained by**: Development Team +**Questions?**: Open an issue with label `documentation` diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..45c7ebd --- /dev/null +++ b/TESTING.md @@ -0,0 +1,226 @@ +# Testing Guide for Table Manager and Column Manager Features + +This document describes the test coverage for the newly implemented Table Manager and Column Manager features in the PostgreSQL Admin Panel. + +## Test Files + +### Integration Tests (Playwright API Tests) + +#### 1. `tests/integration/TableManager.spec.ts` +Tests for the Table Management API endpoints (`/api/admin/table-manage`): + +**Create Table Tests:** +- ✅ Creates new table with proper column definitions +- ✅ Validates table name is required +- ✅ Validates at least one column is required +- ✅ Rejects invalid table names (SQL injection prevention) +- ✅ Requires authentication for all operations + +**Drop Table Tests:** +- ✅ Validates table name is required +- ✅ Rejects invalid table names +- ✅ Requires authentication + +**Test Coverage:** +- Input validation +- SQL injection prevention +- Authentication/authorization +- Error handling + +#### 2. `tests/integration/ColumnManager.spec.ts` +Tests for the Column Management API endpoints (`/api/admin/column-manage`): + +**Add Column Tests:** +- ✅ Requires authentication +- ✅ Validates all required fields (tableName, columnName, dataType) +- ✅ Rejects invalid table names +- ✅ Rejects invalid column names + +**Modify Column Tests:** +- ✅ Requires authentication +- ✅ Validates required fields +- ✅ Rejects invalid identifiers + +**Drop Column Tests:** +- ✅ Requires authentication +- ✅ Validates required fields +- ✅ Rejects invalid identifiers + +**Test Coverage:** +- Input validation +- SQL injection prevention +- Authentication/authorization +- Error handling for all CRUD operations + +### End-to-End Tests (Playwright UI Tests) + +#### 3. `tests/e2e/AdminDashboard.e2e.ts` +Tests for the admin dashboard UI and user flows: + +**Navigation Tests:** +- ✅ Redirects to login when not authenticated +- ✅ Displays login page with proper form elements + +**Table Manager UI Tests:** +- 🔄 Display Table Manager tab (requires auth - skipped) +- 🔄 Open create table dialog (requires auth - skipped) + +**Column Manager UI Tests:** +- 🔄 Display Column Manager tab (requires auth - skipped) +- 🔄 Show table selector (requires auth - skipped) + +**Security Tests:** +- ✅ Blocks admin API access without authentication +- ✅ Blocks table management without authentication +- ✅ Blocks column management without authentication + +**Note:** Some UI tests are skipped because they require an authenticated session. These can be enabled when a test authentication mechanism is implemented. + +## Running Tests + +### Run All Tests +```bash +npm test # Run Vitest unit tests +npm run test:e2e # Run Playwright E2E tests +``` + +### Run Specific Test Files +```bash +# Run integration tests only +npx playwright test tests/integration/ + +# Run specific test file +npx playwright test tests/integration/TableManager.spec.ts + +# Run e2e tests only +npx playwright test tests/e2e/ +``` + +### Run Tests in Watch Mode +```bash +npm run test -- --watch # Vitest watch mode +``` + +### Run Tests with UI +```bash +npx playwright test --ui # Playwright UI mode +``` + +## Test Structure + +### Integration Tests Pattern +```typescript +test.describe('Feature Name', () => { + test.describe('Specific Functionality', () => { + test('should do something specific', async ({ page }) => { + const response = await page.request.post('/api/endpoint', { + data: { /* test data */ }, + }); + + expect(response.status()).toBe(expectedStatus); + }); + }); +}); +``` + +### E2E Tests Pattern +```typescript +test.describe('UI Feature', () => { + test('should display correct elements', async ({ page }) => { + await page.goto('/path'); + + await expect(page.getByRole('button', { name: /action/i })).toBeVisible(); + }); +}); +``` + +## Security Testing + +All tests verify that: +1. **Authentication is required** for admin operations +2. **Input validation** prevents SQL injection +3. **Invalid identifiers** are rejected (table/column names) +4. **Error messages** don't leak sensitive information + +## Test Coverage Summary + +| Feature | API Tests | UI Tests | Security Tests | Total Tests | +|---------|-----------|----------|----------------|-------------| +| Table Manager | 7 | 2 (2 skipped) | 3 | 12 | +| Column Manager | 9 | 2 (2 skipped) | 3 | 14 | +| Admin Dashboard | - | 3 | 3 | 6 | +| **Total** | **16** | **7** | **9** | **32** | + +## Future Test Improvements + +### Short Term +- [ ] Add authenticated session fixture for UI tests +- [ ] Enable skipped UI tests with proper authentication +- [ ] Add tests for success scenarios with valid credentials +- [ ] Test visual column builder interactions +- [ ] Test schema refresh after operations + +### Medium Term +- [ ] Add performance tests for large table operations +- [ ] Add accessibility tests (a11y) +- [ ] Add visual regression tests +- [ ] Test error recovery and rollback scenarios +- [ ] Add tests for concurrent operations + +### Long Term +- [ ] Integration tests with real PostgreSQL database +- [ ] Load testing for multiple simultaneous users +- [ ] Cross-browser compatibility tests +- [ ] Mobile responsiveness tests + +## Continuous Integration + +Tests are designed to run in CI/CD pipelines: + +```yaml +# Example CI configuration +- name: Run Integration Tests + run: npm run test:e2e -- tests/integration/ + +- name: Run E2E Tests + run: npm run test:e2e -- tests/e2e/ +``` + +## Test Data Management + +- Tests use **faker** library for generating random test data +- Each test run creates unique table names to avoid conflicts +- Tests validate authentication is required, so they expect 401 responses when not authenticated +- No database cleanup is required for API validation tests + +## Debugging Tests + +### View Test Results +```bash +npx playwright show-report # View HTML report +``` + +### Debug Specific Test +```bash +npx playwright test --debug tests/integration/TableManager.spec.ts +``` + +### View Test Traces +```bash +npx playwright show-trace trace.zip +``` + +## Contributing + +When adding new features: +1. Add integration tests for new API endpoints +2. Add E2E tests for new UI components +3. Ensure security tests cover authentication +4. Update this documentation with new test coverage +5. Run all tests before submitting PR + +--- + +**Last Updated:** January 2026 +**Test Framework:** Playwright + Vitest +**Coverage Status:** ✅ API Validation | 🔄 UI Tests (partial - needs auth) diff --git a/src/components/admin/ColumnDialog.tsx b/src/components/admin/ColumnDialog.tsx new file mode 100644 index 0000000..bb7db87 --- /dev/null +++ b/src/components/admin/ColumnDialog.tsx @@ -0,0 +1,202 @@ +'use client'; + +import { + Button, + Checkbox, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControlLabel, + MenuItem, + Select, + TextField, + Typography, +} from '@mui/material'; +import { useEffect, useState } from 'react'; + +type ColumnDialogProps = { + open: boolean; + mode: 'add' | 'modify' | 'drop'; + tableName: string; + columns?: Array<{ column_name: string }>; + onClose: () => void; + onSubmit: (data: any) => Promise; + dataTypes?: string[]; +}; + +export default function ColumnDialog({ + open, + mode, + tableName, + columns = [], + onClose, + onSubmit, + dataTypes = ['INTEGER', 'BIGINT', 'VARCHAR', 'TEXT', 'BOOLEAN', 'TIMESTAMP', 'DATE', 'JSON', 'JSONB'], +}: ColumnDialogProps) { + const [columnName, setColumnName] = useState(''); + const [columnType, setColumnType] = useState('VARCHAR'); + const [nullable, setNullable] = useState(true); + const [defaultValue, setDefaultValue] = useState(''); + const [selectedColumn, setSelectedColumn] = useState(''); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (!open) { + // Reset form when dialog closes + setColumnName(''); + setColumnType('VARCHAR'); + setNullable(true); + setDefaultValue(''); + setSelectedColumn(''); + } + }, [open]); + + const handleSubmit = async () => { + setLoading(true); + try { + const data: any = {}; + + if (mode === 'add') { + data.columnName = columnName; + data.dataType = columnType; + data.nullable = nullable; + if (defaultValue) data.defaultValue = defaultValue; + } else if (mode === 'modify') { + data.columnName = selectedColumn; + data.newType = columnType; + data.nullable = nullable; + } else if (mode === 'drop') { + data.columnName = selectedColumn; + } + + await onSubmit(data); + onClose(); + } finally { + setLoading(false); + } + }; + + const getTitle = () => { + switch (mode) { + case 'add': + return `Add Column to ${tableName}`; + case 'modify': + return `Modify Column in ${tableName}`; + case 'drop': + return `Drop Column from ${tableName}`; + default: + return 'Column Operation'; + } + }; + + const isFormValid = () => { + if (mode === 'add') { + return columnName.trim() && columnType; + } + return selectedColumn.trim(); + }; + + return ( + + {getTitle()} + + {mode === 'drop' && ( + + Warning: This will permanently delete the column and all its data! + + )} + + {mode === 'add' ? ( + <> + setColumnName(e.target.value)} + sx={{ mt: 2, mb: 2 }} + /> + + setNullable(e.target.checked)} /> + } + label="Nullable" + sx={{ mb: 2 }} + /> + setDefaultValue(e.target.value)} + /> + + ) : ( + <> + + + {mode === 'modify' && selectedColumn && ( + <> + + setNullable(e.target.checked)} /> + } + label="Nullable" + /> + + )} + + )} + + + + + + + ); +} diff --git a/src/components/admin/ColumnManagerTab.tsx b/src/components/admin/ColumnManagerTab.tsx new file mode 100644 index 0000000..8baaefa --- /dev/null +++ b/src/components/admin/ColumnManagerTab.tsx @@ -0,0 +1,215 @@ +'use client'; + +import AddIcon from '@mui/icons-material/Add'; +import DeleteIcon from '@mui/icons-material/Delete'; +import EditIcon from '@mui/icons-material/Edit'; +import { + Box, + Button, + MenuItem, + Paper, + Select, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, +} from '@mui/material'; +import { useEffect, useState } from 'react'; +import { getDataTypes, getFeatureById } from '@/utils/featureConfig'; +import ColumnDialog from './ColumnDialog'; + +type ColumnManagerTabProps = { + tables: Array<{ table_name: string }>; + onAddColumn: (tableName: string, data: any) => Promise; + onModifyColumn: (tableName: string, data: any) => Promise; + onDropColumn: (tableName: string, data: any) => Promise; +}; + +export default function ColumnManagerTab({ + tables, + onAddColumn, + onModifyColumn, + onDropColumn, +}: ColumnManagerTabProps) { + const [selectedTable, setSelectedTable] = useState(''); + const [tableSchema, setTableSchema] = useState(null); + const [dialogState, setDialogState] = useState<{ + open: boolean; + mode: 'add' | 'modify' | 'drop'; + }>({ open: false, mode: 'add' }); + + // Get feature configuration from JSON + const feature = getFeatureById('column-management'); + const dataTypes = getDataTypes().map(dt => dt.name); + + // Check if actions are enabled from config + const canAdd = feature?.ui.actions.includes('add'); + const canModify = feature?.ui.actions.includes('modify'); + const canDelete = feature?.ui.actions.includes('delete'); + + // Fetch schema when table is selected + useEffect(() => { + if (selectedTable) { + fetchTableSchema(); + } else { + setTableSchema(null); + } + }, [selectedTable]); + + const fetchTableSchema = async () => { + try { + const response = await fetch('/api/admin/table-schema', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ tableName: selectedTable }), + }); + + if (response.ok) { + const data = await response.json(); + setTableSchema(data); + } + } catch (error) { + console.error('Failed to fetch schema:', error); + } + }; + + const handleColumnOperation = async (data: any) => { + switch (dialogState.mode) { + case 'add': + await onAddColumn(selectedTable, data); + break; + case 'modify': + await onModifyColumn(selectedTable, data); + break; + case 'drop': + await onDropColumn(selectedTable, data); + break; + } + await fetchTableSchema(); // Refresh schema + }; + + const openDialog = (mode: 'add' | 'modify' | 'drop') => { + setDialogState({ open: true, mode }); + }; + + const closeDialog = () => { + setDialogState({ ...dialogState, open: false }); + }; + + return ( + <> + + {feature?.name || 'Column Manager'} + + + {feature?.description && ( + + {feature.description} + + )} + + + + Select a table to manage its columns: + + + + + {selectedTable && ( + <> + + {canAdd && ( + + )} + {canModify && ( + + )} + {canDelete && ( + + )} + + + {tableSchema && ( + + + + Current Columns for {selectedTable} + + + + + + Column Name + Data Type + Nullable + Default + + + + {tableSchema.columns?.map((col: any) => ( + + {col.column_name} + {col.data_type} + {col.is_nullable} + {col.column_default || 'NULL'} + + ))} + +
+
+
+
+ )} + + )} + + + + ); +} diff --git a/src/components/admin/CreateTableDialog.tsx b/src/components/admin/CreateTableDialog.tsx new file mode 100644 index 0000000..45dc4da --- /dev/null +++ b/src/components/admin/CreateTableDialog.tsx @@ -0,0 +1,162 @@ +'use client'; + +import AddIcon from '@mui/icons-material/Add'; +import DeleteIcon from '@mui/icons-material/Delete'; +import { + Box, + Button, + Checkbox, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControlLabel, + IconButton, + MenuItem, + Select, + TextField, + Typography, +} from '@mui/material'; +import { useState } from 'react'; + +type Column = { + name: string; + type: string; + length?: number; + nullable: boolean; + primaryKey: boolean; +}; + +type CreateTableDialogProps = { + open: boolean; + onClose: () => void; + onCreate: (tableName: string, columns: Column[]) => Promise; + dataTypes?: string[]; +}; + +export default function CreateTableDialog({ + open, + onClose, + onCreate, + dataTypes = ['INTEGER', 'BIGINT', 'SERIAL', 'VARCHAR', 'TEXT', 'BOOLEAN', 'TIMESTAMP', 'DATE', 'JSON', 'JSONB'], +}: CreateTableDialogProps) { + const [tableName, setTableName] = useState(''); + const [columns, setColumns] = useState([ + { name: '', type: 'VARCHAR', length: 255, nullable: true, primaryKey: false }, + ]); + const [loading, setLoading] = useState(false); + + const handleCreate = async () => { + setLoading(true); + try { + await onCreate(tableName, columns.filter(col => col.name.trim())); + handleClose(); + } finally { + setLoading(false); + } + }; + + const handleClose = () => { + setTableName(''); + setColumns([{ name: '', type: 'VARCHAR', length: 255, nullable: true, primaryKey: false }]); + onClose(); + }; + + const addColumn = () => { + setColumns([...columns, { name: '', type: 'VARCHAR', length: 255, nullable: true, primaryKey: false }]); + }; + + const updateColumn = (index: number, field: string, value: any) => { + const updated = [...columns]; + updated[index] = { ...updated[index], [field]: value }; + setColumns(updated); + }; + + const removeColumn = (index: number) => { + if (columns.length > 1) { + setColumns(columns.filter((_, i) => i !== index)); + } + }; + + return ( + + Create New Table + + setTableName(e.target.value)} + sx={{ mt: 2, mb: 2 }} + /> + + Columns: + + {columns.map((col, index) => ( + + updateColumn(index, 'name', e.target.value)} + sx={{ mr: 1, mb: 1 }} + /> + + {col.type === 'VARCHAR' && ( + updateColumn(index, 'length', e.target.value)} + sx={{ mr: 1, mb: 1, width: 100 }} + /> + )} + updateColumn(index, 'nullable', e.target.checked)} + /> + } + label="Nullable" + sx={{ mr: 1 }} + /> + updateColumn(index, 'primaryKey', e.target.checked)} + /> + } + label="Primary Key" + sx={{ mr: 1 }} + /> + {columns.length > 1 && ( + removeColumn(index)} color="error" size="small"> + + + )} + + ))} + + + + + + + + ); +} diff --git a/src/components/admin/DropTableDialog.tsx b/src/components/admin/DropTableDialog.tsx new file mode 100644 index 0000000..b242ef4 --- /dev/null +++ b/src/components/admin/DropTableDialog.tsx @@ -0,0 +1,85 @@ +'use client'; + +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + MenuItem, + Select, + Typography, +} from '@mui/material'; +import { useState } from 'react'; + +type DropTableDialogProps = { + open: boolean; + tables: Array<{ table_name: string }>; + onClose: () => void; + onDrop: (tableName: string) => Promise; +}; + +export default function DropTableDialog({ + open, + tables, + onClose, + onDrop, +}: DropTableDialogProps) { + const [selectedTable, setSelectedTable] = useState(''); + const [loading, setLoading] = useState(false); + + const handleDrop = async () => { + if (!selectedTable) return; + + setLoading(true); + try { + await onDrop(selectedTable); + handleClose(); + } finally { + setLoading(false); + } + }; + + const handleClose = () => { + setSelectedTable(''); + onClose(); + }; + + return ( + + Drop Table + + + Warning: This will permanently delete the table and all its data! + + + + + + + + + ); +} diff --git a/src/components/admin/TableManagerTab.tsx b/src/components/admin/TableManagerTab.tsx new file mode 100644 index 0000000..9c574f4 --- /dev/null +++ b/src/components/admin/TableManagerTab.tsx @@ -0,0 +1,116 @@ +'use client'; + +import AddIcon from '@mui/icons-material/Add'; +import DeleteIcon from '@mui/icons-material/Delete'; +import TableChartIcon from '@mui/icons-material/TableChart'; +import { + Box, + Button, + List, + ListItem, + ListItemIcon, + ListItemText, + Paper, + Typography, +} from '@mui/material'; +import { useState } from 'react'; +import { getDataTypes, getFeatureById } from '@/utils/featureConfig'; +import CreateTableDialog from './CreateTableDialog'; +import DropTableDialog from './DropTableDialog'; + +type TableManagerTabProps = { + tables: Array<{ table_name: string }>; + onCreateTable: (tableName: string, columns: any[]) => Promise; + onDropTable: (tableName: string) => Promise; +}; + +export default function TableManagerTab({ + tables, + onCreateTable, + onDropTable, +}: TableManagerTabProps) { + const [openCreateDialog, setOpenCreateDialog] = useState(false); + const [openDropDialog, setOpenDropDialog] = useState(false); + + // Get feature configuration from JSON + const feature = getFeatureById('table-management'); + const dataTypes = getDataTypes().map(dt => dt.name); + + // Check if actions are enabled + const canCreate = feature?.ui.actions.includes('create'); + const canDelete = feature?.ui.actions.includes('delete'); + + return ( + <> + + {feature?.name || 'Table Manager'} + + + {feature?.description && ( + + {feature.description} + + )} + + + {canCreate && ( + + )} + {canDelete && ( + + )} + + + + + + Existing Tables + + + {tables.map(table => ( + + + + + + + ))} + {tables.length === 0 && ( + + + + )} + + + + + setOpenCreateDialog(false)} + onCreate={onCreateTable} + dataTypes={dataTypes} + /> + + setOpenDropDialog(false)} + onDrop={onDropTable} + /> + + ); +} diff --git a/src/utils/featureConfig.test.ts b/src/utils/featureConfig.test.ts new file mode 100644 index 0000000..766c39a --- /dev/null +++ b/src/utils/featureConfig.test.ts @@ -0,0 +1,337 @@ +import { describe, expect, it } from 'vitest'; +import { + getDataTypes, + getEnabledFeaturesByPriority, + getFeatureById, + getFeatures, + getNavItems, +} from './featureConfig'; + +describe('FeatureConfig', () => { + describe('getFeatures', () => { + it('should return only enabled features', () => { + const features = getFeatures(); + + expect(features).toBeDefined(); + expect(Array.isArray(features)).toBe(true); + + // All returned features should be enabled + features.forEach(feature => { + expect(feature.enabled).toBe(true); + }); + }); + + it('should return features with required properties', () => { + const features = getFeatures(); + + features.forEach(feature => { + expect(feature).toHaveProperty('id'); + expect(feature).toHaveProperty('name'); + expect(feature).toHaveProperty('description'); + expect(feature).toHaveProperty('enabled'); + expect(feature).toHaveProperty('priority'); + expect(feature).toHaveProperty('endpoints'); + expect(feature).toHaveProperty('ui'); + }); + }); + + it('should return features with valid UI configuration', () => { + const features = getFeatures(); + + features.forEach(feature => { + expect(feature.ui).toHaveProperty('showInNav'); + expect(feature.ui).toHaveProperty('icon'); + expect(feature.ui).toHaveProperty('actions'); + expect(Array.isArray(feature.ui.actions)).toBe(true); + }); + }); + }); + + describe('getFeatureById', () => { + it('should return feature when ID exists and is enabled', () => { + const feature = getFeatureById('database-crud'); + + expect(feature).toBeDefined(); + expect(feature?.id).toBe('database-crud'); + expect(feature?.enabled).toBe(true); + }); + + it('should return feature for table-management', () => { + const feature = getFeatureById('table-management'); + + expect(feature).toBeDefined(); + expect(feature?.id).toBe('table-management'); + expect(feature?.name).toBe('Table Management'); + }); + + it('should return feature for column-management', () => { + const feature = getFeatureById('column-management'); + + expect(feature).toBeDefined(); + expect(feature?.id).toBe('column-management'); + expect(feature?.name).toBe('Column Management'); + }); + + it('should return feature for sql-query', () => { + const feature = getFeatureById('sql-query'); + + expect(feature).toBeDefined(); + expect(feature?.id).toBe('sql-query'); + expect(feature?.name).toBe('SQL Query Interface'); + }); + + it('should return undefined for non-existent feature ID', () => { + const feature = getFeatureById('non-existent-feature'); + + expect(feature).toBeUndefined(); + }); + + it('should return undefined for disabled feature', () => { + // This test assumes there might be disabled features in the config + const features = getFeatures(); + const enabledIds = features.map(f => f.id); + + // Try to get a feature that doesn't exist in enabled list + const disabledFeature = getFeatureById('disabled-test-feature'); + expect(disabledFeature).toBeUndefined(); + }); + }); + + describe('getDataTypes', () => { + it('should return array of data types', () => { + const dataTypes = getDataTypes(); + + expect(dataTypes).toBeDefined(); + expect(Array.isArray(dataTypes)).toBe(true); + expect(dataTypes.length).toBeGreaterThan(0); + }); + + it('should return data types with required properties', () => { + const dataTypes = getDataTypes(); + + dataTypes.forEach(dataType => { + expect(dataType).toHaveProperty('name'); + expect(dataType).toHaveProperty('category'); + expect(dataType).toHaveProperty('requiresLength'); + expect(typeof dataType.name).toBe('string'); + expect(typeof dataType.category).toBe('string'); + expect(typeof dataType.requiresLength).toBe('boolean'); + }); + }); + + it('should include common PostgreSQL data types', () => { + const dataTypes = getDataTypes(); + const typeNames = dataTypes.map(dt => dt.name); + + // Check for essential PostgreSQL types + expect(typeNames).toContain('INTEGER'); + expect(typeNames).toContain('VARCHAR'); + expect(typeNames).toContain('TEXT'); + expect(typeNames).toContain('BOOLEAN'); + expect(typeNames).toContain('TIMESTAMP'); + }); + + it('should have VARCHAR with requiresLength = true', () => { + const dataTypes = getDataTypes(); + const varchar = dataTypes.find(dt => dt.name === 'VARCHAR'); + + expect(varchar).toBeDefined(); + expect(varchar?.requiresLength).toBe(true); + expect(varchar?.defaultLength).toBeDefined(); + }); + + it('should have INTEGER with requiresLength = false', () => { + const dataTypes = getDataTypes(); + const integer = dataTypes.find(dt => dt.name === 'INTEGER'); + + expect(integer).toBeDefined(); + expect(integer?.requiresLength).toBe(false); + }); + + it('should categorize data types correctly', () => { + const dataTypes = getDataTypes(); + + const integer = dataTypes.find(dt => dt.name === 'INTEGER'); + expect(integer?.category).toBe('numeric'); + + const varchar = dataTypes.find(dt => dt.name === 'VARCHAR'); + expect(varchar?.category).toBe('text'); + + const boolean = dataTypes.find(dt => dt.name === 'BOOLEAN'); + expect(boolean?.category).toBe('boolean'); + }); + }); + + describe('getNavItems', () => { + it('should return array of navigation items', () => { + const navItems = getNavItems(); + + expect(navItems).toBeDefined(); + expect(Array.isArray(navItems)).toBe(true); + }); + + it('should return nav items with required properties', () => { + const navItems = getNavItems(); + + navItems.forEach(item => { + expect(item).toHaveProperty('id'); + expect(item).toHaveProperty('label'); + expect(item).toHaveProperty('icon'); + expect(item).toHaveProperty('featureId'); + }); + }); + + it('should only return nav items for enabled features', () => { + const navItems = getNavItems(); + + navItems.forEach(item => { + const feature = getFeatureById(item.featureId); + expect(feature).toBeDefined(); + expect(feature?.enabled).toBe(true); + }); + }); + + it('should include Tables nav item', () => { + const navItems = getNavItems(); + const tablesItem = navItems.find(item => item.id === 'tables'); + + expect(tablesItem).toBeDefined(); + expect(tablesItem?.featureId).toBe('database-crud'); + }); + + it('should include SQL Query nav item', () => { + const navItems = getNavItems(); + const queryItem = navItems.find(item => item.id === 'query'); + + expect(queryItem).toBeDefined(); + expect(queryItem?.featureId).toBe('sql-query'); + }); + + it('should include Table Manager nav item', () => { + const navItems = getNavItems(); + const tableManagerItem = navItems.find(item => item.id === 'table-manager'); + + expect(tableManagerItem).toBeDefined(); + expect(tableManagerItem?.featureId).toBe('table-management'); + }); + }); + + describe('getEnabledFeaturesByPriority', () => { + it('should return features with high priority', () => { + const highPriorityFeatures = getEnabledFeaturesByPriority('high'); + + expect(Array.isArray(highPriorityFeatures)).toBe(true); + + highPriorityFeatures.forEach(feature => { + expect(feature.priority).toBe('high'); + expect(feature.enabled).toBe(true); + }); + }); + + it('should return all enabled features have high priority', () => { + const highPriorityFeatures = getEnabledFeaturesByPriority('high'); + + // All current features should have high priority + expect(highPriorityFeatures.length).toBeGreaterThan(0); + + const ids = highPriorityFeatures.map(f => f.id); + expect(ids).toContain('database-crud'); + expect(ids).toContain('table-management'); + expect(ids).toContain('column-management'); + expect(ids).toContain('sql-query'); + }); + + it('should return empty array for non-existent priority', () => { + const features = getEnabledFeaturesByPriority('non-existent-priority'); + + expect(Array.isArray(features)).toBe(true); + expect(features.length).toBe(0); + }); + + it('should return empty array for low priority (if none exist)', () => { + const lowPriorityFeatures = getEnabledFeaturesByPriority('low'); + + expect(Array.isArray(lowPriorityFeatures)).toBe(true); + // Expecting 0 since current config has all high priority + expect(lowPriorityFeatures.length).toBe(0); + }); + }); + + describe('Feature endpoints', () => { + it('should have valid endpoint structure for database-crud', () => { + const feature = getFeatureById('database-crud'); + + expect(feature?.endpoints).toBeDefined(); + expect(Array.isArray(feature?.endpoints)).toBe(true); + expect(feature?.endpoints.length).toBeGreaterThan(0); + + feature?.endpoints.forEach(endpoint => { + expect(endpoint).toHaveProperty('path'); + expect(endpoint).toHaveProperty('methods'); + expect(endpoint).toHaveProperty('description'); + expect(endpoint.path).toMatch(/^\/api\//); + }); + }); + + it('should have valid endpoint structure for table-management', () => { + const feature = getFeatureById('table-management'); + + expect(feature?.endpoints).toBeDefined(); + const tableManageEndpoint = feature?.endpoints.find( + ep => ep.path === '/api/admin/table-manage' + ); + + expect(tableManageEndpoint).toBeDefined(); + expect(tableManageEndpoint?.methods).toContain('POST'); + expect(tableManageEndpoint?.methods).toContain('DELETE'); + }); + + it('should have valid endpoint structure for column-management', () => { + const feature = getFeatureById('column-management'); + + expect(feature?.endpoints).toBeDefined(); + const columnManageEndpoint = feature?.endpoints.find( + ep => ep.path === '/api/admin/column-manage' + ); + + expect(columnManageEndpoint).toBeDefined(); + expect(columnManageEndpoint?.methods).toContain('POST'); + expect(columnManageEndpoint?.methods).toContain('PUT'); + expect(columnManageEndpoint?.methods).toContain('DELETE'); + }); + }); + + describe('Feature UI configuration', () => { + it('should have correct UI actions for database-crud', () => { + const feature = getFeatureById('database-crud'); + + expect(feature?.ui.actions).toContain('create'); + expect(feature?.ui.actions).toContain('read'); + expect(feature?.ui.actions).toContain('update'); + expect(feature?.ui.actions).toContain('delete'); + }); + + it('should have correct UI actions for table-management', () => { + const feature = getFeatureById('table-management'); + + expect(feature?.ui.actions).toContain('create'); + expect(feature?.ui.actions).toContain('delete'); + }); + + it('should have correct UI actions for column-management', () => { + const feature = getFeatureById('column-management'); + + expect(feature?.ui.actions).toContain('add'); + expect(feature?.ui.actions).toContain('modify'); + expect(feature?.ui.actions).toContain('delete'); + }); + + it('should all features have showInNav in UI config', () => { + const features = getFeatures(); + + features.forEach(feature => { + expect(typeof feature.ui.showInNav).toBe('boolean'); + }); + }); + }); +});