feat: implement database management service layer with MySQL driver and API controllers
This commit is contained in:
@@ -78,4 +78,9 @@ interface DatabaseDriverInterface
|
||||
* Perform batch update on a table.
|
||||
*/
|
||||
public function batchUpdate(string $table, array $changes): bool;
|
||||
|
||||
/**
|
||||
* Create a new database.
|
||||
*/
|
||||
public function createDatabase(string $name, string $charset = 'utf8mb4', string $collation = 'utf8mb4_unicode_ci'): bool;
|
||||
}
|
||||
|
||||
@@ -266,4 +266,25 @@ class SchemaController extends Controller
|
||||
return Response::json(['error' => $e->getMessage()], 400);
|
||||
}
|
||||
}
|
||||
|
||||
public function createDatabase(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:64',
|
||||
'charset' => 'nullable|string|max:32',
|
||||
'collation' => 'nullable|string|max:64',
|
||||
]);
|
||||
|
||||
try {
|
||||
$this->initializeDriver($request);
|
||||
$name = $request->get('name');
|
||||
$charset = $request->get('charset', 'utf8mb4');
|
||||
$collation = $request->get('collation', 'utf8mb4_unicode_ci');
|
||||
|
||||
$this->databaseService->createDatabase($name, $charset, $collation);
|
||||
return Response::json(['message' => "Database '{$name}' created successfully"]);
|
||||
} catch (\Exception $e) {
|
||||
return Response::json(['error' => $e->getMessage()], 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,4 +448,10 @@ class MySqlDriver implements DatabaseDriverInterface, SchemaDiscoveryInterface
|
||||
fclose($handle);
|
||||
return $path;
|
||||
}
|
||||
|
||||
public function createDatabase(string $name, string $charset = 'utf8mb4', string $collation = 'utf8mb4_unicode_ci'): bool
|
||||
{
|
||||
DB::connection($this->connectionName)->statement("CREATE DATABASE `{$name}` CHARACTER SET {$charset} COLLATE {$collation}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,4 +164,12 @@ class DatabaseService
|
||||
{
|
||||
return $this->getDriver()->batchUpdate($table, $changes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new database.
|
||||
*/
|
||||
public function createDatabase(string $name, string $charset = 'utf8mb4', string $collation = 'utf8mb4_unicode_ci'): bool
|
||||
{
|
||||
return $this->getDriver()->createDatabase($name, $charset, $collation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ Route::get('/user', function (Request $request) {
|
||||
|
||||
Route::prefix('schema')->group(function () {
|
||||
Route::get('/databases', [SchemaController::class, 'databases']);
|
||||
Route::post('/databases', [SchemaController::class, 'createDatabase']);
|
||||
Route::get('/tables/{database}', [SchemaController::class, 'tables']);
|
||||
Route::get('/metadata/{database}', [SchemaController::class, 'metadata']);
|
||||
Route::get('/metadata/{database}/tables', [SchemaController::class, 'tablesMetadata']);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
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 { Box, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Typography, Divider, CircularProgress, Stack, TextField, InputAdornment, IconButton, Tooltip, Dialog, DialogTitle, DialogContent, DialogActions, Button, Alert, Snackbar, AlertTitle } from '@mui/material';
|
||||
import { Storage, Search, FilterList, ChevronRight, ExpandMore, TableChart, Folder, Add } from '@mui/icons-material';
|
||||
import { useAppStore } from '../store/useAppStore';
|
||||
import { SchemaService } from '../services/api';
|
||||
import type { MainNotification } from '../types/database';
|
||||
|
||||
const Sidebar: React.FC = () => {
|
||||
const { activeDatabase, setActiveDatabase, setActiveTable, activeTable } = useAppStore();
|
||||
@@ -16,18 +17,30 @@ const Sidebar: React.FC = () => {
|
||||
|
||||
const [tablesLoading, setTablesLoading] = useState(false);
|
||||
|
||||
const [openNewDbDialog, setOpenNewDbDialog] = useState(false);
|
||||
const [newDbName, setNewDbName] = useState('');
|
||||
const [isCreatingDb, setIsCreatingDb] = useState(false);
|
||||
const [notification, setNotification] = useState<MainNotification>({
|
||||
open: false,
|
||||
message: '',
|
||||
title: '',
|
||||
severity: 'success'
|
||||
});
|
||||
|
||||
const fetchDatabases = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await SchemaService.getDatabases();
|
||||
setDatabases(response.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch databases', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch databases on mount
|
||||
useEffect(() => {
|
||||
const fetchDatabases = async () => {
|
||||
try {
|
||||
const response = await SchemaService.getDatabases();
|
||||
setDatabases(response.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch databases', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchDatabases();
|
||||
}, []);
|
||||
|
||||
@@ -86,11 +99,45 @@ const Sidebar: React.FC = () => {
|
||||
return tables.filter(table => table.toLowerCase().includes(tableSearch.toLowerCase()));
|
||||
}, [tables, tableSearch]);
|
||||
|
||||
const handleCreateDatabase = async () => {
|
||||
if (!newDbName.trim()) return;
|
||||
|
||||
setIsCreatingDb(true);
|
||||
try {
|
||||
await SchemaService.createDatabase(newDbName);
|
||||
setNotification({
|
||||
open: true,
|
||||
title: 'Success',
|
||||
message: `Database '${newDbName}' created successfully.`,
|
||||
severity: 'success'
|
||||
});
|
||||
setOpenNewDbDialog(false);
|
||||
setNewDbName('');
|
||||
fetchDatabases();
|
||||
} catch (error: any) {
|
||||
setNotification({
|
||||
open: true,
|
||||
title: 'Error',
|
||||
message: error.response?.data?.error || error.message,
|
||||
severity: 'error'
|
||||
});
|
||||
} finally {
|
||||
setIsCreatingDb(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ width: 280, borderRight: 1, borderColor: 'divider', display: 'flex', flexDirection: 'column', bgcolor: 'background.paper', zIndex: 10 }}>
|
||||
<Box sx={{ p: 2, display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
<Storage color="primary" sx={{ fontSize: 24 }} />
|
||||
<Typography variant="h6" sx={{ fontWeight: 800, fontSize: '1.1rem' }}>Explorer</Typography>
|
||||
<Box sx={{ p: 2, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
<Storage color="primary" sx={{ fontSize: 24 }} />
|
||||
<Typography variant="h6" sx={{ fontWeight: 800, fontSize: '1.1rem' }}>Explorer</Typography>
|
||||
</Box>
|
||||
<Tooltip title="New Database">
|
||||
<IconButton size="small" onClick={() => setOpenNewDbDialog(true)} sx={{ color: 'primary.main' }}>
|
||||
<Add fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ px: 2, pb: 1 }}>
|
||||
@@ -229,6 +276,55 @@ const Sidebar: React.FC = () => {
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* New Database Dialog */}
|
||||
<Dialog open={openNewDbDialog} onClose={() => setOpenNewDbDialog(false)} maxWidth="xs" fullWidth>
|
||||
<DialogTitle sx={{ fontWeight: 800 }}>Create New Database</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
label="Database Name"
|
||||
type="text"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={newDbName}
|
||||
onChange={(e) => setNewDbName(e.target.value)}
|
||||
disabled={isCreatingDb}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') handleCreateDatabase();
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: 2, pt: 0 }}>
|
||||
<Button onClick={() => setOpenNewDbDialog(false)} disabled={isCreatingDb}>Cancel</Button>
|
||||
<Button
|
||||
onClick={handleCreateDatabase}
|
||||
variant="contained"
|
||||
disabled={isCreatingDb || !newDbName.trim()}
|
||||
>
|
||||
{isCreatingDb ? <CircularProgress size={24} /> : 'Create'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* Local Notification */}
|
||||
<Snackbar
|
||||
open={notification.open}
|
||||
autoHideDuration={4000}
|
||||
onClose={() => setNotification({ ...notification, open: false })}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||
>
|
||||
<Alert
|
||||
severity={notification.severity}
|
||||
variant="filled"
|
||||
onClose={() => setNotification({ ...notification, open: false })}
|
||||
sx={{ width: '100%', borderRadius: 2 }}
|
||||
>
|
||||
<AlertTitle sx={{ fontWeight: 700 }}>{notification.title}</AlertTitle>
|
||||
{notification.message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ export default api;
|
||||
|
||||
export const SchemaService = {
|
||||
getDatabases: () => api.get('/schema/databases'),
|
||||
createDatabase: (name: string, charset?: string, collation?: string) => api.post('/schema/databases', { name, charset, collation }),
|
||||
getTables: (db: string) => api.get(`/schema/tables/${db}`, { params: { database: db } }),
|
||||
getDatabaseMetadata: (db: string) => api.get(`/schema/metadata/${db}`, { params: { database: db } }),
|
||||
getTablesMetadata: (db: string) => api.get(`/schema/metadata/${db}/tables`, { params: { database: db } }),
|
||||
|
||||
Reference in New Issue
Block a user