New: App health displayed in UI

This commit is contained in:
Mark McDowall 2014-02-25 21:40:47 -08:00
parent 90a6bcaa47
commit c8ae9f40fb
54 changed files with 873 additions and 44 deletions

View File

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.HealthCheck;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Api.Health
{
public class HealthModule : NzbDroneRestModuleWithSignalR<HealthResource, HealthCheck>,
IHandle<TriggerHealthCheckEvent>
{
private readonly IHealthCheckService _healthCheckService;
public HealthModule(ICommandExecutor commandExecutor, IHealthCheckService healthCheckService)
: base(commandExecutor)
{
_healthCheckService = healthCheckService;
GetResourceAll = GetHealth;
}
private List<HealthResource> GetHealth()
{
return ToListResource(_healthCheckService.PerformHealthCheck);
}
public void Handle(TriggerHealthCheckEvent message)
{
BroadcastResourceChange(ModelAction.Sync);
}
}
}

View File

@ -0,0 +1,13 @@
using System;
using NzbDrone.Api.REST;
using NzbDrone.Core.HealthCheck;
namespace NzbDrone.Api.Health
{
public class HealthResource : RestResource
{
public HealthCheckResultType Type { get; set; }
public String Message { get; set; }
}
}

View File

@ -132,6 +132,8 @@
<Compile Include="Frontend\Mappers\IMapHttpRequestsToDisk.cs" />
<Compile Include="Frontend\Mappers\StaticResourceMapperBase.cs" />
<Compile Include="Frontend\StaticResourceModule.cs" />
<Compile Include="Health\HistoryResource.cs" />
<Compile Include="Health\HealthModule.cs" />
<Compile Include="History\HistoryResource.cs" />
<Compile Include="History\HistoryModule.cs" />
<Compile Include="Metadata\MetadataResource.cs" />

View File

@ -92,7 +92,7 @@ namespace NzbDrone.Automation.Test.PageModel
{
get
{
return Find(By.LinkText("System"));
return Find(By.PartialLinkText("System"));
}
}

View File

@ -52,5 +52,10 @@ namespace NzbDrone.Common
{
return CollapseSpace.Replace(text, " ").Trim();
}
public static bool IsNullOrWhiteSpace(this string text)
{
return String.IsNullOrWhiteSpace(text);
}
}
}

View File

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Download;
using NzbDrone.Core.HealthCheck.Checks;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.HealthCheck.Checks
{
[TestFixture]
public class DownloadClientCheckFixture : CoreTest<DownloadClientCheck>
{
[Test]
public void should_return_warning_when_download_client_has_not_been_configured()
{
Mocker.GetMock<IProvideDownloadClient>()
.Setup(s => s.GetDownloadClient())
.Returns((IDownloadClient)null);
Subject.Check().ShouldBeWarning();
}
[Test]
public void should_return_error_when_download_client_throws()
{
var downloadClient = Mocker.GetMock<IDownloadClient>();
downloadClient.Setup(s => s.GetQueue())
.Throws<Exception>();
Mocker.GetMock<IProvideDownloadClient>()
.Setup(s => s.GetDownloadClient())
.Returns(downloadClient.Object);
Subject.Check().ShouldBeError();
}
[Test]
public void should_return_null_when_download_client_returns()
{
var downloadClient = Mocker.GetMock<IDownloadClient>();
downloadClient.Setup(s => s.GetQueue())
.Returns(new List<QueueItem>());
Mocker.GetMock<IProvideDownloadClient>()
.Setup(s => s.GetDownloadClient())
.Returns(downloadClient.Object);
Subject.Check().Should().BeNull();
}
}
}

View File

