mirror of
https://github.com/crivion/laranode.git
synced 2026-09-03 06:24:09 +08:00
edit db name, charset and collation moved to modal
This commit is contained in:
@@ -122,6 +122,43 @@ class MysqlController extends Controller
|
||||
return redirect()->route('mysql.index')->with('success', 'Database created successfully.');
|
||||
}
|
||||
|
||||
public function update(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => ['required', 'string'],
|
||||
'charset' => ['required', 'string'],
|
||||
'collation' => ['required', 'string'],
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
$prefix = $user->username . '_';
|
||||
|
||||
$name = $request->string('name');
|
||||
$charset = $request->string('charset');
|
||||
$collation = $request->string('collation');
|
||||
|
||||
if (!str_starts_with($name, $prefix)) {
|
||||
return back()->withErrors(['name' => 'Database name must start with ' . $prefix]);
|
||||
}
|
||||
|
||||
// Check if database exists
|
||||
$databases = DB::select("SHOW DATABASES");
|
||||
$dbNames = collect($databases)
|
||||
->map(fn($row) => (array) $row)
|
||||
->map(fn($row) => reset($row))
|
||||
->filter(fn($dbName) => str_starts_with($dbName, $prefix))
|
||||
->values();
|
||||
|
||||
if (!$dbNames->contains($name)) {
|
||||
return back()->withErrors(['name' => 'Database not found or access denied']);
|
||||
}
|
||||
|
||||
// Update database charset and collation
|
||||
DB::statement("ALTER DATABASE `$name` CHARACTER SET $charset COLLATE $collation");
|
||||
|
||||
return redirect()->route('mysql.index')->with('success', 'Database updated successfully.');
|
||||
}
|
||||
|
||||
public function rename(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
|
||||
import { Head, router, usePage } from '@inertiajs/react';
|
||||
import { TbDatabase } from 'react-icons/tb';
|
||||
import { useState } from 'react';
|
||||
import { TiDelete } from 'react-icons/ti';
|
||||
import { toast } from 'react-toastify';
|
||||
import CreateDatabaseForm from './Partials/CreateDatabaseForm';
|
||||
import EditDatabaseForm from './Partials/EditDatabaseForm';
|
||||
import ConfirmationButton from '@/Components/ConfirmationButton';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
|
||||
export default function MysqlIndex({ databases = [] }) {
|
||||
|
||||
const { auth } = usePage().props;
|
||||
const [renames, setRenames] = useState({});
|
||||
|
||||
const renameDb = (from) => {
|
||||
const to = renames[from];
|
||||
if (!to) return toast('Enter new database name');
|
||||
router.patch(route('mysql.rename'), { from, to }, {
|
||||
onBefore: () => toast('Renaming database...'),
|
||||
onSuccess: () => toast('Database renamed.'),
|
||||
onError: () => toast('Failed to rename database.'),
|
||||
});
|
||||
};
|
||||
|
||||
const deleteDb = (name) => {
|
||||
if (!confirm('Are you sure you want to delete this database?')) return;
|
||||
router.delete(route('mysql.destroy'), {
|
||||
data: { name },
|
||||
onBefore: () => toast('Deleting database...'),
|
||||
@@ -69,14 +60,10 @@ export default function MysqlIndex({ databases = [] }) {
|
||||
<td className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">{db.collation || '-'}</td>
|
||||
<td className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
||||
<div className='flex items-center space-x-2'>
|
||||
<input
|
||||
placeholder="New name"
|
||||
className="bg-gray-100 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2 dark:bg-gray-800 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||
value={renames[db.name] || ''}
|
||||
onChange={(e) => setRenames({ ...renames, [db.name]: e.target.value })}
|
||||
/>
|
||||
<button className="bg-blue-600 hover:bg-blue-700 text-white text-sm px-3 py-2 rounded-lg" onClick={() => renameDb(db.name)}>Rename</button>
|
||||
<button className="bg-red-600 hover:bg-red-700 text-white text-sm px-3 py-2 rounded-lg" onClick={() => deleteDb(db.name)}>Delete</button>
|
||||
<EditDatabaseForm database={db} />
|
||||
<ConfirmationButton doAction={() => deleteDb(db.name)}>
|
||||
<TiDelete className='w-6 h-6 text-red-500' />
|
||||
</ConfirmationButton>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
175
resources/js/Pages/Mysql/Partials/EditDatabaseForm.jsx
Normal file
175
resources/js/Pages/Mysql/Partials/EditDatabaseForm.jsx
Normal file
@@ -0,0 +1,175 @@
|
||||
import Modal from '@/Components/Modal';
|
||||
import PrimaryButton from '@/Components/PrimaryButton';
|
||||
import SecondaryButton from '@/Components/SecondaryButton';
|
||||
import InputLabel from '@/Components/InputLabel';
|
||||
import TextInput from '@/Components/TextInput';
|
||||
import InputError from '@/Components/InputError';
|
||||
import SearchableDropdown from '@/Components/SearchableDropdown';
|
||||
import { useForm, usePage } from '@inertiajs/react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { TbDatabase } from 'react-icons/tb';
|
||||
import { FaEdit } from 'react-icons/fa';
|
||||
import axios from 'axios';
|
||||
|
||||
export default function EditDatabaseForm({ database }) {
|
||||
const { auth } = usePage().props;
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [charsets, setCharsets] = useState([]);
|
||||
const [collations, setCollations] = useState([]);
|
||||
const [filteredCollations, setFilteredCollations] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const { data, setData, patch, processing, reset, clearErrors, errors } = useForm({
|
||||
name: database.name,
|
||||
charset: database.charset || 'utf8mb4',
|
||||
collation: database.collation || 'utf8mb4_unicode_ci',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (showModal) {
|
||||
fetchCharsetsAndCollations();
|
||||
}
|
||||
}, [showModal]);
|
||||
|
||||
useEffect(() => {
|
||||
// Filter collations based on selected charset
|
||||
if (data.charset && collations.length > 0) {
|
||||
const filtered = collations.filter(collation => collation.charset === data.charset);
|
||||
setFilteredCollations(filtered);
|
||||
|
||||
// If current collation is not valid for the selected charset, set to default
|
||||
if (data.collation && !filtered.find(c => c.name === data.collation)) {
|
||||
// Find the default collation for this charset
|
||||
const defaultCollation = filtered.find(c => c.default === 'Yes') || filtered[0];
|
||||
if (defaultCollation) {
|
||||
setData('collation', defaultCollation.name);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setFilteredCollations(collations);
|
||||
}
|
||||
}, [data.charset, collations]);
|
||||
|
||||
const fetchCharsetsAndCollations = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await axios.get(route('mysql.charsets-collations'));
|
||||
setCharsets(response.data.charsets);
|
||||
setCollations(response.data.collations);
|
||||
|
||||
// Set default collation for current charset if not already set
|
||||
if (data.charset && !data.collation) {
|
||||
const charsetCollations = response.data.collations.filter(c => c.charset === data.charset);
|
||||
const defaultCollation = charsetCollations.find(c => c.default === 'Yes') || charsetCollations[0];
|
||||
if (defaultCollation) {
|
||||
setData('collation', defaultCollation.name);
|
||||
}
|
||||
}
|
||||
|
||||
setFilteredCollations(response.data.collations);
|
||||
} catch (error) {
|
||||
console.error('Error fetching charsets and collations:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const showEditModal = () => {
|
||||
setShowModal(true);
|
||||
// Reset form with current database values
|
||||
setData({
|
||||
name: database.name,
|
||||
charset: database.charset || 'utf8mb4',
|
||||
collation: database.collation || 'utf8mb4_unicode_ci',
|
||||
});
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setShowModal(false);
|
||||
clearErrors();
|
||||
reset();
|
||||
};
|
||||
|
||||
const updateDatabase = (e) => {
|
||||
e.preventDefault();
|
||||
patch(route('mysql.update'), {
|
||||
preserveScroll: true,
|
||||
onSuccess: closeModal,
|
||||
});
|
||||
};
|
||||
|
||||
const prefix = auth.user.username + '_';
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={showEditModal}
|
||||
className="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
data-tooltip-id={`tooltip-edit-${database.name}`}
|
||||
data-tooltip-content="Edit Database"
|
||||
data-tooltip-place="top"
|
||||
>
|
||||
<FaEdit className='w-4 h-4' />
|
||||
</button>
|
||||
|
||||
<Modal show={showModal} onClose={closeModal}>
|
||||
<form onSubmit={updateDatabase} className="p-6">
|
||||
<h2 className="text-lg font-medium text-gray-900 dark:text-gray-100 flex items-center">
|
||||
<TbDatabase className='mr-2' />
|
||||
Edit Database: {database.name}
|
||||
</h2>
|
||||
|
||||
<div className="mt-6 flex flex-col space-y-4 max-h-[500px]">
|
||||
<div>
|
||||
<InputLabel htmlFor="name" value={`Database name (must start with ${prefix})`} className='my-2' />
|
||||
<TextInput
|
||||
id="name"
|
||||
name="name"
|
||||
value={data.name}
|
||||
onChange={(e) => setData('name', e.target.value)}
|
||||
className="mt-1 block w-full"
|
||||
placeholder={prefix + 'mydb'}
|
||||
required
|
||||
/>
|
||||
<InputError message={errors.name} className="mt-2" />
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel htmlFor="charset" value="Charset" className='my-2' />
|
||||
<select
|
||||
id="charset"
|
||||
name="charset"
|
||||
value={data.charset}
|
||||
onChange={(e) => setData('charset', e.target.value)}
|
||||
className="mt-1 block w-full flex-1 border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 dark:focus:border-indigo-600 dark:focus:ring-indigo-600 rounded-md"
|
||||
disabled={loading}
|
||||
>
|
||||
{charsets.map(charset => (
|
||||
<option key={charset.name} value={charset.name}>
|
||||
{charset.name} - {charset.description}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<InputError message={errors.charset} className="mt-2" />
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel htmlFor="collation" value="Collation" className='my-2' />
|
||||
<SearchableDropdown
|
||||
options={filteredCollations}
|
||||
value={data.collation}
|
||||
onChange={(collation) => setData('collation', collation.name)}
|
||||
placeholder="Select a collation..."
|
||||
className="mt-1"
|
||||
disabled={loading || filteredCollations.length === 0}
|
||||
/>
|
||||
<InputError message={errors.collation} className="mt-2" />
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<PrimaryButton className="mr-3" disabled={processing}>Update Database</PrimaryButton>
|
||||
<SecondaryButton onClick={closeModal}>Cancel</SecondaryButton>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -38,6 +38,7 @@ Route::get('/php/get-versions', [PHPManagerController::class, 'getVersions'])->m
|
||||
Route::get('/mysql', [MysqlController::class, 'index'])->middleware(['auth'])->name('mysql.index');
|
||||
Route::get('/mysql/charsets-collations', [MysqlController::class, 'getCharsetsAndCollations'])->middleware(['auth'])->name('mysql.charsets-collations');
|
||||
Route::post('/mysql', [MysqlController::class, 'store'])->middleware(['auth'])->name('mysql.store');
|
||||
Route::patch('/mysql', [MysqlController::class, 'update'])->middleware(['auth'])->name('mysql.update');
|
||||
Route::patch('/mysql/rename', [MysqlController::class, 'rename'])->middleware(['auth'])->name('mysql.rename');
|
||||
Route::delete('/mysql', [MysqlController::class, 'destroy'])->middleware(['auth'])->name('mysql.destroy');
|
||||
|
||||
@@ -52,8 +53,6 @@ Route::patch('/filemanager/paste-files', [FilemanagerController::class, 'pasteFi
|
||||
Route::post('/filemanager/delete-files', [FilemanagerController::class, 'deleteFiles'])->middleware(['auth'])->name('filemanager.deleteFiles');
|
||||
Route::post('/filemanager/upload-file', [FilemanagerController::class, 'uploadFile'])->middleware(['auth'])->name('filemanager.uploadFile');
|
||||
|
||||
// MySQL [Admin | User]
|
||||
Route::get('/mysql', [MysqlController::class, 'index'])->middleware(['auth'])->name('mysql.index');
|
||||
|
||||
// Stats History [Admin]
|
||||
Route::get('/stats/history', [StatsHistoryController::class, 'cpuAndMemory'])->middleware(['auth', AdminMiddleware::class])->name('stats.history');
|
||||
|
||||
Reference in New Issue
Block a user