From 65e10a2020e338167f6488b80d1956a60ae33e03 Mon Sep 17 00:00:00 2001 From: ahmido Date: Sat, 11 Apr 2026 12:32:10 +0000 Subject: [PATCH] Spec 190: tighten baseline compare matrix scanability (#222) ## Summary - tighten the baseline compare matrix working surface with active filter scope summaries and clearer visible-set disclosure - improve matrix scanability with a sticky subject column, calmer attention-first cell styling, and Filament form-based filter controls - replace the misleading perpetual refresh loading state with a passive auto-refresh note and add focused regression coverage ## Testing - `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/BaselineCompareMatrixPageTest.php` ## Notes - this PR only contains the Spec 190 implementation changes on `190-baseline-compare-matrix` - follow-up spec drafting for high-density operator mode was intentionally left out of this PR Co-authored-by: Ahmed Darrazi Reviewed-on: https://git.cloudarix.de/ahmido/TenantAtlas/pulls/222 --- .../Filament/Pages/BaselineCompareMatrix.php | 148 +++- .../pages/baseline-compare-matrix.blade.php | 765 +++++++++--------- .../Spec190BaselineCompareMatrixSmokeTest.php | 23 +- .../BaselineCompareMatrixPageTest.php | 37 +- specs/190-baseline-compare-matrix/tasks.md | 1 + 5 files changed, 597 insertions(+), 377 deletions(-) diff --git a/apps/platform/app/Filament/Pages/BaselineCompareMatrix.php b/apps/platform/app/Filament/Pages/BaselineCompareMatrix.php index dcdd0670..b6cc5a97 100644 --- a/apps/platform/app/Filament/Pages/BaselineCompareMatrix.php +++ b/apps/platform/app/Filament/Pages/BaselineCompareMatrix.php @@ -23,12 +23,19 @@ use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot; use Filament\Actions\Action; +use Filament\Forms\Components\CheckboxList; +use Filament\Forms\Components\Select; +use Filament\Forms\Concerns\InteractsWithForms; +use Filament\Forms\Contracts\HasForms; use Filament\Notifications\Notification; use Filament\Resources\Pages\Concerns\InteractsWithRecord; use Filament\Resources\Pages\Page; +use Filament\Schemas\Components\Grid; +use Filament\Schemas\Schema; -class BaselineCompareMatrix extends Page +class BaselineCompareMatrix extends Page implements HasForms { + use InteractsWithForms; use InteractsWithRecord; protected static bool $isDiscovered = false; @@ -83,6 +90,82 @@ public function mount(int|string $record): void $this->record = $this->resolveRecord($record); $this->hydrateFiltersFromRequest(); $this->refreshMatrix(); + $this->form->fill($this->filterFormState()); + } + + public function form(Schema $schema): Schema + { + return $schema + ->schema([ + Grid::make([ + 'default' => 1, + 'xl' => 2, + ]) + ->schema([ + Grid::make([ + 'default' => 1, + 'lg' => 5, + ]) + ->schema([ + CheckboxList::make('selectedPolicyTypes') + ->label('Policy types') + ->options(fn (): array => $this->matrixOptions('policyTypeOptions')) + ->helperText(fn (): ?string => $this->matrixOptions('policyTypeOptions') === [] + ? 'Policy type filters appear after a usable reference snapshot is available.' + : null) + ->extraFieldWrapperAttributes([ + 'data-testid' => 'matrix-policy-type-filter', + ]) + ->columns(1) + ->columnSpan([ + 'lg' => 2, + ]) + ->live(), + CheckboxList::make('selectedStates') + ->label('Technical states') + ->options(fn (): array => $this->matrixOptions('stateOptions')) + ->columnSpan([ + 'lg' => 2, + ]) + ->columns(1) + ->live(), + CheckboxList::make('selectedSeverities') + ->label('Severity') + ->options(fn (): array => $this->matrixOptions('severityOptions')) + ->columns(1) + ->live(), + ]) + ->columnSpan([ + 'xl' => 1, + ]), + Grid::make([ + 'default' => 1, + 'md' => 2, + 'xl' => 1, + ]) + ->schema([ + Select::make('tenantSort') + ->label('Tenant sort') + ->options(fn (): array => $this->matrixOptions('tenantSortOptions')) + ->default('tenant_name') + ->native(false) + ->live() + ->extraFieldWrapperAttributes(['data-testid' => 'matrix-tenant-sort']) + ->extraInputAttributes(['data-testid' => 'matrix-tenant-sort']), + Select::make('subjectSort') + ->label('Subject sort') + ->options(fn (): array => $this->matrixOptions('subjectSortOptions')) + ->default('deviation_breadth') + ->native(false) + ->live() + ->extraFieldWrapperAttributes(['data-testid' => 'matrix-subject-sort']) + ->extraInputAttributes(['data-testid' => 'matrix-subject-sort']), + ]) + ->columnSpan([ + 'xl' => 1, + ]), + ]), + ]); } protected function authorizeAccess(): void @@ -168,6 +251,11 @@ public function refreshMatrix(): void ]); } + public function pollMatrix(): void + { + $this->refreshMatrix(); + } + public function tenantCompareUrl(int $tenantId, ?string $subjectKey = null): ?string { $tenant = $this->tenant($tenantId); @@ -254,6 +342,40 @@ public function updatedFocusedSubjectKey(): void $this->refreshMatrix(); } + public function activeFilterCount(): int + { + return count($this->selectedPolicyTypes) + + count($this->selectedStates) + + count($this->selectedSeverities) + + ($this->focusedSubjectKey !== null ? 1 : 0); + } + + /** + * @return array + */ + public function activeFilterSummary(): array + { + $summary = []; + + if ($this->selectedPolicyTypes !== []) { + $summary['Policy types'] = count($this->selectedPolicyTypes); + } + + if ($this->selectedStates !== []) { + $summary['Technical states'] = count($this->selectedStates); + } + + if ($this->selectedSeverities !== []) { + $summary['Severity'] = count($this->selectedSeverities); + } + + if ($this->focusedSubjectKey !== null) { + $summary['Focused subject'] = $this->focusedSubjectKey; + } + + return $summary; + } + /** * @return array */ @@ -283,6 +405,30 @@ private function hydrateFiltersFromRequest(): void $this->focusedSubjectKey = is_string($subjectKey) && trim($subjectKey) !== '' ? trim($subjectKey) : null; } + /** + * @return array + */ + private function filterFormState(): array + { + return [ + 'selectedPolicyTypes' => $this->selectedPolicyTypes, + 'selectedStates' => $this->selectedStates, + 'selectedSeverities' => $this->selectedSeverities, + 'tenantSort' => $this->tenantSort, + 'subjectSort' => $this->subjectSort, + ]; + } + + /** + * @return array + */ + private function matrixOptions(string $key): array + { + $options = $this->matrix[$key] ?? null; + + return is_array($options) ? $options : []; + } + /** * @return list */ diff --git a/apps/platform/resources/views/filament/pages/baseline-compare-matrix.blade.php b/apps/platform/resources/views/filament/pages/baseline-compare-matrix.blade.php index 2ff210e4..66f202c4 100644 --- a/apps/platform/resources/views/filament/pages/baseline-compare-matrix.blade.php +++ b/apps/platform/resources/views/filament/pages/baseline-compare-matrix.blade.php @@ -1,6 +1,6 @@ @if (($hasActiveRuns ?? false) === true) -
+
@endif @php @@ -19,6 +19,9 @@ $currentFilters = is_array($currentFilters ?? null) ? $currentFilters : []; $referenceReady = ($reference['referenceState'] ?? null) === 'ready'; $matrixSourceNavigation = is_array($navigationContext ?? null) ? $navigationContext : null; + $activeFilterCount = $this->activeFilterCount(); + $activeFilterSummary = $this->activeFilterSummary(); + $hiddenAssignedTenantCount = max(0, (int) ($reference['assignedTenantCount'] ?? 0) - (int) ($reference['visibleTenantCount'] ?? 0)); $stateBadge = static fn (mixed $value) => \App\Support\Badges\BadgeCatalog::spec(\App\Support\Badges\BadgeDomain::BaselineCompareMatrixState, $value); $freshnessBadge = static fn (mixed $value) => \App\Support\Badges\BadgeCatalog::spec(\App\Support\Badges\BadgeDomain::BaselineCompareMatrixFreshness, $value); @@ -55,6 +58,12 @@ Snapshot #{{ (int) $reference['referenceSnapshotId'] }} @endif + + @if ($hiddenAssignedTenantCount > 0) + + {{ $hiddenAssignedTenantCount }} hidden by access scope + + @endif
@@ -75,6 +84,12 @@ Reference reason: {{ \Illuminate\Support\Str::headline(str_replace('.', ' ', (string) $reference['referenceReasonCode'])) }}

@endif + + @if ($hiddenAssignedTenantCount > 0) +

+ Showing only the visible assigned set for your current access scope. Hidden tenants are excluded from summaries, rows, and drilldowns. +

+ @endif
@@ -115,424 +130,434 @@ Narrow the matrix by policy type, technical state, severity, or one focused subject. Only the visible tenant set contributes to the rendered counts, rows, and drilldowns. -
-
-
-
Policy types
+
+
+
+
+
Current matrix scope
+

+ @if ($activeFilterCount === 0) + No narrowing filters are active. Showing every visible subject and tenant in the current baseline scope. + @else + {{ $activeFilterCount }} active {{ \Illuminate\Support\Str::plural('filter', $activeFilterCount) }} are narrowing the matrix before you scan drift and follow-up links. + @endif +

+
- @if ($policyTypeOptions !== []) -
- @foreach ($policyTypeOptions as $value => $label) - - @endforeach -
- @else -

Policy type filters appear after a usable reference snapshot is available.

- @endif -
+
+ + @if ($activeFilterCount === 0) + All visible results + @else + {{ $activeFilterCount }} active {{ \Illuminate\Support\Str::plural('filter', $activeFilterCount) }} + @endif + -
-
Technical states
- -
- @foreach ($stateOptions as $value => $label) - @php - $spec = $stateBadge($value); - @endphp - - - @endforeach + @if ($hiddenAssignedTenantCount > 0) + + Visible-set only + + @endif
-
-
Severity
- -
- @foreach ($severityOptions as $value => $label) - @php - $spec = $severityBadge($value); - @endphp - - + @if ($activeFilterSummary !== []) +
+ @foreach ($activeFilterSummary as $label => $value) + + {{ $label }}: {{ $value }} + @endforeach
-
+ @endif
-
-
- +
+ {{ $this->form }} +
- -
+
+
+
+ Focused subject -
-
Focused subject
- - @if (filled($currentFilters['subject_key'] ?? null)) -
+ @if (filled($currentFilters['subject_key'] ?? null)) {{ $currentFilters['subject_key'] }} - + Clear subject focus - -
- @else -

Focus a single row from the matrix when you want a subject-first drilldown.

- @endif + + @else + None set yet. Use Focus subject from a row when you want a subject-first drilldown. + @endif +
-
- - Clear all filters - +
+ @if ($activeFilterCount > 0) + + Clear all filters + + @else + + No filter reset needed + + @endif
+ +
-
- -
- @foreach ($stateLegend as $item) - - {{ $item['label'] }} +
+ @if (($hasActiveRuns ?? false) === true) +
+
+ + Auto-refresh every 5 seconds while compare runs are queued or running. - @endforeach -
- - - -
- @foreach ($freshnessLegend as $item) - - {{ $item['label'] }} - - @endforeach -
-
- - -
- @foreach ($trustLegend as $item) - - {{ $item['label'] }} - - @endforeach -
-
-
- - @if ($emptyState !== null) - -
-
-

{{ $emptyState['title'] ?? 'Nothing to show' }}

-

{{ $emptyState['body'] ?? 'Adjust the current inputs and try again.' }}

-
- @else - - - Tenant-level freshness, trust, and breadth stay visible before you scan the subject-by-tenant body. - + @endif -
- @foreach ($tenantSummaries as $tenantSummary) - @php - $freshnessSpec = $freshnessBadge($tenantSummary['freshnessState'] ?? null); - $trustSpec = $trustBadge($tenantSummary['trustLevel'] ?? null); - $tenantSeveritySpec = filled($tenantSummary['maxSeverity'] ?? null) ? $severityBadge($tenantSummary['maxSeverity']) : null; - $tenantCompareUrl = $this->tenantCompareUrl((int) $tenantSummary['tenantId']); - $tenantRunUrl = filled($tenantSummary['compareRunId'] ?? null) - ? $this->runUrl((int) $tenantSummary['compareRunId'], (int) $tenantSummary['tenantId']) - : null; - @endphp +
+ +
+
+
+
State legend
-
-
-
-

{{ $tenantSummary['tenantName'] }}

-
- - {{ $freshnessSpec->label }} - - - {{ $trustSpec->label }} - - @if ($tenantSeveritySpec) - - {{ $tenantSeveritySpec->label }} - + @foreach ($stateLegend as $item) + + {{ $item['label'] }} + + @endforeach +
+
+ +
+
+
Freshness legend
+ + @foreach ($freshnessLegend as $item) + + {{ $item['label'] }} + + @endforeach +
+
+ +
+
+
Trust legend
+ + @foreach ($trustLegend as $item) + + {{ $item['label'] }} + + @endforeach +
+
+
+ + + @if ($emptyState !== null) + +
+
+

{{ $emptyState['title'] ?? 'Nothing to show' }}

+

{{ $emptyState['body'] ?? 'Adjust the current inputs and try again.' }}

+
+
+
+ @else + + + Tenant-level freshness, trust, and breadth stay visible before you scan the subject-by-tenant body. + + +
+ @foreach ($tenantSummaries as $tenantSummary) + @php + $freshnessSpec = $freshnessBadge($tenantSummary['freshnessState'] ?? null); + $trustSpec = $trustBadge($tenantSummary['trustLevel'] ?? null); + $tenantSeveritySpec = filled($tenantSummary['maxSeverity'] ?? null) ? $severityBadge($tenantSummary['maxSeverity']) : null; + $tenantCompareUrl = $this->tenantCompareUrl((int) $tenantSummary['tenantId']); + $tenantRunUrl = filled($tenantSummary['compareRunId'] ?? null) + ? $this->runUrl((int) $tenantSummary['compareRunId'], (int) $tenantSummary['tenantId']) + : null; + @endphp + +
+
+
+

{{ $tenantSummary['tenantName'] }}

+
+ + {{ $freshnessSpec->label }} + + + {{ $trustSpec->label }} + + @if ($tenantSeveritySpec) + + {{ $tenantSeveritySpec->label }} + + @endif +
+
+ +
+ @if (filled($tenantSummary['lastComparedAt'] ?? null)) + Compared {{ \Illuminate\Support\Carbon::parse($tenantSummary['lastComparedAt'])->diffForHumans() }} + @else + No completed compare yet + @endif +
+
+ +
+
+
Aligned
+
{{ (int) ($tenantSummary['matchedCount'] ?? 0) }}
+
+
+
Drift
+
{{ (int) ($tenantSummary['differingCount'] ?? 0) }}
+
+
+
Missing
+
{{ (int) ($tenantSummary['missingCount'] ?? 0) }}
+
+
+
Ambiguous / not compared
+
+ {{ (int) ($tenantSummary['ambiguousCount'] ?? 0) + (int) ($tenantSummary['notComparedCount'] ?? 0) }} +
+
+
+ +
+ @if ($tenantCompareUrl) + + Open tenant compare + + @endif + + @if ($tenantRunUrl) + + Open latest run + @endif
- -
- @if (filled($tenantSummary['lastComparedAt'] ?? null)) - Compared {{ \Illuminate\Support\Carbon::parse($tenantSummary['lastComparedAt'])->diffForHumans() }} - @else - No completed compare yet - @endif -
-
- -
-
-
Aligned
-
{{ (int) ($tenantSummary['matchedCount'] ?? 0) }}
-
-
-
Drift
-
{{ (int) ($tenantSummary['differingCount'] ?? 0) }}
-
-
-
Missing
-
{{ (int) ($tenantSummary['missingCount'] ?? 0) }}
-
-
-
Ambiguous / not compared
-
- {{ (int) ($tenantSummary['ambiguousCount'] ?? 0) + (int) ($tenantSummary['notComparedCount'] ?? 0) }} -
-
-
- -
- @if ($tenantCompareUrl) - - Open tenant compare - - @endif - - @if ($tenantRunUrl) - - Open latest run - - @endif -
+ @endforeach
- @endforeach -
- + - - - Row click is intentionally disabled. Use the explicit subject, tenant, finding, and run links inside the matrix cells. - + + + Row click is intentionally disabled. The subject column stays pinned while you scan across visible tenants. + -
-
- - - - +
+
+
- Baseline subject -
+ + + - @foreach ($tenantSummaries as $tenantSummary) - @php - $freshnessSpec = $freshnessBadge($tenantSummary['freshnessState'] ?? null); - @endphp - - @endforeach - - - - - @foreach ($rows as $row) - @php - $subject = is_array($row['subject'] ?? null) ? $row['subject'] : []; - $cells = is_array($row['cells'] ?? null) ? $row['cells'] : []; - $subjectSeveritySpec = filled($subject['maxSeverity'] ?? null) ? $severityBadge($subject['maxSeverity']) : null; - $subjectTrustSpec = $trustBadge($subject['trustLevel'] ?? null); - @endphp - - - + @endforeach + + - @foreach ($cells as $cell) + + @foreach ($rows as $row) @php - $cellStateSpec = $stateBadge($cell['state'] ?? null); - $cellTrustSpec = $trustBadge($cell['trustLevel'] ?? null); - $cellSeveritySpec = filled($cell['severity'] ?? null) ? $severityBadge($cell['severity']) : null; - $subjectKey = $subject['subjectKey'] ?? ($cell['subjectKey'] ?? null); - $tenantId = (int) ($cell['tenantId'] ?? 0); - $tenantCompareUrl = $tenantId > 0 ? $this->tenantCompareUrl($tenantId, $subjectKey) : null; - $cellFindingUrl = ($tenantId > 0 && filled($cell['findingId'] ?? null)) - ? $this->findingUrl($tenantId, (int) $cell['findingId'], $subjectKey) - : null; - $cellRunUrl = filled($cell['compareRunId'] ?? null) - ? $this->runUrl((int) $cell['compareRunId'], $tenantId, $subjectKey) - : null; + $subject = is_array($row['subject'] ?? null) ? $row['subject'] : []; + $cells = is_array($row['cells'] ?? null) ? $row['cells'] : []; + $subjectSeveritySpec = filled($subject['maxSeverity'] ?? null) ? $severityBadge($subject['maxSeverity']) : null; + $subjectTrustSpec = $trustBadge($subject['trustLevel'] ?? null); + $rowKey = (string) ($subject['subjectKey'] ?? 'subject-'.$loop->index); + $rowSurfaceClasses = $loop->even + ? 'bg-gray-50/70 dark:bg-gray-950/20' + : 'bg-white dark:bg-gray-900/60'; @endphp - + - @if (filled($cell['lastComparedAt'] ?? null)) -
- Compared {{ \Illuminate\Support\Carbon::parse($cell['lastComparedAt'])->diffForHumans() }} + @foreach ($cells as $cell) + @php + $cellStateSpec = $stateBadge($cell['state'] ?? null); + $cellTrustSpec = $trustBadge($cell['trustLevel'] ?? null); + $cellSeveritySpec = filled($cell['severity'] ?? null) ? $severityBadge($cell['severity']) : null; + $cellState = (string) ($cell['state'] ?? ''); + $cellSeverity = is_string($cell['severity'] ?? null) ? (string) $cell['severity'] : null; + $cellNeedsAttention = in_array($cellState, ['differ', 'missing', 'ambiguous'], true) + || in_array($cellSeverity, ['critical', 'high'], true); + $cellNeedsRefresh = in_array($cellState, ['stale_result', 'not_compared'], true); + $cellLooksHealthy = $cellState === 'match' && $cellNeedsAttention === false && $cellNeedsRefresh === false; + $cellSurfaceClasses = $cellNeedsAttention + ? 'border-danger-300 bg-danger-50/80 ring-1 ring-danger-200/70 dark:border-danger-900/70 dark:bg-danger-950/15 dark:ring-danger-900/40' + : ($cellNeedsRefresh + ? 'border-warning-300 bg-warning-50/80 ring-1 ring-warning-200/70 dark:border-warning-900/70 dark:bg-warning-950/15 dark:ring-warning-900/40' + : ($cellLooksHealthy + ? 'border-success-200 bg-success-50/70 dark:border-success-900/60 dark:bg-success-950/10' + : 'border-gray-200 bg-gray-50 dark:border-gray-800 dark:bg-gray-950/40')); + $cellPriorityLabel = $cellNeedsAttention + ? 'Needs attention' + : ($cellNeedsRefresh ? 'Refresh recommended' : ($cellLooksHealthy ? 'Aligned' : 'Review')); + $cellPriorityClasses = $cellNeedsAttention + ? 'bg-danger-100 text-danger-700 dark:bg-danger-950/40 dark:text-danger-300' + : ($cellNeedsRefresh + ? 'bg-warning-100 text-warning-700 dark:bg-warning-950/40 dark:text-warning-300' + : 'bg-success-100 text-success-700 dark:bg-success-950/40 dark:text-success-300'); + $subjectKey = $subject['subjectKey'] ?? ($cell['subjectKey'] ?? null); + $tenantId = (int) ($cell['tenantId'] ?? 0); + $tenantCompareUrl = $tenantId > 0 ? $this->tenantCompareUrl($tenantId, $subjectKey) : null; + $cellFindingUrl = ($tenantId > 0 && filled($cell['findingId'] ?? null)) + ? $this->findingUrl($tenantId, (int) $cell['findingId'], $subjectKey) + : null; + $cellRunUrl = filled($cell['compareRunId'] ?? null) + ? $this->runUrl((int) $cell['compareRunId'], $tenantId, $subjectKey) + : null; + @endphp + +
+ @if (filled($cell['lastComparedAt'] ?? null)) +
+ Compared {{ \Illuminate\Support\Carbon::parse($cell['lastComparedAt'])->diffForHumans() }} +
+ @endif + + @if (($cell['policyTypeCovered'] ?? true) === false) +
Policy type coverage was not proven in the latest compare run.
+ @endif + + +
+ @if ($cellFindingUrl) + + Open finding + + @elseif ($tenantCompareUrl) + + Open tenant compare + + @endif + + @if ($cellRunUrl) + + Open run + + @endif +
+ + + @endforeach + @endforeach - - @endforeach - -
+ Baseline subject + -
-
{{ $tenantSummary['tenantName'] }}
-
- - {{ $freshnessSpec->label }} - -
-
-
-
-
-
- {{ $subject['displayName'] ?? $subject['subjectKey'] ?? 'Subject' }} -
-
- {{ $subject['policyType'] ?? 'Unknown policy type' }} -
- @if (filled($subject['baselineExternalId'] ?? null)) -
- Reference ID: {{ $subject['baselineExternalId'] }} + @foreach ($tenantSummaries as $tenantSummary) + @php + $freshnessSpec = $freshnessBadge($tenantSummary['freshnessState'] ?? null); + @endphp +
+
+
{{ $tenantSummary['tenantName'] }}
+
+ + {{ $freshnessSpec->label }} +
- @endif -
- -
- - Drift breadth {{ (int) ($subject['deviationBreadth'] ?? 0) }} - - - Missing {{ (int) ($subject['missingBreadth'] ?? 0) }} - - - Ambiguous {{ (int) ($subject['ambiguousBreadth'] ?? 0) }} - - - {{ $subjectTrustSpec->label }} - - @if ($subjectSeveritySpec) - - {{ $subjectSeveritySpec->label }} - - @endif -
- - @if (filled($subject['subjectKey'] ?? null)) -
- - Focus subject -
- @endif - - +
-
-
- - {{ $cellStateSpec->label }} - - - {{ $cellTrustSpec->label }} - - @if ($cellSeveritySpec) - - {{ $cellSeveritySpec->label }} +
+
+
+
+ {{ $subject['displayName'] ?? $subject['subjectKey'] ?? 'Subject' }} +
+
+ {{ $subject['policyType'] ?? 'Unknown policy type' }} +
+ @if (filled($subject['baselineExternalId'] ?? null)) +
+ Reference ID: {{ $subject['baselineExternalId'] }} +
+ @endif +
+ +
+ + Drift breadth {{ (int) ($subject['deviationBreadth'] ?? 0) }} - @endif -
+ + Missing {{ (int) ($subject['missingBreadth'] ?? 0) }} + + + Ambiguous {{ (int) ($subject['ambiguousBreadth'] ?? 0) }} + + + {{ $subjectTrustSpec->label }} + + @if ($subjectSeveritySpec) + + {{ $subjectSeveritySpec->label }} + + @endif +
-
- @if (filled($cell['reasonCode'] ?? null)) -
- Reason: {{ \Illuminate\Support\Str::headline(str_replace('_', ' ', (string) $cell['reasonCode'])) }} + @if (filled($subject['subjectKey'] ?? null)) +
+ + Focus subject +
@endif +
+
+
+
+
+ + {{ $cellStateSpec->label }} + + @if ($cellSeveritySpec) + + {{ $cellSeveritySpec->label }} + + @endif +
+ + + {{ $cellPriorityLabel }} +
- @endif - @if (($cell['policyTypeCovered'] ?? true) === false) -
Policy type coverage was not proven in the latest compare run.
- @endif -
+
+ + {{ $cellTrustSpec->label }} + +
-
- @if ($cellFindingUrl) - - Open finding - - @elseif ($tenantCompareUrl) - - Open tenant compare - - @endif +
+ @if (filled($cell['reasonCode'] ?? null)) +
+ Reason: {{ \Illuminate\Support\Str::headline(str_replace('_', ' ', (string) $cell['reasonCode'])) }} +
+ @endif - @if ($cellRunUrl) - - Open run - - @endif -
-
-
-
-
-
- @endif + + +
+
+
+ @endif +
+
diff --git a/apps/platform/tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php b/apps/platform/tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php index a0f1d22a..0fcd9edf 100644 --- a/apps/platform/tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php +++ b/apps/platform/tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php @@ -48,21 +48,34 @@ ->assertNoJavaScriptErrors() ->waitForText('Visible-set baseline') ->assertSee('Reference overview') + ->assertSee('No narrowing filters are active') ->assertSee('Subject-by-tenant matrix') ->assertSee('WiFi Corp Profile') ->assertSee('Windows Compliance') ->assertSee('Open finding'); $page->script(<<<'JS' -const input = document.querySelector('[data-testid="matrix-filter-state-differ"]'); -if (input instanceof HTMLInputElement) { - input.click(); - input.dispatchEvent(new Event('input', { bubbles: true })); - input.dispatchEvent(new Event('change', { bubbles: true })); +const input = Array.from(document.querySelectorAll('input[type="checkbox"]')).find((element) => { + if (element.getAttribute('aria-label') === 'Drift detected') { + return true; + } + + const label = element.closest('label'); + + return label instanceof HTMLLabelElement && label.innerText.includes('Drift detected'); +}); + +if (! (input instanceof HTMLInputElement)) { + throw new Error('Drift detected checkbox not found.'); } + +input.click(); +input.dispatchEvent(new Event('input', { bubbles: true })); +input.dispatchEvent(new Event('change', { bubbles: true })); JS); $page + ->wait(1) ->waitForText('Open finding') ->assertDontSee('Windows Compliance') ->click('Open finding') diff --git a/apps/platform/tests/Feature/Filament/BaselineCompareMatrixPageTest.php b/apps/platform/tests/Feature/Filament/BaselineCompareMatrixPageTest.php index d1a4d5c1..844fc6ee 100644 --- a/apps/platform/tests/Feature/Filament/BaselineCompareMatrixPageTest.php +++ b/apps/platform/tests/Feature/Filament/BaselineCompareMatrixPageTest.php @@ -39,14 +39,21 @@ ->assertOk() ->assertSee('Visible-set baseline') ->assertSee('Reference overview') + ->assertSee('fi-fo-checkbox-list', false) + ->assertSee('fi-fo-select', false) ->assertSee('State legend') ->assertSee('Tenant summaries') ->assertSee('Subject-by-tenant matrix') + ->assertSee('No narrowing filters are active. Showing every visible subject and tenant in the current baseline scope.') + ->assertSee('1 hidden by access scope') ->assertSee('WiFi Corp Profile') ->assertSee((string) $fixture['visibleTenant']->name) ->assertSee((string) $fixture['visibleTenantTwo']->name) + ->assertSee('Needs attention') ->assertSee('Open finding') - ->assertSee('Open tenant compare'); + ->assertSee('Open tenant compare') + ->assertSee('data-testid="matrix-active-filters"', false) + ->assertSee('sticky left-0', false); }); it('keeps query-backed filters and subject focus on the matrix route and drilldown links', function (): void { @@ -82,6 +89,9 @@ ]) ->actingAs($fixture['user']) ->test(BaselineCompareMatrix::class, ['record' => $fixture['profile']->getKey()]) + ->assertSee('4 active filters') + ->assertSee('Policy types: 1') + ->assertSee('Focused subject: wifi-corp-profile') ->assertSee('Clear subject focus') ->assertDontSee('Windows Compliance'); @@ -139,6 +149,31 @@ ->assertSee('No visible assigned tenants'); }); +it('renders a passive auto-refresh note instead of a perpetual loading state while compare runs remain active', function (): void { + $fixture = $this->makeBaselineCompareMatrixFixture(); + + $this->makeBaselineCompareMatrixRun( + $fixture['visibleTenant'], + $fixture['profile'], + $fixture['snapshot'], + attributes: [ + 'status' => \App\Support\OperationRunStatus::Queued->value, + 'outcome' => \App\Support\OperationRunOutcome::Pending->value, + 'completed_at' => null, + 'started_at' => now(), + ], + ); + + $session = $this->setAdminWorkspaceContext($fixture['user'], $fixture['workspace']); + + $this->withSession($session) + ->get(BaselineProfileResource::compareMatrixUrl($fixture['profile'])) + ->assertOk() + ->assertSee('Auto-refresh every 5 seconds while compare runs are queued or running.') + ->assertSee('wire:poll.5s="pollMatrix"', false) + ->assertDontSee('Refreshing matrix'); +}); + it('renders an empty state when no rows match the current filters', function (): void { $fixture = $this->makeBaselineCompareMatrixFixture(); diff --git a/specs/190-baseline-compare-matrix/tasks.md b/specs/190-baseline-compare-matrix/tasks.md index 667f307f..53864398 100644 --- a/specs/190-baseline-compare-matrix/tasks.md +++ b/specs/190-baseline-compare-matrix/tasks.md @@ -139,6 +139,7 @@ ## Phase 7: Polish & Cross-Cutting Concerns - [X] T036 [P] Add no-ad-hoc-badge and no-diagnostic-warning guard coverage for matrix state surfaces in `apps/platform/tests/Feature/Guards/NoAdHocStatusBadgesTest.php` and `apps/platform/tests/Feature/Guards/NoDiagnosticWarningBadgesTest.php` - [X] T037 [P] Add browser smoke coverage for matrix render, one filter interaction, and one drilldown or compare affordance in `apps/platform/tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php` - [X] T038 [P] Review `Verb + Object`, `visible-set only`, and `simulation only` copy in `apps/platform/app/Filament/Pages/BaselineCompareMatrix.php`, `apps/platform/app/Filament/Resources/BaselineProfileResource/Pages/ViewBaselineProfile.php`, and `apps/platform/resources/views/filament/pages/baseline-compare-matrix.blade.php` +- [X] T041 [P] Tighten matrix scanability with active-filter scope summaries, visible-set scope disclosure, non-blocking refresh feedback, sticky subject-column treatment, and focused UI regression coverage in `apps/platform/app/Filament/Pages/BaselineCompareMatrix.php`, `apps/platform/resources/views/filament/pages/baseline-compare-matrix.blade.php`, `apps/platform/tests/Feature/Filament/BaselineCompareMatrixPageTest.php`, and `apps/platform/tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php` - [X] T039 [P] Run formatting with `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` using `specs/190-baseline-compare-matrix/quickstart.md` - [ ] T040 Run the focused verification pack from `specs/190-baseline-compare-matrix/quickstart.md` against `apps/platform/tests/Unit/Badges/BaselineCompareMatrixBadgesTest.php`, `apps/platform/tests/Feature/Baselines/BaselineCompareMatrixBuilderTest.php`, `apps/platform/tests/Feature/Baselines/BaselineCompareMatrixCompareAllActionTest.php`, `apps/platform/tests/Feature/Baselines/BaselineCompareStatsTest.php`, `apps/platform/tests/Feature/Baselines/BaselineCompareFindingsTest.php`, `apps/platform/tests/Feature/Baselines/BaselineComparePerformanceGuardTest.php`, `apps/platform/tests/Feature/Filament/BaselineCompareMatrixPageTest.php`, `apps/platform/tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php`, `apps/platform/tests/Feature/Rbac/BaselineCompareMatrixAuthorizationTest.php`, `apps/platform/tests/Feature/Guards/ActionSurfaceContractTest.php`, `apps/platform/tests/Feature/Guards/NoAdHocStatusBadgesTest.php`, `apps/platform/tests/Feature/Guards/NoDiagnosticWarningBadgesTest.php`, and `apps/platform/tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php`