add: [WiP] background jobs basic ui

pull/9318/head
Luciano Righetti 2023-10-05 11:32:33 +02:00
parent 1151d4779b
commit 14c675fa33
9 changed files with 738 additions and 65 deletions

View File

@ -18,6 +18,7 @@ declare(strict_types=1);
namespace App\Controller;
use App\Lib\Tools\BackgroundJobsTool;
use Cake\Controller\Controller;
use Cake\Core\Configure;
use Cake\Event\EventInterface;
@ -41,6 +42,9 @@ class AppController extends Controller
public $breadcrumb = [];
public $request_ip = null;
/** @var BackgroundJobsTool */
private static $loadedBackgroundJobsTool;
/**
* Initialization hook method.
*
@ -60,7 +64,7 @@ class AppController extends Controller
$this->loadComponent(
'ParamHandler',
[
'request' => $this->request
'request' => $this->request
]
);
$this->loadModel('MetaFields');
@ -69,30 +73,30 @@ class AppController extends Controller
$this->loadComponent(
'CRUD',
[
'request' => $this->request,
'table' => $table,
'MetaFields' => $this->MetaFields,
'MetaTemplates' => $this->MetaTemplates
'request' => $this->request,
'table' => $table,
'MetaFields' => $this->MetaFields,
'MetaTemplates' => $this->MetaTemplates
]
);
$this->loadComponent('Authentication.Authentication');
$this->loadComponent(
'ACL',
[
'request' => $this->request,
'Authentication' => $this->Authentication
'request' => $this->request,
'Authentication' => $this->Authentication
]
);
$this->loadComponent(
'Navigation',
[
'request' => $this->request,
'request' => $this->request,
]
);
$this->loadComponent(
'Notification',
[
'request' => $this->request,
'request' => $this->request,
]
);
if (Configure::read('debug')) {
@ -121,7 +125,7 @@ class AppController extends Controller
$user = $this->Users->get(
$this->request->getAttribute('identity')->getIdentifier(),
[
'contain' => ['Roles', /*'UserSettings',*/ 'Organisations']
'contain' => ['Roles', /*'UserSettings',*/ 'Organisations']
]
);
if (!empty($user['disabled'])) {
@ -204,11 +208,11 @@ class AppController extends Controller
$user = $this->Users->get($authKey['user_id']);
$logModel->insert(
[
'request_action' => 'login',
'model' => 'Users',
'model_id' => $user['id'],
'model_title' => $user['username'],
'changed' => []
'request_action' => 'login',
'model' => 'Users',
'model_id' => $user['id'],
'model_title' => $user['username'],
'changed' => []
]
);
if (!empty($user)) {
@ -218,11 +222,11 @@ class AppController extends Controller
$user = $logModel->userInfo();
$logModel->insert(
[
'request_action' => 'login',
'model' => 'Users',
'model_id' => $user['id'],
'model_title' => $user['name'],
'changed' => []
'request_action' => 'login',
'model' => 'Users',
'model_id' => $user['id'],
'model_title' => $user['name'],
'changed' => []
]
);
}
@ -273,4 +277,16 @@ class AppController extends Controller
session_abort();
return $user;
}
/**
* @return BackgroundJobsTool
*/
public function getBackgroundJobsTool(): BackgroundJobsTool
{
if (!self::$loadedBackgroundJobsTool) {
$backgroundJobsTool = new BackgroundJobsTool(Configure::read('BackgroundJobs'));
self::$loadedBackgroundJobsTool = $backgroundJobsTool;
}
return self::$loadedBackgroundJobsTool;
}
}

View File

