|null */ public ?array $statusCounts = null; public static function canAccess(): bool { return FindingResource::canAccess(); } public function mount(): void { $tenant = Tenant::current(); $user = auth()->user(); if (! $user instanceof User) { abort(403, 'Not allowed'); } $latestSuccessful = OperationRun::query() ->where('tenant_id', $tenant->getKey()) ->where('type', 'inventory_sync') ->where('status', OperationRunStatus::Completed->value) ->whereIn('outcome', [ OperationRunOutcome::Succeeded->value, OperationRunOutcome::PartiallySucceeded->value, ]) ->whereNotNull('completed_at') ->orderByDesc('completed_at') ->first(); if (! $latestSuccessful instanceof OperationRun) { $this->state = 'blocked'; $this->message = 'No successful inventory runs found yet.'; return; } $latestContext = is_array($latestSuccessful->context) ? $latestSuccessful->context : []; $scopeKey = (string) ($latestContext['selection_hash'] ?? ''); if ($scopeKey === '') { $this->state = 'blocked'; $this->message = 'No inventory scope key was found on the latest successful inventory run.'; return; } $this->scopeKey = $scopeKey; $selector = app(DriftRunSelector::class); $comparison = $selector->selectBaselineAndCurrent($tenant, $scopeKey); if ($comparison === null) { $this->state = 'blocked'; $this->message = 'Need at least 2 successful runs for this scope to calculate drift.'; return; } $baseline = $comparison['baseline']; $current = $comparison['current']; $this->baselineRunId = (int) $baseline->getKey(); $this->currentRunId = (int) $current->getKey(); $this->baselineFinishedAt = $baseline->completed_at?->toDateTimeString(); $this->currentFinishedAt = $current->completed_at?->toDateTimeString(); $existingOperationRun = OperationRun::query() ->where('tenant_id', $tenant->getKey()) ->where('type', 'drift_generate_findings') ->where('context->scope_key', $scopeKey) ->where('context->baseline_operation_run_id', (int) $baseline->getKey()) ->where('context->current_operation_run_id', (int) $current->getKey()) ->latest('id') ->first(); if ($existingOperationRun instanceof OperationRun) { $this->operationRunId = (int) $existingOperationRun->getKey(); } $exists = Finding::query() ->where('tenant_id', $tenant->getKey()) ->where('finding_type', Finding::FINDING_TYPE_DRIFT) ->where('scope_key', $scopeKey) ->where('baseline_operation_run_id', $baseline->getKey()) ->where('current_operation_run_id', $current->getKey()) ->exists(); if ($exists) { $this->state = 'ready'; $newCount = (int) Finding::query() ->where('tenant_id', $tenant->getKey()) ->where('finding_type', Finding::FINDING_TYPE_DRIFT) ->where('scope_key', $scopeKey) ->where('baseline_operation_run_id', $baseline->getKey()) ->where('current_operation_run_id', $current->getKey()) ->where('status', Finding::STATUS_NEW) ->count(); $this->statusCounts = [Finding::STATUS_NEW => $newCount]; return; } $existingOperationRun?->refresh(); if ($existingOperationRun instanceof OperationRun && in_array($existingOperationRun->status, ['queued', 'running'], true) ) { $this->state = 'generating'; $this->operationRunId = (int) $existingOperationRun->getKey(); return; } if ($existingOperationRun instanceof OperationRun && $existingOperationRun->status === 'completed' ) { $counts = is_array($existingOperationRun->summary_counts ?? null) ? $existingOperationRun->summary_counts : []; $created = (int) ($counts['created'] ?? 0); if ($existingOperationRun->outcome === 'failed') { $this->state = 'error'; $this->message = 'Drift generation failed for this comparison. See the run for details.'; $this->operationRunId = (int) $existingOperationRun->getKey(); return; } if ($created === 0) { $this->state = 'ready'; $this->statusCounts = [Finding::STATUS_NEW => 0]; $this->message = 'No drift findings for this comparison. If you changed settings after the current run, run Inventory Sync again to capture a newer snapshot.'; $this->operationRunId = (int) $existingOperationRun->getKey(); return; } } /** @var CapabilityResolver $resolver */ $resolver = app(CapabilityResolver::class); if (! $resolver->can($user, $tenant, Capabilities::TENANT_SYNC)) { $this->state = 'blocked'; $this->message = 'You can view existing drift findings and run history, but you do not have permission to generate drift.'; return; } /** @var BulkSelectionIdentity $selection */ $selection = app(BulkSelectionIdentity::class); $selectionIdentity = $selection->fromQuery([ 'scope_key' => $scopeKey, 'baseline_operation_run_id' => (int) $baseline->getKey(), 'current_operation_run_id' => (int) $current->getKey(), ]); /** @var OperationRunService $opService */ $opService = app(OperationRunService::class); $opRun = $opService->enqueueBulkOperation( tenant: $tenant, type: 'drift_generate_findings', targetScope: [ 'entra_tenant_id' => (string) ($tenant->tenant_id ?? $tenant->external_id), ], selectionIdentity: $selectionIdentity, dispatcher: function ($operationRun) use ($tenant, $user, $baseline, $current, $scopeKey): void { GenerateDriftFindingsJob::dispatch( tenantId: (int) $tenant->getKey(), userId: (int) $user->getKey(), baselineRunId: (int) $baseline->getKey(), currentRunId: (int) $current->getKey(), scopeKey: $scopeKey, operationRun: $operationRun, ); }, initiator: $user, extraContext: [ 'scope_key' => $scopeKey, 'baseline_operation_run_id' => (int) $baseline->getKey(), 'current_operation_run_id' => (int) $current->getKey(), ], emitQueuedNotification: false, ); $this->operationRunId = (int) $opRun->getKey(); $this->state = 'generating'; if (! $opRun->wasRecentlyCreated) { Notification::make() ->title('Drift generation already active') ->body('This operation is already queued or running.') ->warning() ->actions([ Action::make('view_run') ->label('View run') ->url(OperationRunLinks::view($opRun, $tenant)), ]) ->send(); return; } OpsUxBrowserEvents::dispatchRunEnqueued($this); OperationUxPresenter::queuedToast((string) $opRun->type) ->actions([ Action::make('view_run') ->label('View run') ->url(OperationRunLinks::view($opRun, $tenant)), ]) ->send(); } public function getFindingsUrl(): string { return FindingResource::getUrl('index', tenant: Tenant::current()); } public function getBaselineRunUrl(): ?string { if (! is_int($this->baselineRunId)) { return null; } return route('admin.operations.view', ['run' => $this->baselineRunId]); } public function getCurrentRunUrl(): ?string { if (! is_int($this->currentRunId)) { return null; } return route('admin.operations.view', ['run' => $this->currentRunId]); } public function getOperationRunUrl(): ?string { if (! is_int($this->operationRunId)) { return null; } return OperationRunLinks::view($this->operationRunId, Tenant::current()); } }