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.
|
* Perform batch update on a table.
|
||||||
*/
|
*/
|
||||||
public function batchUpdate(string $table, array $changes): bool;
|
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);
|
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);
|
fclose($handle);
|
||||||
return $path;
|
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);
|
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::prefix('schema')->group(function () {
|
||||||
Route::get('/databases', [SchemaController::class, 'databases']);
|
Route::get('/databases', [SchemaController::class, 'databases']);
|
||||||
|
Route::post('/databases', [SchemaController::class, 'createDatabase']);
|
||||||
Route::get('/tables/{database}', [SchemaController::class, 'tables']);
|
Route::get('/tables/{database}', [SchemaController::class, 'tables']);
|
||||||
Route::get('/metadata/{database}', [SchemaController::class, 'metadata']);
|
Route::get('/metadata/{database}', [SchemaController::class, 'metadata']);
|
||||||
Route::get('/metadata/{database}/tables', [SchemaController::class, 'tablesMetadata']);
|
Route::get('/metadata/{database}/tables', [SchemaController::class, 'tablesMetadata']);
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { useEffect, useState, useMemo } from 'react';
|
import React, { useEffect, useState, useMemo } from 'react';
|
||||||
import { Box, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Typography, Divider, CircularProgress, Stack, TextField, InputAdornment } from '@mui/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 } from '@mui/icons-material';
|
import { Storage, Search, FilterList, ChevronRight, ExpandMore, TableChart, Folder, Add } from '@mui/icons-material';
|
||||||
import { useAppStore } from '../store/useAppStore';
|
import { useAppStore } from '../store/useAppStore';
|
||||||
import { SchemaService } from '../services/api';
|
import { SchemaService } from '../services/api';
|
||||||
|
import type { MainNotification } from '../types/database';
|
||||||
|
|
||||||
const Sidebar: React.FC = () => {
|
const Sidebar: React.FC = () => {
|
||||||
const { activeDatabase, setActiveDatabase, setActiveTable, activeTable } = useAppStore();
|
const { activeDatabase, setActiveDatabase, setActiveTable, activeTable } = useAppStore();
|
||||||
@@ -16,9 +17,18 @@ const Sidebar: React.FC = () => {
|
|||||||
|
|
||||||
const [tablesLoading, setTablesLoading] = useState(false);
|
const [tablesLoading, setTablesLoading] = useState(false);
|
||||||
|
|
||||||
// Fetch databases on mount
|
const [openNewDbDialog, setOpenNewDbDialog] = useState(false);
|
||||||
useEffect(() => {
|
const [newDbName, setNewDbName] = useState('');
|
||||||
|
const [isCreatingDb, setIsCreatingDb] = useState(false);
|
||||||
|
const [notification, setNotification] = useState<MainNotification>({
|
||||||
|
open: false,
|
||||||
|
message: '',
|
||||||
|
title: '',
|
||||||
|
severity: 'success'
|
||||||
|
});
|
||||||
|
|
||||||
const fetchDatabases = async () => {
|
const fetchDatabases = async () => {
|
||||||
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await SchemaService.getDatabases();
|
const response = await SchemaService.getDatabases();
|
||||||
setDatabases(response.data);
|
setDatabases(response.data);
|
||||||
@@ -28,6 +38,9 @@ const Sidebar: React.FC = () => {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Fetch databases on mount
|
||||||
|
useEffect(() => {
|
||||||
fetchDatabases();
|
fetchDatabases();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -86,12 +99,46 @@ const Sidebar: React.FC = () => {
|
|||||||
return tables.filter(table => table.toLowerCase().includes(tableSearch.toLowerCase()));
|
return tables.filter(table => table.toLowerCase().includes(tableSearch.toLowerCase()));
|
||||||
}, [tables, tableSearch]);
|
}, [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 (
|
return (
|
||||||
<Box sx={{ width: 280, borderRight: 1, borderColor: 'divider', display: 'flex', flexDirection: 'column', bgcolor: 'background.paper', zIndex: 10 }}>
|
<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 }}>
|
<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 }} />
|
<Storage color="primary" sx={{ fontSize: 24 }} />
|
||||||
<Typography variant="h6" sx={{ fontWeight: 800, fontSize: '1.1rem' }}>Explorer</Typography>
|
<Typography variant="h6" sx={{ fontWeight: 800, fontSize: '1.1rem' }}>Explorer</Typography>
|
||||||
</Box>
|
</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 }}>
|
<Box sx={{ px: 2, pb: 1 }}>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -229,6 +276,55 @@ const Sidebar: React.FC = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</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>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export default api;
|
|||||||
|
|
||||||
export const SchemaService = {
|
export const SchemaService = {
|
||||||
getDatabases: () => api.get('/schema/databases'),
|
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 } }),
|
getTables: (db: string) => api.get(`/schema/tables/${db}`, { params: { database: db } }),
|
||||||
getDatabaseMetadata: (db: string) => api.get(`/schema/metadata/${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 } }),
|
getTablesMetadata: (db: string) => api.get(`/schema/metadata/${db}/tables`, { params: { database: db } }),
|
||||||
|
|||||||
Reference in New Issue
Block a user