*/ public array $supportDiagnosticsAuditKeys = []; /** * @param array $parameters */ public static function getUrl(array $parameters = [], bool $isAbsolute = true, ?string $panel = null, ?Model $tenant = null, bool $shouldGuessMissingParameters = false): string { return parent::getUrl($parameters, $isAbsolute, $panel ?? 'tenant', $tenant, $shouldGuessMissingParameters); } /** * @return array | WidgetConfiguration> */ public function getWidgets(): array { return [ TenantTriageArrivalContinuity::class, RecoveryReadiness::class, DashboardKpis::class, NeedsAttention::class, BaselineCompareNow::class, RecentDriftFindings::class, RecentOperations::class, ]; } public function getColumns(): int|array { return 2; } /** * @return array */ protected function getHeaderActions(): array { return [ $this->requestSupportAction(), $this->openSupportDiagnosticsAction(), ]; } public function authorizeTenantSupportRequest(): void { $this->resolveCurrentTenantForCapability(Capabilities::SUPPORT_REQUESTS_CREATE); } private function requestSupportAction(): Action { $action = Action::make('requestSupport') ->label('Request support') ->icon('heroicon-o-paper-airplane') ->color('gray') ->slideOver() ->stickyModalHeader() ->modalHeading('Request support') ->modalDescription('Share a concise summary and TenantAtlas will attach redacted context from existing records.') ->modalSubmitActionLabel('Submit request') ->form([ Placeholder::make('included_context') ->label('Included context') ->content(fn (): string => $this->tenantSupportRequestAttachmentSummary()) ->columnSpanFull(), Select::make('severity') ->label('Severity') ->options(SupportRequest::severityOptions()) ->default(SupportRequest::SEVERITY_NORMAL) ->required() ->native(false), TextInput::make('summary') ->label('Summary') ->required() ->columnSpanFull(), Textarea::make('reproduction_notes') ->label('Reproduction notes') ->rows(4) ->columnSpanFull(), TextInput::make('contact_name') ->label('Contact name') ->default(fn (): ?string => $this->resolveDashboardActor()->name), TextInput::make('contact_email') ->label('Contact email') ->email() ->default(fn (): ?string => $this->resolveDashboardActor()->email), ]) ->action(function (array $data): void { $actor = $this->resolveDashboardActor(); $tenant = $this->resolveCurrentTenantForCapability(Capabilities::SUPPORT_REQUESTS_CREATE); $supportRequest = app(SupportRequestSubmissionService::class)->submitForTenant($tenant, $actor, $data); Notification::make() ->title('Support request submitted') ->body('Reference '.$supportRequest->internal_reference) ->success() ->send(); }); return UiEnforcement::forAction($action) ->requireCapability(Capabilities::SUPPORT_REQUESTS_CREATE) ->apply(); } private function openSupportDiagnosticsAction(): Action { $action = Action::make('openSupportDiagnostics') ->label('Open support diagnostics') ->icon('heroicon-o-lifebuoy') ->color('gray') ->modal() ->slideOver() ->stickyModalHeader() ->modalHeading('Support diagnostics') ->modalDescription('Redacted tenant context from existing records.') ->modalSubmitAction(false) ->modalCancelAction(fn (Action $action): Action => $action->label('Close')) ->mountUsing(function (): void { $this->auditTenantSupportDiagnosticsOpen(); }) ->modalContent(fn (): View => view('filament.modals.support-diagnostic-bundle', [ 'bundle' => $this->tenantSupportDiagnosticBundle(), ])); return UiEnforcement::forAction($action) ->requireCapability(Capabilities::SUPPORT_DIAGNOSTICS_VIEW) ->apply(); } /** * @return array */ public function tenantSupportDiagnosticBundle(): array { $user = $this->resolveDashboardActor(); $tenant = $this->resolveCurrentTenantForCapability(Capabilities::SUPPORT_DIAGNOSTICS_VIEW); return app(SupportDiagnosticBundleBuilder::class)->forTenant($tenant, $user); } private function auditTenantSupportDiagnosticsOpen(): void { $user = $this->resolveDashboardActor(); $tenant = $this->resolveCurrentTenantForCapability(Capabilities::SUPPORT_DIAGNOSTICS_VIEW); $this->recordSupportDiagnosticsOpened( tenant: $tenant, bundle: $this->tenantSupportDiagnosticBundle(), user: $user, ); } /** * @param array $bundle */ private function recordSupportDiagnosticsOpened(Tenant $tenant, array $bundle, User $user): void { $auditKey = 'tenant:'.$tenant->getKey(); if (in_array($auditKey, $this->supportDiagnosticsAuditKeys, true)) { return; } app(WorkspaceAuditLogger::class)->logSupportDiagnosticsOpened( tenant: $tenant, contextType: 'tenant', bundle: $bundle, actor: $user, ); app(ProductTelemetryRecorder::class)->record( eventName: ProductUsageEventCatalog::SUPPORT_DIAGNOSTICS_OPENED, workspaceId: (int) $tenant->workspace_id, tenantId: (int) $tenant->getKey(), userId: (int) $user->getKey(), subjectType: 'tenant', subjectId: (int) $tenant->getKey(), metadata: [ 'source_surface' => 'tenant_dashboard', ], ); $this->supportDiagnosticsAuditKeys[] = $auditKey; } private function resolveDashboardActor(): User { $user = auth()->user(); if (! $user instanceof User) { abort(404); } return $user; } private function resolveCurrentTenantForCapability(string $capability): Tenant { $user = $this->resolveDashboardActor(); $tenant = Filament::getTenant(); if (! $tenant instanceof Tenant) { abort(404); } $resolver = app(CapabilityResolver::class); if (! $resolver->isMember($user, $tenant)) { abort(404); } if (! $resolver->can($user, $tenant, $capability)) { abort(403); } return $tenant; } private function tenantSupportRequestAttachmentSummary(): string { $tenant = Filament::getTenant(); $user = auth()->user(); if (! $tenant instanceof Tenant || ! $user instanceof User) { return 'Only canonical redacted tenant context will be attached.'; } $resolver = app(CapabilityResolver::class); if (! $resolver->isMember($user, $tenant)) { return 'Only canonical redacted tenant context will be attached.'; } return $resolver->can($user, $tenant, Capabilities::SUPPORT_DIAGNOSTICS_VIEW) ? 'A redacted diagnostic snapshot and the canonical tenant context will be attached.' : 'Only the canonical redacted tenant context will be attached because you cannot view support diagnostics.'; } }