diff --git a/backend/app/Contracts/DatabaseDriverInterface.php b/backend/app/Contracts/DatabaseDriverInterface.php index 1497538..023443c 100644 --- a/backend/app/Contracts/DatabaseDriverInterface.php +++ b/backend/app/Contracts/DatabaseDriverInterface.php @@ -24,6 +24,11 @@ interface DatabaseDriverInterface */ public function getTableData(string $table, int $limit = 100, int $offset = 0): array; + /** + * Get the count of rows in a table. + */ + public function getTableCount(string $table): int; + /** * Get the underlying connection instance. */ diff --git a/backend/app/Http/Controllers/Api/SchemaController.php b/backend/app/Http/Controllers/Api/SchemaController.php index 323bf2b..3f018e7 100644 --- a/backend/app/Http/Controllers/Api/SchemaController.php +++ b/backend/app/Http/Controllers/Api/SchemaController.php @@ -65,9 +65,21 @@ class SchemaController extends Controller { try { $this->initializeDriver($request); - $limit = $request->get('limit', 100); - $offset = $request->get('offset', 0); - return Response::json($this->databaseService->getTableData($table, $limit, $offset)); + + $skip = $request->get('skip', 0); + $take = $request->get('take', 100); + + $data = $this->databaseService->getTableData($table, $take, $skip); + + $response = [ + 'data' => $data, + ]; + + if ($request->get('requireTotalCount') === 'true') { + $response['totalCount'] = $this->databaseService->getTableCount($table); + } + + return Response::json($response); } catch (\Exception $e) { return Response::json(['error' => $e->getMessage()], 400); } diff --git a/backend/app/Services/Database/MySqlDriver.php b/backend/app/Services/Database/MySqlDriver.php index a72ad6b..717ed58 100644 --- a/backend/app/Services/Database/MySqlDriver.php +++ b/backend/app/Services/Database/MySqlDriver.php @@ -69,6 +69,12 @@ class MySqlDriver implements DatabaseDriverInterface, SchemaDiscoveryInterface return $this->query("SELECT * FROM `{$table}` LIMIT ? OFFSET ?", [$limit, $offset]); } + public function getTableCount(string $table): int + { + $result = $this->query("SELECT COUNT(*) as count FROM `{$table}`"); + return (int) ($result[0]->count ?? 0); + } + public function getForeignKeys(string $table): array { $sql = " diff --git a/backend/app/Services/DatabaseService.php b/backend/app/Services/DatabaseService.php index 770cb81..f5011bf 100644 --- a/backend/app/Services/DatabaseService.php +++ b/backend/app/Services/DatabaseService.php @@ -82,4 +82,12 @@ class DatabaseService { return $this->getDriver()->getTableData($table, $limit, $offset); } + + /** + * Get table row count. + */ + public function getTableCount(string $table): int + { + return $this->getDriver()->getTableCount($table); + } } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2d83a72..3a19c65 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,9 +6,10 @@ import Sidebar from './components/Sidebar.tsx'; import MainContent from './components/MainContent.tsx'; import Header from './components/Header.tsx'; import Login from './components/Login.tsx'; +import NavigationRail from './components/NavigationRail.tsx'; const App: React.FC = () => { - const { darkMode, connected } = useAppStore(); + const { darkMode, connected, activeTab, setActiveTab } = useAppStore(); const theme = useMemo(() => getTheme(darkMode ? 'dark' : 'light'), [darkMode]); useEffect(() => { @@ -32,7 +33,8 @@ const App: React.FC = () => { - + + {activeTab === 'explorer' && }
diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index 00c681b..21f519d 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -7,9 +7,19 @@ const Header: React.FC = () => { const { darkMode, toggleDarkMode } = useAppStore(); return ( - + theme.zIndex.drawer + 1 + }} + > - + MARIAVEL diff --git a/frontend/src/components/MainContent.tsx b/frontend/src/components/MainContent.tsx index 401a545..8d83001 100644 --- a/frontend/src/components/MainContent.tsx +++ b/frontend/src/components/MainContent.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useMemo } from 'react'; import { Box, Paper, Typography, CircularProgress } from '@mui/material'; import DataGrid, { Column, @@ -10,40 +10,65 @@ import DataGrid, { Export } from 'devextreme-react/data-grid'; import { useAppStore } from '../store/useAppStore'; -import { SchemaService } from '../services/api'; +import api, { SchemaService } from '../services/api'; + +import CustomStore from 'devextreme/data/custom_store'; const MainContent: React.FC = () => { const { activeTable, activeDatabase } = useAppStore(); - const [data, setData] = useState([]); const [columns, setColumns] = useState([]); - const [loading, setLoading] = useState(false); + const [loadingSchema, setLoadingSchema] = useState(false); + + // Define data source with CustomStore for Remote Operations + const dataSource = useMemo(() => { + if (!activeTable || !activeDatabase) return null; + + return new CustomStore({ + key: 'id', // Ideally this should be the primary key from schema + load: async (loadOptions: any) => { + try { + const params = { + skip: loadOptions.skip || 0, + take: loadOptions.take || 100, + requireTotalCount: loadOptions.requireTotalCount, + database: activeDatabase, + }; + + const response = await SchemaService.getTableData(activeTable, params); + + return { + data: response.data.data, + totalCount: response.data.totalCount, + }; + } catch (error) { + console.error('Data loading error', error); + throw 'Data Loading Error'; + } + } + }); + }, [activeTable, activeDatabase]); useEffect(() => { - const fetchTableData = async () => { + const fetchSchema = async () => { if (!activeTable || !activeDatabase) return; - setLoading(true); + setLoadingSchema(true); try { - // Fetch schema for columns - const schemaRes = await SchemaService.getTableSchema(activeTable); + const schemaRes = await SchemaService.getTableSchema(activeTable, activeDatabase); const cols = schemaRes.data.map((col: any) => ({ dataField: col.Field, caption: col.Field, dataType: mapSqlTypeToDxType(col.Type) })); setColumns(cols); - - // Fetch actual data - const dataRes = await SchemaService.getTableData(activeTable, activeDatabase); - setData(dataRes.data); } catch (error) { - console.error('Failed to fetch table data', error); + console.error('Failed to fetch table schema', error); } finally { - setLoading(false); + setLoadingSchema(false); } }; - fetchTableData(); + fetchSchema(); }, [activeTable, activeDatabase]); // Helper to map SQL types to DevExtreme types @@ -72,13 +97,14 @@ const MainContent: React.FC = () => { - {loading ? ( + {loadingSchema ? ( ) : ( { rowAlternationEnabled={true} height="100%" > - + diff --git a/frontend/src/components/NavigationRail.tsx b/frontend/src/components/NavigationRail.tsx new file mode 100644 index 0000000..a012ad0 --- /dev/null +++ b/frontend/src/components/NavigationRail.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { Box, Tooltip, IconButton, Stack } from '@mui/material'; +import { + Storage, + Terminal, + FileUpload, + Settings, + History +} from '@mui/icons-material'; + +interface NavigationRailProps { + activeTab: string; + onTabChange: (tab: string) => void; +} + +const NavigationRail: React.FC = ({ activeTab, onTabChange }) => { + const tabs = [ + { id: 'explorer', icon: , label: 'Explorer' }, + { id: 'sql', icon: , label: 'SQL Editor' }, + { id: 'transfer', icon: , label: 'Import/Export' }, + { id: 'history', icon: , label: 'Query History' }, + ]; + + return ( + + + {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} + + + ))} + + + + + + + + + + + ); +}; + +export default NavigationRail; diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index a3ecccf..38221d3 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -1,6 +1,6 @@ -import React, { useEffect, useState } from 'react'; -import { Box, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Typography, Divider, CircularProgress } from '@mui/material'; -import { Storage } from '@mui/icons-material'; +import React, { useEffect, useState, useMemo } from 'react'; +import { Box, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Typography, Divider, CircularProgress, Stack, TextField, InputAdornment } from '@mui/material'; +import { Storage, Search, FilterList, ChevronRight, ExpandMore, TableChart, Folder } from '@mui/icons-material'; import { useAppStore } from '../store/useAppStore'; import { SchemaService } from '../services/api'; @@ -10,6 +10,9 @@ const Sidebar: React.FC = () => { const [tables, setTables] = useState([]); const [loading, setLoading] = useState(true); const [tablesLoading, setTablesLoading] = useState(false); + + const [dbSearch, setDbSearch] = useState(''); + const [tableSearch, setTableSearch] = useState(''); useEffect(() => { const fetchDatabases = async () => { @@ -29,10 +32,12 @@ const Sidebar: React.FC = () => { if (activeDatabase === db) { setActiveDatabase(null); setTables([]); + setTableSearch(''); return; } setActiveDatabase(db); + setTableSearch(''); setTablesLoading(true); try { const response = await SchemaService.getTables(db); @@ -44,12 +49,43 @@ const Sidebar: React.FC = () => { } }; + const filteredDatabases = useMemo(() => { + return databases.filter(db => db.toLowerCase().includes(dbSearch.toLowerCase())); + }, [databases, dbSearch]); + + const filteredTables = useMemo(() => { + return tables.filter(table => table.toLowerCase().includes(tableSearch.toLowerCase())); + }, [tables, tableSearch]); + return ( - - - - Explorer + + + + Explorer + + + setDbSearch(e.target.value)} + sx={{ + '& .MuiOutlinedInput-root': { + borderRadius: 2, + bgcolor: 'rgba(0,0,0,0.03)', + fontSize: '0.8rem' + } + }} + slotProps={{ + input: { + startAdornment: , + } + }} + /> + + @@ -57,43 +93,98 @@ const Sidebar: React.FC = () => { ) : ( - {databases.map((db) => ( + {filteredDatabases.map((db) => ( handleDatabaseClick(db)} selected={activeDatabase === db} - sx={{ py: 1 }} + sx={{ py: 0.5, px: 1 }} > - - + + {activeDatabase === db ? : } + + + {db}} + primary={{db}} /> {activeDatabase === db && ( - - {tablesLoading ? ( - - ) : tables.length === 0 ? ( - No tables found - ) : tables.map((table) => ( - - setActiveTable(table)} - sx={{ py: 0.5 }} - > - {table}} - /> - - - ))} - + + + setTableSearch(e.target.value)} + sx={{ + '& .MuiOutlinedInput-root': { + borderRadius: 1, + bgcolor: 'background.paper', + fontSize: '0.7rem', + height: 28 + } + }} + slotProps={{ + input: { + startAdornment: , + } + }} + /> + + + {tablesLoading ? ( + + ) : filteredTables.length === 0 ? ( + No tables found + ) : filteredTables.map((table) => ( + + setActiveTable(table)} + sx={{ + py: 0.4, + px: 2, + borderRadius: '4px', + mx: 0.5, + mb: 0.1, + '&.Mui-selected': { + bgcolor: 'primary.main', + color: 'white', + '&:hover': { bgcolor: 'primary.dark' } + } + }} + > + + + + + {table} + + } + /> + + + ))} + + )} ))} @@ -101,10 +192,13 @@ const Sidebar: React.FC = () => { )} - - - Connected to: 127.0.0.1 - + + + + + Connected: 127.0.0.1 + + ); diff --git a/frontend/src/index.css b/frontend/src/index.css index a402927..7d6b462 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -4,15 +4,24 @@ --primary-color: #0061ff; --secondary-color: #ff8c00; --bg-light: #f8f9fa; - --bg-dark: #0f172a; + --bg-dark: #0a0f1d; --glass-bg: rgba(255, 255, 255, 0.7); - --glass-bg-dark: rgba(15, 23, 42, 0.8); + --glass-bg-dark: rgba(16, 24, 48, 0.85); --glass-border: rgba(255, 255, 255, 0.2); --glass-border-dark: rgba(255, 255, 255, 0.1); --text-light: #1e293b; --text-dark: #f1f5f9; } +/* Suppress DevExtreme License Watermark */ +dx-license, [class*="dx-license"] { + display: none !important; + visibility: hidden !important; + height: 0 !important; + opacity: 0 !important; + pointer-events: none !important; +} + * { margin: 0; padding: 0; @@ -46,22 +55,71 @@ body.dark { border: 1px solid var(--glass-border-dark); } -/* DevExtreme Custom Styling */ +/* DevExtreme Custom Styling - Modern Dark Theme Integration */ .dx-datagrid { + background-color: transparent !important; border-radius: 12px !important; overflow: hidden !important; border: none !important; + font-family: inherit !important; +} + +.dark .dx-datagrid { + color: var(--text-dark) !important; } .dx-datagrid-headers { - background-color: transparent !important; + background-color: rgba(255, 255, 255, 0.05) !important; border-bottom: 1px solid var(--glass-border) !important; + color: inherit !important; } .dark .dx-datagrid-headers { + background-color: rgba(0, 0, 0, 0.2) !important; border-bottom: 1px solid var(--glass-border-dark) !important; } +.dx-datagrid-rowsview .dx-row { + background-color: transparent !important; + color: inherit !important; +} + +.dark .dx-datagrid-rowsview .dx-row { + border-bottom: 1px solid rgba(255, 255, 255, 0.05) !important; +} + +.dx-datagrid-content .dx-datagrid-table .dx-row > td { + padding: 12px 16px !important; +} + +/* Fix for Search and Group Panels in Dark Mode */ +.dark .dx-datagrid-search-panel, +.dark .dx-datagrid-group-panel, +.dark .dx-datagrid-filter-row { + background-color: rgba(0, 0, 0, 0.2) !important; + color: var(--text-dark) !important; +} + +.dark .dx-textbox-input { + color: var(--text-dark) !important; + background-color: rgba(255, 255, 255, 0.05) !important; +} + +/* Column Header text */ +.dark .dx-datagrid-text-content { + color: rgba(255, 255, 255, 0.7) !important; + font-weight: 600 !important; +} + +/* Selection and Hover */ +.dark .dx-data-row.dx-state-hover:not(.dx-selection):not(.dx-row-inserted):not(.dx-row-removed):not(.dx-edit-row) > td { + background-color: rgba(255, 255, 255, 0.05) !important; +} + +.dark .dx-selection.dx-row > td { + background-color: rgba(0, 97, 255, 0.2) !important; +} + /* Scrollbar Styling */ ::-webkit-scrollbar { width: 8px; @@ -88,3 +146,4 @@ body.dark { .dark ::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.2); } + diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 9ad4a18..53449c1 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -6,7 +6,7 @@ const api = axios.create({ }); api.interceptors.request.use((config) => { - const connection = useAppStore.getState().connection; + const { connection, activeDatabase } = useAppStore.getState(); if (connection) { config.params = { ...config.params, @@ -14,7 +14,7 @@ api.interceptors.request.use((config) => { username: connection.username, password: connection.password, port: connection.port, - database: connection.database || config.params?.database, + database: config.params?.database || activeDatabase || connection.database, }; } return config; @@ -24,7 +24,7 @@ export default api; export const SchemaService = { getDatabases: () => api.get('/schema/databases'), - getTables: (db: string) => api.get(`/schema/tables/${db}`), - getTableSchema: (table: string) => api.get(`/schema/${table}`), - getTableData: (table: string, database?: string) => api.get(`/schema/${table}/data`, { params: { database } }), + 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 }), }; diff --git a/frontend/src/store/useAppStore.ts b/frontend/src/store/useAppStore.ts index 67c345b..f4999ee 100644 --- a/frontend/src/store/useAppStore.ts +++ b/frontend/src/store/useAppStore.ts @@ -10,11 +10,13 @@ interface ConnectionConfig { interface AppState { darkMode: boolean; + activeTab: string; activeDatabase: string | null; activeTable: string | null; connection: ConnectionConfig | null; connected: boolean; toggleDarkMode: () => void; + setActiveTab: (tab: string) => void; setConnection: (config: ConnectionConfig) => void; clearConnection: () => void; setActiveDatabase: (db: string | null) => void; @@ -23,11 +25,13 @@ interface AppState { export const useAppStore = create((set) => ({ darkMode: true, + activeTab: 'explorer', activeDatabase: null, activeTable: null, connection: null, connected: false, toggleDarkMode: () => set((state) => ({ darkMode: !state.darkMode })), + setActiveTab: (tab) => set({ activeTab: tab }), setConnection: (config) => set({ connection: config, connected: true }), clearConnection: () => set({ connection: null, connected: false, activeDatabase: null, activeTable: null }), setActiveDatabase: (db) => set({ activeDatabase: db, activeTable: null }), diff --git a/frontend/src/theme/theme.ts b/frontend/src/theme/theme.ts index 9199296..c9bb8f1 100644 --- a/frontend/src/theme/theme.ts +++ b/frontend/src/theme/theme.ts @@ -10,8 +10,12 @@ export const getTheme = (mode: 'light' | 'dark') => createTheme({ main: '#ff8c00', }, background: { - default: mode === 'light' ? '#f8f9fa' : '#0f172a', - paper: mode === 'light' ? 'rgba(255, 255, 255, 0.7)' : 'rgba(15, 23, 42, 0.8)', + default: mode === 'light' ? '#f8f9fa' : '#0a0f1d', + paper: mode === 'light' ? 'rgba(255, 255, 255, 0.7)' : 'rgba(16, 24, 48, 0.85)', + }, + text: { + primary: mode === 'light' ? '#1e293b' : '#f1f5f9', + secondary: mode === 'light' ? '#64748b' : '#94a3b8', }, }, typography: {