TenantAtlas/app/Filament/Resources/BackupSetResource/RelationManagers/BackupItemsRelationManager.php
ahmido 76e10fc404 015-policy-picker-ux (#21)
Replaces the “Add Policies” picker with a modal table (search, pagination, multi-select).
Adds filters: policy type, platform, last synced, ignored, has versions; “Select all” applies to the current filtered results.
Improves identifiers shown (short external id), and fixes has-versions filtering behavior.
Backup set items table: groups row actions (View policy / Remove) into an action group.
Adds bulk action to remove multiple backup items at once.
Updates/adds tests covering the picker table bulk add and backup items bulk remove.

Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.local>
Reviewed-on: #21
2026-01-02 13:59:15 +00:00

216 lines
9.5 KiB
PHP

<?php
namespace App\Filament\Resources\BackupSetResource\RelationManagers;
use App\Filament\Resources\PolicyResource;
use App\Models\BackupItem;
use App\Services\Intune\AuditLogger;
use Filament\Actions;
use Filament\Notifications\Notification;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
class BackupItemsRelationManager extends RelationManager
{
protected static string $relationship = 'items';
public function table(Table $table): Table
{
return $table
->modifyQueryUsing(fn (Builder $query) => $query->with('policyVersion'))
->columns([
Tables\Columns\TextColumn::make('policy.display_name')
->label('Item')
->sortable()
->searchable()
->getStateUsing(fn (BackupItem $record) => $record->resolvedDisplayName()),
Tables\Columns\TextColumn::make('policyVersion.version_number')
->label('Version')
->badge()
->default('—')
->getStateUsing(fn (BackupItem $record): ?int => $record->policyVersion?->version_number),
Tables\Columns\TextColumn::make('policy_type')
->label('Type')
->badge()
->formatStateUsing(fn (?string $state) => static::typeMeta($state)['label'] ?? $state),
Tables\Columns\TextColumn::make('restore_mode')
->label('Restore')
->badge()
->state(fn (BackupItem $record) => static::typeMeta($record->policy_type)['restore'] ?? 'enabled')
->color(fn (?string $state) => $state === 'preview-only' ? 'warning' : 'success'),
Tables\Columns\TextColumn::make('risk')
->label('Risk')
->badge()
->state(fn (BackupItem $record) => static::typeMeta($record->policy_type)['risk'] ?? 'n/a')
->color(fn (?string $state) => str_contains((string) $state, 'high') ? 'danger' : 'gray'),
Tables\Columns\TextColumn::make('policy_identifier')
->label('Policy ID')
->copyable(),
Tables\Columns\TextColumn::make('platform')->badge(),
Tables\Columns\TextColumn::make('assignments')
->label('Assignments')
->badge()
->color('info')
->getStateUsing(function (BackupItem $record): string {
$assignments = $record->policyVersion?->assignments ?? $record->assignments;
if (is_array($assignments)) {
return (string) count($assignments);
}
$assignmentsFetched = $record->policyVersion?->metadata['assignments_fetched']
?? $record->metadata['assignments_fetched']
?? false;
return $assignmentsFetched ? '0' : '—';
}),
Tables\Columns\TextColumn::make('scope_tags')
->label('Scope Tags')
->default('—')
->getStateUsing(function (BackupItem $record): array {
$tags = $record->policyVersion?->scope_tags['names']
?? $record->metadata['scope_tag_names']
?? [];
return is_array($tags) ? $tags : [];
})
->formatStateUsing(function ($state): string {
if (is_array($state)) {
return $state === [] ? '—' : implode(', ', $state);
}
if (is_string($state) && $state !== '') {
return $state;
}
return '—';
}),
Tables\Columns\TextColumn::make('captured_at')->dateTime(),
Tables\Columns\TextColumn::make('created_at')->since(),
])
->filters([])
->headerActions([
Actions\Action::make('addPolicies')
->label('Add Policies')
->icon('heroicon-o-plus')
->modalHeading('Add Policies')
->modalSubmitAction(false)
->modalCancelActionLabel('Close')
->modalContent(function (): View {
$backupSet = $this->getOwnerRecord();
return view('filament.modals.backup-set-policy-picker', [
'backupSetId' => $backupSet->getKey(),
]);
}),
])
->actions([
Actions\ActionGroup::make([
Actions\ViewAction::make()
->label('View policy')
->url(fn ($record) => $record->policy_id ? PolicyResource::getUrl('view', ['record' => $record->policy_id]) : null)
->hidden(fn ($record) => ! $record->policy_id)
->openUrlInNewTab(true),
Actions\Action::make('remove')
->label('Remove')
->color('danger')
->icon('heroicon-o-x-mark')
->requiresConfirmation()
->action(function (BackupItem $record, AuditLogger $auditLogger) {
$record->delete();
if ($record->backupSet) {
$record->backupSet->update([
'item_count' => $record->backupSet->items()->count(),
]);
}
if ($record->tenant) {
$auditLogger->log(
tenant: $record->tenant,
action: 'backup.item_removed',
resourceType: 'backup_set',
resourceId: (string) $record->backup_set_id,
status: 'success',
context: ['metadata' => ['policy_id' => $record->policy_id]]
);
}
Notification::make()
->title('Policy removed from backup')
->success()
->send();
}),
])->icon('heroicon-o-ellipsis-vertical'),
])
->bulkActions([
Actions\BulkActionGroup::make([
Actions\BulkAction::make('bulk_remove')
->label('Remove selected')
->icon('heroicon-o-x-mark')
->color('danger')
->requiresConfirmation()
->action(function (Collection $records, AuditLogger $auditLogger) {
if ($records->isEmpty()) {
return;
}
$backupSet = $this->getOwnerRecord();
$records->each(fn (BackupItem $record) => $record->delete());
$backupSet->update([
'item_count' => $backupSet->items()->count(),
]);
$tenant = $records->first()?->tenant;
if ($tenant) {
$auditLogger->log(
tenant: $tenant,
action: 'backup.items_removed',
resourceType: 'backup_set',
resourceId: (string) $backupSet->id,
status: 'success',
context: [
'metadata' => [
'removed_count' => $records->count(),
'policy_ids' => $records->pluck('policy_id')->filter()->values()->all(),
'policy_identifiers' => $records->pluck('policy_identifier')->filter()->values()->all(),
],
]
);
}
Notification::make()
->title('Policies removed from backup')
->success()
->send();
}),
]),
]);
}
/**
* @return array{label:?string,category:?string,restore:?string,risk:?string}|array<string,mixed>
*/
private static function typeMeta(?string $type): array
{
if ($type === null) {
return [];
}
$types = array_merge(
config('tenantpilot.supported_policy_types', []),
config('tenantpilot.foundation_types', [])
);
return collect($types)
->firstWhere('type', $type) ?? [];
}
}