385 lines
15 KiB
PHP
385 lines
15 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire;
|
|
|
|
use App\Jobs\AddPoliciesToBackupSetJob;
|
|
use App\Models\BackupSet;
|
|
use App\Models\Policy;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Services\OperationRunService;
|
|
use App\Services\Operations\BulkSelectionIdentity;
|
|
use App\Support\OperationRunLinks;
|
|
use App\Support\OpsUx\OperationUxPresenter;
|
|
use App\Support\OpsUx\OpsUxBrowserEvents;
|
|
use Filament\Actions\BulkAction;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Tables\Columns\TextColumn;
|
|
use Filament\Tables\Filters\SelectFilter;
|
|
use Filament\Tables\Filters\TernaryFilter;
|
|
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
|
|
{
|
|
$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')
|
|
->authorize(function (): bool {
|
|
$this->dispatch(OpsUxBrowserEvents::RunEnqueued);
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
$tenant = Tenant::current();
|
|
} catch (\RuntimeException) {
|
|
return false;
|
|
}
|
|
|
|
if (! $user->canSyncTenant($tenant)) {
|
|
return false;
|
|
}
|
|
|
|
return BackupSet::query()
|
|
->whereKey($this->backupSetId)
|
|
->where('tenant_id', $tenant->getKey())
|
|
->exists();
|
|
})
|
|
->action(function (Collection $records): void {
|
|
$backupSet = BackupSet::query()->findOrFail($this->backupSetId);
|
|
$tenant = null;
|
|
|
|
try {
|
|
$tenant = Tenant::current();
|
|
} catch (\RuntimeException) {
|
|
$tenant = $backupSet->tenant;
|
|
}
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User) {
|
|
Notification::make()
|
|
->title('Not allowed')
|
|
->danger()
|
|
->send();
|
|
|
|
return;
|
|
}
|
|
|
|
if (! $tenant instanceof Tenant) {
|
|
Notification::make()
|
|
->title('Not allowed')
|
|
->danger()
|
|
->send();
|
|
|
|
return;
|
|
}
|
|
|
|
if ((int) $tenant->getKey() !== (int) $backupSet->tenant_id) {
|
|
Notification::make()
|
|
->title('Not allowed')
|
|
->danger()
|
|
->send();
|
|
|
|
return;
|
|
}
|
|
|
|
if (! $user->canSyncTenant($tenant)) {
|
|
Notification::make()
|
|
->title('Not allowed')
|
|
->danger()
|
|
->send();
|
|
|
|
return;
|
|
}
|
|
|
|
$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);
|
|
|
|
/** @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.add_policies',
|
|
targetScope: [
|
|
'entra_tenant_id' => (string) ($tenant->tenant_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)) {
|
|
Notification::make()
|
|
->title('Add policies already queued')
|
|
->body('A matching run is already queued or running. Open the run to monitor progress.')
|
|
->actions([
|
|
\Filament\Actions\Action::make('view_run')
|
|
->label('View run')
|
|
->url(OperationRunLinks::view($opRun, $tenant)),
|
|
])
|
|
->info()
|
|
->send();
|
|
|
|
return;
|
|
}
|
|
|
|
OperationUxPresenter::queuedToast((string) $opRun->type)
|
|
->actions([
|
|
\Filament\Actions\Action::make('view_run')
|
|
->label('View run')
|
|
->url(OperationRunLinks::view($opRun, $tenant)),
|
|
])
|
|
->send();
|
|
|
|
$this->resetTable();
|
|
|
|
$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();
|
|
}
|
|
}
|