@ -0,0 +1,218 @@
<?php
namespace App\Controller;
use App\Controller\AppController;
use Cake\Http\Exception\MethodNotAllowedException;
use Cake\Http\Exception\NotFoundException;
use Cake\Http\Response;
use Cake\ORM\Locator\LocatorAwareTrait;
use Cake\Core\Configure;
use Cake\Event\EventInterface;
class JobsController extends AppController
{
use LocatorAwareTrait;
public $paginate = [
'limit' => 20,
'recursive' => 0,
'order' => [
'Job.id' => 'DESC'
],
'contain' => [
'Organisations' => [
'fields' => ['id', 'name', 'uuid'],
],
]
];
public function beforeFilter(EventInterface $event)
{
parent::beforeFilter($event);
if ($this->request->getParam('action') === 'getGenerateCorrelationProgress') {
$this->Security->doNotGenerateToken = true;
}
}
public function index($queue = false)
{
if (!Configure::read('BackgroundJobs.enabled')) {
throw new NotFoundException('Background jobs are not enabled on this instance.');
}
$ServerTable = $this->fetchTable('Servers');
$issueCount = 0;
$workers = $ServerTable->workerDiagnostics($issueCount);
$queues = ['email', 'default', 'cache', 'prio', 'update'];
if ($queue && in_array($queue, $queues, true)) {
$this->paginate['conditions'] = ['Job.worker' => $queue];
}
$jobs = $this->paginate()->toArray();
foreach ($jobs as &$job) {
if (!empty($job['process_id'])) {
$job['job_status'] = $this->getJobStatus($job['process_id']);
$job['failed'] = $job['job_status'] === 'Failed';
} else {
$job['job_status'] = 'Unknown';
$job['failed'] = null;
}
if (Configure::read('BackgroundJobs.enabled')) {
$job['worker_status'] = true;
} else {
$job['worker_status'] = isset($workers[$job['worker']]) && $workers[$job['worker']]['ok'];
}
}
if ($this->ParamHandler->isRest()) {
return $this->RestResponse->viewData($jobs);
}
$this->set('jobs', $jobs);
$this->set('queue', $queue);
}
public function getError($id)
{
$fields = array(
'Failed at' => 'failed_at',
'Exception' => 'exception',
'Error' => 'error'
);
$this->set('fields', $fields);
$this->set('response', $this->getFailedJobLog($id));
$this->render('/Jobs/ajax/error');
}
private function jobStatusConverter($status)
{
switch ($status) {
case 1:
return 'Waiting';
case 2:
return 'Running';
case 3:
return 'Failed';
case 4:
return 'Completed';
default:
return 'Unknown';
}
}
public function getGenerateCorrelationProgress($ids)
{
$this->closeSession();
$ids = explode(",", $ids);
$jobs = $this->Jobs->find('all', [
'fields' => ['id', 'progress', 'process_id'],
'conditions' => ['id' => $ids],
'recursive' => -1,
]);
if (empty($jobs)) {
throw new NotFoundException('No jobs found');
}
$output = [];
foreach ($jobs as $job) {
$output[$job['id']] = [
'job_status' => $this->getJobStatus($job['process_id']),
'progress' => (int)$job['progress'],
];
}
return $this->RestResponse->viewData($output, 'json');
}
public function getProgress($type)
{
$org_id = $this->Auth->user('org_id');
if ($this->isSiteAdmin()) {
$org_id = 0;
}
if (is_numeric($type)) {
$progress = $this->Jobs->find('first', array(
'conditions' => array(
'Job.id' => $type,
'org_id' => $org_id
),
'fields' => array('id', 'progress'),
'order' => array('Job.id' => 'desc'),
));
} else {
$progress = $this->Jobs->find('first', array(
'conditions' => array(
'job_type' => $type,
'org_id' => $org_id
),
'fields' => array('id', 'progress'),
'order' => array('Job.id' => 'desc'),
));
}
if (!$progress) {
$progress = 0;
} else {
$progress = $progress['progress'];
}
if ($this->ParamHandler->isRest()) {
return $this->RestResponse->viewData(array('progress' => $progress . '%'));
} else {
return new Response(array('body' => json_encode($progress), 'type' => 'json'));
}
}
public function cache($type)
{
if (Configure::read('MISP.disable_cached_exports')) {
throw new MethodNotAllowedException('This feature is currently disabled');
}
if ($this->isSiteAdmin()) {
$target = 'All events.';
} else {
$target = 'Events visible to: ' . $this->Auth->user('Organisation')['name'];
}
$id = $this->Job->cache($type, $this->Auth->user());
if ($this->ParamHandler->isRest()) {
return $this->RestResponse->viewData(array('job_id' => $id));
} else {
return new Response(array('body' => json_encode($id), 'type' => 'json'));
}
}
public function clearJobs($type = 'completed')
{
if ($this->request->is('post')) {
if ($type === 'all') {
$conditions = array('Job.id !=' => 0);
$message = __('All jobs have been purged');
} else {
$conditions = array('Job.progress' => 100);
$message = __('All completed jobs have been purged');
}
$this->Jobs->deleteAll($conditions, false);
$this->Flash->success($message);
$this->redirect(array('action' => 'index'));
}
}
private function getJobStatus($id): string
{
$status = null;
if (!empty($id)) {
$job = $this->getBackgroundJobsTool()->getJob($id);
$status = $job ? $job->status() : $status;
}
return $this->jobStatusConverter($status);
}
private function getFailedJobLog(string $id): array
{
$job = $this->getBackgroundJobsTool()->getJob($id);
$output = $job ? $job->output() : __('Job status not found.');
$backtrace = $job ? explode("\n", $job->error()) : [];
return [
'error' => $output ?? $backtrace[0] ?? '',
'backtrace' => $backtrace
];
}
}

