Ajustes realizados: Components: - MetabaseCardDisplay: grid responsive, queries con wrap, botones apilables - MetabaseCardsTable: filtros verticales en móvil, acciones con wrap Page: - Header responsive con elementos apilados - Stats en grid 2x2 para móvil, 4 columnas en desktop - Tamaños de fuente adaptados con breakpoints Todas las queries SQL y JSON ahora usan whitespace-pre-wrap y break-words para aprovechar el espacio vertical en lugar de scroll horizontal.
188 lines
5.1 KiB
Vue
188 lines
5.1 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>
|
|
<UBadge color="green">
|
|
{{ queryResult.data?.rows?.length || 0 }} filas en {{ queryResult.running_time || 0 }}ms
|
|
</UBadge>
|
|
</div>
|
|
|
|
<div class="overflow-x-auto">
|
|
<pre class="p-3 bg-gray-50 dark:bg-gray-900 rounded text-xs whitespace-pre-wrap break-words">{{ JSON.stringify(queryResult.data, null, 2) }}</pre>
|
|
</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 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'
|
|
})
|
|
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')
|
|
}
|
|
</script>
|