TenantAtlas/resources/views/livewire/policy-version-assignments-widget.blade.php
ahmido 61b0b1bc23 feat(010): Administrative Templates – restore from PolicyVersion + version visibility (#13)
Problem: Restore nutzt bisher den Snapshot aus dem BackupSet (BackupItem). Wenn der Snapshot “unvollständig”/nicht der gewünschte Stand ist, landen nach Restore nur wenige Admin-Template-Settings in Intune.
Lösung:
Neue Action “Restore to Intune” direkt an einer konkreten PolicyVersion (inkl. Dry-Run Toggle) → reproduzierbarer Rollback auf exakt diese Version.
Restore-UI zeigt jetzt PolicyVersion-Nummer (version: X) in der Item-Auswahl + BackupSet Items Tabelle hat eine Version-Spalte.
Implementierung:
RestoreService::executeFromPolicyVersion() erzeugt dafür einen kleinen, temporären BackupSet+BackupItem aus der Version und startet einen normalen RestoreRun.
Pest-Test: PolicyVersionRestoreToIntuneTest.php
Specs/TODO:
Offene Follow-ups sind dokumentiert in tasks.md unter “Open TODOs (Follow-up)”.
QA (GUI):
Inventory → Policies → <Policy> → Versions → Restore to Intune (erst Dry-Run, dann Execute)
Backups & Restore → Restore Runs → Create (bei Items steht version: X)
Backups & Restore → Backup Sets → <Set> (Version-Spalte)
Tests: PolicyVersionRestoreToIntuneTest.php

Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.local>
Reviewed-on: #13
2025-12-30 01:50:05 +00:00

173 lines
8.6 KiB
PHP

<div class="space-y-4">
@php
$scopeTags = $version->scope_tags['names'] ?? [];
@endphp
@if(!empty($scopeTags))
<x-filament::section heading="Scope Tags">
<div class="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>
</x-filament::section>
@endif
@if($version->assignments && count($version->assignments) > 0)
<x-filament::section
heading="Assignments"
:description="'Captured with this version on ' . $version->captured_at->format('M d, Y H:i')"
>
<div class="space-y-4">
<div>
<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>
<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'] ?? '';
$typeKey = strtolower((string) $type);
$intent = $assignment['intent'] ?? 'apply';
$typeName = match (true) {
str_contains($typeKey, 'exclusiongroupassignmenttarget') => 'Exclude group',
str_contains($typeKey, 'groupassignmenttarget') => 'Include group',
str_contains($typeKey, 'alllicensedusersassignmenttarget') => 'All Users',
str_contains($typeKey, 'alldevicesassignmenttarget') => 'All Devices',
default => 'Unknown',
};
$groupId = $target['groupId'] ?? null;
$groupName = $target['group_display_name'] ?? null;
$groupOrphaned = $target['group_orphaned'] ?? ($version->metadata['has_orphaned_assignments'] ?? false);
$filterId = $target['deviceAndAppManagementAssignmentFilterId'] ?? null;
$filterTypeRaw = strtolower((string) ($target['deviceAndAppManagementAssignmentFilterType'] ?? 'none'));
$filterType = $filterTypeRaw !== '' ? $filterTypeRaw : 'none';
$filterName = $target['assignment_filter_name'] ?? null;
$filterLabel = $filterName ?? $filterId;
@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($groupOrphaned)
<span class="text-warning-600 dark:text-warning-400">
⚠️ Unknown group (ID: {{ $groupId }})
</span>
@elseif($groupName)
<span class="text-gray-700 dark:text-gray-300">
{{ $groupName }}
</span>
<span class="text-xs text-gray-500 dark:text-gray-500">
({{ $groupId }})
</span>
@else
<span class="text-gray-700 dark:text-gray-300">
Group ID: {{ $groupId }}
</span>
@endif
@endif
@if($filterLabel)
<span class="text-xs text-gray-500 dark:text-gray-500">
Filter{{ $filterType !== 'none' ? " ({$filterType})" : '' }}: {{ $filterLabel }}
</span>
@endif
<span class="ml-auto text-xs text-gray-500 dark:text-gray-500">({{ $intent }})</span>
</div>
@endforeach
</div>
</div>
</div>
</x-filament::section>
@else
<x-filament::section heading="Assignments">
@php
$assignmentsFetched = $version->metadata['assignments_fetched'] ?? false;
$assignmentsFetchFailed = $version->metadata['assignments_fetch_failed'] ?? false;
$assignmentsFetchError = $version->metadata['assignments_fetch_error'] ?? null;
@endphp
@if($assignmentsFetchFailed)
<p class="text-sm text-gray-500 dark:text-gray-400">
Assignments could not be fetched from Microsoft Graph.
</p>
@if($assignmentsFetchError)
<p class="mt-2 text-xs text-gray-500 dark:text-gray-500">
{{ $assignmentsFetchError }}
</p>
@endif
@elseif($assignmentsFetched)
<p class="text-sm text-gray-500 dark:text-gray-400">
No assignments found for this version.
</p>
@else
<p class="text-sm text-gray-500 dark:text-gray-400">
Assignments were not captured for this version.
</p>
@endif
@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
</x-filament::section>
@endif
@php
$complianceTotal = $compliance['total'] ?? 0;
$complianceTemplates = $compliance['templates'] ?? [];
@endphp
@if($complianceTotal > 0)
<x-filament::section
heading="Compliance notifications"
:description="$complianceTotal . ' action(s) • ' . count($complianceTemplates) . ' template(s)'"
>
<div class="space-y-2">
@foreach($compliance['items'] ?? [] as $item)
@php
$ruleName = $item['rule_name'] ?? null;
$templateId = $item['template_id'] ?? null;
@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">
{{ $ruleName ?: 'Default rule' }}
</span>
@if($templateId)
<span class="text-xs text-gray-500 dark:text-gray-500">
Template: {{ $templateId }}
</span>
@endif
</div>
@endforeach
</div>
</x-filament::section>
@endif
</div>