From a8fb275793a92c0e2f5483aef01d79ea769646a6 Mon Sep 17 00:00:00 2001 From: Ovidiu U Date: Sat, 4 Apr 2026 14:09:32 +0100 Subject: [PATCH] feat: add ApiLogResource with filters and view page Co-Authored-By: Claude Sonnet 4.6 --- app/Filament/Resources/ApiLogResource.php | 134 ++++++++++++++++++ .../ApiLogResource/Pages/ListApiLogs.php | 16 +++ .../ApiLogResource/Pages/ViewApiLog.php | 16 +++ tests/Feature/Admin/ApiLogResourceTest.php | 32 +++++ 4 files changed, 198 insertions(+) create mode 100644 app/Filament/Resources/ApiLogResource.php create mode 100644 app/Filament/Resources/ApiLogResource/Pages/ListApiLogs.php create mode 100644 app/Filament/Resources/ApiLogResource/Pages/ViewApiLog.php create mode 100644 tests/Feature/Admin/ApiLogResourceTest.php diff --git a/app/Filament/Resources/ApiLogResource.php b/app/Filament/Resources/ApiLogResource.php new file mode 100644 index 0000000..33f5394 --- /dev/null +++ b/app/Filament/Resources/ApiLogResource.php @@ -0,0 +1,134 @@ +columns([ + TextColumn::make('service') + ->badge() + ->color(fn (string $state) => match ($state) { + 'fuel_finder' => 'success', + 'fred' => 'info', + 'anthropic' => 'warning', + default => 'gray', + }) + ->sortable(), + TextColumn::make('method') + ->badge() + ->color('gray'), + TextColumn::make('url') + ->limit(60) + ->tooltip(fn (ApiLog $record) => $record->url), + TextColumn::make('status_code') + ->badge() + ->color(fn (?int $state) => match (true) { + $state === null => 'danger', + $state >= 500 => 'danger', + $state >= 400 => 'warning', + default => 'success', + }), + TextColumn::make('duration_ms') + ->label('Duration (ms)') + ->sortable(), + TextColumn::make('error') + ->limit(40) + ->placeholder('—'), + TextColumn::make('created_at') + ->dateTime('d M Y H:i') + ->sortable(), + ]) + ->defaultSort('created_at', 'desc') + ->filters([ + SelectFilter::make('service') + ->options([ + 'fuel_finder' => 'Fuel Finder', + 'fred' => 'FRED', + 'anthropic' => 'Anthropic', + 'postcodes_io' => 'Postcodes.io', + ]), + Filter::make('errors_only') + ->label('Errors only') + ->query(fn (Builder $query) => $query->where( + fn (Builder $q) => $q->where('status_code', '>=', 400) + ->orWhereNotNull('error') + )), + Filter::make('created_at') + ->schema([ + DatePicker::make('from')->label('From'), + DatePicker::make('until')->label('Until'), + ]) + ->query(function (Builder $query, array $data) { + $query + ->when($data['from'], fn ($q, $d) => $q->whereDate('created_at', '>=', $d)) + ->when($data['until'], fn ($q, $d) => $q->whereDate('created_at', '<=', $d)); + }), + ]) + ->recordActions([ + ViewAction::make(), + ]) + ->toolbarActions([]); + } + + public static function infolist(Schema $schema): Schema + { + return $schema->components([ + Section::make('Request')->schema([ + TextEntry::make('service')->badge(), + TextEntry::make('method'), + TextEntry::make('url')->columnSpanFull(), + TextEntry::make('status_code') + ->badge() + ->color(fn (?int $state) => match (true) { + $state === null => 'danger', + $state >= 500 => 'danger', + $state >= 400 => 'warning', + default => 'success', + }), + TextEntry::make('duration_ms')->label('Duration (ms)'), + TextEntry::make('created_at')->dateTime('d M Y H:i:s'), + ])->columns(3), + Section::make('Error')->schema([ + TextEntry::make('error') + ->columnSpanFull() + ->placeholder('No error recorded'), + ])->collapsed(fn (ApiLog $record) => $record->error === null), + ]); + } + + public static function getPages(): array + { + return [ + 'index' => ListApiLogs::route('/'), + 'view' => ViewApiLog::route('/{record}'), + ]; + } +} diff --git a/app/Filament/Resources/ApiLogResource/Pages/ListApiLogs.php b/app/Filament/Resources/ApiLogResource/Pages/ListApiLogs.php new file mode 100644 index 0000000..f801946 --- /dev/null +++ b/app/Filament/Resources/ApiLogResource/Pages/ListApiLogs.php @@ -0,0 +1,16 @@ +admin = User::factory()->admin()->create(); + $this->actingAs($this->admin); +}); + +it('renders the api log list page', function () { + $logs = ApiLog::factory()->count(3)->create(); + + Livewire::test(ListApiLogs::class) + ->assertOk() + ->assertCanSeeTableRecords($logs); +}); + +it('filters to errors only', function () { + $ok = ApiLog::factory()->create(['status_code' => 200, 'error' => null]); + $err = ApiLog::factory()->failed()->create(); + + Livewire::test(ListApiLogs::class) + ->filterTable('errors_only') + ->assertCanSeeTableRecords([$err]) + ->assertCanNotSeeTableRecords([$ok]); +});