From 5560cc184ac57b6fbca157e78a579968b40b9291 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 02:28:39 +0000 Subject: [PATCH] Add configuration-driven features with JSON and reusable components Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com> --- src/app/admin/dashboard/page.tsx | 42 ++++- src/app/api/admin/column-manage/route.ts | 214 +++++++++++++++++++++++ src/app/api/admin/record/route.ts | 204 +++++++++++++++++++++ src/app/api/admin/table-manage/route.ts | 154 ++++++++++++++++ src/app/api/admin/table-schema/route.ts | 104 +++++++++++ src/components/admin/ConfirmDialog.tsx | 45 +++++ src/components/admin/DataGrid.tsx | 77 ++++++++ src/components/admin/FormDialog.tsx | 99 +++++++++++ src/config/features.json | 164 +++++++++++++++++ src/utils/featureConfig.ts | 59 +++++++ 10 files changed, 1158 insertions(+), 4 deletions(-) create mode 100644 src/app/api/admin/column-manage/route.ts create mode 100644 src/app/api/admin/record/route.ts create mode 100644 src/app/api/admin/table-manage/route.ts create mode 100644 src/app/api/admin/table-schema/route.ts create mode 100644 src/components/admin/ConfirmDialog.tsx create mode 100644 src/components/admin/DataGrid.tsx create mode 100644 src/components/admin/FormDialog.tsx create mode 100644 src/config/features.json create mode 100644 src/utils/featureConfig.ts diff --git a/src/app/admin/dashboard/page.tsx b/src/app/admin/dashboard/page.tsx index ac4721c..275d9ea 100644 --- a/src/app/admin/dashboard/page.tsx +++ b/src/app/admin/dashboard/page.tsx @@ -1,15 +1,24 @@ 'use client'; +import AddIcon from '@mui/icons-material/Add'; 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 StorageIcon from '@mui/icons-material/Storage'; +import TableChartIcon from '@mui/icons-material/TableChart'; import { Alert, AppBar, Box, Button, CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogTitle, Drawer, + IconButton, List, ListItem, ListItemButton, @@ -62,8 +71,18 @@ export default function AdminDashboard() { const [selectedTable, setSelectedTable] = useState(''); const [queryText, setQueryText] = useState(''); const [queryResult, setQueryResult] = useState(null); + const [tableSchema, setTableSchema] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); + const [successMessage, setSuccessMessage] = useState(''); + + // Dialog states + const [openCreateDialog, setOpenCreateDialog] = useState(false); + const [openEditDialog, setOpenEditDialog] = useState(false); + const [openDeleteDialog, setOpenDeleteDialog] = useState(false); + const [editingRecord, setEditingRecord] = useState(null); + const [deletingRecord, setDeletingRecord] = useState(null); + const [formData, setFormData] = useState({}); const fetchTables = useCallback(async () => { try { @@ -90,11 +109,12 @@ export default function AdminDashboard() { setSelectedTable(tableName); setLoading(true); setError(''); + setSuccessMessage(''); setQueryResult(null); try { - // Use dedicated API with table name validation - const response = await fetch('/api/admin/table-data', { + // Fetch table data + const dataResponse = await fetch('/api/admin/table-data', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -103,12 +123,26 @@ export default function AdminDashboard() { }); if (!response.ok) { - const data = await response.json(); + const data = await dataResponse.json(); throw new Error(data.error || 'Query failed'); } - const data = await response.json(); + const data = await dataResponse.json(); setQueryResult(data); + + // Fetch table schema + const schemaResponse = await fetch('/api/admin/table-schema', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ tableName }), + }); + + if (schemaResponse.ok) { + const schemaData = await schemaResponse.json(); + setTableSchema(schemaData); + } } catch (err: any) { setError(err.message); } finally { diff --git a/src/app/api/admin/column-manage/route.ts b/src/app/api/admin/column-manage/route.ts new file mode 100644 index 0000000..ea29437 --- /dev/null +++ b/src/app/api/admin/column-manage/route.ts @@ -0,0 +1,214 @@ +import { sql } from 'drizzle-orm'; +import { NextResponse } from 'next/server'; +import { db } from '@/utils/db'; +import { getSession } from '@/utils/session'; + +// Validate identifier format (prevent SQL injection) +function isValidIdentifier(name: string): boolean { + return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name); +} + +// Validate table exists +async function validateTable(tableName: string): Promise { + const result = await db.execute(sql` + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = ${tableName} + `); + return result.rows.length > 0; +} + +// ADD COLUMN +export async function POST(request: Request) { + try { + const session = await getSession(); + + if (!session) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 }, + ); + } + + const { tableName, columnName, dataType, nullable, defaultValue } = await request.json(); + + if (!tableName || !columnName || !dataType) { + return NextResponse.json( + { error: 'Table name, column name, and data type are required' }, + { status: 400 }, + ); + } + + // Validate identifiers + if (!isValidIdentifier(tableName) || !isValidIdentifier(columnName)) { + return NextResponse.json( + { error: 'Invalid table or column name format' }, + { status: 400 }, + ); + } + + // Validate table exists + if (!(await validateTable(tableName))) { + return NextResponse.json( + { error: 'Table not found' }, + { status: 404 }, + ); + } + + let alterQuery = `ALTER TABLE "${tableName}" ADD COLUMN "${columnName}" ${dataType}`; + + if (!nullable) { + alterQuery += ' NOT NULL'; + } + + if (defaultValue !== undefined && defaultValue !== null) { + if (typeof defaultValue === 'string') { + alterQuery += ` DEFAULT '${defaultValue}'`; + } else { + alterQuery += ` DEFAULT ${defaultValue}`; + } + } + + await db.execute(sql.raw(alterQuery)); + + return NextResponse.json({ + success: true, + message: `Column '${columnName}' added successfully`, + }); + } catch (error: any) { + console.error('Add column error:', error); + return NextResponse.json( + { error: error.message || 'Failed to add column' }, + { status: 500 }, + ); + } +} + +// DROP COLUMN +export async function DELETE(request: Request) { + try { + const session = await getSession(); + + if (!session) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 }, + ); + } + + const { tableName, columnName } = await request.json(); + + if (!tableName || !columnName) { + return NextResponse.json( + { error: 'Table name and column name are required' }, + { status: 400 }, + ); + } + + // Validate identifiers + if (!isValidIdentifier(tableName) || !isValidIdentifier(columnName)) { + return NextResponse.json( + { error: 'Invalid table or column name format' }, + { status: 400 }, + ); + } + + // Validate table exists + if (!(await validateTable(tableName))) { + return NextResponse.json( + { error: 'Table not found' }, + { status: 404 }, + ); + } + + const alterQuery = `ALTER TABLE "${tableName}" DROP COLUMN "${columnName}"`; + await db.execute(sql.raw(alterQuery)); + + return NextResponse.json({ + success: true, + message: `Column '${columnName}' dropped successfully`, + }); + } catch (error: any) { + console.error('Drop column error:', error); + return NextResponse.json( + { error: error.message || 'Failed to drop column' }, + { status: 500 }, + ); + } +} + +// MODIFY COLUMN +export async function PUT(request: Request) { + try { + const session = await getSession(); + + if (!session) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 }, + ); + } + + const { tableName, columnName, newType, nullable } = await request.json(); + + if (!tableName || !columnName) { + return NextResponse.json( + { error: 'Table name and column name are required' }, + { status: 400 }, + ); + } + + // Validate identifiers + if (!isValidIdentifier(tableName) || !isValidIdentifier(columnName)) { + return NextResponse.json( + { error: 'Invalid table or column name format' }, + { status: 400 }, + ); + } + + // Validate table exists + if (!(await validateTable(tableName))) { + return NextResponse.json( + { error: 'Table not found' }, + { status: 404 }, + ); + } + + const alterQueries = []; + + if (newType) { + alterQueries.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" TYPE ${newType}`); + } + + if (nullable !== undefined) { + if (nullable) { + alterQueries.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" DROP NOT NULL`); + } else { + alterQueries.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" SET NOT NULL`); + } + } + + if (alterQueries.length === 0) { + return NextResponse.json( + { error: 'No modifications specified' }, + { status: 400 }, + ); + } + + for (const query of alterQueries) { + await db.execute(sql.raw(query)); + } + + return NextResponse.json({ + success: true, + message: `Column '${columnName}' modified successfully`, + }); + } catch (error: any) { + console.error('Modify column error:', error); + return NextResponse.json( + { error: error.message || 'Failed to modify column' }, + { status: 500 }, + ); + } +} diff --git a/src/app/api/admin/record/route.ts b/src/app/api/admin/record/route.ts new file mode 100644 index 0000000..7088574 --- /dev/null +++ b/src/app/api/admin/record/route.ts @@ -0,0 +1,204 @@ +import { sql } from 'drizzle-orm'; +import { NextResponse } from 'next/server'; +import { db } from '@/utils/db'; +import { getSession } from '@/utils/session'; + +// Validate table name exists in schema +async function validateTable(tableName: string): Promise { + const result = await db.execute(sql` + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = ${tableName} + `); + return result.rows.length > 0; +} + +// CREATE - Insert a new record +export async function POST(request: Request) { + try { + const session = await getSession(); + + if (!session) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 }, + ); + } + + const { tableName, data } = await request.json(); + + if (!tableName || !data) { + return NextResponse.json( + { error: 'Table name and data are required' }, + { status: 400 }, + ); + } + + // Validate table exists + if (!(await validateTable(tableName))) { + return NextResponse.json( + { error: 'Table not found' }, + { status: 404 }, + ); + } + + const columns = Object.keys(data); + const values = Object.values(data); + + if (columns.length === 0) { + return NextResponse.json( + { error: 'No data provided' }, + { status: 400 }, + ); + } + + // Build parameterized insert query + const columnList = columns.map(col => `"${col}"`).join(', '); + const placeholders = values.map((_, idx) => `$${idx + 1}`).join(', '); + + const query = `INSERT INTO "${tableName}" (${columnList}) VALUES (${placeholders}) RETURNING *`; + + const result = await db.execute(sql.raw(query, values)); + + return NextResponse.json({ + success: true, + record: result.rows[0], + }); + } catch (error: any) { + console.error('Insert error:', error); + return NextResponse.json( + { error: error.message || 'Failed to insert record' }, + { status: 500 }, + ); + } +} + +// UPDATE - Update an existing record +export async function PUT(request: Request) { + try { + const session = await getSession(); + + if (!session) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 }, + ); + } + + const { tableName, primaryKey, data } = await request.json(); + + if (!tableName || !primaryKey || !data) { + return NextResponse.json( + { error: 'Table name, primary key, and data are required' }, + { status: 400 }, + ); + } + + // Validate table exists + if (!(await validateTable(tableName))) { + return NextResponse.json( + { error: 'Table not found' }, + { status: 404 }, + ); + } + + const columns = Object.keys(data); + const values = Object.values(data); + + if (columns.length === 0) { + return NextResponse.json( + { error: 'No data provided' }, + { status: 400 }, + ); + } + + // Build parameterized update query + const setClause = columns.map((col, idx) => `"${col}" = $${idx + 1}`).join(', '); + const whereClause = Object.keys(primaryKey) + .map((key, idx) => `"${key}" = $${values.length + idx + 1}`) + .join(' AND '); + + const query = `UPDATE "${tableName}" SET ${setClause} WHERE ${whereClause} RETURNING *`; + const allValues = [...values, ...Object.values(primaryKey)]; + + const result = await db.execute(sql.raw(query, allValues)); + + if (result.rowCount === 0) { + return NextResponse.json( + { error: 'Record not found' }, + { status: 404 }, + ); + } + + return NextResponse.json({ + success: true, + record: result.rows[0], + }); + } catch (error: any) { + console.error('Update error:', error); + return NextResponse.json( + { error: error.message || 'Failed to update record' }, + { status: 500 }, + ); + } +} + +// DELETE - Delete a record +export async function DELETE(request: Request) { + try { + const session = await getSession(); + + if (!session) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 }, + ); + } + + const { tableName, primaryKey } = await request.json(); + + if (!tableName || !primaryKey) { + return NextResponse.json( + { error: 'Table name and primary key are required' }, + { status: 400 }, + ); + } + + // Validate table exists + if (!(await validateTable(tableName))) { + return NextResponse.json( + { error: 'Table not found' }, + { status: 404 }, + ); + } + + // Build parameterized delete query + const whereClause = Object.keys(primaryKey) + .map((key, idx) => `"${key}" = $${idx + 1}`) + .join(' AND '); + + const query = `DELETE FROM "${tableName}" WHERE ${whereClause} RETURNING *`; + const values = Object.values(primaryKey); + + const result = await db.execute(sql.raw(query, values)); + + if (result.rowCount === 0) { + return NextResponse.json( + { error: 'Record not found' }, + { status: 404 }, + ); + } + + return NextResponse.json({ + success: true, + deletedRecord: result.rows[0], + }); + } catch (error: any) { + console.error('Delete error:', error); + return NextResponse.json( + { error: error.message || 'Failed to delete record' }, + { status: 500 }, + ); + } +} diff --git a/src/app/api/admin/table-manage/route.ts b/src/app/api/admin/table-manage/route.ts new file mode 100644 index 0000000..eebae52 --- /dev/null +++ b/src/app/api/admin/table-manage/route.ts @@ -0,0 +1,154 @@ +import { sql } from 'drizzle-orm'; +import { NextResponse } from 'next/server'; +import { db } from '@/utils/db'; +import { getSession } from '@/utils/session'; + +// Validate table name format (prevent SQL injection) +function isValidIdentifier(name: string): boolean { + return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name); +} + +// CREATE TABLE +export async function POST(request: Request) { + try { + const session = await getSession(); + + if (!session) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 }, + ); + } + + const { tableName, columns } = await request.json(); + + if (!tableName || !columns || !Array.isArray(columns) || columns.length === 0) { + return NextResponse.json( + { error: 'Table name and columns are required' }, + { status: 400 }, + ); + } + + // Validate table name + if (!isValidIdentifier(tableName)) { + return NextResponse.json( + { error: 'Invalid table name format' }, + { status: 400 }, + ); + } + + // Build column definitions + const columnDefs = columns.map((col: any) => { + if (!col.name || !col.type) { + throw new Error('Each column must have a name and type'); + } + + if (!isValidIdentifier(col.name)) { + throw new Error(`Invalid column name: ${col.name}`); + } + + let def = `"${col.name}" ${col.type}`; + + if (col.length && (col.type === 'VARCHAR' || col.type === 'CHARACTER VARYING')) { + def += `(${col.length})`; + } + + if (col.primaryKey) { + def += ' PRIMARY KEY'; + } + + if (col.unique) { + def += ' UNIQUE'; + } + + if (!col.nullable) { + def += ' NOT NULL'; + } + + if (col.default !== undefined && col.default !== null) { + if (typeof col.default === 'string') { + def += ` DEFAULT '${col.default}'`; + } else { + def += ` DEFAULT ${col.default}`; + } + } + + return def; + }).join(', '); + + const createQuery = `CREATE TABLE "${tableName}" (${columnDefs})`; + + await db.execute(sql.raw(createQuery)); + + return NextResponse.json({ + success: true, + message: `Table '${tableName}' created successfully`, + }); + } catch (error: any) { + console.error('Create table error:', error); + return NextResponse.json( + { error: error.message || 'Failed to create table' }, + { status: 500 }, + ); + } +} + +// DROP TABLE +export async function DELETE(request: Request) { + try { + const session = await getSession(); + + if (!session) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 }, + ); + } + + const { tableName } = await request.json(); + + if (!tableName) { + return NextResponse.json( + { error: 'Table name is required' }, + { status: 400 }, + ); + } + + // Validate table name + if (!isValidIdentifier(tableName)) { + return NextResponse.json( + { error: 'Invalid table name format' }, + { status: 400 }, + ); + } + + // Verify table exists + const tablesResult = await db.execute(sql` + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = ${tableName} + `); + + if (tablesResult.rows.length === 0) { + return NextResponse.json( + { error: 'Table not found' }, + { status: 404 }, + ); + } + + const dropQuery = `DROP TABLE "${tableName}"`; + await db.execute(sql.raw(dropQuery)); + + return NextResponse.json({ + success: true, + message: `Table '${tableName}' dropped successfully`, + }); + } catch (error: any) { + console.error('Drop table error:', error); + return NextResponse.json( + { error: error.message || 'Failed to drop table' }, + { status: 500 }, + ); + } +} diff --git a/src/app/api/admin/table-schema/route.ts b/src/app/api/admin/table-schema/route.ts new file mode 100644 index 0000000..c6df8a5 --- /dev/null +++ b/src/app/api/admin/table-schema/route.ts @@ -0,0 +1,104 @@ +import { sql } from 'drizzle-orm'; +import { NextResponse } from 'next/server'; +import { db } from '@/utils/db'; +import { getSession } from '@/utils/session'; + +export async function POST(request: Request) { + try { + const session = await getSession(); + + if (!session) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 }, + ); + } + + const { tableName } = await request.json(); + + if (!tableName) { + return NextResponse.json( + { error: 'Table name is required' }, + { status: 400 }, + ); + } + + // Validate table exists + const tablesResult = await db.execute(sql` + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = ${tableName} + `); + + if (tablesResult.rows.length === 0) { + return NextResponse.json( + { error: 'Table not found' }, + { status: 404 }, + ); + } + + // Get column information + const columnsResult = await db.execute(sql` + SELECT + column_name, + data_type, + character_maximum_length, + is_nullable, + column_default + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = ${tableName} + ORDER BY ordinal_position + `); + + // Get primary key information + const pkResult = await db.execute(sql` + SELECT kcu.column_name + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema + WHERE tc.constraint_type = 'PRIMARY KEY' + AND tc.table_schema = 'public' + AND tc.table_name = ${tableName} + `); + + // Get foreign key information + const fkResult = await db.execute(sql` + SELECT + kcu.column_name, + ccu.table_name AS foreign_table_name, + ccu.column_name AS foreign_column_name + FROM information_schema.table_constraints AS tc + JOIN information_schema.key_column_usage AS kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema + JOIN information_schema.constraint_column_usage AS ccu + ON ccu.constraint_name = tc.constraint_name + AND ccu.table_schema = tc.table_schema + WHERE tc.constraint_type = 'FOREIGN KEY' + AND tc.table_schema = 'public' + AND tc.table_name = ${tableName} + `); + + const primaryKeys = pkResult.rows.map((row: any) => row.column_name); + const foreignKeys = fkResult.rows.map((row: any) => ({ + column: row.column_name, + foreignTable: row.foreign_table_name, + foreignColumn: row.foreign_column_name, + })); + + return NextResponse.json({ + columns: columnsResult.rows, + primaryKeys, + foreignKeys, + }); + } catch (error: any) { + console.error('Schema query error:', error); + return NextResponse.json( + { error: error.message || 'Failed to fetch schema' }, + { status: 500 }, + ); + } +} diff --git a/src/components/admin/ConfirmDialog.tsx b/src/components/admin/ConfirmDialog.tsx new file mode 100644 index 0000000..aab7bd7 --- /dev/null +++ b/src/components/admin/ConfirmDialog.tsx @@ -0,0 +1,45 @@ +'use client'; + +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, +} from '@mui/material'; + +type ConfirmDialogProps = { + open: boolean; + title: string; + message: string; + onConfirm: () => void; + onCancel: () => void; + confirmLabel?: string; + cancelLabel?: string; +}; + +export default function ConfirmDialog({ + open, + title, + message, + onConfirm, + onCancel, + confirmLabel = 'Confirm', + cancelLabel = 'Cancel', +}: ConfirmDialogProps) { + return ( + + {title} + + {message} + + + + + + + ); +} diff --git a/src/components/admin/DataGrid.tsx b/src/components/admin/DataGrid.tsx new file mode 100644 index 0000000..35d645c --- /dev/null +++ b/src/components/admin/DataGrid.tsx @@ -0,0 +1,77 @@ +'use client'; + +import DeleteIcon from '@mui/icons-material/Delete'; +import EditIcon from '@mui/icons-material/Edit'; +import { + IconButton, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Tooltip, +} from '@mui/material'; + +type DataGridProps = { + columns: Array<{ name: string; label?: string }>; + rows: any[]; + onEdit?: (row: any) => void; + onDelete?: (row: any) => void; + primaryKey?: string; +}; + +export default function DataGrid({ columns, rows, onEdit, onDelete, primaryKey = 'id' }: DataGridProps) { + return ( + + + + + {columns.map(col => ( + + {col.label || col.name} + + ))} + {(onEdit || onDelete) && ( + + Actions + + )} + + + + {rows.map((row, idx) => ( + + {columns.map(col => ( + + {row[col.name] !== null && row[col.name] !== undefined + ? String(row[col.name]) + : 'NULL'} + + ))} + {(onEdit || onDelete) && ( + + {onEdit && ( + + onEdit(row)}> + + + + )} + {onDelete && ( + + onDelete(row)}> + + + + )} + + )} + + ))} + +
+
+ ); +} diff --git a/src/components/admin/FormDialog.tsx b/src/components/admin/FormDialog.tsx new file mode 100644 index 0000000..65bebdc --- /dev/null +++ b/src/components/admin/FormDialog.tsx @@ -0,0 +1,99 @@ +'use client'; + +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + TextField, +} from '@mui/material'; +import { useEffect, useState } from 'react'; + +type FormField = { + name: string; + label: string; + type?: string; + required?: boolean; + defaultValue?: any; +}; + +type FormDialogProps = { + open: boolean; + title: string; + fields: FormField[]; + initialData?: any; + onClose: () => void; + onSubmit: (data: any) => Promise; + submitLabel?: string; +}; + +export default function FormDialog({ + open, + title, + fields, + initialData, + onClose, + onSubmit, + submitLabel = 'Submit', +}: FormDialogProps) { + const [formData, setFormData] = useState({}); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (initialData) { + setFormData(initialData); + } else { + setFormData({}); + } + }, [initialData, open]); + + const handleSubmit = async () => { + setLoading(true); + try { + await onSubmit(formData); + setFormData({}); + onClose(); + } catch (error) { + console.error('Form submission error:', error); + } finally { + setLoading(false); + } + }; + + const handleChange = (fieldName: string, value: any) => { + setFormData((prev: any) => ({ + ...prev, + [fieldName]: value, + })); + }; + + return ( + + {title} + + {fields.map(field => ( + handleChange(field.name, e.target.value)} + disabled={loading} + /> + ))} + + + + + + + ); +} diff --git a/src/config/features.json b/src/config/features.json new file mode 100644 index 0000000..cd7ce69 --- /dev/null +++ b/src/config/features.json @@ -0,0 +1,164 @@ +{ + "features": [ + { + "id": "database-crud", + "name": "Database CRUD Operations", + "description": "Create, read, update, and delete database records", + "enabled": true, + "priority": "high", + "endpoints": [ + { + "path": "/api/admin/record", + "methods": ["POST", "PUT", "DELETE"], + "description": "Manage database records" + }, + { + "path": "/api/admin/table-data", + "methods": ["POST"], + "description": "Fetch table data" + }, + { + "path": "/api/admin/table-schema", + "methods": ["POST"], + "description": "Fetch table schema information" + } + ], + "ui": { + "showInNav": true, + "icon": "Storage", + "actions": ["create", "read", "update", "delete"] + } + }, + { + "id": "table-management", + "name": "Table Management", + "description": "Create and manage database tables", + "enabled": true, + "priority": "high", + "endpoints": [ + { + "path": "/api/admin/table-manage", + "methods": ["POST", "DELETE"], + "description": "Create and drop tables" + } + ], + "ui": { + "showInNav": true, + "icon": "TableChart", + "actions": ["create", "delete"] + } + }, + { + "id": "column-management", + "name": "Column Management", + "description": "Add, modify, and delete table columns", + "enabled": true, + "priority": "high", + "endpoints": [ + { + "path": "/api/admin/column-manage", + "methods": ["POST", "PUT", "DELETE"], + "description": "Manage table columns" + } + ], + "ui": { + "showInNav": true, + "icon": "ViewColumn", + "actions": ["add", "modify", "delete"] + } + }, + { + "id": "sql-query", + "name": "SQL Query Interface", + "description": "Execute custom SQL queries", + "enabled": true, + "priority": "high", + "endpoints": [ + { + "path": "/api/admin/query", + "methods": ["POST"], + "description": "Execute SQL queries" + } + ], + "ui": { + "showInNav": true, + "icon": "Code", + "actions": ["execute"] + } + } + ], + "dataTypes": [ + { + "name": "INTEGER", + "category": "numeric", + "requiresLength": false + }, + { + "name": "BIGINT", + "category": "numeric", + "requiresLength": false + }, + { + "name": "SERIAL", + "category": "numeric", + "requiresLength": false, + "autoIncrement": true + }, + { + "name": "VARCHAR", + "category": "text", + "requiresLength": true, + "defaultLength": 255 + }, + { + "name": "TEXT", + "category": "text", + "requiresLength": false + }, + { + "name": "BOOLEAN", + "category": "boolean", + "requiresLength": false + }, + { + "name": "TIMESTAMP", + "category": "datetime", + "requiresLength": false + }, + { + "name": "DATE", + "category": "datetime", + "requiresLength": false + }, + { + "name": "JSON", + "category": "json", + "requiresLength": false + }, + { + "name": "JSONB", + "category": "json", + "requiresLength": false + } + ], + "navItems": [ + { + "id": "tables", + "label": "Tables", + "icon": "Storage", + "featureId": "database-crud" + }, + { + "id": "query", + "label": "SQL Query", + "icon": "Code", + "featureId": "sql-query" + }, + { + "id": "table-manager", + "label": "Table Manager", + "icon": "TableChart", + "featureId": "table-management" + } + ] +} diff --git a/src/utils/featureConfig.ts b/src/utils/featureConfig.ts new file mode 100644 index 0000000..394c473 --- /dev/null +++ b/src/utils/featureConfig.ts @@ -0,0 +1,59 @@ +import featuresConfig from '@/config/features.json'; + +export type Feature = { + id: string; + name: string; + description: string; + enabled: boolean; + priority: string; + endpoints: Array<{ + path: string; + methods: string[]; + description: string; + }>; + ui: { + showInNav: boolean; + icon: string; + actions: string[]; + }; +}; + +export type DataType = { + name: string; + category: string; + requiresLength: boolean; + defaultLength?: number; + autoIncrement?: boolean; +}; + +export type NavItem = { + id: string; + label: string; + icon: string; + featureId: string; +}; + +export function getFeatures(): Feature[] { + return featuresConfig.features.filter(f => f.enabled); +} + +export function getFeatureById(id: string): Feature | undefined { + return featuresConfig.features.find(f => f.id === id && f.enabled); +} + +export function getDataTypes(): DataType[] { + return featuresConfig.dataTypes; +} + +export function getNavItems(): NavItem[] { + return featuresConfig.navItems.filter(item => { + const feature = getFeatureById(item.featureId); + return feature && feature.enabled; + }); +} + +export function getEnabledFeaturesByPriority(priority: string): Feature[] { + return featuresConfig.features.filter( + f => f.enabled && f.priority === priority, + ); +}