backupSetId = $backupSetId; } public static function externalIdShort(?string $externalId): string { $value = (string) ($externalId ?? ''); $normalized = preg_replace('/[^A-Za-z0-9]/', '', $value) ?? ''; if ($normalized === '') { return '—'; } return substr($normalized, -8); } public function table(Table $table): Table { $backupSet = BackupSet::query()->find($this->backupSetId); $tenantId = $backupSet?->tenant_id ?? Tenant::current()->getKey(); $existingPolicyIds = $backupSet ? $backupSet->items()->pluck('policy_id')->filter()->all() : []; return $table ->queryStringIdentifier('backupSetPolicyPicker'.Str::studly((string) $this->backupSetId)) ->query( Policy::query() ->where('tenant_id', $tenantId) ->when($existingPolicyIds !== [], fn (Builder $query) => $query->whereNotIn('id', $existingPolicyIds)) ) ->deferLoading(! app()->runningUnitTests()) ->paginated([25, 50, 100]) ->defaultPaginationPageOption(25) ->searchable() ->striped() ->columns([ TextColumn::make('display_name') ->label('Name') ->searchable() ->sortable() ->wrap(), TextColumn::make('policy_type') ->label('Type') ->badge() ->formatStateUsing(fn (?string $state): string => (string) (static::typeMeta($state)['label'] ?? $state ?? '—')), TextColumn::make('platform') ->label('Platform') ->badge() ->default('—') ->sortable(), TextColumn::make('external_id') ->label('External ID') ->formatStateUsing(fn (?string $state): string => static::externalIdShort($state)) ->tooltip(fn (?string $state): ?string => filled($state) ? $state : null) ->extraAttributes(['class' => 'font-mono text-xs']) ->toggleable(), TextColumn::make('versions_count') ->label('Versions') ->state(fn (Policy $record): int => (int) ($record->versions_count ?? 0)) ->badge() ->sortable(), TextColumn::make('last_synced_at') ->label('Last synced') ->dateTime() ->since() ->sortable() ->toggleable(), TextColumn::make('ignored_at') ->label('Ignored') ->badge() ->formatStateUsing(BadgeRenderer::label(BadgeDomain::IgnoredAt)) ->color(BadgeRenderer::color(BadgeDomain::IgnoredAt)) ->icon(BadgeRenderer::icon(BadgeDomain::IgnoredAt)) ->iconColor(BadgeRenderer::iconColor(BadgeDomain::IgnoredAt)) ->toggleable(isToggledHiddenByDefault: true), ]) ->modifyQueryUsing(fn (Builder $query) => $query->withCount('versions')) ->filters([ SelectFilter::make('policy_type') ->label('Policy type') ->options(static::policyTypeOptions()), SelectFilter::make('platform') ->label('Platform') ->options(fn (): array => Policy::query() ->where('tenant_id', $tenantId) ->whereNotNull('platform') ->distinct() ->orderBy('platform') ->pluck('platform', 'platform') ->all()), SelectFilter::make('synced_within') ->label('Last synced') ->options([ '7' => 'Within 7 days', '30' => 'Within 30 days', '90' => 'Within 90 days', 'any' => 'Any time', ]) ->default('7') ->query(function (Builder $query, array $data): Builder { $value = (string) ($data['value'] ?? '7'); if ($value === 'any') { return $query; } $days = is_numeric($value) ? (int) $value : 7; return $query->where('last_synced_at', '>', now()->subDays(max(1, $days))); }), TernaryFilter::make('ignored') ->label('Ignored') ->nullable() ->queries( true: fn (Builder $query) => $query->whereNotNull('ignored_at'), false: fn (Builder $query) => $query->whereNull('ignored_at'), ) ->default(false), SelectFilter::make('has_versions') ->label('Has versions') ->options([ '1' => 'Has versions', '0' => 'No versions', ]) ->query(function (Builder $query, array $data): Builder { $value = $data['value'] ?? null; if ($value === null || $value === '') { return $query; } return match ((string) $value) { '1' => $query->whereHas('versions'), '0' => $query->whereDoesntHave('versions'), default => $query, }; }), ]) ->bulkActions([ BulkAction::make('add_selected_to_backup_set') ->label('Add selected') ->icon('heroicon-m-plus') ->authorize(function (): bool { $this->dispatch(OpsUxBrowserEvents::RunEnqueued); $user = auth()->user(); if (! $user instanceof User) { return false; } try { $tenant = Tenant::current(); } catch (\RuntimeException) { return false; } if (! $user->canSyncTenant($tenant)) { return false; } return BackupSet::query() ->whereKey($this->backupSetId) ->where('tenant_id', $tenant->getKey()) ->exists(); }) ->action(function (Collection $records): void { $backupSet = BackupSet::query()->findOrFail($this->backupSetId); $tenant = null; try { $tenant = Tenant::current(); } catch (\RuntimeException) { $tenant = $backupSet->tenant; } $user = auth()->user(); if (! $user instanceof User) { Notification::make() ->title('Not allowed') ->danger() ->send(); return; } if (! $tenant instanceof Tenant) { Notification::make() ->title('Not allowed') ->danger() ->send(); return; } if ((int) $tenant->getKey() !== (int) $backupSet->tenant_id) { Notification::make() ->title('Not allowed') ->danger() ->send(); return; } if (! $user->canSyncTenant($tenant)) { Notification::make() ->title('Not allowed') ->danger() ->send(); return; } $policyIds = $records ->pluck('id') ->map(fn (mixed $value): int => (int) $value) ->filter(fn (int $value): bool => $value > 0) ->unique() ->values() ->all(); if ($policyIds === []) { Notification::make() ->title('No policies selected') ->warning() ->send(); return; } sort($policyIds); /** @var BulkSelectionIdentity $selection */ $selection = app(BulkSelectionIdentity::class); $selectionIdentity = $selection->fromIds($policyIds); /** @var OperationRunService $opService */ $opService = app(OperationRunService::class); $opRun = $opService->enqueueBulkOperation( tenant: $tenant, type: 'backup_set.add_policies', targetScope: [ 'entra_tenant_id' => (string) ($tenant->tenant_id ?? $tenant->external_id), ], selectionIdentity: $selectionIdentity, dispatcher: function ($operationRun) use ($tenant, $user, $backupSet, $policyIds): void { $fingerprint = (string) data_get($operationRun?->context ?? [], 'idempotency.fingerprint', ''); AddPoliciesToBackupSetJob::dispatch( tenantId: (int) $tenant->getKey(), userId: (int) $user->getKey(), backupSetId: (int) $backupSet->getKey(), policyIds: $policyIds, options: [ 'include_assignments' => (bool) $this->include_assignments, 'include_scope_tags' => (bool) $this->include_scope_tags, 'include_foundations' => (bool) $this->include_foundations, ], idempotencyKey: $fingerprint, operationRun: $operationRun, ); }, initiator: $user, extraContext: [ 'backup_set_id' => (int) $backupSet->getKey(), 'policy_count' => count($policyIds), ], ); if (! $opRun->wasRecentlyCreated && in_array($opRun->status, ['queued', 'running'], true)) { Notification::make() ->title('Add policies already queued') ->body('A matching run is already queued or running. Open the run to monitor progress.') ->actions([ \Filament\Actions\Action::make('view_run') ->label('View run') ->url(OperationRunLinks::view($opRun, $tenant)), ]) ->info() ->send(); return; } OperationUxPresenter::queuedToast((string) $opRun->type) ->actions([ \Filament\Actions\Action::make('view_run') ->label('View run') ->url(OperationRunLinks::view($opRun, $tenant)), ]) ->send(); $this->dispatch('backup-set-policy-picker:close') ->to(\App\Filament\Resources\BackupSetResource\RelationManagers\BackupItemsRelationManager::class); }), ]); } public function render(): View { return view('livewire.backup-set-policy-picker-table'); } /** * @return array{label:?string,category:?string,restore:?string,risk:?string}|array */ 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) ?? []; } /** * @return array */ private static function policyTypeOptions(): array { $types = array_merge( config('tenantpilot.supported_policy_types', []), config('tenantpilot.foundation_types', []) ); return collect($types) ->mapWithKeys(function (array $meta): array { $type = (string) ($meta['type'] ?? ''); if ($type === '') { return []; } $label = (string) ($meta['label'] ?? $type); return [$type => $label]; }) ->all(); } }