diff --git a/backend/app/Contracts/DatabaseDriverInterface.php b/backend/app/Contracts/DatabaseDriverInterface.php
index 023443c..3b3c4f5 100644
--- a/backend/app/Contracts/DatabaseDriverInterface.php
+++ b/backend/app/Contracts/DatabaseDriverInterface.php
@@ -33,4 +33,19 @@ interface DatabaseDriverInterface
* Get the underlying connection instance.
*/
public function getConnection();
+
+ /**
+ * Export the database.
+ */
+ public function export(array $config): string;
+
+ /**
+ * Import the database.
+ */
+ public function import(array $config, string $filePath): bool;
+
+ /**
+ * Get database metadata (charset, collation, size, etc.)
+ */
+ public function getDatabaseMetadata(string $database): array;
}
diff --git a/backend/app/Http/Controllers/Api/SchemaController.php b/backend/app/Http/Controllers/Api/SchemaController.php
index cf50323..a8f6e7b 100644
--- a/backend/app/Http/Controllers/Api/SchemaController.php
+++ b/backend/app/Http/Controllers/Api/SchemaController.php
@@ -104,4 +104,54 @@ class SchemaController extends Controller
return Response::json(['error' => $e->getMessage()], 400);
}
}
+
+ public function export(Request $request)
+ {
+ try {
+ $this->initializeDriver($request);
+ $config = $request->only(['host', 'username', 'password', 'database', 'port']);
+ $filePath = $this->databaseService->export($config);
+
+ return Response::download($filePath)->deleteFileAfterSend(true);
+ } catch (\Exception $e) {
+ return Response::json(['error' => $e->getMessage()], 400);
+ }
+ }
+
+ public function import(Request $request)
+ {
+ try {
+ if (!$request->hasFile('file')) {
+ return Response::json(['error' => 'No file uploaded'], 400);
+ }
+
+ $this->initializeDriver($request);
+ $config = $request->only(['host', 'username', 'password', 'database', 'port']);
+
+ $file = $request->file('file');
+ $tempPath = $file->storeAs('temp', $file->getClientOriginalName());
+ $fullPath = storage_path('app/' . $tempPath);
+
+ $this->databaseService->import($config, $fullPath);
+
+ if (file_exists($fullPath)) {
+ unlink($fullPath);
+ }
+
+ return Response::json(['message' => 'Database imported successfully']);
+ } catch (\Exception $e) {
+ return Response::json(['error' => $e->getMessage()], 400);
+ }
+ }
+
+ public function metadata(Request $request, $database)
+ {
+ try {
+ $request->merge(['database' => $database]);
+ $this->initializeDriver($request);
+ return Response::json($this->databaseService->getDatabaseMetadata($database));
+ } 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 6000db0..d35ec20 100644
--- a/backend/app/Services/Database/MySqlDriver.php
+++ b/backend/app/Services/Database/MySqlDriver.php
@@ -93,4 +93,120 @@ class MySqlDriver implements DatabaseDriverInterface, SchemaDiscoveryInterface
$dbName = DB::connection($this->connectionName)->getDatabaseName();
return $this->query($sql, [$table, $dbName]);
}
+
+ public function export(array $config): string
+ {
+ $filename = 'backup-' . ($config['database'] ?? 'all') . '-' . date('Y-m-d-H-i-s') . '.sql';
+ $directory = storage_path('app/backups');
+
+ if (!is_dir($directory)) {
+ mkdir($directory, 0755, true);
+ }
+
+ $path = $directory . DIRECTORY_SEPARATOR . $filename;
+ $errorPath = $directory . DIRECTORY_SEPARATOR . $filename . '.err';
+
+ // Ensure we have a username
+ $username = $config['username'] ?? 'root';
+ $password = $config['password'] ?? '';
+ $host = $config['host'] ?? '127.0.0.1';
+ $port = $config['port'] ?? '3306';
+ $database = $config['database'] ?? '';
+
+ // Build command
+ $passwordPart = !empty($password) ? "-p" . escapeshellarg($password) : "";
+ $dbPart = !empty($database) ? escapeshellarg($database) : "--all-databases";
+
+ // On Windows, we might need to handle double quotes in escapeshellarg
+ // Let's use a more robust way to execute and capture errors
+ $command = sprintf(
+ 'mysqldump -u %s %s -h %s -P %s %s > %s 2> %s',
+ escapeshellarg($username),
+ $passwordPart,
+ escapeshellarg($host),
+ escapeshellarg($port),
+ $dbPart,
+ escapeshellarg($path),
+ escapeshellarg($errorPath)
+ );
+
+ shell_exec($command);
+
+ if (file_exists($errorPath)) {
+ $errors = file_get_contents($errorPath);
+ unlink($errorPath);
+ if (!empty(trim($errors))) {
+ // If the file is 0 bytes and there are errors, it definitely failed
+ if (!file_exists($path) || filesize($path) === 0) {
+ throw new \Exception("Export failed: " . $errors);
+ }
+ }
+ }
+
+ if (!file_exists($path) || filesize($path) === 0) {
+ throw new \Exception("Export failed: Resulting file is empty. Ensure mysqldump is installed and in the system PATH.");
+ }
+
+ return $path;
+ }
+
+ public function import(array $config, string $filePath): bool
+ {
+ $directory = storage_path('app/backups');
+ if (!is_dir($directory)) {
+ mkdir($directory, 0755, true);
+ }
+ $errorPath = $directory . DIRECTORY_SEPARATOR . 'import_error.err';
+
+ $username = $config['username'] ?? 'root';
+ $password = $config['password'] ?? '';
+ $host = $config['host'] ?? '127.0.0.1';
+ $port = $config['port'] ?? '3306';
+ $database = $config['database'] ?? '';
+
+ $passwordPart = !empty($password) ? "-p" . escapeshellarg($password) : "";
+ $dbPart = !empty($database) ? escapeshellarg($database) : "";
+
+ $command = sprintf(
+ 'mysql -u %s %s -h %s -P %s %s < %s 2> %s',
+ escapeshellarg($username),
+ $passwordPart,
+ escapeshellarg($host),
+ escapeshellarg($port),
+ $dbPart,
+ escapeshellarg($filePath),
+ escapeshellarg($errorPath)
+ );
+
+ shell_exec($command);
+
+ if (file_exists($errorPath)) {
+ $errors = file_get_contents($errorPath);
+ unlink($errorPath);
+ if (!empty(trim($errors))) {
+ throw new \Exception("Import failed: " . $errors);
+ }
+ }
+
+ return true;
+ }
+
+ public function getDatabaseMetadata(string $database): array
+ {
+ $sql = "
+ SELECT
+ s.DEFAULT_CHARACTER_SET_NAME as charset,
+ s.DEFAULT_COLLATION_NAME as collation,
+ (SELECT SUM(data_length + index_length) FROM information_schema.TABLES WHERE table_schema = s.SCHEMA_NAME) as size_bytes,
+ (SELECT COUNT(*) FROM information_schema.TABLES WHERE table_schema = s.SCHEMA_NAME) as table_count,
+ (SELECT COUNT(*) FROM information_schema.VIEWS WHERE table_schema = s.SCHEMA_NAME) as view_count
+ FROM
+ information_schema.SCHEMATA s
+ WHERE
+ s.SCHEMA_NAME = ?
+ ";
+
+ $results = $this->query($sql, [$database]);
+ return (array) ($results[0] ?? []);
+ }
}
diff --git a/backend/app/Services/DatabaseService.php b/backend/app/Services/DatabaseService.php
index 97d4197..7c2c66a 100644
--- a/backend/app/Services/DatabaseService.php
+++ b/backend/app/Services/DatabaseService.php
@@ -98,4 +98,28 @@ class DatabaseService
{
return $this->getDriver()->query($sql, $bindings);
}
+
+ /**
+ * Export the database.
+ */
+ public function export(array $config): string
+ {
+ return $this->getDriver()->export($config);
+ }
+
+ /**
+ * Import the database.
+ */
+ public function import(array $config, string $filePath): bool
+ {
+ return $this->getDriver()->import($config, $filePath);
+ }
+
+ /**
+ * Get database metadata.
+ */
+ public function getDatabaseMetadata(string $database): array
+ {
+ return $this->getDriver()->getDatabaseMetadata($database);
+ }
}
diff --git a/backend/routes/api.php b/backend/routes/api.php
index d6d07ed..fc47972 100644
--- a/backend/routes/api.php
+++ b/backend/routes/api.php
@@ -11,7 +11,10 @@ Route::get('/user', function (Request $request) {
Route::prefix('schema')->group(function () {
Route::get('/databases', [SchemaController::class, 'databases']);
Route::get('/tables/{database}', [SchemaController::class, 'tables']);
+ Route::get('/metadata/{database}', [SchemaController::class, 'metadata']);
Route::get('/{table}', [SchemaController::class, 'schema']);
Route::get('/{table}/data', [SchemaController::class, 'data']);
Route::post('/execute', [SchemaController::class, 'execute']);
+ Route::post('/export', [SchemaController::class, 'export']);
+ Route::post('/import', [SchemaController::class, 'import']);
});
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index a2eabf1..77ec8d9 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -9,6 +9,7 @@ import MainContent from './components/MainContent.tsx';
import Header from './components/Header.tsx';
import Login from './components/Login.tsx';
import NavigationRail from './components/NavigationRail.tsx';
+import TransferContent from './components/TransferContent.tsx';
// Create emotion cache to handle the :first-child warning and ensure proper style injection
const cache = createCache({
@@ -48,7 +49,7 @@ const App: React.FC = () => {
{activeTab === 'explorer' && }
-
+ {activeTab === 'transfer' ? : }
diff --git a/frontend/src/components/MainContent.tsx b/frontend/src/components/MainContent.tsx
index 6969f2e..57e1c5c 100644
--- a/frontend/src/components/MainContent.tsx
+++ b/frontend/src/components/MainContent.tsx
@@ -10,17 +10,31 @@ import {
Tooltip,
Snackbar,
Alert,
- AlertTitle
+ AlertTitle,
+ Tabs,
+ Tab
} from '@mui/material';
-import { PlayArrow, History, Save, CleaningServices, Close } from '@mui/icons-material';
+import {
+ PlayArrow,
+ History,
+ Save,
+ CleaningServices,
+ Close,
+ TableChart,
+ Terminal,
+ CloudDownload,
+ CloudUpload,
+ Info
+} 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';
+import TransferContent from './TransferContent';
const MainContent: React.FC = () => {
- const { activeTable, activeDatabase, darkMode } = useAppStore();
+ const { activeTable, activeDatabase, darkMode, dbTab, setDbTab } = useAppStore();
const [columns, setColumns] = useState([]);
const [rows, setRows] = useState([]);
const [rowCount, setRowCount] = useState(0);
@@ -170,180 +184,228 @@ const MainContent: React.FC = () => {
}
};
- if (!activeTable) {
+ if (!activeDatabase) {
return (
- Select a table to view data
+ Select a database to start
);
}
+ const handleTabChange = (_: React.SyntheticEvent, newValue: string) => {
+ setDbTab(newValue);
+ };
+
+ const DatabaseOverview = ({ database }: { database: string }) => {
+ const [meta, setMeta] = useState(null);
+ const [loadingMeta, setLoadingMeta] = useState(true);
+
+ useEffect(() => {
+ const fetchMeta = async () => {
+ setLoadingMeta(true);
+ try {
+ const res = await SchemaService.getDatabaseMetadata(database);
+ setMeta(res.data);
+ } catch (e) {
+ console.error(e);
+ } finally {
+ setLoadingMeta(false);
+ }
+ };
+ fetchMeta();
+ }, [database]);
+
+ if (loadingMeta) return ;
+ if (!meta) return Could not load metadata;
+
+ const stats = [
+ { label: 'Character Set', value: meta.charset, icon: },
+ { label: 'Collation', value: meta.collation, icon: },
+ { label: 'Tables', value: meta.table_count, icon: },
+ { label: 'Views', value: meta.view_count, icon: },
+ { label: 'Total Size', value: `${(meta.size_bytes / 1024 / 1024).toFixed(2)} MB`, icon: },
+ ];
+
+ return (
+
+ Technical Specifications: {database}
+
+ {stats.map((stat, i) => (
+ theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.02)' : 'rgba(0,0,0,0.01)',
+ transition: 'all 0.2s',
+ '&:hover': { borderColor: 'primary.main', transform: 'translateY(-2px)' }
+ }}>
+
+ {stat.label}
+ {stat.icon}
+
+ {stat.value}
+
+ ))}
+
+
+ );
+ };
+
return (
- {/* SQL Editor Section */}
+ {/* Database Navigation Tabs */}
-
-
- SQL EDITOR
-
-
- {isCustomQuery ? 'Custom Query' : `${activeDatabase}.${activeTable}`}
-
-
-
-
- setSqlQuery('')}>
-
-
-
-
-
-
-
- : }
- sx={{ ml: 1, px: 3, borderRadius: 2, textTransform: 'none', fontWeight: 700, boxShadow: '0 4px 12px rgba(0, 97, 255, 0.3)' }}
- >
- Execute
-
-
-
- setSqlQuery(value || '')}
- options={{
- minimap: { enabled: false },
- fontSize: 14,
- fontFamily: "'Fira Code', 'Cascadia Code', Consolas, monospace",
- lineNumbers: 'on',
- scrollBeyondLastLine: false,
- automaticLayout: true,
- padding: { top: 16, bottom: 16 },
- cursorSmoothCaretAnimation: 'on',
- smoothScrolling: true
+
+ >
+ } iconPosition="start" label="Data Explorer" />
+ } iconPosition="start" label="SQL Editor" />
+ } iconPosition="start" label="Import" />
+ } iconPosition="start" label="Export" />
+ } iconPosition="start" label="Technical Info" />
+
- {/* Data Section */}
-
-
-
-
- {isCustomQuery ? 'Query Results' : 'Table Results'}
-
-
- {rowCount} rows found
-
-
- {isCustomQuery && (
-
- )}
-
-
-
- {(loadingSchema && !isCustomQuery) ? (
-
-
+ {/* Tables View */}
+ {dbTab === 'tables' && (
+
+ {!activeTable ? (
+
+
+ Select a table from the explorer to view data
) : (
+ <>
+
+
+ Table Results: {activeTable}
+
+ {rowCount} rows found
+
+
+
+
+
+ {loadingSchema ? (
+
+
+
+ ) : (
+
+ )}
+
+ >
+ )}
+
+ )}
+
+ {/* SQL View */}
+ {dbTab === 'sql' && (
+
+
+
+ SQL CONSOLE
+
+ : }
+ sx={{ borderRadius: 2, textTransform: 'none', fontWeight: 700 }}
+ >
+ Execute Query
+
+
+
+ setSqlQuery(value || '')}
+ options={{ minimap: { enabled: false }, fontSize: 14, automaticLayout: true }}
+ />
+
+
+
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)',
- },
- '& .MuiDataGrid-columnHeaderTitle': {
- fontWeight: 700,
- opacity: 0.8
- }
- }}
- slotProps={{
- loadingOverlay: {
- variant: 'linear-progress',
- noRowsVariant: 'linear-progress',
- }
- }}
+ sx={{ border: 'none' }}
/>
- )}
-
-
+
+
+ )}
+
+ {/* Import/Export Views */}
+ {dbTab === 'import' && }
+ {dbTab === 'export' && }
+
+ {/* Technical Info View */}
+ {dbTab === 'info' && }
{/* Custom Alert (Snackbar) */}
-
-
-
-
- }
- >
+
+
{errorInfo.title}
-
- {errorInfo.message}
-
+ {errorInfo.message}
diff --git a/frontend/src/components/TransferContent.tsx b/frontend/src/components/TransferContent.tsx
new file mode 100644
index 0000000..00b949c
--- /dev/null
+++ b/frontend/src/components/TransferContent.tsx
@@ -0,0 +1,233 @@
+import React, { useState } from 'react';
+import {
+ Box,
+ Paper,
+ Typography,
+ Button,
+ Stack,
+ Divider,
+ Alert,
+ LinearProgress,
+ IconButton
+} from '@mui/material';
+import {
+ CloudDownload,
+ CloudUpload,
+ Storage,
+ CheckCircle,
+ Error as ErrorIcon,
+ Close
+} from '@mui/icons-material';
+import { SchemaService } from '../services/api';
+import { useAppStore } from '../store/useAppStore';
+
+interface TransferContentProps {
+ mode?: 'import' | 'export' | 'both';
+}
+
+const TransferContent: React.FC = ({ mode = 'both' }) => {
+ const { activeDatabase, darkMode } = useAppStore();
+ const [loading, setLoading] = useState(false);
+ const [success, setSuccess] = useState(null);
+ const [error, setError] = useState(null);
+ const [file, setFile] = useState(null);
+
+ const handleExport = async () => {
+ setLoading(true);
+ setError(null);
+ setSuccess(null);
+ try {
+ const response = await SchemaService.exportDatabase(activeDatabase || undefined);
+ const url = window.URL.createObjectURL(new Blob([response.data]));
+ const link = document.createElement('a');
+ link.href = url;
+ const filename = `backup-${activeDatabase || 'all'}-${new Date().toISOString().split('T')[0]}.sql`;
+ link.setAttribute('download', filename);
+ document.body.appendChild(link);
+ link.click();
+ setSuccess('Database exported successfully!');
+ } catch (err: any) {
+ setError('Export failed: ' + (err.response?.data?.error || err.message));
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleFileChange = (e: React.ChangeEvent) => {
+ if (e.target.files && e.target.files[0]) {
+ setFile(e.target.files[0]);
+ }
+ };
+
+ const handleImport = async () => {
+ if (!file) return;
+ setLoading(true);
+ setError(null);
+ setSuccess(null);
+ try {
+ const formData = new FormData();
+ formData.append('file', file);
+ if (activeDatabase) formData.append('database', activeDatabase);
+
+ await SchemaService.importDatabase(formData);
+ setSuccess('Database imported successfully!');
+ setFile(null);
+ } catch (err: any) {
+ setError('Import failed: ' + (err.response?.data?.error || err.message));
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+ Database Transfer
+
+ Export your database to a SQL file or import an existing SQL dump using mysqldump.
+
+
+
+ {(success || error) && (
+ : }
+ action={
+ { setSuccess(null); setError(null); }}>
+
+
+ }
+ sx={{ borderRadius: 2, boxShadow: '0 4px 12px rgba(0,0,0,0.05)' }}
+ >
+ {success || error}
+
+ )}
+
+
+ {/* Export Card */}
+ {(mode === 'export' || mode === 'both') && (
+
+
+
+
+ Export Database
+
+ Create a full backup of the current database: {activeDatabase || 'All Databases'}
+
+
+ }
+ onClick={handleExport}
+ disabled={loading}
+ sx={{ borderRadius: 2, py: 1.5, fontWeight: 700 }}
+ >
+ Start Export
+
+
+ )}
+
+ {/* Import Card */}
+ {(mode === 'import' || mode === 'both') && (
+
+
+
+
+ Import Database
+
+ Upload a .sql file to restore or migrate your data.
+
+
+
+
+
+ {file ? file.name : "Click or drag .sql file here"}
+
+
+
+
+ }
+ onClick={handleImport}
+ disabled={loading || !file}
+ sx={{ borderRadius: 2, py: 1.5, fontWeight: 700 }}
+ >
+ Start Import
+
+
+ )}
+
+
+ {loading && (
+
+
+ Processing... This may take a while for large databases.
+
+
+
+ )}
+
+
+
+
+
+ System Requirements
+
+ This system requires mysqldump and mysql clients to be installed and available in the server's PATH.
+
+
+
+
+
+
+ );
+};
+
+export default TransferContent;
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts
index 86d3b28..6f04640 100644
--- a/frontend/src/services/api.ts
+++ b/frontend/src/services/api.ts
@@ -25,7 +25,12 @@ export default api;
export const SchemaService = {
getDatabases: () => api.get('/schema/databases'),
getTables: (db: string) => api.get(`/schema/tables/${db}`, { params: { database: db } }),
+ getDatabaseMetadata: (db: string) => api.get(`/schema/metadata/${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 }),
+ exportDatabase: (database?: string) => api.post('/schema/export', { database }, { responseType: 'blob' }),
+ importDatabase: (formData: FormData) => api.post('/schema/import', formData, {
+ headers: { 'Content-Type': 'multipart/form-data' }
+ }),
};
diff --git a/frontend/src/store/useAppStore.ts b/frontend/src/store/useAppStore.ts
index c28f5ae..defd94e 100644
--- a/frontend/src/store/useAppStore.ts
+++ b/frontend/src/store/useAppStore.ts
@@ -14,10 +14,12 @@ interface AppState {
activeTab: string;
activeDatabase: string | null;
activeTable: string | null;
+ dbTab: string; // 'tables', 'sql', 'import', 'export'
connection: ConnectionConfig | null;
connected: boolean;
toggleDarkMode: () => void;
setActiveTab: (tab: string) => void;
+ setDbTab: (tab: string) => void;
setConnection: (config: ConnectionConfig) => void;
clearConnection: () => void;
setActiveDatabase: (db: string | null) => void;
@@ -31,14 +33,16 @@ export const useAppStore = create()(
activeTab: 'explorer',
activeDatabase: null,
activeTable: null,
+ dbTab: 'tables',
connection: null,
connected: false,
toggleDarkMode: () => set((state) => ({ darkMode: !state.darkMode })),
setActiveTab: (tab) => set({ activeTab: tab }),
+ setDbTab: (tab) => set({ dbTab: 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 }),
- setActiveTable: (table) => set({ activeTable: table }),
+ setActiveDatabase: (db) => set({ activeDatabase: db, activeTable: null, dbTab: 'tables' }),
+ setActiveTable: (table) => set({ activeTable: table, dbTab: 'tables' }),
}),
{
name: 'mariavel-storage',
@@ -50,7 +54,8 @@ export const useAppStore = create()(
darkMode: state.darkMode,
activeDatabase: state.activeDatabase,
activeTable: state.activeTable,
- activeTab: state.activeTab
+ activeTab: state.activeTab,
+ dbTab: state.dbTab
}),
}
)