TenantAtlas/app/Filament/Resources/PolicyVersionResource.php
2026-01-28 22:04:45 +01:00

840 lines
43 KiB
PHP

<?php
namespace App\Filament\Resources;
use App\Filament\Resources\PolicyVersionResource\Pages;
use App\Jobs\BulkPolicyVersionForceDeleteJob;
use App\Jobs\BulkPolicyVersionPruneJob;
use App\Jobs\BulkPolicyVersionRestoreJob;
use App\Models\BackupItem;
use App\Models\BackupSet;
use App\Models\PolicyVersion;
use App\Models\Tenant;
use App\Models\User;
use App\Services\Intune\AuditLogger;
use App\Services\Intune\PolicyNormalizer;
use App\Services\Intune\VersionDiff;
use App\Services\OperationRunService;
use App\Services\Operations\BulkSelectionIdentity;
use App\Support\Auth\Capabilities;
use App\Support\Badges\TagBadgeDomain;
use App\Support\Badges\TagBadgeRenderer;
use App\Support\OperationRunLinks;
use App\Support\OpsUx\OperationUxPresenter;
use BackedEnum;
use Carbon\CarbonImmutable;
use Filament\Actions;
use Filament\Actions\BulkAction;
use Filament\Actions\BulkActionGroup;
use Filament\Forms;
use Filament\Infolists;
use Filament\Notifications\Notification;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Filters\TrashedFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Gate;
use UnitEnum;
class PolicyVersionResource extends Resource
{
protected static ?string $model = PolicyVersion::class;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-list-bullet';
protected static string|UnitEnum|null $navigationGroup = 'Inventory';
public static function infolist(Schema $schema): Schema
{
return $schema
->schema([
Infolists\Components\TextEntry::make('policy.display_name')->label('Policy'),
Infolists\Components\TextEntry::make('version_number')->label('Version'),
Infolists\Components\TextEntry::make('policy_type')
->badge()
->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::PolicyType))
->color(TagBadgeRenderer::color(TagBadgeDomain::PolicyType)),
Infolists\Components\TextEntry::make('platform')
->badge()
->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::Platform))
->color(TagBadgeRenderer::color(TagBadgeDomain::Platform)),
Infolists\Components\TextEntry::make('created_by')->label('Actor'),
Infolists\Components\TextEntry::make('captured_at')->dateTime(),
Tabs::make()
->activeTab(1)
->persistTabInQueryString('tab')
->columnSpanFull()
->tabs([
Tab::make('Normalized settings')
->id('normalized-settings')
->schema([
Infolists\Components\ViewEntry::make('normalized_settings_catalog')
->view('filament.infolists.entries.normalized-settings')
->state(function (PolicyVersion $record) {
$normalized = app(PolicyNormalizer::class)->normalize(
is_array($record->snapshot) ? $record->snapshot : [],
$record->policy_type ?? '',
$record->platform
);
$normalized['context'] = 'version';
$normalized['record_id'] = (string) $record->getKey();
return $normalized;
})
->visible(fn (PolicyVersion $record) => in_array($record->policy_type, ['settingsCatalogPolicy', 'endpointSecurityPolicy', 'securityBaselinePolicy'], true)),
Infolists\Components\ViewEntry::make('normalized_settings_standard')
->view('filament.infolists.entries.policy-settings-standard')
->state(function (PolicyVersion $record) {
$normalized = app(PolicyNormalizer::class)->normalize(
is_array($record->snapshot) ? $record->snapshot : [],
$record->policy_type ?? '',
$record->platform
);
$normalized['context'] = 'version';
$normalized['record_id'] = (string) $record->getKey();
$normalized['policy_type'] = $record->policy_type;
return $normalized;
})
->visible(fn (PolicyVersion $record) => ! in_array($record->policy_type, ['settingsCatalogPolicy', 'endpointSecurityPolicy', 'securityBaselinePolicy'], true)),
]),
Tab::make('Raw JSON')
->id('raw-json')
->schema([
Infolists\Components\ViewEntry::make('snapshot_pretty')
->view('filament.infolists.entries.snapshot-json')
->state(fn (PolicyVersion $record) => $record->snapshot ?? []),
]),
Tab::make('Diff')
->id('diff')
->schema([
Infolists\Components\ViewEntry::make('normalized_diff')
->view('filament.infolists.entries.normalized-diff')
->state(function (PolicyVersion $record) {
$normalizer = app(PolicyNormalizer::class);
$diff = app(VersionDiff::class);
$previous = $record->previous();
$from = $previous
? $normalizer->flattenForDiff($previous->snapshot ?? [], $previous->policy_type ?? '', $previous->platform)
: [];
$to = $normalizer->flattenForDiff($record->snapshot ?? [], $record->policy_type ?? '', $record->platform);
$result = $diff->compare($from, $to);
$result['policy_type'] = $record->policy_type;
return $result;
}),
Infolists\Components\ViewEntry::make('diff_json')
->label('Raw diff (advanced)')
->view('filament.infolists.entries.snapshot-json')
->state(function (PolicyVersion $record) {
$previous = $record->previous();
if (! $previous) {
return ['summary' => 'No previous version'];
}
$diff = app(VersionDiff::class)->compare(
$previous->snapshot ?? [],
$record->snapshot ?? []
);
$filter = static fn (array $items): array => array_filter(
$items,
static fn (mixed $value, string $key): bool => ! str_contains($key, '@odata.context'),
ARRAY_FILTER_USE_BOTH
);
$added = $filter($diff['added'] ?? []);
$removed = $filter($diff['removed'] ?? []);
$changed = $filter($diff['changed'] ?? []);
return [
'summary' => [
'added' => count($added),
'removed' => count($removed),
'changed' => count($changed),
'message' => sprintf(
'%d added, %d removed, %d changed',
count($added),
count($removed),
count($changed)
),
],
'added' => $added,
'removed' => $removed,
'changed' => $changed,
];
}),
]),
]),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('policy.display_name')->label('Policy')->sortable()->searchable(),
Tables\Columns\TextColumn::make('version_number')->sortable(),
Tables\Columns\TextColumn::make('policy_type')
->badge()
->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::PolicyType))
->color(TagBadgeRenderer::color(TagBadgeDomain::PolicyType)),
Tables\Columns\TextColumn::make('platform')
->badge()
->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::Platform))
->color(TagBadgeRenderer::color(TagBadgeDomain::Platform)),
Tables\Columns\TextColumn::make('created_by')->label('Actor'),
Tables\Columns\TextColumn::make('captured_at')->dateTime()->sortable(),
])
->filters([
TrashedFilter::make()
->label('Archived')
->placeholder('Active')
->trueLabel('All')
->falseLabel('Archived'),
])
->actions([
Actions\ViewAction::make(),
Actions\ActionGroup::make([
Actions\Action::make('restore_via_wizard')
->label('Restore via Wizard')
->icon('heroicon-o-arrow-path-rounded-square')
->color('primary')
->disabled(fn (PolicyVersion $record): bool => ($record->metadata['source'] ?? null) === 'metadata_only'
|| ! (($tenant = Tenant::current()) instanceof Tenant && Gate::allows(Capabilities::TENANT_MANAGE, $tenant)))
->tooltip(function (PolicyVersion $record): ?string {
if (! (($tenant = Tenant::current()) instanceof Tenant && Gate::allows(Capabilities::TENANT_MANAGE, $tenant))) {
return 'You do not have permission to create restore runs.';
}
if (($record->metadata['source'] ?? null) === 'metadata_only') {
return 'Disabled for metadata-only snapshots (Graph did not provide policy settings).';
}
return null;
})
->requiresConfirmation()
->modalHeading(fn (PolicyVersion $record): string => "Restore version {$record->version_number} via wizard?")
->modalSubheading('Creates a 1-item backup set from this snapshot and opens the restore run wizard prefilled.')
->action(function (PolicyVersion $record) {
$tenant = Tenant::current();
$user = auth()->user();
abort_unless($tenant instanceof Tenant && Gate::allows(Capabilities::TENANT_MANAGE, $tenant), 403);
if (! $tenant || $record->tenant_id !== $tenant->id) {
Notification::make()
->title('Policy version belongs to a different tenant')
->danger()
->send();
return;
}
$policy = $record->policy;
if (! $policy) {
Notification::make()
->title('Policy could not be found for this version')
->danger()
->send();
return;
}
$backupSet = BackupSet::create([
'tenant_id' => $tenant->id,
'name' => sprintf(
'Policy Version Restore • %s • v%d',
$policy->display_name,
$record->version_number
),
'created_by' => $user?->email,
'status' => 'completed',
'item_count' => 1,
'completed_at' => CarbonImmutable::now(),
'metadata' => [
'source' => 'policy_version',
'policy_version_id' => $record->id,
'policy_version_number' => $record->version_number,
'policy_id' => $policy->id,
],
]);
$scopeTags = is_array($record->scope_tags) ? $record->scope_tags : [];
$scopeTagIds = $scopeTags['ids'] ?? null;
$scopeTagNames = $scopeTags['names'] ?? null;
$backupItemMetadata = [
'source' => 'policy_version',
'display_name' => $policy->display_name,
'policy_version_id' => $record->id,
'policy_version_number' => $record->version_number,
'version_captured_at' => $record->captured_at?->toIso8601String(),
];
if (is_array($scopeTagIds) && $scopeTagIds !== []) {
$backupItemMetadata['scope_tag_ids'] = $scopeTagIds;
}
if (is_array($scopeTagNames) && $scopeTagNames !== []) {
$backupItemMetadata['scope_tag_names'] = $scopeTagNames;
}
$backupItem = BackupItem::create([
'tenant_id' => $tenant->id,
'backup_set_id' => $backupSet->id,
'policy_id' => $policy->id,
'policy_version_id' => $record->id,
'policy_identifier' => $policy->external_id,
'policy_type' => $policy->policy_type,
'platform' => $policy->platform,
'captured_at' => $record->captured_at ?? CarbonImmutable::now(),
'payload' => $record->snapshot ?? [],
'metadata' => $backupItemMetadata,
'assignments' => $record->assignments,
]);
return redirect()->to(RestoreRunResource::getUrl('create', [
'backup_set_id' => $backupSet->id,
'scope_mode' => 'selected',
'backup_item_ids' => [$backupItem->id],
]));
}),
Actions\Action::make('archive')
->label('Archive')
->color('danger')
->icon('heroicon-o-archive-box-x-mark')
->requiresConfirmation()
->visible(fn (PolicyVersion $record) => ! $record->trashed())
->disabled(function (PolicyVersion $record): bool {
$user = auth()->user();
$tenant = $record->tenant;
if (! $user instanceof User || ! $tenant instanceof Tenant) {
return true;
}
return Gate::forUser($user)->denies(Capabilities::TENANT_MANAGE, $tenant);
})
->tooltip(function (PolicyVersion $record): ?string {
$user = auth()->user();
$tenant = $record->tenant;
if (! $user instanceof User || ! $tenant instanceof Tenant) {
return null;
}
return Gate::forUser($user)->allows(Capabilities::TENANT_MANAGE, $tenant)
? null
: 'You do not have permission to manage policy versions.';
})
->action(function (PolicyVersion $record, AuditLogger $auditLogger) {
$user = auth()->user();
$tenant = $record->tenant;
abort_unless($user instanceof User && $tenant instanceof Tenant, 403);
abort_unless(Gate::forUser($user)->allows(Capabilities::TENANT_MANAGE, $tenant), 403);
$record->delete();
if ($record->tenant) {
$auditLogger->log(
tenant: $record->tenant,
action: 'policy_version.deleted',
resourceType: 'policy_version',
resourceId: (string) $record->id,
status: 'success',
context: ['metadata' => ['policy_id' => $record->policy_id, 'version' => $record->version_number]]
);
}
Notification::make()
->title('Policy version archived')
->success()
->send();
}),
Actions\Action::make('forceDelete')
->label('Force delete')
->color('danger')
->icon('heroicon-o-trash')
->requiresConfirmation()
->visible(fn (PolicyVersion $record) => $record->trashed())
->disabled(function (PolicyVersion $record): bool {
$user = auth()->user();
$tenant = $record->tenant;
if (! $user instanceof User || ! $tenant instanceof Tenant) {
return true;
}
return Gate::forUser($user)->denies(Capabilities::TENANT_MANAGE, $tenant);
})
->tooltip(function (PolicyVersion $record): ?string {
$user = auth()->user();
$tenant = $record->tenant;
if (! $user instanceof User || ! $tenant instanceof Tenant) {
return null;
}
return Gate::forUser($user)->allows(Capabilities::TENANT_MANAGE, $tenant)
? null
: 'You do not have permission to manage policy versions.';
})
->action(function (PolicyVersion $record, AuditLogger $auditLogger) {
$user = auth()->user();
$tenant = $record->tenant;
abort_unless($user instanceof User && $tenant instanceof Tenant, 403);
abort_unless(Gate::forUser($user)->allows(Capabilities::TENANT_MANAGE, $tenant), 403);
if ($record->tenant) {
$auditLogger->log(
tenant: $record->tenant,
action: 'policy_version.force_deleted',
resourceType: 'policy_version',
resourceId: (string) $record->id,
status: 'success',
context: ['metadata' => ['policy_id' => $record->policy_id, 'version' => $record->version_number]]
);
}
$record->forceDelete();
Notification::make()
->title('Policy version permanently deleted')
->success()
->send();
}),
Actions\Action::make('restore')
->label('Restore')
->color('success')
->icon('heroicon-o-arrow-uturn-left')
->requiresConfirmation()
->visible(fn (PolicyVersion $record) => $record->trashed())
->disabled(function (PolicyVersion $record): bool {
$user = auth()->user();
$tenant = $record->tenant;
if (! $user instanceof User || ! $tenant instanceof Tenant) {
return true;
}
return Gate::forUser($user)->denies(Capabilities::TENANT_MANAGE, $tenant);
})
->tooltip(function (PolicyVersion $record): ?string {
$user = auth()->user();
$tenant = $record->tenant;
if (! $user instanceof User || ! $tenant instanceof Tenant) {
return null;
}
return Gate::forUser($user)->allows(Capabilities::TENANT_MANAGE, $tenant)
? null
: 'You do not have permission to manage policy versions.';
})
->action(function (PolicyVersion $record, AuditLogger $auditLogger) {
$user = auth()->user();
$tenant = $record->tenant;
abort_unless($user instanceof User && $tenant instanceof Tenant, 403);
abort_unless(Gate::forUser($user)->allows(Capabilities::TENANT_MANAGE, $tenant), 403);
$record->restore();
if ($record->tenant) {
$auditLogger->log(
tenant: $record->tenant,
action: 'policy_version.restored',
resourceType: 'policy_version',
resourceId: (string) $record->id,
status: 'success',
context: ['metadata' => ['policy_id' => $record->policy_id, 'version' => $record->version_number]]
);
}
Notification::make()
->title('Policy version restored')
->success()
->send();
}),
])->icon('heroicon-o-ellipsis-vertical'),
])
->bulkActions([
BulkActionGroup::make([
BulkAction::make('bulk_prune_versions')
->label('Prune Versions')
->icon('heroicon-o-trash')
->color('danger')
->requiresConfirmation()
->modalDescription('Only versions captured more than the specified retention window (in days) are eligible. Newer versions will be skipped.')
->hidden(function (HasTable $livewire): bool {
$trashedFilterState = $livewire->getTableFilterState(TrashedFilter::class) ?? [];
$value = $trashedFilterState['value'] ?? null;
$isOnlyTrashed = in_array($value, [0, '0', false], true);
return $isOnlyTrashed;
})
->disabled(function (): bool {
$tenant = Tenant::current();
$user = auth()->user();
if (! $tenant instanceof Tenant || ! $user instanceof User) {
return true;
}
return Gate::forUser($user)->denies(Capabilities::TENANT_MANAGE, $tenant);
})
->tooltip(function (): ?string {
$tenant = Tenant::current();
$user = auth()->user();
if (! $tenant instanceof Tenant || ! $user instanceof User) {
return null;
}
return Gate::forUser($user)->allows(Capabilities::TENANT_MANAGE, $tenant)
? null
: 'You do not have permission to manage policy versions.';
})
->form(function (Collection $records) {
$fields = [
Forms\Components\TextInput::make('retention_days')
->label('Retention Days')
->helperText('Versions captured within the last N days will be skipped.')
->numeric()
->required()
->default(90)
->minValue(1),
];
if ($records->count() >= 20) {
$fields[] = Forms\Components\TextInput::make('confirmation')
->label('Type DELETE to confirm')
->required()
->in(['DELETE'])
->validationMessages([
'in' => 'Please type DELETE to confirm.',
]);
}
return $fields;
})
->action(function (Collection $records, array $data) {
$tenant = Tenant::current();
$user = auth()->user();
$count = $records->count();
$ids = $records->pluck('id')->toArray();
$retentionDays = (int) ($data['retention_days'] ?? 90);
if (! $tenant instanceof Tenant) {
return;
}
abort_unless($user instanceof User, 403);
abort_unless(Gate::forUser($user)->allows(Capabilities::TENANT_MANAGE, $tenant), 403);
$initiator = $user instanceof User ? $user : null;
/** @var BulkSelectionIdentity $selection */
$selection = app(BulkSelectionIdentity::class);
$selectionIdentity = $selection->fromIds($ids);
/** @var OperationRunService $runs */
$runs = app(OperationRunService::class);
$opRun = $runs->enqueueBulkOperation(
tenant: $tenant,
type: 'policy_version.prune',
targetScope: [
'entra_tenant_id' => (string) ($tenant->tenant_id ?? $tenant->external_id),
],
selectionIdentity: $selectionIdentity,
dispatcher: function ($operationRun) use ($tenant, $initiator, $ids, $retentionDays): void {
BulkPolicyVersionPruneJob::dispatch(
tenantId: (int) $tenant->getKey(),
userId: (int) ($initiator?->getKey() ?? 0),
policyVersionIds: $ids,
retentionDays: $retentionDays,
operationRun: $operationRun,
);
},
initiator: $initiator,
extraContext: [
'policy_version_count' => $count,
'retention_days' => $retentionDays,
],
emitQueuedNotification: false,
);
if ($initiator instanceof User) {
Notification::make()
->title('Policy version prune queued')
->body("Queued prune for {$count} policy versions.")
->icon('heroicon-o-arrow-path')
->iconColor('warning')
->info()
->actions([
Actions\Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($opRun, $tenant)),
])
->duration(8000)
->sendToDatabase($initiator);
}
OperationUxPresenter::queuedToast('policy_version.prune')
->actions([
Actions\Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($opRun, $tenant)),
])
->send();
})
->deselectRecordsAfterCompletion(),
BulkAction::make('bulk_restore_versions')
->label('Restore Versions')
->icon('heroicon-o-arrow-uturn-left')
->color('success')
->requiresConfirmation()
->hidden(function (HasTable $livewire): bool {
$trashedFilterState = $livewire->getTableFilterState(TrashedFilter::class) ?? [];
$value = $trashedFilterState['value'] ?? null;
$isOnlyTrashed = in_array($value, [0, '0', false], true);
return ! $isOnlyTrashed;
})
->disabled(function (): bool {
$tenant = Tenant::current();
$user = auth()->user();
if (! $tenant instanceof Tenant || ! $user instanceof User) {
return true;
}
return Gate::forUser($user)->denies(Capabilities::TENANT_MANAGE, $tenant);
})
->tooltip(function (): ?string {
$tenant = Tenant::current();
$user = auth()->user();
if (! $tenant instanceof Tenant || ! $user instanceof User) {
return null;
}
return Gate::forUser($user)->allows(Capabilities::TENANT_MANAGE, $tenant)
? null
: 'You do not have permission to manage policy versions.';
})
->modalHeading(fn (Collection $records) => "Restore {$records->count()} policy versions?")
->modalDescription('Archived versions will be restored back to the active list. Active versions will be skipped.')
->action(function (Collection $records) {
$tenant = Tenant::current();
$user = auth()->user();
$count = $records->count();
$ids = $records->pluck('id')->toArray();
if (! $tenant instanceof Tenant) {
return;
}
abort_unless($user instanceof User, 403);
abort_unless(Gate::forUser($user)->allows(Capabilities::TENANT_MANAGE, $tenant), 403);
$initiator = $user instanceof User ? $user : null;
/** @var BulkSelectionIdentity $selection */
$selection = app(BulkSelectionIdentity::class);
$selectionIdentity = $selection->fromIds($ids);
/** @var OperationRunService $runs */
$runs = app(OperationRunService::class);
$opRun = $runs->enqueueBulkOperation(
tenant: $tenant,
type: 'policy_version.restore',
targetScope: [
'entra_tenant_id' => (string) ($tenant->tenant_id ?? $tenant->external_id),
],
selectionIdentity: $selectionIdentity,
dispatcher: function ($operationRun) use ($tenant, $initiator, $ids): void {
BulkPolicyVersionRestoreJob::dispatch(
tenantId: (int) $tenant->getKey(),
userId: (int) ($initiator?->getKey() ?? 0),
policyVersionIds: $ids,
operationRun: $operationRun,
);
},
initiator: $initiator,
extraContext: [
'policy_version_count' => $count,
],
emitQueuedNotification: false,
);
OperationUxPresenter::queuedToast('policy_version.restore')
->actions([
Actions\Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($opRun, $tenant)),
])
->send();
})
->deselectRecordsAfterCompletion(),
BulkAction::make('bulk_force_delete_versions')
->label('Force Delete Versions')
->icon('heroicon-o-trash')
->color('danger')
->requiresConfirmation()
->hidden(function (HasTable $livewire): bool {
$trashedFilterState = $livewire->getTableFilterState(TrashedFilter::class) ?? [];
$value = $trashedFilterState['value'] ?? null;
$isOnlyTrashed = in_array($value, [0, '0', false], true);
return ! $isOnlyTrashed;
})
->disabled(function (): bool {
$tenant = Tenant::current();
$user = auth()->user();
if (! $tenant instanceof Tenant || ! $user instanceof User) {
return true;
}
return Gate::forUser($user)->denies(Capabilities::TENANT_MANAGE, $tenant);
})
->tooltip(function (): ?string {
$tenant = Tenant::current();
$user = auth()->user();
if (! $tenant instanceof Tenant || ! $user instanceof User) {
return null;
}
return Gate::forUser($user)->allows(Capabilities::TENANT_MANAGE, $tenant)
? null
: 'You do not have permission to manage policy versions.';
})
->modalHeading(fn (Collection $records) => "Force delete {$records->count()} policy versions?")
->modalDescription('This is permanent. Only archived versions will be permanently deleted; active versions will be skipped.')
->form([
Forms\Components\TextInput::make('confirmation')
->label('Type DELETE to confirm')
->required()
->in(['DELETE'])
->validationMessages([
'in' => 'Please type DELETE to confirm.',
]),
])
->action(function (Collection $records, array $data) {
$tenant = Tenant::current();
$user = auth()->user();
$count = $records->count();
$ids = $records->pluck('id')->toArray();
if (! $tenant instanceof Tenant) {
return;
}
abort_unless($user instanceof User, 403);
abort_unless(Gate::forUser($user)->allows(Capabilities::TENANT_MANAGE, $tenant), 403);
$initiator = $user instanceof User ? $user : null;
/** @var BulkSelectionIdentity $selection */
$selection = app(BulkSelectionIdentity::class);
$selectionIdentity = $selection->fromIds($ids);
/** @var OperationRunService $runs */
$runs = app(OperationRunService::class);
$opRun = $runs->enqueueBulkOperation(
tenant: $tenant,
type: 'policy_version.force_delete',
targetScope: [
'entra_tenant_id' => (string) ($tenant->tenant_id ?? $tenant->external_id),
],
selectionIdentity: $selectionIdentity,
dispatcher: function ($operationRun) use ($tenant, $initiator, $ids): void {
BulkPolicyVersionForceDeleteJob::dispatch(
tenantId: (int) $tenant->getKey(),
userId: (int) ($initiator?->getKey() ?? 0),
policyVersionIds: $ids,
operationRun: $operationRun,
);
},
initiator: $initiator,
extraContext: [
'policy_version_count' => $count,
],
emitQueuedNotification: false,
);
if ($initiator instanceof User) {
Notification::make()
->title('Policy version force delete queued')
->body("Queued force delete for {$count} policy versions.")
->icon('heroicon-o-arrow-path')
->iconColor('warning')
->info()
->actions([
Actions\Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($opRun, $tenant)),
])
->duration(8000)
->sendToDatabase($initiator);
}
OperationUxPresenter::queuedToast('policy_version.force_delete')
->actions([
Actions\Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($opRun, $tenant)),
])
->send();
})
->deselectRecordsAfterCompletion(),
]),
]);
}
public static function getEloquentQuery(): Builder
{
$tenantId = Tenant::current()->getKey();
return parent::getEloquentQuery()
->when($tenantId, fn (Builder $query) => $query->where('tenant_id', $tenantId))
->with('policy');
}
public static function getPages(): array
{
return [
'index' => Pages\ListPolicyVersions::route('/'),
'view' => Pages\ViewPolicyVersion::route('/{record}'),
];
}
}