@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.HealthCheck.Checks;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.HealthCheck.Checks
{
[TestFixture]
public class DroneFactoryCheckFixture : CoreTest<DroneFactoryCheck>
{
private const string DRONE_FACTORY_FOLDER = @"C:\Test\Unsorted";
private void GivenDroneFactoryFolder(bool exists = false)
{
Mocker.GetMock<IConfigService>()
.SetupGet(s => s.DownloadedEpisodesFolder)
.Returns(DRONE_FACTORY_FOLDER);
Mocker.GetMock<IDiskProvider>()
.Setup(s => s.FolderExists(DRONE_FACTORY_FOLDER))
.Returns(exists);
}
[Test]
public void should_return_warning_when_drone_factory_folder_is_not_configured()
{
Mocker.GetMock<IConfigService>()
.SetupGet(s => s.DownloadedEpisodesFolder)
.Returns("");
Subject.Check().ShouldBeWarning();
}
[Test]
public void should_return_error_when_drone_factory_folder_does_not_exist()
{
GivenDroneFactoryFolder();
Subject.Check().ShouldBeError();
}
[Test]
public void should_return_error_when_unable_to_write_to_drone_factory_folder()
{
GivenDroneFactoryFolder(true);
Mocker.GetMock<IDiskProvider>()
.Setup(s => s.WriteAllText(It.IsAny<String>(), It.IsAny<String>()))
.Throws<Exception>();
Subject.Check().ShouldBeError();
}
[Test]
public void should_return_null_when_no_issues_found()
{
GivenDroneFactoryFolder(true);
Subject.Check().Should().BeNull();
}
}
}

View File

@ -0,0 +1,18 @@
using FluentAssertions;
using NzbDrone.Core.HealthCheck;
namespace NzbDrone.Core.Test.HealthCheck.Checks
{
public static class HealthCheckFixtureExtensions
{
public static void ShouldBeWarning(this Core.HealthCheck.HealthCheck result)
{
result.Type.Should().Be(HealthCheckResultType.Warning);
}
public static void ShouldBeError(this Core.HealthCheck.HealthCheck result)
{
result.Type.Should().Be(HealthCheckResultType.Error);
}
}
}

View File

@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Download;
using NzbDrone.Core.HealthCheck.Checks;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.HealthCheck.Checks
{
[TestFixture]
public class IndexerCheckFixture : CoreTest<IndexerCheck>
{
[Test]
public void should_return_error_when_not_indexers_are_enabled()
{
Mocker.GetMock<IIndexerFactory>()
.Setup(s => s.GetAvailableProviders())
.Returns(new List<IIndexer>());
Subject.Check().ShouldBeError();
}
[Test]
public void should_return_warning_when_only_enabled_indexer_is_wombles()
{
var indexer = Mocker.GetMock<IIndexer>();
indexer.SetupGet(s => s.SupportsSearching).Returns(false);
Mocker.GetMock<IIndexerFactory>()
.Setup(s => s.GetAvailableProviders())
.Returns(new List<IIndexer>{indexer.Object});
Subject.Check().ShouldBeWarning();
}
[Test]
public void should_return_null_when_multiple_multiple_indexers_are_enabled()
{
var indexers = new List<IIndexer>{Mocker.GetMock<IIndexer>().Object, Mocker.GetMock<IIndexer>().Object};
Mocker.GetMock<IIndexerFactory>()
.Setup(s => s.GetAvailableProviders())
.Returns(indexers);
Subject.Check().Should().BeNull();
}
}
}

View File

@ -0,0 +1,30 @@
using System;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.HealthCheck.Checks;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.HealthCheck.Checks
{
[TestFixture]
public class UpdateCheckFixture : CoreTest<UpdateCheck>
{
[Test]
public void should_return_error_when_app_folder_is_write_protected()
{
WindowsOnly();
Mocker.GetMock<IAppFolderInfo>()
.Setup(s => s.StartUpFolder)
.Returns(@"C:\NzbDrone");
Mocker.GetMock<IDiskProvider>()
.Setup(s => s.WriteAllText(It.IsAny<String>(), It.IsAny<String>()))
.Throws<Exception>();
Subject.Check().ShouldBeError();
}
}
}

View File

@ -125,6 +125,11 @@
<Compile Include="Framework\CoreTest.cs" />
<Compile Include="Framework\DbTest.cs" />
<Compile Include="Framework\NBuilderExtensions.cs" />
<Compile Include="HealthCheck\Checks\UpdateCheckFixture.cs" />
<Compile Include="HealthCheck\Checks\IndexerCheckFixture.cs" />
<Compile Include="HealthCheck\Checks\DroneFactoryCheckFixture.cs" />
<Compile Include="HealthCheck\Checks\DownloadClientCheckFixture.cs" />
<Compile Include="HealthCheck\Checks\HealthCheckFixtureExtentions.cs" />
<Compile Include="HistoryTests\HistoryServiceFixture.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedHistoryItemsFixture.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedEpisodeFilesFixture.cs" />

View File

