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
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user