View File

@ -4,17 +4,17 @@ declare(strict_types=1);
namespace App\Lib\Tools;
use Cake\Utility\Text;
use App\Model\Entity\Worker;
use App\Model\Entity\BackgroundJob;
use Redis;
use Exception;
use RuntimeException;
use Cake\Log\LogTrait;
use InvalidArgumentException;
use Cake\ORM\Locator\LocatorAwareTrait;
use App\Model\Entity\Worker;
use Cake\Datasource\Exception\RecordNotFoundException;
use Cake\Http\Exception\NotFoundException;
use Cake\Log\LogTrait;
use Cake\ORM\Locator\LocatorAwareTrait;
use Cake\Utility\Text;
use Exception;
use InvalidArgumentException;
use Redis;
use RuntimeException;
/**
* BackgroundJobs Tool
@ -41,53 +41,52 @@ class BackgroundJobsTool
/** @var \Supervisor\Supervisor */
private $Supervisor;
const MISP_WORKERS_PROCESS_GROUP = 'misp-workers';
public const MISP_WORKERS_PROCESS_GROUP = 'misp-workers';
const
public const
STATUS_RUNNING = 0,
STATUS_NOT_ENABLED = 1,
STATUS_REDIS_NOT_OK = 2,
STATUS_SUPERVISOR_NOT_OK = 3,
STATUS_REDIS_AND_SUPERVISOR_NOT_OK = 4;
const
public const
DEFAULT_QUEUE = 'default',
EMAIL_QUEUE = 'email',
CACHE_QUEUE = 'cache',
PRIO_QUEUE = 'prio',
UPDATE_QUEUE = 'update',
SCHEDULER_QUEUE = 'scheduler';
UPDATE_QUEUE = 'update';
const VALID_QUEUES = [
self::DEFAULT_QUEUE,
self::EMAIL_QUEUE,
self::CACHE_QUEUE,
self::PRIO_QUEUE,
self::UPDATE_QUEUE,
self::SCHEDULER_QUEUE,
];
public const
VALID_QUEUES = [
self::DEFAULT_QUEUE,
self::EMAIL_QUEUE,
self::CACHE_QUEUE,
self::PRIO_QUEUE,
self::UPDATE_QUEUE
];
const
public const
CMD_EVENT = 'event',
CMD_SERVER = 'server',
CMD_ADMIN = 'admin',
CMD_WORKFLOW = 'workflow';
const ALLOWED_COMMANDS = [
public const ALLOWED_COMMANDS = [
self::CMD_EVENT,
self::CMD_SERVER,
self::CMD_ADMIN,
self::CMD_WORKFLOW,
self::CMD_WORKFLOW
];
const CMD_TO_SHELL_DICT = [
public const CMD_TO_SHELL_DICT = [
self::CMD_EVENT => 'EventShell',
self::CMD_SERVER => 'ServerShell',
self::CMD_ADMIN => 'AdminShell',
self::CMD_WORKFLOW => 'WorkflowShell',
self::CMD_WORKFLOW => 'WorkflowShell'
];
const JOB_STATUS_PREFIX = 'job_status',
private const JOB_STATUS_PREFIX = 'job_status',
DATA_CONTENT_PREFIX = 'data_content';
/** @var array */
@ -193,7 +192,8 @@ class BackgroundJobsTool
'id' => Text::uuid(),
'command' => $command,
'args' => $args,
'metadata' => $metadata
'metadata' => $metadata,
'worker' => $queue,
]
);
@ -203,7 +203,7 @@ class BackgroundJobsTool
$this->RedisConnection->exec();
if ($jobId) {
$this->updateJobProcessId($jobId, $backgroundJob->id());
$this->updateJobProcessId($jobId, $backgroundJob);
}
return $backgroundJob->id();
@ -301,14 +301,16 @@ class BackgroundJobsTool
foreach ($procs as $proc) {
if ($proc->offsetGet('group') === self::MISP_WORKERS_PROCESS_GROUP) {
if ($proc->offsetGet('pid') > 0) {
$workers[] = new Worker([
'pid' => $proc->offsetGet('pid'),
'queue' => explode("_", $proc->offsetGet('name'))[0],
'user' => $this->processUser((int) $proc->offsetGet('pid')),
'createdAt' => $proc->offsetGet('start'),
'updatedAt' => $proc->offsetGet('now'),
'status' => $this->convertProcessStatus($proc->offsetGet('state'))
]);
$workers[] = new Worker(
[
'pid' => $proc->offsetGet('pid'),
'queue' => explode("_", $proc->offsetGet('name'))[0],
'user' => $this->processUser((int) $proc->offsetGet('pid')),
'createdAt' => $proc->offsetGet('start'),
'updatedAt' => $proc->offsetGet('now'),
'status' => $this->convertProcessStatus($proc->offsetGet('state'))
]
);
}
}
}
@ -672,11 +674,24 @@ class BackgroundJobsTool
return new \Supervisor\Supervisor($client);
}
private function updateJobProcessId(int $jobId, string $processId)
private function updateJobProcessId(int $jobId, BackgroundJob $backgroundJob)
{
$JobTable = $this->fetchTable('Jobs');
$jobEntity = $JobTable->get($jobId);
$jobEntity->set('process_id', $processId);
try {
$jobEntity = $JobTable->get($jobId);
} catch (RecordNotFoundException $e) {
$this->log("Job ID does not exist in the database, creating Job database record.", 'warning');
$jobEntity = $JobTable->newEntity(
[
'id' => $jobId,
'worker' => $backgroundJob->worker(),
'job_type' => '?',
'job_input' => '?',
'message' => '?'
]
);
}
$jobEntity->set('process_id', $backgroundJob->id());
$JobTable->save($jobEntity);
}

