label('Capture snapshot') ->requiresConfirmation() ->modalHeading('Capture snapshot now') ->modalSubheading('This will fetch the latest configuration from Microsoft Graph and store a new policy version.') ->action(function () { $policy = $this->record; try { $tenant = $policy->tenant; if (! $tenant) { Notification::make() ->title('Policy has no tenant associated.') ->danger() ->send(); return; } app(VersionService::class)->captureFromGraph($tenant, $policy, auth()->user()?->email ?? null); Notification::make() ->title('Snapshot captured successfully.') ->success() ->send(); $this->redirect($this->getResource()::getUrl('view', ['record' => $policy->getKey()])); } catch (\Throwable $e) { Notification::make() ->title('Failed to capture snapshot: '.$e->getMessage()) ->danger() ->send(); } }) ->color('primary'), ]; } public function infolist(Schema $schema): Schema { $latestBackupItem = $this->record->backupItems() ->whereNotNull('assignments') ->latest('created_at') ->first(); if (! $latestBackupItem || ! $latestBackupItem->hasAssignments()) { return $schema ->schema([ Section::make('Policy Information') ->schema([ TextEntry::make('display_name')->label('Name'), TextEntry::make('policy_type')->label('Type'), TextEntry::make('platform')->label('Platform'), TextEntry::make('external_id')->label('Policy ID')->copyable(), ]), Section::make('Assignments') ->schema([ TextEntry::make('no_assignments') ->label('') ->default('No assignments captured yet. Create a backup with "Include Assignments" enabled to view assignment data.') ->columnSpanFull(), ]) ->collapsible(), ]); } return $schema ->schema([ Section::make('Policy Information') ->schema([ TextEntry::make('display_name')->label('Name'), TextEntry::make('policy_type')->label('Type'), TextEntry::make('platform')->label('Platform'), TextEntry::make('external_id')->label('Policy ID')->copyable(), ]), Section::make('Assignments') ->description('Captured from backup on '.$latestBackupItem->created_at->format('M d, Y H:i')) ->schema([ TextEntry::make('assignment_summary') ->label('Summary') ->default(function () use ($latestBackupItem) { $count = $latestBackupItem->assignment_count; $orphaned = $latestBackupItem->hasOrphanedAssignments() ? ' (includes orphaned groups)' : ''; return "{$count} assignment(s){$orphaned}"; }), TextEntry::make('scope_tags') ->label('Scope Tags') ->badge() ->separator(',') ->default(fn () => $latestBackupItem->scope_tag_names), TextEntry::make('assignments_detail') ->label('Assignments') ->columnSpanFull() ->default(function () use ($latestBackupItem) { if (empty($latestBackupItem->assignments)) { return 'No assignments'; } $lines = []; foreach ($latestBackupItem->assignments as $assignment) { $target = $assignment['target'] ?? []; $type = $target['@odata.type'] ?? 'unknown'; $intent = $assignment['intent'] ?? 'apply'; $typeName = match ($type) { '#microsoft.graph.groupAssignmentTarget' => 'Group', '#microsoft.graph.allLicensedUsersAssignmentTarget' => 'All Users', '#microsoft.graph.allDevicesAssignmentTarget' => 'All Devices', default => 'Unknown', }; if ($type === '#microsoft.graph.groupAssignmentTarget') { $groupId = $target['groupId'] ?? 'unknown'; $groupName = $this->resolveGroupName($groupId, $latestBackupItem); $lines[] = "• {$typeName}: {$groupName} ({$intent})"; } else { $lines[] = "• {$typeName} ({$intent})"; } } return implode("\n", $lines); }) ->markdown(), ]) ->collapsible(), ]); } private function resolveGroupName(string $groupId, $backupItem): string { // Try to find group name in backup metadata or show as orphaned if ($backupItem->hasOrphanedAssignments()) { return "⚠️ Unknown Group (ID: {$groupId})"; } return "Group ID: {$groupId}"; } }