diff --git a/Gruntfile.js b/Gruntfile.js index f79d06706f..ce4fa59190 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -52,7 +52,7 @@ module.exports = function(grunt) { "autobahnjs": { "expand": true, "src": [ - "<%= path.bower %>/autobahnjs/build/autobahn.min.js", + "<%= path.bower %>/autobahnjs/build/autobahn.js", "<%= path.bower %>/autobahnjs/LICENSE" ], "dest": "<%= path.asset %>/autobahnjs/", diff --git a/lib/Alchemy/Phrasea/Controller/Admin/TaskManager.php b/lib/Alchemy/Phrasea/Controller/Admin/TaskManager.php index 74fb6923aa..0de5cc7b83 100644 --- a/lib/Alchemy/Phrasea/Controller/Admin/TaskManager.php +++ b/lib/Alchemy/Phrasea/Controller/Admin/TaskManager.php @@ -14,6 +14,7 @@ namespace Alchemy\Phrasea\Controller\Admin; use Alchemy\Phrasea\Exception\InvalidArgumentException; use Alchemy\Phrasea\Form\TaskForm; use Alchemy\Phrasea\Model\Entities\Task; +use Alchemy\Phrasea\TaskManager\TaskManagerStatus; use Silex\Application; use Silex\ControllerProviderInterface; use Symfony\Component\HttpFoundation\Request; @@ -46,6 +47,10 @@ class TaskManager implements ControllerProviderInterface ->get('/scheduler', 'controller.admin.task:getScheduler') ->bind('admin_scheduler'); + $controllers + ->get('/live', 'controller.admin.task:getLiveInformation') + ->bind('admin_tasks_live_info'); + $controllers ->post('/tasks/create', 'controller.admin.task:postCreateTask') ->bind('admin_tasks_task_create'); @@ -124,22 +129,38 @@ class TaskManager implements ControllerProviderInterface return $app->redirectPath('admin_tasks_list'); } + public function getLiveInformation(Application $app, Request $request) + { + if ($request->getRequestFormat() !== "json") { + $app->abort(406, 'Only JSON format is accepted.'); + } + + foreach ($app['manipulator.task']->getRepository()->findAll() as $task) { + $tasks[$task->getId()] = $app['task-manager.live-information']->getTask($task); + } + + return $app->json([ + 'manager' => $app['task-manager.live-information']->getManager(), + 'tasks' => $tasks + ]); + } + + public function getScheduler(Application $app, Request $request) { if ($request->getRequestFormat() !== "json") { $app->abort(406, 'Only JSON format is accepted.'); } - $scheduler = array_replace($app['task-manager.live-information']->getManager(), [ + return $app->json([ 'name' => $app->trans('Task Scheduler'), + 'configuration' => $app['task-manager.status']->getStatus(), 'urls' => [ 'start' => $app->path('admin_tasks_scheduler_start'), 'stop' => $app->path('admin_tasks_scheduler_stop'), 'log' => $app->path('admin_tasks_scheduler_log'), ] ]); - - return $app->json($scheduler); } public function getTasks(Application $app, Request $request) @@ -147,11 +168,11 @@ class TaskManager implements ControllerProviderInterface $tasks = []; foreach ($app['repo.tasks']->findAll() as $task) { - $tasks[] = array_replace( - $app['task-manager.live-information']->getTask($task), [ + $tasks[] = [ 'id' => $task->getId(), - 'name' => $task->getName() - ]); + 'name' => $task->getName(), + 'configuration' => $task->getStatus() + ]; } if ($request->getRequestFormat() === "json") { @@ -165,10 +186,11 @@ class TaskManager implements ControllerProviderInterface return $app['twig']->render('admin/task-manager/index.html.twig', [ 'available_jobs' => $app['task-manager.available-jobs'], 'tasks' => $tasks, - 'scheduler' => array_replace( - $app['task-manager.live-information']->getManager(), [ - 'name' => $app->trans('Task Scheduler') - ]) + 'scheduler' => [ + 'id' => '', + 'name' => $app->trans('Task Scheduler'), + 'configuration' => $app['task-manager.status']->getStatus(), + ] ]); } diff --git a/lib/conf.d/minifyGroupsConfig.php b/lib/conf.d/minifyGroupsConfig.php index 882630cd52..a2ec8486e9 100644 --- a/lib/conf.d/minifyGroupsConfig.php +++ b/lib/conf.d/minifyGroupsConfig.php @@ -51,6 +51,7 @@ $groups = [ , '//assets/blueimp-load-image/load-image.js' , '//assets/jquery-file-upload/jquery.iframe-transport.js' , '//assets/jquery-file-upload/jquery.fileupload.js' + , '//assets/autobahnjs/autobahn.js' ], 'report' => [ '//assets/jquery.ui/i18n/jquery-ui-i18n.js' diff --git a/templates/web/admin/task-manager/index.html.twig b/templates/web/admin/task-manager/index.html.twig index a4dfcb6bd8..fb479c949c 100644 --- a/templates/web/admin/task-manager/index.html.twig +++ b/templates/web/admin/task-manager/index.html.twig @@ -1,19 +1,25 @@ - -
+ + + - + @@ -22,7 +28,7 @@ - + - - + + - + {% for task in tasks %} - - + + + - @@ -115,6 +121,8 @@ +{##} + + + diff --git a/www/scripts/apps/admin/tasks-manager/app.js b/www/scripts/apps/admin/tasks-manager/app.js index 0190c729de..dcde165f95 100644 --- a/www/scripts/apps/admin/tasks-manager/app.js +++ b/www/scripts/apps/admin/tasks-manager/app.js @@ -12,58 +12,90 @@ define([ "underscore", "backbone", "models/scheduler", + "common/websockets/connection", "apps/admin/tasks-manager/views/scheduler", "apps/admin/tasks-manager/views/tasks", "apps/admin/tasks-manager/views/ping", + "apps/admin/tasks-manager/views/refresh", "apps/admin/tasks-manager/collections/tasks" -], function ($, _, Backbone, Scheduler, SchedulerView, TasksView, PingView, TasksCollection) { +], function ($, _, Backbone, Scheduler, WSConnection, SchedulerView, TasksView, PingView, RefreshView, TasksCollection) { var create = function() { window.TaskManagerApp = { $scope: $("#task-manager-app"), - $tasksListView : $("#tasks-list-view", this.$scope), - $schedulerView : $("#scheduler-view", this.$scope), - $pingView : $("#pingTime", this.$scope) + $tasksListView : $(".tasks-list-view", this.$scope), + $schedulerView : $(".scheduler-view", this.$scope), + $pingView : $(".ping-view", this.$scope), + $refreshView : $(".refresh-view", this.$scope), + eventAggregator: _.extend({}, Backbone.Events), + wsuri: "ws://dev.phrasea.net:9090/websockets", + wstopic: "http://phraseanet.com/topics/admin/task-manager" }; TaskManagerApp.tasksCollection = new TasksCollection(); TaskManagerApp.Scheduler = new Scheduler(); - - TaskManagerApp.pingView = new PingView({ - el: TaskManagerApp.$pingView + TaskManagerApp.pingView = new PingView({el: TaskManagerApp.$pingView}); + TaskManagerApp.refreshView = new RefreshView({ + el: TaskManagerApp.$refreshView, + pingView: TaskManagerApp.pingView, + tasksCollection: TaskManagerApp.tasksCollection, + scheduler: TaskManagerApp.Scheduler }); } var load = function() { + TaskManagerApp.refreshView.refreshAction(); // fetch objects $.when.apply($, [ TaskManagerApp.tasksCollection.fetch(), TaskManagerApp.Scheduler.fetch() ]).done( function () { - TaskManagerApp.schedulerView = new SchedulerView({ - model: TaskManagerApp.Scheduler, - el: TaskManagerApp.$schedulerView - }); - TaskManagerApp.tasksView = new TasksView({ - collection: TaskManagerApp.tasksCollection, - el: TaskManagerApp.$tasksListView - }); + // Init & render views + TaskManagerApp.schedulerView = new SchedulerView({model: TaskManagerApp.Scheduler, el: TaskManagerApp.$schedulerView}); + TaskManagerApp.tasksView = new TasksView({collection: TaskManagerApp.tasksCollection, el: TaskManagerApp.$tasksListView}); - // render views TaskManagerApp.tasksView.render(); TaskManagerApp.schedulerView.render(); + + // Sets connection to the web socket + var ws = new WSConnection({url:TaskManagerApp.wsuri, topic: TaskManagerApp.wstopic, eventAggregator: TaskManagerApp.eventAggregator}); + ws.run(); + + // On ticks re-render ping view, update tasks & scheduler model + TaskManagerApp.eventAggregator.on("ws:manager-tick", function(response) { + var $this = this; + $this.pingView.render(); + $this.Scheduler.set({"actual": "started", "process-id": response.message.manager["process-id"]}); + _.each(response.message.jobs, function(data, id) { + var jobModel = $this.tasksCollection.get(id); + if ("undefined" !== jobModel) { + jobModel.set({"actual": data["status"], "process-id": data["process-id"]}); + } + }); + }); } ); }; var initialize = function () { create(); + var regexp = /task-manager/; + $(document).ajaxComplete(function(event, request, settings) { + if ("undefined" !== typeof settings && regexp.test(settings.url)) { + TaskManagerApp.refreshView.loadState(false); + } + }); + + $(document).ajaxStart(function(event, request, settings) { + if ("undefined" !== typeof settings && regexp.test(settings.url)) { + TaskManagerApp.refreshView.loadState(true); + } + }); + load(); }; return { - create: create, - load: load, initialize: initialize }; }); diff --git a/www/scripts/apps/admin/tasks-manager/views/refresh.js b/www/scripts/apps/admin/tasks-manager/views/refresh.js new file mode 100644 index 0000000000..039548706a --- /dev/null +++ b/www/scripts/apps/admin/tasks-manager/views/refresh.js @@ -0,0 +1,72 @@ +/* + * This file is part of Phraseanet + * + * (c) 2005-2014 Alchemy + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +define([ + "jquery", + "underscore", + "backbone", + "" +], function ($, _, Backbone) { + var RefreshView = Backbone.View.extend({ + initialize: function(options) { + if (!"pingView" in options) { + throw "You must set the ping view" + } + this.pingView = options.pingView; + if (!"scheduler" in options) { + throw "You must set the scheduler model" + } + this.scheduler = options.scheduler; + if (!"tasksCollection" in options) { + throw "You must set the tasks collection model" + } + this.tasksCollection = options.tasksCollection; + + this.refreshUrl = this.$el.data('refresh-url'); + }, + events: { + "click .btn-refresh": "refreshAction" + }, + refreshAction: function(event) { + var $this = this; + $.ajax({ + dataType: "json", + url: $this.refreshUrl, + data: {}, + success: function(response) { + $this.pingView.render(); + $this.scheduler.set({ + "actual": response.manager["actual"], + "process-id": response.manager["process-id"], + "configuration": response.manager["configuration"] + }); + _.each(response.tasks, function(data, id) { + var jobModel = $this.tasksCollection.get(id); + if ("undefined" !== jobModel) { + jobModel.set({ + "actual": data["actual"], + "process-id": data["process-id"], + "configuration": data["configuration"] + }); + } + }); + } + }); + }, + loadState: function(state) { + if (state) { + $("#spinner", this.$el).addClass('icon-spinner icon-spin'); + } else { + $("#spinner", this.$el).removeClass('icon-spinner icon-spin'); + } + } + }); + + return RefreshView; +}); diff --git a/www/scripts/common/websockets/connection.js b/www/scripts/common/websockets/connection.js new file mode 100644 index 0000000000..4a6364f3af --- /dev/null +++ b/www/scripts/common/websockets/connection.js @@ -0,0 +1,36 @@ +define([ + "underscore" +], function (_) { + return function (options) { + if (!"url" in options) { + throw "You must set the websocket 'url'" + } + if (!"topic" in options) { + throw "You must set the websocket 'topic'" + } + if (!"eventAggregator" in options) { + throw "You must set an event aggregator" + } + + var eventAggregator = options.eventAggregator; + + return { + run: function() { + // autobahn js is defined as a global object there is no way to load + // it as a UMD module + ab.connect(options.url, function (session) { + eventAggregator.trigger("ws:connect", session); + session.subscribe(options.topic, function (topic, msg) { + // double encoded string + var msg = JSON.parse(JSON.parse(msg)); + eventAggregator.trigger("ws:"+msg.event, msg, session); + } + ); + }, + function (code, reason) { + eventAggregator.trigger("ws:session-gone", code,reason); + }); + } + } + } +});
+ + + ID PID !{{ "name" | trans | upper }}
{{ scheduler["process-id"] }} {{ scheduler["actual"] }} {{ scheduler["configuration"] }} {{ scheduler["name"] }}
{% if task["id"] != "taskmanager" %}{{ task["id"] }}{% endif %}{{ task["process-id"] }}{{ task["id"] }} {{ task["actual"] }} {{ task["configuration"] }} {{ task["name"] }}