*/ public function build(Workspace $workspace, User $user): array { $workspaceId = (int) $workspace->getKey(); $accessibleTenants = $this->accessibleTenants($workspace, $user); $accessibleTenantIds = $accessibleTenants ->pluck('id') ->map(static fn (mixed $id): int => (int) $id) ->all(); $this->capabilityResolver->primeMemberships($user, $accessibleTenantIds); $canViewAlerts = $this->workspaceCapabilityResolver->can($user, $workspace, Capabilities::ALERTS_VIEW); $navigationContext = $this->workspaceOverviewNavigationContext(); $tenantContexts = $this->tenantContexts($accessibleTenants, $workspaceId, $canViewAlerts); $attentionItems = $this->attentionItems($tenantContexts, $user, $canViewAlerts, $navigationContext); $governanceAttentionTenantCount = count(array_filter( $tenantContexts, static fn (array $context): bool => (bool) ($context['has_governance_attention'] ?? false), )); $totalActiveOperationsCount = (int) $this->scopeToAuthorizedTenants( OperationRun::query(), $workspaceId, $accessibleTenantIds, ) ->healthyActive() ->count(); $totalAlertFailuresCount = $canViewAlerts ? (int) $this->scopeToAuthorizedTenants( AlertDelivery::query(), $workspaceId, $accessibleTenantIds, ) ->where('created_at', '>=', now()->subDays(7)) ->where('status', AlertDelivery::STATUS_FAILED) ->count() : 0; $calmness = $this->calmnessState( accessibleTenantCount: $accessibleTenants->count(), attentionItems: $attentionItems, governanceAttentionTenantCount: $governanceAttentionTenantCount, totalActiveOperationsCount: $totalActiveOperationsCount, totalAlertFailuresCount: $totalAlertFailuresCount, canViewAlerts: $canViewAlerts, navigationContext: $navigationContext, ); $attentionEmptyState = [ 'title' => $calmness['title'], 'body' => $calmness['body'], 'action_label' => $calmness['next_action']['label'] ?? 'Choose tenant', 'action_url' => $calmness['next_action']['url'] ?? ChooseTenant::getUrl(panel: 'admin'), ]; $zeroTenantState = null; if ($accessibleTenants->isEmpty()) { $zeroTenantState = $attentionEmptyState; } $summaryMetrics = $this->summaryMetrics( accessibleTenantCount: $accessibleTenants->count(), governanceAttentionTenantCount: $governanceAttentionTenantCount, totalActiveOperationsCount: $totalActiveOperationsCount, totalAlertFailuresCount: $totalAlertFailuresCount, canViewAlerts: $canViewAlerts, navigationContext: $navigationContext, ); $recentOperations = $this->recentOperations($workspaceId, $accessibleTenantIds, $navigationContext); $quickActions = $this->quickActions( workspace: $workspace, accessibleTenantCount: $accessibleTenants->count(), canViewAlerts: $canViewAlerts, user: $user, navigationContext: $navigationContext, ); return [ 'workspace' => [ 'id' => $workspaceId, 'name' => (string) $workspace->name, 'slug' => filled($workspace->slug) ? (string) $workspace->slug : null, ], 'workspace_id' => $workspaceId, 'workspace_name' => (string) $workspace->name, 'accessible_tenant_count' => $accessibleTenants->count(), 'summary_metrics' => $summaryMetrics, 'attention_items' => $attentionItems, 'attention_empty_state' => $attentionEmptyState, 'recent_operations' => $recentOperations, 'recent_operations_empty_state' => [ 'title' => 'No recent operations yet', 'body' => 'Diagnostic execution context will appear here once runs start. This section does not define workspace health on its own.', 'action_label' => 'Open operations', 'action_url' => OperationRunLinks::index(context: $navigationContext, allTenants: true), ], 'quick_actions' => $quickActions, 'zero_tenant_state' => $zeroTenantState, 'calmness' => $calmness, ]; } /** * @return Collection */ private function accessibleTenants(Workspace $workspace, User $user): Collection { return Tenant::query() ->where('workspace_id', (int) $workspace->getKey()) ->where('status', 'active') ->whereIn('id', $user->tenantMemberships()->select('tenant_id')) ->orderBy('name') ->get(['id', 'name', 'external_id', 'workspace_id']); } /** * @param Collection $accessibleTenants * @return list> */ private function tenantContexts(Collection $accessibleTenants, int $workspaceId, bool $canViewAlerts): array { $accessibleTenantIds = $accessibleTenants ->pluck('id') ->map(static fn (mixed $id): int => (int) $id) ->all(); if ($accessibleTenantIds === []) { return []; } $followUpRuns = $this->scopeToVisibleTenants( OperationRun::query()->with('tenant'), $workspaceId, $accessibleTenantIds, ) ->dashboardNeedsFollowUp() ->latest('created_at') ->get() ->groupBy(static fn (OperationRun $run): int => (int) $run->tenant_id); $followUpCounts = $followUpRuns ->map(static fn (Collection $runs): int => $runs->count()) ->all(); $activeOperationCounts = $this->scopeToVisibleTenants( OperationRun::query(), $workspaceId, $accessibleTenantIds, ) ->healthyActive() ->selectRaw('tenant_id, count(*) as aggregate_count') ->groupBy('tenant_id') ->pluck('aggregate_count', 'tenant_id') ->map(static fn (mixed $count): int => (int) $count) ->all(); $alertFailureCounts = $canViewAlerts ? $this->scopeToVisibleTenants( AlertDelivery::query(), $workspaceId, $accessibleTenantIds, ) ->where('created_at', '>=', now()->subDays(7)) ->where('status', AlertDelivery::STATUS_FAILED) ->selectRaw('tenant_id, count(*) as aggregate_count') ->groupBy('tenant_id') ->pluck('aggregate_count', 'tenant_id') ->map(static fn (mixed $count): int => (int) $count) ->all() : []; return $accessibleTenants ->map(function (Tenant $tenant) use ($followUpCounts, $followUpRuns, $activeOperationCounts, $alertFailureCounts): array { $tenantId = (int) $tenant->getKey(); $aggregate = $this->governanceAggregate($tenant); return [ 'tenant' => $tenant, 'aggregate' => $aggregate, 'has_governance_attention' => $this->hasGovernanceAttention($aggregate), 'follow_up_operations_count' => (int) ($followUpCounts[$tenantId] ?? 0), 'latest_follow_up_run' => $followUpRuns->get($tenantId)?->first(), 'active_operations_count' => (int) ($activeOperationCounts[$tenantId] ?? 0), 'alert_failures_count' => (int) ($alertFailureCounts[$tenantId] ?? 0), ]; }) ->all(); } private function governanceAggregate(Tenant $tenant): TenantGovernanceAggregate { /** @var TenantGovernanceAggregate $aggregate */ $aggregate = $this->tenantGovernanceAggregateResolver->forTenant($tenant); return $aggregate; } private function hasGovernanceAttention(TenantGovernanceAggregate $aggregate): bool { if ( $aggregate->lapsedGovernanceCount > 0 || $aggregate->overdueOpenFindingsCount > 0 || $aggregate->expiringGovernanceCount > 0 || $aggregate->highSeverityActiveFindingsCount > 0 ) { return true; } return $this->shouldPromoteCompareAttention($aggregate); } private function shouldPromoteCompareAttention(TenantGovernanceAggregate $aggregate): bool { if ($aggregate->summaryAssessment->stateFamily === BaselineCompareSummaryAssessment::STATE_STALE) { return true; } return $aggregate->summaryAssessment->stateFamily === BaselineCompareSummaryAssessment::STATE_ACTION_REQUIRED && in_array($aggregate->nextActionTarget, [ BaselineCompareSummaryAssessment::NEXT_TARGET_LANDING, BaselineCompareSummaryAssessment::NEXT_TARGET_RUN, ], true); } /** * @param list> $tenantContexts * @return list> */ private function attentionItems( array $tenantContexts, User $user, bool $canViewAlerts, CanonicalNavigationContext $navigationContext, ): array { $items = collect($tenantContexts) ->map(function (array $context) use ($user, $canViewAlerts, $navigationContext): ?array { $tenant = $context['tenant'] ?? null; $aggregate = $context['aggregate'] ?? null; if (! $tenant instanceof Tenant || ! $aggregate instanceof TenantGovernanceAggregate) { return null; } if ($aggregate->lapsedGovernanceCount > 0) { return $this->makeAttentionItem( tenant: $tenant, key: 'tenant_lapsed_governance', family: 'governance', urgency: 'critical', title: 'Lapsed accepted-risk governance', body: sprintf( '%d accepted-risk finding%s no longer have valid governance backing.', $aggregate->lapsedGovernanceCount, $aggregate->lapsedGovernanceCount === 1 ? '' : 's', ), badge: 'Governance', badgeColor: 'danger', destination: $this->tenantDashboardTarget($tenant, $user, 'Open tenant dashboard'), supportingMessage: 'Open the tenant dashboard to review the full invalid-governance family without narrowing the findings set.', ); } if ($aggregate->overdueOpenFindingsCount > 0) { return $this->makeAttentionItem( tenant: $tenant, key: 'tenant_overdue_findings', family: 'findings', urgency: 'critical', title: 'Overdue findings', body: sprintf( '%d open finding%s are overdue and still need workflow follow-up.', $aggregate->overdueOpenFindingsCount, $aggregate->overdueOpenFindingsCount === 1 ? '' : 's', ), badge: 'Findings', badgeColor: 'danger', destination: $this->findingsTarget( tenant: $tenant, user: $user, filters: ['tab' => 'overdue'], ), ); } if ($this->shouldPromoteCompareAttention($aggregate)) { return $this->makeAttentionItem( tenant: $tenant, key: 'tenant_compare_attention', family: 'compare', urgency: $aggregate->summaryAssessment->stateFamily === BaselineCompareSummaryAssessment::STATE_STALE ? 'high' : 'critical', title: 'Baseline compare posture', body: $aggregate->headline, badge: 'Baseline', badgeColor: $aggregate->tone, destination: $this->baselineCompareTarget($tenant, $user), supportingMessage: $aggregate->supportingMessage, ); } if ($aggregate->highSeverityActiveFindingsCount > 0) { return $this->makeAttentionItem( tenant: $tenant, key: 'tenant_high_severity_findings', family: 'findings', urgency: 'high', title: 'High severity active findings', body: sprintf( '%d high or critical finding%s are still active.', $aggregate->highSeverityActiveFindingsCount, $aggregate->highSeverityActiveFindingsCount === 1 ? '' : 's', ), badge: 'Findings', badgeColor: 'danger', destination: $this->findingsTarget( tenant: $tenant, user: $user, filters: ['tab' => 'needs_action', 'high_severity' => true], ), ); } if ($aggregate->expiringGovernanceCount > 0) { return $this->makeAttentionItem( tenant: $tenant, key: 'tenant_expiring_governance', family: 'governance', urgency: 'medium', title: 'Expiring accepted-risk governance', body: sprintf( '%d accepted-risk finding%s need governance review soon.', $aggregate->expiringGovernanceCount, $aggregate->expiringGovernanceCount === 1 ? '' : 's', ), badge: 'Governance', badgeColor: 'warning', destination: $this->findingsTarget( tenant: $tenant, user: $user, filters: [ 'tab' => 'risk_accepted', 'governance_validity' => FindingException::VALIDITY_EXPIRING, ], ), ); } $followUpOperationsCount = (int) ($context['follow_up_operations_count'] ?? 0); if ($followUpOperationsCount > 0) { return $this->makeAttentionItem( tenant: $tenant, key: 'tenant_operations_follow_up', family: 'operations', urgency: 'medium', title: 'Operations need follow-up', body: sprintf( '%d run%s failed, completed with warnings, or still need operator follow-up.', $followUpOperationsCount, $followUpOperationsCount === 1 ? '' : 's', ), badge: 'Operations', badgeColor: 'danger', destination: $this->operationsIndexTarget($tenant, $navigationContext, 'blocked'), ); } $activeOperationsCount = (int) ($context['active_operations_count'] ?? 0); if ($activeOperationsCount > 0) { return $this->makeAttentionItem( tenant: $tenant, key: 'tenant_active_operations', family: 'operations', urgency: 'supporting', title: 'Operations are running', body: sprintf( '%d run%s are currently queued or in progress for this tenant.', $activeOperationsCount, $activeOperationsCount === 1 ? '' : 's', ), badge: 'Operations', badgeColor: 'warning', destination: $this->operationsIndexTarget($tenant, $navigationContext, 'active'), ); } $alertFailuresCount = (int) ($context['alert_failures_count'] ?? 0); if ($canViewAlerts && $alertFailuresCount > 0) { return $this->makeAttentionItem( tenant: $tenant, key: 'tenant_alert_delivery_failures', family: 'alerts', urgency: 'supporting', title: 'Alert deliveries failed', body: sprintf( '%d alert delivery attempt%s failed in the last 7 days for this tenant.', $alertFailuresCount, $alertFailuresCount === 1 ? '' : 's', ), badge: 'Alerts', badgeColor: 'danger', destination: $this->alertsOverviewTarget($navigationContext, true), ); } return null; }) ->filter() ->values() ->all(); usort($items, function (array $left, array $right): int { $leftPriority = $this->attentionPriority($left); $rightPriority = $this->attentionPriority($right); if ($leftPriority !== $rightPriority) { return $rightPriority <=> $leftPriority; } return strcmp((string) ($left['tenant_label'] ?? ''), (string) ($right['tenant_label'] ?? '')); }); return array_slice($items, 0, 5); } private function attentionPriority(array $item): int { return match ($item['key'] ?? null) { 'tenant_lapsed_governance' => 100, 'tenant_overdue_findings' => 95, 'tenant_compare_attention' => 90, 'tenant_high_severity_findings' => 80, 'tenant_expiring_governance' => 70, 'tenant_operations_follow_up' => 40, 'tenant_active_operations' => 20, 'tenant_alert_delivery_failures' => 10, default => 0, }; } /** * @return array */ private function makeAttentionItem( Tenant $tenant, string $key, string $family, string $urgency, string $title, string $body, string $badge, string $badgeColor, array $destination, ?string $supportingMessage = null, ): array { $item = [ 'key' => $key, 'tenant_id' => (int) $tenant->getKey(), 'tenant_label' => (string) $tenant->name, 'tenant_route_key' => $this->tenantRouteKey($tenant), 'family' => $family, 'urgency' => $urgency, 'title' => $title, 'body' => $body, 'supporting_message' => $supportingMessage, 'badge' => $badge, 'badge_color' => $badgeColor, 'destination' => $destination, 'action_disabled' => (bool) ($destination['disabled'] ?? false), 'helper_text' => $destination['helper_text'] ?? null, ]; $item['url'] = $destination['disabled'] === true ? null : ($destination['url'] ?? null); return $item; } /** * @return list> */ private function summaryMetrics( int $accessibleTenantCount, int $governanceAttentionTenantCount, int $totalActiveOperationsCount, int $totalAlertFailuresCount, bool $canViewAlerts, CanonicalNavigationContext $navigationContext, ): array { $metrics = [ $this->makeSummaryMetric( key: 'accessible_tenants', label: 'Accessible tenants', value: $accessibleTenantCount, category: 'scope', description: $accessibleTenantCount > 0 ? 'Tenant drill-down stays explicit from this workspace home.' : 'No tenant memberships are available in this workspace yet.', color: $accessibleTenantCount > 0 ? 'primary' : 'warning', destination: $accessibleTenantCount > 0 ? $this->chooseTenantTarget() : $this->switchWorkspaceTarget(), ), $this->makeSummaryMetric( key: 'governance_attention_tenants', label: 'Governance attention', value: $governanceAttentionTenantCount, category: 'governance_risk', description: 'Affected visible tenants with overdue findings, governance expiry, lapsed governance, or compare posture that needs review.', color: $governanceAttentionTenantCount > 0 ? 'danger' : 'gray', destination: $governanceAttentionTenantCount > 0 ? $this->chooseTenantTarget('Choose tenant') : null, ), $this->makeSummaryMetric( key: 'active_operations', label: 'Active operations', value: $totalActiveOperationsCount, category: 'activity', description: 'Activity only. Active execution does not imply governance health.', color: $totalActiveOperationsCount > 0 ? 'warning' : 'gray', destination: $totalActiveOperationsCount > 0 ? $this->operationsIndexTarget(null, $navigationContext, 'active') : null, ), ]; if ($canViewAlerts) { $metrics[] = $this->makeSummaryMetric( key: 'alert_failures', label: 'Alert failures (7d)', value: $totalAlertFailuresCount, category: 'alerts', description: 'Alert delivery follow-up for the visible workspace slice in the last 7 days.', color: $totalAlertFailuresCount > 0 ? 'danger' : 'gray', destination: $totalAlertFailuresCount > 0 ? $this->alertsOverviewTarget($navigationContext, true) : null, ); } return $metrics; } /** * @return array */ private function makeSummaryMetric( string $key, string $label, int $value, string $category, string $description, string $color, ?array $destination, ): array { return [ 'key' => $key, 'label' => $label, 'value' => $value, 'category' => $category, 'description' => $description, 'color' => $color, 'destination' => $destination, 'destination_url' => $destination !== null && ($destination['disabled'] ?? false) === false ? ($destination['url'] ?? null) : null, ]; } /** * @param array $accessibleTenantIds * @return list> */ private function recentOperations( int $workspaceId, array $accessibleTenantIds, CanonicalNavigationContext $navigationContext, ): array { $statusSpec = BadgeRenderer::label(BadgeDomain::OperationRunStatus); $statusColorSpec = BadgeRenderer::color(BadgeDomain::OperationRunStatus); $outcomeSpec = BadgeRenderer::label(BadgeDomain::OperationRunOutcome); $outcomeColorSpec = BadgeRenderer::color(BadgeDomain::OperationRunOutcome); return $this->scopeToAuthorizedTenants( OperationRun::query()->with('tenant'), $workspaceId, $accessibleTenantIds, ) ->latest('created_at') ->limit(5) ->get() ->map(function (OperationRun $run) use ($navigationContext, $statusSpec, $statusColorSpec, $outcomeSpec, $outcomeColorSpec): array { $destination = $this->operationDetailTarget($run, $navigationContext); return [ 'id' => (int) $run->getKey(), 'title' => OperationCatalog::label((string) $run->type), 'tenant_label' => $run->tenant instanceof Tenant ? (string) $run->tenant->name : null, 'status_label' => $statusSpec($run->status), 'status_color' => $statusColorSpec($run->status), 'outcome_label' => $outcomeSpec($run->outcome), 'outcome_color' => $outcomeColorSpec($run->outcome), 'guidance' => OperationUxPresenter::surfaceGuidance($run), 'started_at' => $run->created_at?->diffForHumans() ?? 'just now', 'destination' => $destination, 'url' => $destination['url'], ]; }) ->all(); } /** * @return array */ private function calmnessState( int $accessibleTenantCount, array $attentionItems, int $governanceAttentionTenantCount, int $totalActiveOperationsCount, int $totalAlertFailuresCount, bool $canViewAlerts, CanonicalNavigationContext $navigationContext, ): array { $checkedDomains = ['tenant_access', 'governance', 'findings', 'compare', 'operations']; if ($canViewAlerts) { $checkedDomains[] = 'alerts'; } if ($accessibleTenantCount === 0) { return [ 'is_calm' => false, 'checked_domains' => $checkedDomains, 'title' => 'No accessible tenants in this workspace', 'body' => 'This workspace is not calm or healthy yet because your current scope has no visible tenants. Switch workspace or review workspace-wide operations while access is being restored.', 'next_action' => $this->switchWorkspaceTarget(), ]; } $hasActivityAttention = $totalActiveOperationsCount > 0 || ($canViewAlerts && $totalAlertFailuresCount > 0); $isCalm = $governanceAttentionTenantCount === 0 && ! $hasActivityAttention; if ($isCalm) { return [ 'is_calm' => true, 'checked_domains' => $checkedDomains, 'title' => 'Nothing urgent in your visible workspace slice', 'body' => 'Visible governance, findings, compare posture, and activity currently look calm. Choose a tenant deliberately if you want to inspect one in more detail.', 'next_action' => $this->chooseTenantTarget(), ]; } if ($attentionItems === []) { return [ 'is_calm' => false, 'checked_domains' => $checkedDomains, 'title' => 'Workspace activity still needs review', 'body' => 'This workspace is not calm in the domains it can check right now, but your current scope does not expose a more specific tenant drill-through here. Review operations first.', 'next_action' => $this->operationsIndexTarget(null, $navigationContext, 'blocked'), ]; } return [ 'is_calm' => false, 'checked_domains' => $checkedDomains, 'title' => 'Visible tenants still need attention', 'body' => 'Governance risk or execution follow-up is still present in this workspace.', 'next_action' => $attentionItems[0]['destination'] ?? $this->chooseTenantTarget(), ]; } /** * @param array $accessibleTenantIds */ private function scopeToAuthorizedTenants(Builder $query, int $workspaceId, array $accessibleTenantIds): Builder { return $query ->where('workspace_id', $workspaceId) ->where(function (Builder $query) use ($accessibleTenantIds): void { $query->whereNull('tenant_id'); if ($accessibleTenantIds !== []) { $query->orWhereIn('tenant_id', $accessibleTenantIds); } }); } /** * @param array $accessibleTenantIds */ private function scopeToVisibleTenants(Builder $query, int $workspaceId, array $accessibleTenantIds): Builder { return $query ->where('workspace_id', $workspaceId) ->whereIn('tenant_id', $accessibleTenantIds === [] ? [0] : $accessibleTenantIds); } /** * @return list> */ private function quickActions( Workspace $workspace, int $accessibleTenantCount, bool $canViewAlerts, User $user, CanonicalNavigationContext $navigationContext, ): array { $actions = [ [ 'key' => 'choose_tenant', 'label' => 'Choose tenant', 'description' => 'Deliberately enter tenant context from this workspace.', 'url' => ChooseTenant::getUrl(panel: 'admin'), 'icon' => 'heroicon-o-building-office-2', 'color' => 'primary', 'visible' => $accessibleTenantCount > 0, ], [ 'key' => 'operations', 'label' => 'Open operations', 'description' => 'Review current and recent workspace-wide operations.', 'url' => OperationRunLinks::index(context: $navigationContext, allTenants: true), 'icon' => 'heroicon-o-queue-list', 'color' => 'gray', 'visible' => true, ], [ 'key' => 'alerts', 'label' => 'Open alerts', 'description' => 'Inspect alert overview, rules, and deliveries.', 'url' => $this->alertsOverviewUrl($navigationContext), 'icon' => 'heroicon-o-bell-alert', 'color' => 'gray', 'visible' => $canViewAlerts, ], [ 'key' => 'switch_workspace', 'label' => 'Switch workspace', 'description' => 'Change the active workspace context.', 'url' => $this->switchWorkspaceUrl(), 'icon' => 'heroicon-o-arrows-right-left', 'color' => 'gray', 'visible' => true, ], [ 'key' => 'manage_workspaces', 'label' => 'Manage workspaces', 'description' => 'Open workspace management and memberships.', 'url' => route('filament.admin.resources.workspaces.index'), 'icon' => 'heroicon-o-squares-2x2', 'color' => 'gray', 'visible' => $this->canManageWorkspaces($workspace, $user), ], ]; return collect($actions) ->filter(fn (array $action): bool => (bool) $action['visible']) ->map(function (array $action): array { unset($action['visible']); return $action; }) ->values() ->all(); } private function canManageWorkspaces(Workspace $workspace, User $user): bool { if ($this->workspaceCapabilityResolver->can($user, $workspace, Capabilities::WORKSPACE_MANAGE)) { return true; } $roles = WorkspaceRoleCapabilityMap::rolesWithCapability(Capabilities::WORKSPACE_MEMBERSHIP_MANAGE); return $user->workspaceMemberships() ->whereIn('role', $roles) ->exists(); } private function tenantRouteKey(Tenant $tenant): string { return filled($tenant->external_id) ? (string) $tenant->external_id : (string) $tenant->getKey(); } /** * @return array */ private function chooseTenantTarget(string $label = 'Choose tenant'): array { return $this->destination( kind: 'choose_tenant', url: ChooseTenant::getUrl(panel: 'admin'), label: $label, ); } /** * @return array */ private function switchWorkspaceTarget(string $label = 'Switch workspace'): array { return $this->destination( kind: 'switch_workspace', url: $this->switchWorkspaceUrl(), label: $label, ); } /** * @return array */ private function tenantDashboardTarget(Tenant $tenant, User $user, string $label = 'Open tenant dashboard'): array { if (! $this->canTenantView($user, $tenant)) { return $this->disabledDestination( kind: 'tenant_dashboard', label: $label, tenant: $tenant, ); } return $this->destination( kind: 'tenant_dashboard', url: TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant), label: $label, tenant: $tenant, ); } /** * @param array $filters * @return array */ private function findingsTarget(Tenant $tenant, User $user, array $filters, string $label = 'Open findings'): array { if ($this->canOpenFindings($user, $tenant)) { return $this->destination( kind: 'tenant_findings', url: FindingResource::getUrl('index', $filters, panel: 'tenant', tenant: $tenant), label: $label, tenant: $tenant, filters: $filters, ); } if ($this->canTenantView($user, $tenant)) { return $this->tenantDashboardTarget($tenant, $user, 'Open tenant dashboard'); } return $this->disabledDestination( kind: 'tenant_findings', label: $label, tenant: $tenant, filters: $filters, ); } /** * @return array */ private function baselineCompareTarget(Tenant $tenant, User $user, string $label = 'Open Baseline Compare'): array { if (! $this->canTenantView($user, $tenant)) { return $this->disabledDestination( kind: 'baseline_compare_landing', label: $label, tenant: $tenant, ); } return $this->destination( kind: 'baseline_compare_landing', url: BaselineCompareLanding::getUrl(panel: 'tenant', tenant: $tenant), label: $label, tenant: $tenant, ); } /** * @return array */ private function operationsIndexTarget( ?Tenant $tenant, CanonicalNavigationContext $navigationContext, ?string $activeTab = null, string $label = 'Open operations', ): array { return $this->destination( kind: 'operations_index', url: OperationRunLinks::index($tenant, $navigationContext, $activeTab, $tenant === null), label: $label, tenant: $tenant, filters: array_filter([ 'tenant_id' => $tenant?->getKey(), 'activeTab' => $activeTab, ], static fn (mixed $value): bool => $value !== null && $value !== ''), ); } /** * @return array */ private function operationDetailTarget(OperationRun $run, CanonicalNavigationContext $navigationContext): array { $tenant = $run->tenant instanceof Tenant ? $run->tenant : null; return $this->destination( kind: 'operation_detail', url: OperationRunLinks::tenantlessView($run, $navigationContext), label: 'Open operation', tenant: $tenant, filters: ['run' => (int) $run->getKey()], ); } /** * @return array */ private function alertsOverviewTarget(CanonicalNavigationContext $navigationContext, bool $enabled, string $label = 'Open alerts'): array { if (! $enabled) { return $this->disabledDestination( kind: 'alerts_overview', label: $label, ); } return $this->destination( kind: 'alerts_overview', url: $this->alertsOverviewUrl($navigationContext), label: $label, ); } private function alertsOverviewUrl(CanonicalNavigationContext $navigationContext): string { return $this->appendQuery(route('filament.admin.alerts'), $navigationContext->toQuery()); } private function canTenantView(User $user, Tenant $tenant): bool { return $this->capabilityResolver->can($user, $tenant, Capabilities::TENANT_VIEW); } private function canOpenFindings(User $user, Tenant $tenant): bool { return $this->capabilityResolver->can($user, $tenant, Capabilities::TENANT_FINDINGS_VIEW); } /** * @param array $filters * @return array */ private function destination( string $kind, string $url, string $label, ?Tenant $tenant = null, array $filters = [], ): array { return [ 'kind' => $kind, 'url' => $url, 'tenant_route_key' => $tenant instanceof Tenant ? $this->tenantRouteKey($tenant) : null, 'label' => $label, 'disabled' => false, 'helper_text' => null, 'filters' => $filters !== [] ? $filters : null, ]; } /** * @param array $filters * @return array */ private function disabledDestination( string $kind, string $label, ?Tenant $tenant = null, array $filters = [], ): array { return [ 'kind' => $kind, 'url' => null, 'tenant_route_key' => $tenant instanceof Tenant ? $this->tenantRouteKey($tenant) : null, 'label' => $label, 'disabled' => true, 'helper_text' => UiTooltips::INSUFFICIENT_PERMISSION, 'filters' => $filters !== [] ? $filters : null, ]; } private function workspaceOverviewNavigationContext(): CanonicalNavigationContext { return new CanonicalNavigationContext( sourceSurface: 'workspace.overview', canonicalRouteName: 'admin.home', tenantId: null, backLinkLabel: 'Back to overview', backLinkUrl: route('admin.home'), ); } /** * @param array $query */ private function appendQuery(string $url, array $query): string { if ($query === []) { return $url; } return $url.(str_contains($url, '?') ? '&' : '?').http_build_query($query); } private function switchWorkspaceUrl(): string { return route('filament.admin.pages.choose-workspace').'?choose=1'; } }