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([ TextInput::make('name') ->required() ->maxLength(255), Textarea::make('description') ->rows(3) ->maxLength(1000), TextInput::make('version_label') ->label('Version label') ->maxLength(50), Select::make('status') ->required() ->options([ BaselineProfile::STATUS_DRAFT => 'Draft', BaselineProfile::STATUS_ACTIVE => 'Active', BaselineProfile::STATUS_ARCHIVED => 'Archived', ]) ->default(BaselineProfile::STATUS_DRAFT) ->native(false), Select::make('scope_jsonb.policy_types') ->label('Policy type scope') ->multiple() ->options(self::policyTypeOptions()) ->helperText('Leave empty to include all policy types.') ->native(false), ]); } public static function table(Table $table): Table { $workspace = self::resolveWorkspace(); return $table ->defaultSort('name') ->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('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::all()) ->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(), ); } 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.') ->visible(fn (BaselineProfile $record): bool => $record->status !== BaselineProfile::STATUS_ARCHIVED && self::hasManageCapability()) ->action(function (BaselineProfile $record): void { if (! self::hasManageCapability()) { throw new AuthorizationException; } $record->forceFill(['status' => BaselineProfile::STATUS_ARCHIVED])->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; } }