From 5b59988d485a89f14e21d7b752016e7c56ff4008 Mon Sep 17 00:00:00 2001 From: Ahmed Darrazi Date: Thu, 25 Dec 2025 14:26:50 +0100 Subject: [PATCH] fix: add missing single-row restore/actions --- app/Filament/Resources/BackupSetResource.php | 26 ++++ app/Filament/Resources/PolicyResource.php | 65 ++++++++++ app/Filament/Resources/RestoreRunResource.php | 25 ++++ tests/Feature/Filament/HousekeepingTest.php | 111 ++++++++++++++++++ 4 files changed, 227 insertions(+) diff --git a/app/Filament/Resources/BackupSetResource.php b/app/Filament/Resources/BackupSetResource.php index 890898a..b0d5664 100644 --- a/app/Filament/Resources/BackupSetResource.php +++ b/app/Filament/Resources/BackupSetResource.php @@ -71,6 +71,32 @@ public static function table(Table $table): Table ->url(fn (BackupSet $record) => static::getUrl('view', ['record' => $record])) ->openUrlInNewTab(false), ActionGroup::make([ + Actions\Action::make('restore') + ->label('Restore') + ->color('success') + ->icon('heroicon-o-arrow-uturn-left') + ->requiresConfirmation() + ->visible(fn (BackupSet $record) => $record->trashed()) + ->action(function (BackupSet $record, AuditLogger $auditLogger) { + $record->restore(); + $record->items()->withTrashed()->restore(); + + if ($record->tenant) { + $auditLogger->log( + tenant: $record->tenant, + action: 'backup.restored', + resourceType: 'backup_set', + resourceId: (string) $record->id, + status: 'success', + context: ['metadata' => ['name' => $record->name]] + ); + } + + Notification::make() + ->title('Backup set restored') + ->success() + ->send(); + }), Actions\Action::make('archive') ->label('Archive') ->color('danger') diff --git a/app/Filament/Resources/PolicyResource.php b/app/Filament/Resources/PolicyResource.php index acfe7f1..b2d2ed5 100644 --- a/app/Filament/Resources/PolicyResource.php +++ b/app/Filament/Resources/PolicyResource.php @@ -14,6 +14,7 @@ use App\Services\Intune\PolicyNormalizer; use BackedEnum; use Filament\Actions; +use Filament\Actions\ActionGroup; use Filament\Actions\BulkAction; use Filament\Actions\BulkActionGroup; use Filament\Forms; @@ -332,6 +333,70 @@ public static function table(Table $table): Table ]) ->actions([ Actions\ViewAction::make(), + ActionGroup::make([ + Actions\Action::make('ignore') + ->label('Ignore') + ->icon('heroicon-o-trash') + ->color('danger') + ->requiresConfirmation() + ->visible(fn (Policy $record) => $record->ignored_at === null) + ->action(function (Policy $record) { + $record->ignore(); + + Notification::make() + ->title('Policy ignored') + ->success() + ->send(); + }), + Actions\Action::make('restore') + ->label('Restore') + ->icon('heroicon-o-arrow-uturn-left') + ->color('success') + ->requiresConfirmation() + ->visible(fn (Policy $record) => $record->ignored_at !== null) + ->action(function (Policy $record) { + $record->unignore(); + + Notification::make() + ->title('Policy restored') + ->success() + ->send(); + }), + Actions\Action::make('sync') + ->label('Sync') + ->icon('heroicon-o-arrow-path') + ->color('primary') + ->requiresConfirmation() + ->visible(fn (Policy $record) => $record->ignored_at === null) + ->action(function (Policy $record) { + $tenant = Tenant::current(); + $user = auth()->user(); + + $service = app(BulkOperationService::class); + $run = $service->createRun($tenant, $user, 'policy', 'sync', [$record->id], 1); + + BulkPolicySyncJob::dispatchSync($run->id); + }), + Actions\Action::make('export') + ->label('Export to Backup') + ->icon('heroicon-o-archive-box-arrow-down') + ->visible(fn (Policy $record) => $record->ignored_at === null) + ->form([ + Forms\Components\TextInput::make('backup_name') + ->label('Backup Name') + ->required() + ->default(fn () => 'Backup '.now()->toDateTimeString()), + ]) + ->action(function (Policy $record, array $data) { + $tenant = Tenant::current(); + $user = auth()->user(); + + $service = app(BulkOperationService::class); + $run = $service->createRun($tenant, $user, 'policy', 'export', [$record->id], 1); + + BulkPolicyExportJob::dispatchSync($run->id, $data['backup_name']); + }), + ])->icon('heroicon-o-ellipsis-vertical'), ]) ->bulkActions([ BulkActionGroup::make([ diff --git a/app/Filament/Resources/RestoreRunResource.php b/app/Filament/Resources/RestoreRunResource.php index 4482068..12c136b 100644 --- a/app/Filament/Resources/RestoreRunResource.php +++ b/app/Filament/Resources/RestoreRunResource.php @@ -182,6 +182,31 @@ public static function table(Table $table): Table ->actions([ Actions\ViewAction::make(), ActionGroup::make([ + Actions\Action::make('restore') + ->label('Restore') + ->color('success') + ->icon('heroicon-o-arrow-uturn-left') + ->requiresConfirmation() + ->visible(fn (RestoreRun $record) => $record->trashed()) + ->action(function (RestoreRun $record, \App\Services\Intune\AuditLogger $auditLogger) { + $record->restore(); + + if ($record->tenant) { + $auditLogger->log( + tenant: $record->tenant, + action: 'restore_run.restored', + resourceType: 'restore_run', + resourceId: (string) $record->id, + status: 'success', + context: ['metadata' => ['backup_set_id' => $record->backup_set_id]] + ); + } + + Notification::make() + ->title('Restore run restored') + ->success() + ->send(); + }), Actions\Action::make('archive') ->label('Archive') ->color('danger') diff --git a/tests/Feature/Filament/HousekeepingTest.php b/tests/Feature/Filament/HousekeepingTest.php index 5967be1..9693d8b 100644 --- a/tests/Feature/Filament/HousekeepingTest.php +++ b/tests/Feature/Filament/HousekeepingTest.php @@ -1,6 +1,7 @@ 'tenant-restore-backup-set', + 'name' => 'Tenant Restore Backup Set', + ]); + + $backupSet = BackupSet::create([ + 'tenant_id' => $tenant->id, + 'name' => 'Set restore', + 'status' => 'completed', + ]); + + BackupItem::create([ + 'tenant_id' => $tenant->id, + 'backup_set_id' => $backupSet->id, + 'policy_id' => null, + 'policy_identifier' => 'policy-restore', + 'policy_type' => 'deviceConfiguration', + 'platform' => 'windows', + 'payload' => ['id' => 'policy-restore'], + ]); + + $user = User::factory()->create(); + $this->actingAs($user); + + Livewire::test(ListBackupSets::class) + ->callTableAction('archive', $backupSet) + ->set('tableFilters.trashed.value', 1) + ->callTableAction('restore', $backupSet); + + $this->assertDatabaseHas('backup_sets', ['id' => $backupSet->id, 'deleted_at' => null]); + $this->assertDatabaseHas('backup_items', ['backup_set_id' => $backupSet->id, 'deleted_at' => null]); + $this->assertDatabaseHas('audit_logs', [ + 'resource_type' => 'backup_set', + 'resource_id' => (string) $backupSet->id, + 'action' => 'backup.restored', + ]); +}); + test('restore run can be archived and force deleted', function () { $tenant = Tenant::create([ 'tenant_id' => 'tenant-restore-run', @@ -161,6 +201,77 @@ ]); }); +test('restore run can be restored when archived', function () { + $tenant = Tenant::create([ + 'tenant_id' => 'tenant-restore-restore-run', + 'name' => 'Tenant Restore Restore Run', + ]); + + $backupSet = BackupSet::create([ + 'tenant_id' => $tenant->id, + 'name' => 'Set for restore run restore', + 'status' => 'completed', + 'item_count' => 1, + ]); + + $restoreRun = RestoreRun::create([ + 'tenant_id' => $tenant->id, + 'backup_set_id' => $backupSet->id, + 'status' => 'completed', + 'is_dry_run' => true, + ]); + + $user = User::factory()->create(); + $this->actingAs($user); + + Livewire::test(ListRestoreRuns::class) + ->callTableAction('archive', $restoreRun) + ->set('tableFilters.trashed.value', 1) + ->callTableAction('restore', $restoreRun); + + $this->assertDatabaseHas('restore_runs', ['id' => $restoreRun->id, 'deleted_at' => null]); + $this->assertDatabaseHas('audit_logs', [ + 'resource_type' => 'restore_run', + 'resource_id' => (string) $restoreRun->id, + 'action' => 'restore_run.restored', + ]); +}); + +test('policy can be ignored and restored via row actions', function () { + $tenant = Tenant::create([ + 'tenant_id' => 'tenant-policy-row-actions', + 'name' => 'Tenant Policy Row Actions', + 'metadata' => [], + ]); + + $tenant->makeCurrent(); + + $policy = Policy::create([ + 'tenant_id' => $tenant->id, + 'external_id' => 'policy-row-1', + 'policy_type' => 'deviceConfiguration', + 'display_name' => 'Row Action Policy', + 'platform' => 'windows', + 'last_synced_at' => now(), + ]); + + $user = User::factory()->create(); + $this->actingAs($user); + + Livewire::test(ListPolicies::class) + ->callTableAction('ignore', $policy); + + $policy->refresh(); + expect($policy->ignored_at)->not->toBeNull(); + + Livewire::test(ListPolicies::class) + ->set('tableFilters.visibility.value', 'ignored') + ->callTableAction('restore', $policy); + + $policy->refresh(); + expect($policy->ignored_at)->toBeNull(); +}); + test('policy version can be archived with audit log', function () { $tenant = Tenant::create([ 'tenant_id' => 'tenant-3',