fix: bulk tenant sync shows progress
This commit is contained in:
parent
8d90550abe
commit
364b2e9f10
@ -4,9 +4,11 @@
|
|||||||
|
|
||||||
use App\Filament\Resources\TenantResource\Pages;
|
use App\Filament\Resources\TenantResource\Pages;
|
||||||
use App\Http\Controllers\RbacDelegatedAuthController;
|
use App\Http\Controllers\RbacDelegatedAuthController;
|
||||||
|
use App\Jobs\BulkTenantSyncJob;
|
||||||
use App\Jobs\SyncPoliciesJob;
|
use App\Jobs\SyncPoliciesJob;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Services\BulkOperationService;
|
||||||
use App\Services\Graph\GraphClientInterface;
|
use App\Services\Graph\GraphClientInterface;
|
||||||
use App\Services\Intune\AuditLogger;
|
use App\Services\Intune\AuditLogger;
|
||||||
use App\Services\Intune\RbacHealthService;
|
use App\Services\Intune\RbacHealthService;
|
||||||
@ -207,6 +209,7 @@ public static function table(Table $table): Table
|
|||||||
->icon('heroicon-o-arrow-path')
|
->icon('heroicon-o-arrow-path')
|
||||||
->iconColor('warning')
|
->iconColor('warning')
|
||||||
->success()
|
->success()
|
||||||
|
->sendToDatabase(auth()->user())
|
||||||
->send();
|
->send();
|
||||||
}),
|
}),
|
||||||
Actions\Action::make('openTenant')
|
Actions\Action::make('openTenant')
|
||||||
@ -391,6 +394,30 @@ public static function table(Table $table): Table
|
|||||||
->filter(fn ($record) => $record instanceof Tenant && $record->isActive())
|
->filter(fn ($record) => $record instanceof Tenant && $record->isActive())
|
||||||
->filter(fn (Tenant $tenant) => $user->canSyncTenant($tenant));
|
->filter(fn (Tenant $tenant) => $user->canSyncTenant($tenant));
|
||||||
|
|
||||||
|
if ($eligible->isEmpty()) {
|
||||||
|
Notification::make()
|
||||||
|
->title('Bulk sync skipped')
|
||||||
|
->body('No eligible tenants selected.')
|
||||||
|
->icon('heroicon-o-information-circle')
|
||||||
|
->info()
|
||||||
|
->sendToDatabase($user)
|
||||||
|
->send();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenantContext = Tenant::current() ?? $eligible->first();
|
||||||
|
|
||||||
|
if (! $tenantContext) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ids = $eligible->pluck('id')->toArray();
|
||||||
|
$count = $eligible->count();
|
||||||
|
|
||||||
|
$service = app(BulkOperationService::class);
|
||||||
|
$run = $service->createRun($tenantContext, $user, 'tenant', 'sync', $ids, $count);
|
||||||
|
|
||||||
foreach ($eligible as $tenant) {
|
foreach ($eligible as $tenant) {
|
||||||
SyncPoliciesJob::dispatch($tenant->getKey());
|
SyncPoliciesJob::dispatch($tenant->getKey());
|
||||||
|
|
||||||
@ -408,11 +435,15 @@ public static function table(Table $table): Table
|
|||||||
|
|
||||||
Notification::make()
|
Notification::make()
|
||||||
->title('Bulk sync started')
|
->title('Bulk sync started')
|
||||||
->body("Dispatched sync for {$count} tenant(s).")
|
->body("Syncing {$count} tenant(s) in the background. Check the progress bar in the bottom right corner.")
|
||||||
->icon('heroicon-o-arrow-path')
|
->icon('heroicon-o-arrow-path')
|
||||||
->iconColor('warning')
|
->iconColor('warning')
|
||||||
->success()
|
->success()
|
||||||
|
->duration(8000)
|
||||||
|
->sendToDatabase($user)
|
||||||
->send();
|
->send();
|
||||||
|
|
||||||
|
BulkTenantSyncJob::dispatch($run->id);
|
||||||
})
|
})
|
||||||
->deselectRecordsAfterCompletion(),
|
->deselectRecordsAfterCompletion(),
|
||||||
])
|
])
|
||||||
|
|||||||
152
app/Jobs/BulkTenantSyncJob.php
Normal file
152
app/Jobs/BulkTenantSyncJob.php
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Models\BulkOperationRun;
|
||||||
|
use App\Models\Tenant;
|
||||||
|
use App\Services\BulkOperationService;
|
||||||
|
use App\Services\Intune\PolicySyncService;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class BulkTenantSyncJob implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public function __construct(public int $bulkRunId) {}
|
||||||
|
|
||||||
|
public function handle(BulkOperationService $service, PolicySyncService $syncService): void
|
||||||
|
{
|
||||||
|
$run = BulkOperationRun::with(['tenant', 'user'])->find($this->bulkRunId);
|
||||||
|
|
||||||
|
if (! $run || $run->status !== 'pending') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$service->start($run);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$chunkSize = max(1, (int) config('tenantpilot.bulk_operations.chunk_size', 10));
|
||||||
|
$itemCount = 0;
|
||||||
|
|
||||||
|
$supported = config('tenantpilot.supported_policy_types');
|
||||||
|
|
||||||
|
$totalItems = $run->total_items ?: count($run->item_ids ?? []);
|
||||||
|
$failureThreshold = (int) floor($totalItems / 2);
|
||||||
|
|
||||||
|
foreach (($run->item_ids ?? []) as $tenantId) {
|
||||||
|
$itemCount++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$tenant = Tenant::query()->whereKey($tenantId)->first();
|
||||||
|
|
||||||
|
if (! $tenant) {
|
||||||
|
$service->recordFailure($run, (string) $tenantId, 'Tenant not found');
|
||||||
|
|
||||||
|
if ($run->failed > $failureThreshold) {
|
||||||
|
$service->abort($run, 'Circuit breaker: more than 50% of items failed.');
|
||||||
|
|
||||||
|
if ($run->user) {
|
||||||
|
Notification::make()
|
||||||
|
->title('Bulk Sync Aborted')
|
||||||
|
->body('Circuit breaker triggered: too many failures (>50%).')
|
||||||
|
->icon('heroicon-o-exclamation-triangle')
|
||||||
|
->danger()
|
||||||
|
->sendToDatabase($run->user)
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $tenant->isActive()) {
|
||||||
|
$service->recordSkippedWithReason($run, (string) $tenantId, 'Tenant is not active');
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $run->user || ! $run->user->canSyncTenant($tenant)) {
|
||||||
|
$service->recordSkippedWithReason($run, (string) $tenantId, 'Not authorized to sync tenant');
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$syncService->syncPolicies($tenant, $supported);
|
||||||
|
|
||||||
|
$service->recordSuccess($run);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$service->recordFailure($run, (string) $tenantId, $e->getMessage());
|
||||||
|
|
||||||
|
if ($run->failed > $failureThreshold) {
|
||||||
|
$service->abort($run, 'Circuit breaker: more than 50% of items failed.');
|
||||||
|
|
||||||
|
if ($run->user) {
|
||||||
|
Notification::make()
|
||||||
|
->title('Bulk Sync Aborted')
|
||||||
|
->body('Circuit breaker triggered: too many failures (>50%).')
|
||||||
|
->icon('heroicon-o-exclamation-triangle')
|
||||||
|
->danger()
|
||||||
|
->sendToDatabase($run->user)
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($itemCount % $chunkSize === 0) {
|
||||||
|
$run->refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$service->complete($run);
|
||||||
|
|
||||||
|
if ($run->user) {
|
||||||
|
$message = "Synced {$run->succeeded} tenant(s)";
|
||||||
|
|
||||||
|
if ($run->skipped > 0) {
|
||||||
|
$message .= " ({$run->skipped} skipped)";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($run->failed > 0) {
|
||||||
|
$message .= " ({$run->failed} failed)";
|
||||||
|
}
|
||||||
|
|
||||||
|
$message .= '.';
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title('Bulk Sync Completed')
|
||||||
|
->body($message)
|
||||||
|
->icon('heroicon-o-check-circle')
|
||||||
|
->success()
|
||||||
|
->sendToDatabase($run->user)
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$service->fail($run, $e->getMessage());
|
||||||
|
|
||||||
|
$run->refresh();
|
||||||
|
$run->load('user');
|
||||||
|
|
||||||
|
if ($run->user) {
|
||||||
|
Notification::make()
|
||||||
|
->title('Bulk Sync Failed')
|
||||||
|
->body($e->getMessage())
|
||||||
|
->icon('heroicon-o-x-circle')
|
||||||
|
->danger()
|
||||||
|
->sendToDatabase($run->user)
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Filament\Resources\TenantResource\Pages\ListTenants;
|
use App\Filament\Resources\TenantResource\Pages\ListTenants;
|
||||||
use App\Jobs\SyncPoliciesJob;
|
use App\Jobs\BulkTenantSyncJob;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Filament\Events\TenantSet;
|
use Filament\Events\TenantSet;
|
||||||
@ -65,10 +65,15 @@
|
|||||||
->assertTableBulkActionVisible('syncSelected')
|
->assertTableBulkActionVisible('syncSelected')
|
||||||
->callTableBulkAction('syncSelected', collect([$tenantA, $tenantB]));
|
->callTableBulkAction('syncSelected', collect([$tenantA, $tenantB]));
|
||||||
|
|
||||||
Bus::assertDispatchedTimes(SyncPoliciesJob::class, 2);
|
Bus::assertDispatchedTimes(BulkTenantSyncJob::class, 1);
|
||||||
|
|
||||||
Bus::assertDispatched(SyncPoliciesJob::class, fn (SyncPoliciesJob $job) => $job->tenantId === $tenantA->id);
|
$this->assertDatabaseHas('bulk_operation_runs', [
|
||||||
Bus::assertDispatched(SyncPoliciesJob::class, fn (SyncPoliciesJob $job) => $job->tenantId === $tenantB->id);
|
'tenant_id' => $tenantA->id,
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'resource' => 'tenant',
|
||||||
|
'action' => 'sync',
|
||||||
|
'total_items' => 2,
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tenant portfolio bulk sync is hidden for readonly users', function () {
|
test('tenant portfolio bulk sync is hidden for readonly users', function () {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user