|null */ private ?array $authorizedTenants = null; public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration { return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::RunLog) ->withDefaults(new ActionSurfaceDefaults( moreGroupLabel: 'More', exportIsDefaultBulkActionForReadOnly: false, )) ->satisfy(ActionSurfaceSlot::ListHeader, 'Header actions keep the Monitoring scope visible and expose selected-event detail actions.') ->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ViewAction->value) ->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'Audit history is immutable and intentionally omits bulk actions.') ->satisfy(ActionSurfaceSlot::ListEmptyState, 'The table exposes a clear-filters CTA when no audit events match the current view.') ->satisfy(ActionSurfaceSlot::DetailHeader, 'Selected-event detail keeps close-inspection and related-navigation actions at the page header.'); } public function mount(): void { $this->authorizePageAccess(); $this->selectedAuditLogId = is_numeric(request()->query('event')) ? (int) request()->query('event') : null; app(CanonicalAdminTenantFilterState::class)->sync($this->getTableFiltersSessionKey(), request: request()); $this->mountInteractsWithTable(); if ($this->selectedAuditLogId !== null) { $this->selectedAuditLog(); } } /** * @return array */ protected function getHeaderActions(): array { $actions = app(OperateHubShell::class)->headerActions( scopeActionName: 'operate_hub_scope_audit_log', returnActionName: 'operate_hub_return_audit_log', ); if ($this->selectedAuditLog() instanceof AuditLogModel) { $actions[] = Action::make('clear_selected_audit_event') ->label('Close details') ->color('gray') ->action(function (): void { $this->clearSelectedAuditLog(); }); $relatedLink = $this->selectedAuditLink(); if (is_array($relatedLink)) { $actions[] = Action::make('open_selected_audit_target') ->label($relatedLink['label']) ->icon('heroicon-o-arrow-top-right-on-square') ->color('gray') ->url($relatedLink['url']); } } return $actions; } public function table(Table $table): Table { return $table ->query(fn (): Builder => $this->auditBaseQuery()) ->defaultSort('recorded_at', 'desc') ->paginated(TablePaginationProfiles::customPage()) ->persistFiltersInSession() ->persistSearchInSession() ->persistSortInSession() ->columns([ TextColumn::make('outcome') ->label('Outcome') ->badge() ->getStateUsing(fn (AuditLogModel $record): string => $record->normalizedOutcome()->value) ->formatStateUsing(fn (string $state): string => BadgeRenderer::label(BadgeDomain::AuditOutcome)($state)) ->color(fn (string $state): string => BadgeRenderer::color(BadgeDomain::AuditOutcome)($state)) ->icon(fn (string $state): ?string => BadgeRenderer::icon(BadgeDomain::AuditOutcome)($state)) ->iconColor(fn (string $state): ?string => BadgeRenderer::iconColor(BadgeDomain::AuditOutcome)($state)), TextColumn::make('summary') ->label('Event') ->getStateUsing(fn (AuditLogModel $record): string => $record->summaryText()) ->description(fn (AuditLogModel $record): string => AuditActionId::labelFor((string) $record->action)) ->searchable() ->wrap(), TextColumn::make('actor_label') ->label('Actor') ->getStateUsing(fn (AuditLogModel $record): string => $record->actorDisplayLabel()) ->description(fn (AuditLogModel $record): string => BadgeRenderer::label(BadgeDomain::AuditActorType)($record->actorSnapshot()->type->value)) ->searchable(), TextColumn::make('target_label') ->label('Target') ->getStateUsing(fn (AuditLogModel $record): string => $record->targetDisplayLabel() ?? 'No target snapshot') ->searchable() ->toggleable(), TextColumn::make('tenant.name') ->label('Tenant') ->formatStateUsing(fn (?string $state): string => $state ?: 'Workspace') ->toggleable(), TextColumn::make('recorded_at') ->label('Recorded') ->since() ->sortable(), ]) ->filters([ SelectFilter::make('tenant_id') ->label('Tenant') ->options(fn (): array => $this->tenantFilterOptions()) ->default(fn (): ?string => $this->defaultTenantFilter()) ->searchable(), SelectFilter::make('action') ->label('Event type') ->options(fn (): array => $this->actionFilterOptions()) ->searchable(), SelectFilter::make('outcome') ->label('Outcome') ->options(FilterOptionCatalog::auditOutcomes()), SelectFilter::make('actor_label') ->label('Actor') ->options(fn (): array => $this->actorFilterOptions()) ->searchable(), SelectFilter::make('resource_type') ->label('Target type') ->options(fn (): array => $this->targetTypeFilterOptions()), FilterPresets::dateRange('recorded_at', 'Recorded', 'recorded_at'), ]) ->actions([ Action::make('inspect') ->label('Inspect event') ->icon('heroicon-o-eye') ->color('gray') ->action(function (AuditLogModel $record): void { $this->selectedAuditLogId = (int) $record->getKey(); }), ]) ->bulkActions([]) ->emptyStateHeading('No audit events match this view') ->emptyStateDescription('Clear the current search or filters to return to the workspace-wide audit history.') ->emptyStateIcon('heroicon-o-funnel') ->emptyStateActions([ Action::make('clear_filters') ->label('Clear filters') ->icon('heroicon-o-x-mark') ->color('gray') ->action(function (): void { $this->selectedAuditLogId = null; $this->resetTable(); }), ]); } public function clearSelectedAuditLog(): void { $this->selectedAuditLogId = null; } public function selectedAuditLog(): ?AuditLogModel { if (! is_numeric($this->selectedAuditLogId)) { return null; } $record = $this->auditBaseQuery() ->whereKey((int) $this->selectedAuditLogId) ->first(); if (! $record instanceof AuditLogModel) { throw new NotFoundHttpException; } return $record; } /** * @return array{label: string, url: string}|null */ public function selectedAuditLink(): ?array { $record = $this->selectedAuditLog(); if (! $record instanceof AuditLogModel) { return null; } return app(RelatedNavigationResolver::class)->auditTargetLink($record); } /** * @return array */ public function authorizedTenants(): array { if ($this->authorizedTenants !== null) { return $this->authorizedTenants; } $user = auth()->user(); $workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request()); if (! $user instanceof User || ! is_numeric($workspaceId)) { return $this->authorizedTenants = []; } $tenants = collect($user->getTenants(Filament::getCurrentOrDefaultPanel())) ->filter(static fn (Tenant $tenant): bool => (int) $tenant->workspace_id === (int) $workspaceId) ->filter(static fn (Tenant $tenant): bool => $tenant->isActive()) ->keyBy(fn (Tenant $tenant): int => (int) $tenant->getKey()) ->all(); return $this->authorizedTenants = $tenants; } private function authorizePageAccess(): void { $user = auth()->user(); if (! $user instanceof User) { abort(403); } $workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request()); $workspace = is_numeric($workspaceId) ? Workspace::query()->whereKey((int) $workspaceId)->first() : null; if (! $workspace instanceof Workspace) { throw new NotFoundHttpException; } $resolver = app(WorkspaceCapabilityResolver::class); if (! $resolver->isMember($user, $workspace)) { throw new NotFoundHttpException; } if (! $resolver->can($user, $workspace, Capabilities::AUDIT_VIEW)) { abort(403); } } private function auditBaseQuery(): Builder { $workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request()); $authorizedTenantIds = array_map( static fn (Tenant $tenant): int => (int) $tenant->getKey(), $this->authorizedTenants(), ); return AuditLogModel::query() ->with(['tenant', 'workspace', 'operationRun']) ->forWorkspace((int) $workspaceId) ->where(function (Builder $query) use ($authorizedTenantIds): void { $query->whereNull('tenant_id'); if ($authorizedTenantIds !== []) { $query->orWhereIn('tenant_id', $authorizedTenantIds); } }) ->latestFirst(); } /** * @return array */ private function tenantFilterOptions(): array { return collect($this->authorizedTenants()) ->mapWithKeys(static fn (Tenant $tenant): array => [ (string) $tenant->getKey() => $tenant->getFilamentName(), ]) ->all(); } private function defaultTenantFilter(): ?string { $activeTenant = app(OperateHubShell::class)->activeEntitledTenant(request()); if (! $activeTenant instanceof Tenant) { return null; } return array_key_exists((int) $activeTenant->getKey(), $this->authorizedTenants()) ? (string) $activeTenant->getKey() : null; } /** * @return array */ private function actionFilterOptions(): array { $values = (clone $this->auditBaseQuery()) ->reorder() ->select('action') ->distinct() ->orderBy('action') ->pluck('action') ->all(); return FilterOptionCatalog::auditActions($values); } /** * @return array */ private function actorFilterOptions(): array { return (clone $this->auditBaseQuery()) ->reorder() ->whereNotNull('actor_label') ->select('actor_label') ->distinct() ->orderBy('actor_label') ->pluck('actor_label', 'actor_label') ->all(); } /** * @return array */ private function targetTypeFilterOptions(): array { $values = (clone $this->auditBaseQuery()) ->reorder() ->whereNotNull('resource_type') ->select('resource_type') ->distinct() ->orderBy('resource_type') ->pluck('resource_type') ->all(); return FilterOptionCatalog::auditTargetTypes($values); } }