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', AlertRule::class); } public static function canCreate(): bool { $user = auth()->user(); if (! $user instanceof User) { return false; } return $user->can('create', AlertRule::class); } public static function canEdit(Model $record): bool { $user = auth()->user(); if (! $user instanceof User || ! $record instanceof AlertRule) { return false; } return $user->can('update', $record); } public static function canDelete(Model $record): bool { $user = auth()->user(); if (! $user instanceof User || ! $record instanceof AlertRule) { 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 rules 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() ->with('destinations') ->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), Toggle::make('is_enabled') ->label('Enabled') ->default(true), Select::make('event_type') ->required() ->options(self::eventTypeOptions()) ->native(false), Select::make('minimum_severity') ->required() ->options(self::severityOptions()) ->native(false), Select::make('tenant_scope_mode') ->required() ->options([ AlertRule::TENANT_SCOPE_ALL => 'All tenants', AlertRule::TENANT_SCOPE_ALLOWLIST => 'Allowlist', ]) ->default(AlertRule::TENANT_SCOPE_ALL) ->native(false) ->live(), Select::make('tenant_allowlist') ->label('Tenant allowlist') ->multiple() ->options(self::tenantOptions()) ->visible(fn (Get $get): bool => $get('tenant_scope_mode') === AlertRule::TENANT_SCOPE_ALLOWLIST) ->native(false), TextInput::make('cooldown_seconds') ->label('Cooldown (seconds)') ->numeric() ->minValue(0) ->nullable(), Toggle::make('quiet_hours_enabled') ->label('Enable quiet hours') ->default(false) ->live(), TextInput::make('quiet_hours_start') ->label('Quiet hours start') ->type('time') ->visible(fn (Get $get): bool => (bool) $get('quiet_hours_enabled')), TextInput::make('quiet_hours_end') ->label('Quiet hours end') ->type('time') ->visible(fn (Get $get): bool => (bool) $get('quiet_hours_enabled')), Select::make('quiet_hours_timezone') ->label('Quiet hours timezone') ->options(self::timezoneOptions()) ->searchable() ->native(false) ->visible(fn (Get $get): bool => (bool) $get('quiet_hours_enabled')), Select::make('destination_ids') ->label('Destinations') ->multiple() ->required() ->options(self::destinationOptions()) ->native(false), ]); } public static function table(Table $table): Table { return $table ->defaultSort('name') ->recordUrl(fn (AlertRule $record): ?string => static::canEdit($record) ? static::getUrl('edit', ['record' => $record]) : null) ->columns([ TextColumn::make('name') ->searchable(), TextColumn::make('event_type') ->label('Event') ->badge() ->formatStateUsing(fn (?string $state): string => self::eventTypeLabel((string) $state)), TextColumn::make('minimum_severity') ->label('Min severity') ->badge() ->formatStateUsing(fn (?string $state): string => self::severityOptions()[(string) $state] ?? ucfirst((string) $state)), TextColumn::make('destinations_count') ->label('Destinations') ->counts('destinations'), TextColumn::make('is_enabled') ->label('Enabled') ->badge() ->formatStateUsing(fn (bool $state): string => $state ? 'Yes' : 'No') ->color(fn (bool $state): string => $state ? 'success' : 'gray'), ]) ->actions([ EditAction::make() ->label('Edit') ->visible(fn (AlertRule $record): bool => static::canEdit($record)), ActionGroup::make([ Action::make('toggle_enabled') ->label(fn (AlertRule $record): string => $record->is_enabled ? 'Disable' : 'Enable') ->icon(fn (AlertRule $record): string => $record->is_enabled ? 'heroicon-o-pause' : 'heroicon-o-play') ->requiresConfirmation() ->action(function (AlertRule $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::AlertRuleEnabled : AuditActionId::AlertRuleDisabled; self::audit($record, $actionId, [ 'alert_rule_id' => (int) $record->getKey(), 'name' => (string) $record->name, 'event_type' => (string) $record->event_type, 'is_enabled' => $enabled, ]); Notification::make() ->title($enabled ? 'Rule enabled' : 'Rule disabled') ->success() ->send(); }), Action::make('delete') ->label('Delete') ->icon('heroicon-o-trash') ->color('danger') ->requiresConfirmation() ->action(function (AlertRule $record): void { $user = auth()->user(); if (! $user instanceof User || ! $user->can('delete', $record)) { throw new AuthorizationException; } self::audit($record, AuditActionId::AlertRuleDeleted, [ 'alert_rule_id' => (int) $record->getKey(), 'name' => (string) $record->name, 'event_type' => (string) $record->event_type, ]); $record->delete(); Notification::make() ->title('Rule deleted') ->success() ->send(); }), ])->label('More'), ]) ->bulkActions([ BulkActionGroup::make([])->label('More'), ]); } public static function getPages(): array { return [ 'index' => Pages\ListAlertRules::route('/'), 'create' => Pages\CreateAlertRule::route('/create'), 'edit' => Pages\EditAlertRule::route('/{record}/edit'), ]; } /** * @param array $data * @return array */ public static function normalizePayload(array $data): array { $tenantAllowlist = Arr::wrap($data['tenant_allowlist'] ?? []); $tenantAllowlist = array_values(array_unique(array_filter(array_map(static fn (mixed $value): int => (int) $value, $tenantAllowlist)))); if (($data['tenant_scope_mode'] ?? AlertRule::TENANT_SCOPE_ALL) !== AlertRule::TENANT_SCOPE_ALLOWLIST) { $tenantAllowlist = []; } $quietHoursEnabled = (bool) ($data['quiet_hours_enabled'] ?? false); $data['is_enabled'] = (bool) ($data['is_enabled'] ?? true); $data['tenant_allowlist'] = $tenantAllowlist; $data['cooldown_seconds'] = is_numeric($data['cooldown_seconds'] ?? null) ? (int) $data['cooldown_seconds'] : null; $data['quiet_hours_enabled'] = $quietHoursEnabled; if (! $quietHoursEnabled) { $data['quiet_hours_start'] = null; $data['quiet_hours_end'] = null; $data['quiet_hours_timezone'] = null; } return $data; } /** * @param array $destinationIds */ public static function syncDestinations(AlertRule $record, array $destinationIds): void { $allowedDestinationIds = AlertDestination::query() ->where('workspace_id', (int) $record->workspace_id) ->whereIn('id', $destinationIds) ->pluck('id') ->map(static fn (mixed $value): int => (int) $value) ->all(); $record->destinations()->syncWithPivotValues( array_values(array_unique($allowedDestinationIds)), ['workspace_id' => (int) $record->workspace_id], ); } /** * @return array */ public static function eventTypeOptions(): array { return [ AlertRule::EVENT_HIGH_DRIFT => 'High drift', AlertRule::EVENT_COMPARE_FAILED => 'Compare failed', AlertRule::EVENT_SLA_DUE => 'SLA due', ]; } /** * @return array */ public static function severityOptions(): array { return [ 'low' => 'Low', 'medium' => 'Medium', 'high' => 'High', 'critical' => 'Critical', ]; } public static function eventTypeLabel(string $eventType): string { return self::eventTypeOptions()[$eventType] ?? ucfirst(str_replace('_', ' ', $eventType)); } /** * @return array */ private static function destinationOptions(): array { $workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request()); if (! is_int($workspaceId)) { return []; } return AlertDestination::query() ->where('workspace_id', $workspaceId) ->orderBy('name') ->pluck('name', 'id') ->all(); } /** * @return array */ private static function tenantOptions(): array { $workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request()); if (! is_int($workspaceId)) { return []; } return Tenant::query() ->where('workspace_id', $workspaceId) ->where('status', 'active') ->orderBy('name') ->pluck('name', 'id') ->all(); } /** * @return array */ private static function timezoneOptions(): array { $identifiers = \DateTimeZone::listIdentifiers(); sort($identifiers); return array_combine($identifiers, $identifiers); } /** * @param array $metadata */ public static function audit(AlertRule $record, AuditActionId $actionId, array $metadata): void { $workspace = $record->workspace; if ($workspace === null) { return; } $actor = auth()->user(); app(WorkspaceAuditLogger::class)->log( workspace: $workspace, action: $actionId->value, context: [ 'metadata' => $metadata, ], actor: $actor instanceof User ? $actor : null, resourceType: 'alert_rule', resourceId: (string) $record->getKey(), ); } }