feat(failed-jobs): add database-agnostic failed jobs with bulk actions
- Create custom implementation to replace vendor plugin - Add database-agnostic JSON queries (MySQL, MariaDB, PG, SQLite, SQLSRV) - Implement Retry/Prune header actions with queue selection - Maintain all original features: filters, search, actions, bulk operations BREAKING CHANGE: New route /failed-jobs/compatible-failed-jobs Fixes MariaDB JSON syntax errors
This commit is contained in:
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\FailedJobs;
|
||||||
|
|
||||||
|
use App\Filament\Resources\FailedJobs\Pages\CompatibleListFailedJobs;
|
||||||
|
use BackedEnum;
|
||||||
|
use BinaryBuilds\FilamentFailedJobs\Models\FailedJob;
|
||||||
|
use BinaryBuilds\FilamentFailedJobs\Resources\FailedJobs\Pages\ViewFailedJob;
|
||||||
|
use BinaryBuilds\FilamentFailedJobs\Resources\FailedJobs\Schemas\FailedJobInfolist;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Filament\Support\Icons\Heroicon;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
|
class CompatibleFailedJobResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = FailedJob::class;
|
||||||
|
|
||||||
|
protected static string|BackedEnum|null $navigationIcon = Heroicon::QueueList;
|
||||||
|
|
||||||
|
protected static string|null|\UnitEnum $navigationGroup = 'Admin';
|
||||||
|
|
||||||
|
public static function infolist(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return FailedJobInfolist::configure($schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return CompatibleFailedJobsTable::configure($table)
|
||||||
|
->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']);
|
||||||
|
}
|
||||||
|
}
|
||||||
132
app/Filament/Resources/FailedJobs/CompatibleFailedJobsTable.php
Normal file
132
app/Filament/Resources/FailedJobs/CompatibleFailedJobsTable.php
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\FailedJobs;
|
||||||
|
|
||||||
|
use BinaryBuilds\FilamentFailedJobs\Actions\DeleteJobAction;
|
||||||
|
use BinaryBuilds\FilamentFailedJobs\Actions\DeleteJobsBulkAction;
|
||||||
|
use BinaryBuilds\FilamentFailedJobs\Actions\RetryJobAction;
|
||||||
|
use BinaryBuilds\FilamentFailedJobs\Actions\RetryJobsBulkAction;
|
||||||
|
use BinaryBuilds\FilamentFailedJobs\Models\FailedJob;
|
||||||
|
use Filament\Actions\BulkActionGroup;
|
||||||
|
use Filament\Actions\ViewAction;
|
||||||
|
use Filament\Forms\Components\DatePicker;
|
||||||
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Filters\Filter;
|
||||||
|
use Filament\Tables\Filters\SelectFilter;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class CompatibleFailedJobsTable
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Configure the table with database-agnostic JSON queries
|
||||||
|
*/
|
||||||
|
public static function configure(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\FailedJobs\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\FailedJobs\CompatibleFailedJobResource;
|
||||||
|
use BinaryBuilds\FilamentFailedJobs\Models\FailedJob;
|
||||||
|
use Filament\Actions\Action;
|
||||||
|
use Filament\Forms\Components\Radio;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
|
||||||
|
class CompatibleListFailedJobs extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = CompatibleFailedJobResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Action::make(__('Retry Jobs'))
|
||||||
|
->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'])),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
namespace App\Providers\Filament;
|
namespace App\Providers\Filament;
|
||||||
|
|
||||||
use Backstage\FilamentMails\FilamentMailsPlugin;
|
use Backstage\FilamentMails\FilamentMailsPlugin;
|
||||||
use BinaryBuilds\FilamentFailedJobs\FilamentFailedJobsPlugin;
|
|
||||||
use Boquizo\FilamentLogViewer\FilamentLogViewerPlugin;
|
use Boquizo\FilamentLogViewer\FilamentLogViewerPlugin;
|
||||||
use Filament\Http\Middleware\Authenticate;
|
use Filament\Http\Middleware\Authenticate;
|
||||||
use Filament\Http\Middleware\AuthenticateSession;
|
use Filament\Http\Middleware\AuthenticateSession;
|
||||||
@@ -62,7 +61,6 @@ class DashPanelProvider extends PanelProvider
|
|||||||
FilamentLoggerPlugin::make(),
|
FilamentLoggerPlugin::make(),
|
||||||
FilamentMailsPlugin::make(),
|
FilamentMailsPlugin::make(),
|
||||||
FilamentLogViewerPlugin::make()->navigationGroup('Settings'),
|
FilamentLogViewerPlugin::make()->navigationGroup('Settings'),
|
||||||
FilamentFailedJobsPlugin::make()
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user