@ -58,11 +58,21 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
{
}
public void Execute(TestBlackholeCommand message)
public override void Test()
{
var testPath = Path.Combine(message.Folder, "drone_test.txt");
PerformTest(Settings.Folder);
}
private void PerformTest(string folder)
{
var testPath = Path.Combine(folder, "drone_test.txt");
_diskProvider.WriteAllText(testPath, DateTime.Now.ToString());
_diskProvider.DeleteFile(testPath);
}
public void Execute(TestBlackholeCommand message)
{
PerformTest(message.Folder);
}
}
}

View File

@ -80,7 +80,12 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
throw new NotImplementedException();
}
public VersionResponse GetVersion(string host = null, int port = 0, string username = null, string password = null)
public override void Test()
{
_proxy.GetVersion(Settings);
}
private VersionResponse GetVersion(string host = null, int port = 0, string username = null, string password = null)
{
return _proxy.GetVersion(Settings);
}

View File

@ -80,11 +80,21 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic
{
}
public void Execute(TestPneumaticCommand message)
public override void Test()
{
var testPath = Path.Combine(message.Folder, "drone_test.txt");
PerformTest(Settings.Folder);
}
private void PerformTest(string folder)
{
var testPath = Path.Combine(folder, "drone_test.txt");
_diskProvider.WriteAllText(testPath, DateTime.Now.ToString());
_diskProvider.DeleteFile(testPath);
}
public void Execute(TestPneumaticCommand message)
{
PerformTest(message.Folder);
}
}
}

View File

@ -122,6 +122,11 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
_sabnzbdProxy.RemoveFrom("history", id, Settings);
}
public override void Test()
{
_sabnzbdProxy.GetCategories(Settings);
}
public void Execute(TestSabnzbdCommand message)
{
var settings = new SabnzbdSettings();

View File

@ -44,5 +44,6 @@ namespace NzbDrone.Core.Download
public abstract IEnumerable<HistoryItem> GetHistory(int start = 0, int limit = 10);
public abstract void RemoveFromQueue(string id);
public abstract void RemoveFromHistory(string id);
public abstract void Test();
}
}

View File

@ -2,6 +2,7 @@
using System.Linq;
using NLog;
using NzbDrone.Common.Composition;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Download
@ -15,8 +16,8 @@ namespace NzbDrone.Core.Download
{
private readonly IDownloadClientRepository _providerRepository;
public DownloadClientFactory(IDownloadClientRepository providerRepository, IEnumerable<IDownloadClient> providers, IContainer container, Logger logger)
: base(providerRepository, providers, container, logger)
public DownloadClientFactory(IDownloadClientRepository providerRepository, IEnumerable<IDownloadClient> providers, IContainer container, IEventAggregator eventAggregator, Logger logger)
: base(providerRepository, providers, container, eventAggregator, logger)
{
_providerRepository = providerRepository;
}

View File

@ -1,9 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.Clients;
using NzbDrone.Core.Download.Clients.Nzbget;
using NzbDrone.Core.Download.Clients.Sabnzbd;
using System.Linq;
namespace NzbDrone.Core.Download
{

View File

@ -11,5 +11,6 @@ namespace NzbDrone.Core.Download
IEnumerable<HistoryItem> GetHistory(int start = 0, int limit = 0);
void RemoveFromQueue(string id);
void RemoveFromHistory(string id);
void Test();
}
}

View File

@ -0,0 +1,8 @@
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.HealthCheck
{
public class CheckHealthCommand : Command
{
}
}

View File

@ -0,0 +1,36 @@
using System;
using NzbDrone.Core.Download;
namespace NzbDrone.Core.HealthCheck.Checks
{
public class DownloadClientCheck : IProvideHealthCheck
{
private readonly IProvideDownloadClient _downloadClientProvider;
public DownloadClientCheck(IProvideDownloadClient downloadClientProvider)
{
_downloadClientProvider = downloadClientProvider;
}
public HealthCheck Check()
{
var downloadClient = _downloadClientProvider.GetDownloadClient();
if (downloadClient == null)
{
return new HealthCheck(HealthCheckResultType.Warning, "No download client is available");
}
try
{
downloadClient.GetQueue();
}
catch (Exception)
{
return new HealthCheck(HealthCheckResultType.Error, "Unable to communicate with download client");
}
return null;
}
}
}

View File

