feat(query-runner): helpers DBA para conexiones SQL

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Lizandro Guarnizo
2026-05-20 11:14:11 -05:00
co-authored by Copilot
+120
View File
@@ -174,6 +174,83 @@
class="w-full bg-gray-900 text-gray-100 font-mono text-sm p-4 resize-none outline-none leading-relaxed"
style="height:180px; tab-size:2;"></textarea>
</div>
<!-- ─── Helpers SQL (solo MySQL/PostgreSQL) ─── -->
<div x-show="!isMongo && selectedConxId" class="mt-1.5">
<!-- Chips rápidos -->
<div class="flex flex-wrap gap-1 mb-1.5">
<template x-for="h in sqlHelpers" :key="h.label">
<button @click="sqlText = h.cmd" class="text-[10px] px-2 py-0.5 rounded bg-gray-100 hover:bg-gray-200 text-gray-600 font-mono transition" x-text="h.label"></button>
</template>
</div>
<!-- ══ Generador DBA ══ -->
<div class="border rounded-lg overflow-hidden">
<button @click="showDbHelper = !showDbHelper"
class="w-full flex items-center gap-1.5 px-3 py-1.5 bg-gray-50 hover:bg-[#e9f0cf] text-xs text-gray-600 transition text-left select-none">
<svg class="w-3 h-3 transition-transform duration-150" :class="showDbHelper ? 'rotate-90' : ''"
fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/>
</svg>
<span class="font-semibold">Generador DBA</span>
<span class="ml-auto text-[10px] text-gray-400">CREATE · GRANT · DROP · Setup</span>
</button>
<div x-show="showDbHelper" x-transition class="p-3 bg-white border-t">
<div class="grid grid-cols-2 gap-2 text-xs">
<div>
<label class="text-[10px] text-gray-500 font-semibold uppercase block mb-0.5">Nombre BD</label>
<input x-model="dbHelper.db" list="dba-dbs"
class="w-full border rounded px-2 py-1 font-mono focus:outline-none focus:ring-1 focus:ring-[#8eb02f]"
placeholder="mi_base_datos">
<datalist id="dba-dbs">
<template x-for="d in databases" :key="d"><option :value="d"></option></template>
</datalist>
</div>
<div>
<label class="text-[10px] text-gray-500 font-semibold uppercase block mb-0.5">Operación</label>
<select x-model="dbHelper.op" class="w-full border rounded px-2 py-1 focus:outline-none focus:ring-1 focus:ring-[#8eb02f]">
<option value="full_setup">✦ Setup completo (BD + usuario + permisos)</option>
<option value="create_db">Crear base de datos</option>
<option value="create_user">Crear usuario</option>
<option value="grant">Dar permisos totales (GRANT ALL)</option>
<option value="revoke">Revocar permisos</option>
<option value="show_grants">Ver permisos de usuario</option>
<option value="drop_user">Eliminar usuario</option>
<option value="drop_db">Eliminar base de datos</option>
</select>
</div>
<div x-show="['full_setup','create_user','grant','revoke','show_grants','drop_user'].includes(dbHelper.op)">
<label class="text-[10px] text-gray-500 font-semibold uppercase block mb-0.5">Usuario</label>
<input x-model="dbHelper.user"
class="w-full border rounded px-2 py-1 font-mono focus:outline-none focus:ring-1 focus:ring-[#8eb02f]"
placeholder="usuario_app">
</div>
<div x-show="['full_setup','create_user','grant','revoke','show_grants','drop_user'].includes(dbHelper.op)">
<label class="text-[10px] text-gray-500 font-semibold uppercase block mb-0.5">Host</label>
<input x-model="dbHelper.host"
class="w-full border rounded px-2 py-1 font-mono focus:outline-none focus:ring-1 focus:ring-[#8eb02f]"
placeholder="%">
</div>
<div x-show="['full_setup','create_user'].includes(dbHelper.op)" class="col-span-2">
<label class="text-[10px] text-gray-500 font-semibold uppercase block mb-0.5">Contraseña</label>
<input x-model="dbHelper.pass" type="text"
class="w-full border rounded px-2 py-1 font-mono focus:outline-none focus:ring-1 focus:ring-[#8eb02f]"
placeholder="Contraseña segura">
<p class="text-[9px] text-gray-400 mt-0.5">Se insertará tal cual en el SQL generado.</p>
</div>
</div>
<div class="flex items-center gap-2 mt-2.5 pt-2.5 border-t">
<button @click="generateDbHelper()"
class="flex-1 py-1.5 text-xs text-white font-semibold rounded transition"
style="background-color:#8eb02f"
onmouseover="this.style.backgroundColor='#6d8c24'"
onmouseout="this.style.backgroundColor='#8eb02f'">
↗ Generar en editor
</button>
</div>
</div>
</div>
</div>
<!-- Ayuda rápida MongoDB -->
<div x-show="isMongo" class="mt-1.5">
<div class="flex flex-wrap gap-1 mb-1.5">
@@ -369,6 +446,20 @@ document.addEventListener('alpine:init', () => {
loadingFields: false,
showUpdateBuilder: false,
updateBuilder: { collection: '', field: '', value: '', filterField: '_id', filterValue: '""', op: '$set', multi: 'one' },
// SQL DBA helpers
showDbHelper: false,
dbHelper: { db: '', user: '', pass: '', host: '%', op: 'full_setup' },
sqlHelpers: [
{ label: 'SHOW DATABASES', cmd: 'SHOW DATABASES;' },
{ label: 'SHOW TABLES', cmd: 'SHOW TABLES;' },
{ label: 'SHOW USERS', cmd: "SELECT User, Host, plugin FROM mysql.user ORDER BY User;" },
{ label: 'SHOW GRANTS (actual)',cmd: 'SHOW GRANTS FOR CURRENT_USER;' },
{ label: 'SHOW PROCESSLIST', cmd: 'SHOW FULL PROCESSLIST;' },
{ label: 'SHOW CREATE TABLE', cmd: 'SHOW CREATE TABLE nombre_tabla;' },
{ label: 'DESC tabla', cmd: 'DESCRIBE nombre_tabla;' },
{ label: 'ENGINE/CHARSET', cmd: "SELECT table_name, engine, table_collation FROM information_schema.tables WHERE table_schema = DATABASE();" },
],
mongoExamples: [
{ label: 'find()', cmd: 'db.coleccion.find({})' },
{ label: 'findOne()', cmd: 'db.coleccion.findOne({})' },
@@ -603,6 +694,35 @@ document.addEventListener('alpine:init', () => {
this.sqlText = `db.${col}.${method}(\n { "${filterField}": ${filterValue} },\n { "${op}": ${updateDoc} }\n)`;
},
generateDbHelper() {
const db = this.dbHelper.db || 'nombre_bd';
const user = this.dbHelper.user || 'usuario_app';
const host = this.dbHelper.host || '%';
const pass = this.dbHelper.pass || 'Contraseña123!';
const createDb = `CREATE DATABASE IF NOT EXISTS \`${db}\`\n CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;`;
const createUser = `CREATE USER IF NOT EXISTS '${user}'@'${host}' IDENTIFIED BY '${pass}';`;
const grant = `GRANT ALL PRIVILEGES ON \`${db}\`.* TO '${user}'@'${host}';\nFLUSH PRIVILEGES;`;
const revoke = `REVOKE ALL PRIVILEGES ON \`${db}\`.* FROM '${user}'@'${host}';\nFLUSH PRIVILEGES;`;
const showGrants = `SHOW GRANTS FOR '${user}'@'${host}';`;
const dropUser = `DROP USER IF EXISTS '${user}'@'${host}';\nFLUSH PRIVILEGES;`;
const dropDb = `DROP DATABASE IF EXISTS \`${db}\`;`;
const map = {
full_setup: `-- 1. Crear base de datos\n${createDb}\n\n-- 2. Crear usuario\n${createUser}\n\n-- 3. Dar permisos totales\n${grant}`,
create_db: createDb,
create_user: createUser,
grant: grant,
revoke: revoke,
show_grants: showGrants,
drop_user: dropUser,
drop_db: dropDb,
};
this.sqlText = map[this.dbHelper.op] || '';
document.getElementById('sql-editor')?.focus();
},
clearEditor() { this.sqlText = ''; this.results = []; this.columns = []; this.statusMsg = ''; },
nullStr(v) { return v === null || v === undefined ? 'NULL' : String(v); },