'closeAddPoliciesModal', ]; public function closeAddPoliciesModal(): void { $this->unmountAction(); $this->resetTable(); $this->pollUntil = now()->addSeconds(20)->getTimestamp(); } public function shouldPollTable(): bool { return $this->pollUntil !== null && now()->getTimestamp() < $this->pollUntil; } public function table(Table $table): Table { return $table ->modifyQueryUsing(fn (Builder $query) => $query->with('policyVersion')) ->poll(fn (): ?string => (filled($this->mountedActions) || ! $this->shouldPollTable()) ? null : '2s') ->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(function (BackupItem $record): ?string { if (! $record->policy_id) { return null; } $tenant = $this->getOwnerRecord()->tenant ?? \App\Models\Tenant::current(); return PolicyResource::getUrl('view', ['record' => $record->policy_id], tenant: $tenant); }) ->hidden(fn (BackupItem $record) => ! $record->policy_id) ->openUrlInNewTab(true), Actions\Action::make('remove') ->label('Remove') ->color('danger') ->icon('heroicon-o-x-mark') ->requiresConfirmation() ->action(function (BackupItem $record): void { $backupSet = $this->getOwnerRecord(); $user = auth()->user(); if (! $user instanceof User) { abort(403); } $tenant = $backupSet->tenant ?? Tenant::current(); if (! $user->canSyncTenant($tenant)) { abort(403); } if ((int) $tenant->getKey() !== (int) $backupSet->tenant_id) { abort(403); } $backupItemIds = [(int) $record->getKey()]; /** @var OperationRunService $opService */ $opService = app(OperationRunService::class); $opRun = $opService->ensureRun( tenant: $tenant, type: 'backup_set.remove_policies', inputs: [ 'backup_set_id' => (int) $backupSet->getKey(), 'backup_item_ids' => $backupItemIds, ], initiator: $user, ); if (! $opRun->wasRecentlyCreated && in_array($opRun->status, ['queued', 'running'], true)) { Notification::make() ->title('Removal already queued') ->body('A matching remove operation is already queued or running.') ->info() ->actions([ Actions\Action::make('view_run') ->label('View run') ->url(OperationRunLinks::view($opRun, $tenant)), ]) ->send(); return; } $opService->dispatchOrFail($opRun, function () use ($backupSet, $backupItemIds, $user, $opRun): void { RemovePoliciesFromBackupSetJob::dispatch( backupSetId: (int) $backupSet->getKey(), backupItemIds: $backupItemIds, initiatorUserId: (int) $user->getKey(), operationRun: $opRun, ); }); OpsUxBrowserEvents::dispatchRunEnqueued($this); OperationUxPresenter::queuedToast((string) $opRun->type) ->actions([ Actions\Action::make('view_run') ->label('View run') ->url(OperationRunLinks::view($opRun, $tenant)), ]) ->send(); $this->resetTable(); $this->pollUntil = now()->addSeconds(20)->getTimestamp(); }), ])->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() ->deselectRecordsAfterCompletion() ->action(function (Collection $records): void { if ($records->isEmpty()) { return; } $backupSet = $this->getOwnerRecord(); $user = auth()->user(); if (! $user instanceof User) { abort(403); } $tenant = $backupSet->tenant ?? Tenant::current(); if (! $user->canSyncTenant($tenant)) { abort(403); } if ((int) $tenant->getKey() !== (int) $backupSet->tenant_id) { abort(403); } $backupItemIds = $records ->pluck('id') ->map(fn (mixed $value): int => (int) $value) ->filter(fn (int $value): bool => $value > 0) ->unique() ->sort() ->values() ->all(); if ($backupItemIds === []) { return; } /** @var OperationRunService $opService */ $opService = app(OperationRunService::class); $opRun = $opService->ensureRun( tenant: $tenant, type: 'backup_set.remove_policies', inputs: [ 'backup_set_id' => (int) $backupSet->getKey(), 'backup_item_ids' => $backupItemIds, ], initiator: $user, ); if (! $opRun->wasRecentlyCreated && in_array($opRun->status, ['queued', 'running'], true)) { Notification::make() ->title('Removal already queued') ->body('A matching remove operation is already queued or running.') ->info() ->actions([ Actions\Action::make('view_run') ->label('View run') ->url(OperationRunLinks::view($opRun, $tenant)), ]) ->send(); return; } $opService->dispatchOrFail($opRun, function () use ($backupSet, $backupItemIds, $user, $opRun): void { RemovePoliciesFromBackupSetJob::dispatch( backupSetId: (int) $backupSet->getKey(), backupItemIds: $backupItemIds, initiatorUserId: (int) $user->getKey(), operationRun: $opRun, ); }); OpsUxBrowserEvents::dispatchRunEnqueued($this); OperationUxPresenter::queuedToast((string) $opRun->type) ->actions([ Actions\Action::make('view_run') ->label('View run') ->url(OperationRunLinks::view($opRun, $tenant)), ]) ->send(); $this->resetTable(); $this->pollUntil = now()->addSeconds(20)->getTimestamp(); }), ]), ]); } /** * @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) ?? []; } }