diff --git a/app/Filament/Resources/FailedJobs/CompatibleFailedJobResource.php b/app/Filament/Resources/FailedJobs/CompatibleFailedJobResource.php new file mode 100644 index 0000000..462ad9f --- /dev/null +++ b/app/Filament/Resources/FailedJobs/CompatibleFailedJobResource.php @@ -0,0 +1,52 @@ +defaultSort('id', 'desc'); + } + + public static function getPages(): array + { + return [ + 'index' => CompatibleListFailedJobs::route('/'), + 'view' => ViewFailedJob::route('/{record}'), + ]; + } + + /** + * Only make this resource available in specific panels + */ + public static function canViewAny(): bool + { + // Check if we're in the 0xdash panel or other admin panels + $panel = filament()->getCurrentPanel(); + + return in_array($panel->getId(), ['0xdash', 'admin']); + } +} diff --git a/app/Filament/Resources/FailedJobs/CompatibleFailedJobsTable.php b/app/Filament/Resources/FailedJobs/CompatibleFailedJobsTable.php new file mode 100644 index 0000000..5dfa9a3 --- /dev/null +++ b/app/Filament/Resources/FailedJobs/CompatibleFailedJobsTable.php @@ -0,0 +1,132 @@ +columns(array_filter([ + TextColumn::make('id') + ->numeric() + ->sortable(), + + TextColumn::make('connection')->searchable(), + + TextColumn::make('queue')->searchable(), + + TextColumn::make('payload')->label('Job') + ->formatStateUsing(function ($state) { + return json_decode($state, true)['displayName']; + })->searchable(), + + TextColumn::make('exception')->wrap()->limit(100), + + TextColumn::make('failed_at')->searchable(), + ])) + ->filters(self::getCompatibleFiltersForIndex()) + ->recordActions([ + RetryJobAction::make()->iconButton()->tooltip(__('Retry Job')), + ViewAction::make()->iconButton()->tooltip(__('View Job')), + DeleteJobAction::make()->iconButton()->tooltip(__('Delete Job')), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + RetryJobsBulkAction::make(), + DeleteJobsBulkAction::make(), + ]), + ]); + } + + /** + * Database-agnostic version of getFiltersForIndex + */ + private static function getCompatibleFiltersForIndex(): array + { + $jobs = FailedJob::query() + ->select(['connection', 'queue']) + ->selectRaw(self::getJsonExtractExpression('displayName', 'job')) + ->get(); + + $connections = $jobs->pluck('connection', 'connection')->map(fn ($conn) => ucfirst($conn))->toArray(); + $queues = $jobs->pluck('queue', 'queue')->map(fn ($queue) => ucfirst($queue))->toArray(); + $jobNames = $jobs->pluck('job', 'job')->toArray(); + + return [ + SelectFilter::make('Connection')->options($connections), + SelectFilter::make('Queue')->options($queues), + Filter::make('Job') + ->schema([ + Select::make('job')->options($jobNames), + ]) + ->query(function (Builder $query, array $data): Builder { + return $query + ->when( + $data['job'], + fn (Builder $query, $job): Builder => $query->whereRaw(self::getJsonExtractExpression('displayName').' = ?', [$job]), + ); + }), + Filter::make('failed_at') + ->schema([ + DatePicker::make('failed_at'), + ]) + ->query(function (Builder $query, array $data): Builder { + return $query + ->when( + $data['failed_at'], + fn (Builder $query, $date): Builder => $query->whereDate('failed_at', '>=', $date), + ); + }), + ]; + } + + /** + * Get database-agnostic JSON extraction expression + */ + private static function getJsonExtractExpression(string $path, ?string $alias = null): string + { + $driver = DB::getDriverName(); + + switch ($driver) { + case 'mysql': + case 'mariadb': + $expression = "JSON_UNQUOTE(JSON_EXTRACT(payload, '$.".$path."'))"; + break; + case 'pgsql': + $expression = "payload->>'".$path."'"; + break; + case 'sqlite': + $expression = "json_extract(payload, '$.".$path."')"; + break; + case 'sqlsrv': + $expression = "JSON_VALUE(payload, '$.".$path."')"; + break; + default: + // Fallback to MySQL syntax + $expression = "JSON_UNQUOTE(JSON_EXTRACT(payload, '$.".$path."'))"; + break; + } + + return $alias ? $expression.' AS '.$alias : $expression; + } +} diff --git a/app/Filament/Resources/FailedJobs/Pages/CompatibleListFailedJobs.php b/app/Filament/Resources/FailedJobs/Pages/CompatibleListFailedJobs.php new file mode 100644 index 0000000..f9804b0 --- /dev/null +++ b/app/Filament/Resources/FailedJobs/Pages/CompatibleListFailedJobs.php @@ -0,0 +1,68 @@ +requiresConfirmation() + ->schema(function () { + + $queues = FailedJob::query() + ->select('queue') + ->distinct() + ->pluck('queue') + ->toArray(); + + $options = [ + 'all' => 'All Queues', + ]; + + $descriptions = [ + 'all' => 'Retry all Jobs', + ]; + + foreach ($queues as $queue) { + $options[$queue] = $queue; + $descriptions[$queue] = 'Retry jobs from '.$queue.' queue'; + } + + return [ + Radio::make('queue') + ->options($options) + ->descriptions($descriptions) + ->default('all') + ->required(), + ]; + }) + ->successNotificationTitle(__('Jobs pushed to queue successfully!')) + ->action(fn (array $data) => Artisan::call('queue:retry '.$data['queue'])), + + Action::make(__('Prune Jobs')) + ->requiresConfirmation() + ->schema([ + TextInput::make('hours') + ->numeric() + ->required() + ->default(1) + ->helperText(__("Prune's all failed jobs older than given hours.")), + ]) + ->color('danger') + ->successNotificationTitle(__('Jobs pruned successfully!')) + ->action(fn (array $data) => Artisan::call('queue:prune-failed --hours='.$data['hours'])), + ]; + } +} diff --git a/app/Providers/Filament/DashPanelProvider.php b/app/Providers/Filament/DashPanelProvider.php index dd18c0d..867c171 100644 --- a/app/Providers/Filament/DashPanelProvider.php +++ b/app/Providers/Filament/DashPanelProvider.php @@ -3,7 +3,6 @@ namespace App\Providers\Filament; use Backstage\FilamentMails\FilamentMailsPlugin; -use BinaryBuilds\FilamentFailedJobs\FilamentFailedJobsPlugin; use Boquizo\FilamentLogViewer\FilamentLogViewerPlugin; use Filament\Http\Middleware\Authenticate; use Filament\Http\Middleware\AuthenticateSession; @@ -62,7 +61,6 @@ class DashPanelProvider extends PanelProvider FilamentLoggerPlugin::make(), FilamentMailsPlugin::make(), FilamentLogViewerPlugin::make()->navigationGroup('Settings'), - FilamentFailedJobsPlugin::make() ]); } }