TenantAtlas/resources/views/livewire/policy-version-assignments-widget.blade.php
Ahmed Darrazi c3bdcf4d2d feat(004): implement PolicyCaptureOrchestrator for assignment consistency
BREAKING CHANGE: Assignment capture flow completely refactored

Core Changes:
- Created PolicyCaptureOrchestrator service for centralized capture coordination
- Refactored BackupService to use orchestrator (version-first approach)
- Fixed domain model bug: PolicyVersion now stores assignments (source of truth)
- BackupItem references PolicyVersion and copies assignments for restore

Database:
- Added assignments, scope_tags, assignments_hash, scope_tags_hash to policy_versions
- Added policy_version_id foreign key to backup_items
- Migrations: 2025_12_22_171525, 2025_12_22_171545

Services:
- PolicyCaptureOrchestrator: Intelligent version reuse, idempotent backfilling
- VersionService: Enhanced to capture assignments during version creation
- BackupService: Uses orchestrator, version-first capture flow

UI:
- Moved assignments widget from Policy to PolicyVersion view
- Created PolicyVersionAssignmentsWidget Livewire component
- Updated BackupItemsRelationManager columns for new assignment fields

Tests:
- Deleted BackupWithAssignmentsTest (old behavior)
- Created BackupWithAssignmentsConsistencyTest (4 tests, all passing)
- Fixed AssignmentFetcherTest and GroupResolverTest for GraphResponse
- All 162 tests passing

Issue: Assignments/scope tags not displaying in BackupSet items table (UI only)
Status: Database contains correct data, UI column definitions need adjustment
2025-12-22 20:19:10 +01:00

119 lines
6.0 KiB
PHP

<div class="fi-section">
@if($version->assignments && count($version->assignments) > 0)
<div class="rounded-lg bg-white shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10">
<div class="px-6 py-4">
<div class="flex items-center justify-between">
<div>
<h3 class="text-base font-semibold leading-6 text-gray-950 dark:text-white">
Assignments
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Captured with this version on {{ $version->captured_at->format('M d, Y H:i') }}
</p>
</div>
</div>
</div>
<div class="border-t border-gray-200 px-6 py-4 dark:border-white/10">
<!-- Summary -->
<div class="mb-4">
<h4 class="text-sm font-medium text-gray-950 dark:text-white">Summary</h4>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{{ count($version->assignments) }} assignment(s)
@php
$hasOrphaned = $version->metadata['has_orphaned_assignments'] ?? false;
@endphp
@if($hasOrphaned)
<span class="text-warning-600 dark:text-warning-400">(includes orphaned groups)</span>
@endif
</p>
</div>
<!-- Scope Tags -->
@php
$scopeTags = $version->scope_tags['names'] ?? [];
@endphp
@if(!empty($scopeTags))
<div class="mb-4">
<h4 class="text-sm font-medium text-gray-950 dark:text-white">Scope Tags</h4>
<div class="mt-2 flex flex-wrap gap-2">
@foreach($scopeTags as $tag)
<span class="inline-flex items-center rounded-md bg-primary-50 px-2 py-1 text-xs font-medium text-primary-700 ring-1 ring-inset ring-primary-700/10 dark:bg-primary-400/10 dark:text-primary-400 dark:ring-primary-400/30">
{{ $tag }}
</span>
@endforeach
</div>
</div>
@endif
<!-- Assignment Details -->
<div>
<h4 class="text-sm font-medium text-gray-950 dark:text-white">Assignment Details</h4>
<div class="mt-2 space-y-2">
@foreach($version->assignments as $assignment)
@php
$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'
};
$groupId = $target['groupId'] ?? null;
$hasOrphaned = $version->metadata['has_orphaned_assignments'] ?? false;
@endphp
<div class="flex items-center gap-2 text-sm">
<span class="text-gray-600 dark:text-gray-400"></span>
<span class="font-medium text-gray-900 dark:text-white">{{ $typeName }}</span>
@if($groupId)
<span class="text-gray-600 dark:text-gray-400">:</span>
@if($hasOrphaned)
<span class="text-warning-600 dark:text-warning-400">
⚠️ Unknown Group (ID: {{ $groupId }})
</span>
@else
<span class="text-gray-700 dark:text-gray-300">
Group ID: {{ $groupId }}
</span>
@endif
@endif
<span class="ml-auto text-xs text-gray-500 dark:text-gray-500">({{ $intent }})</span>
</div>
@endforeach
</div>
</div>
</div>
</div>
@else
<div class="rounded-lg bg-white shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10">
<div class="px-6 py-4">
<h3 class="text-base font-semibold leading-6 text-gray-950 dark:text-white">
Assignments
</h3>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
Assignments were not captured for this version.
</p>
@php
$hasBackupItem = $version->policy->backupItems()
->whereNotNull('assignments')
->where('created_at', '<=', $version->captured_at)
->exists();
@endphp
@if($hasBackupItem)
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
💡 Assignment data may be available in related backup items.
</p>
@endif
</div>
</div>
@endif
</div>