feat: initialize frontend project structure with Material UI and DataGrid implementation
This commit is contained in:
@@ -5,9 +5,6 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Generated
+98
@@ -13,6 +13,7 @@
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@mui/icons-material": "^9.0.0",
|
||||
"@mui/material": "^9.0.0",
|
||||
"@mui/x-data-grid": "^9.0.2",
|
||||
"axios": "^1.15.2",
|
||||
"devextreme": "^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": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
|
||||
@@ -3712,6 +3795,12 @@
|
||||
"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": {
|
||||
"version": "1.22.12",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
|
||||
@@ -4050,6 +4139,15 @@
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@mui/icons-material": "^9.0.0",
|
||||
"@mui/material": "^9.0.0",
|
||||
"@mui/x-data-grid": "^9.0.2",
|
||||
"axios": "^1.15.2",
|
||||
"devextreme": "^25.2.6",
|
||||
"devextreme-react": "^25.2.6",
|
||||
|
||||
@@ -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);
|
||||
|
||||
// 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';
|
||||
}
|
||||
}
|
||||
const [loadingData, setLoadingData] = useState(false);
|
||||
const [paginationModel, setPaginationModel] = useState<GridPaginationModel>({
|
||||
page: 0,
|
||||
pageSize: 100,
|
||||
});
|
||||
}, [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,15 +65,41 @@ 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 (
|
||||
<Box sx={{ flexGrow: 1, p: 3, display: 'flex', alignItems: 'center', justifyContent: 'center', bgcolor: 'background.default' }}>
|
||||
@@ -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>
|
||||
|
||||
+1
-73
@@ -13,15 +13,7 @@
|
||||
--text-dark: #f1f5f9;
|
||||
}
|
||||
|
||||
/* Suppress DevExtreme License Watermark */
|
||||
dx-license, [class*="dx-license"] {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
height: 0 !important;
|
||||
opacity: 0 !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
/* Global Styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -55,70 +47,6 @@ body.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 */
|
||||
::-webkit-scrollbar {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
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'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
|
||||
Reference in New Issue
Block a user