Improve desktop inspector layout

This commit is contained in:
2026-08-05 15:57:10 +02:00
parent 20fb2d29e2
commit f2757c9b1c
7 changed files with 181 additions and 131 deletions
+2
View File
@@ -40,6 +40,8 @@ class Inspector extends Component
public bool $responseSaved = false; public bool $responseSaved = false;
public bool $showEndpointSettings = false;
public function mount(WebhookEndpoint $endpoint): void public function mount(WebhookEndpoint $endpoint): void
{ {
Gate::authorize('view', $endpoint); Gate::authorize('view', $endpoint);
+1 -1
View File
@@ -1,7 +1,7 @@
@extends('layouts.app') @extends('layouts.app')
@section('content') @section('content')
<div class="mx-auto max-w-7xl px-6 py-10 lg:px-8"> <div class="mx-auto px-6 py-10 lg:px-8">
<livewire:dashboard /> <livewire:dashboard />
</div> </div>
@endsection @endsection
+1 -1
View File
@@ -1,7 +1,7 @@
@extends('layouts.app') @extends('layouts.app')
@section('content') @section('content')
<div class="mx-auto max-w-7xl px-6 py-10 lg:px-8"> <div class="mx-auto px-6 py-10 lg:px-8">
<livewire:inspector :endpoint="$endpoint" /> <livewire:inspector :endpoint="$endpoint" />
</div> </div>
@endsection @endsection
+2 -2
View File
@@ -14,7 +14,7 @@
</head> </head>
<body class="min-h-screen bg-slate-950 font-sans text-slate-100 antialiased"> <body class="min-h-screen bg-slate-950 font-sans text-slate-100 antialiased">
<header class="border-b border-white/10 bg-slate-950/90 backdrop-blur"> <header class="border-b border-white/10 bg-slate-950/90 backdrop-blur">
<div class="mx-auto flex max-w-7xl items-center justify-between gap-6 px-6 py-4 lg:px-8"> <div class="mx-auto flex items-center justify-between gap-6 px-6 py-4 lg:px-8">
<a href="{{ route('home') }}" class="flex items-center gap-3 font-semibold tracking-tight text-white"> <a href="{{ route('home') }}" class="flex items-center gap-3 font-semibold tracking-tight text-white">
<span class="flex size-9 items-center justify-center rounded-xl bg-cyan-400 font-mono text-sm font-bold text-slate-950">WI</span> <span class="flex size-9 items-center justify-center rounded-xl bg-cyan-400 font-mono text-sm font-bold text-slate-950">WI</span>
<span>Webhook Inspector</span> <span>Webhook Inspector</span>
@@ -40,7 +40,7 @@
</header> </header>
@if (session('status')) @if (session('status'))
<div class="mx-auto max-w-7xl px-6 pt-6 lg:px-8"> <div class="mx-auto px-6 pt-6 lg:px-8">
<div class="rounded-xl border border-emerald-400/30 bg-emerald-400/10 px-4 py-3 text-sm text-emerald-200"> <div class="rounded-xl border border-emerald-400/30 bg-emerald-400/10 px-4 py-3 text-sm text-emerald-200">
{{ session('status') }} {{ session('status') }}
</div> </div>
+160 -125
View File
@@ -1,4 +1,4 @@
<div wire:poll.15s class="space-y-6"> <div wire:poll.15s wire:keydown.escape="$set('showEndpointSettings', false)" class="space-y-6">
<div class="flex flex-col justify-between gap-6 lg:flex-row lg:items-end"> <div class="flex flex-col justify-between gap-6 lg:flex-row lg:items-end">
<div class="min-w-0"> <div class="min-w-0">
<div class="flex flex-wrap items-center gap-3 text-sm text-slate-400"> <div class="flex flex-wrap items-center gap-3 text-sm text-slate-400">
@@ -12,30 +12,36 @@
</div> </div>
<div class="flex flex-wrap items-center gap-3"> <div class="flex flex-wrap items-center gap-3">
<button type="button" wire:click="$toggle('showEndpointSettings')" class="cursor-pointer rounded-xl border border-cyan-300/25 bg-cyan-300/10 px-4 py-2.5 text-sm font-medium text-cyan-200 transition hover:border-cyan-300/50 hover:bg-cyan-300/15">
{{ $showEndpointSettings ? 'Close settings' : 'Endpoint settings' }}
</button>
@if ($endpoint->is_public) @if ($endpoint->is_public)
<form action="{{ route('inspect.destroy', ['token' => $endpoint->token]) }}" method="POST"> <form action="{{ route('inspect.destroy', ['token' => $endpoint->token]) }}" method="POST">
@csrf @csrf
@method('DELETE') @method('DELETE')
<button type="submit" class="rounded-xl border border-rose-300/20 px-4 py-2.5 text-sm font-medium text-rose-300 transition hover:bg-rose-400/10" onclick="return confirm('Delete this endpoint and all captured requests?')">Delete URL</button> <button type="submit" class="cursor-pointer rounded-xl border border-rose-300/20 px-4 py-2.5 text-sm font-medium text-rose-300 transition hover:bg-rose-400/10" onclick="return confirm('Delete this endpoint and all captured requests?')">Delete URL</button>
</form> </form>
@else @else
<button type="button" wire:click="deleteEndpoint" wire:confirm="Delete this endpoint and all captured requests?" class="rounded-xl border border-rose-300/20 px-4 py-2.5 text-sm font-medium text-rose-300 transition hover:bg-rose-400/10">Delete URL</button> <button type="button" wire:click="deleteEndpoint" wire:confirm="Delete this endpoint and all captured requests?" class="cursor-pointer rounded-xl border border-rose-300/20 px-4 py-2.5 text-sm font-medium text-rose-300 transition hover:bg-rose-400/10">Delete URL</button>
@endif @endif
</div> </div>
</div> </div>
<div class="grid gap-4 xl:grid-cols-[1fr_20rem]"> <div class="grid gap-4 lg:grid-cols-[22rem_minmax(0,1fr)] lg:items-start">
<section class="min-w-0 rounded-2xl border border-white/10 bg-white/[0.04] p-5 sm:p-6"> <section class="min-w-0 rounded-2xl border border-white/10 bg-white/[0.04] p-4 sm:p-5 lg:sticky lg:top-6 lg:flex lg:h-[calc(100vh-13rem)] lg:min-h-[32rem] lg:flex-col">
<div class="flex flex-col justify-between gap-4 border-b border-white/10 pb-5 sm:flex-row sm:items-center"> <div class="shrink-0 border-b border-white/10 pb-4">
<div> <div class="flex items-start justify-between gap-3">
<h2 class="font-medium text-white">Incoming requests</h2> <div>
<p class="mt-1 text-sm text-slate-500">New requests are stored immediately and appear here live.</p> <h2 class="font-medium text-white">Incoming requests</h2>
<p class="mt-1 text-sm text-slate-500">Live request history</p>
</div>
<span class="rounded-full border border-white/10 px-2 py-1 text-[11px] text-slate-500">{{ $webhookRequests->total() }} total</span>
</div> </div>
<div class="flex flex-col gap-2 sm:flex-row"> <div class="mt-4 grid gap-2">
<label class="sr-only" for="request-search">Search requests</label> <label class="sr-only" for="request-search">Search requests</label>
<input id="request-search" type="search" wire:model.live.debounce.350ms="search" placeholder="Search URI, IP, agent" class="rounded-lg border border-white/10 bg-slate-950/70 px-3 py-2 text-sm text-white outline-none placeholder:text-slate-600 focus:border-cyan-300/60"> <input id="request-search" type="search" wire:model.live.debounce.350ms="search" placeholder="Search URI, IP, agent" class="w-full rounded-lg border border-white/10 bg-slate-950/70 px-3 py-2 text-sm text-white outline-none placeholder:text-slate-600 focus:border-cyan-300/60">
<label class="sr-only" for="method-filter">Filter method</label> <label class="sr-only" for="method-filter">Filter method</label>
<select id="method-filter" wire:model.live="methodFilter" class="rounded-lg border border-white/10 bg-slate-950/70 px-3 py-2 text-sm text-white outline-none focus:border-cyan-300/60"> <select id="method-filter" wire:model.live="methodFilter" class="w-full rounded-lg border border-white/10 bg-slate-950/70 px-3 py-2 text-sm text-white outline-none focus:border-cyan-300/60">
@foreach (['ALL', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'] as $method) @foreach (['ALL', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'] as $method)
<option value="{{ $method }}">{{ $method === 'ALL' ? 'All methods' : $method }}</option> <option value="{{ $method }}">{{ $method === 'ALL' ? 'All methods' : $method }}</option>
@endforeach @endforeach
@@ -44,147 +50,176 @@
</div> </div>
@if ($webhookRequests->isEmpty()) @if ($webhookRequests->isEmpty())
<div class="py-16 text-center"> <div class="flex min-h-[20rem] flex-1 flex-col items-center justify-center px-3 text-center">
<p class="font-mono text-sm text-slate-500">waiting for request...</p> <p class="font-mono text-sm text-slate-500">waiting for request...</p>
<p class="mt-2 text-sm text-slate-600">Send a request to the webhook URL below to see it here.</p> <p class="mt-2 text-sm leading-6 text-slate-600">Open endpoint settings to find the URL for the next request.</p>
</div> </div>
@else @else
<div class="mt-5 overflow-x-auto"> <div class="mt-4 min-h-0 flex-1 overflow-y-auto pr-1">
<table class="w-full min-w-[38rem] text-left text-sm"> <div class="space-y-2">
<thead class="text-xs uppercase tracking-wider text-slate-600"> @foreach ($webhookRequests as $webhookRequest)
<tr> <button
<th class="pb-3 pr-4 font-medium">Method</th> type="button"
<th class="pb-3 pr-4 font-medium">URI</th> wire:key="request-{{ $webhookRequest->id }}"
<th class="pb-3 pr-4 font-medium">Type</th> wire:click="selectRequest('{{ $webhookRequest->id }}')"
<th class="pb-3 pr-4 font-medium">Size</th> class="w-full cursor-pointer rounded-xl border px-3 py-3 text-left transition {{ $selectedRequestId === $webhookRequest->id ? 'border-cyan-300/50 bg-cyan-300/[0.08]' : 'border-white/10 bg-white/[0.02] hover:border-white/20 hover:bg-white/[0.04]' }}"
<th class="pb-3 text-right font-medium">Received</th> >
</tr> <div class="flex items-center justify-between gap-3">
</thead> <span class="rounded-md bg-cyan-300/10 px-2 py-1 font-mono text-[11px] font-medium text-cyan-200">{{ $webhookRequest->method }}</span>
<tbody class="divide-y divide-white/5"> <span class="shrink-0 text-[11px] text-slate-600">{{ $webhookRequest->received_at?->diffForHumans() }}</span>
@foreach ($webhookRequests as $webhookRequest) </div>
<tr wire:key="request-{{ $webhookRequest->id }}" class="group cursor-pointer transition hover:bg-white/[0.03]" wire:click="selectRequest('{{ $webhookRequest->id }}')"> <div class="mt-2 truncate font-mono text-xs text-slate-300">{{ $webhookRequest->request_uri }}</div>
<td class="py-4 pr-4 align-top"><span class="rounded-md bg-cyan-300/10 px-2 py-1 font-mono text-xs font-medium text-cyan-200">{{ $webhookRequest->method }}</span></td> <div class="mt-2 flex items-center justify-between gap-3 text-[11px] text-slate-600">
<td class="max-w-[24rem] truncate py-4 pr-4 font-mono text-xs text-slate-300">{{ $webhookRequest->request_uri }}</td> <span class="truncate">{{ $webhookRequest->content_type ?: 'No content type' }}</span>
<td class="py-4 pr-4 text-xs text-slate-500">{{ $webhookRequest->content_type ?: '—' }}</td> <span class="shrink-0 font-mono">{{ $webhookRequest->body_size }} B</span>
<td class="py-4 pr-4 font-mono text-xs text-slate-500">{{ $webhookRequest->body_size }} B</td> </div>
<td class="py-4 text-right text-xs text-slate-500">{{ $webhookRequest->received_at?->diffForHumans() }}</td> </button>
</tr> @endforeach
@endforeach </div>
</tbody>
</table>
</div> </div>
<div class="mt-5">{{ $webhookRequests->links() }}</div> <div class="mt-4 shrink-0 overflow-x-auto border-t border-white/10 pt-4">{{ $webhookRequests->links() }}</div>
@endif @endif
</section> </section>
<aside class="space-y-4"> <section class="min-w-0 overflow-hidden rounded-2xl border border-white/10 bg-white/[0.04] lg:h-[calc(100vh-13rem)] lg:overflow-y-auto">
<section class="rounded-2xl border border-white/10 bg-white/[0.04] p-5"> @if ($selectedRequest)
<h2 class="font-medium text-white">Send requests here</h2> <div class="flex flex-col justify-between gap-4 border-b border-white/10 p-5 sm:flex-row sm:items-start sm:p-6">
<p class="mt-1 text-sm text-slate-500">Any HTTP method is accepted.</p> <div class="min-w-0">
<div class="mt-4 space-y-3"> <div class="flex flex-wrap items-center gap-3">
<div> <span class="rounded-md bg-cyan-300/10 px-2 py-1 font-mono text-xs font-medium text-cyan-200">{{ $selectedRequest->method }}</span>
<div class="mb-1 text-[11px] uppercase tracking-wider text-slate-600">Webhook URL</div> <span class="text-xs uppercase tracking-wider text-slate-600">Request analysis</span>
<code class="block break-all rounded-lg bg-slate-950/80 p-3 text-xs leading-5 text-cyan-200">{{ $endpoint->webhookUrl() }}</code> </div>
<h2 class="mt-3 break-all font-mono text-sm text-white">{{ $selectedRequest->request_uri }}</h2>
<p class="mt-2 font-mono text-xs text-slate-600">{{ $selectedRequest->id }}</p>
</div> </div>
<div> <div class="flex shrink-0 items-center gap-3">
<div class="mb-1 text-[11px] uppercase tracking-wider text-slate-600">Example</div> <button type="button" wire:click="deleteRequest('{{ $selectedRequest->id }}')" wire:confirm="Delete this request?" class="cursor-pointer text-sm text-rose-300 transition hover:text-rose-200">Delete request</button>
<code class="block break-all rounded-lg bg-slate-950/80 p-3 text-xs leading-5 text-slate-400">curl -X POST -d '{"event":"test"}' {{ $endpoint->webhookUrl() }}</code> <button type="button" wire:click="clearSelectedRequest" class="cursor-pointer rounded-lg border border-white/10 px-3 py-2 text-sm text-slate-300 transition hover:border-white/25 hover:text-white">Close</button>
</div> </div>
</div> </div>
</section>
@if (! $endpoint->is_public) <div class="space-y-6 p-5 sm:p-6">
<section class="rounded-2xl border border-white/10 bg-white/[0.04] p-5"> <div class="grid gap-6 lg:grid-cols-2">
<div class="flex items-start justify-between gap-3">
<div> <div>
<h2 class="font-medium text-white">Response</h2> <h3 class="text-xs font-medium uppercase tracking-wider text-slate-500">Request metadata</h3>
<p class="mt-1 text-sm text-slate-500">Configure what the sender receives.</p> <dl class="mt-3 divide-y divide-white/5 rounded-xl border border-white/10 bg-slate-950/40 px-4 text-sm">
<div class="flex justify-between gap-4 py-3"><dt class="text-slate-500">Received</dt><dd class="text-right text-slate-200">{{ $selectedRequest->received_at?->format('Y-m-d H:i:s T') }}</dd></div>
<div class="flex justify-between gap-4 py-3"><dt class="text-slate-500">Content-Type</dt><dd class="max-w-[65%] break-all text-right font-mono text-xs text-slate-200">{{ $selectedRequest->content_type ?: '—' }}</dd></div>
<div class="flex justify-between gap-4 py-3"><dt class="text-slate-500">Size</dt><dd class="text-right text-slate-200">{{ $selectedRequest->body_size }} bytes</dd></div>
<div class="flex justify-between gap-4 py-3"><dt class="text-slate-500">IP address</dt><dd class="text-right font-mono text-xs text-slate-200">{{ $selectedRequest->ip_address ?: '—' }}</dd></div>
<div class="flex justify-between gap-4 py-3"><dt class="text-slate-500">User-Agent</dt><dd class="max-w-[65%] break-all text-right text-xs text-slate-200">{{ $selectedRequest->user_agent ?: '—' }}</dd></div>
</dl>
</div>
<div>
<h3 class="text-xs font-medium uppercase tracking-wider text-slate-500">Query parameters</h3>
<pre class="mt-3 max-h-64 overflow-auto rounded-xl border border-white/10 bg-slate-950/70 p-4 font-mono text-xs leading-6 text-slate-300">{{ json_encode($selectedRequest->query_parameters ?? [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) }}</pre>
</div> </div>
@if ($responseSaved)
<span class="text-xs text-emerald-300">Saved</span>
@endif
</div> </div>
<form wire:submit="saveResponse" class="mt-5 space-y-4">
<div class="grid gap-6 lg:grid-cols-2">
<div> <div>
<label for="response-status" class="text-xs font-medium uppercase tracking-wider text-slate-500">Status</label> <h3 class="text-xs font-medium uppercase tracking-wider text-slate-500">Headers</h3>
<input id="response-status" type="number" wire:model="responseStatus" min="100" max="599" class="mt-2 w-full rounded-lg border border-white/10 bg-slate-950/70 px-3 py-2 text-sm text-white outline-none focus:border-cyan-300/60"> <div class="mt-3 max-h-80 overflow-auto rounded-xl border border-white/10 bg-slate-950/70 p-4 font-mono text-xs leading-6">
@error('responseStatus') <p class="mt-1 text-xs text-rose-300">{{ $message }}</p> @enderror @forelse ($selectedRequest->headers ?? [] as $name => $values)
<div class="flex gap-3"><span class="shrink-0 text-cyan-300">{{ $name }}:</span><span class="break-all text-slate-300">{{ implode(', ', (array) $values) }}</span></div>
@empty
<span class="text-slate-600">No headers captured.</span>
@endforelse
</div>
</div> </div>
<div> <div>
<label for="response-headers" class="text-xs font-medium uppercase tracking-wider text-slate-500">Headers (JSON)</label> <h3 class="text-xs font-medium uppercase tracking-wider text-slate-500">Body</h3>
<textarea id="response-headers" wire:model="responseHeadersJson" rows="4" spellcheck="false" class="mt-2 w-full rounded-lg border border-white/10 bg-slate-950/70 px-3 py-2 font-mono text-xs text-white outline-none focus:border-cyan-300/60">{{ $responseHeadersJson }}</textarea> <pre class="mt-3 max-h-80 overflow-auto whitespace-pre-wrap break-words rounded-xl border border-white/10 bg-slate-950/70 p-4 font-mono text-xs leading-6 text-emerald-200">{{ $selectedRequest->body ?? '' }}</pre>
@error('responseHeadersJson') <p class="mt-1 text-xs text-rose-300">{{ $message }}</p> @enderror
</div> </div>
</div>
@if ($selectedRequest->json_payload !== null)
<div> <div>
<label for="response-body" class="text-xs font-medium uppercase tracking-wider text-slate-500">Body</label> <h3 class="text-xs font-medium uppercase tracking-wider text-slate-500">Parsed JSON</h3>
<textarea id="response-body" wire:model="responseBody" rows="5" spellcheck="false" class="mt-2 w-full rounded-lg border border-white/10 bg-slate-950/70 px-3 py-2 font-mono text-xs text-white outline-none focus:border-cyan-300/60">{{ $responseBody }}</textarea> <pre class="mt-3 max-h-80 overflow-auto rounded-xl border border-white/10 bg-slate-950/70 p-4 font-mono text-xs leading-6 text-emerald-200">{{ json_encode($selectedRequest->json_payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) }}</pre>
@error('responseBody') <p class="mt-1 text-xs text-rose-300">{{ $message }}</p> @enderror
</div> </div>
<button type="submit" class="w-full rounded-lg bg-white px-3 py-2.5 text-sm font-semibold text-slate-950 transition hover:bg-cyan-50">Save response</button> @endif
</form> </div>
</section>
@else @else
<section class="rounded-2xl border border-amber-300/15 bg-amber-300/[0.05] p-5 text-sm leading-6 text-amber-100/80"> <div class="flex min-h-[32rem] flex-col items-center justify-center p-8 text-center lg:min-h-[calc(100vh-13rem)]">
This temporary endpoint expires after {{ $endpoint->expires_at?->diffForHumans(null, true) }}. The default response is <code class="font-mono text-amber-200">200 {}</code>. <div class="flex size-12 items-center justify-center rounded-2xl bg-cyan-300/10 font-mono text-sm text-cyan-300">//</div>
</section> <p class="mt-5 text-xs font-medium uppercase tracking-[0.18em] text-cyan-300">Request analysis</p>
<h2 class="mt-3 text-xl font-medium text-white">Select an incoming request</h2>
<p class="mt-2 max-w-md text-sm leading-6 text-slate-500">Choose a request from the left to inspect its headers, query parameters, body, and parsed payload here.</p>
</div>
@endif @endif
</aside> </section>
</div> </div>
@if ($selectedRequest) @if ($showEndpointSettings)
<section class="rounded-2xl border border-white/10 bg-white/[0.04] p-5 sm:p-6"> <div class="fixed inset-0 z-50 overflow-y-auto p-4 sm:p-6" role="dialog" aria-modal="true" aria-labelledby="endpoint-settings-title">
<div class="flex flex-col justify-between gap-4 border-b border-white/10 pb-5 sm:flex-row sm:items-start"> <button type="button" wire:click="$set('showEndpointSettings', false)" class="absolute inset-0 size-full cursor-pointer bg-slate-950/80 backdrop-blur-sm" aria-label="Close endpoint settings"></button>
<div>
<div class="flex items-center gap-3"><span class="rounded-md bg-cyan-300/10 px-2 py-1 font-mono text-xs font-medium text-cyan-200">{{ $selectedRequest->method }}</span><span class="font-mono text-xs text-slate-500">{{ $selectedRequest->id }}</span></div>
<h2 class="mt-3 break-all font-mono text-sm text-white">{{ $selectedRequest->request_uri }}</h2>
</div>
<div class="flex items-center gap-3">
<button type="button" wire:click="deleteRequest('{{ $selectedRequest->id }}')" wire:confirm="Delete this request?" class="text-sm text-rose-300 transition hover:text-rose-200">Delete request</button>
<button type="button" wire:click="clearSelectedRequest" class="rounded-lg border border-white/10 px-3 py-2 text-sm text-slate-300 transition hover:border-white/25 hover:text-white">Close</button>
</div>
</div>
<div class="mt-6 grid gap-6 lg:grid-cols-2"> <div class="relative z-10 mx-auto my-4 w-full max-w-5xl overflow-hidden rounded-2xl border border-white/15 bg-slate-900 shadow-2xl shadow-black/50 sm:my-8">
<div> <div class="flex items-start justify-between gap-4 border-b border-white/10 p-5 sm:p-6">
<h3 class="text-xs font-medium uppercase tracking-wider text-slate-500">Request metadata</h3> <div>
<dl class="mt-3 divide-y divide-white/5 rounded-xl border border-white/10 bg-slate-950/40 px-4 text-sm"> <p class="text-xs font-medium uppercase tracking-[0.18em] text-cyan-300">Endpoint configuration</p>
<div class="flex justify-between gap-4 py-3"><dt class="text-slate-500">Received</dt><dd class="text-right text-slate-200">{{ $selectedRequest->received_at?->format('Y-m-d H:i:s T') }}</dd></div> <h2 id="endpoint-settings-title" class="mt-2 text-xl font-semibold text-white">Endpoint settings</h2>
<div class="flex justify-between gap-4 py-3"><dt class="text-slate-500">Content-Type</dt><dd class="max-w-[65%] break-all text-right font-mono text-xs text-slate-200">{{ $selectedRequest->content_type ?: '—' }}</dd></div> <p class="mt-2 text-sm text-slate-500">Manage the URL for incoming calls and the response sent back to private endpoints.</p>
<div class="flex justify-between gap-4 py-3"><dt class="text-slate-500">Size</dt><dd class="text-right text-slate-200">{{ $selectedRequest->body_size }} bytes</dd></div>
<div class="flex justify-between gap-4 py-3"><dt class="text-slate-500">IP address</dt><dd class="text-right font-mono text-xs text-slate-200">{{ $selectedRequest->ip_address ?: '—' }}</dd></div>
<div class="flex justify-between gap-4 py-3"><dt class="text-slate-500">User-Agent</dt><dd class="max-w-[65%] break-all text-right text-xs text-slate-200">{{ $selectedRequest->user_agent ?: '—' }}</dd></div>
</dl>
</div>
<div>
<h3 class="text-xs font-medium uppercase tracking-wider text-slate-500">Query parameters</h3>
<pre class="mt-3 max-h-48 overflow-auto rounded-xl border border-white/10 bg-slate-950/70 p-4 font-mono text-xs leading-6 text-slate-300">{{ json_encode($selectedRequest->query_parameters ?? [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) }}</pre>
</div>
</div>
<div class="mt-6 grid gap-6 lg:grid-cols-2">
<div>
<h3 class="text-xs font-medium uppercase tracking-wider text-slate-500">Headers</h3>
<div class="mt-3 max-h-72 overflow-auto rounded-xl border border-white/10 bg-slate-950/70 p-4 font-mono text-xs leading-6">
@forelse ($selectedRequest->headers ?? [] as $name => $values)
<div class="flex gap-3"><span class="shrink-0 text-cyan-300">{{ $name }}:</span><span class="break-all text-slate-300">{{ implode(', ', (array) $values) }}</span></div>
@empty
<span class="text-slate-600">No headers captured.</span>
@endforelse
</div> </div>
<button type="button" wire:click="$set('showEndpointSettings', false)" class="shrink-0 cursor-pointer rounded-lg border border-white/10 px-3 py-2 text-sm text-slate-300 transition hover:border-white/25 hover:text-white">Close</button>
</div> </div>
<div>
<h3 class="text-xs font-medium uppercase tracking-wider text-slate-500">Body</h3> <div class="grid gap-6 p-5 sm:p-6 xl:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)]">
<pre class="mt-3 max-h-72 overflow-auto rounded-xl border border-white/10 bg-slate-950/70 p-4 font-mono text-xs leading-6 text-emerald-200">{{ $selectedRequest->body ?? '' }}</pre> <section class="rounded-xl border border-white/10 bg-slate-950/40 p-5">
<h3 class="font-medium text-white">Send requests here</h3>
<p class="mt-1 text-sm text-slate-500">Any HTTP method is accepted.</p>
<div class="mt-5 space-y-4">
<div>
<div class="mb-1 text-[11px] uppercase tracking-wider text-slate-600">Webhook URL</div>
<code class="block break-all rounded-lg bg-slate-950/80 p-3 text-xs leading-5 text-cyan-200">{{ $endpoint->webhookUrl() }}</code>
</div>
<div>
<div class="mb-1 text-[11px] uppercase tracking-wider text-slate-600">Example</div>
<code class="block break-all rounded-lg bg-slate-950/80 p-3 text-xs leading-5 text-slate-400">curl -X POST -d '{"event":"test"}' {{ $endpoint->webhookUrl() }}</code>
</div>
</div>
</section>
@if (! $endpoint->is_public)
<section class="rounded-xl border border-white/10 bg-slate-950/40 p-5">
<div class="flex items-start justify-between gap-3">
<div>
<h3 class="font-medium text-white">Response</h3>
<p class="mt-1 text-sm text-slate-500">Configure what the sender receives.</p>
</div>
@if ($responseSaved)
<span class="text-xs text-emerald-300">Saved</span>
@endif
</div>
<form wire:submit="saveResponse" class="mt-5 space-y-4">
<div>
<label for="response-status" class="text-xs font-medium uppercase tracking-wider text-slate-500">Status</label>
<input id="response-status" type="number" wire:model="responseStatus" min="100" max="599" class="mt-2 w-full rounded-lg border border-white/10 bg-slate-950/70 px-3 py-2 text-sm text-white outline-none focus:border-cyan-300/60">
@error('responseStatus') <p class="mt-1 text-xs text-rose-300">{{ $message }}</p> @enderror
</div>
<div>
<label for="response-headers" class="text-xs font-medium uppercase tracking-wider text-slate-500">Headers (JSON)</label>
<textarea id="response-headers" wire:model="responseHeadersJson" rows="5" spellcheck="false" class="mt-2 w-full rounded-lg border border-white/10 bg-slate-950/70 px-3 py-2 font-mono text-xs text-white outline-none focus:border-cyan-300/60">{{ $responseHeadersJson }}</textarea>
@error('responseHeadersJson') <p class="mt-1 text-xs text-rose-300">{{ $message }}</p> @enderror
</div>
<div>
<label for="response-body" class="text-xs font-medium uppercase tracking-wider text-slate-500">Body</label>
<textarea id="response-body" wire:model="responseBody" rows="7" spellcheck="false" class="mt-2 w-full rounded-lg border border-white/10 bg-slate-950/70 px-3 py-2 font-mono text-xs text-white outline-none focus:border-cyan-300/60">{{ $responseBody }}</textarea>
@error('responseBody') <p class="mt-1 text-xs text-rose-300">{{ $message }}</p> @enderror
</div>
<button type="submit" class="w-full cursor-pointer rounded-lg bg-white px-3 py-2.5 text-sm font-semibold text-slate-950 transition hover:bg-cyan-50">Save response</button>
</form>
</section>
@else
<section class="rounded-xl border border-amber-300/15 bg-amber-300/[0.05] p-5 text-sm leading-6 text-amber-100/80">
<h3 class="font-medium text-amber-100">Response</h3>
<p class="mt-2">This temporary endpoint expires after {{ $endpoint->expires_at?->diffForHumans(null, true) }}. The default response is <code class="font-mono text-amber-200">200 {}</code>.</p>
</section>
@endif
</div> </div>
</div> </div>
</div>
@if ($selectedRequest->json_payload !== null)
<div class="mt-6">
<h3 class="text-xs font-medium uppercase tracking-wider text-slate-500">Parsed JSON</h3>
<pre class="mt-3 max-h-72 overflow-auto rounded-xl border border-white/10 bg-slate-950/70 p-4 font-mono text-xs leading-6 text-emerald-200">{{ json_encode($selectedRequest->json_payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) }}</pre>
</div>
@endif
</section>
@endif @endif
</div> </div>
+2 -2
View File
@@ -4,7 +4,7 @@
<div class="relative isolate overflow-hidden"> <div class="relative isolate overflow-hidden">
<div class="absolute inset-x-0 top-0 -z-10 h-[34rem] bg-[radial-gradient(circle_at_top_right,rgba(34,211,238,0.16),transparent_45%),radial-gradient(circle_at_top_left,rgba(99,102,241,0.16),transparent_42%)]"></div> <div class="absolute inset-x-0 top-0 -z-10 h-[34rem] bg-[radial-gradient(circle_at_top_right,rgba(34,211,238,0.16),transparent_45%),radial-gradient(circle_at_top_left,rgba(99,102,241,0.16),transparent_42%)]"></div>
<div class="mx-auto grid max-w-7xl gap-16 px-6 py-20 lg:grid-cols-[1.1fr_0.9fr] lg:items-center lg:px-8 lg:py-28"> <div class="mx-auto grid gap-16 px-6 py-20 lg:grid-cols-[1.1fr_0.9fr] lg:items-center lg:px-8 lg:py-28">
<div> <div>
<div class="mb-6 inline-flex items-center gap-2 rounded-full border border-cyan-300/20 bg-cyan-300/10 px-3 py-1 text-xs font-medium uppercase tracking-[0.18em] text-cyan-200"> <div class="mb-6 inline-flex items-center gap-2 rounded-full border border-cyan-300/20 bg-cyan-300/10 px-3 py-1 text-xs font-medium uppercase tracking-[0.18em] text-cyan-200">
<span class="size-1.5 rounded-full bg-cyan-300"></span> <span class="size-1.5 rounded-full bg-cyan-300"></span>
@@ -53,7 +53,7 @@
</div> </div>
</div> </div>
<div class="mx-auto grid max-w-7xl gap-4 px-6 pb-20 sm:grid-cols-3 lg:px-8"> <div class="mx-auto grid gap-4 px-6 pb-20 sm:grid-cols-3 lg:px-8">
@foreach ([['Temporary URLs', 'Start without an account.'], ['Full request context', 'Headers, query and raw body.'], ['Live updates', 'Requests appear as they arrive.']] as [$title, $description]) @foreach ([['Temporary URLs', 'Start without an account.'], ['Full request context', 'Headers, query and raw body.'], ['Live updates', 'Requests appear as they arrive.']] as [$title, $description])
<div class="rounded-2xl border border-white/10 bg-white/[0.03] p-5"> <div class="rounded-2xl border border-white/10 bg-white/[0.03] p-5">
<h2 class="font-medium text-white">{{ $title }}</h2> <h2 class="font-medium text-white">{{ $title }}</h2>
+13
View File
@@ -115,6 +115,19 @@ class WebhookInspectorTest extends TestCase
$this->assertSame('queued', $endpoint->response_body); $this->assertSame('queued', $endpoint->response_body);
} }
public function test_private_endpoint_settings_are_available_in_a_modal(): void
{
$user = User::factory()->create();
$endpoint = WebhookEndpoint::factory()->ownedBy($user)->create();
Livewire::actingAs($user)
->test(Inspector::class, ['endpoint' => $endpoint])
->set('showEndpointSettings', true)
->assertSee('Endpoint settings')
->assertSee('Send requests here')
->assertSee('Save response');
}
public function test_request_body_is_escaped_in_the_inspector(): void public function test_request_body_is_escaped_in_the_inspector(): void
{ {
$endpoint = WebhookEndpoint::factory()->create(); $endpoint = WebhookEndpoint::factory()->create();