Files
mariavel/frontend/src/components/MainContent.tsx
T

263 lines
9.9 KiB
TypeScript

import React, { useEffect, useState, useCallback } from 'react';
import { Box, Paper, Typography, CircularProgress, Button, Divider, IconButton, Tooltip } from '@mui/material';
import { PlayArrow, History, Save, CleaningServices } 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 [paginationModel, setPaginationModel] = useState<GridPaginationModel>({
page: 0,
pageSize: 100,
});
// 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;`);
}
}, [activeTable, activeDatabase]);
// Fetch Schema
useEffect(() => {
const fetchSchema = async () => {
if (!activeTable || !activeDatabase) 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]);
// Fetch Data
const fetchData = useCallback(async () => {
if (!activeTable || !activeDatabase) 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]);
useEffect(() => {
fetchData();
}, [fetchData]);
const handleExecute = async () => {
if (!sqlQuery) return;
setLoadingData(true);
try {
const response = await SchemaService.executeQuery(sqlQuery);
const rawData = response.data.data;
if (rawData && rawData.length > 0) {
// Generate dynamic columns from the result set
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);
// In a real app, we'd use a nicer toast or error panel
alert(error.response?.data?.error || 'Execution failed');
} 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={{
borderRadius: 2,
overflow: 'hidden',
border: 1,
borderColor: 'divider',
bgcolor: darkMode ? 'rgba(30, 41, 59, 0.5)' : 'rgba(255, 255, 255, 0.5)',
backdropFilter: 'blur(8px)'
}}>
<Box sx={{ px: 2, py: 1, 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: 700, opacity: 0.7 }}>SQL EDITOR</Typography>
<Divider orientation="vertical" flexItem sx={{ height: 16, my: 'auto' }} />
<Typography variant="caption" sx={{ opacity: 0.5 }}>{activeDatabase}.sql</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: 2, borderRadius: 1.5, textTransform: 'none', fontWeight: 700 }}
>
Execute
</Button>
</Box>
</Box>
<Editor
height="200px"
defaultLanguage="sql"
theme={darkMode ? 'vs-dark' : 'light'}
value={sqlQuery}
onChange={(value) => setSqlQuery(value || '')}
options={{
minimap: { enabled: false },
fontSize: 13,
fontFamily: "'Fira Code', 'Cascadia Code', Consolas, monospace",
lineNumbers: 'on',
scrollBeyondLastLine: false,
automaticLayout: true,
padding: { top: 10, bottom: 10 }
}}
/>
</Paper>
{/* Data Section */}
<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={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="h6" sx={{ fontWeight: 700 }}>Results</Typography>
<Typography variant="caption" sx={{ opacity: 0.5, mt: 0.5 }}>{rowCount} rows found</Typography>
</Box>
</Box>
<Paper sx={{ flexGrow: 1, borderRadius: 2, overflow: 'hidden', display: 'flex', flexDirection: 'column', width: '100%', border: 1, borderColor: 'divider' }}>
{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',
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)',
},
}}
slotProps={{
loadingOverlay: {
variant: 'linear-progress',
noRowsVariant: 'linear-progress',
}
}}
/>
)}
</Paper>
</Box>
</Box>
);
};
export default MainContent;