getId() !== 'admin') { return false; } return parent::shouldRegisterNavigation(); } public static function canViewAny(): bool { $user = auth()->user(); if (! $user instanceof User) { return false; } return $user->can('viewAny', AlertDestination::class); } public static function canCreate(): bool { $user = auth()->user(); if (! $user instanceof User) { return false; } return $user->can('create', AlertDestination::class); } public static function canEdit(Model $record): bool { $user = auth()->user(); if (! $user instanceof User || ! $record instanceof AlertDestination) { return false; } return $user->can('update', $record); } public static function canDelete(Model $record): bool { $user = auth()->user(); if (! $user instanceof User || ! $record instanceof AlertDestination) { return false; } return $user->can('delete', $record); } public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration { return ActionSurfaceDeclaration::forResource(ActionSurfaceProfile::CrudListAndEdit) ->satisfy(ActionSurfaceSlot::ListHeader, 'Header actions include capability-gated create.') ->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ClickableRow->value) ->satisfy(ActionSurfaceSlot::ListRowMoreMenu, 'Secondary row actions are grouped under "More".') ->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'No bulk mutations are exposed for alert destinations in v1.') ->satisfy(ActionSurfaceSlot::ListEmptyState, 'List page defines an empty-state create CTA.') ->satisfy(ActionSurfaceSlot::DetailHeader, 'Edit page provides default save/cancel actions.'); } public static function getEloquentQuery(): Builder { $workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request()); return parent::getEloquentQuery() ->when( $workspaceId !== null, fn (Builder $query): Builder => $query->where('workspace_id', (int) $workspaceId), ) ->when( $workspaceId === null, fn (Builder $query): Builder => $query->whereRaw('1 = 0'), ); } public static function form(Schema $schema): Schema { return $schema ->schema([ TextInput::make('name') ->required() ->maxLength(255), Select::make('type') ->required() ->options(self::typeOptions()) ->native(false) ->live(), Toggle::make('is_enabled') ->label('Enabled') ->default(true), TextInput::make('teams_webhook_url') ->label('Teams webhook URL') ->placeholder('https://...') ->url() ->visible(fn (Get $get): bool => $get('type') === AlertDestination::TYPE_TEAMS_WEBHOOK), TagsInput::make('email_recipients') ->label('Email recipients') ->visible(fn (Get $get): bool => $get('type') === AlertDestination::TYPE_EMAIL) ->placeholder('ops@example.com') ->nestedRecursiveRules(['email']), ]); } public static function table(Table $table): Table { return $table ->defaultSort('name') ->recordUrl(fn (AlertDestination $record): ?string => static::canEdit($record) ? static::getUrl('edit', ['record' => $record]) : static::getUrl('view', ['record' => $record])) ->columns([ TextColumn::make('name') ->searchable(), TextColumn::make('type') ->badge() ->formatStateUsing(fn (?string $state): string => self::typeLabel((string) $state)), TextColumn::make('is_enabled') ->label('Enabled') ->badge() ->formatStateUsing(fn (bool $state): string => $state ? 'Yes' : 'No') ->color(fn (bool $state): string => $state ? 'success' : 'gray'), TextColumn::make('updated_at') ->since(), ]) ->actions([ EditAction::make() ->label('Edit') ->visible(fn (AlertDestination $record): bool => static::canEdit($record)), ActionGroup::make([ Action::make('toggle_enabled') ->label(fn (AlertDestination $record): string => $record->is_enabled ? 'Disable' : 'Enable') ->icon(fn (AlertDestination $record): string => $record->is_enabled ? 'heroicon-o-pause' : 'heroicon-o-play') ->action(function (AlertDestination $record): void { $user = auth()->user(); if (! $user instanceof User || ! $user->can('update', $record)) { throw new AuthorizationException; } $enabled = ! (bool) $record->is_enabled; $record->forceFill([ 'is_enabled' => $enabled, ])->save(); $actionId = $enabled ? AuditActionId::AlertDestinationEnabled : AuditActionId::AlertDestinationDisabled; self::audit($record, $actionId, [ 'alert_destination_id' => (int) $record->getKey(), 'name' => (string) $record->name, 'type' => (string) $record->type, 'is_enabled' => $enabled, ]); Notification::make() ->title($enabled ? 'Destination enabled' : 'Destination disabled') ->success() ->send(); }), Action::make('delete') ->label('Delete') ->icon('heroicon-o-trash') ->color('danger') ->requiresConfirmation() ->action(function (AlertDestination $record): void { $user = auth()->user(); if (! $user instanceof User || ! $user->can('delete', $record)) { throw new AuthorizationException; } self::audit($record, AuditActionId::AlertDestinationDeleted, [ 'alert_destination_id' => (int) $record->getKey(), 'name' => (string) $record->name, 'type' => (string) $record->type, ]); $record->delete(); Notification::make() ->title('Destination deleted') ->success() ->send(); }), ])->label('More'), ]) ->bulkActions([ BulkActionGroup::make([])->label('More'), ]) ->emptyStateActions([ \Filament\Actions\CreateAction::make() ->label('Create target') ->disabled(fn (): bool => ! static::canCreate()), ]); } public static function getPages(): array { return [ 'index' => Pages\ListAlertDestinations::route('/'), 'create' => Pages\CreateAlertDestination::route('/create'), 'view' => Pages\ViewAlertDestination::route('/{record}'), 'edit' => Pages\EditAlertDestination::route('/{record}/edit'), ]; } /** * @param array $data */ public static function normalizePayload(array $data, ?AlertDestination $record = null): array { $type = trim((string) ($data['type'] ?? $record?->type ?? '')); $existingConfig = is_array($record?->config ?? null) ? $record->config : []; if ($type === AlertDestination::TYPE_TEAMS_WEBHOOK) { $webhookUrl = trim((string) ($data['teams_webhook_url'] ?? '')); if ($webhookUrl === '' && $record instanceof AlertDestination) { $webhookUrl = trim((string) Arr::get($existingConfig, 'webhook_url', '')); } $data['config'] = [ 'webhook_url' => $webhookUrl, ]; } if ($type === AlertDestination::TYPE_EMAIL) { $recipients = Arr::wrap($data['email_recipients'] ?? []); $recipients = array_values(array_filter(array_map(static fn (mixed $value): string => trim((string) $value), $recipients))); if ($recipients === [] && $record instanceof AlertDestination) { $existingRecipients = Arr::get($existingConfig, 'recipients', []); $recipients = is_array($existingRecipients) ? array_values(array_filter(array_map(static fn (mixed $value): string => trim((string) $value), $existingRecipients))) : []; } $data['config'] = [ 'recipients' => array_values(array_unique($recipients)), ]; } unset($data['teams_webhook_url'], $data['email_recipients']); return $data; } /** * @return array */ public static function typeOptions(): array { return [ AlertDestination::TYPE_TEAMS_WEBHOOK => 'Microsoft Teams webhook', AlertDestination::TYPE_EMAIL => 'Email', ]; } public static function typeLabel(string $type): string { return self::typeOptions()[$type] ?? ucfirst($type); } /** * @param array $data */ public static function assertValidConfigPayload(array $data): void { $type = (string) ($data['type'] ?? ''); $config = is_array($data['config'] ?? null) ? $data['config'] : []; if ($type === AlertDestination::TYPE_TEAMS_WEBHOOK) { $webhook = trim((string) Arr::get($config, 'webhook_url', '')); if ($webhook === '') { throw ValidationException::withMessages([ 'teams_webhook_url' => ['The Teams webhook URL is required.'], ]); } } if ($type === AlertDestination::TYPE_EMAIL) { $recipients = Arr::get($config, 'recipients', []); $recipients = is_array($recipients) ? array_values(array_filter(array_map(static fn (mixed $value): string => trim((string) $value), $recipients))) : []; if ($recipients === []) { throw ValidationException::withMessages([ 'email_recipients' => ['At least one recipient is required for email destinations.'], ]); } } } /** * @param array $metadata */ public static function audit(AlertDestination $record, AuditActionId $actionId, array $metadata): void { $workspace = $record->workspace; if ($workspace === null) { return; } app(WorkspaceAuditLogger::class)->log( workspace: $workspace, action: $actionId->value, context: [ 'metadata' => $metadata, ], actor: auth()->user() instanceof User ? auth()->user() : null, resourceType: 'alert_destination', resourceId: (string) $record->getKey(), ); } }