'closeAddPoliciesModal', ]; public function closeAddPoliciesModal(): void { $this->unmountAction(); } /** * @param array $arguments * @param array $context */ public function mountAction(string $name, array $arguments = [], array $context = []): mixed { if (($context['table'] ?? false) === true) { $backupSet = $this->getOwnerRecord(); if ($name === 'remove' && filled($context['recordKey'] ?? null)) { $this->resolveOwnerScopedBackupItemId($backupSet, $context['recordKey']); } if ($name === 'bulk_remove' && ($context['bulk'] ?? false) === true) { $this->resolveOwnerScopedBackupItemIdsFromKeys($backupSet, $this->selectedTableRecords); } } return parent::mountAction($name, $arguments, $context); } public function table(Table $table): Table { $refreshTable = Actions\Action::make('refreshTable') ->label('Refresh') ->icon('heroicon-o-arrow-path') ->action(function (): void { $this->resetTable(); }); $addPolicies = Actions\Action::make('addPolicies') ->label('Add Policies') ->icon('heroicon-o-plus') ->tooltip('You do not have permission to add policies.') ->modalHeading('Add Policies') ->modalSubmitAction(false) ->modalCancelActionLabel('Close') ->modalContent(function (): View { $backupSet = $this->getOwnerRecord(); return view('filament.modals.backup-set-policy-picker', [ 'backupSetId' => $backupSet->getKey(), ]); }); UiEnforcement::forAction($addPolicies) ->requireCapability(Capabilities::TENANT_SYNC) ->tooltip('You do not have permission to add policies.') ->apply(); $removeItem = Actions\Action::make('remove') ->label('Remove') ->color('danger') ->icon('heroicon-o-x-mark') ->requiresConfirmation() ->action(function (mixed $record): void { $backupSet = $this->getOwnerRecord(); $user = auth()->user(); if (! $user instanceof User) { abort(403); } $tenant = $backupSet->tenant ?? Tenant::current(); if (! $tenant instanceof Tenant) { abort(404); } if ((int) $tenant->getKey() !== (int) $backupSet->tenant_id) { abort(404); } $backupItemIds = [$this->resolveOwnerScopedBackupItemId($backupSet, $record)]; /** @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)) { OpsUxBrowserEvents::dispatchRunEnqueued($this); OperationUxPresenter::alreadyQueuedToast((string) $opRun->type) ->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(); }); UiEnforcement::forAction($removeItem) ->requireCapability(Capabilities::TENANT_SYNC) ->tooltip('You do not have permission to remove policies.') ->apply(); $bulkRemove = 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 (! $tenant instanceof Tenant) { abort(404); } if ((int) $tenant->getKey() !== (int) $backupSet->tenant_id) { abort(404); } $backupItemIds = $this->resolveOwnerScopedBackupItemIdsFromKeys($backupSet, $this->selectedTableRecords); 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)) { OpsUxBrowserEvents::dispatchRunEnqueued($this); OperationUxPresenter::alreadyQueuedToast((string) $opRun->type) ->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(); }); UiEnforcement::forBulkAction($bulkRemove) ->requireCapability(Capabilities::TENANT_SYNC) ->tooltip('You do not have permission to remove policies.') ->apply(); return $table ->modifyQueryUsing(fn (Builder $query) => $query->with(['policy', 'policyVersion', 'policyVersion.policy'])) ->defaultSort('policy.display_name') ->paginated(TablePaginationProfiles::relationManager()) ->persistFiltersInSession() ->persistSearchInSession() ->persistSortInSession() ->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(TagBadgeRenderer::label(TagBadgeDomain::PolicyType)) ->color(TagBadgeRenderer::color(TagBadgeDomain::PolicyType)), Tables\Columns\TextColumn::make('restore_mode') ->label('Restore') ->badge() ->state(fn (BackupItem $record) => static::typeMeta($record->policy_type)['restore'] ?? 'enabled') ->formatStateUsing(BadgeRenderer::label(BadgeDomain::PolicyRestoreMode)) ->color(BadgeRenderer::color(BadgeDomain::PolicyRestoreMode)) ->icon(BadgeRenderer::icon(BadgeDomain::PolicyRestoreMode)) ->iconColor(BadgeRenderer::iconColor(BadgeDomain::PolicyRestoreMode)), Tables\Columns\TextColumn::make('risk') ->label('Risk') ->badge() ->state(fn (BackupItem $record) => static::typeMeta($record->policy_type)['risk'] ?? 'n/a') ->formatStateUsing(BadgeRenderer::label(BadgeDomain::PolicyRisk)) ->color(BadgeRenderer::color(BadgeDomain::PolicyRisk)) ->icon(BadgeRenderer::icon(BadgeDomain::PolicyRisk)) ->iconColor(BadgeRenderer::iconColor(BadgeDomain::PolicyRisk)), Tables\Columns\TextColumn::make('policy_identifier') ->label('Policy ID') ->copyable() ->toggleable(isToggledHiddenByDefault: true), Tables\Columns\TextColumn::make('platform') ->badge() ->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::Platform)) ->color(TagBadgeRenderer::color(TagBadgeDomain::Platform)), 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 '—'; }) ->toggleable(isToggledHiddenByDefault: true), Tables\Columns\TextColumn::make('captured_at')->dateTime()->toggleable(isToggledHiddenByDefault: true), Tables\Columns\TextColumn::make('created_at')->since()->toggleable(isToggledHiddenByDefault: true), ]) ->filters([ SelectFilter::make('policy_type') ->label('Type') ->options(FilterOptionCatalog::policyTypes()) ->searchable(), SelectFilter::make('restore_mode') ->label('Restore') ->options(static::restoreModeOptions()) ->query(fn (Builder $query, array $data): Builder => static::applyRestoreModeFilter($query, $data['value'] ?? null)), SelectFilter::make('platform') ->options(FilterOptionCatalog::platforms()) ->searchable(), ]) ->headerActions([ $refreshTable, $addPolicies, ]) ->actions([ Actions\ActionGroup::make([ Actions\ViewAction::make() ->label(fn (BackupItem $record): string => $record->policy_version_id ? 'View version' : 'View policy') ->url(function (BackupItem $record): ?string { $tenant = $this->getOwnerRecord()->tenant ?? \App\Models\Tenant::current(); if ($record->policy_version_id) { return PolicyVersionResource::getUrl('view', ['record' => $record->policy_version_id], tenant: $tenant); } if (! $record->policy_id) { return null; } return PolicyResource::getUrl('view', ['record' => $record->policy_id], tenant: $tenant); }) ->hidden(fn (BackupItem $record) => ! $record->policy_version_id && ! $record->policy_id) ->openUrlInNewTab(true), $removeItem, ]) ->label('More') ->icon('heroicon-o-ellipsis-vertical') ->color('gray'), ]) ->bulkActions([ Actions\BulkActionGroup::make([ $bulkRemove, ])->label('More'), ]) ->emptyStateHeading('No policies in this backup set') ->emptyStateDescription('Add policies to capture versions and assignments inside this backup set.') ->emptyStateActions([ $addPolicies->name('addPoliciesEmpty'), ]); } /** * @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 restoreModeOptions(): array { return collect(InventoryPolicyTypeMeta::all()) ->pluck('restore') ->filter(fn (mixed $value): bool => is_string($value) && trim($value) !== '') ->map(fn (string $value): string => trim($value)) ->unique() ->sort() ->mapWithKeys(fn (string $value): array => [ $value => BadgeRenderer::spec(BadgeDomain::PolicyRestoreMode, $value)->label, ]) ->all(); } private static function applyRestoreModeFilter(Builder $query, mixed $value): Builder { if (! is_string($value) || trim($value) === '') { return $query; } $types = collect(InventoryPolicyTypeMeta::all()) ->filter(fn (array $meta): bool => ($meta['restore'] ?? null) === $value) ->pluck('type') ->filter(fn (mixed $type): bool => is_string($type) && trim($type) !== '') ->map(fn (string $type): string => trim($type)) ->values() ->all(); if ($types === []) { return $query->whereRaw('1 = 0'); } return $query->whereIn('policy_type', $types); } private function resolveOwnerScopedBackupItemId(\App\Models\BackupSet $backupSet, mixed $record): int { $recordId = $this->normalizeBackupItemKey($record); if ($recordId <= 0) { abort(404); } $resolvedId = $backupSet->items() ->where('tenant_id', (int) $backupSet->tenant_id) ->whereKey($recordId) ->value('id'); if (! is_numeric($resolvedId) || (int) $resolvedId <= 0) { abort(404); } return (int) $resolvedId; } /** * @return array */ private function resolveOwnerScopedBackupItemIdsFromKeys(\App\Models\BackupSet $backupSet, array $recordKeys): array { $requestedIds = collect($recordKeys) ->map(fn (mixed $record): int => $this->normalizeBackupItemKey($record)) ->filter(fn (int $value): bool => $value > 0) ->unique() ->sort() ->values() ->all(); if ($requestedIds === []) { return []; } $resolvedIds = $backupSet->items() ->where('tenant_id', (int) $backupSet->tenant_id) ->whereIn('id', $requestedIds) ->pluck('id') ->map(fn (mixed $value): int => (int) $value) ->filter(fn (int $value): bool => $value > 0) ->unique() ->sort() ->values() ->all(); if (count($resolvedIds) !== count($requestedIds)) { abort(404); } return $resolvedIds; } private function normalizeBackupItemKey(mixed $record): int { if ($record instanceof BackupItem) { return (int) $record->getKey(); } return is_numeric($record) ? (int) $record : 0; } }