Files
analiticaNucleo/nuxt4-app/app/components/metabase/MetabaseCardDisplay.vue
josedario87 c69c3bdafc
All checks were successful
build-and-deploy / build (push) Successful in 43s
build-and-deploy / deploy (push) Successful in 3s
feat: mejorar visualización de resultados de queries en Metabase Debug
- Agregar tabla estructurada para mostrar resultados de queries
- Agregar botón de copiar JSON profesional con feedback visual
- Agregar formato automático de números y fechas según tipo de columna
- Mantener vista JSON colapsable para ver datos completos
- Mejorar UX con tabla responsive y estilos consistentes
2025-10-14 03:42:52 -06:00

290 lines
9.3 KiB
Vue

<template>
<UCard>
<template #header>
<div class="flex justify-between items-start">
<div class="flex-1">
<h3 class="text-lg font-semibold">{{ card.name }}</h3>
<p v-if="card.description" class="text-sm text-gray-500 dark:text-gray-400 mt-1">
{{ card.description }}
</p>
</div>
<UBadge :color="getStatusColor(card)">
ID: {{ card.id }}
</UBadge>
</div>
</template>
<!-- Card Details -->
<div class="space-y-3">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
<div>
<span class="font-medium">Database ID:</span>
<span class="ml-2">{{ card.database_id }}</span>
</div>
<div>
<span class="font-medium">Query Type:</span>
<span class="ml-2">{{ card.query_type || card.dataset_query?.type }}</span>
</div>
<div>
<span class="font-medium">Collection:</span>
<span class="ml-2">{{ card.collection_id || 'Root' }}</span>
</div>
<div>
<span class="font-medium">Created:</span>
<span class="ml-2">{{ formatDate(card.created_at) }}</span>
</div>
</div>
<!-- Query Preview -->
<div v-if="card.dataset_query?.native?.query" class="mt-4">
<details class="group">
<summary class="cursor-pointer text-sm font-medium text-primary">
Ver SQL
</summary>
<pre class="mt-2 p-3 bg-gray-50 dark:bg-gray-900 rounded text-xs overflow-x-auto whitespace-pre-wrap break-words">{{ card.dataset_query.native.query }}</pre>
</details>
</div>
<!-- Actions -->
<div class="flex flex-wrap gap-2 mt-4 pt-4 border-t dark:border-gray-700">
<UButton
@click="executeQuery"
:loading="executing"
color="primary"
size="sm"
>
Ejecutar Query
</UButton>
<UButton
@click="executeQueryCached"
:loading="executingCached"
color="gray"
variant="soft"
size="sm"
>
Con Caché
</UButton>
<UDropdown :items="exportItems">
<UButton
color="gray"
variant="soft"
size="sm"
trailing-icon="i-heroicons-chevron-down-20-solid"
>
Exportar
</UButton>
</UDropdown>
</div>
<!-- Query Results -->
<div v-if="queryResult" class="mt-4">
<div class="flex flex-wrap justify-between items-center gap-2 mb-2">
<h4 class="font-medium text-sm">Resultados</h4>
<div class="flex items-center gap-2">
<UBadge color="green">
{{ queryResult.data?.rows?.length || 0 }} filas en {{ queryResult.running_time || 0 }}ms
</UBadge>
<UButton
v-if="queryResult.data"
@click="copyResults"
color="gray"
variant="soft"
size="xs"
icon="i-heroicons-clipboard-document"
>
{{ copied ? 'Copiado!' : 'Copiar JSON' }}
</UButton>
</div>
</div>
<!-- Empty State -->
<div v-if="!queryResult.data?.rows || queryResult.data.rows.length === 0" class="p-8 text-center bg-gray-50 dark:bg-gray-900 rounded">
<div class="mx-auto w-16 h-16 mb-4 flex items-center justify-center bg-gray-200 dark:bg-gray-800 rounded-full">
<svg class="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"></path>
</svg>
</div>
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-1">No hay datos</h3>
<p class="text-xs text-gray-500 dark:text-gray-400">La consulta se ejecutó correctamente pero no devolvió resultados.</p>
</div>
<!-- Data Display -->
<div v-else>
<!-- Table View -->
<div v-if="queryResult.data.cols && queryResult.data.rows" class="overflow-x-auto mb-4">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-800">
<tr>
<th
v-for="col in queryResult.data.cols"
:key="col.name"
class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
{{ col.display_name || col.name }}
</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
<tr v-for="(row, rowIndex) in queryResult.data.rows" :key="rowIndex">
<td
v-for="(cell, cellIndex) in row"
:key="cellIndex"
class="px-4 py-3 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100"
>
{{ formatCell(cell, queryResult.data.cols[cellIndex]) }}
</td>
</tr>
</tbody>
</table>
</div>
<!-- JSON View (collapsible) -->
<details class="group">
<summary class="cursor-pointer text-sm font-medium text-primary flex items-center gap-2">
<span>Ver JSON completo</span>
<UIcon name="i-heroicons-chevron-down-20-solid" class="w-4 h-4 transition-transform group-open:rotate-180" />
</summary>
<pre class="mt-2 p-3 bg-gray-50 dark:bg-gray-900 rounded text-xs whitespace-pre-wrap break-words">{{ JSON.stringify(queryResult.data, null, 2) }}</pre>
</details>
</div>
</div>
<!-- Error Display -->
<UAlert
v-if="error"
color="red"
variant="soft"
:title="error"
:close-button="{ icon: 'i-heroicons-x-mark-20-solid', color: 'red', variant: 'link' }"
@close="error = null"
/>
</div>
</UCard>
</template>
<script setup lang="ts">
const props = defineProps<{
card: any
}>()
const executing = ref(false)
const executingCached = ref(false)
const queryResult = ref<any>(null)
const error = ref<string | null>(null)
const copied = ref(false)
const exportItems = [[
{
label: 'CSV',
icon: 'i-heroicons-document-text',
click: () => downloadExport('csv')
},
{
label: 'JSON',
icon: 'i-heroicons-code-bracket',
click: () => downloadExport('json')
},
{
label: 'XLSX',
icon: 'i-heroicons-table-cells',
click: () => downloadExport('xlsx')
}
]]
function getStatusColor(card: any) {
if (card.archived) return 'red'
if (card.dataset) return 'blue'
return 'gray'
}
function formatDate(dateString: string) {
if (!dateString) return 'N/A'
return new Date(dateString).toLocaleDateString('es-ES', {
year: 'numeric',
month: 'short',
day: 'numeric'
})
}
async function executeQuery() {
executing.value = true
error.value = null
queryResult.value = null
try {
const result = await $fetch(`/api/metabase/cards/${props.card.id}/query`, {
method: 'POST',
body: {
parameters: [
{ type: 'category', target: ['variable', ['template-tag', 'incluir_anulados']], value: [false] },
{ type: 'date/single', target: ['variable', ['template-tag', 'fecha_desde']], value: null },
{ type: 'date/single', target: ['variable', ['template-tag', 'fecha_hasta']], value: null }
]
}
})
queryResult.value = result
} catch (e: any) {
error.value = e.message || 'Error al ejecutar la query'
} finally {
executing.value = false
}
}
async function executeQueryCached() {
executingCached.value = true
error.value = null
queryResult.value = null
try {
const result = await $fetch(`/api/metabase/cards/${props.card.id}/query`)
queryResult.value = result
} catch (e: any) {
error.value = e.message || 'Error al ejecutar la query con caché'
} finally {
executingCached.value = false
}
}
function downloadExport(format: 'csv' | 'json' | 'xlsx') {
const url = `${window.location.origin}/api/metabase/cards/${props.card.id}/query/${format}`
window.open(url, '_blank')
}
async function copyResults() {
try {
await navigator.clipboard.writeText(JSON.stringify(queryResult.value.data, null, 2))
copied.value = true
setTimeout(() => {
copied.value = false
}, 2000)
} catch (e) {
console.error('Error al copiar:', e)
}
}
function formatCell(value: any, col: any) {
if (value === null || value === undefined) return '-'
// Format numbers based on type
if (col.base_type === 'type/Float' || col.base_type === 'type/Decimal') {
return new Intl.NumberFormat('es-ES', {
minimumFractionDigits: 0,
maximumFractionDigits: 2
}).format(value)
}
if (col.base_type === 'type/Integer') {
return new Intl.NumberFormat('es-ES').format(value)
}
// Format dates
if (col.base_type === 'type/Date' || col.base_type === 'type/DateTime') {
return new Date(value).toLocaleDateString('es-ES')
}
return value
}
</script>