@ -0,0 +1,50 @@
using System;
using System.IO;
using NzbDrone.Common;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Configuration;
namespace NzbDrone.Core.HealthCheck.Checks
{
public class DroneFactoryCheck : IProvideHealthCheck
{
private readonly IConfigService _configService;
private readonly IDiskProvider _diskProvider;
public DroneFactoryCheck(IConfigService configService, IDiskProvider diskProvider)
{
_configService = configService;
_diskProvider = diskProvider;
}
public HealthCheck Check()
{
var droneFactoryFolder = _configService.DownloadedEpisodesFolder;
if (droneFactoryFolder.IsNullOrWhiteSpace())
{
return new HealthCheck(HealthCheckResultType.Warning, "Drone factory folder is not configured");
}
if (!_diskProvider.FolderExists(droneFactoryFolder))
{
return new HealthCheck(HealthCheckResultType.Error, "Drone factory folder does not exist");
}
try
{
var testPath = Path.Combine(droneFactoryFolder, "drone_test.txt");
_diskProvider.WriteAllText(testPath, DateTime.Now.ToString());
_diskProvider.DeleteFile(testPath);
}
catch (Exception)
{
return new HealthCheck(HealthCheckResultType.Error, "Unable to write to drone factory folder");
}
//Todo: Unable to import one or more files/folders from
return null;
}
}
}

View File

@ -0,0 +1,34 @@
using System.Linq;
using NzbDrone.Core.Indexers;
namespace NzbDrone.Core.HealthCheck.Checks
{
public class IndexerCheck : IProvideHealthCheck
{
private readonly IIndexerFactory _indexerFactory;
public IndexerCheck(IIndexerFactory indexerFactory)
{
_indexerFactory = indexerFactory;
}
public HealthCheck Check()
{
var enabled = _indexerFactory.GetAvailableProviders();
if (!enabled.Any())
{
return new HealthCheck(HealthCheckResultType.Error, "No indexers are enabled");
}
if (enabled.All(i => i.SupportsSearching == false))
{
return new HealthCheck(HealthCheckResultType.Warning, "Enabled indexers do not support searching");
}
return null;
}
}
}

View File

@ -0,0 +1,51 @@
using System;
using System.IO;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Update;
namespace NzbDrone.Core.HealthCheck.Checks
{
public class UpdateCheck : IProvideHealthCheck
{
private readonly IDiskProvider _diskProvider;
private readonly IAppFolderInfo _appFolderInfo;
private readonly ICheckUpdateService _checkUpdateService;
public UpdateCheck(IDiskProvider diskProvider, IAppFolderInfo appFolderInfo, ICheckUpdateService checkUpdateService)
{
_diskProvider = diskProvider;
_appFolderInfo = appFolderInfo;
_checkUpdateService = checkUpdateService;
}
public HealthCheck Check()
{
if (OsInfo.IsWindows)
{
try
{
var testPath = Path.Combine(_appFolderInfo.StartUpFolder, "drone_test.txt");
_diskProvider.WriteAllText(testPath, DateTime.Now.ToString());
_diskProvider.DeleteFile(testPath);
}
catch (Exception)
{
return new HealthCheck(HealthCheckResultType.Error,
"Unable to update, running from write-protected folder");
}
}
if (BuildInfo.BuildDateTime < DateTime.UtcNow.AddDays(-14))
{
if (_checkUpdateService.AvailableUpdate() != null)
{
return new HealthCheck(HealthCheckResultType.Warning, "New update is available");
}
}
return null;
}
}
}

View File

@ -0,0 +1,23 @@
using System;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.HealthCheck
{
public class HealthCheck : ModelBase
{
public HealthCheckResultType Type { get; set; }
public String Message { get; set; }
public HealthCheck(HealthCheckResultType type, string message)
{
Type = type;
Message = message;
}
}
public enum HealthCheckResultType
{
Warning = 1,
Error = 2
}
}

View File

