TenantAtlas/apps/platform/app/Livewire/BackupSetPolicyPickerTable.php
2026-05-24 22:48:22 +02:00

461 lines
18 KiB
PHP

<?php
namespace App\Livewire;
use App\Jobs\AddPoliciesToBackupSetJob;
use App\Models\BackupSet;
use App\Models\ManagedEnvironment;
use App\Models\Policy;
use App\Models\User;
use App\Services\Auth\CapabilityResolver;
use App\Services\OperationRunService;
use App\Services\Operations\BulkSelectionIdentity;
use App\Support\Auth\Capabilities;
use App\Support\Badges\BadgeDomain;
use App\Support\Badges\BadgeRenderer;
use App\Support\Badges\TagBadgeDomain;
use App\Support\Badges\TagBadgeRenderer;
use App\Support\OperationRunLinks;
use App\Support\OpsUx\OperationUxPresenter;
use App\Support\OpsUx\OpsUxBrowserEvents;
use Filament\Actions\BulkAction;
use Filament\Facades\Filament;
use Filament\Notifications\Notification;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Filament\Tables\TableComponent;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
class BackupSetPolicyPickerTable extends TableComponent
{
public int $backupSetId;
public bool $include_assignments = true;
public bool $include_scope_tags = true;
public bool $include_foundations = true;
public function mount(int $backupSetId): void
{
$this->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
{
$context = $this->resolveBackupSetContext();
$existingPolicyIds = [];
$tenantId = $context !== null ? (int) $context['tenant']->getKey() : null;
if ($context !== null) {
$existingPolicyIds = $context['backupSet']
->items()
->pluck('policy_id')
->filter()
->all();
}
return $table
->queryStringIdentifier('backupSetPolicyPicker'.Str::studly((string) $this->backupSetId))
->query(
Policy::query()
->when(
$context !== null,
fn (Builder $query) => $query->where('managed_environment_id', (int) $context['tenant']->getKey()),
fn (Builder $query) => $query->whereRaw('1 = 0'),
)
->when($existingPolicyIds !== [], fn (Builder $query) => $query->whereNotIn('id', $existingPolicyIds))
)
->deferLoading(! app()->runningUnitTests())
->defaultSort('display_name')
->paginated(\App\Support\Filament\TablePaginationProfiles::picker())
->defaultPaginationPageOption(25)
->searchable()
->striped()
->columns([
TextColumn::make('display_name')
->label('Name')
->searchable()
->sortable()
->wrap(),
TextColumn::make('policy_type')
->label('Type')
->badge()
->placeholder('—')
->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::PolicyType))
->color(TagBadgeRenderer::color(TagBadgeDomain::PolicyType)),
TextColumn::make('platform')
->label('Platform')
->badge()
->placeholder('—')
->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::Platform))
->color(TagBadgeRenderer::color(TagBadgeDomain::Platform))
->sortable(),
TextColumn::make('visibility_state')
->label('Visibility')
->badge()
->state(fn (Policy $record): string => $record->visibilityState())
->formatStateUsing(BadgeRenderer::label(BadgeDomain::PolicyProviderPresence))
->color(BadgeRenderer::color(BadgeDomain::PolicyProviderPresence))
->icon(BadgeRenderer::icon(BadgeDomain::PolicyProviderPresence))
->iconColor(BadgeRenderer::iconColor(BadgeDomain::PolicyProviderPresence))
->description(fn (Policy $record): ?string => $record->isCurrentBackupEligible()
? null
: $record->currentBackupBlockedReasonLabel()),
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(function () use ($tenantId): array {
if (! is_int($tenantId)) {
return [];
}
return Policy::query()
->where('managed_environment_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('any')
->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)));
}),
SelectFilter::make('visibility')
->label('Visibility')
->options([
'active' => 'Active',
'ignored' => 'Ignored locally',
'provider_missing' => 'Provider missing',
'all' => 'All',
])
->query(function (Builder $query, array $data): Builder {
$value = $data['value'] ?? null;
if (blank($value) || $value === 'all') {
return $query;
}
return match ($value) {
'active' => $query->active(),
'ignored' => $query->whereNotNull('ignored_at'),
'provider_missing' => $query->whereNotNull('missing_from_provider_at'),
default => $query,
};
}),
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,
};
}),
])
->emptyStateHeading('No matching policies available')
->emptyStateDescription('Adjust the current filters or sync additional policies before adding them to this backup set.')
->checkIfRecordIsSelectableUsing(fn (Policy $record): bool => $record->isCurrentBackupEligible())
->bulkActions([
BulkAction::make('add_selected_to_backup_set')
->label('Add selected')
->icon('heroicon-m-plus')
->authorize(function (): bool {
$this->dispatch(OpsUxBrowserEvents::RunEnqueued);
$context = $this->resolveBackupSetContext(requireSyncCapability: true);
return $context !== null;
})
->action(function (Collection $records): void {
$context = $this->resolveBackupSetContext(requireSyncCapability: true);
if ($context === null) {
abort(403);
}
$user = $context['user'];
$tenant = $context['tenant'];
$backupSet = $context['backupSet'];
$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);
$blocked = $records->first(
fn ($record): bool => $record instanceof Policy && ! $record->isCurrentBackupEligible()
);
if ($blocked instanceof Policy) {
Notification::make()
->title('Current backup unavailable')
->body($blocked->currentBackupBlockedReasonLabel())
->warning()
->send();
return;
}
$validatedPolicyIds = Policy::query()
->where('managed_environment_id', (int) $tenant->getKey())
->whereIn('id', $policyIds)
->pluck('id')
->map(fn (mixed $value): int => (int) $value)
->unique()
->values()
->all();
sort($validatedPolicyIds);
if ($validatedPolicyIds !== $policyIds) {
abort(403);
}
/** @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.update',
targetScope: [
'entra_tenant_id' => (string) ($tenant->managed_environment_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)) {
OperationUxPresenter::alreadyQueuedToast((string) $opRun->type)
->actions([
\Filament\Actions\Action::make('view_run')
->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)),
])
->send();
return;
}
OperationUxPresenter::queuedToast((string) $opRun->type)
->actions([
\Filament\Actions\Action::make('view_run')
->label('Open operation')
->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<string,mixed>
*/
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<string, string>
*/
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();
}
/**
* @return array{user: User, tenant: ManagedEnvironment, backupSet: BackupSet}|null
*/
private function resolveBackupSetContext(bool $requireSyncCapability = false): ?array
{
$user = auth()->user();
if (! $user instanceof User) {
return null;
}
$backupSet = BackupSet::query()
->with(['tenant'])
->find($this->backupSetId);
if (! $backupSet instanceof BackupSet) {
return null;
}
$tenant = $backupSet->tenant;
if (! $tenant instanceof ManagedEnvironment) {
return null;
}
$ambientTenant = Filament::getTenant();
if ($ambientTenant instanceof ManagedEnvironment && ! $ambientTenant->is($tenant)) {
return null;
}
/** @var CapabilityResolver $resolver */
$resolver = app(CapabilityResolver::class);
if (! $resolver->isMember($user, $tenant)) {
return null;
}
if ($requireSyncCapability && ! $resolver->can($user, $tenant, Capabilities::TENANT_SYNC)) {
return null;
}
return [
'user' => $user,
'tenant' => $tenant,
'backupSet' => $backupSet,
];
}
}