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.
This commit is contained in:
Gary Sharp
2014-06-01 23:27:07 +10:00
parent f6fae26bc7
commit 4cd57f4a90
116 changed files with 9874 additions and 6462 deletions
@@ -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);
+124
View File
@@ -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<JobUpdatesHub>();
// 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);
}
}
}
}
}
@@ -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<HeldDeviceItem> FromJobs(IQueryable<Job> 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
});
}
}
}
@@ -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<string> MonitorJobProperties = new List<string>() {
"DeviceSerialNumber",
"UserId",
"ExpectedClosedDate",
"ClosedDate",
"WaitingForUserAction",
"DeviceHeld",
"DeviceReadyForReturn",
"DeviceReturnedDate"
};
private readonly static List<string> MonitorJobMetaNonWarrantyProperties = new List<string>(){
"AccountingChargeRequiredDate",
"AccountingChargeAddedDate",
"AccountingChargePaidDate"
};
private readonly static List<string> MonitorDeviceProperties = new List<string>(){
"Location",
"DeviceProfileId",
"DeviceDomainId",
"AssignedUserId",
};
private readonly static List<string> MonitorDeviceProfileProperties = new List<string>(){
"DefaultOrganisationAddress"
};
private readonly static List<string> MonitorUserProperties = new List<string>(){
"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<RepositoryMonitorEvent> e)
{
List<string> deviceSerialNumbers = new List<string>();
List<string> userIds = new List<string>();
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<string>("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<string>("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<string> 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<IHeldDeviceItem> GetHeldDevices(IQueryable<Job> 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<IHeldDeviceItem> 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<HeldDeviceItem> SelectHeldDeviceItems(this IQueryable<Job> jobs)
{
return HeldDeviceItem.FromJobs(jobs);
}
}
}
@@ -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<string> 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<IHeldDeviceItem> GetHeldDevicesForUsers(IQueryable<Job> 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<IHeldDeviceItem> 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();
}
}
}
@@ -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<NoticeboardUpdatesHub>();
}
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();
}
}
}