feat: implement MySQL driver and API schema services for database management
This commit is contained in:
@@ -10,17 +10,31 @@ import {
|
||||
Tooltip,
|
||||
Snackbar,
|
||||
Alert,
|
||||
AlertTitle
|
||||
AlertTitle,
|
||||
Tabs,
|
||||
Tab
|
||||
} 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 type { GridColDef, GridPaginationModel } from '@mui/x-data-grid';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import { useAppStore } from '../store/useAppStore';
|
||||
import { SchemaService } from '../services/api';
|
||||
import TransferContent from './TransferContent';
|
||||
|
||||
const MainContent: React.FC = () => {
|
||||
const { activeTable, activeDatabase, darkMode } = useAppStore();
|
||||
const { activeTable, activeDatabase, darkMode, dbTab, setDbTab } = useAppStore();
|
||||
const [columns, setColumns] = useState<GridColDef[]>([]);
|
||||
const [rows, setRows] = useState<any[]>([]);
|
||||
const [rowCount, setRowCount] = useState(0);
|
||||
@@ -170,180 +184,228 @@ const MainContent: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
if (!activeTable) {
|
||||
if (!activeDatabase) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<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={{
|
||||
flexShrink: 0,
|
||||
flexShrink: 0,
|
||||
borderRadius: 3,
|
||||
overflow: 'hidden',
|
||||
border: 1,
|
||||
borderColor: 'divider',
|
||||
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)' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 800, opacity: 0.8, letterSpacing: 1 }}>SQL EDITOR</Typography>
|
||||
<Divider orientation="vertical" flexItem sx={{ height: 16, my: 'auto' }} />
|
||||
<Typography variant="caption" sx={{ opacity: 0.5, fontWeight: 500 }}>
|
||||
{isCustomQuery ? 'Custom Query' : `${activeDatabase}.${activeTable}`}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Tooltip title="Clear Editor">
|
||||
<IconButton size="small" onClick={() => setSqlQuery('')}><CleaningServices fontSize="small" /></IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Save Query">
|
||||
<IconButton size="small"><Save fontSize="small" /></IconButton>
|
||||
</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
|
||||
<Tabs
|
||||
value={dbTab}
|
||||
onChange={handleTabChange}
|
||||
sx={{
|
||||
px: 2,
|
||||
minHeight: 56,
|
||||
'& .MuiTabs-indicator': { height: 3, borderRadius: '3px 3px 0 0' },
|
||||
'& .MuiTab-root': {
|
||||
textTransform: 'none',
|
||||
fontWeight: 700,
|
||||
fontSize: '0.9rem',
|
||||
minHeight: 56,
|
||||
minWidth: 120,
|
||||
gap: 1
|
||||
}
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<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>
|
||||
|
||||
{/* Data Section */}
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', minWidth: 0, overflow: 'hidden' }}>
|
||||
<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 }}>
|
||||
{isCustomQuery ? 'Query Results' : 'Table Results'}
|
||||
</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 />
|
||||
{/* Tables View */}
|
||||
{dbTab === 'tables' && (
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', minWidth: 0, overflow: 'hidden' }}>
|
||||
{!activeTable ? (
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 2, opacity: 0.5 }}>
|
||||
<TableChart sx={{ fontSize: 64 }} />
|
||||
<Typography variant="h6">Select a table from the explorer to view data</Typography>
|
||||
</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
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
rowCount={rowCount}
|
||||
loading={loadingData}
|
||||
paginationMode={isCustomQuery ? 'client' : 'server'}
|
||||
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',
|
||||
}
|
||||
}}
|
||||
sx={{ border: 'none' }}
|
||||
/>
|
||||
)}
|
||||
</Paper>
|
||||
</Box>
|
||||
</Paper>
|
||||
</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) */}
|
||||
<Snackbar
|
||||
open={errorInfo.open}
|
||||
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>
|
||||
}
|
||||
>
|
||||
<Snackbar open={errorInfo.open} autoHideDuration={10000} onClose={handleCloseError} anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}>
|
||||
<Alert onClose={handleCloseError} severity="error" variant="filled" sx={{ width: '100%', borderRadius: 2 }}>
|
||||
<AlertTitle sx={{ fontWeight: 800 }}>{errorInfo.title}</AlertTitle>
|
||||
<Typography variant="body2" sx={{ fontFamily: 'monospace', fontSize: '0.8rem' }}>
|
||||
{errorInfo.message}
|
||||
</Typography>
|
||||
<Typography variant="body2">{errorInfo.message}</Typography>
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user