TenantAtlas/app/Filament/Resources/AlertDestinationResource/Pages/ViewAlertDestination.php
ahmido d49d33ac27 feat(alerts): test message + last test status + deep links (#122)
Implements feature 100 (Alert Targets):

- US1: “Send test message” action (RBAC + confirmation + rate limit + audit + async job)
- US2: Derived “Last test” status badge (Never/Sent/Failed/Pending) on view + edit surfaces
- US3: “View last delivery” deep link + deliveries viewer filters (event_type, destination) incl. tenantless test deliveries

Tests:
- Full suite green (1348 passed, 7 skipped)
- Added focused feature tests for send test, last test resolver/badges, and deep-link filters

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #122
2026-02-18 23:12:38 +00:00

155 lines
5.9 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Filament\Resources\AlertDestinationResource\Pages;
use App\Filament\Resources\AlertDeliveryResource;
use App\Filament\Resources\AlertDestinationResource;
use App\Models\AlertDestination;
use App\Models\User;
use App\Services\Alerts\AlertDestinationLastTestResolver;
use App\Services\Alerts\AlertDestinationTestMessageService;
use App\Support\Alerts\AlertDestinationLastTestStatus;
use App\Support\Badges\BadgeDomain;
use App\Support\Badges\BadgeRenderer;
use Filament\Actions\Action;
use Filament\Infolists\Components\TextEntry;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\ViewRecord;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
class ViewAlertDestination extends ViewRecord
{
protected static string $resource = AlertDestinationResource::class;
private ?AlertDestinationLastTestStatus $lastTestStatus = null;
public function mount(int|string $record): void
{
parent::mount($record);
$this->resolveLastTestStatus();
}
protected function getHeaderActions(): array
{
$user = auth()->user();
$record = $this->record;
$canManage = $user instanceof User
&& $record instanceof AlertDestination
&& $user->can('update', $record);
return [
Action::make('send_test_message')
->label('Send test message')
->icon('heroicon-o-paper-airplane')
->requiresConfirmation()
->modalHeading('Send test message')
->modalDescription('A test delivery will be queued for this destination. This verifies the delivery pipeline is working.')
->modalSubmitActionLabel('Send')
->visible(fn (): bool => $record instanceof AlertDestination)
->disabled(fn (): bool => ! $canManage)
->action(function () use ($record): void {
$user = auth()->user();
if (! $user instanceof User || ! $record instanceof AlertDestination) {
return;
}
$service = app(AlertDestinationTestMessageService::class);
$result = $service->sendTest($record, $user);
if ($result['success']) {
Notification::make()
->title($result['message'])
->success()
->send();
} else {
Notification::make()
->title($result['message'])
->warning()
->send();
}
$this->resolveLastTestStatus();
}),
Action::make('view_last_delivery')
->label('View last delivery')
->icon('heroicon-o-arrow-top-right-on-square')
->url(fn (): ?string => $this->buildDeepLinkUrl())
->openUrlInNewTab()
->visible(fn (): bool => $this->lastTestStatus?->deliveryId !== null),
];
}
public function infolist(Schema $schema): Schema
{
$lastTest = $this->lastTestStatus ?? AlertDestinationLastTestStatus::never();
return $schema
->schema([
Section::make('Last test')
->schema([
TextEntry::make('last_test_status')
->label('Status')
->badge()
->state($lastTest->status->value)
->formatStateUsing(BadgeRenderer::label(BadgeDomain::AlertDestinationLastTestStatus))
->color(BadgeRenderer::color(BadgeDomain::AlertDestinationLastTestStatus))
->icon(BadgeRenderer::icon(BadgeDomain::AlertDestinationLastTestStatus)),
TextEntry::make('last_test_timestamp')
->label('Timestamp')
->state($lastTest->timestamp?->toDateTimeString())
->placeholder('—'),
])
->columns(2),
Section::make('Details')
->schema([
TextEntry::make('name'),
TextEntry::make('type')
->badge()
->formatStateUsing(fn (?string $state): string => AlertDestinationResource::typeLabel((string) $state)),
TextEntry::make('is_enabled')
->label('Enabled')
->badge()
->formatStateUsing(fn (bool $state): string => $state ? 'Yes' : 'No')
->color(fn (bool $state): string => $state ? 'success' : 'gray'),
TextEntry::make('created_at')
->dateTime(),
TextEntry::make('updated_at')
->dateTime(),
])
->columns(2),
]);
}
private function resolveLastTestStatus(): void
{
$record = $this->record;
if (! $record instanceof AlertDestination) {
return;
}
$this->lastTestStatus = app(AlertDestinationLastTestResolver::class)->resolve($record);
}
private function buildDeepLinkUrl(): ?string
{
$record = $this->record;
if (! $record instanceof AlertDestination || $this->lastTestStatus?->deliveryId === null) {
return null;
}
return AlertDeliveryResource::getUrl(panel: 'admin').'?'.http_build_query([
'filters' => [
'event_type' => ['value' => 'alerts.test'],
'alert_destination_id' => ['value' => (string) $record->getKey()],
],
]);
}
}