getId() !== 'admin') { return false; } return parent::shouldRegisterNavigation(); } public static function canViewAny(): bool { $user = auth()->user(); if (! $user instanceof User) { return false; } $workspace = self::resolveWorkspace(); if (! $workspace instanceof Workspace) { return false; } $resolver = app(\App\Services\Auth\WorkspaceCapabilityResolver::class); return $resolver->isMember($user, $workspace) && $resolver->can($user, $workspace, Capabilities::WORKSPACE_BASELINES_VIEW); } public static function canCreate(): bool { return self::hasManageCapability(); } public static function canEdit(Model $record): bool { return self::hasManageCapability(); } public static function canDelete(Model $record): bool { return self::hasManageCapability(); } public static function canView(Model $record): bool { return self::canViewAny(); } public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration { return ActionSurfaceDeclaration::forResource(ActionSurfaceProfile::CrudListAndView) ->satisfy(ActionSurfaceSlot::ListHeader, 'Header action: Create baseline profile (capability-gated).') ->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ViewAction->value) ->satisfy(ActionSurfaceSlot::ListRowMoreMenu, 'Secondary row actions (edit, archive) under "More".') ->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'No bulk mutations for baseline profiles in v1.') ->satisfy(ActionSurfaceSlot::ListEmptyState, 'List defines empty-state create CTA.') ->satisfy(ActionSurfaceSlot::DetailHeader, 'View page provides capture + edit actions.'); } public static function getEloquentQuery(): Builder { $workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request()); return parent::getEloquentQuery() ->with(['activeSnapshot', 'createdByUser']) ->when( $workspaceId !== null, fn (Builder $query): Builder => $query->where('workspace_id', (int) $workspaceId), ) ->when( $workspaceId === null, fn (Builder $query): Builder => $query->whereRaw('1 = 0'), ); } public static function form(Schema $schema): Schema { return $schema ->schema([ Section::make('Profile') ->schema([ TextInput::make('name') ->required() ->maxLength(255) ->rule(fn (?BaselineProfile $record): Unique => Rule::unique('baseline_profiles', 'name') ->where('workspace_id', $record?->workspace_id ?? app(WorkspaceContext::class)->currentWorkspaceId(request())) ->ignore($record)) ->helperText('A descriptive name for this baseline profile.'), Textarea::make('description') ->rows(3) ->maxLength(1000) ->helperText('Explain the purpose and scope of this baseline.'), ]), Section::make('Controls') ->schema([ Select::make('status') ->required() ->options(fn (?BaselineProfile $record): array => self::statusOptionsForRecord($record)) ->default(BaselineProfileStatus::Draft->value) ->native(false) ->disabled(fn (?BaselineProfile $record): bool => $record?->status === BaselineProfileStatus::Archived) ->helperText(fn (?BaselineProfile $record): string => match ($record?->status) { BaselineProfileStatus::Archived => 'Archived baselines cannot be reactivated.', BaselineProfileStatus::Active => 'Changing status to Archived is permanent.', default => 'Only active baselines are enforced during compliance checks.', }), Select::make('capture_mode') ->label('Capture mode') ->required() ->options(BaselineCaptureMode::selectOptions()) ->default(BaselineCaptureMode::Opportunistic->value) ->native(false) ->disabled(fn (?BaselineProfile $record): bool => $record?->status === BaselineProfileStatus::Archived) ->disableOptionWhen(function (string $value): bool { if ($value !== BaselineCaptureMode::FullContent->value) { return false; } return ! app(BaselineFullContentRolloutGate::class)->enabled(); }) ->helperText(fn (): string => app(BaselineFullContentRolloutGate::class)->enabled() ? 'Full content capture enables deep drift detection by capturing policy evidence on demand.' : 'Full content capture is currently disabled by rollout configuration.'), TextInput::make('version_label') ->label('Version label') ->maxLength(50) ->placeholder('e.g. v2.1 — February rollout') ->helperText('Optional label to identify this version.'), Select::make('scope_jsonb.policy_types') ->label('Policy types') ->multiple() ->options(self::policyTypeOptions()) ->helperText('Leave empty to include all supported policy types (excluding foundations).') ->native(false), Select::make('scope_jsonb.foundation_types') ->label('Foundations') ->multiple() ->options(self::foundationTypeOptions()) ->helperText('Leave empty to exclude foundations. Select foundations to include them.') ->native(false), Placeholder::make('metadata') ->label('Last modified') ->content(fn (?BaselineProfile $record): string => $record?->updated_at ? $record->updated_at->diffForHumans() : '—') ->visible(fn (?BaselineProfile $record): bool => $record !== null), ]) ->columns(2), ]); } public static function infolist(Schema $schema): Schema { return $schema ->schema([ Section::make('Profile') ->schema([ TextEntry::make('name'), TextEntry::make('status') ->badge() ->formatStateUsing(BadgeRenderer::label(BadgeDomain::BaselineProfileStatus)) ->color(BadgeRenderer::color(BadgeDomain::BaselineProfileStatus)) ->icon(BadgeRenderer::icon(BadgeDomain::BaselineProfileStatus)), TextEntry::make('capture_mode') ->label('Capture mode') ->badge() ->formatStateUsing(function (mixed $state): string { if ($state instanceof BaselineCaptureMode) { return $state->label(); } $parsed = is_string($state) ? BaselineCaptureMode::tryFrom($state) : null; return $parsed?->label() ?? (is_string($state) ? $state : '—'); }) ->color(function (mixed $state): string { $mode = $state instanceof BaselineCaptureMode ? $state : (is_string($state) ? BaselineCaptureMode::tryFrom($state) : null); return match ($mode) { BaselineCaptureMode::FullContent => 'success', BaselineCaptureMode::Opportunistic => 'warning', BaselineCaptureMode::MetaOnly => 'gray', default => 'gray', }; }), TextEntry::make('version_label') ->label('Version') ->placeholder('—'), TextEntry::make('description') ->placeholder('No description') ->columnSpanFull(), ]) ->columns(2) ->columnSpanFull(), Section::make('Scope') ->schema([ TextEntry::make('scope_jsonb.policy_types') ->label('Policy types') ->badge() ->formatStateUsing(function (string $state): string { $options = self::policyTypeOptions(); return $options[$state] ?? $state; }) ->placeholder('All supported policy types (excluding foundations)'), TextEntry::make('scope_jsonb.foundation_types') ->label('Foundations') ->badge() ->formatStateUsing(function (string $state): string { $options = self::foundationTypeOptions(); return $options[$state] ?? $state; }) ->placeholder('None'), ]) ->columnSpanFull(), Section::make('Metadata') ->schema([ TextEntry::make('createdByUser.name') ->label('Created by') ->placeholder('—'), TextEntry::make('activeSnapshot.captured_at') ->label('Last snapshot') ->dateTime() ->placeholder('No snapshot yet'), TextEntry::make('created_at') ->dateTime(), TextEntry::make('updated_at') ->dateTime(), ]) ->columns(2) ->columnSpanFull(), ]); } public static function table(Table $table): Table { $workspace = self::resolveWorkspace(); return $table ->defaultSort('name') ->paginated(\App\Support\Filament\TablePaginationProfiles::resource()) ->columns([ TextColumn::make('name') ->searchable() ->sortable(), TextColumn::make('status') ->badge() ->formatStateUsing(BadgeRenderer::label(BadgeDomain::BaselineProfileStatus)) ->color(BadgeRenderer::color(BadgeDomain::BaselineProfileStatus)) ->icon(BadgeRenderer::icon(BadgeDomain::BaselineProfileStatus)) ->sortable(), TextColumn::make('capture_mode') ->label('Capture mode') ->badge() ->formatStateUsing(function (mixed $state): string { if ($state instanceof BaselineCaptureMode) { return $state->label(); } $parsed = is_string($state) ? BaselineCaptureMode::tryFrom($state) : null; return $parsed?->label() ?? (is_string($state) ? $state : '—'); }) ->color(function (mixed $state): string { $mode = $state instanceof BaselineCaptureMode ? $state : (is_string($state) ? BaselineCaptureMode::tryFrom($state) : null); return match ($mode) { BaselineCaptureMode::FullContent => 'success', BaselineCaptureMode::Opportunistic => 'warning', BaselineCaptureMode::MetaOnly => 'gray', default => 'gray', }; }) ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('version_label') ->label('Version') ->placeholder('—'), TextColumn::make('activeSnapshot.captured_at') ->label('Last snapshot') ->dateTime() ->placeholder('No snapshot'), TextColumn::make('created_at') ->dateTime() ->sortable() ->toggleable(isToggledHiddenByDefault: true), ]) ->actions([ Action::make('view') ->label('View') ->url(fn (BaselineProfile $record): string => static::getUrl('view', ['record' => $record])) ->icon('heroicon-o-eye'), ActionGroup::make([ Action::make('edit') ->label('Edit') ->url(fn (BaselineProfile $record): string => static::getUrl('edit', ['record' => $record])) ->icon('heroicon-o-pencil-square') ->visible(fn (): bool => self::hasManageCapability()), self::archiveTableAction($workspace), ])->label('More'), ]) ->bulkActions([ BulkActionGroup::make([])->label('More'), ]) ->emptyStateHeading('No baseline profiles') ->emptyStateDescription('Create a baseline profile to define what "good" looks like for your tenants.') ->emptyStateActions([ Action::make('create') ->label('Create baseline profile') ->url(fn (): string => static::getUrl('create')) ->icon('heroicon-o-plus') ->visible(fn (): bool => self::hasManageCapability()), ]); } public static function getRelations(): array { return [ BaselineProfileResource\RelationManagers\BaselineTenantAssignmentsRelationManager::class, ]; } public static function getPages(): array { return [ 'index' => Pages\ListBaselineProfiles::route('/'), 'create' => Pages\CreateBaselineProfile::route('/create'), 'view' => Pages\ViewBaselineProfile::route('/{record}'), 'edit' => Pages\EditBaselineProfile::route('/{record}/edit'), ]; } /** * @return array */ public static function policyTypeOptions(): array { return collect(InventoryPolicyTypeMeta::supported()) ->filter(fn (array $row): bool => filled($row['type'] ?? null)) ->mapWithKeys(fn (array $row): array => [ (string) $row['type'] => (string) ($row['label'] ?? $row['type']), ]) ->sort() ->all(); } /** * @return array */ public static function foundationTypeOptions(): array { return collect(InventoryPolicyTypeMeta::foundations()) ->filter(fn (array $row): bool => filled($row['type'] ?? null)) ->mapWithKeys(fn (array $row): array => [ (string) $row['type'] => (string) ($row['label'] ?? $row['type']), ]) ->sort() ->all(); } /** * @param array $metadata */ public static function audit(BaselineProfile $record, AuditActionId $actionId, array $metadata): void { $workspace = $record->workspace; if ($workspace === null) { return; } $actor = auth()->user(); app(WorkspaceAuditLogger::class)->log( workspace: $workspace, action: $actionId->value, context: ['metadata' => $metadata], actor: $actor instanceof User ? $actor : null, resourceType: 'baseline_profile', resourceId: (string) $record->getKey(), ); } /** * Status options scoped to valid transitions from the current record state. * * @return array */ private static function statusOptionsForRecord(?BaselineProfile $record): array { if ($record === null) { return [BaselineProfileStatus::Draft->value => BaselineProfileStatus::Draft->label()]; } $currentStatus = $record->status instanceof BaselineProfileStatus ? $record->status : (BaselineProfileStatus::tryFrom((string) $record->status) ?? BaselineProfileStatus::Draft); return $currentStatus->selectOptions(); } private static function resolveWorkspace(): ?Workspace { $workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request()); if ($workspaceId === null) { return null; } return Workspace::query()->whereKey($workspaceId)->first(); } private static function hasManageCapability(): bool { $user = auth()->user(); $workspace = self::resolveWorkspace(); if (! $user instanceof User || ! $workspace instanceof Workspace) { return false; } $resolver = app(\App\Services\Auth\WorkspaceCapabilityResolver::class); return $resolver->isMember($user, $workspace) && $resolver->can($user, $workspace, Capabilities::WORKSPACE_BASELINES_MANAGE); } private static function archiveTableAction(?Workspace $workspace): Action { $action = Action::make('archive') ->label('Archive') ->icon('heroicon-o-archive-box') ->color('warning') ->requiresConfirmation() ->modalHeading('Archive baseline profile') ->modalDescription('Archiving is permanent in v1. This profile can no longer be used for captures or compares.') ->hidden(fn (BaselineProfile $record): bool => $record->status === BaselineProfileStatus::Archived) ->action(function (BaselineProfile $record): void { if (! self::hasManageCapability()) { throw new AuthorizationException; } $record->forceFill(['status' => BaselineProfileStatus::Archived->value])->save(); self::audit($record, AuditActionId::BaselineProfileArchived, [ 'baseline_profile_id' => (int) $record->getKey(), 'name' => (string) $record->name, ]); Notification::make() ->title('Baseline profile archived') ->success() ->send(); }); if ($workspace instanceof Workspace) { $action = WorkspaceUiEnforcement::forTableAction($action, $workspace) ->requireCapability(Capabilities::WORKSPACE_BASELINES_MANAGE) ->destructive() ->apply(); } return $action; } }