@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.Download;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ThingiProvider.Events;
namespace NzbDrone.Core.HealthCheck
{
public interface IHealthCheckService
{
List<HealthCheck> PerformHealthCheck();
}
public class HealthCheckService : IHealthCheckService,
IExecute<CheckHealthCommand>,
IHandleAsync<ConfigSavedEvent>,
IHandleAsync<ProviderUpdatedEvent<IIndexer>>,
IHandleAsync<ProviderUpdatedEvent<IDownloadClient>>
{
private readonly IEnumerable<IProvideHealthCheck> _healthChecks;
private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger;
public HealthCheckService(IEnumerable<IProvideHealthCheck> healthChecks, IEventAggregator eventAggregator, Logger logger)
{
_healthChecks = healthChecks;
_eventAggregator = eventAggregator;
_logger = logger;
}
public List<HealthCheck> PerformHealthCheck()
{
_logger.Trace("Checking health");
var result = _healthChecks.Select(c => c.Check()).Where(c => c != null).ToList();
return result;
}
public void Execute(CheckHealthCommand message)
{
//Until we have stored health checks we should just trigger the complete event
//and let the clients check in
//Multiple connected clients means we're going to compute the health check multiple times
//Multiple checks feels a bit ugly, but means the most up to date information goes to the client
_eventAggregator.PublishEvent(new TriggerHealthCheckEvent());
}
public void HandleAsync(ConfigSavedEvent message)
{
_eventAggregator.PublishEvent(new TriggerHealthCheckEvent());
}
public void HandleAsync(ProviderUpdatedEvent<IIndexer> message)
{
_eventAggregator.PublishEvent(new TriggerHealthCheckEvent());
}
public void HandleAsync(ProviderUpdatedEvent<IDownloadClient> message)
{
_eventAggregator.PublishEvent(new TriggerHealthCheckEvent());
}
}
}

View File

@ -0,0 +1,7 @@
namespace NzbDrone.Core.HealthCheck
{
public interface IProvideHealthCheck
{
HealthCheck Check();
}
}

View File

@ -0,0 +1,8 @@
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.HealthCheck
{
public class TriggerHealthCheckEvent : IEvent
{
}
}

View File

@ -9,6 +9,7 @@ namespace NzbDrone.Core.Indexers
IParseFeed Parser { get; }
DownloadProtocol Protocol { get; }
Boolean SupportsPaging { get; }
Boolean SupportsSearching { get; }
IEnumerable<string> RecentFeed { get; }
IEnumerable<string> GetEpisodeSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int episodeNumber);

View File

