diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..529f68e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,37 @@ +# Changelog + +All notable changes to `filament-job-manager` will be documented in this file. + +## v0.6.1 - 2022-12-08 + +- focus relevant information in failed jobs index table + +## v0.6.0 - 2022-10-06 + +- introduce https://github.com/invaders-xx/filament-jsoneditor for `payload` + +## v0.5.1 - 2022-09-08 + +- do not show 'options' column in JobBatchesResource + +## v0.5.0 - 2022-09-08 + +- place FailedJobResource and JobBatchesResource in 'jobs' Navigation Group + +## v0.4.0 - 2022-08-29 + +- introduced ability to delete failed jobs or job batches + +## v0.3.2 - 2022-08-29 + +- introduced `job_batches` Resource (if the sql table is present) + +## v0.2.0 - 2022-08-09 + +- introduced 'view' ViewAction to see job details as popup +- introduced Bulk Action for Retry +- introduced 'Retry all failed jobs' Button + +## v0.1.0 - 2022-08-09 + +- initial release diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..846c963 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) thyseus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..137bf08 --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# Filament Job Manager + +Work in progress. Should become a Filament panel for managing job queues including failed jobs and batches. Currently buggy as hell. + +## Installation + +You should install the package via Composer: + +```bash +composer require adrolli/filament-job-manager +php artisan vendor:publish --tag=filament-job-manager +``` + +### Authorization + +If you would like to prevent certain users from accessing your page, you should register a policy: + +```php +use App\Policies\FailedJobPolicy; +use Adrolli\FilamentJobManager\Models\FailedJob; +use Adrolli\FilamentJobManager\Models\JobBatch; + +class AuthServiceProvider extends ServiceProvider +{ + protected $policies = [ + FailedJob::class => FailedJobPolicy::class, + JobBatch::class => JobBatchPolicy::class, + ]; +} +``` + +```php +namespace App\Policies; + +use App\Models\User; +use Illuminate\Auth\Access\HandlesAuthorization; + +class FailedJobPolicy +{ + use HandlesAuthorization; + + public function viewAny(User $user): bool + { + return $user->can('manage_failed_jobs'); + } +} +``` + +(same for JobPolicy and JobBatchPolicy, if necessary). + +This will prevent the navigation item(s) from being registered. + +## Changelog + +Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. + +## License + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..ef96986 --- /dev/null +++ b/composer.json @@ -0,0 +1,51 @@ +{ + "name": "adrolli/filament-job-manager", + "description": "A Filament manager for job queues including failed jobs and batches.", + "keywords": [ + "laravel", + "filament", + "jobs", + "queues", + "failed-jobs", + "batches", + "monitor", + "redis" + ], + "homepage": "https://github.com/adrolli/filament-job-manager", + "license": "MIT", + "authors": [ + { + "name": "Alf Drollinger", + "email": "alf@drollinger.info", + "role": "Developer" + } + ], + "require": { + "php": "^8.0.2", + "spatie/laravel-package-tools": "^1.9.2", + "invaders-xx/filament-jsoneditor": "^3.0", + "filament/filament": "^3.0" + }, + "require-dev": {}, + "autoload": { + "psr-4": { + "Adrolli\\FilamentJobManager\\": "src" + } + }, + "config": { + "sort-packages": true + }, + "extra": { + "laravel": { + "providers": [ + "Adrolli\\FilamentJobManager\\FilamentJobManagerServiceProvider", + "Adrolli\\FilamentJobManager\\JobMonitorProvider" + ], + "aliases": { + "JobMonitor": "Adrolli\\FilamentJobManager\\QueueMonitorProvider\\Facade" + } + } + }, + "minimum-stability": "dev", + "prefer-stable": true +} \ No newline at end of file diff --git a/config/filament-failed-jobs.php b/config/filament-failed-jobs.php new file mode 100644 index 0000000..9dcf891 --- /dev/null +++ b/config/filament-failed-jobs.php @@ -0,0 +1,40 @@ + [ + 'jobs' => [ + 'enabled' => true, + 'label' => 'Job', + 'plural_label' => 'Jobs', + 'navigation_group' => 'Job manager', + 'navigation_icon' => 'heroicon-o-cpu-chip', + 'navigation_sort' => 1, + 'navigation_count_badge' => false, + 'resource' => Adrolli\FilamentJobManager\Resources\JobsResource::class, + ], + 'failed_jobs' => [ + 'enabled' => true, + 'label' => 'Failed Job', + 'plural_label' => 'Failed Jobs', + 'navigation_group' => 'Job manager', + 'navigation_icon' => 'heroicon-o-exclamation-circle', + 'navigation_sort' => 2, + 'navigation_count_badge' => false, + 'resource' => Adrolli\FilamentJobManager\Resources\FailedJobsResource::class, + ], + 'job_batches' => [ + 'enabled' => true, + 'label' => 'Job Batch', + 'plural_label' => 'Job Batches', + 'navigation_group' => 'Job manager', + 'navigation_icon' => 'heroicon-o-inbox-stack', + 'navigation_sort' => 3, + 'navigation_count_badge' => false, + 'resource' => Adrolli\FilamentJobManager\Resources\JobBatchesResource::class, + ], + ], + 'pruning' => [ + 'enabled' => true, + 'retention_days' => 7, + ], +]; diff --git a/resources/lang/en/translations.php b/resources/lang/en/translations.php new file mode 100644 index 0000000..119a188 --- /dev/null +++ b/resources/lang/en/translations.php @@ -0,0 +1,19 @@ + 'Queued Jobs Monitor', + 'title' => 'Queued Jobs', + 'navigation_label' => 'Jobs', + 'navigation_group' => 'System', + 'total_jobs' => 'Total Jobs Executed', + 'execution_time' => 'Total Execution Time', + 'average_time' => 'Average Execution Time', + 'succeeded' => 'Succeeded', + 'failed' => 'Failed', + 'running' => 'Running', + 'status' => 'Status', + 'name' => 'Name', + 'queue' => 'Queue', + 'progress' => 'Progress', + 'started_at' => 'Started at', +]; diff --git a/resources/lang/es/translations.php b/resources/lang/es/translations.php new file mode 100644 index 0000000..b53ee5a --- /dev/null +++ b/resources/lang/es/translations.php @@ -0,0 +1,19 @@ + 'Monitor de Trabajos En Cola', + 'title' => 'Trabajos En Cola', + 'navigation_label' => 'Trabajos', + 'navigation_group' => 'Sistema', + 'total_jobs' => 'Total Trabajos Ejecutados', + 'execution_time' => 'Tiempo Total de Ejecución', + 'average_time' => 'Tiempo Promedio de Ejecución', + 'succeeded' => 'Exitoso', + 'failed' => 'Fallido', + 'running' => 'En ejecución', + 'status' => 'Estado', + 'name' => 'Nombre', + 'queue' => 'Cola', + 'progress' => 'Progreso', + 'started_at' => 'Iniciado a las', +]; diff --git a/resources/lang/fr/translations.php b/resources/lang/fr/translations.php new file mode 100644 index 0000000..18b1ea8 --- /dev/null +++ b/resources/lang/fr/translations.php @@ -0,0 +1,19 @@ + 'Monitor de Jobs En File', + 'title' => 'Jobs', + 'navigation_label' => 'Jobs', + 'navigation_group' => 'Système', + 'total_jobs' => 'Total Jobs Executé(s)', + 'execution_time' => "Temps Total d'Execution", + 'average_time' => "Temps moyen d'Execution", + 'succeeded' => 'Succes', + 'failed' => 'Echec', + 'running' => 'En cours', + 'status' => 'Statut', + 'name' => 'Nom', + 'queue' => 'File', + 'progress' => 'Progression', + 'started_at' => 'Débuté à', +]; diff --git a/src/FilamentFailedJobsPlugin.php b/src/FilamentFailedJobsPlugin.php new file mode 100644 index 0000000..97415b2 --- /dev/null +++ b/src/FilamentFailedJobsPlugin.php @@ -0,0 +1,293 @@ +resources([ + $this->getResource(), + ]); + } + + /** + * Boot the plugin. + */ + public function boot(Panel $panel): void + { + // + } + + /** + * Make a new instance of the plugin. + */ + public static function make(): static + { + return app(static::class); + } + + /** + * Get the plugin instance. + */ + public static function get(): static + { + return filament(app(static::class)->getId()); + } + + /** + * Get the resource class. + */ + public function getResource(): string + { + return $this->resource ?? config('filament-job-manager.resources.failed_jobs.resource'); + } + + /** + * Set the resource class. + */ + public function resource(string $resource): static + { + $this->resource = $resource; + + return $this; + } + + /** + * Get the resource label. + */ + public function getLabel(): ?string + { + return $this->evaluate($this->label) ?? config('filament-job-manager.resources.failed_jobs.label'); + } + + /** + * Set the resource label. + */ + public function label(string $label): static + { + $this->label = $label; + + return $this; + } + + /** + * Get the plural resource label. + */ + public function getPluralLabel(): ?string + { + return $this->evaluate($this->pluralLabel) ?? config('filament-job-manager.resources.failed_jobs.plural_label'); + } + + /** + * Set the plural resource label. + */ + public function pluralLabel(string $pluralLabel): static + { + $this->pluralLabel = $pluralLabel; + + return $this; + } + + /** + * Get the resource navigation group. + */ + public function getNavigationGroup(): ?string + { + return $this->navigationGroup ?? config('filament-job-manager.resources.failed_jobs.navigation_group'); + } + + /** + * Set the resource navigation group. + */ + public function navigationGroup(string $navigationGroup): static + { + $this->navigationGroup = $navigationGroup; + + return $this; + } + + /** + * Get the resource icon. + */ + public function getNavigationIcon(): ?string + { + return $this->navigationIcon ?? config('filament-job-manager.resources.failed_jobs.navigation_icon'); + } + + /** + * Set the resource icon. + */ + public function navigationIcon(string $navigationIcon): static + { + $this->navigationIcon = $navigationIcon; + + return $this; + } + + /** + * Get the resource sort. + */ + public function getNavigationSort(): ?int + { + return $this->navigationSort ?? config('filament-job-manager.resources.failed_jobs.navigation_sort'); + } + + /** + * Set the resource sort. + */ + public function navigationSort(int $navigationSort): static + { + $this->navigationSort = $navigationSort; + + return $this; + } + + /** + * Get the resource navigation count badge status. + */ + public function getNavigationCountBadge(): ?bool + { + return $this->navigationCountBadge ?? config('filament-job-manager.resources.failed_jobs.navigation_count_badge'); + } + + /** + * Set the resource navigation count badge status. + */ + public function navigationCountBadge(bool $navigationCountBadge = true): static + { + $this->navigationCountBadge = $navigationCountBadge; + + return $this; + } + + /** + * Determine whether the resource navigation is enabled. + */ + public function shouldRegisterNavigation(): bool + { + return $this->navigation ?? config('filament-job-manager.resources.failed_jobs.enabled'); + } + + /** + * Enable the resource navigation. + */ + public function enableNavigation(bool $status = true): static + { + $this->navigation = $status; + + return $this; + } + + /** + * Get the pruning status. + */ + public function getPruning(): ?bool + { + return $this->pruning ?? config('filament-job-manager.pruning.enabled'); + } + + /** + * Set the pruning status. + */ + public function enablePruning(bool $status = true): static + { + $this->pruning = $status; + + return $this; + } + + /** + * Get the pruning retention. + */ + public function getPruningRetention(): ?int + { + return $this->pruningRetention ?? config('filament-job-manager.pruning.retention_days'); + } + + /** + * Set the pruning retention. + */ + public function pruningRetention(int $pruningRetention): static + { + $this->pruningRetention = $pruningRetention; + + return $this; + } + + /** + * Get the resource breadcrumb. + */ + public function getBreadcrumb(): string + { + return __('filament-job-manager::translations.breadcrumb'); + } +} diff --git a/src/FilamentFailedJobsServiceProvider.php b/src/FilamentFailedJobsServiceProvider.php new file mode 100644 index 0000000..be84417 --- /dev/null +++ b/src/FilamentFailedJobsServiceProvider.php @@ -0,0 +1,19 @@ +name('filament-job-manager'); + + $this->publishes([ + __DIR__.'/../config/filament-job-manager.php' => config_path('filament-job-manager.php'), + ], 'filament-job-manager'); + + } +} diff --git a/src/FilamentJobBatchesPlugin.php b/src/FilamentJobBatchesPlugin.php new file mode 100644 index 0000000..a924edf --- /dev/null +++ b/src/FilamentJobBatchesPlugin.php @@ -0,0 +1,293 @@ +resources([ + $this->getResource(), + ]); + } + + /** + * Boot the plugin. + */ + public function boot(Panel $panel): void + { + // + } + + /** + * Make a new instance of the plugin. + */ + public static function make(): static + { + return app(static::class); + } + + /** + * Get the plugin instance. + */ + public static function get(): static + { + return filament(app(static::class)->getId()); + } + + /** + * Get the resource class. + */ + public function getResource(): string + { + return $this->resource ?? config('filament-job-manager.resources.job_batches.resource'); + } + + /** + * Set the resource class. + */ + public function resource(string $resource): static + { + $this->resource = $resource; + + return $this; + } + + /** + * Get the resource label. + */ + public function getLabel(): ?string + { + return $this->evaluate($this->label) ?? config('filament-job-manager.resources.job_batches.label'); + } + + /** + * Set the resource label. + */ + public function label(string $label): static + { + $this->label = $label; + + return $this; + } + + /** + * Get the plural resource label. + */ + public function getPluralLabel(): ?string + { + return $this->evaluate($this->pluralLabel) ?? config('filament-job-manager.resources.job_batches.plural_label'); + } + + /** + * Set the plural resource label. + */ + public function pluralLabel(string $pluralLabel): static + { + $this->pluralLabel = $pluralLabel; + + return $this; + } + + /** + * Get the resource navigation group. + */ + public function getNavigationGroup(): ?string + { + return $this->navigationGroup ?? config('filament-job-manager.resources.job_batches.navigation_group'); + } + + /** + * Set the resource navigation group. + */ + public function navigationGroup(string $navigationGroup): static + { + $this->navigationGroup = $navigationGroup; + + return $this; + } + + /** + * Get the resource icon. + */ + public function getNavigationIcon(): ?string + { + return $this->navigationIcon ?? config('filament-job-manager.resources.job_batches.navigation_icon'); + } + + /** + * Set the resource icon. + */ + public function navigationIcon(string $navigationIcon): static + { + $this->navigationIcon = $navigationIcon; + + return $this; + } + + /** + * Get the resource sort. + */ + public function getNavigationSort(): ?int + { + return $this->navigationSort ?? config('filament-job-manager.resources.job_batches.navigation_sort'); + } + + /** + * Set the resource sort. + */ + public function navigationSort(int $navigationSort): static + { + $this->navigationSort = $navigationSort; + + return $this; + } + + /** + * Get the resource navigation count badge status. + */ + public function getNavigationCountBadge(): ?bool + { + return $this->navigationCountBadge ?? config('filament-job-manager.resources.job_batches.navigation_count_badge'); + } + + /** + * Set the resource navigation count badge status. + */ + public function navigationCountBadge(bool $navigationCountBadge = true): static + { + $this->navigationCountBadge = $navigationCountBadge; + + return $this; + } + + /** + * Determine whether the resource navigation is enabled. + */ + public function shouldRegisterNavigation(): bool + { + return $this->navigation ?? config('filament-job-manager.resources.job_batches.enabled'); + } + + /** + * Enable the resource navigation. + */ + public function enableNavigation(bool $status = true): static + { + $this->navigation = $status; + + return $this; + } + + /** + * Get the pruning status. + */ + public function getPruning(): ?bool + { + return $this->pruning ?? config('filament-job-batches.pruning.enabled'); + } + + /** + * Set the pruning status. + */ + public function enablePruning(bool $status = true): static + { + $this->pruning = $status; + + return $this; + } + + /** + * Get the pruning retention. + */ + public function getPruningRetention(): ?int + { + return $this->pruningRetention ?? config('filament-job-batches.pruning.retention_days'); + } + + /** + * Set the pruning retention. + */ + public function pruningRetention(int $pruningRetention): static + { + $this->pruningRetention = $pruningRetention; + + return $this; + } + + /** + * Get the resource breadcrumb. + */ + public function getBreadcrumb(): string + { + return __('filament-job-batches::translations.breadcrumb'); + } +} diff --git a/src/FilamentJobsPlugin.php b/src/FilamentJobsPlugin.php new file mode 100644 index 0000000..3345318 --- /dev/null +++ b/src/FilamentJobsPlugin.php @@ -0,0 +1,293 @@ +resources([ + $this->getResource(), + ]); + } + + /** + * Boot the plugin. + */ + public function boot(Panel $panel): void + { + // + } + + /** + * Make a new instance of the plugin. + */ + public static function make(): static + { + return app(static::class); + } + + /** + * Get the plugin instance. + */ + public static function get(): static + { + return filament(app(static::class)->getId()); + } + + /** + * Get the resource class. + */ + public function getResource(): string + { + return $this->resource ?? config('filament-job-manager.resources.jobs.resource'); + } + + /** + * Set the resource class. + */ + public function resource(string $resource): static + { + $this->resource = $resource; + + return $this; + } + + /** + * Get the resource label. + */ + public function getLabel(): ?string + { + return $this->evaluate($this->label) ?? config('filament-job-manager.resources.jobs.label'); + } + + /** + * Set the resource label. + */ + public function label(string $label): static + { + $this->label = $label; + + return $this; + } + + /** + * Get the plural resource label. + */ + public function getPluralLabel(): ?string + { + return $this->evaluate($this->pluralLabel) ?? config('filament-job-manager.resources.jobs.plural_label'); + } + + /** + * Set the plural resource label. + */ + public function pluralLabel(string $pluralLabel): static + { + $this->pluralLabel = $pluralLabel; + + return $this; + } + + /** + * Get the resource navigation group. + */ + public function getNavigationGroup(): ?string + { + return $this->navigationGroup ?? config('filament-job-manager.resources.jobs.navigation_group'); + } + + /** + * Set the resource navigation group. + */ + public function navigationGroup(string $navigationGroup): static + { + $this->navigationGroup = $navigationGroup; + + return $this; + } + + /** + * Get the resource icon. + */ + public function getNavigationIcon(): ?string + { + return $this->navigationIcon ?? config('filament-job-manager.resources.jobs.navigation_icon'); + } + + /** + * Set the resource icon. + */ + public function navigationIcon(string $navigationIcon): static + { + $this->navigationIcon = $navigationIcon; + + return $this; + } + + /** + * Get the resource sort. + */ + public function getNavigationSort(): ?int + { + return $this->navigationSort ?? config('filament-job-manager.resources.jobs.navigation_sort'); + } + + /** + * Set the resource sort. + */ + public function navigationSort(int $navigationSort): static + { + $this->navigationSort = $navigationSort; + + return $this; + } + + /** + * Get the resource navigation count badge status. + */ + public function getNavigationCountBadge(): ?bool + { + return $this->navigationCountBadge ?? config('filament-job-manager.resources.jobs.navigation_count_badge'); + } + + /** + * Set the resource navigation count badge status. + */ + public function navigationCountBadge(bool $navigationCountBadge = true): static + { + $this->navigationCountBadge = $navigationCountBadge; + + return $this; + } + + /** + * Determine whether the resource navigation is enabled. + */ + public function shouldRegisterNavigation(): bool + { + return $this->navigation ?? config('filament-job-manager.resources.jobs.enabled'); + } + + /** + * Enable the resource navigation. + */ + public function enableNavigation(bool $status = true): static + { + $this->navigation = $status; + + return $this; + } + + /** + * Get the pruning status. + */ + public function getPruning(): ?bool + { + return $this->pruning ?? config('filament-jobs.pruning.enabled'); + } + + /** + * Set the pruning status. + */ + public function enablePruning(bool $status = true): static + { + $this->pruning = $status; + + return $this; + } + + /** + * Get the pruning retention. + */ + public function getPruningRetention(): ?int + { + return $this->pruningRetention ?? config('filament-jobs.pruning.retention_days'); + } + + /** + * Set the pruning retention. + */ + public function pruningRetention(int $pruningRetention): static + { + $this->pruningRetention = $pruningRetention; + + return $this; + } + + /** + * Get the resource breadcrumb. + */ + public function getBreadcrumb(): string + { + return __('filament-jobs::translations.breadcrumb'); + } +} diff --git a/src/Models/FailedJob.php b/src/Models/FailedJob.php new file mode 100644 index 0000000..ccb3246 --- /dev/null +++ b/src/Models/FailedJob.php @@ -0,0 +1,10 @@ +getNavigationCountBadge() ? number_format(static::getModel()::count()) : null; + } + + public static function getModelLabel(): string + { + return FilamentJobManagerPlugin::get()->getLabel(); + } + + public static function getPluralModelLabel(): string + { + return FilamentJobManagerPlugin::get()->getPluralLabel(); + } + + public static function getNavigationLabel(): string + { + return Str::title(static::getPluralModelLabel()) ?? Str::title(static::getModelLabel()); + } + + public static function getNavigationGroup(): ?string + { + return FilamentJobManagerPlugin::get()->getNavigationGroup(); + } + + public static function getNavigationSort(): ?int + { + return FilamentJobManagerPlugin::get()->getNavigationSort(); + } + + public static function getBreadcrumb(): string + { + return FilamentJobManagerPlugin::get()->getBreadcrumb(); + } + + public static function shouldRegisterNavigation(): bool + { + return FilamentJobManagerPlugin::get()->shouldRegisterNavigation(); + } + + public static function getNavigationIcon(): string + { + return FilamentJobManagerPlugin::get()->getNavigationIcon(); + } + + public static function form(Form $form): Form + { + return $form + ->schema([ + TextInput::make('uuid')->disabled()->columnSpan(4), + TextInput::make('failed_at')->disabled(), + TextInput::make('id')->disabled(), + TextInput::make('connection')->disabled(), + TextInput::make('queue')->disabled(), + + // make text a little bit smaller because often a complete Stack Trace is shown: + TextArea::make('exception')->disabled()->columnSpan(4)->extraInputAttributes(['style' => 'font-size: 80%;']), + JSONEditor::make('payload')->disabled()->columnSpan(4), + ])->columns(4); + } + + public static function table(Table $table): Table + { + return $table + ->defaultSort('id', 'desc') + ->columns([ + TextColumn::make('id')->sortable()->searchable()->toggleable(), + TextColumn::make('failed_at')->sortable()->searchable(false)->toggleable(), + TextColumn::make('exception') + ->sortable() + ->searchable() + ->toggleable() + ->wrap() + ->limit(200) + ->tooltip(fn (FailedJob $record) => "{$record->failed_at} UUID: {$record->uuid}; Connection: {$record->connection}; Queue: {$record->queue};"), + TextColumn::make('uuid')->sortable()->searchable()->toggleable(isToggledHiddenByDefault: true), + TextColumn::make('connection')->sortable()->searchable()->toggleable(isToggledHiddenByDefault: true), + TextColumn::make('queue')->sortable()->searchable()->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([]) + ->bulkActions([ + BulkAction::make('retry') + ->label('Retry') + ->requiresConfirmation() + ->action(function (Collection $records): void { + foreach ($records as $record) { + Artisan::call("queue:retry {$record->uuid}"); + } + Notification::make() + ->title("{$records->count()} jobs have been pushed back onto the queue.") + ->success() + ->send(); + }), + ]) + ->actions([ + DeleteAction::make('Delete'), + ViewAction::make('View'), + Action::make('retry') + ->label('Retry') + ->requiresConfirmation() + ->action(function (FailedJob $record): void { + Artisan::call("queue:retry {$record->uuid}"); + Notification::make() + ->title("The job with uuid '{$record->uuid}' has been pushed back onto the queue.") + ->success() + ->send(); + }), + ]); + } + + public static function getPages(): array + { + return [ + 'index' => ListFailedJobs::route('/'), + ]; + } +} diff --git a/src/Resources/FailedJobsResource/Pages/ListFailedJobs.php b/src/Resources/FailedJobsResource/Pages/ListFailedJobs.php new file mode 100644 index 0000000..ea7f8d1 --- /dev/null +++ b/src/Resources/FailedJobsResource/Pages/ListFailedJobs.php @@ -0,0 +1,43 @@ +label('Retry all failed Jobs') + ->requiresConfirmation() + ->action(function (): void { + Artisan::call('queue:retry all'); + Notification::make() + ->title('All failed jobs have been pushed back onto the queue.') + ->success() + ->send(); + }), + + Action::make('delete_all') + ->label('Delete all failed Jobs') + ->requiresConfirmation() + ->color('danger') + ->action(function (): void { + FailedJob::truncate(); + Notification::make() + ->title('All failed jobs have been removed.') + ->success() + ->send(); + }), + ]; + } +} diff --git a/src/Resources/JobBatchesResource.php b/src/Resources/JobBatchesResource.php new file mode 100644 index 0000000..7f381a1 --- /dev/null +++ b/src/Resources/JobBatchesResource.php @@ -0,0 +1,90 @@ +getNavigationCountBadge() ? number_format(static::getModel()::count()) : null; + } + + public static function getModelLabel(): string + { + return FilamentJobBatchesPlugin::get()->getLabel(); + } + + public static function getPluralModelLabel(): string + { + return FilamentJobBatchesPlugin::get()->getPluralLabel(); + } + + public static function getNavigationLabel(): string + { + return Str::title(static::getPluralModelLabel()) ?? Str::title(static::getModelLabel()); + } + + public static function getNavigationGroup(): ?string + { + return FilamentJobBatchesPlugin::get()->getNavigationGroup(); + } + + public static function getNavigationSort(): ?int + { + return FilamentJobBatchesPlugin::get()->getNavigationSort(); + } + + public static function getBreadcrumb(): string + { + return FilamentJobBatchesPlugin::get()->getBreadcrumb(); + } + + public static function shouldRegisterNavigation(): bool + { + return FilamentJobBatchesPlugin::get()->shouldRegisterNavigation(); + } + + public static function getNavigationIcon(): string + { + return FilamentJobBatchesPlugin::get()->getNavigationIcon(); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + TextColumn::make('created_at')->dateTime()->sortable()->searchable()->toggleable(), + TextColumn::make('id')->sortable()->searchable()->toggleable(), + TextColumn::make('name')->sortable()->searchable()->toggleable(), + TextColumn::make('cancelled_at')->dateTime()->sortable()->searchable()->toggleable(), + TextColumn::make('failed_at')->dateTime()->sortable()->searchable()->toggleable(), + TextColumn::make('finished_at')->dateTime()->sortable()->searchable()->toggleable(), + TextColumn::make('total_jobs')->sortable()->searchable()->toggleable(), + TextColumn::make('pending_jobs')->sortable()->searchable()->toggleable(), + TextColumn::make('failed_jobs')->sortable()->searchable()->toggleable(), + TextColumn::make('failed_job_ids')->sortable()->searchable()->toggleable(), + ]) + ->actions([ + DeleteAction::make('Delete'), + ]) + ->defaultSort('created_at', 'desc'); + } + + public static function getPages(): array + { + return [ + 'index' => ListJobBatches::route('/'), + ]; + } +} diff --git a/src/Resources/JobBatchesResource/Pages/ListJobBatches.php b/src/Resources/JobBatchesResource/Pages/ListJobBatches.php new file mode 100644 index 0000000..6729e4a --- /dev/null +++ b/src/Resources/JobBatchesResource/Pages/ListJobBatches.php @@ -0,0 +1,31 @@ +label('Prune all batches') + ->requiresConfirmation() + ->color('danger') + ->action(function (): void { + Artisan::call('queue:prune-batches'); + Notification::make() + ->title('All batches have been pruned.') + ->success() + ->send(); + }), + ]; + } +} diff --git a/src/Resources/JobsResource.php b/src/Resources/JobsResource.php new file mode 100644 index 0000000..baf76e8 --- /dev/null +++ b/src/Resources/JobsResource.php @@ -0,0 +1,146 @@ +getNavigationCountBadge() ? number_format(static::getModel()::count()) : null; + } + + public static function getModelLabel(): string + { + return FilamentJobsPlugin::get()->getLabel(); + } + + public static function getPluralModelLabel(): string + { + return FilamentJobsPlugin::get()->getPluralLabel(); + } + + public static function getNavigationLabel(): string + { + return Str::title(static::getPluralModelLabel()) ?? Str::title(static::getModelLabel()); + } + + public static function getNavigationGroup(): ?string + { + return FilamentJobsPlugin::get()->getNavigationGroup(); + } + + public static function getNavigationSort(): ?int + { + return FilamentJobsPlugin::get()->getNavigationSort(); + } + + public static function getBreadcrumb(): string + { + return FilamentJobsPlugin::get()->getBreadcrumb(); + } + + public static function shouldRegisterNavigation(): bool + { + return FilamentJobsPlugin::get()->shouldRegisterNavigation(); + } + + public static function getNavigationIcon(): string + { + return FilamentJobsPlugin::get()->getNavigationIcon(); + } + + public static function form(Form $form): Form + { + return $form + ->schema([ + TextInput::make('job_id') + ->required() + ->maxLength(255), + TextInput::make('name') + ->maxLength(255), + TextInput::make('queue') + ->maxLength(255), + DateTimePicker::make('started_at'), + DateTimePicker::make('finished_at'), + Toggle::make('failed') + ->required(), + TextInput::make('attempt') + ->required(), + Textarea::make('exception_message') + ->maxLength(65535), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + TextColumn::make('status') + ->badge() + ->label(__('filament-jobs-monitor::translations.status')) + ->sortable() + ->formatStateUsing(fn (string $state): string => __("filament-jobs-monitor::translations.{$state}")) + ->color(fn (string $state): string => match ($state) { + 'running' => 'primary', + 'succeeded' => 'success', + 'failed' => 'danger', + }), + TextColumn::make('name') + ->label(__('filament-jobs-monitor::translations.name')) + ->sortable(), + TextColumn::make('queue') + ->label(__('filament-jobs-monitor::translations.queue')) + ->sortable(), + TextColumn::make('progress') + ->label(__('filament-jobs-monitor::translations.progress')) + ->formatStateUsing(fn (string $state) => "{$state}%") + ->sortable(), + // ProgressColumn::make('progress')->label(__('filament-jobs-monitor::translations.progress'))->color('warning'), + TextColumn::make('started_at') + ->label(__('filament-jobs-monitor::translations.started_at')) + ->since() + ->sortable(), + ]) + ->bulkActions([ + DeleteBulkAction::make(), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => ListJobs::route('/'), + ]; + } + + public static function getWidgets(): array + { + return [ + JobStatsOverview::class, + ]; + } +} diff --git a/src/Resources/JobsResource/Pages/ListJobs.php b/src/Resources/JobsResource/Pages/ListJobs.php new file mode 100644 index 0000000..b3bbe9c --- /dev/null +++ b/src/Resources/JobsResource/Pages/ListJobs.php @@ -0,0 +1,29 @@ +select($aggregationColumns) + ->first(); + + return [ + Stat::make(__('filament-jobs-monitor::translations.total_jobs'), $aggregatedInfo->count ?? 0), + Stat::make(__('filament-jobs-monitor::translations.execution_time'), ($aggregatedInfo->total_time_elapsed ?? 0).'s'), + Stat::make(__('filament-jobs-monitor::translations.average_time'), ceil((float) $aggregatedInfo->average_time_elapsed).'s' ?? 0), + ]; + } +} diff --git a/src/Traits/QueueProgress.php b/src/Traits/QueueProgress.php new file mode 100644 index 0000000..6473640 --- /dev/null +++ b/src/Traits/QueueProgress.php @@ -0,0 +1,50 @@ +getQueueMonitor()) { + return; + } + + $monitor->update([ + 'progress' => $progress, + ]); + + $this->progressLastUpdated = time(); + } + + /** + * Return Queue Monitor Model. + */ + protected function getQueueMonitor(): ?Job + { + if (! property_exists($this, 'job')) { + return null; + } + + if (! $this->job) { + return null; + } + + if (! $jobId = Job::getJobId($this->job)) { + return null; + } + + $model = Job::getModel(); + + return $model::whereJobId($jobId) + ->orderBy('started_at', 'desc') + ->first(); + } +}