From 4cd57f4a90bf909456460586f92554620e738349 Mon Sep 17 00:00:00 2001 From: Gary Sharp Date: Sun, 1 Jun 2014 23:27:07 +1000 Subject: [PATCH] Update: SignalR 2.0.3 Migration; Noticeboards Migrate all SignalR 1.x Persistent Connections to SignalR 2.x Hubs. Abstracts ScheduledTaskStatus with core interface and adds a Mock for optional status reporting. Noticeboards rewritten (with new theme) to be more resilient and accurate. --- .nuget/packages.config | 4 + .../Importer/DocumentImporterCleanCacheJob.cs | 3 +- .../Expressions/ExpressionCachePreloadTask.cs | 1 + Disco.BI/BI/Interop/Community/UpdateCheck.cs | 19 +- .../AuthorizedPersistentConnection.cs | 35 - .../HeldDeviceNotifications.cs | 72 - .../SignalRHandlers/LogNotifications.cs | 79 - .../RepositoryMonitorNotifications.cs | 57 - .../ScheduledTasksStatusNotifications.cs | 75 - .../SignalRAuthenticationWorkaround.cs | 58 - .../UserHeldDeviceNotifications.cs | 82 - .../BI/JobBI/Statistics/DailyOpenedClosed.cs | 1 + Disco.BI/Disco.BI.csproj | 35 +- Disco.BI/packages.config | 7 - Disco.Models/Disco.Models.csproj | 5 +- .../Jobs/Noticeboards/IHeldDeviceItem.cs | 40 + .../ConfigSharedTaskStatusModel.cs} | 4 +- Disco.Services/Devices/DeviceUpdatesHub.cs | 86 + .../Devices/Exporting/DeviceExport.cs | 24 +- .../Devices/Importing/DeviceImport.cs | 18 +- Disco.Services/Disco.Services.csproj | 44 +- Disco.Services/Extensions/RxExtensions.cs | 182 ++ .../ADTaskUpdateNetworkLogonDates.cs | 2 +- .../Jobs/JobQueues/JobQueueService.cs | 2 +- Disco.Services/Jobs/JobUpdatesHub.cs | 124 + .../Jobs/Noticeboards/HeldDeviceItem.cs | 137 ++ .../Jobs/Noticeboards/HeldDevices.cs | 239 ++ .../Jobs/Noticeboards/HeldDevicesForUsers.cs | 92 + .../Noticeboards/NoticeboardUpdatesHub.cs | 40 + Disco.Services/Logging/LogContext.cs | 12 +- Disco.Services/Logging/LogNotificationsHub.cs | 67 + .../LogPersistContext.cs | 2 +- .../LogPersistContextInitializer.cs | 2 +- Disco.Services/Logging/ReadLogContext.cs | 19 +- .../Logging/Targets/LogLiveContext.cs | 21 - .../Results/PluginResourceCssResult.cs | 4 +- .../Results/PluginResourceScriptResult.cs | 4 +- Disco.Services/Plugins/PluginExtensions.cs | 16 +- ...BasicStatus.cs => IScheduledTaskStatus.cs} | 20 +- Disco.Services/Tasks/ScheduledTask.cs | 2 +- .../Tasks/ScheduledTaskMockStatus.cs | 72 +- .../Tasks/ScheduledTaskNotificationsHub.cs | 89 + Disco.Services/Tasks/ScheduledTaskStatus.cs | 171 +- Disco.Services/Tasks/ScheduledTasks.cs | 5 +- Disco.Services/Users/UserExtensions.cs | 9 +- Disco.Services/Users/UserUpdatesHub.cs | 86 + .../Web/Bundles/BundleExtensions.cs | 10 +- Disco.Services/Web/Bundles/BundleHandler.cs | 4 +- Disco.Services/Web/Bundles/BundleTable.cs | 12 +- .../Web/Bundles/{Bundle.cs => FileBundle.cs} | 11 +- Disco.Services/Web/Bundles/IBundle.cs | 23 + Disco.Services/Web/Bundles/UrlBundle.cs | 32 + .../DiscoHubAuthorizeAllAttribute.cs | 35 + .../DiscoHubAuthorizeAnyAttribute.cs | 35 + .../Signalling/DiscoHubAuthorizeAttribute.cs | 37 + Disco.Services/packages.config | 7 +- Disco.Web/App_Start/AppConfig.cs | 2 +- Disco.Web/App_Start/BundleConfig.cs | 36 +- Disco.Web/App_Start/OwinStartupConfig.cs | 21 + Disco.Web/App_Start/RazorGeneratorMvcStart.cs | 4 +- Disco.Web/App_Start/RouteConfig.cs | 6 - Disco.Web/Areas/API/APIAreaRegistration.cs | 10 - .../API/Models/Attachment/_AttachmentModel.cs | 14 +- .../Config/Controllers/LoggingController.cs | 5 +- .../Config/Models/Logging/TaskStatusModel.cs | 13 - .../Config/Models/Shared/TaskStatusModel.cs | 13 + .../Config/Views/Logging/TaskStatus.cshtml | 254 +- .../Views/Logging/TaskStatus.generated.cs | 260 +-- .../Config/Views/Shared/LogEvents.cshtml | 33 +- .../Views/Shared/LogEvents.generated.cs | 111 +- .../Config/Views/Shared/TaskStatus.cshtml | 242 ++ .../Views/Shared/TaskStatus.generated.cs | 281 +++ .../Controllers/HeldDevicesController.cs | 86 +- .../Controllers/UserHeldDevicesController.cs | 85 +- .../Models/HeldDevices/HeldDeviceModel.cs | 44 - .../HeldDevices/HeldDeviceQueryModel.cs | 61 - .../Areas/Public/PublicAreaRegistration.cs | 6 - .../Public/Views/HeldDevices/Index.cshtml | 28 +- .../Views/HeldDevices/Index.generated.cs | 46 +- .../Views/HeldDevices/Noticeboard.cshtml | 774 +++--- .../HeldDevices/Noticeboard.generated.cs | 633 +++-- .../Public/Views/UserHeldDevices/Index.cshtml | 38 +- .../Views/UserHeldDevices/Index.generated.cs | 123 +- .../Views/UserHeldDevices/Noticeboard.cshtml | 779 +++--- .../UserHeldDevices/Noticeboard.generated.cs | 631 +++-- .../ClientSource/Scripts/Modules/Knockout.js | 180 +- .../Scripts/Modules/Knockout.js.bundle | 2 +- .../Scripts/Modules/Knockout.min.js | 6 +- .../Scripts/Modules/Knockout.min.js.map | 6 +- .../Modules/Knockout/knockout-2.3.0.js | 88 - .../Modules/Knockout/knockout-3.1.0.js | 96 + .../Scripts/Modules/jQuery-SignalR.js | 1948 ++++++++++----- .../Scripts/Modules/jQuery-SignalR.js.bundle | 3 +- .../Scripts/Modules/jQuery-SignalR.min.js | 6 +- .../Scripts/Modules/jQuery-SignalR.min.js.map | 6 +- .../Modules/jQuery-SignalR/disco-hubs.js | 117 + ...gnalR-1.1.2.js => jquery.signalR-2.0.3.js} | 1828 ++++++++++----- Disco.Web/ClientSource/Style/Config.css | 12 +- Disco.Web/ClientSource/Style/Config.less | 2 +- Disco.Web/ClientSource/Style/Config.min.css | 2 +- .../Style/Public/HeldDevicesNoticeboard.css | 2080 ++++++++++++++++- .../Style/Public/HeldDevicesNoticeboard.less | 263 ++- .../Public/HeldDevicesNoticeboard.min.css | 2 +- .../Public/UserHeldDevicesXml_Sharepoint.xslt | 10 +- Disco.Web/Disco.Web.csproj | 49 +- .../Device/DeviceParts/_Resources.cshtml | 207 +- .../DeviceParts/_Resources.generated.cs | 465 ++-- Disco.Web/Views/Job/JobParts/Resources.cshtml | 200 +- .../Views/Job/JobParts/Resources.generated.cs | 393 ++-- Disco.Web/Views/Job/Show.cshtml | 10 +- Disco.Web/Views/Job/Show.generated.cs | 18 +- Disco.Web/Views/Update/Index.cshtml | 254 +- Disco.Web/Views/Update/Index.generated.cs | 258 +- .../Views/User/UserParts/_Resources.cshtml | 216 +- .../User/UserParts/_Resources.generated.cs | 581 ++--- Disco.Web/packages.config | 15 +- 116 files changed, 9874 insertions(+), 6462 deletions(-) create mode 100644 .nuget/packages.config delete mode 100644 Disco.BI/BI/Interop/SignalRHandlers/AuthorizedPersistentConnection.cs delete mode 100644 Disco.BI/BI/Interop/SignalRHandlers/HeldDeviceNotifications.cs delete mode 100644 Disco.BI/BI/Interop/SignalRHandlers/LogNotifications.cs delete mode 100644 Disco.BI/BI/Interop/SignalRHandlers/RepositoryMonitorNotifications.cs delete mode 100644 Disco.BI/BI/Interop/SignalRHandlers/ScheduledTasksStatusNotifications.cs delete mode 100644 Disco.BI/BI/Interop/SignalRHandlers/SignalRAuthenticationWorkaround.cs delete mode 100644 Disco.BI/BI/Interop/SignalRHandlers/UserHeldDeviceNotifications.cs create mode 100644 Disco.Models/Services/Jobs/Noticeboards/IHeldDeviceItem.cs rename Disco.Models/UI/Config/{Logging/ConfigLoggingTaskStatusModel.cs => Shared/ConfigSharedTaskStatusModel.cs} (62%) create mode 100644 Disco.Services/Devices/DeviceUpdatesHub.cs create mode 100644 Disco.Services/Extensions/RxExtensions.cs create mode 100644 Disco.Services/Jobs/JobUpdatesHub.cs create mode 100644 Disco.Services/Jobs/Noticeboards/HeldDeviceItem.cs create mode 100644 Disco.Services/Jobs/Noticeboards/HeldDevices.cs create mode 100644 Disco.Services/Jobs/Noticeboards/HeldDevicesForUsers.cs create mode 100644 Disco.Services/Jobs/Noticeboards/NoticeboardUpdatesHub.cs create mode 100644 Disco.Services/Logging/LogNotificationsHub.cs rename Disco.Services/Logging/{Targets => Persistance}/LogPersistContext.cs (93%) rename Disco.Services/Logging/{Targets => Persistance}/LogPersistContextInitializer.cs (90%) delete mode 100644 Disco.Services/Logging/Targets/LogLiveContext.cs rename Disco.Services/Tasks/{IScheduledTaskBasicStatus.cs => IScheduledTaskStatus.cs} (52%) create mode 100644 Disco.Services/Tasks/ScheduledTaskNotificationsHub.cs create mode 100644 Disco.Services/Users/UserUpdatesHub.cs rename Disco.Services/Web/Bundles/{Bundle.cs => FileBundle.cs} (91%) create mode 100644 Disco.Services/Web/Bundles/IBundle.cs create mode 100644 Disco.Services/Web/Bundles/UrlBundle.cs create mode 100644 Disco.Services/Web/Signalling/DiscoHubAuthorizeAllAttribute.cs create mode 100644 Disco.Services/Web/Signalling/DiscoHubAuthorizeAnyAttribute.cs create mode 100644 Disco.Services/Web/Signalling/DiscoHubAuthorizeAttribute.cs create mode 100644 Disco.Web/App_Start/OwinStartupConfig.cs delete mode 100644 Disco.Web/Areas/Config/Models/Logging/TaskStatusModel.cs create mode 100644 Disco.Web/Areas/Config/Models/Shared/TaskStatusModel.cs create mode 100644 Disco.Web/Areas/Config/Views/Shared/TaskStatus.cshtml create mode 100644 Disco.Web/Areas/Config/Views/Shared/TaskStatus.generated.cs delete mode 100644 Disco.Web/Areas/Public/Models/HeldDevices/HeldDeviceModel.cs delete mode 100644 Disco.Web/Areas/Public/Models/HeldDevices/HeldDeviceQueryModel.cs delete mode 100644 Disco.Web/ClientSource/Scripts/Modules/Knockout/knockout-2.3.0.js create mode 100644 Disco.Web/ClientSource/Scripts/Modules/Knockout/knockout-3.1.0.js create mode 100644 Disco.Web/ClientSource/Scripts/Modules/jQuery-SignalR/disco-hubs.js rename Disco.Web/ClientSource/Scripts/Modules/jQuery-SignalR/{jquery.signalR-1.1.2.js => jquery.signalR-2.0.3.js} (51%) diff --git a/.nuget/packages.config b/.nuget/packages.config new file mode 100644 index 00000000..715540c0 --- /dev/null +++ b/.nuget/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Disco.BI/BI/DocumentTemplateBI/Importer/DocumentImporterCleanCacheJob.cs b/Disco.BI/BI/DocumentTemplateBI/Importer/DocumentImporterCleanCacheJob.cs index c9674fc0..ec92420d 100644 --- a/Disco.BI/BI/DocumentTemplateBI/Importer/DocumentImporterCleanCacheJob.cs +++ b/Disco.BI/BI/DocumentTemplateBI/Importer/DocumentImporterCleanCacheJob.cs @@ -12,7 +12,8 @@ namespace Disco.BI.DocumentTemplateBI.Importer public override bool SingleInstanceTask { get { return true; } } public override bool CancelInitiallySupported { get { return false; } } - + public override bool LogExceptionsOnly { get { return true; } } + public override void InitalizeScheduledTask(DiscoDataContext Database) { // Trigger Daily @ 12:30am diff --git a/Disco.BI/BI/Expressions/ExpressionCachePreloadTask.cs b/Disco.BI/BI/Expressions/ExpressionCachePreloadTask.cs index 783ad1c1..3efa8fda 100644 --- a/Disco.BI/BI/Expressions/ExpressionCachePreloadTask.cs +++ b/Disco.BI/BI/Expressions/ExpressionCachePreloadTask.cs @@ -16,6 +16,7 @@ namespace Disco.BI.Expressions public override string TaskName { get { return "Expression Cache - Preload Task"; } } public override bool SingleInstanceTask { get { return true; } } public override bool CancelInitiallySupported { get { return false; } } + public override bool LogExceptionsOnly { get { return true; } } public override void InitalizeScheduledTask(DiscoDataContext Database) { diff --git a/Disco.BI/BI/Interop/Community/UpdateCheck.cs b/Disco.BI/BI/Interop/Community/UpdateCheck.cs index 91e07c94..85579910 100644 --- a/Disco.BI/BI/Interop/Community/UpdateCheck.cs +++ b/Disco.BI/BI/Interop/Community/UpdateCheck.cs @@ -29,16 +29,13 @@ namespace Disco.BI.Interop.Community return string.Format("{0}.{1}.{2:0000}.{3:0000}", v.Major, v.Minor, v.Build, v.Revision); } - public static UpdateResponse Check(DiscoDataContext Database, bool UseProxy, ScheduledTaskStatus status = null) + public static UpdateResponse Check(DiscoDataContext Database, bool UseProxy, IScheduledTaskStatus status) { - if (status != null) - status.UpdateStatus(10, "Building Update Request"); + status.UpdateStatus(10, "Building Update Request"); var request = BuildRequest(Database); - //var requestJson = JsonConvert.SerializeObject(request); - if (status != null) - status.UpdateStatus(40, "Sending Request"); + status.UpdateStatus(40, "Sending Request"); var DiscoBIVersion = CurrentDiscoVersionFormatted(); @@ -61,21 +58,18 @@ namespace Disco.BI.Interop.Community XmlSerializer xml = new XmlSerializer(typeof(UpdateRequestV1)); xml.Serialize(wrStream, request); } - if (status != null) - status.UpdateStatus(50, "Waiting for Response"); + status.UpdateStatus(50, "Waiting for Response"); using (HttpWebResponse webResponse = (HttpWebResponse)webRequest.GetResponse()) { if (webResponse.StatusCode == HttpStatusCode.OK) { - if (status != null) - status.UpdateStatus(90, "Reading Response"); + status.UpdateStatus(90, "Reading Response"); UpdateResponse result; using (var wResStream = webResponse.GetResponseStream()) { XmlSerializer xml = new XmlSerializer(typeof(UpdateResponse)); result = (UpdateResponse)xml.Deserialize(wResStream); } - //var result = JsonConvert.DeserializeObject(responseContent); Database.DiscoConfiguration.UpdateLastCheck = result; Database.SaveChanges(); @@ -85,8 +79,7 @@ namespace Disco.BI.Interop.Community } else { - if (status != null) - status.SetTaskException(new WebException(string.Format("Server responded with: [{0}] {1}", webResponse.StatusCode, webResponse.StatusDescription))); + status.SetTaskException(new WebException(string.Format("Server responded with: [{0}] {1}", webResponse.StatusCode, webResponse.StatusDescription))); return null; } } diff --git a/Disco.BI/BI/Interop/SignalRHandlers/AuthorizedPersistentConnection.cs b/Disco.BI/BI/Interop/SignalRHandlers/AuthorizedPersistentConnection.cs deleted file mode 100644 index 4ced4fba..00000000 --- a/Disco.BI/BI/Interop/SignalRHandlers/AuthorizedPersistentConnection.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Disco.Services.Users; -using Microsoft.AspNet.SignalR; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Disco.BI.Interop.SignalRHandlers -{ - public class AuthorizedPersistentConnection : PersistentConnection - { - private string authorizedClaim = null; - - protected virtual string AuthorizedClaim { get { return authorizedClaim; } } - - protected override bool AuthorizeRequest(IRequest request) - { - if (!request.User.Identity.IsAuthenticated) - return false; - else - { - var authToken = UserService.CurrentAuthorization; - - if (authToken == null) - return false; // No Current User - - if (authorizedClaim == null) - return true; // Just Authenticate - no Authorization - else - return authToken.Has(authorizedClaim); - } - } - } -} diff --git a/Disco.BI/BI/Interop/SignalRHandlers/HeldDeviceNotifications.cs b/Disco.BI/BI/Interop/SignalRHandlers/HeldDeviceNotifications.cs deleted file mode 100644 index 6f8c58d6..00000000 --- a/Disco.BI/BI/Interop/SignalRHandlers/HeldDeviceNotifications.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Microsoft.AspNet.SignalR; -using System.Reactive.Linq; -using Disco.Data.Repository.Monitor; -using Disco.Models.Repository; - -namespace Disco.BI.Interop.SignalRHandlers -{ - public class HeldDeviceNotifications : PersistentConnection - { - private static bool subscribed = false; - private static object subscribeLock = new object(); - private static IPersistentConnectionContext notificationContext; - - static HeldDeviceNotifications() - { - if (!subscribed) - lock (subscribeLock) - if (!subscribed) - { - notificationContext = GlobalHost.ConnectionManager.GetConnectionContext(); - - Disco.Data.Repository.Monitor.RepositoryMonitor.StreamAfterCommit.Where(e => e.EntityType == typeof(Job)).Subscribe(JobUpdated); - - Disco.Data.Repository.Monitor.RepositoryMonitor.StreamAfterCommit.Where(e => - e.EntityType == typeof(Device) && - (e.ModifiedProperties.Contains("Location") || - e.ModifiedProperties.Contains("DeviceModelId") || - e.ModifiedProperties.Contains("DeviceProfileId") || - e.ModifiedProperties.Contains("DeviceBatchId") || - e.ModifiedProperties.Contains("ComputerName") || - e.ModifiedProperties.Contains("AssignedUserId")) - ).Subscribe(DeviceUpdated); - - Disco.Data.Repository.Monitor.RepositoryMonitor.StreamAfterCommit.Where(e => - e.EntityType == typeof(User) && - e.ModifiedProperties.Contains("DisplayName") - ).Subscribe(UserUpdated); - - subscribed = true; - } - } - - private static void JobUpdated(RepositoryMonitorEvent e) - { - Job j = (Job)e.Entity; - - if (j.DeviceSerialNumber != null) - notificationContext.Connection.Broadcast(j.DeviceSerialNumber); - } - private static void DeviceUpdated(RepositoryMonitorEvent e) - { - Device d = (Device)e.Entity; - - notificationContext.Connection.Broadcast(d.SerialNumber); - } - private static void UserUpdated(RepositoryMonitorEvent e) - { - User u = (User)e.Entity; - - var userDevices = e.Database.Devices.Where(d => d.AssignedUserId == u.UserId); - - foreach (var userDevice in userDevices) - { - notificationContext.Connection.Broadcast(userDevice.SerialNumber); - } - } - } -} diff --git a/Disco.BI/BI/Interop/SignalRHandlers/LogNotifications.cs b/Disco.BI/BI/Interop/SignalRHandlers/LogNotifications.cs deleted file mode 100644 index 29f1dc50..00000000 --- a/Disco.BI/BI/Interop/SignalRHandlers/LogNotifications.cs +++ /dev/null @@ -1,79 +0,0 @@ -using Disco.Services.Authorization; -using Disco.Services.Logging; -using Disco.Services.Logging.Models; -using Microsoft.AspNet.SignalR; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Disco.BI.Interop.SignalRHandlers -{ - public class LogNotifications : AuthorizedPersistentConnection - { - public static bool initialized = false; - - protected override string AuthorizedClaim { get { return Claims.DiscoAdminAccount; } } - - public LogNotifications() - { - if (!initialized) - { - initialized = true; - Disco.Services.Logging.Targets.LogLiveContext.LogBroadcast += Broadcast; - } - } - - protected override Task OnConnected(IRequest request, string connectionId) - { - string addToGroups = request.QueryString["addToGroups"]; - - if (!string.IsNullOrWhiteSpace(addToGroups)) - { - var groups = addToGroups.Split(','); - foreach (var g in groups) - { - this.Groups.Add(connectionId, g); - } - } - - return base.OnConnected(request, connectionId); - } - - protected override System.Threading.Tasks.Task OnReceived(IRequest request, string connectionId, string data) - { - // Add to Group - if (!string.IsNullOrWhiteSpace(data) && data.StartsWith("/addToGroups:") && data.Length > 13) - { - var groups = data.Substring(13).Split(','); - foreach (var g in groups) - { - this.Groups.Add(connectionId, g); - } - } - - return base.OnReceived(request, connectionId, data); - } - - internal static void Broadcast(LogBase logModule, LogEventType eventType, DateTime Timestamp, params object[] Arguments) - { - var message = LogLiveEvent.Create(logModule, eventType, Timestamp, Arguments); - - var connectionManager = GlobalHost.ConnectionManager; - var connectionContext = connectionManager.GetConnectionContext(); - connectionContext.Groups.Send(_GroupNameAll, message); - connectionContext.Groups.Send(logModule.ModuleName, message); - } - - private const string _GroupNameAll = "__All"; - - public static string AllNotifications - { - get - { - return _GroupNameAll; - } - } - } -} diff --git a/Disco.BI/BI/Interop/SignalRHandlers/RepositoryMonitorNotifications.cs b/Disco.BI/BI/Interop/SignalRHandlers/RepositoryMonitorNotifications.cs deleted file mode 100644 index 57635705..00000000 --- a/Disco.BI/BI/Interop/SignalRHandlers/RepositoryMonitorNotifications.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Disco.Data.Repository.Monitor; -using Disco.Services.Authorization; -using Microsoft.AspNet.SignalR; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Disco.BI.Interop.SignalRHandlers -{ - public class RepositoryMonitorNotifications : AuthorizedPersistentConnection - { - protected override string AuthorizedClaim { get { return Claims.DiscoAdminAccount; } } - - public static void Initialize() - { - RepositoryMonitor.StreamAfterCommit.Subscribe(AfterCommit); - } - - protected override Task OnConnected(IRequest request, string connectionId) - { - string addToGroups = request.QueryString["addToGroups"]; - - if (!string.IsNullOrWhiteSpace(addToGroups)) - { - var groups = addToGroups.Split(','); - foreach (var g in groups) - { - this.Groups.Add(connectionId, g); - } - } - - return base.OnConnected(request, connectionId); - } - - protected override Task OnReceived(IRequest request, string connectionId, string data) - { - // Add to Group - if (!string.IsNullOrWhiteSpace(data) && data.StartsWith("/addToGroups:") && data.Length > 13) - { - var groups = data.Substring(13).Split(','); - foreach (var g in groups) - { - this.Groups.Add(connectionId, g); - } - } - - return base.OnReceived(request, connectionId, data); - } - - private static void AfterCommit(RepositoryMonitorEvent e) - { - GlobalHost.ConnectionManager.GetConnectionContext().Groups.Send(e.EntityType.Name, e); - } - } -} diff --git a/Disco.BI/BI/Interop/SignalRHandlers/ScheduledTasksStatusNotifications.cs b/Disco.BI/BI/Interop/SignalRHandlers/ScheduledTasksStatusNotifications.cs deleted file mode 100644 index 9de3b197..00000000 --- a/Disco.BI/BI/Interop/SignalRHandlers/ScheduledTasksStatusNotifications.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Disco.Services.Authorization; -using Disco.Services.Tasks; -using Microsoft.AspNet.SignalR; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Disco.BI.Interop.SignalRHandlers -{ - public class ScheduledTasksStatusNotifications : AuthorizedPersistentConnection - { - public static bool initialized = false; - - protected override string AuthorizedClaim { get { return Claims.DiscoAdminAccount; } } - - public ScheduledTasksStatusNotifications() - { - if (!initialized) - { - initialized = true; - Disco.Services.Tasks.ScheduledTaskStatus.UpdatedBroadcast += Broadcast; - } - } - - protected override Task OnConnected(IRequest request, string connectionId) - { - string addToGroups = request.QueryString["addToGroups"]; - - if (!string.IsNullOrWhiteSpace(addToGroups)) - { - var groups = addToGroups.Split(','); - foreach (var g in groups) - { - this.Groups.Add(connectionId, g); - } - } - - return base.OnConnected(request, connectionId); - } - - protected override System.Threading.Tasks.Task OnReceived(IRequest request, string connectionId, string data) - { - // Add to Group - if (!string.IsNullOrWhiteSpace(data) && data.StartsWith("/addToGroups:") && data.Length > 13) - { - var groups = data.Substring(13).Split(','); - foreach (var g in groups) - { - this.Groups.Add(connectionId, g); - } - } - return base.OnReceived(request, connectionId, data); - } - - internal static void Broadcast(ScheduledTaskStatusLive SessionStatus) - { - var connectionManager = GlobalHost.ConnectionManager; - var connectionContext = connectionManager.GetConnectionContext(); - connectionContext.Groups.Send(_GroupNameAll, SessionStatus); - connectionContext.Groups.Send(SessionStatus.SessionId, SessionStatus); - } - - private const string _GroupNameAll = "__All"; - - public static string AllNotifications - { - get - { - return _GroupNameAll; - } - } - } -} diff --git a/Disco.BI/BI/Interop/SignalRHandlers/SignalRAuthenticationWorkaround.cs b/Disco.BI/BI/Interop/SignalRHandlers/SignalRAuthenticationWorkaround.cs deleted file mode 100644 index fba2d313..00000000 --- a/Disco.BI/BI/Interop/SignalRHandlers/SignalRAuthenticationWorkaround.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using System.Web; -using System.Web.Routing; -using System.Web.Security; -using System.Web.SessionState; -using Microsoft.AspNet.SignalR; -using Owin; -using AppFunc = System.Func, System.Threading.Tasks.Task>; - -namespace Disco.BI.Interop.SignalRHandlers -{ - /// - /// Required for SignalR 1.1.0 NTLM support in Firefox & Safari - /// Returns 401 (Unauthorized) instead of 403 (Forbidden) when an unauthenticated request is processed - /// - /// TODO: Remove this workaround when implementing SignalR 2.x - /// - /// Thanks to David Fowler (@davidfowl) - /// - public static class SignalRAuthenticationWorkaround - { - public static void AddMiddleware(IAppBuilder app) - { - Func convert403To401 = Convert403To401; - - app.Use(convert403To401); - } - - private static AppFunc Convert403To401(AppFunc next) - { - return env => - { - // Execute the SignalR pipeline - Task task = next(env); - - // Get the status code - int statusCode = 0; - if (env.ContainsKey("owin.ResponseStatusCode")) - { - statusCode = (int)env["owin.ResponseStatusCode"]; - } - - // If its 403 then convert it to 401 (we shouldn't do - // this if it's a cross domain request since it doesn't make sense) - if (statusCode == 403) - { - env["owin.ResponseStatusCode"] = 401; - } - - // Return the original task - return task; - }; - } - } -} diff --git a/Disco.BI/BI/Interop/SignalRHandlers/UserHeldDeviceNotifications.cs b/Disco.BI/BI/Interop/SignalRHandlers/UserHeldDeviceNotifications.cs deleted file mode 100644 index ec7f31c7..00000000 --- a/Disco.BI/BI/Interop/SignalRHandlers/UserHeldDeviceNotifications.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Microsoft.AspNet.SignalR; -using System.Reactive.Linq; -using Disco.Data.Repository.Monitor; -using Disco.Models.Repository; - -namespace Disco.BI.Interop.SignalRHandlers -{ - public class UserHeldDeviceNotifications : PersistentConnection - { - private static bool subscribed = false; - private static object subscribeLock = new object(); - private static IPersistentConnectionContext notificationContext; - - static UserHeldDeviceNotifications() - { - if (!subscribed) - lock (subscribeLock) - if (!subscribed) - { - notificationContext = GlobalHost.ConnectionManager.GetConnectionContext(); - - Disco.Data.Repository.Monitor.RepositoryMonitor.StreamAfterCommit.Where(e => e.EntityType == typeof(Job)).Subscribe(JobUpdated); - - Disco.Data.Repository.Monitor.RepositoryMonitor.StreamBeforeCommit.Where(e => - e.EntityType == typeof(Device) && - (e.ModifiedProperties.Contains("DeviceModelId") || - e.ModifiedProperties.Contains("DeviceProfileId") || - e.ModifiedProperties.Contains("DeviceBatchId") || - e.ModifiedProperties.Contains("AssignedUserId")) - ).Subscribe(DeviceUpdated); - - Disco.Data.Repository.Monitor.RepositoryMonitor.StreamAfterCommit.Where(e => - e.EntityType == typeof(User) && - e.ModifiedProperties.Contains("DisplayName") - ).Subscribe(UserUpdated); - - subscribed = true; - } - } - - private static void JobUpdated(RepositoryMonitorEvent e) - { - Job j = (Job)e.Entity; - - if (j.DeviceSerialNumber != null) - { - var jobDevice = e.Database.Devices.Where(d => d.SerialNumber == j.DeviceSerialNumber).FirstOrDefault(); - - if (jobDevice.AssignedUserId != null) - notificationContext.Connection.Broadcast(jobDevice.AssignedUserId); - } - } - private static void DeviceUpdated(RepositoryMonitorEvent e) - { - Device d = (Device)e.Entity; - - string previouslyAssignedUserId = null; - - if (e.ModifiedProperties.Contains("AssignedUserId")) - previouslyAssignedUserId = e.GetPreviousPropertyValue("AssignedUserId"); - - e.ExecuteAfterCommit(me => - { - if (previouslyAssignedUserId != null) - notificationContext.Connection.Broadcast(previouslyAssignedUserId); - - if (d.AssignedUserId != null) - notificationContext.Connection.Broadcast(d.AssignedUserId); - }); - } - private static void UserUpdated(RepositoryMonitorEvent e) - { - User u = (User)e.Entity; - - notificationContext.Connection.Broadcast(u.UserId); - } - } -} diff --git a/Disco.BI/BI/JobBI/Statistics/DailyOpenedClosed.cs b/Disco.BI/BI/JobBI/Statistics/DailyOpenedClosed.cs index beae69ff..fb5c1428 100644 --- a/Disco.BI/BI/JobBI/Statistics/DailyOpenedClosed.cs +++ b/Disco.BI/BI/JobBI/Statistics/DailyOpenedClosed.cs @@ -24,6 +24,7 @@ namespace Disco.BI.JobBI.Statistics public override string TaskName { get { return "Job Statistics - Daily Opened/Closed Task"; } } public override bool SingleInstanceTask { get { return true; } } public override bool CancelInitiallySupported { get { return false; } } + public override bool LogExceptionsOnly { get { return true; } } public override void InitalizeScheduledTask(DiscoDataContext Database) { diff --git a/Disco.BI/Disco.BI.csproj b/Disco.BI/Disco.BI.csproj index 5ab598f9..ab0ebde5 100644 --- a/Disco.BI/Disco.BI.csproj +++ b/Disco.BI/Disco.BI.csproj @@ -46,32 +46,6 @@ ..\Resources\Libraries\iTextSharp\itextsharp.dll - - False - ..\packages\Microsoft.AspNet.SignalR.Core.1.1.2\lib\net40\Microsoft.AspNet.SignalR.Core.dll - - - False - ..\packages\Microsoft.AspNet.SignalR.Owin.1.1.2\lib\net45\Microsoft.AspNet.SignalR.Owin.dll - - - False - ..\packages\Microsoft.AspNet.SignalR.SystemWeb.1.1.2\lib\net45\Microsoft.AspNet.SignalR.SystemWeb.dll - - - ..\packages\Microsoft.Owin.Host.SystemWeb.1.0.1\lib\net45\Microsoft.Owin.Host.SystemWeb.dll - - - True - ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll - - - False - ..\packages\Newtonsoft.Json.5.0.8\lib\net45\Newtonsoft.Json.dll - - - ..\packages\Owin.1.0\lib\net40\Owin.dll - ..\Resources\Libraries\Quartz\Quartz.dll @@ -172,13 +146,6 @@ - - - - - - - @@ -235,7 +202,7 @@ - + diff --git a/Disco.BI/packages.config b/Disco.BI/packages.config index 4265cdb7..8a544b6f 100644 --- a/Disco.BI/packages.config +++ b/Disco.BI/packages.config @@ -1,13 +1,6 @@  - - - - - - - diff --git a/Disco.Models/Disco.Models.csproj b/Disco.Models/Disco.Models.csproj index ac48ac8e..c5951838 100644 --- a/Disco.Models/Disco.Models.csproj +++ b/Disco.Models/Disco.Models.csproj @@ -115,6 +115,7 @@ + @@ -151,7 +152,7 @@ - + @@ -175,7 +176,7 @@ - + diff --git a/Disco.Models/Services/Jobs/Noticeboards/IHeldDeviceItem.cs b/Disco.Models/Services/Jobs/Noticeboards/IHeldDeviceItem.cs new file mode 100644 index 00000000..eb43fc8f --- /dev/null +++ b/Disco.Models/Services/Jobs/Noticeboards/IHeldDeviceItem.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Disco.Models.Services.Jobs.Noticeboards +{ + public interface IHeldDeviceItem + { + int JobId { get; } + + string DeviceSerialNumber { get; } + string DeviceComputerNameFriendly { get; } + string DeviceComputerName { get; } + + string DeviceLocation { get; } + string DeviceDescription { get; } + + int DeviceProfileId { get; } + int? DeviceAddressId { get; } + string DeviceAddressShortName { get; } + + string UserId { get; } + string UserIdFriendly { get; } + string UserDisplayName { get; } + + bool WaitingForUserAction { get; } + DateTime? WaitingForUserActionSince { get; } + long? WaitingForUserActionSinceUnixEpoc { get; } + + bool ReadyForReturn { get; } + DateTime? EstimatedReturnTime { get; } + long? EstimatedReturnTimeUnixEpoc { get; } + DateTime? ReadyForReturnSince { get; } + long? ReadyForReturnSinceUnixEpoc { get; } + + bool IsAlert { get; } + } +} diff --git a/Disco.Models/UI/Config/Logging/ConfigLoggingTaskStatusModel.cs b/Disco.Models/UI/Config/Shared/ConfigSharedTaskStatusModel.cs similarity index 62% rename from Disco.Models/UI/Config/Logging/ConfigLoggingTaskStatusModel.cs rename to Disco.Models/UI/Config/Shared/ConfigSharedTaskStatusModel.cs index 3e292ae7..afdd226c 100644 --- a/Disco.Models/UI/Config/Logging/ConfigLoggingTaskStatusModel.cs +++ b/Disco.Models/UI/Config/Shared/ConfigSharedTaskStatusModel.cs @@ -4,9 +4,9 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Disco.Models.UI.Config.Logging +namespace Disco.Models.UI.Config.Shared { - public interface ConfigLoggingTaskStatusModel : BaseUIModel + public interface ConfigSharedTaskStatusModel : BaseUIModel { string SessionId { get; set; } } diff --git a/Disco.Services/Devices/DeviceUpdatesHub.cs b/Disco.Services/Devices/DeviceUpdatesHub.cs new file mode 100644 index 00000000..3a487262 --- /dev/null +++ b/Disco.Services/Devices/DeviceUpdatesHub.cs @@ -0,0 +1,86 @@ +using Disco.Data.Repository.Monitor; +using Disco.Services.Authorization; +using Disco.Services.Web.Signalling; +using Microsoft.AspNet.SignalR; +using Microsoft.AspNet.SignalR.Hubs; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Reactive.Linq; +using Disco.Models.Repository; +using Disco.Data.Repository; + +namespace Disco.Services.Devices +{ + [HubName("deviceUpdates"), DiscoHubAuthorizeAll(Claims.Device.Show, Claims.Device.ShowAttachments)] + public class DeviceUpdatesHub : Hub + { + private const string UserPrefix = "Device_"; + public static IHubContext HubContext { get; private set; } + + private static IDisposable RepositoryBeforeSubscription; + private static IDisposable RepositoryAfterSubscription; + + static DeviceUpdatesHub() + { + HubContext = GlobalHost.ConnectionManager.GetHubContext(); + + // Subscribe to Repository Monitor for Changes + RepositoryBeforeSubscription = RepositoryMonitor.StreamBeforeCommit + .Where(e => e.EntityType == typeof(DeviceAttachment) && e.EventType == RepositoryMonitorEventType.Deleted) + .Subscribe(RepositoryEventBefore); + RepositoryAfterSubscription = RepositoryMonitor.StreamAfterCommit + .Where(e => e.EntityType == typeof(DeviceAttachment) && e.EventType == RepositoryMonitorEventType.Added) + .Subscribe(RepositoryAfterEvent); + } + + private static string GroupName(string DeviceSerialNumber) + { + return UserPrefix + DeviceSerialNumber; + } + + public override Task OnConnected() + { + var deviceSerialNumber = Context.QueryString["DeviceSerialNumber"]; + + if (string.IsNullOrWhiteSpace(deviceSerialNumber)) + throw new ArgumentNullException("DeviceSerialNumber"); + + Groups.Add(Context.ConnectionId, GroupName(deviceSerialNumber)); + + return base.OnConnected(); + } + + private static void RepositoryEventBefore(RepositoryMonitorEvent e) + { + if (e.EventType == RepositoryMonitorEventType.Deleted) + { + if (e.EntityType == typeof(DeviceAttachment)) + { + var repositoryAttachment = (DeviceAttachment)e.Entity; + string attachmentDeviceSerialNumber; + + using (DiscoDataContext Database = new DiscoDataContext()) + attachmentDeviceSerialNumber = Database.DeviceAttachments.Where(a => a.Id == repositoryAttachment.Id).Select(a => a.DeviceSerialNumber).First(); + + HubContext.Clients.Group(GroupName(attachmentDeviceSerialNumber)).removeAttachment(repositoryAttachment.Id); + } + } + } + + private static void RepositoryAfterEvent(RepositoryMonitorEvent e) + { + if (e.EventType == RepositoryMonitorEventType.Added) + { + if (e.EntityType == typeof(DeviceAttachment)) + { + var a = (DeviceAttachment)e.Entity; + + HubContext.Clients.Group(GroupName(a.DeviceSerialNumber)).addAttachment(a.Id); + } + } + } + } +} diff --git a/Disco.Services/Devices/Exporting/DeviceExport.cs b/Disco.Services/Devices/Exporting/DeviceExport.cs index 618ba74b..2d2b2ec1 100644 --- a/Disco.Services/Devices/Exporting/DeviceExport.cs +++ b/Disco.Services/Devices/Exporting/DeviceExport.cs @@ -16,7 +16,7 @@ namespace Disco.Services.Devices.Exporting public static class DeviceExport { - public static DeviceExportResult GenerateExport(DiscoDataContext Database, IQueryable Devices, DeviceExportOptions Options, IScheduledTaskBasicStatus TaskStatus) + public static DeviceExportResult GenerateExport(DiscoDataContext Database, IQueryable Devices, DeviceExportOptions Options, IScheduledTaskStatus TaskStatus) { TaskStatus.UpdateStatus(15, "Building metadata and database query"); var metadata = Options.BuildMetadata(); @@ -32,15 +32,17 @@ namespace Disco.Services.Devices.Exporting Options.AssignedUserEmailAddress) { TaskStatus.UpdateStatus(20, "Updating Assigned User details"); + var users = Devices.Where(d => d.AssignedUserId != null).Select(d => d.AssignedUserId).Distinct().ToList(); - Devices.Where(d => d.AssignedUserId != null).Select(d => d.AssignedUserId).Distinct().ToList().ForEach(userId => + users.Select((userId, index) => { + TaskStatus.UpdateStatus(20 + (((double)20 / users.Count) * index), string.Format("Updating Assigned User details: {0}", userId)); try { - UserService.GetUser(userId, Database); + return UserService.GetUser(userId, Database); } - catch (Exception) { } // Ignore Errors - }); + catch (Exception) { return null; } // Ignore Errors + }).ToList(); } // Update Last Network Logon Date @@ -49,8 +51,16 @@ namespace Disco.Services.Devices.Exporting TaskStatus.UpdateStatus(40, "Updating device last network logon dates"); try { - Interop.ActiveDirectory.ADTaskUpdateNetworkLogonDates.UpdateLastNetworkLogonDates(Database, ScheduledTaskMockStatus.Create()); + TaskStatus.IgnoreCurrentProcessChanges = true; + TaskStatus.ProgressMultiplier = 20 / 100; + TaskStatus.ProgressOffset = 40; + + Interop.ActiveDirectory.ADTaskUpdateNetworkLogonDates.UpdateLastNetworkLogonDates(Database, TaskStatus); Database.SaveChanges(); + + TaskStatus.IgnoreCurrentProcessChanges = false; + TaskStatus.ProgressMultiplier = 1; + TaskStatus.ProgressOffset = 0; } catch (Exception) { } // Ignore Errors } @@ -103,7 +113,7 @@ namespace Disco.Services.Devices.Exporting return GenerateExport(Database, Devices, Options, ScheduledTaskMockStatus.Create()); } - public static DeviceExportResult GenerateExport(DiscoDataContext Database, DeviceExportOptions Options, IScheduledTaskBasicStatus TaskStatus) + public static DeviceExportResult GenerateExport(DiscoDataContext Database, DeviceExportOptions Options, IScheduledTaskStatus TaskStatus) { switch (Options.ExportType) { diff --git a/Disco.Services/Devices/Importing/DeviceImport.cs b/Disco.Services/Devices/Importing/DeviceImport.cs index c91f4a14..e68806b5 100644 --- a/Disco.Services/Devices/Importing/DeviceImport.cs +++ b/Disco.Services/Devices/Importing/DeviceImport.cs @@ -102,7 +102,7 @@ namespace Disco.Services.Devices.Importing Context.Header = Context.Header.Zip(HeaderTypes, (h, ht) => Tuple.Create(h.Item1, ht)).ToList(); } - public static void ParseRecords(this DeviceImportContext Context, DiscoDataContext Database, IScheduledTaskBasicStatus Status) + public static void ParseRecords(this DeviceImportContext Context, DiscoDataContext Database, IScheduledTaskStatus Status) { if (Context.Header == null) throw new InvalidOperationException("The Import Context has not been initialized"); @@ -129,18 +129,13 @@ namespace Disco.Services.Devices.Importing .Select(h => new Tuple, Type>(h.Item1, h.Item2, (f) => f[h.Item3], DeviceImport.FieldHandlers.Value[h.Item2])) .ToList(); - DateTime nextProgress = DateTime.Now; Status.UpdateStatus(0, "Parsing Import Records", "Starting..."); Context.Records = Context.RawData.Select((d, recordIndex) => { string deviceSerialNumber = Fields.DeviceSerialNumberImportField.ParseRawDeviceSerialNumber(d[Context.HeaderDeviceSerialNumberIndex]); - if (nextProgress <= DateTime.Now) - { - Status.UpdateStatus(((double)recordIndex / Context.RawData.Count) * 100, string.Format("Parsing: {0}", deviceSerialNumber)); - nextProgress = DateTime.Now.AddSeconds(.5); - } + Status.UpdateStatus(((double)recordIndex / Context.RawData.Count) * 100, string.Format("Parsing: {0}", deviceSerialNumber)); Device existingDevice = null; if (Fields.DeviceSerialNumberImportField.IsDeviceSerialNumberValid(deviceSerialNumber)) @@ -170,7 +165,7 @@ namespace Disco.Services.Devices.Importing }).Cast().ToList(); } - public static int ApplyRecords(this DeviceImportContext Context, DiscoDataContext Database, IScheduledTaskBasicStatus Status) + public static int ApplyRecords(this DeviceImportContext Context, DiscoDataContext Database, IScheduledTaskStatus Status) { if (Context.Records == null) throw new InvalidOperationException("Import Records have not been parsed"); @@ -178,18 +173,13 @@ namespace Disco.Services.Devices.Importing if (Context.Records.Count == 0) throw new InvalidOperationException("There are no records to import"); - DateTime nextProgress = DateTime.Now; Status.UpdateStatus(0, "Applying Import Records to Database", "Starting..."); int affectedRecords = 0; foreach (var record in Context.Records.Cast().Select((r, i) => Tuple.Create(r, i))) { - if (nextProgress <= DateTime.Now) - { - Status.UpdateStatus(((double)record.Item2 / Context.Records.Count) * 100, string.Format("Applying: {0}", record.Item1.DeviceSerialNumber)); - nextProgress = DateTime.Now.AddSeconds(.5); - } + Status.UpdateStatus(((double)record.Item2 / Context.Records.Count) * 100, string.Format("Applying: {0}", record.Item1.DeviceSerialNumber)); if (record.Item1.Apply(Database)) affectedRecords++; diff --git a/Disco.Services/Disco.Services.csproj b/Disco.Services/Disco.Services.csproj index cafef053..d64044cc 100644 --- a/Disco.Services/Disco.Services.csproj +++ b/Disco.Services/Disco.Services.csproj @@ -42,21 +42,21 @@ ..\packages\LumenWorks.Framework.IO.3.8.0\lib\net20\LumenWorks.Framework.IO.dll - - False - ..\packages\Microsoft.AspNet.SignalR.Core.1.1.2\lib\net40\Microsoft.AspNet.SignalR.Core.dll + + ..\packages\Microsoft.AspNet.SignalR.Core.2.0.3\lib\net45\Microsoft.AspNet.SignalR.Core.dll - - False - ..\packages\Microsoft.AspNet.SignalR.Owin.1.1.2\lib\net45\Microsoft.AspNet.SignalR.Owin.dll + + ..\packages\Microsoft.Owin.2.0.1\lib\net45\Microsoft.Owin.dll + + + ..\packages\Microsoft.Owin.Security.2.0.1\lib\net45\Microsoft.Owin.Security.dll True ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll - - False - ..\packages\Newtonsoft.Json.5.0.8\lib\net45\Newtonsoft.Json.dll + + ..\packages\Newtonsoft.Json.5.0.1\lib\net45\Newtonsoft.Json.dll ..\packages\Owin.1.0\lib\net40\Owin.dll @@ -201,7 +201,9 @@ + + @@ -227,6 +229,11 @@ + + + + + @@ -236,9 +243,8 @@ - - - + + @@ -278,9 +284,10 @@ - + + @@ -290,17 +297,24 @@ + - + + + + + + + @@ -328,7 +342,7 @@ - + diff --git a/Disco.Services/Extensions/RxExtensions.cs b/Disco.Services/Extensions/RxExtensions.cs new file mode 100644 index 00000000..04baec0e --- /dev/null +++ b/Disco.Services/Extensions/RxExtensions.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Concurrency; +using System.Reactive.Disposables; +using System.Reactive.Linq; + +namespace Disco +{ + public static class RxExtensions + { + + #region RxExtensions + + public static IObservable> DelayBuffer(this IObservable source, TimeSpan delay) + { + return Observable.Create>(o => + { + var gate = new object(); + var buffer = new List(); + var trigger = (IDisposable)null; + var subscription = (IDisposable)null; + var scheduler = Scheduler.Default; + + Action dump = () => + { + var bts = buffer.ToArray(); + buffer = new List(); + if (o != null) + o.OnNext(bts); + if (trigger != null) + { + trigger.Dispose(); + trigger = null; + } + }; + + Action dispose = () => + { + if (subscription != null) + subscription.Dispose(); + if (trigger != null) + { + trigger.Dispose(); + trigger = null; + } + }; + + Action>>> onErrorOrCompleted = + onAction => + { + lock (gate) + { + dispose(); + dump(); + if (o != null) + onAction(o); + } + }; + + Action onError = ex => + onErrorOrCompleted(x => x.OnError(ex)); + + Action onCompleted = () => onErrorOrCompleted(x => x.OnCompleted()); + + Action onNext = t => + { + lock (gate) + { + buffer.Add(t); + + if (trigger == null) + { + trigger = scheduler.Schedule(delay, () => + { + lock (gate) + { + dump(); + } + }); + } + } + }; + + subscription = + source + .ObserveOn(scheduler) + .Subscribe(onNext, onError, onCompleted); + + return () => + { + lock (gate) + { + o = null; + dispose(); + } + }; + }); + } + + public static IObservable> BufferWithInactivity(this IObservable source, TimeSpan inactivity) + { + return Observable.Create>(o => + { + var gate = new object(); + var buffer = new List(); + var mutable = new SerialDisposable(); + var subscription = (IDisposable)null; + var scheduler = Scheduler.Default; + + Action dump = () => + { + var bts = buffer.ToArray(); + buffer = new List(); + if (o != null) + { + o.OnNext(bts); + } + }; + + Action dispose = () => + { + if (subscription != null) + { + subscription.Dispose(); + } + mutable.Dispose(); + }; + + Action>>> onErrorOrCompleted = + onAction => + { + lock (gate) + { + dispose(); + dump(); + if (o != null) + { + onAction(o); + } + } + }; + + Action onError = ex => + onErrorOrCompleted(x => x.OnError(ex)); + + Action onCompleted = () => onErrorOrCompleted(x => x.OnCompleted()); + + Action onNext = t => + { + lock (gate) + { + buffer.Add(t); + mutable.Disposable = scheduler.Schedule(inactivity, () => + { + lock (gate) + { + dump(); + } + }); + } + }; + + subscription = + source + .ObserveOn(scheduler) + .Subscribe(onNext, onError, onCompleted); + + return () => + { + lock (gate) + { + o = null; + dispose(); + } + }; + }); + } + + #endregion + + } +} diff --git a/Disco.Services/Interop/ActiveDirectory/ADTaskUpdateNetworkLogonDates.cs b/Disco.Services/Interop/ActiveDirectory/ADTaskUpdateNetworkLogonDates.cs index cb407b63..237e73d6 100644 --- a/Disco.Services/Interop/ActiveDirectory/ADTaskUpdateNetworkLogonDates.cs +++ b/Disco.Services/Interop/ActiveDirectory/ADTaskUpdateNetworkLogonDates.cs @@ -114,7 +114,7 @@ namespace Disco.Services.Interop.ActiveDirectory return false; } - public static void UpdateLastNetworkLogonDates(DiscoDataContext Database, IScheduledTaskBasicStatus status) + public static void UpdateLastNetworkLogonDates(DiscoDataContext Database, IScheduledTaskStatus status) { var context = ActiveDirectory.Context; const string ldapFilter = "(objectCategory=Computer)"; diff --git a/Disco.Services/Jobs/JobQueues/JobQueueService.cs b/Disco.Services/Jobs/JobQueues/JobQueueService.cs index bef9f9d4..10a31482 100644 --- a/Disco.Services/Jobs/JobQueues/JobQueueService.cs +++ b/Disco.Services/Jobs/JobQueues/JobQueueService.cs @@ -72,7 +72,7 @@ namespace Disco.Services.Jobs.JobQueues return _cache.UpdateQueue(JobQueue); } - public static void DeleteJobQueue(DiscoDataContext Database, int JobQueueId, ScheduledTaskStatus Status) + public static void DeleteJobQueue(DiscoDataContext Database, int JobQueueId, IScheduledTaskStatus Status) { JobQueue queue = Database.JobQueues.Find(JobQueueId); diff --git a/Disco.Services/Jobs/JobUpdatesHub.cs b/Disco.Services/Jobs/JobUpdatesHub.cs new file mode 100644 index 00000000..76dfccba --- /dev/null +++ b/Disco.Services/Jobs/JobUpdatesHub.cs @@ -0,0 +1,124 @@ +using Disco.Data.Repository; +using Disco.Data.Repository.Monitor; +using Disco.Models.Repository; +using Disco.Services.Authorization; +using Disco.Services.Web.Signalling; +using Microsoft.AspNet.SignalR; +using Microsoft.AspNet.SignalR.Hubs; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Disco.Services.Users; + +namespace Disco.Services.Jobs +{ + [HubName("jobUpdates"), DiscoHubAuthorize(Claims.Job.Show)] + public class JobUpdatesHub : Hub + { + private const string JobLogsPrefix = "JobLogs_"; + private const string JobAttachmentsPrefix = "JobAttachments_"; + public static IHubContext HubContext { get; private set; } + + private static IDisposable RepositoryBeforeSubscription; + private static IDisposable RepositoryAfterSubscription; + + static JobUpdatesHub() + { + HubContext = GlobalHost.ConnectionManager.GetHubContext(); + + // Subscribe to Repository Monitor for Changes + RepositoryBeforeSubscription = RepositoryMonitor.StreamBeforeCommit + .Where(e => ( + e.EntityType == typeof(JobLog) && e.EventType == RepositoryMonitorEventType.Deleted || + e.EntityType == typeof(JobAttachment) && e.EventType == RepositoryMonitorEventType.Deleted + )) + .Subscribe(RepositoryEventBefore); + RepositoryAfterSubscription = RepositoryMonitor.StreamAfterCommit + .Where(e => ( + e.EntityType == typeof(JobLog) && e.EventType == RepositoryMonitorEventType.Added || + e.EntityType == typeof(JobAttachment) && e.EventType == RepositoryMonitorEventType.Added + )) + .Subscribe(RepositoryAfterEvent); + } + + private static string LogsGroupName(int JobId) + { + return JobLogsPrefix + JobId.ToString(); + } + private static string AttachmentsGroupName(int JobId) + { + return JobAttachmentsPrefix + JobId.ToString(); + } + + public override Task OnConnected() + { + int jobId; + string jobIdParam; + + jobIdParam = Context.QueryString["JobId"]; + + if (string.IsNullOrWhiteSpace(jobIdParam)) + throw new ArgumentNullException("JobId"); + if (!int.TryParse(jobIdParam, out jobId)) + throw new ArgumentException("An integer was expected", "JobId"); + + var userAuth = UserService.GetAuthorization(Context.User.Identity.Name); + + if (userAuth.Has(Claims.Job.ShowLogs)) + Groups.Add(Context.ConnectionId, LogsGroupName(jobId)); + if (userAuth.Has(Claims.Job.ShowAttachments)) + Groups.Add(Context.ConnectionId, AttachmentsGroupName(jobId)); + + return base.OnConnected(); + } + + private static void RepositoryEventBefore(RepositoryMonitorEvent e) + { + if (e.EventType == RepositoryMonitorEventType.Deleted) + { + if (e.EntityType == typeof(JobLog)) + { + var repositoryLog = (JobLog)e.Entity; + int logJobId; + + using (DiscoDataContext Database = new DiscoDataContext()) + logJobId = Database.JobLogs.Where(l => l.Id == repositoryLog.Id).Select(a => a.JobId).First(); + + HubContext.Clients.Group(LogsGroupName(logJobId)).removeLog(repositoryLog.Id); + } + if (e.EntityType == typeof(JobAttachment)) + { + var repositoryAttachment = (JobAttachment)e.Entity; + int attachmentJobId; + + using (DiscoDataContext Database = new DiscoDataContext()) + attachmentJobId = Database.JobAttachments.Where(a => a.Id == repositoryAttachment.Id).Select(a => a.JobId).First(); + + HubContext.Clients.Group(AttachmentsGroupName(attachmentJobId)).removeAttachment(repositoryAttachment.Id); + } + } + } + + private static void RepositoryAfterEvent(RepositoryMonitorEvent e) + { + if (e.EventType == RepositoryMonitorEventType.Added) + { + if (e.EntityType == typeof(JobLog)) + { + var a = (JobLog)e.Entity; + + HubContext.Clients.Group(LogsGroupName(a.JobId)).addLog(a.Id); + } + if (e.EntityType == typeof(JobAttachment)) + { + var a = (JobAttachment)e.Entity; + + HubContext.Clients.Group(AttachmentsGroupName(a.JobId)).addAttachment(a.Id); + } + } + } + } +} diff --git a/Disco.Services/Jobs/Noticeboards/HeldDeviceItem.cs b/Disco.Services/Jobs/Noticeboards/HeldDeviceItem.cs new file mode 100644 index 00000000..6c9b85c3 --- /dev/null +++ b/Disco.Services/Jobs/Noticeboards/HeldDeviceItem.cs @@ -0,0 +1,137 @@ +using Disco.Data.Configuration.Modules; +using Disco.Models.Repository; +using Disco.Models.Services.Jobs.Noticeboards; +using Disco.Services.Interop.ActiveDirectory; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Disco.Services.Jobs.Noticeboards +{ + public class HeldDeviceItem : IHeldDeviceItem + { + public int JobId { get; set; } + + public string DeviceSerialNumber { get; set; } + public string DeviceComputerNameFriendly + { + get + { + return DeviceComputerName == null ? null : UserExtensions.FriendlyUserId(DeviceComputerName); + } + } + public string DeviceComputerName { get; set; } + + public string DeviceLocation { get; set; } + public string DeviceDescription + { + get + { + StringBuilder sb = new StringBuilder(this.DeviceComputerNameFriendly); + + if (UserId != null) + sb.Append(" - ").Append(this.UserDisplayName).Append(" (").Append(this.UserIdFriendly).Append(")"); + + if (!string.IsNullOrWhiteSpace(this.DeviceLocation)) + sb.Append(" - ").Append(this.DeviceLocation); + else if (UserId == null) + sb.Append(" - ").Append(this.DeviceSerialNumber); + + return sb.ToString(); + } + } + + public int DeviceProfileId { get; set; } + public int? DeviceAddressId { get; set; } + public string DeviceAddressShortName + { + get + { + if (DeviceAddressId.HasValue) + { + var config = new OrganisationAddressesConfiguration(null); + var address = config.GetAddress(DeviceAddressId.Value); + if (address != null) + return address.ShortName; + } + + return null; + } + } + + public string UserId { get; set; } + public string UserIdFriendly + { + get + { + return UserId == null ? null : UserExtensions.FriendlyUserId(UserId); + } + } + public string UserDisplayName { get; set; } + + public bool WaitingForUserAction { get; set; } + public DateTime? WaitingForUserActionSince { get; set; } + public long? WaitingForUserActionSinceUnixEpoc + { + get + { + return WaitingForUserActionSince.ToUnixEpoc(); + } + } + + public bool ReadyForReturn { get; set; } + public DateTime? EstimatedReturnTime { get; set; } + public long? EstimatedReturnTimeUnixEpoc + { + get + { + return EstimatedReturnTime.ToUnixEpoc(); + } + } + + public DateTime? ReadyForReturnSince { get; set; } + public long? ReadyForReturnSinceUnixEpoc + { + get + { + return ReadyForReturnSince.ToUnixEpoc(); + } + } + + public bool IsAlert + { + get + { + if (this.ReadyForReturn && (this.ReadyForReturnSince.Value < DateTime.Now.AddDays(-3))) + return true; + + if (this.WaitingForUserAction && (this.WaitingForUserActionSince.Value < DateTime.Now.AddDays(-6))) + return true; + + return false; + } + } + + internal static IEnumerable FromJobs(IQueryable jobs) + { + return jobs.Select(j => + new HeldDeviceItem + { + JobId = j.Id, + DeviceSerialNumber = j.DeviceSerialNumber, + DeviceComputerName = j.Device.DeviceDomainId, + DeviceLocation = j.Device.Location, + DeviceProfileId = j.Device.DeviceProfileId, + DeviceAddressId = j.Device.DeviceProfile.DefaultOrganisationAddress, + UserId = j.Device.AssignedUserId, + UserDisplayName = j.Device.AssignedUser.DisplayName, + WaitingForUserAction = j.WaitingForUserAction.HasValue || ((j.JobMetaNonWarranty.AccountingChargeRequiredDate.HasValue || j.JobMetaNonWarranty.AccountingChargeAddedDate.HasValue) && !j.JobMetaNonWarranty.AccountingChargePaidDate.HasValue), + WaitingForUserActionSince = j.WaitingForUserAction.HasValue ? j.WaitingForUserAction : (j.JobMetaNonWarranty.AccountingChargeRequiredDate.HasValue ? j.JobMetaNonWarranty.AccountingChargeRequiredDate : j.JobMetaNonWarranty.AccountingChargeAddedDate), + ReadyForReturn = j.DeviceReadyForReturn.HasValue && !j.DeviceReturnedDate.HasValue, + EstimatedReturnTime = j.ExpectedClosedDate, + ReadyForReturnSince = j.DeviceReadyForReturn + }); + } + } +} diff --git a/Disco.Services/Jobs/Noticeboards/HeldDevices.cs b/Disco.Services/Jobs/Noticeboards/HeldDevices.cs new file mode 100644 index 00000000..09ef3051 --- /dev/null +++ b/Disco.Services/Jobs/Noticeboards/HeldDevices.cs @@ -0,0 +1,239 @@ +using Disco.Data.Repository; +using Disco.Data.Repository.Monitor; +using Disco.Models.Repository; +using Disco.Models.Services.Jobs.Noticeboards; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; + +namespace Disco.Services.Jobs.Noticeboards +{ + public static class HeldDevices + { + public const string Name = "HeldDevices"; + + private readonly static List MonitorJobProperties = new List() { + "DeviceSerialNumber", + "UserId", + "ExpectedClosedDate", + "ClosedDate", + "WaitingForUserAction", + "DeviceHeld", + "DeviceReadyForReturn", + "DeviceReturnedDate" + }; + private readonly static List MonitorJobMetaNonWarrantyProperties = new List(){ + "AccountingChargeRequiredDate", + "AccountingChargeAddedDate", + "AccountingChargePaidDate" + }; + private readonly static List MonitorDeviceProperties = new List(){ + "Location", + "DeviceProfileId", + "DeviceDomainId", + "AssignedUserId", + }; + private readonly static List MonitorDeviceProfileProperties = new List(){ + "DefaultOrganisationAddress" + }; + private readonly static List MonitorUserProperties = new List(){ + "DisplayName" + }; + + static HeldDevices() + { + // Subscribe to Repository Notifications + RepositoryMonitor.StreamAfterCommit.Where(e => + (e.EntityType == typeof(Job) && + (e.EventType == RepositoryMonitorEventType.Added || + e.EventType == RepositoryMonitorEventType.Deleted || + (e.EventType == RepositoryMonitorEventType.Modified && e.ModifiedProperties.Any(p => MonitorJobProperties.Contains(p)))) + ) || + (e.EntityType == typeof(JobMetaNonWarranty) && + (e.EventType == RepositoryMonitorEventType.Added || + e.EventType == RepositoryMonitorEventType.Deleted || + (e.EventType == RepositoryMonitorEventType.Modified && e.ModifiedProperties.Any(p => MonitorJobMetaNonWarrantyProperties.Contains(p)))) + ) || + (e.EntityType == typeof(Device) && + (e.EventType == RepositoryMonitorEventType.Modified && e.ModifiedProperties.Any(p => MonitorDeviceProperties.Contains(p))) + ) || + (e.EntityType == typeof(DeviceProfile) && + (e.EventType == RepositoryMonitorEventType.Modified && e.ModifiedProperties.Any(p => MonitorDeviceProfileProperties.Contains(p))) + ) || + (e.EntityType == typeof(User) && + (e.EventType == RepositoryMonitorEventType.Modified && e.ModifiedProperties.Any(p => MonitorUserProperties.Contains(p))) + ) + ) + .DelayBuffer(TimeSpan.FromMilliseconds(500)) + .Subscribe(RepositoryEvent); + } + + private static void RepositoryEvent(IEnumerable e) + { + List deviceSerialNumbers = new List(); + List userIds = new List(); + + using (DiscoDataContext Database = new DiscoDataContext()) + { + foreach (var i in e) + { + if (i.EntityType == typeof(Job)) + { + if (i.EventType == RepositoryMonitorEventType.Modified && + i.ModifiedProperties.Contains("DeviceSerialNumber")) + { + var p = i.GetPreviousPropertyValue("DeviceSerialNumber"); + if (p != null) + deviceSerialNumbers.Add(p); + } + + var j = (Job)i.Entity; + if (j.DeviceSerialNumber != null) + deviceSerialNumbers.Add(j.DeviceSerialNumber); + } + else if (i.EntityType == typeof(JobMetaNonWarranty)) + { + var jmnw = (JobMetaNonWarranty)i.Entity; + + if (jmnw.Job != null) + { + if (jmnw.Job.DeviceSerialNumber != null) + deviceSerialNumbers.Add(jmnw.Job.DeviceSerialNumber); + } + else + { + var sn = Database.Jobs.Where(j => j.Id == jmnw.JobId).Select(j => j.DeviceSerialNumber).FirstOrDefault(); + if (sn != null) + deviceSerialNumbers.Add(sn); + } + } + else if (i.EntityType == typeof(Device)) + { + var d = (Device)i.Entity; + deviceSerialNumbers.Add(d.SerialNumber); + + if (i.EventType == RepositoryMonitorEventType.Modified && + i.ModifiedProperties.Contains("AssignedUserId")) + { + var p = i.GetPreviousPropertyValue("AssignedUserId"); + if (p != null) + userIds.Add(p); + } + } + else if (i.EntityType == typeof(DeviceProfile)) + { + var dp = (DeviceProfile)i.Entity; + + deviceSerialNumbers.AddRange( + Database.Jobs + .Where(j => !j.ClosedDate.HasValue && j.Device.DeviceProfileId == dp.Id) + .Select(j => j.DeviceSerialNumber) + ); + } + else if (i.EntityType == typeof(User)) + { + var u = (User)i.Entity; + + deviceSerialNumbers.AddRange( + Database.Jobs + .Where(j => !j.ClosedDate.HasValue && j.Device.AssignedUserId == u.UserId) + .Select(j => j.DeviceSerialNumber) + ); + } + } + + deviceSerialNumbers = deviceSerialNumbers.Distinct().ToList(); + + // Determine Held Devices for Users + userIds.AddRange( + Database.Devices + .Where(d => d.AssignedUserId != null && deviceSerialNumbers.Contains(d.SerialNumber)) + .Select(d => d.AssignedUserId) + ); + userIds = userIds.Distinct().ToList(); + + + // Notify Held Devices + HeldDevices.BroadcastUpdates(Database, deviceSerialNumbers); + + // Notify Held Devices for Users + HeldDevicesForUsers.BroadcastUpdates(Database, userIds); + } + } + + internal static void BroadcastUpdates(DiscoDataContext Database, List DeviceSerialNumbers) + { + var jobs = Database.Jobs.Where(j => DeviceSerialNumbers.Contains(j.DeviceSerialNumber)); + + var items = GetHeldDevices(jobs).ToDictionary(i => i.DeviceSerialNumber, StringComparer.OrdinalIgnoreCase); + + for (int skipAmount = 0; skipAmount < DeviceSerialNumbers.Count; skipAmount = skipAmount + 30) + { + var updates = DeviceSerialNumbers + .Skip(skipAmount).Take(30) + .ToDictionary(dsn => dsn, + dsn => { + IHeldDeviceItem item; + items.TryGetValue(dsn, out item); + return item; + }); + + NoticeboardUpdatesHub.HubContext.Clients + .Group(HeldDevices.Name) + .updateHeldDevice(updates); + } + } + + private static IEnumerable GetHeldDevices(IQueryable query) + { + var jobs = query + .Where(j => + !j.ClosedDate.HasValue && + j.DeviceSerialNumber != null && + ((j.DeviceHeld.HasValue && !j.DeviceReturnedDate.HasValue) || j.WaitingForUserAction.HasValue) + ) + .SelectHeldDeviceItems() + .GroupBy(j => j.DeviceSerialNumber); + + foreach (var job in jobs.ToList()) + { + if (job.Any(j => j.WaitingForUserAction)) + { + var item = job.Where(j => j.WaitingForUserAction).OrderBy(j => j.WaitingForUserActionSince).First(); + + yield return item; + } + else + { + if (job.All(j => j.ReadyForReturn)) + { + var item = job.OrderByDescending(j => j.ReadyForReturnSince).First(); + + yield return item; + } + else + { + var item = job.Where(j => !j.ReadyForReturn).OrderByDescending(j => j.EstimatedReturnTime).First(); + + yield return item; + } + } + } + } + + public static IEnumerable GetHeldDevices(DiscoDataContext Database) + { + return GetHeldDevices(Database.Jobs); + } + public static IHeldDeviceItem GetHeldDevice(DiscoDataContext Database, string DeviceSerialNumber) + { + return GetHeldDevices(Database.Jobs.Where(j => j.DeviceSerialNumber == DeviceSerialNumber)).FirstOrDefault(); + } + + internal static IEnumerable SelectHeldDeviceItems(this IQueryable jobs) + { + return HeldDeviceItem.FromJobs(jobs); + } + } +} diff --git a/Disco.Services/Jobs/Noticeboards/HeldDevicesForUsers.cs b/Disco.Services/Jobs/Noticeboards/HeldDevicesForUsers.cs new file mode 100644 index 00000000..301b63d0 --- /dev/null +++ b/Disco.Services/Jobs/Noticeboards/HeldDevicesForUsers.cs @@ -0,0 +1,92 @@ +using Disco.Data.Repository; +using Disco.Models.Repository; +using Disco.Models.Services.Jobs.Noticeboards; +using Disco.Services.Interop.ActiveDirectory; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Disco.Services.Jobs.Noticeboards +{ + public class HeldDevicesForUsers + { + public const string Name = "HeldDevicesForUsers"; + + // NOTE: Calculation for updates is performed by HeldDevices to avoid duplication + + internal static void BroadcastUpdates(DiscoDataContext Database, List UserIds) + { + var jobs = Database.Devices.Where(d => UserIds.Contains(d.AssignedUserId)).SelectMany(d => d.Jobs); + + var items = GetHeldDevicesForUsers(jobs).ToDictionary(i => i.UserId, StringComparer.OrdinalIgnoreCase); + + for (int skipAmount = 0; skipAmount < UserIds.Count; skipAmount = skipAmount + 30) + { + var updates = UserIds + .Skip(skipAmount).Take(30) + .ToDictionary(userId => userId, + userId => + { + IHeldDeviceItem item; + items.TryGetValue(userId, out item); + return item; + }); + + NoticeboardUpdatesHub.HubContext.Clients + .Group(HeldDevicesForUsers.Name) + .updateHeldDeviceForUser(updates); + } + } + + private static IEnumerable GetHeldDevicesForUsers(IQueryable query) + { + var jobs = query + .Where(j => + !j.ClosedDate.HasValue && + j.DeviceSerialNumber != null && + j.Device.AssignedUserId != null && + ((j.DeviceHeld.HasValue && !j.DeviceReturnedDate.HasValue) || j.WaitingForUserAction.HasValue) + ) + .SelectHeldDeviceItems() + .GroupBy(j => j.UserId); + + foreach (var job in jobs.ToList()) + { + if (job.Any(j => j.WaitingForUserAction)) + { + var item = job.Where(j => j.WaitingForUserAction).OrderBy(j => j.WaitingForUserActionSince).First(); + + yield return item; + } + else + { + if (job.All(j => j.ReadyForReturn)) + { + var item = job.OrderByDescending(j => j.ReadyForReturnSince).First(); + + yield return item; + } + else + { + var item = job.Where(j => !j.ReadyForReturn).OrderByDescending(j => j.EstimatedReturnTime).First(); + + yield return item; + } + } + } + } + + public static IEnumerable GetHeldDevicesForUsers(DiscoDataContext Database) + { + return GetHeldDevicesForUsers(Database.Jobs); + } + public static IHeldDeviceItem GetHeldDeviceForUsers(DiscoDataContext Database, string UserId) + { + var split = UserExtensions.SplitUserId(UserId); + if (split.Item1 == null) + UserId = string.Format(@"{0}\{1}", ActiveDirectory.Context.PrimaryDomain.NetBiosName, UserId); + + return GetHeldDevicesForUsers(Database.Devices.Where(d => d.AssignedUserId == UserId).SelectMany(d => d.Jobs)).FirstOrDefault(); + } + } +} diff --git a/Disco.Services/Jobs/Noticeboards/NoticeboardUpdatesHub.cs b/Disco.Services/Jobs/Noticeboards/NoticeboardUpdatesHub.cs new file mode 100644 index 00000000..c1c537d7 --- /dev/null +++ b/Disco.Services/Jobs/Noticeboards/NoticeboardUpdatesHub.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNet.SignalR; +using Microsoft.AspNet.SignalR.Hubs; +using System; +using System.Threading.Tasks; + +namespace Disco.Services.Jobs.Noticeboards +{ + [HubName("noticeboardUpdates")] // Public + public class NoticeboardUpdatesHub : Hub + { + public static IHubContext HubContext { get; private set; } + + static NoticeboardUpdatesHub() + { + HubContext = GlobalHost.ConnectionManager.GetHubContext(); + } + + public override Task OnConnected() + { + var noticeboardId = Context.QueryString["Noticeboard"]; + + if (string.IsNullOrWhiteSpace(noticeboardId)) + throw new ArgumentNullException("Noticeboard"); + + switch (noticeboardId) + { + case HeldDevicesForUsers.Name: + Groups.Add(Context.ConnectionId, HeldDevicesForUsers.Name); + break; + case HeldDevices.Name: + Groups.Add(Context.ConnectionId, HeldDevices.Name); + break; + default: + throw new ArgumentException("Invalid Noticeboard Specified", "Noticeboard"); + } + + return base.OnConnected(); + } + } +} diff --git a/Disco.Services/Logging/LogContext.cs b/Disco.Services/Logging/LogContext.cs index 4be5a8f1..ce9aa7ea 100644 --- a/Disco.Services/Logging/LogContext.cs +++ b/Disco.Services/Logging/LogContext.cs @@ -70,7 +70,7 @@ namespace Disco.Services.Logging } } - private static void InitalizeDatabase(Targets.LogPersistContext LogDatabase) + private static void InitalizeDatabase(Persistance.LogPersistContext LogDatabase) { // Add Modules var existingModules = LogDatabase.Modules.Include("EventTypes").ToDictionary(m => m.Id); @@ -183,7 +183,7 @@ namespace Disco.Services.Logging if (!File.Exists(logPath)) { // Create Database - using (var context = new Targets.LogPersistContext(connectionString)) + using (var context = new Persistance.LogPersistContext(connectionString)) { context.Database.CreateIfNotExists(); } @@ -191,7 +191,7 @@ namespace Disco.Services.Logging // Add Modules/Event Types InitalizeModules(); - using (var context = new Targets.LogPersistContext(connectionString)) + using (var context = new Persistance.LogPersistContext(connectionString)) { InitalizeDatabase(context); } @@ -211,7 +211,7 @@ namespace Disco.Services.Logging sqlCeCSB.DataSource = yesterdaysLogPath; var connectionString = sqlCeCSB.ToString(); int logCount; - using (var context = new Targets.LogPersistContext(connectionString)) + using (var context = new Persistance.LogPersistContext(connectionString)) { logCount = context.Events.Where(e => !(e.ModuleId == 0 && e.EventTypeId == 100)).Count(); if (logCount == 0) @@ -267,7 +267,7 @@ namespace Disco.Services.Logging var eventTimestamp = DateTime.Now; if (eventType.UseLive) { - Targets.LogLiveContext.Broadcast(logModule, eventType, eventTimestamp, Args); + LogNotificationsHub.BroadcastLog(logModule, eventType, eventTimestamp, Args); } if (eventType.UsePersist) { @@ -276,7 +276,7 @@ namespace Disco.Services.Logging { args = JsonConvert.SerializeObject(Args); } - using (var context = new Targets.LogPersistContext(PersistantStoreConnectionString)) + using (var context = new Persistance.LogPersistContext(PersistantStoreConnectionString)) { var e = new Models.LogEvent() { diff --git a/Disco.Services/Logging/LogNotificationsHub.cs b/Disco.Services/Logging/LogNotificationsHub.cs new file mode 100644 index 00000000..426425df --- /dev/null +++ b/Disco.Services/Logging/LogNotificationsHub.cs @@ -0,0 +1,67 @@ +using Disco.Services.Authorization; +using Disco.Services.Logging.Models; +using Disco.Services.Web.Signalling; +using Microsoft.AspNet.SignalR; +using Microsoft.AspNet.SignalR.Hubs; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Disco.Services.Logging +{ + [HubName("logNotifications"), DiscoHubAuthorize(Claims.Config.Logging.Show)] + public class LogNotificationsHub : Hub + { + private const string NotificationsModulePrefix = "Logging_"; + public const string AllLoggingNotification = NotificationsModulePrefix + "_ALL"; + + public override Task OnConnected() + { + var logModules = Context.QueryString["LogModules"]; + if (!string.IsNullOrWhiteSpace(logModules) && logModules.Length > 0) + { + foreach (var modules in ValidLogModuleGroupNames(logModules)) + Groups.Add(Context.ConnectionId, modules); + } + + return base.OnConnected(); + } + + internal static void BroadcastLog(LogBase logModule, LogEventType eventType, DateTime Timestamp, params object[] Arguments) + { + var message = LogLiveEvent.Create(logModule, eventType, Timestamp, Arguments); + + var connectionManager = GlobalHost.ConnectionManager; + var context = connectionManager.GetHubContext(); + var targets = new List { AllLoggingNotification, NotificationsModulePrefix + logModule.ModuleName }; + context.Clients.Groups(targets).receiveLog(message); + } + + private IEnumerable ValidLogModuleGroupNames(string ModuleNames) + { + if (string.IsNullOrWhiteSpace(ModuleNames)) + yield break; + + var names = ModuleNames.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + + foreach (var name in names) + { + // Special Case: __ALL + if (name.Equals(AllLoggingNotification, StringComparison.OrdinalIgnoreCase)) + { + yield return AllLoggingNotification; + } + else + { + var module = LogContext.LogModules.Values.FirstOrDefault(m => m.ModuleName.Equals(name, StringComparison.OrdinalIgnoreCase)); + + if (module == null) + throw new ArgumentException(string.Format("Invalid Module Name specified: {0}", name), "ModuleNames"); + + yield return NotificationsModulePrefix + module.ModuleName; + } + } + } + } +} diff --git a/Disco.Services/Logging/Targets/LogPersistContext.cs b/Disco.Services/Logging/Persistance/LogPersistContext.cs similarity index 93% rename from Disco.Services/Logging/Targets/LogPersistContext.cs rename to Disco.Services/Logging/Persistance/LogPersistContext.cs index 4f305f96..dae65db3 100644 --- a/Disco.Services/Logging/Targets/LogPersistContext.cs +++ b/Disco.Services/Logging/Persistance/LogPersistContext.cs @@ -5,7 +5,7 @@ using System.Text; using System.Data.Entity; using System.Data.Entity.Infrastructure; -namespace Disco.Services.Logging.Targets +namespace Disco.Services.Logging.Persistance { public class LogPersistContext : DbContext { diff --git a/Disco.Services/Logging/Targets/LogPersistContextInitializer.cs b/Disco.Services/Logging/Persistance/LogPersistContextInitializer.cs similarity index 90% rename from Disco.Services/Logging/Targets/LogPersistContextInitializer.cs rename to Disco.Services/Logging/Persistance/LogPersistContextInitializer.cs index 1f56cb26..5dc12cc5 100644 --- a/Disco.Services/Logging/Targets/LogPersistContextInitializer.cs +++ b/Disco.Services/Logging/Persistance/LogPersistContextInitializer.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Disco.Services.Logging.Targets +namespace Disco.Services.Logging.Persistance { public class LogPersistContextInitializer : IDatabaseInitializer { diff --git a/Disco.Services/Logging/ReadLogContext.cs b/Disco.Services/Logging/ReadLogContext.cs index 5d8093b9..ea6dcbc5 100644 --- a/Disco.Services/Logging/ReadLogContext.cs +++ b/Disco.Services/Logging/ReadLogContext.cs @@ -1,13 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Disco.Services.Logging.Targets; -using Disco.Data.Repository; -using System.IO; -using System.Text.RegularExpressions; -using System.Data.SqlServerCe; +using Disco.Data.Repository; using Disco.Services.Logging.Models; +using Disco.Services.Logging.Persistance; +using System; +using System.Collections.Generic; +using System.Data.SqlServerCe; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; namespace Disco.Services.Logging { @@ -45,7 +44,7 @@ namespace Disco.Services.Logging var logModules = LogContext.LogModules; - using (var context = new Targets.LogPersistContext(sqlCeCSB.ToString())) + using (var context = new LogPersistContext(sqlCeCSB.ToString())) { var query = this.BuildQuery(context, logFile.Item2, results.Count); IEnumerable queryResults = query; // Run the Query diff --git a/Disco.Services/Logging/Targets/LogLiveContext.cs b/Disco.Services/Logging/Targets/LogLiveContext.cs deleted file mode 100644 index d96125c5..00000000 --- a/Disco.Services/Logging/Targets/LogLiveContext.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Disco.Services.Logging.Models; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Disco.Services.Logging.Targets -{ - public static class LogLiveContext - { - public delegate void LogBroadcastEvent(LogBase logModule, LogEventType eventType, DateTime Timestamp, params object[] Arguments); - public static event LogBroadcastEvent LogBroadcast; - - internal static void Broadcast(LogBase logModule, LogEventType eventType, DateTime Timestamp, params object[] Arguments) - { - if (LogBroadcast != null) - LogBroadcast.Invoke(logModule, eventType, Timestamp, Arguments); - } - } -} diff --git a/Disco.Services/Plugins/Features/UIExtension/Results/PluginResourceCssResult.cs b/Disco.Services/Plugins/Features/UIExtension/Results/PluginResourceCssResult.cs index 1d14eb95..7aadd20d 100644 --- a/Disco.Services/Plugins/Features/UIExtension/Results/PluginResourceCssResult.cs +++ b/Disco.Services/Plugins/Features/UIExtension/Results/PluginResourceCssResult.cs @@ -21,11 +21,11 @@ namespace Disco.Services.Plugins.Features.UIExtension.Results this._resource = Resource; this._resourceUrl = Source.PluginManifest.WebResourceUrl(Resource); - var deferredBundles = HttpContext.Current.Items[Bundle.UIExtensionCssKey] as List; + var deferredBundles = HttpContext.Current.Items[BundleTable.UIExtensionCssKey] as List; if (deferredBundles == null) { deferredBundles = new List(); - HttpContext.Current.Items[Bundle.UIExtensionCssKey] = deferredBundles; + HttpContext.Current.Items[BundleTable.UIExtensionCssKey] = deferredBundles; } if (!deferredBundles.Contains(this._resourceUrl)) deferredBundles.Add(this._resourceUrl); diff --git a/Disco.Services/Plugins/Features/UIExtension/Results/PluginResourceScriptResult.cs b/Disco.Services/Plugins/Features/UIExtension/Results/PluginResourceScriptResult.cs index 4836bb4e..a7c18269 100644 --- a/Disco.Services/Plugins/Features/UIExtension/Results/PluginResourceScriptResult.cs +++ b/Disco.Services/Plugins/Features/UIExtension/Results/PluginResourceScriptResult.cs @@ -23,11 +23,11 @@ namespace Disco.Services.Plugins.Features.UIExtension.Results if (this._placeInPageHead) { - var deferredBundles = HttpContext.Current.Items[Bundle.UIExtensionScriptsKey] as List; + var deferredBundles = HttpContext.Current.Items[BundleTable.UIExtensionScriptsKey] as List; if (deferredBundles == null) { deferredBundles = new List(); - HttpContext.Current.Items[Bundle.UIExtensionScriptsKey] = deferredBundles; + HttpContext.Current.Items[BundleTable.UIExtensionScriptsKey] = deferredBundles; } if (!deferredBundles.Contains(this._resourceUrl)) deferredBundles.Add(this._resourceUrl); diff --git a/Disco.Services/Plugins/PluginExtensions.cs b/Disco.Services/Plugins/PluginExtensions.cs index e5081398..8d5b8294 100644 --- a/Disco.Services/Plugins/PluginExtensions.cs +++ b/Disco.Services/Plugins/PluginExtensions.cs @@ -97,12 +97,12 @@ namespace Disco.Services.Plugins public static void IncludeStyleSheetResource(this HttpContextBase Context, string Resource, PluginManifest manifest) { var resourceUrl = manifest.WebResourceUrl(Resource); - - var deferredBundles = Context.Items[Bundle.UIExtensionCssKey] as List; + + var deferredBundles = Context.Items[BundleTable.UIExtensionCssKey] as List; if (deferredBundles == null) { deferredBundles = new List(); - HttpContext.Current.Items[Bundle.UIExtensionCssKey] = deferredBundles; + HttpContext.Current.Items[BundleTable.UIExtensionCssKey] = deferredBundles; } if (!deferredBundles.Contains(resourceUrl)) deferredBundles.Add(resourceUrl); @@ -111,11 +111,11 @@ namespace Disco.Services.Plugins { var resourcePath = manifest.WebResourceUrl(Resource); - var deferredBundles = Context.Items[Bundle.UIExtensionScriptsKey] as List; + var deferredBundles = Context.Items[BundleTable.UIExtensionScriptsKey] as List; if (deferredBundles == null) { deferredBundles = new List(); - HttpContext.Current.Items[Bundle.UIExtensionScriptsKey] = deferredBundles; + HttpContext.Current.Items[BundleTable.UIExtensionScriptsKey] = deferredBundles; } if (!deferredBundles.Contains(resourcePath)) deferredBundles.Add(resourcePath); @@ -223,7 +223,9 @@ namespace Disco.Services.Plugins var routeValues = new RouteValueDictionary(new { PluginId = manifest.Id, PluginAction = PluginAction }); string pluginActionUrl = UrlHelper.GenerateUrl("Plugin", null, null, routeValues, RouteTable.Routes, ViewPage.ViewContext.RequestContext, false); +#pragma warning disable 618 return ViewPage.FormHelper(pluginActionUrl, method, htmlAttributes); +#pragma warning restore 618 } [Obsolete("Inherit ViewPages from 'Disco.Services.Plugins.WebViewPage' instead.")] public static MvcForm DiscoPluginActionBeginForm(this WebViewPage ViewPage, string PluginAction, FormMethod method) @@ -302,11 +304,11 @@ namespace Disco.Services.Plugins HtmlString pluginResourceUrlHtml = new HtmlString(pluginResourceUrl); - var deferredBundles = RequestContext.HttpContext.Items[Bundle.UIExtensionCssKey] as List; + var deferredBundles = RequestContext.HttpContext.Items[BundleTable.UIExtensionCssKey] as List; if (deferredBundles == null) { deferredBundles = new List(); - HttpContext.Current.Items[Bundle.UIExtensionCssKey] = deferredBundles; + HttpContext.Current.Items[BundleTable.UIExtensionCssKey] = deferredBundles; } if (!deferredBundles.Contains(pluginResourceUrlHtml)) deferredBundles.Add(pluginResourceUrlHtml); diff --git a/Disco.Services/Tasks/IScheduledTaskBasicStatus.cs b/Disco.Services/Tasks/IScheduledTaskStatus.cs similarity index 52% rename from Disco.Services/Tasks/IScheduledTaskBasicStatus.cs rename to Disco.Services/Tasks/IScheduledTaskStatus.cs index d00a0dd6..26586395 100644 --- a/Disco.Services/Tasks/IScheduledTaskBasicStatus.cs +++ b/Disco.Services/Tasks/IScheduledTaskStatus.cs @@ -6,12 +6,22 @@ using System.Threading.Tasks; namespace Disco.Services.Tasks { - public interface IScheduledTaskBasicStatus + public interface IScheduledTaskStatus { byte Progress { get; } string CurrentProcess { get; } string CurrentDescription { get; } + bool IgnoreCurrentProcessChanges { get; set; } + bool IgnoreCurrentDescription { get; set; } + double ProgressMultiplier { get; set; } + byte ProgressOffset { get; set; } + + string FinishedMessage { get; } + string FinishedUrl { get; } + + Exception TaskException { get; } + void UpdateStatus(byte Progress); void UpdateStatus(double Progress); void UpdateStatus(string CurrentDescription); @@ -19,5 +29,13 @@ namespace Disco.Services.Tasks void UpdateStatus(double Progress, string CurrentDescription); void UpdateStatus(byte Progress, string CurrentProcess, string CurrentDescription); void UpdateStatus(double Progress, string CurrentProcess, string CurrentDescription); + + void SetFinishedUrl(string FinishedUrl); + void SetFinishedMessage(string FinishedMessage); + void Finished(); + void Finished(string FinishedMessage); + void Finished(string FinishedMessage, string FinishedUrl); + + void SetTaskException(Exception TaskException); } } diff --git a/Disco.Services/Tasks/ScheduledTask.cs b/Disco.Services/Tasks/ScheduledTask.cs index cd5f0623..0ca9f8bd 100644 --- a/Disco.Services/Tasks/ScheduledTask.cs +++ b/Disco.Services/Tasks/ScheduledTask.cs @@ -16,8 +16,8 @@ namespace Disco.Services.Tasks public virtual bool CancelInitiallySupported { get { return true; } } public virtual bool SingleInstanceTask { get { return true; } } - public virtual bool IsSilent { get { return false; } } public virtual bool LogExceptionsOnly { get { return false; } } + public abstract string TaskName { get; } protected abstract void ExecuteTask(); diff --git a/Disco.Services/Tasks/ScheduledTaskMockStatus.cs b/Disco.Services/Tasks/ScheduledTaskMockStatus.cs index b1561e4a..8adef281 100644 --- a/Disco.Services/Tasks/ScheduledTaskMockStatus.cs +++ b/Disco.Services/Tasks/ScheduledTaskMockStatus.cs @@ -6,19 +6,30 @@ using System.Threading.Tasks; namespace Disco.Services.Tasks { - public class ScheduledTaskMockStatus : IScheduledTaskBasicStatus + public class ScheduledTaskMockStatus : IScheduledTaskStatus { - private byte progress; - private string currentProcess; - private string currentDescription; + public byte Progress { get; set; } + public string CurrentProcess { get; set; } + public string CurrentDescription { get; set; } - public byte Progress { get { return this.progress; } } - public string CurrentProcess { get { return this.currentProcess; } } - public string CurrentDescription { get { return this.currentDescription; } } + public bool IgnoreCurrentProcessChanges { get; set; } + public bool IgnoreCurrentDescription { get; set; } + public double ProgressMultiplier { get; set; } + public byte ProgressOffset { get; set; } + + public string FinishedMessage { get; set; } + public string FinishedUrl { get; set; } + + public Exception TaskException { get; set; } + + private byte CalculateProgressValue(byte Progress) + { + return (byte)((Progress * this.ProgressMultiplier) + this.ProgressOffset); + } public void UpdateStatus(byte Progress) { - this.progress = Progress; + this.Progress = CalculateProgressValue(Progress); } public void UpdateStatus(double Progress) { @@ -26,12 +37,14 @@ namespace Disco.Services.Tasks } public void UpdateStatus(string CurrentDescription) { - this.currentDescription = CurrentDescription; + if (!IgnoreCurrentDescription) + this.CurrentDescription = CurrentDescription; } public void UpdateStatus(byte Progress, string CurrentDescription) { - this.progress = Progress; - this.currentDescription = CurrentDescription; + this.Progress = CalculateProgressValue(Progress); + if (!IgnoreCurrentDescription) + this.CurrentDescription = CurrentDescription; } public void UpdateStatus(double Progress, string CurrentDescription) { @@ -39,15 +52,46 @@ namespace Disco.Services.Tasks } public void UpdateStatus(byte Progress, string CurrentProcess, string CurrentDescription) { - this.progress = Progress; - this.currentProcess = CurrentProcess; - this.currentDescription = CurrentDescription; + this.Progress = CalculateProgressValue(Progress); + if (!IgnoreCurrentProcessChanges) + this.CurrentProcess = CurrentProcess; + if (!IgnoreCurrentDescription) + this.CurrentDescription = CurrentDescription; } public void UpdateStatus(double Progress, string CurrentProcess, string CurrentDescription) { UpdateStatus((byte)Progress, CurrentProcess, CurrentDescription); } + public void SetFinishedUrl(string FinishedUrl) + { + this.FinishedUrl = FinishedUrl; + } + public void SetFinishedMessage(string FinishedMessage) + { + this.FinishedMessage = FinishedMessage; + } + public void Finished() + { + Finished(this.FinishedMessage, this.FinishedUrl); + } + public void Finished(string FinishedMessage) + { + Finished(FinishedMessage, this.FinishedUrl); + } + public void Finished(string FinishedMessage, string FinishedUrl) + { + this.FinishedMessage = FinishedMessage; + this.FinishedUrl = FinishedUrl; + } + + public void SetTaskException(Exception TaskException) + { + this.TaskException = TaskException; + } + + + public static ScheduledTaskMockStatus Create() { return new ScheduledTaskMockStatus(); diff --git a/Disco.Services/Tasks/ScheduledTaskNotificationsHub.cs b/Disco.Services/Tasks/ScheduledTaskNotificationsHub.cs new file mode 100644 index 00000000..570eba99 --- /dev/null +++ b/Disco.Services/Tasks/ScheduledTaskNotificationsHub.cs @@ -0,0 +1,89 @@ +using Disco.Services.Web.Signalling; +using Microsoft.AspNet.SignalR; +using Microsoft.AspNet.SignalR.Hubs; +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Linq; + +namespace Disco.Services.Tasks +{ + using System.Reactive.Subjects; + using ChangedItem = KeyValuePair; + + [HubName("scheduledTaskNotifications"), DiscoHubAuthorize()] + public class ScheduledTaskNotificationsHub : Hub + { + private const string NotificationsPrefix = "Logging_"; + private static Subject>> taskUpdatesStream = new Subject>>(); + private static IDisposable taskUpdatesStreamSubscription; + + internal static void Initialize() + { + if (taskUpdatesStreamSubscription == null) + { + lock (taskUpdatesStream) + { + if (taskUpdatesStreamSubscription == null) + { + taskUpdatesStreamSubscription = taskUpdatesStream + .DelayBuffer(TimeSpan.FromMilliseconds(200)) + .Subscribe(BroadcastBufferedEvents); + } + } + } + } + + internal static void PublishEvent(string TaskSessionId, IEnumerable ChangedItems) + { + taskUpdatesStream.OnNext(Tuple.Create(TaskSessionId, ChangedItems)); + } + + public override System.Threading.Tasks.Task OnConnected() + { + var taskSessionId = Context.QueryString["TaskSessionId"]; + + if (string.IsNullOrEmpty(taskSessionId)) + throw new ArgumentNullException("TaskSessionId"); + + var status = ScheduledTasks.GetTaskStatus(taskSessionId); + + if (status == null) + throw new ArgumentException("Invalid ScheduledTask SessionId", "TaskSessionId"); + + // Send Status: + var currentStatus = ScheduledTaskStatusLive.FromScheduledTaskStatus(status, null); + Clients.Caller.initializeTaskStatus(currentStatus); + + // Add to Group + var groupName = NotificationsPrefix + taskSessionId; + Groups.Add(Context.ConnectionId, groupName); + + return base.OnConnected(); + } + + private static void BroadcastBufferedEvents(IEnumerable>> Events) + { + var connectionManager = GlobalHost.ConnectionManager; + var context = connectionManager.GetHubContext(); + + var taskStatusEvents = Events.GroupBy(e => e.Item1).Select(taskEventsGroup => + { + Dictionary changes = new Dictionary(); + + foreach (var changeEvents in taskEventsGroup.Select(taskEvents => taskEvents.Item2)) + foreach (var changeEvent in changeEvents) + changes[changeEvent.Key] = changeEvent.Value; + + return Tuple.Create(taskEventsGroup.Key, changes); + }); + + foreach (var taskStatusEvent in taskStatusEvents) + { + var groupName = NotificationsPrefix + taskStatusEvent.Item1; + context.Clients.Group(groupName).updateTaskStatus(taskStatusEvent.Item2); + } + } + + } +} diff --git a/Disco.Services/Tasks/ScheduledTaskStatus.cs b/Disco.Services/Tasks/ScheduledTaskStatus.cs index 2b5fb737..3d095105 100644 --- a/Disco.Services/Tasks/ScheduledTaskStatus.cs +++ b/Disco.Services/Tasks/ScheduledTaskStatus.cs @@ -9,7 +9,9 @@ using System.Threading.Tasks; namespace Disco.Services.Tasks { - public class ScheduledTaskStatus : IScheduledTaskBasicStatus + using ChangedItem = KeyValuePair; + + public class ScheduledTaskStatus : IScheduledTaskStatus { #region Backing Fields @@ -52,6 +54,11 @@ namespace Disco.Services.Tasks public string CurrentProcess { get { return this._currentProcess; } } public string CurrentDescription { get { return this._currentDescription; } } + public bool IgnoreCurrentProcessChanges { get; set; } + public bool IgnoreCurrentDescription { get; set; } + public double ProgressMultiplier { get; set; } + public byte ProgressOffset { get; set; } + public Exception TaskException { get { return this._taskException; } } public bool CancelSupported { get { return this._cancelSupported; } } public bool IsCanceling { get { return this._isCanceling; } } @@ -73,21 +80,13 @@ namespace Disco.Services.Tasks } } - public Task CompletionTask - { - get - { - return _tcs.Task; - } - } + public Task CompletionTask { get { return _tcs.Task; } } #endregion #region Events - public delegate void UpdatedBroadcastEvent(ScheduledTaskStatusLive SessionStatus); - public delegate void UpdatedEvent(ScheduledTaskStatus sender, string[] ChangedProperties); + public delegate void UpdatedEvent(ScheduledTaskStatus sender, KeyValuePair[] ChangedProperties); public delegate void CancelingEvent(ScheduledTaskStatus sender); - public static event UpdatedBroadcastEvent UpdatedBroadcast; public event UpdatedEvent Updated; public event CancelingEvent Canceling; #endregion @@ -112,10 +111,15 @@ namespace Disco.Services.Tasks } #region Progress Actions + private byte CalculateProgressValue(byte Progress) + { + return (byte)((Progress * this.ProgressMultiplier) + this.ProgressOffset); + } + public void UpdateStatus(byte Progress) { - this._progress = Progress; - UpdateTriggered(new string[] { "Progress" }); + this._progress = CalculateProgressValue(Progress); + UpdateTriggered("Progress", this._progress); } public void UpdateStatus(double Progress) { @@ -123,14 +127,27 @@ namespace Disco.Services.Tasks } public void UpdateStatus(string CurrentDescription) { - this._currentDescription = CurrentDescription; - UpdateTriggered(new string[] { "CurrentDescription" }); + if (!IgnoreCurrentDescription) + { + this._currentDescription = CurrentDescription; + UpdateTriggered("CurrentDescription", this._currentDescription); + } } public void UpdateStatus(byte Progress, string CurrentDescription) { - this._progress = Progress; - this._currentDescription = CurrentDescription; - UpdateTriggered(new string[] { "Progress", "CurrentDescription" }); + this._progress = CalculateProgressValue(Progress); + + var changedProperties = new List() { + new ChangedItem("Progress", Progress) + }; + + if (!IgnoreCurrentDescription) + { + this._currentDescription = CurrentDescription; + changedProperties.Add(new ChangedItem("CurrentDescription", CurrentDescription)); + } + + UpdateTriggered(changedProperties.ToArray()); } public void UpdateStatus(double Progress, string CurrentDescription) { @@ -138,10 +155,24 @@ namespace Disco.Services.Tasks } public void UpdateStatus(byte Progress, string CurrentProcess, string CurrentDescription) { - this._progress = Progress; - this._currentProcess = CurrentProcess; - this._currentDescription = CurrentDescription; - UpdateTriggered(new string[] { "Progress", "CurrentProcess", "CurrentDescription" }); + this._progress = CalculateProgressValue(Progress); + + var changedProperties = new List() { + new ChangedItem("Progress", Progress) + }; + + if (!IgnoreCurrentProcessChanges) + { + this._currentProcess = CurrentProcess; + changedProperties.Add(new ChangedItem("CurrentProcess", CurrentProcess)); + } + if (!IgnoreCurrentDescription) + { + this._currentDescription = CurrentDescription; + changedProperties.Add(new ChangedItem("CurrentDescription", CurrentDescription)); + } + + UpdateTriggered(changedProperties.ToArray()); } public void UpdateStatus(double Progress, string CurrentProcess, string CurrentDescription) { @@ -157,7 +188,7 @@ namespace Disco.Services.Tasks if (_cancelSupported) { // Cancelling this._isCanceling = true; - UpdateTriggered(new string[] { "IsCancelling" }); + UpdateTriggered("IsCancelling", true); if (this.Canceling != null) Canceling(this); return true; @@ -177,7 +208,7 @@ namespace Disco.Services.Tasks if (this._cancelSupported != CancelSupported) { this._cancelSupported = CancelSupported; - UpdateTriggered(new string[] { "CancelSupported" }); + UpdateTriggered("CancelSupported", CancelSupported); } } public void SetTaskException(Exception TaskException) @@ -185,7 +216,7 @@ namespace Disco.Services.Tasks if (this._taskException != TaskException) { this._taskException = TaskException; - UpdateTriggered(new string[] { "TaskException" }); + UpdateTriggered("TaskExceptionMessage", (this._taskException == null ? null : this._taskException.Message)); } } public void SetIsSilent(bool IsSilent) @@ -198,7 +229,7 @@ namespace Disco.Services.Tasks if (this._finishedUrl != FinishedUrl) { this._finishedUrl = FinishedUrl; - UpdateTriggered(new string[] { "FinishedUrl" }); + UpdateTriggered("FinishedUrl", FinishedUrl); } } public void SetFinishedMessage(string FinishedMessage) @@ -206,7 +237,7 @@ namespace Disco.Services.Tasks if (this._finishedMessage != FinishedMessage) { this._finishedMessage = FinishedMessage; - UpdateTriggered(new string[] { "FinishedMessage" }); + UpdateTriggered("FinishedMessage", FinishedMessage); } } public void SetNextScheduledTimestamp(DateTime? NextScheduledTimestamp) @@ -214,59 +245,62 @@ namespace Disco.Services.Tasks if (this._nextScheduledTimestamp != NextScheduledTimestamp) { this._nextScheduledTimestamp = NextScheduledTimestamp; - UpdateTriggered(new string[] { "NextScheduledTimestamp" }); + UpdateTriggered("NextScheduledTimestamp", NextScheduledTimestamp); } } public void Started() { - List changedProperties = new List() { "IsRunning", "StartedTimestamp" }; + var changedProperties = new List(); + // Change StartedTimestamp this._startedTimestamp = DateTime.Now; + changedProperties.Add(new ChangedItem("StartedTimestamp", this.StartedTimestamp)); + + if (this._finishedTimestamp != null) + { + this._finishedTimestamp = null; + changedProperties.Add(new ChangedItem("FinishedTimestamp", this._finishedTimestamp)); + } if (this._nextScheduledTimestamp != null) { this._nextScheduledTimestamp = null; - changedProperties.Add("NextScheduledTimestamp"); - } - if (this._finishedTimestamp != null) - { - this._finishedTimestamp = null; - changedProperties.Add("FinishedTimestamp"); + changedProperties.Add(new ChangedItem("NextScheduledTimestamp", this._nextScheduledTimestamp)); } + + changedProperties.Add(new ChangedItem("IsRunning", this.IsRunning)); + if (this._progress != 0) { this._progress = 0; - changedProperties.Add("Progress"); + changedProperties.Add(new ChangedItem("Progress", this._progress)); } if (this._currentProcess != "Starting") { this._currentProcess = "Starting"; - changedProperties.Add("CurrentProcess"); + changedProperties.Add(new ChangedItem("CurrentProcess", this._currentProcess)); } if (this._currentDescription != "Initializing Task for Execution") { this._currentDescription = "Initializing Task for Execution"; - changedProperties.Add("CurrentDescription"); + changedProperties.Add(new ChangedItem("CurrentDescription", this._currentDescription)); } if (this._taskException != null) { this._taskException = null; - changedProperties.Add("TaskException"); + changedProperties.Add(new ChangedItem("TaskExceptionMessage", (this._taskException == null ? null : this._taskException.Message))); } if (this._cancelSupported != this._cancelInitiallySupported) { this._cancelSupported = this._cancelInitiallySupported; - changedProperties.Add("CancelSupported"); - } - { - this._isCanceling = false; - changedProperties.Add("IsCanceling"); + changedProperties.Add(new ChangedItem("CancelSupported", this._cancelSupported)); } if (this._isCanceling) { this._isCanceling = false; - changedProperties.Add("IsCanceling"); + changedProperties.Add(new ChangedItem("IsCanceling", this._isCanceling)); } + UpdateTriggered(changedProperties.ToArray()); } public void Finished() @@ -279,26 +313,29 @@ namespace Disco.Services.Tasks } public void Finished(string FinishedMessage, string FinishedUrl) { - List changedProperties = new List() { "IsRunning", "FinishedTimestamp" }; + var changedProperties = new List(); this._finishedTimestamp = DateTime.Now; + changedProperties.Add(new ChangedItem("FinishedTimestamp", this._finishedTimestamp)); + changedProperties.Add(new ChangedItem("IsRunning", this.IsRunning)); if (FinishedMessage != this._finishedMessage) { this._finishedMessage = FinishedMessage; - changedProperties.Add("FinishedMessage"); + changedProperties.Add(new ChangedItem("FinishedMessage", this._finishedMessage)); } if (FinishedUrl != this._finishedUrl) { this._finishedUrl = FinishedUrl; - changedProperties.Add("FinishedUrl"); + changedProperties.Add(new ChangedItem("FinishedUrl", this._finishedUrl)); } if (this._isCanceling) { this._isCanceling = false; - changedProperties.Add("IsCanceling"); + changedProperties.Add(new ChangedItem("IsCanceling", this._isCanceling)); } + UpdateTriggered(changedProperties.ToArray()); } internal void Finally() @@ -311,53 +348,58 @@ namespace Disco.Services.Tasks this._tcs.Task.Dispose(); this._tcs = new TaskCompletionSource(); - List changedProperties = new List(); + var changedProperties = new List(); if (this._nextScheduledTimestamp != NextScheduledTimestamp) { this._nextScheduledTimestamp = NextScheduledTimestamp; - changedProperties.Add("NextScheduledTimestamp"); + changedProperties.Add(new ChangedItem("NextScheduledTimestamp", this._nextScheduledTimestamp)); } if (this._startedTimestamp != null) { this._startedTimestamp = null; - changedProperties.Add("StartedTimestamp"); + changedProperties.Add(new ChangedItem("StartedTimestamp", this._startedTimestamp)); } if (this._finishedTimestamp != null) { this._finishedTimestamp = null; - changedProperties.Add("FinishedTimestamp"); + changedProperties.Add(new ChangedItem("FinishedTimestamp", this._finishedTimestamp)); } if (this._finishedMessage != null) { this._finishedMessage = null; - changedProperties.Add("FinishedMessage"); + changedProperties.Add(new ChangedItem("FinishedMessage", this._finishedMessage)); } if (this._finishedUrl != null) { this._finishedUrl = null; - changedProperties.Add("FinishedUrl"); + changedProperties.Add(new ChangedItem("FinishedUrl", this._finishedUrl)); } if (this._progress != 0) { this._progress = 0; - changedProperties.Add("Progress"); + changedProperties.Add(new ChangedItem("Progress", this._progress)); } + this.ProgressMultiplier = 0; + this.ProgressOffset = 0; + this.IgnoreCurrentDescription = false; + this.IgnoreCurrentProcessChanges = false; + if (this._currentProcess != "Scheduled") { this._currentProcess = "Scheduled"; - changedProperties.Add("CurrentProcess"); + changedProperties.Add(new ChangedItem("CurrentProcess", this._currentProcess)); } if (this._currentDescription != "Scheduled Task for Execution") { this._currentDescription = "Scheduled Task for Execution"; - changedProperties.Add("CurrentDescription"); + changedProperties.Add(new ChangedItem("CurrentDescription", this._currentDescription)); } if (this._isCanceling) { this._isCanceling = false; - changedProperties.Add("IsCanceling"); + changedProperties.Add(new ChangedItem("IsCanceling", this._isCanceling)); } UpdateTriggered(changedProperties.ToArray()); } @@ -373,15 +415,20 @@ namespace Disco.Services.Tasks } #endregion - private void UpdateTriggered(string[] ChangedProperties) + private void UpdateTriggered(string ChangedProperty, object NewValue) + { + UpdateTriggered(new ChangedItem[] { new ChangedItem(ChangedProperty, NewValue) }); + } + + private void UpdateTriggered(params ChangedItem[] ChangedProperties) { this._statusVersion++; if (Updated != null) Updated(this, ChangedProperties); - if (!_isSilent && UpdatedBroadcast != null) - UpdatedBroadcast.Invoke(ScheduledTaskStatusLive.FromScheduledTaskStatus(this, ChangedProperties)); + if (!_isSilent) + ScheduledTaskNotificationsHub.PublishEvent(this.SessionId, ChangedProperties); } } } diff --git a/Disco.Services/Tasks/ScheduledTasks.cs b/Disco.Services/Tasks/ScheduledTasks.cs index 293b2211..f901894e 100644 --- a/Disco.Services/Tasks/ScheduledTasks.cs +++ b/Disco.Services/Tasks/ScheduledTasks.cs @@ -28,13 +28,16 @@ namespace Disco.Services.Tasks // Scheduled Cleanup ScheduledTaskCleanup.Schedule(_TaskScheduler); + ScheduledTaskNotificationsHub.Initialize(); + if (InitiallySchedule) { // Discover DiscoScheduledTask var appDomain = AppDomain.CurrentDomain; + var scheduledTasksHostAssemblyName = typeof(ScheduledTask).Assembly.GetName().Name; var scheduledTaskTypes = (from a in appDomain.GetAssemblies() - where !a.GlobalAssemblyCache && !a.IsDynamic + where !a.GlobalAssemblyCache && !a.IsDynamic && a.GetReferencedAssemblies().Any(ra => ra.Name == scheduledTasksHostAssemblyName) from type in a.GetTypes() where typeof(ScheduledTask).IsAssignableFrom(type) && !type.IsAbstract select type); diff --git a/Disco.Services/Users/UserExtensions.cs b/Disco.Services/Users/UserExtensions.cs index f4c888eb..7539e8a2 100644 --- a/Disco.Services/Users/UserExtensions.cs +++ b/Disco.Services/Users/UserExtensions.cs @@ -1,10 +1,6 @@ using Disco.Models.Repository; using Disco.Services.Interop.ActiveDirectory; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Disco.Services { @@ -15,6 +11,11 @@ namespace Disco.Services return u.Domain.Equals(ActiveDirectory.Context.PrimaryDomain.NetBiosName, StringComparison.OrdinalIgnoreCase); } + public static string ToStringFriendly(this User u) + { + return string.Format("{0} ({1})", u.DisplayName, u.FriendlyId()); + } + public static string FriendlyId(this User u) { return FriendlyUserId(u.UserId); diff --git a/Disco.Services/Users/UserUpdatesHub.cs b/Disco.Services/Users/UserUpdatesHub.cs new file mode 100644 index 00000000..9b6e2928 --- /dev/null +++ b/Disco.Services/Users/UserUpdatesHub.cs @@ -0,0 +1,86 @@ +using Disco.Data.Repository.Monitor; +using Disco.Services.Authorization; +using Disco.Services.Web.Signalling; +using Microsoft.AspNet.SignalR; +using Microsoft.AspNet.SignalR.Hubs; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Reactive.Linq; +using Disco.Models.Repository; +using Disco.Data.Repository; + +namespace Disco.Services.Users +{ + [HubName("userUpdates"), DiscoHubAuthorizeAll(Claims.User.Show, Claims.User.ShowAttachments)] + public class UserUpdatesHub : Hub + { + private const string UserPrefix = "User_"; + public static IHubContext HubContext { get; private set; } + + private static IDisposable RepositoryBeforeSubscription; + private static IDisposable RepositoryAfterSubscription; + + static UserUpdatesHub() + { + HubContext = GlobalHost.ConnectionManager.GetHubContext(); + + // Subscribe to Repository Monitor for Changes + RepositoryBeforeSubscription = RepositoryMonitor.StreamBeforeCommit + .Where(e => e.EntityType == typeof(UserAttachment) && e.EventType == RepositoryMonitorEventType.Deleted) + .Subscribe(RepositoryEventBefore); + RepositoryAfterSubscription = RepositoryMonitor.StreamAfterCommit + .Where(e => e.EntityType == typeof(UserAttachment) && e.EventType == RepositoryMonitorEventType.Added) + .Subscribe(RepositoryAfterEvent); + } + + private static string GroupName(string UserId) + { + return UserPrefix + UserId; + } + + public override Task OnConnected() + { + var userId = Context.QueryString["UserId"]; + + if (string.IsNullOrWhiteSpace(userId)) + throw new ArgumentNullException("UserId"); + + Groups.Add(Context.ConnectionId, GroupName(userId)); + + return base.OnConnected(); + } + + private static void RepositoryEventBefore(RepositoryMonitorEvent e) + { + if (e.EventType == RepositoryMonitorEventType.Deleted) + { + if (e.EntityType == typeof(UserAttachment)) + { + var repositoryAttachment = (UserAttachment)e.Entity; + string attachmentUserId; + + using (DiscoDataContext Database = new DiscoDataContext()) + attachmentUserId = Database.UserAttachments.Where(a => a.Id == repositoryAttachment.Id).Select(a => a.UserId).First(); + + HubContext.Clients.Group(GroupName(attachmentUserId)).removeAttachment(repositoryAttachment.Id); + } + } + } + + private static void RepositoryAfterEvent(RepositoryMonitorEvent e) + { + if (e.EventType == RepositoryMonitorEventType.Added) + { + if (e.EntityType == typeof(UserAttachment)) + { + var a = (UserAttachment)e.Entity; + + HubContext.Clients.Group(GroupName(a.UserId)).addAttachment(a.Id); + } + } + } + } +} diff --git a/Disco.Services/Web/Bundles/BundleExtensions.cs b/Disco.Services/Web/Bundles/BundleExtensions.cs index d1dfa7d5..303f5bb6 100644 --- a/Disco.Services/Web/Bundles/BundleExtensions.cs +++ b/Disco.Services/Web/Bundles/BundleExtensions.cs @@ -16,21 +16,21 @@ namespace Disco.Services.Web // Ensure 'App-Relative' Url: BundleUrl = BundleUrl.StartsWith("~/") ? BundleUrl : (BundleUrl.StartsWith("/") ? string.Concat("~", BundleUrl) : string.Concat("~/", BundleUrl)); - var deferredBundles = htmlHelper.ViewContext.HttpContext.Items[Bundle.DeferredKey] as List; + var deferredBundles = htmlHelper.ViewContext.HttpContext.Items[BundleTable.DeferredKey] as List; if (deferredBundles == null) { deferredBundles = new List(); - htmlHelper.ViewContext.HttpContext.Items[Bundle.DeferredKey] = deferredBundles; + htmlHelper.ViewContext.HttpContext.Items[BundleTable.DeferredKey] = deferredBundles; } if (!deferredBundles.Contains(BundleUrl)) deferredBundles.Add(BundleUrl); } public static HtmlString BundleRenderDeferred(this HtmlHelper htmlHelper) { - var deferredBundles = htmlHelper.ViewContext.HttpContext.Items[Bundle.DeferredKey] as List; + var deferredBundles = htmlHelper.ViewContext.HttpContext.Items[BundleTable.DeferredKey] as List; - var uiExtensionScripts = htmlHelper.ViewContext.HttpContext.Items[Bundle.UIExtensionScriptsKey] as List; - var uiExtensionCss = htmlHelper.ViewContext.HttpContext.Items[Bundle.UIExtensionCssKey] as List; + var uiExtensionScripts = htmlHelper.ViewContext.HttpContext.Items[BundleTable.UIExtensionScriptsKey] as List; + var uiExtensionCss = htmlHelper.ViewContext.HttpContext.Items[BundleTable.UIExtensionCssKey] as List; if (deferredBundles != null || uiExtensionScripts != null || uiExtensionCss != null) { diff --git a/Disco.Services/Web/Bundles/BundleHandler.cs b/Disco.Services/Web/Bundles/BundleHandler.cs index f4b6af82..c6b81d14 100644 --- a/Disco.Services/Web/Bundles/BundleHandler.cs +++ b/Disco.Services/Web/Bundles/BundleHandler.cs @@ -9,10 +9,10 @@ namespace Disco.Services.Web.Bundles { internal sealed class BundleHandler : IHttpHandler { - public Bundle RequestBundle { get; private set; } + public IBundle RequestBundle { get; private set; } public string BundleVirtualPath { get; private set; } - public BundleHandler(Bundle requestBundle, string bundleVirtualPath) + public BundleHandler(IBundle requestBundle, string bundleVirtualPath) { this.RequestBundle = requestBundle; this.BundleVirtualPath = bundleVirtualPath; diff --git a/Disco.Services/Web/Bundles/BundleTable.cs b/Disco.Services/Web/Bundles/BundleTable.cs index bee13e15..8b76234a 100644 --- a/Disco.Services/Web/Bundles/BundleTable.cs +++ b/Disco.Services/Web/Bundles/BundleTable.cs @@ -9,14 +9,18 @@ namespace Disco.Services.Web.Bundles { public static class BundleTable { - private static Dictionary _bundles; + public const string DeferredKey = "Bundles.Deferred"; + public const string UIExtensionScriptsKey = "Bundles.UIExtensionScripts"; + public const string UIExtensionCssKey = "Bundles.UIExtensionCss"; + + private static Dictionary _bundles; static BundleTable() { - _bundles = new Dictionary(); + _bundles = new Dictionary(); } - public static void Add(Bundle Bundle) + public static void Add(IBundle Bundle) { _bundles[Bundle.Url] = Bundle; } @@ -29,7 +33,7 @@ namespace Disco.Services.Web.Bundles } } - internal static Bundle GetBundleFor(string Url) + internal static IBundle GetBundleFor(string Url) { if (_bundles.ContainsKey(Url)) { diff --git a/Disco.Services/Web/Bundles/Bundle.cs b/Disco.Services/Web/Bundles/FileBundle.cs similarity index 91% rename from Disco.Services/Web/Bundles/Bundle.cs rename to Disco.Services/Web/Bundles/FileBundle.cs index 9afb511b..8ab65d98 100644 --- a/Disco.Services/Web/Bundles/Bundle.cs +++ b/Disco.Services/Web/Bundles/FileBundle.cs @@ -9,16 +9,13 @@ using System.Web; namespace Disco.Services.Web.Bundles { - public class Bundle + public class FileBundle : IBundle { - public const string DeferredKey = "Bundles.Deferred"; - public const string UIExtensionScriptsKey = "Bundles.UIExtensionScripts"; - public const string UIExtensionCssKey = "Bundles.UIExtensionCss"; - private DateTime? _FileLastModified { get; set; } private string _FileHash { get; set; } private string _VersionUrl { get; set; } + public bool RemapRequest { get { return true; } } public string Url { get; private set; } public string File { get; private set; } public string FileHash @@ -44,7 +41,7 @@ namespace Disco.Services.Web.Bundles } } - public Bundle(string Url, string File) + public FileBundle(string Url, string File) { if (string.IsNullOrWhiteSpace(Url)) throw new ArgumentNullException("Url"); @@ -117,7 +114,7 @@ namespace Disco.Services.Web.Bundles this._FileHash = string.Empty; } - internal void ProcessRequest(HttpContext context) + public void ProcessRequest(HttpContext context) { // Write Content Type context.Response.ContentType = this.ContentType; diff --git a/Disco.Services/Web/Bundles/IBundle.cs b/Disco.Services/Web/Bundles/IBundle.cs new file mode 100644 index 00000000..87ecf75d --- /dev/null +++ b/Disco.Services/Web/Bundles/IBundle.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using System.Web; + +namespace Disco.Services.Web.Bundles +{ + public interface IBundle + { + bool RemapRequest { get; } + + string Url { get; } + string VersionUrl { get; } + + string ContentType { get; } + + void ProcessRequest(HttpContext context); + } +} diff --git a/Disco.Services/Web/Bundles/UrlBundle.cs b/Disco.Services/Web/Bundles/UrlBundle.cs new file mode 100644 index 00000000..16174e2d --- /dev/null +++ b/Disco.Services/Web/Bundles/UrlBundle.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Disco.Services.Web.Bundles +{ + public class UrlBundle : IBundle + { + public bool RemapRequest { get { return false; } } + + public string Url { get; private set; } + public string VersionUrl { get; private set; } + + public string ContentType { get; private set; } + + public void ProcessRequest(System.Web.HttpContext context) + { + // Not needed for Url Bundle + throw new NotImplementedException(); + } + + public UrlBundle(string Url, string ContentType) + { + this.Url = Url; + this.VersionUrl = Url; + + this.ContentType = ContentType; + } + } +} diff --git a/Disco.Services/Web/Signalling/DiscoHubAuthorizeAllAttribute.cs b/Disco.Services/Web/Signalling/DiscoHubAuthorizeAllAttribute.cs new file mode 100644 index 00000000..72f3073d --- /dev/null +++ b/Disco.Services/Web/Signalling/DiscoHubAuthorizeAllAttribute.cs @@ -0,0 +1,35 @@ +using Disco.Services.Users; +using Microsoft.AspNet.SignalR; +using System; +using System.Security.Principal; + +namespace Disco.Services.Web.Signalling +{ + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)] + public class DiscoHubAuthorizeAllAttribute : AuthorizeAttribute + { + string[] authorizedClaims; + + public DiscoHubAuthorizeAllAttribute(params string[] AuthorisedClaims) + { + if (AuthorisedClaims == null || AuthorisedClaims.Length == 0) + throw new ArgumentNullException("AuthorisedClaims"); + + this.authorizedClaims = AuthorisedClaims; + } + + protected override bool UserAuthorized(IPrincipal user) + { + if (user == null || !user.Identity.IsAuthenticated) + return false; + + var username = user.Identity.Name; + var userToken = UserService.GetAuthorization(username); + + if (userToken == null) + return false; // No User + + return userToken.HasAll(authorizedClaims); + } + } +} diff --git a/Disco.Services/Web/Signalling/DiscoHubAuthorizeAnyAttribute.cs b/Disco.Services/Web/Signalling/DiscoHubAuthorizeAnyAttribute.cs new file mode 100644 index 00000000..c4f831c8 --- /dev/null +++ b/Disco.Services/Web/Signalling/DiscoHubAuthorizeAnyAttribute.cs @@ -0,0 +1,35 @@ +using Disco.Services.Users; +using Microsoft.AspNet.SignalR; +using System; +using System.Security.Principal; + +namespace Disco.Services.Web.Signalling +{ + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)] + public class DiscoHubAuthorizeAnyAttribute : AuthorizeAttribute + { + string[] authorizedClaims; + + public DiscoHubAuthorizeAnyAttribute(params string[] AuthorisedClaims) + { + if (AuthorisedClaims == null || AuthorisedClaims.Length == 0) + throw new ArgumentNullException("AuthorisedClaims"); + + this.authorizedClaims = AuthorisedClaims; + } + + protected override bool UserAuthorized(IPrincipal user) + { + if (user == null || !user.Identity.IsAuthenticated) + return false; + + var username = user.Identity.Name; + var userToken = UserService.GetAuthorization(username); + + if (userToken == null) + return false; // No User + + return userToken.HasAny(authorizedClaims); + } + } +} diff --git a/Disco.Services/Web/Signalling/DiscoHubAuthorizeAttribute.cs b/Disco.Services/Web/Signalling/DiscoHubAuthorizeAttribute.cs new file mode 100644 index 00000000..c2a9d05d --- /dev/null +++ b/Disco.Services/Web/Signalling/DiscoHubAuthorizeAttribute.cs @@ -0,0 +1,37 @@ +using Disco.Services.Users; +using Microsoft.AspNet.SignalR; +using System; +using System.Security.Principal; + +namespace Disco.Services.Web.Signalling +{ + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)] + public class DiscoHubAuthorizeAttribute : AuthorizeAttribute + { + string authorizedClaim; + + public DiscoHubAuthorizeAttribute() { } + + public DiscoHubAuthorizeAttribute(string AuthorisedClaim) + { + this.authorizedClaim = AuthorisedClaim; + } + + protected override bool UserAuthorized(IPrincipal user) + { + if (user == null || !user.Identity.IsAuthenticated) + return false; + + var username = user.Identity.Name; + var userToken = UserService.GetAuthorization(username); + + if (userToken == null) + return false; // No User + + if (authorizedClaim == null) + return userToken.RoleTokens.Count > 0; // Just Authenticate - no Authorization (but require at least 1 role) + else + return userToken.Has(authorizedClaim); + } + } +} diff --git a/Disco.Services/packages.config b/Disco.Services/packages.config index cb32afc4..96754774 100644 --- a/Disco.Services/packages.config +++ b/Disco.Services/packages.config @@ -4,12 +4,13 @@ - - + + + - + diff --git a/Disco.Web/App_Start/AppConfig.cs b/Disco.Web/App_Start/AppConfig.cs index 0310f84b..e16cb6b5 100644 --- a/Disco.Web/App_Start/AppConfig.cs +++ b/Disco.Web/App_Start/AppConfig.cs @@ -76,7 +76,7 @@ namespace Disco.Web DiscoApplication.DocumentDropBoxMonitor.ScheduleCurrentFiles(10); // Enable SignalR-based Repository Notifications - Disco.BI.Interop.SignalRHandlers.RepositoryMonitorNotifications.Initialize(); + //Disco.BI.Interop.SignalRHandlers.RepositoryMonitorNotifications.Initialize(); } public static void InitializeUpdateEnvironment(DiscoDataContext Database, Version PreviousVersion) diff --git a/Disco.Web/App_Start/BundleConfig.cs b/Disco.Web/App_Start/BundleConfig.cs index 257b10d8..dd4db4b9 100644 --- a/Disco.Web/App_Start/BundleConfig.cs +++ b/Disco.Web/App_Start/BundleConfig.cs @@ -9,40 +9,40 @@ namespace Disco.Web public static void RegisterBundles() { // Styles - Site Core - BundleTable.Add(new Bundle("~/Style/Site", Links.ClientSource.Style.BundleSite_min_css)); + BundleTable.Add(new FileBundle("~/Style/Site", Links.ClientSource.Style.BundleSite_min_css)); // Styles - Targeted - BundleTable.Add(new Bundle("~/Style/Config", Links.ClientSource.Style.Config_min_css)); - BundleTable.Add(new Bundle("~/Style/Device", Links.ClientSource.Style.Device_min_css)); - BundleTable.Add(new Bundle("~/Style/Dialog", Links.ClientSource.Style.Dialog_min_css)); - BundleTable.Add(new Bundle("~/Style/Job", Links.ClientSource.Style.Job_min_css)); - BundleTable.Add(new Bundle("~/Style/User", Links.ClientSource.Style.User_min_css)); - BundleTable.Add(new Bundle("~/Style/Credits", Links.ClientSource.Style.Credits_min_css)); - BundleTable.Add(new Bundle("~/Style/AppMaintenance", Links.ClientSource.Style.AppMaintenance_min_css)); - BundleTable.Add(new Bundle("~/Style/jQueryUI/dynatree", Links.ClientSource.Style.jQueryUI.dynatree.ui_dynatree_min_css)); - BundleTable.Add(new Bundle("~/Style/Fancytree", Links.ClientSource.Style.Fancytree.disco_fancytree_min_css)); - BundleTable.Add(new Bundle("~/Style/Shadowbox", Links.ClientSource.Style.Shadowbox_min_css)); - BundleTable.Add(new Bundle("~/Style/Timeline", Links.ClientSource.Style.Timeline_min_css)); + BundleTable.Add(new FileBundle("~/Style/Config", Links.ClientSource.Style.Config_min_css)); + BundleTable.Add(new FileBundle("~/Style/Device", Links.ClientSource.Style.Device_min_css)); + BundleTable.Add(new FileBundle("~/Style/Dialog", Links.ClientSource.Style.Dialog_min_css)); + BundleTable.Add(new FileBundle("~/Style/Job", Links.ClientSource.Style.Job_min_css)); + BundleTable.Add(new FileBundle("~/Style/User", Links.ClientSource.Style.User_min_css)); + BundleTable.Add(new FileBundle("~/Style/Credits", Links.ClientSource.Style.Credits_min_css)); + BundleTable.Add(new FileBundle("~/Style/AppMaintenance", Links.ClientSource.Style.AppMaintenance_min_css)); + BundleTable.Add(new FileBundle("~/Style/jQueryUI/dynatree", Links.ClientSource.Style.jQueryUI.dynatree.ui_dynatree_min_css)); + BundleTable.Add(new FileBundle("~/Style/Fancytree", Links.ClientSource.Style.Fancytree.disco_fancytree_min_css)); + BundleTable.Add(new FileBundle("~/Style/Shadowbox", Links.ClientSource.Style.Shadowbox_min_css)); + BundleTable.Add(new FileBundle("~/Style/Timeline", Links.ClientSource.Style.Timeline_min_css)); // Styles - Public Targeted - BundleTable.Add(new Bundle("~/Style/Public/HeldDevices", Links.ClientSource.Style.Public.HeldDevices_min_css)); - BundleTable.Add(new Bundle("~/Style/Public/HeldDevicesNoticeboard", Links.ClientSource.Style.Public.HeldDevicesNoticeboard_min_css)); + BundleTable.Add(new FileBundle("~/Style/Public/HeldDevices", Links.ClientSource.Style.Public.HeldDevices_min_css)); + BundleTable.Add(new FileBundle("~/Style/Public/HeldDevicesNoticeboard", Links.ClientSource.Style.Public.HeldDevicesNoticeboard_min_css)); // Scripts - Core #if DEBUG - BundleTable.Add(new Bundle("~/ClientScripts/Core", Links.ClientSource.Scripts.Core_js)); + BundleTable.Add(new FileBundle("~/ClientScripts/Core", Links.ClientSource.Scripts.Core_js)); #else - BundleTable.Add(new Bundle("~/ClientScripts/Core", Links.ClientSource.Scripts.Core_min_js)); + BundleTable.Add(new FileBundle("~/ClientScripts/Core", Links.ClientSource.Scripts.Core_min_js)); #endif // Scripts - Modules #if DEBUG foreach (FileInfo f in new DirectoryInfo(HttpContext.Current.Server.MapPath("~/ClientSource/Scripts/Modules")).EnumerateFiles("*.js", SearchOption.TopDirectoryOnly)) - BundleTable.Add(new Bundle(string.Format("~/ClientScripts/Modules/{0}", f.Name.Substring(0, f.Name.Length - 3)), f.FullName)); + BundleTable.Add(new FileBundle(string.Format("~/ClientScripts/Modules/{0}", f.Name.Substring(0, f.Name.Length - 3)), f.FullName)); #else foreach (FileInfo f in new DirectoryInfo(HttpContext.Current.Server.MapPath("~/ClientSource/Scripts/Modules")).EnumerateFiles("*.min.js", SearchOption.TopDirectoryOnly)) - BundleTable.Add(new Bundle(string.Format("~/ClientScripts/Modules/{0}", f.Name.Substring(0, f.Name.Length - 7)), f.FullName)); + BundleTable.Add(new FileBundle(string.Format("~/ClientScripts/Modules/{0}", f.Name.Substring(0, f.Name.Length - 7)), f.FullName)); #endif } diff --git a/Disco.Web/App_Start/OwinStartupConfig.cs b/Disco.Web/App_Start/OwinStartupConfig.cs new file mode 100644 index 00000000..50650ae6 --- /dev/null +++ b/Disco.Web/App_Start/OwinStartupConfig.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNet.SignalR; +using Microsoft.Owin; +using Owin; + +[assembly: OwinStartup(typeof(Disco.Web.OwinStartupConfig))] + +namespace Disco.Web +{ + public class OwinStartupConfig + { + public void Configuration(IAppBuilder app) + { + var hubConfig = new HubConfiguration() + { + EnableJavaScriptProxies = false + }; + + app.MapSignalR("/API/Signalling", hubConfig); + } + } +} \ No newline at end of file diff --git a/Disco.Web/App_Start/RazorGeneratorMvcStart.cs b/Disco.Web/App_Start/RazorGeneratorMvcStart.cs index 13c7f1ec..c16ddfed 100644 --- a/Disco.Web/App_Start/RazorGeneratorMvcStart.cs +++ b/Disco.Web/App_Start/RazorGeneratorMvcStart.cs @@ -3,9 +3,9 @@ using System.Web.Mvc; using System.Web.WebPages; using RazorGenerator.Mvc; -[assembly: WebActivatorEx.PostApplicationStartMethod(typeof(Disco.Web.App_Start.RazorGeneratorMvcStart), "Start")] +[assembly: WebActivatorEx.PostApplicationStartMethod(typeof(Disco.Web.RazorGeneratorMvcStart), "Start")] -namespace Disco.Web.App_Start { +namespace Disco.Web { public static class RazorGeneratorMvcStart { public static void Start() { var engine = new PrecompiledMvcEngine(typeof(RazorGeneratorMvcStart).Assembly) { diff --git a/Disco.Web/App_Start/RouteConfig.cs b/Disco.Web/App_Start/RouteConfig.cs index e1a1c257..5f8a8028 100644 --- a/Disco.Web/App_Start/RouteConfig.cs +++ b/Disco.Web/App_Start/RouteConfig.cs @@ -5,7 +5,6 @@ using System.Web; using System.Web.Mvc; using System.Web.Routing; using Microsoft.AspNet.SignalR; -using Disco.BI.Interop.SignalRHandlers; namespace Disco.Web { @@ -74,11 +73,6 @@ namespace Disco.Web public static void RegisterUpdateRoutes(RouteCollection routes) { - // Task Status SignalR Route - routes.MapConnection( - "API_Logging_TaskStatusNotifications", - "API/Logging/TaskStatusNotifications", new ConnectionConfiguration(), SignalRAuthenticationWorkaround.AddMiddleware); - // Task Status Ajax Route routes.MapRoute( name: "API_Logging_ScheduledTaskStatus", // Route name diff --git a/Disco.Web/Areas/API/APIAreaRegistration.cs b/Disco.Web/Areas/API/APIAreaRegistration.cs index da471ee0..8eacdc6d 100644 --- a/Disco.Web/Areas/API/APIAreaRegistration.cs +++ b/Disco.Web/Areas/API/APIAreaRegistration.cs @@ -1,7 +1,6 @@ using System.Web.Mvc; using System.Web.Routing; using Microsoft.AspNet.SignalR; -using Disco.BI.Interop.SignalRHandlers; namespace Disco.Web.Areas.API { @@ -17,15 +16,6 @@ namespace Disco.Web.Areas.API public override void RegisterArea(AreaRegistrationContext context) { - context.Routes.MapConnection( - "API_Logging_Notifications", "API/Logging/Notifications", new ConnectionConfiguration(), SignalRAuthenticationWorkaround.AddMiddleware); - - context.Routes.MapConnection( - "API_Logging_TaskStatusNotifications", "API/Logging/TaskStatusNotifications", new ConnectionConfiguration(), SignalRAuthenticationWorkaround.AddMiddleware); - - context.Routes.MapConnection( - "API_Repository_Notifications", "API/Repository/Notifications", new ConnectionConfiguration(), SignalRAuthenticationWorkaround.AddMiddleware); - context.MapRoute( "API_Update", "API/{controller}/Update/{id}/{key}", diff --git a/Disco.Web/Areas/API/Models/Attachment/_AttachmentModel.cs b/Disco.Web/Areas/API/Models/Attachment/_AttachmentModel.cs index 0ae26a1d..fee6df51 100644 --- a/Disco.Web/Areas/API/Models/Attachment/_AttachmentModel.cs +++ b/Disco.Web/Areas/API/Models/Attachment/_AttachmentModel.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; -using Disco.BI; -using Disco.BI.Extensions; +using Disco.Services; +using System; namespace Disco.Web.Areas.API.Models.Attachment { @@ -27,7 +23,7 @@ namespace Disco.Web.Areas.API.Models.Attachment ParentId = ua.UserId, Id = ua.Id, AuthorId = ua.TechUserId, - Author = ua.TechUser.ToString(), + Author = ua.TechUser.ToStringFriendly(), Timestamp = ua.Timestamp, Comments = ua.Comments, Filename = ua.Filename, @@ -41,7 +37,7 @@ namespace Disco.Web.Areas.API.Models.Attachment ParentId = ja.JobId.ToString(), Id = ja.Id, AuthorId = ja.TechUserId, - Author = ja.TechUser.ToString(), + Author = ja.TechUser.ToStringFriendly(), Timestamp = ja.Timestamp, Comments = ja.Comments, Filename = ja.Filename, @@ -55,7 +51,7 @@ namespace Disco.Web.Areas.API.Models.Attachment ParentId = da.DeviceSerialNumber, Id = da.Id, AuthorId = da.TechUserId, - Author = da.TechUser.ToString(), + Author = da.TechUser.ToStringFriendly(), Timestamp = da.Timestamp, Comments = da.Comments, Filename = da.Filename, diff --git a/Disco.Web/Areas/Config/Controllers/LoggingController.cs b/Disco.Web/Areas/Config/Controllers/LoggingController.cs index d0887e31..94592ec8 100644 --- a/Disco.Web/Areas/Config/Controllers/LoggingController.cs +++ b/Disco.Web/Areas/Config/Controllers/LoggingController.cs @@ -1,4 +1,5 @@ using Disco.Models.UI.Config.Logging; +using Disco.Models.UI.Config.Shared; using Disco.Services.Authorization; using Disco.Services.Logging; using Disco.Services.Logging.Models; @@ -42,10 +43,10 @@ namespace Disco.Web.Areas.Config.Controllers if (taskStatus == null) return RedirectToAction(MVC.Config.Logging.Index()); - var m = new Models.Logging.TaskStatusModel() { SessionId = taskStatus.SessionId }; + var m = new Models.Shared.TaskStatusModel() { SessionId = taskStatus.SessionId }; // UI Extensions - UIExtensions.ExecuteExtensions(this.ControllerContext, m); + UIExtensions.ExecuteExtensions(this.ControllerContext, m); return View(m); } diff --git a/Disco.Web/Areas/Config/Models/Logging/TaskStatusModel.cs b/Disco.Web/Areas/Config/Models/Logging/TaskStatusModel.cs deleted file mode 100644 index 67e22132..00000000 --- a/Disco.Web/Areas/Config/Models/Logging/TaskStatusModel.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Disco.Models.UI.Config.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; - -namespace Disco.Web.Areas.Config.Models.Logging -{ - public class TaskStatusModel : ConfigLoggingTaskStatusModel - { - public string SessionId { get; set; } - } -} \ No newline at end of file diff --git a/Disco.Web/Areas/Config/Models/Shared/TaskStatusModel.cs b/Disco.Web/Areas/Config/Models/Shared/TaskStatusModel.cs new file mode 100644 index 00000000..f87bad44 --- /dev/null +++ b/Disco.Web/Areas/Config/Models/Shared/TaskStatusModel.cs @@ -0,0 +1,13 @@ +using Disco.Models.UI.Config.Shared; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace Disco.Web.Areas.Config.Models.Shared +{ + public class TaskStatusModel : ConfigSharedTaskStatusModel + { + public string SessionId { get; set; } + } +} \ No newline at end of file diff --git a/Disco.Web/Areas/Config/Views/Logging/TaskStatus.cshtml b/Disco.Web/Areas/Config/Views/Logging/TaskStatus.cshtml index a1e4a90e..c401f911 100644 --- a/Disco.Web/Areas/Config/Views/Logging/TaskStatus.cshtml +++ b/Disco.Web/Areas/Config/Views/Logging/TaskStatus.cshtml @@ -1,255 +1,5 @@ -@model Disco.Web.Areas.Config.Models.Logging.TaskStatusModel +@model Disco.Web.Areas.Config.Models.Shared.TaskStatusModel @{ ViewBag.Title = Html.ToBreadcrumb("Configuration", MVC.Config.Config.Index(), "Logging", MVC.Config.Logging.Index(), "Task Status"); - Html.BundleDeferred("~/ClientScripts/Modules/Knockout"); - Html.BundleDeferred("~/ClientScripts/Modules/jQuery-SignalR"); } -
-
-

 

- - - - - - - - - - - - - - - - - - - - - - -
  -
  -
-
-
-
-

Finished: -

-
- -
Last Error: -
-
-
Next Scheduled: -
-
-
- - +@Html.PartialCompiled(typeof(Disco.Web.Areas.Config.Views.Shared.TaskStatus), Model.SessionId) \ No newline at end of file diff --git a/Disco.Web/Areas/Config/Views/Logging/TaskStatus.generated.cs b/Disco.Web/Areas/Config/Views/Logging/TaskStatus.generated.cs index 6a452c2f..a90064b9 100644 --- a/Disco.Web/Areas/Config/Views/Logging/TaskStatus.generated.cs +++ b/Disco.Web/Areas/Config/Views/Logging/TaskStatus.generated.cs @@ -2,7 +2,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.34011 +// Runtime Version:4.0.30319.34014 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -37,7 +37,7 @@ namespace Disco.Web.Areas.Config.Views.Logging [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "2.0.0.0")] [System.Web.WebPages.PageVirtualPathAttribute("~/Areas/Config/Views/Logging/TaskStatus.cshtml")] - public partial class TaskStatus : Disco.Services.Web.WebViewPage + public partial class TaskStatus : Disco.Services.Web.WebViewPage { public TaskStatus() { @@ -48,269 +48,19 @@ namespace Disco.Web.Areas.Config.Views.Logging #line 2 "..\..\Areas\Config\Views\Logging\TaskStatus.cshtml" ViewBag.Title = Html.ToBreadcrumb("Configuration", MVC.Config.Config.Index(), "Logging", MVC.Config.Logging.Index(), "Task Status"); - Html.BundleDeferred("~/ClientScripts/Modules/Knockout"); - Html.BundleDeferred("~/ClientScripts/Modules/jQuery-SignalR"); #line default #line hidden -WriteLiteral("\r\n\r\n \r\n  \r\n \r\n \r\n  \r\n \r\n \r\n \r\n  \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n " + -" \r\n \r\n

Finished: \r\n

\r\n \r\n \r\n " + -" \r\n \r\n \r\n \r\n \r\n \r\n Last Error:\r\n \r\n \r\n \r\n \r\n " + -" \r\n Next Scheduled: \r\n \r\n \r\n
\r\n \r" + -"\n\r\n\r\n ko.bindingHandlers.progressValue = {\r\n init: function (element, val" + -"ueAccessor, allBindingsAccessor, viewModel) {\r\n var $element = $(elem" + -"ent);\r\n if (!$element.is(\'.ui-progressbar\'))\r\n $elemen" + -"t.progressbar();\r\n },\r\n update: function (element, valueAccessor, " + -"allBindingsAccessor, viewModel) {\r\n var v = ko.utils.unwrapObservable" + -"(valueAccessor());\r\n var vInt = parseInt(v);\r\n if (vInt >=" + -" 0) {\r\n $(element).progressbar(\'option\', \'value\', vInt);\r\n " + -" }\r\n }\r\n };\r\n //* http://webcloud.se/log/JavaScript-and-ISO-860" + -"1/\r\n Date.prototype.setISO8601 = function (string) {\r\n var regexp = \"(" + -"[0-9]{4})(-([0-9]{2})(-([0-9]{2})\" +\r\n \"(T([0-9]{2}):([0-9]{2})(:([0-9]{2" + -"})(\\.([0-9]+))?)?\" +\r\n \"(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?\";\r\n " + -" var d = string.match(new RegExp(regexp));\r\n\r\n var offset = 0;\r\n " + -" var date = new Date(d[1], 0, 1);\r\n\r\n if (d[3]) { date.setMonth(d[3] - 1)" + -"; }\r\n if (d[5]) { date.setDate(d[5]); }\r\n if (d[7]) { date.setHour" + -"s(d[7]); }\r\n if (d[8]) { date.setMinutes(d[8]); }\r\n if (d[10]) { d" + -"ate.setSeconds(d[10]); }\r\n if (d[12]) { date.setMilliseconds(Number(\"0.\" " + -"+ d[12]) * 1000); }\r\n if (d[14]) {\r\n offset = (Number(d[16]) *" + -" 60) + Number(d[17]);\r\n offset *= ((d[15] == \'-\') ? 1 : -1);\r\n " + -" }\r\n\r\n offset -= date.getTimezoneOffset();\r\n time = (Number(date) " + -"+ (offset * 60 * 1000));\r\n this.setTime(Number(time));\r\n return th" + -"is;\r\n }\r\n\r\n\r\n $(function () {\r\n var sessionId = \'"); +WriteLiteral("\r\n"); - #line 93 "..\..\Areas\Config\Views\Logging\TaskStatus.cshtml" - Write(Model.SessionId); + #line 5 "..\..\Areas\Config\Views\Logging\TaskStatus.cshtml" +Write(Html.PartialCompiled(typeof(Disco.Web.Areas.Config.Views.Shared.TaskStatus), Model.SessionId)); #line default #line hidden -WriteLiteral("\';\r\n var sessionStatusUrl = \'"); - - - #line 94 "..\..\Areas\Config\Views\Logging\TaskStatus.cshtml" - Write(Url.Action(MVC.API.Logging.ScheduledTaskStatus(Model.SessionId))); - - - #line default - #line hidden -WriteLiteral("\';\r\n\r\n var view = $(\'#scheduledTaskStatus\');\r\n var vm = null;\r\n\r\n " + -" var liveConnection = null;\r\n\r\n var statusViewModel = function (sess" + -"ionId) {\r\n var self = this;\r\n\r\n self.Initialized = ko.obse" + -"rvable(false);\r\n\r\n self.TimestampParse = function (timestamp) {\r\n " + -" if (timestamp) {\r\n if (timestamp.indexOf(\"\\/Date(" + -"\") == 0)\r\n return new Date(parseInt(timestamp.substr(6)))" + -";\r\n else\r\n return (new Date()).setISO8" + -"601(timestamp);\r\n }\r\n return new Date();\r\n " + -" }\r\n self.TimestampFormat = function (timestamp) {\r\n " + -" var addZero = function (v) { v = v + \'\'; if (v.length == 1) v = \'0\' + v; retur" + -"n v; }\r\n return timestamp.getFullYear() + \'/\' + addZero((1 + time" + -"stamp.getMonth())) + \'/\' + addZero(timestamp.getDate()) + \' \' + addZero(timestam" + -"p.getHours()) + \':\' + addZero(timestamp.getMinutes()) + \':\' + addZero(timestamp." + -"getSeconds());\r\n }\r\n\r\n self.SessionId = sessionId;\r\n " + -" self.TaskName = ko.observable(null);\r\n self.StatusVersion = -1;" + -"\r\n\r\n self.Progress = ko.observable(0);\r\n self.CurrentProce" + -"ss = ko.observable(null);\r\n self.CurrentDescription = ko.observable(n" + -"ull);\r\n\r\n self.IsRunning = ko.observable(null);\r\n\r\n self.T" + -"askExceptionMessage = ko.observable(null);\r\n\r\n self.FinishedTimestamp" + -" = ko.observable(null);\r\n self.NextScheduledTimestamp = ko.observable" + -"(null)\r\n\r\n self.NextScheduledTimestampFormatted = ko.computed(functio" + -"n () {\r\n return self.TimestampFormat(self.TimestampParse(self.Nex" + -"tScheduledTimestamp()));\r\n });\r\n self.FinishedTimestampFor" + -"matted = ko.computed(function () {\r\n return self.TimestampFormat(" + -"self.TimestampParse(self.FinishedTimestamp()));\r\n });\r\n\r\n " + -"self.FinishedUrl = ko.observable(null);\r\n self.FinishedMessage = ko.o" + -"bservable(null);\r\n\r\n self.Finished = function () {\r\n i" + -"f (self.FinishedTimestamp()) {\r\n if (self.FinishedUrl() && !s" + -"elf.TaskExceptionMessage()) {\r\n if (self.FinishedMessage(" + -"))\r\n window.setTimeout(function () { window.location." + -"href = self.FinishedUrl(); }, 3000);\r\n else\r\n " + -" window.location.href = self.FinishedUrl();\r\n " + -"}\r\n }\r\n }\r\n\r\n self.Initialize = function (t" + -"askStatus) {\r\n self.TaskName(taskStatus.TaskName);\r\n " + -" self.FinishedUrl(taskStatus.FinishedUrl);\r\n\r\n self.Progress(ta" + -"skStatus.Progress);\r\n self.CurrentProcess(taskStatus.CurrentProce" + -"ss);\r\n self.CurrentDescription(taskStatus.CurrentDescription);\r\n\r" + -"\n self.IsRunning(taskStatus.IsRunning);\r\n\r\n self.T" + -"askExceptionMessage(taskStatus.TaskExceptionMessage);\r\n\r\n self.Fi" + -"nishedTimestamp(taskStatus.FinishedTimestamp);\r\n self.NextSchedul" + -"edTimestamp(taskStatus.NextScheduledTimestamp);\r\n\r\n self.Finished" + -"Message(taskStatus.FinishedMessage);\r\n\r\n self.Initialized(true);\r" + -"\n\r\n self.Finished();\r\n }\r\n self.Update = fu" + -"nction (taskStatus) {\r\n if (!self.Initialized())\r\n " + -" return self.Initialize(taskStatus);\r\n\r\n if (taskStatus.Statu" + -"sVersion < self.StatusVersion)\r\n return; // Have Newer Status" + -" Update\r\n self.StatusVersion = taskStatus.StatusVersion;\r\n\r\n " + -" if (taskStatus.ChangedProperties) {\r\n for (var cha" + -"ngedPropertyIndex = 0; changedPropertyIndex < taskStatus.ChangedProperties.lengt" + -"h; changedPropertyIndex++) {\r\n switch (taskStatus.Changed" + -"Properties[changedPropertyIndex]) {\r\n case \'Progress\'" + -":\r\n self.Progress(taskStatus.Progress);\r\n " + -" break;\r\n case \'CurrentProcess" + -"\':\r\n self.CurrentProcess(taskStatus.CurrentProces" + -"s);\r\n break;\r\n case \'C" + -"urrentDescription\':\r\n self.CurrentDescription(tas" + -"kStatus.CurrentDescription);\r\n break;\r\n " + -" case \'IsRunning\':\r\n self.IsRunn" + -"ing(taskStatus.IsRunning);\r\n break;\r\n " + -" case \'TaskException\':\r\n self.Task" + -"ExceptionMessage(taskStatus.TaskExceptionMessage);\r\n " + -" break;\r\n case \'NextScheduledTimestamp\':\r\n " + -" self.NextScheduledTimestamp(taskStatus.NextScheduledTime" + -"stamp);\r\n break;\r\n cas" + -"e \'FinishedUrl\':\r\n self.FinishedUrl(taskStatus.Fi" + -"nishedUrl);\r\n break;\r\n " + -" case \'FinishedMessage\':\r\n self.FinishedMessage(t" + -"askStatus.FinishedMessage);\r\n break;\r\n " + -" case \'FinishedTimestamp\':\r\n self" + -".FinishedTimestamp(taskStatus.FinishedTimestamp);\r\n " + -" window.setTimeout(self.Finished, 1);\r\n break;\r" + -"\n default:\r\n // Ignore" + -"\r\n }\r\n }\r\n }\r\n " + -" }\r\n }\r\n\r\n vm = new statusViewModel(sessionId);\r\n ko.appl" + -"yBindings(vm, view[0]);\r\n\r\n // Start Live Connection\r\n updateWithL" + -"ive();\r\n\r\n function updateWithAjax(onSuccess) {\r\n $.ajax({\r\n " + -" url: sessionStatusUrl,\r\n dataType: \'json\',\r\n " + -" type: \'POST\',\r\n traditional: true,\r\n succ" + -"ess: update_Received,\r\n error: function (jqXHR, textStatus, error" + -"Thrown) {\r\n alert(\'Unable to load Session: \' + errorThrown);\r" + -"\n }\r\n });\r\n }\r\n function updateWithLive(" + -") {\r\n liveConnection = $.connection(\'"); - - - #line 243 "..\..\Areas\Config\Views\Logging\TaskStatus.cshtml" - Write(Url.Content("~/API/Logging/TaskStatusNotifications")); - - - #line default - #line hidden -WriteLiteral(@"', { addToGroups: sessionId }); - liveConnection.received(update_Received); - liveConnection.error(function (e) { if (e.status != 200) alert('Live-Status Error: ' + e.statusText + ': ' + e.responseText); }); - liveConnection.start(function () { - updateWithAjax(); - }); - } - function update_Received(taskStatus) { - vm.Update(taskStatus); - } - - }); - -"); - } } } diff --git a/Disco.Web/Areas/Config/Views/Shared/LogEvents.cshtml b/Disco.Web/Areas/Config/Views/Shared/LogEvents.cshtml index 281a96ec..32bfce68 100644 --- a/Disco.Web/Areas/Config/Views/Shared/LogEvents.cshtml +++ b/Disco.Web/Areas/Config/Views/Shared/LogEvents.cshtml @@ -1,7 +1,7 @@ @model Disco.Web.Areas.Config.Models.Shared.LogEventsModel @{ Authorization.Require(Claims.Config.Logging.Show); - + Html.BundleDeferred("~/ClientScripts/Modules/Knockout"); Html.BundleDeferred("~/ClientScripts/Modules/jQuery-SignalR"); var uniqueId = Guid.NewGuid().ToString("N"); @@ -44,12 +44,12 @@ $(function () { var logEventsHost = $('LogEvents_@(uniqueId)'); var logModuleId = '@(Model.ModuleFilter != null ? Model.ModuleFilter.ModuleId.ToString() : null)'; - var logModuleLiveGroupName = '@(Model.ModuleFilter != null ? Model.ModuleFilter.LiveLogGroupName : Disco.BI.Interop.SignalRHandlers.LogNotifications.AllNotifications)'; - var logEventTypeFiltered = @(eventTypesFilterJson); + var logModuleLiveGroupName = '@(Model.ModuleFilter != null ? Model.ModuleFilter.LiveLogGroupName : Disco.Services.Logging.LogNotificationsHub.AllLoggingNotification)'; + var logEventTypeFiltered = @(eventTypesFilterJson); var logStartFiler = @(AjaxHelpers.JsonDate(Model.StartFilter)); var logEndFiler = @(AjaxHelpers.JsonDate(Model.EndFilter)); var logTakeFiler = '@(Model.TakeFilter)'; - var liveConnection = null; + var logHub = null; var liveEventReceivedFunction = '@(Model.JavascriptLiveEventFunctionName)'; var useLive = ('True'==='@(Model.IsLive)'); @@ -89,7 +89,7 @@ dataType: 'json', type: 'POST', data: loadData, - success: function (d) { + success: function (d) { initLogs(d); }, error: function (jqXHR, textStatus, errorThrown) { @@ -113,16 +113,21 @@ } } - liveConnection = $.connection('@(Url.Content("~/API/Logging/Notifications"))', {addToGroups: logModuleLiveGroupName}); - liveConnection.received(logReceived); - liveConnection.error(function(e){if (e.status != 200) alert('Live-Log Error: '+e.statusText +': '+e.responseText);}); - liveConnection.start(); - } - } + logHub = $.connection.logNotifications; + logHub.client.receiveLog = function(message){ + if (message.UseDisplay) logsViewModel.EventLogs.unshift(message); + if (liveEventReceivedFunction) liveEventReceivedFunction(message); + }; - function logReceived(log){ - if (log.UseDisplay) logsViewModel.EventLogs.unshift(log); - if (liveEventReceivedFunction) liveEventReceivedFunction(log); + $.connection.hub.qs = {LogModules: logModuleLiveGroupName}; + $.connection.hub.error(function(error){ + alert('Live-Log Error: '+error); + }); + + $.connection.hub.start().fail(function(error){ + alert('Live-Log Connection Error: '+error); + }); + } } loadInitialData(); diff --git a/Disco.Web/Areas/Config/Views/Shared/LogEvents.generated.cs b/Disco.Web/Areas/Config/Views/Shared/LogEvents.generated.cs index 64c01810..35cfb980 100644 --- a/Disco.Web/Areas/Config/Views/Shared/LogEvents.generated.cs +++ b/Disco.Web/Areas/Config/Views/Shared/LogEvents.generated.cs @@ -2,7 +2,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.34011 +// Runtime Version:4.0.30319.34014 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -48,7 +48,7 @@ namespace Disco.Web.Areas.Config.Views.Shared #line 2 "..\..\Areas\Config\Views\Shared\LogEvents.cshtml" Authorization.Require(Claims.Config.Logging.Show); - + Html.BundleDeferred("~/ClientScripts/Modules/Knockout"); Html.BundleDeferred("~/ClientScripts/Modules/jQuery-SignalR"); var uniqueId = Guid.NewGuid().ToString("N"); @@ -58,15 +58,15 @@ namespace Disco.Web.Areas.Config.Views.Shared #line hidden WriteLiteral("\r\n(uniqueId +, Tuple.Create(Tuple.Create("", 324), Tuple.Create(uniqueId #line default #line hidden -, 328), false) +, 324), false) ); WriteLiteral(" class=\"logEventsViewport\""); @@ -96,21 +96,21 @@ WriteLiteral(">Message\r\n \r\n \r\n WriteLiteral(" class=\"logEventsViewportContainer\""); -WriteAttribute("style", Tuple.Create(" style=\"", 814), Tuple.Create("\"", 1024) +WriteAttribute("style", Tuple.Create(" style=\"", 810), Tuple.Create("\"", 1020) #line 24 "..\..\Areas\Config\Views\Shared\LogEvents.cshtml" -, Tuple.Create(Tuple.Create("", 822), Tuple.Create(Model.ViewPortWidth.HasValue ? string.Format("width:{0}px;", Model.ViewPortWidth.Value) : null +, Tuple.Create(Tuple.Create("", 818), Tuple.Create(Model.ViewPortWidth.HasValue ? string.Format("width:{0}px;", Model.ViewPortWidth.Value) : null #line default #line hidden -, 822), false) +, 818), false) #line 24 "..\..\Areas\Config\Views\Shared\LogEvents.cshtml" - , Tuple.Create(Tuple.Create("", 919), Tuple.Create(Model.ViewPortHeight.HasValue ? string.Format("height:{0}px;", Model.ViewPortHeight.Value - 18) : null + , Tuple.Create(Tuple.Create("", 915), Tuple.Create(Model.ViewPortHeight.HasValue ? string.Format("height:{0}px;", Model.ViewPortHeight.Value - 18) : null #line default #line hidden -, 919), false) +, 915), false) ); WriteLiteral(">\r\n