feat: implement MySQL driver and API schema services for database management
This commit is contained in:
@@ -33,4 +33,19 @@ interface DatabaseDriverInterface
|
|||||||
* Get the underlying connection instance.
|
* Get the underlying connection instance.
|
||||||
*/
|
*/
|
||||||
public function getConnection();
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,4 +104,54 @@ class SchemaController extends Controller
|
|||||||
return Response::json(['error' => $e->getMessage()], 400);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,4 +93,120 @@ class MySqlDriver implements DatabaseDriverInterface, SchemaDiscoveryInterface
|
|||||||
$dbName = DB::connection($this->connectionName)->getDatabaseName();
|
$dbName = DB::connection($this->connectionName)->getDatabaseName();
|
||||||
return $this->query($sql, [$table, $dbName]);
|
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] ?? []);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,4 +98,28 @@ class DatabaseService
|
|||||||
{
|
{
|
||||||
return $this->getDriver()->query($sql, $bindings);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ Route::get('/user', function (Request $request) {
|
|||||||
Route::prefix('schema')->group(function () {
|
Route::prefix('schema')->group(function () {
|
||||||
Route::get('/databases', [SchemaController::class, 'databases']);
|
Route::get('/databases', [SchemaController::class, 'databases']);
|
||||||
Route::get('/tables/{database}', [SchemaController::class, 'tables']);
|
Route::get('/tables/{database}', [SchemaController::class, 'tables']);
|
||||||
|
Route::get('/metadata/{database}', [SchemaController::class, 'metadata']);
|
||||||
Route::get('/{table}', [SchemaController::class, 'schema']);
|
Route::get('/{table}', [SchemaController::class, 'schema']);
|
||||||
Route::get('/{table}/data', [SchemaController::class, 'data']);
|
Route::get('/{table}/data', [SchemaController::class, 'data']);
|
||||||
Route::post('/execute', [SchemaController::class, 'execute']);
|
Route::post('/execute', [SchemaController::class, 'execute']);
|
||||||
|
Route::post('/export', [SchemaController::class, 'export']);
|
||||||
|
Route::post('/import', [SchemaController::class, 'import']);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import MainContent from './components/MainContent.tsx';
|
|||||||
import Header from './components/Header.tsx';
|
import Header from './components/Header.tsx';
|
||||||
import Login from './components/Login.tsx';
|
import Login from './components/Login.tsx';
|
||||||
import NavigationRail from './components/NavigationRail.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
|
// Create emotion cache to handle the :first-child warning and ensure proper style injection
|
||||||
const cache = createCache({
|
const cache = createCache({
|
||||||
@@ -48,7 +49,7 @@ const App: React.FC = () => {
|
|||||||
{activeTab === 'explorer' && <Sidebar />}
|
{activeTab === 'explorer' && <Sidebar />}
|
||||||
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', minWidth: 0, overflow: 'hidden' }}>
|
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', minWidth: 0, overflow: 'hidden' }}>
|
||||||
<Header />
|
<Header />
|
||||||
<MainContent />
|
{activeTab === 'transfer' ? <TransferContent /> : <MainContent />}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|||||||
@@ -10,17 +10,31 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Snackbar,
|
Snackbar,
|
||||||
Alert,
|
Alert,
|
||||||
AlertTitle
|
AlertTitle,
|
||||||
|
Tabs,
|
||||||
|
Tab
|
||||||
} from '@mui/material';
|
} 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 { DataGrid } from '@mui/x-data-grid';
|
||||||
import type { GridColDef, GridPaginationModel } from '@mui/x-data-grid';
|
import type { GridColDef, GridPaginationModel } from '@mui/x-data-grid';
|
||||||
import Editor from '@monaco-editor/react';
|
import Editor from '@monaco-editor/react';
|
||||||
import { useAppStore } from '../store/useAppStore';
|
import { useAppStore } from '../store/useAppStore';
|
||||||
import { SchemaService } from '../services/api';
|
import { SchemaService } from '../services/api';
|
||||||
|
import TransferContent from './TransferContent';
|
||||||
|
|
||||||
const MainContent: React.FC = () => {
|
const MainContent: React.FC = () => {
|
||||||
const { activeTable, activeDatabase, darkMode } = useAppStore();
|
const { activeTable, activeDatabase, darkMode, dbTab, setDbTab } = useAppStore();
|
||||||
const [columns, setColumns] = useState<GridColDef[]>([]);
|
const [columns, setColumns] = useState<GridColDef[]>([]);
|
||||||
const [rows, setRows] = useState<any[]>([]);
|
const [rows, setRows] = useState<any[]>([]);
|
||||||
const [rowCount, setRowCount] = useState(0);
|
const [rowCount, setRowCount] = useState(0);
|
||||||
@@ -170,180 +184,228 @@ const MainContent: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!activeTable) {
|
if (!activeDatabase) {
|
||||||
return (
|
return (
|
||||||
<Box sx={{ flexGrow: 1, p: 3, display: 'flex', alignItems: 'center', justifyContent: 'center', bgcolor: 'background.default' }}>
|
<Box sx={{ flexGrow: 1, p: 3, display: 'flex', alignItems: 'center', justifyContent: 'center', bgcolor: 'background.default' }}>
|
||||||
<Typography variant="h6" color="text.secondary">Select a table to view data</Typography>
|
<Typography variant="h6" color="text.secondary">Select a database to start</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleTabChange = (_: React.SyntheticEvent, newValue: string) => {
|
||||||
|
setDbTab(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DatabaseOverview = ({ database }: { database: string }) => {
|
||||||
|
const [meta, setMeta] = useState<any>(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 <Box sx={{ display: 'flex', justifyContent: 'center', p: 8 }}><CircularProgress /></Box>;
|
||||||
|
if (!meta) return <Alert severity="error">Could not load metadata</Alert>;
|
||||||
|
|
||||||
|
const stats = [
|
||||||
|
{ label: 'Character Set', value: meta.charset, icon: <History /> },
|
||||||
|
{ label: 'Collation', value: meta.collation, icon: <CleaningServices /> },
|
||||||
|
{ label: 'Tables', value: meta.table_count, icon: <TableChart /> },
|
||||||
|
{ label: 'Views', value: meta.view_count, icon: <History /> },
|
||||||
|
{ label: 'Total Size', value: `${(meta.size_bytes / 1024 / 1024).toFixed(2)} MB`, icon: <Save /> },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 1 }}>
|
||||||
|
<Typography variant="h5" sx={{ fontWeight: 800, mb: 4, letterSpacing: -0.5 }}>Technical Specifications: {database}</Typography>
|
||||||
|
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))', gap: 3 }}>
|
||||||
|
{stats.map((stat, i) => (
|
||||||
|
<Paper key={i} sx={{
|
||||||
|
p: 3,
|
||||||
|
borderRadius: 4,
|
||||||
|
border: 1,
|
||||||
|
borderColor: 'divider',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 1,
|
||||||
|
bgcolor: (theme) => 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)' }
|
||||||
|
}}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<Typography variant="caption" sx={{ fontWeight: 700, color: 'text.secondary', textTransform: 'uppercase', letterSpacing: 1 }}>{stat.label}</Typography>
|
||||||
|
<Box sx={{ color: 'primary.main', opacity: 0.5 }}>{stat.icon}</Box>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="h5" sx={{ fontWeight: 800 }}>{stat.value}</Typography>
|
||||||
|
</Paper>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ flexGrow: 1, p: 3, bgcolor: 'background.default', display: 'flex', flexDirection: 'column', width: '100%', minWidth: 0, overflow: 'hidden', gap: 2 }}>
|
<Box sx={{ flexGrow: 1, p: 3, bgcolor: 'background.default', display: 'flex', flexDirection: 'column', width: '100%', minWidth: 0, overflow: 'hidden', gap: 2 }}>
|
||||||
{/* SQL Editor Section */}
|
{/* Database Navigation Tabs */}
|
||||||
<Paper elevation={0} sx={{
|
<Paper elevation={0} sx={{
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
borderRadius: 3,
|
borderRadius: 3,
|
||||||
overflow: 'hidden',
|
|
||||||
border: 1,
|
border: 1,
|
||||||
borderColor: 'divider',
|
borderColor: 'divider',
|
||||||
bgcolor: darkMode ? '#111827' : '#ffffff',
|
bgcolor: darkMode ? '#111827' : '#ffffff',
|
||||||
boxShadow: '0 4px 20px rgba(0,0,0,0.1)'
|
overflow: 'hidden'
|
||||||
}}>
|
}}>
|
||||||
<Box sx={{ px: 3, py: 1.5, display: 'flex', alignItems: 'center', justifyContent: 'space-between', bgcolor: 'rgba(0,0,0,0.02)' }}>
|
<Tabs
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
value={dbTab}
|
||||||
<Typography variant="subtitle2" sx={{ fontWeight: 800, opacity: 0.8, letterSpacing: 1 }}>SQL EDITOR</Typography>
|
onChange={handleTabChange}
|
||||||
<Divider orientation="vertical" flexItem sx={{ height: 16, my: 'auto' }} />
|
sx={{
|
||||||
<Typography variant="caption" sx={{ opacity: 0.5, fontWeight: 500 }}>
|
px: 2,
|
||||||
{isCustomQuery ? 'Custom Query' : `${activeDatabase}.${activeTable}`}
|
minHeight: 56,
|
||||||
</Typography>
|
'& .MuiTabs-indicator': { height: 3, borderRadius: '3px 3px 0 0' },
|
||||||
</Box>
|
'& .MuiTab-root': {
|
||||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
textTransform: 'none',
|
||||||
<Tooltip title="Clear Editor">
|
fontWeight: 700,
|
||||||
<IconButton size="small" onClick={() => setSqlQuery('')}><CleaningServices fontSize="small" /></IconButton>
|
fontSize: '0.9rem',
|
||||||
</Tooltip>
|
minHeight: 56,
|
||||||
<Tooltip title="Save Query">
|
minWidth: 120,
|
||||||
<IconButton size="small"><Save fontSize="small" /></IconButton>
|
gap: 1
|
||||||
</Tooltip>
|
}
|
||||||
<Tooltip title="Query History">
|
|
||||||
<IconButton size="small"><History fontSize="small" /></IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
size="small"
|
|
||||||
onClick={handleExecute}
|
|
||||||
disabled={loadingData}
|
|
||||||
startIcon={loadingData ? <CircularProgress size={16} color="inherit" /> : <PlayArrow />}
|
|
||||||
sx={{ ml: 1, px: 3, borderRadius: 2, textTransform: 'none', fontWeight: 700, boxShadow: '0 4px 12px rgba(0, 97, 255, 0.3)' }}
|
|
||||||
>
|
|
||||||
Execute
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Editor
|
|
||||||
height="220px"
|
|
||||||
defaultLanguage="sql"
|
|
||||||
theme={darkMode ? 'vs-dark' : 'light'}
|
|
||||||
value={sqlQuery}
|
|
||||||
onChange={(value) => 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
|
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<Tab value="tables" icon={<TableChart sx={{ fontSize: 18 }} />} iconPosition="start" label="Data Explorer" />
|
||||||
|
<Tab value="sql" icon={<Terminal sx={{ fontSize: 18 }} />} iconPosition="start" label="SQL Editor" />
|
||||||
|
<Tab value="import" icon={<CloudUpload sx={{ fontSize: 18 }} />} iconPosition="start" label="Import" />
|
||||||
|
<Tab value="export" icon={<CloudDownload sx={{ fontSize: 18 }} />} iconPosition="start" label="Export" />
|
||||||
|
<Tab value="info" icon={<Info sx={{ fontSize: 18 }} />} iconPosition="start" label="Technical Info" />
|
||||||
|
</Tabs>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Data Section */}
|
{/* Tables View */}
|
||||||
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', minWidth: 0, overflow: 'hidden' }}>
|
{dbTab === 'tables' && (
|
||||||
<Box sx={{ mb: 1.5, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', minWidth: 0, overflow: 'hidden' }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
{!activeTable ? (
|
||||||
<Typography variant="h6" sx={{ fontWeight: 800 }}>
|
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 2, opacity: 0.5 }}>
|
||||||
{isCustomQuery ? 'Query Results' : 'Table Results'}
|
<TableChart sx={{ fontSize: 64 }} />
|
||||||
</Typography>
|
<Typography variant="h6">Select a table from the explorer to view data</Typography>
|
||||||
<Box sx={{ px: 1.5, py: 0.5, borderRadius: 10, bgcolor: 'rgba(0, 97, 255, 0.1)', color: 'primary.main' }}>
|
|
||||||
<Typography variant="caption" sx={{ fontWeight: 700 }}>{rowCount} rows found</Typography>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
{isCustomQuery && (
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="outlined"
|
|
||||||
onClick={() => setIsCustomQuery(false)}
|
|
||||||
sx={{ textTransform: 'none', borderRadius: 2 }}
|
|
||||||
>
|
|
||||||
Back to Table View
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Paper sx={{
|
|
||||||
flexGrow: 1,
|
|
||||||
borderRadius: 3,
|
|
||||||
overflow: 'hidden',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
width: '100%',
|
|
||||||
border: 1,
|
|
||||||
borderColor: 'divider',
|
|
||||||
boxShadow: '0 4px 12px rgba(0,0,0,0.05)'
|
|
||||||
}}>
|
|
||||||
{(loadingSchema && !isCustomQuery) ? (
|
|
||||||
<Box sx={{ display: 'flex', height: '100%', alignItems: 'center', justifyContent: 'center' }}>
|
|
||||||
<CircularProgress />
|
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
|
<Box sx={{ mb: 1.5, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 800 }}>Table Results: {activeTable}</Typography>
|
||||||
|
<Box sx={{ px: 1.5, py: 0.5, borderRadius: 10, bgcolor: 'rgba(0, 97, 255, 0.1)', color: 'primary.main' }}>
|
||||||
|
<Typography variant="caption" sx={{ fontWeight: 700 }}>{rowCount} rows found</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Paper sx={{
|
||||||
|
flexGrow: 1,
|
||||||
|
borderRadius: 3,
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
width: '100%',
|
||||||
|
border: 1,
|
||||||
|
borderColor: 'divider',
|
||||||
|
boxShadow: '0 4px 12px rgba(0,0,0,0.05)'
|
||||||
|
}}>
|
||||||
|
{loadingSchema ? (
|
||||||
|
<Box sx={{ display: 'flex', height: '100%', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<DataGrid
|
||||||
|
rows={rows}
|
||||||
|
columns={columns}
|
||||||
|
rowCount={rowCount}
|
||||||
|
loading={loadingData}
|
||||||
|
paginationMode="server"
|
||||||
|
paginationModel={paginationModel}
|
||||||
|
onPaginationModelChange={setPaginationModel}
|
||||||
|
pageSizeOptions={[25, 50, 100]}
|
||||||
|
sx={{
|
||||||
|
border: 'none',
|
||||||
|
'& .MuiDataGrid-row:hover': { bgcolor: 'rgba(0, 97, 255, 0.04)' },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* SQL View */}
|
||||||
|
{dbTab === 'sql' && (
|
||||||
|
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', gap: 2, overflow: 'hidden' }}>
|
||||||
|
<Paper elevation={0} sx={{
|
||||||
|
flexShrink: 0,
|
||||||
|
borderRadius: 3,
|
||||||
|
overflow: 'hidden',
|
||||||
|
border: 1,
|
||||||
|
borderColor: 'divider',
|
||||||
|
bgcolor: darkMode ? '#111827' : '#ffffff',
|
||||||
|
}}>
|
||||||
|
<Box sx={{ px: 3, py: 1.5, display: 'flex', alignItems: 'center', justifyContent: 'space-between', bgcolor: 'rgba(0,0,0,0.02)' }}>
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 800 }}>SQL CONSOLE</Typography>
|
||||||
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
onClick={handleExecute}
|
||||||
|
disabled={loadingData}
|
||||||
|
startIcon={loadingData ? <CircularProgress size={16} color="inherit" /> : <PlayArrow />}
|
||||||
|
sx={{ borderRadius: 2, textTransform: 'none', fontWeight: 700 }}
|
||||||
|
>
|
||||||
|
Execute Query
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Editor
|
||||||
|
height="200px"
|
||||||
|
defaultLanguage="sql"
|
||||||
|
theme={darkMode ? 'vs-dark' : 'light'}
|
||||||
|
value={sqlQuery}
|
||||||
|
onChange={(value) => setSqlQuery(value || '')}
|
||||||
|
options={{ minimap: { enabled: false }, fontSize: 14, automaticLayout: true }}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Paper sx={{ flexGrow: 1, borderRadius: 3, overflow: 'hidden', border: 1, borderColor: 'divider' }}>
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowCount={rowCount}
|
|
||||||
loading={loadingData}
|
loading={loadingData}
|
||||||
paginationMode={isCustomQuery ? 'client' : 'server'}
|
sx={{ border: 'none' }}
|
||||||
paginationModel={paginationModel}
|
|
||||||
onPaginationModelChange={setPaginationModel}
|
|
||||||
pageSizeOptions={[25, 50, 100]}
|
|
||||||
sx={{
|
|
||||||
border: 'none',
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
'& .MuiDataGrid-cell:focus': { outline: 'none' },
|
|
||||||
'& .MuiDataGrid-columnHeader:focus': { outline: 'none' },
|
|
||||||
'& .MuiDataGrid-row:nth-of-type(even)': {
|
|
||||||
bgcolor: (theme) => 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',
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</Paper>
|
||||||
</Paper>
|
</Box>
|
||||||
</Box>
|
)}
|
||||||
|
|
||||||
|
{/* Import/Export Views */}
|
||||||
|
{dbTab === 'import' && <TransferContent mode="import" />}
|
||||||
|
{dbTab === 'export' && <TransferContent mode="export" />}
|
||||||
|
|
||||||
|
{/* Technical Info View */}
|
||||||
|
{dbTab === 'info' && <DatabaseOverview database={activeDatabase} />}
|
||||||
|
|
||||||
{/* Custom Alert (Snackbar) */}
|
{/* Custom Alert (Snackbar) */}
|
||||||
<Snackbar
|
<Snackbar open={errorInfo.open} autoHideDuration={10000} onClose={handleCloseError} anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}>
|
||||||
open={errorInfo.open}
|
<Alert onClose={handleCloseError} severity="error" variant="filled" sx={{ width: '100%', borderRadius: 2 }}>
|
||||||
autoHideDuration={10000}
|
|
||||||
onClose={handleCloseError}
|
|
||||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
|
||||||
>
|
|
||||||
<Alert
|
|
||||||
onClose={handleCloseError}
|
|
||||||
severity="error"
|
|
||||||
variant="filled"
|
|
||||||
sx={{
|
|
||||||
width: '100%',
|
|
||||||
maxWidth: 600,
|
|
||||||
borderRadius: 2,
|
|
||||||
boxShadow: '0 8px 32px rgba(0,0,0,0.2)',
|
|
||||||
'& .MuiAlert-message': { wordBreak: 'break-word' }
|
|
||||||
}}
|
|
||||||
action={
|
|
||||||
<IconButton size="small" color="inherit" onClick={handleCloseError}>
|
|
||||||
<Close fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<AlertTitle sx={{ fontWeight: 800 }}>{errorInfo.title}</AlertTitle>
|
<AlertTitle sx={{ fontWeight: 800 }}>{errorInfo.title}</AlertTitle>
|
||||||
<Typography variant="body2" sx={{ fontFamily: 'monospace', fontSize: '0.8rem' }}>
|
<Typography variant="body2">{errorInfo.message}</Typography>
|
||||||
{errorInfo.message}
|
|
||||||
</Typography>
|
|
||||||
</Alert>
|
</Alert>
|
||||||
</Snackbar>
|
</Snackbar>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -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<TransferContentProps> = ({ mode = 'both' }) => {
|
||||||
|
const { activeDatabase, darkMode } = useAppStore();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [success, setSuccess] = useState<string | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [file, setFile] = useState<File | null>(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<HTMLInputElement>) => {
|
||||||
|
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 (
|
||||||
|
<Box sx={{ flexGrow: 1, p: 4, bgcolor: 'background.default', display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<Stack spacing={4} sx={{ width: '100%', maxWidth: 800 }}>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h4" sx={{ fontWeight: 800, mb: 1 }}>Database Transfer</Typography>
|
||||||
|
<Typography variant="body1" color="text.secondary">
|
||||||
|
Export your database to a SQL file or import an existing SQL dump using <strong>mysqldump</strong>.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{(success || error) && (
|
||||||
|
<Alert
|
||||||
|
severity={success ? "success" : "error"}
|
||||||
|
icon={success ? <CheckCircle /> : <ErrorIcon />}
|
||||||
|
action={
|
||||||
|
<IconButton size="small" onClick={() => { setSuccess(null); setError(null); }}>
|
||||||
|
<Close fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
sx={{ borderRadius: 2, boxShadow: '0 4px 12px rgba(0,0,0,0.05)' }}
|
||||||
|
>
|
||||||
|
{success || error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Stack direction={{ xs: 'column', md: 'row' }} spacing={3}>
|
||||||
|
{/* Export Card */}
|
||||||
|
{(mode === 'export' || mode === 'both') && (
|
||||||
|
<Paper sx={{
|
||||||
|
flex: 1,
|
||||||
|
p: 4,
|
||||||
|
borderRadius: 4,
|
||||||
|
border: 1,
|
||||||
|
borderColor: 'divider',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
textAlign: 'center',
|
||||||
|
gap: 2,
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
'&:hover': { transform: 'translateY(-4px)', boxShadow: '0 12px 24px rgba(0,0,0,0.1)' }
|
||||||
|
}}>
|
||||||
|
<Box sx={{ p: 2, borderRadius: '50%', bgcolor: 'primary.main', color: 'white', mb: 1 }}>
|
||||||
|
<CloudDownload sx={{ fontSize: 40 }} />
|
||||||
|
</Box>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 700 }}>Export Database</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Create a full backup of the current database: <strong>{activeDatabase || 'All Databases'}</strong>
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ flexGrow: 1 }} />
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
fullWidth
|
||||||
|
size="large"
|
||||||
|
startIcon={<CloudDownload />}
|
||||||
|
onClick={handleExport}
|
||||||
|
disabled={loading}
|
||||||
|
sx={{ borderRadius: 2, py: 1.5, fontWeight: 700 }}
|
||||||
|
>
|
||||||
|
Start Export
|
||||||
|
</Button>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Import Card */}
|
||||||
|
{(mode === 'import' || mode === 'both') && (
|
||||||
|
<Paper sx={{
|
||||||
|
flex: 1,
|
||||||
|
p: 4,
|
||||||
|
borderRadius: 4,
|
||||||
|
border: 1,
|
||||||
|
borderColor: 'divider',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
textAlign: 'center',
|
||||||
|
gap: 2,
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
'&:hover': { transform: 'translateY(-4px)', boxShadow: '0 12px 24px rgba(0,0,0,0.1)' }
|
||||||
|
}}>
|
||||||
|
<Box sx={{ p: 2, borderRadius: '50%', bgcolor: 'secondary.main', color: 'white', mb: 1 }}>
|
||||||
|
<CloudUpload sx={{ fontSize: 40 }} />
|
||||||
|
</Box>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 700 }}>Import Database</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Upload a .sql file to restore or migrate your data.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box sx={{
|
||||||
|
width: '100%',
|
||||||
|
p: 2,
|
||||||
|
border: '2px dashed',
|
||||||
|
borderColor: file ? 'secondary.main' : 'divider',
|
||||||
|
borderRadius: 2,
|
||||||
|
bgcolor: 'rgba(0,0,0,0.02)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
position: 'relative'
|
||||||
|
}}>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".sql"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', opacity: 0, cursor: 'pointer' }}
|
||||||
|
/>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{file ? file.name : "Click or drag .sql file here"}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ flexGrow: 1 }} />
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
size="large"
|
||||||
|
color="secondary"
|
||||||
|
startIcon={<CloudUpload />}
|
||||||
|
onClick={handleImport}
|
||||||
|
disabled={loading || !file}
|
||||||
|
sx={{ borderRadius: 2, py: 1.5, fontWeight: 700 }}
|
||||||
|
>
|
||||||
|
Start Import
|
||||||
|
</Button>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{loading && (
|
||||||
|
<Box sx={{ width: '100%' }}>
|
||||||
|
<Typography variant="caption" sx={{ mb: 1, display: 'block', textAlign: 'center' }}>
|
||||||
|
Processing... This may take a while for large databases.
|
||||||
|
</Typography>
|
||||||
|
<LinearProgress sx={{ borderRadius: 5, height: 8 }} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box sx={{ p: 3, borderRadius: 3, bgcolor: 'rgba(0, 0, 0, 0.03)', border: 1, borderColor: 'divider' }}>
|
||||||
|
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
||||||
|
<Storage color="primary" />
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>System Requirements</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
This system requires <code>mysqldump</code> and <code>mysql</code> clients to be installed and available in the server's PATH.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TransferContent;
|
||||||
@@ -25,7 +25,12 @@ export default api;
|
|||||||
export const SchemaService = {
|
export const SchemaService = {
|
||||||
getDatabases: () => api.get('/schema/databases'),
|
getDatabases: () => api.get('/schema/databases'),
|
||||||
getTables: (db: string) => api.get(`/schema/tables/${db}`, { params: { database: db } }),
|
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 } }),
|
getTableSchema: (table: string, database?: string) => api.get(`/schema/${table}`, { params: { database } }),
|
||||||
getTableData: (table: string, params: any) => api.get(`/schema/${table}/data`, { params }),
|
getTableData: (table: string, params: any) => api.get(`/schema/${table}/data`, { params }),
|
||||||
executeQuery: (query: string) => api.post('/schema/execute', { query }),
|
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' }
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,10 +14,12 @@ interface AppState {
|
|||||||
activeTab: string;
|
activeTab: string;
|
||||||
activeDatabase: string | null;
|
activeDatabase: string | null;
|
||||||
activeTable: string | null;
|
activeTable: string | null;
|
||||||
|
dbTab: string; // 'tables', 'sql', 'import', 'export'
|
||||||
connection: ConnectionConfig | null;
|
connection: ConnectionConfig | null;
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
toggleDarkMode: () => void;
|
toggleDarkMode: () => void;
|
||||||
setActiveTab: (tab: string) => void;
|
setActiveTab: (tab: string) => void;
|
||||||
|
setDbTab: (tab: string) => void;
|
||||||
setConnection: (config: ConnectionConfig) => void;
|
setConnection: (config: ConnectionConfig) => void;
|
||||||
clearConnection: () => void;
|
clearConnection: () => void;
|
||||||
setActiveDatabase: (db: string | null) => void;
|
setActiveDatabase: (db: string | null) => void;
|
||||||
@@ -31,14 +33,16 @@ export const useAppStore = create<AppState>()(
|
|||||||
activeTab: 'explorer',
|
activeTab: 'explorer',
|
||||||
activeDatabase: null,
|
activeDatabase: null,
|
||||||
activeTable: null,
|
activeTable: null,
|
||||||
|
dbTab: 'tables',
|
||||||
connection: null,
|
connection: null,
|
||||||
connected: false,
|
connected: false,
|
||||||
toggleDarkMode: () => set((state) => ({ darkMode: !state.darkMode })),
|
toggleDarkMode: () => set((state) => ({ darkMode: !state.darkMode })),
|
||||||
setActiveTab: (tab) => set({ activeTab: tab }),
|
setActiveTab: (tab) => set({ activeTab: tab }),
|
||||||
|
setDbTab: (tab) => set({ dbTab: tab }),
|
||||||
setConnection: (config) => set({ connection: config, connected: true }),
|
setConnection: (config) => set({ connection: config, connected: true }),
|
||||||
clearConnection: () => set({ connection: null, connected: false, activeDatabase: null, activeTable: null }),
|
clearConnection: () => set({ connection: null, connected: false, activeDatabase: null, activeTable: null }),
|
||||||
setActiveDatabase: (db) => set({ activeDatabase: db, activeTable: null }),
|
setActiveDatabase: (db) => set({ activeDatabase: db, activeTable: null, dbTab: 'tables' }),
|
||||||
setActiveTable: (table) => set({ activeTable: table }),
|
setActiveTable: (table) => set({ activeTable: table, dbTab: 'tables' }),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'mariavel-storage',
|
name: 'mariavel-storage',
|
||||||
@@ -50,7 +54,8 @@ export const useAppStore = create<AppState>()(
|
|||||||
darkMode: state.darkMode,
|
darkMode: state.darkMode,
|
||||||
activeDatabase: state.activeDatabase,
|
activeDatabase: state.activeDatabase,
|
||||||
activeTable: state.activeTable,
|
activeTable: state.activeTable,
|
||||||
activeTab: state.activeTab
|
activeTab: state.activeTab,
|
||||||
|
dbTab: state.dbTab
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user