No al cierre de webs
ShareCode
Permalink: http://www.treeweb.es/u/974/ 01/02/2011

ShareCode

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: