changes
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import Dropdown from "@/Components/Dropdown.vue";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/Components/ui/table';
|
||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/Components/ui/select';
|
||||
import { Button } from '@/Components/ui/button';
|
||||
import { Badge } from '@/Components/ui/badge';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/Components/ui/dialog';
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/Components/ui/accordion';
|
||||
|
||||
const props = defineProps({
|
||||
events: Array,
|
||||
@@ -8,8 +14,8 @@ const props = defineProps({
|
||||
limit: Number,
|
||||
});
|
||||
const emits = defineEmits(["update:limit", "refresh"]);
|
||||
function onLimit(e) {
|
||||
emits("update:limit", Number(e.target.value));
|
||||
function onLimit(val) {
|
||||
emits("update:limit", Number(val));
|
||||
emits("refresh");
|
||||
}
|
||||
|
||||
@@ -46,6 +52,32 @@ function toggleExpand(id) {
|
||||
expanded.value = new Set(expanded.value);
|
||||
}
|
||||
|
||||
// Entity details dialog
|
||||
const detailsDialog = ref(false);
|
||||
const selectedEvent = ref(null);
|
||||
|
||||
function hasEntityDetails(ev) {
|
||||
const ctx = tryJson(ev.context);
|
||||
return ctx && Array.isArray(ctx.entity_details) && ctx.entity_details.length > 0;
|
||||
}
|
||||
|
||||
function showEntityDetails(ev) {
|
||||
selectedEvent.value = ev;
|
||||
detailsDialog.value = true;
|
||||
}
|
||||
|
||||
function getEntityDetails(ev) {
|
||||
if (!ev) return [];
|
||||
const ctx = tryJson(ev.context);
|
||||
return ctx?.entity_details || [];
|
||||
}
|
||||
|
||||
function getRawData(ev) {
|
||||
if (!ev) return {};
|
||||
const ctx = tryJson(ev.context);
|
||||
return ctx?.raw_data || {};
|
||||
}
|
||||
|
||||
function isLong(msg) {
|
||||
return msg && String(msg).length > 160;
|
||||
}
|
||||
@@ -138,68 +170,72 @@ function formattedContext(ctx) {
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h3 class="font-semibold">Logs</h3>
|
||||
<div class="flex items-center flex-wrap gap-2 text-sm">
|
||||
<label class="text-gray-600">Show</label>
|
||||
<select :value="limit" class="border rounded p-1" @change="onLimit">
|
||||
<option :value="50">50</option>
|
||||
<option :value="100">100</option>
|
||||
<option :value="200">200</option>
|
||||
<option :value="500">500</option>
|
||||
</select>
|
||||
<label class="text-gray-600 ml-2">Level</label>
|
||||
<select v-model="levelFilter" class="border rounded p-1">
|
||||
<option v-for="opt in levelOptions" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</option>
|
||||
</select>
|
||||
<button
|
||||
<span class="text-muted-foreground">Show</span>
|
||||
<Select :model-value="limit.toString()" @update:model-value="onLimit">
|
||||
<SelectTrigger class="w-20 h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="50">50</SelectItem>
|
||||
<SelectItem value="100">100</SelectItem>
|
||||
<SelectItem value="200">200</SelectItem>
|
||||
<SelectItem value="500">500</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span class="text-muted-foreground ml-2">Level</span>
|
||||
<Select v-model="levelFilter">
|
||||
<SelectTrigger class="w-32 h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem v-for="opt in levelOptions" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click.prevent="$emit('refresh')"
|
||||
class="px-2 py-1 border rounded text-sm"
|
||||
:disabled="loading"
|
||||
>
|
||||
{{ loading ? "Refreshing…" : "Refresh" }}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-x-auto max-h-[30rem] overflow-y-auto rounded border">
|
||||
<table class="min-w-full bg-white text-sm table-fixed">
|
||||
<colgroup>
|
||||
<col class="w-40" />
|
||||
<col class="w-20" />
|
||||
<col class="w-40" />
|
||||
<col />
|
||||
<col class="w-16" />
|
||||
</colgroup>
|
||||
<thead class="bg-gray-50 sticky top-0 z-10 shadow">
|
||||
<tr class="text-left text-xs uppercase text-gray-600">
|
||||
<th class="p-2 border">Time</th>
|
||||
<th class="p-2 border">Level</th>
|
||||
<th class="p-2 border">Event</th>
|
||||
<th class="p-2 border">Message</th>
|
||||
<th class="p-2 border">Row</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="ev in filteredEvents" :key="ev.id" class="border-t align-top">
|
||||
<td class="p-2 border whitespace-nowrap">
|
||||
<div class="overflow-x-auto max-h-[30rem] overflow-y-auto rounded-lg border">
|
||||
<Table>
|
||||
<TableHeader class="sticky top-0 z-10">
|
||||
<TableRow>
|
||||
<TableHead class="w-[160px]">Time</TableHead>
|
||||
<TableHead class="w-[80px]">Level</TableHead>
|
||||
<TableHead class="w-[160px]">Event</TableHead>
|
||||
<TableHead>Message</TableHead>
|
||||
<TableHead class="w-[64px]">Row</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-for="ev in filteredEvents" :key="ev.id">
|
||||
<TableCell class="whitespace-nowrap">
|
||||
{{ new Date(ev.created_at).toLocaleString() }}
|
||||
</td>
|
||||
<td class="p-2 border">
|
||||
<span
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
:variant="ev.level === 'error' ? 'destructive' : ev.level === 'warning' ? 'default' : 'secondary'"
|
||||
:class="[
|
||||
'px-2 py-0.5 rounded text-xs',
|
||||
ev.level === 'error'
|
||||
? 'bg-red-100 text-red-800'
|
||||
: ev.level === 'warning'
|
||||
? 'bg-amber-100 text-amber-800'
|
||||
: 'bg-gray-100 text-gray-700',
|
||||
'text-xs',
|
||||
ev.level === 'warning' ? 'bg-amber-100 text-amber-800 hover:bg-amber-100' : ''
|
||||
]"
|
||||
>{{ ev.level }}</span
|
||||
>
|
||||
</td>
|
||||
<td class="p-2 border break-words max-w-[9rem]">
|
||||
>{{ ev.level }}</Badge>
|
||||
</TableCell>
|
||||
<TableCell class="max-w-[9rem]">
|
||||
<span class="block truncate" :title="ev.event">{{ ev.event }}</span>
|
||||
</td>
|
||||
<td class="p-2 border align-top max-w-[28rem]">
|
||||
</TableCell>
|
||||
<TableCell class="max-w-[28rem]">
|
||||
<div class="space-y-1 break-words">
|
||||
<div class="leading-snug whitespace-pre-wrap">
|
||||
<span v-if="!isLong(ev.message)">{{ ev.message }}</span>
|
||||
@@ -215,7 +251,15 @@ function formattedContext(ctx) {
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="ev.context" class="text-xs text-gray-600">
|
||||
<div v-if="ev.context" class="text-xs text-gray-600 flex items-center gap-2">
|
||||
<button
|
||||
v-if="hasEntityDetails(ev)"
|
||||
type="button"
|
||||
class="px-2 py-1 rounded border border-indigo-300 bg-indigo-50 hover:bg-indigo-100 text-indigo-700 transition text-[11px] font-medium"
|
||||
@click="showEntityDetails(ev)"
|
||||
>
|
||||
📋 Entity Details
|
||||
</button>
|
||||
<Dropdown
|
||||
align="left"
|
||||
width="wide"
|
||||
@@ -255,14 +299,91 @@ function formattedContext(ctx) {
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="p-2 border">{{ ev.import_row_id ?? "—" }}</td>
|
||||
</tr>
|
||||
<tr v-if="!filteredEvents.length">
|
||||
<td class="p-3 text-center text-gray-500" colspan="5">No events yet</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</TableCell>
|
||||
<TableCell>{{ ev.import_row_id ?? "—" }}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow v-if="!filteredEvents.length">
|
||||
<TableCell colspan="5" class="text-center text-muted-foreground">No events yet</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<!-- Entity Details Dialog -->
|
||||
<Dialog v-model:open="detailsDialog">
|
||||
<DialogContent class="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Entity Processing Details</DialogTitle>
|
||||
<DialogDescription v-if="selectedEvent">
|
||||
Row {{ tryJson(selectedEvent.context)?.row || '—' }} - {{ selectedEvent.event }}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div v-if="selectedEvent" class="space-y-3 mt-4">
|
||||
<div
|
||||
v-for="(detail, idx) in getEntityDetails(selectedEvent)"
|
||||
:key="idx"
|
||||
class="p-3 rounded-lg border"
|
||||
:class="{
|
||||
'bg-red-50 border-red-200': detail.level === 'error',
|
||||
'bg-amber-50 border-amber-200': detail.level === 'warning',
|
||||
'bg-green-50 border-green-200': detail.level === 'info' && detail.action === 'inserted',
|
||||
'bg-blue-50 border-blue-200': detail.level === 'info' && detail.action === 'updated',
|
||||
'bg-gray-50 border-gray-200': detail.level === 'info' && detail.action === 'skipped'
|
||||
}"
|
||||
>
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<div class="font-medium text-sm capitalize">{{ detail.entity }}</div>
|
||||
<Badge
|
||||
:variant="detail.level === 'error' ? 'destructive' : detail.level === 'warning' ? 'default' : 'secondary'"
|
||||
:class="[
|
||||
'text-xs',
|
||||
detail.level === 'warning' ? 'bg-amber-100 text-amber-800 hover:bg-amber-100' : '',
|
||||
detail.action === 'inserted' ? 'bg-green-100 text-green-800 hover:bg-green-100' : '',
|
||||
detail.action === 'updated' ? 'bg-blue-100 text-blue-800 hover:bg-blue-100' : '',
|
||||
detail.action === 'skipped' ? 'bg-gray-200 text-gray-700 hover:bg-gray-200' : ''
|
||||
]"
|
||||
>
|
||||
{{ detail.action }}{{ detail.count > 1 ? ` (${detail.count})` : '' }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div v-if="detail.message" class="text-sm text-gray-700 mb-1">
|
||||
{{ detail.message }}
|
||||
</div>
|
||||
<div v-if="detail.errors && detail.errors.length" class="mt-2 space-y-1">
|
||||
<div class="text-xs font-medium text-red-700">Errors:</div>
|
||||
<div
|
||||
v-for="(err, errIdx) in detail.errors"
|
||||
:key="errIdx"
|
||||
class="text-xs text-red-600 pl-3"
|
||||
>
|
||||
• {{ err }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="detail.exception" class="mt-2 p-2 bg-red-100 rounded border border-red-200">
|
||||
<div class="text-xs font-semibold text-red-800 mb-1">Exception:</div>
|
||||
<div class="text-xs text-red-700">{{ detail.exception.message }}</div>
|
||||
<div v-if="detail.exception.file" class="text-xs text-red-600 mt-1">
|
||||
{{ detail.exception.file }}:{{ detail.exception.line }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="getEntityDetails(selectedEvent).length === 0" class="text-center text-muted-foreground py-4">
|
||||
No entity details available
|
||||
</div>
|
||||
|
||||
<!-- Raw Row Data Accordion -->
|
||||
<Accordion type="single" collapsible class="mt-4 border-t pt-4">
|
||||
<AccordionItem value="raw-data" class="border-b-0">
|
||||
<AccordionTrigger class="text-sm font-medium hover:no-underline py-2">
|
||||
📄 Raw Row Data (JSON)
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto mt-2">{{ JSON.stringify(getRawData(selectedEvent), null, 2) }}</pre>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user