feat: implement MainContent component with SQL editor and interactive data grid table viewer
This commit is contained in:
@@ -1,6 +1,18 @@
|
|||||||
import React, { useEffect, useState, useCallback } from 'react';
|
import React, { useEffect, useState, useCallback } from 'react';
|
||||||
import { Box, Paper, Typography, CircularProgress, Button, Divider, IconButton, Tooltip } from '@mui/material';
|
import {
|
||||||
import { PlayArrow, History, Save, CleaningServices } from '@mui/icons-material';
|
Box,
|
||||||
|
Paper,
|
||||||
|
Typography,
|
||||||
|
CircularProgress,
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
IconButton,
|
||||||
|
Tooltip,
|
||||||
|
Snackbar,
|
||||||
|
Alert,
|
||||||
|
AlertTitle
|
||||||
|
} from '@mui/material';
|
||||||
|
import { PlayArrow, History, Save, CleaningServices, Close } from '@mui/icons-material';
|
||||||
import { DataGrid } from '@mui/x-data-grid';
|
import { DataGrid } from '@mui/x-data-grid';
|
||||||
import type { GridColDef, GridPaginationModel } from '@mui/x-data-grid';
|
import type { GridColDef, GridPaginationModel } from '@mui/x-data-grid';
|
||||||
import Editor from '@monaco-editor/react';
|
import Editor from '@monaco-editor/react';
|
||||||
@@ -21,6 +33,15 @@ const MainContent: React.FC = () => {
|
|||||||
pageSize: 100,
|
pageSize: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Custom Alert State
|
||||||
|
const [errorInfo, setErrorInfo] = useState<{ open: boolean; message: string; title: string }>({
|
||||||
|
open: false,
|
||||||
|
message: '',
|
||||||
|
title: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCloseError = () => setErrorInfo({ ...errorInfo, open: false });
|
||||||
|
|
||||||
// Helper to map SQL types to MUI X Data Grid types
|
// Helper to map SQL types to MUI X Data Grid types
|
||||||
const mapSqlTypeToMuiType = (sqlType: string): 'string' | 'number' | 'date' | 'dateTime' | 'boolean' => {
|
const mapSqlTypeToMuiType = (sqlType: string): 'string' | 'number' | 'date' | 'dateTime' | 'boolean' => {
|
||||||
sqlType = sqlType.toLowerCase();
|
sqlType = sqlType.toLowerCase();
|
||||||
@@ -35,7 +56,7 @@ const MainContent: React.FC = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeTable && activeDatabase) {
|
if (activeTable && activeDatabase) {
|
||||||
setSqlQuery(`SELECT * FROM ${activeDatabase}.${activeTable} LIMIT 1000;`);
|
setSqlQuery(`SELECT * FROM ${activeDatabase}.${activeTable} LIMIT 1000;`);
|
||||||
setIsCustomQuery(false); // Reset to table mode when a new table is selected
|
setIsCustomQuery(false);
|
||||||
}
|
}
|
||||||
}, [activeTable, activeDatabase]);
|
}, [activeTable, activeDatabase]);
|
||||||
|
|
||||||
@@ -111,7 +132,7 @@ const MainContent: React.FC = () => {
|
|||||||
const handleExecute = async () => {
|
const handleExecute = async () => {
|
||||||
if (!sqlQuery) return;
|
if (!sqlQuery) return;
|
||||||
setLoadingData(true);
|
setLoadingData(true);
|
||||||
setIsCustomQuery(true); // Switch to custom query mode
|
setIsCustomQuery(true);
|
||||||
try {
|
try {
|
||||||
const response = await SchemaService.executeQuery(sqlQuery);
|
const response = await SchemaService.executeQuery(sqlQuery);
|
||||||
const rawData = response.data.data;
|
const rawData = response.data.data;
|
||||||
@@ -138,7 +159,12 @@ const MainContent: React.FC = () => {
|
|||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Execution error', error);
|
console.error('Execution error', error);
|
||||||
alert(error.response?.data?.error || 'Execution failed');
|
const msg = error.response?.data?.error || 'An unexpected error occurred during SQL execution.';
|
||||||
|
setErrorInfo({
|
||||||
|
open: true,
|
||||||
|
title: 'SQL Execution Error',
|
||||||
|
message: msg
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingData(false);
|
setLoadingData(false);
|
||||||
}
|
}
|
||||||
@@ -156,19 +182,19 @@ const MainContent: React.FC = () => {
|
|||||||
<Box sx={{ flexGrow: 1, p: 3, bgcolor: 'background.default', display: 'flex', flexDirection: 'column', width: '100%', minWidth: 0, overflow: 'hidden', gap: 2 }}>
|
<Box sx={{ flexGrow: 1, p: 3, bgcolor: 'background.default', display: 'flex', flexDirection: 'column', width: '100%', minWidth: 0, overflow: 'hidden', gap: 2 }}>
|
||||||
{/* SQL Editor Section */}
|
{/* SQL Editor Section */}
|
||||||
<Paper elevation={0} sx={{
|
<Paper elevation={0} sx={{
|
||||||
flexShrink: 0, // Prevent editor from collapsing
|
flexShrink: 0,
|
||||||
borderRadius: 2,
|
borderRadius: 3,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
border: 1,
|
border: 1,
|
||||||
borderColor: 'divider',
|
borderColor: 'divider',
|
||||||
bgcolor: darkMode ? 'rgba(30, 41, 59, 0.5)' : 'rgba(255, 255, 255, 0.5)',
|
bgcolor: darkMode ? '#111827' : '#ffffff',
|
||||||
backdropFilter: 'blur(8px)'
|
boxShadow: '0 4px 20px rgba(0,0,0,0.1)'
|
||||||
}}>
|
}}>
|
||||||
<Box sx={{ px: 2, py: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between', bgcolor: 'rgba(0,0,0,0.02)' }}>
|
<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 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, opacity: 0.7 }}>SQL EDITOR</Typography>
|
<Typography variant="subtitle2" sx={{ fontWeight: 800, opacity: 0.8, letterSpacing: 1 }}>SQL EDITOR</Typography>
|
||||||
<Divider orientation="vertical" flexItem sx={{ height: 16, my: 'auto' }} />
|
<Divider orientation="vertical" flexItem sx={{ height: 16, my: 'auto' }} />
|
||||||
<Typography variant="caption" sx={{ opacity: 0.5 }}>
|
<Typography variant="caption" sx={{ opacity: 0.5, fontWeight: 500 }}>
|
||||||
{isCustomQuery ? 'Custom Query' : `${activeDatabase}.${activeTable}`}
|
{isCustomQuery ? 'Custom Query' : `${activeDatabase}.${activeTable}`}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -188,47 +214,66 @@ const MainContent: React.FC = () => {
|
|||||||
onClick={handleExecute}
|
onClick={handleExecute}
|
||||||
disabled={loadingData}
|
disabled={loadingData}
|
||||||
startIcon={loadingData ? <CircularProgress size={16} color="inherit" /> : <PlayArrow />}
|
startIcon={loadingData ? <CircularProgress size={16} color="inherit" /> : <PlayArrow />}
|
||||||
sx={{ ml: 1, px: 2, borderRadius: 1.5, textTransform: 'none', fontWeight: 700 }}
|
sx={{ ml: 1, px: 3, borderRadius: 2, textTransform: 'none', fontWeight: 700, boxShadow: '0 4px 12px rgba(0, 97, 255, 0.3)' }}
|
||||||
>
|
>
|
||||||
Execute
|
Execute
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Editor
|
<Editor
|
||||||
height="200px"
|
height="220px"
|
||||||
defaultLanguage="sql"
|
defaultLanguage="sql"
|
||||||
theme={darkMode ? 'vs-dark' : 'light'}
|
theme={darkMode ? 'vs-dark' : 'light'}
|
||||||
value={sqlQuery}
|
value={sqlQuery}
|
||||||
onChange={(value) => setSqlQuery(value || '')}
|
onChange={(value) => setSqlQuery(value || '')}
|
||||||
options={{
|
options={{
|
||||||
minimap: { enabled: false },
|
minimap: { enabled: false },
|
||||||
fontSize: 13,
|
fontSize: 14,
|
||||||
fontFamily: "'Fira Code', 'Cascadia Code', Consolas, monospace",
|
fontFamily: "'Fira Code', 'Cascadia Code', Consolas, monospace",
|
||||||
lineNumbers: 'on',
|
lineNumbers: 'on',
|
||||||
scrollBeyondLastLine: false,
|
scrollBeyondLastLine: false,
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
padding: { top: 10, bottom: 10 }
|
padding: { top: 16, bottom: 16 },
|
||||||
|
cursorSmoothCaretAnimation: 'on',
|
||||||
|
smoothScrolling: true
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Data Section */}
|
{/* Data Section */}
|
||||||
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', minWidth: 0, overflow: 'hidden' }}>
|
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', minWidth: 0, overflow: 'hidden' }}>
|
||||||
<Box sx={{ mb: 1, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<Box sx={{ mb: 1.5, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
<Typography variant="h6" sx={{ fontWeight: 700 }}>
|
<Typography variant="h6" sx={{ fontWeight: 800 }}>
|
||||||
{isCustomQuery ? 'Query Results' : 'Table Results'}
|
{isCustomQuery ? 'Query Results' : 'Table Results'}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" sx={{ opacity: 0.5, mt: 0.5 }}>{rowCount} rows found</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>
|
||||||
{isCustomQuery && (
|
{isCustomQuery && (
|
||||||
<Button size="small" onClick={() => setIsCustomQuery(false)} sx={{ textTransform: 'none' }}>
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => setIsCustomQuery(false)}
|
||||||
|
sx={{ textTransform: 'none', borderRadius: 2 }}
|
||||||
|
>
|
||||||
Back to Table View
|
Back to Table View
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Paper sx={{ flexGrow: 1, borderRadius: 2, overflow: 'hidden', display: 'flex', flexDirection: 'column', width: '100%', border: 1, borderColor: 'divider' }}>
|
<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) ? (
|
{(loadingSchema && !isCustomQuery) ? (
|
||||||
<Box sx={{ display: 'flex', height: '100%', alignItems: 'center', justifyContent: 'center' }}>
|
<Box sx={{ display: 'flex', height: '100%', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
@@ -239,7 +284,7 @@ const MainContent: React.FC = () => {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
rowCount={rowCount}
|
rowCount={rowCount}
|
||||||
loading={loadingData}
|
loading={loadingData}
|
||||||
paginationMode={isCustomQuery ? 'client' : 'server'} // Client pagination for SQL results
|
paginationMode={isCustomQuery ? 'client' : 'server'}
|
||||||
paginationModel={paginationModel}
|
paginationModel={paginationModel}
|
||||||
onPaginationModelChange={setPaginationModel}
|
onPaginationModelChange={setPaginationModel}
|
||||||
pageSizeOptions={[25, 50, 100]}
|
pageSizeOptions={[25, 50, 100]}
|
||||||
@@ -255,6 +300,10 @@ const MainContent: React.FC = () => {
|
|||||||
'& .MuiDataGrid-row:hover': {
|
'& .MuiDataGrid-row:hover': {
|
||||||
bgcolor: (theme) => theme.palette.mode === 'light' ? 'rgba(0, 97, 255, 0.04)' : 'rgba(0, 97, 255, 0.08)',
|
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={{
|
slotProps={{
|
||||||
loadingOverlay: {
|
loadingOverlay: {
|
||||||
@@ -266,6 +315,37 @@ const MainContent: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<AlertTitle sx={{ fontWeight: 800 }}>{errorInfo.title}</AlertTitle>
|
||||||
|
<Typography variant="body2" sx={{ fontFamily: 'monospace', fontSize: '0.8rem' }}>
|
||||||
|
{errorInfo.message}
|
||||||
|
</Typography>
|
||||||
|
</Alert>
|
||||||
|
</Snackbar>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user