feat: initialize frontend project structure with Material UI and DataGrid implementation
This commit is contained in:
@@ -14,6 +14,7 @@ const Header: React.FC = () => {
|
||||
borderBottom: 1,
|
||||
borderColor: 'divider',
|
||||
bgcolor: darkMode ? 'rgba(16, 24, 48, 0.95)' : 'rgba(255, 255, 255, 0.95)',
|
||||
color: darkMode ? '#f1f5f9' : '#1e293b',
|
||||
backdropFilter: 'blur(10px)',
|
||||
zIndex: (theme) => theme.zIndex.drawer + 1
|
||||
}}
|
||||
@@ -23,10 +24,10 @@ const Header: React.FC = () => {
|
||||
MARIAVEL
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<IconButton onClick={toggleDarkMode} color="inherit">
|
||||
<IconButton onClick={toggleDarkMode} color="inherit" sx={{ opacity: 0.8, '&:hover': { opacity: 1 } }}>
|
||||
{darkMode ? <Brightness7 /> : <Brightness4 />}
|
||||
</IconButton>
|
||||
<IconButton color="inherit">
|
||||
<IconButton color="inherit" sx={{ opacity: 0.8, '&:hover': { opacity: 1 } }}>
|
||||
<Settings />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
@@ -1,53 +1,33 @@
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { Box, Paper, Typography, CircularProgress } from '@mui/material';
|
||||
import DataGrid, {
|
||||
Column,
|
||||
Scrolling,
|
||||
FilterRow,
|
||||
HeaderFilter,
|
||||
SearchPanel,
|
||||
GroupPanel,
|
||||
Export
|
||||
} from 'devextreme-react/data-grid';
|
||||
import { DataGrid } from '@mui/x-data-grid';
|
||||
import type { GridColDef, GridPaginationModel } from '@mui/x-data-grid';
|
||||
import { useAppStore } from '../store/useAppStore';
|
||||
import api, { SchemaService } from '../services/api';
|
||||
|
||||
import CustomStore from 'devextreme/data/custom_store';
|
||||
import { SchemaService } from '../services/api';
|
||||
|
||||
const MainContent: React.FC = () => {
|
||||
const { activeTable, activeDatabase } = useAppStore();
|
||||
const [columns, setColumns] = useState<any[]>([]);
|
||||
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 [paginationModel, setPaginationModel] = useState<GridPaginationModel>({
|
||||
page: 0,
|
||||
pageSize: 100,
|
||||
});
|
||||
|
||||
// Define data source with CustomStore for Remote Operations
|
||||
const dataSource = useMemo(() => {
|
||||
if (!activeTable || !activeDatabase) return null;
|
||||
|
||||
return new CustomStore({
|
||||
key: 'id', // Ideally this should be the primary key from schema
|
||||
load: async (loadOptions: any) => {
|
||||
try {
|
||||
const params = {
|
||||
skip: loadOptions.skip || 0,
|
||||
take: loadOptions.take || 100,
|
||||
requireTotalCount: loadOptions.requireTotalCount,
|
||||
database: activeDatabase,
|
||||
};
|
||||
|
||||
const response = await SchemaService.getTableData(activeTable, params);
|
||||
|
||||
return {
|
||||
data: response.data.data,
|
||||
totalCount: response.data.totalCount,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Data loading error', error);
|
||||
throw 'Data Loading Error';
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [activeTable, activeDatabase]);
|
||||
// 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';
|
||||
};
|
||||
|
||||
// Fetch Schema
|
||||
useEffect(() => {
|
||||
const fetchSchema = async () => {
|
||||
if (!activeTable || !activeDatabase) return;
|
||||
@@ -55,12 +35,26 @@ const MainContent: React.FC = () => {
|
||||
setLoadingSchema(true);
|
||||
try {
|
||||
const schemaRes = await SchemaService.getTableSchema(activeTable, activeDatabase);
|
||||
const cols = schemaRes.data.map((col: any) => ({
|
||||
dataField: col.Field,
|
||||
caption: col.Field,
|
||||
dataType: mapSqlTypeToDxType(col.Type)
|
||||
}));
|
||||
const cols: GridColDef[] = schemaRes.data.map((col: any) => {
|
||||
const type = mapSqlTypeToMuiType(col.Type);
|
||||
return {
|
||||
field: col.Field,
|
||||
headerName: col.Field,
|
||||
type: type,
|
||||
flex: 1,
|
||||
minWidth: 150,
|
||||
valueGetter: (value: any) => {
|
||||
if ((type === 'date' || type === 'dateTime') && value && typeof value === 'string') {
|
||||
return new Date(value);
|
||||
}
|
||||
return value;
|
||||
},
|
||||
};
|
||||
});
|
||||
setColumns(cols);
|
||||
|
||||
// Reset pagination when table changes
|
||||
setPaginationModel({ page: 0, pageSize: 100 });
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch table schema', error);
|
||||
} finally {
|
||||
@@ -71,14 +65,40 @@ const MainContent: React.FC = () => {
|
||||
fetchSchema();
|
||||
}, [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';
|
||||
};
|
||||
// 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);
|
||||
|
||||
// MUI X Data Grid needs a unique id for each row
|
||||
// If the table doesn't have an 'id' column, we might need to generate one
|
||||
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]);
|
||||
|
||||
if (!activeTable) {
|
||||
return (
|
||||
@@ -89,40 +109,45 @@ const MainContent: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ flexGrow: 1, p: 3, bgcolor: 'background.default', overflow: 'hidden' }}>
|
||||
<Box sx={{ flexGrow: 1, p: 3, bgcolor: 'background.default', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
|
||||
<Box sx={{ mb: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700 }}>
|
||||
{activeDatabase}.{activeTable}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Paper sx={{ height: 'calc(100vh - 180px)', borderRadius: 2, overflow: 'hidden' }}>
|
||||
<Paper sx={{ flexGrow: 1, borderRadius: 2, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
|
||||
{loadingSchema ? (
|
||||
<Box sx={{ display: 'flex', height: '100%', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : (
|
||||
<DataGrid
|
||||
dataSource={dataSource}
|
||||
remoteOperations={true}
|
||||
showBorders={true}
|
||||
focusedRowEnabled={true}
|
||||
columnAutoWidth={true}
|
||||
allowColumnReordering={true}
|
||||
rowAlternationEnabled={true}
|
||||
height="100%"
|
||||
>
|
||||
<Scrolling mode="virtual" rowRenderingMode="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>
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
rowCount={rowCount}
|
||||
loading={loadingData}
|
||||
paginationMode="server"
|
||||
paginationModel={paginationModel}
|
||||
onPaginationModelChange={setPaginationModel}
|
||||
pageSizeOptions={[25, 50, 100]}
|
||||
sx={{
|
||||
border: 'none',
|
||||
'& .MuiDataGrid-cell:focus': {
|
||||
outline: 'none',
|
||||
},
|
||||
'& .MuiDataGrid-columnHeader:focus': {
|
||||
outline: 'none',
|
||||
},
|
||||
height: '100%',
|
||||
}}
|
||||
slotProps={{
|
||||
loadingOverlay: {
|
||||
variant: 'linear-progress',
|
||||
noRowsVariant: 'linear-progress',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user