219 lines
8.2 KiB
Vue
219 lines
8.2 KiB
Vue
<template>
|
||
<div class="flex flex-col gap-8">
|
||
<UCard class="brand-card border border-transparent backdrop-blur-sm">
|
||
<template #header>
|
||
<div class="flex flex-col gap-2">
|
||
<h2 class="text-xl font-semibold text-[var(--brand-text)]">Metadatos de Tablas</h2>
|
||
<p class="text-sm text-[var(--brand-text-muted)]">
|
||
Información detallada sobre las tablas disponibles en la base de datos.
|
||
</p>
|
||
</div>
|
||
</template>
|
||
|
||
<div class="flex flex-col gap-4">
|
||
<!-- Refresh Controls -->
|
||
<div class="flex items-center justify-between">
|
||
<div class="flex flex-col gap-1">
|
||
<span class="text-sm font-medium text-[var(--brand-text)]">
|
||
Última actualización: {{ metadataStore.formattedLastUpdated }}
|
||
</span>
|
||
<span v-if="metadataStore.isStale" class="text-xs text-yellow-400">
|
||
⚠️ Los metadatos pueden estar desactualizados
|
||
</span>
|
||
</div>
|
||
|
||
<UButton
|
||
:loading="metadataStore.loading"
|
||
:ui="{ base: 'bg-[#c08040] text-[#1b1209] border border-[#d99a56] hover:bg-[#d99a56] hover:border-[#f0c07c]' }"
|
||
@click="refreshMetadata"
|
||
>
|
||
<template #leading>
|
||
<UIcon name="i-lucide-refresh-cw" :class="{ 'animate-spin': metadataStore.loading }" />
|
||
</template>
|
||
Actualizar metadatos
|
||
</UButton>
|
||
</div>
|
||
|
||
<!-- Stats Summary -->
|
||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||
<div class="rounded-lg border border-[#3a2a16] bg-[#1c140c] px-4 py-3">
|
||
<div class="text-2xl font-bold text-[var(--brand-text)]">{{ metadataStore.totalTables }}</div>
|
||
<div class="text-xs text-[var(--brand-text-muted)] uppercase tracking-wide">Tablas totales</div>
|
||
</div>
|
||
|
||
<div class="rounded-lg border border-[#3a2a16] bg-[#1c140c] px-4 py-3">
|
||
<div class="text-2xl font-bold text-[var(--brand-text)]">{{ formatNumber(metadataStore.totalRecords) }}</div>
|
||
<div class="text-xs text-[var(--brand-text-muted)] uppercase tracking-wide">Registros totales</div>
|
||
</div>
|
||
|
||
<div class="rounded-lg border border-[#3a2a16] bg-[#1c140c] px-4 py-3">
|
||
<div class="text-2xl font-bold text-[var(--brand-text)]">{{ formatSize(totalSize) }}</div>
|
||
<div class="text-xs text-[var(--brand-text-muted)] uppercase tracking-wide">Tamaño aproximado</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</UCard>
|
||
|
||
<!-- Error State -->
|
||
<div v-if="metadataStore.hasError" class="rounded-lg border border-red-500/40 bg-red-500/18 p-4 text-sm text-red-200">
|
||
{{ metadataStore.error }}
|
||
</div>
|
||
|
||
<!-- Loading State -->
|
||
<UCard v-if="metadataStore.loading && !metadataStore.hasMetadata" class="brand-card border border-transparent">
|
||
<div class="flex items-center justify-center gap-3 py-10 text-[var(--brand-text-muted)]">
|
||
<span class="inline-flex h-8 w-8 animate-spin rounded-full border-2 border-[#c08040] border-t-transparent align-middle" aria-hidden="true" />
|
||
<span class="text-sm uppercase tracking-[0.3em]">Cargando metadatos...</span>
|
||
</div>
|
||
</UCard>
|
||
|
||
<!-- Metadata Cards -->
|
||
<section v-if="metadataStore.hasMetadata" class="flex flex-col gap-5">
|
||
<div v-if="metadataStore.allTables.length" class="grid gap-5 md:grid-cols-2">
|
||
<UCard v-for="meta in metadataStore.allTables" :key="meta.table" class="brand-card border border-transparent">
|
||
<template #header>
|
||
<div class="flex items-center justify-between">
|
||
<h2 class="text-lg font-semibold brand-section-title">Tabla {{ meta.table }}</h2>
|
||
<span class="brand-badge inline-flex items-center gap-1 rounded-full px-3 py-1 text-xs font-semibold tracking-wide">
|
||
{{ formatNumber(meta.rowCount) }} registros
|
||
</span>
|
||
</div>
|
||
</template>
|
||
|
||
<dl class="grid grid-cols-2 gap-3 text-sm text-[var(--brand-text-muted)]">
|
||
<div>
|
||
<dt class="uppercase tracking-wide text-xs">Clave primaria</dt>
|
||
<dd class="font-medium text-[var(--brand-text)]">{{ meta.primaryKey || '—' }}</dd>
|
||
</div>
|
||
<div>
|
||
<dt class="uppercase tracking-wide text-xs">Tamaño aprox.</dt>
|
||
<dd class="font-medium text-[var(--brand-text)]">{{ formatSize(meta.approxSizeBytes) }}</dd>
|
||
</div>
|
||
<div>
|
||
<dt class="uppercase tracking-wide text-xs">Creación desde</dt>
|
||
<dd class="font-medium text-[var(--brand-text)]">{{ formatDate(meta.createdAtRange?.from) }}</dd>
|
||
</div>
|
||
<div>
|
||
<dt class="uppercase tracking-wide text-xs">Creación hasta</dt>
|
||
<dd class="font-medium text-[var(--brand-text)]">{{ formatDate(meta.createdAtRange?.to) }}</dd>
|
||
</div>
|
||
</dl>
|
||
|
||
<template #footer>
|
||
<div class="brand-divider pt-3 text-xs text-[var(--brand-text-muted)]">
|
||
Columnas detectadas ({{ meta.columns?.length || 0 }}): {{ (meta.columns || []).join(', ') || 'Ninguna' }}
|
||
</div>
|
||
</template>
|
||
</UCard>
|
||
</div>
|
||
|
||
<!-- Sample Row Card (if available) -->
|
||
<template v-for="meta in metadataStore.allTables.filter(m => m.sampleRow)" :key="`sample-${meta.table}`">
|
||
<UCard class="brand-card border border-transparent">
|
||
<template #header>
|
||
<h2 class="text-lg font-semibold brand-section-title">Registro de ejemplo - {{ meta.table }}</h2>
|
||
</template>
|
||
<pre class="overflow-auto rounded bg-[#22180f] p-4 text-sm text-[var(--brand-text-muted)]">{{ formatSample(meta.sampleRow) }}</pre>
|
||
</UCard>
|
||
</template>
|
||
</section>
|
||
|
||
<!-- Empty State -->
|
||
<UCard v-else-if="!metadataStore.loading" class="brand-card border border-transparent">
|
||
<div class="py-10 text-center text-sm text-[var(--brand-text-muted)]">
|
||
<UIcon name="i-lucide-database-zap" class="mx-auto mb-4 size-12 text-[var(--brand-text-muted)]" />
|
||
<h3 class="text-lg font-semibold text-[var(--brand-text)] mb-2">No hay metadatos disponibles</h3>
|
||
<p class="text-sm text-[var(--brand-text-muted)] mb-4">
|
||
Haz clic en "Actualizar metadatos" para cargar la información de las tablas.
|
||
</p>
|
||
<UButton
|
||
:loading="metadataStore.loading"
|
||
:ui="{ base: 'bg-[#c08040] text-[#1b1209] border border-[#d99a56] hover:bg-[#d99a56] hover:border-[#f0c07c]' }"
|
||
@click="refreshMetadata"
|
||
>
|
||
<template #leading>
|
||
<UIcon name="i-lucide-refresh-cw" />
|
||
</template>
|
||
Cargar metadatos
|
||
</UButton>
|
||
</div>
|
||
</UCard>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { useMetadataStore } from '~/stores/metadata'
|
||
|
||
definePageMeta({
|
||
layout: 'dashboard',
|
||
title: 'Metadatos'
|
||
})
|
||
|
||
const metadataStore = useMetadataStore()
|
||
|
||
// Computed properties
|
||
const totalSize = computed(() => {
|
||
return metadataStore.allTables.reduce((sum, meta) => sum + (meta.approxSizeBytes || 0), 0)
|
||
})
|
||
|
||
// Methods
|
||
async function refreshMetadata() {
|
||
await metadataStore.refreshMetadata()
|
||
}
|
||
|
||
function formatSize(bytes: number | null | undefined): string {
|
||
if (!bytes) {
|
||
return 'No disponible'
|
||
}
|
||
|
||
if (bytes < 1024) {
|
||
return `${bytes} B`
|
||
}
|
||
|
||
const units = ['KB', 'MB', 'GB', 'TB']
|
||
let size = bytes / 1024
|
||
let unitIndex = 0
|
||
|
||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||
size /= 1024
|
||
unitIndex += 1
|
||
}
|
||
|
||
return `${size.toFixed(1)} ${units[unitIndex]}`
|
||
}
|
||
|
||
function formatDate(value: string | null | undefined): string {
|
||
if (!value) {
|
||
return '—'
|
||
}
|
||
|
||
const date = new Date(value)
|
||
|
||
if (Number.isNaN(date.getTime())) {
|
||
return value
|
||
}
|
||
|
||
return date.toLocaleString('es-ES', {
|
||
year: 'numeric',
|
||
month: 'short',
|
||
day: 'numeric'
|
||
})
|
||
}
|
||
|
||
function formatNumber(value: number): string {
|
||
return new Intl.NumberFormat('es-ES').format(value)
|
||
}
|
||
|
||
function formatSample(value: unknown): string {
|
||
try {
|
||
return JSON.stringify(value, null, 2)
|
||
} catch (error) {
|
||
return String(value)
|
||
}
|
||
}
|
||
|
||
// Initialize store on component mount
|
||
onMounted(async () => {
|
||
await metadataStore.initialize()
|
||
})
|
||
</script> |