239 lines
9.9 KiB
PHP
239 lines
9.9 KiB
PHP
<?php
|
|
|
|
namespace App\Filament\Resources;
|
|
|
|
use App\Filament\Resources\RestoreRunResource\Pages;
|
|
use App\Models\BackupItem;
|
|
use App\Models\BackupSet;
|
|
use App\Models\RestoreRun;
|
|
use App\Models\Tenant;
|
|
use App\Services\Intune\RestoreService;
|
|
use BackedEnum;
|
|
use Filament\Actions;
|
|
use Filament\Actions\ActionGroup;
|
|
use Filament\Forms;
|
|
use Filament\Infolists;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Resources\Resource;
|
|
use Filament\Schemas\Components\Utilities\Get;
|
|
use Filament\Schemas\Schema;
|
|
use Filament\Tables;
|
|
use Filament\Tables\Table;
|
|
use UnitEnum;
|
|
|
|
class RestoreRunResource extends Resource
|
|
{
|
|
protected static ?string $model = RestoreRun::class;
|
|
|
|
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-arrow-path-rounded-square';
|
|
|
|
protected static string|UnitEnum|null $navigationGroup = 'Backups & Restore';
|
|
|
|
public static function form(Schema $schema): Schema
|
|
{
|
|
return $schema
|
|
->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\ViewEntry::make('results')
|
|
->label('Results')
|
|
->view('filament.infolists.entries.restore-results')
|
|
->state(fn ($record) => $record->results ?? []),
|
|
]);
|
|
}
|
|
|
|
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<string,mixed>
|
|
*/
|
|
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,
|
|
);
|
|
}
|
|
}
|