1 <!doctype html>2 <html lang="es">3 <head>4 <meta charset="utf-8" />5 <meta name="viewport" content="width=device-width, initial-scale=1" />6 <title>InceptionDB • SPA UI (mock) — Vue3 + Tailwind</title>7 <!-- Tailwind CDN -->8 <script src="https://cdn.tailwindcss.com"></script>9 <!-- Vue 3 CDN -->10 <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>11 <style>12 /* Light scrollbars */13 ::-webkit-scrollbar{height:10px;width:10px}::-webkit-scrollbar-thumb{background:#cbd5e1;border-radius:8px}::-webkit-scrollbar-track{background:#f1f5f9}14 .mono{font-family: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}15 </style>16 </head>17 <body class="bg-slate-50 text-slate-900">18 <div id="app" class="min-h-screen flex">19 <!-- Sidebar -->20 <aside class="w-72 bg-white border-r border-slate-200 p-4 space-y-4">21 <div class="flex items-center gap-2">22 <div class="h-9 w-9 rounded-2xl bg-slate-900 text-white grid place-content-center text-lg font-bold">IDB</div>23 <div>24 <div class="font-semibold">InceptionDB</div>25 <div class="text-xs text-slate-500">SPA (mock) · Vue3 + Tailwind</div>26 </div>27 </div>28 29 <div>30 <div class="text-xs uppercase tracking-wider text-slate-500 mb-2">Navegación</div>31 <nav class="space-y-1">32 <button @click="go('home')" :class="navCls('home')" class="w-full text-left">🏠 Resumen</button>33 <button @click="go('databases')" :class="navCls('databases')" class="w-full text-left">🗄️ Bases de datos</button>34 <button @click="go('collections')" :class="navCls('collections')" class="w-full text-left">📚 Colecciones</button>35 <button @click="go('indexes')" :class="navCls('indexes')" class="w-full text-left">#️⃣ Índices</button>36 <button @click="go('apiKeys')" :class="navCls('apiKeys')" class="w-full text-left">🔑 API Keys</button>37 <button @click="go('owners')" :class="navCls('owners')" class="w-full text-left">👤 Propietarios</button>38 <button @click="go('console')" :class="navCls('console')" class="w-full text-left">🧪 Consola (find/insert)</button>39 <button @click="go('about')" :class="navCls('about')" class="w-full text-left">ℹ️ Acerca de</button>40 </nav>41 </div>42 43 <div class="mt-6">44 <div class="text-xs uppercase tracking-wider text-slate-500 mb-2">Servidor</div>45 <div class="bg-slate-100 rounded-xl p-3 text-sm">46 <div class="flex items-center justify-between">47 <span class="text-slate-600">Endpoint</span>48 <span class="mono bg-white border rounded px-2 py-0.5">https://inceptiondb.hola.cloud</span>49 </div>50 <div class="mt-2 flex items-center justify-between">51 <span class="text-slate-600">Version</span>52 <span class="mono">v1</span>53 </div>54 </div>55 </div>56 </aside>57 58 <!-- Main -->59 <main class="flex-1 p-6">60 <!-- Breadcrumbs / header -->61 <header class="flex items-center justify-between mb-6">62 <div>63 <h1 class="text-2xl font-bold">{{ pageTitle }}</h1>64 <p class="text-sm text-slate-500" v-if="subtitle">{{ subtitle }}</p>65 </div>66 <div class="flex gap-2">67 <button @click="prefill()" class="px-3 py-2 text-sm rounded-lg border border-slate-300 hover:bg-slate-100">Rellenar ejemplos</button>68 <button @click="reset()" class="px-3 py-2 text-sm rounded-lg border border-rose-200 text-rose-600 hover:bg-rose-50">Reset</button>69 </div>70 </header>71 72 <!-- PAGES -->73 <section v-if="route==='home'" class="space-y-6">74 <div class="grid md:grid-cols-3 gap-4">75 <StatCard title="Bases de datos" :value="dbs.length" hint="/v1/databases"/>76 <StatCard title="Colecciones" :value="totalCollections" hint="/v1/databases/{databaseId}/collections"/>77 <StatCard title="API Keys" :value="totalApiKeys" hint="getDatabase.api_keys"/>78 </div>79 80 <div class="bg-white rounded-2xl shadow-sm border p-4">81 <h2 class="font-semibold mb-3">Qué incluye este mock</h2>82 <ul class="list-disc ml-6 space-y-1 text-slate-700">83 <li>Listar/crear/eliminar <span class="mono">databases</span> (campos reales del schema).</li>84 <li>Listar/crear <span class="mono">collections</span>, ver detalles y defaults.</li>85 <li>Listar/crear/eliminar <span class="mono">indexes</span>.</li>86 <li>Gestionar <span class="mono">API Keys</span> y <span class="mono">owners</span>.</li>87 <li>Consola para <span class="mono">find/insert/remove/patch</span> (solo mock).</li>88 </ul>89 </div>90 </section>91 92 <!-- DATABASES LIST -->93 <section v-if="route==='databases'" class="space-y-6">94 <div class="flex items-center justify-between">95 <h2 class="text-lg font-semibold">/v1/databases — Listar y crear</h2>96 <button @click="show.createDb=true" class="px-3 py-2 rounded-xl bg-slate-900 text-white">+ Nueva base de datos</button>97 </div>98 99 <div class="grid md:grid-cols-2 gap-4">100 <div v-for="db in dbs" :key="db.id" class="bg-white border rounded-2xl p-4">101 <div class="flex items-start justify-between">102 <div>103 <div class="font-semibold">{{ db.name }}</div>104 <div class="text-xs text-slate-500 mono">id: {{ db.id }}</div>105 </div>106 <button @click="openDb(db.id)" class="text-sm px-3 py-1 rounded-lg border">Abrir</button>107 </div>108 <div class="mt-3 text-sm text-slate-600">owners_length: <span class="mono">{{ db.owners_length }}</span></div>109 </div>110 </div>111 112 <!-- Create DB modal -->113 <Modal v-if="show.createDb" @close="show.createDb=false">114 <template #title>Crear database (createDatabaseRequest)</template>115 <div class="space-y-3">116 <Labeled field="name" desc="string">117 <input v-model="forms.createDb.name" class="w-full input" placeholder="my_database" />118 </Labeled>119 </div>120 <template #footer>121 <button @click="submitCreateDb" class="btn-primary">Crear</button>122 <button @click="show.createDb=false" class="btn">Cancelar</button>123 </template>124 </Modal>125 </section>126 127 <!-- DATABASE DETAIL -->128 <section v-if="route==='database' && currentDb" class="space-y-6">129 <div class="flex items-center justify-between">130 <div>131 <div class="text-sm text-slate-500">/v1/databases/{databaseId}</div>132 <h2 class="text-xl font-semibold">{{ currentDbDetail.name }}</h2>133 <div class="text-xs text-slate-500 mono">id: {{ currentDbDetail.id }} · creation_date: {{ currentDbDetail.creation_date }}</div>134 </div>135 <div class="flex gap-2">136 <button @click="go('collections',{db: currentDbDetail.id})" class="btn">Ver colecciones</button>137 <button @click="confirmDropDb(currentDbDetail.id)" class="btn-danger">Eliminar</button>138 </div>139 </div>140 141 <div class="grid md:grid-cols-2 gap-4">142 <!-- API Keys -->143 <div class="bg-white border rounded-2xl p-4">144 <div class="flex items-center justify-between mb-3">145 <h3 class="font-semibold">API Keys (getDatabaseResponse.api_keys)</h3>146 <button @click="show.createKey=true" class="btn-sm">+ Nueva</button>147 </div>148 <table class="w-full text-sm">149 <thead class="text-left text-slate-500">150 <tr><th class="py-1">name</th><th>key</th><th>creation_date</th><th></th></tr>151 </thead>152 <tbody>153 <tr v-for="k in currentDbDetail.api_keys" :key="k.key" class="border-t">154 <td class="py-2">{{ k.name }}</td>155 <td class="mono">{{ k.key }}</td>156 <td class="mono">{{ k.creation_date }}</td>157 <td class="text-right">158 <button @click="deleteApiKey(k.key)" class="text-rose-600 hover:underline">Eliminar</button>159 </td>160 </tr>161 </tbody>162 </table>163 </div>164 165 <!-- Owners -->166 <div class="bg-white border rounded-2xl p-4">167 <div class="flex items-center justify-between mb-3">168 <h3 class="font-semibold">Owners (getDatabaseResponse.owners)</h3>169 <div class="flex gap-2">170 <button @click="show.addOwner=true" class="btn-sm">+ Añadir</button>171 </div>172 </div>173 <ul class="text-sm space-y-1">174 <li v-for="o in currentDbDetail.owners" :key="o" class="flex items-center justify-between border rounded-lg px-2 py-1">175 <span class="mono">{{ o }}</span>176 <button @click="deleteOwner(o)" class="text-rose-600 hover:underline">Eliminar</button>177 </li>178 </ul>179 </div>180 </div>181 182 <!-- Create Key modal -->183 <Modal v-if="show.createKey" @close="show.createKey=false">184 <template #title>Crear API Key (createApiKeyInput → createApiKeyOutput)</template>185 <div class="space-y-3">186 <Labeled field="name" desc="string">187 <input v-model="forms.createKey.name" class="w-full input" placeholder="ci-build" />188 </Labeled>189 </div>190 <template #footer>191 <button @click="submitCreateKey" class="btn-primary">Crear</button>192 <button @click="show.createKey=false" class="btn">Cancelar</button>193 </template>194 </Modal>195 196 <!-- Add Owner modal -->197 <Modal v-if="show.addOwner" @close="show.addOwner=false">198 <template #title>Añadir Owner (addOwnerInput)</template>199 <div class="space-y-3">200 <Labeled field="owner_id" desc="string">201 <input v-model="forms.addOwner.owner_id" class="w-full input" placeholder="user_123" />202 </Labeled>203 </div>204 <template #footer>205 <button @click="submitAddOwner" class="btn-primary">Añadir</button>206 <button @click="show.addOwner=false" class="btn">Cancelar</button>207 </template>208 </Modal>209 </section>210 211 <!-- COLLECTIONS (per DB) -->212 <section v-if="route==='collections'" class="space-y-6">213 <div class="flex items-center justify-between">214 <h2 class="text-lg font-semibold">/v1/databases/{databaseId}/collections</h2>215 <div class="flex gap-2">216 <select v-model="filters.db" class="input">217 <option v-for="d in dbs" :value="d.id">{{ d.name }} ({{ d.id }})</option>218 </select>219 <button @click="show.createCollection=true" class="btn-primary">+ Nueva colección</button>220 </div>221 </div>222 <div class="grid md:grid-cols-2 gap-4">223 <div v-for="c in collectionsOf(filters.db)" :key="c.name" class="bg-white border rounded-2xl p-4">224 <div class="flex items-start justify-between">225 <div>226 <div class="font-semibold">{{ c.name }}</div>227 <div class="text-xs text-slate-500">indexes: <span class="mono">{{ c.indexes }}</span> · total: <span class="mono">{{ c.total }}</span></div>228 </div>229 <div class="flex gap-2">230 <button @click="openCollection(filters.db,c.name)" class="btn-sm">Abrir</button>231 <button @click="dropCollection(filters.db,c.name)" class="btn-sm text-rose-600">Eliminar</button>232 </div>233 </div>234 <div class="mt-3 text-xs text-slate-500">defaults: <span class="mono">{{ toSmallJson(c.defaults) }}</span></div>235 </div>236 </div>237 238 <!-- Create Collection modal -->239 <Modal v-if="show.createCollection" @close="show.createCollection=false">240 <template #title>Crear colección (createCollectionRequest)</template>241 <div class="space-y-3">242 <Labeled field="name" desc="string">243 <input v-model="forms.createCollection.name" class="w-full input" placeholder="users" />244 </Labeled>245 <Labeled field="defaults" desc="object">246 <textarea v-model="forms.createCollection.defaultsRaw" rows="5" class="w-full input mono" placeholder='{"country":"ES"}'></textarea>247 <p class="text-xs text-slate-500">(Se guarda tal cual en <span class="mono">defaults</span>)</p>248 </Labeled>249 </div>250 <template #footer>251 <button @click="submitCreateCollection" class="btn-primary">Crear</button>252 <button @click="show.createCollection=false" class="btn">Cancelar</button>253 </template>254 </Modal>255 </section>256 257 <!-- COLLECTION DETAIL: Indexes, Size, Defaults, GetIndex -->258 <section v-if="route==='collection' && currentCollection" class="space-y-6">259 <div class="flex items-center justify-between">260 <div>261 <div class="text-sm text-slate-500">/v1/databases/{databaseId}/collections/{collectionName}</div>262 <h2 class="text-xl font-semibold">{{ currentCollection.name }}</h2>263 <div class="text-xs text-slate-500">DB: <span class="mono">{{ params.db }}</span> · total: <span class="mono">{{ currentCollection.total }}</span> · indexes: <span class="mono">{{ currentCollection.indexes }}</span></div>264 </div>265 <div class="flex gap-2">266 <button @click="go('indexes',{db: params.db, col: currentCollection.name})" class="btn">Ver índices</button>267 <button @click="go('console',{db: params.db, col: currentCollection.name})" class="btn">Abrir consola</button>268 </div>269 </div>270 271 <div class="grid md:grid-cols-2 gap-4">272 <div class="bg-white border rounded-2xl p-4">273 <h3 class="font-semibold mb-2">Defaults (CollectionResponse.defaults)</h3>274 <pre class="mono text-sm bg-slate-50 border rounded-xl p-3 overflow-auto">{{ pretty(currentCollection.defaults) }}</pre>275 </div>276 <div class="bg-white border rounded-2xl p-4">277 <h3 class="font-semibold mb-2">Tamaño (size)</h3>278 <div class="text-sm text-slate-600">Respuesta mock de <span class="mono">/v1/databases/{databaseId}/collections/{collectionName}:size</span></div>279 <pre class="mono text-sm bg-slate-50 border rounded-xl p-3 overflow-auto">{{ pretty({bytes: 24576, docs: currentCollection.total}) }}</pre>280 </div>281 </div>282 </section>283 284 <!-- INDEXES -->285 <section v-if="route==='indexes'" class="space-y-6">286 <div class="flex items-center justify-between">287 <div>288 <h2 class="text-lg font-semibold">Índices (list/get/create/drop)</h2>289 <p class="text-sm text-slate-500">DB: <span class="mono">{{ params.db || filters.db }}</span> · Collection: <span class="mono">{{ params.col || currentColOf(filters.db) }}</span></p>290 </div>291 <div class="flex gap-2">292 <select v-model="filters.db" class="input">293 <option v-for="d in dbs" :value="d.id">{{ d.name }} ({{ d.id }})</option>294 </select>295 <select v-model="filters.col" class="input">296 <option v-for="c in collectionsOf(filters.db)" :value="c.name">{{ c.name }}</option>297 </select>298 <button @click="show.createIndex=true" class="btn-primary">+ Nuevo índice</button>299 </div>300 </div>301 302 <div class="bg-white border rounded-2xl p-4">303 <table class="w-full text-sm">304 <thead class="text-left text-slate-500">305 <tr><th class="py-1">name</th><th>type</th><th>options</th><th class="text-right">Acciones</th></tr>306 </thead>307 <tbody>308 <tr v-for="idx in indexesOf(filters.db, filters.col)" :key="idx.name" class="border-t">309 <td class="py-2 mono">{{ idx.name }}</td>310 <td class="mono">{{ idx.type }}</td>311 <td><span class="text-xs mono">{{ toSmallJson(idx.options) }}</span></td>312 <td class="text-right">313 <button @click="getIndex(idx.name)" class="btn-sm">getIndex</button>314 <button @click="dropIndex(idx.name)" class="btn-sm text-rose-600">dropIndex</button>315 </td>316 </tr>317 </tbody>318 </table>319 </div>320 321 <!-- Create Index modal -->322 <Modal v-if="show.createIndex" @close="show.createIndex=false">323 <template #title>Crear índice (createIndex → listIndexesItem)</template>324 <div class="space-y-3">325 <Labeled field="name" desc="string">326 <input v-model="forms.createIndex.name" class="w-full input" placeholder="idx_email" />327 </Labeled>328 <Labeled field="type" desc="string">329 <input v-model="forms.createIndex.type" class="w-full input" placeholder="hash|btree|text" />330 </Labeled>331 <Labeled field="options" desc="object">332 <textarea v-model="forms.createIndex.optionsRaw" rows="4" class="w-full input mono" placeholder='{"unique":true}'></textarea>333 </Labeled>334 </div>335 <template #footer>336 <button @click="submitCreateIndex" class="btn-primary">Crear</button>337 <button @click="show.createIndex=false" class="btn">Cancelar</button>338 </template>339 </Modal>340 </section>341 342 <!-- CONSOLE (find/insert/etc. mock) -->343 <section v-if="route==='console'" class="space-y-6">344 <div class="flex items-center justify-between">345 <h2 class="text-lg font-semibold">Consola (find / insert / remove / patch)</h2>346 <div class="flex gap-2">347 <select v-model="filters.db" class="input">348 <option v-for="d in dbs" :value="d.id">{{ d.name }} ({{ d.id }})</option>349 </select>350 <select v-model="filters.col" class="input">351 <option v-for="c in collectionsOf(filters.db)" :value="c.name">{{ c.name }}</option>352 </select>353 </div>354 </div>355 356 <div class="grid md:grid-cols-2 gap-4">357 <div class="bg-white border rounded-2xl p-4">358 <div class="flex items-center justify-between mb-2">359 <h3 class="font-semibold">find (request mock)</h3>360 <button @click="runFind" class="btn-sm">Ejecutar</button>361 </div>362 <textarea v-model="forms.find.query" rows="10" class="w-full input mono" placeholder='{"email":{"$eq":"a@b.com"}}'></textarea>363 </div>364 <div class="bg-white border rounded-2xl p-4">365 <h3 class="font-semibold mb-2">find (response mock)</h3>366 <pre class="mono text-sm bg-slate-50 border rounded-xl p-3 overflow-auto">{{ pretty(results.find) }}</pre>367 </div>368 </div>369 370 <div class="grid md:grid-cols-2 gap-4">371 <div class="bg-white border rounded-2xl p-4">372 <div class="flex items-center justify-between mb-2">373 <h3 class="font-semibold">insert (request mock)</h3>374 <button @click="runInsert" class="btn-sm">Ejecutar</button>375 </div>376 <textarea v-model="forms.insert.docs" rows="8" class="w-full input mono" placeholder='[{"_id":"1","email":"a@b.com"}]'></textarea>377 </div>378 <div class="bg-white border rounded-2xl p-4">379 <h3 class="font-semibold mb-2">insert (response mock)</h3>380 <pre class="mono text-sm bg-slate-50 border rounded-xl p-3 overflow-auto">{{ pretty(results.insert) }}</pre>381 </div>382 </div>383 </section>384 385 <!-- ABOUT -->386 <section v-if="route==='about'" class="space-y-6">387 <div class="bg-white border rounded-2xl p-6">388 <h2 class="font-semibold text-lg mb-2">Sobre este proyecto</h2>389 <p class="text-slate-600">SPA de ejemplo con Vue3 + Tailwind, basada en el OpenAPI de InceptionDB. De momento usa datos simulados pero los formularios y vistas reflejan los campos reales de requests/responses.</p>390 <ul class="list-disc ml-6 mt-3 text-sm text-slate-700">391 <li>Endpoints cubiertos: /v1/databases, /v1/databases/{id}, /collections, :listIndexes, :getIndex, :createIndex, :dropIndex, :size, :dropCollection, :createApiKey, :deleteApiKey, :addOwner, :deleteOwner, :find, :insert (mock).</li>392 <li>Siguientes pasos: conectar a API real, manejo de errores HTTP, paginación, auth (API Key), estados de carga.</li>393 </ul>394 </div>395 </section>396 397 <!-- TOASTS -->398 <transition name="fade">399 <div v-if="toast" class="fixed bottom-4 right-4 bg-slate-900 text-white rounded-xl px-4 py-2 shadow-lg">{{ toast }}</div>400 </transition>401 </main>402 </div>403 404 <script>405 const { createApp, reactive, computed, onMounted } = Vue406 407 // --- Components ---408 const StatCard = {409 props: {title:String, value:[String,Number], hint:String},410 template:`<div class='bg-white border rounded-2xl p-4'>411 <div class='text-sm text-slate-500'>{{ hint }}</div>412 <div class='text-xl font-semibold mt-1'>{{ value }}</div>413 <div class='text-slate-800'>{{ title }}</div>414 </div>`415 }416 417 const Modal = {418 emits: ['close'],419 template:`<div class='fixed inset-0 bg-black/30 grid place-items-center p-4 z-50'>420 <div class='bg-white w-full max-w-2xl rounded-2xl shadow-xl border overflow-hidden'>421 <div class='px-4 py-3 border-b flex items-center justify-between'>422 <div class='font-semibold'><slot name='title'>Modal</slot></div>423 <button @click="$emit('close')" class='text-slate-500 hover:text-slate-900'>✕</button>424 </div>425 <div class='p-4'><slot></slot></div>426 <div class='px-4 py-3 border-t bg-slate-50 flex items-center justify-end gap-2'>427 <slot name='footer'><button class='btn' @click="$emit('close')">Cerrar</button></slot>428 </div>429 </div>430 </div>`431 }432 433 const Labeled = {434 props: { field:String, desc:String },435 template:`<label class='block'>436 <div class='text-xs uppercase tracking-wider text-slate-500 mb-1'>{{ field }} <span class='text-slate-400'>({{ desc }})</span></div>437 <slot></slot>438 </label>`439 }440 441 // Tailwind-friendly input/button classes442 const tw = {443 install(app){444 app.config.globalProperties.inputCls = 'input'445 }446 }447 448 // --- App ---449 createApp({450 components:{StatCard, Modal, Labeled},451 setup(){452 const route = Vue.ref('home')453 const params = reactive({})454 const toast = Vue.ref('')455 456 const show = reactive({457 createDb:false, createKey:false, addOwner:false,458 createCollection:false, createIndex:false,459 })460 461 const filters = reactive({ db:'', col:'' })462 463 const forms = reactive({464 createDb: { name:'' },465 createKey: { name:'' },466 addOwner: { owner_id:'' },467 createCollection: { name:'', defaultsRaw:'' },468 createIndex: { name:'', type:'', optionsRaw:'' },469 find: { query:'' },470 insert: { docs:'' },471 })472 473 const results = reactive({ find:[], insert:{ inserted:0, ids:[] } })474 475 // --- Mock data aligned to OpenAPI schemas ---476 const dbs = reactive([477 { id:'db_001', name:'analytics', owners_length:2 },478 { id:'db_002', name:'prod', owners_length:1 },479 ])480 481 const dbDetail = reactive({482 db_001: {483 id:'db_001', name:'analytics', creation_date:'2023-05-01T12:00:00Z',484 owners:['alice','bob'],485 api_keys:[486 { name:'ci-build', key:'key_ci_123', creation_date:'2024-02-11T09:17:00Z' },487 { name:'dashboard', key:'key_dash_456', creation_date:'2024-09-21T13:00:00Z' },488 ],489 },490 db_002: {491 id:'db_002', name:'prod', creation_date:'2024-01-10T08:00:00Z',492 owners:['root'],493 api_keys:[ { name:'admin', key:'key_admin_999', creation_date:'2024-01-10T08:05:00Z' } ],494 },495 })496 497 const collections = reactive({498 db_001: [499 { name:'events', indexes:2, total:12012, defaults:{source:'web'} },500 { name:'users', indexes:3, total:523, defaults:{role:'user', country:'ES'} },501 ],502 db_002: [503 { name:'orders', indexes:2, total:2334, defaults:{currency:'EUR'} },504 ],505 })506 507 const indexes = reactive({508 'db_001/events': [509 { name:'idx_ts', type:'btree', options:{descending:true} },510 { name:'idx_src', type:'hash', options:{}} ,511 ],512 'db_001/users': [513 { name:'idx_email', type:'hash', options:{unique:true} },514 { name:'idx_country', type:'btree', options:{} },515 ],516 'db_002/orders': [517 { name:'idx_order_id', type:'hash', options:{unique:true} },518 ],519 })520 521 // --- Derived ---522 const totalCollections = computed(()=> Object.values(collections).reduce((a,arr)=>a+arr.length,0))523 const totalApiKeys = computed(()=> Object.values(dbDetail).reduce((a,d)=>a + (d.api_keys?.length||0),0))524 525 const currentDb = computed(()=> params.db && dbs.find(d=>d.id===params.db))526 const currentDbDetail = computed(()=> currentDb.value ? dbDetail[currentDb.value.id] : null)527 528 const currentCollection = computed(()=> {529 if (!params.db || !params.col) return null530 return (collections[params.db]||[]).find(c=>c.name===params.col)531 })532 533 const pageTitle = computed(()=>{534 switch(route.value){535 case 'home': return 'Resumen'536 case 'databases': return 'Bases de datos'537 case 'database': return `Database: ${currentDbDetail.value?.name||''}`538 case 'collections': return 'Colecciones'539 case 'collection': return `Colección: ${params.col}`540 case 'indexes': return 'Índices'541 case 'apiKeys': return 'API Keys'542 case 'owners': return 'Propietarios'543 case 'console': return 'Consola'544 case 'about': return 'Acerca de'545 }546 return ''547 })548 549 const subtitle = computed(()=>{550 if(route.value==='database') return '/v1/databases/{databaseId}'551 if(route.value==='collections') return '/v1/databases/{databaseId}/collections'552 if(route.value==='collection') return '/v1/databases/{databaseId}/collections/{collectionName}'553 if(route.value==='indexes') return 'listIndexes, getIndex, createIndex, dropIndex'554 return ''555 })556 557 // --- Helpers ---558 function go(name, p={}){559 route.value = name560 Object.assign(params, p)561 if(name==='collections' && !filters.db){ filters.db = dbs[0]?.id }562 if(name==='indexes'){563 if(!filters.db) filters.db = dbs[0]?.id564 if(!filters.col) filters.col = collections[filters.db]?.[0]?.name565 }566 window.scrollTo({top:0,behavior:'smooth'})567 }568 function navCls(name){569 return 'px-3 py-2 rounded-lg hover:bg-slate-100 ' + (route.value===name? 'bg-slate-900 text-white hover:bg-slate-900':'')570 }571 function toSmallJson(o){ try{return JSON.stringify(o)}catch{return String(o)} }572 function pretty(o){ try{return JSON.stringify(o,null,2)}catch{return String(o)} }573 function notify(msg){ toast.value = msg; setTimeout(()=>toast.value='',1600) }574 575 // --- DB actions (mock) ---576 function openDb(id){ go('database',{db:id}) }577 function confirmDropDb(id){578 if(!confirm('Eliminar database '+id+'? (mock)')) return579 const idx = dbs.findIndex(d=>d.id===id)580 if(idx>=0){ dbs.splice(idx,1); delete dbDetail[id]; delete collections[id]; notify('Database eliminada (mock)'); go('databases') }581 }582 function submitCreateDb(){583 if(!forms.createDb.name) return notify('Falta name')584 const id = 'db_'+Math.random().toString(36).slice(2,5)585 dbs.push({ id, name: forms.createDb.name, owners_length: 1 })586 dbDetail[id] = { id, name: forms.createDb.name, creation_date: new Date().toISOString(), owners:['owner'], api_keys:[] }587 collections[id] = []588 show.createDb=false; forms.createDb.name=''; notify('Database creada (mock)')589 }590 591 // --- API Keys ---592 function submitCreateKey(){593 if(!forms.createKey.name) return notify('Falta name')594 const d = currentDbDetail.value595 d.api_keys.push({ name: forms.createKey.name, key: 'key_'+Math.random().toString(36).slice(2,8), creation_date:new Date().toISOString() })596 show.createKey=false; forms.createKey.name=''; notify('API Key creada (mock)')597 }598 function deleteApiKey(key){599 const d = currentDbDetail.value600 d.api_keys = d.api_keys.filter(k=>k.key!==key)601 notify('API Key eliminada (mock)')602 }603 604 // --- Owners ---605 function submitAddOwner(){606 if(!forms.addOwner.owner_id) return notify('Falta owner_id')607 const d = currentDbDetail.value608 d.owners.push(forms.addOwner.owner_id)609 forms.addOwner.owner_id=''610 show.addOwner=false611 notify('Owner añadido (mock)')612 }613 function deleteOwner(owner){614 const d = currentDbDetail.value615 d.owners = d.owners.filter(o=>o!==owner)616 notify('Owner eliminado (mock)')617 }618 619 // --- Collections ---620 function collectionsOf(db){ return collections[db] || [] }621 function openCollection(db, col){ go('collection',{db, col}) }622 function dropCollection(db, col){623 if(!confirm('Eliminar colección '+col+'? (mock)')) return624 const arr = collections[db] || []625 const i = arr.findIndex(c=>c.name===col)626 if(i>=0){ arr.splice(i,1); notify('Colección eliminada (mock)') }627 }628 function submitCreateCollection(){629 if(!forms.createCollection.name) return notify('Falta name')630 let defaults = {}631 if(forms.createCollection.defaultsRaw){ try{ defaults = JSON.parse(forms.createCollection.defaultsRaw) }catch{ return notify('defaults no es JSON') } }632 collections[filters.db] = collections[filters.db] || []633 collections[filters.db].push({ name: forms.createCollection.name, indexes:0, total:0, defaults })634 show.createCollection=false; forms.createCollection.name=''; forms.createCollection.defaultsRaw=''635 notify('Colección creada (mock)')636 }637 638 // --- Indexes ---639 function currentColOf(db){ return (collections[db]||[])[0]?.name }640 function indexesOf(db,col){ return indexes[`${db}/${col||currentColOf(db)}`] || [] }641 function submitCreateIndex(){642 if(!forms.createIndex.name) return notify('Falta name')643 const key = `${filters.db}/${filters.col}`644 let options={}; if(forms.createIndex.optionsRaw){ try{ options = JSON.parse(forms.createIndex.optionsRaw) }catch{ return notify('options no es JSON') } }645 indexes[key] = indexes[key] || []646 indexes[key].push({ name: forms.createIndex.name, type: forms.createIndex.type || 'btree', options })647 forms.createIndex.name=''; forms.createIndex.type=''; forms.createIndex.optionsRaw=''; show.createIndex=false648 notify('Índice creado (mock)')649 }650 function dropIndex(name){651 const key = `${filters.db}/${filters.col}`652 indexes[key] = (indexes[key]||[]).filter(i=>i.name!==name)653 notify('Índice eliminado (mock)')654 }655 function getIndex(name){656 const key = `${filters.db}/${filters.col}`657 const found = (indexes[key]||[]).find(i=>i.name===name)658 alert('getIndex → '+ JSON.stringify({ name: found?.name||name, type: found?.type||'', options: found?.options||{} }, null, 2))659 }660 661 // --- Console (mock) ---662 function runFind(){663 let q={}; try{ q = JSON.parse(forms.find.query||'{}') }catch{ return notify('Query no es JSON') }664 results.find = [{ _id:'1', email:'a@b.com', country:'ES' }]665 notify('find ejecutado (mock)')666 }667 function runInsert(){668 let docs=[]; try{ docs = JSON.parse(forms.insert.docs||'[]') }catch{ return notify('Docs no es JSON') }669 const ids = docs.map(d=> d._id || Math.random().toString(36).slice(2,8))670 results.insert = { inserted: ids.length, ids }671 notify('insert ejecutado (mock)')672 }673 674 // --- UX helpers ---675 function prefill(){676 forms.createDb.name = 'staging'677 forms.createKey.name = 'grafana'678 forms.addOwner.owner_id = 'charlie'679 forms.createCollection.name = 'sessions'680 forms.createCollection.defaultsRaw = '{"ttl":86400}'681 forms.createIndex.name = 'idx_user_id'682 forms.createIndex.type = 'hash'683 forms.createIndex.optionsRaw = '{"unique":true}'684 forms.find.query = '{"email":{"$eq":"a@b.com"}}'685 forms.insert.docs = '[{"_id":"2","email":"b@c.com"}]'686 notify('Campos de ejemplo rellenados')687 }688 function reset(){689 Object.assign(forms, { createDb:{name:''}, createKey:{name:''}, addOwner:{owner_id:''}, createCollection:{name:'',defaultsRaw:''}, createIndex:{name:'',type:'',optionsRaw:''}, find:{query:''}, insert:{docs:''} })690 notify('Formularios reseteados')691 }692 693 onMounted(()=>{694 // default selections695 filters.db = dbs[0]?.id696 filters.col = collectionsOf(filters.db)[0]?.name697 })698 699 return { route, params, go, navCls, pageTitle, subtitle, toast,700 show, filters, forms, results,701 dbs, dbDetail, currentDb, currentDbDetail,702 collections, collectionsOf, currentCollection,703 indexes, indexesOf, currentColOf,704 openDb, confirmDropDb, submitCreateDb,705 submitCreateKey, deleteApiKey, submitAddOwner, deleteOwner,706 openCollection, dropCollection, submitCreateCollection,707 submitCreateIndex, dropIndex, getIndex,708 runFind, runInsert,709 toSmallJson, pretty,710 prefill, reset,711 totalCollections, totalApiKeys712 }713 },714 })715 .component('Modal',Modal)716 .mount('#app')717 </script>718 719 <!-- Small Tailwind-friendly utility classes -->720 <script>721 document.addEventListener('DOMContentLoaded', ()=>{722 const style = document.createElement('style')723 style.textContent = `724 .input{ @apply border rounded-lg px-3 py-2 bg-white text-slate-900 w-auto; }725 .btn{ @apply px-3 py-2 rounded-lg border border-slate-300 hover:bg-slate-100; }726 .btn-sm{ @apply px-2 py-1 rounded-md border border-slate-300 hover:bg-slate-100 text-sm; }727 .btn-primary{ @apply px-3 py-2 rounded-lg bg-slate-900 text-white hover:bg-slate-800; }728 .btn-danger{ @apply px-3 py-2 rounded-lg bg-rose-600 text-white hover:bg-rose-700; }729 `730 document.head.appendChild(style)731 })732 </script>733 </body>734 </html>735 Enlace
El enlace para compartir es:

