Files
kite/frontend/src/views/admin/AdminOverviewView.vue
T
curo1305 1e14e15cbf feat(09-02): create AdminOverviewView.vue
- Single-fetch view calling getAdminOverview() on mount with loading/error states
- Four stat cards grid (Users, Storage, Processing, Ready) per ADMIN-11 and D-01
- Recent audit table with 5 columns (When/Event/Actor/Target/IP) matching AuditLogTab.vue structure
- Uses formatSize/formatDate from shared utils/formatters.js (no duplication)
- No top-level padding (AdminLayout owns p-8 max-w-5xl mx-auto)
- No Pinia store — single-fetch component consistent with admin tab pattern
2026-06-12 15:48:19 +02:00

99 lines
4.1 KiB
Vue

<template>
<div>
<h2 class="text-xl font-semibold text-gray-900 mb-6">Overview</h2>
<!-- Loading -->
<div v-if="loading" class="text-gray-500">Loading overview</div>
<!-- Error -->
<div v-else-if="error" class="text-red-600">{{ error }}</div>
<!-- Content -->
<template v-else-if="overview">
<!-- Stat cards -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
<!-- Users -->
<div class="bg-white border border-gray-200 rounded-xl p-6">
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-1">Users</p>
<p class="text-2xl font-bold text-gray-900">{{ overview.user_count }}</p>
</div>
<!-- Storage -->
<div class="bg-white border border-gray-200 rounded-xl p-6">
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-1">Storage</p>
<p class="text-2xl font-bold text-gray-900">{{ formatSize(overview.total_storage_bytes) }}</p>
</div>
<!-- Processing -->
<div class="bg-white border border-gray-200 rounded-xl p-6">
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-1">Processing</p>
<p class="text-2xl font-bold text-gray-900">{{ overview.doc_status?.processing ?? 0 }}</p>
</div>
<!-- Ready -->
<div class="bg-white border border-gray-200 rounded-xl p-6">
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-1">Ready</p>
<p class="text-2xl font-bold text-gray-900">{{ overview.doc_status?.ready ?? 0 }}</p>
</div>
</div>
<!-- Recent audit table -->
<div>
<h3 class="text-sm font-semibold text-gray-700 mb-3">Recent Activity</h3>
<div v-if="overview.recent_audit && overview.recent_audit.length === 0" class="text-sm text-gray-400 italic">
No recent activity
</div>
<div v-else class="bg-white rounded-xl border border-gray-200 overflow-hidden">
<table class="w-full text-sm border-collapse">
<thead>
<tr class="bg-gray-50 border-b border-gray-200">
<th scope="col" class="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">When</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">Event</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">Actor</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">Target</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">IP</th>
</tr>
</thead>
<tbody>
<tr
v-for="entry in overview.recent_audit"
:key="entry.id"
class="border-b border-gray-100 hover:bg-gray-50 transition-colors"
>
<td class="px-4 py-3 font-mono text-xs text-gray-500">{{ formatDate(entry.created_at) }}</td>
<td class="px-4 py-3 text-sm text-gray-700">{{ entry.event_type }}</td>
<td class="px-4 py-3 text-sm text-gray-700">{{ entry.owner_handle ?? '—' }}</td>
<td class="px-4 py-3 text-sm text-gray-500">{{ entry.target_user_handle ?? '—' }}</td>
<td class="px-4 py-3 font-mono text-xs text-gray-700">{{ entry.ip_address ?? '—' }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getAdminOverview } from '../../api/admin.js'
import { formatSize, formatDate } from '../../utils/formatters.js'
const loading = ref(false)
const error = ref(null)
const overview = ref(null)
onMounted(async () => {
loading.value = true
try {
overview.value = await getAdminOverview()
} catch (e) {
error.value = e?.message || 'Failed to load overview'
} finally {
loading.value = false
}
})
</script>