TenantAtlas/app/Filament/Resources/BaselineSnapshotResource/Pages/ViewBaselineSnapshot.php
ahmido b15d1950b4 feat: add cross-resource navigation cohesion (#160)
## Summary
- add a shared cross-resource navigation layer with canonical navigation context and related-context rendering
- wire findings, policy versions, baseline snapshots, backup sets, and canonical operations surfaces into consistent drill-down flows
- extend focused Pest coverage for canonical operations links, related navigation, and tenant-context preservation

## Testing
- focused Pest coverage for spec 131 was added and the task list marks the implementation verification and Pint steps as completed

## Follow-up
- manual QA checklist item `T036` in `specs/131-cross-resource-navigation/tasks.md` is still open and should be completed during review

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #160
2026-03-10 16:08:14 +00:00

182 lines
8.0 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Filament\Resources\BaselineSnapshotResource\Pages;
use App\Filament\Resources\BaselineSnapshotResource;
use App\Models\BaselineSnapshot;
use App\Models\User;
use App\Models\Workspace;
use App\Services\Auth\WorkspaceCapabilityResolver;
use App\Services\Baselines\SnapshotRendering\BaselineSnapshotPresenter;
use App\Support\Auth\Capabilities;
use App\Support\Badges\BadgeDomain;
use App\Support\Badges\BadgeRenderer;
use App\Support\Navigation\CrossResourceNavigationMatrix;
use App\Support\Navigation\RelatedContextEntry;
use App\Support\Navigation\RelatedNavigationResolver;
use Filament\Actions\Action;
use Filament\Infolists\Components\TextEntry;
use Filament\Infolists\Components\ViewEntry;
use Filament\Resources\Pages\ViewRecord;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
class ViewBaselineSnapshot extends ViewRecord
{
protected static string $resource = BaselineSnapshotResource::class;
/**
* @var array<string, mixed>
*/
public array $presentedSnapshot = [];
public function mount(int|string $record): void
{
parent::mount($record);
$snapshot = $this->getRecord();
if ($snapshot instanceof BaselineSnapshot) {
$this->presentedSnapshot = app(BaselineSnapshotPresenter::class)
->present($snapshot)
->toArray();
}
}
protected function authorizeAccess(): void
{
$user = auth()->user();
$snapshot = $this->getRecord();
$workspace = BaselineSnapshotResource::resolveWorkspace();
if (! $user instanceof User || ! $snapshot instanceof BaselineSnapshot || ! $workspace instanceof Workspace) {
abort(404);
}
if ((int) $snapshot->workspace_id !== (int) $workspace->getKey()) {
abort(404);
}
/** @var WorkspaceCapabilityResolver $resolver */
$resolver = app(WorkspaceCapabilityResolver::class);
if (! $resolver->isMember($user, $workspace)) {
abort(404);
}
if (! $resolver->can($user, $workspace, Capabilities::WORKSPACE_BASELINES_VIEW)) {
abort(403);
}
}
public function infolist(Schema $schema): Schema
{
return $schema
->schema([
Section::make('Snapshot')
->schema([
TextEntry::make('snapshot_id')
->label('Snapshot')
->state(function (): string {
$snapshotId = data_get($this->presentedSnapshot, 'snapshot.snapshotId');
return is_numeric($snapshotId) ? '#'.$snapshotId : '—';
}),
TextEntry::make('baseline_profile_name')
->label('Baseline')
->state(fn (): string => data_get($this->presentedSnapshot, 'snapshot.baselineProfileName', '—'))
->placeholder('—'),
TextEntry::make('captured_at')
->label('Captured')
->state(fn (): ?string => data_get($this->presentedSnapshot, 'snapshot.capturedAt'))
->dateTime()
->placeholder('—'),
TextEntry::make('state_label')
->label('State')
->badge()
->state(fn (): string => data_get($this->presentedSnapshot, 'snapshot.stateLabel', 'Complete'))
->color(fn (string $state): string => $state === 'Captured with gaps' ? 'warning' : 'success'),
TextEntry::make('overall_fidelity')
->label('Overall fidelity')
->badge()
->state(fn (): ?string => data_get($this->presentedSnapshot, 'snapshot.overallFidelity'))
->formatStateUsing(BadgeRenderer::label(BadgeDomain::BaselineSnapshotFidelity))
->color(BadgeRenderer::color(BadgeDomain::BaselineSnapshotFidelity))
->icon(BadgeRenderer::icon(BadgeDomain::BaselineSnapshotFidelity)),
TextEntry::make('fidelity_summary')
->label('Evidence mix')
->state(fn (): string => data_get($this->presentedSnapshot, 'snapshot.fidelitySummary', 'Content 0, Meta 0')),
TextEntry::make('overall_gap_count')
->label('Evidence gaps')
->state(fn (): int => (int) data_get($this->presentedSnapshot, 'snapshot.overallGapCount', 0)),
TextEntry::make('snapshot_identity_hash')
->label('Identity hash')
->state(fn (): ?string => data_get($this->presentedSnapshot, 'snapshot.snapshotIdentityHash'))
->copyable()
->placeholder('—')
->columnSpanFull(),
])
->columns(2)
->columnSpanFull(),
Section::make('Coverage summary')
->schema([
ViewEntry::make('summary_rows')
->label('')
->view('filament.infolists.entries.baseline-snapshot-summary-table')
->state(fn (): array => data_get($this->presentedSnapshot, 'summaryRows', []))
->columnSpanFull(),
])
->columnSpanFull(),
Section::make('Related context')
->schema([
ViewEntry::make('related_context')
->label('')
->view('filament.infolists.entries.related-context')
->state(fn (): array => app(RelatedNavigationResolver::class)
->detailEntries(CrossResourceNavigationMatrix::SOURCE_BASELINE_SNAPSHOT, $this->getRecord()))
->columnSpanFull(),
])
->columnSpanFull(),
Section::make('Captured policy types')
->schema([
ViewEntry::make('groups')
->label('')
->view('filament.infolists.entries.baseline-snapshot-groups')
->state(fn (): array => data_get($this->presentedSnapshot, 'groups', []))
->columnSpanFull(),
])
->columnSpanFull(),
Section::make('Technical detail')
->schema([
ViewEntry::make('technical_detail')
->label('')
->view('filament.infolists.entries.baseline-snapshot-technical-detail')
->state(fn (): array => data_get($this->presentedSnapshot, 'technicalDetail', []))
->columnSpanFull(),
])
->collapsible()
->collapsed()
->columnSpanFull(),
]);
}
protected function getHeaderActions(): array
{
return [
Action::make('primary_related')
->label(fn (): string => $this->primaryRelatedEntry()?->actionLabel ?? 'Open related record')
->url(fn (): ?string => $this->primaryRelatedEntry()?->targetUrl)
->hidden(fn (): bool => ! ($this->primaryRelatedEntry()?->isAvailable() ?? false))
->color('gray'),
];
}
private function primaryRelatedEntry(): ?RelatedContextEntry
{
return app(RelatedNavigationResolver::class)
->headerEntries(CrossResourceNavigationMatrix::SOURCE_BASELINE_SNAPSHOT, $this->getRecord())[0] ?? null;
}
}