feat: agregar página de debug para Metabase
- Crear componente MetabaseCardDisplay para mostrar detalles de queries - Crear componente MetabaseCardsTable para listar todas las queries - Crear página /metabase-debug con vistas de tabla, cards y queries Panorama - Agregar API routes para cards de Metabase (GET, POST, export) - Actualizar metabase.ts para soportar API Key authentication - Agregar configuración de Metabase API Key en nuxt.config.ts - Documentar todos los endpoints disponibles en METABASE_API_ENDPOINTS.md
This commit is contained in:
266
nuxt4-app/app/components/metabase/MetabaseCardsTable.vue
Normal file
266
nuxt4-app/app/components/metabase/MetabaseCardsTable.vue
Normal file
@@ -0,0 +1,266 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<!-- Filters and Search -->
|
||||
<div class="flex gap-3">
|
||||
<UInput
|
||||
v-model="search"
|
||||
placeholder="Buscar por nombre o ID..."
|
||||
icon="i-heroicons-magnifying-glass"
|
||||
class="flex-1"
|
||||
/>
|
||||
<USelectMenu
|
||||
v-model="selectedFilter"
|
||||
:options="filterOptions"
|
||||
placeholder="Filtrar..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<UTable
|
||||
:rows="filteredCards"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
@select="selectCard"
|
||||
>
|
||||
<template #id-data="{ row }">
|
||||
<UBadge color="gray" variant="subtle">{{ row.id }}</UBadge>
|
||||
</template>
|
||||
|
||||
<template #name-data="{ row }">
|
||||
<div>
|
||||
<div class="font-medium">{{ row.name }}</div>
|
||||
<div v-if="row.description" class="text-xs text-gray-500 dark:text-gray-400 truncate max-w-md">
|
||||
{{ row.description }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #query_type-data="{ row }">
|
||||
<UBadge :color="getQueryTypeColor(row.query_type || row.dataset_query?.type)">
|
||||
{{ row.query_type || row.dataset_query?.type || 'N/A' }}
|
||||
</UBadge>
|
||||
</template>
|
||||
|
||||
<template #database_id-data="{ row }">
|
||||
<span class="text-sm">{{ row.database_id || 'N/A' }}</span>
|
||||
</template>
|
||||
|
||||
<template #actions-data="{ row }">
|
||||
<div class="flex gap-1">
|
||||
<UButton
|
||||
@click.stop="executeCard(row)"
|
||||
:loading="executingCards.has(row.id)"
|
||||
color="primary"
|
||||
variant="soft"
|
||||
size="xs"
|
||||
>
|
||||
Ejecutar
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
@click.stop="executeCachedCard(row)"
|
||||
:loading="executingCachedCards.has(row.id)"
|
||||
color="gray"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
>
|
||||
Caché
|
||||
</UButton>
|
||||
|
||||
<UDropdown :items="getExportItems(row)">
|
||||
<UButton
|
||||
color="gray"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
icon="i-heroicons-arrow-down-tray"
|
||||
/>
|
||||
</UDropdown>
|
||||
</div>
|
||||
</template>
|
||||
</UTable>
|
||||
|
||||
<!-- Results Modal -->
|
||||
<UModal v-model="showResults">
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-lg font-semibold">Resultados: {{ selectedCardForResults?.name }}</h3>
|
||||
<UButton
|
||||
color="gray"
|
||||
variant="ghost"
|
||||
icon="i-heroicons-x-mark"
|
||||
@click="showResults = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="currentResult" class="space-y-3">
|
||||
<div class="flex gap-4 text-sm">
|
||||
<div>
|
||||
<span class="font-medium">Filas:</span>
|
||||
<span class="ml-2">{{ currentResult.data?.rows?.length || 0 }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium">Tiempo:</span>
|
||||
<span class="ml-2">{{ currentResult.running_time || 0 }}ms</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium">Estado:</span>
|
||||
<UBadge :color="currentResult.status === 'completed' ? 'green' : 'yellow'">
|
||||
{{ currentResult.status }}
|
||||
</UBadge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto max-h-96">
|
||||
<pre class="p-3 bg-gray-50 dark:bg-gray-900 rounded text-xs">{{ JSON.stringify(currentResult.data, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UAlert
|
||||
v-if="currentError"
|
||||
color="red"
|
||||
variant="soft"
|
||||
:title="currentError"
|
||||
class="mt-3"
|
||||
/>
|
||||
</UCard>
|
||||
</UModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
cards: any[]
|
||||
loading?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [card: any]
|
||||
}>()
|
||||
|
||||
const search = ref('')
|
||||
const selectedFilter = ref('all')
|
||||
const executingCards = ref(new Set<number>())
|
||||
const executingCachedCards = ref(new Set<number>())
|
||||
const showResults = ref(false)
|
||||
const currentResult = ref<any>(null)
|
||||
const currentError = ref<string | null>(null)
|
||||
const selectedCardForResults = ref<any>(null)
|
||||
|
||||
const filterOptions = [
|
||||
{ value: 'all', label: 'Todas' },
|
||||
{ value: 'native', label: 'SQL Nativo' },
|
||||
{ value: 'query', label: 'Query Builder' }
|
||||
]
|
||||
|
||||
const columns = [
|
||||
{ key: 'id', label: 'ID' },
|
||||
{ key: 'name', label: 'Nombre' },
|
||||
{ key: 'query_type', label: 'Tipo' },
|
||||
{ key: 'database_id', label: 'DB' },
|
||||
{ key: 'actions', label: 'Acciones' }
|
||||
]
|
||||
|
||||
const filteredCards = computed(() => {
|
||||
let result = props.cards
|
||||
|
||||
// Filter by search
|
||||
if (search.value) {
|
||||
const searchLower = search.value.toLowerCase()
|
||||
result = result.filter(card =>
|
||||
card.name?.toLowerCase().includes(searchLower) ||
|
||||
card.id?.toString().includes(searchLower) ||
|
||||
card.description?.toLowerCase().includes(searchLower)
|
||||
)
|
||||
}
|
||||
|
||||
// Filter by type
|
||||
if (selectedFilter.value !== 'all') {
|
||||
result = result.filter(card => {
|
||||
const type = card.query_type || card.dataset_query?.type
|
||||
return type === selectedFilter.value
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
function getQueryTypeColor(type: string) {
|
||||
switch (type) {
|
||||
case 'native':
|
||||
return 'blue'
|
||||
case 'query':
|
||||
return 'green'
|
||||
default:
|
||||
return 'gray'
|
||||
}
|
||||
}
|
||||
|
||||
function selectCard(card: any) {
|
||||
emit('select', card)
|
||||
}
|
||||
|
||||
async function executeCard(card: any) {
|
||||
executingCards.value.add(card.id)
|
||||
currentError.value = null
|
||||
currentResult.value = null
|
||||
selectedCardForResults.value = card
|
||||
|
||||
try {
|
||||
const result = await $fetch(`/api/metabase/cards/${card.id}/query`, {
|
||||
method: 'POST'
|
||||
})
|
||||
currentResult.value = result
|
||||
showResults.value = true
|
||||
} catch (e: any) {
|
||||
currentError.value = e.message || 'Error al ejecutar la query'
|
||||
showResults.value = true
|
||||
} finally {
|
||||
executingCards.value.delete(card.id)
|
||||
}
|
||||
}
|
||||
|
||||
async function executeCachedCard(card: any) {
|
||||
executingCachedCards.value.add(card.id)
|
||||
currentError.value = null
|
||||
currentResult.value = null
|
||||
selectedCardForResults.value = card
|
||||
|
||||
try {
|
||||
const result = await $fetch(`/api/metabase/cards/${card.id}/query`)
|
||||
currentResult.value = result
|
||||
showResults.value = true
|
||||
} catch (e: any) {
|
||||
currentError.value = e.message || 'Error al ejecutar la query con caché'
|
||||
showResults.value = true
|
||||
} finally {
|
||||
executingCachedCards.value.delete(card.id)
|
||||
}
|
||||
}
|
||||
|
||||
function getExportItems(card: any) {
|
||||
return [[
|
||||
{
|
||||
label: 'CSV',
|
||||
icon: 'i-heroicons-document-text',
|
||||
click: () => downloadExport(card, 'csv')
|
||||
},
|
||||
{
|
||||
label: 'JSON',
|
||||
icon: 'i-heroicons-code-bracket',
|
||||
click: () => downloadExport(card, 'json')
|
||||
},
|
||||
{
|
||||
label: 'XLSX',
|
||||
icon: 'i-heroicons-table-cells',
|
||||
click: () => downloadExport(card, 'xlsx')
|
||||
}
|
||||
]]
|
||||
}
|
||||
|
||||
function downloadExport(card: any, format: 'csv' | 'json' | 'xlsx') {
|
||||
const url = `${window.location.origin}/api/metabase/cards/${card.id}/query/${format}`
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user