View File

@ -56,6 +56,9 @@ class BackgroundJob implements JsonSerializable
/** @var integer */
private $returnCode;
/** @var string */
private $worker;
public function __construct(array $properties)
{
$this->id = $properties['id'];
@ -67,6 +70,7 @@ class BackgroundJob implements JsonSerializable
$this->error = $properties['error'] ?? null;
$this->progress = $properties['progress'] ?? 0;
$this->metadata = $properties['metadata'] ?? [];
$this->worker = $properties['worker'] ?? Job::WORKER_DEFAULT;
}
/**
@ -114,7 +118,7 @@ class BackgroundJob implements JsonSerializable
while (true) {
$read = [$pipes[1], $pipes[2]];
$write = null;
$fcept = null;
$except = null;
if (false === ($changedStreams = stream_select($read, $write, $except, 5))) {
throw new RuntimeException("Could not select stream");
@ -213,6 +217,11 @@ class BackgroundJob implements JsonSerializable
return $this->returnCode;
}
public function worker(): string
{
return $this->worker;
}
public function setStatus(int $status)
{
$this->status = $status;
@ -237,4 +246,9 @@ class BackgroundJob implements JsonSerializable
{
$this->updatedAt = $updatedAt;
}
public function setWorker(string $worker)
{
$this->worker = $worker;
}
}

19
src/Model/Entity/Job.php Normal file
View File

@ -0,0 +1,19 @@
<?php
namespace App\Model\Entity;
use App\Model\Entity\AppModel;
class Job extends AppModel
{
public const STATUS_WAITING = 1,
STATUS_RUNNING = 2,
STATUS_FAILED = 3,
STATUS_COMPLETED = 4;
public const WORKER_EMAIL = 'email',
WORKER_PRIO = 'prio',
WORKER_DEFAULT = 'default',
WORKER_CACHE = 'cache',
WORKER_UPDATE = 'update';
}

View File

@ -2,14 +2,189 @@
namespace App\Model\Table;
use App\Lib\Tools\BackgroundJobsTool;
use App\Model\Entity\Job;
use App\Model\Table\AppTable;
use ArrayObject;
use Cake\Event\EventInterface;
use Cake\Http\Exception\MethodNotAllowedException;
use Cake\ORM\Locator\LocatorAwareTrait;
use Exception;
class JobsTable extends AppTable
{
use LocatorAwareTrait;
public function initialize(array $config): void
{
parent::initialize($config);
$this->belongsTo(
'Organisations',
[
'dependent' => false,
'cascadeCallbacks' => false,
'foreignKey' => 'org_id',
'propertyName' => 'Organisation'
]
);
$this->setDisplayField('name');
}
public function beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObject $options)
{
$date = date('Y-m-d H:i:s');
if (!isset($data['date_created'])) {
$data['date_created'] = $date;
}
$data['date_modified'] = $date;
}
public function cache($type, $user)
{
$jobId = $this->createJob(
$user,
Job::WORKER_CACHE,
'cache_' . $type,
$user['Role']['perm_site_admin'] ? 'All events.' : 'Events visible to: ' . $user['Organisation']['name'],
'Fetching events.'
);
$EventsTable = $this->fetchTable('Events');
if (in_array($type, array_keys($EventsTable->exportTypes())) && $type !== 'bro') {
$this->getBackgroundJobsTool()->enqueue(
BackgroundJobsTool::CACHE_QUEUE,
BackgroundJobsTool::CMD_EVENT,
[
'cache',
$user['id'],
$jobId,
$type
],
true,
$jobId
);
} elseif ($type === 'bro') {
$this->getBackgroundJobsTool()->enqueue(
BackgroundJobsTool::CACHE_QUEUE,
BackgroundJobsTool::CMD_EVENT,
[
'cachebro',
$user['id'],
$jobId,
$type
],
true,
$jobId
);
} else {
throw new MethodNotAllowedException('Invalid export type.');
}
return $jobId;
}
/**
* @param array|string $user
* @param string $worker
* @param string $jobType
* @param string$jobInput
* @param string $message
* @return int Job ID
* @throws Exception
*/
public function createJob($user, $worker, $jobType, $jobInput, $message = '')
{
$job = $this->newEntity(
[
'worker' => $worker,
'status' => 0,
'retries' => 0,
'org_id' => $user === 'SYSTEM' ? 0 : $user['org_id'],
'job_type' => $jobType,
'job_input' => $jobInput,
'message' => $message,
]
);
if (!$this->save($job, ['atomic' => false])) { // no need to start transaction for single insert
throw new Exception("Could not save job.");
}
return $job->id;
}
/**
* @param int|null $jobId
* @param string|null $message
* @param int|null $progress
* @return bool|null
*/
public function saveProgress($jobId = null, $message = null, $progress = null)
{
if ($jobId === null) {
return null;
}
$jobData = [
$this->primaryKey => $jobId,
];
if ($message !== null) {
$jobData['message'] = $message;
}
if ($progress !== null) {
$jobData['progress'] = $progress;
if ($progress >= 100) {
$jobData['status'] = Job::STATUS_COMPLETED;
}
}
$jobEntity = $this->newEntity($jobData);
try {
if ($this->save($jobEntity, ['atomic' => false])) {
return true;
}
$this->log("Could not save progress for job $jobId because of validation errors: " . json_encode($this->validationErrors), LOG_NOTICE);
} catch (Exception $e) {
$this->logException("Could not save progress for job $jobId", $e, LOG_NOTICE);
}
return false;
}
/**
* @param int|null $jobId
* @param bool $success
* @param string|null $message
* @return bool|null
*/
public function saveStatus($jobId = null, $success = true, $message = null)
{
if ($jobId === null) {
return null;
}
if (!$message) {
$message = $success ? __('Job done.') : __('Job failed.');
}
$jobData = $this->newEntity(
[
$this->primaryKey => $jobId,
'status' => $success ? Job::STATUS_COMPLETED : Job::STATUS_FAILED,
'message' => $message,
'progress' => 100,
]
);
try {
if ($this->save($jobData)) {
return true;
}
$this->log("Could not save status for job $jobId because of validation errors: " . json_encode($this->validationErrors), LOG_NOTICE);
} catch (Exception $e) {
$this->logException("Could not save progress for job $jobId", $e, LOG_NOTICE);
}
return false;
}
}

