feat: implement database management API with MySQL driver and schema operations
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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 } }),
|
||||
|
||||
Reference in New Issue
Block a user