pp-planer/resources/js/Pages/ApiLogs/Index.vue

173 lines
7.7 KiB
Vue

<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'
import { Head, Link, router } from '@inertiajs/vue3'
import { ref, watch } from 'vue'
const props = defineProps({
logs: {
type: Object,
required: true,
},
filters: {
type: Object,
default: () => ({ search: '', status: '' }),
},
})
const search = ref(props.filters.search ?? '')
const status = ref(props.filters.status ?? '')
let searchTimeout = null
function loadLogs(page = 1) {
router.get(route('api-logs.index'), {
search: search.value || undefined,
status: status.value || undefined,
page,
}, {
preserveState: true,
preserveScroll: true,
replace: true,
})
}
watch(status, () => loadLogs(1))
watch(search, () => {
if (searchTimeout) {
clearTimeout(searchTimeout)
}
searchTimeout = setTimeout(() => loadLogs(1), 300)
})
function formatDateTime(value) {
if (!value) return '—'
return new Date(value).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
}
function statusBadgeClass(logStatus) {
if (logStatus === 'error') {
return 'bg-red-100 text-red-700 ring-red-200'
}
return 'bg-emerald-100 text-emerald-700 ring-emerald-200'
}
function statusText(logStatus) {
return logStatus === 'error' ? 'Fehler' : 'Erfolg'
}
</script>
<template>
<Head title="API-Log" />
<AuthenticatedLayout>
<template #header>
<div class="flex flex-wrap items-center justify-between gap-4">
<div>
<h1 class="text-xl font-semibold text-gray-900">API-Log</h1>
<p class="mt-1 text-sm text-gray-500">Hier siehst du alle CTS-API-Aufrufe mit Status und Laufzeit.</p>
</div>
</div>
</template>
<div class="py-8">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div class="mb-4 grid gap-3 rounded-xl border border-gray-200 bg-white p-4 shadow-sm sm:grid-cols-2">
<label class="block">
<span class="mb-1 block text-xs font-semibold uppercase tracking-wide text-gray-500">Suche</span>
<input
v-model="search"
data-testid="api-log-search"
type="search"
placeholder="Methode, Endpunkt oder Fehlertext"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-800 shadow-sm outline-none transition focus:border-amber-400 focus:ring-2 focus:ring-amber-200"
>
</label>
<label class="block">
<span class="mb-1 block text-xs font-semibold uppercase tracking-wide text-gray-500">Status</span>
<select
v-model="status"
data-testid="api-log-status-filter"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-800 shadow-sm outline-none transition focus:border-amber-400 focus:ring-2 focus:ring-amber-200"
>
<option value="">Alle</option>
<option value="success">Erfolg</option>
<option value="error">Fehler</option>
</select>
</label>
</div>
<div class="overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm">
<div class="overflow-x-auto">
<table data-testid="api-log-table" class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-gray-500">Zeitpunkt</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-gray-500">Methode</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-gray-500">Endpunkt</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-gray-500">Status</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-gray-500">Dauer (ms)</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-gray-500">Fehler</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 bg-white">
<tr
v-for="log in logs.data"
:key="log.id"
:class="log.status === 'error' ? 'bg-red-50/70' : ''"
data-testid="api-log-row"
>
<td class="px-4 py-3 text-sm text-gray-700">{{ formatDateTime(log.created_at) }}</td>
<td class="px-4 py-3 text-sm font-medium text-gray-900">{{ log.method }}</td>
<td class="px-4 py-3 text-sm text-gray-700">{{ log.endpoint }}</td>
<td class="px-4 py-3 text-sm">
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-semibold ring-1 ring-inset" :class="statusBadgeClass(log.status)">
{{ statusText(log.status) }}
</span>
</td>
<td class="px-4 py-3 text-sm text-gray-700">{{ log.duration_ms }}</td>
<td class="px-4 py-3 text-sm text-red-700">{{ log.error_message || '—' }}</td>
</tr>
<tr v-if="logs.data.length === 0">
<td colspan="6" class="px-4 py-8 text-center text-sm text-gray-500">Keine API-Logs für deinen Filter gefunden.</td>
</tr>
</tbody>
</table>
</div>
<div v-if="logs.links?.length > 3" class="flex flex-wrap items-center gap-1 border-t border-gray-200 px-4 py-3">
<template v-for="link in logs.links" :key="`${link.label}-${link.url}`">
<span
v-if="!link.url"
class="rounded-md border border-gray-200 px-3 py-1.5 text-xs text-gray-400"
v-html="link.label"
/>
<Link
v-else
:href="link.url"
preserve-state
preserve-scroll
class="rounded-md border px-3 py-1.5 text-xs transition"
:class="link.active ? 'border-amber-500 bg-amber-50 font-semibold text-amber-700' : 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50'"
v-html="link.label"
/>
</template>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>