diff --git a/backend/app/Http/Controllers/Api/SchemaController.php b/backend/app/Http/Controllers/Api/SchemaController.php index 3f018e7..cf50323 100644 --- a/backend/app/Http/Controllers/Api/SchemaController.php +++ b/backend/app/Http/Controllers/Api/SchemaController.php @@ -84,4 +84,24 @@ class SchemaController extends Controller return Response::json(['error' => $e->getMessage()], 400); } } + public function execute(Request $request) + { + try { + $this->initializeDriver($request); + $sql = $request->get('query'); + + if (empty($sql)) { + return Response::json(['error' => 'Query is empty'], 400); + } + + $results = $this->databaseService->executeQuery($sql); + + return Response::json([ + 'data' => $results, + 'count' => count($results) + ]); + } catch (\Exception $e) { + return Response::json(['error' => $e->getMessage()], 400); + } + } } diff --git a/backend/app/Services/DatabaseService.php b/backend/app/Services/DatabaseService.php index f5011bf..97d4197 100644 --- a/backend/app/Services/DatabaseService.php +++ b/backend/app/Services/DatabaseService.php @@ -90,4 +90,12 @@ class DatabaseService { return $this->getDriver()->getTableCount($table); } + + /** + * Execute a raw SQL query. + */ + public function executeQuery(string $sql, array $bindings = []): array + { + return $this->getDriver()->query($sql, $bindings); + } } diff --git a/backend/routes/api.php b/backend/routes/api.php index ebf2b23..d6d07ed 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -13,4 +13,5 @@ Route::prefix('schema')->group(function () { Route::get('/tables/{database}', [SchemaController::class, 'tables']); Route::get('/{table}', [SchemaController::class, 'schema']); Route::get('/{table}/data', [SchemaController::class, 'data']); + Route::post('/execute', [SchemaController::class, 'execute']); }); diff --git a/frontend/src/components/MainContent.tsx b/frontend/src/components/MainContent.tsx index f9a09fa..055a102 100644 --- a/frontend/src/components/MainContent.tsx +++ b/frontend/src/components/MainContent.tsx @@ -1,17 +1,20 @@ import React, { useEffect, useState, useCallback } from 'react'; -import { Box, Paper, Typography, CircularProgress } from '@mui/material'; +import { Box, Paper, Typography, CircularProgress, Button, Divider, IconButton, Tooltip } from '@mui/material'; +import { PlayArrow, History, Save, CleaningServices } from '@mui/icons-material'; import { DataGrid } from '@mui/x-data-grid'; import type { GridColDef, GridPaginationModel } from '@mui/x-data-grid'; +import Editor from '@monaco-editor/react'; import { useAppStore } from '../store/useAppStore'; import { SchemaService } from '../services/api'; const MainContent: React.FC = () => { - const { activeTable, activeDatabase } = useAppStore(); + const { activeTable, activeDatabase, darkMode } = useAppStore(); const [columns, setColumns] = useState([]); const [rows, setRows] = useState([]); const [rowCount, setRowCount] = useState(0); const [loadingSchema, setLoadingSchema] = useState(false); const [loadingData, setLoadingData] = useState(false); + const [sqlQuery, setSqlQuery] = useState(''); const [paginationModel, setPaginationModel] = useState({ page: 0, pageSize: 100, @@ -27,6 +30,13 @@ const MainContent: React.FC = () => { return 'string'; }; + // Set initial SQL query when table changes + useEffect(() => { + if (activeTable && activeDatabase) { + setSqlQuery(`SELECT * FROM ${activeDatabase}.${activeTable} LIMIT 1000;`); + } + }, [activeTable, activeDatabase]); + // Fetch Schema useEffect(() => { const fetchSchema = async () => { @@ -52,8 +62,6 @@ const MainContent: React.FC = () => { }; }); setColumns(cols); - - // Reset pagination when table changes setPaginationModel({ page: 0, pageSize: 100 }); } catch (error) { console.error('Failed to fetch table schema', error); @@ -80,8 +88,6 @@ const MainContent: React.FC = () => { const response = await SchemaService.getTableData(activeTable, params); - // MUI X Data Grid needs a unique id for each row - // If the table doesn't have an 'id' column, we might need to generate one const dataWithIds = response.data.data.map((row: any, index: number) => ({ id: row.id || row.ID || `row-${index}-${paginationModel.page}`, ...row, @@ -100,6 +106,43 @@ const MainContent: React.FC = () => { fetchData(); }, [fetchData]); + const handleExecute = async () => { + if (!sqlQuery) return; + setLoadingData(true); + try { + const response = await SchemaService.executeQuery(sqlQuery); + const rawData = response.data.data; + + if (rawData && rawData.length > 0) { + // Generate dynamic columns from the result set + const fields = Object.keys(rawData[0]); + const newCols: GridColDef[] = fields.map(field => ({ + field, + headerName: field, + width: 150, + flex: fields.length < 6 ? 1 : 0 + })); + setColumns(newCols); + + const dataWithIds = rawData.map((row: any, index: number) => ({ + id: row.id || row.ID || `row-${index}`, + ...row, + })); + setRows(dataWithIds); + setRowCount(response.data.count); + } else { + setRows([]); + setRowCount(0); + } + } catch (error: any) { + console.error('Execution error', error); + // In a real app, we'd use a nicer toast or error panel + alert(error.response?.data?.error || 'Execution failed'); + } finally { + setLoadingData(false); + } + }; + if (!activeTable) { return ( @@ -109,58 +152,109 @@ const MainContent: React.FC = () => { } return ( - - - - {activeDatabase}.{activeTable} - - - - - {loadingSchema ? ( - - + + {/* SQL Editor Section */} + + + + SQL EDITOR + + {activeDatabase}.sql - ) : ( - theme.palette.mode === 'light' - ? 'rgba(0, 0, 0, 0.03)' - : 'rgba(255, 255, 255, 0.03)', - }, - '& .MuiDataGrid-row:hover': { - bgcolor: (theme) => theme.palette.mode === 'light' - ? 'rgba(0, 97, 255, 0.08)' - : 'rgba(0, 97, 255, 0.15)', - }, - }} - slotProps={{ - loadingOverlay: { - variant: 'linear-progress', - noRowsVariant: 'linear-progress', - } - }} - /> - )} + + + setSqlQuery('')}> + + + + + + + + + + + setSqlQuery(value || '')} + options={{ + minimap: { enabled: false }, + fontSize: 13, + fontFamily: "'Fira Code', 'Cascadia Code', Consolas, monospace", + lineNumbers: 'on', + scrollBeyondLastLine: false, + automaticLayout: true, + padding: { top: 10, bottom: 10 } + }} + /> + + {/* Data Section */} + + + + Results + {rowCount} rows found + + + + + {loadingSchema ? ( + + + + ) : ( + theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.02)' : 'rgba(255, 255, 255, 0.02)', + }, + '& .MuiDataGrid-row:hover': { + bgcolor: (theme) => theme.palette.mode === 'light' ? 'rgba(0, 97, 255, 0.04)' : 'rgba(0, 97, 255, 0.08)', + }, + }} + slotProps={{ + loadingOverlay: { + variant: 'linear-progress', + noRowsVariant: 'linear-progress', + } + }} + /> + )} + + ); }; diff --git a/frontend/src/components/NavigationRail.tsx b/frontend/src/components/NavigationRail.tsx index a012ad0..1c8f8b6 100644 --- a/frontend/src/components/NavigationRail.tsx +++ b/frontend/src/components/NavigationRail.tsx @@ -23,37 +23,79 @@ const NavigationRail: React.FC = ({ activeTab, onTabChange return ( theme.palette.mode === 'dark' ? '#0a0f1d' : '#ffffff', borderRight: 1, - borderColor: 'divider' + borderColor: 'divider', + zIndex: 1200, + boxShadow: (theme) => theme.palette.mode === 'dark' ? 'none' : '4px 0 10px rgba(0,0,0,0.02)' }}> - - {tabs.map((tab) => ( - - onTabChange(tab.id)} - sx={{ - color: activeTab === tab.id ? 'primary.main' : 'text.secondary', - bgcolor: activeTab === tab.id ? 'rgba(0, 97, 255, 0.1)' : 'transparent', - borderRadius: 2, - '&:hover': { bgcolor: 'rgba(0, 97, 255, 0.05)' } - }} - > - {tab.icon} - - - ))} + + {tabs.map((tab) => { + const isActive = activeTab === tab.id; + return ( + + {/* Active Indicator - Elegant Pill Style */} + {isActive && ( + + )} + + + onTabChange(tab.id)} + sx={{ + width: 48, + height: 48, + color: isActive ? 'primary.main' : 'text.secondary', + bgcolor: isActive ? 'rgba(0, 97, 255, 0.05)' : 'transparent', + borderRadius: '8px', + transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', + '&:hover': { + bgcolor: isActive ? 'rgba(0, 97, 255, 0.1)' : 'rgba(0, 0, 0, 0.03)', + color: isActive ? 'primary.main' : 'primary.main', + '& .MuiSvgIcon-root': { + transform: 'scale(1.1)', + } + }, + '& .MuiSvgIcon-root': { + fontSize: 26, + transition: 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)', + } + }} + > + {tab.icon} + + + + ); + })} - - + + diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 53449c1..86d3b28 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -27,4 +27,5 @@ export const SchemaService = { getTables: (db: string) => api.get(`/schema/tables/${db}`, { params: { database: db } }), getTableSchema: (table: string, database?: string) => api.get(`/schema/${table}`, { params: { database } }), getTableData: (table: string, params: any) => api.get(`/schema/${table}/data`, { params }), + executeQuery: (query: string) => api.post('/schema/execute', { query }), };