feat: implement dynamic database connection service with MySQL driver and API schema endpoints
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Paper, TextField, Button, Typography, Stack, InputAdornment } from '@mui/material';
|
||||
import { Storage, Person, Lock, Dns } from '@mui/icons-material';
|
||||
import { useAppStore } from '../store/useAppStore';
|
||||
import { SchemaService } from '../services/api';
|
||||
|
||||
const Login: React.FC = () => {
|
||||
const setConnection = useAppStore((state) => state.setConnection);
|
||||
const [config, setConfig] = useState({
|
||||
host: '127.0.0.1',
|
||||
username: 'root',
|
||||
password: '',
|
||||
port: 3306,
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleConnect = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
// Temporarily set connection to test connectivity
|
||||
// In a real app, we'd have a specific /connect endpoint
|
||||
// Here we just try to fetch databases to verify
|
||||
const tempConnection = { ...config };
|
||||
setConnection(tempConnection);
|
||||
|
||||
await SchemaService.getDatabases();
|
||||
// If success, we are already "connected" in the store
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.error || 'Failed to connect to database');
|
||||
useAppStore.getState().clearConnection();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'linear-gradient(135deg, #0f172a 0%, #1e293b 100%)'
|
||||
}}>
|
||||
<Paper elevation={24} sx={{
|
||||
p: 4,
|
||||
width: '100%',
|
||||
maxWidth: 400,
|
||||
borderRadius: 4,
|
||||
background: 'rgba(255, 255, 255, 0.05)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
color: 'white'
|
||||
}}>
|
||||
<Box sx={{ textAlign: 'center', mb: 4 }}>
|
||||
<Typography variant="h4" sx={{ fontWeight: 800, color: '#0061ff', mb: 1 }}>
|
||||
MARIAVEL
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.6)' }}>
|
||||
Connect to your MySQL Database
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<form onSubmit={handleConnect}>
|
||||
<Stack spacing={3}>
|
||||
<TextField
|
||||
label="Host"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={config.host}
|
||||
onChange={(e) => setConfig({ ...config, host: e.target.value })}
|
||||
InputProps={{
|
||||
startAdornment: <InputAdornment position="start"><Dns sx={{ color: 'primary.main' }} /></InputAdornment>,
|
||||
}}
|
||||
sx={{ '& .MuiOutlinedInput-root': { color: 'white' } }}
|
||||
/>
|
||||
<TextField
|
||||
label="Port"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
type="number"
|
||||
value={config.port}
|
||||
onChange={(e) => setConfig({ ...config, port: parseInt(e.target.value) })}
|
||||
sx={{ '& .MuiOutlinedInput-root': { color: 'white' } }}
|
||||
/>
|
||||
<TextField
|
||||
label="Username"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={config.username}
|
||||
onChange={(e) => setConfig({ ...config, username: e.target.value })}
|
||||
InputProps={{
|
||||
startAdornment: <InputAdornment position="start"><Person sx={{ color: 'primary.main' }} /></InputAdornment>,
|
||||
}}
|
||||
sx={{ '& .MuiOutlinedInput-root': { color: 'white' } }}
|
||||
/>
|
||||
<TextField
|
||||
label="Password"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
type="password"
|
||||
value={config.password}
|
||||
onChange={(e) => setConfig({ ...config, password: e.target.value })}
|
||||
InputProps={{
|
||||
startAdornment: <InputAdornment position="start"><Lock sx={{ color: 'primary.main' }} /></InputAdornment>,
|
||||
}}
|
||||
sx={{ '& .MuiOutlinedInput-root': { color: 'white' } }}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<Typography variant="caption" color="error" sx={{ textAlign: 'center' }}>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
size="large"
|
||||
disabled={loading}
|
||||
sx={{
|
||||
py: 1.5,
|
||||
fontWeight: 700,
|
||||
background: 'linear-gradient(90deg, #0061ff 0%, #60a5fa 100%)',
|
||||
boxShadow: '0 8px 16px rgba(0, 97, 255, 0.3)'
|
||||
}}
|
||||
>
|
||||
{loading ? 'Connecting...' : 'Connect Now'}
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
@@ -1,59 +1,103 @@
|
||||
import React from 'react';
|
||||
import { Box, Paper, Typography } from '@mui/material';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Box, Paper, Typography, CircularProgress } from '@mui/material';
|
||||
import DataGrid, {
|
||||
Column,
|
||||
Editing,
|
||||
Scrolling,
|
||||
Paging,
|
||||
FilterRow,
|
||||
HeaderFilter
|
||||
HeaderFilter,
|
||||
SearchPanel,
|
||||
GroupPanel,
|
||||
Export
|
||||
} from 'devextreme-react/data-grid';
|
||||
import { useAppStore } from '../store/useAppStore';
|
||||
import { SchemaService } from '../services/api';
|
||||
|
||||
const MainContent: React.FC = () => {
|
||||
const { activeDatabase } = useAppStore();
|
||||
const { activeTable, activeDatabase } = useAppStore();
|
||||
const [data, setData] = useState<any[]>([]);
|
||||
const [columns, setColumns] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Mock data for initial UI
|
||||
const dummyData = [
|
||||
{ id: 1, name: 'Users', type: 'BASE TABLE', engine: 'InnoDB', rows: 1500, size: '256 KB' },
|
||||
{ id: 2, name: 'Products', type: 'BASE TABLE', engine: 'InnoDB', rows: 54200, size: '12 MB' },
|
||||
{ id: 3, name: 'Orders', type: 'BASE TABLE', engine: 'InnoDB', rows: 120400, size: '45 MB' },
|
||||
{ id: 4, name: 'Order_Items', type: 'BASE TABLE', engine: 'InnoDB', rows: 450000, size: '120 MB' },
|
||||
];
|
||||
useEffect(() => {
|
||||
const fetchTableData = async () => {
|
||||
if (!activeTable || !activeDatabase) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
// Fetch schema for columns
|
||||
const schemaRes = await SchemaService.getTableSchema(activeTable);
|
||||
const cols = schemaRes.data.map((col: any) => ({
|
||||
dataField: col.Field,
|
||||
caption: col.Field,
|
||||
dataType: mapSqlTypeToDxType(col.Type)
|
||||
}));
|
||||
setColumns(cols);
|
||||
|
||||
// Fetch actual data
|
||||
const dataRes = await SchemaService.getTableData(activeTable, activeDatabase);
|
||||
setData(dataRes.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch table data', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTableData();
|
||||
}, [activeTable, activeDatabase]);
|
||||
|
||||
// Helper to map SQL types to DevExtreme types
|
||||
const mapSqlTypeToDxType = (sqlType: string) => {
|
||||
sqlType = sqlType.toLowerCase();
|
||||
if (sqlType.includes('int') || sqlType.includes('decimal') || sqlType.includes('float')) return 'number';
|
||||
if (sqlType.includes('date') || sqlType.includes('time')) return 'date';
|
||||
if (sqlType.includes('bool')) return 'boolean';
|
||||
return 'string';
|
||||
};
|
||||
|
||||
if (!activeTable) {
|
||||
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>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ flexGrow: 1, p: 3, overflow: 'hidden', display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Box>
|
||||
<Box sx={{ flexGrow: 1, p: 3, bgcolor: 'background.default', overflow: 'hidden' }}>
|
||||
<Box sx={{ mb: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700 }}>
|
||||
{activeDatabase ? `Tables in ${activeDatabase}` : 'Welcome to Mariavel'}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Manage your database tables with high-performance tools.
|
||||
{activeDatabase}.{activeTable}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Paper sx={{ flexGrow: 1, p: 0, overflow: 'hidden', borderRadius: 3 }}>
|
||||
<DataGrid
|
||||
dataSource={dummyData}
|
||||
keyExpr="id"
|
||||
showBorders={false}
|
||||
focusedRowEnabled={true}
|
||||
height="100%"
|
||||
width="100%"
|
||||
>
|
||||
<Paging enabled={false} />
|
||||
<Scrolling mode="virtual" />
|
||||
<FilterRow visible={true} />
|
||||
<HeaderFilter visible={true} />
|
||||
<Editing mode="cell" allowUpdating={true} allowDeleting={true} />
|
||||
|
||||
<Column dataField="id" width={50} />
|
||||
<Column dataField="name" caption="Table Name" />
|
||||
<Column dataField="type" />
|
||||
<Column dataField="engine" width={100} />
|
||||
<Column dataField="rows" dataType="number" format="fixedPoint" width={100} />
|
||||
<Column dataField="size" width={100} />
|
||||
</DataGrid>
|
||||
|
||||
<Paper sx={{ height: 'calc(100vh - 180px)', borderRadius: 2, overflow: 'hidden' }}>
|
||||
{loading ? (
|
||||
<Box sx={{ display: 'flex', height: '100%', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : (
|
||||
<DataGrid
|
||||
dataSource={data}
|
||||
showBorders={true}
|
||||
focusedRowEnabled={true}
|
||||
columnAutoWidth={true}
|
||||
allowColumnReordering={true}
|
||||
rowAlternationEnabled={true}
|
||||
height="100%"
|
||||
>
|
||||
<Scrolling mode="virtual" />
|
||||
<FilterRow visible={true} />
|
||||
<HeaderFilter visible={true} />
|
||||
<SearchPanel visible={true} width={240} placeholder="Search..." />
|
||||
<GroupPanel visible={true} />
|
||||
<Export enabled={true} allowExportSelectedData={true} />
|
||||
|
||||
{columns.map(col => (
|
||||
<Column key={col.dataField} {...col} />
|
||||
))}
|
||||
</DataGrid>
|
||||
)}
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -1,45 +1,105 @@
|
||||
import React from 'react';
|
||||
import { Box, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Typography, Divider } from '@mui/material';
|
||||
import { Storage, TableChart, Folder } from '@mui/icons-material';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Box, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Typography, Divider, CircularProgress } from '@mui/material';
|
||||
import { Storage } from '@mui/icons-material';
|
||||
import { useAppStore } from '../store/useAppStore';
|
||||
import { SchemaService } from '../services/api';
|
||||
|
||||
const Sidebar: React.FC = () => {
|
||||
const { activeDatabase, setActiveDatabase } = useAppStore();
|
||||
const { activeDatabase, setActiveDatabase, setActiveTable, activeTable } = useAppStore();
|
||||
const [databases, setDatabases] = useState<string[]>([]);
|
||||
const [tables, setTables] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [tablesLoading, setTablesLoading] = useState(false);
|
||||
|
||||
// Mock data for initial UI
|
||||
const databases = ['information_schema', 'mysql', 'performance_schema', 'sys', 'mariavel_db'];
|
||||
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();
|
||||
}, []);
|
||||
|
||||
const handleDatabaseClick = async (db: string) => {
|
||||
if (activeDatabase === db) {
|
||||
setActiveDatabase(null);
|
||||
setTables([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveDatabase(db);
|
||||
setTablesLoading(true);
|
||||
try {
|
||||
const response = await SchemaService.getTables(db);
|
||||
setTables(response.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch tables', error);
|
||||
} finally {
|
||||
setTablesLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ width: 260, borderRight: 1, borderColor: 'divider', display: 'flex', flexDirection: 'column' }}>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="overline" sx={{ fontWeight: 700, color: 'text.secondary' }}>
|
||||
Databases
|
||||
</Typography>
|
||||
<Box sx={{ width: 260, borderRight: 1, borderColor: 'divider', display: 'flex', flexDirection: 'column', bgcolor: 'background.paper' }}>
|
||||
<Box sx={{ p: 2, display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Storage color="primary" />
|
||||
<Typography variant="h6" sx={{ fontWeight: 700, fontSize: '1.1rem' }}>Explorer</Typography>
|
||||
</Box>
|
||||
<List sx={{ flexGrow: 1, overflow: 'auto' }}>
|
||||
{databases.map((db) => (
|
||||
<ListItem key={db} disablePadding>
|
||||
<ListItemButton
|
||||
selected={activeDatabase === db}
|
||||
onClick={() => setActiveDatabase(db)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Storage color={activeDatabase === db ? 'primary' : 'inherit'} />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ fontWeight: activeDatabase === db ? 600 : 400 }}
|
||||
<Divider />
|
||||
|
||||
<Box sx={{ flexGrow: 1, overflowY: 'auto' }}>
|
||||
{loading ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}><CircularProgress size={24} /></Box>
|
||||
) : (
|
||||
<List dense sx={{ p: 0 }}>
|
||||
{databases.map((db) => (
|
||||
<React.Fragment key={db}>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton
|
||||
onClick={() => handleDatabaseClick(db)}
|
||||
selected={activeDatabase === db}
|
||||
sx={{ py: 1 }}
|
||||
>
|
||||
{db}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
<ListItemIcon sx={{ minWidth: 36 }}>
|
||||
<Storage fontSize="small" color={activeDatabase === db ? 'primary' : 'inherit'} />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={<Typography variant="body2" sx={{ fontWeight: activeDatabase === db ? 600 : 400 }}>{db}</Typography>}
|
||||
/>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
|
||||
{activeDatabase === db && (
|
||||
<List dense sx={{ pl: 4, bgcolor: 'rgba(0,0,0,0.02)' }}>
|
||||
{tablesLoading ? (
|
||||
<ListItem><CircularProgress size={16} /></ListItem>
|
||||
) : tables.length === 0 ? (
|
||||
<ListItem><Typography variant="caption" sx={{ color: 'text.secondary', fontStyle: 'italic' }}>No tables found</Typography></ListItem>
|
||||
) : tables.map((table) => (
|
||||
<ListItem key={table} disablePadding>
|
||||
<ListItemButton
|
||||
selected={activeTable === table}
|
||||
onClick={() => setActiveTable(table)}
|
||||
sx={{ py: 0.5 }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={<Typography variant="caption" sx={{ fontWeight: activeTable === table ? 600 : 400 }}>{table}</Typography>}
|
||||
/>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</Box>
|
||||
<Divider />
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
||||
|
||||
Reference in New Issue
Block a user