TenantAtlas/app/Filament/Resources/OperationRunResource.php
ahmido 6ca496233b feat: centralize tenant lifecycle presentation (#175)
## Summary
- add a shared tenant lifecycle presentation contract and referenced-tenant adapter for canonical lifecycle labels and helper copy
- align tenant, chooser, onboarding, archived-banner, and tenantless operation viewer surfaces with the shared lifecycle vocabulary
- add Spec 146 design artifacts, audit notes, and regression coverage for lifecycle presentation across Filament and onboarding surfaces

## Validation
- `vendor/bin/sail bin pint --dirty --format agent`
- `vendor/bin/sail artisan test --compact tests/Feature/Badges/TenantStatusBadgeTest.php tests/Unit/Badges/TenantBadgesTest.php tests/Unit/Tenants/TenantLifecycleTest.php tests/Unit/Support/Tenants/TenantLifecyclePresentationTest.php tests/Feature/Filament/TenantLifecyclePresentationAcrossTenantSurfacesTest.php tests/Feature/Filament/ReferencedTenantLifecyclePresentationTest.php tests/Feature/Filament/TenantLifecycleStatusDomainSeparationTest.php tests/Feature/Filament/TenantViewHeaderUiEnforcementTest.php tests/Feature/Onboarding/TenantLifecyclePresentationCopyTest.php tests/Feature/Onboarding/OnboardingDraftAuthorizationTest.php tests/Feature/Onboarding/OnboardingDraftLifecycleTest.php`

## Notes
- Livewire v4.0+ compliance preserved; this change is presentation-only on existing Filament v5 surfaces.
- Panel provider registration remains unchanged in `bootstrap/providers.php`.
- No global-search behavior changed; no resource was newly made globally searchable or disabled.
- No destructive actions were added or changed.
- No asset registration strategy changed; existing deploy flow for `php artisan filament:assets` remains unchanged.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #175
2026-03-16 18:18:53 +00:00

685 lines
30 KiB
PHP

<?php
namespace App\Filament\Resources;
use App\Filament\Support\VerificationReportChangeIndicator;
use App\Filament\Support\VerificationReportViewer;
use App\Models\OperationRun;
use App\Models\Tenant;
use App\Models\User;
use App\Models\VerificationCheckAcknowledgement;
use App\Support\Badges\BadgeDomain;
use App\Support\Badges\BadgeRenderer;
use App\Support\Baselines\BaselineCompareReasonCode;
use App\Support\Filament\FilterOptionCatalog;
use App\Support\Filament\FilterPresets;
use App\Support\Filament\TablePaginationProfiles;
use App\Support\Navigation\CrossResourceNavigationMatrix;
use App\Support\Navigation\RelatedNavigationResolver;
use App\Support\OperateHub\OperateHubShell;
use App\Support\OperationCatalog;
use App\Support\OperationRunLinks;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\OpsUx\RunDurationInsights;
use App\Support\Tenants\ReferencedTenantLifecyclePresentation;
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
use App\Support\Ui\ActionSurface\ActionSurfaceDefaults;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
use App\Support\Workspaces\WorkspaceContext;
use BackedEnum;
use Filament\Actions;
use Filament\Facades\Filament;
use Filament\Infolists\Components\ViewEntry;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use UnitEnum;
class OperationRunResource extends Resource
{
protected static bool $isScopedToTenant = false;
protected static ?string $model = OperationRun::class;
protected static ?string $slug = 'operations';
protected static bool $shouldRegisterNavigation = false;
protected static bool $isGloballySearchable = false;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-queue-list';
protected static string|UnitEnum|null $navigationGroup = 'Monitoring';
protected static ?string $navigationLabel = 'Operations';
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
{
return ActionSurfaceDeclaration::forResource(ActionSurfaceProfile::RunLog)
->withDefaults(new ActionSurfaceDefaults(
moreGroupLabel: 'More',
exportIsDefaultBulkActionForReadOnly: false,
))
->exempt(
ActionSurfaceSlot::ListHeader,
'Run-log list intentionally has no list-header actions; navigation actions are provided by Monitoring shell pages.',
)
->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ViewAction->value)
->exempt(
ActionSurfaceSlot::ListBulkMoreGroup,
'Operation runs are immutable records; bulk export is deferred and tracked outside this retrofit.',
)
->exempt(
ActionSurfaceSlot::ListEmptyState,
'Empty-state action is intentionally omitted; users can adjust filters/date range in-page.',
)
->satisfy(
ActionSurfaceSlot::DetailHeader,
'Tenantless detail view keeps back-navigation, refresh, related links, and resumable operation actions in the header.',
);
}
public static function getEloquentQuery(): Builder
{
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId();
return parent::getEloquentQuery()
->with('user')
->latest('id')
->when($workspaceId, fn (Builder $query) => $query->where('workspace_id', (int) $workspaceId))
->when(! $workspaceId, fn (Builder $query) => $query->whereRaw('1 = 0'));
}
public static function form(Schema $schema): Schema
{
return $schema;
}
public static function infolist(Schema $schema): Schema
{
return $schema
->schema([
ViewEntry::make('enterprise_detail')
->label('')
->view('filament.infolists.entries.enterprise-detail.layout')
->state(fn (OperationRun $record): array => static::enterpriseDetailPage($record)->toArray())
->columnSpanFull(),
]);
}
public static function table(Table $table): Table
{
return $table
->defaultSort('created_at', 'desc')
->paginated(TablePaginationProfiles::resource())
->persistFiltersInSession()
->persistSearchInSession()
->persistSortInSession()
->columns([
Tables\Columns\TextColumn::make('status')
->badge()
->formatStateUsing(BadgeRenderer::label(BadgeDomain::OperationRunStatus))
->color(BadgeRenderer::color(BadgeDomain::OperationRunStatus))
->icon(BadgeRenderer::icon(BadgeDomain::OperationRunStatus))
->iconColor(BadgeRenderer::iconColor(BadgeDomain::OperationRunStatus)),
Tables\Columns\TextColumn::make('type')
->label('Operation')
->formatStateUsing(fn (?string $state): string => OperationCatalog::label((string) $state))
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('initiator_name')
->label('Initiator')
->searchable(),
Tables\Columns\TextColumn::make('created_at')
->label('Started')
->since()
->sortable(),
Tables\Columns\TextColumn::make('duration')
->getStateUsing(function (OperationRun $record): string {
if ($record->started_at && $record->completed_at) {
return $record->completed_at->diffForHumans($record->started_at, true);
}
return '—';
}),
Tables\Columns\TextColumn::make('outcome')
->badge()
->formatStateUsing(BadgeRenderer::label(BadgeDomain::OperationRunOutcome))
->color(BadgeRenderer::color(BadgeDomain::OperationRunOutcome))
->icon(BadgeRenderer::icon(BadgeDomain::OperationRunOutcome))
->iconColor(BadgeRenderer::iconColor(BadgeDomain::OperationRunOutcome)),
])
->filters([
Tables\Filters\SelectFilter::make('tenant_id')
->label('Tenant')
->options(function (): array {
$user = auth()->user();
if (! $user instanceof User) {
return [];
}
return collect($user->getTenants(Filament::getCurrentOrDefaultPanel()))
->mapWithKeys(static fn (Tenant $tenant): array => [
(string) $tenant->getKey() => $tenant->getFilamentName(),
])
->all();
})
->default(function (): ?string {
$activeTenant = app(OperateHubShell::class)->activeEntitledTenant(request());
if (! $activeTenant instanceof Tenant) {
return null;
}
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId();
if ($workspaceId === null || (int) $activeTenant->workspace_id !== (int) $workspaceId) {
return null;
}
return (string) $activeTenant->getKey();
})
->searchable(),
Tables\Filters\SelectFilter::make('type')
->options(function (): array {
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId();
if ($workspaceId === null) {
return [];
}
$types = OperationRun::query()
->where('workspace_id', (int) $workspaceId)
->select('type')
->distinct()
->orderBy('type')
->pluck('type', 'type')
->all();
return FilterOptionCatalog::operationTypes(array_keys($types));
}),
Tables\Filters\SelectFilter::make('status')
->options([
OperationRunStatus::Queued->value => 'Queued',
OperationRunStatus::Running->value => 'Running',
OperationRunStatus::Completed->value => 'Completed',
]),
Tables\Filters\SelectFilter::make('outcome')
->options(OperationRunOutcome::uiLabels(includeReserved: false)),
Tables\Filters\SelectFilter::make('initiator_name')
->label('Initiator')
->options(function (): array {
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId();
if ($workspaceId === null) {
return [];
}
return OperationRun::query()
->where('workspace_id', (int) $workspaceId)
->whereNotNull('initiator_name')
->select('initiator_name')
->distinct()
->orderBy('initiator_name')
->pluck('initiator_name', 'initiator_name')
->all();
})
->searchable(),
FilterPresets::dateRange('created_at', 'Created', 'created_at', [
'from' => now()->subDays(30)->toDateString(),
'until' => now()->toDateString(),
]),
])
->actions([
Actions\ViewAction::make()
->label('View run')
->url(fn (OperationRun $record): string => OperationRunLinks::tenantlessView($record)),
])
->bulkActions([])
->emptyStateHeading('No operation runs found')
->emptyStateDescription('Queued, running, and completed operations will appear here when work is triggered in this scope.')
->emptyStateIcon('heroicon-o-queue-list');
}
private static function enterpriseDetailPage(OperationRun $record): \App\Support\Ui\EnterpriseDetail\EnterpriseDetailPageData
{
$factory = new \App\Support\Ui\EnterpriseDetail\EnterpriseDetailSectionFactory;
$statusSpec = BadgeRenderer::spec(BadgeDomain::OperationRunStatus, $record->status);
$outcomeSpec = BadgeRenderer::spec(BadgeDomain::OperationRunOutcome, $record->outcome);
$targetScope = static::targetScopeDisplay($record);
$summaryLine = \App\Support\OpsUx\SummaryCountsNormalizer::renderSummaryLine(is_array($record->summary_counts) ? $record->summary_counts : []);
$referencedTenantLifecycle = $record->tenant instanceof Tenant
? ReferencedTenantLifecyclePresentation::forOperationRun($record->tenant)
: null;
$builder = \App\Support\Ui\EnterpriseDetail\EnterpriseDetailBuilder::make('operation_run', 'workspace-context')
->header(new \App\Support\Ui\EnterpriseDetail\SummaryHeaderData(
title: OperationCatalog::label((string) $record->type),
subtitle: 'Run #'.$record->getKey(),
statusBadges: [
$factory->statusBadge($statusSpec->label, $statusSpec->color, $statusSpec->icon, $statusSpec->iconColor),
$factory->statusBadge($outcomeSpec->label, $outcomeSpec->color, $outcomeSpec->icon, $outcomeSpec->iconColor),
],
keyFacts: [
$factory->keyFact('Target', $targetScope ?? 'No target scope details were recorded for this run.'),
$factory->keyFact('Initiator', $record->initiator_name),
$factory->keyFact('Elapsed', RunDurationInsights::elapsedHuman($record)),
$factory->keyFact('Expected', RunDurationInsights::expectedHuman($record)),
],
descriptionHint: 'Run identity, outcome, scope, and related next steps stay ahead of stored payloads and diagnostic fragments.',
))
->addSection(
$factory->factsSection(
id: 'run_summary',
kind: 'core_details',
title: 'Run summary',
items: [
$factory->keyFact('Operation', OperationCatalog::label((string) $record->type)),
$factory->keyFact('Initiator', $record->initiator_name),
$factory->keyFact('Target scope', $targetScope ?? 'No target scope details were recorded for this run.'),
$factory->keyFact('Expected duration', RunDurationInsights::expectedHuman($record)),
],
),
$factory->viewSection(
id: 'related_context',
kind: 'related_context',
title: 'Related context',
view: 'filament.infolists.entries.related-context',
viewData: ['entries' => app(RelatedNavigationResolver::class)
->detailEntries(CrossResourceNavigationMatrix::SOURCE_OPERATION_RUN, $record)],
emptyState: $factory->emptyState('No related context is available for this record.'),
),
)
->addSupportingCard(
$factory->supportingFactsCard(
kind: 'status',
title: 'Current state',
items: array_values(array_filter([
$factory->keyFact('Status', $statusSpec->label, badge: $factory->statusBadge($statusSpec->label, $statusSpec->color, $statusSpec->icon, $statusSpec->iconColor)),
$factory->keyFact('Outcome', $outcomeSpec->label, badge: $factory->statusBadge($outcomeSpec->label, $outcomeSpec->color, $outcomeSpec->icon, $outcomeSpec->iconColor)),
$referencedTenantLifecycle !== null
? $factory->keyFact(
'Tenant lifecycle',
$referencedTenantLifecycle->presentation->label,
badge: $factory->statusBadge(
$referencedTenantLifecycle->presentation->label,
$referencedTenantLifecycle->presentation->badgeColor,
$referencedTenantLifecycle->presentation->badgeIcon,
$referencedTenantLifecycle->presentation->badgeIconColor,
),
)
: null,
$referencedTenantLifecycle?->selectorAvailabilityMessage() !== null
? $factory->keyFact('Tenant selector context', $referencedTenantLifecycle->selectorAvailabilityMessage())
: null,
$referencedTenantLifecycle?->contextNote !== null
? $factory->keyFact('Viewer context', $referencedTenantLifecycle->contextNote)
: null,
$summaryLine !== null ? $factory->keyFact('Counts', $summaryLine) : null,
RunDurationInsights::stuckGuidance($record) !== null ? $factory->keyFact('Guidance', RunDurationInsights::stuckGuidance($record)) : null,
])),
),
$factory->supportingFactsCard(
kind: 'timestamps',
title: 'Timing',
items: [
$factory->keyFact('Created', static::formatDetailTimestamp($record->created_at)),
$factory->keyFact('Started', static::formatDetailTimestamp($record->started_at)),
$factory->keyFact('Completed', static::formatDetailTimestamp($record->completed_at)),
$factory->keyFact('Elapsed', RunDurationInsights::elapsedHuman($record)),
],
),
)
->addTechnicalSection(
$factory->technicalDetail(
title: 'Context',
entries: [
$factory->keyFact('Identity hash', $record->run_identity_hash),
$factory->keyFact('Workspace scope', $record->workspace_id),
$factory->keyFact('Tenant scope', $record->tenant_id),
],
description: 'Stored run context stays available for debugging without dominating the default reading path.',
view: 'filament.infolists.entries.snapshot-json',
viewData: ['payload' => static::contextPayload($record)],
),
);
$counts = static::summaryCountFacts($record, $factory);
if ($counts !== []) {
$builder->addSection(
$factory->factsSection(
id: 'counts',
kind: 'current_status',
title: 'Counts',
items: $counts,
),
);
}
if (! empty($record->failure_summary)) {
$builder->addSection(
$factory->viewSection(
id: 'failures',
kind: 'operational_context',
title: 'Failures',
view: 'filament.infolists.entries.snapshot-json',
viewData: ['payload' => $record->failure_summary ?? []],
),
);
}
if ((string) $record->type === 'baseline_compare') {
$baselineCompareFacts = static::baselineCompareFacts($record, $factory);
$baselineCompareEvidence = static::baselineCompareEvidencePayload($record);
if ($baselineCompareFacts !== []) {
$builder->addSection(
$factory->factsSection(
id: 'baseline_compare',
kind: 'operational_context',
title: 'Baseline compare',
items: $baselineCompareFacts,
),
);
}
if ($baselineCompareEvidence !== []) {
$builder->addSection(
$factory->viewSection(
id: 'baseline_compare_evidence',
kind: 'operational_context',
title: 'Baseline compare evidence',
view: 'filament.infolists.entries.snapshot-json',
viewData: ['payload' => $baselineCompareEvidence],
),
);
}
}
if ((string) $record->type === 'baseline_capture') {
$baselineCaptureEvidence = static::baselineCaptureEvidencePayload($record);
if ($baselineCaptureEvidence !== []) {
$builder->addSection(
$factory->viewSection(
id: 'baseline_capture_evidence',
kind: 'operational_context',
title: 'Baseline capture evidence',
view: 'filament.infolists.entries.snapshot-json',
viewData: ['payload' => $baselineCaptureEvidence],
),
);
}
}
if (VerificationReportViewer::shouldRenderForRun($record)) {
$builder->addSection(
$factory->viewSection(
id: 'verification_report',
kind: 'operational_context',
title: 'Verification report',
view: 'filament.components.verification-report-viewer',
viewData: static::verificationReportViewData($record),
),
);
}
return $builder->build();
}
/**
* @return list<array<string, mixed>>
*/
private static function summaryCountFacts(
OperationRun $record,
\App\Support\Ui\EnterpriseDetail\EnterpriseDetailSectionFactory $factory,
): array {
$counts = \App\Support\OpsUx\SummaryCountsNormalizer::normalize(is_array($record->summary_counts) ? $record->summary_counts : []);
return array_map(
static fn (string $key, int $value): array => $factory->keyFact(ucfirst(str_replace('_', ' ', $key)), $value),
array_keys($counts),
array_values($counts),
);
}
/**
* @return list<array<string, mixed>>
*/
private static function baselineCompareFacts(
OperationRun $record,
\App\Support\Ui\EnterpriseDetail\EnterpriseDetailSectionFactory $factory,
): array {
$context = is_array($record->context) ? $record->context : [];
$facts = [];
$fidelity = data_get($context, 'baseline_compare.fidelity');
if (is_string($fidelity) && trim($fidelity) !== '') {
$facts[] = $factory->keyFact('Fidelity', $fidelity);
}
$proof = data_get($context, 'baseline_compare.coverage.proof');
$uncoveredTypes = data_get($context, 'baseline_compare.coverage.uncovered_types');
$uncoveredTypes = is_array($uncoveredTypes) ? array_values(array_filter($uncoveredTypes, 'is_string')) : [];
$facts[] = $factory->keyFact(
'Coverage',
match (true) {
$proof === false => 'Unproven',
$uncoveredTypes !== [] => 'Warnings',
$proof === true => 'Covered',
default => 'Unknown',
},
);
$reasonCode = data_get($context, 'baseline_compare.reason_code');
if (is_string($reasonCode) && trim($reasonCode) !== '') {
$enum = BaselineCompareReasonCode::tryFrom(trim($reasonCode));
$facts[] = $factory->keyFact(
'Why no findings',
$enum?->message() ?? trim($reasonCode),
trim($reasonCode),
);
}
if ($uncoveredTypes !== []) {
sort($uncoveredTypes, SORT_STRING);
$facts[] = $factory->keyFact('Uncovered types', implode(', ', array_slice($uncoveredTypes, 0, 12)).(count($uncoveredTypes) > 12 ? '…' : ''));
}
$inventorySyncRunId = data_get($context, 'baseline_compare.inventory_sync_run_id');
if (is_numeric($inventorySyncRunId)) {
$facts[] = $factory->keyFact('Inventory sync run', '#'.(int) $inventorySyncRunId);
}
return $facts;
}
/**
* @return array<string, mixed>
*/
private static function baselineCompareEvidencePayload(OperationRun $record): array
{
$context = is_array($record->context) ? $record->context : [];
return array_filter([
'subjects_total' => is_numeric(data_get($context, 'baseline_compare.subjects_total'))
? (int) data_get($context, 'baseline_compare.subjects_total')
: null,
'evidence_gaps_count' => is_numeric(data_get($context, 'baseline_compare.evidence_gaps.count'))
? (int) data_get($context, 'baseline_compare.evidence_gaps.count')
: null,
'resume_token' => data_get($context, 'baseline_compare.resume_token'),
'evidence_capture' => is_array(data_get($context, 'baseline_compare.evidence_capture'))
? data_get($context, 'baseline_compare.evidence_capture')
: null,
'evidence_gaps' => is_array(data_get($context, 'baseline_compare.evidence_gaps'))
? data_get($context, 'baseline_compare.evidence_gaps')
: null,
], static fn (mixed $value): bool => $value !== null && $value !== []);
}
/**
* @return array<string, mixed>
*/
private static function baselineCaptureEvidencePayload(OperationRun $record): array
{
$context = is_array($record->context) ? $record->context : [];
return array_filter([
'subjects_total' => is_numeric(data_get($context, 'baseline_capture.subjects_total'))
? (int) data_get($context, 'baseline_capture.subjects_total')
: null,
'gaps_count' => is_numeric(data_get($context, 'baseline_capture.gaps.count'))
? (int) data_get($context, 'baseline_capture.gaps.count')
: null,
'resume_token' => data_get($context, 'baseline_capture.resume_token'),
'evidence_capture' => is_array(data_get($context, 'baseline_capture.evidence_capture'))
? data_get($context, 'baseline_capture.evidence_capture')
: null,
'gaps' => is_array(data_get($context, 'baseline_capture.gaps'))
? data_get($context, 'baseline_capture.gaps')
: null,
], static fn (mixed $value): bool => $value !== null && $value !== []);
}
/**
* @return array<string, mixed>
*/
private static function verificationReportViewData(OperationRun $record): array
{
$report = VerificationReportViewer::report($record);
$fingerprint = is_array($report) ? VerificationReportViewer::fingerprint($report) : null;
$changeIndicator = VerificationReportChangeIndicator::forRun($record);
$previousRunUrl = null;
if ($changeIndicator !== null) {
$tenant = app(OperateHubShell::class)->activeEntitledTenant(request());
$previousRunUrl = $tenant instanceof Tenant
? OperationRunLinks::view($changeIndicator['previous_report_id'], $tenant)
: OperationRunLinks::tenantlessView($changeIndicator['previous_report_id']);
}
$acknowledgements = VerificationCheckAcknowledgement::query()
->where('tenant_id', (int) ($record->tenant_id ?? 0))
->where('workspace_id', (int) ($record->workspace_id ?? 0))
->where('operation_run_id', (int) $record->getKey())
->with('acknowledgedByUser')
->get()
->mapWithKeys(static function (VerificationCheckAcknowledgement $ack): array {
$user = $ack->acknowledgedByUser;
return [
(string) $ack->check_key => [
'check_key' => (string) $ack->check_key,
'ack_reason' => (string) $ack->ack_reason,
'acknowledged_at' => $ack->acknowledged_at?->toJSON(),
'expires_at' => $ack->expires_at?->toJSON(),
'acknowledged_by' => $user instanceof User
? [
'id' => (int) $user->getKey(),
'name' => (string) $user->name,
]
: null,
],
];
})
->all();
return [
'report' => $report,
'run' => [
'id' => (int) $record->getKey(),
'type' => (string) $record->type,
'status' => (string) $record->status,
'outcome' => (string) $record->outcome,
'started_at' => $record->started_at?->toJSON(),
'completed_at' => $record->completed_at?->toJSON(),
],
'fingerprint' => $fingerprint,
'changeIndicator' => $changeIndicator,
'previousRunUrl' => $previousRunUrl,
'acknowledgements' => $acknowledgements,
'redactionNotes' => VerificationReportViewer::redactionNotes($report),
];
}
/**
* @return array<string, mixed>
*/
private static function contextPayload(OperationRun $record): array
{
$context = is_array($record->context) ? $record->context : [];
if (array_key_exists('verification_report', $context)) {
$context['verification_report'] = [
'redacted' => true,
'note' => 'Rendered in the Verification report section.',
];
}
return $context;
}
private static function formatDetailTimestamp(mixed $value): string
{
if (! $value instanceof \Illuminate\Support\Carbon) {
return '—';
}
return $value->toDayDateTimeString();
}
public static function getPages(): array
{
return [];
}
private static function targetScopeDisplay(OperationRun $record): ?string
{
$context = is_array($record->context) ? $record->context : [];
$targetScope = $context['target_scope'] ?? null;
if (! is_array($targetScope)) {
return null;
}
$entraTenantName = $targetScope['entra_tenant_name'] ?? null;
$entraTenantId = $targetScope['entra_tenant_id'] ?? null;
$directoryContextId = $targetScope['directory_context_id'] ?? null;
$entraTenantName = is_string($entraTenantName) ? trim($entraTenantName) : null;
$entraTenantId = is_string($entraTenantId) ? trim($entraTenantId) : null;
$directoryContextId = match (true) {
is_string($directoryContextId) => trim($directoryContextId),
is_int($directoryContextId) => (string) $directoryContextId,
default => null,
};
$entra = null;
if ($entraTenantName !== null && $entraTenantName !== '') {
$entra = $entraTenantId ? "{$entraTenantName} ({$entraTenantId})" : $entraTenantName;
} elseif ($entraTenantId !== null && $entraTenantId !== '') {
$entra = $entraTenantId;
}
$parts = array_values(array_filter([
$entra,
$directoryContextId ? "directory_context_id: {$directoryContextId}" : null,
], fn (?string $value): bool => $value !== null && $value !== ''));
return $parts !== [] ? implode(' · ', $parts) : null;
}
}