From 553a8f2a0a3ee58a6b6d2ca660d501f4366d72ef Mon Sep 17 00:00:00 2001 From: Qstick Date: Sun, 24 Jan 2021 23:33:54 -0500 Subject: [PATCH] New: Show previously installed version in Updates UI Co-Authored-By: Taloth --- frontend/src/App/AppUpdatedModalContent.js | 37 +++++++++- .../App/AppUpdatedModalContentConnector.js | 4 +- frontend/src/Store/Actions/appActions.js | 3 + frontend/src/System/Updates/Updates.js | 25 ++++++- .../src/System/Updates/UpdatesConnector.js | 4 +- .../UpdatePackageProviderFixture.cs | 2 +- .../Converters/SystemVersionConverter.cs | 24 +++++++ .../Migration/189_add_update_history.cs | 20 ++++++ src/NzbDrone.Core/Datastore/TableMapping.cs | 4 ++ .../Update/Events/UpdateInstalledEvent.cs | 17 +++++ .../Update/History/UpdateHistory.cs | 12 ++++ .../Update/History/UpdateHistoryEventType.cs | 9 +++ .../Update/History/UpdateHistoryRepository.cs | 55 ++++++++++++++ .../Update/History/UpdateHistoryService.cs | 71 +++++++++++++++++++ .../Update/RecentUpdateProvider.cs | 13 +++- .../Update/UpdatePackageProvider.cs | 9 ++- src/Radarr.Api.V3/Update/UpdateModule.cs | 18 ++++- src/Radarr.Api.V3/Update/UpdateResource.cs | 1 + 18 files changed, 317 insertions(+), 11 deletions(-) create mode 100644 src/NzbDrone.Core/Datastore/Converters/SystemVersionConverter.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/189_add_update_history.cs create mode 100644 src/NzbDrone.Core/Update/Events/UpdateInstalledEvent.cs create mode 100644 src/NzbDrone.Core/Update/History/UpdateHistory.cs create mode 100644 src/NzbDrone.Core/Update/History/UpdateHistoryEventType.cs create mode 100644 src/NzbDrone.Core/Update/History/UpdateHistoryRepository.cs create mode 100644 src/NzbDrone.Core/Update/History/UpdateHistoryService.cs diff --git a/frontend/src/App/AppUpdatedModalContent.js b/frontend/src/App/AppUpdatedModalContent.js index 72bf59ec6..c7f32d9df 100644 --- a/frontend/src/App/AppUpdatedModalContent.js +++ b/frontend/src/App/AppUpdatedModalContent.js @@ -11,9 +11,43 @@ import UpdateChanges from 'System/Updates/UpdateChanges'; import translate from 'Utilities/String/translate'; import styles from './AppUpdatedModalContent.css'; +function mergeUpdates(items, version, prevVersion) { + let installedIndex = items.findIndex((u) => u.version === version); + let installedPreviouslyIndex = items.findIndex((u) => u.version === prevVersion); + + if (installedIndex === -1) { + installedIndex = 0; + } + + if (installedPreviouslyIndex === -1) { + installedPreviouslyIndex = items.length; + } else if (installedPreviouslyIndex === installedIndex && items.size()) { + installedPreviouslyIndex += 1; + } + + const appliedUpdates = items.slice(installedIndex, installedPreviouslyIndex); + + if (!appliedUpdates.length) { + return null; + } + + const appliedChanges = { new: [], fixed: [] }; + appliedUpdates.forEach((u) => { + if (u.changes) { + appliedChanges.new.push(... u.changes.new); + appliedChanges.fixed.push(... u.changes.fixed); + } + }); + + const mergedUpdate = Object.assign({}, appliedUpdates[0], { changes: appliedChanges }); + + return mergedUpdate; +} + function AppUpdatedModalContent(props) { const { version, + prevVersion, isPopulated, error, items, @@ -21,7 +55,7 @@ function AppUpdatedModalContent(props) { onModalClose } = props; - const update = items[0]; + const update = mergeUpdates(items, version, prevVersion); return ( @@ -89,6 +123,7 @@ function AppUpdatedModalContent(props) { AppUpdatedModalContent.propTypes = { version: PropTypes.string.isRequired, + prevVersion: PropTypes.string, isPopulated: PropTypes.bool.isRequired, error: PropTypes.object, items: PropTypes.arrayOf(PropTypes.object).isRequired, diff --git a/frontend/src/App/AppUpdatedModalContentConnector.js b/frontend/src/App/AppUpdatedModalContentConnector.js index 325a1aa95..6f81170b2 100644 --- a/frontend/src/App/AppUpdatedModalContentConnector.js +++ b/frontend/src/App/AppUpdatedModalContentConnector.js @@ -8,8 +8,9 @@ import AppUpdatedModalContent from './AppUpdatedModalContent'; function createMapStateToProps() { return createSelector( (state) => state.app.version, + (state) => state.app.prevVersion, (state) => state.system.updates, - (version, updates) => { + (version, prevVersion, updates) => { const { isPopulated, error, @@ -18,6 +19,7 @@ function createMapStateToProps() { return { version, + prevVersion, isPopulated, error, items diff --git a/frontend/src/Store/Actions/appActions.js b/frontend/src/Store/Actions/appActions.js index 66387468c..0fd5283e9 100644 --- a/frontend/src/Store/Actions/appActions.js +++ b/frontend/src/Store/Actions/appActions.js @@ -117,6 +117,9 @@ export const reducers = createHandleActions({ }; if (state.version !== version) { + if (!state.prevVersion) { + newState.prevVersion = state.version; + } newState.isUpdated = true; } diff --git a/frontend/src/System/Updates/Updates.js b/frontend/src/System/Updates/Updates.js index 9a018296c..429e74e04 100644 --- a/frontend/src/System/Updates/Updates.js +++ b/frontend/src/System/Updates/Updates.js @@ -10,6 +10,7 @@ import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; import { icons, kinds } from 'Helpers/Props'; import formatDate from 'Utilities/Date/formatDate'; +import formatDateTime from 'Utilities/Date/formatDateTime'; import translate from 'Utilities/String/translate'; import UpdateChanges from './UpdateChanges'; import styles from './Updates.css'; @@ -32,6 +33,8 @@ class Updates extends Component { isDocker, updateMechanismMessage, shortDateFormat, + longDateFormat, + timeFormat, onInstallLatestPress } = this.props; @@ -138,7 +141,12 @@ class Updates extends Component {
{update.version}
-
{formatDate(update.releaseDate, shortDateFormat)}
+
+ {formatDate(update.releaseDate, shortDateFormat)} +
{ update.branch === 'master' ? @@ -155,11 +163,24 @@ class Updates extends Component { : null } + + { + update.version !== currentVersion && update.installedOn ? + : + null + }
{ @@ -222,6 +243,8 @@ Updates.propTypes = { updateMechanism: PropTypes.string, updateMechanismMessage: PropTypes.string, shortDateFormat: PropTypes.string.isRequired, + longDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, onInstallLatestPress: PropTypes.func.isRequired }; diff --git a/frontend/src/System/Updates/UpdatesConnector.js b/frontend/src/System/Updates/UpdatesConnector.js index 7b5318991..343106d24 100644 --- a/frontend/src/System/Updates/UpdatesConnector.js +++ b/frontend/src/System/Updates/UpdatesConnector.js @@ -48,7 +48,9 @@ function createMapStateToProps() { isDocker: systemStatus.isDocker, updateMechanism: generalSettings.item.updateMechanism, updateMechanismMessage: status.packageUpdateMechanismMessage, - shortDateFormat: uiSettings.shortDateFormat + shortDateFormat: uiSettings.shortDateFormat, + longDateFormat: uiSettings.longDateFormat, + timeFormat: uiSettings.timeFormat }; } ); diff --git a/src/NzbDrone.Core.Test/UpdateTests/UpdatePackageProviderFixture.cs b/src/NzbDrone.Core.Test/UpdateTests/UpdatePackageProviderFixture.cs index da1a775d5..7d7d13646 100644 --- a/src/NzbDrone.Core.Test/UpdateTests/UpdatePackageProviderFixture.cs +++ b/src/NzbDrone.Core.Test/UpdateTests/UpdatePackageProviderFixture.cs @@ -45,7 +45,7 @@ namespace NzbDrone.Core.Test.UpdateTests { const string branch = "nightly"; UseRealHttp(); - var recent = Subject.GetRecentUpdates(branch, new Version(2, 0)); + var recent = Subject.GetRecentUpdates(branch, new Version(2, 0), null); var recentWithChanges = recent.Where(c => c.Changes != null); recent.Should().NotBeEmpty(); diff --git a/src/NzbDrone.Core/Datastore/Converters/SystemVersionConverter.cs b/src/NzbDrone.Core/Datastore/Converters/SystemVersionConverter.cs new file mode 100644 index 000000000..d532df430 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Converters/SystemVersionConverter.cs @@ -0,0 +1,24 @@ +using System; +using System.Data; +using Dapper; + +namespace NzbDrone.Core.Datastore.Converters +{ + public class SystemVersionConverter : SqlMapper.TypeHandler + { + public override Version Parse(object value) + { + if (value is string version) + { + return Version.Parse((string)value); + } + + return null; + } + + public override void SetValue(IDbDataParameter parameter, Version value) + { + parameter.Value = value.ToString(); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/189_add_update_history.cs b/src/NzbDrone.Core/Datastore/Migration/189_add_update_history.cs new file mode 100644 index 000000000..111a7e1c9 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/189_add_update_history.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Data; +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(189)] + public class add_update_history : NzbDroneMigrationBase + { + protected override void LogDbUpgrade() + { + Create.TableForModel("UpdateHistory") + .WithColumn("Date").AsDateTime().NotNullable().Indexed() + .WithColumn("Version").AsString().NotNullable() + .WithColumn("EventType").AsInt32().NotNullable(); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 6f6b44b75..1014d4bae 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -41,6 +41,7 @@ using NzbDrone.Core.Restrictions; using NzbDrone.Core.RootFolders; using NzbDrone.Core.Tags; using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Update.History; using static Dapper.SqlMapper; namespace NzbDrone.Core.Datastore @@ -150,6 +151,8 @@ namespace NzbDrone.Core.Datastore Mapper.Entity("CustomFilters").RegisterModel(); Mapper.Entity("DownloadHistory").RegisterModel(); + + Mapper.Entity("UpdateHistory").RegisterModel(); } private static void RegisterMappers() @@ -181,6 +184,7 @@ namespace NzbDrone.Core.Datastore SqlMapper.RemoveTypeMap(typeof(Guid?)); SqlMapper.AddTypeHandler(new GuidConverter()); SqlMapper.AddTypeHandler(new CommandConverter()); + SqlMapper.AddTypeHandler(new SystemVersionConverter()); } private static void RegisterProviderSettingConverter() diff --git a/src/NzbDrone.Core/Update/Events/UpdateInstalledEvent.cs b/src/NzbDrone.Core/Update/Events/UpdateInstalledEvent.cs new file mode 100644 index 000000000..29290b17f --- /dev/null +++ b/src/NzbDrone.Core/Update/Events/UpdateInstalledEvent.cs @@ -0,0 +1,17 @@ +using System; +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Update.History.Events +{ + public class UpdateInstalledEvent : IEvent + { + public Version PreviousVerison { get; set; } + public Version NewVersion { get; set; } + + public UpdateInstalledEvent(Version previousVersion, Version newVersion) + { + PreviousVerison = previousVersion; + NewVersion = newVersion; + } + } +} diff --git a/src/NzbDrone.Core/Update/History/UpdateHistory.cs b/src/NzbDrone.Core/Update/History/UpdateHistory.cs new file mode 100644 index 000000000..62a3a6fc0 --- /dev/null +++ b/src/NzbDrone.Core/Update/History/UpdateHistory.cs @@ -0,0 +1,12 @@ +using System; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.Update.History +{ + public class UpdateHistory : ModelBase + { + public DateTime Date { get; set; } + public Version Version { get; set; } + public UpdateHistoryEventType EventType { get; set; } + } +} diff --git a/src/NzbDrone.Core/Update/History/UpdateHistoryEventType.cs b/src/NzbDrone.Core/Update/History/UpdateHistoryEventType.cs new file mode 100644 index 000000000..2ad99d624 --- /dev/null +++ b/src/NzbDrone.Core/Update/History/UpdateHistoryEventType.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.Update.History +{ + public enum UpdateHistoryEventType + { + Unknown = 0, + Initiated = 1, + Installed = 2 + } +} diff --git a/src/NzbDrone.Core/Update/History/UpdateHistoryRepository.cs b/src/NzbDrone.Core/Update/History/UpdateHistoryRepository.cs new file mode 100644 index 000000000..527f6d655 --- /dev/null +++ b/src/NzbDrone.Core/Update/History/UpdateHistoryRepository.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Update.History +{ + public interface IUpdateHistoryRepository : IBasicRepository + { + UpdateHistory LastInstalled(); + UpdateHistory PreviouslyInstalled(); + List InstalledSince(DateTime dateTime); + } + + public class UpdateHistoryRepository : BasicRepository, IUpdateHistoryRepository + { + public UpdateHistoryRepository(ILogDatabase logDatabase, IEventAggregator eventAggregator) + : base(logDatabase, eventAggregator) + { + } + + public UpdateHistory LastInstalled() + { + var history = Query(x => x.EventType == UpdateHistoryEventType.Installed) + .OrderBy(v => v.Date) + .Take(1) + .FirstOrDefault(); + + return history; + } + + public UpdateHistory PreviouslyInstalled() + { + var history = Query(x => x.EventType == UpdateHistoryEventType.Installed) + .OrderBy(v => v.Date) + .Skip(1) + .Take(1) + .FirstOrDefault(); + + return history; + } + + public List InstalledSince(DateTime dateTime) + { + var history = Query(v => v.EventType == UpdateHistoryEventType.Installed && v.Date >= dateTime) + .OrderBy(v => v.Date) + .ToList(); + + return history; + } + } +} diff --git a/src/NzbDrone.Core/Update/History/UpdateHistoryService.cs b/src/NzbDrone.Core/Update/History/UpdateHistoryService.cs new file mode 100644 index 000000000..8d11ea054 --- /dev/null +++ b/src/NzbDrone.Core/Update/History/UpdateHistoryService.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Update.History.Events; + +namespace NzbDrone.Core.Update.History +{ + public interface IUpdateHistoryService + { + Version PreviouslyInstalled(); + List InstalledSince(DateTime dateTime); + } + + public class UpdateHistoryService : IUpdateHistoryService, IHandle, IHandleAsync + { + private readonly IUpdateHistoryRepository _repository; + private readonly IEventAggregator _eventAggregator; + private Version _prevVersion; + + public UpdateHistoryService(IUpdateHistoryRepository repository, IEventAggregator eventAggregator) + { + _repository = repository; + _eventAggregator = eventAggregator; + } + + public Version PreviouslyInstalled() + { + var history = _repository.PreviouslyInstalled(); + + return history?.Version; + } + + public List InstalledSince(DateTime dateTime) + { + return _repository.InstalledSince(dateTime); + } + + public void Handle(ApplicationStartedEvent message) + { + if (BuildInfo.Version.Major == 10) + { + // Don't save dev versions, they change constantly + return; + } + + var history = _repository.LastInstalled(); + + if (history == null || history.Version != BuildInfo.Version) + { + _prevVersion = history.Version; + + _repository.Insert(new UpdateHistory + { + Date = DateTime.UtcNow, + Version = BuildInfo.Version, + EventType = UpdateHistoryEventType.Installed + }); + } + } + + public void HandleAsync(ApplicationStartedEvent message) + { + if (_prevVersion != null) + { + _eventAggregator.PublishEvent(new UpdateInstalledEvent(_prevVersion, BuildInfo.Version)); + } + } + } +} diff --git a/src/NzbDrone.Core/Update/RecentUpdateProvider.cs b/src/NzbDrone.Core/Update/RecentUpdateProvider.cs index 6fcaf42c2..42b338f5f 100644 --- a/src/NzbDrone.Core/Update/RecentUpdateProvider.cs +++ b/src/NzbDrone.Core/Update/RecentUpdateProvider.cs @@ -1,6 +1,8 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Update.History; namespace NzbDrone.Core.Update { @@ -13,18 +15,23 @@ namespace NzbDrone.Core.Update { private readonly IConfigFileProvider _configFileProvider; private readonly IUpdatePackageProvider _updatePackageProvider; + private readonly IUpdateHistoryService _updateHistoryService; public RecentUpdateProvider(IConfigFileProvider configFileProvider, - IUpdatePackageProvider updatePackageProvider) + IUpdatePackageProvider updatePackageProvider, + IUpdateHistoryService updateHistoryService) { _configFileProvider = configFileProvider; _updatePackageProvider = updatePackageProvider; + _updateHistoryService = updateHistoryService; } public List GetRecentUpdatePackages() { var branch = _configFileProvider.Branch; - return _updatePackageProvider.GetRecentUpdates(branch, BuildInfo.Version); + var version = BuildInfo.Version; + var prevVersion = _updateHistoryService.PreviouslyInstalled(); + return _updatePackageProvider.GetRecentUpdates(branch, version, prevVersion); } } } diff --git a/src/NzbDrone.Core/Update/UpdatePackageProvider.cs b/src/NzbDrone.Core/Update/UpdatePackageProvider.cs index aed22c43b..13aee9b6e 100644 --- a/src/NzbDrone.Core/Update/UpdatePackageProvider.cs +++ b/src/NzbDrone.Core/Update/UpdatePackageProvider.cs @@ -11,7 +11,7 @@ namespace NzbDrone.Core.Update public interface IUpdatePackageProvider { UpdatePackage GetLatestUpdate(string branch, Version currentVersion); - List GetRecentUpdates(string branch, Version currentVersion); + List GetRecentUpdates(string branch, Version currentVersion, Version previousVersion = null); } public class UpdatePackageProvider : IUpdatePackageProvider @@ -56,7 +56,7 @@ namespace NzbDrone.Core.Update return update.UpdatePackage; } - public List GetRecentUpdates(string branch, Version currentVersion) + public List GetRecentUpdates(string branch, Version currentVersion, Version previousVersion) { var request = _requestBuilder.Create() .Resource("/update/{branch}/changes") @@ -67,6 +67,11 @@ namespace NzbDrone.Core.Update .AddQueryParam("runtimeVer", _platformInfo.Version) .SetSegment("branch", branch); + if (previousVersion != null && previousVersion != currentVersion) + { + request.AddQueryParam("prevVersion", previousVersion); + } + if (_analyticsService.IsEnabled) { // Send if the system is active so we know which versions to deprecate/ignore diff --git a/src/Radarr.Api.V3/Update/UpdateModule.cs b/src/Radarr.Api.V3/Update/UpdateModule.cs index bee86c83b..dbc6a3915 100644 --- a/src/Radarr.Api.V3/Update/UpdateModule.cs +++ b/src/Radarr.Api.V3/Update/UpdateModule.cs @@ -1,7 +1,9 @@ using System.Collections.Generic; using System.Linq; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Update; +using NzbDrone.Core.Update.History; using Radarr.Http; namespace Radarr.Api.V3.Update @@ -9,10 +11,12 @@ namespace Radarr.Api.V3.Update public class UpdateModule : RadarrRestModule { private readonly IRecentUpdateProvider _recentUpdateProvider; + private readonly IUpdateHistoryService _updateHistoryService; - public UpdateModule(IRecentUpdateProvider recentUpdateProvider) + public UpdateModule(IRecentUpdateProvider recentUpdateProvider, IUpdateHistoryService updateHistoryService) { _recentUpdateProvider = recentUpdateProvider; + _updateHistoryService = updateHistoryService; GetResourceAll = GetRecentUpdates; } @@ -38,6 +42,18 @@ namespace Radarr.Api.V3.Update { installed.Installed = true; } + + var installDates = _updateHistoryService.InstalledSince(resources.Last().ReleaseDate) + .DistinctBy(v => v.Version) + .ToDictionary(v => v.Version); + + foreach (var resource in resources) + { + if (installDates.TryGetValue(resource.Version, out var installDate)) + { + resource.InstalledOn = installDate.Date; + } + } } return resources; diff --git a/src/Radarr.Api.V3/Update/UpdateResource.cs b/src/Radarr.Api.V3/Update/UpdateResource.cs index 127ae45b4..365619a88 100644 --- a/src/Radarr.Api.V3/Update/UpdateResource.cs +++ b/src/Radarr.Api.V3/Update/UpdateResource.cs @@ -17,6 +17,7 @@ namespace Radarr.Api.V3.Update public string FileName { get; set; } public string Url { get; set; } public bool Installed { get; set; } + public DateTime? InstalledOn { get; set; } public bool Installable { get; set; } public bool Latest { get; set; } public UpdateChanges Changes { get; set; }