schema([ Section::make('Policy Details') ->schema([ TextEntry::make('display_name')->label('Policy'), TextEntry::make('policy_type')->label('Type'), TextEntry::make('platform'), TextEntry::make('external_id')->label('External ID'), TextEntry::make('last_synced_at')->dateTime()->label('Last synced'), TextEntry::make('created_at')->since(), ]) ->columns(2), // For Settings Catalog policies: Tabs with Settings table + JSON viewer Tabs::make('policy_content') ->tabs([ Tab::make('Settings') ->schema([ ViewEntry::make('settings_grouped') ->label('') ->view('filament.infolists.entries.settings-catalog-grouped') ->state(function (Policy $record) { $snapshot = static::latestSnapshot($record); $settings = $snapshot['payload']['settings'] ?? $snapshot['settings'] ?? []; if (empty($settings)) { return ['groups' => []]; } return app(PolicyNormalizer::class)->normalizeSettingsCatalogGrouped($settings); }) ->visible(fn (Policy $record) => $record->policy_type === 'settingsCatalogPolicy' && $record->versions()->exists() ), ViewEntry::make('settings_standard') ->label('') ->view('filament.infolists.entries.policy-settings-standard') ->state(function (Policy $record) { $snapshot = static::latestSnapshot($record); $normalizer = app(PolicyNormalizer::class); return $normalizer->normalize( $snapshot, $record->policy_type, $record->platform ); }) ->visible(fn (Policy $record) => $record->policy_type !== 'settingsCatalogPolicy' && $record->versions()->exists() ), TextEntry::make('no_settings_available') ->label('Settings') ->state('No policy snapshot available yet.') ->helperText('This policy has been inventoried but no configuration snapshot has been captured yet.') ->visible(fn (Policy $record) => ! $record->versions()->exists()), ]), Tab::make('JSON') ->schema([ ViewEntry::make('snapshot_json') ->view('filament.infolists.entries.snapshot-json') ->state(fn (Policy $record) => static::latestSnapshot($record)) ->columnSpanFull(), TextEntry::make('snapshot_size') ->label('Payload Size') ->state(fn (Policy $record) => strlen(json_encode(static::latestSnapshot($record) ?: []))) ->formatStateUsing(function ($state) { if ($state > 512000) { return ' Large payload ('.number_format($state / 1024, 0).' KB) - May impact performance '; } return number_format($state / 1024, 1).' KB'; }) ->html() ->visible(fn (Policy $record) => strlen(json_encode(static::latestSnapshot($record) ?: [])) > 512000), ]) ->visible(fn (Policy $record) => $record->versions()->exists()), ]) ->visible(function (Policy $record) { return str_contains(strtolower($record->policy_type ?? ''), 'settings') || str_contains(strtolower($record->policy_type ?? ''), 'catalog'); }), // For non-Settings Catalog policies: Simple sections without tabs Section::make('Settings') ->schema([ ViewEntry::make('settings') ->label('') ->view('filament.infolists.entries.normalized-settings') ->state(function (Policy $record) { $normalized = app(PolicyNormalizer::class)->normalize( static::latestSnapshot($record), $record->policy_type ?? '', $record->platform ); $normalized['context'] = 'policy'; $normalized['record_id'] = (string) $record->getKey(); return $normalized; }), ]) ->visible(function (Policy $record) { // Show simple settings section for non-Settings Catalog policies return ! str_contains(strtolower($record->policy_type ?? ''), 'settings') && ! str_contains(strtolower($record->policy_type ?? ''), 'catalog'); }), Section::make('Policy Snapshot (JSON)') ->schema([ ViewEntry::make('snapshot_json') ->view('filament.infolists.entries.snapshot-json') ->state(fn (Policy $record) => static::latestSnapshot($record)) ->columnSpanFull(), TextEntry::make('snapshot_size') ->label('Payload Size') ->state(fn (Policy $record) => strlen(json_encode(static::latestSnapshot($record) ?: []))) ->formatStateUsing(function ($state) { if ($state > 512000) { return ' Large payload ('.number_format($state / 1024, 0).' KB) - May impact performance '; } return number_format($state / 1024, 1).' KB'; }) ->html() ->visible(fn (Policy $record) => strlen(json_encode(static::latestSnapshot($record) ?: [])) > 512000), ]) ->collapsible() ->collapsed(fn (Policy $record) => strlen(json_encode(static::latestSnapshot($record) ?: [])) > 512000) ->description('Raw JSON configuration from Microsoft Graph API') ->visible(function (Policy $record) { // Show standalone JSON section only for non-Settings Catalog policies return ! str_contains(strtolower($record->policy_type ?? ''), 'settings') && ! str_contains(strtolower($record->policy_type ?? ''), 'catalog'); }), ]); } public static function table(Table $table): Table { return $table ->columns([ Tables\Columns\TextColumn::make('display_name') ->label('Policy') ->searchable(), Tables\Columns\TextColumn::make('policy_type') ->label('Type') ->badge() ->formatStateUsing(fn (?string $state, Policy $record) => static::typeMeta($record->policy_type)['label'] ?? $state), Tables\Columns\TextColumn::make('category') ->label('Category') ->badge() ->state(fn (Policy $record) => static::typeMeta($record->policy_type)['category'] ?? 'Unknown'), Tables\Columns\TextColumn::make('restore_mode') ->label('Restore') ->badge() ->state(fn (Policy $record) => static::typeMeta($record->policy_type)['restore'] ?? 'enabled'), Tables\Columns\TextColumn::make('platform') ->badge() ->sortable(), Tables\Columns\TextColumn::make('settings_status') ->label('Settings') ->badge() ->state(function (Policy $record) { $latest = $record->versions->first(); $snapshot = $latest?->snapshot ?? []; $hasSettings = is_array($snapshot) && ! empty($snapshot['settings']); return $hasSettings ? 'Available' : 'Missing'; }) ->color(function (Policy $record) { $latest = $record->versions->first(); $snapshot = $latest?->snapshot ?? []; $hasSettings = is_array($snapshot) && ! empty($snapshot['settings']); return $hasSettings ? 'success' : 'gray'; }), Tables\Columns\TextColumn::make('external_id') ->label('External ID') ->copyable() ->limit(32), Tables\Columns\TextColumn::make('last_synced_at') ->label('Last synced') ->dateTime() ->sortable(), Tables\Columns\TextColumn::make('created_at') ->dateTime() ->since() ->toggleable(isToggledHiddenByDefault: true), ]) ->filters([ Tables\Filters\SelectFilter::make('policy_type') ->options(function () { return collect(config('tenantpilot.supported_policy_types', [])) ->pluck('label', 'type') ->map(fn ($label, $type) => $label ?? $type) ->all(); }), Tables\Filters\SelectFilter::make('category') ->options(function () { return collect(config('tenantpilot.supported_policy_types', [])) ->pluck('category', 'category') ->filter() ->unique() ->sort() ->all(); }) ->query(function (Builder $query, array $data) { $category = $data['value'] ?? null; if (! $category) { return; } $types = collect(config('tenantpilot.supported_policy_types', [])) ->where('category', $category) ->pluck('type') ->all(); $query->whereIn('policy_type', $types); }), Tables\Filters\SelectFilter::make('platform') ->options(fn () => Policy::query()->distinct()->pluck('platform', 'platform')->filter()->all()), ]) ->actions([ Actions\ViewAction::make(), ]) ->bulkActions([]); } public static function getEloquentQuery(): Builder { $tenantId = Tenant::current()->getKey(); return parent::getEloquentQuery() ->when($tenantId, fn (Builder $query) => $query->where('tenant_id', $tenantId)) ->withCount('versions') ->with([ 'versions' => fn ($query) => $query->orderByDesc('captured_at')->limit(1), ]); } public static function getRelations(): array { return [ VersionsRelationManager::class, ]; } public static function getPages(): array { return [ 'index' => Pages\ListPolicies::route('/'), 'view' => Pages\ViewPolicy::route('/{record}'), ]; } private static function latestSnapshot(Policy $record): array { $snapshot = $record->relationLoaded('versions') ? $record->versions->first()?->snapshot : $record->versions()->orderByDesc('captured_at')->value('snapshot'); if (is_string($snapshot)) { $decoded = json_decode($snapshot, true); $snapshot = $decoded ?? []; } if (is_array($snapshot)) { return $snapshot; } return []; } /** * @return array{label:?string,category:?string,restore:?string,risk:?string}|array|array */ private static function typeMeta(?string $type): array { if ($type === null) { return []; } return collect(config('tenantpilot.supported_policy_types', [])) ->firstWhere('type', $type) ?? []; } }