View File

@ -2,8 +2,10 @@
namespace App\Model\Table;
use App\Lib\Tools\ProcessTool;
use App\Model\Table\AppTable;
use Cake\Core\Configure;
use Exception;
class ServersTable extends AppTable
{
@ -46,15 +48,99 @@ class ServersTable extends AppTable
$conditions = ['OR' => [
'LOWER(Servers.name)' => strtolower($id),
'LOWER(Servers.url)' => strtolower($id)
]];
]
];
}
$server = $this->find(
'all',
[
'conditions' => $conditions,
'recursive' => -1
'conditions' => $conditions,
'recursive' => -1
]
)->disableHydration()->first();
return (empty($server)) ? false : $server;
}
/**
* @param int $workerIssueCount
* @return array
* @throws ProcessException
*/
public function workerDiagnostics(&$workerIssueCount)
{
$worker_array = [
'cache' => ['ok' => false],
'default' => ['ok' => false],
'email' => ['ok' => false],
'prio' => ['ok' => false],
'update' => ['ok' => false]
];
try {
$workers = $this->getWorkers();
} catch (Exception $e) {
// TODO: [3.x-MIGRATION] check exception logging in 3.x
// $this->logException('Could not get list of workers.', $e);
return $worker_array;
}
$currentUser = ProcessTool::whoami();
$procAccessible = file_exists('/proc');
foreach ($workers as $pid => $worker) {
if (!is_numeric($pid)) {
throw new Exception('Non numeric PID found.');
}
$entry = $worker['type'] === 'regular' ? $worker['queue'] : $worker['type'];
$correctUser = ($currentUser === $worker['user']);
if ($procAccessible) {
$alive = $correctUser && file_exists("/proc/$pid");
} else {
$alive = 'N/A';
}
$ok = true;
if (!$alive || !$correctUser) {
$ok = false;
$workerIssueCount++;
}
$worker_array[$entry]['workers'][] = [
'pid' => $pid,
'user' => $worker['user'],
'alive' => $alive,
'correct_user' => $correctUser,
'ok' => $ok
];
}
foreach ($worker_array as $k => $queue) {
if (isset($queue['workers'])) {
foreach ($queue['workers'] as $worker) {
if ($worker['ok']) {
$worker_array[$k]['ok'] = true; // If at least one worker is up, the queue can be considered working
}
}
}
$worker_array[$k]['jobCount'] = $this->getBackgroundJobsTool()->getQueueSize($k);
if (!isset($queue['workers'])) {
$workerIssueCount++;
$worker_array[$k]['ok'] = false;
}
}
$worker_array['proc_accessible'] = $procAccessible;
$worker_array['controls'] = 1;
if (Configure::check('MISP.manage_workers')) {
$worker_array['controls'] = Configure::read('MISP.manage_workers');
}
if (Configure::read('BackgroundJobs.enabled')) {
try {
$worker_array['supervisord_status'] = $this->getBackgroundJobsTool()->getSupervisorStatus();
} catch (Exception $exception) {
$this->logException('Error getting supervisor status.', $exception);
$worker_array['supervisord_status'] = false;
}
}
return $worker_array;
}
}

