354 lines
12 KiB
TypeScript
354 lines
12 KiB
TypeScript
import React, { useEffect, useState, useCallback } from 'react';
|
|
import {
|
|
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 type { GridColDef, GridPaginationModel } from '@mui/x-data-grid';
|
|
import Editor from '@monaco-editor/react';
|
|
import { useAppStore } from '../store/useAppStore';
|
|
import { SchemaService } from '../services/api';
|
|
|
|
const MainContent: React.FC = () => {
|
|
const { activeTable, activeDatabase, darkMode } = useAppStore();
|
|
const [columns, setColumns] = useState<GridColDef[]>([]);
|
|
const [rows, setRows] = useState<any[]>([]);
|
|
const [rowCount, setRowCount] = useState(0);
|
|
const [loadingSchema, setLoadingSchema] = useState(false);
|
|
const [loadingData, setLoadingData] = useState(false);
|
|
const [sqlQuery, setSqlQuery] = useState('');
|
|
const [isCustomQuery, setIsCustomQuery] = useState(false);
|
|
const [paginationModel, setPaginationModel] = useState<GridPaginationModel>({
|
|
page: 0,
|
|
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
|
|
const mapSqlTypeToMuiType = (sqlType: string): 'string' | 'number' | 'date' | 'dateTime' | 'boolean' => {
|
|
sqlType = sqlType.toLowerCase();
|
|
if (sqlType.includes('int') || sqlType.includes('decimal') || sqlType.includes('float') || sqlType.includes('double')) return 'number';
|
|
if (sqlType.includes('datetime') || sqlType.includes('timestamp')) return 'dateTime';
|
|
if (sqlType.includes('date')) return 'date';
|
|
if (sqlType.includes('bool') || sqlType.includes('tinyint(1)')) return 'boolean';
|
|
return 'string';
|
|
};
|
|
|
|
// Set initial SQL query when table changes
|
|
useEffect(() => {
|
|
if (activeTable && activeDatabase) {
|
|
setSqlQuery(`SELECT * FROM ${activeDatabase}.${activeTable} LIMIT 1000;`);
|
|
setIsCustomQuery(false);
|
|
}
|
|
}, [activeTable, activeDatabase]);
|
|
|
|
// Fetch Schema
|
|
useEffect(() => {
|
|
const fetchSchema = async () => {
|
|
if (!activeTable || !activeDatabase || isCustomQuery) return;
|
|
|
|
setLoadingSchema(true);
|
|
try {
|
|
const schemaRes = await SchemaService.getTableSchema(activeTable, activeDatabase);
|
|
const cols: GridColDef[] = schemaRes.data.map((col: any) => {
|
|
const type = mapSqlTypeToMuiType(col.Type);
|
|
return {
|
|
field: col.Field,
|
|
headerName: col.Field,
|
|
type: type,
|
|
width: 200,
|
|
minWidth: 100,
|
|
valueGetter: (value: any) => {
|
|
if ((type === 'date' || type === 'dateTime') && value && typeof value === 'string') {
|
|
return new Date(value);
|
|
}
|
|
return value;
|
|
},
|
|
};
|
|
});
|
|
setColumns(cols);
|
|
setPaginationModel({ page: 0, pageSize: 100 });
|
|
} catch (error) {
|
|
console.error('Failed to fetch table schema', error);
|
|
} finally {
|
|
setLoadingSchema(false);
|
|
}
|
|
};
|
|
|
|
fetchSchema();
|
|
}, [activeTable, activeDatabase, isCustomQuery]);
|
|
|
|
// Fetch Data
|
|
const fetchData = useCallback(async () => {
|
|
if (!activeTable || !activeDatabase || isCustomQuery) return;
|
|
|
|
setLoadingData(true);
|
|
try {
|
|
const params = {
|
|
skip: paginationModel.page * paginationModel.pageSize,
|
|
take: paginationModel.pageSize,
|
|
requireTotalCount: true,
|
|
database: activeDatabase,
|
|
};
|
|
|
|
const response = await SchemaService.getTableData(activeTable, params);
|
|
|
|
const dataWithIds = response.data.data.map((row: any, index: number) => ({
|
|
id: row.id || row.ID || `row-${index}-${paginationModel.page}`,
|
|
...row,
|
|
}));
|
|
|
|
setRows(dataWithIds);
|
|
setRowCount(response.data.totalCount || 0);
|
|
} catch (error) {
|
|
console.error('Data loading error', error);
|
|
} finally {
|
|
setLoadingData(false);
|
|
}
|
|
}, [activeTable, activeDatabase, paginationModel, isCustomQuery]);
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, [fetchData]);
|
|
|
|
const handleExecute = async () => {
|
|
if (!sqlQuery) return;
|
|
setLoadingData(true);
|
|
setIsCustomQuery(true);
|
|
try {
|
|
const response = await SchemaService.executeQuery(sqlQuery);
|
|
const rawData = response.data.data;
|
|
|
|
if (rawData && rawData.length > 0) {
|
|
const fields = Object.keys(rawData[0]);
|
|
const newCols: GridColDef[] = fields.map(field => ({
|
|
field,
|
|
headerName: field,
|
|
width: 150,
|
|
flex: fields.length < 6 ? 1 : 0
|
|
}));
|
|
setColumns(newCols);
|
|
|
|
const dataWithIds = rawData.map((row: any, index: number) => ({
|
|
id: row.id || row.ID || `row-${index}`,
|
|
...row,
|
|
}));
|
|
setRows(dataWithIds);
|
|
setRowCount(response.data.count);
|
|
} else {
|
|
setRows([]);
|
|
setRowCount(0);
|
|
}
|
|
} catch (error: any) {
|
|
console.error('Execution error', error);
|
|
const msg = error.response?.data?.error || 'An unexpected error occurred during SQL execution.';
|
|
setErrorInfo({
|
|
open: true,
|
|
title: 'SQL Execution Error',
|
|
message: msg
|
|
});
|
|
} finally {
|
|
setLoadingData(false);
|
|
}
|
|
};
|
|
|
|
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, bgcolor: 'background.default', display: 'flex', flexDirection: 'column', width: '100%', minWidth: 0, overflow: 'hidden', gap: 2 }}>
|
|
{/* SQL Editor Section */}
|
|
<Paper elevation={0} sx={{
|
|
flexShrink: 0,
|
|
borderRadius: 3,
|
|
overflow: 'hidden',
|
|
border: 1,
|
|
borderColor: 'divider',
|
|
bgcolor: darkMode ? '#111827' : '#ffffff',
|
|
boxShadow: '0 4px 20px rgba(0,0,0,0.1)'
|
|
}}>
|
|
<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
|
|
}}
|
|
/>
|
|
</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 />
|
|
</Box>
|
|
) : (
|
|
<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',
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
</Paper>
|
|
</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>
|
|
);
|
|
};
|
|
|
|
export default MainContent;
|