This commit is contained in:
Lizandro Guarnizo
2026-05-01 22:55:51 -05:00
parent ddbabea23f
commit 560359173b
8 changed files with 755 additions and 80 deletions
+217 -19
View File
@@ -44,20 +44,68 @@
</select>
</div>
<!-- Árbol de tablas / colecciones -->
<div class="flex-1 overflow-y-auto p-2" x-show="tables.length > 0">
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wide px-1 mb-1" x-text="isMongo ? 'Colecciones' : 'Tablas'"></p>
<template x-for="t in tables" :key="t">
<button @click="insertTable(t)"
class="w-full text-left text-xs px-2 py-1 rounded hover:bg-[#e9f0cf] text-gray-700 truncate flex items-center gap-1">
<svg class="w-3 h-3 shrink-0 text-[#8eb02f]" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/>
<line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/>
</svg>
<span x-text="t"></span>
</button>
</template>
<!-- Árbol de tablas / colecciones + visor de campos -->
<div class="flex-1 flex flex-col overflow-hidden" x-show="tables.length > 0">
<div class="flex-1 overflow-y-auto p-2">
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wide px-1 mb-1" x-text="isMongo ? 'Colecciones' : 'Tablas'"></p>
<template x-for="t in tables" :key="t">
<div class="flex items-center group mb-0.5">
<button @click="insertTable(t)"
class="flex-1 text-left text-xs px-2 py-1 rounded-l hover:bg-[#e9f0cf] text-gray-700 truncate flex items-center gap-1">
<svg class="w-3 h-3 shrink-0 text-[#8eb02f]" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/>
<line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/>
</svg>
<span x-text="t"></span>
</button>
<!-- Botón ver campos (solo MongoDB) -->
<button x-show="isMongo" @click.stop="loadCollectionFields(t)"
class="shrink-0 px-1.5 py-1 rounded-r hover:bg-[#d4e89c] text-gray-400 transition"
:class="selectedCollection === t ? 'bg-[#d4e89c] !text-[#5a7a1e] opacity-100' : 'opacity-0 group-hover:opacity-100'"
title="Ver campos de la colección">
<svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 10h16M4 14h16M4 18h7"/>
</svg>
</button>
</div>
</template>
</div>
<!-- Visor de campos de colección (MongoDB) -->
<div x-show="isMongo && selectedCollection" class="border-t shrink-0 bg-white flex flex-col" style="max-height:220px">
<div class="flex items-center justify-between px-2 py-1.5 bg-[#e9f0cf] border-b shrink-0">
<div class="flex items-center gap-1.5 min-w-0">
<svg class="w-3 h-3 shrink-0 text-[#5a7a1e]" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
<span class="text-[10px] font-semibold text-[#4a6e18] font-mono truncate" x-text="selectedCollection"></span>
</div>
<div class="flex items-center gap-1 shrink-0 ml-1">
<span x-show="loadingFields" class="text-[10px] text-gray-500"></span>
<span x-show="!loadingFields && collectionFields.length" class="text-[10px] text-gray-500" x-text="collectionFields.length + ' campos'"></span>
<button @click="selectedCollection=''; collectionFields=[]" class="text-gray-400 hover:text-gray-700 ml-1 text-xs leading-none" title="Cerrar"></button>
</div>
</div>
<div class="overflow-y-auto flex-1">
<div x-show="loadingFields && !collectionFields.length" class="px-3 py-2 text-xs text-gray-400">Cargando campos…</div>
<div x-show="!loadingFields && !collectionFields.length && selectedCollection" class="px-3 py-2 text-xs text-gray-400">Sin documentos en esta colección.</div>
<template x-for="f in collectionFields" :key="f.name">
<div class="flex items-center gap-1 px-2 py-1.5 hover:bg-[#f0f7d8] group border-b border-gray-50">
<div class="flex-1 min-w-0">
<div class="font-mono text-xs text-gray-800 truncate" x-text="f.name"></div>
<div class="font-mono text-[9px] text-gray-400 truncate" x-text="f.preview"></div>
</div>
<div class="shrink-0 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition">
<span class="text-[9px] px-1 py-0.5 rounded bg-gray-100 text-gray-500" x-text="f.type"></span>
<button @click="buildUpdateField(f.name)"
class="text-[10px] px-1.5 py-0.5 rounded text-white"
style="background-color:#8eb02f" title="Generar updateOne para este campo"></button>
</div>
</div>
</template>
</div>
</div>
</div>
<!-- Mensaje sin conexión -->
@@ -127,10 +175,90 @@
style="height:180px; tab-size:2;"></textarea>
</div>
<!-- Ayuda rápida MongoDB -->
<div x-show="isMongo" class="mt-1.5 flex flex-wrap gap-1">
<template x-for="ex in mongoExamples" :key="ex.label">
<button @click="sqlText = ex.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="ex.label"></button>
</template>
<div x-show="isMongo" class="mt-1.5">
<div class="flex flex-wrap gap-1 mb-1.5">
<template x-for="ex in mongoExamples" :key="ex.label">
<button @click="sqlText = ex.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="ex.label"></button>
</template>
</div>
<!-- ══ Generador de actualización rápida ══ -->
<div class="border rounded-lg overflow-hidden">
<button @click="showUpdateBuilder = !showUpdateBuilder"
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="showUpdateBuilder ? '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 de actualización</span>
<span class="ml-auto text-[10px] text-gray-400">updateOne / updateMany</span>
</button>
<div x-show="showUpdateBuilder" 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">Colección</label>
<input x-model="updateBuilder.collection" list="ub-collections"
class="w-full border rounded px-2 py-1 font-mono focus:outline-none focus:ring-1 focus:ring-[#8eb02f]"
:placeholder="selectedCollection || 'coleccion'">
<datalist id="ub-collections">
<template x-for="t in tables" :key="t"><option :value="t"></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="updateBuilder.op" class="w-full border rounded px-2 py-1 focus:outline-none focus:ring-1 focus:ring-[#8eb02f]">
<option value="$set">$set — asignar valor</option>
<option value="$unset">$unset — eliminar campo</option>
<option value="$inc">$inc — incrementar número</option>
<option value="$push">$push — añadir a array</option>
<option value="$pull">$pull — quitar de array</option>
<option value="$rename">$rename — renombrar campo</option>
</select>
</div>
<div>
<label class="text-[10px] text-gray-500 font-semibold uppercase block mb-0.5">Campo a actualizar</label>
<input x-model="updateBuilder.field" list="ub-fields"
class="w-full border rounded px-2 py-1 font-mono focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" placeholder="nombre_campo">
<datalist id="ub-fields">
<template x-for="f in collectionFields" :key="f.name"><option :value="f.name"></option></template>
</datalist>
</div>
<div>
<label class="text-[10px] text-gray-500 font-semibold uppercase block mb-0.5">
<span x-text="updateBuilder.op === '$rename' ? 'Nuevo nombre' : 'Nuevo valor'"></span>
</label>
<input x-model="updateBuilder.value"
class="w-full border rounded px-2 py-1 font-mono focus:outline-none focus:ring-1 focus:ring-[#8eb02f]"
:placeholder="updateBuilder.op === '$inc' ? '1' : updateBuilder.op === '$rename' ? 'nuevo_nombre' : '&quot;valor&quot;'"
x-show="updateBuilder.op !== '$unset'">
<span x-show="updateBuilder.op === '$unset'" class="text-[10px] text-gray-400 block py-1.5 italic">El campo será eliminado del documento.</span>
</div>
<div>
<label class="text-[10px] text-gray-500 font-semibold uppercase block mb-0.5">Filtro — campo</label>
<input x-model="updateBuilder.filterField" list="ub-fields"
class="w-full border rounded px-2 py-1 font-mono focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" placeholder="_id">
</div>
<div>
<label class="text-[10px] text-gray-500 font-semibold uppercase block mb-0.5">Filtro — valor</label>
<input x-model="updateBuilder.filterValue"
class="w-full border rounded px-2 py-1 font-mono focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" placeholder='"id_del_doc"'>
</div>
</div>
<div class="flex items-center gap-2 mt-2.5 pt-2.5 border-t">
<select x-model="updateBuilder.multi" class="border rounded px-2 py-1.5 text-xs focus:outline-none focus:ring-1 focus:ring-[#8eb02f]">
<option value="one">updateOne</option>
<option value="many">updateMany</option>
</select>
<button @click="generateUpdate()"
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>
</div>
@@ -233,12 +361,22 @@ document.addEventListener('alpine:init', () => {
testOk: true,
toast: { show: false, msg: '', type: 'ok' },
isMongo: false,
selectedCollection: '',
collectionFields: [],
loadingFields: false,
showUpdateBuilder: false,
updateBuilder: { collection: '', field: '', value: '', filterField: '_id', filterValue: '""', op: '$set', multi: 'one' },
mongoExamples: [
{ label: 'find()', cmd: 'db.coleccion.find({})' },
{ label: 'findOne()', cmd: 'db.coleccion.findOne({})' },
{ label: 'count()', cmd: 'db.coleccion.countDocuments({})' },
{ label: 'insertOne()', cmd: 'db.coleccion.insertOne({"campo": "valor"})' },
{ label: 'updateOne()', cmd: 'db.coleccion.updateOne({"_id": ""}, {"$set": {"campo": "valor"}})' },
{ label: 'updateOne()', cmd: 'db.coleccion.updateOne(\n {"_id": ""},\n {"$set": {"campo": "valor"}}\n)' },
{ label: 'updateMany()', cmd: 'db.coleccion.updateMany(\n {},\n {"$set": {"campo": "valor"}}\n)' },
{ label: '$unset', cmd: 'db.coleccion.updateOne(\n {"_id": ""},\n {"$unset": {"campo": ""}}\n)' },
{ label: '$inc', cmd: 'db.coleccion.updateOne(\n {"_id": ""},\n {"$inc": {"numero": 1}}\n)' },
{ label: '$push', cmd: 'db.coleccion.updateOne(\n {"_id": ""},\n {"$push": {"array": "nuevo_elemento"}}\n)' },
{ label: '$pull', cmd: 'db.coleccion.updateOne(\n {"_id": ""},\n {"$pull": {"array": "elemento"}}\n)' },
{ label: 'deleteOne()', cmd: 'db.coleccion.deleteOne({"_id": ""})' },
{ label: 'aggregate()', cmd: 'db.coleccion.aggregate([{"$group": {"_id": "$campo", "total": {"$sum": 1}}}])' },
],
@@ -266,6 +404,8 @@ document.addEventListener('alpine:init', () => {
this.statusMsg = '';
this.history = [];
this.isMongo = false;
this.selectedCollection = '';
this.collectionFields = [];
if (!this.selectedConxId) return;
const conx = this.conexiones.find(c => c.ID == this.selectedConxId);
this.isMongo = conx?.tipo_db?.nombre?.toLowerCase().includes('mongo') ?? false;
@@ -280,6 +420,8 @@ document.addEventListener('alpine:init', () => {
async loadTables() {
this.tables = [];
this.selectedCollection = '';
this.collectionFields = [];
if (!this.selectedDb) return;
try {
const res = await axios.get('/app/query-runner/tables?conx_db_id=' + this.selectedConxId + '&db=' + encodeURIComponent(this.selectedDb));
@@ -384,6 +526,62 @@ document.addEventListener('alpine:init', () => {
this.runQuery();
},
async loadCollectionFields(name) {
this.selectedCollection = name;
this.collectionFields = [];
if (!this.selectedConxId || !name) return;
this.loadingFields = true;
try {
const res = await axios.post('/app/query-runner/run', {
conx_db_id: parseInt(this.selectedConxId),
database: this.selectedDb,
sql: `db.${name}.findOne({})`
});
const rows = res.data?.rows;
if (rows && rows.length > 0) {
this.collectionFields = Object.entries(rows[0]).map(([k, v]) => ({
name: k,
type: v === null ? 'null' : Array.isArray(v) ? 'array' : typeof v,
preview: v === null ? 'null' : Array.isArray(v) ? `[${v.length} elem]` : String(v).substring(0, 30),
sample: v
}));
}
} catch (_) {}
this.loadingFields = false;
},
buildUpdateField(fieldName) {
const col = this.selectedCollection || 'coleccion';
const f = this.collectionFields.find(f => f.name === fieldName);
let sampleVal = '"nuevo_valor"';
if (f && f.sample !== null && f.sample !== undefined) {
if (typeof f.sample === 'number') sampleVal = String(f.sample);
else if (typeof f.sample === 'boolean') sampleVal = String(f.sample);
else if (Array.isArray(f.sample)) sampleVal = '[]';
else if (typeof f.sample === 'object') sampleVal = '{}';
else {
const safe = String(f.sample).replace(/\\/g, '\\\\').replace(/"/g, '\\"').substring(0, 50);
sampleVal = `"${safe}"`;
}
}
this.sqlText = `db.${col}.updateOne(\n { "_id": "" },\n { "$set": { "${fieldName}": ${sampleVal} } }\n)`;
document.getElementById('sql-editor')?.focus();
},
generateUpdate() {
const col = this.updateBuilder.collection || this.selectedCollection || 'coleccion';
const field = this.updateBuilder.field || 'campo';
const value = this.updateBuilder.value || '"nuevo_valor"';
const filterField = this.updateBuilder.filterField || '_id';
const filterValue = this.updateBuilder.filterValue || '""';
const op = this.updateBuilder.op || '$set';
const method = this.updateBuilder.multi === 'many' ? 'updateMany' : 'updateOne';
let updateDoc;
if (op === '$unset') updateDoc = `{ "${field}": "" }`;
else updateDoc = `{ "${field}": ${value} }`;
this.sqlText = `db.${col}.${method}(\n { "${filterField}": ${filterValue} },\n { "${op}": ${updateDoc} }\n)`;
},
clearEditor() { this.sqlText = ''; this.results = []; this.columns = []; this.statusMsg = ''; },
nullStr(v) { return v === null || v === undefined ? 'NULL' : String(v); },