Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6f59200
Add modern isolated admin dashboard with Bootstrap 5
dereuromark Mar 17, 2026
b410e2f
Fix PHPStan error for optional Authorization dependency
dereuromark Mar 17, 2026
085ba92
Apply suggestions from code review
dereuromark Mar 17, 2026
47bcee8
Potential fix for pull request finding
dereuromark Mar 17, 2026
8adab94
Potential fix for pull request finding
dereuromark Mar 17, 2026
a2e9576
Add runtime fallback for Tools Time->duration() method (#462)
dereuromark Mar 17, 2026
3a47926
Remove redundant sidebar links and improve template styling
dereuromark Mar 17, 2026
f1d533e
Make demo jobs section collapsible (collapsed by default)
dereuromark Mar 17, 2026
66b01a0
Modernize all edit/form templates to Bootstrap 5
dereuromark Mar 17, 2026
f5325c3
Fix Execute Job placement and search task dropdown
dereuromark Mar 17, 2026
6f395cf
Merge origin/master into feature/isolated-admin-dashboard
dereuromark Mar 18, 2026
46b51b9
Fixes and improvements for admin dashboard
dereuromark Mar 18, 2026
ba66587
Default to isolated layout, add missing tests
dereuromark Mar 18, 2026
1a5f03f
Fix CS
dereuromark Mar 18, 2026
275e9f6
Fix CS
dereuromark Mar 18, 2026
8fe1a43
Add title attributes to duration and memory icons
dereuromark Mar 18, 2026
03813e8
Add Server Time title to clock icons
dereuromark Mar 18, 2026
f2a2232
Simplify
dereuromark Mar 18, 2026
04ebc4b
Fix status display for requeued jobs
dereuromark Mar 18, 2026
2b56a17
Fix dashboard stats double-counting jobs
dereuromark Mar 18, 2026
fc13957
Improve dashboard stats display
dereuromark Mar 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions config/app.example.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,22 @@
'ignoredTasks' => [],

// per-task configuration overrides (timeout, retries, rate, costs, unique)
// 'tasks' => [
// 'Queue.ProgressExample' => [
// 'timeout' => 300,
// ],
// ],
'tasks' => [
//'Queue.ProgressExample' => [
// 'timeout' => 300,
//],
],

// Admin dashboard settings

// Layout for admin pages:
// - null (default): Uses 'Queue.queue' isolated Bootstrap 5 layout
// - false: Disables plugin layout, uses app's default layout
// - string: Uses specified layout
'adminLayout' => null,

// auto-refresh dashboard in seconds (0 = disabled)
'dashboardAutoRefresh' => 0,
],
'Icon' => [
'sets' => [
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* [Custom Tasks](sections/custom_tasks.md)

## Detailed documentation
* [Admin Dashboard](sections/admin_dashboard.md) [**NEW**]
* [Configuration](sections/configuration.md)
* [Cron](sections/cron.md) for Cronjob management
* [Real-Time Progress](sections/realtime_progress.md) with Mercure/FrankenPHP
Expand Down
111 changes: 111 additions & 0 deletions docs/sections/admin_dashboard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Admin Dashboard

The Queue plugin includes a modern, self-contained admin dashboard for managing your queued jobs. The dashboard is completely isolated from your application's CSS/JS, using Bootstrap 5 and Font Awesome 6 via CDN.

## Features

- **Statistics Overview**: Real-time counts of pending, scheduled, running, and failed jobs
- **Status Banner**: Visual indicator showing whether the queue is running or idle
- **Job Management**: View, reset, and remove jobs directly from the dashboard
- **Worker Management**: Monitor active workers and terminate them if needed
- **Process History**: Track all queue processes with pagination
- **Trigger Jobs**: Manually add jobs that implement `AddFromBackendInterface`
- **Configuration View**: See current runtime configuration at a glance

## Layout Configuration

By default, the admin dashboard uses the isolated Bootstrap 5 layout (`Queue.queue`). This ensures the dashboard works independently of your application's styles.

### Configuration Options

```php
'Queue' => [
// Layout for admin pages:
// - null (default): Uses 'Queue.queue' isolated Bootstrap 5 layout
// - false: Disables plugin layout, uses app's default layout
// - string: Uses specified layout
'adminLayout' => null,

// Auto-refresh dashboard every N seconds (0 = disabled)
'dashboardAutoRefresh' => 30,
],
```

### Using Your Application's Layout

To use your application's default layout instead of the isolated Bootstrap 5 layout:

```php
'Queue' => [
'adminLayout' => false,
],
```

## Accessing the Dashboard

Navigate to `/admin/queue` to access the dashboard. The main pages are:

- `/admin/queue` - Dashboard with overview statistics
- `/admin/queue/processes` - Active workers management
- `/admin/queued-jobs` - Full job listing with search
- `/admin/queue-processes` - Process history

## Customization

### Overriding Templates

You can override any template by creating the same file structure in your application's `templates/plugin/Queue/` directory:

```
templates/
└── plugin/
└── Queue/
├── layout/
│ └── queue.php
└── Admin/
└── Queue/
└── index.php
```

### Custom Elements

The dashboard uses several reusable elements that you can override:

- `Queue.Queue/sidebar` - Sidebar navigation
- `Queue.Queue/stats_card` - Statistics cards
- `Queue.Queue/status_badge` - Job status badges
- `Queue.flash/success` - Success flash messages
- `Queue.flash/error` - Error flash messages
- `Queue.flash/warning` - Warning flash messages
- `Queue.flash/info` - Info flash messages

### CSS Variables

The isolated layout uses CSS variables that you can override:

```css
:root {
--queue-primary: #0d6efd;
--queue-success: #198754;
--queue-warning: #ffc107;
--queue-danger: #dc3545;
--queue-info: #0dcaf0;
--queue-secondary: #6c757d;
--queue-dark: #212529;
--queue-light: #f8f9fa;
--queue-sidebar-bg: linear-gradient(135deg, #2c3e50 0%, #1a252f 100%);
--queue-sidebar-width: 260px;
}
```

## Screenshots

The dashboard provides:

1. **Status Banner** - Shows queue status (Running/Idle) with last activity timestamp
2. **Stats Cards** - Quick overview of job counts by status
3. **Pending Jobs Table** - List of pending/running jobs with inline actions
4. **Scheduled Jobs** - Jobs scheduled for future execution
5. **Statistics** - Aggregated statistics for completed jobs
6. **Trigger Jobs** - Buttons to manually trigger addable jobs
7. **Configuration** - Current runtime configuration display
39 changes: 39 additions & 0 deletions src/Controller/Admin/QueueAppController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);

namespace Queue\Controller\Admin;

use Cake\Controller\Controller;
use Cake\Core\Configure;

/**
* QueueAppController
*
* Isolated base controller for Queue admin that doesn't depend on host app's AppController.
* This ensures the admin dashboard can function independently with its own layout and styling.
*/
class QueueAppController extends Controller {

use LoadHelperTrait;

/**
* @return void
*/
public function initialize(): void {
parent::initialize();

$this->loadComponent('Flash');

$this->loadHelpers();

// Layout configuration:
// - null (default): Uses 'Queue.queue' isolated Bootstrap 5 layout
// - false: Disables plugin layout, uses app's default layout
// - string: Uses specified layout (e.g., 'Queue.queue' or custom)
$layout = Configure::read('Queue.adminLayout');
if ($layout !== false) {
$this->viewBuilder()->setLayout($layout ?: 'Queue.queue');
}
}

}
54 changes: 40 additions & 14 deletions src/Controller/Admin/QueueController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

namespace Queue\Controller\Admin;

use App\Controller\AppController;
use Cake\Core\App;
use Cake\Core\Configure;
use Cake\Http\Exception\NotFoundException;
use Queue\Queue\AddFromBackendInterface;
use Queue\Queue\AddInterface;
Expand All @@ -14,24 +14,13 @@
* @property \Queue\Model\Table\QueuedJobsTable $QueuedJobs
* @property \Queue\Model\Table\QueueProcessesTable $QueueProcesses
*/
class QueueController extends AppController {

use LoadHelperTrait;
class QueueController extends QueueAppController {

/**
* @var string|null
*/
protected ?string $defaultTable = 'Queue.QueuedJobs';

/**
* @return void
*/
public function initialize(): void {
parent::initialize();

$this->loadHelpers();
}

/**
* Admin center.
* Manage queues from admin backend (without the need to open ssh console window).
Expand Down Expand Up @@ -61,7 +50,44 @@ public function index() {
$addableTasks = $taskFinder->allAddable(AddFromBackendInterface::class);

$servers = $QueueProcesses->serverList();
$this->set(compact('new', 'current', 'data', 'pendingDetails', 'scheduledDetails', 'status', 'tasks', 'addableTasks', 'servers'));
$workers = $status ? $status['workers'] : 0;

$scheduledJobs = count($scheduledDetails);
$runningJobs = $this->QueuedJobs->find()
->where([
'completed IS' => null,
'fetched IS NOT' => null,
'failure_message IS' => null,
])
->count();
$failedJobs = $this->QueuedJobs->find()
->where([
'completed IS' => null,
'failure_message IS NOT' => null,
])
->count();
// Pending = total pending minus running and failed (to avoid double counting)
$pendingJobs = max(0, count($pendingDetails) - $runningJobs - $failedJobs);

$configurations = (array)Configure::read('Queue');

$this->set(compact(
'new',
'current',
'data',
'pendingDetails',
'scheduledDetails',
'status',
'tasks',
'addableTasks',
'servers',
'workers',
'pendingJobs',
'scheduledJobs',
'runningJobs',
'failedJobs',
'configurations',
));
}

/**
Expand Down
14 changes: 1 addition & 13 deletions src/Controller/Admin/QueueProcessesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

namespace Queue\Controller\Admin;

use App\Controller\AppController;
use Cake\Core\Configure;
use Exception;
use const SIGTERM;
Expand All @@ -13,9 +12,7 @@
* @method \Cake\Datasource\ResultSetInterface<\Queue\Model\Entity\QueueProcess> paginate(\Cake\Datasource\RepositoryInterface|\Cake\Datasource\QueryInterface|string|null $object = null, array $settings = [])
* @property \Queue\Model\Table\QueuedJobsTable $QueuedJobs
*/
class QueueProcessesController extends AppController {

use LoadHelperTrait;
class QueueProcessesController extends QueueAppController {

/**
* @var array<string, mixed>
Expand All @@ -26,15 +23,6 @@ class QueueProcessesController extends AppController {
],
];

/**
* @return void
*/
public function initialize(): void {
parent::initialize();

$this->loadHelpers();
}

/**
* Index method
*
Expand Down
18 changes: 9 additions & 9 deletions src/Controller/Admin/QueuedJobsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

namespace Queue\Controller\Admin;

use App\Controller\AppController;
use Cake\Core\Configure;
use Cake\Core\Plugin;
use Cake\Http\Exception\NotFoundException;
Expand All @@ -17,9 +16,7 @@
* @method \Cake\Datasource\ResultSetInterface<\Queue\Model\Entity\QueuedJob> paginate($object = null, array $settings = [])
* @property \Search\Controller\Component\SearchComponent $Search
*/
class QueuedJobsController extends AppController {

use LoadHelperTrait;
class QueuedJobsController extends QueueAppController {

/**
* @var array<string, mixed>
Expand All @@ -37,7 +34,6 @@ public function initialize(): void {
parent::initialize();

$this->enableSearch();
$this->loadHelpers();
}

/**
Expand Down Expand Up @@ -81,27 +77,31 @@ public function index() {
}

/**
* Index method
* Stats method
*
* @param string|null $jobType
* Uses query parameter `job_type` to filter by specific job type.
* Query parameter is used instead of route parameter to support job types
* containing slashes (e.g., Vendor/Plugin.Task).
*
* @throws \Cake\Http\Exception\NotFoundException
*
* @return void
*/
public function stats(?string $jobType = null): void {
public function stats(): void {
if (!Configure::read('Queue.isStatisticEnabled')) {
throw new NotFoundException('Not enabled');
}

// Use query parameter to avoid routing issues with job types containing slashes (e.g., Vendor/Plugin.Task)
$jobType = $this->request->getQuery('job_type');
$stats = $this->QueuedJobs->getFullStats($jobType);

$jobTypes = $this->QueuedJobs->find()->where()->find(
'list',
keyField: 'job_task',
valueField: 'job_task',
)->distinct('job_task')->toArray();
$this->set(compact('stats', 'jobTypes'));
$this->set(compact('stats', 'jobTypes', 'jobType'));
}

/**
Expand Down
Loading
Loading