Some checks failed
Main Confidence / confidence (push) Failing after 54s
Integrates latest TenantPilot platform changes from `platform-dev` into `dev`. Refresh method in this update: merge from `origin/dev` into `platform-dev` on explicit user request. This PR was created by agent on user request; do not merge automatically. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #308
208 lines
13 KiB
PHP
208 lines
13 KiB
PHP
<x-filament-panels::page>
|
|
@php
|
|
$preview = is_array($preview ?? null) ? $preview : null;
|
|
$preflight = is_array($preflight ?? null) ? $preflight : null;
|
|
$previewSummary = is_array($preview['summary'] ?? null) ? $preview['summary'] : [];
|
|
$preflightSummary = is_array($preflight['summary'] ?? null) ? $preflight['summary'] : [];
|
|
$blockedReasonCounts = is_array($preflight['blockedReasonCounts'] ?? null) ? $preflight['blockedReasonCounts'] : [];
|
|
$sourceTenantName = data_get($preview, 'selection.sourceTenantName');
|
|
$targetTenantName = data_get($preview, 'selection.targetTenantName');
|
|
$selectedPolicyTypes = data_get($preview, 'selection.policyTypes', []);
|
|
@endphp
|
|
|
|
<x-filament::section heading="Cross-tenant compare">
|
|
<x-slot name="description">
|
|
Compare one authorized source tenant to one authorized target tenant from a canonical workspace surface. Preview stays read only and promotion remains preflight only.
|
|
</x-slot>
|
|
|
|
<div class="space-y-4">
|
|
<div class="rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900/70">
|
|
<form wire:submit.prevent="applySelection" class="space-y-4">
|
|
{{ $this->form }}
|
|
|
|
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
|
<div class="flex-1 rounded-xl border border-dashed border-gray-300 bg-gray-50 px-4 py-3 dark:border-gray-700 dark:bg-gray-900/40">
|
|
<div class="text-sm font-semibold text-gray-950 dark:text-white">Shareable compare scope</div>
|
|
<p class="mt-1 text-sm text-gray-600 dark:text-gray-300">
|
|
Source tenant, target tenant, and governed-subject filters live on the URL so the same compare preview can be reopened or shared.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="flex flex-wrap items-center gap-3 lg:shrink-0">
|
|
<x-filament::button type="submit" color="primary" size="sm" wire:loading.attr="disabled" wire:target="applySelection,generatePromotionPreflight">
|
|
Run compare preview
|
|
</x-filament::button>
|
|
|
|
@if ($this->hasActiveSelection())
|
|
<x-filament::button tag="a" :href="$this->clearSelectionUrl()" color="gray" size="sm">
|
|
Clear selection
|
|
</x-filament::button>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
@if (filled($selectionMessage))
|
|
<div class="rounded-2xl border border-warning-200 bg-warning-50 px-4 py-3 text-sm text-warning-900 dark:border-warning-500/30 dark:bg-warning-500/10 dark:text-warning-100">
|
|
{{ $selectionMessage }}
|
|
</div>
|
|
@endif
|
|
|
|
@if ($preview === null)
|
|
<div class="rounded-2xl border border-dashed border-gray-300 bg-gray-50/70 px-5 py-6 text-sm text-gray-600 dark:border-gray-700 dark:bg-gray-900/40 dark:text-gray-300">
|
|
Choose a source tenant and a target tenant to build a compare preview. The source and target must be different tenants inside the active workspace.
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</x-filament::section>
|
|
|
|
@if ($preview !== null)
|
|
<x-filament::section heading="Compare preview">
|
|
<x-slot name="description">
|
|
Decision-first summary of governed subjects. Raw payloads stay on the existing tenant and baseline surfaces.
|
|
</x-slot>
|
|
|
|
<div class="space-y-4" data-testid="cross-tenant-compare-preview">
|
|
<div class="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between">
|
|
<div class="space-y-2">
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<x-filament::badge color="info" size="sm">Source tenant: {{ $sourceTenantName }}</x-filament::badge>
|
|
<x-filament::badge color="gray" size="sm">Target tenant: {{ $targetTenantName }}</x-filament::badge>
|
|
@foreach ($selectedPolicyTypes as $policyType)
|
|
<x-filament::badge color="gray" size="sm">{{ $this->stateLabel($policyType) }}</x-filament::badge>
|
|
@endforeach
|
|
</div>
|
|
|
|
<div class="flex flex-wrap items-center gap-3 text-sm">
|
|
@if (filled($this->sourceTenantUrl()))
|
|
<a href="{{ $this->sourceTenantUrl() }}" class="font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300">
|
|
Open source tenant
|
|
</a>
|
|
@endif
|
|
|
|
@if (filled($this->targetTenantUrl()))
|
|
<a href="{{ $this->targetTenantUrl() }}" class="font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300">
|
|
Open target tenant
|
|
</a>
|
|
@endif
|
|
</div>
|
|
|
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
|
The preview groups governed subjects into reproducible compare states so you can decide whether the target is aligned, missing, blocked, or needs manual review.
|
|
</p>
|
|
</div>
|
|
|
|
<dl class="grid gap-3 sm:grid-cols-3 xl:w-[28rem]">
|
|
@foreach (['match', 'different', 'missing', 'ambiguous', 'blocked', 'total'] as $state)
|
|
<div class="rounded-2xl border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/70">
|
|
<dt class="text-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">{{ $this->stateLabel($state) }}</dt>
|
|
<dd class="mt-1 text-2xl font-semibold text-gray-950 dark:text-white">{{ (int) ($previewSummary[$state] ?? 0) }}</dd>
|
|
</div>
|
|
@endforeach
|
|
</dl>
|
|
</div>
|
|
|
|
<div class="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900/70">
|
|
<div class="grid grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)_minmax(0,0.8fr)] gap-3 border-b border-gray-200 px-4 py-3 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:border-gray-800 dark:text-gray-400">
|
|
<div>Governed subject</div>
|
|
<div>Reasoning</div>
|
|
<div>Compare state</div>
|
|
</div>
|
|
|
|
<div class="divide-y divide-gray-200 dark:divide-gray-800">
|
|
@foreach (data_get($preview, 'subjects', []) as $subject)
|
|
<div class="grid grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)_minmax(0,0.8fr)] gap-3 px-4 py-4" data-testid="cross-tenant-compare-subject">
|
|
<div class="space-y-2">
|
|
<div class="text-sm font-semibold text-gray-950 dark:text-white">{{ data_get($subject, 'displayName') }}</div>
|
|
<div class="flex flex-wrap items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
|
|
<span>{{ $this->stateLabel((string) data_get($subject, 'policyType', 'unknown')) }}</span>
|
|
@if (filled(data_get($subject, 'subjectKey')))
|
|
<x-filament::badge color="gray" size="sm">{{ data_get($subject, 'subjectKey') }}</x-filament::badge>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-wrap gap-2">
|
|
@forelse (data_get($subject, 'reasonCodes', []) as $reasonCode)
|
|
<x-filament::badge color="gray" size="sm">{{ $this->reasonLabel((string) $reasonCode) }}</x-filament::badge>
|
|
@empty
|
|
<span class="text-sm text-gray-500 dark:text-gray-400">No blocking reason.</span>
|
|
@endforelse
|
|
</div>
|
|
|
|
<div class="flex items-start justify-start xl:justify-end">
|
|
<x-filament::badge :color="$this->stateColor((string) data_get($subject, 'state', 'unknown'))" size="sm">
|
|
{{ $this->stateLabel((string) data_get($subject, 'state', 'unknown')) }}
|
|
</x-filament::badge>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</x-filament::section>
|
|
@endif
|
|
|
|
@if ($preflight !== null)
|
|
<x-filament::section heading="Promotion preflight">
|
|
<x-slot name="description">
|
|
Read-only readiness view. No target mutation, queue dispatch, or operation run is created in this slice.
|
|
</x-slot>
|
|
|
|
<div class="space-y-4" data-testid="cross-tenant-preflight">
|
|
<div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
|
@foreach (['ready', 'blocked', 'manual_mapping_required', 'total'] as $state)
|
|
<div class="rounded-2xl border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/70">
|
|
<div class="text-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">{{ $this->stateLabel($state) }}</div>
|
|
<div class="mt-1 text-2xl font-semibold text-gray-950 dark:text-white">{{ (int) ($preflightSummary[$state] ?? 0) }}</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
|
|
@if ($blockedReasonCounts !== [])
|
|
<div class="rounded-2xl border border-gray-200 bg-gray-50/80 p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900/50">
|
|
<div class="text-sm font-semibold text-gray-950 dark:text-white">Top blocked reasons</div>
|
|
<div class="mt-3 flex flex-wrap gap-2">
|
|
@foreach ($blockedReasonCounts as $reasonCode => $count)
|
|
<x-filament::badge color="danger" size="sm">{{ $this->reasonLabel((string) $reasonCode) }}: {{ (int) $count }}</x-filament::badge>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<div class="grid gap-4 xl:grid-cols-3">
|
|
@foreach (['ready', 'manual_mapping_required', 'blocked'] as $bucket)
|
|
<div class="rounded-2xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900/70">
|
|
<div class="flex items-center justify-between gap-3">
|
|
<div class="text-sm font-semibold text-gray-950 dark:text-white">{{ $this->stateLabel($bucket) }}</div>
|
|
<x-filament::badge :color="$this->stateColor($bucket)" size="sm">
|
|
{{ count(data_get($preflight, 'buckets.'.$bucket, [])) }}
|
|
</x-filament::badge>
|
|
</div>
|
|
|
|
<div class="mt-3 space-y-3">
|
|
@forelse (data_get($preflight, 'buckets.'.$bucket, []) as $subject)
|
|
<div class="rounded-xl border border-gray-200 bg-gray-50/80 px-3 py-3 dark:border-gray-800 dark:bg-gray-900/50">
|
|
<div class="text-sm font-semibold text-gray-950 dark:text-white">{{ data_get($subject, 'displayName') }}</div>
|
|
@if (data_get($subject, 'preflight.reasonLabels', []) !== [])
|
|
<div class="mt-2 flex flex-wrap gap-2">
|
|
@foreach (data_get($subject, 'preflight.reasonLabels', []) as $reasonLabel)
|
|
<x-filament::badge color="gray" size="sm">{{ $reasonLabel }}</x-filament::badge>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@empty
|
|
<div class="text-sm text-gray-500 dark:text-gray-400">No governed subjects in this bucket.</div>
|
|
@endforelse
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
</x-filament::section>
|
|
@endif
|
|
|
|
<x-filament-actions::modals />
|
|
</x-filament-panels::page> |