feat: initialize frontend project structure with Material UI and DataGrid implementation

This commit is contained in:
Ümit Tunç
2026-04-24 07:45:40 +03:00
parent 914fd0affd
commit f546a0e20b
7 changed files with 205 additions and 157 deletions
+102 -77
View File
@@ -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>