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() ->color(fn (?string $state): string => filled($state) ? 'warning' : 'gray') ->formatStateUsing(fn (?string $state): string => filled($state) ? 'yes' : 'no') ->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') ->action(function (Collection $records, BackupService $service): void { $backupSet = BackupSet::query()->findOrFail($this->backupSetId); $tenant = $backupSet->tenant ?? Tenant::current(); $beforeFailures = (array) (($backupSet->metadata ?? [])['failures'] ?? []); $beforeFailureCount = count($beforeFailures); $policyIds = $records->pluck('id')->all(); if ($policyIds === []) { Notification::make() ->title('No policies selected') ->warning() ->send(); return; } $service->addPoliciesToSet( tenant: $tenant, backupSet: $backupSet, policyIds: $policyIds, actorEmail: auth()->user()?->email, actorName: auth()->user()?->name, includeAssignments: $this->include_assignments, includeScopeTags: $this->include_scope_tags, includeFoundations: $this->include_foundations, ); $notificationTitle = $this->include_foundations ? 'Backup items added' : 'Policies added to backup'; $backupSet->refresh(); $afterFailures = (array) (($backupSet->metadata ?? [])['failures'] ?? []); $afterFailureCount = count($afterFailures); if ($afterFailureCount > $beforeFailureCount) { Notification::make() ->title($notificationTitle.' with failures') ->body('Some policies could not be captured from Microsoft Graph. Check the backup set failures list for details.') ->warning() ->send(); } else { Notification::make() ->title($notificationTitle) ->success() ->send(); } $this->resetTable(); }), ]); } 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(); } }