TenantAtlas/apps/platform/resources/views/filament/pages/choose-environment.blade.php
ahmido 12ea7f9924 feat: review pack output contract and readiness semantics (spec 347/348) (#419)
Implemented the output contract and readiness semantics for review packs. Also added spec 348.
Includes changes to ChooseEnvironment, CustomerReviewWorkspace, GenerateReviewPackJob and related blade views.
Added comprehensive tests.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #419
2026-06-02 23:17:08 +00:00

206 lines
14 KiB
PHP

<x-filament-panels::page>
@php
$allTenants = $this->getTenants();
$tenants = $this->getVisibleTenants($allTenants);
$workspace = app(\App\Support\Workspaces\WorkspaceContext::class)->currentWorkspace();
$environmentCount = $allTenants->count();
$visibleEnvironmentCount = $tenants->count();
$hasSearch = trim($this->search) !== '';
@endphp
@if ($allTenants->isEmpty())
{{-- Empty state --}}
<div class="mx-auto max-w-md">
<div class="rounded-xl border border-gray-200 bg-white p-8 text-center shadow-sm dark:border-white/10 dark:bg-white/5">
@if ($workspace)
<div class="mb-5 inline-flex items-center gap-1.5 rounded-full border border-gray-200 bg-gray-50 px-3 py-1 text-xs font-medium text-gray-600 dark:border-white/10 dark:bg-white/5 dark:text-gray-400">
<x-filament::icon icon="heroicon-m-building-office-2" class="h-3.5 w-3.5" />
{{ $workspace->name }}
</div>
@endif
<div class="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-primary-50 dark:bg-primary-950/30">
<x-filament::icon
icon="heroicon-o-server-stack"
class="h-7 w-7 text-primary-500 dark:text-primary-400"
/>
</div>
<h3 class="text-base font-semibold text-gray-900 dark:text-white">{{ __('localization.shell.no_active_environments') }}</h3>
<p class="mx-auto mt-2 max-w-xs text-sm text-gray-500 dark:text-gray-400">
{{ __('localization.shell.no_active_environments_description') }}
</p>
<div class="mt-6 flex flex-col items-center gap-3">
<x-filament::button
tag="a"
href="{{ $workspace ? route('admin.workspace.managed-environments.index', ['workspace' => $workspace]) : route('admin.onboarding') }}"
icon="heroicon-m-arrow-right"
size="lg"
>
{{ __('localization.shell.view_managed_environments') }}
</x-filament::button>
<a href="{{ route('filament.admin.pages.choose-workspace') }}"
class="inline-flex items-center gap-1.5 text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
<x-filament::icon icon="heroicon-m-arrows-right-left" class="h-4 w-4" />
{{ __('localization.shell.switch_workspace') }}
</a>
</div>
</div>
</div>
@else
{{-- ManagedEnvironment list --}}
<div class="mx-auto max-w-xl" data-testid="choose-environment-selector">
{{-- Header row --}}
<div class="mb-5 space-y-3">
<div class="flex min-w-0 flex-wrap items-center gap-2">
@if ($workspace)
<div class="inline-flex max-w-full min-w-0 items-center gap-1.5 rounded-full border border-gray-200 bg-gray-50 px-3 py-1 text-xs font-medium text-gray-600 dark:border-white/10 dark:bg-white/5 dark:text-gray-400">
<x-filament::icon icon="heroicon-m-building-office-2" class="h-3.5 w-3.5 shrink-0" />
<span class="min-w-0 truncate" title="{{ $workspace->name }}">{{ $workspace->name }}</span>
</div>
@endif
<span class="text-sm text-gray-500 dark:text-gray-400">
{{ trans_choice('localization.shell.environment_count', $environmentCount, ['count' => $environmentCount]) }}
</span>
</div>
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
<a href="{{ route('admin.onboarding') }}"
data-testid="choose-environment-add"
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-gray-200 bg-white px-3 py-1.5 text-sm font-medium text-gray-600 shadow-sm transition-colors hover:bg-gray-50 hover:text-gray-800 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:border-white/10 dark:bg-white/5 dark:text-gray-300 dark:hover:bg-white/10 dark:hover:text-white dark:focus:ring-offset-gray-900">
<x-filament::icon icon="heroicon-m-plus" class="h-4 w-4" />
{{ __('localization.shell.add_environment') }}
</a>
<a href="{{ route('filament.admin.pages.choose-workspace') }}"
data-testid="choose-environment-switch-workspace"
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-gray-200 bg-white px-3 py-1.5 text-sm font-medium text-gray-600 shadow-sm transition-colors hover:bg-gray-50 hover:text-gray-800 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:border-white/10 dark:bg-white/5 dark:text-gray-300 dark:hover:bg-white/10 dark:hover:text-white dark:focus:ring-offset-gray-900">
<x-filament::icon icon="heroicon-m-arrows-right-left" class="h-4 w-4" />
{{ __('localization.shell.switch_workspace') }}
</a>
</div>
</div>
<p class="mb-2 text-sm text-gray-500 dark:text-gray-400">{{ __('localization.shell.choose_environment_description') }}</p>
<p class="mb-4 text-xs text-gray-500 dark:text-gray-400">{{ __('localization.shell.workspace_wide_available_without_environment') }}</p>
<div class="mb-4 space-y-2" data-testid="choose-environment-filter">
<label for="choose-environment-search" class="fi-sr-only">{{ __('localization.shell.search_environments') }}</label>
<x-filament::input.wrapper
:prefix-icon="\Filament\Support\Icons\Heroicon::MagnifyingGlass"
inline-prefix
class="w-full"
wire:target="search"
>
<x-filament::input
id="choose-environment-search"
type="search"
wire:model.live.debounce.250ms="search"
placeholder="{{ __('localization.shell.search_environments') }}"
data-testid="choose-environment-search"
inline-prefix
/>
</x-filament::input.wrapper>
@if ($hasSearch)
<div class="mt-2 flex flex-wrap items-center justify-between gap-2 text-xs text-gray-500 dark:text-gray-400">
<span>{{ __('localization.shell.environment_search_results_count', ['visible' => $visibleEnvironmentCount, 'total' => $environmentCount]) }}</span>
<button
type="button"
wire:click="$set('search', '')"
class="font-medium text-gray-600 transition hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:text-gray-300 dark:hover:text-white dark:focus:ring-offset-gray-900"
>
{{ __('localization.shell.clear_search') }}
</button>
</div>
@endif
</div>
{{-- ManagedEnvironment cards --}}
@if ($tenants->isEmpty())
<div class="rounded-xl border border-gray-200 bg-white p-6 text-center shadow-sm dark:border-white/10 dark:bg-white/5" data-testid="choose-environment-no-results">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">{{ __('localization.shell.no_environment_search_results') }}</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ __('localization.shell.no_environment_search_results_description') }}</p>
<button
type="button"
wire:click="$set('search', '')"
class="mt-4 inline-flex items-center gap-1.5 rounded-lg border border-gray-200 bg-white px-3 py-1.5 text-sm font-medium text-gray-600 shadow-sm transition hover:bg-gray-50 hover:text-gray-800 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:border-white/10 dark:bg-white/5 dark:text-gray-300 dark:hover:bg-white/10 dark:hover:text-white dark:focus:ring-offset-gray-900"
>
{{ __('localization.shell.clear_search') }}
</button>
</div>
@else
<div class="grid grid-cols-1 gap-3" data-testid="choose-environment-results">
@foreach ($tenants as $tenant)
@php
$presentation = $this->tenantLifecyclePresentation($tenant);
$badgeClasses = match ($presentation->badgeColor) {
'success' => 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-200',
'warning' => 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-500/20 dark:bg-amber-500/10 dark:text-amber-200',
'danger' => 'border-rose-200 bg-rose-50 text-rose-700 dark:border-rose-500/20 dark:bg-rose-500/10 dark:text-rose-200',
default => 'border-gray-200 bg-gray-100 text-gray-700 dark:border-white/10 dark:bg-white/10 dark:text-gray-300',
};
$environmentLabel = $tenant->environment && $tenant->environment !== 'managed_environment'
? strtoupper($tenant->environment)
: null;
@endphp
<button
type="button"
wire:key="tenant-{{ $tenant->id }}"
wire:click="selectEnvironment({{ (int) $tenant->id }})"
aria-label="{{ __('localization.shell.select_environment') }}: {{ $tenant->name }}"
data-testid="choose-environment-card"
class="group relative w-full overflow-hidden rounded-xl border border-gray-200 bg-white p-4 text-left shadow-sm transition-all duration-150 hover:border-gray-300 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:border-white/10 dark:bg-white/5 dark:hover:border-white/20 dark:focus:ring-offset-gray-900"
>
{{-- Loading overlay --}}
<div wire:loading wire:target="selectEnvironment({{ (int) $tenant->id }})"
class="absolute inset-0 z-10 flex items-center justify-center rounded-xl bg-white/80 dark:bg-gray-900/80">
<x-filament::loading-indicator class="h-5 w-5 text-primary-500" />
</div>
<div class="flex min-w-0 items-start gap-3">
<div class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-gray-100 transition-colors group-hover:bg-gray-200 dark:bg-white/10 dark:group-hover:bg-white/15">
<x-filament::icon
icon="heroicon-o-server-stack"
class="h-5 w-5 text-gray-500 group-hover:text-gray-600 dark:text-gray-400 dark:group-hover:text-gray-300"
/>
</div>
<div class="min-w-0 flex-1">
<div class="flex min-w-0 flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
<div class="min-w-0">
<h3 class="truncate text-sm font-semibold text-gray-900 dark:text-white">
{{ $tenant->name }}
</h3>
@if ($tenant->domain)
<p class="mt-0.5 truncate text-xs text-gray-500 dark:text-gray-400">
{{ $tenant->domain }}
</p>
@endif
</div>
<div class="flex max-w-full shrink-0 flex-wrap items-center gap-2 sm:max-w-48 sm:justify-end">
@if ($environmentLabel)
<span class="inline-flex max-w-full items-center truncate rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-600 dark:bg-white/10 dark:text-gray-400">
{{ $environmentLabel }}
</span>
@endif
<span class="inline-flex max-w-full items-center truncate rounded-full border px-2 py-0.5 text-xs font-medium {{ $badgeClasses }}">
{{ $presentation->label }}
</span>
</div>
</div>
<p class="mt-2 text-xs leading-5 text-gray-500 dark:text-gray-400">
{{ $presentation->shortDescription }}
</p>
</div>
</div>
</button>
@endforeach
</div>
@endif
</div>
@endif
</x-filament-panels::page>