schema([ Forms\Components\Select::make('backup_set_id') ->label('Backup set') ->options(function () { $tenantId = Tenant::current()->getKey(); return BackupSet::query() ->when($tenantId, fn ($query) => $query->where('tenant_id', $tenantId)) ->orderByDesc('created_at') ->get() ->mapWithKeys(function (BackupSet $set) { $label = sprintf( '%s • %s items • %s', $set->name, $set->item_count ?? 0, optional($set->created_at)->format('Y-m-d H:i') ); return [$set->id => $label]; }); }) ->reactive() ->required(), Forms\Components\CheckboxList::make('backup_item_ids') ->label('Items to restore (optional)') ->options(function (Get $get) { $backupSetId = $get('backup_set_id'); if (! $backupSetId) { return []; } return BackupItem::query() ->where('backup_set_id', $backupSetId) ->whereHas('backupSet', function ($query) { $tenantId = Tenant::current()->getKey(); $query->where('tenant_id', $tenantId); }) ->get() ->mapWithKeys(function (BackupItem $item) { $meta = static::typeMeta($item->policy_type); $typeLabel = $meta['label'] ?? $item->policy_type; $restore = $meta['restore'] ?? 'enabled'; $label = sprintf( '%s (%s • restore: %s)', $item->policy_identifier ?? $item->policy_type, $typeLabel, $restore ); return [$item->id => $label]; }); }) ->columns(2) ->helperText('Preview-only types stay in dry-run; leave empty to include all items.'), Forms\Components\Toggle::make('is_dry_run') ->label('Preview only (dry-run)') ->default(true), ]); } public static function table(Table $table): Table { return $table ->columns([ Tables\Columns\TextColumn::make('backupSet.name')->label('Backup set'), Tables\Columns\IconColumn::make('is_dry_run')->label('Dry-run')->boolean(), Tables\Columns\TextColumn::make('status')->badge(), Tables\Columns\TextColumn::make('started_at')->dateTime()->since(), Tables\Columns\TextColumn::make('completed_at')->dateTime()->since(), Tables\Columns\TextColumn::make('requested_by')->label('Requested by'), ]) ->filters([ Tables\Filters\TrashedFilter::make(), ]) ->actions([ Actions\ViewAction::make(), ActionGroup::make([ Actions\Action::make('archive') ->label('Archive') ->color('danger') ->icon('heroicon-o-archive-box-x-mark') ->requiresConfirmation() ->visible(fn (RestoreRun $record) => ! $record->trashed()) ->action(function (RestoreRun $record, \App\Services\Intune\AuditLogger $auditLogger) { $record->delete(); if ($record->tenant) { $auditLogger->log( tenant: $record->tenant, action: 'restore_run.deleted', resourceType: 'restore_run', resourceId: (string) $record->id, status: 'success', context: ['metadata' => ['backup_set_id' => $record->backup_set_id]] ); } Notification::make() ->title('Restore run archived') ->success() ->send(); }), Actions\Action::make('forceDelete') ->label('Force delete') ->color('danger') ->icon('heroicon-o-trash') ->requiresConfirmation() ->visible(fn (RestoreRun $record) => $record->trashed()) ->action(function (RestoreRun $record, \App\Services\Intune\AuditLogger $auditLogger) { if ($record->tenant) { $auditLogger->log( tenant: $record->tenant, action: 'restore_run.force_deleted', resourceType: 'restore_run', resourceId: (string) $record->id, status: 'success', context: ['metadata' => ['backup_set_id' => $record->backup_set_id]] ); } $record->forceDelete(); Notification::make() ->title('Restore run permanently deleted') ->success() ->send(); }), ])->icon('heroicon-o-ellipsis-vertical'), ]) ->bulkActions([]); } public static function infolist(Schema $schema): Schema { return $schema ->schema([ Infolists\Components\TextEntry::make('backupSet.name')->label('Backup set'), Infolists\Components\TextEntry::make('status')->badge(), Infolists\Components\TextEntry::make('is_dry_run') ->label('Dry-run') ->formatStateUsing(fn ($state) => $state ? 'Yes' : 'No') ->badge(), Infolists\Components\TextEntry::make('requested_by'), Infolists\Components\TextEntry::make('started_at')->dateTime(), Infolists\Components\TextEntry::make('completed_at')->dateTime(), Infolists\Components\ViewEntry::make('preview') ->label('Preview') ->view('filament.infolists.entries.restore-preview') ->state(fn ($record) => $record->preview ?? []), Infolists\Components\TextEntry::make('results') ->label('Results') ->formatStateUsing(fn ($state) => json_encode($state ?? [], JSON_PRETTY_PRINT)) ->copyable(), ]); } public static function getPages(): array { return [ 'index' => Pages\ListRestoreRuns::route('/'), 'create' => Pages\CreateRestoreRun::route('/create'), 'view' => Pages\ViewRestoreRun::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) ?? []; } public static function createRestoreRun(array $data): RestoreRun { /** @var Tenant $tenant */ $tenant = Tenant::current(); /** @var BackupSet $backupSet */ $backupSet = BackupSet::findOrFail($data['backup_set_id']); if ($backupSet->tenant_id !== $tenant->id) { abort(403, 'Backup set does not belong to the active tenant.'); } /** @var RestoreService $service */ $service = app(RestoreService::class); return $service->execute( tenant: $tenant, backupSet: $backupSet, selectedItemIds: $data['backup_item_ids'] ?? null, dryRun: (bool) ($data['is_dry_run'] ?? true), actorEmail: auth()->user()?->email, actorName: auth()->user()?->name, ); } }