197 lines
9.6 KiB
Vue
197 lines
9.6 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'
|
|
}
|
|
|
|
const expandedId = ref(null)
|
|
|
|
function toggleExpanded(id) {
|
|
expandedId.value = expandedId.value === id ? null : id
|
|
}
|
|
</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">
|
|
<template v-for="log in logs.data" :key="log.id">
|
|
<tr
|
|
:class="[log.status === 'error' ? 'bg-red-50/70' : '', 'cursor-pointer hover:bg-gray-50 transition-colors']"
|
|
data-testid="api-log-row"
|
|
@click="toggleExpanded(log.id)"
|
|
>
|
|
<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="expandedId === log.id" class="bg-gray-50/50" data-testid="api-log-detail">
|
|
<td colspan="6" class="px-4 py-4">
|
|
<div class="space-y-3">
|
|
<div>
|
|
<span class="text-xs font-semibold uppercase tracking-wide text-gray-500">Anfrage-Kontext</span>
|
|
<pre v-if="log.request_context" class="mt-1 overflow-x-auto rounded-lg bg-gray-100 p-3 text-xs text-gray-800">{{ JSON.stringify(log.request_context, null, 2) }}</pre>
|
|
<p v-else class="mt-1 text-sm italic text-gray-400">Kein Kontext verfügbar</p>
|
|
</div>
|
|
<div>
|
|
<span class="text-xs font-semibold uppercase tracking-wide text-gray-500">Antwort-Zusammenfassung</span>
|
|
<p v-if="log.response_summary" class="mt-1 text-sm text-gray-700">{{ log.response_summary }}</p>
|
|
<p v-else class="mt-1 text-sm italic text-gray-400">Keine Zusammenfassung verfügbar</p>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
|
|
<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>
|