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
-3
View File
@@ -5,9 +5,6 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mariavel - Premium Database Manager</title> <title>Mariavel - Premium Database Manager</title>
<!-- DevExtreme CDN -->
<link rel="stylesheet" href="https://cdn3.devexpress.com/jslib/23.1.5/css/dx.light.css">
<script type="text/javascript" src="https://cdn3.devexpress.com/jslib/23.1.5/js/dx.all.js"></script>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+98
View File
@@ -13,6 +13,7 @@
"@monaco-editor/react": "^4.7.0", "@monaco-editor/react": "^4.7.0",
"@mui/icons-material": "^9.0.0", "@mui/icons-material": "^9.0.0",
"@mui/material": "^9.0.0", "@mui/material": "^9.0.0",
"@mui/x-data-grid": "^9.0.2",
"axios": "^1.15.2", "axios": "^1.15.2",
"devextreme": "^25.2.6", "devextreme": "^25.2.6",
"devextreme-react": "^25.2.6", "devextreme-react": "^25.2.6",
@@ -990,6 +991,88 @@
} }
} }
}, },
"node_modules/@mui/x-data-grid": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-9.0.2.tgz",
"integrity": "sha512-9hkBS73x3G5MniOpkCh54iH5iwBr55obchF5IS1eybURZEgPxSXFizNMrDbyM2EGaYG9DQ87MvC5IoV7g0F2Vw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.6",
"@mui/utils": "9.0.0",
"@mui/x-internals": "^9.0.0",
"@mui/x-virtualizer": "9.0.0-alpha.0",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"use-sync-external-store": "^1.6.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1",
"@mui/material": "^7.3.0 || ^9.0.0",
"@mui/system": "^7.3.0 || ^9.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
}
}
},
"node_modules/@mui/x-internals": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-9.0.0.tgz",
"integrity": "sha512-E/4rdg69JjhyybpPGypCjAKSKLLnSdCFM+O6P/nkUg47+qt3uftxQEhjQO53rcn6ahHl6du/uNZ9BLgeY6kYxQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.6",
"@mui/utils": "9.0.0",
"reselect": "^5.1.1",
"use-sync-external-store": "^1.6.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@mui/x-virtualizer": {
"version": "9.0.0-alpha.0",
"resolved": "https://registry.npmjs.org/@mui/x-virtualizer/-/x-virtualizer-9.0.0-alpha.0.tgz",
"integrity": "sha512-K52TKCuWlkMEWOeB2nPfhIAHaWsYEb9h1ME9Wb+gmw4FloMA03VvKsrqvn8o6l8hYUi4/5F8NfYOIfPwqW3EhA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.6",
"@mui/utils": "9.0.0",
"@mui/x-internals": "^9.0.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@napi-rs/wasm-runtime": { "node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
@@ -3712,6 +3795,12 @@
"util-deprecate": "~1.0.1" "util-deprecate": "~1.0.1"
} }
}, },
"node_modules/reselect": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
"license": "MIT"
},
"node_modules/resolve": { "node_modules/resolve": {
"version": "1.22.12", "version": "1.22.12",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
@@ -4050,6 +4139,15 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+1
View File
@@ -15,6 +15,7 @@
"@monaco-editor/react": "^4.7.0", "@monaco-editor/react": "^4.7.0",
"@mui/icons-material": "^9.0.0", "@mui/icons-material": "^9.0.0",
"@mui/material": "^9.0.0", "@mui/material": "^9.0.0",
"@mui/x-data-grid": "^9.0.2",
"axios": "^1.15.2", "axios": "^1.15.2",
"devextreme": "^25.2.6", "devextreme": "^25.2.6",
"devextreme-react": "^25.2.6", "devextreme-react": "^25.2.6",
+3 -2
View File
@@ -14,6 +14,7 @@ const Header: React.FC = () => {
borderBottom: 1, borderBottom: 1,
borderColor: 'divider', borderColor: 'divider',
bgcolor: darkMode ? 'rgba(16, 24, 48, 0.95)' : 'rgba(255, 255, 255, 0.95)', bgcolor: darkMode ? 'rgba(16, 24, 48, 0.95)' : 'rgba(255, 255, 255, 0.95)',
color: darkMode ? '#f1f5f9' : '#1e293b',
backdropFilter: 'blur(10px)', backdropFilter: 'blur(10px)',
zIndex: (theme) => theme.zIndex.drawer + 1 zIndex: (theme) => theme.zIndex.drawer + 1
}} }}
@@ -23,10 +24,10 @@ const Header: React.FC = () => {
MARIAVEL MARIAVEL
</Typography> </Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <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 />} {darkMode ? <Brightness7 /> : <Brightness4 />}
</IconButton> </IconButton>
<IconButton color="inherit"> <IconButton color="inherit" sx={{ opacity: 0.8, '&:hover': { opacity: 1 } }}>
<Settings /> <Settings />
</IconButton> </IconButton>
</Box> </Box>
+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 { Box, Paper, Typography, CircularProgress } from '@mui/material';
import DataGrid, { import { DataGrid } from '@mui/x-data-grid';
Column, import type { GridColDef, GridPaginationModel } from '@mui/x-data-grid';
Scrolling,
FilterRow,
HeaderFilter,
SearchPanel,
GroupPanel,
Export
} from 'devextreme-react/data-grid';
import { useAppStore } from '../store/useAppStore'; import { useAppStore } from '../store/useAppStore';
import api, { SchemaService } from '../services/api'; import { SchemaService } from '../services/api';
import CustomStore from 'devextreme/data/custom_store';
const MainContent: React.FC = () => { const MainContent: React.FC = () => {
const { activeTable, activeDatabase } = useAppStore(); 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 [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 // Helper to map SQL types to MUI X Data Grid types
const dataSource = useMemo(() => { const mapSqlTypeToMuiType = (sqlType: string): 'string' | 'number' | 'date' | 'dateTime' | 'boolean' => {
if (!activeTable || !activeDatabase) return null; sqlType = sqlType.toLowerCase();
if (sqlType.includes('int') || sqlType.includes('decimal') || sqlType.includes('float') || sqlType.includes('double')) return 'number';
return new CustomStore({ if (sqlType.includes('datetime') || sqlType.includes('timestamp')) return 'dateTime';
key: 'id', // Ideally this should be the primary key from schema if (sqlType.includes('date')) return 'date';
load: async (loadOptions: any) => { if (sqlType.includes('bool') || sqlType.includes('tinyint(1)')) return 'boolean';
try { return 'string';
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]);
// Fetch Schema
useEffect(() => { useEffect(() => {
const fetchSchema = async () => { const fetchSchema = async () => {
if (!activeTable || !activeDatabase) return; if (!activeTable || !activeDatabase) return;
@@ -55,12 +35,26 @@ const MainContent: React.FC = () => {
setLoadingSchema(true); setLoadingSchema(true);
try { try {
const schemaRes = await SchemaService.getTableSchema(activeTable, activeDatabase); const schemaRes = await SchemaService.getTableSchema(activeTable, activeDatabase);
const cols = schemaRes.data.map((col: any) => ({ const cols: GridColDef[] = schemaRes.data.map((col: any) => {
dataField: col.Field, const type = mapSqlTypeToMuiType(col.Type);
caption: col.Field, return {
dataType: mapSqlTypeToDxType(col.Type) 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); setColumns(cols);
// Reset pagination when table changes
setPaginationModel({ page: 0, pageSize: 100 });
} catch (error) { } catch (error) {
console.error('Failed to fetch table schema', error); console.error('Failed to fetch table schema', error);
} finally { } finally {
@@ -71,14 +65,40 @@ const MainContent: React.FC = () => {
fetchSchema(); fetchSchema();
}, [activeTable, activeDatabase]); }, [activeTable, activeDatabase]);
// Helper to map SQL types to DevExtreme types // Fetch Data
const mapSqlTypeToDxType = (sqlType: string) => { const fetchData = useCallback(async () => {
sqlType = sqlType.toLowerCase(); if (!activeTable || !activeDatabase) return;
if (sqlType.includes('int') || sqlType.includes('decimal') || sqlType.includes('float')) return 'number';
if (sqlType.includes('date') || sqlType.includes('time')) return 'date'; setLoadingData(true);
if (sqlType.includes('bool')) return 'boolean'; try {
return 'string'; 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) { if (!activeTable) {
return ( return (
@@ -89,40 +109,45 @@ const MainContent: React.FC = () => {
} }
return ( 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' }}> <Box sx={{ mb: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="h5" sx={{ fontWeight: 700 }}> <Typography variant="h5" sx={{ fontWeight: 700 }}>
{activeDatabase}.{activeTable} {activeDatabase}.{activeTable}
</Typography> </Typography>
</Box> </Box>
<Paper sx={{ height: 'calc(100vh - 180px)', borderRadius: 2, overflow: 'hidden' }}> <Paper sx={{ flexGrow: 1, borderRadius: 2, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
{loadingSchema ? ( {loadingSchema ? (
<Box sx={{ display: 'flex', height: '100%', alignItems: 'center', justifyContent: 'center' }}> <Box sx={{ display: 'flex', height: '100%', alignItems: 'center', justifyContent: 'center' }}>
<CircularProgress /> <CircularProgress />
</Box> </Box>
) : ( ) : (
<DataGrid <DataGrid
dataSource={dataSource} rows={rows}
remoteOperations={true} columns={columns}
showBorders={true} rowCount={rowCount}
focusedRowEnabled={true} loading={loadingData}
columnAutoWidth={true} paginationMode="server"
allowColumnReordering={true} paginationModel={paginationModel}
rowAlternationEnabled={true} onPaginationModelChange={setPaginationModel}
height="100%" pageSizeOptions={[25, 50, 100]}
> sx={{
<Scrolling mode="virtual" rowRenderingMode="virtual" /> border: 'none',
<FilterRow visible={true} /> '& .MuiDataGrid-cell:focus': {
<HeaderFilter visible={true} /> outline: 'none',
<SearchPanel visible={true} width={240} placeholder="Search..." /> },
<GroupPanel visible={true} /> '& .MuiDataGrid-columnHeader:focus': {
<Export enabled={true} allowExportSelectedData={true} /> outline: 'none',
},
{columns.map(col => ( height: '100%',
<Column key={col.dataField} {...col} /> }}
))} slotProps={{
</DataGrid> loadingOverlay: {
variant: 'linear-progress',
noRowsVariant: 'linear-progress',
}
}}
/>
)} )}
</Paper> </Paper>
</Box> </Box>
+1 -73
View File
@@ -13,15 +13,7 @@
--text-dark: #f1f5f9; --text-dark: #f1f5f9;
} }
/* Suppress DevExtreme License Watermark */ /* Global Styles */
dx-license, [class*="dx-license"] {
display: none !important;
visibility: hidden !important;
height: 0 !important;
opacity: 0 !important;
pointer-events: none !important;
}
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
@@ -55,70 +47,6 @@ body.dark {
border: 1px solid var(--glass-border-dark); border: 1px solid var(--glass-border-dark);
} }
/* DevExtreme Custom Styling - Modern Dark Theme Integration */
.dx-datagrid {
background-color: transparent !important;
border-radius: 12px !important;
overflow: hidden !important;
border: none !important;
font-family: inherit !important;
}
.dark .dx-datagrid {
color: var(--text-dark) !important;
}
.dx-datagrid-headers {
background-color: rgba(255, 255, 255, 0.05) !important;
border-bottom: 1px solid var(--glass-border) !important;
color: inherit !important;
}
.dark .dx-datagrid-headers {
background-color: rgba(0, 0, 0, 0.2) !important;
border-bottom: 1px solid var(--glass-border-dark) !important;
}
.dx-datagrid-rowsview .dx-row {
background-color: transparent !important;
color: inherit !important;
}
.dark .dx-datagrid-rowsview .dx-row {
border-bottom: 1px solid rgba(255, 255, 255, 0.05) !important;
}
.dx-datagrid-content .dx-datagrid-table .dx-row > td {
padding: 12px 16px !important;
}
/* Fix for Search and Group Panels in Dark Mode */
.dark .dx-datagrid-search-panel,
.dark .dx-datagrid-group-panel,
.dark .dx-datagrid-filter-row {
background-color: rgba(0, 0, 0, 0.2) !important;
color: var(--text-dark) !important;
}
.dark .dx-textbox-input {
color: var(--text-dark) !important;
background-color: rgba(255, 255, 255, 0.05) !important;
}
/* Column Header text */
.dark .dx-datagrid-text-content {
color: rgba(255, 255, 255, 0.7) !important;
font-weight: 600 !important;
}
/* Selection and Hover */
.dark .dx-data-row.dx-state-hover:not(.dx-selection):not(.dx-row-inserted):not(.dx-row-removed):not(.dx-edit-row) > td {
background-color: rgba(255, 255, 255, 0.05) !important;
}
.dark .dx-selection.dx-row > td {
background-color: rgba(0, 97, 255, 0.2) !important;
}
/* Scrollbar Styling */ /* Scrollbar Styling */
::-webkit-scrollbar { ::-webkit-scrollbar {
-2
View File
@@ -1,8 +1,6 @@
import { StrictMode } from 'react' import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import './index.css' import './index.css'
import 'devextreme/dist/css/dx.common.css';
import 'devextreme/dist/css/dx.material.blue.dark.compact.css';
import App from './App.tsx' import App from './App.tsx'
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(