Summary Adds a tenant-scoped Entra Groups “Directory Cache” to enable DB-only group name resolution across the app (no render-time Graph calls), plus sync runs + observability. What’s included • Entra Groups cache • New entra_groups storage (tenant-scoped) for group metadata (no memberships). • Retention semantics: groups become stale / retained per spec (no hard delete on first miss). • Group Sync Runs • New “Group Sync Runs” UI (list + detail) with tenant isolation (403 on cross-tenant access). • Manual “Sync Groups” action: creates/reuses a run, dispatches job, DB notification with “View run” link. • Scheduled dispatcher command wired in console.php. • DB-only label resolution (US3) • Shared EntraGroupLabelResolver with safe fallback Unresolved (…last8) and UUID guarding. • Refactors to prefer cached names (no typeahead / no live Graph) in: • Tenant RBAC group selects • Policy version assignments widget • Restore results + restore wizard group mapping labels Safety / Guardrails • No render-time Graph calls: fail-hard guard test verifies UI paths don’t call GraphClientInterface during page render. • Tenant isolation & authorization: policies + scoped queries enforced (cross-tenant access returns 403, not 404). • Data minimization: only group metadata is cached (no membership/owners). Tests / Verification • Added/updated tests under tests/Feature/DirectoryGroups and tests/Unit/DirectoryGroups: • Start sync → run record + job dispatch + upserts • Retention purge semantics • Scheduled dispatch wiring • Render-time Graph guard • UI/resource access isolation • Ran: • ./vendor/bin/pint --dirty • ./vendor/bin/sail artisan test tests/Feature/DirectoryGroups • ./vendor/bin/sail artisan test tests/Unit/DirectoryGroups Notes / Follow-ups • UI polish remains (picker/lookup UX, consistent progress widget/toasts across modules, navigation grouping). • pr-gate checklist still has non-blocking open items (mostly UX/ops polish); requirements gate is green. Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.local> Reviewed-on: #57
83 lines
5.5 KiB
PHP
83 lines
5.5 KiB
PHP
<div wire:poll.{{ $pollSeconds }}s="loadRuns">
|
|
<!-- Bulk Operation Progress Component: {{ $runs->count() }} active runs -->
|
|
@if($runs->isNotEmpty())
|
|
<div class="fixed bottom-4 right-4 z-[999999] w-96 space-y-2" style="pointer-events: auto;">
|
|
@foreach ($runs as $run)
|
|
@php($effectiveTotal = max((int) $run->total_items, (int) $run->processed_items))
|
|
@php($percent = $effectiveTotal > 0 ? min(100, round(($run->processed_items / $effectiveTotal) * 100)) : 0)
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl border-2 border-primary-500 dark:border-primary-400 p-4 transition-all animate-in slide-in-from-right duration-300"
|
|
wire:key="run-{{ $run->id }}">
|
|
|
|
<div class="flex justify-between items-start mb-3">
|
|
<div class="flex-1">
|
|
<h4 class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
|
{{ ucfirst($run->action) }} {{ ucfirst(str_replace('_', ' ', $run->resource)) }}
|
|
</h4>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
|
@if($run->status === 'pending')
|
|
@php($isStalePending = $run->created_at->lt(now()->subSeconds(30)))
|
|
<span class="inline-flex items-center">
|
|
<svg class="animate-spin -ml-1 mr-1.5 h-3 w-3 text-primary-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
{{ $isStalePending ? 'Queued…' : 'Starting...' }}
|
|
</span>
|
|
@elseif($run->status === 'running')
|
|
<span class="inline-flex items-center">
|
|
<svg class="animate-spin -ml-1 mr-1.5 h-3 w-3 text-primary-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
Processing...
|
|
</span>
|
|
@elseif(in_array($run->status, ['completed', 'completed_with_errors'], true))
|
|
<span class="text-success-600 dark:text-success-400">Done</span>
|
|
@elseif(in_array($run->status, ['failed', 'aborted'], true))
|
|
<span class="text-danger-600 dark:text-danger-400">Failed</span>
|
|
@endif
|
|
</p>
|
|
</div>
|
|
<div class="text-right">
|
|
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">
|
|
{{ $run->processed_items }} / {{ $effectiveTotal }}
|
|
</span>
|
|
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
|
{{ $percent }}%
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="w-full bg-gray-200 rounded-full h-3 dark:bg-gray-700 overflow-hidden">
|
|
<div class="bg-primary-600 dark:bg-primary-500 h-3 rounded-full transition-all duration-300 ease-out"
|
|
style="width: {{ $percent }}%"></div>
|
|
</div>
|
|
|
|
<div class="mt-2 flex items-center justify-between text-xs">
|
|
<div class="flex items-center gap-3">
|
|
@if ($run->succeeded > 0)
|
|
<span class="text-success-600 dark:text-success-400">
|
|
✓ {{ $run->succeeded }} succeeded
|
|
</span>
|
|
@endif
|
|
@if ($run->failed > 0)
|
|
<span class="text-danger-600 dark:text-danger-400">
|
|
✗ {{ $run->failed }} failed
|
|
</span>
|
|
@endif
|
|
@if ($run->skipped > 0)
|
|
<span class="text-gray-500 dark:text-gray-400">
|
|
⊘ {{ $run->skipped }} skipped
|
|
</span>
|
|
@endif
|
|
</div>
|
|
<span class="text-gray-400 dark:text-gray-500">
|
|
{{ $run->created_at->diffForHumans(null, true, true) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
</div>
|