schema([ Forms\Components\TextInput::make('name') ->label('Backup name') ->default(fn () => now()->format('Y-m-d H:i:s').' backup') ->required(), ]); } public static function table(Table $table): Table { return $table ->columns([ Tables\Columns\TextColumn::make('name')->searchable(), Tables\Columns\TextColumn::make('status')->badge(), Tables\Columns\TextColumn::make('item_count')->label('Items'), Tables\Columns\TextColumn::make('created_by')->label('Created by'), Tables\Columns\TextColumn::make('completed_at')->dateTime()->since(), Tables\Columns\TextColumn::make('created_at')->dateTime()->since(), ]) ->filters([ Tables\Filters\TrashedFilter::make() ->label('Archived') ->placeholder('Active') ->trueLabel('All') ->falseLabel('Archived'), ]) ->actions([ Actions\ViewAction::make() ->url(fn (BackupSet $record) => static::getUrl('view', ['record' => $record])) ->openUrlInNewTab(false), ActionGroup::make([ Actions\Action::make('archive') ->label('Archive') ->color('danger') ->icon('heroicon-o-archive-box-x-mark') ->requiresConfirmation() ->visible(fn (BackupSet $record) => ! $record->trashed()) ->action(function (BackupSet $record, AuditLogger $auditLogger) { if ($record->restoreRuns()->withTrashed()->exists()) { Notification::make() ->title('Cannot archive backup set') ->body('Backup sets used by restore runs cannot be archived.') ->danger() ->send(); return; } $record->delete(); if ($record->tenant) { $auditLogger->log( tenant: $record->tenant, action: 'backup.deleted', resourceType: 'backup_set', resourceId: (string) $record->id, status: 'success', context: ['metadata' => ['name' => $record->name]] ); } Notification::make() ->title('Backup set archived') ->success() ->send(); }), Actions\Action::make('forceDelete') ->label('Force delete') ->color('danger') ->icon('heroicon-o-trash') ->requiresConfirmation() ->visible(fn (BackupSet $record) => $record->trashed()) ->action(function (BackupSet $record, AuditLogger $auditLogger) { if ($record->restoreRuns()->withTrashed()->exists()) { Notification::make() ->title('Cannot force delete backup set') ->body('Backup sets referenced by restore runs cannot be removed.') ->danger() ->send(); return; } if ($record->tenant) { $auditLogger->log( tenant: $record->tenant, action: 'backup.force_deleted', resourceType: 'backup_set', resourceId: (string) $record->id, status: 'success', context: ['metadata' => ['name' => $record->name]] ); } $record->items()->withTrashed()->forceDelete(); $record->forceDelete(); Notification::make() ->title('Backup set permanently deleted') ->success() ->send(); }), ])->icon('heroicon-o-ellipsis-vertical'), ]) ->bulkActions([ BulkActionGroup::make([ BulkAction::make('bulk_delete') ->label('Archive Backup Sets') ->icon('heroicon-o-archive-box-x-mark') ->color('danger') ->requiresConfirmation() ->hidden(function (HasTable $livewire): bool { $trashedFilterState = $livewire->getTableFilterState(TrashedFilter::class) ?? []; $value = $trashedFilterState['value'] ?? null; $isOnlyTrashed = in_array($value, [0, '0', false], true); return $isOnlyTrashed; }) ->modalDescription('This archives backup sets (soft delete). Backup sets referenced by restore runs will be skipped.') ->form(function (Collection $records) { if ($records->count() >= 10) { return [ Forms\Components\TextInput::make('confirmation') ->label('Type DELETE to confirm') ->required() ->in(['DELETE']) ->validationMessages([ 'in' => 'Please type DELETE to confirm.', ]), ]; } return []; }) ->action(function (Collection $records) { $tenant = Tenant::current(); $user = auth()->user(); $count = $records->count(); $ids = $records->pluck('id')->toArray(); $service = app(BulkOperationService::class); $run = $service->createRun($tenant, $user, 'backup_set', 'delete', $ids, $count); if ($count >= 10) { Notification::make() ->title('Bulk archive started') ->body("Archiving {$count} backup sets in the background. Check the progress bar in the bottom right corner.") ->icon('heroicon-o-arrow-path') ->iconColor('warning') ->info() ->duration(8000) ->sendToDatabase($user) ->send(); BulkBackupSetDeleteJob::dispatch($run->id); } else { BulkBackupSetDeleteJob::dispatchSync($run->id); } }) ->deselectRecordsAfterCompletion(), BulkAction::make('bulk_restore') ->label('Restore Backup Sets') ->icon('heroicon-o-arrow-uturn-left') ->color('success') ->requiresConfirmation() ->hidden(function (HasTable $livewire): bool { $trashedFilterState = $livewire->getTableFilterState(TrashedFilter::class) ?? []; $value = $trashedFilterState['value'] ?? null; $isOnlyTrashed = in_array($value, [0, '0', false], true); return ! $isOnlyTrashed; }) ->modalHeading(fn (Collection $records) => "Restore {$records->count()} backup sets?") ->modalDescription('Archived backup sets will be restored back to the active list. Active backup sets will be skipped.') ->action(function (Collection $records) { $tenant = Tenant::current(); $user = auth()->user(); $count = $records->count(); $ids = $records->pluck('id')->toArray(); $service = app(BulkOperationService::class); $run = $service->createRun($tenant, $user, 'backup_set', 'restore', $ids, $count); if ($count >= 10) { Notification::make() ->title('Bulk restore started') ->body("Restoring {$count} backup sets in the background. Check the progress bar in the bottom right corner.") ->icon('heroicon-o-arrow-path') ->iconColor('warning') ->info() ->duration(8000) ->sendToDatabase($user) ->send(); BulkBackupSetRestoreJob::dispatch($run->id); } else { BulkBackupSetRestoreJob::dispatchSync($run->id); } }) ->deselectRecordsAfterCompletion(), BulkAction::make('bulk_force_delete') ->label('Force Delete Backup Sets') ->icon('heroicon-o-trash') ->color('danger') ->requiresConfirmation() ->hidden(function (HasTable $livewire): bool { $trashedFilterState = $livewire->getTableFilterState(TrashedFilter::class) ?? []; $value = $trashedFilterState['value'] ?? null; $isOnlyTrashed = in_array($value, [0, '0', false], true); return ! $isOnlyTrashed; }) ->modalHeading(fn (Collection $records) => "Force delete {$records->count()} backup sets?") ->modalDescription('This is permanent. Only archived backup sets will be permanently deleted; active backup sets will be skipped.') ->form(function (Collection $records) { if ($records->count() >= 10) { return [ Forms\Components\TextInput::make('confirmation') ->label('Type DELETE to confirm') ->required() ->in(['DELETE']) ->validationMessages([ 'in' => 'Please type DELETE to confirm.', ]), ]; } return []; }) ->action(function (Collection $records) { $tenant = Tenant::current(); $user = auth()->user(); $count = $records->count(); $ids = $records->pluck('id')->toArray(); $service = app(BulkOperationService::class); $run = $service->createRun($tenant, $user, 'backup_set', 'force_delete', $ids, $count); if ($count >= 10) { Notification::make() ->title('Bulk force delete started') ->body("Force deleting {$count} backup sets in the background. Check the progress bar in the bottom right corner.") ->icon('heroicon-o-arrow-path') ->iconColor('warning') ->info() ->duration(8000) ->sendToDatabase($user) ->send(); BulkBackupSetForceDeleteJob::dispatch($run->id); } else { BulkBackupSetForceDeleteJob::dispatchSync($run->id); } }) ->deselectRecordsAfterCompletion(), ]), ]); } public static function infolist(Schema $schema): Schema { return $schema ->schema([ Infolists\Components\TextEntry::make('name'), Infolists\Components\TextEntry::make('status')->badge(), Infolists\Components\TextEntry::make('item_count')->label('Items'), Infolists\Components\TextEntry::make('created_by')->label('Created by'), Infolists\Components\TextEntry::make('completed_at')->dateTime(), Infolists\Components\TextEntry::make('metadata') ->label('Metadata') ->formatStateUsing(fn ($state) => json_encode($state ?? [], JSON_PRETTY_PRINT)) ->copyable() ->copyMessage('Metadata copied'), ]); } public static function getRelations(): array { return [ BackupItemsRelationManager::class, ]; } public static function getPages(): array { return [ 'index' => Pages\ListBackupSets::route('/'), 'create' => Pages\CreateBackupSet::route('/create'), 'view' => Pages\ViewBackupSet::route('/{record}'), ]; } /** * @return array{label:?string,category:?string,restore:?string,risk:?string}|array */ private static function typeMeta(?string $type): array { if ($type === null) { return []; } return collect(config('tenantpilot.supported_policy_types', [])) ->firstWhere('type', $type) ?? []; } /** * Create a backup set via the domain service instead of direct model mass-assignment. */ public static function createBackupSet(array $data): BackupSet { /** @var Tenant $tenant */ $tenant = Tenant::current(); /** @var BackupService $service */ $service = app(BackupService::class); return $service->createBackupSet( tenant: $tenant, policyIds: $data['policy_ids'] ?? [], name: $data['name'] ?? null, actorEmail: auth()->user()?->email, actorName: auth()->user()?->name, ); } }