getId() === 'admin') { return false; } return parent::shouldRegisterNavigation(); } public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration { return ActionSurfaceDeclaration::forResource(ActionSurfaceProfile::CrudListAndView) ->exempt(ActionSurfaceSlot::ListHeader, 'Directory groups list intentionally has no header actions; sync is started from the sync-runs surface.') ->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ClickableRow->value) ->exempt(ActionSurfaceSlot::ListRowMoreMenu, 'No secondary row actions are provided on this read-only list.') ->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'Bulk actions are intentionally omitted for directory groups.') ->exempt(ActionSurfaceSlot::ListEmptyState, 'Empty-state CTA is intentionally omitted; groups appear after sync.') ->exempt(ActionSurfaceSlot::DetailHeader, 'No canonical related destination exists for directory groups yet.'); } public static function form(Schema $schema): Schema { return $schema; } public static function infolist(Schema $schema): Schema { return $schema ->schema([ ViewEntry::make('enterprise_detail') ->label('') ->view('filament.infolists.entries.enterprise-detail.layout') ->state(fn (EntraGroup $record): array => static::enterpriseDetailPage($record)->toArray()) ->columnSpanFull(), ]); } public static function table(Table $table): Table { return $table ->defaultSort('display_name') ->paginated(TablePaginationProfiles::resource()) ->persistFiltersInSession() ->persistSearchInSession() ->persistSortInSession() ->recordUrl(static fn (EntraGroup $record): ?string => static::canView($record) ? static::scopedUrl('view', ['record' => $record], static::panelTenantContext()) : null) ->columns([ Tables\Columns\TextColumn::make('display_name') ->label('Name') ->searchable() ->sortable(), Tables\Columns\TextColumn::make('entra_id') ->label('Entra ID') ->copyable() ->toggleable(isToggledHiddenByDefault: true), Tables\Columns\TextColumn::make('type') ->label('Type') ->badge() ->state(fn (EntraGroup $record): string => static::groupTypeLabel(static::groupType($record))) ->color(fn (EntraGroup $record): string => static::groupTypeColor(static::groupType($record))), Tables\Columns\TextColumn::make('last_seen_at') ->label('Last seen') ->since(), ]) ->filters([ SelectFilter::make('stale') ->label('Stale') ->options([ '1' => 'Stale', '0' => 'Fresh', ]) ->query(function (Builder $query, array $data): Builder { $value = $data['value'] ?? null; if ($value === null || $value === '') { return $query; } $stalenessDays = (int) config('directory_groups.staleness_days', 30); $cutoff = now('UTC')->subDays(max(1, $stalenessDays)); if ((string) $value === '1') { return $query->where(function (Builder $q) use ($cutoff): void { $q->whereNull('last_seen_at') ->orWhere('last_seen_at', '<', $cutoff); }); } return $query->where('last_seen_at', '>=', $cutoff); }), SelectFilter::make('group_type') ->label('Type') ->options([ 'security' => 'Security', 'microsoft365' => 'Microsoft 365', 'mail' => 'Mail-enabled', 'unknown' => 'Unknown', ]) ->query(function (Builder $query, array $data): Builder { $value = (string) ($data['value'] ?? ''); if ($value === '') { return $query; } return match ($value) { 'microsoft365' => $query->whereJsonContains('group_types', 'Unified'), 'security' => $query ->where('security_enabled', true) ->where(function (Builder $q): void { $q->whereNull('group_types') ->orWhereJsonDoesntContain('group_types', 'Unified'); }), 'mail' => $query ->where('mail_enabled', true) ->where(function (Builder $q): void { $q->whereNull('group_types') ->orWhereJsonDoesntContain('group_types', 'Unified'); }), 'unknown' => $query ->where(function (Builder $q): void { $q->whereNull('group_types') ->orWhereJsonDoesntContain('group_types', 'Unified'); }) ->where('security_enabled', false) ->where('mail_enabled', false), default => $query, }; }), ]) ->actions([]) ->bulkActions([]) ->emptyStateHeading('No groups cached yet') ->emptyStateDescription('Sync groups for the current tenant to browse directory data here.') ->emptyStateIcon('heroicon-o-user-group'); } public static function getEloquentQuery(): Builder { $tenant = static::panelTenantContext(); return parent::getEloquentQuery() ->when( $tenant instanceof Tenant, fn (Builder $query): Builder => $query->where('tenant_id', (int) $tenant->getKey()), fn (Builder $query): Builder => $query->whereRaw('1 = 0'), ) ->latest('id'); } public static function getGlobalSearchResultUrl(Model $record): string { $tenant = $record instanceof EntraGroup && $record->tenant instanceof Tenant ? $record->tenant : static::panelTenantContext(); return static::scopedUrl('view', ['record' => $record], $tenant); } public static function getPages(): array { return [ 'index' => Pages\ListEntraGroups::route('/'), 'view' => Pages\ViewEntraGroup::route('/{record}'), ]; } public static function panelTenantContext(): ?Tenant { if (Filament::getCurrentPanel()?->getId() === 'admin') { $tenant = app(OperateHubShell::class)->activeEntitledTenant(request()); return $tenant instanceof Tenant ? $tenant : null; } $tenant = Tenant::current(); return $tenant instanceof Tenant ? $tenant : null; } /** * @param array $parameters */ public static function scopedUrl( string $page = 'index', array $parameters = [], ?Tenant $tenant = null, ?string $panel = null, ): string { $panelId = $panel ?? Filament::getCurrentOrDefaultPanel()?->getId() ?? 'admin'; return static::getUrl($page, $parameters, panel: $panelId, tenant: $tenant); } private static function groupType(EntraGroup $record): string { $groupTypes = $record->group_types; if (is_array($groupTypes) && in_array('Unified', $groupTypes, true)) { return 'microsoft365'; } if ($record->security_enabled) { return 'security'; } if ($record->mail_enabled) { return 'mail'; } return 'unknown'; } private static function groupTypeLabel(string $type): string { return match ($type) { 'microsoft365' => 'Microsoft 365', 'security' => 'Security', 'mail' => 'Mail-enabled', default => 'Unknown', }; } private static function groupTypeColor(string $type): string { return match ($type) { 'microsoft365' => 'info', 'security' => 'success', 'mail' => 'warning', default => 'gray', }; } private static function enterpriseDetailPage(EntraGroup $record): EnterpriseDetailPageData { $factory = new EnterpriseDetailSectionFactory; $groupType = static::groupType($record); $groupTypeLabel = static::groupTypeLabel($groupType); $groupTypeBadge = $factory->statusBadge($groupTypeLabel, static::groupTypeColor($groupType)); $securityBadge = $factory->statusBadge( BadgeRenderer::label(BadgeDomain::BooleanEnabled)($record->security_enabled), BadgeRenderer::color(BadgeDomain::BooleanEnabled)($record->security_enabled), BadgeRenderer::icon(BadgeDomain::BooleanEnabled)($record->security_enabled), BadgeRenderer::iconColor(BadgeDomain::BooleanEnabled)($record->security_enabled), ); $mailBadge = $factory->statusBadge( BadgeRenderer::label(BadgeDomain::BooleanEnabled)($record->mail_enabled), BadgeRenderer::color(BadgeDomain::BooleanEnabled)($record->mail_enabled), BadgeRenderer::icon(BadgeDomain::BooleanEnabled)($record->mail_enabled), BadgeRenderer::iconColor(BadgeDomain::BooleanEnabled)($record->mail_enabled), ); $technicalPayload = [ 'entra_id' => $record->entra_id, 'group_types' => is_array($record->group_types) ? $record->group_types : [], ]; return EnterpriseDetailBuilder::make('entra_group', 'tenant') ->header(new SummaryHeaderData( title: (string) $record->display_name, subtitle: 'Directory group #'.$record->getKey(), statusBadges: [$groupTypeBadge, $securityBadge, $mailBadge], keyFacts: [ $factory->keyFact('Type', $groupTypeLabel, badge: $groupTypeBadge), $factory->keyFact('Last seen', static::formatDetailTimestamp($record->last_seen_at)), $factory->keyFact('Security enabled', $record->security_enabled, badge: $securityBadge), $factory->keyFact('Mail enabled', $record->mail_enabled, badge: $mailBadge), ], descriptionHint: 'Group identity and classification stay ahead of provider-oriented metadata.', )) ->addSection( $factory->factsSection( id: 'classification_overview', kind: 'core_details', title: 'Classification overview', items: [ $factory->keyFact('Type', $groupTypeLabel, badge: $groupTypeBadge), $factory->keyFact('Security enabled', $record->security_enabled, badge: $securityBadge), $factory->keyFact('Mail enabled', $record->mail_enabled, badge: $mailBadge), $factory->keyFact('Last seen', static::formatDetailTimestamp($record->last_seen_at)), ], ), $factory->viewSection( id: 'related_context', kind: 'related_context', title: 'Related context', view: 'filament.infolists.entries.related-context', viewData: ['entries' => []], emptyState: $factory->emptyState('No related context is available for this record.'), ), ) ->addSupportingCard( $factory->supportingFactsCard( kind: 'summary', title: 'Directory identity', items: [ $factory->keyFact('Display name', $record->display_name), $factory->keyFact('Entra ID', $record->entra_id), ], ), $factory->supportingFactsCard( kind: 'timestamps', title: 'Freshness', items: [ $factory->keyFact('Last seen', static::formatDetailTimestamp($record->last_seen_at)), $factory->keyFact('Cached group types', count($technicalPayload['group_types'])), ], ), ) ->addTechnicalSection( $factory->technicalDetail( title: 'Technical detail', entries: [ $factory->keyFact('Entra ID', $record->entra_id), $factory->keyFact('Cached group types', count($technicalPayload['group_types'])), ], description: 'Provider identifiers and raw group-type arrays stay secondary to group identity and classification.', view: 'filament.infolists.entries.snapshot-json', viewData: ['payload' => $technicalPayload], ), ) ->build(); } private static function formatDetailTimestamp(mixed $value): string { if (! $value instanceof Carbon) { return '—'; } return $value->toDayDateTimeString(); } }