## Summary - replace the baseline snapshot detail page with a structured summary-first rendering flow - add a presenter plus renderer registry with RBAC, compliance, and fallback renderers - add grouped policy-type browsing, fidelity and gap badges, and workspace authorization coverage - add Feature 130 spec, plan, contract, research, quickstart, and completed task artifacts ## Testing - focused Pest coverage was added for structured rendering, fallback behavior, degraded states, authorization, presenter logic, renderer resolution, and badge mapping - I did not rerun the full validation suite in this final PR step ## Notes - base branch: `dev` - feature branch: `130-structured-snapshot-rendering` Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #158
156 lines
6.6 KiB
PHP
156 lines
6.6 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 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('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 [];
|
|
}
|
|
}
|