Initial commit
This commit is contained in:
@@ -72,8 +72,8 @@ return array(
|
|||||||
/**
|
/**
|
||||||
* Configuration for: Google reCAPTCHA v2
|
* Configuration for: Google reCAPTCHA v2
|
||||||
*/
|
*/
|
||||||
'RECAPTCHA_SITE_KEY' => 'recaptcha-site-key',
|
'RECAPTCHA_SITE_KEY' => '6Lfl-EcsAAAAAG9svnagihb5y6HCNK2cd5W9jQm-',
|
||||||
'RECAPTCHA_SECRET_KEY' => 'recaptcha-secret-key',
|
'RECAPTCHA_SECRET_KEY' => '6Lfl-EcsAAAAADusuMYTprgTZ42BVIWPsF_jVtk6',
|
||||||
/**
|
/**
|
||||||
* Configuration for: Cookies
|
* Configuration for: Cookies
|
||||||
* 1209600 seconds = 2 weeks
|
* 1209600 seconds = 2 weeks
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ class DatabaseController extends Controller
|
|||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
|
|
||||||
// Only logged-in users can access the database manager
|
// Only admin users can access the database manager
|
||||||
Auth::checkAuthentication();
|
Auth::checkAuthentication();
|
||||||
|
Auth::checkAdminAuthentication();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,7 +24,7 @@ class DatabaseController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$this->View->render('database/index', array(
|
$this->View->renderDbManager('database/index', array(
|
||||||
'databases' => DatabaseModel::getAllDatabases(),
|
'databases' => DatabaseModel::getAllDatabases(),
|
||||||
'current_db' => Config::get('DB_NAME')
|
'current_db' => Config::get('DB_NAME')
|
||||||
));
|
));
|
||||||
@@ -39,7 +40,7 @@ class DatabaseController extends Controller
|
|||||||
$database_name = Config::get('DB_NAME');
|
$database_name = Config::get('DB_NAME');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->View->render('database/show', array(
|
$this->View->renderDbManager('database/show', array(
|
||||||
'tables' => DatabaseModel::getTablesInDatabase($database_name),
|
'tables' => DatabaseModel::getTablesInDatabase($database_name),
|
||||||
'database_name' => $database_name,
|
'database_name' => $database_name,
|
||||||
'table_info' => DatabaseModel::getTableDetails($database_name)
|
'table_info' => DatabaseModel::getTableDetails($database_name)
|
||||||
@@ -133,6 +134,18 @@ class DatabaseController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export database as raw SQL text
|
||||||
|
* @param string $database_name
|
||||||
|
*/
|
||||||
|
public function export($database_name)
|
||||||
|
{
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
header('Content-Disposition: inline; filename="' . $database_name . '.sql"');
|
||||||
|
|
||||||
|
echo DatabaseModel::exportDatabase($database_name);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the request is an AJAX request
|
* Check if the request is an AJAX request
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class DbUserController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$this->View->render('dbuser/index', array(
|
$this->View->renderDbManager('dbuser/index', array(
|
||||||
'users' => DbUserModel::getAllUsers(),
|
'users' => DbUserModel::getAllUsers(),
|
||||||
'current_user' => Config::get('DB_USER')
|
'current_user' => Config::get('DB_USER')
|
||||||
));
|
));
|
||||||
@@ -39,11 +39,15 @@ class DbUserController extends Controller
|
|||||||
$username = Request::post('username');
|
$username = Request::post('username');
|
||||||
$password = Request::post('password');
|
$password = Request::post('password');
|
||||||
$host = Request::post('host');
|
$host = Request::post('host');
|
||||||
|
$privileges = Request::post('privileges');
|
||||||
|
|
||||||
if ($this->isAjaxRequest()) {
|
if ($this->isAjaxRequest()) {
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
if (DbUserModel::createUser($username, $password, $host)) {
|
if (DbUserModel::createUser($username, $password, $host)) {
|
||||||
|
if (!empty($privileges)) {
|
||||||
|
DbUserModel::updateUserPrivileges($username, $host, $privileges);
|
||||||
|
}
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'User created successfully',
|
'message' => 'User created successfully',
|
||||||
@@ -59,6 +63,9 @@ class DbUserController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (DbUserModel::createUser($username, $password, $host)) {
|
if (DbUserModel::createUser($username, $password, $host)) {
|
||||||
|
if (!empty($privileges)) {
|
||||||
|
DbUserModel::updateUserPrivileges($username, $host, $privileges);
|
||||||
|
}
|
||||||
Redirect::to('dbuser');
|
Redirect::to('dbuser');
|
||||||
} else {
|
} else {
|
||||||
Redirect::to('dbuser');
|
Redirect::to('dbuser');
|
||||||
@@ -66,8 +73,7 @@ class DbUserController extends Controller
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show create user form
|
$this->View->renderDbManager('dbuser/create');
|
||||||
$this->View->render('dbuser/create');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -127,7 +133,7 @@ class DbUserController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show edit user form
|
// Show edit user form
|
||||||
$this->View->render('dbuser/edit', array(
|
$this->View->renderDbManager('dbuser/edit', array(
|
||||||
'user' => DbUserModel::getUserDetails($username, $host),
|
'user' => DbUserModel::getUserDetails($username, $host),
|
||||||
'privileges' => DbUserModel::getUserPrivileges($username, $host),
|
'privileges' => DbUserModel::getUserPrivileges($username, $host),
|
||||||
'databases' => DatabaseModel::getAllDatabases()
|
'databases' => DatabaseModel::getAllDatabases()
|
||||||
@@ -184,7 +190,7 @@ class DbUserController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function privileges($username, $host)
|
public function privileges($username, $host)
|
||||||
{
|
{
|
||||||
$this->View->render('dbuser/privileges', array(
|
$this->View->renderDbManager('dbuser/privileges', array(
|
||||||
'user' => DbUserModel::getUserDetails($username, $host),
|
'user' => DbUserModel::getUserDetails($username, $host),
|
||||||
'privileges' => DbUserModel::getUserPrivileges($username, $host)
|
'privileges' => DbUserModel::getUserPrivileges($username, $host)
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ class SqlController extends Controller
|
|||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
|
|
||||||
// Only logged-in users can access the SQL console
|
// Only admin users can access the SQL console
|
||||||
Auth::checkAuthentication();
|
Auth::checkAuthentication();
|
||||||
|
Auth::checkAdminAuthentication();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,7 +29,7 @@ class SqlController extends Controller
|
|||||||
$database_name = Config::get('DB_NAME');
|
$database_name = Config::get('DB_NAME');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->View->render('sql/index', array(
|
$this->View->renderDbManager('sql/index', array(
|
||||||
'database_name' => $database_name,
|
'database_name' => $database_name,
|
||||||
'databases' => DatabaseModel::getAllDatabases(),
|
'databases' => DatabaseModel::getAllDatabases(),
|
||||||
'history' => SqlModel::getQueryHistory(Session::get('user_id'))
|
'history' => SqlModel::getQueryHistory(Session::get('user_id'))
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ class TableController extends Controller
|
|||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
|
|
||||||
// Only logged-in users can access the table manager
|
// Only admin users can access the table manager
|
||||||
Auth::checkAuthentication();
|
Auth::checkAuthentication();
|
||||||
|
Auth::checkAdminAuthentication();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,7 +39,7 @@ class TableController extends Controller
|
|||||||
$page = (int)$page;
|
$page = (int)$page;
|
||||||
$per_page = 20;
|
$per_page = 20;
|
||||||
|
|
||||||
$this->View->render('table/show', array(
|
$this->View->renderDbManager('table/show', array(
|
||||||
'database_name' => $database_name,
|
'database_name' => $database_name,
|
||||||
'table_name' => $table_name,
|
'table_name' => $table_name,
|
||||||
'columns' => TableModel::getTableColumns($database_name, $table_name),
|
'columns' => TableModel::getTableColumns($database_name, $table_name),
|
||||||
@@ -91,7 +92,7 @@ class TableController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show create table form
|
// Show create table form
|
||||||
$this->View->render('table/create', array(
|
$this->View->renderDbManager('table/create', array(
|
||||||
'database_name' => $database_name
|
'database_name' => $database_name
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -112,7 +113,7 @@ class TableController extends Controller
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->View->render('table/structure', array(
|
$this->View->renderDbManager('table/structure', array(
|
||||||
'database_name' => $database_name,
|
'database_name' => $database_name,
|
||||||
'table_name' => $table_name,
|
'table_name' => $table_name,
|
||||||
'columns' => TableModel::getTableColumns($database_name, $table_name),
|
'columns' => TableModel::getTableColumns($database_name, $table_name),
|
||||||
@@ -172,7 +173,7 @@ class TableController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show add column form
|
// Show add column form
|
||||||
$this->View->render('table/add_column', array(
|
$this->View->renderDbManager('table/add_column', array(
|
||||||
'database_name' => $database_name,
|
'database_name' => $database_name,
|
||||||
'table_name' => $table_name
|
'table_name' => $table_name
|
||||||
));
|
));
|
||||||
@@ -237,6 +238,141 @@ class TableController extends Controller
|
|||||||
Redirect::to('database/show/' . urlencode($database_name));
|
Redirect::to('database/show/' . urlencode($database_name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a row in the table (AJAX)
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
*/
|
||||||
|
public function updateRow($database_name = null, $table_name = null)
|
||||||
|
{
|
||||||
|
if (!$database_name || !$table_name) {
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Invalid parameters']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Redirect::to('database/index');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pk_value = Request::post('pk_value');
|
||||||
|
$data = Request::post('data');
|
||||||
|
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if (!$pk_value || !$data) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Missing required data']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TableModel::updateRow($database_name, $table_name, $pk_value, $data)) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Row updated successfully'
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Failed to update row'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Redirect::to('table/show/' . urlencode($database_name) . '/' . urlencode($table_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a row from the table (AJAX)
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
*/
|
||||||
|
public function deleteRow($database_name = null, $table_name = null)
|
||||||
|
{
|
||||||
|
if (!$database_name || !$table_name) {
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Invalid parameters']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Redirect::to('database/index');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pk_value = Request::post('pk_value');
|
||||||
|
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if (!$pk_value) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Missing primary key value']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TableModel::deleteRow($database_name, $table_name, $pk_value)) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Row deleted successfully'
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Failed to delete row'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Redirect::to('table/show/' . urlencode($database_name) . '/' . urlencode($table_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a new row into the table (AJAX)
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
*/
|
||||||
|
public function insertRow($database_name = null, $table_name = null)
|
||||||
|
{
|
||||||
|
if (!$database_name || !$table_name) {
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Invalid parameters']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Redirect::to('database/index');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = Request::post('data');
|
||||||
|
|
||||||
|
if ($this->isAjaxRequest()) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if (!$data) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Missing row data']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$insertId = TableModel::insertRow($database_name, $table_name, $data);
|
||||||
|
if ($insertId !== false) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Row inserted successfully',
|
||||||
|
'insert_id' => $insertId
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Failed to insert row'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Redirect::to('table/show/' . urlencode($database_name) . '/' . urlencode($table_name));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the request is an AJAX request
|
* Check if the request is an AJAX request
|
||||||
*/
|
*/
|
||||||
@@ -245,4 +381,17 @@ class TableController extends Controller
|
|||||||
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
|
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
|
||||||
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
|
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export table as raw SQL text
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
*/
|
||||||
|
public function export($database_name, $table_name)
|
||||||
|
{
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
header('Content-Disposition: inline; filename="' . $table_name . '.sql"');
|
||||||
|
|
||||||
|
echo DatabaseModel::exportTable($database_name, $table_name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -26,6 +26,27 @@ class View
|
|||||||
public $messages;
|
public $messages;
|
||||||
public $other_user;
|
public $other_user;
|
||||||
|
|
||||||
|
/* Group properties */
|
||||||
|
public $groups;
|
||||||
|
|
||||||
|
/* Database Manager properties */
|
||||||
|
public $databases;
|
||||||
|
public $database_name;
|
||||||
|
public $current_db;
|
||||||
|
public $tables;
|
||||||
|
public $table_name;
|
||||||
|
public $table_info;
|
||||||
|
public $columns;
|
||||||
|
public $rows;
|
||||||
|
public $indexes;
|
||||||
|
public $total_rows;
|
||||||
|
public $current_page;
|
||||||
|
public $per_page;
|
||||||
|
public $history;
|
||||||
|
public $user;
|
||||||
|
public $privileges;
|
||||||
|
public $current_user;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Static property to track if header has been rendered
|
* Static property to track if header has been rendered
|
||||||
*/
|
*/
|
||||||
@@ -116,6 +137,24 @@ class View
|
|||||||
echo json_encode($data);
|
echo json_encode($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a view using the database manager layout
|
||||||
|
* @param string $filename Path of the to-be-rendered view
|
||||||
|
* @param array $data Data to be used in the view
|
||||||
|
*/
|
||||||
|
public function renderDbManager($filename, $data = null)
|
||||||
|
{
|
||||||
|
if ($data) {
|
||||||
|
foreach ($data as $key => $value) {
|
||||||
|
$this->{$key} = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require Config::get('PATH_VIEW') . '_templates/dbmanager_header.php';
|
||||||
|
require Config::get('PATH_VIEW') . $filename . '.php';
|
||||||
|
require Config::get('PATH_VIEW') . '_templates/dbmanager_footer.php';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset header render flag at start of request
|
* Reset header render flag at start of request
|
||||||
*/
|
*/
|
||||||
@@ -206,6 +245,25 @@ class View
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the passed array of controllers contains the currently active controller.
|
||||||
|
* Useful for navigation items that span multiple controllers (e.g., database manager).
|
||||||
|
*
|
||||||
|
* @param string $filename
|
||||||
|
* @param array $controllers Array of controller names to check
|
||||||
|
*
|
||||||
|
* @return bool Shows if any of the controllers is active
|
||||||
|
*/
|
||||||
|
public static function checkForActiveControllers($filename, $controllers)
|
||||||
|
{
|
||||||
|
foreach ($controllers as $controller) {
|
||||||
|
if (self::checkForActiveController($filename, $controller)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts characters to HTML entities
|
* Converts characters to HTML entities
|
||||||
* This is important to avoid XSS attacks, and attempts to inject malicious code in your page.
|
* This is important to avoid XSS attacks, and attempts to inject malicious code in your page.
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ class DatabaseModel
|
|||||||
|
|
||||||
$databases = $query->fetchAll(PDO::FETCH_COLUMN);
|
$databases = $query->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
// Filter out system databases
|
|
||||||
$system_dbs = ['information_schema', 'performance_schema', 'mysql', 'sys'];
|
$system_dbs = ['information_schema', 'performance_schema', 'mysql', 'sys'];
|
||||||
return array_diff($databases, $system_dbs);
|
return array_diff($databases, $system_dbs);
|
||||||
}
|
}
|
||||||
@@ -35,7 +34,7 @@ class DatabaseModel
|
|||||||
{
|
{
|
||||||
$database = DatabaseFactory::getFactory()->getConnection();
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
$sql = "SHOW TABLES FROM " . $database_name;
|
$sql = "SHOW TABLES FROM `" . $database_name . "`";
|
||||||
$query = $database->prepare($sql);
|
$query = $database->prepare($sql);
|
||||||
$query->execute();
|
$query->execute();
|
||||||
|
|
||||||
@@ -54,7 +53,7 @@ class DatabaseModel
|
|||||||
$table_details = array();
|
$table_details = array();
|
||||||
|
|
||||||
foreach ($tables as $table) {
|
foreach ($tables as $table) {
|
||||||
$sql = "SHOW TABLE STATUS FROM " . $database_name . " LIKE :table_name";
|
$sql = "SHOW TABLE STATUS FROM `" . $database_name . "` LIKE :table_name";
|
||||||
$query = $database->prepare($sql);
|
$query = $database->prepare($sql);
|
||||||
$query->execute(array(':table_name' => $table));
|
$query->execute(array(':table_name' => $table));
|
||||||
|
|
||||||
@@ -87,7 +86,7 @@ class DatabaseModel
|
|||||||
$tables = self::getTablesInDatabase($database_name);
|
$tables = self::getTablesInDatabase($database_name);
|
||||||
|
|
||||||
foreach ($tables as $table) {
|
foreach ($tables as $table) {
|
||||||
$sql = "DESCRIBE " . $database_name . "." . $table;
|
$sql = "DESCRIBE `" . $database_name . "`.`" . $table . "`";
|
||||||
$query = $database->prepare($sql);
|
$query = $database->prepare($sql);
|
||||||
$query->execute();
|
$query->execute();
|
||||||
|
|
||||||
@@ -152,13 +151,106 @@ class DatabaseModel
|
|||||||
{
|
{
|
||||||
$database = DatabaseFactory::getFactory()->getConnection();
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
$sql = "SHOW COLUMNS FROM " . $database_name . "." . $table_name;
|
$sql = "SHOW COLUMNS FROM `" . $database_name . "`.`" . $table_name . "`";
|
||||||
$query = $database->prepare($sql);
|
$query = $database->prepare($sql);
|
||||||
$query->execute();
|
$query->execute();
|
||||||
|
|
||||||
return $query->fetchAll(PDO::FETCH_ASSOC);
|
return $query->fetchAll(PDO::FETCH_ASSOC);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export database as SQL dump
|
||||||
|
* @param string $database_name
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function exportDatabase($database_name)
|
||||||
|
{
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
$output = "-- Database Export: " . $database_name . "\n";
|
||||||
|
$output .= "-- Generated: " . date('Y-m-d H:i:s') . "\n\n";
|
||||||
|
$output .= "SET FOREIGN_KEY_CHECKS=0;\n";
|
||||||
|
$output .= "SET SQL_MODE = \"NO_AUTO_VALUE_ON_ZERO\";\n\n";
|
||||||
|
|
||||||
|
$tables = self::getTablesInDatabase($database_name);
|
||||||
|
|
||||||
|
foreach ($tables as $table) {
|
||||||
|
$sql = "SHOW CREATE TABLE `" . $database_name . "`.`" . $table . "`";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
$query->execute();
|
||||||
|
$row = $query->fetch(PDO::FETCH_NUM);
|
||||||
|
|
||||||
|
$output .= "DROP TABLE IF EXISTS `" . $table . "`;\n";
|
||||||
|
$output .= $row[1] . ";\n\n";
|
||||||
|
|
||||||
|
$sql = "SELECT * FROM `" . $database_name . "`.`" . $table . "`";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
$query->execute();
|
||||||
|
$rows = $query->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!empty($rows)) {
|
||||||
|
foreach ($rows as $dataRow) {
|
||||||
|
$columns = array_keys($dataRow);
|
||||||
|
$values = array_map(function($val) use ($database) {
|
||||||
|
if ($val === null) {
|
||||||
|
return 'NULL';
|
||||||
|
}
|
||||||
|
return $database->quote($val);
|
||||||
|
}, array_values($dataRow));
|
||||||
|
|
||||||
|
$output .= "INSERT INTO `" . $table . "` (`" . implode("`, `", $columns) . "`) VALUES (" . implode(", ", $values) . ");\n";
|
||||||
|
}
|
||||||
|
$output .= "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$output .= "SET FOREIGN_KEY_CHECKS=1;\n";
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export single table as SQL dump
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function exportTable($database_name, $table_name)
|
||||||
|
{
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
$output = "-- Table Export: " . $table_name . " from " . $database_name . "\n";
|
||||||
|
$output .= "-- Generated: " . date('Y-m-d H:i:s') . "\n\n";
|
||||||
|
$output .= "SET FOREIGN_KEY_CHECKS=0;\n\n";
|
||||||
|
|
||||||
|
$sql = "SHOW CREATE TABLE `" . $database_name . "`.`" . $table_name . "`";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
$query->execute();
|
||||||
|
$row = $query->fetch(PDO::FETCH_NUM);
|
||||||
|
|
||||||
|
$output .= "DROP TABLE IF EXISTS `" . $table_name . "`;\n";
|
||||||
|
$output .= $row[1] . ";\n\n";
|
||||||
|
|
||||||
|
$sql = "SELECT * FROM `" . $database_name . "`.`" . $table_name . "`";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
$query->execute();
|
||||||
|
$rows = $query->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!empty($rows)) {
|
||||||
|
foreach ($rows as $dataRow) {
|
||||||
|
$columns = array_keys($dataRow);
|
||||||
|
$values = array_map(function($val) use ($database) {
|
||||||
|
if ($val === null) {
|
||||||
|
return 'NULL';
|
||||||
|
}
|
||||||
|
return $database->quote($val);
|
||||||
|
}, array_values($dataRow));
|
||||||
|
|
||||||
|
$output .= "INSERT INTO `" . $table_name . "` (`" . implode("`, `", $columns) . "`) VALUES (" . implode(", ", $values) . ");\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$output .= "\nSET FOREIGN_KEY_CHECKS=1;\n";
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format bytes to human readable format
|
* Format bytes to human readable format
|
||||||
* @param int $bytes
|
* @param int $bytes
|
||||||
|
|||||||
202
application/model/DbUserModel.php
Normal file
202
application/model/DbUserModel.php
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class DbUserModel
|
||||||
|
*
|
||||||
|
* Model for managing MySQL database users
|
||||||
|
*/
|
||||||
|
class DbUserModel
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get all database users
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getAllUsers()
|
||||||
|
{
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$sql = "SELECT User, Host FROM mysql.user ORDER BY User, Host";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
return $query->fetchAll(PDO::FETCH_OBJ);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user details
|
||||||
|
* @param string $username
|
||||||
|
* @param string $host
|
||||||
|
* @return object|null
|
||||||
|
*/
|
||||||
|
public static function getUserDetails($username, $host)
|
||||||
|
{
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$sql = "SELECT * FROM mysql.user WHERE User = :username AND Host = :host";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
$query->execute(array(':username' => $username, ':host' => $host));
|
||||||
|
|
||||||
|
return $query->fetch(PDO::FETCH_OBJ);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user privileges
|
||||||
|
* @param string $username
|
||||||
|
* @param string $host
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getUserPrivileges($username, $host)
|
||||||
|
{
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Escape username and host for SHOW GRANTS
|
||||||
|
$sql = "SHOW GRANTS FOR " . $database->quote($username) . "@" . $database->quote($host);
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
$grants = array();
|
||||||
|
while ($row = $query->fetch(PDO::FETCH_NUM)) {
|
||||||
|
$grants[] = $row[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $grants;
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new database user
|
||||||
|
* @param string $username
|
||||||
|
* @param string $password
|
||||||
|
* @param string $host
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function createUser($username, $password, $host)
|
||||||
|
{
|
||||||
|
if (!self::validateUsername($username) || empty($password) || empty($host)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$sql = "CREATE USER " . $database->quote($username) . "@" . $database->quote($host) .
|
||||||
|
" IDENTIFIED BY " . $database->quote($password);
|
||||||
|
$database->exec($sql);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update user password
|
||||||
|
* @param string $username
|
||||||
|
* @param string $host
|
||||||
|
* @param string $password
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function updateUserPassword($username, $host, $password)
|
||||||
|
{
|
||||||
|
if (empty($password)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$sql = "ALTER USER " . $database->quote($username) . "@" . $database->quote($host) .
|
||||||
|
" IDENTIFIED BY " . $database->quote($password);
|
||||||
|
$database->exec($sql);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update user privileges
|
||||||
|
* @param string $username
|
||||||
|
* @param string $host
|
||||||
|
* @param array $privileges
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function updateUserPrivileges($username, $host, $privileges)
|
||||||
|
{
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$sql = "REVOKE ALL PRIVILEGES, GRANT OPTION FROM " .
|
||||||
|
$database->quote($username) . "@" . $database->quote($host);
|
||||||
|
$database->exec($sql);
|
||||||
|
|
||||||
|
if (!empty($privileges) && is_array($privileges)) {
|
||||||
|
if (in_array('ALL PRIVILEGES', $privileges)) {
|
||||||
|
$sql = "GRANT ALL PRIVILEGES ON *.* TO " .
|
||||||
|
$database->quote($username) . "@" . $database->quote($host);
|
||||||
|
$database->exec($sql);
|
||||||
|
} else {
|
||||||
|
$valid_privs = array('SELECT', 'INSERT', 'UPDATE', 'DELETE', 'CREATE', 'DROP', 'ALTER', 'INDEX',
|
||||||
|
'REFERENCES', 'CREATE TEMPORARY TABLES', 'LOCK TABLES', 'EXECUTE',
|
||||||
|
'CREATE VIEW', 'SHOW VIEW', 'CREATE ROUTINE', 'ALTER ROUTINE', 'EVENT', 'TRIGGER');
|
||||||
|
$privileges = array_intersect($privileges, $valid_privs);
|
||||||
|
|
||||||
|
if (!empty($privileges)) {
|
||||||
|
$priv_string = implode(', ', $privileges);
|
||||||
|
$sql = "GRANT " . $priv_string . " ON *.* TO " .
|
||||||
|
$database->quote($username) . "@" . $database->quote($host);
|
||||||
|
$database->exec($sql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$database->exec("FLUSH PRIVILEGES");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a database user
|
||||||
|
* @param string $username
|
||||||
|
* @param string $host
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function deleteUser($username, $host)
|
||||||
|
{
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$sql = "DROP USER " . $database->quote($username) . "@" . $database->quote($host);
|
||||||
|
$database->exec($sql);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate username format
|
||||||
|
* @param string $username
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private static function validateUsername($username)
|
||||||
|
{
|
||||||
|
return !empty($username) && preg_match('/^[a-zA-Z0-9_]+$/', $username);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -131,7 +131,6 @@ class TableModel
|
|||||||
|
|
||||||
$database = DatabaseFactory::getFactory()->getConnection();
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
// Build column definitions
|
|
||||||
$column_definitions = array();
|
$column_definitions = array();
|
||||||
foreach ($columns as $column) {
|
foreach ($columns as $column) {
|
||||||
$definition = "`" . $column['name'] . "` " . $column['type'];
|
$definition = "`" . $column['name'] . "` " . $column['type'];
|
||||||
@@ -151,7 +150,6 @@ class TableModel
|
|||||||
$column_definitions[] = $definition;
|
$column_definitions[] = $definition;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle primary key
|
|
||||||
foreach ($columns as $column) {
|
foreach ($columns as $column) {
|
||||||
if (isset($column['key']) && $column['key'] === 'PRI') {
|
if (isset($column['key']) && $column['key'] === 'PRI') {
|
||||||
$column_definitions[] = "PRIMARY KEY (`" . $column['name'] . "`)";
|
$column_definitions[] = "PRIMARY KEY (`" . $column['name'] . "`)";
|
||||||
@@ -260,6 +258,169 @@ class TableModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the primary key column name for a table
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public static function getPrimaryKeyColumn($database_name, $table_name)
|
||||||
|
{
|
||||||
|
$columns = self::getTableColumns($database_name, $table_name);
|
||||||
|
foreach ($columns as $column) {
|
||||||
|
if ($column['Key'] === 'PRI') {
|
||||||
|
return $column['Field'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single row by primary key
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
* @param mixed $pk_value
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
public static function getRow($database_name, $table_name, $pk_value)
|
||||||
|
{
|
||||||
|
$pk_column = self::getPrimaryKeyColumn($database_name, $table_name);
|
||||||
|
if (!$pk_column) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
$sql = "SELECT * FROM `" . $database_name . "`.`" . $table_name . "` WHERE `" . $pk_column . "` = :pk_value LIMIT 1";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
$query->execute(array(':pk_value' => $pk_value));
|
||||||
|
|
||||||
|
return $query->fetch(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a row in the table
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
* @param mixed $pk_value
|
||||||
|
* @param array $data - associative array of column => value
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function updateRow($database_name, $table_name, $pk_value, $data)
|
||||||
|
{
|
||||||
|
if (!$database_name || !$table_name || !$pk_value || empty($data)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pk_column = self::getPrimaryKeyColumn($database_name, $table_name);
|
||||||
|
if (!$pk_column) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
$set_parts = array();
|
||||||
|
$params = array();
|
||||||
|
$i = 0;
|
||||||
|
foreach ($data as $column => $value) {
|
||||||
|
if ($column === $pk_column) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$param_name = ':param_' . $i;
|
||||||
|
$set_parts[] = "`" . $column . "` = " . $param_name;
|
||||||
|
$params[$param_name] = $value === '' ? null : $value;
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($set_parts)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$params[':pk_value'] = $pk_value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$sql = "UPDATE `" . $database_name . "`.`" . $table_name . "` SET " . implode(', ', $set_parts) . " WHERE `" . $pk_column . "` = :pk_value";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
return $query->execute($params);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a row from the table
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
* @param mixed $pk_value
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function deleteRow($database_name, $table_name, $pk_value)
|
||||||
|
{
|
||||||
|
if (!$database_name || !$table_name || !$pk_value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pk_column = self::getPrimaryKeyColumn($database_name, $table_name);
|
||||||
|
if (!$pk_column) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$sql = "DELETE FROM `" . $database_name . "`.`" . $table_name . "` WHERE `" . $pk_column . "` = :pk_value";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
return $query->execute(array(':pk_value' => $pk_value));
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a new row into the table
|
||||||
|
* @param string $database_name
|
||||||
|
* @param string $table_name
|
||||||
|
* @param array $data - associative array of column => value
|
||||||
|
* @return bool|int - returns insert ID on success, false on failure
|
||||||
|
*/
|
||||||
|
public static function insertRow($database_name, $table_name, $data)
|
||||||
|
{
|
||||||
|
if (!$database_name || !$table_name || empty($data)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$database = DatabaseFactory::getFactory()->getConnection();
|
||||||
|
|
||||||
|
$columns = array();
|
||||||
|
$placeholders = array();
|
||||||
|
$params = array();
|
||||||
|
$i = 0;
|
||||||
|
|
||||||
|
foreach ($data as $column => $value) {
|
||||||
|
if ($value === '' || $value === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$columns[] = "`" . $column . "`";
|
||||||
|
$param_name = ':param_' . $i;
|
||||||
|
$placeholders[] = $param_name;
|
||||||
|
$params[$param_name] = $value;
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($columns)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$sql = "INSERT INTO `" . $database_name . "`.`" . $table_name . "` (" . implode(', ', $columns) . ") VALUES (" . implode(', ', $placeholders) . ")";
|
||||||
|
$query = $database->prepare($sql);
|
||||||
|
$query->execute($params);
|
||||||
|
return $database->lastInsertId();
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format bytes to human readable format
|
* Format bytes to human readable format
|
||||||
* @param int $bytes
|
* @param int $bytes
|
||||||
|
|||||||
105
application/view/_templates/dbmanager_footer.php
Normal file
105
application/view/_templates/dbmanager_footer.php
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
</main>
|
||||||
|
</div><!-- end dbm-main -->
|
||||||
|
|
||||||
|
<!-- SQL Console (Full Width at Bottom) -->
|
||||||
|
<div class="dbm-console expanded">
|
||||||
|
<div class="dbm-console-header">
|
||||||
|
<div class="dbm-console-title">
|
||||||
|
<i data-lucide="terminal" class="icon"></i>
|
||||||
|
SQL Console
|
||||||
|
</div>
|
||||||
|
<i data-lucide="chevron-up" class="dbm-console-toggle"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dbm-console-body">
|
||||||
|
<form id="sql-form" method="post" action="<?php echo Config::get('URL'); ?>sql/execute">
|
||||||
|
<?php $current_database = isset($this->database_name) ? $this->database_name : Config::get('DB_NAME'); ?>
|
||||||
|
<input type="hidden" name="database_name" value="<?php echo htmlspecialchars($current_database); ?>">
|
||||||
|
|
||||||
|
<div class="dbm-sql-editor">
|
||||||
|
<div id="sql-highlight" class="dbm-sql-highlight"></div>
|
||||||
|
<textarea name="sql_query" id="sql_query" placeholder="SELECT * FROM table_name LIMIT 10;
|
||||||
|
|
||||||
|
-- Write your SQL query here
|
||||||
|
-- Press Execute or Ctrl+Enter to run"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dbm-sql-actions">
|
||||||
|
<button type="submit" class="dbm-btn dbm-btn-success">
|
||||||
|
<i data-lucide="play"></i>
|
||||||
|
Execute
|
||||||
|
</button>
|
||||||
|
<button type="button" class="dbm-btn dbm-btn-secondary" onclick="document.getElementById('sql_query').value = ''; document.getElementById('sql-highlight').innerHTML = '';">
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
<select class="db-select" onchange="document.querySelector('input[name=database_name]').value = this.value;">
|
||||||
|
<?php foreach (DatabaseModel::getAllDatabases() as $db): ?>
|
||||||
|
<option value="<?php echo htmlspecialchars($db); ?>" <?php echo $db === $current_database ? 'selected' : ''; ?>>
|
||||||
|
<?php echo htmlspecialchars($db); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="sql-result" class="dbm-sql-result">
|
||||||
|
<?php
|
||||||
|
// Check for session result
|
||||||
|
$result = Session::get('sql_result');
|
||||||
|
if ($result) {
|
||||||
|
Session::set('sql_result', null);
|
||||||
|
|
||||||
|
if ($result['success']) {
|
||||||
|
echo '<div class="dbm-sql-result success">';
|
||||||
|
echo '<div class="dbm-sql-result-header">';
|
||||||
|
echo '<i data-lucide="check-circle"></i>';
|
||||||
|
echo htmlspecialchars($result['message']);
|
||||||
|
echo '<span style="margin-left: auto; color: var(--text-muted); font-size: 12px;">' . $result['execution_time'] . 'ms</span>';
|
||||||
|
echo '</div>';
|
||||||
|
|
||||||
|
if (!empty($result['result'])) {
|
||||||
|
echo '<div class="dbm-sql-result-body"><div class="dbm-table-wrapper"><table class="dbm-table"><thead><tr>';
|
||||||
|
foreach (array_keys($result['result'][0]) as $col) {
|
||||||
|
echo '<th>' . htmlspecialchars($col) . '</th>';
|
||||||
|
}
|
||||||
|
echo '</tr></thead><tbody>';
|
||||||
|
foreach ($result['result'] as $row) {
|
||||||
|
echo '<tr>';
|
||||||
|
foreach ($row as $value) {
|
||||||
|
echo '<td>' . ($value === null ? '<span class="null-value">NULL</span>' : htmlspecialchars(substr($value, 0, 100))) . '</td>';
|
||||||
|
}
|
||||||
|
echo '</tr>';
|
||||||
|
}
|
||||||
|
echo '</tbody></table></div></div>';
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
} else {
|
||||||
|
echo '<div class="dbm-sql-result error">';
|
||||||
|
echo '<div class="dbm-sql-result-header">';
|
||||||
|
echo '<i data-lucide="x-circle"></i>';
|
||||||
|
echo htmlspecialchars($result['message']);
|
||||||
|
echo '</div>';
|
||||||
|
if (!empty($result['error'])) {
|
||||||
|
echo '<div class="dbm-sql-result-body" style="padding: 16px; font-family: monospace; font-size: 13px; color: var(--accent-red);">' . htmlspecialchars($result['error']) . '</div>';
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- end dbm-wrapper -->
|
||||||
|
|
||||||
|
</div><!-- end wrapper -->
|
||||||
|
|
||||||
|
<script src="<?php echo Config::get('URL'); ?>js/dbmanager.js"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
lucide.createIcons();
|
||||||
|
DBManager.init('<?php echo Config::get('URL'); ?>');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
107
application/view/_templates/dbmanager_header.php
Normal file
107
application/view/_templates/dbmanager_header.php
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Database Manager - HUGE</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" href="data:;base64,=">
|
||||||
|
<link rel="stylesheet" href="<?php echo Config::get('URL'); ?>css/style.css" />
|
||||||
|
<link rel="stylesheet" href="<?php echo Config::get('URL'); ?>css/dbmanager.css" />
|
||||||
|
<script src="https://unpkg.com/lucide@latest"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrapper dbm-page-wrapper">
|
||||||
|
<?php
|
||||||
|
$uri = trim($_SERVER['REQUEST_URI'], '/');
|
||||||
|
$uri_parts = explode('/', $uri);
|
||||||
|
$current_controller = isset($uri_parts[0]) ? strtolower($uri_parts[0]) : 'database';
|
||||||
|
$is_db_page = in_array($current_controller, ['database', 'table', 'sql']);
|
||||||
|
$is_user_page = ($current_controller === 'dbuser');
|
||||||
|
?>
|
||||||
|
<ul class="navigation">
|
||||||
|
<li><a href="<?php echo Config::get('URL'); ?>index/index">Home</a></li>
|
||||||
|
<li class="<?php echo $is_db_page ? 'active' : ''; ?>"><a href="<?php echo Config::get('URL'); ?>database/index">Database</a></li>
|
||||||
|
<li class="<?php echo $is_user_page ? 'active' : ''; ?>"><a href="<?php echo Config::get('URL'); ?>dbuser/index">Users</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul class="navigation right">
|
||||||
|
<li><a href="<?php echo Config::get('URL'); ?>admin/">Admin</a></li>
|
||||||
|
<li><a href="<?php echo Config::get('URL'); ?>login/logout">Logout</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="dbm-wrapper">
|
||||||
|
<div class="dbm-main">
|
||||||
|
<aside class="dbm-sidebar">
|
||||||
|
<div class="dbm-sidebar-header">
|
||||||
|
<i data-lucide="database" class="icon"></i>
|
||||||
|
<h3>Databases</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="dbm-tree">
|
||||||
|
<?php
|
||||||
|
$all_databases = DatabaseModel::getAllDatabases();
|
||||||
|
$current_database = isset($this->database_name) ? $this->database_name : Config::get('DB_NAME');
|
||||||
|
$current_table = isset($this->table_name) ? $this->table_name : null;
|
||||||
|
|
||||||
|
foreach ($all_databases as $db):
|
||||||
|
$is_current_db = ($db === $current_database);
|
||||||
|
$tables = $is_current_db ? DatabaseModel::getTablesInDatabase($db) : [];
|
||||||
|
?>
|
||||||
|
<div class="tree-item <?php echo $is_current_db ? 'expanded' : ''; ?>" data-db="<?php echo htmlspecialchars($db); ?>">
|
||||||
|
<div class="tree-header <?php echo $is_current_db ? 'active' : ''; ?>" data-href="<?php echo Config::get('URL'); ?>database/show/<?php echo urlencode($db); ?>">
|
||||||
|
<span class="tree-toggle">
|
||||||
|
<i data-lucide="chevron-right"></i>
|
||||||
|
</span>
|
||||||
|
<span class="tree-icon database">
|
||||||
|
<i data-lucide="database"></i>
|
||||||
|
</span>
|
||||||
|
<span class="tree-label"><?php echo htmlspecialchars($db); ?></span>
|
||||||
|
<?php if (!empty($tables)): ?>
|
||||||
|
<span class="tree-badge"><?php echo count($tables); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="tree-children" <?php echo $is_current_db ? 'data-loaded="true"' : ''; ?>>
|
||||||
|
<?php if ($is_current_db && !empty($tables)): ?>
|
||||||
|
<?php foreach ($tables as $table):
|
||||||
|
$is_current_table = ($table === $current_table);
|
||||||
|
$columns = $is_current_table ? TableModel::getTableColumns($db, $table) : [];
|
||||||
|
?>
|
||||||
|
<div class="tree-item <?php echo $is_current_table ? 'expanded' : ''; ?>" data-table="<?php echo htmlspecialchars($table); ?>">
|
||||||
|
<div class="tree-header <?php echo $is_current_table ? 'active' : ''; ?>" data-href="<?php echo Config::get('URL'); ?>table/show/<?php echo urlencode($db); ?>/<?php echo urlencode($table); ?>">
|
||||||
|
<span class="tree-toggle">
|
||||||
|
<i data-lucide="chevron-right"></i>
|
||||||
|
</span>
|
||||||
|
<span class="tree-icon table">
|
||||||
|
<i data-lucide="table"></i>
|
||||||
|
</span>
|
||||||
|
<span class="tree-label"><?php echo htmlspecialchars($table); ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="tree-children">
|
||||||
|
<?php if ($is_current_table && !empty($columns)): ?>
|
||||||
|
<?php foreach ($columns as $col): ?>
|
||||||
|
<div class="tree-item">
|
||||||
|
<div class="tree-header">
|
||||||
|
<span class="tree-icon <?php echo $col['Key'] === 'PRI' ? 'key' : 'column'; ?>">
|
||||||
|
<?php if ($col['Key'] === 'PRI'): ?>
|
||||||
|
<i data-lucide="key-round"></i>
|
||||||
|
<?php else: ?>
|
||||||
|
<i data-lucide="columns-2"></i>
|
||||||
|
<?php endif; ?>
|
||||||
|
</span>
|
||||||
|
<span class="tree-label"><?php echo htmlspecialchars($col['Field']); ?></span>
|
||||||
|
<span class="tree-badge"><?php echo htmlspecialchars($col['Type']); ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="dbm-content">
|
||||||
@@ -80,6 +80,11 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<?php if (Session::get("user_account_type") == 7) : ?>
|
<?php if (Session::get("user_account_type") == 7) : ?>
|
||||||
|
<li <?php if (View::checkForActiveControllers($filename, ['database', 'table', 'sql', 'dbuser'])) {
|
||||||
|
echo ' class="active" ';
|
||||||
|
} ?> >
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>database/index">Database</a>
|
||||||
|
</li>
|
||||||
<li <?php if (View::checkForActiveController($filename, "admin")) {
|
<li <?php if (View::checkForActiveController($filename, "admin")) {
|
||||||
echo ' class="active" ';
|
echo ' class="active" ';
|
||||||
} ?> >
|
} ?> >
|
||||||
|
|||||||
86
application/view/database/index.php
Normal file
86
application/view/database/index.php
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<div class="dbm-content-header">
|
||||||
|
<div class="dbm-breadcrumb">
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>database/index">Databases</a>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-title">
|
||||||
|
<h1>All Databases</h1>
|
||||||
|
<span class="badge"><?php echo count($this->databases); ?> total</span>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-actions">
|
||||||
|
<button type="button" class="dbm-btn dbm-btn-success" onclick="document.getElementById('create-db-modal').style.display='flex'">
|
||||||
|
<i data-lucide="plus"></i>
|
||||||
|
Create Database
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dbm-content-body">
|
||||||
|
<div class="dbm-stats">
|
||||||
|
<div class="dbm-stat">
|
||||||
|
<div class="dbm-stat-value"><?php echo count($this->databases); ?></div>
|
||||||
|
<div class="dbm-stat-label">Databases</div>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-stat">
|
||||||
|
<div class="dbm-stat-value"><?php echo htmlspecialchars($this->current_db); ?></div>
|
||||||
|
<div class="dbm-stat-label">Current Database</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dbm-table-wrapper">
|
||||||
|
<table class="dbm-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Database Name</th>
|
||||||
|
<th>Tables</th>
|
||||||
|
<th style="width: 200px;">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($this->databases as $db):
|
||||||
|
$tables = DatabaseModel::getTablesInDatabase($db);
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>database/show/<?php echo urlencode($db); ?>" style="color: var(--accent-blue); text-decoration: none;">
|
||||||
|
<?php echo htmlspecialchars($db); ?>
|
||||||
|
</a>
|
||||||
|
<?php if ($db === $this->current_db): ?>
|
||||||
|
<span style="margin-left: 8px; font-size: 10px; padding: 2px 6px; background: var(--accent-green); color: #fff; border-radius: 3px;">ACTIVE</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td><?php echo count($tables); ?></td>
|
||||||
|
<td>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>database/show/<?php echo urlencode($db); ?>" class="dbm-btn dbm-btn-sm dbm-btn-secondary">Browse</a>
|
||||||
|
<?php if ($db !== $this->current_db): ?>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>database/delete/<?php echo urlencode($db); ?>"
|
||||||
|
class="dbm-btn dbm-btn-sm dbm-btn-danger"
|
||||||
|
data-confirm="Delete database '<?php echo htmlspecialchars($db); ?>'? This cannot be undone!">Drop</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Create Database Modal -->
|
||||||
|
<div id="create-db-modal" style="display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.8); z-index:1000; align-items:center; justify-content:center;">
|
||||||
|
<div class="dbm-card" style="width: 400px; max-width: 90%;">
|
||||||
|
<div class="dbm-card-header">
|
||||||
|
<h3>Create New Database</h3>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-card-body">
|
||||||
|
<form method="post" action="<?php echo Config::get('URL'); ?>database/create" data-ajax-form>
|
||||||
|
<div class="dbm-form-group">
|
||||||
|
<label class="dbm-form-label">Database Name</label>
|
||||||
|
<input type="text" name="database_name" class="dbm-form-input" required pattern="[a-zA-Z0-9_]+" placeholder="my_database" style="width:100%;max-width:100%;">
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;">
|
||||||
|
<button type="button" class="dbm-btn dbm-btn-secondary" onclick="document.getElementById('create-db-modal').style.display='none'">Cancel</button>
|
||||||
|
<button type="submit" class="dbm-btn dbm-btn-success">Create</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
76
application/view/database/show.php
Normal file
76
application/view/database/show.php
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<div class="dbm-content-header">
|
||||||
|
<div class="dbm-breadcrumb">
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>database/index">Databases</a>
|
||||||
|
<span class="separator">/</span>
|
||||||
|
<span><?php echo htmlspecialchars($this->database_name); ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-title">
|
||||||
|
<h1><?php echo htmlspecialchars($this->database_name); ?></h1>
|
||||||
|
<span class="badge"><?php echo count($this->tables); ?> tables</span>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-actions">
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>table/create/<?php echo urlencode($this->database_name); ?>" class="dbm-btn dbm-btn-success">
|
||||||
|
<i data-lucide="plus"></i>
|
||||||
|
New Table
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>database/export/<?php echo urlencode($this->database_name); ?>" class="dbm-btn dbm-btn-secondary" target="_blank">
|
||||||
|
<i data-lucide="download"></i>
|
||||||
|
Export SQL
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>sql/index/<?php echo urlencode($this->database_name); ?>" class="dbm-btn dbm-btn-secondary">
|
||||||
|
<i data-lucide="terminal"></i>
|
||||||
|
SQL Console
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dbm-content-body">
|
||||||
|
<?php if (empty($this->tables)): ?>
|
||||||
|
<div class="dbm-empty">
|
||||||
|
<i data-lucide="table" class="dbm-empty-icon"></i>
|
||||||
|
<h3>No tables yet</h3>
|
||||||
|
<p>This database is empty. Create your first table to get started.</p>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>table/create/<?php echo urlencode($this->database_name); ?>" class="dbm-btn dbm-btn-success" style="margin-top: 16px;">Create Table</a>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="dbm-table-wrapper">
|
||||||
|
<table class="dbm-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Table Name</th>
|
||||||
|
<th>Engine</th>
|
||||||
|
<th>Rows</th>
|
||||||
|
<th>Size</th>
|
||||||
|
<th>Collation</th>
|
||||||
|
<th style="width: 220px;">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($this->tables as $table):
|
||||||
|
$info = isset($this->table_info[$table]) ? $this->table_info[$table] : [];
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>table/show/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($table); ?>" style="color: var(--accent-blue); text-decoration: none; font-weight: 500;">
|
||||||
|
<?php echo htmlspecialchars($table); ?>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td><span class="type-column"><?php echo isset($info['engine']) ? htmlspecialchars($info['engine']) : '-'; ?></span></td>
|
||||||
|
<td><?php echo isset($info['rows']) ? number_format($info['rows']) : '-'; ?></td>
|
||||||
|
<td><?php echo isset($info['total_size']) ? $info['total_size'] : '-'; ?></td>
|
||||||
|
<td><span style="font-size: 11px;"><?php echo isset($info['collation']) ? htmlspecialchars($info['collation']) : '-'; ?></span></td>
|
||||||
|
<td>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>table/show/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($table); ?>" class="dbm-btn dbm-btn-sm dbm-btn-primary">Browse</a>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>table/structure/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($table); ?>" class="dbm-btn dbm-btn-sm dbm-btn-secondary">Structure</a>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>table/export/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($table); ?>" class="dbm-btn dbm-btn-sm dbm-btn-secondary" target="_blank">Export</a>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>table/delete/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($table); ?>"
|
||||||
|
class="dbm-btn dbm-btn-sm dbm-btn-danger"
|
||||||
|
data-confirm="Drop table '<?php echo htmlspecialchars($table); ?>'? This cannot be undone!">Drop</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
68
application/view/dbuser/create.php
Normal file
68
application/view/dbuser/create.php
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<div class="dbm-content-header">
|
||||||
|
<div class="dbm-breadcrumb">
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>dbuser/index">Users</a>
|
||||||
|
<span class="separator">/</span>
|
||||||
|
<span>Create User</span>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-title">
|
||||||
|
<h1>Create New User</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dbm-content-body">
|
||||||
|
<div class="dbm-card">
|
||||||
|
<div class="dbm-card-body">
|
||||||
|
<form method="post" action="<?php echo Config::get('URL'); ?>dbuser/create">
|
||||||
|
<div class="dbm-form-group">
|
||||||
|
<label class="dbm-form-label">Username</label>
|
||||||
|
<input type="text" name="username" class="dbm-form-input" required pattern="[a-zA-Z0-9_]+" placeholder="username">
|
||||||
|
</div>
|
||||||
|
<div class="dbm-form-group">
|
||||||
|
<label class="dbm-form-label">Password</label>
|
||||||
|
<input type="password" name="password" class="dbm-form-input" required placeholder="password">
|
||||||
|
</div>
|
||||||
|
<div class="dbm-form-group">
|
||||||
|
<label class="dbm-form-label">Host</label>
|
||||||
|
<select name="host" class="dbm-form-select">
|
||||||
|
<option value="localhost">localhost</option>
|
||||||
|
<option value="%">% (any host)</option>
|
||||||
|
<option value="127.0.0.1">127.0.0.1</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dbm-form-group">
|
||||||
|
<label class="dbm-form-label">Privileges</label>
|
||||||
|
<div style="margin-top: 8px; padding: 12px; background: var(--dbm-bg-secondary); border-radius: var(--dbm-radius);">
|
||||||
|
<div style="margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid var(--dbm-border);">
|
||||||
|
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 13px; font-weight: 500; color: var(--dbm-text);">
|
||||||
|
<input type="checkbox" name="privileges[]" value="ALL PRIVILEGES" id="all-privs-check"
|
||||||
|
onchange="document.querySelectorAll('.priv-checkbox').forEach(cb => { cb.checked = this.checked; cb.disabled = this.checked; })">
|
||||||
|
ALL PRIVILEGES (*)
|
||||||
|
</label>
|
||||||
|
<small style="color: var(--dbm-text-muted); font-size: 11px; margin-left: 22px;">Grant all privileges on all databases</small>
|
||||||
|
</div>
|
||||||
|
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 10px;">
|
||||||
|
<?php
|
||||||
|
$all_privileges = ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'CREATE', 'DROP', 'ALTER', 'INDEX', 'REFERENCES', 'CREATE TEMPORARY TABLES', 'LOCK TABLES', 'EXECUTE', 'CREATE VIEW', 'SHOW VIEW', 'CREATE ROUTINE', 'ALTER ROUTINE', 'EVENT', 'TRIGGER'];
|
||||||
|
foreach ($all_privileges as $priv):
|
||||||
|
?>
|
||||||
|
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 12px; color: var(--dbm-text-secondary);">
|
||||||
|
<input type="checkbox" name="privileges[]" value="<?php echo $priv; ?>" class="priv-checkbox">
|
||||||
|
<?php echo $priv; ?>
|
||||||
|
</label>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 20px; display: flex; gap: 8px;">
|
||||||
|
<button type="submit" name="submit_create_user" class="dbm-btn dbm-btn-success">
|
||||||
|
<i data-lucide="plus"></i>
|
||||||
|
Create User
|
||||||
|
</button>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>dbuser/index" class="dbm-btn dbm-btn-secondary">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
92
application/view/dbuser/edit.php
Normal file
92
application/view/dbuser/edit.php
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<div class="dbm-content-header">
|
||||||
|
<div class="dbm-breadcrumb">
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>dbuser/index">Users</a>
|
||||||
|
<span class="separator">/</span>
|
||||||
|
<span><?php echo htmlspecialchars($this->user->User); ?>@<?php echo htmlspecialchars($this->user->Host); ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-title">
|
||||||
|
<h1>Edit User</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dbm-content-body">
|
||||||
|
<form method="post" action="<?php echo Config::get('URL'); ?>dbuser/edit/<?php echo urlencode($this->user->User); ?>/<?php echo urlencode($this->user->Host); ?>">
|
||||||
|
<div class="dbm-card" style="margin-bottom: 20px;">
|
||||||
|
<div class="dbm-card-header">
|
||||||
|
<h3>User Details</h3>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-card-body">
|
||||||
|
<div class="dbm-form-group">
|
||||||
|
<label class="dbm-form-label">Username</label>
|
||||||
|
<input type="text" class="dbm-form-input" value="<?php echo htmlspecialchars($this->user->User); ?>" disabled>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-form-group">
|
||||||
|
<label class="dbm-form-label">Host</label>
|
||||||
|
<input type="text" class="dbm-form-input" value="<?php echo htmlspecialchars($this->user->Host); ?>" disabled>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-form-group">
|
||||||
|
<label class="dbm-form-label">New Password</label>
|
||||||
|
<input type="password" name="password" class="dbm-form-input" placeholder="Leave empty to keep current password">
|
||||||
|
<small style="color: var(--dbm-text-muted); font-size: 11px; display: block; margin-top: 4px;">Only fill this if you want to change the password</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dbm-card" style="margin-bottom: 20px;">
|
||||||
|
<div class="dbm-card-header">
|
||||||
|
<h3>Global Privileges</h3>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-card-body">
|
||||||
|
<?php
|
||||||
|
$all_privileges = ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'CREATE', 'DROP', 'ALTER', 'INDEX', 'REFERENCES', 'CREATE TEMPORARY TABLES', 'LOCK TABLES', 'EXECUTE', 'CREATE VIEW', 'SHOW VIEW', 'CREATE ROUTINE', 'ALTER ROUTINE', 'EVENT', 'TRIGGER'];
|
||||||
|
$current_grants = implode(' ', $this->privileges);
|
||||||
|
$has_all = stripos($current_grants, 'ALL PRIVILEGES') !== false;
|
||||||
|
?>
|
||||||
|
<div style="margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid var(--dbm-border);">
|
||||||
|
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 13px; font-weight: 500; color: var(--dbm-text);">
|
||||||
|
<input type="checkbox" name="privileges[]" value="ALL PRIVILEGES" id="all-privs-check"
|
||||||
|
<?php echo $has_all ? 'checked' : ''; ?>
|
||||||
|
onchange="document.querySelectorAll('.priv-checkbox').forEach(cb => { cb.checked = this.checked; cb.disabled = this.checked; })">
|
||||||
|
ALL PRIVILEGES (*)
|
||||||
|
</label>
|
||||||
|
<small style="color: var(--dbm-text-muted); font-size: 11px; margin-left: 22px;">Grant all privileges on all databases</small>
|
||||||
|
</div>
|
||||||
|
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 10px;">
|
||||||
|
<?php foreach ($all_privileges as $priv): ?>
|
||||||
|
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 12px; color: var(--dbm-text-secondary);">
|
||||||
|
<input type="checkbox" name="privileges[]" value="<?php echo $priv; ?>" class="priv-checkbox"
|
||||||
|
<?php echo ($has_all || stripos($current_grants, $priv) !== false) ? 'checked' : ''; ?>
|
||||||
|
<?php echo $has_all ? 'disabled' : ''; ?>>
|
||||||
|
<?php echo $priv; ?>
|
||||||
|
</label>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dbm-card">
|
||||||
|
<div class="dbm-card-header">
|
||||||
|
<h3>Current Grants</h3>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-card-body">
|
||||||
|
<?php if (!empty($this->privileges)): ?>
|
||||||
|
<?php foreach ($this->privileges as $grant): ?>
|
||||||
|
<div style="font-family: monospace; font-size: 11px; padding: 8px; background: var(--dbm-bg-secondary); border-radius: var(--dbm-radius); margin-bottom: 6px; word-break: break-all;">
|
||||||
|
<?php echo htmlspecialchars($grant); ?>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<p style="color: var(--dbm-text-muted); font-size: 12px; margin: 0;">No grants found</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 20px; display: flex; gap: 8px;">
|
||||||
|
<button type="submit" name="submit_edit_user" class="dbm-btn dbm-btn-success">
|
||||||
|
<i data-lucide="check"></i>
|
||||||
|
Save Changes
|
||||||
|
</button>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>dbuser/index" class="dbm-btn dbm-btn-secondary">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
101
application/view/dbuser/index.php
Normal file
101
application/view/dbuser/index.php
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<div class="dbm-content-header">
|
||||||
|
<div class="dbm-breadcrumb">
|
||||||
|
<span>Users</span>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-title">
|
||||||
|
<h1>MySQL Users</h1>
|
||||||
|
<span class="badge"><?php echo count($this->users); ?> users</span>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-actions">
|
||||||
|
<button type="button" class="dbm-btn dbm-btn-success" onclick="document.getElementById('create-user-modal').style.display='flex'">
|
||||||
|
<i data-lucide="plus"></i>
|
||||||
|
Create User
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dbm-content-body">
|
||||||
|
<div class="dbm-card" style="margin-bottom: 16px;">
|
||||||
|
<div class="dbm-card-body" style="padding: 12px 16px;">
|
||||||
|
<span style="color: var(--dbm-text-muted); font-size: 12px;">Connected as:</span>
|
||||||
|
<strong style="margin-left: 6px; color: var(--dbm-text);"><?php echo htmlspecialchars($this->current_user); ?></strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dbm-table-wrapper">
|
||||||
|
<table class="dbm-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Host</th>
|
||||||
|
<th style="width: 200px;">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if (!empty($this->users)): ?>
|
||||||
|
<?php foreach ($this->users as $user): ?>
|
||||||
|
<tr>
|
||||||
|
<td style="font-weight: 500; color: var(--dbm-text);">
|
||||||
|
<i data-lucide="user" style="width: 12px; height: 12px; margin-right: 6px; color: var(--dbm-text-muted);"></i>
|
||||||
|
<?php echo htmlspecialchars($user->User); ?>
|
||||||
|
<?php if ($user->User === $this->current_user): ?>
|
||||||
|
<span class="badge" style="margin-left: 6px; font-size: 9px;">current</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td><span class="type-column"><?php echo htmlspecialchars($user->Host); ?></span></td>
|
||||||
|
<td>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>dbuser/edit/<?php echo urlencode($user->User); ?>/<?php echo urlencode($user->Host); ?>" class="dbm-btn dbm-btn-sm dbm-btn-secondary">
|
||||||
|
<i data-lucide="pencil" style="width: 11px; height: 11px;"></i>
|
||||||
|
Edit
|
||||||
|
</a>
|
||||||
|
<?php if ($user->User !== $this->current_user): ?>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>dbuser/delete/<?php echo urlencode($user->User); ?>/<?php echo urlencode($user->Host); ?>"
|
||||||
|
class="dbm-btn dbm-btn-sm dbm-btn-danger"
|
||||||
|
data-confirm="Delete user '<?php echo htmlspecialchars($user->User); ?>'@'<?php echo htmlspecialchars($user->Host); ?>'? This cannot be undone!">
|
||||||
|
<i data-lucide="trash-2" style="width: 11px; height: 11px;"></i>
|
||||||
|
Delete
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<tr>
|
||||||
|
<td colspan="3" style="text-align: center; padding: 40px; color: var(--dbm-text-muted);">
|
||||||
|
No users found
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="create-user-modal" class="dbm-modal" style="display: none;">
|
||||||
|
<div class="dbm-modal-content">
|
||||||
|
<div class="dbm-modal-header">
|
||||||
|
<h3>Create New User</h3>
|
||||||
|
<button type="button" class="dbm-modal-close" onclick="this.closest('.dbm-modal').style.display='none'">×</button>
|
||||||
|
</div>
|
||||||
|
<form method="post" action="<?php echo Config::get('URL'); ?>dbuser/create">
|
||||||
|
<div class="dbm-modal-body">
|
||||||
|
<div class="dbm-form-group">
|
||||||
|
<label class="dbm-form-label">Username</label>
|
||||||
|
<input type="text" name="username" class="dbm-form-input" required pattern="[a-zA-Z0-9_]+" placeholder="username">
|
||||||
|
</div>
|
||||||
|
<div class="dbm-form-group">
|
||||||
|
<label class="dbm-form-label">Password</label>
|
||||||
|
<input type="password" name="password" class="dbm-form-input" required placeholder="password">
|
||||||
|
</div>
|
||||||
|
<div class="dbm-form-group">
|
||||||
|
<label class="dbm-form-label">Host</label>
|
||||||
|
<input type="text" name="host" class="dbm-form-input" required value="localhost" placeholder="localhost or %">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-modal-footer">
|
||||||
|
<button type="button" class="dbm-btn dbm-btn-secondary" onclick="this.closest('.dbm-modal').style.display='none'">Cancel</button>
|
||||||
|
<button type="submit" name="submit_create_user" class="dbm-btn dbm-btn-success">Create User</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
37
application/view/dbuser/privileges.php
Normal file
37
application/view/dbuser/privileges.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<div class="dbm-content-header">
|
||||||
|
<div class="dbm-breadcrumb">
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>dbuser/index">Users</a>
|
||||||
|
<span class="separator">/</span>
|
||||||
|
<span><?php echo htmlspecialchars($this->user->User); ?>@<?php echo htmlspecialchars($this->user->Host); ?></span>
|
||||||
|
<span class="separator">/</span>
|
||||||
|
<span>Privileges</span>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-title">
|
||||||
|
<h1>User Privileges</h1>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-actions">
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>dbuser/edit/<?php echo urlencode($this->user->User); ?>/<?php echo urlencode($this->user->Host); ?>" class="dbm-btn dbm-btn-secondary">
|
||||||
|
<i data-lucide="pencil"></i>
|
||||||
|
Edit User
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dbm-content-body">
|
||||||
|
<div class="dbm-card">
|
||||||
|
<div class="dbm-card-header">
|
||||||
|
<h3>Grant Statements</h3>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-card-body">
|
||||||
|
<?php if (!empty($this->privileges)): ?>
|
||||||
|
<?php foreach ($this->privileges as $grant): ?>
|
||||||
|
<div style="font-family: monospace; font-size: 11px; padding: 10px; background: var(--dbm-bg-secondary); border-radius: var(--dbm-radius); margin-bottom: 8px; word-break: break-all; border: 1px solid var(--dbm-border);">
|
||||||
|
<?php echo htmlspecialchars($grant); ?>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<p style="color: var(--dbm-text-muted); font-size: 13px; margin: 0;">No privileges found for this user.</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
282
application/view/sql/index.php
Normal file
282
application/view/sql/index.php
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
<div class="dbmanager-container">
|
||||||
|
<div class="dbmanager-sidebar">
|
||||||
|
<h3>Databases</h3>
|
||||||
|
<ul class="db-tree">
|
||||||
|
<?php foreach ($this->databases as $db): ?>
|
||||||
|
<li class="db-item <?php echo ($db === $this->database_name) ? 'active' : ''; ?>">
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>sql/index/<?php echo urlencode($db); ?>" class="db-link">
|
||||||
|
<?php echo htmlspecialchars($db); ?>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="db-actions">
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>database/index" class="btn" style="display:block;text-align:center;">Back to Databases</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (!empty($this->history)): ?>
|
||||||
|
<div style="margin-top:20px;">
|
||||||
|
<h4>Query History</h4>
|
||||||
|
<ul class="query-history">
|
||||||
|
<?php foreach (array_slice($this->history, 0, 10) as $item): ?>
|
||||||
|
<li class="history-item" onclick="document.getElementById('sql_query').value = this.dataset.query;" data-query="<?php echo htmlspecialchars($item['query_text']); ?>">
|
||||||
|
<?php echo htmlspecialchars(substr($item['query_text'], 0, 40)); ?>...
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>sql/clearHistory" class="btn btn-small btn-danger" style="margin-top:10px;">Clear History</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dbmanager-content">
|
||||||
|
<h2>SQL Console</h2>
|
||||||
|
<p>Database: <strong><?php echo htmlspecialchars($this->database_name); ?></strong></p>
|
||||||
|
|
||||||
|
<form method="post" action="<?php echo Config::get('URL'); ?>sql/execute" id="sql-form">
|
||||||
|
<input type="hidden" name="database_name" value="<?php echo htmlspecialchars($this->database_name); ?>">
|
||||||
|
|
||||||
|
<div class="sql-editor">
|
||||||
|
<textarea name="sql_query" id="sql_query" rows="6" placeholder="Enter your SQL query here...
|
||||||
|
Example: SELECT * FROM users LIMIT 10"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sql-actions">
|
||||||
|
<button type="submit" class="btn">Execute Query</button>
|
||||||
|
<button type="button" class="btn" style="background:#6c757d;" onclick="document.getElementById('sql_query').value = '';">Clear</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="sql-result" class="sql-result">
|
||||||
|
<?php
|
||||||
|
// Check for session result
|
||||||
|
$result = Session::get('sql_result');
|
||||||
|
if ($result) {
|
||||||
|
Session::set('sql_result', null);
|
||||||
|
|
||||||
|
if ($result['success']) {
|
||||||
|
echo '<div class="result-success">';
|
||||||
|
echo '<p>' . htmlspecialchars($result['message']) . '</p>';
|
||||||
|
echo '<p class="execution-time">Execution time: ' . $result['execution_time'] . ' ms</p>';
|
||||||
|
|
||||||
|
if (!empty($result['result'])) {
|
||||||
|
echo '<div class="table-wrapper"><table class="data-table"><thead><tr>';
|
||||||
|
foreach (array_keys($result['result'][0]) as $col) {
|
||||||
|
echo '<th>' . htmlspecialchars($col) . '</th>';
|
||||||
|
}
|
||||||
|
echo '</tr></thead><tbody>';
|
||||||
|
foreach ($result['result'] as $row) {
|
||||||
|
echo '<tr>';
|
||||||
|
foreach ($row as $value) {
|
||||||
|
echo '<td>' . ($value === null ? '<span style="color:#999;">NULL</span>' : htmlspecialchars(substr($value, 0, 100))) . '</td>';
|
||||||
|
}
|
||||||
|
echo '</tr>';
|
||||||
|
}
|
||||||
|
echo '</tbody></table></div>';
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
} else {
|
||||||
|
echo '<div class="result-error">';
|
||||||
|
echo '<p><strong>Error:</strong> ' . htmlspecialchars($result['message']) . '</p>';
|
||||||
|
if (!empty($result['error'])) {
|
||||||
|
echo '<p class="error-details">' . htmlspecialchars($result['error']) . '</p>';
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.dbmanager-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbmanager-sidebar {
|
||||||
|
width: 250px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbmanager-sidebar h3, .dbmanager-sidebar h4 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.db-tree {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.db-item {
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.db-item:hover {
|
||||||
|
background: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.db-item.active {
|
||||||
|
background: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.db-item.active .db-link {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.db-link {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #333;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-history {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item {
|
||||||
|
padding: 5px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #666;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item:hover {
|
||||||
|
background: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.db-actions {
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
padding-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbmanager-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbmanager-content h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sql-editor textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
font-family: 'Consolas', 'Monaco', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
resize: vertical;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sql-actions {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sql-result {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-success {
|
||||||
|
background: #d4edda;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-error {
|
||||||
|
background: #f8d7da;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.execution-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-details {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper {
|
||||||
|
overflow-x: auto;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table th,
|
||||||
|
.data-table td {
|
||||||
|
padding: 8px;
|
||||||
|
text-align: left;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
max-width: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table th {
|
||||||
|
background: #f8f9fa;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8px 16px;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
background: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-small {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: #c82333;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
68
application/view/table/add_column.php
Normal file
68
application/view/table/add_column.php
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<div class="dbm-content-header">
|
||||||
|
<div class="dbm-breadcrumb">
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>database/index">Databases</a>
|
||||||
|
<span class="separator">/</span>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>database/show/<?php echo urlencode($this->database_name); ?>"><?php echo htmlspecialchars($this->database_name); ?></a>
|
||||||
|
<span class="separator">/</span>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>table/structure/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>"><?php echo htmlspecialchars($this->table_name); ?></a>
|
||||||
|
<span class="separator">/</span>
|
||||||
|
<span>Add Column</span>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-title">
|
||||||
|
<h1>Add Column</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dbm-content-body">
|
||||||
|
<div class="dbm-card" style="max-width: 500px;">
|
||||||
|
<div class="dbm-card-body">
|
||||||
|
<form method="post" action="<?php echo Config::get('URL'); ?>table/addColumn/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>">
|
||||||
|
<div class="dbm-form-group">
|
||||||
|
<label class="dbm-form-label">Column Name</label>
|
||||||
|
<input type="text" name="column_name" class="dbm-form-input" required pattern="[a-zA-Z0-9_]+" placeholder="column_name" style="width: 100%; max-width: 100%;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dbm-form-group">
|
||||||
|
<label class="dbm-form-label">Type</label>
|
||||||
|
<select name="column_type" class="dbm-form-select" style="width: 100%; max-width: 100%;">
|
||||||
|
<option value="INT">INT</option>
|
||||||
|
<option value="VARCHAR(255)">VARCHAR(255)</option>
|
||||||
|
<option value="TEXT">TEXT</option>
|
||||||
|
<option value="DATETIME">DATETIME</option>
|
||||||
|
<option value="TIMESTAMP">TIMESTAMP</option>
|
||||||
|
<option value="DECIMAL(10,2)">DECIMAL(10,2)</option>
|
||||||
|
<option value="BOOLEAN">BOOLEAN</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dbm-form-group">
|
||||||
|
<label class="dbm-form-label">Null</label>
|
||||||
|
<select name="column_null" class="dbm-form-select" style="width: 100%; max-width: 100%;">
|
||||||
|
<option value="YES">NULL</option>
|
||||||
|
<option value="NO">NOT NULL</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dbm-form-group">
|
||||||
|
<label class="dbm-form-label">Default Value</label>
|
||||||
|
<input type="text" name="column_default" class="dbm-form-input" placeholder="Leave empty for none" style="width: 100%; max-width: 100%;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dbm-form-group">
|
||||||
|
<label class="dbm-form-label">Extra</label>
|
||||||
|
<select name="column_extra" class="dbm-form-select" style="width: 100%; max-width: 100%;">
|
||||||
|
<option value="">None</option>
|
||||||
|
<option value="auto_increment">AUTO_INCREMENT</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="column_key" value="">
|
||||||
|
|
||||||
|
<div style="margin-top: 24px; display: flex; gap: 10px;">
|
||||||
|
<button type="submit" name="submit_add_column" class="dbm-btn dbm-btn-success">Add Column</button>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>table/structure/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>" class="dbm-btn dbm-btn-secondary">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
122
application/view/table/create.php
Normal file
122
application/view/table/create.php
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<div class="dbm-content-header">
|
||||||
|
<div class="dbm-breadcrumb">
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>database/index">Databases</a>
|
||||||
|
<span class="separator">/</span>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>database/show/<?php echo urlencode($this->database_name); ?>"><?php echo htmlspecialchars($this->database_name); ?></a>
|
||||||
|
<span class="separator">/</span>
|
||||||
|
<span>New Table</span>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-title">
|
||||||
|
<h1>Create New Table</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dbm-content-body">
|
||||||
|
<div class="dbm-card">
|
||||||
|
<div class="dbm-card-body">
|
||||||
|
<form method="post" action="<?php echo Config::get('URL'); ?>table/create/<?php echo urlencode($this->database_name); ?>" id="create-table-form">
|
||||||
|
<div class="dbm-form-group">
|
||||||
|
<label class="dbm-form-label">Table Name</label>
|
||||||
|
<input type="text" name="table_name" class="dbm-form-input" required pattern="[a-zA-Z0-9_]+" placeholder="my_table">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="color: var(--text-primary); margin: 24px 0 16px;">Columns</h3>
|
||||||
|
|
||||||
|
<div id="columns-container">
|
||||||
|
<div class="column-row" style="display: flex; gap: 10px; margin-bottom: 12px; padding: 16px; background: var(--bg-input); border-radius: var(--radius); flex-wrap: wrap; align-items: center;">
|
||||||
|
<input type="text" name="columns[0][name]" class="dbm-form-input" placeholder="Column name" required style="width: 150px; max-width: 150px;">
|
||||||
|
<select name="columns[0][type]" class="dbm-form-select" style="width: 140px; max-width: 140px;">
|
||||||
|
<option value="INT">INT</option>
|
||||||
|
<option value="VARCHAR(255)">VARCHAR(255)</option>
|
||||||
|
<option value="TEXT">TEXT</option>
|
||||||
|
<option value="DATETIME">DATETIME</option>
|
||||||
|
<option value="TIMESTAMP">TIMESTAMP</option>
|
||||||
|
<option value="DECIMAL(10,2)">DECIMAL(10,2)</option>
|
||||||
|
<option value="BOOLEAN">BOOLEAN</option>
|
||||||
|
</select>
|
||||||
|
<select name="columns[0][null]" class="dbm-form-select" style="width: 100px; max-width: 100px;">
|
||||||
|
<option value="YES">NULL</option>
|
||||||
|
<option value="NO">NOT NULL</option>
|
||||||
|
</select>
|
||||||
|
<select name="columns[0][key]" class="dbm-form-select" style="width: 130px; max-width: 130px;">
|
||||||
|
<option value="">No Key</option>
|
||||||
|
<option value="PRI">PRIMARY KEY</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" name="columns[0][default]" class="dbm-form-input" placeholder="Default" style="width: 100px; max-width: 100px;">
|
||||||
|
<select name="columns[0][extra]" class="dbm-form-select" style="width: 140px; max-width: 140px;">
|
||||||
|
<option value="">None</option>
|
||||||
|
<option value="auto_increment">AUTO_INCREMENT</option>
|
||||||
|
</select>
|
||||||
|
<button type="button" class="dbm-btn dbm-btn-sm dbm-btn-danger remove-column" style="display: none;">Remove</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" id="add-column" class="dbm-btn dbm-btn-secondary" style="margin-top: 8px;">
|
||||||
|
<i data-lucide="plus"></i>
|
||||||
|
Add Column
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div style="margin-top: 32px; padding-top: 24px; border-top: 1px solid var(--border-color);">
|
||||||
|
<button type="submit" name="submit_create_table" class="dbm-btn dbm-btn-success">
|
||||||
|
<i data-lucide="check-circle"></i>
|
||||||
|
Create Table
|
||||||
|
</button>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>database/show/<?php echo urlencode($this->database_name); ?>" class="dbm-btn dbm-btn-secondary">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
let columnIndex = 1;
|
||||||
|
|
||||||
|
document.getElementById('add-column').addEventListener('click', function() {
|
||||||
|
const container = document.getElementById('columns-container');
|
||||||
|
const newRow = document.createElement('div');
|
||||||
|
newRow.className = 'column-row';
|
||||||
|
newRow.style.cssText = 'display: flex; gap: 10px; margin-bottom: 12px; padding: 16px; background: var(--bg-input); border-radius: var(--radius); flex-wrap: wrap; align-items: center;';
|
||||||
|
newRow.innerHTML = `
|
||||||
|
<input type="text" name="columns[${columnIndex}][name]" class="dbm-form-input" placeholder="Column name" required style="width: 150px; max-width: 150px;">
|
||||||
|
<select name="columns[${columnIndex}][type]" class="dbm-form-select" style="width: 140px; max-width: 140px;">
|
||||||
|
<option value="INT">INT</option>
|
||||||
|
<option value="VARCHAR(255)">VARCHAR(255)</option>
|
||||||
|
<option value="TEXT">TEXT</option>
|
||||||
|
<option value="DATETIME">DATETIME</option>
|
||||||
|
<option value="TIMESTAMP">TIMESTAMP</option>
|
||||||
|
<option value="DECIMAL(10,2)">DECIMAL(10,2)</option>
|
||||||
|
<option value="BOOLEAN">BOOLEAN</option>
|
||||||
|
</select>
|
||||||
|
<select name="columns[${columnIndex}][null]" class="dbm-form-select" style="width: 100px; max-width: 100px;">
|
||||||
|
<option value="YES">NULL</option>
|
||||||
|
<option value="NO">NOT NULL</option>
|
||||||
|
</select>
|
||||||
|
<select name="columns[${columnIndex}][key]" class="dbm-form-select" style="width: 130px; max-width: 130px;">
|
||||||
|
<option value="">No Key</option>
|
||||||
|
<option value="PRI">PRIMARY KEY</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" name="columns[${columnIndex}][default]" class="dbm-form-input" placeholder="Default" style="width: 100px; max-width: 100px;">
|
||||||
|
<select name="columns[${columnIndex}][extra]" class="dbm-form-select" style="width: 140px; max-width: 140px;">
|
||||||
|
<option value="">None</option>
|
||||||
|
<option value="auto_increment">AUTO_INCREMENT</option>
|
||||||
|
</select>
|
||||||
|
<button type="button" class="dbm-btn dbm-btn-sm dbm-btn-danger remove-column">Remove</button>
|
||||||
|
`;
|
||||||
|
container.appendChild(newRow);
|
||||||
|
columnIndex++;
|
||||||
|
|
||||||
|
document.querySelectorAll('.remove-column').forEach(btn => btn.style.display = 'inline-flex');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('columns-container').addEventListener('click', function(e) {
|
||||||
|
if (e.target.classList.contains('remove-column')) {
|
||||||
|
e.target.closest('.column-row').remove();
|
||||||
|
const rows = document.querySelectorAll('.column-row');
|
||||||
|
if (rows.length === 1) {
|
||||||
|
rows[0].querySelector('.remove-column').style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
435
application/view/table/show.php
Normal file
435
application/view/table/show.php
Normal file
@@ -0,0 +1,435 @@
|
|||||||
|
<?php
|
||||||
|
$total_pages = ceil($this->total_rows / $this->per_page);
|
||||||
|
$pk_column = null;
|
||||||
|
foreach ($this->columns as $col) {
|
||||||
|
if ($col['Key'] === 'PRI') {
|
||||||
|
$pk_column = $col['Field'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="dbm-content-header">
|
||||||
|
<div class="dbm-breadcrumb">
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>database/index">Databases</a>
|
||||||
|
<span class="separator">/</span>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>database/show/<?php echo urlencode($this->database_name); ?>"><?php echo htmlspecialchars($this->database_name); ?></a>
|
||||||
|
<span class="separator">/</span>
|
||||||
|
<span><?php echo htmlspecialchars($this->table_name); ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-title">
|
||||||
|
<h1><?php echo htmlspecialchars($this->table_name); ?></h1>
|
||||||
|
<span class="badge"><?php echo number_format($this->total_rows); ?> rows</span>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-actions">
|
||||||
|
<button type="button" id="btn-add-row" class="dbm-btn dbm-btn-success">
|
||||||
|
<i data-lucide="plus"></i>
|
||||||
|
Add Row
|
||||||
|
</button>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>table/structure/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>" class="dbm-btn dbm-btn-secondary">
|
||||||
|
<i data-lucide="settings"></i>
|
||||||
|
Structure
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>table/export/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>" class="dbm-btn dbm-btn-secondary" target="_blank">
|
||||||
|
<i data-lucide="download"></i>
|
||||||
|
Export
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>table/delete/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>"
|
||||||
|
class="dbm-btn dbm-btn-danger"
|
||||||
|
data-confirm="Drop table '<?php echo htmlspecialchars($this->table_name); ?>'? This cannot be undone!">
|
||||||
|
<i data-lucide="trash-2"></i>
|
||||||
|
Drop Table
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dbm-content-body">
|
||||||
|
<?php if (!empty($this->table_info)): ?>
|
||||||
|
<div class="dbm-stats">
|
||||||
|
<div class="dbm-stat">
|
||||||
|
<div class="dbm-stat-value"><?php echo number_format($this->total_rows); ?></div>
|
||||||
|
<div class="dbm-stat-label">Total Rows</div>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-stat">
|
||||||
|
<div class="dbm-stat-value"><?php echo count($this->columns); ?></div>
|
||||||
|
<div class="dbm-stat-label">Columns</div>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-stat">
|
||||||
|
<div class="dbm-stat-value"><?php echo $this->table_info['total_size'] ?? '-'; ?></div>
|
||||||
|
<div class="dbm-stat-label">Size</div>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-stat">
|
||||||
|
<div class="dbm-stat-value"><?php echo $this->table_info['engine'] ?? '-'; ?></div>
|
||||||
|
<div class="dbm-stat-label">Engine</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="dbm-table-wrapper" id="data-table-wrapper"
|
||||||
|
data-database="<?php echo htmlspecialchars($this->database_name); ?>"
|
||||||
|
data-table="<?php echo htmlspecialchars($this->table_name); ?>"
|
||||||
|
data-pk-column="<?php echo htmlspecialchars($pk_column ?? ''); ?>"
|
||||||
|
style="max-height: 500px; overflow: auto;">
|
||||||
|
<table class="dbm-table" id="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<?php foreach ($this->columns as $col): ?>
|
||||||
|
<th data-column="<?php echo htmlspecialchars($col['Field']); ?>" data-type="<?php echo htmlspecialchars($col['Type']); ?>">
|
||||||
|
<?php if ($col['Key'] === 'PRI'): ?>
|
||||||
|
<span style="color: var(--accent-green);" title="Primary Key">
|
||||||
|
<i data-lucide="key-round" style="width: 12px; height: 12px; vertical-align: middle;"></i>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php echo htmlspecialchars($col['Field']); ?>
|
||||||
|
</th>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<th style="width: 120px; text-align: center;">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if (empty($this->rows)): ?>
|
||||||
|
<tr class="empty-row">
|
||||||
|
<td colspan="<?php echo count($this->columns) + 1; ?>" style="text-align: center; padding: 40px; color: var(--text-muted);">
|
||||||
|
No data in this table. Click "Add Row" to insert data.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($this->rows as $row): ?>
|
||||||
|
<tr class="data-row" data-pk="<?php echo htmlspecialchars($pk_column ? ($row[$pk_column] ?? '') : ''); ?>">
|
||||||
|
<?php foreach ($this->columns as $col): ?>
|
||||||
|
<td class="editable-cell" data-column="<?php echo htmlspecialchars($col['Field']); ?>" data-original="<?php echo htmlspecialchars($row[$col['Field']] ?? ''); ?>">
|
||||||
|
<span class="cell-display"><?php
|
||||||
|
$value = $row[$col['Field']] ?? null;
|
||||||
|
if ($value === null) {
|
||||||
|
echo '<span class="null-value">NULL</span>';
|
||||||
|
} else {
|
||||||
|
$display = htmlspecialchars(substr($value, 0, 100));
|
||||||
|
if (strlen($value) > 100) $display .= '...';
|
||||||
|
echo $display;
|
||||||
|
}
|
||||||
|
?></span>
|
||||||
|
<input type="text" class="cell-input dbm-form-input" style="display: none; width: 100%; padding: 4px 8px; font-size: 13px;" value="<?php echo htmlspecialchars($row[$col['Field']] ?? ''); ?>">
|
||||||
|
</td>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<td class="actions-cell" style="text-align: center; white-space: nowrap;">
|
||||||
|
<div class="view-actions">
|
||||||
|
<button type="button" class="dbm-btn dbm-btn-sm dbm-btn-secondary btn-edit-row" title="Edit">
|
||||||
|
<i data-lucide="pencil" style="width: 12px; height: 12px;"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="dbm-btn dbm-btn-sm dbm-btn-danger btn-delete-row" title="Delete">
|
||||||
|
<i data-lucide="trash-2" style="width: 12px; height: 12px;"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="edit-actions" style="display: none;">
|
||||||
|
<button type="button" class="dbm-btn dbm-btn-sm dbm-btn-success btn-save-row" title="Save">
|
||||||
|
<i data-lucide="check" style="width: 12px; height: 12px;"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="dbm-btn dbm-btn-sm dbm-btn-secondary btn-cancel-edit" title="Cancel">
|
||||||
|
<i data-lucide="x" style="width: 12px; height: 12px;"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($total_pages > 1): ?>
|
||||||
|
<div class="dbm-pagination">
|
||||||
|
<div class="dbm-pagination-info">
|
||||||
|
Showing <?php echo (($this->current_page - 1) * $this->per_page) + 1; ?> - <?php echo min($this->current_page * $this->per_page, $this->total_rows); ?> of <?php echo number_format($this->total_rows); ?>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-pagination-controls">
|
||||||
|
<?php if ($this->current_page > 1): ?>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>table/show/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>/1" class="dbm-pagination-btn">First</a>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>table/show/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>/<?php echo $this->current_page - 1; ?>" class="dbm-pagination-btn">Prev</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$start = max(1, $this->current_page - 2);
|
||||||
|
$end = min($total_pages, $this->current_page + 2);
|
||||||
|
for ($i = $start; $i <= $end; $i++):
|
||||||
|
?>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>table/show/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>/<?php echo $i; ?>"
|
||||||
|
class="dbm-pagination-btn <?php echo $i === $this->current_page ? 'active' : ''; ?>"><?php echo $i; ?></a>
|
||||||
|
<?php endfor; ?>
|
||||||
|
|
||||||
|
<?php if ($this->current_page < $total_pages): ?>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>table/show/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>/<?php echo $this->current_page + 1; ?>" class="dbm-pagination-btn">Next</a>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>table/show/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>/<?php echo $total_pages; ?>" class="dbm-pagination-btn">Last</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const wrapper = document.getElementById('data-table-wrapper');
|
||||||
|
const table = document.getElementById('data-table');
|
||||||
|
const database = wrapper.dataset.database;
|
||||||
|
const tableName = wrapper.dataset.table;
|
||||||
|
const pkColumn = wrapper.dataset.pkColumn;
|
||||||
|
const baseUrl = '<?php echo Config::get('URL'); ?>';
|
||||||
|
|
||||||
|
// Get column info from table headers
|
||||||
|
function getColumns() {
|
||||||
|
const headers = table.querySelectorAll('thead th[data-column]');
|
||||||
|
return Array.from(headers).map(th => ({
|
||||||
|
name: th.dataset.column,
|
||||||
|
type: th.dataset.type
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit row
|
||||||
|
table.addEventListener('click', function(e) {
|
||||||
|
const editBtn = e.target.closest('.btn-edit-row');
|
||||||
|
if (editBtn) {
|
||||||
|
const row = editBtn.closest('tr');
|
||||||
|
enterEditMode(row);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save row
|
||||||
|
table.addEventListener('click', function(e) {
|
||||||
|
const saveBtn = e.target.closest('.btn-save-row');
|
||||||
|
if (saveBtn) {
|
||||||
|
const row = saveBtn.closest('tr');
|
||||||
|
saveRow(row);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cancel edit
|
||||||
|
table.addEventListener('click', function(e) {
|
||||||
|
const cancelBtn = e.target.closest('.btn-cancel-edit');
|
||||||
|
if (cancelBtn) {
|
||||||
|
const row = cancelBtn.closest('tr');
|
||||||
|
cancelEdit(row);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete row
|
||||||
|
table.addEventListener('click', function(e) {
|
||||||
|
const deleteBtn = e.target.closest('.btn-delete-row');
|
||||||
|
if (deleteBtn) {
|
||||||
|
const row = deleteBtn.closest('tr');
|
||||||
|
if (confirm('Delete this row? This cannot be undone.')) {
|
||||||
|
deleteRow(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add new row
|
||||||
|
document.getElementById('btn-add-row').addEventListener('click', function() {
|
||||||
|
addNewRow();
|
||||||
|
});
|
||||||
|
|
||||||
|
function enterEditMode(row) {
|
||||||
|
row.classList.add('editing');
|
||||||
|
row.querySelectorAll('.cell-display').forEach(el => el.style.display = 'none');
|
||||||
|
row.querySelectorAll('.cell-input').forEach(el => el.style.display = 'block');
|
||||||
|
row.querySelector('.view-actions').style.display = 'none';
|
||||||
|
row.querySelector('.edit-actions').style.display = 'inline-flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitEditMode(row) {
|
||||||
|
row.classList.remove('editing');
|
||||||
|
row.querySelectorAll('.cell-display').forEach(el => el.style.display = 'inline');
|
||||||
|
row.querySelectorAll('.cell-input').forEach(el => el.style.display = 'none');
|
||||||
|
row.querySelector('.view-actions').style.display = 'inline-flex';
|
||||||
|
row.querySelector('.edit-actions').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelEdit(row) {
|
||||||
|
if (row.classList.contains('new-row')) {
|
||||||
|
row.remove();
|
||||||
|
// Show empty message if no rows left
|
||||||
|
const remainingRows = table.querySelectorAll('tbody tr.data-row');
|
||||||
|
if (remainingRows.length === 0) {
|
||||||
|
const cols = getColumns();
|
||||||
|
const emptyRow = document.createElement('tr');
|
||||||
|
emptyRow.className = 'empty-row';
|
||||||
|
emptyRow.innerHTML = '<td colspan="' + (cols.length + 1) + '" style="text-align: center; padding: 40px; color: var(--text-muted);">No data in this table. Click "Add Row" to insert data.</td>';
|
||||||
|
table.querySelector('tbody').appendChild(emptyRow);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Restore original values
|
||||||
|
row.querySelectorAll('.editable-cell').forEach(cell => {
|
||||||
|
const original = cell.dataset.original;
|
||||||
|
cell.querySelector('.cell-input').value = original;
|
||||||
|
});
|
||||||
|
exitEditMode(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveRow(row) {
|
||||||
|
const isNew = row.classList.contains('new-row');
|
||||||
|
const pkValue = row.dataset.pk;
|
||||||
|
const data = {};
|
||||||
|
|
||||||
|
row.querySelectorAll('.editable-cell').forEach(cell => {
|
||||||
|
const column = cell.dataset.column;
|
||||||
|
const input = cell.querySelector('.cell-input');
|
||||||
|
data[column] = input.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = isNew
|
||||||
|
? baseUrl + 'table/insertRow/' + encodeURIComponent(database) + '/' + encodeURIComponent(tableName)
|
||||||
|
: baseUrl + 'table/updateRow/' + encodeURIComponent(database) + '/' + encodeURIComponent(tableName);
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
if (!isNew) {
|
||||||
|
formData.append('pk_value', pkValue);
|
||||||
|
}
|
||||||
|
for (const key in data) {
|
||||||
|
formData.append('data[' + key + ']', data[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(result => {
|
||||||
|
if (result.success) {
|
||||||
|
// Update display values
|
||||||
|
row.querySelectorAll('.editable-cell').forEach(cell => {
|
||||||
|
const input = cell.querySelector('.cell-input');
|
||||||
|
const display = cell.querySelector('.cell-display');
|
||||||
|
const value = input.value;
|
||||||
|
cell.dataset.original = value;
|
||||||
|
if (value === '' || value === null) {
|
||||||
|
display.innerHTML = '<span class="null-value">NULL</span>';
|
||||||
|
} else {
|
||||||
|
display.textContent = value.length > 100 ? value.substring(0, 100) + '...' : value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isNew && result.insert_id && pkColumn) {
|
||||||
|
row.dataset.pk = result.insert_id;
|
||||||
|
// Update the PK cell display
|
||||||
|
const pkCell = row.querySelector('.editable-cell[data-column="' + pkColumn + '"]');
|
||||||
|
if (pkCell) {
|
||||||
|
pkCell.querySelector('.cell-input').value = result.insert_id;
|
||||||
|
pkCell.querySelector('.cell-display').textContent = result.insert_id;
|
||||||
|
pkCell.dataset.original = result.insert_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
row.classList.remove('new-row');
|
||||||
|
exitEditMode(row);
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + result.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert('Error saving row: ' + error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteRow(row) {
|
||||||
|
const pkValue = row.dataset.pk;
|
||||||
|
|
||||||
|
if (!pkValue) {
|
||||||
|
alert('Cannot delete: no primary key');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('pk_value', pkValue);
|
||||||
|
|
||||||
|
fetch(baseUrl + 'table/deleteRow/' + encodeURIComponent(database) + '/' + encodeURIComponent(tableName), {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(result => {
|
||||||
|
if (result.success) {
|
||||||
|
row.remove();
|
||||||
|
// Show empty message if no rows left
|
||||||
|
const remainingRows = table.querySelectorAll('tbody tr.data-row');
|
||||||
|
if (remainingRows.length === 0) {
|
||||||
|
const cols = getColumns();
|
||||||
|
const emptyRow = document.createElement('tr');
|
||||||
|
emptyRow.className = 'empty-row';
|
||||||
|
emptyRow.innerHTML = '<td colspan="' + (cols.length + 1) + '" style="text-align: center; padding: 40px; color: var(--text-muted);">No data in this table. Click "Add Row" to insert data.</td>';
|
||||||
|
table.querySelector('tbody').appendChild(emptyRow);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + result.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert('Error deleting row: ' + error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNewRow() {
|
||||||
|
// Remove empty message if present
|
||||||
|
const emptyRow = table.querySelector('tbody tr.empty-row');
|
||||||
|
if (emptyRow) {
|
||||||
|
emptyRow.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = getColumns();
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.className = 'data-row new-row editing';
|
||||||
|
tr.dataset.pk = '';
|
||||||
|
|
||||||
|
columns.forEach(col => {
|
||||||
|
const td = document.createElement('td');
|
||||||
|
td.className = 'editable-cell';
|
||||||
|
td.dataset.column = col.name;
|
||||||
|
td.dataset.original = '';
|
||||||
|
td.innerHTML = `
|
||||||
|
<span class="cell-display" style="display: none;"><span class="null-value">NULL</span></span>
|
||||||
|
<input type="text" class="cell-input dbm-form-input" style="display: block; width: 100%; padding: 4px 8px; font-size: 13px;" value="">
|
||||||
|
`;
|
||||||
|
tr.appendChild(td);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Actions cell
|
||||||
|
const actionsTd = document.createElement('td');
|
||||||
|
actionsTd.className = 'actions-cell';
|
||||||
|
actionsTd.style.textAlign = 'center';
|
||||||
|
actionsTd.style.whiteSpace = 'nowrap';
|
||||||
|
actionsTd.innerHTML = `
|
||||||
|
<div class="view-actions" style="display: none;">
|
||||||
|
<button type="button" class="dbm-btn dbm-btn-sm dbm-btn-secondary btn-edit-row" title="Edit">
|
||||||
|
<i data-lucide="pencil" style="width: 12px; height: 12px;"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="dbm-btn dbm-btn-sm dbm-btn-danger btn-delete-row" title="Delete">
|
||||||
|
<i data-lucide="trash-2" style="width: 12px; height: 12px;"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="edit-actions" style="display: inline-flex;">
|
||||||
|
<button type="button" class="dbm-btn dbm-btn-sm dbm-btn-success btn-save-row" title="Save">
|
||||||
|
<i data-lucide="check" style="width: 12px; height: 12px;"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="dbm-btn dbm-btn-sm dbm-btn-secondary btn-cancel-edit" title="Cancel">
|
||||||
|
<i data-lucide="x" style="width: 12px; height: 12px;"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
lucide.createIcons();
|
||||||
|
tr.appendChild(actionsTd);
|
||||||
|
|
||||||
|
// Insert at the top of tbody
|
||||||
|
const tbody = table.querySelector('tbody');
|
||||||
|
tbody.insertBefore(tr, tbody.firstChild);
|
||||||
|
|
||||||
|
// Focus first input
|
||||||
|
const firstInput = tr.querySelector('.cell-input');
|
||||||
|
if (firstInput) {
|
||||||
|
firstInput.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
112
application/view/table/structure.php
Normal file
112
application/view/table/structure.php
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<div class="dbm-content-header">
|
||||||
|
<div class="dbm-breadcrumb">
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>database/index">Databases</a>
|
||||||
|
<span class="separator">/</span>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>database/show/<?php echo urlencode($this->database_name); ?>"><?php echo htmlspecialchars($this->database_name); ?></a>
|
||||||
|
<span class="separator">/</span>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>table/show/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>"><?php echo htmlspecialchars($this->table_name); ?></a>
|
||||||
|
<span class="separator">/</span>
|
||||||
|
<span>Structure</span>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-title">
|
||||||
|
<h1>Structure: <?php echo htmlspecialchars($this->table_name); ?></h1>
|
||||||
|
<span class="badge"><?php echo count($this->columns); ?> columns</span>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-actions">
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>table/show/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>" class="dbm-btn dbm-btn-primary">
|
||||||
|
<i data-lucide="list"></i>
|
||||||
|
Browse Data
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>table/addColumn/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>" class="dbm-btn dbm-btn-success">
|
||||||
|
<i data-lucide="plus"></i>
|
||||||
|
Add Column
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dbm-content-body">
|
||||||
|
<div class="dbm-card" style="margin-bottom: 24px;">
|
||||||
|
<div class="dbm-card-header">
|
||||||
|
<h3>Columns</h3>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-table-wrapper" style="border: none; border-radius: 0;">
|
||||||
|
<table class="dbm-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Field</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Null</th>
|
||||||
|
<th>Key</th>
|
||||||
|
<th>Default</th>
|
||||||
|
<th>Extra</th>
|
||||||
|
<th style="width: 80px;">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($this->columns as $col): ?>
|
||||||
|
<tr>
|
||||||
|
<td style="font-weight: 500;">
|
||||||
|
<?php if ($col['Key'] === 'PRI'): ?>
|
||||||
|
<span class="key-column">
|
||||||
|
<i data-lucide="key-round" style="width: 12px; height: 12px; vertical-align: middle; margin-right: 4px;"></i>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php echo htmlspecialchars($col['Field']); ?>
|
||||||
|
</td>
|
||||||
|
<td><span class="type-column"><?php echo htmlspecialchars($col['Type']); ?></span></td>
|
||||||
|
<td><?php echo $col['Null'] === 'YES' ? '<span style="color: var(--accent-orange);">YES</span>' : 'NO'; ?></td>
|
||||||
|
<td>
|
||||||
|
<?php if ($col['Key'] === 'PRI'): ?>
|
||||||
|
<span style="color: var(--accent-green);">PRIMARY</span>
|
||||||
|
<?php elseif ($col['Key'] === 'UNI'): ?>
|
||||||
|
<span style="color: var(--accent-blue);">UNIQUE</span>
|
||||||
|
<?php elseif ($col['Key'] === 'MUL'): ?>
|
||||||
|
<span style="color: var(--accent-purple);">INDEX</span>
|
||||||
|
<?php else: ?>
|
||||||
|
-
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td><?php echo $col['Default'] !== null ? htmlspecialchars($col['Default']) : '<span class="null-value">NULL</span>'; ?></td>
|
||||||
|
<td><span class="type-column"><?php echo htmlspecialchars($col['Extra']); ?></span></td>
|
||||||
|
<td>
|
||||||
|
<a href="<?php echo Config::get('URL'); ?>table/dropColumn/<?php echo urlencode($this->database_name); ?>/<?php echo urlencode($this->table_name); ?>/<?php echo urlencode($col['Field']); ?>"
|
||||||
|
class="dbm-btn dbm-btn-sm dbm-btn-danger"
|
||||||
|
data-confirm="Drop column '<?php echo htmlspecialchars($col['Field']); ?>'? This cannot be undone!">Drop</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (!empty($this->indexes)): ?>
|
||||||
|
<div class="dbm-card">
|
||||||
|
<div class="dbm-card-header">
|
||||||
|
<h3>Indexes</h3>
|
||||||
|
</div>
|
||||||
|
<div class="dbm-table-wrapper" style="border: none; border-radius: 0;">
|
||||||
|
<table class="dbm-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Key Name</th>
|
||||||
|
<th>Column</th>
|
||||||
|
<th>Unique</th>
|
||||||
|
<th>Type</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($this->indexes as $idx): ?>
|
||||||
|
<tr>
|
||||||
|
<td style="font-weight: 500;"><?php echo htmlspecialchars($idx['Key_name']); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars($idx['Column_name']); ?></td>
|
||||||
|
<td><?php echo $idx['Non_unique'] ? '<span style="color: var(--text-muted);">No</span>' : '<span style="color: var(--accent-green);">Yes</span>'; ?></td>
|
||||||
|
<td><span class="type-column"><?php echo htmlspecialchars($idx['Index_type']); ?></span></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
945
public/css/dbmanager.css
Normal file
945
public/css/dbmanager.css
Normal file
@@ -0,0 +1,945 @@
|
|||||||
|
:root {
|
||||||
|
--dbm-bg: #ffffff;
|
||||||
|
--dbm-bg-secondary: #fafafa;
|
||||||
|
--dbm-bg-tertiary: #f0f0f0;
|
||||||
|
--dbm-border: #e0e0e0;
|
||||||
|
--dbm-text: #333333;
|
||||||
|
--dbm-text-secondary: #666666;
|
||||||
|
--dbm-text-muted: #999999;
|
||||||
|
--dbm-accent: #555555;
|
||||||
|
--dbm-accent-light: #888888;
|
||||||
|
--dbm-success: #6b9b6b;
|
||||||
|
--dbm-danger: #b57575;
|
||||||
|
--dbm-warning: #a89a6b;
|
||||||
|
--dbm-info: #6b8a9b;
|
||||||
|
--dbm-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||||
|
--dbm-radius: 4px;
|
||||||
|
--dbm-transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-lucide] {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
vertical-align: middle;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-page-wrapper {
|
||||||
|
max-width: 1600px;
|
||||||
|
width: 95%;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: calc(100vh - 120px);
|
||||||
|
background: var(--dbm-bg);
|
||||||
|
border: 1px solid var(--dbm-border);
|
||||||
|
border-radius: var(--dbm-radius);
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 20px 0;
|
||||||
|
box-shadow: var(--dbm-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-main {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-sidebar {
|
||||||
|
width: 260px;
|
||||||
|
background: var(--dbm-bg-secondary);
|
||||||
|
border-right: 1px solid var(--dbm-border);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-sidebar-header {
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-bottom: 1px solid var(--dbm-border);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: var(--dbm-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-sidebar-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--dbm-text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-sidebar-header .icon {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
color: var(--dbm-text-muted);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-sidebar-header .icon svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-tree {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-tree::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-tree::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-tree::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--dbm-border);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-item {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--dbm-transition);
|
||||||
|
border-left: 2px solid transparent;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-header:hover {
|
||||||
|
background: var(--dbm-bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-header.active {
|
||||||
|
background: var(--dbm-bg-tertiary);
|
||||||
|
border-left-color: var(--dbm-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-toggle {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 4px;
|
||||||
|
color: var(--dbm-text-muted);
|
||||||
|
transition: var(--dbm-transition);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-toggle svg,
|
||||||
|
.tree-toggle [data-lucide] {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-item.expanded > .tree-header .tree-toggle svg,
|
||||||
|
.tree-item.expanded > .tree-header .tree-toggle [data-lucide] {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-icon {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
margin-right: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--dbm-text-muted);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-icon [data-lucide] {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-icon.database { color: var(--dbm-accent-light); }
|
||||||
|
.tree-icon.table { color: var(--dbm-accent-light); }
|
||||||
|
.tree-icon.column { color: var(--dbm-text-muted); }
|
||||||
|
.tree-icon.key { color: var(--dbm-success); }
|
||||||
|
|
||||||
|
.tree-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--dbm-text);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
flex: 1;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tree-label:hover {
|
||||||
|
color: var(--dbm-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-badge {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 1px 5px;
|
||||||
|
background: var(--dbm-bg);
|
||||||
|
color: var(--dbm-text-muted);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-left: 6px;
|
||||||
|
border: 1px solid var(--dbm-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-children {
|
||||||
|
display: none;
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-item.expanded > .tree-children {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-children .tree-header {
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-children .tree-children .tree-header {
|
||||||
|
padding-left: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-sidebar-actions {
|
||||||
|
padding: 12px;
|
||||||
|
border-top: 1px solid var(--dbm-border);
|
||||||
|
background: var(--dbm-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--dbm-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-content-header {
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid var(--dbm-border);
|
||||||
|
background: var(--dbm-bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-breadcrumb {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--dbm-text-secondary);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-breadcrumb a {
|
||||||
|
color: var(--dbm-text-secondary);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-breadcrumb a:hover {
|
||||||
|
color: var(--dbm-text);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-breadcrumb .separator {
|
||||||
|
color: var(--dbm-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-title h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--dbm-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-title .badge {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
background: var(--dbm-bg);
|
||||||
|
color: var(--dbm-text-muted);
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid var(--dbm-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-content-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-table-wrapper {
|
||||||
|
background: var(--dbm-bg);
|
||||||
|
border-radius: var(--dbm-radius);
|
||||||
|
border: 1px solid var(--dbm-border);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-table th {
|
||||||
|
background: var(--dbm-bg-secondary);
|
||||||
|
padding: 10px 14px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--dbm-text-secondary);
|
||||||
|
border-bottom: 1px solid var(--dbm-border);
|
||||||
|
white-space: nowrap;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-table td {
|
||||||
|
padding: 8px 14px;
|
||||||
|
color: var(--dbm-text-secondary);
|
||||||
|
border-bottom: 1px solid var(--dbm-border);
|
||||||
|
max-width: 300px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-table tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-table tr:hover td {
|
||||||
|
background: var(--dbm-bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-table .null-value {
|
||||||
|
color: var(--dbm-text-muted);
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-table .key-column {
|
||||||
|
color: var(--dbm-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-table .type-column {
|
||||||
|
color: var(--dbm-text-muted);
|
||||||
|
font-family: 'Consolas', 'Monaco', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--dbm-bg-secondary);
|
||||||
|
border-top: 1px solid var(--dbm-border);
|
||||||
|
margin-top: 16px;
|
||||||
|
border-radius: var(--dbm-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-pagination-info {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--dbm-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-pagination-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-pagination-btn {
|
||||||
|
padding: 5px 10px;
|
||||||
|
background: var(--dbm-bg);
|
||||||
|
border: 1px solid var(--dbm-border);
|
||||||
|
border-radius: var(--dbm-radius);
|
||||||
|
color: var(--dbm-text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--dbm-transition);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-pagination-btn:hover:not(:disabled) {
|
||||||
|
background: var(--dbm-bg-secondary);
|
||||||
|
border-color: var(--dbm-accent-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-pagination-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-pagination-btn.active {
|
||||||
|
background: var(--dbm-accent);
|
||||||
|
border-color: var(--dbm-accent);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-console {
|
||||||
|
background: var(--dbm-bg);
|
||||||
|
border-top: 1px solid var(--dbm-border);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-console-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 16px;
|
||||||
|
background: var(--dbm-bg-secondary);
|
||||||
|
border-bottom: 1px solid var(--dbm-border);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--dbm-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-console-header:hover {
|
||||||
|
background: var(--dbm-bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-console-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--dbm-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-console-title .icon {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
color: var(--dbm-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-console-toggle {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
color: var(--dbm-text-muted);
|
||||||
|
transition: var(--dbm-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-console.expanded .dbm-console-toggle {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-console-body {
|
||||||
|
display: none;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-console.expanded .dbm-console-body {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-sql-editor {
|
||||||
|
position: relative;
|
||||||
|
background: var(--dbm-bg-secondary);
|
||||||
|
border: 1px solid var(--dbm-border);
|
||||||
|
border-radius: var(--dbm-radius);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-sql-editor textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100px;
|
||||||
|
padding: 12px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--dbm-text);
|
||||||
|
font-family: 'Consolas', 'Monaco', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
resize: vertical;
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-sql-editor textarea::placeholder {
|
||||||
|
color: var(--dbm-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-sql-highlight {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 12px;
|
||||||
|
font-family: 'Consolas', 'Monaco', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
pointer-events: none;
|
||||||
|
color: transparent;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sql-keyword { color: #555; font-weight: 600; }
|
||||||
|
.sql-function { color: #666; }
|
||||||
|
.sql-string { color: #888; }
|
||||||
|
.sql-number { color: #777; }
|
||||||
|
.sql-operator { color: #444; }
|
||||||
|
.sql-comment { color: #aaa; font-style: italic; }
|
||||||
|
|
||||||
|
.dbm-sql-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-sql-actions .db-select {
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: var(--dbm-bg);
|
||||||
|
border: 1px solid var(--dbm-border);
|
||||||
|
border-radius: var(--dbm-radius);
|
||||||
|
color: var(--dbm-text);
|
||||||
|
font-size: 12px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-sql-actions .db-select:focus {
|
||||||
|
border-color: var(--dbm-accent-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-sql-result {
|
||||||
|
margin-top: 16px;
|
||||||
|
border-radius: var(--dbm-radius);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-sql-result.success {
|
||||||
|
background: #f5f8f5;
|
||||||
|
border: 1px solid #dde5dd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-sql-result.error {
|
||||||
|
background: #f9f5f5;
|
||||||
|
border: 1px solid #e5dddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-sql-result-header {
|
||||||
|
padding: 10px 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-sql-result-header i {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-sql-result.success .dbm-sql-result-header {
|
||||||
|
color: #5a7a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-sql-result.error .dbm-sql-result-header {
|
||||||
|
color: #8a5a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-sql-result-body {
|
||||||
|
padding: 0 14px 14px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-sql-result-body .dbm-table-wrapper {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 7px 14px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: var(--dbm-radius);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--dbm-transition);
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-btn svg,
|
||||||
|
.dbm-btn i {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-btn-primary {
|
||||||
|
background: var(--dbm-accent);
|
||||||
|
color: #fff;
|
||||||
|
border-color: var(--dbm-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-btn-primary:hover {
|
||||||
|
background: #444;
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-btn-success {
|
||||||
|
background: var(--dbm-success);
|
||||||
|
color: #fff;
|
||||||
|
border-color: var(--dbm-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-btn-success:hover {
|
||||||
|
background: #5a8a5a;
|
||||||
|
border-color: #5a8a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-btn-danger {
|
||||||
|
background: var(--dbm-danger);
|
||||||
|
color: #fff;
|
||||||
|
border-color: var(--dbm-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-btn-danger:hover {
|
||||||
|
background: #a56565;
|
||||||
|
border-color: #a56565;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-btn-secondary {
|
||||||
|
background: var(--dbm-bg);
|
||||||
|
border-color: var(--dbm-border);
|
||||||
|
color: var(--dbm-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-btn-secondary:hover {
|
||||||
|
background: var(--dbm-bg-secondary);
|
||||||
|
border-color: var(--dbm-accent-light);
|
||||||
|
color: var(--dbm-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-btn-sm {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-btn-sm svg,
|
||||||
|
.dbm-btn-sm i {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-form-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-form-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--dbm-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-form-input,
|
||||||
|
.dbm-form-select {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 350px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--dbm-bg);
|
||||||
|
border: 1px solid var(--dbm-border);
|
||||||
|
border-radius: var(--dbm-radius);
|
||||||
|
color: var(--dbm-text);
|
||||||
|
font-size: 13px;
|
||||||
|
outline: none;
|
||||||
|
transition: var(--dbm-transition);
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-form-input:focus,
|
||||||
|
.dbm-form-select:focus {
|
||||||
|
border-color: var(--dbm-accent-light);
|
||||||
|
box-shadow: 0 0 0 2px rgba(85, 85, 85, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-form-input::placeholder {
|
||||||
|
color: var(--dbm-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-card {
|
||||||
|
background: var(--dbm-bg);
|
||||||
|
border: 1px solid var(--dbm-border);
|
||||||
|
border-radius: var(--dbm-radius);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-card-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid var(--dbm-border);
|
||||||
|
background: var(--dbm-bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-card-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--dbm-text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-card-body {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-stat {
|
||||||
|
background: var(--dbm-bg-secondary);
|
||||||
|
border: 1px solid var(--dbm-border);
|
||||||
|
border-radius: var(--dbm-radius);
|
||||||
|
padding: 14px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-stat-value {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--dbm-text);
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-stat-label {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--dbm-text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 50px 20px;
|
||||||
|
color: var(--dbm-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-empty-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin: 0 auto 16px;
|
||||||
|
color: var(--dbm-border);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-empty h3 {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--dbm-text-secondary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-empty p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--dbm-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-spinner {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid var(--dbm-border);
|
||||||
|
border-top-color: var(--dbm-accent-light);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.7s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.dbm-sidebar {
|
||||||
|
width: 220px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.dbm-main {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-sidebar {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 250px;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid var(--dbm-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-content-body {
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-row.editing {
|
||||||
|
background: #fafaf5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-row.new-row {
|
||||||
|
background: #f5faf5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editable-cell {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editable-cell .cell-input {
|
||||||
|
border: 1px solid var(--dbm-accent-light);
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--dbm-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editable-cell .cell-input:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px rgba(85, 85, 85, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-actions,
|
||||||
|
.edit-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-cell .dbm-btn-sm {
|
||||||
|
padding: 4px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-row:hover {
|
||||||
|
background: var(--dbm-bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-row.editing:hover {
|
||||||
|
background: #fafaf5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-row.new-row:hover {
|
||||||
|
background: #f5faf5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-modal-content {
|
||||||
|
background: var(--dbm-bg);
|
||||||
|
border-radius: var(--dbm-radius);
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 18px;
|
||||||
|
border-bottom: 1px solid var(--dbm-border);
|
||||||
|
background: var(--dbm-bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-modal-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--dbm-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-modal-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 20px;
|
||||||
|
color: var(--dbm-text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-modal-close:hover {
|
||||||
|
color: var(--dbm-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-modal-body {
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-modal-body .dbm-form-input,
|
||||||
|
.dbm-modal-body .dbm-form-select {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbm-modal-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 14px 18px;
|
||||||
|
border-top: 1px solid var(--dbm-border);
|
||||||
|
background: var(--dbm-bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
background: var(--dbm-bg-tertiary);
|
||||||
|
color: var(--dbm-text-muted);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--dbm-border);
|
||||||
|
}
|
||||||
389
public/js/dbmanager.js
Normal file
389
public/js/dbmanager.js
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
const DBManager = {
|
||||||
|
baseUrl: '',
|
||||||
|
|
||||||
|
init(baseUrl) {
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
|
this.initTree();
|
||||||
|
this.initConsole();
|
||||||
|
this.initSqlHighlighting();
|
||||||
|
this.initAjaxForms();
|
||||||
|
},
|
||||||
|
|
||||||
|
initTree() {
|
||||||
|
document.querySelectorAll('.tree-header').forEach(header => {
|
||||||
|
header.addEventListener('click', (e) => {
|
||||||
|
const item = header.closest('.tree-item');
|
||||||
|
const children = item.querySelector('.tree-children');
|
||||||
|
const href = header.dataset.href;
|
||||||
|
const toggle = e.target.closest('.tree-toggle');
|
||||||
|
|
||||||
|
// If clicking on toggle icon, just expand/collapse
|
||||||
|
if (toggle) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (item.dataset.db && children && !children.dataset.loaded) {
|
||||||
|
this.loadTables(item.dataset.db, children);
|
||||||
|
}
|
||||||
|
this.toggleTreeItem(item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a table or database item with href, navigate directly
|
||||||
|
if (href) {
|
||||||
|
// For databases, also load tables if not loaded
|
||||||
|
if (item.dataset.db && children && !children.dataset.loaded) {
|
||||||
|
this.loadTables(item.dataset.db, children);
|
||||||
|
}
|
||||||
|
window.location.href = href;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleTreeItem(item) {
|
||||||
|
item.classList.toggle('expanded');
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadTables(dbName, container) {
|
||||||
|
container.innerHTML = '<div class="tree-loading">Loading...</div>';
|
||||||
|
container.dataset.loaded = 'true';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.baseUrl}database/getStructure/${encodeURIComponent(dbName)}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success && data.structure) {
|
||||||
|
let html = '';
|
||||||
|
for (const [table, columns] of Object.entries(data.structure)) {
|
||||||
|
html += this.renderTableTreeItem(dbName, table, columns);
|
||||||
|
}
|
||||||
|
container.innerHTML = html || '<div class="tree-empty">No tables</div>';
|
||||||
|
this.initTree();
|
||||||
|
this.refreshIcons();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
container.innerHTML = '<div class="tree-error">Failed to load</div>';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
renderTableTreeItem(dbName, tableName, columns) {
|
||||||
|
const columnsHtml = columns.map(col => `
|
||||||
|
<div class="tree-item">
|
||||||
|
<div class="tree-header">
|
||||||
|
<span class="tree-icon column">
|
||||||
|
${col.Key === 'PRI' ? this.icons.key : this.icons.column}
|
||||||
|
</span>
|
||||||
|
<span class="tree-label">${this.escapeHtml(col.Field)}</span>
|
||||||
|
<span class="tree-badge">${this.escapeHtml(col.Type)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="tree-item" data-table="${this.escapeHtml(tableName)}">
|
||||||
|
<div class="tree-header" data-href="${this.baseUrl}table/show/${encodeURIComponent(dbName)}/${encodeURIComponent(tableName)}">
|
||||||
|
<span class="tree-toggle">${this.icons.chevron}</span>
|
||||||
|
<span class="tree-icon table">${this.icons.table}</span>
|
||||||
|
<span class="tree-label">${this.escapeHtml(tableName)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tree-children">
|
||||||
|
${columnsHtml}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
|
||||||
|
initConsole() {
|
||||||
|
const consoleHeader = document.querySelector('.dbm-console-header');
|
||||||
|
if (consoleHeader) {
|
||||||
|
consoleHeader.addEventListener('click', () => {
|
||||||
|
const console = consoleHeader.closest('.dbm-console');
|
||||||
|
console.classList.toggle('expanded');
|
||||||
|
localStorage.setItem('dbm-console-expanded', console.classList.contains('expanded'));
|
||||||
|
});
|
||||||
|
|
||||||
|
const wasExpanded = localStorage.getItem('dbm-console-expanded') === 'true';
|
||||||
|
if (wasExpanded) {
|
||||||
|
consoleHeader.closest('.dbm-console').classList.add('expanded');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sqlForm = document.getElementById('sql-form');
|
||||||
|
if (sqlForm) {
|
||||||
|
sqlForm.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
await this.executeQuery(sqlForm);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async executeQuery(form) {
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const resultContainer = document.getElementById('sql-result');
|
||||||
|
|
||||||
|
resultContainer.innerHTML = '<div class="dbm-loading"><div class="dbm-spinner"></div></div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.baseUrl}sql/execute`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
this.renderSqlResult(data, resultContainer);
|
||||||
|
} catch (error) {
|
||||||
|
resultContainer.innerHTML = `
|
||||||
|
<div class="dbm-sql-result error">
|
||||||
|
<div class="dbm-sql-result-header">
|
||||||
|
${this.icons.error} Error: Failed to execute query
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
renderSqlResult(data, container) {
|
||||||
|
if (data.success) {
|
||||||
|
let tableHtml = '';
|
||||||
|
|
||||||
|
if (data.result && data.result.length > 0) {
|
||||||
|
const columns = Object.keys(data.result[0]);
|
||||||
|
tableHtml = `
|
||||||
|
<div class="dbm-table-wrapper">
|
||||||
|
<table class="dbm-table">
|
||||||
|
<thead>
|
||||||
|
<tr>${columns.map(col => `<th>${this.escapeHtml(col)}</th>`).join('')}</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${data.result.map(row => `
|
||||||
|
<tr>${columns.map(col => `
|
||||||
|
<td>${row[col] === null ? '<span class="null-value">NULL</span>' : this.escapeHtml(String(row[col]).substring(0, 100))}</td>
|
||||||
|
`).join('')}</tr>
|
||||||
|
`).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="dbm-sql-result success">
|
||||||
|
<div class="dbm-sql-result-header">
|
||||||
|
${this.icons.success} ${this.escapeHtml(data.message)}
|
||||||
|
<span style="margin-left: auto; color: var(--text-muted); font-size: 12px;">
|
||||||
|
${data.execution_time}ms
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
${tableHtml ? `<div class="dbm-sql-result-body">${tableHtml}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="dbm-sql-result error">
|
||||||
|
<div class="dbm-sql-result-header">
|
||||||
|
${this.icons.error} ${this.escapeHtml(data.message)}
|
||||||
|
</div>
|
||||||
|
${data.error ? `<div class="dbm-sql-result-body" style="padding: 16px; font-family: monospace; font-size: 13px; color: var(--accent-red);">${this.escapeHtml(data.error)}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
this.refreshIcons();
|
||||||
|
},
|
||||||
|
|
||||||
|
initSqlHighlighting() {
|
||||||
|
const textarea = document.getElementById('sql_query');
|
||||||
|
if (!textarea) return;
|
||||||
|
|
||||||
|
textarea.addEventListener('input', () => this.highlightSql(textarea));
|
||||||
|
textarea.addEventListener('scroll', () => this.syncScroll(textarea));
|
||||||
|
this.highlightSql(textarea);
|
||||||
|
},
|
||||||
|
|
||||||
|
highlightSql(textarea) {
|
||||||
|
const highlight = document.getElementById('sql-highlight');
|
||||||
|
if (!highlight) return;
|
||||||
|
|
||||||
|
let code = textarea.value;
|
||||||
|
code = this.escapeHtml(code);
|
||||||
|
code = this.applySqlSyntax(code);
|
||||||
|
highlight.innerHTML = code + '\n';
|
||||||
|
},
|
||||||
|
|
||||||
|
applySqlSyntax(code) {
|
||||||
|
const keywords = [
|
||||||
|
'SELECT', 'FROM', 'WHERE', 'AND', 'OR', 'NOT', 'IN', 'LIKE', 'BETWEEN',
|
||||||
|
'ORDER BY', 'GROUP BY', 'HAVING', 'LIMIT', 'OFFSET', 'JOIN', 'INNER JOIN',
|
||||||
|
'LEFT JOIN', 'RIGHT JOIN', 'OUTER JOIN', 'ON', 'AS', 'DISTINCT', 'ALL',
|
||||||
|
'INSERT', 'INTO', 'VALUES', 'UPDATE', 'SET', 'DELETE', 'CREATE', 'TABLE',
|
||||||
|
'DATABASE', 'INDEX', 'VIEW', 'DROP', 'ALTER', 'ADD', 'COLUMN', 'PRIMARY KEY',
|
||||||
|
'FOREIGN KEY', 'REFERENCES', 'CONSTRAINT', 'DEFAULT', 'NULL', 'NOT NULL',
|
||||||
|
'AUTO_INCREMENT', 'UNIQUE', 'ENGINE', 'CHARSET', 'COLLATE', 'IF', 'EXISTS',
|
||||||
|
'SHOW', 'DESCRIBE', 'EXPLAIN', 'USE', 'GRANT', 'REVOKE', 'UNION', 'CASE',
|
||||||
|
'WHEN', 'THEN', 'ELSE', 'END', 'IS', 'TRUE', 'FALSE', 'ASC', 'DESC'
|
||||||
|
];
|
||||||
|
|
||||||
|
const functions = [
|
||||||
|
'COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'CONCAT', 'SUBSTRING', 'LENGTH',
|
||||||
|
'UPPER', 'LOWER', 'TRIM', 'REPLACE', 'NOW', 'CURDATE', 'DATE', 'YEAR',
|
||||||
|
'MONTH', 'DAY', 'HOUR', 'MINUTE', 'COALESCE', 'IFNULL', 'NULLIF', 'CAST',
|
||||||
|
'CONVERT', 'FORMAT', 'ROUND', 'FLOOR', 'CEIL', 'ABS', 'MOD', 'RAND'
|
||||||
|
];
|
||||||
|
|
||||||
|
code = code.replace(/'([^'\\]|\\.)*'/g, '<span class="sql-string">$&</span>');
|
||||||
|
code = code.replace(/"([^"\\]|\\.)*"/g, '<span class="sql-string">$&</span>');
|
||||||
|
code = code.replace(/\b(\d+\.?\d*)\b/g, '<span class="sql-number">$1</span>');
|
||||||
|
|
||||||
|
functions.forEach(func => {
|
||||||
|
const regex = new RegExp(`\\b(${func})\\s*\\(`, 'gi');
|
||||||
|
code = code.replace(regex, '<span class="sql-function">$1</span>(');
|
||||||
|
});
|
||||||
|
|
||||||
|
keywords.forEach(keyword => {
|
||||||
|
const regex = new RegExp(`\\b(${keyword.replace(' ', '\\s+')})\\b`, 'gi');
|
||||||
|
code = code.replace(regex, '<span class="sql-keyword">$1</span>');
|
||||||
|
});
|
||||||
|
|
||||||
|
code = code.replace(/(--[^\n]*)/g, '<span class="sql-comment">$1</span>');
|
||||||
|
code = code.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="sql-comment">$1</span>');
|
||||||
|
|
||||||
|
return code;
|
||||||
|
},
|
||||||
|
|
||||||
|
syncScroll(textarea) {
|
||||||
|
const highlight = document.getElementById('sql-highlight');
|
||||||
|
if (highlight) {
|
||||||
|
highlight.scrollTop = textarea.scrollTop;
|
||||||
|
highlight.scrollLeft = textarea.scrollLeft;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
initAjaxForms() {
|
||||||
|
document.querySelectorAll('[data-ajax-form]').forEach(form => {
|
||||||
|
form.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
await this.submitAjaxForm(form);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-confirm]').forEach(el => {
|
||||||
|
el.addEventListener('click', (e) => {
|
||||||
|
if (!confirm(el.dataset.confirm)) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async submitAjaxForm(form) {
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const submitBtn = form.querySelector('[type="submit"]');
|
||||||
|
const originalText = submitBtn?.innerHTML;
|
||||||
|
|
||||||
|
if (submitBtn) {
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.innerHTML = '<span class="dbm-spinner" style="width:16px;height:16px;border-width:2px;"></span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(form.action, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
if (data.redirect) {
|
||||||
|
window.location.href = data.redirect;
|
||||||
|
} else if (data.reload) {
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
this.showNotification(data.message, 'success');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.showNotification(data.message || 'An error occurred', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.showNotification('Request failed', 'error');
|
||||||
|
} finally {
|
||||||
|
if (submitBtn) {
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.innerHTML = originalText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
showNotification(message, type) {
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `dbm-notification ${type}`;
|
||||||
|
notification.innerHTML = `
|
||||||
|
${type === 'success' ? this.icons.success : this.icons.error}
|
||||||
|
<span>${this.escapeHtml(message)}</span>
|
||||||
|
`;
|
||||||
|
notification.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
padding: 12px 20px;
|
||||||
|
background: ${type === 'success' ? 'var(--accent-green)' : 'var(--accent-red)'};
|
||||||
|
color: #fff;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
z-index: 1000;
|
||||||
|
animation: slideIn 0.3s ease;
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
this.refreshIcons();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.animation = 'slideOut 0.3s ease';
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
|
},
|
||||||
|
|
||||||
|
escapeHtml(str) {
|
||||||
|
if (!str) return '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = str;
|
||||||
|
return div.innerHTML;
|
||||||
|
},
|
||||||
|
|
||||||
|
icons: {
|
||||||
|
chevron: '<i data-lucide="chevron-right"></i>',
|
||||||
|
database: '<i data-lucide="database"></i>',
|
||||||
|
table: '<i data-lucide="table"></i>',
|
||||||
|
column: '<i data-lucide="columns-2"></i>',
|
||||||
|
key: '<i data-lucide="key-round"></i>',
|
||||||
|
success: '<i data-lucide="check-circle"></i>',
|
||||||
|
error: '<i data-lucide="x-circle"></i>',
|
||||||
|
terminal: '<i data-lucide="terminal"></i>'
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshIcons() {
|
||||||
|
if (typeof lucide !== 'undefined') {
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
@keyframes slideIn {
|
||||||
|
from { transform: translateX(100%); opacity: 0; }
|
||||||
|
to { transform: translateX(0); opacity: 1; }
|
||||||
|
}
|
||||||
|
@keyframes slideOut {
|
||||||
|
from { transform: translateX(0); opacity: 1; }
|
||||||
|
to { transform: translateX(100%); opacity: 0; }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
Reference in New Issue
Block a user