View File

@ -0,0 +1,55 @@
<div class="confirmation">
<legend><?php echo __('Background Job Error Browser');?></legend>
<div style="padding-left:5px;padding-right:5px;padding-bottom:5px;">
<div>
<?php
if (!empty($response)):
$stackTrace = "";
if (isset($response['backtrace']) && !empty($response['backtrace'])) {
foreach ($response['backtrace'] as $line) {
$stackTrace .= h($line) . '<br>';
}
}
foreach ($fields as $name => $content):
if (isset($response[$content])):
?>
<span class="bold red"><?php echo h($name); ?></span>: <?php echo h($response[$content]); ?><br />
<?php
endif;
endforeach;
?>
<a href="#" id="show_stacktrace">(<?php echo __('Click to show stack trace');?>)</a>
<a href="#" id="hide_stacktrace" class="hidden">(<?php echo __('Click to hide stack trace');?>)</a>
<div id="stacktrace" class="hidden">
<?php echo $stackTrace; ?>
</div>
<?php
else:
?>
<p><?php echo __('No error data found. Generally job error data is purged from Redis after 24 hours, however, you can still view the errors in the log files in "/app/tmp/logs".');?></p>
<?php
endif;
?>
</div>
<span role="button" tabindex="0" aria-label="<?php echo __('Cancel');?>" title="<?php echo __('Cancel');?>" class="btn btn-inverse" id="PromptNoButton" onClick="cancelPopoverForm();"><?php echo __('Close');?></span>
</div>
</div>
<script type="text/javascript">
$("#show_stacktrace").click(function() {
$("#show_stacktrace").hide();
$("#hide_stacktrace").show();
$("#stacktrace").show();
});
$("#hide_stacktrace").click(function() {
$("#hide_stacktrace").hide();
$("#show_stacktrace").show();
$("#stacktrace").hide();
});
$(document).ready(function() {
resizePopoverBody();
});
$(window).resize(function() {
resizePopoverBody();
});
</script>

