schema([ Section::make('Run') ->schema([ TextEntry::make('user.name') ->label('Initiator') ->placeholder('—'), TextEntry::make('resource')->badge(), TextEntry::make('action')->badge(), TextEntry::make('status') ->label('Outcome') ->badge() ->state(fn (BulkOperationRun $record): string => $record->statusBucket()) ->color(fn (BulkOperationRun $record): string => static::statusBucketColor($record->statusBucket())), TextEntry::make('total_items')->label('Total')->numeric(), TextEntry::make('processed_items')->label('Processed')->numeric(), TextEntry::make('succeeded')->numeric(), TextEntry::make('failed')->numeric(), TextEntry::make('skipped')->numeric(), TextEntry::make('created_at')->dateTime(), TextEntry::make('updated_at')->dateTime(), TextEntry::make('idempotency_key')->label('Idempotency key')->copyable()->placeholder('—'), ]) ->columns(2) ->columnSpanFull(), Section::make('Related') ->schema([ TextEntry::make('related_backup_set') ->label('Backup set') ->state(function (BulkOperationRun $record): ?string { $backupSetId = static::backupSetIdFromItemIds($record); if (! $backupSetId) { return null; } return "#{$backupSetId}"; }) ->url(function (BulkOperationRun $record): ?string { $backupSetId = static::backupSetIdFromItemIds($record); if (! $backupSetId) { return null; } return BackupSetResource::getUrl('view', ['record' => $backupSetId], tenant: Tenant::current()); }) ->visible(fn (BulkOperationRun $record): bool => static::backupSetIdFromItemIds($record) !== null) ->placeholder('—') ->columnSpanFull(), TextEntry::make('related_drift_findings') ->label('Drift findings') ->state('View') ->url(function (BulkOperationRun $record): ?string { if ($record->runType() !== 'drift.generate') { return null; } $payload = $record->item_ids ?? []; if (! is_array($payload)) { return FindingResource::getUrl('index', tenant: Tenant::current()); } $scopeKey = null; $baselineRunId = null; $currentRunId = null; if (array_is_list($payload) && isset($payload[0]) && is_string($payload[0])) { $scopeKey = $payload[0]; } else { $scopeKey = is_string($payload['scope_key'] ?? null) ? $payload['scope_key'] : null; if (is_numeric($payload['baseline_run_id'] ?? null)) { $baselineRunId = (int) $payload['baseline_run_id']; } if (is_numeric($payload['current_run_id'] ?? null)) { $currentRunId = (int) $payload['current_run_id']; } } $tableFilters = []; if (is_string($scopeKey) && $scopeKey !== '') { $tableFilters['scope_key'] = ['scope_key' => $scopeKey]; } if (is_int($baselineRunId) || is_int($currentRunId)) { $tableFilters['run_ids'] = [ 'baseline_run_id' => $baselineRunId, 'current_run_id' => $currentRunId, ]; } $parameters = $tableFilters !== [] ? ['tableFilters' => $tableFilters] : []; return FindingResource::getUrl('index', $parameters, tenant: Tenant::current()); }) ->visible(fn (BulkOperationRun $record): bool => $record->runType() === 'drift.generate') ->placeholder('—') ->columnSpanFull(), ]) ->visible(fn (BulkOperationRun $record): bool => in_array($record->runType(), ['backup_set.add_policies', 'drift.generate'], true)) ->columnSpanFull(), Section::make('Items') ->schema([ ViewEntry::make('item_ids') ->label('') ->view('filament.infolists.entries.snapshot-json') ->state(fn (BulkOperationRun $record) => $record->item_ids ?? []) ->columnSpanFull(), ]) ->columnSpanFull(), Section::make('Failures') ->schema([ ViewEntry::make('failures') ->label('') ->view('filament.infolists.entries.snapshot-json') ->state(fn (BulkOperationRun $record) => $record->failures ?? []) ->columnSpanFull(), ]) ->columnSpanFull(), ]); } public static function table(Table $table): Table { return $table ->defaultSort('id', 'desc') ->modifyQueryUsing(function (Builder $query): Builder { $tenantId = Tenant::current()->getKey(); return $query->when($tenantId, fn (Builder $q) => $q->where('tenant_id', $tenantId)); }) ->columns([ Tables\Columns\TextColumn::make('user.name') ->label('Initiator') ->placeholder('—') ->toggleable(), Tables\Columns\TextColumn::make('resource')->badge(), Tables\Columns\TextColumn::make('action')->badge(), Tables\Columns\TextColumn::make('status') ->label('Outcome') ->badge() ->formatStateUsing(fn (BulkOperationRun $record): string => $record->statusBucket()) ->color(fn (BulkOperationRun $record): string => static::statusBucketColor($record->statusBucket())), Tables\Columns\TextColumn::make('created_at')->since(), Tables\Columns\TextColumn::make('total_items')->label('Total')->numeric(), Tables\Columns\TextColumn::make('processed_items')->label('Processed')->numeric(), Tables\Columns\TextColumn::make('failed')->numeric(), ]) ->filters([ Tables\Filters\SelectFilter::make('run_type') ->label('Run type') ->options(fn (): array => static::runTypeOptions()) ->query(function (Builder $query, array $data): Builder { $value = $data['value'] ?? null; if (! is_string($value) || $value === '' || ! str_contains($value, '.')) { return $query; } [$resource, $action] = explode('.', $value, 2); if ($resource === '' || $action === '') { return $query; } return $query ->where('resource', $resource) ->where('action', $action); }), Tables\Filters\SelectFilter::make('status_bucket') ->label('Status') ->options([ 'queued' => 'Queued', 'running' => 'Running', 'succeeded' => 'Succeeded', 'partially succeeded' => 'Partially succeeded', 'failed' => 'Failed', ]) ->query(function (Builder $query, array $data): Builder { $value = $data['value'] ?? null; if (! is_string($value) || $value === '') { return $query; } $nonSkippedFailureSql = "EXISTS (SELECT 1 FROM jsonb_array_elements(COALESCE(failures, '[]'::jsonb)) AS elem WHERE (elem->>'type' IS NULL OR elem->>'type' <> 'skipped'))"; return match ($value) { 'queued' => $query->where('status', 'pending'), 'running' => $query->where('status', 'running'), 'succeeded' => $query ->whereIn('status', ['completed', 'completed_with_errors']) ->where('failed', 0) ->whereRaw("NOT {$nonSkippedFailureSql}"), 'partially succeeded' => $query ->whereNotIn('status', ['pending', 'running']) ->where('succeeded', '>', 0) ->where(function (Builder $q) use ($nonSkippedFailureSql): void { $q->where('failed', '>', 0)->orWhereRaw($nonSkippedFailureSql); }), 'failed' => $query ->whereNotIn('status', ['pending', 'running']) ->where(function (Builder $q) use ($nonSkippedFailureSql): void { $q->where(function (Builder $q) use ($nonSkippedFailureSql): void { $q->where('succeeded', 0) ->where(function (Builder $q) use ($nonSkippedFailureSql): void { $q->where('failed', '>', 0)->orWhereRaw($nonSkippedFailureSql); }); })->orWhere(function (Builder $q) use ($nonSkippedFailureSql): void { $q->whereIn('status', ['failed', 'aborted']) ->whereNot(function (Builder $q) use ($nonSkippedFailureSql): void { $q->where('succeeded', '>', 0) ->where(function (Builder $q) use ($nonSkippedFailureSql): void { $q->where('failed', '>', 0)->orWhereRaw($nonSkippedFailureSql); }); }); }); }), default => $query, }; }), Tables\Filters\Filter::make('created_at') ->label('Created') ->form([ DatePicker::make('created_from') ->label('From') ->default(fn () => now()->subDays(30)), DatePicker::make('created_until') ->label('Until') ->default(fn () => now()), ]) ->query(function (Builder $query, array $data): Builder { $from = $data['created_from'] ?? null; if ($from) { $query->whereDate('created_at', '>=', $from); } $until = $data['created_until'] ?? null; if ($until) { $query->whereDate('created_at', '<=', $until); } return $query; }), ]) ->actions([ Actions\ViewAction::make(), ]) ->bulkActions([]); } public static function getEloquentQuery(): Builder { return parent::getEloquentQuery() ->with('user') ->latest('id'); } public static function getPages(): array { return [ 'index' => Pages\ListBulkOperationRuns::route('/'), 'view' => Pages\ViewBulkOperationRun::route('/{record}'), ]; } /** * @return array */ private static function runTypeOptions(): array { $tenantId = Tenant::current()->getKey(); $knownTypes = [ 'drift.generate' => 'drift.generate', 'backup_set.add_policies' => 'backup_set.add_policies', ]; $storedTypes = BulkOperationRun::query() ->where('tenant_id', $tenantId) ->select(['resource', 'action']) ->distinct() ->orderBy('resource') ->orderBy('action') ->get() ->mapWithKeys(function (BulkOperationRun $run): array { $type = "{$run->resource}.{$run->action}"; return [$type => $type]; }) ->all(); return array_replace($storedTypes, $knownTypes); } private static function statusBucketColor(string $statusBucket): string { return match ($statusBucket) { 'succeeded' => 'success', 'partially succeeded' => 'warning', 'failed' => 'danger', 'running' => 'info', 'queued' => 'gray', default => 'gray', }; } private static function backupSetIdFromItemIds(BulkOperationRun $record): ?int { if ($record->runType() !== 'backup_set.add_policies') { return null; } $payload = $record->item_ids ?? []; if (! is_array($payload)) { return null; } $backupSetId = $payload['backup_set_id'] ?? null; if (! is_numeric($backupSetId)) { return null; } $backupSetId = (int) $backupSetId; return $backupSetId > 0 ? $backupSetId : null; } }