diff --git a/frontend/index.html b/frontend/index.html index 040fe1b..ba2e436 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,9 +5,6 @@ Mariavel - Premium Database Manager - - -
diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 56c0e78..cc0daac 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index f954aad..5132e2e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index 21f519d..5a8db64 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -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 - + {darkMode ? : } - + diff --git a/frontend/src/components/MainContent.tsx b/frontend/src/components/MainContent.tsx index 8d83001..f4bb3a1 100644 --- a/frontend/src/components/MainContent.tsx +++ b/frontend/src/components/MainContent.tsx @@ -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([]); + const [columns, setColumns] = useState([]); + const [rows, setRows] = useState([]); + const [rowCount, setRowCount] = useState(0); const [loadingSchema, setLoadingSchema] = useState(false); + const [loadingData, setLoadingData] = useState(false); + const [paginationModel, setPaginationModel] = useState({ + 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 ( - + {activeDatabase}.{activeTable} - + {loadingSchema ? ( ) : ( - - - - - - - - {columns.map(col => ( - - ))} - + 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', + } + }} + /> )} diff --git a/frontend/src/index.css b/frontend/src/index.css index 7d6b462..011755d 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -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 { diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 63bb10c..bef5202 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -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(