@ -35,6 +35,7 @@ namespace NzbDrone.Core.Indexers
public abstract DownloadProtocol Protocol { get; }
public abstract bool SupportsPaging { get; }
public virtual bool SupportsSearching { get { return true; } }
protected TSettings Settings
{

View File

@ -2,6 +2,7 @@
using System.Linq;
using NLog;
using NzbDrone.Common.Composition;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Indexers
@ -16,8 +17,8 @@ namespace NzbDrone.Core.Indexers
private readonly IIndexerRepository _providerRepository;
private readonly INewznabTestService _newznabTestService;
public IndexerFactory(IIndexerRepository providerRepository, IEnumerable<IIndexer> providers, IContainer container, INewznabTestService newznabTestService, Logger logger)
: base(providerRepository, providers, container, logger)
public IndexerFactory(IIndexerRepository providerRepository, IEnumerable<IIndexer> providers, IContainer container, IEventAggregator eventAggregator, INewznabTestService newznabTestService, Logger logger)
: base(providerRepository, providers, container, eventAggregator, logger)
{
_providerRepository = providerRepository;
_newznabTestService = newznabTestService;

View File

@ -22,6 +22,14 @@ namespace NzbDrone.Core.Indexers.Wombles
}
}
public override bool SupportsSearching
{
get
{
return false;
}
}
public override IParseFeed Parser
{
get

View File

@ -7,6 +7,7 @@ using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.DataAugmentation.Xem;
using NzbDrone.Core.Download;
using NzbDrone.Core.HealthCheck;
using NzbDrone.Core.Housekeeping;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Instrumentation.Commands;
@ -50,6 +51,7 @@ namespace NzbDrone.Core.Jobs
new ScheduledTask{ Interval = 1, TypeName = typeof(DownloadedEpisodesScanCommand).FullName},
new ScheduledTask{ Interval = 1, TypeName = typeof(TrackedCommandCleanupCommand).FullName},
new ScheduledTask{ Interval = 1, TypeName = typeof(CheckForFailedDownloadCommand).FullName},
new ScheduledTask{ Interval = 5, TypeName = typeof(CheckHealthCommand).FullName},
new ScheduledTask{ Interval = 1*60, TypeName = typeof(ApplicationUpdateCommand).FullName},
new ScheduledTask{ Interval = 1*60, TypeName = typeof(TrimLogCommand).FullName},
new ScheduledTask{ Interval = 3*60, TypeName = typeof(UpdateSceneMappingCommand).FullName},

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Common.Composition;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Metadata.Consumers.Fake;
using NzbDrone.Core.ThingiProvider;
@ -17,8 +18,8 @@ namespace NzbDrone.Core.Metadata
{
private readonly IMetadataRepository _providerRepository;
public MetadataFactory(IMetadataRepository providerRepository, IEnumerable<IMetadata> providers, IContainer container, Logger logger)
: base(providerRepository, providers, container, logger)
public MetadataFactory(IMetadataRepository providerRepository, IEnumerable<IMetadata> providers, IContainer container, IEventAggregator eventAggregator, Logger logger)
: base(providerRepository, providers, container, eventAggregator, logger)
{
_providerRepository = providerRepository;
}

View File

@ -2,6 +2,7 @@
using System.Linq;
using NLog;
using NzbDrone.Common.Composition;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Notifications
@ -15,8 +16,8 @@ namespace NzbDrone.Core.Notifications
public class NotificationFactory : ProviderFactory<INotification, NotificationDefinition>, INotificationFactory
{
public NotificationFactory(INotificationRepository providerRepository, IEnumerable<INotification> providers, IContainer container, Logger logger)
: base(providerRepository, providers, container, logger)
public NotificationFactory(INotificationRepository providerRepository, IEnumerable<INotification> providers, IContainer container, IEventAggregator eventAggregator, Logger logger)
: base(providerRepository, providers, container, eventAggregator, logger)
{
}

View File

@ -264,6 +264,13 @@
<Compile Include="Exceptions\DownstreamException.cs" />
<Compile Include="Exceptions\NzbDroneClientException.cs" />
<Compile Include="Exceptions\StatusCodeToExceptions.cs" />
<Compile Include="HealthCheck\CheckHealthCommand.cs" />
<Compile Include="HealthCheck\Checks\DownloadClientCheck.cs" />
<Compile Include="HealthCheck\Checks\DroneFactoryCheck.cs" />
<Compile Include="HealthCheck\Checks\IndexerCheck.cs" />
<Compile Include="HealthCheck\Checks\UpdateCheck.cs" />
<Compile Include="HealthCheck\HealthCheck.cs" />
<Compile Include="HealthCheck\TriggerHealthCheckEvent.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedEpisodes.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedHistoryItems.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedMetadataFiles.cs" />
@ -342,6 +349,8 @@
<Compile Include="Metadata\MetadataRepository.cs" />
<Compile Include="Metadata\MetadataService.cs" />
<Compile Include="Metadata\MetadataType.cs" />
<Compile Include="HealthCheck\HealthCheckService.cs" />
<Compile Include="HealthCheck\IProvideHealthCheck.cs" />
<Compile Include="Notifications\NotificationFactory.cs" />
<Compile Include="Notifications\NotificationService.cs" />
<Compile Include="Notifications\DownloadMessage.cs" />
@ -503,6 +512,7 @@
<Compile Include="Rest\JsonNetSerializer.cs" />
<Compile Include="RootFolders\RootFolderRepository.cs" />
<Compile Include="ThingiProvider\ConfigContractNotFoundException.cs" />
<Compile Include="ThingiProvider\Events\ProviderUpdatedEvent.cs" />
<Compile Include="ThingiProvider\IProvider.cs" />
<Compile Include="Qualities\QualityProfileInUseException.cs" />
<Compile Include="Qualities\QualityDefinitionRepository.cs" />

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.ThingiProvider.Events
{
public class ProviderUpdatedEvent<TProvider> : IEvent
{
}
}

View File

@ -5,6 +5,7 @@ using NLog;
using NzbDrone.Common.Composition;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ThingiProvider.Events;
namespace NzbDrone.Core.ThingiProvider
{
@ -14,6 +15,7 @@ namespace NzbDrone.Core.ThingiProvider
{
private readonly IProviderRepository<TProviderDefinition> _providerRepository;
private readonly IContainer _container;
private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger;
protected readonly List<TProvider> _providers;
@ -21,10 +23,12 @@ namespace NzbDrone.Core.ThingiProvider
protected ProviderFactory(IProviderRepository<TProviderDefinition> providerRepository,
IEnumerable<TProvider> providers,
IContainer container,
IEventAggregator eventAggregator,
Logger logger)
{
_providerRepository = providerRepository;
_container = container;
_eventAggregator = eventAggregator;
_providers = providers.ToList();
_logger = logger;
}
@ -62,6 +66,7 @@ namespace NzbDrone.Core.ThingiProvider
public virtual void Update(TProviderDefinition definition)
{
_providerRepository.Update(definition);
_eventAggregator.PublishEvent(new ProviderUpdatedEvent<TProvider>());
}
public void Delete(int id)

View File

@ -48,7 +48,7 @@
.icon-nd-warning:before {
.icon(@warning-sign);
color : #f89406;
color : @orange;
}
.icon-nd-edit:before {
@ -79,7 +79,7 @@
.icon-nd-form-warning:before {
.icon(@warning-sign);
color: #f89406;
color: @orange;
}
.icon-nd-form-danger:before {
@ -175,4 +175,14 @@
.icon-nd-restart:before {
.icon(@repeat);
}
.icon-nd-health-warning:before {
.icon(@exclamation-sign);
color : @orange
}
.icon-nd-health-error:before {
.icon(@exclamation-sign);
color : @errorText
}

View File

@ -22,6 +22,8 @@
li {
list-style-type : none;
display : inline-block;
position : relative;
a {
&:focus {
@ -38,21 +40,20 @@
font-weight : 100;
}
span.label.pull-right {
position : relative;
top : 24px;
right : 14px;
position : absolute;
top : 28px;
right : 18px;
}
}
}
.backdrop #nav-region {
background-color : #000000;
.opacity(0.85);
}
.backdrop {
#nav-region {
background-color : #000000;
.opacity(0.85);
}
#nav-region li a:hover, #in-sub-nav li a.active {
background-color : #555555;
text-decoration : none;
}
#nav-region {
@ -62,6 +63,19 @@
.span12 {
margin-left : 0px;
}
li {
a {
&:hover {
background-color : #555555;
text-decoration : none;
}
.label {
cursor: pointer;
}
}
}
}
.search {

View File

@ -0,0 +1,16 @@
'use strict';
define(
[
'backbone',
'Health/HealthModel',
'Mixins/backbone.signalr.mixin'
], function (Backbone, HealthModel) {
var Collection = Backbone.Collection.extend({
url : window.NzbDrone.ApiRoot + '/health',
model: HealthModel
});
var collection = new Collection().bindSignalR();
collection.fetch();
return collection;
});

View File

@ -0,0 +1,9 @@
'use strict';
define(
[
'backbone'
], function (Backbone) {
return Backbone.Model.extend({
});
});

View File

@ -0,0 +1,39 @@
'use strict';
define(
[
'underscore',
'marionette',
'Health/HealthCollection'
], function (_, Marionette, HealthCollection) {
return Marionette.ItemView.extend({
initialize: function () {
this.listenTo(HealthCollection, 'sync', this._healthSync);
HealthCollection.fetch();
},
render: function () {
this.$el.empty();
if (HealthCollection.length === 0) {
return this;
}
var count = HealthCollection.length;
var label = 'label-warning';
var errors = HealthCollection.some(function (model) {
return model.get('type') === 'error';
});
if (errors) {
label = 'label-important';
}
this.$el.html('<span class="label pull-right {0}">{1}</span>'.format(label, count));
return this;
},
_healthSync: function () {
this.render();
}
});
});

View File

@ -47,6 +47,7 @@
<i class="icon-laptop"></i>
<br>
System
<span id="x-health"></span>
</a>
</li>
<li>

View File

@ -3,10 +3,15 @@ define(
[
'marionette',
'jquery',
'Health/HealthView',
'Navbar/Search'
], function (Marionette, $) {
return Marionette.ItemView.extend({
template: 'Navbar/NavbarTemplate',
], function (Marionette, $, HealthView) {
return Marionette.Layout.extend({
template: 'Navbar/NavbarLayoutTemplate',
regions: {
health: '#x-health'
},
ui: {
search: '.x-series-search'
@ -18,6 +23,7 @@ define(
onRender: function () {
this.ui.search.bindSearch();
this.health.show(new HealthView());
},
onClick: function (event) {
@ -30,9 +36,9 @@ define(
var href = event.target.getAttribute('href');
//if couldn't find it look up'
if (!href && target.parent('a') && target.parent('a')[0]) {
if (!href && target.closest('a') && target.closest('a')[0]) {
var linkElement = target.parent('a')[0];
var linkElement = target.closest('a')[0];
href = linkElement.getAttribute('href');
this.setActive(linkElement);

View File

@ -1,7 +1,7 @@
'use strict';
define([
'vent',
'marionette',
'vent',
'marionette',
'backgrid',
'System/Info/DiskSpace/DiskSpaceCollection',
'Shared/LoadingView',
@ -14,6 +14,7 @@ define([
regions: {
grid: '#x-grid'
},
columns:
[
{
@ -37,6 +38,7 @@ define([
this.collection = new DiskSpaceCollection();
this.listenTo(this.collection, 'sync', this._showTable);
},
onRender : function() {
this.grid.show(new LoadingView());
},
@ -44,6 +46,7 @@ define([
onShow: function() {
this.collection.fetch();
},
_showTable: function() {
this.grid.show(new Backgrid.Grid({
row: Backgrid.Row,

View File

@ -0,0 +1,18 @@
'use strict';
define(
[
'Cells/NzbDroneCell'
], function (NzbDroneCell) {
return NzbDroneCell.extend({
className: 'log-level-cell',
render: function () {
var level = this._getValue();
this.$el.html('<i class="icon-nd-health-{0}" title="{1}"/>'.format(this._getValue().toLowerCase(), level));
return this;
}
});
});

View File

@ -0,0 +1,55 @@
'use strict';
define(
[
'marionette',
'backgrid',
'Health/HealthCollection',
'System/Info/Health/HealthCell',
'System/Info/Health/HealthOkView'
], function (Marionette, Backgrid, HealthCollection, HealthCell, HealthOkView) {
return Marionette.Layout.extend({
template: 'System/Info/Health/HealthLayoutTemplate',
regions: {
grid: '#x-health-grid'
},
columns:
[
{
name: 'type',
label: '',
cell: HealthCell
},
{
name: 'message',
label: 'Message',
cell: 'string'
}
],
initialize: function () {
this.listenTo(HealthCollection, 'sync', this.render);
HealthCollection.fetch();
},
onRender : function() {
if (HealthCollection.length === 0) {
this.grid.show(new HealthOkView());
}
else {
this._showTable();
}
},
_showTable: function() {
this.grid.show(new Backgrid.Grid({
row: Backgrid.Row,
columns: this.columns,
collection: HealthCollection,
className:'table table-hover'
}));
}
});
});

View File

@ -0,0 +1,6 @@
<fieldset class="x-health">
<legend>Health</legend>
<div id="x-health-grid"/>
</fieldset>

View File

@ -0,0 +1,9 @@
'use strict';
define(
[
'marionette'
], function (Marionette) {
return Marionette.ItemView.extend({
template: 'System/Info/Health/HealthOkViewTemplate'
});
});

View File

@ -0,0 +1,2 @@
No issues with your configuration

View File

@ -4,22 +4,26 @@ define(
'backbone',
'marionette',
'System/Info/About/AboutView',
'System/Info/DiskSpace/DiskSpaceLayout'
'System/Info/DiskSpace/DiskSpaceLayout',
'System/Info/Health/HealthLayout'
], function (Backbone,
Marionette,
AboutView,
DiskSpaceLayout) {
DiskSpaceLayout,
HealthLayout) {
return Marionette.Layout.extend({
template: 'System/Info/SystemInfoLayoutTemplate',
regions: {
about : '#about',
diskSpace: '#diskspace'
diskSpace: '#diskspace',
health : '#health'
},
onRender: function () {
this.about.show(new AboutView());
this.diskSpace.show(new DiskSpaceLayout());
this.health.show(new HealthLayout());
}
});
});

View File

@ -1,4 +1,8 @@
<div class="row">
<div class="span12" id="health"></div>
</div>
<div class="row">
<div class="span12" id="about"></div>
</div>

View File

@ -33,9 +33,9 @@ define(
var href = event.target.getAttribute('href');
if (!href && $target.parent('a') && $target.parent('a')[0]) {
if (!href && $target.closest('a') && $target.closest('a')[0]) {
var linkElement = $target.parent('a')[0];
var linkElement = $target.closest('a')[0];
href = linkElement.getAttribute('href');
}