TenantAtlas/apps/platform/resources/views/filament/pages/environment-required-permissions.blade.php
ahmido d2876af95b feat: provider connections resolution guidance v1 (spec 353) (#424)
Implemented the first version of provider readiness resolution guidance. Added the ProviderReadinessResolutionAdapter, provider readiness guidance card, and updated EnvironmentRequiredPermissions, ProviderConnectionResource, and ListProviderConnections/ViewProviderConnection. Added tests and updated the design coverage matrix.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #424
2026-06-04 22:41:04 +00:00

567 lines
33 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@php
use App\Support\Badges\BadgeDomain;
use App\Support\Badges\BadgeRenderer;
use App\Support\Links\RequiredPermissionsLinks;
use Illuminate\Support\Carbon;
$tenant = $this->currentTenant();
$vm = $this->viewModel();
$overview = is_array($vm['overview'] ?? null) ? $vm['overview'] : [];
$counts = is_array($overview['counts'] ?? null) ? $overview['counts'] : [];
$featureImpacts = is_array($overview['feature_impacts'] ?? null) ? $overview['feature_impacts'] : [];
$capabilityGroups = is_array($overview['capability_groups'] ?? null) ? $overview['capability_groups'] : [];
$primaryCapabilityGroup = is_array($overview['primary_capability_group'] ?? null) ? $overview['primary_capability_group'] : null;
$freshness = is_array($overview['freshness'] ?? null) ? $overview['freshness'] : [];
$filters = is_array($vm['filters'] ?? null) ? $vm['filters'] : [];
$selectedFeatures = is_array($filters['features'] ?? null) ? $filters['features'] : [];
$overall = $overview['overall'] ?? null;
$overallSpec = $overall !== null ? BadgeRenderer::spec(BadgeDomain::VerificationReportOverall, $overall) : null;
$copy = is_array($vm['copy'] ?? null) ? $vm['copy'] : [];
$copyApplication = (string) ($copy['application'] ?? '');
$copyDelegated = (string) ($copy['delegated'] ?? '');
$missingApplication = (int) ($counts['missing_application'] ?? 0);
$missingDelegated = (int) ($counts['missing_delegated'] ?? 0);
$presentCount = (int) ($counts['present'] ?? 0);
$errorCount = (int) ($counts['error'] ?? 0);
$missingTotal = $missingApplication + $missingDelegated + $errorCount;
$requiredTotal = $missingTotal + $presentCount;
$adminConsentUrl = $tenant ? RequiredPermissionsLinks::adminConsentUrl($tenant) : null;
$adminConsentPrimaryUrl = $tenant ? RequiredPermissionsLinks::adminConsentPrimaryUrl($tenant) : RequiredPermissionsLinks::adminConsentGuideUrl();
$adminConsentLabel = $adminConsentUrl ? 'Open admin consent' : 'Admin consent guide';
$reRunUrl = $this->reRunVerificationUrl();
$manageProviderConnectionUrl = $this->manageProviderConnectionUrl();
$guidance = $this->guidanceCase();
$guidancePrimaryAction = is_array($guidance['primary_action'] ?? null) ? $guidance['primary_action'] : [];
$canRunProviderVerification = $this->canRunProviderVerification();
$showGuidancePrimaryAction = (is_string($guidancePrimaryAction['url'] ?? null) && $guidancePrimaryAction['url'] !== '')
|| ($canRunProviderVerification && ($guidancePrimaryAction['action_name'] ?? null) === 'runProviderVerification');
$lastRefreshedAt = is_string($freshness['last_refreshed_at'] ?? null) ? (string) $freshness['last_refreshed_at'] : null;
$lastRefreshedLabel = $lastRefreshedAt ? Carbon::parse($lastRefreshedAt)->diffForHumans() : 'Unknown';
$isStale = (bool) ($freshness['is_stale'] ?? true);
$hasStoredPermissionData = $lastRefreshedAt !== null;
$issues = [];
if ($missingApplication > 0) {
$issues[] = [
'severity' => 'Blocker',
'title' => 'Missing application permissions',
'description' => "{$missingApplication} required application permission(s) are missing.",
'links' => array_values(array_filter([
['label' => $adminConsentLabel, 'url' => $adminConsentPrimaryUrl, 'external' => true],
$manageProviderConnectionUrl ? ['label' => 'Manage provider connection', 'url' => $manageProviderConnectionUrl, 'external' => false] : null,
['label' => 'Open environment dashboard', 'url' => $reRunUrl, 'external' => false],
])),
];
}
if ($missingDelegated > 0) {
$issues[] = [
'severity' => 'Warning',
'title' => 'Missing delegated permissions',
'description' => "{$missingDelegated} delegated permission(s) are missing.",
'links' => [
['label' => $adminConsentLabel, 'url' => $adminConsentPrimaryUrl, 'external' => true],
['label' => 'Open provider connection', 'url' => $manageProviderConnectionUrl ?? $reRunUrl, 'external' => false],
],
];
}
if ($errorCount > 0) {
$issues[] = [
'severity' => 'Warning',
'title' => 'Verification results need review',
'description' => "{$errorCount} permission row(s) are in an unknown/error state and require follow-up.",
'links' => [
['label' => 'Open provider connection', 'url' => $manageProviderConnectionUrl ?? $reRunUrl, 'external' => false],
$manageProviderConnectionUrl ? ['label' => 'Manage provider connection', 'url' => $manageProviderConnectionUrl, 'external' => false] : ['label' => 'Admin consent guide', 'url' => RequiredPermissionsLinks::adminConsentGuideUrl(), 'external' => true],
],
];
}
if ($isStale) {
$issues[] = [
'severity' => 'Warning',
'title' => 'Freshness warning',
'description' => $hasStoredPermissionData
? "Permission data is older than 30 days (last refresh {$lastRefreshedLabel})."
: 'No stored verification data is available yet.',
'links' => [
['label' => 'Open provider connection', 'url' => $manageProviderConnectionUrl ?? $reRunUrl, 'external' => false],
],
];
}
@endphp
<x-filament::page>
<div class="space-y-6">
<x-filament::section heading="Summary">
<div class="flex flex-col gap-4" x-data="{ showCopyApplication: false, showCopyDelegated: false }">
<div class="flex flex-wrap items-start justify-between gap-3">
<div class="flex flex-wrap items-center gap-2">
@if ($overallSpec)
<x-filament::badge :color="$overallSpec->color" :icon="$overallSpec->icon">
{{ $overallSpec->label }}
</x-filament::badge>
@endif
<span class="text-xs text-gray-500 dark:text-gray-400">
Stored data · refreshed {{ $lastRefreshedLabel }}{{ $isStale ? ' · stale' : '' }}
</span>
</div>
<div class="grid w-full grid-cols-2 gap-2 sm:w-auto sm:grid-cols-4">
<div class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950">
<div class="text-xs font-medium text-gray-500 dark:text-gray-400">Missing (app)</div>
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ (int) ($counts['missing_application'] ?? 0) }}</div>
</div>
<div class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950">
<div class="text-xs font-medium text-gray-500 dark:text-gray-400">Missing (delegated)</div>
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ (int) ($counts['missing_delegated'] ?? 0) }}</div>
</div>
<div class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950">
<div class="text-xs font-medium text-gray-500 dark:text-gray-400">Present</div>
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ (int) ($counts['present'] ?? 0) }}</div>
</div>
<div class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950">
<div class="text-xs font-medium text-gray-500 dark:text-gray-400">Errors</div>
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ (int) ($counts['error'] ?? 0) }}</div>
</div>
</div>
</div>
@if (! $hasStoredPermissionData)
<div class="rounded-xl border border-warning-200 bg-warning-50 p-4 text-sm text-warning-800 dark:border-warning-800 dark:bg-warning-950/30 dark:text-warning-200">
<div class="font-semibold">No data available</div>
<div class="mt-1">
No stored verification data is available for this environment.
@if ($canRunProviderVerification)
<button
type="button"
wire:click="runProviderVerification"
class="font-medium underline"
>
Run provider verification
</button>.
@elseif ($manageProviderConnectionUrl)
<a href="{{ $manageProviderConnectionUrl }}" class="font-medium underline">Open provider connection</a>.
@else
<a href="{{ $reRunUrl }}" class="font-medium underline">Open environment dashboard</a>.
@endif
</div>
</div>
@endif
@if (is_array($guidance) && $guidance !== [])
@include('filament.partials.provider-readiness-guidance-card', [
'guidance' => $guidance,
'inlinePrimaryAction' => $showGuidancePrimaryAction,
'primaryActionMethod' => 'runProviderVerification',
])
@endif
@if ($capabilityGroups !== [])
<div class="rounded-xl border border-gray-200 bg-white p-4 text-sm text-gray-700 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-200">
<div class="flex flex-wrap items-start justify-between gap-3">
<div class="min-w-0">
<div class="text-sm font-semibold text-gray-900 dark:text-white">Provider capabilities</div>
<div class="mt-1 text-xs text-gray-600 dark:text-gray-300">
Capability-first view of the provider prerequisites used by operation start gates.
</div>
</div>
@if ($primaryCapabilityGroup)
@php
$primaryStatus = (string) ($primaryCapabilityGroup['status'] ?? 'unknown');
$primarySpec = BadgeRenderer::spec(BadgeDomain::ProviderCapabilityStatus, $primaryStatus);
@endphp
<x-filament::badge :color="$primarySpec->color" :icon="$primarySpec->icon" class="max-w-full whitespace-normal text-left">
{{ (string) ($primaryCapabilityGroup['label'] ?? 'Provider capability') }}: {{ $primarySpec->label }}
</x-filament::badge>
@endif
</div>
<div class="mt-4 grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
@foreach ($capabilityGroups as $capabilityGroup)
@php
if (! is_array($capabilityGroup)) {
continue;
}
$capabilityLabel = (string) ($capabilityGroup['label'] ?? 'Provider capability');
$capabilityStatus = (string) ($capabilityGroup['status'] ?? 'unknown');
$capabilitySpec = BadgeRenderer::spec(BadgeDomain::ProviderCapabilityStatus, $capabilityStatus);
$message = (string) ($capabilityGroup['message'] ?? '');
$capabilityCounts = is_array($capabilityGroup['evidence_counts'] ?? null) ? $capabilityGroup['evidence_counts'] : [];
$missing = (int) ($capabilityCounts['missing'] ?? 0);
$errors = (int) ($capabilityCounts['errors'] ?? 0);
@endphp
<div class="rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-800 dark:bg-gray-950">
<div class="flex flex-col gap-2">
<div class="min-w-0">
<div class="text-sm font-semibold text-gray-950 dark:text-white">
{{ $capabilityLabel }}
</div>
<div class="mt-1 text-xs text-gray-600 dark:text-gray-300">
{{ $message }}
</div>
</div>
<x-filament::badge :color="$capabilitySpec->color" :icon="$capabilitySpec->icon" size="sm" class="w-fit max-w-full whitespace-normal text-left">
{{ $capabilitySpec->label }}
</x-filament::badge>
</div>
<div class="mt-3 text-xs text-gray-500 dark:text-gray-400">
{{ $missing }} missing, {{ $errors }} error(s)
</div>
</div>
@endforeach
</div>
</div>
@endif
<div class="rounded-xl border border-gray-200 bg-white p-4 text-sm text-gray-700 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-200">
<div class="text-sm font-semibold text-gray-900 dark:text-white">Permission handoff</div>
<div class="mt-2 space-y-1">
<div>
<span class="font-medium">Who can fix this?</span>
Global Administrator / Privileged Role Administrator.
</div>
<div>
<span class="font-medium">Primary next step:</span>
<a
href="{{ $adminConsentPrimaryUrl }}"
class="text-primary-600 hover:underline dark:text-primary-400"
target="_blank"
rel="noreferrer"
>
{{ $adminConsentLabel }}
</a>
</div>
@if ($canRunProviderVerification)
<div>
<span class="font-medium">After granting consent:</span>
<button
type="button"
wire:click="runProviderVerification"
class="font-medium text-primary-600 hover:underline dark:text-primary-400"
>
Run provider verification
</button>
</div>
@elseif ($manageProviderConnectionUrl)
<div>
<span class="font-medium">After granting consent:</span>
<a
href="{{ $manageProviderConnectionUrl }}"
class="text-primary-600 hover:underline dark:text-primary-400"
>
Open provider connection to run verification
</a>
</div>
@endif
</div>
<div class="mt-3 flex flex-wrap gap-2">
<x-filament::button
color="primary"
size="sm"
x-on:click="showCopyApplication = true"
:disabled="$copyApplication === ''"
>
Copy missing application permissions
</x-filament::button>
<x-filament::button
color="gray"
size="sm"
x-on:click="showCopyDelegated = true"
:disabled="$copyDelegated === ''"
>
Copy missing delegated permissions
</x-filament::button>
</div>
</div>
@if (is_array($featureImpacts) && $featureImpacts !== [])
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
@foreach ($featureImpacts as $impact)
@php
$featureKey = is_array($impact) ? ($impact['feature'] ?? null) : null;
$featureKey = is_string($featureKey) ? $featureKey : null;
$missingCount = is_array($impact) ? (int) ($impact['missing'] ?? 0) : 0;
$isBlocked = is_array($impact) ? (bool) ($impact['blocked'] ?? false) : false;
if ($featureKey === null) {
continue;
}
$selected = in_array($featureKey, $selectedFeatures, true);
@endphp
<div
class="rounded-xl border p-4 text-left {{ $selected ? 'border-primary-300 bg-primary-50 dark:border-primary-700 dark:bg-primary-950/40' : 'border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900' }}"
>
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<div class="truncate text-sm font-semibold text-gray-950 dark:text-white">
{{ $featureKey }}
</div>
<div class="mt-1 text-xs text-gray-600 dark:text-gray-300">
{{ $missingCount }} missing
</div>
</div>
<x-filament::badge :color="$isBlocked ? 'danger' : ($missingCount > 0 ? 'warning' : 'success')" size="sm">
{{ $isBlocked ? 'Blocked' : ($missingCount > 0 ? 'At risk' : 'OK') }}
</x-filament::badge>
</div>
</div>
@endforeach
</div>
@endif
<div
x-cloak
x-show="showCopyApplication"
class="fixed inset-0 z-50 flex items-center justify-center px-4 py-6"
x-on:keydown.escape.window="showCopyApplication = false"
>
<div class="absolute inset-0 bg-gray-950/50" x-on:click="showCopyApplication = false"></div>
<div class="relative w-full max-w-2xl rounded-xl border border-gray-200 bg-white p-5 shadow-xl dark:border-gray-800 dark:bg-gray-900">
<div class="flex items-start justify-between gap-4">
<div>
<div class="text-base font-semibold text-gray-950 dark:text-white">Missing application permissions</div>
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">Newline-separated list for admin consent.</div>
</div>
<x-filament::button color="gray" size="sm" x-on:click="showCopyApplication = false">Close</x-filament::button>
</div>
@if ($copyApplication === '')
<div class="mt-4 rounded-lg border border-gray-200 bg-gray-50 p-4 text-sm text-gray-700 dark:border-gray-800 dark:bg-gray-950 dark:text-gray-200">
Nothing to copy no missing application permissions in the current feature filter.
</div>
@else
<div
class="mt-4 space-y-2"
x-data="{
text: @js($copyApplication),
copied: false,
async copyPayload() {
try {
if (navigator.clipboard && (location.protocol === 'https:' || location.hostname === 'localhost')) {
await navigator.clipboard.writeText(this.text);
} else {
const ta = document.createElement('textarea');
ta.value = this.text;
ta.style.position = 'fixed';
ta.style.inset = '0';
document.body.appendChild(ta);
ta.focus();
ta.select();
document.execCommand('copy');
ta.remove();
}
this.copied = true;
setTimeout(() => (this.copied = false), 1500);
} catch (e) {
this.copied = false;
}
},
}"
>
<div class="flex items-center justify-end gap-2">
<span x-show="copied" x-transition class="text-sm text-gray-500 dark:text-gray-400">
Copied
</span>
<x-filament::button size="sm" color="primary" x-on:click="copyPayload()">
Copy
</x-filament::button>
</div>
<div class="rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-800 dark:bg-gray-950">
<pre class="max-h-72 overflow-auto whitespace-pre font-mono text-xs text-gray-900 dark:text-gray-100" x-text="text"></pre>
</div>
</div>
@endif
</div>
</div>
<div
x-cloak
x-show="showCopyDelegated"
class="fixed inset-0 z-50 flex items-center justify-center px-4 py-6"
x-on:keydown.escape.window="showCopyDelegated = false"
>
<div class="absolute inset-0 bg-gray-950/50" x-on:click="showCopyDelegated = false"></div>
<div class="relative w-full max-w-2xl rounded-xl border border-gray-200 bg-white p-5 shadow-xl dark:border-gray-800 dark:bg-gray-900">
<div class="flex items-start justify-between gap-4">
<div>
<div class="text-base font-semibold text-gray-950 dark:text-white">Missing delegated permissions</div>
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">Newline-separated list for delegated consent.</div>
</div>
<x-filament::button color="gray" size="sm" x-on:click="showCopyDelegated = false">Close</x-filament::button>
</div>
@if ($copyDelegated === '')
<div class="mt-4 rounded-lg border border-gray-200 bg-gray-50 p-4 text-sm text-gray-700 dark:border-gray-800 dark:bg-gray-950 dark:text-gray-200">
Nothing to copy no missing delegated permissions in the current feature filter.
</div>
@else
<div
class="mt-4 space-y-2"
x-data="{
text: @js($copyDelegated),
copied: false,
async copyPayload() {
try {
if (navigator.clipboard && (location.protocol === 'https:' || location.hostname === 'localhost')) {
await navigator.clipboard.writeText(this.text);
} else {
const ta = document.createElement('textarea');
ta.value = this.text;
ta.style.position = 'fixed';
ta.style.inset = '0';
document.body.appendChild(ta);
ta.focus();
ta.select();
document.execCommand('copy');
ta.remove();
}
this.copied = true;
setTimeout(() => (this.copied = false), 1500);
} catch (e) {
this.copied = false;
}
},
}"
>
<div class="flex items-center justify-end gap-2">
<span x-show="copied" x-transition class="text-sm text-gray-500 dark:text-gray-400">
Copied
</span>
<x-filament::button size="sm" color="primary" x-on:click="copyPayload()">
Copy
</x-filament::button>
</div>
<div class="rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-800 dark:bg-gray-950">
<pre class="max-h-72 overflow-auto whitespace-pre font-mono text-xs text-gray-900 dark:text-gray-100" x-text="text"></pre>
</div>
</div>
@endif
</div>
</div>
</div>
</x-filament::section>
<x-filament::section heading="Issues">
@if ($issues === [])
<div class="rounded-xl border border-success-200 bg-success-50 p-4 text-sm text-success-800 dark:border-success-800 dark:bg-success-950/30 dark:text-success-200">
No blockers or warnings detected from stored data.
</div>
@else
<div class="space-y-3">
@foreach ($issues as $issue)
@php
$severity = (string) ($issue['severity'] ?? 'Warning');
$severityColor = $severity === 'Blocker' ? 'danger' : 'warning';
$title = (string) ($issue['title'] ?? 'Issue');
$description = (string) ($issue['description'] ?? '');
$links = is_array($issue['links'] ?? null) ? $issue['links'] : [];
@endphp
<div class="rounded-xl border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-900">
<div class="flex flex-wrap items-center gap-2">
<x-filament::badge :color="$severityColor" size="sm">{{ $severity }}</x-filament::badge>
<div class="text-sm font-semibold text-gray-950 dark:text-white">{{ $title }}</div>
</div>
<div class="mt-2 text-sm text-gray-700 dark:text-gray-300">{{ $description }}</div>
@if ($links !== [])
<div class="mt-3 flex flex-wrap gap-3 text-sm">
@foreach ($links as $link)
@php
$label = is_array($link) ? (string) ($link['label'] ?? '') : '';
$url = is_array($link) ? (string) ($link['url'] ?? '') : '';
$external = is_array($link) ? (bool) ($link['external'] ?? false) : false;
@endphp
@if ($label !== '' && $url !== '')
<a
href="{{ $url }}"
class="text-primary-600 hover:underline dark:text-primary-400"
@if ($external)
target="_blank"
rel="noreferrer"
@endif
>
{{ $label }}
</a>
@endif
@endforeach
</div>
@endif
</div>
@endforeach
</div>
@endif
</x-filament::section>
<x-filament::section heading="Passed">
<div class="rounded-xl border border-gray-200 bg-white p-4 text-sm text-gray-700 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-200">
<div class="font-semibold text-gray-950 dark:text-white">
{{ $presentCount }} permission(s) currently pass.
</div>
<div class="mt-1">
{{ $requiredTotal > 0 ? "Out of {$requiredTotal} required permissions, {$presentCount} are currently granted." : 'No required permissions are configured yet.' }}
</div>
</div>
</x-filament::section>
<x-filament::section heading="Technical details">
<details
data-testid="technical-details"
x-data="{ open: false }"
x-bind:open="open"
x-on:toggle="open = $el.open"
class="group rounded-xl border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-900"
>
<summary class="cursor-pointer list-none text-sm font-semibold text-gray-900 dark:text-white">
Expand technical details
</summary>
<div class="mt-4">
@if (! $tenant)
<div class="text-sm text-gray-600 dark:text-gray-300">
No environment selected.
</div>
@else
<div class="space-y-6">
<div class="rounded-xl border border-gray-200 bg-white p-4 text-sm text-gray-700 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-200">
<div class="font-semibold text-gray-950 dark:text-white">Native permission matrix</div>
<div class="mt-1 text-xs text-gray-600 dark:text-gray-300">
Search doesnt affect copy actions. Feature filters do.
</div>
</div>
{{ $this->table }}
</div>
@endif
</div>
</details>
</x-filament::section>
</div>
</x-filament::page>