## Summary - add the baseline compare landing experience for the environment dashboard productization flow - expand the environment dashboard overview and summary-building logic to support richer baseline comparison states and assessments - update the supporting Blade templates for the new compare and overview presentation - add English and German translations for the baseline compare surface - include the Spec 330 planning and task artifacts alongside the implementation ## Tests - touched browser, feature, and unit coverage for the new baseline compare flow - updated test files include `Spec330EnvironmentDashboardBaselineCompareSmokeTest`, `BaselineCompareLandingWhyNoFindingsTest`, `Spec330EnvironmentDashboardBaselineCompareProductizationTest`, `HeaderContextBarTest`, and `ManagedEnvironmentModelTest` - no additional test run was performed as part of this commit/push/PR workflow Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #392
265 lines
18 KiB
PHP
265 lines
18 KiB
PHP
@php
|
|
$tenantDashboardStatusBadgeClasses = static fn (?string $tone): string => 'inline-flex items-center rounded-md border px-2 py-0.5 text-left text-xs font-medium leading-5 whitespace-normal break-words '.match ($tone) {
|
|
'danger' => 'border-danger-200 bg-danger-50 text-danger-700 dark:border-danger-800 dark:bg-danger-500/10 dark:text-danger-300',
|
|
'success' => 'border-success-200 bg-success-50 text-success-700 dark:border-success-800 dark:bg-success-500/10 dark:text-success-300',
|
|
'warning' => 'border-warning-200 bg-warning-50 text-warning-700 dark:border-warning-800 dark:bg-warning-500/10 dark:text-warning-300',
|
|
'info' => 'border-info-200 bg-info-50 text-info-700 dark:border-info-800 dark:bg-info-500/10 dark:text-info-300',
|
|
'primary' => 'border-primary-200 bg-primary-50 text-primary-700 dark:border-primary-800 dark:bg-primary-500/10 dark:text-primary-300',
|
|
default => 'border-gray-200 bg-gray-50 text-gray-700 dark:border-white/10 dark:bg-white/5 dark:text-gray-200',
|
|
};
|
|
@endphp
|
|
|
|
<div
|
|
@if ($pollingInterval)
|
|
wire:poll.{{ $pollingInterval }}
|
|
@endif
|
|
data-testid="tenant-dashboard-overview"
|
|
class="grid w-full min-w-0 gap-6 xl:grid-cols-12"
|
|
style="grid-column: 1 / -1;"
|
|
>
|
|
<!-- Left Column (Main) -->
|
|
<div data-testid="tenant-dashboard-overview-main" class="flex w-full min-w-0 flex-col gap-6 xl:col-span-8">
|
|
<x-filament::section>
|
|
<div data-testid="tenant-dashboard-readiness-decision" class="grid gap-5 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-start">
|
|
<div class="min-w-0 space-y-4">
|
|
<div class="flex flex-wrap items-center gap-3">
|
|
<h2 class="text-base font-semibold text-gray-950 dark:text-white">
|
|
{{ $readinessDecision['question'] ?? 'Is this environment ready, blocked, stale, or requiring review?' }}
|
|
</h2>
|
|
<span data-testid="tenant-dashboard-status-badge" class="{{ $tenantDashboardStatusBadgeClasses($readinessDecision['tone'] ?? 'gray') }}">
|
|
{{ $readinessDecision['status'] ?? ($posture['status'] ?? __('localization.dashboard.overview.status_unavailable')) }}
|
|
</span>
|
|
</div>
|
|
|
|
<dl class="grid gap-3 md:grid-cols-3">
|
|
<div class="rounded-xl border border-gray-200 bg-gray-50/80 p-4 dark:border-white/10 dark:bg-white/5">
|
|
<dt class="text-xs font-semibold text-gray-500 dark:text-gray-400">{{ $readinessDecision['statusLabel'] ?? 'Status' }}</dt>
|
|
<dd class="mt-1 text-sm font-medium text-gray-950 dark:text-white">{{ $readinessDecision['status'] ?? ($posture['status'] ?? __('localization.dashboard.overview.status_unavailable')) }}</dd>
|
|
</div>
|
|
|
|
<div class="rounded-xl border border-gray-200 bg-gray-50/80 p-4 dark:border-white/10 dark:bg-white/5">
|
|
<dt class="text-xs font-semibold text-gray-500 dark:text-gray-400">{{ $readinessDecision['reasonLabel'] ?? 'Reason' }}</dt>
|
|
<dd class="mt-1 text-sm text-gray-700 dark:text-gray-200">{{ $readinessDecision['reason'] ?? ($posture['headline'] ?? __('localization.dashboard.overview.tenant_context_unavailable_headline')) }}</dd>
|
|
</div>
|
|
|
|
<div class="rounded-xl border border-gray-200 bg-gray-50/80 p-4 dark:border-white/10 dark:bg-white/5">
|
|
<dt class="text-xs font-semibold text-gray-500 dark:text-gray-400">{{ $readinessDecision['impactLabel'] ?? 'Impact' }}</dt>
|
|
<dd class="mt-1 text-sm text-gray-700 dark:text-gray-200">{{ $readinessDecision['impact'] ?? ($posture['summary'] ?? __('localization.dashboard.overview.tenant_context_unavailable_summary')) }}</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
|
|
<div data-testid="tenant-dashboard-primary-next-action" class="min-w-0 rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5 lg:w-64">
|
|
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">{{ $readinessDecision['nextActionLabel'] ?? 'Next action' }}</div>
|
|
<div class="mt-2 text-sm font-semibold text-gray-950 dark:text-white">{{ $readinessDecision['actionLabel'] ?? 'Review readiness proof' }}</div>
|
|
|
|
@if (filled($readinessDecision['actionUrl'] ?? null))
|
|
<x-filament::button class="mt-4" tag="a" :href="$readinessDecision['actionUrl']" size="sm">
|
|
{{ $readinessDecision['actionLabel'] ?? 'Review readiness proof' }}
|
|
</x-filament::button>
|
|
@else
|
|
<x-filament::button class="mt-4" size="sm" color="gray" disabled>
|
|
{{ $readinessDecision['actionLabel'] ?? 'Review readiness proof' }}
|
|
</x-filament::button>
|
|
@endif
|
|
|
|
@if (filled($readinessDecision['helperText'] ?? null))
|
|
<p class="mt-3 text-xs leading-5 text-gray-500 dark:text-gray-400">{{ $readinessDecision['helperText'] }}</p>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</x-filament::section>
|
|
|
|
<x-filament::section heading="Readiness dimensions">
|
|
<div data-testid="tenant-dashboard-readiness-dimensions" class="grid gap-3 md:grid-cols-2">
|
|
@forelse ($readinessDimensions as $dimension)
|
|
<div data-testid="tenant-dashboard-readiness-dimension" data-readiness-key="{{ $dimension['key'] ?? '' }}" class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
|
|
<div class="flex flex-col gap-2">
|
|
<div class="min-w-0">
|
|
<div class="text-sm font-semibold text-gray-950 dark:text-white">{{ $dimension['title'] ?? 'Readiness signal' }}</div>
|
|
</div>
|
|
<div>
|
|
<span data-testid="tenant-dashboard-status-badge" class="{{ $tenantDashboardStatusBadgeClasses($dimension['tone'] ?? 'gray') }}">
|
|
{{ $dimension['status'] ?? __('localization.dashboard.overview.status_unavailable') }}
|
|
</span>
|
|
</div>
|
|
<p class="text-xs leading-5 text-gray-500 dark:text-gray-400">{{ $dimension['description'] ?? '' }}</p>
|
|
</div>
|
|
</div>
|
|
@empty
|
|
<div class="rounded-xl border border-gray-200 bg-white p-4 text-sm text-gray-600 shadow-sm dark:border-white/10 dark:bg-white/5 dark:text-gray-300">
|
|
Readiness dimensions are unavailable until an environment context is selected.
|
|
</div>
|
|
@endforelse
|
|
</div>
|
|
</x-filament::section>
|
|
|
|
<!-- Recommended Actions -->
|
|
<x-filament::section :heading="__('localization.dashboard.overview.section_recommended_actions')">
|
|
<x-slot name="description">
|
|
Recommended next actions are derived from repo-backed blockers and proof gaps.
|
|
</x-slot>
|
|
|
|
@if ($recommendedActions === [])
|
|
<div data-testid="tenant-dashboard-recommended-actions-empty" class="rounded-xl border border-success-200 bg-success-50/80 p-5 dark:border-success-800 dark:bg-success-500/10">
|
|
<div class="text-sm font-semibold text-success-700 dark:text-success-300">{{ __('localization.dashboard.overview.empty_recommended_actions_headline') }}</div>
|
|
<p class="mt-2 text-sm leading-6 text-success-700/90 dark:text-success-200/90">
|
|
{{ __('localization.dashboard.overview.empty_recommended_actions_summary') }}
|
|
</p>
|
|
</div>
|
|
@else
|
|
<div data-testid="tenant-dashboard-recommended-actions" class="grid min-w-0 gap-4">
|
|
@foreach (array_slice($recommendedActions, 0, 3) as $index => $action)
|
|
<div data-testid="tenant-dashboard-recommended-action" data-action-key="{{ $action['key'] }}" class="flex min-w-0 items-start gap-4 rounded-xl border border-gray-200 bg-white p-5 shadow-sm dark:border-white/10 dark:bg-white/5 max-sm:flex-col max-sm:items-stretch">
|
|
<div data-testid="tenant-dashboard-recommended-action-priority" class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-gray-200 bg-gray-50 text-sm font-semibold text-gray-700 dark:border-white/10 dark:bg-white/5 dark:text-gray-200">
|
|
{{ $index + 1 }}
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center gap-2">
|
|
@if (filled($action['icon'] ?? null))
|
|
<x-filament::icon
|
|
data-testid="tenant-dashboard-recommended-action-icon"
|
|
data-action-key="{{ $action['key'] }}"
|
|
data-icon="{{ $action['icon'] }}"
|
|
:icon="$action['icon']"
|
|
class="h-4 w-4 shrink-0 text-gray-400 dark:text-gray-500"
|
|
/>
|
|
@endif
|
|
|
|
<h3 class="min-w-0 text-sm font-semibold text-gray-950 dark:text-white">{{ $action['title'] }}</h3>
|
|
</div>
|
|
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
|
<span class="font-medium text-gray-700 dark:text-gray-300">Reason:</span> {{ $action['reason'] }}
|
|
</p>
|
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-500">
|
|
<span class="font-medium text-gray-700 dark:text-gray-300">Impact:</span> {{ $action['impact'] }}
|
|
</p>
|
|
</div>
|
|
@if (filled($action['actionUrl'] ?? null))
|
|
<div class="shrink-0 max-sm:ml-0 sm:ml-4">
|
|
<x-filament::button data-testid="tenant-dashboard-secondary-action" :href="$action['actionUrl']" tag="a" color="gray" size="sm">
|
|
{{ $action['actionLabel'] ?? 'Review' }}
|
|
</x-filament::button>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
</x-filament::section>
|
|
|
|
<x-filament::section heading="Supporting signals">
|
|
<x-slot name="description">
|
|
Additional readiness signals used to explain the current recommendation.
|
|
</x-slot>
|
|
|
|
<div data-testid="tenant-dashboard-supporting-signals" class="rounded-xl border border-gray-200 bg-white shadow-sm dark:border-white/10 dark:bg-white/5">
|
|
@if ($supportingSignals === [])
|
|
<div class="p-4 text-sm text-gray-600 dark:text-gray-300">
|
|
Supporting signals are unavailable until an environment context is selected.
|
|
</div>
|
|
@else
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-gray-100 text-sm dark:divide-white/10">
|
|
<thead class="bg-gray-50/80 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:bg-white/5 dark:text-gray-400">
|
|
<tr>
|
|
<th scope="col" class="px-4 py-2 text-left">Signal</th>
|
|
<th scope="col" class="px-4 py-2 text-left">State</th>
|
|
<th scope="col" class="px-4 py-2 text-right">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-100 dark:divide-white/10">
|
|
@foreach ($supportingSignals as $signal)
|
|
<tr data-testid="tenant-dashboard-supporting-signal" data-signal-key="{{ $signal['key'] ?? '' }}">
|
|
<th scope="row" class="px-4 py-3 text-left align-middle font-semibold text-gray-950 dark:text-white">
|
|
{{ $signal['label'] ?? 'Readiness signal' }}
|
|
</th>
|
|
<td class="px-4 py-3 align-middle">
|
|
<span data-testid="tenant-dashboard-status-badge" class="{{ $tenantDashboardStatusBadgeClasses($signal['tone'] ?? 'gray') }}">
|
|
{{ $signal['value'] ?? __('localization.dashboard.overview.status_unavailable') }}
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-3 text-right align-middle">
|
|
@if (filled($signal['actionLabel'] ?? null))
|
|
@if (filled($signal['actionUrl'] ?? null) && ! ($signal['actionDisabled'] ?? false))
|
|
<x-filament::button
|
|
data-testid="tenant-dashboard-supporting-signal-action"
|
|
tag="a"
|
|
:href="$signal['actionUrl']"
|
|
size="sm"
|
|
color="gray"
|
|
>
|
|
{{ $signal['actionLabel'] }}
|
|
</x-filament::button>
|
|
@else
|
|
<x-filament::button
|
|
data-testid="tenant-dashboard-supporting-signal-action"
|
|
size="sm"
|
|
color="gray"
|
|
disabled
|
|
>
|
|
{{ $signal['actionLabel'] }}
|
|
</x-filament::button>
|
|
@endif
|
|
@endif
|
|
</td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</x-filament::section>
|
|
|
|
<x-filament::section>
|
|
<details data-testid="tenant-dashboard-diagnostics" class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
|
|
<summary class="cursor-pointer text-sm font-semibold text-gray-950 dark:text-white">
|
|
{{ $diagnosticsDisclosure['label'] ?? 'Diagnostics - Collapsed' }}
|
|
</summary>
|
|
<p class="mt-3 text-sm leading-6 text-gray-600 dark:text-gray-300">
|
|
{{ $diagnosticsDisclosure['summary'] ?? 'Support diagnostics stay closed by default and require the existing diagnostics capability.' }}
|
|
</p>
|
|
</details>
|
|
</x-filament::section>
|
|
</div>
|
|
|
|
<!-- Right Column (Aside) -->
|
|
<div data-testid="tenant-dashboard-overview-aside" class="flex w-full min-w-0 flex-col gap-6 xl:col-span-4">
|
|
<x-filament::section heading="Readiness proof">
|
|
<div data-testid="tenant-dashboard-readiness-proof-panel" class="flex flex-col gap-3">
|
|
@foreach ($readinessProofPanel as $proof)
|
|
<div data-testid="tenant-dashboard-readiness-proof-item" data-proof-key="{{ $proof['key'] ?? '' }}" class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
|
|
<div class="flex flex-col gap-2">
|
|
<div>
|
|
<div class="text-sm font-semibold text-gray-950 dark:text-white">{{ $proof['label'] ?? 'Proof path' }}</div>
|
|
</div>
|
|
<div>
|
|
<span data-testid="tenant-dashboard-status-badge" class="{{ $tenantDashboardStatusBadgeClasses($proof['tone'] ?? 'gray') }}">
|
|
{{ $proof['value'] ?? __('localization.dashboard.overview.status_unavailable') }}
|
|
</span>
|
|
</div>
|
|
<p class="text-xs leading-5 text-gray-500 dark:text-gray-400">{{ $proof['description'] ?? '' }}</p>
|
|
</div>
|
|
|
|
@if (filled($proof['actionLabel'] ?? null))
|
|
<div class="mt-3">
|
|
@if (filled($proof['actionUrl'] ?? null))
|
|
<x-filament::button tag="a" :href="$proof['actionUrl']" size="sm" color="gray">
|
|
{{ $proof['actionLabel'] }}
|
|
</x-filament::button>
|
|
@else
|
|
<x-filament::button size="sm" color="gray" disabled>
|
|
{{ $proof['actionLabel'] }}
|
|
</x-filament::button>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endforeach
|
|
|
|
</div>
|
|
</x-filament::section>
|
|
</div>
|
|
</div>
|