75
templates/Jobs/index.php Normal file
View File

@ -0,0 +1,75 @@
<?php
$fields = [
[
'name' => __('ID'),
'sort' => 'id',
'data_path' => 'id'
],
[
'name' => __('Date Created'),
'sort' => 'date_created',
'data_path' => 'date_created'
],
[
'name' => __('Date Modified'),
'sort' => 'date_modified',
'data_path' => 'date_modified'
],
[
'name' => __('Process ID'),
'data_path' => 'process_id'
],
[
'name' => __('Worker'),
'sort' => 'worker',
'data_path' => 'worker'
],
[
'name' => __('Job Type'),
'sort' => 'job_type',
'data_path' => 'job_type'
],
[
'name' => __('Job Input'),
'data_path' => 'job_input'
],
[
'name' => __('Message'),
'data_path' => 'message'
],
[
'name' => __('Organisation'),
'sort' => 'Organisation.name',
'element' => 'org',
'data_path' => 'Organisation',
'class' => 'short',
],
[
'name' => __('Status'),
'sort' => 'job_status',
'data_path' => 'job_status'
],
[
'name' => __('Progress'),
'data_path' => 'progress'
],
];
echo $this->element(
'genericElements/IndexTable/index_table',
[
'data' => [
'data' => $jobs,
'top_bar' => [
'children' => [
[
'type' => 'context_filters',
]
]
],
'fields' => $fields,
'title' => empty($ajax) ? __('Jobs') : false
]
]
);