feat: implement database management API with MySQL driver and schema operations

This commit is contained in:
Ümit Tunç
2026-04-28 20:19:56 +03:00
parent 2e529bb61c
commit 01ddb81aa9
7 changed files with 276 additions and 9 deletions
+184 -9
View File
@@ -1,6 +1,6 @@
import React, { useEffect, useState, useMemo } from 'react';
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 { Box, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Typography, Divider, CircularProgress, Stack, TextField, InputAdornment, IconButton, Tooltip, Dialog, DialogTitle, DialogContent, DialogActions, Button, Alert, Snackbar, AlertTitle, Menu, MenuItem } from '@mui/material';
import { Storage, Search, FilterList, ChevronRight, ExpandMore, TableChart, Folder, Add, MoreVert, Delete, Edit } from '@mui/icons-material';
import { useAppStore } from '../store/useAppStore';
import { SchemaService } from '../services/api';
import type { MainNotification } from '../types/database';
@@ -19,7 +19,19 @@ const Sidebar: React.FC = () => {
const [openNewDbDialog, setOpenNewDbDialog] = useState(false);
const [newDbName, setNewDbName] = useState('');
const [isCreatingDb, setIsCreatingDb] = useState(false);
const [openRenameDialog, setOpenRenameDialog] = useState(false);
const [dbToRename, setDbToRename] = useState('');
const [renamedDbName, setRenamedDbName] = useState('');
const [openDeleteConfirm, setOpenDeleteConfirm] = useState(false);
const [dbToDelete, setDbToDelete] = useState('');
const [isProcessing, setIsProcessing] = useState(false);
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
const [selectedDbForMenu, setSelectedDbForMenu] = useState('');
const [notification, setNotification] = useState<MainNotification>({
open: false,
message: '',
@@ -102,7 +114,7 @@ const Sidebar: React.FC = () => {
const handleCreateDatabase = async () => {
if (!newDbName.trim()) return;
setIsCreatingDb(true);
setIsProcessing(true);
try {
await SchemaService.createDatabase(newDbName);
setNotification({
@@ -122,10 +134,95 @@ const Sidebar: React.FC = () => {
severity: 'error'
});
} finally {
setIsCreatingDb(false);
setIsProcessing(false);
}
};
const handleRenameDatabase = async () => {
if (!renamedDbName.trim() || !dbToRename) return;
setIsProcessing(true);
try {
await SchemaService.renameDatabase(dbToRename, renamedDbName);
setNotification({
open: true,
title: 'Success',
message: `Database '${dbToRename}' renamed to '${renamedDbName}' successfully.`,
severity: 'success'
});
if (activeDatabase === dbToRename) {
setActiveDatabase(renamedDbName);
}
setOpenRenameDialog(false);
setDbToRename('');
setRenamedDbName('');
fetchDatabases();
} catch (error: any) {
setNotification({
open: true,
title: 'Error',
message: error.response?.data?.error || error.message,
severity: 'error'
});
} finally {
setIsProcessing(false);
}
};
const handleDeleteDatabase = async () => {
if (!dbToDelete) return;
setIsProcessing(true);
try {
await SchemaService.dropDatabase(dbToDelete);
setNotification({
open: true,
title: 'Success',
message: `Database '${dbToDelete}' deleted successfully.`,
severity: 'success'
});
if (activeDatabase === dbToDelete) {
setActiveDatabase(null);
}
setOpenDeleteConfirm(false);
setDbToDelete('');
fetchDatabases();
} catch (error: any) {
setNotification({
open: true,
title: 'Error',
message: error.response?.data?.error || error.message,
severity: 'error'
});
} finally {
setIsProcessing(false);
}
};
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, db: string) => {
event.stopPropagation();
setMenuAnchorEl(event.currentTarget);
setSelectedDbForMenu(db);
};
const handleMenuClose = () => {
setMenuAnchorEl(null);
setSelectedDbForMenu('');
};
const handleMenuRename = () => {
setDbToRename(selectedDbForMenu);
setRenamedDbName(selectedDbForMenu);
setOpenRenameDialog(true);
handleMenuClose();
};
const handleMenuDelete = () => {
setDbToDelete(selectedDbForMenu);
setOpenDeleteConfirm(true);
handleMenuClose();
};
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', justifyContent: 'space-between' }}>
@@ -186,6 +283,9 @@ const Sidebar: React.FC = () => {
<ListItemText
primary={<Typography variant="body2" sx={{ fontWeight: activeDatabase === db ? 600 : 400, fontSize: '0.85rem' }}>{db}</Typography>}
/>
<IconButton size="small" onClick={(e) => handleMenuOpen(e, db)} sx={{ opacity: 0, '&:hover': { opacity: 1 }, '.MuiListItemButton-root:hover &': { opacity: 0.5 } }}>
<MoreVert fontSize="inherit" />
</IconButton>
</ListItemButton>
</ListItem>
@@ -290,24 +390,99 @@ const Sidebar: React.FC = () => {
variant="outlined"
value={newDbName}
onChange={(e) => setNewDbName(e.target.value)}
disabled={isCreatingDb}
disabled={isProcessing}
onKeyDown={(e) => {
if (e.key === 'Enter') handleCreateDatabase();
}}
/>
</DialogContent>
<DialogActions sx={{ p: 2, pt: 0 }}>
<Button onClick={() => setOpenNewDbDialog(false)} disabled={isCreatingDb}>Cancel</Button>
<Button onClick={() => setOpenNewDbDialog(false)} disabled={isProcessing}>Cancel</Button>
<Button
onClick={handleCreateDatabase}
variant="contained"
disabled={isCreatingDb || !newDbName.trim()}
disabled={isProcessing || !newDbName.trim()}
>
{isCreatingDb ? <CircularProgress size={24} /> : 'Create'}
{isProcessing ? <CircularProgress size={24} /> : 'Create'}
</Button>
</DialogActions>
</Dialog>
{/* Rename Database Dialog */}
<Dialog open={openRenameDialog} onClose={() => setOpenRenameDialog(false)} maxWidth="xs" fullWidth>
<DialogTitle sx={{ fontWeight: 800 }}>Rename Database</DialogTitle>
<DialogContent>
<Typography variant="body2" sx={{ mb: 2, color: 'text.secondary' }}>
Renaming database <strong>{dbToRename}</strong>.
This will create a new database and move all tables.
</Typography>
<TextField
autoFocus
margin="dense"
label="New Database Name"
type="text"
fullWidth
variant="outlined"
value={renamedDbName}
onChange={(e) => setRenamedDbName(e.target.value)}
disabled={isProcessing}
onKeyDown={(e) => {
if (e.key === 'Enter') handleRenameDatabase();
}}
/>
</DialogContent>
<DialogActions sx={{ p: 2, pt: 0 }}>
<Button onClick={() => setOpenRenameDialog(false)} disabled={isProcessing}>Cancel</Button>
<Button
onClick={handleRenameDatabase}
variant="contained"
disabled={isProcessing || !renamedDbName.trim() || renamedDbName === dbToRename}
>
{isProcessing ? <CircularProgress size={24} /> : 'Rename'}
</Button>
</DialogActions>
</Dialog>
{/* Delete Database Confirm */}
<Dialog open={openDeleteConfirm} onClose={() => setOpenDeleteConfirm(false)}>
<DialogTitle sx={{ fontWeight: 800, color: 'error.main' }}>Delete Database?</DialogTitle>
<DialogContent>
<Typography>
Are you sure you want to delete database <strong>{dbToDelete}</strong>?
This action cannot be undone and all data will be lost.
</Typography>
</DialogContent>
<DialogActions sx={{ p: 2, pt: 0 }}>
<Button onClick={() => setOpenDeleteConfirm(false)} disabled={isProcessing}>Cancel</Button>
<Button
onClick={handleDeleteDatabase}
variant="contained"
color="error"
disabled={isProcessing}
>
{isProcessing ? <CircularProgress size={24} color="inherit" /> : 'Delete'}
</Button>
</DialogActions>
</Dialog>
{/* Database Item Menu */}
<Menu
anchorEl={menuAnchorEl}
open={Boolean(menuAnchorEl)}
onClose={handleMenuClose}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
>
<MenuItem onClick={handleMenuRename}>
<ListItemIcon><Edit fontSize="small" /></ListItemIcon>
<ListItemText>Rename</ListItemText>
</MenuItem>
<MenuItem onClick={handleMenuDelete} sx={{ color: 'error.main' }}>
<ListItemIcon><Delete fontSize="small" sx={{ color: 'error.main' }} /></ListItemIcon>
<ListItemText>Delete</ListItemText>
</MenuItem>
</Menu>
{/* Local Notification */}
<Snackbar
open={notification.open}
+2
View File
@@ -25,6 +25,8 @@ 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 }),
dropDatabase: (db: string) => api.delete(`/schema/databases/${db}`, { params: { database: db } }),
renameDatabase: (db: string, newName: string) => api.post(`/schema/databases/${db}/rename`, { newName }, { 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 } }),
getTablesMetadata: (db: string) => api.get(`/schema/metadata/${db}/tables`, { params: { database: db } }),