phase 3
This commit is contained in:
@@ -0,0 +1,324 @@
|
||||
<x-app-layout>
|
||||
@php($currentProject = $project)
|
||||
|
||||
<x-slot name="header">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<a href="{{ route('projects.issues.index', $project) }}" class="text-sm text-gray-500 hover:text-gray-700">← Issues</a>
|
||||
</div>
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight truncate">{{ $issue->title }}</h2>
|
||||
@if($issue->culprit)
|
||||
<p class="text-sm text-gray-500 mt-0.5">{{ $issue->culprit }}</p>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0 ml-4">
|
||||
<form method="POST" action="{{ route('projects.issues.status', [$project, $issue]) }}">
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
@if($issue->status === 'unresolved')
|
||||
<button name="action" value="resolve" class="px-3 py-1.5 text-sm font-medium rounded-md bg-green-100 text-green-800 hover:bg-green-200 transition">Resolve</button>
|
||||
<button name="action" value="ignore" class="px-3 py-1.5 text-sm font-medium rounded-md bg-yellow-100 text-yellow-800 hover:bg-yellow-200 transition">Ignore</button>
|
||||
@elseif($issue->status === 'resolved')
|
||||
<button name="action" value="unresolve" class="px-3 py-1.5 text-sm font-medium rounded-md bg-red-100 text-red-800 hover:bg-red-200 transition">Unresolve</button>
|
||||
@else
|
||||
<button name="action" value="unresolve" class="px-3 py-1.5 text-sm font-medium rounded-md bg-red-100 text-red-800 hover:bg-red-200 transition">Unresolve</button>
|
||||
<button name="action" value="resolve" class="px-3 py-1.5 text-sm font-medium rounded-md bg-green-100 text-green-800 hover:bg-green-200 transition">Resolve</button>
|
||||
@endif
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-8">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
||||
|
||||
@if(session('status'))
|
||||
<div class="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded-md text-sm">
|
||||
{{ session('status') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Issue Meta --}}
|
||||
@php
|
||||
$statusColors = [
|
||||
'unresolved' => 'bg-red-100 text-red-800',
|
||||
'resolved' => 'bg-green-100 text-green-800',
|
||||
'ignored' => 'bg-yellow-100 text-yellow-800',
|
||||
'muted' => 'bg-gray-100 text-gray-600',
|
||||
];
|
||||
$levelColors = [
|
||||
'fatal' => 'bg-red-100 text-red-800',
|
||||
'error' => 'bg-red-50 text-red-700',
|
||||
'warning' => 'bg-yellow-50 text-yellow-700',
|
||||
'info' => 'bg-blue-50 text-blue-700',
|
||||
'debug' => 'bg-gray-100 text-gray-600',
|
||||
];
|
||||
@endphp
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg p-6">
|
||||
<div class="flex flex-wrap gap-6 text-sm">
|
||||
<div>
|
||||
<span class="text-gray-500">Status</span>
|
||||
<div class="mt-1">
|
||||
<span class="inline-flex px-2 py-0.5 rounded-full text-xs font-medium {{ $statusColors[$issue->status] ?? '' }}">
|
||||
{{ ucfirst($issue->status) }}
|
||||
@if($issue->is_regression)
|
||||
(regression)
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-500">Level</span>
|
||||
<div class="mt-1">
|
||||
<span class="inline-flex px-2 py-0.5 rounded-full text-xs font-medium {{ $levelColors[$issue->level] ?? '' }}">
|
||||
{{ ucfirst($issue->level) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-500">Events</span>
|
||||
<div class="mt-1 font-mono font-medium text-gray-900">{{ number_format($issue->times_seen) }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-500">First Seen</span>
|
||||
<div class="mt-1 text-gray-900" title="{{ $issue->first_seen_at }}">{{ $issue->first_seen_at->diffForHumans() }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-500">Last Seen</span>
|
||||
<div class="mt-1 text-gray-900" title="{{ $issue->last_seen_at }}">{{ $issue->last_seen_at->diffForHumans() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($latestEvent)
|
||||
{{-- Stacktrace --}}
|
||||
@if(!empty($latestEvent->exception['values']))
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">Stacktrace</h3>
|
||||
</div>
|
||||
@foreach($latestEvent->exception['values'] as $exc)
|
||||
<div class="border-b border-gray-100 last:border-b-0">
|
||||
<div class="px-6 py-3 bg-gray-50">
|
||||
<span class="font-mono text-sm font-semibold text-red-700">{{ $exc['type'] ?? 'Exception' }}</span>
|
||||
@if(!empty($exc['value']))
|
||||
<span class="text-sm text-gray-600 ml-2">{{ $exc['value'] }}</span>
|
||||
@endif
|
||||
</div>
|
||||
@if(!empty($exc['stacktrace']['frames']))
|
||||
<div x-data="{ expandedFrames: {} }" class="divide-y divide-gray-100">
|
||||
@foreach(array_reverse($exc['stacktrace']['frames']) as $i => $frame)
|
||||
@php
|
||||
$file = $frame['filename'] ?? $frame['abs_path'] ?? '?';
|
||||
$fn = $frame['function'] ?? null;
|
||||
$lineno = $frame['lineno'] ?? null;
|
||||
$inApp = $frame['in_app'] ?? false;
|
||||
$context = $frame['context_line'] ?? null;
|
||||
$preContext = $frame['pre_context'] ?? [];
|
||||
$postContext = $frame['post_context'] ?? [];
|
||||
@endphp
|
||||
<div class="{{ $inApp ? 'bg-white' : 'bg-gray-50/50' }}">
|
||||
<button @click="expandedFrames[{{ $i }}] = !expandedFrames[{{ $i }}]"
|
||||
class="w-full px-6 py-2 text-left text-sm flex items-center gap-2 hover:bg-gray-50 transition">
|
||||
<svg :class="expandedFrames[{{ $i }}] ? 'rotate-90' : ''" class="w-3 h-3 text-gray-400 transition-transform shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M6 4l8 6-8 6V4z"/>
|
||||
</svg>
|
||||
<span class="font-mono {{ $inApp ? 'text-gray-900 font-medium' : 'text-gray-500' }} truncate">
|
||||
{{ $file }}@if($lineno):{{ $lineno }}@endif
|
||||
</span>
|
||||
@if($fn)
|
||||
<span class="text-gray-400 shrink-0">in</span>
|
||||
<span class="font-mono {{ $inApp ? 'text-indigo-700' : 'text-gray-500' }} truncate">{{ $fn }}</span>
|
||||
@endif
|
||||
@if($inApp)
|
||||
<span class="ml-auto shrink-0 inline-flex px-1.5 py-0.5 rounded text-[10px] font-medium bg-indigo-50 text-indigo-600">app</span>
|
||||
@endif
|
||||
</button>
|
||||
<div x-show="expandedFrames[{{ $i }}]" x-cloak class="px-6 pb-3">
|
||||
@if($context || !empty($preContext) || !empty($postContext))
|
||||
<pre class="bg-gray-900 rounded-md p-3 text-xs text-gray-300 overflow-x-auto"><code>@foreach($preContext as $ci => $line)
|
||||
<span class="text-gray-600">{{ ($lineno ?? 0) - count($preContext) + $ci }} {{ $line }}</span>
|
||||
@endforeach
|
||||
@if($context)<span class="text-white bg-red-900/40 block -mx-3 px-3">{{ $lineno }} {{ $context }}</span>@endif
|
||||
@foreach($postContext as $ci => $line)
|
||||
<span class="text-gray-600">{{ ($lineno ?? 0) + $ci + 1 }} {{ $line }}</span>
|
||||
@endforeach</code></pre>
|
||||
@elseif(!empty($frame['vars']))
|
||||
<pre class="bg-gray-900 rounded-md p-3 text-xs text-gray-300 overflow-x-auto"><code>{{ json_encode($frame['vars'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) }}</code></pre>
|
||||
@else
|
||||
<p class="text-xs text-gray-400 italic">No source context available.</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Breadcrumbs --}}
|
||||
@if(!empty($latestEvent->breadcrumbs['values'] ?? $latestEvent->breadcrumbs))
|
||||
@php
|
||||
$crumbs = $latestEvent->breadcrumbs['values'] ?? $latestEvent->breadcrumbs;
|
||||
if (!is_array($crumbs)) $crumbs = [];
|
||||
@endphp
|
||||
@if(count($crumbs) > 0)
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">Breadcrumbs</h3>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-100 max-h-96 overflow-y-auto">
|
||||
@foreach($crumbs as $crumb)
|
||||
<div class="px-6 py-2 flex items-start gap-4 text-sm">
|
||||
<span class="shrink-0 text-xs text-gray-400 font-mono w-20 pt-0.5">
|
||||
{{ isset($crumb['timestamp']) ? \Carbon\Carbon::parse($crumb['timestamp'])->format('H:i:s') : '' }}
|
||||
</span>
|
||||
@php
|
||||
$catColor = match($crumb['type'] ?? $crumb['category'] ?? '') {
|
||||
'error' => 'text-red-600',
|
||||
'warning' => 'text-yellow-600',
|
||||
'http', 'navigation' => 'text-blue-600',
|
||||
default => 'text-gray-600',
|
||||
};
|
||||
@endphp
|
||||
<span class="shrink-0 w-20 text-xs font-medium {{ $catColor }} truncate">{{ $crumb['category'] ?? $crumb['type'] ?? 'default' }}</span>
|
||||
<span class="text-gray-800 flex-1 truncate">{{ $crumb['message'] ?? json_encode($crumb['data'] ?? '') }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
{{-- Tags --}}
|
||||
@if(!empty($latestEvent->tags))
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg p-6">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">Tags</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@foreach($latestEvent->tags as $key => $value)
|
||||
<a href="{{ route('projects.issues.index', [$project, 'search' => $key . ':' . $value]) }}"
|
||||
class="inline-flex items-center gap-1 px-2.5 py-1 rounded-md bg-gray-100 hover:bg-gray-200 text-sm transition">
|
||||
<span class="font-medium text-gray-700">{{ $key }}</span>
|
||||
<span class="text-gray-400">=</span>
|
||||
<span class="text-gray-600">{{ $value }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Context Panels --}}
|
||||
@php
|
||||
$contexts = $latestEvent->contexts ?? [];
|
||||
$userCtx = $latestEvent->user_context;
|
||||
$requestCtx = $latestEvent->request_context;
|
||||
@endphp
|
||||
@if(!empty($contexts) || $userCtx || $requestCtx)
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
@if($requestCtx)
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg p-6">
|
||||
<h3 class="text-sm font-medium text-gray-900 mb-3">Request</h3>
|
||||
<dl class="space-y-2 text-sm">
|
||||
@if(!empty($requestCtx['method']))
|
||||
<div class="flex gap-2"><dt class="font-medium text-gray-500 w-20">Method</dt><dd class="text-gray-900 font-mono">{{ $requestCtx['method'] }}</dd></div>
|
||||
@endif
|
||||
@if(!empty($requestCtx['url']))
|
||||
<div class="flex gap-2"><dt class="font-medium text-gray-500 w-20">URL</dt><dd class="text-gray-900 font-mono truncate">{{ $requestCtx['url'] }}</dd></div>
|
||||
@endif
|
||||
@if(!empty($requestCtx['headers']))
|
||||
<div>
|
||||
<dt class="font-medium text-gray-500 mb-1">Headers</dt>
|
||||
<dd><pre class="bg-gray-50 rounded p-2 text-xs overflow-x-auto">{{ json_encode($requestCtx['headers'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) }}</pre></dd>
|
||||
</div>
|
||||
@endif
|
||||
</dl>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($userCtx)
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg p-6">
|
||||
<h3 class="text-sm font-medium text-gray-900 mb-3">User</h3>
|
||||
<dl class="space-y-2 text-sm">
|
||||
@foreach($userCtx as $k => $v)
|
||||
<div class="flex gap-2">
|
||||
<dt class="font-medium text-gray-500 w-20">{{ ucfirst($k) }}</dt>
|
||||
<dd class="text-gray-900">{{ is_string($v) ? $v : json_encode($v) }}</dd>
|
||||
</div>
|
||||
@endforeach
|
||||
</dl>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@foreach($contexts as $ctxName => $ctxData)
|
||||
@if(is_array($ctxData))
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg p-6">
|
||||
<h3 class="text-sm font-medium text-gray-900 mb-3">{{ ucfirst(str_replace('_', ' ', $ctxName)) }}</h3>
|
||||
<dl class="space-y-2 text-sm">
|
||||
@foreach($ctxData as $k => $v)
|
||||
@if($k !== 'type')
|
||||
<div class="flex gap-2">
|
||||
<dt class="font-medium text-gray-500 w-28 shrink-0">{{ ucfirst(str_replace('_', ' ', $k)) }}</dt>
|
||||
<dd class="text-gray-900 truncate">{{ is_string($v) || is_numeric($v) ? $v : json_encode($v) }}</dd>
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</dl>
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Extra Data --}}
|
||||
@if(!empty($latestEvent->extra))
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg p-6">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">Extra Data</h3>
|
||||
<pre class="bg-gray-50 rounded-md p-4 text-sm text-gray-800 overflow-x-auto">{{ json_encode($latestEvent->extra, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) }}</pre>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
{{-- Events List --}}
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">Events</h3>
|
||||
</div>
|
||||
@if($events->isEmpty())
|
||||
<div class="p-6 text-sm text-gray-500">No events recorded.</div>
|
||||
@else
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Event ID</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Environment</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Release</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Timestamp</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@foreach($events as $ev)
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-6 py-3">
|
||||
<a href="{{ route('projects.issues.events.show', [$project, $issue, $ev]) }}" class="text-indigo-600 hover:text-indigo-900 font-mono text-sm">
|
||||
{{ Str::limit($ev->event_id, 16) }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-6 py-3 text-sm text-gray-500">{{ $ev->environment ?? '—' }}</td>
|
||||
<td class="px-6 py-3 text-sm text-gray-500 font-mono">{{ $ev->release ?? '—' }}</td>
|
||||
<td class="px-6 py-3 text-sm text-gray-500 text-right" title="{{ $ev->timestamp }}">{{ $ev->timestamp->diffForHumans() }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="px-6 py-3 border-t border-gray-200">
|
||||
{{ $events->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
Reference in New Issue
Block a user