diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index 51303d2de..33f3953e8 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -207,6 +207,8 @@ + + diff --git a/src/NzbDrone.Api/System/Tasks/TaskModule.cs b/src/NzbDrone.Api/System/Tasks/TaskModule.cs new file mode 100644 index 000000000..e5f454e5b --- /dev/null +++ b/src/NzbDrone.Api/System/Tasks/TaskModule.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Jobs; + +namespace NzbDrone.Api.System.Tasks +{ + public class TaskModule : NzbDroneRestModule + { + private readonly ITaskManager _taskManager; + + public TaskModule(ITaskManager taskManager) + : base("system/task") + { + _taskManager = taskManager; + GetResourceAll = GetAll; + } + + private List GetAll() + { + return _taskManager.GetAll().Select(ConvertToResource).ToList(); + } + + private static TaskResource ConvertToResource(ScheduledTask scheduledTask) + { + return new TaskResource + { + Id = scheduledTask.Id, + Name = scheduledTask.TypeName.Split('.').Last().Replace("Command", ""), + CommandName = scheduledTask.TypeName.Split('.').Last(), + Interval = scheduledTask.Interval, + LastExecution = scheduledTask.LastExecution, + NextExecution = scheduledTask.LastExecution.AddMinutes(scheduledTask.Interval) + }; + } + } +} diff --git a/src/NzbDrone.Api/System/Tasks/TaskResource.cs b/src/NzbDrone.Api/System/Tasks/TaskResource.cs new file mode 100644 index 000000000..900457ccc --- /dev/null +++ b/src/NzbDrone.Api/System/Tasks/TaskResource.cs @@ -0,0 +1,14 @@ +using System; +using NzbDrone.Api.REST; + +namespace NzbDrone.Api.System.Tasks +{ + public class TaskResource : RestResource + { + public String Name { get; set; } + public String CommandName { get; set; } + public Int32 Interval { get; set; } + public DateTime LastExecution { get; set; } + public DateTime NextExecution { get; set; } + } +} diff --git a/src/NzbDrone.Core/Jobs/TaskManager.cs b/src/NzbDrone.Core/Jobs/TaskManager.cs index 46c6f79d5..99e600fa6 100644 --- a/src/NzbDrone.Core/Jobs/TaskManager.cs +++ b/src/NzbDrone.Core/Jobs/TaskManager.cs @@ -23,6 +23,7 @@ namespace NzbDrone.Core.Jobs public interface ITaskManager { IList GetPending(); + List GetAll(); } public class TaskManager : ITaskManager, IHandle, IHandle, IHandleAsync @@ -45,6 +46,11 @@ namespace NzbDrone.Core.Jobs .ToList(); } + public List GetAll() + { + return _scheduledTaskRepository.All().ToList(); + } + public void Handle(ApplicationStartedEvent message) { var defaultTasks = new[] diff --git a/src/UI/Cells/RelativeTimeCell.js b/src/UI/Cells/RelativeTimeCell.js new file mode 100644 index 000000000..d7a6363e1 --- /dev/null +++ b/src/UI/Cells/RelativeTimeCell.js @@ -0,0 +1,38 @@ +'use strict'; +define( + [ + 'Cells/NzbDroneCell', + 'moment', + 'Shared/FormatHelpers', + 'Shared/UiSettingsModel' + ], function (NzbDroneCell, moment, FormatHelpers, UiSettings) { + return NzbDroneCell.extend({ + + className: 'relative-time-cell', + + render: function () { + + var dateStr = this.model.get(this.column.get('name')); + + if (dateStr) { + var date = moment(dateStr); + var result = '{1}'; + + if (UiSettings.get('showRelativeDates')) { + var tooltip = date.format(UiSettings.longDateTime()); + var text = date.fromNow(); + + this.$el.html(result.format(tooltip, text)); + } + + else { + this.$el.html(date.format(UiSettings.longDateTime())); + } + + + } + + return this; + } + }); + }); diff --git a/src/UI/Cells/cells.less b/src/UI/Cells/cells.less index 85f255088..44437b487 100644 --- a/src/UI/Cells/cells.less +++ b/src/UI/Cells/cells.less @@ -50,6 +50,11 @@ .text-overflow(); } +.relative-time-cell { + cursor: default; + .text-overflow(); +} + .history-event-type-cell { width : 10px; } @@ -193,3 +198,11 @@ td.delete-episode-file-cell { word-break: break-all; word-wrap: break-word; } + +.execute-task-cell { + width : 28px; + + i { + .clickable(); + } +} diff --git a/src/UI/System/SystemLayout.js b/src/UI/System/SystemLayout.js index 29e8562f7..08fe635ca 100644 --- a/src/UI/System/SystemLayout.js +++ b/src/UI/System/SystemLayout.js @@ -8,6 +8,7 @@ define( 'System/Logs/LogsLayout', 'System/Update/UpdateLayout', 'System/Backup/BackupLayout', + 'System/Task/TaskLayout', 'Shared/Messenger' ], function ($, Backbone, @@ -16,6 +17,7 @@ define( LogsLayout, UpdateLayout, BackupLayout, + TaskLayout, Messenger) { return Marionette.Layout.extend({ template: 'System/SystemLayoutTemplate', @@ -24,23 +26,26 @@ define( info : '#info', logs : '#logs', updates : '#updates', - backup : '#backup' + backup : '#backup', + tasks : '#tasks' }, ui: { infoTab : '.x-info-tab', logsTab : '.x-logs-tab', updatesTab : '.x-updates-tab', - backupTab : '.x-backup-tab' + backupTab : '.x-backup-tab', + tasksTab : '.x-tasks-tab' }, events: { - 'click .x-info-tab' : '_showInfo', - 'click .x-logs-tab' : '_showLogs', - 'click .x-updates-tab': '_showUpdates', - 'click .x-backup-tab': '_showBackup', - 'click .x-shutdown' : '_shutdown', - 'click .x-restart' : '_restart' + 'click .x-info-tab' : '_showInfo', + 'click .x-logs-tab' : '_showLogs', + 'click .x-updates-tab' : '_showUpdates', + 'click .x-backup-tab' : '_showBackup', + 'click .x-tasks-tab' : '_showTasks', + 'click .x-shutdown' : '_shutdown', + 'click .x-restart' : '_restart' }, initialize: function (options) { @@ -60,6 +65,9 @@ define( case 'backup': this._showBackup(); break; + case 'tasks': + this._showTasks(); + break; default: this._showInfo(); } @@ -109,6 +117,16 @@ define( this._navigate('system/backup'); }, + _showTasks: function (e) { + if (e) { + e.preventDefault(); + } + + this.tasks.show(new TaskLayout()); + this.ui.tasksTab.tab('show'); + this._navigate('system/tasks'); + }, + _shutdown: function () { $.ajax({ url: window.NzbDrone.ApiRoot + '/system/shutdown', diff --git a/src/UI/System/SystemLayoutTemplate.hbs b/src/UI/System/SystemLayoutTemplate.hbs index aa6996850..f0bb81574 100644 --- a/src/UI/System/SystemLayoutTemplate.hbs +++ b/src/UI/System/SystemLayoutTemplate.hbs @@ -3,6 +3,7 @@
  • Logs
  • Updates
  • Backup
  • +
  • Tasks
  • \ No newline at end of file diff --git a/src/UI/System/Task/ExecuteTaskCell.js b/src/UI/System/Task/ExecuteTaskCell.js new file mode 100644 index 000000000..2969d0bd1 --- /dev/null +++ b/src/UI/System/Task/ExecuteTaskCell.js @@ -0,0 +1,41 @@ +'use strict'; +define( + [ + 'Cells/NzbDroneCell', + 'Commands/CommandController' + ], function (NzbDroneCell, CommandController) { + return NzbDroneCell.extend({ + + className: 'execute-task-cell', + + events: { + 'click .x-execute' : '_executeTask' + }, + + render: function () { + + this.$el.empty(); + + var task = this.model.get('name'); + + this.$el.html( + ''.format(task) + ); + + CommandController.bindToCommand({ + element: this.$el.find('.x-execute'), + command: { + name : task + } + }); + + return this; + }, + + _executeTask: function () { + CommandController.Execute(this.model.get('name'), { + name : this.model.get('name') + }); + } + }); + }); diff --git a/src/UI/System/Task/TaskCollection.js b/src/UI/System/Task/TaskCollection.js new file mode 100644 index 000000000..3f8e09e70 --- /dev/null +++ b/src/UI/System/Task/TaskCollection.js @@ -0,0 +1,19 @@ +'use strict'; +define( + [ + 'backbone.pageable', + 'System/Task/TaskModel' + ], function (PageableCollection, TaskModel) { + return PageableCollection.extend({ + url : window.NzbDrone.ApiRoot + '/system/task', + model: TaskModel, + + state: { + sortKey : 'name', + order : -1, + pageSize : 100000 + }, + + mode: 'client' + }); + }); diff --git a/src/UI/System/Task/TaskIntervalCell.js b/src/UI/System/Task/TaskIntervalCell.js new file mode 100644 index 000000000..694fc042e --- /dev/null +++ b/src/UI/System/Task/TaskIntervalCell.js @@ -0,0 +1,25 @@ +'use strict'; +define( + [ + 'Cells/NzbDroneCell', + 'moment' + ], function (NzbDroneCell, moment) { + return NzbDroneCell.extend({ + + className: 'task-interval-cell', + + render: function () { + + this.$el.empty(); + + var interval = this.model.get('interval'); + var duration = moment.duration(interval, 'minutes').humanize(); + + this.$el.html( + duration.replace(/an?(?=\s)/, '1') + ); + + return this; + } + }); + }); diff --git a/src/UI/System/Task/TaskLayout.js b/src/UI/System/Task/TaskLayout.js new file mode 100644 index 000000000..c55337c92 --- /dev/null +++ b/src/UI/System/Task/TaskLayout.js @@ -0,0 +1,73 @@ +'use strict'; +define( + [ + 'marionette', + 'backgrid', + 'System/Task/TaskCollection', + 'Cells/RelativeTimeCell', + 'System/Task/TaskIntervalCell', + 'System/Task/ExecuteTaskCell', + 'Shared/LoadingView' + ], function (Marionette, Backgrid, BackupCollection, RelativeTimeCell, TaskIntervalCell, ExecuteTaskCell, LoadingView) { + return Marionette.Layout.extend({ + template: 'System/Task/TaskLayoutTemplate', + + regions: { + tasks : '#x-tasks' + }, + + columns: [ + { + name : 'name', + label : 'Name', + sortable : true, + cell : 'string' + }, + { + name : 'interval', + label : 'Interval', + sortable : true, + cell : TaskIntervalCell + }, + { + name : 'lastExecution', + label : 'Last Execution', + sortable : true, + cell : RelativeTimeCell + }, + { + name : 'nextExecution', + label : 'Next Execution', + sortable : true, + cell : RelativeTimeCell + }, + { + name : 'this', + label : '', + sortable : false, + cell : ExecuteTaskCell + } + ], + + initialize: function () { + this.taskCollection = new BackupCollection(); + + this.listenTo(this.taskCollection, 'sync', this._showTasks); + }, + + onRender: function () { + this.tasks.show(new LoadingView()); + + this.taskCollection.fetch(); + }, + + _showTasks: function () { + + this.tasks.show(new Backgrid.Grid({ + columns : this.columns, + collection: this.taskCollection, + className : 'table table-hover' + })); + } + }); + }); diff --git a/src/UI/System/Task/TaskLayoutTemplate.hbs b/src/UI/System/Task/TaskLayoutTemplate.hbs new file mode 100644 index 000000000..87b503704 --- /dev/null +++ b/src/UI/System/Task/TaskLayoutTemplate.hbs @@ -0,0 +1,5 @@ +
    +
    +
    +
    +
    diff --git a/src/UI/System/Task/TaskModel.js b/src/UI/System/Task/TaskModel.js new file mode 100644 index 000000000..530a080c6 --- /dev/null +++ b/src/UI/System/Task/TaskModel.js @@ -0,0 +1,9 @@ +'use strict'; +define( + [ + 'backbone' + ], function (Backbone) { + return Backbone.Model.extend({ + + }); + });