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
160 lines
5.1 KiB
PHP
160 lines
5.1 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\Audit\AuditActionId;
|
|
use Filament\Actions\Action;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Resources\Pages\EditRecord;
|
|
|
|
class EditAlertDestination extends EditRecord
|
|
{
|
|
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 getSubheading(): ?string
|
|
{
|
|
if ($this->lastTestStatus === null) {
|
|
return null;
|
|
}
|
|
|
|
$label = ucfirst($this->lastTestStatus->status->value);
|
|
$timestamp = $this->lastTestStatus->timestamp?->diffForHumans();
|
|
|
|
return $timestamp !== null
|
|
? "Last test: {$label} ({$timestamp})"
|
|
: "Last test: {$label}";
|
|
}
|
|
|
|
protected function mutateFormDataBeforeSave(array $data): array
|
|
{
|
|
$record = $this->record;
|
|
$data = AlertDestinationResource::normalizePayload(
|
|
data: $data,
|
|
record: $record instanceof AlertDestination ? $record : null,
|
|
);
|
|
AlertDestinationResource::assertValidConfigPayload($data);
|
|
|
|
return $data;
|
|
}
|
|
|
|
protected function afterSave(): void
|
|
{
|
|
$record = $this->record;
|
|
|
|
if (! $record instanceof AlertDestination) {
|
|
return;
|
|
}
|
|
|
|
AlertDestinationResource::audit($record, AuditActionId::AlertDestinationUpdated, [
|
|
'alert_destination_id' => (int) $record->getKey(),
|
|
'name' => (string) $record->name,
|
|
'type' => (string) $record->type,
|
|
'is_enabled' => (bool) $record->is_enabled,
|
|
]);
|
|
|
|
Notification::make()
|
|
->title('Destination updated')
|
|
->success()
|
|
->send();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
$baseUrl = AlertDeliveryResource::getUrl('index');
|
|
$params = http_build_query([
|
|
'filters' => [
|
|
'event_type' => ['value' => 'alerts.test'],
|
|
'alert_destination_id' => ['value' => (string) $record->getKey()],
|
|
],
|
|
]);
|
|
|
|
return "{$baseUrl}?{$params}";
|
|
}
|
|
}
|