feat: initialize Mariavel project with Laravel backend API and React dashboard using DevExtreme DataGrid
This commit is contained in:
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Services\DatabaseService;
|
||||||
|
use App\Services\Database\MySqlDriver;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Response;
|
||||||
|
|
||||||
|
class SchemaController extends Controller
|
||||||
|
{
|
||||||
|
protected DatabaseService $databaseService;
|
||||||
|
|
||||||
|
public function __construct(DatabaseService $databaseService)
|
||||||
|
{
|
||||||
|
$this->databaseService = $databaseService;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function initializeDriver(Request $request)
|
||||||
|
{
|
||||||
|
// In a real app, these would come from encrypted session or token
|
||||||
|
$config = $request->only(['host', 'username', 'password', 'database', 'port']);
|
||||||
|
$driver = new MySqlDriver();
|
||||||
|
|
||||||
|
if (!$driver->connect($config)) {
|
||||||
|
throw new \Exception("Could not connect to database.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->databaseService->setDriver($driver);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function databases(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->initializeDriver($request);
|
||||||
|
return Response::json($this->databaseService->getDatabases());
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return Response::json(['error' => $e->getMessage()], 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tables(Request $request, $database)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$request->merge(['database' => $database]);
|
||||||
|
$this->initializeDriver($request);
|
||||||
|
return Response::json($this->databaseService->getTables());
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return Response::json(['error' => $e->getMessage()], 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function schema(Request $request, $table)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->initializeDriver($request);
|
||||||
|
return Response::json($this->databaseService->getTableSchema($table));
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return Response::json(['error' => $e->getMessage()], 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,14 @@
|
|||||||
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use App\Http\Controllers\Api\SchemaController;
|
||||||
|
|
||||||
Route::get('/user', function (Request $request) {
|
Route::get('/user', function (Request $request) {
|
||||||
return $request->user();
|
return $request->user();
|
||||||
})->middleware('auth:sanctum');
|
})->middleware('auth:sanctum');
|
||||||
|
|
||||||
|
Route::prefix('schema')->group(function () {
|
||||||
|
Route::get('/databases', [SchemaController::class, 'databases']);
|
||||||
|
Route::get('/tables/{database}', [SchemaController::class, 'tables']);
|
||||||
|
Route::get('/{table}', [SchemaController::class, 'schema']);
|
||||||
|
});
|
||||||
|
|||||||
+31
-118
@@ -1,122 +1,35 @@
|
|||||||
import { useState } from 'react'
|
import React, { useMemo, useEffect } from 'react';
|
||||||
import reactLogo from './assets/react.svg'
|
import { ThemeProvider, CssBaseline, Box } from '@mui/material';
|
||||||
import viteLogo from './assets/vite.svg'
|
import { getTheme } from './theme/theme';
|
||||||
import heroImg from './assets/hero.png'
|
import { useAppStore } from './store/useAppStore';
|
||||||
import './App.css'
|
import Sidebar from './components/Sidebar';
|
||||||
|
import MainContent from './components/MainContent';
|
||||||
|
import Header from './components/Header';
|
||||||
|
|
||||||
function App() {
|
const App: React.FC = () => {
|
||||||
const [count, setCount] = useState(0)
|
const darkMode = useAppStore((state) => state.darkMode);
|
||||||
|
const theme = useMemo(() => getTheme(darkMode ? 'dark' : 'light'), [darkMode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (darkMode) {
|
||||||
|
document.body.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove('dark');
|
||||||
|
}
|
||||||
|
}, [darkMode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ThemeProvider theme={theme}>
|
||||||
<section id="center">
|
<CssBaseline />
|
||||||
<div className="hero">
|
<Box sx={{ display: 'flex', height: '100vh', overflow: 'hidden' }}>
|
||||||
<img src={heroImg} className="base" width="170" height="179" alt="" />
|
<Sidebar />
|
||||||
<img src={reactLogo} className="framework" alt="React logo" />
|
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column' }}>
|
||||||
<img src={viteLogo} className="vite" alt="Vite logo" />
|
<Header />
|
||||||
</div>
|
<MainContent />
|
||||||
<div>
|
</Box>
|
||||||
<h1>Get started</h1>
|
</Box>
|
||||||
<p>
|
</ThemeProvider>
|
||||||
Edit <code>src/App.tsx</code> and save to test <code>HMR</code>
|
);
|
||||||
</p>
|
};
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="counter"
|
|
||||||
onClick={() => setCount((count) => count + 1)}
|
|
||||||
>
|
|
||||||
Count is {count}
|
|
||||||
</button>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div className="ticks"></div>
|
export default App;
|
||||||
|
|
||||||
<section id="next-steps">
|
|
||||||
<div id="docs">
|
|
||||||
<svg className="icon" role="presentation" aria-hidden="true">
|
|
||||||
<use href="/icons.svg#documentation-icon"></use>
|
|
||||||
</svg>
|
|
||||||
<h2>Documentation</h2>
|
|
||||||
<p>Your questions, answered</p>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href="https://vite.dev/" target="_blank">
|
|
||||||
<img className="logo" src={viteLogo} alt="" />
|
|
||||||
Explore Vite
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://react.dev/" target="_blank">
|
|
||||||
<img className="button-icon" src={reactLogo} alt="" />
|
|
||||||
Learn more
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div id="social">
|
|
||||||
<svg className="icon" role="presentation" aria-hidden="true">
|
|
||||||
<use href="/icons.svg#social-icon"></use>
|
|
||||||
</svg>
|
|
||||||
<h2>Connect with us</h2>
|
|
||||||
<p>Join the Vite community</p>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href="https://github.com/vitejs/vite" target="_blank">
|
|
||||||
<svg
|
|
||||||
className="button-icon"
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<use href="/icons.svg#github-icon"></use>
|
|
||||||
</svg>
|
|
||||||
GitHub
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://chat.vite.dev/" target="_blank">
|
|
||||||
<svg
|
|
||||||
className="button-icon"
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<use href="/icons.svg#discord-icon"></use>
|
|
||||||
</svg>
|
|
||||||
Discord
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://x.com/vite_js" target="_blank">
|
|
||||||
<svg
|
|
||||||
className="button-icon"
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<use href="/icons.svg#x-icon"></use>
|
|
||||||
</svg>
|
|
||||||
X.com
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://bsky.app/profile/vite.dev" target="_blank">
|
|
||||||
<svg
|
|
||||||
className="button-icon"
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<use href="/icons.svg#bluesky-icon"></use>
|
|
||||||
</svg>
|
|
||||||
Bluesky
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div className="ticks"></div>
|
|
||||||
<section id="spacer"></section>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { AppBar, Toolbar, Typography, IconButton, Box } from '@mui/material';
|
||||||
|
import { Brightness4, Brightness7, Settings } from '@mui/icons-material';
|
||||||
|
import { useAppStore } from '../store/useAppStore';
|
||||||
|
|
||||||
|
const Header: React.FC = () => {
|
||||||
|
const { darkMode, toggleDarkMode } = useAppStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppBar position="static" color="transparent" elevation={0} sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||||
|
<Toolbar>
|
||||||
|
<Typography variant="h6" sx={{ flexGrow: 1, fontWeight: 700, color: 'primary.main' }}>
|
||||||
|
MARIAVEL
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<IconButton onClick={toggleDarkMode} color="inherit">
|
||||||
|
{darkMode ? <Brightness7 /> : <Brightness4 />}
|
||||||
|
</IconButton>
|
||||||
|
<IconButton color="inherit">
|
||||||
|
<Settings />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, Paper, Typography } from '@mui/material';
|
||||||
|
import DataGrid, {
|
||||||
|
Column,
|
||||||
|
Editing,
|
||||||
|
Scrolling,
|
||||||
|
Paging,
|
||||||
|
FilterRow,
|
||||||
|
HeaderFilter
|
||||||
|
} from 'devextreme-react/data-grid';
|
||||||
|
import { useAppStore } from '../store/useAppStore';
|
||||||
|
|
||||||
|
const MainContent: React.FC = () => {
|
||||||
|
const { activeDatabase } = useAppStore();
|
||||||
|
|
||||||
|
// Mock data for initial UI
|
||||||
|
const dummyData = [
|
||||||
|
{ id: 1, name: 'Users', type: 'BASE TABLE', engine: 'InnoDB', rows: 1500, size: '256 KB' },
|
||||||
|
{ id: 2, name: 'Products', type: 'BASE TABLE', engine: 'InnoDB', rows: 54200, size: '12 MB' },
|
||||||
|
{ id: 3, name: 'Orders', type: 'BASE TABLE', engine: 'InnoDB', rows: 120400, size: '45 MB' },
|
||||||
|
{ id: 4, name: 'Order_Items', type: 'BASE TABLE', engine: 'InnoDB', rows: 450000, size: '120 MB' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ flexGrow: 1, p: 3, overflow: 'hidden', display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h5" sx={{ fontWeight: 700 }}>
|
||||||
|
{activeDatabase ? `Tables in ${activeDatabase}` : 'Welcome to Mariavel'}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Manage your database tables with high-performance tools.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Paper sx={{ flexGrow: 1, p: 0, overflow: 'hidden', borderRadius: 3 }}>
|
||||||
|
<DataGrid
|
||||||
|
dataSource={dummyData}
|
||||||
|
keyExpr="id"
|
||||||
|
showBorders={false}
|
||||||
|
focusedRowEnabled={true}
|
||||||
|
height="100%"
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
|
<Paging enabled={false} />
|
||||||
|
<Scrolling mode="virtual" />
|
||||||
|
<FilterRow visible={true} />
|
||||||
|
<HeaderFilter visible={true} />
|
||||||
|
<Editing mode="cell" allowUpdating={true} allowDeleting={true} />
|
||||||
|
|
||||||
|
<Column dataField="id" width={50} />
|
||||||
|
<Column dataField="name" caption="Table Name" />
|
||||||
|
<Column dataField="type" />
|
||||||
|
<Column dataField="engine" width={100} />
|
||||||
|
<Column dataField="rows" dataType="number" format="fixedPoint" width={100} />
|
||||||
|
<Column dataField="size" width={100} />
|
||||||
|
</DataGrid>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MainContent;
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Typography, Divider } from '@mui/material';
|
||||||
|
import { Storage, TableChart, Folder } from '@mui/icons-material';
|
||||||
|
import { useAppStore } from '../store/useAppStore';
|
||||||
|
|
||||||
|
const Sidebar: React.FC = () => {
|
||||||
|
const { activeDatabase, setActiveDatabase } = useAppStore();
|
||||||
|
|
||||||
|
// Mock data for initial UI
|
||||||
|
const databases = ['information_schema', 'mysql', 'performance_schema', 'sys', 'mariavel_db'];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ width: 260, borderRight: 1, borderColor: 'divider', display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<Box sx={{ p: 2 }}>
|
||||||
|
<Typography variant="overline" sx={{ fontWeight: 700, color: 'text.secondary' }}>
|
||||||
|
Databases
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<List sx={{ flexGrow: 1, overflow: 'auto' }}>
|
||||||
|
{databases.map((db) => (
|
||||||
|
<ListItem key={db} disablePadding>
|
||||||
|
<ListItemButton
|
||||||
|
selected={activeDatabase === db}
|
||||||
|
onClick={() => setActiveDatabase(db)}
|
||||||
|
>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Storage color={activeDatabase === db ? 'primary' : 'inherit'} />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={db} primaryTypographyProps={{ variant: 'body2', fontWeight: activeDatabase === db ? 600 : 400 }} />
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
<Divider />
|
||||||
|
<Box sx={{ p: 2 }}>
|
||||||
|
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
||||||
|
Connected to: 127.0.0.1
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Sidebar;
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
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(
|
||||||
|
|||||||
Reference in New Issue
Block a user