|null */ public ?array $navigationContextPayload = null; public bool $opsUxIsTabHidden = false; /** * @return array */ protected function getHeaderActions(): array { $operateHubShell = app(OperateHubShell::class); $navigationContext = $this->navigationContext(); $activeTenant = $operateHubShell->activeEntitledTenant(request()); $runTenantId = isset($this->run) ? (int) ($this->run->tenant_id ?? 0) : 0; $actions = [ Action::make('operate_hub_scope_run_detail') ->label($operateHubShell->scopeLabel(request())) ->color('gray') ->disabled(), ]; if ($navigationContext?->backLinkLabel !== null && $navigationContext->backLinkUrl !== null) { $actions[] = Action::make('operate_hub_back_to_origin_run_detail') ->label($navigationContext->backLinkLabel) ->color('gray') ->url($navigationContext->backLinkUrl); } elseif ($activeTenant instanceof Tenant && (int) $activeTenant->getKey() === $runTenantId) { $actions[] = Action::make('operate_hub_back_to_tenant_run_detail') ->label('← Back to '.$activeTenant->name) ->color('gray') ->url(\App\Filament\Pages\TenantDashboard::getUrl(panel: 'tenant', tenant: $activeTenant)); } else { $actions[] = Action::make('operate_hub_back_to_operations') ->label('Back to Operations') ->color('gray') ->url(fn (): string => route('admin.operations.index')); } if ($activeTenant instanceof Tenant) { $actions[] = Action::make('operate_hub_show_all_operations') ->label('Show all operations') ->color('gray') ->url(fn (): string => route('admin.operations.index')); } $actions[] = Action::make('refresh') ->label('Refresh') ->icon('heroicon-o-arrow-path') ->color('gray') ->url(fn (): string => isset($this->run) ? OperationRunLinks::tenantlessView($this->run, $navigationContext) : route('admin.operations.index')); if (! isset($this->run)) { return $actions; } $user = auth()->user(); $tenant = $this->run->tenant; if ($tenant instanceof Tenant && (! $user instanceof User || ! app(CapabilityResolver::class)->isMember($user, $tenant))) { $tenant = null; } $related = OperationRunLinks::related($this->run, $tenant); $relatedActions = []; foreach ($related as $label => $url) { $relatedActions[] = Action::make(Str::slug((string) $label, '_')) ->label((string) $label) ->url((string) $url) ->openUrlInNewTab(); } if ($relatedActions !== []) { $actions[] = ActionGroup::make($relatedActions) ->label('Open') ->icon('heroicon-o-arrow-top-right-on-square') ->color('gray'); } $actions[] = $this->resumeCaptureAction(); return $actions; } public function mount(OperationRun $run): void { $user = auth()->user(); if (! $user instanceof User) { abort(403); } $this->authorize('view', $run); $this->run = $run->loadMissing(['workspace', 'tenant', 'user']); $this->navigationContextPayload = is_array(request()->query('nav')) ? request()->query('nav') : null; } public function infolist(Schema $schema): Schema { return OperationRunResource::infolist($schema); } public function defaultInfolist(Schema $schema): Schema { return $schema ->record($this->run) ->columns(2); } public function redactionIntegrityNote(): ?string { return isset($this->run) ? RedactionIntegrity::noteForRun($this->run) : null; } /** * @return array{tone: string, title: string, body: string}|null */ public function canonicalContextBanner(): ?array { if (! isset($this->run)) { return null; } $activeTenant = app(OperateHubShell::class)->activeEntitledTenant(request()); $runTenant = $this->run->tenant; if (! $runTenant instanceof Tenant) { return [ 'tone' => 'slate', 'title' => 'Workspace-level run', 'body' => $activeTenant instanceof Tenant ? 'This canonical workspace view is not tied to the current tenant context ('.$activeTenant->name.').' : 'This canonical workspace view is not tied to any tenant.', ]; } $messages = ['Run tenant: '.$runTenant->name.'.']; $tone = 'sky'; $title = null; if ($activeTenant instanceof Tenant && ! $activeTenant->is($runTenant)) { $title = 'Current tenant context differs from this run'; array_unshift($messages, 'Current tenant context: '.$activeTenant->name.'.'); $messages[] = 'This canonical workspace view remains valid without switching tenant context.'; } $referencedTenant = ReferencedTenantLifecyclePresentation::forOperationRun($runTenant); if ($selectorAvailabilityMessage = $referencedTenant->selectorAvailabilityMessage()) { $title ??= 'Run tenant is not available in the current tenant selector'; $tone = 'amber'; $messages[] = $selectorAvailabilityMessage; if ($referencedTenant->contextNote !== null) { $messages[] = $referencedTenant->contextNote; } } elseif (! $activeTenant instanceof Tenant) { $title ??= 'Canonical workspace view'; $messages[] = 'No tenant context is currently selected.'; } if ($title === null) { return null; } return [ 'tone' => $tone, 'title' => $title, 'body' => implode(' ', $messages), ]; } public function pollInterval(): ?string { if (! isset($this->run)) { return null; } if ($this->opsUxIsTabHidden === true) { return null; } if (filled($this->mountedActions ?? null)) { return null; } return RunDetailPolling::interval($this->run); } public function content(Schema $schema): Schema { return $schema->schema([ EmbeddedSchema::make('infolist'), ]); } private function resumeCaptureAction(): Action { return Action::make('resumeCapture') ->label('Resume capture') ->icon('heroicon-o-forward') ->requiresConfirmation() ->modalHeading('Resume capture') ->modalDescription('This will start a follow-up operation to capture remaining baseline evidence for this scope.') ->visible(fn (): bool => $this->canResumeCapture()) ->action(function (): void { $user = auth()->user(); if (! $user instanceof User) { abort(403); } if (! isset($this->run)) { Notification::make() ->title('Run not loaded') ->danger() ->send(); return; } $service = app(BaselineEvidenceCaptureResumeService::class); $result = $service->resume($this->run, $user); if (! ($result['ok'] ?? false)) { $reason = is_string($result['reason_code'] ?? null) ? (string) $result['reason_code'] : 'unknown'; Notification::make() ->title('Cannot resume capture') ->body('Reason: '.str_replace('.', ' ', $reason)) ->danger() ->send(); return; } $run = $result['run'] ?? null; if (! $run instanceof OperationRun) { Notification::make() ->title('Cannot resume capture') ->body('Reason: missing operation run') ->danger() ->send(); return; } $viewAction = Action::make('view_run') ->label('View run') ->url(OperationRunLinks::tenantlessView($run)); if (! $run->wasRecentlyCreated && in_array((string) $run->status, ['queued', 'running'], true)) { OpsUxBrowserEvents::dispatchRunEnqueued($this); OperationUxPresenter::alreadyQueuedToast((string) $run->type) ->actions([$viewAction]) ->send(); return; } OpsUxBrowserEvents::dispatchRunEnqueued($this); OperationUxPresenter::queuedToast((string) $run->type) ->actions([$viewAction]) ->send(); }); } private function navigationContext(): ?CanonicalNavigationContext { if (! is_array($this->navigationContextPayload)) { return CanonicalNavigationContext::fromRequest(request()); } $request = request()->duplicate(query: ['nav' => $this->navigationContextPayload]); return CanonicalNavigationContext::fromRequest($request); } private function canResumeCapture(): bool { if (! isset($this->run)) { return false; } if ((string) $this->run->status !== 'completed') { return false; } if (! in_array((string) $this->run->type, ['baseline_capture', 'baseline_compare'], true)) { return false; } $context = is_array($this->run->context) ? $this->run->context : []; $tokenKey = (string) $this->run->type === 'baseline_capture' ? 'baseline_capture.resume_token' : 'baseline_compare.resume_token'; $token = data_get($context, $tokenKey); if (! is_string($token) || trim($token) === '') { return false; } $user = auth()->user(); if (! $user instanceof User) { return false; } $workspace = $this->run->workspace; if (! $workspace instanceof \App\Models\Workspace) { return false; } $resolver = app(WorkspaceCapabilityResolver::class); return $resolver->isMember($user, $workspace) && $resolver->can($user, $workspace, Capabilities::WORKSPACE_BASELINES_MANAGE); } }