From 4c536a077f1d26f91121c09361b9447974bdfc3b Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 28 Jul 2013 16:13:14 -0700 Subject: [PATCH] Log files are viewable in the UI --- NzbDrone.Api/Frontend/LogFileMapper.cs | 29 +++++++++ NzbDrone.Api/Logs/LogFileModule.cs | 45 ++++++++++++++ NzbDrone.Api/Logs/LogFileResource.cs | 11 ++++ NzbDrone.Api/Logs/LogModule.cs | 2 - NzbDrone.Api/NzbDrone.Api.csproj | 3 + UI/Cells/EpisodeTitleCell.js | 10 ++-- UI/Controller.js | 16 +++-- UI/Logs/Files/Collection.js | 13 ++++ UI/Logs/Files/ContentsModel.js | 8 +++ UI/Logs/Files/ContentsView.js | 11 ++++ UI/Logs/Files/ContentsViewTemplate.html | 11 ++++ UI/Logs/Files/FilenameCell.js | 18 ++++++ UI/Logs/Files/Layout.js | 79 +++++++++++++++++++++++++ UI/Logs/Files/LayoutTemplate.html | 11 ++++ UI/Logs/Files/Model.js | 8 +++ UI/Logs/Files/Row.js | 19 ++++++ UI/Logs/Layout.js | 46 ++++++++++---- UI/Logs/Logs.less | 5 +- UI/Missing/Row.js | 16 ----- UI/Router.js | 1 + UI/app.js | 3 +- 21 files changed, 325 insertions(+), 40 deletions(-) create mode 100644 NzbDrone.Api/Frontend/LogFileMapper.cs create mode 100644 NzbDrone.Api/Logs/LogFileModule.cs create mode 100644 NzbDrone.Api/Logs/LogFileResource.cs create mode 100644 UI/Logs/Files/Collection.js create mode 100644 UI/Logs/Files/ContentsModel.js create mode 100644 UI/Logs/Files/ContentsView.js create mode 100644 UI/Logs/Files/ContentsViewTemplate.html create mode 100644 UI/Logs/Files/FilenameCell.js create mode 100644 UI/Logs/Files/Layout.js create mode 100644 UI/Logs/Files/LayoutTemplate.html create mode 100644 UI/Logs/Files/Model.js create mode 100644 UI/Logs/Files/Row.js delete mode 100644 UI/Missing/Row.js diff --git a/NzbDrone.Api/Frontend/LogFileMapper.cs b/NzbDrone.Api/Frontend/LogFileMapper.cs new file mode 100644 index 000000000..9446bc1a2 --- /dev/null +++ b/NzbDrone.Api/Frontend/LogFileMapper.cs @@ -0,0 +1,29 @@ +using System.IO; +using NzbDrone.Common; +using NzbDrone.Common.EnvironmentInfo; + +namespace NzbDrone.Api.Frontend +{ + public class LogFileMapper : IMapHttpRequestsToDisk + { + private readonly IAppFolderInfo _appFolderInfo; + + public LogFileMapper(IAppFolderInfo appFolderInfo) + { + _appFolderInfo = appFolderInfo; + } + + public string Map(string resourceUrl) + { + var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar); + path = Path.GetFileName(path); + + return Path.Combine(_appFolderInfo.GetLogFolder(), path); + } + + public bool CanHandle(string resourceUrl) + { + return resourceUrl.StartsWith("/log"); + } + } +} \ No newline at end of file diff --git a/NzbDrone.Api/Logs/LogFileModule.cs b/NzbDrone.Api/Logs/LogFileModule.cs new file mode 100644 index 000000000..02d260485 --- /dev/null +++ b/NzbDrone.Api/Logs/LogFileModule.cs @@ -0,0 +1,45 @@ +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NzbDrone.Common; +using NzbDrone.Common.EnvironmentInfo; + +namespace NzbDrone.Api.Logs +{ + public class LogFileModule : NzbDroneRestModule + { + private readonly IAppFolderInfo _appFolderInfo; + private readonly IDiskProvider _diskProvider; + + public LogFileModule(IAppFolderInfo appFolderInfo, + IDiskProvider diskProvider) + : base("log/files") + { + _appFolderInfo = appFolderInfo; + _diskProvider = diskProvider; + GetResourceAll = GetLogFiles; + } + + private List GetLogFiles() + { + var result = new List(); + + var files = _diskProvider.GetFiles(_appFolderInfo.GetLogFolder(), SearchOption.TopDirectoryOnly); + + for (int i = 0; i < files.Length; i++) + { + var file = files[i]; + + result.Add(new LogFileResource + { + Id = i + 1, + Filename = Path.GetFileName(file), + LastWriteTime = _diskProvider.GetLastFileWrite(file) + }); + } + + return result.OrderByDescending(l => l.LastWriteTime).ToList(); + } + } +} \ No newline at end of file diff --git a/NzbDrone.Api/Logs/LogFileResource.cs b/NzbDrone.Api/Logs/LogFileResource.cs new file mode 100644 index 000000000..9d2847e89 --- /dev/null +++ b/NzbDrone.Api/Logs/LogFileResource.cs @@ -0,0 +1,11 @@ +using System; +using NzbDrone.Api.REST; + +namespace NzbDrone.Api.Logs +{ + public class LogFileResource : RestResource + { + public String Filename { get; set; } + public DateTime LastWriteTime { get; set; } + } +} diff --git a/NzbDrone.Api/Logs/LogModule.cs b/NzbDrone.Api/Logs/LogModule.cs index f65e99602..e1c39d92b 100644 --- a/NzbDrone.Api/Logs/LogModule.cs +++ b/NzbDrone.Api/Logs/LogModule.cs @@ -1,5 +1,4 @@ using NzbDrone.Core.Datastore; -using NzbDrone.Core.History; using NzbDrone.Core.Instrumentation; using NzbDrone.Api.Mapping; @@ -8,7 +7,6 @@ namespace NzbDrone.Api.Logs public class LogModule : NzbDroneRestModule { private readonly ILogService _logService; - private readonly IHistoryService _historyService; public LogModule(ILogService logService) { diff --git a/NzbDrone.Api/NzbDrone.Api.csproj b/NzbDrone.Api/NzbDrone.Api.csproj index 8d82ee148..abb6b8e09 100644 --- a/NzbDrone.Api/NzbDrone.Api.csproj +++ b/NzbDrone.Api/NzbDrone.Api.csproj @@ -92,6 +92,7 @@ + @@ -106,6 +107,8 @@ + + diff --git a/UI/Cells/EpisodeTitleCell.js b/UI/Cells/EpisodeTitleCell.js index c83447365..578952623 100644 --- a/UI/Cells/EpisodeTitleCell.js +++ b/UI/Cells/EpisodeTitleCell.js @@ -10,11 +10,7 @@ define( className: 'episode-title-cell', events: { - 'click': 'showDetails' - }, - - showDetails: function () { - App.vent.trigger(App.Commands.ShowEpisodeDetails, {episode: this.cellValue}); + 'click': '_showDetails' }, render: function () { @@ -26,6 +22,10 @@ define( this.$el.html(title); return this; + }, + + _showDetails: function () { + App.vent.trigger(App.Commands.ShowEpisodeDetails, {episode: this.cellValue}); } }); }); diff --git a/UI/Controller.js b/UI/Controller.js index a457e420a..68ae91770 100644 --- a/UI/Controller.js +++ b/UI/Controller.js @@ -12,12 +12,13 @@ define( 'Series/SeriesModel', 'Calendar/CalendarLayout', 'Logs/Layout', + 'Logs/Files/Layout', 'Release/Layout', 'System/Layout', 'Shared/NotFoundView', 'Shared/Modal/Region' ], function (App, Marionette, HistoryLayout, SettingsLayout, AddSeriesLayout, SeriesIndexLayout, SeriesDetailsLayout, MissingLayout, SeriesModel, CalendarLayout, - LogsLayout, ReleaseLayout, SystemLayout, NotFoundView) { + LogsLayout, LogFileLayout, ReleaseLayout, SystemLayout, NotFoundView) { return Marionette.Controller.extend({ series : function () { @@ -71,9 +72,16 @@ define( App.mainRegion.show(new ReleaseLayout()); }, - logs: function () { - this._setTitle('logs'); - App.mainRegion.show(new LogsLayout()); + logs: function (action) { + if (action) { + this._setTitle('log files'); + App.mainRegion.show(new LogFileLayout()); + } + + else { + this._setTitle('logs'); + App.mainRegion.show(new LogsLayout()); + } }, system: function () { diff --git a/UI/Logs/Files/Collection.js b/UI/Logs/Files/Collection.js new file mode 100644 index 000000000..969070232 --- /dev/null +++ b/UI/Logs/Files/Collection.js @@ -0,0 +1,13 @@ +'use strict'; +define(['Logs/Files/Model' ], +function (LogFileModel) { + return Backbone.Collection.extend({ + url : window.ApiRoot + '/log/files', + model: LogFileModel, + + state: { + sortKey : 'lastWriteTime', + order : 1 + } + }); +}); diff --git a/UI/Logs/Files/ContentsModel.js b/UI/Logs/Files/ContentsModel.js new file mode 100644 index 000000000..17c0fd6da --- /dev/null +++ b/UI/Logs/Files/ContentsModel.js @@ -0,0 +1,8 @@ +'use strict'; +define( + [ + 'backbone' + ], function (Backbone) { + return Backbone.Model.extend({ + }); + }); diff --git a/UI/Logs/Files/ContentsView.js b/UI/Logs/Files/ContentsView.js new file mode 100644 index 000000000..9105467bc --- /dev/null +++ b/UI/Logs/Files/ContentsView.js @@ -0,0 +1,11 @@ +'use strict'; + +define( + [ + 'app', + 'marionette' + ], function (App, Marionette) { + return Marionette.ItemView.extend({ + template: 'Logs/Files/ContentsViewTemplate' + }); + }); diff --git a/UI/Logs/Files/ContentsViewTemplate.html b/UI/Logs/Files/ContentsViewTemplate.html new file mode 100644 index 000000000..2f779020e --- /dev/null +++ b/UI/Logs/Files/ContentsViewTemplate.html @@ -0,0 +1,11 @@ +
+
+

{{filename}}

+
+
+ +
+
+
{{contents}}
+
+
\ No newline at end of file diff --git a/UI/Logs/Files/FilenameCell.js b/UI/Logs/Files/FilenameCell.js new file mode 100644 index 000000000..3fefb67a5 --- /dev/null +++ b/UI/Logs/Files/FilenameCell.js @@ -0,0 +1,18 @@ +'use strict'; +define( + [ + 'Cells/NzbDroneCell' + ], function (NzbDroneCell) { + return NzbDroneCell.extend({ + + className: 'log-filename-cell', + + render: function () { + + var filename = this._getValue(); + this.$el.html(filename); + + return this; + } + }); + }); diff --git a/UI/Logs/Files/Layout.js b/UI/Logs/Files/Layout.js new file mode 100644 index 000000000..dafb51a6c --- /dev/null +++ b/UI/Logs/Files/Layout.js @@ -0,0 +1,79 @@ +'use strict'; +define( + [ + 'app', + 'marionette', + 'backgrid', + 'Logs/Files/FilenameCell', + 'Cells/RelativeDateCell', + 'Logs/Files/Collection', + 'Logs/Files/Row', + 'Logs/Files/ContentsView', + 'Logs/Files/ContentsModel' + ], function (App, Marionette, Backgrid, FilenameCell, RelativeDateCell, LogFileCollection, LogFileRow, ContentsView, ContentsModel) { + return Marionette.Layout.extend({ + template: 'Logs/Files/LayoutTemplate', + + regions: { + grid : '#x-grid', + contents : '#x-contents' + }, + + columns: + [ + { + name : 'filename', + label: 'Filename', + cell : FilenameCell + }, + { + name : 'lastWriteTime', + label: 'Last Write Time', + cell : RelativeDateCell + } + ], + + initialize: function () { + this.collection = new LogFileCollection(); + this.collectionPromise = this.collection.fetch(); + + App.vent.on(App.Commands.ShowLogFile, this._showLogFile, this); + }, + + onShow: function () { + var self = this; + this._showTable(); + + this.collectionPromise.done(function (){ + self._showLogFile({ model: _.first(self.collection.models) }); + }); + }, + + _showTable: function () { + + this.grid.show(new Backgrid.Grid({ + row : LogFileRow, + columns : this.columns, + collection: this.collection, + className : 'table table-hover' + })); + }, + + _showLogFile: function (options) { + var self = this; + var filename = options.model.get('filename'); + + $.ajax({ + url: '/log/' + filename, + success: function (data) { + var model = new ContentsModel({ + filename: filename, + contents: data + }); + + self.contents.show(new ContentsView({ model: model })); + } + }); + } + }); + }); diff --git a/UI/Logs/Files/LayoutTemplate.html b/UI/Logs/Files/LayoutTemplate.html new file mode 100644 index 000000000..5852266e8 --- /dev/null +++ b/UI/Logs/Files/LayoutTemplate.html @@ -0,0 +1,11 @@ +
+
+
+
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/UI/Logs/Files/Model.js b/UI/Logs/Files/Model.js new file mode 100644 index 000000000..17c0fd6da --- /dev/null +++ b/UI/Logs/Files/Model.js @@ -0,0 +1,8 @@ +'use strict'; +define( + [ + 'backbone' + ], function (Backbone) { + return Backbone.Model.extend({ + }); + }); diff --git a/UI/Logs/Files/Row.js b/UI/Logs/Files/Row.js new file mode 100644 index 000000000..67f207667 --- /dev/null +++ b/UI/Logs/Files/Row.js @@ -0,0 +1,19 @@ +'use strict'; +define( + [ + 'app', + 'backgrid' + ], function (App, Backgrid) { + + return Backgrid.Row.extend({ + className: 'log-file-row', + + events: { + 'click': '_showContents' + }, + + _showContents: function () { + App.vent.trigger(App.Commands.ShowLogFile, { model: this.model }); + } + }); + }); diff --git a/UI/Logs/Layout.js b/UI/Logs/Layout.js index 54176e3b4..f6965761d 100644 --- a/UI/Logs/Layout.js +++ b/UI/Logs/Layout.js @@ -6,8 +6,9 @@ define( 'Logs/LogTimeCell', 'Logs/LogLevelCell', 'Shared/Grid/Pager', - 'Logs/Collection' - ], function (Marionette, Backgrid, LogTimeCell, LogLevelCell, GridPager, LogCollection) { + 'Logs/Collection', + 'Shared/Toolbar/ToolbarLayout' + ], function (Marionette, Backgrid, LogTimeCell, LogLevelCell, GridPager, LogCollection, ToolbarLayout) { return Marionette.Layout.extend({ template: 'Logs/LayoutTemplate', @@ -52,7 +53,30 @@ define( } ], - showTable: function () { + leftSideButtons: { + type : 'default', + storeState: false, + items : + [ + { + title: 'Files', + icon : 'icon-file', + route: 'logs/files' + } + ] + }, + + initialize: function () { + this.collection = new LogCollection(); + this.collection.fetch(); + }, + + onShow: function () { + this._showToolbar(); + this._showTable(); + }, + + _showTable: function () { this.grid.show(new Backgrid.Grid({ row : Backgrid.Row, @@ -67,14 +91,14 @@ define( })); }, - initialize: function () { - this.collection = new LogCollection(); - this.collection.fetch(); - }, - - onShow: function () { - this.showTable(); + _showToolbar: function () { + this.toolbar.show(new ToolbarLayout({ + left : + [ + this.leftSideButtons + ], + context: this + })); } - }); }); diff --git a/UI/Logs/Logs.less b/UI/Logs/Logs.less index 6df59bf63..3c3a2a5a0 100644 --- a/UI/Logs/Logs.less +++ b/UI/Logs/Logs.less @@ -1,4 +1,5 @@ @import "../Content/FontAwesome/font-awesome"; +@import "../Shared/Styles/clickable"; #logs-screen { @@ -45,6 +46,8 @@ .icon(@remove-sign); color : purple; } - } +.log-file-row { + .clickable; +} \ No newline at end of file diff --git a/UI/Missing/Row.js b/UI/Missing/Row.js deleted file mode 100644 index 86c880fb6..000000000 --- a/UI/Missing/Row.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; -define( - [ - 'backgrid' - ], function (Backgrid) { - - return Backgrid.Row.extend({ - events: { - 'click .x-search': 'search' - }, - search: function () { - window.alert('Episode Search'); - } - }); - }); - diff --git a/UI/Router.js b/UI/Router.js index 87e51b16d..d82cfb07b 100644 --- a/UI/Router.js +++ b/UI/Router.js @@ -22,6 +22,7 @@ require( 'missing' : 'missing', 'history' : 'history', 'logs' : 'logs', + 'logs/:action' : 'logs', 'rss' : 'rss', 'system' : 'system', ':whatever' : 'notFound' diff --git a/UI/app.js b/UI/app.js index 26f7a129b..3b616641a 100644 --- a/UI/app.js +++ b/UI/app.js @@ -179,7 +179,8 @@ define( DeleteSeriesCommand: 'DeleteSeriesCommand', CloseModalCommand : 'CloseModalCommand', ShowEpisodeDetails : 'ShowEpisodeDetails', - SaveSettings : 'saveSettings' + SaveSettings : 'saveSettings', + ShowLogFile : 'showLogFile' }; app.addInitializer(function () {