From 6a1b54410acce1cdeaecc32f40443830ac711b21 Mon Sep 17 00:00:00 2001 From: yangxiaodong Date: Fri, 25 Aug 2017 18:29:58 +0800 Subject: [PATCH] add dashboard --- .../Dashboard/CombinedResourceDispatcher.cs | 33 + .../Dashboard/CommandDispatcher.cs | 39 + .../Content/resx/Strings.Designer.cs | 5 +- .../Dashboard/DashboardContext.cs | 19 +- .../Dashboard/DashboardMetric.cs | 24 + .../Dashboard/DashboardMetrics.cs | 179 ++++ .../Dashboard/DashboardRequest.cs | 18 + .../Dashboard/DashboardResponse.cs | 17 + .../Dashboard/EmbeddedResourceDispatcher.cs | 54 + src/DotNetCore.CAP/Dashboard/HtmlHelper.cs | 244 +++++ .../IDashboardAuthorizationFilter.cs | 7 + .../Dashboard/IDashboardDispatcher.cs | 12 + .../Dashboard/IMonitoringApi.cs | 20 +- .../Dashboard/JobHistoryRenderer.cs | 258 +++++ .../Dashboard/JobsSidebarMenu.cs | 57 ++ src/DotNetCore.CAP/Dashboard/JsonStats.cs | 45 + .../LocalRequestsOnlyAuthorizationFilter.cs | 26 + src/DotNetCore.CAP/Dashboard/MenuItem.cs | 33 + src/DotNetCore.CAP/Dashboard/Metric.cs | 42 + .../Dashboard/Monitoring/DeletedJobDto.cs | 6 +- .../Dashboard/Monitoring/EnqueuedJobDto.cs | 24 +- .../Dashboard/Monitoring/FailedJobDto.cs | 24 +- .../Dashboard/Monitoring/FetchedJobDto.cs | 24 +- .../Dashboard/Monitoring/JobDetailsDto.cs | 24 +- .../Dashboard/Monitoring/JobList.cs | 18 +- .../Dashboard/Monitoring/ProcessingJobDto.cs | 24 +- .../Monitoring/QueueWithTopEnqueuedJobsDto.cs | 18 +- .../Dashboard/Monitoring/ScheduledJobDto.cs | 24 +- .../Dashboard/Monitoring/ServerDto.cs | 20 +- .../Dashboard/Monitoring/StateHistoryDto.cs | 20 +- .../Dashboard/Monitoring/StatisticsDto.cs | 18 +- .../Dashboard/Monitoring/SucceededJobDto.cs | 6 +- .../Dashboard/NavigationMenu.cs | 42 + .../Dashboard/NonEscapedString.cs | 17 + src/DotNetCore.CAP/Dashboard/Pager.cs | 156 +++ .../{Views => Pages}/AwaitingJobsPage.cshtml | 23 +- .../Dashboard/{Views => Pages}/BlockMetric.cs | 2 +- .../Dashboard/{Views => Pages}/Breadcrumbs.cs | 2 +- .../Dashboard/{Views => Pages}/HomePage.cs | 6 +- .../{Views => Pages}/HomePage.cshtml | 6 +- .../{Views => Pages}/HomePage.generated.cs | 8 +- .../{Views => Pages}/InlineMetric.cs | 4 +- .../Dashboard/{Views => Pages}/LayoutPage.cs | 2 +- .../{Views => Pages}/LayoutPage.cshtml | 6 +- .../{Views => Pages}/LayoutPage.generated.cs | 8 +- .../ProcessingJobsPage.cshtml | 6 +- .../Dashboard/Pages/SidebarMenu.cs | 16 + .../{Views => Pages}/SucceededJobs.cshtml | 6 +- .../{Views => Pages}/_BlockMetric.cshtml | 6 +- .../_BlockMetric.generated.cs | 6 +- .../{Views => Pages}/_Breadcrumbs.cshtml | 2 +- .../{Views => Pages}/_InlineMetric.cshtml | 2 +- .../_InlineMetric.generated.cs | 4 +- .../{Views => Pages}/_Navigation.cshtml | 2 +- .../{Views => Pages}/_Navigation.generated.cs | 4 +- .../Dashboard/{Views => Pages}/_Paginator.cs | 2 +- .../{Views => Pages}/_Paginator.cshtml | 4 +- .../{Views => Pages}/_PerPageSelector.cs | 2 +- .../{Views => Pages}/_PerPageSelector.cshtml | 4 +- .../{Views => Pages}/_SidebarMenu.cshtml | 2 +- src/DotNetCore.CAP/Dashboard/RazorPage.cs | 25 +- .../Dashboard/RazorPageDispatcher.cs | 26 + src/DotNetCore.CAP/Dashboard/UrlHelper.cs | 44 + .../Views/AwaitingJobsPage.generated.cs | 638 ------------ .../Dashboard/Views/DeletedJobsPage.cshtml | 102 -- .../Views/DeletedJobsPage.generated.cs | 433 -------- .../Dashboard/Views/EnqueuedJobsPage.cs | 12 - .../Dashboard/Views/EnqueuedJobsPage.cshtml | 118 --- .../Views/EnqueuedJobsPage.generated.cs | 517 ---------- .../Dashboard/Views/FailedJobsPage.cshtml | 135 --- .../Views/FailedJobsPage.generated.cs | 606 ------------ .../Dashboard/Views/FetchedJobsPage.cs | 12 - .../Dashboard/Views/FetchedJobsPage.cshtml | 121 --- .../Views/FetchedJobsPage.generated.cs | 497 ---------- .../Dashboard/Views/JobDetailsPage.cs | 15 - .../Dashboard/Views/JobDetailsPage.cshtml | 211 ---- .../Views/JobDetailsPage1.generated.cs | 932 ------------------ .../Views/ProcessingJobsPage.generated.cs | 544 ---------- .../Dashboard/Views/QueuesPage.cshtml | 122 --- .../Dashboard/Views/QueuesPage.generated.cs | 574 ----------- .../Dashboard/Views/RecurringJobsPage.cshtml | 196 ---- .../Views/RecurringJobsPage.generated.cs | 831 ---------------- .../Dashboard/Views/RetriesPage.cshtml | 143 --- .../Dashboard/Views/RetriesPage.generated.cs | 555 ----------- .../Dashboard/Views/ScheduledJobsPage.cshtml | 106 -- .../Views/ScheduledJobsPage.generated.cs | 449 --------- .../Dashboard/Views/ServersPage.cshtml | 60 -- .../Dashboard/Views/ServersPage.generated.cs | 294 ------ .../Dashboard/Views/SidebarMenu.cs | 33 - .../Views/SucceededJobs1.generated.cs | 481 --------- .../Dashboard/Views/_Breadcrumbs.generated.cs | 105 -- .../Dashboard/Views/_Paginator.generated.cs | 248 ----- .../Views/_PerPageSelector.generated.cs | 106 -- .../Dashboard/Views/_SidebarMenu.generated.cs | 139 --- 94 files changed, 1513 insertions(+), 9649 deletions(-) create mode 100644 src/DotNetCore.CAP/Dashboard/CombinedResourceDispatcher.cs create mode 100644 src/DotNetCore.CAP/Dashboard/CommandDispatcher.cs create mode 100644 src/DotNetCore.CAP/Dashboard/DashboardMetric.cs create mode 100644 src/DotNetCore.CAP/Dashboard/DashboardMetrics.cs create mode 100644 src/DotNetCore.CAP/Dashboard/DashboardRequest.cs create mode 100644 src/DotNetCore.CAP/Dashboard/DashboardResponse.cs create mode 100644 src/DotNetCore.CAP/Dashboard/EmbeddedResourceDispatcher.cs create mode 100644 src/DotNetCore.CAP/Dashboard/HtmlHelper.cs create mode 100644 src/DotNetCore.CAP/Dashboard/IDashboardAuthorizationFilter.cs create mode 100644 src/DotNetCore.CAP/Dashboard/IDashboardDispatcher.cs create mode 100644 src/DotNetCore.CAP/Dashboard/JobHistoryRenderer.cs create mode 100644 src/DotNetCore.CAP/Dashboard/JobsSidebarMenu.cs create mode 100644 src/DotNetCore.CAP/Dashboard/JsonStats.cs create mode 100644 src/DotNetCore.CAP/Dashboard/LocalRequestsOnlyAuthorizationFilter.cs create mode 100644 src/DotNetCore.CAP/Dashboard/MenuItem.cs create mode 100644 src/DotNetCore.CAP/Dashboard/Metric.cs create mode 100644 src/DotNetCore.CAP/Dashboard/NavigationMenu.cs create mode 100644 src/DotNetCore.CAP/Dashboard/NonEscapedString.cs create mode 100644 src/DotNetCore.CAP/Dashboard/Pager.cs rename src/DotNetCore.CAP/Dashboard/{Views => Pages}/AwaitingJobsPage.cshtml (92%) rename src/DotNetCore.CAP/Dashboard/{Views => Pages}/BlockMetric.cs (83%) rename src/DotNetCore.CAP/Dashboard/{Views => Pages}/Breadcrumbs.cs (88%) rename src/DotNetCore.CAP/Dashboard/{Views => Pages}/HomePage.cs (52%) rename src/DotNetCore.CAP/Dashboard/{Views => Pages}/HomePage.cshtml (95%) rename src/DotNetCore.CAP/Dashboard/{Views => Pages}/HomePage.generated.cs (98%) rename src/DotNetCore.CAP/Dashboard/{Views => Pages}/InlineMetric.cs (70%) rename src/DotNetCore.CAP/Dashboard/{Views => Pages}/LayoutPage.cs (79%) rename src/DotNetCore.CAP/Dashboard/{Views => Pages}/LayoutPage.cshtml (96%) rename src/DotNetCore.CAP/Dashboard/{Views => Pages}/LayoutPage.generated.cs (98%) rename src/DotNetCore.CAP/Dashboard/{Views => Pages}/ProcessingJobsPage.cshtml (97%) create mode 100644 src/DotNetCore.CAP/Dashboard/Pages/SidebarMenu.cs rename src/DotNetCore.CAP/Dashboard/{Views => Pages}/SucceededJobs.cshtml (97%) rename src/DotNetCore.CAP/Dashboard/{Views => Pages}/_BlockMetric.cshtml (82%) rename src/DotNetCore.CAP/Dashboard/{Views => Pages}/_BlockMetric.generated.cs (95%) rename src/DotNetCore.CAP/Dashboard/{Views => Pages}/_Breadcrumbs.cshtml (90%) rename src/DotNetCore.CAP/Dashboard/{Views => Pages}/_InlineMetric.cshtml (85%) rename src/DotNetCore.CAP/Dashboard/{Views => Pages}/_InlineMetric.generated.cs (96%) rename src/DotNetCore.CAP/Dashboard/{Views => Pages}/_Navigation.cshtml (95%) rename src/DotNetCore.CAP/Dashboard/{Views => Pages}/_Navigation.generated.cs (97%) rename src/DotNetCore.CAP/Dashboard/{Views => Pages}/_Paginator.cs (79%) rename src/DotNetCore.CAP/Dashboard/{Views => Pages}/_Paginator.cshtml (95%) rename src/DotNetCore.CAP/Dashboard/{Views => Pages}/_PerPageSelector.cs (80%) rename src/DotNetCore.CAP/Dashboard/{Views => Pages}/_PerPageSelector.cshtml (87%) rename src/DotNetCore.CAP/Dashboard/{Views => Pages}/_SidebarMenu.cshtml (95%) create mode 100644 src/DotNetCore.CAP/Dashboard/RazorPageDispatcher.cs create mode 100644 src/DotNetCore.CAP/Dashboard/UrlHelper.cs delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/AwaitingJobsPage.generated.cs delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/DeletedJobsPage.cshtml delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/DeletedJobsPage.generated.cs delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/EnqueuedJobsPage.cs delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/EnqueuedJobsPage.cshtml delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/EnqueuedJobsPage.generated.cs delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/FailedJobsPage.cshtml delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/FailedJobsPage.generated.cs delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/FetchedJobsPage.cs delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/FetchedJobsPage.cshtml delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/FetchedJobsPage.generated.cs delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/JobDetailsPage.cs delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/JobDetailsPage.cshtml delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/JobDetailsPage1.generated.cs delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/ProcessingJobsPage.generated.cs delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/QueuesPage.cshtml delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/QueuesPage.generated.cs delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/RecurringJobsPage.cshtml delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/RecurringJobsPage.generated.cs delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/RetriesPage.cshtml delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/RetriesPage.generated.cs delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/ScheduledJobsPage.cshtml delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/ScheduledJobsPage.generated.cs delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/ServersPage.cshtml delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/ServersPage.generated.cs delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/SidebarMenu.cs delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/SucceededJobs1.generated.cs delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/_Breadcrumbs.generated.cs delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/_Paginator.generated.cs delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/_PerPageSelector.generated.cs delete mode 100644 src/DotNetCore.CAP/Dashboard/Views/_SidebarMenu.generated.cs diff --git a/src/DotNetCore.CAP/Dashboard/CombinedResourceDispatcher.cs b/src/DotNetCore.CAP/Dashboard/CombinedResourceDispatcher.cs new file mode 100644 index 0000000..6c6d3af --- /dev/null +++ b/src/DotNetCore.CAP/Dashboard/CombinedResourceDispatcher.cs @@ -0,0 +1,33 @@ +using System.Reflection; + +namespace DotNetCore.CAP.Dashboard +{ + internal class CombinedResourceDispatcher : EmbeddedResourceDispatcher + { + private readonly Assembly _assembly; + private readonly string _baseNamespace; + private readonly string[] _resourceNames; + + public CombinedResourceDispatcher( + string contentType, + Assembly assembly, + string baseNamespace, + params string[] resourceNames) : base(contentType, assembly, null) + { + _assembly = assembly; + _baseNamespace = baseNamespace; + _resourceNames = resourceNames; + } + + protected override void WriteResponse(DashboardResponse response) + { + foreach (var resourceName in _resourceNames) + { + WriteResource( + response, + _assembly, + $"{_baseNamespace}.{resourceName}"); + } + } + } +} diff --git a/src/DotNetCore.CAP/Dashboard/CommandDispatcher.cs b/src/DotNetCore.CAP/Dashboard/CommandDispatcher.cs new file mode 100644 index 0000000..3abde72 --- /dev/null +++ b/src/DotNetCore.CAP/Dashboard/CommandDispatcher.cs @@ -0,0 +1,39 @@ +using System; +using System.Net; +using System.Threading.Tasks; + +namespace DotNetCore.CAP.Dashboard +{ + internal class CommandDispatcher : IDashboardDispatcher + { + private readonly Func _command; + + public CommandDispatcher(Func command) + { + _command = command; + } + + public Task Dispatch(DashboardContext context) + { + var request = context.Request; + var response = context.Response; + + if (!"POST".Equals(request.Method, StringComparison.OrdinalIgnoreCase)) + { + response.StatusCode = (int)HttpStatusCode.MethodNotAllowed; + return Task.FromResult(false); + } + + if (_command(context)) + { + response.StatusCode = (int)HttpStatusCode.NoContent; + } + else + { + response.StatusCode = 422; + } + + return Task.FromResult(true); + } + } +} diff --git a/src/DotNetCore.CAP/Dashboard/Content/resx/Strings.Designer.cs b/src/DotNetCore.CAP/Dashboard/Content/resx/Strings.Designer.cs index 8b1d990..a7ea40e 100644 --- a/src/DotNetCore.CAP/Dashboard/Content/resx/Strings.Designer.cs +++ b/src/DotNetCore.CAP/Dashboard/Content/resx/Strings.Designer.cs @@ -10,7 +10,8 @@ using System.Reflection; -namespace Hangfire.Dashboard.Resources { +namespace DotNetCore.CAP.Dashboard.Resources +{ using System; @@ -41,7 +42,7 @@ namespace Hangfire.Dashboard.Resources { public static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Hangfire.Dashboard.Content.resx.Strings", typeof(Strings).GetTypeInfo().Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DotNetCore.CAP.Dashboard.Content.resx.Strings", typeof(Strings).GetTypeInfo().Assembly); resourceMan = temp; } return resourceMan; diff --git a/src/DotNetCore.CAP/Dashboard/DashboardContext.cs b/src/DotNetCore.CAP/Dashboard/DashboardContext.cs index a45ee11..21a7811 100644 --- a/src/DotNetCore.CAP/Dashboard/DashboardContext.cs +++ b/src/DotNetCore.CAP/Dashboard/DashboardContext.cs @@ -1,10 +1,27 @@ using System; using System.Collections.Generic; using System.Text; +using System.Text.RegularExpressions; namespace DotNetCore.CAP.Dashboard { - class DashboardContext + public abstract class DashboardContext { + protected DashboardContext(IStorage storage, DashboardOptions options) + { + if (storage == null) throw new ArgumentNullException(nameof(storage)); + if (options == null) throw new ArgumentNullException(nameof(options)); + + Storage = storage; + Options = options; + } + + public IStorage Storage { get; } + public DashboardOptions Options { get; } + + public Match UriMatch { get; set; } + + public DashboardRequest Request { get; protected set; } + public DashboardResponse Response { get; protected set; } } } diff --git a/src/DotNetCore.CAP/Dashboard/DashboardMetric.cs b/src/DotNetCore.CAP/Dashboard/DashboardMetric.cs new file mode 100644 index 0000000..cef8593 --- /dev/null +++ b/src/DotNetCore.CAP/Dashboard/DashboardMetric.cs @@ -0,0 +1,24 @@ +using System; + +namespace DotNetCore.CAP.Dashboard +{ + public class DashboardMetric + { + public DashboardMetric(string name, Func func) + : this(name, name, func) + { + } + + public DashboardMetric(string name, string title, Func func) + { + Name = name; + Title = title; + Func = func; + } + + public string Name { get; } + public Func Func { get; } + + public string Title { get; set; } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Dashboard/DashboardMetrics.cs b/src/DotNetCore.CAP/Dashboard/DashboardMetrics.cs new file mode 100644 index 0000000..9b6c07c --- /dev/null +++ b/src/DotNetCore.CAP/Dashboard/DashboardMetrics.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using DotNetCore.CAP.Dashboard.Resources; + +namespace DotNetCore.CAP.Dashboard +{ + public static class DashboardMetrics + { + private static readonly Dictionary Metrics = new Dictionary(); + + static DashboardMetrics() + { + AddMetric(ServerCount); + AddMetric(RecurringJobCount); + AddMetric(RetriesCount); + AddMetric(EnqueuedCountOrNull); + AddMetric(FailedCountOrNull); + AddMetric(EnqueuedAndQueueCount); + AddMetric(ScheduledCount); + AddMetric(ProcessingCount); + AddMetric(SucceededCount); + AddMetric(FailedCount); + AddMetric(DeletedCount); + AddMetric(AwaitingCount); + } + + public static void AddMetric(DashboardMetric metric) + { + if (metric == null) throw new ArgumentNullException(nameof(metric)); + + lock (Metrics) + { + Metrics[metric.Name] = metric; + } + } + + public static IEnumerable GetMetrics() + { + lock (Metrics) + { + return Metrics.Values.ToList(); + } + } + + public static readonly DashboardMetric ServerCount = new DashboardMetric( + "servers:count", + "Metrics_Servers", + page => new Metric(page.Statistics.Servers.ToString("N0")) + { + Style = page.Statistics.Servers == 0 ? MetricStyle.Warning : MetricStyle.Default, + Highlighted = page.Statistics.Servers == 0, + Title = page.Statistics.Servers == 0 + ? "No active servers found. Jobs will not be processed." + : null + }); + + public static readonly DashboardMetric RecurringJobCount = new DashboardMetric( + "recurring:count", + "Metrics_RecurringJobs", + page => new Metric(page.Statistics.Recurring.ToString("N0"))); + + public static readonly DashboardMetric RetriesCount = new DashboardMetric( + "retries:count", + "Metrics_Retries", + page => + { + long retryCount; + using (var connection = page.Storage.GetConnection()) + { + var storageConnection = connection as IStorageConnection; + if (storageConnection == null) + { + return null; + } + + retryCount = storageConnection.GetSetCount("retries"); + } + + return new Metric(retryCount.ToString("N0")) + { + Style = retryCount > 0 ? MetricStyle.Warning : MetricStyle.Default + }; + }); + + public static readonly DashboardMetric EnqueuedCountOrNull = new DashboardMetric( + "enqueued:count-or-null", + "Metrics_EnqueuedCountOrNull", + page => page.Statistics.Enqueued > 0 || page.Statistics.Failed == 0 + ? new Metric(page.Statistics.Enqueued.ToString("N0")) + { + Style = page.Statistics.Enqueued > 0 ? MetricStyle.Info : MetricStyle.Default, + Highlighted = page.Statistics.Enqueued > 0 && page.Statistics.Failed == 0 + } + : null); + + public static readonly DashboardMetric FailedCountOrNull = new DashboardMetric( + "failed:count-or-null", + "Metrics_FailedJobs", + page => page.Statistics.Failed > 0 + ? new Metric(page.Statistics.Failed.ToString("N0")) + { + Style = MetricStyle.Danger, + Highlighted = true, + Title = string.Format(Strings.Metrics_FailedCountOrNull, page.Statistics.Failed) + } + : null); + + public static readonly DashboardMetric EnqueuedAndQueueCount = new DashboardMetric( + "enqueued-queues:count", + "Metrics_EnqueuedQueuesCount", + page => new Metric($"{page.Statistics.Enqueued:N0} / {page.Statistics.Queues:N0}") + { + Style = page.Statistics.Enqueued > 0 ? MetricStyle.Info : MetricStyle.Default, + Highlighted = page.Statistics.Enqueued > 0 + }); + + public static readonly DashboardMetric ScheduledCount = new DashboardMetric( + "scheduled:count", + "Metrics_ScheduledJobs", + page => new Metric(page.Statistics.Scheduled.ToString("N0")) + { + Style = page.Statistics.Scheduled > 0 ? MetricStyle.Info : MetricStyle.Default + }); + + public static readonly DashboardMetric ProcessingCount = new DashboardMetric( + "processing:count", + "Metrics_ProcessingJobs", + page => new Metric(page.Statistics.Processing.ToString("N0")) + { + Style = page.Statistics.Processing > 0 ? MetricStyle.Warning : MetricStyle.Default + }); + + public static readonly DashboardMetric SucceededCount = new DashboardMetric( + "succeeded:count", + "Metrics_SucceededJobs", + page => new Metric(page.Statistics.Succeeded.ToString("N0")) + { + IntValue = page.Statistics.Succeeded + }); + + public static readonly DashboardMetric FailedCount = new DashboardMetric( + "failed:count", + "Metrics_FailedJobs", + page => new Metric(page.Statistics.Failed.ToString("N0")) + { + IntValue = page.Statistics.Failed, + Style = page.Statistics.Failed > 0 ? MetricStyle.Danger : MetricStyle.Default, + Highlighted = page.Statistics.Failed > 0 + }); + + public static readonly DashboardMetric DeletedCount = new DashboardMetric( + "deleted:count", + "Metrics_DeletedJobs", + page => new Metric(page.Statistics.Deleted.ToString("N0"))); + + public static readonly DashboardMetric AwaitingCount = new DashboardMetric( + "awaiting:count", + "Metrics_AwaitingCount", + page => + { + long awaitingCount = -1; + + using (var connection = page.Storage.GetConnection()) + { + var storageConnection = connection as IStorageConnection; + if (storageConnection != null) + { + awaitingCount = storageConnection.GetSetCount("awaiting"); + } + } + + return new Metric(awaitingCount.ToString("N0")) + { + Style = awaitingCount > 0 ? MetricStyle.Info : MetricStyle.Default + }; + }); + } +} diff --git a/src/DotNetCore.CAP/Dashboard/DashboardRequest.cs b/src/DotNetCore.CAP/Dashboard/DashboardRequest.cs new file mode 100644 index 0000000..ee04d5a --- /dev/null +++ b/src/DotNetCore.CAP/Dashboard/DashboardRequest.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace DotNetCore.CAP.Dashboard +{ + public abstract class DashboardRequest + { + public abstract string Method { get; } + public abstract string Path { get; } + public abstract string PathBase { get; } + + public abstract string LocalIpAddress { get; } + public abstract string RemoteIpAddress { get; } + + public abstract string GetQuery(string key); + public abstract Task> GetFormValuesAsync(string key); + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Dashboard/DashboardResponse.cs b/src/DotNetCore.CAP/Dashboard/DashboardResponse.cs new file mode 100644 index 0000000..7fb9024 --- /dev/null +++ b/src/DotNetCore.CAP/Dashboard/DashboardResponse.cs @@ -0,0 +1,17 @@ +using System; +using System.IO; +using System.Threading.Tasks; + +namespace DotNetCore.CAP.Dashboard +{ + public abstract class DashboardResponse + { + public abstract string ContentType { get; set; } + public abstract int StatusCode { get; set; } + + public abstract Stream Body { get; } + + public abstract void SetExpire(DateTimeOffset? value); + public abstract Task WriteAsync(string text); + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Dashboard/EmbeddedResourceDispatcher.cs b/src/DotNetCore.CAP/Dashboard/EmbeddedResourceDispatcher.cs new file mode 100644 index 0000000..f04ee60 --- /dev/null +++ b/src/DotNetCore.CAP/Dashboard/EmbeddedResourceDispatcher.cs @@ -0,0 +1,54 @@ +using System; +using System.Reflection; +using System.Threading.Tasks; + +namespace DotNetCore.CAP.Dashboard +{ + internal class EmbeddedResourceDispatcher : IDashboardDispatcher + { + private readonly Assembly _assembly; + private readonly string _resourceName; + private readonly string _contentType; + + public EmbeddedResourceDispatcher( + string contentType, + Assembly assembly, + string resourceName) + { + if (contentType == null) throw new ArgumentNullException(nameof(contentType)); + if (assembly == null) throw new ArgumentNullException(nameof(assembly)); + + _assembly = assembly; + _resourceName = resourceName; + _contentType = contentType; + } + + public Task Dispatch(DashboardContext context) + { + context.Response.ContentType = _contentType; + context.Response.SetExpire(DateTimeOffset.Now.AddYears(1)); + + WriteResponse(context.Response); + + return Task.FromResult(true); + } + + protected virtual void WriteResponse(DashboardResponse response) + { + WriteResource(response, _assembly, _resourceName); + } + + protected void WriteResource(DashboardResponse response, Assembly assembly, string resourceName) + { + using (var inputStream = assembly.GetManifestResourceStream(resourceName)) + { + if (inputStream == null) + { + throw new ArgumentException($@"Resource with name {resourceName} not found in assembly {assembly}."); + } + + inputStream.CopyTo(response.Body); + } + } + } +} diff --git a/src/DotNetCore.CAP/Dashboard/HtmlHelper.cs b/src/DotNetCore.CAP/Dashboard/HtmlHelper.cs new file mode 100644 index 0000000..fa4e52f --- /dev/null +++ b/src/DotNetCore.CAP/Dashboard/HtmlHelper.cs @@ -0,0 +1,244 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.ComponentModel; +using System.Reflection; +using System.Text.RegularExpressions; +using DotNetCore.CAP.Dashboard.Resources; +using DotNetCore.CAP.Dashboard.Pages; +using DotNetCore.CAP.Infrastructure; +using DotNetCore.CAP.Models; + +namespace DotNetCore.CAP.Dashboard +{ + public class HtmlHelper + { + private readonly RazorPage _page; + + public HtmlHelper(RazorPage page) + { + if (page == null) throw new ArgumentNullException(nameof(page)); + _page = page; + } + + //public NonEscapedString Breadcrumbs(string title, IDictionary items) + //{ + // if (items == null) throw new ArgumentNullException(nameof(items)); + // return RenderPartial(new Breadcrumbs(title, items)); + //} + + //public NonEscapedString JobsSidebar() + //{ + // return RenderPartial(new SidebarMenu(JobsSidebarMenu.Items)); + //} + + //public NonEscapedString SidebarMenu(IEnumerable> items) + //{ + // if (items == null) throw new ArgumentNullException(nameof(items)); + // return RenderPartial(new SidebarMenu(items)); + //} + + public NonEscapedString BlockMetric(DashboardMetric metric) + { + if (metric == null) throw new ArgumentNullException(nameof(metric)); + return RenderPartial(new BlockMetric(metric)); + } + + public NonEscapedString InlineMetric(DashboardMetric metric) + { + if (metric == null) throw new ArgumentNullException(nameof(metric)); + return RenderPartial(new InlineMetric(metric)); + } + + //public NonEscapedString Paginator(Pager pager) + //{ + // if (pager == null) throw new ArgumentNullException(nameof(pager)); + // return RenderPartial(new Paginator(pager)); + //} + + //public NonEscapedString PerPageSelector(Pager pager) + //{ + // if (pager == null) throw new ArgumentNullException(nameof(pager)); + // return RenderPartial(new PerPageSelector(pager)); + //} + + public NonEscapedString RenderPartial(RazorPage partialPage) + { + partialPage.Assign(_page); + return new NonEscapedString(partialPage.ToString()); + } + + public NonEscapedString Raw(string value) + { + return new NonEscapedString(value); + } + + public NonEscapedString JobId(string jobId, bool shorten = true) + { + Guid guid; + return new NonEscapedString(Guid.TryParse(jobId, out guid) + ? (shorten ? jobId.Substring(0, 8) + "…" : jobId) + : $"#{jobId}"); + } + + public string JobName(Message job) + { + if (job == null) + { + return Strings.Common_CannotFindTargetMethod; + } + + return job.ToString(); + } + + public NonEscapedString StateLabel(string stateName) + { + if (String.IsNullOrWhiteSpace(stateName)) + { + return Raw($"{Strings.Common_NoState}"); + } + + return Raw($"{stateName}"); + } + + public NonEscapedString JobIdLink(string jobId) + { + return Raw($"{JobId(jobId)}"); + } + + public NonEscapedString JobNameLink(string jobId, Message job) + { + return Raw($"{HtmlEncode(JobName(job))}"); + } + + public NonEscapedString RelativeTime(DateTime value) + { + return Raw($"{value}"); + } + + public NonEscapedString MomentTitle(DateTime time, string value) + { + return Raw($"{value}"); + } + + public NonEscapedString LocalTime(DateTime value) + { + return Raw($"{value}"); + } + + public string ToHumanDuration(TimeSpan? duration, bool displaySign = true) + { + if (duration == null) return null; + + var builder = new StringBuilder(); + if (displaySign) + { + builder.Append(duration.Value.TotalMilliseconds < 0 ? "-" : "+"); + } + + duration = duration.Value.Duration(); + + if (duration.Value.Days > 0) + { + builder.Append($"{duration.Value.Days}d "); + } + + if (duration.Value.Hours > 0) + { + builder.Append($"{duration.Value.Hours}h "); + } + + if (duration.Value.Minutes > 0) + { + builder.Append($"{duration.Value.Minutes}m "); + } + + if (duration.Value.TotalHours < 1) + { + if (duration.Value.Seconds > 0) + { + builder.Append(duration.Value.Seconds); + if (duration.Value.Milliseconds > 0) + { + builder.Append($".{duration.Value.Milliseconds.ToString().PadLeft(3, '0')}"); + } + + builder.Append("s "); + } + else + { + if (duration.Value.Milliseconds > 0) + { + builder.Append($"{duration.Value.Milliseconds}ms "); + } + } + } + + if (builder.Length <= 1) + { + builder.Append(" <1ms "); + } + + builder.Remove(builder.Length - 1, 1); + + return builder.ToString(); + } + + public string FormatProperties(IDictionary properties) + { + return String.Join(", ", properties.Select(x => $"{x.Key}: \"{x.Value}\"")); + } + + public NonEscapedString QueueLabel(string queue) + { + var label = queue != null + ? $"{queue}" + : $"{Strings.Common_Unknown}"; + + return new NonEscapedString(label); + } + + public NonEscapedString ServerId(string serverId) + { + var parts = serverId.Split(':'); + var shortenedId = parts.Length > 1 + ? String.Join(":", parts.Take(parts.Length - 1)) + : serverId; + + return new NonEscapedString( + $"{shortenedId}"); + } + + //private static readonly StackTraceHtmlFragments StackTraceHtmlFragments = new StackTraceHtmlFragments + //{ + // BeforeFrame = "" , AfterFrame = "", + // BeforeType = "" , AfterType = "", + // BeforeMethod = "" , AfterMethod = "", + // BeforeParameters = "" , AfterParameters = "", + // BeforeParameterType = "", AfterParameterType = "", + // BeforeParameterName = "" , AfterParameterName = "", + // BeforeFile = "" , AfterFile = "", + // BeforeLine = "" , AfterLine = "", + //}; + + public NonEscapedString StackTrace(string stackTrace) + { + try + { + //return new NonEscapedString(StackTraceFormatter.FormatHtml(stackTrace, StackTraceHtmlFragments)); + return new NonEscapedString(stackTrace); + } + catch (RegexMatchTimeoutException) + { + return new NonEscapedString(HtmlEncode(stackTrace)); + } + } + + public string HtmlEncode(string text) + { + return WebUtility.HtmlEncode(text); + } + } +} diff --git a/src/DotNetCore.CAP/Dashboard/IDashboardAuthorizationFilter.cs b/src/DotNetCore.CAP/Dashboard/IDashboardAuthorizationFilter.cs new file mode 100644 index 0000000..741dd1a --- /dev/null +++ b/src/DotNetCore.CAP/Dashboard/IDashboardAuthorizationFilter.cs @@ -0,0 +1,7 @@ +namespace DotNetCore.CAP.Dashboard +{ + public interface IDashboardAuthorizationFilter + { + bool Authorize( DashboardContext context); + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Dashboard/IDashboardDispatcher.cs b/src/DotNetCore.CAP/Dashboard/IDashboardDispatcher.cs new file mode 100644 index 0000000..fec2faa --- /dev/null +++ b/src/DotNetCore.CAP/Dashboard/IDashboardDispatcher.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace DotNetCore.CAP.Dashboard +{ + public interface IDashboardDispatcher + { + Task Dispatch( DashboardContext context); + } +} diff --git a/src/DotNetCore.CAP/Dashboard/IMonitoringApi.cs b/src/DotNetCore.CAP/Dashboard/IMonitoringApi.cs index fad0eae..b7bb626 100644 --- a/src/DotNetCore.CAP/Dashboard/IMonitoringApi.cs +++ b/src/DotNetCore.CAP/Dashboard/IMonitoringApi.cs @@ -1,24 +1,8 @@ -// This file is part of Hangfire. -// Copyright © 2013-2014 Sergey Odinokov. -// -// Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 -// of the License, or any later version. -// -// Hangfire is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with Hangfire. If not, see . - using System; using System.Collections.Generic; -using Hangfire.Storage.Monitoring; +using DotNetCore.CAP.Dashboard.Monitoring; -namespace Hangfire.Storage +namespace DotNetCore.CAP.Dashboard { public interface IMonitoringApi { diff --git a/src/DotNetCore.CAP/Dashboard/JobHistoryRenderer.cs b/src/DotNetCore.CAP/Dashboard/JobHistoryRenderer.cs new file mode 100644 index 0000000..04d0d0e --- /dev/null +++ b/src/DotNetCore.CAP/Dashboard/JobHistoryRenderer.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections.Generic; +using System.Text; +using DotNetCore.CAP.Infrastructure; +using DotNetCore.CAP.Processor.States; +using Newtonsoft.Json; + +namespace DotNetCore.CAP.Dashboard +{ + public static class JobHistoryRenderer + { + private static readonly IDictionary, NonEscapedString>> + Renderers = new Dictionary, NonEscapedString>>(); + + private static readonly IDictionary BackgroundStateColors + = new Dictionary(); + private static readonly IDictionary ForegroundStateColors + = new Dictionary(); + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline")] + static JobHistoryRenderer() + { + Register(SucceededState.StateName, SucceededRenderer); + Register(FailedState.StateName, FailedRenderer); + Register(ProcessingState.StateName, ProcessingRenderer); + Register(EnqueuedState.StateName, EnqueuedRenderer); + Register(ScheduledState.StateName, ScheduledRenderer); + //Register(DeletedState.StateName, NullRenderer); + //Register(AwaitingState.StateName, AwaitingRenderer); + + BackgroundStateColors.Add(EnqueuedState.StateName, "#F5F5F5"); + BackgroundStateColors.Add(SucceededState.StateName, "#EDF7ED"); + BackgroundStateColors.Add(FailedState.StateName, "#FAEBEA"); + BackgroundStateColors.Add(ProcessingState.StateName, "#FCEFDC"); + BackgroundStateColors.Add(ScheduledState.StateName, "#E0F3F8"); + //BackgroundStateColors.Add(DeletedState.StateName, "#ddd"); + //BackgroundStateColors.Add(AwaitingState.StateName, "#F5F5F5"); + + ForegroundStateColors.Add(EnqueuedState.StateName, "#999"); + ForegroundStateColors.Add(SucceededState.StateName, "#5cb85c"); + ForegroundStateColors.Add(FailedState.StateName, "#d9534f"); + ForegroundStateColors.Add(ProcessingState.StateName, "#f0ad4e"); + ForegroundStateColors.Add(ScheduledState.StateName, "#5bc0de"); + //ForegroundStateColors.Add(DeletedState.StateName, "#777"); + //ForegroundStateColors.Add(AwaitingState.StateName, "#999"); + } + + public static void AddBackgroundStateColor(string stateName, string color) + { + BackgroundStateColors.Add(stateName, color); + } + + public static string GetBackgroundStateColor(string stateName) + { + if (stateName == null || !BackgroundStateColors.ContainsKey(stateName)) + { + return "inherit"; + } + + return BackgroundStateColors[stateName]; + } + + public static void AddForegroundStateColor(string stateName, string color) + { + ForegroundStateColors.Add(stateName, color); + } + + public static string GetForegroundStateColor(string stateName) + { + if (stateName == null || !ForegroundStateColors.ContainsKey(stateName)) + { + return "inherit"; + } + + return ForegroundStateColors[stateName]; + } + + public static void Register(string state, Func, NonEscapedString> renderer) + { + if (!Renderers.ContainsKey(state)) + { + Renderers.Add(state, renderer); + } + else + { + Renderers[state] = renderer; + } + } + + public static bool Exists(string state) + { + return Renderers.ContainsKey(state); + } + + public static NonEscapedString RenderHistory( + this HtmlHelper helper, + string state, IDictionary properties) + { + var renderer = Renderers.ContainsKey(state) + ? Renderers[state] + : DefaultRenderer; + + return renderer?.Invoke(helper, properties); + } + + public static NonEscapedString NullRenderer(HtmlHelper helper, IDictionary properties) + { + return null; + } + + public static NonEscapedString DefaultRenderer(HtmlHelper helper, IDictionary stateData) + { + if (stateData == null || stateData.Count == 0) return null; + + var builder = new StringBuilder(); + builder.Append("
"); + + foreach (var item in stateData) + { + builder.Append($"
{item.Key}
"); + builder.Append($"
{item.Value}
"); + } + + builder.Append("
"); + + return new NonEscapedString(builder.ToString()); + } + + public static NonEscapedString SucceededRenderer(HtmlHelper html, IDictionary stateData) + { + var builder = new StringBuilder(); + builder.Append("
"); + + var itemsAdded = false; + + if (stateData.ContainsKey("Latency")) + { + var latency = TimeSpan.FromMilliseconds(long.Parse(stateData["Latency"])); + + builder.Append($"
Latency:
{html.ToHumanDuration(latency, false)}
"); + + itemsAdded = true; + } + + if (stateData.ContainsKey("PerformanceDuration")) + { + var duration = TimeSpan.FromMilliseconds(long.Parse(stateData["PerformanceDuration"])); + builder.Append($"
Duration:
{html.ToHumanDuration(duration, false)}
"); + + itemsAdded = true; + } + + + if (stateData.ContainsKey("Result") && !String.IsNullOrWhiteSpace(stateData["Result"])) + { + var result = stateData["Result"]; + builder.Append($"
Result:
{System.Net.WebUtility.HtmlEncode(result)}
"); + + itemsAdded = true; + } + + builder.Append("
"); + + if (!itemsAdded) return null; + + return new NonEscapedString(builder.ToString()); + } + + private static NonEscapedString FailedRenderer(HtmlHelper html, IDictionary stateData) + { + var stackTrace = html.StackTrace(stateData["ExceptionDetails"]).ToString(); + return new NonEscapedString( + $"

{stateData["ExceptionType"]}

{stateData["ExceptionMessage"]}

{"
" + stackTrace + "
"}"); + } + + private static NonEscapedString ProcessingRenderer(HtmlHelper helper, IDictionary stateData) + { + var builder = new StringBuilder(); + builder.Append("
"); + + string serverId = null; + + if (stateData.ContainsKey("ServerId")) + { + serverId = stateData["ServerId"]; + } + else if (stateData.ContainsKey("ServerName")) + { + serverId = stateData["ServerName"]; + } + + if (serverId != null) + { + builder.Append("
Server:
"); + builder.Append($"
{helper.ServerId(serverId)}
"); + } + + if (stateData.ContainsKey("WorkerId")) + { + builder.Append("
Worker:
"); + builder.Append($"
{stateData["WorkerId"].Substring(0, 8)}
"); + } + else if (stateData.ContainsKey("WorkerNumber")) + { + builder.Append("
Worker:
"); + builder.Append($"
#{stateData["WorkerNumber"]}
"); + } + + builder.Append("
"); + + return new NonEscapedString(builder.ToString()); + } + + private static NonEscapedString EnqueuedRenderer(HtmlHelper helper, IDictionary stateData) + { + return new NonEscapedString( + $"
Queue:
{helper.QueueLabel(stateData["Queue"])}
"); + } + + private static NonEscapedString ScheduledRenderer(HtmlHelper helper, IDictionary stateData) + { + var enqueueAt = Helper.DeserializeDateTime(stateData["EnqueueAt"]); + + return new NonEscapedString( + $"
Enqueue at:
{enqueueAt}
"); + } + + private static NonEscapedString AwaitingRenderer(HtmlHelper helper, IDictionary stateData) + { + var builder = new StringBuilder(); + + builder.Append("
"); + + if (stateData.ContainsKey("ParentId")) + { + builder.Append($"
Parent
{helper.JobIdLink(stateData["ParentId"])}
"); + } + + if (stateData.ContainsKey("NextState")) + { + var nextState = JsonConvert.DeserializeObject( + stateData["NextState"], + new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }); + + builder.Append($"
Next State
{helper.StateLabel(nextState.Name)}
"); + } + + if (stateData.ContainsKey("Options")) + { + builder.Append($"
Options
{helper.HtmlEncode(stateData["Options"])}
"); + } + + builder.Append("
"); + + return new NonEscapedString(builder.ToString()); + } + } +} diff --git a/src/DotNetCore.CAP/Dashboard/JobsSidebarMenu.cs b/src/DotNetCore.CAP/Dashboard/JobsSidebarMenu.cs new file mode 100644 index 0000000..350af27 --- /dev/null +++ b/src/DotNetCore.CAP/Dashboard/JobsSidebarMenu.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using DotNetCore.CAP.Dashboard.Resources; + +namespace DotNetCore.CAP.Dashboard +{ + public static class JobsSidebarMenu + { + public static readonly List> Items + = new List>(); + + static JobsSidebarMenu() + { + Items.Add(page => new MenuItem(Strings.JobsSidebarMenu_Enqueued, page.Url.LinkToQueues()) + { + Active = page.RequestPath.StartsWith("/jobs/enqueued"), + Metric = DashboardMetrics.EnqueuedAndQueueCount + }); + + Items.Add(page => new MenuItem(Strings.JobsSidebarMenu_Scheduled, page.Url.To("/jobs/scheduled")) + { + Active = page.RequestPath.StartsWith("/jobs/scheduled"), + Metric = DashboardMetrics.ScheduledCount + }); + + Items.Add(page => new MenuItem(Strings.JobsSidebarMenu_Processing, page.Url.To("/jobs/processing")) + { + Active = page.RequestPath.StartsWith("/jobs/processing"), + Metric = DashboardMetrics.ProcessingCount + }); + + Items.Add(page => new MenuItem(Strings.JobsSidebarMenu_Succeeded, page.Url.To("/jobs/succeeded")) + { + Active = page.RequestPath.StartsWith("/jobs/succeeded"), + Metric = DashboardMetrics.SucceededCount + }); + + Items.Add(page => new MenuItem(Strings.JobsSidebarMenu_Failed, page.Url.To("/jobs/failed")) + { + Active = page.RequestPath.StartsWith("/jobs/failed"), + Metric = DashboardMetrics.FailedCount + }); + + Items.Add(page => new MenuItem(Strings.JobsSidebarMenu_Deleted, page.Url.To("/jobs/deleted")) + { + Active = page.RequestPath.StartsWith("/jobs/deleted"), + Metric = DashboardMetrics.DeletedCount + }); + + Items.Add(page => new MenuItem(Strings.JobsSidebarMenu_Awaiting, page.Url.To("/jobs/awaiting")) + { + Active = page.RequestPath.StartsWith("/jobs/awaiting"), + Metric = DashboardMetrics.AwaitingCount + }); + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Dashboard/JsonStats.cs b/src/DotNetCore.CAP/Dashboard/JsonStats.cs new file mode 100644 index 0000000..5500bd8 --- /dev/null +++ b/src/DotNetCore.CAP/Dashboard/JsonStats.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; + +namespace DotNetCore.CAP.Dashboard +{ + internal class JsonStats : IDashboardDispatcher + { + public async Task Dispatch(DashboardContext context) + { + var requestedMetrics = await context.Request.GetFormValuesAsync("metrics[]"); + var page = new StubPage(); + page.Assign(context); + + var metrics = DashboardMetrics.GetMetrics().Where(x => requestedMetrics.Contains(x.Name)); + var result = new Dictionary(); + + foreach (var metric in metrics) + { + var value = metric.Func(page); + result.Add(metric.Name, value); + } + + var settings = new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + Converters = new JsonConverter[]{ new StringEnumConverter { CamelCaseText = true } } + }; + var serialized = JsonConvert.SerializeObject(result, settings); + + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync(serialized); + } + + private class StubPage : RazorPage + { + public override void Execute() + { + } + } + } +} diff --git a/src/DotNetCore.CAP/Dashboard/LocalRequestsOnlyAuthorizationFilter.cs b/src/DotNetCore.CAP/Dashboard/LocalRequestsOnlyAuthorizationFilter.cs new file mode 100644 index 0000000..f8788c3 --- /dev/null +++ b/src/DotNetCore.CAP/Dashboard/LocalRequestsOnlyAuthorizationFilter.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace DotNetCore.CAP.Dashboard +{ + public class LocalRequestsOnlyAuthorizationFilter : IDashboardAuthorizationFilter + { + public bool Authorize(DashboardContext context) + { + // if unknown, assume not local + if (String.IsNullOrEmpty(context.Request.RemoteIpAddress)) + return false; + + // check if localhost + if (context.Request.RemoteIpAddress == "127.0.0.1" || context.Request.RemoteIpAddress == "::1") + return true; + + // compare with local address + if (context.Request.RemoteIpAddress == context.Request.LocalIpAddress) + return true; + + return false; + } + } +} diff --git a/src/DotNetCore.CAP/Dashboard/MenuItem.cs b/src/DotNetCore.CAP/Dashboard/MenuItem.cs new file mode 100644 index 0000000..d6735f8 --- /dev/null +++ b/src/DotNetCore.CAP/Dashboard/MenuItem.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; + +namespace DotNetCore.CAP.Dashboard +{ + public class MenuItem + { + public MenuItem(string text, string url) + { + Text = text; + Url = url; + } + + public string Text { get; } + public string Url { get; } + + public bool Active { get; set; } + public DashboardMetric Metric { get; set; } + public DashboardMetric[] Metrics { get; set; } + + public IEnumerable GetAllMetrics() + { + var metrics = new List { Metric }; + + if (Metrics != null) + { + metrics.AddRange(Metrics); + } + + return metrics.Where(x => x != null).ToList(); + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Dashboard/Metric.cs b/src/DotNetCore.CAP/Dashboard/Metric.cs new file mode 100644 index 0000000..50a48f9 --- /dev/null +++ b/src/DotNetCore.CAP/Dashboard/Metric.cs @@ -0,0 +1,42 @@ + +namespace DotNetCore.CAP.Dashboard +{ + public class Metric + { + public Metric(string value) + { + Value = value; + } + + public string Value { get; } + public long IntValue { get; set; } + public MetricStyle Style { get; set; } + public bool Highlighted { get; set; } + public string Title { get; set; } + } + + public enum MetricStyle + { + Default, + Info, + Success, + Warning, + Danger, + } + + internal static class MetricStyleExtensions + { + public static string ToClassName(this MetricStyle style) + { + switch (style) + { + case MetricStyle.Default: return "metric-default"; + case MetricStyle.Info: return "metric-info"; + case MetricStyle.Success: return "metric-success"; + case MetricStyle.Warning: return "metric-warning"; + case MetricStyle.Danger: return "metric-danger"; + default: return "metric-null"; + } + } + } +} diff --git a/src/DotNetCore.CAP/Dashboard/Monitoring/DeletedJobDto.cs b/src/DotNetCore.CAP/Dashboard/Monitoring/DeletedJobDto.cs index 7e35d9d..bdc2bd6 100644 --- a/src/DotNetCore.CAP/Dashboard/Monitoring/DeletedJobDto.cs +++ b/src/DotNetCore.CAP/Dashboard/Monitoring/DeletedJobDto.cs @@ -1,7 +1,7 @@ using System; -using Hangfire.Common; +using DotNetCore.CAP.Models; -namespace Hangfire.Storage.Monitoring +namespace DotNetCore.CAP.Dashboard.Monitoring { public class DeletedJobDto { @@ -10,7 +10,7 @@ namespace Hangfire.Storage.Monitoring InDeletedState = true; } - public Job Job { get; set; } + public Message Message { get; set; } public DateTime? DeletedAt { get; set; } public bool InDeletedState { get; set; } } diff --git a/src/DotNetCore.CAP/Dashboard/Monitoring/EnqueuedJobDto.cs b/src/DotNetCore.CAP/Dashboard/Monitoring/EnqueuedJobDto.cs index df215b6..4ec62dd 100644 --- a/src/DotNetCore.CAP/Dashboard/Monitoring/EnqueuedJobDto.cs +++ b/src/DotNetCore.CAP/Dashboard/Monitoring/EnqueuedJobDto.cs @@ -1,23 +1,7 @@ -// This file is part of Hangfire. -// Copyright © 2013-2014 Sergey Odinokov. -// -// Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 -// of the License, or any later version. -// -// Hangfire is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with Hangfire. If not, see . +using System; +using DotNetCore.CAP.Models; -using System; -using Hangfire.Common; - -namespace Hangfire.Storage.Monitoring +namespace DotNetCore.CAP.Dashboard.Monitoring { public class EnqueuedJobDto { @@ -26,7 +10,7 @@ namespace Hangfire.Storage.Monitoring InEnqueuedState = true; } - public Job Job { get; set; } + public Message Message { get; set; } public string State { get; set; } public DateTime? EnqueuedAt { get; set; } public bool InEnqueuedState { get; set; } diff --git a/src/DotNetCore.CAP/Dashboard/Monitoring/FailedJobDto.cs b/src/DotNetCore.CAP/Dashboard/Monitoring/FailedJobDto.cs index 51e7016..75fff5a 100644 --- a/src/DotNetCore.CAP/Dashboard/Monitoring/FailedJobDto.cs +++ b/src/DotNetCore.CAP/Dashboard/Monitoring/FailedJobDto.cs @@ -1,23 +1,7 @@ -// This file is part of Hangfire. -// Copyright © 2013-2014 Sergey Odinokov. -// -// Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 -// of the License, or any later version. -// -// Hangfire is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with Hangfire. If not, see . +using System; +using DotNetCore.CAP.Models; -using System; -using Hangfire.Common; - -namespace Hangfire.Storage.Monitoring +namespace DotNetCore.CAP.Dashboard.Monitoring { public class FailedJobDto { @@ -26,7 +10,7 @@ namespace Hangfire.Storage.Monitoring InFailedState = true; } - public Job Job { get; set; } + public Message Message { get; set; } public string Reason { get; set; } public DateTime? FailedAt { get; set; } public string ExceptionType { get; set; } diff --git a/src/DotNetCore.CAP/Dashboard/Monitoring/FetchedJobDto.cs b/src/DotNetCore.CAP/Dashboard/Monitoring/FetchedJobDto.cs index 2f57176..7794982 100644 --- a/src/DotNetCore.CAP/Dashboard/Monitoring/FetchedJobDto.cs +++ b/src/DotNetCore.CAP/Dashboard/Monitoring/FetchedJobDto.cs @@ -1,27 +1,11 @@ -// This file is part of Hangfire. -// Copyright © 2013-2014 Sergey Odinokov. -// -// Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 -// of the License, or any later version. -// -// Hangfire is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with Hangfire. If not, see . +using System; +using DotNetCore.CAP.Models; -using System; -using Hangfire.Common; - -namespace Hangfire.Storage.Monitoring +namespace DotNetCore.CAP.Dashboard.Monitoring { public class FetchedJobDto { - public Job Job { get; set; } + public Message Message { get; set; } public string State { get; set; } public DateTime? FetchedAt { get; set; } } diff --git a/src/DotNetCore.CAP/Dashboard/Monitoring/JobDetailsDto.cs b/src/DotNetCore.CAP/Dashboard/Monitoring/JobDetailsDto.cs index fd96e06..f63a8ba 100644 --- a/src/DotNetCore.CAP/Dashboard/Monitoring/JobDetailsDto.cs +++ b/src/DotNetCore.CAP/Dashboard/Monitoring/JobDetailsDto.cs @@ -1,28 +1,12 @@ -// This file is part of Hangfire. -// Copyright © 2013-2014 Sergey Odinokov. -// -// Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 -// of the License, or any later version. -// -// Hangfire is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with Hangfire. If not, see . - -using System; +using System; using System.Collections.Generic; -using Hangfire.Common; +using DotNetCore.CAP.Models; -namespace Hangfire.Storage.Monitoring +namespace DotNetCore.CAP.Dashboard.Monitoring { public class JobDetailsDto { - public Job Job { get; set; } + public Message Message { get; set; } public DateTime? CreatedAt { get; set; } public IDictionary Properties { get; set; } public IList History { get; set; } diff --git a/src/DotNetCore.CAP/Dashboard/Monitoring/JobList.cs b/src/DotNetCore.CAP/Dashboard/Monitoring/JobList.cs index 69a38e9..5099afb 100644 --- a/src/DotNetCore.CAP/Dashboard/Monitoring/JobList.cs +++ b/src/DotNetCore.CAP/Dashboard/Monitoring/JobList.cs @@ -1,22 +1,6 @@ -// This file is part of Hangfire. -// Copyright © 2013-2014 Sergey Odinokov. -// -// Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 -// of the License, or any later version. -// -// Hangfire is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with Hangfire. If not, see . - using System.Collections.Generic; -namespace Hangfire.Storage.Monitoring +namespace DotNetCore.CAP.Dashboard.Monitoring { public class JobList : List> { diff --git a/src/DotNetCore.CAP/Dashboard/Monitoring/ProcessingJobDto.cs b/src/DotNetCore.CAP/Dashboard/Monitoring/ProcessingJobDto.cs index ef6b9c5..7e5f81e 100644 --- a/src/DotNetCore.CAP/Dashboard/Monitoring/ProcessingJobDto.cs +++ b/src/DotNetCore.CAP/Dashboard/Monitoring/ProcessingJobDto.cs @@ -1,23 +1,7 @@ -// This file is part of Hangfire. -// Copyright © 2013-2014 Sergey Odinokov. -// -// Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 -// of the License, or any later version. -// -// Hangfire is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with Hangfire. If not, see . +using System; +using DotNetCore.CAP.Models; -using System; -using Hangfire.Common; - -namespace Hangfire.Storage.Monitoring +namespace DotNetCore.CAP.Dashboard.Monitoring { public class ProcessingJobDto { @@ -26,7 +10,7 @@ namespace Hangfire.Storage.Monitoring InProcessingState = true; } - public Job Job { get; set; } + public Message Message { get; set; } public bool InProcessingState { get; set; } public string ServerId { get; set; } public DateTime? StartedAt { get; set; } diff --git a/src/DotNetCore.CAP/Dashboard/Monitoring/QueueWithTopEnqueuedJobsDto.cs b/src/DotNetCore.CAP/Dashboard/Monitoring/QueueWithTopEnqueuedJobsDto.cs index 14fb5dc..ec40525 100644 --- a/src/DotNetCore.CAP/Dashboard/Monitoring/QueueWithTopEnqueuedJobsDto.cs +++ b/src/DotNetCore.CAP/Dashboard/Monitoring/QueueWithTopEnqueuedJobsDto.cs @@ -1,20 +1,4 @@ -// This file is part of Hangfire. -// Copyright © 2013-2014 Sergey Odinokov. -// -// Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 -// of the License, or any later version. -// -// Hangfire is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with Hangfire. If not, see . - -namespace Hangfire.Storage.Monitoring +namespace DotNetCore.CAP.Dashboard.Monitoring { public class QueueWithTopEnqueuedJobsDto { diff --git a/src/DotNetCore.CAP/Dashboard/Monitoring/ScheduledJobDto.cs b/src/DotNetCore.CAP/Dashboard/Monitoring/ScheduledJobDto.cs index 13d3644..fa6840e 100644 --- a/src/DotNetCore.CAP/Dashboard/Monitoring/ScheduledJobDto.cs +++ b/src/DotNetCore.CAP/Dashboard/Monitoring/ScheduledJobDto.cs @@ -1,23 +1,7 @@ -// This file is part of Hangfire. -// Copyright © 2013-2014 Sergey Odinokov. -// -// Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 -// of the License, or any later version. -// -// Hangfire is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with Hangfire. If not, see . +using System; +using DotNetCore.CAP.Models; -using System; -using Hangfire.Common; - -namespace Hangfire.Storage.Monitoring +namespace DotNetCore.CAP.Dashboard.Monitoring { public class ScheduledJobDto { @@ -26,7 +10,7 @@ namespace Hangfire.Storage.Monitoring InScheduledState = true; } - public Job Job { get; set; } + public Message Message { get; set; } public DateTime EnqueueAt { get; set; } public DateTime? ScheduledAt { get; set; } public bool InScheduledState { get; set; } diff --git a/src/DotNetCore.CAP/Dashboard/Monitoring/ServerDto.cs b/src/DotNetCore.CAP/Dashboard/Monitoring/ServerDto.cs index 7ae2894..f1932cb 100644 --- a/src/DotNetCore.CAP/Dashboard/Monitoring/ServerDto.cs +++ b/src/DotNetCore.CAP/Dashboard/Monitoring/ServerDto.cs @@ -1,23 +1,7 @@ -// This file is part of Hangfire. -// Copyright © 2013-2014 Sergey Odinokov. -// -// Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 -// of the License, or any later version. -// -// Hangfire is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with Hangfire. If not, see . - -using System; +using System; using System.Collections.Generic; -namespace Hangfire.Storage.Monitoring +namespace DotNetCore.CAP.Dashboard.Monitoring { public class ServerDto { diff --git a/src/DotNetCore.CAP/Dashboard/Monitoring/StateHistoryDto.cs b/src/DotNetCore.CAP/Dashboard/Monitoring/StateHistoryDto.cs index 0506c2c..0f391e8 100644 --- a/src/DotNetCore.CAP/Dashboard/Monitoring/StateHistoryDto.cs +++ b/src/DotNetCore.CAP/Dashboard/Monitoring/StateHistoryDto.cs @@ -1,23 +1,7 @@ -// This file is part of Hangfire. -// Copyright © 2013-2014 Sergey Odinokov. -// -// Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 -// of the License, or any later version. -// -// Hangfire is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with Hangfire. If not, see . - -using System; +using System; using System.Collections.Generic; -namespace Hangfire.Storage.Monitoring +namespace DotNetCore.CAP.Dashboard.Monitoring { public class StateHistoryDto { diff --git a/src/DotNetCore.CAP/Dashboard/Monitoring/StatisticsDto.cs b/src/DotNetCore.CAP/Dashboard/Monitoring/StatisticsDto.cs index 3ec9229..56048ac 100644 --- a/src/DotNetCore.CAP/Dashboard/Monitoring/StatisticsDto.cs +++ b/src/DotNetCore.CAP/Dashboard/Monitoring/StatisticsDto.cs @@ -1,20 +1,4 @@ -// This file is part of Hangfire. -// Copyright © 2013-2014 Sergey Odinokov. -// -// Hangfire is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 -// of the License, or any later version. -// -// Hangfire is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with Hangfire. If not, see . - -namespace Hangfire.Storage.Monitoring +namespace DotNetCore.CAP.Dashboard.Monitoring { public class StatisticsDto { diff --git a/src/DotNetCore.CAP/Dashboard/Monitoring/SucceededJobDto.cs b/src/DotNetCore.CAP/Dashboard/Monitoring/SucceededJobDto.cs index b222b99..6487d0a 100644 --- a/src/DotNetCore.CAP/Dashboard/Monitoring/SucceededJobDto.cs +++ b/src/DotNetCore.CAP/Dashboard/Monitoring/SucceededJobDto.cs @@ -1,7 +1,7 @@ using System; -using Hangfire.Common; +using DotNetCore.CAP.Models; -namespace Hangfire.Storage.Monitoring +namespace DotNetCore.CAP.Dashboard.Monitoring { public class SucceededJobDto { @@ -10,7 +10,7 @@ namespace Hangfire.Storage.Monitoring InSucceededState = true; } - public Job Job { get; set; } + public Message Message { get; set; } public object Result { get; set; } public long? TotalDuration { get; set; } public DateTime? SucceededAt { get; set; } diff --git a/src/DotNetCore.CAP/Dashboard/NavigationMenu.cs b/src/DotNetCore.CAP/Dashboard/NavigationMenu.cs new file mode 100644 index 0000000..0ed59c0 --- /dev/null +++ b/src/DotNetCore.CAP/Dashboard/NavigationMenu.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using DotNetCore.CAP.Dashboard.Resources; + +namespace DotNetCore.CAP.Dashboard +{ + public static class NavigationMenu + { + public static readonly List> Items = new List>(); + + static NavigationMenu() + { + Items.Add(page => new MenuItem(Strings.NavigationMenu_Jobs, page.Url.LinkToQueues()) + { + Active = page.RequestPath.StartsWith("/jobs"), + Metrics = new [] + { + DashboardMetrics.EnqueuedCountOrNull, + DashboardMetrics.FailedCountOrNull + } + }); + + Items.Add(page => new MenuItem(Strings.NavigationMenu_Retries, page.Url.To("/retries")) + { + Active = page.RequestPath.StartsWith("/retries"), + Metric = DashboardMetrics.RetriesCount + }); + + Items.Add(page => new MenuItem(Strings.NavigationMenu_RecurringJobs, page.Url.To("/recurring")) + { + Active = page.RequestPath.StartsWith("/recurring"), + Metric = DashboardMetrics.RecurringJobCount + }); + + Items.Add(page => new MenuItem(Strings.NavigationMenu_Servers, page.Url.To("/servers")) + { + Active = page.RequestPath.Equals("/servers"), + Metric = DashboardMetrics.ServerCount + }); + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Dashboard/NonEscapedString.cs b/src/DotNetCore.CAP/Dashboard/NonEscapedString.cs new file mode 100644 index 0000000..f95cf4f --- /dev/null +++ b/src/DotNetCore.CAP/Dashboard/NonEscapedString.cs @@ -0,0 +1,17 @@ +namespace DotNetCore.CAP.Dashboard +{ + public class NonEscapedString + { + private readonly string _value; + + public NonEscapedString(string value) + { + _value = value; + } + + public override string ToString() + { + return _value; + } + } +} diff --git a/src/DotNetCore.CAP/Dashboard/Pager.cs b/src/DotNetCore.CAP/Dashboard/Pager.cs new file mode 100644 index 0000000..b964569 --- /dev/null +++ b/src/DotNetCore.CAP/Dashboard/Pager.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; + +namespace DotNetCore.CAP.Dashboard +{ + public class Pager + { + private const int PageItemsCount = 7; + private const int DefaultRecordsPerPage = 10; + + private int _startPageIndex = 1; + private int _endPageIndex = 1; + + public Pager(int from, int perPage, long total) + { + FromRecord = from >= 0 ? from : 0; + RecordsPerPage = perPage > 0 ? perPage : DefaultRecordsPerPage; + TotalRecordCount = total; + CurrentPage = FromRecord / RecordsPerPage + 1; + TotalPageCount = (int)Math.Ceiling((double)TotalRecordCount / RecordsPerPage); + + PagerItems = GenerateItems(); + } + + public string BasePageUrl { get; set; } + + public int FromRecord { get; } + public int RecordsPerPage { get; } + public int CurrentPage { get; } + + public int TotalPageCount { get; } + public long TotalRecordCount { get; } + + internal ICollection PagerItems { get; } + + public string PageUrl(int page) + { + if (page < 1 || page > TotalPageCount) return "#"; + + return BasePageUrl + "?from=" + (page - 1) * RecordsPerPage + "&count=" + RecordsPerPage; + } + + public string RecordsPerPageUrl(int perPage) + { + if (perPage <= 0) return "#"; + return BasePageUrl + "?from=0&count=" + perPage; + } + + private ICollection GenerateItems() + { + // start page index + _startPageIndex = CurrentPage - PageItemsCount / 2; + if (_startPageIndex + PageItemsCount > TotalPageCount) + _startPageIndex = TotalPageCount + 1 - PageItemsCount; + if (_startPageIndex < 1) + _startPageIndex = 1; + + // end page index + _endPageIndex = _startPageIndex + PageItemsCount - 1; + if (_endPageIndex > TotalPageCount) + _endPageIndex = TotalPageCount; + + var pagerItems = new List(); + if (TotalPageCount == 0) return pagerItems; + + AddPrevious(pagerItems); + + // first page + if (_startPageIndex > 1) + pagerItems.Add(new Item(1, false, ItemType.Page)); + + // more page before numeric page buttons + AddMoreBefore(pagerItems); + + // numeric page + AddPageNumbers(pagerItems); + + // more page after numeric page buttons + AddMoreAfter(pagerItems); + + // last page + if (_endPageIndex < TotalPageCount) + pagerItems.Add(new Item(TotalPageCount, false, ItemType.Page)); + + // Next page + AddNext(pagerItems); + + return pagerItems; + } + + private void AddPrevious(ICollection results) + { + var item = new Item(CurrentPage - 1, CurrentPage == 1, ItemType.PrevPage); + results.Add(item); + } + + private void AddMoreBefore(ICollection results) + { + if (_startPageIndex > 2) + { + var index = _startPageIndex - 1; + if (index < 1) index = 1; + var item = new Item(index, false, ItemType.MorePage); + results.Add(item); + } + } + + private void AddMoreAfter(ICollection results) + { + if (_endPageIndex < TotalPageCount - 1) + { + var index = _startPageIndex + PageItemsCount; + if (index > TotalPageCount) { index = TotalPageCount; } + var item = new Item(index, false, ItemType.MorePage); + results.Add(item); + } + } + + private void AddPageNumbers(ICollection results) + { + for (var pageIndex = _startPageIndex; pageIndex <= _endPageIndex; pageIndex++) + { + var item = new Item(pageIndex, false, ItemType.Page); + results.Add(item); + } + } + + private void AddNext(ICollection results) + { + var item = new Item(CurrentPage + 1, CurrentPage >= TotalPageCount, ItemType.NextPage); + results.Add(item); + } + + internal class Item + { + public Item(int pageIndex, bool disabled, ItemType type) + { + PageIndex = pageIndex; + Disabled = disabled; + Type = type; + } + + public int PageIndex { get; } + public bool Disabled { get; } + public ItemType Type { get; } + } + + internal enum ItemType + { + Page, + PrevPage, + NextPage, + MorePage + } + } +} diff --git a/src/DotNetCore.CAP/Dashboard/Views/AwaitingJobsPage.cshtml b/src/DotNetCore.CAP/Dashboard/Pages/AwaitingJobsPage.cshtml similarity index 92% rename from src/DotNetCore.CAP/Dashboard/Views/AwaitingJobsPage.cshtml rename to src/DotNetCore.CAP/Dashboard/Pages/AwaitingJobsPage.cshtml index d3f5e56..582e068 100644 --- a/src/DotNetCore.CAP/Dashboard/Views/AwaitingJobsPage.cshtml +++ b/src/DotNetCore.CAP/Dashboard/Pages/AwaitingJobsPage.cshtml @@ -1,11 +1,10 @@ @* Generator: Template TypeVisibility: Internal GeneratePrettyNames: true *@ @using System @using System.Collections.Generic -@using Hangfire.Dashboard -@using Hangfire.Dashboard.Pages -@using Hangfire.Dashboard.Resources -@using Hangfire.States -@using Hangfire.Storage +@using DotNetCore.CAP +@using DotNetCore.CAP.Dashboard +@using DotNetCore.CAP.Dashboard.Pages +@using DotNetCore.CAP.Dashboard.Resources @inherits RazorPage @{ Layout = new LayoutPage(Strings.AwaitingJobsPage_Title); @@ -20,7 +19,7 @@ using (var connection = Storage.GetConnection()) { - var storageConnection = connection as JobStorageConnection; + var storageConnection = connection as IStorageConnection; if (storageConnection != null) { @@ -83,7 +82,7 @@ @foreach (var jobId in jobIds) { - JobData jobData; + MessageData jobData; StateData stateData; StateData parentStateData = null; @@ -92,10 +91,10 @@ jobData = connection.GetJobData(jobId); stateData = connection.GetStateData(jobId); - if (stateData != null && stateData.Name == AwaitingState.StateName) - { - parentStateData = connection.GetStateData(stateData.Data["ParentId"]); - } + //if (stateData != null && stateData.Name == AwaitingState.StateName) + //{ + // parentStateData = connection.GetStateData(stateData.Data["ParentId"]); + //} } @@ -112,7 +111,7 @@ else { - @Html.JobNameLink(jobId, jobData.Job) + @Html.JobNameLink(jobId, jobData.Message) @if (stateData != null && stateData.Data.ContainsKey("Options") && !String.IsNullOrWhiteSpace(stateData.Data["Options"])) diff --git a/src/DotNetCore.CAP/Dashboard/Views/BlockMetric.cs b/src/DotNetCore.CAP/Dashboard/Pages/BlockMetric.cs similarity index 83% rename from src/DotNetCore.CAP/Dashboard/Views/BlockMetric.cs rename to src/DotNetCore.CAP/Dashboard/Pages/BlockMetric.cs index 16a5fad..f102675 100644 --- a/src/DotNetCore.CAP/Dashboard/Views/BlockMetric.cs +++ b/src/DotNetCore.CAP/Dashboard/Pages/BlockMetric.cs @@ -1,4 +1,4 @@ -namespace Hangfire.Dashboard.Pages +namespace DotNetCore.CAP.Dashboard.Pages { partial class BlockMetric { diff --git a/src/DotNetCore.CAP/Dashboard/Views/Breadcrumbs.cs b/src/DotNetCore.CAP/Dashboard/Pages/Breadcrumbs.cs similarity index 88% rename from src/DotNetCore.CAP/Dashboard/Views/Breadcrumbs.cs rename to src/DotNetCore.CAP/Dashboard/Pages/Breadcrumbs.cs index 9ef7642..ad66efa 100644 --- a/src/DotNetCore.CAP/Dashboard/Views/Breadcrumbs.cs +++ b/src/DotNetCore.CAP/Dashboard/Pages/Breadcrumbs.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace Hangfire.Dashboard.Pages +namespace DotNetCore.CAP.Dashboard.Pages { partial class Breadcrumbs { diff --git a/src/DotNetCore.CAP/Dashboard/Views/HomePage.cs b/src/DotNetCore.CAP/Dashboard/Pages/HomePage.cs similarity index 52% rename from src/DotNetCore.CAP/Dashboard/Views/HomePage.cs rename to src/DotNetCore.CAP/Dashboard/Pages/HomePage.cs index 395f40b..d2ed24e 100644 --- a/src/DotNetCore.CAP/Dashboard/Views/HomePage.cs +++ b/src/DotNetCore.CAP/Dashboard/Pages/HomePage.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; -namespace Hangfire.Dashboard.Pages +namespace DotNetCore.CAP.Dashboard.Pages { - partial class HomePage + internal partial class HomePage { - public static readonly List Metrics = new List(); + public static readonly List Metrics = new List(); } } diff --git a/src/DotNetCore.CAP/Dashboard/Views/HomePage.cshtml b/src/DotNetCore.CAP/Dashboard/Pages/HomePage.cshtml similarity index 95% rename from src/DotNetCore.CAP/Dashboard/Views/HomePage.cshtml rename to src/DotNetCore.CAP/Dashboard/Pages/HomePage.cshtml index 5ae1bf9..099064a 100644 --- a/src/DotNetCore.CAP/Dashboard/Views/HomePage.cshtml +++ b/src/DotNetCore.CAP/Dashboard/Pages/HomePage.cshtml @@ -1,9 +1,9 @@ @* Generator: Template TypeVisibility: Internal GeneratePrettyNames: True *@ @using System @using System.Collections.Generic -@using Hangfire.Dashboard -@using Hangfire.Dashboard.Pages -@using Hangfire.Dashboard.Resources +@using DotNetCore.CAP.Dashboard +@using DotNetCore.CAP.Dashboard.Pages +@using DotNetCore.CAP.Dashboard.Resources @using Newtonsoft.Json @inherits RazorPage @{ diff --git a/src/DotNetCore.CAP/Dashboard/Views/HomePage.generated.cs b/src/DotNetCore.CAP/Dashboard/Pages/HomePage.generated.cs similarity index 98% rename from src/DotNetCore.CAP/Dashboard/Views/HomePage.generated.cs rename to src/DotNetCore.CAP/Dashboard/Pages/HomePage.generated.cs index ca7681f..8d67de0 100644 --- a/src/DotNetCore.CAP/Dashboard/Views/HomePage.generated.cs +++ b/src/DotNetCore.CAP/Dashboard/Pages/HomePage.generated.cs @@ -9,7 +9,7 @@ // //------------------------------------------------------------------------------ -namespace Hangfire.Dashboard.Pages +namespace DotNetCore.CAP.Dashboard.Pages { #line 2 "..\..\Dashboard\Pages\HomePage.cshtml" @@ -27,19 +27,19 @@ namespace Hangfire.Dashboard.Pages using System.Text; #line 4 "..\..\Dashboard\Pages\HomePage.cshtml" - using Hangfire.Dashboard; + using DotNetCore.CAP.Dashboard; #line default #line hidden #line 5 "..\..\Dashboard\Pages\HomePage.cshtml" - using Hangfire.Dashboard.Pages; + using DotNetCore.CAP.Dashboard.Pages; #line default #line hidden #line 6 "..\..\Dashboard\Pages\HomePage.cshtml" - using Hangfire.Dashboard.Resources; + using DotNetCore.CAP.Dashboard.Resources; #line default #line hidden diff --git a/src/DotNetCore.CAP/Dashboard/Views/InlineMetric.cs b/src/DotNetCore.CAP/Dashboard/Pages/InlineMetric.cs similarity index 70% rename from src/DotNetCore.CAP/Dashboard/Views/InlineMetric.cs rename to src/DotNetCore.CAP/Dashboard/Pages/InlineMetric.cs index b754d7d..d02f228 100644 --- a/src/DotNetCore.CAP/Dashboard/Views/InlineMetric.cs +++ b/src/DotNetCore.CAP/Dashboard/Pages/InlineMetric.cs @@ -1,6 +1,6 @@ -namespace Hangfire.Dashboard.Pages +namespace DotNetCore.CAP.Dashboard.Pages { - partial class InlineMetric + internal partial class InlineMetric { public InlineMetric(DashboardMetric dashboardMetric) { diff --git a/src/DotNetCore.CAP/Dashboard/Views/LayoutPage.cs b/src/DotNetCore.CAP/Dashboard/Pages/LayoutPage.cs similarity index 79% rename from src/DotNetCore.CAP/Dashboard/Views/LayoutPage.cs rename to src/DotNetCore.CAP/Dashboard/Pages/LayoutPage.cs index 9ade99b..9cbd688 100644 --- a/src/DotNetCore.CAP/Dashboard/Views/LayoutPage.cs +++ b/src/DotNetCore.CAP/Dashboard/Pages/LayoutPage.cs @@ -1,4 +1,4 @@ -namespace Hangfire.Dashboard.Pages +namespace DotNetCore.CAP.Dashboard.Pages { partial class LayoutPage { diff --git a/src/DotNetCore.CAP/Dashboard/Views/LayoutPage.cshtml b/src/DotNetCore.CAP/Dashboard/Pages/LayoutPage.cshtml similarity index 96% rename from src/DotNetCore.CAP/Dashboard/Views/LayoutPage.cshtml rename to src/DotNetCore.CAP/Dashboard/Pages/LayoutPage.cshtml index 4338c87..3d927df 100644 --- a/src/DotNetCore.CAP/Dashboard/Views/LayoutPage.cshtml +++ b/src/DotNetCore.CAP/Dashboard/Pages/LayoutPage.cshtml @@ -2,9 +2,9 @@ @using System @using System.Globalization @using System.Reflection -@using Hangfire.Dashboard -@using Hangfire.Dashboard.Pages -@using Hangfire.Dashboard.Resources +@using DotNetCore.CAP.Dashboard +@using DotNetCore.CAP.Dashboard.Pages +@using DotNetCore.CAP.Dashboard.Resources @inherits RazorPage diff --git a/src/DotNetCore.CAP/Dashboard/Views/LayoutPage.generated.cs b/src/DotNetCore.CAP/Dashboard/Pages/LayoutPage.generated.cs similarity index 98% rename from src/DotNetCore.CAP/Dashboard/Views/LayoutPage.generated.cs rename to src/DotNetCore.CAP/Dashboard/Pages/LayoutPage.generated.cs index 91ab133..1bc5b6a 100644 --- a/src/DotNetCore.CAP/Dashboard/Views/LayoutPage.generated.cs +++ b/src/DotNetCore.CAP/Dashboard/Pages/LayoutPage.generated.cs @@ -9,7 +9,7 @@ // //------------------------------------------------------------------------------ -namespace Hangfire.Dashboard.Pages +namespace DotNetCore.CAP.Dashboard.Pages { #line 2 "..\..\Dashboard\Pages\LayoutPage.cshtml" @@ -34,19 +34,19 @@ namespace Hangfire.Dashboard.Pages using System.Text; #line 5 "..\..\Dashboard\Pages\LayoutPage.cshtml" - using Hangfire.Dashboard; + using DotNetCore.CAP.Dashboard; #line default #line hidden #line 6 "..\..\Dashboard\Pages\LayoutPage.cshtml" - using Hangfire.Dashboard.Pages; + using DotNetCore.CAP.Dashboard.Pages; #line default #line hidden #line 7 "..\..\Dashboard\Pages\LayoutPage.cshtml" - using Hangfire.Dashboard.Resources; + using DotNetCore.CAP.Dashboard.Resources; #line default #line hidden diff --git a/src/DotNetCore.CAP/Dashboard/Views/ProcessingJobsPage.cshtml b/src/DotNetCore.CAP/Dashboard/Pages/ProcessingJobsPage.cshtml similarity index 97% rename from src/DotNetCore.CAP/Dashboard/Views/ProcessingJobsPage.cshtml rename to src/DotNetCore.CAP/Dashboard/Pages/ProcessingJobsPage.cshtml index 6850ed7..af50fb9 100644 --- a/src/DotNetCore.CAP/Dashboard/Views/ProcessingJobsPage.cshtml +++ b/src/DotNetCore.CAP/Dashboard/Pages/ProcessingJobsPage.cshtml @@ -1,9 +1,9 @@ @* Generator: Template TypeVisibility: Internal GeneratePrettyNames: True *@ @using System @using System.Linq -@using Hangfire.Dashboard -@using Hangfire.Dashboard.Pages -@using Hangfire.Dashboard.Resources +@using DotNetCore.CAP.Dashboard +@using DotNetCore.CAP.Dashboard.Pages +@using DotNetCore.CAP.Dashboard.Resources @inherits RazorPage @{ Layout = new LayoutPage(Strings.ProcessingJobsPage_Title); diff --git a/src/DotNetCore.CAP/Dashboard/Pages/SidebarMenu.cs b/src/DotNetCore.CAP/Dashboard/Pages/SidebarMenu.cs new file mode 100644 index 0000000..285fcde --- /dev/null +++ b/src/DotNetCore.CAP/Dashboard/Pages/SidebarMenu.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace DotNetCore.CAP.Dashboard.Pages +{ + partial class SidebarMenu + { + public SidebarMenu( IEnumerable> items) + { + if (items == null) throw new ArgumentNullException(nameof(items)); + Items = items; + } + + public IEnumerable> Items { get; } + } +} diff --git a/src/DotNetCore.CAP/Dashboard/Views/SucceededJobs.cshtml b/src/DotNetCore.CAP/Dashboard/Pages/SucceededJobs.cshtml similarity index 97% rename from src/DotNetCore.CAP/Dashboard/Views/SucceededJobs.cshtml rename to src/DotNetCore.CAP/Dashboard/Pages/SucceededJobs.cshtml index 05d68a3..581185a 100644 --- a/src/DotNetCore.CAP/Dashboard/Views/SucceededJobs.cshtml +++ b/src/DotNetCore.CAP/Dashboard/Pages/SucceededJobs.cshtml @@ -1,8 +1,8 @@ @* Generator: Template TypeVisibility: Internal GeneratePrettyNames: True *@ @using System -@using Hangfire.Dashboard -@using Hangfire.Dashboard.Pages -@using Hangfire.Dashboard.Resources +@using DotNetCore.CAP.Dashboard +@using DotNetCore.CAP.Dashboard.Pages +@using DotNetCore.CAP.Dashboard.Resources @inherits RazorPage @{ Layout = new LayoutPage(Strings.SucceededJobsPage_Title); diff --git a/src/DotNetCore.CAP/Dashboard/Views/_BlockMetric.cshtml b/src/DotNetCore.CAP/Dashboard/Pages/_BlockMetric.cshtml similarity index 82% rename from src/DotNetCore.CAP/Dashboard/Views/_BlockMetric.cshtml rename to src/DotNetCore.CAP/Dashboard/Pages/_BlockMetric.cshtml index 279f48a..47abdb4 100644 --- a/src/DotNetCore.CAP/Dashboard/Views/_BlockMetric.cshtml +++ b/src/DotNetCore.CAP/Dashboard/Pages/_BlockMetric.cshtml @@ -1,13 +1,13 @@ @* Generator: Template TypeVisibility: Internal GeneratePrettyNames: True TrimLeadingUnderscores : true *@ -@using Hangfire.Dashboard -@using Hangfire.Dashboard.Resources +@using DotNetCore.CAP.Dashboard +@using DotNetCore.CAP.Dashboard.Resources @inherits RazorPage @{ var metric = DashboardMetric.Func(this); var className = metric == null ? "metric-null" : metric.Style.ToClassName(); var highlighted = metric != null && metric.Highlighted ? "highlighted" : null; } -
+
@(metric?.Value)
diff --git a/src/DotNetCore.CAP/Dashboard/Views/_BlockMetric.generated.cs b/src/DotNetCore.CAP/Dashboard/Pages/_BlockMetric.generated.cs similarity index 95% rename from src/DotNetCore.CAP/Dashboard/Views/_BlockMetric.generated.cs rename to src/DotNetCore.CAP/Dashboard/Pages/_BlockMetric.generated.cs index 9eaa616..e8ad9cc 100644 --- a/src/DotNetCore.CAP/Dashboard/Views/_BlockMetric.generated.cs +++ b/src/DotNetCore.CAP/Dashboard/Pages/_BlockMetric.generated.cs @@ -9,7 +9,7 @@ // //------------------------------------------------------------------------------ -namespace Hangfire.Dashboard.Pages +namespace DotNetCore.CAP.Dashboard.Pages { using System; using System.Collections.Generic; @@ -17,13 +17,13 @@ namespace Hangfire.Dashboard.Pages using System.Text; #line 2 "..\..\Dashboard\Pages\_BlockMetric.cshtml" - using Hangfire.Dashboard; + using DotNetCore.CAP.Dashboard; #line default #line hidden #line 3 "..\..\Dashboard\Pages\_BlockMetric.cshtml" - using Hangfire.Dashboard.Resources; + using DotNetCore.CAP.Dashboard.Resources; #line default #line hidden diff --git a/src/DotNetCore.CAP/Dashboard/Views/_Breadcrumbs.cshtml b/src/DotNetCore.CAP/Dashboard/Pages/_Breadcrumbs.cshtml similarity index 90% rename from src/DotNetCore.CAP/Dashboard/Views/_Breadcrumbs.cshtml rename to src/DotNetCore.CAP/Dashboard/Pages/_Breadcrumbs.cshtml index 542c380..590b8d6 100644 --- a/src/DotNetCore.CAP/Dashboard/Views/_Breadcrumbs.cshtml +++ b/src/DotNetCore.CAP/Dashboard/Pages/_Breadcrumbs.cshtml @@ -1,5 +1,5 @@ @* Generator: Template TypeVisibility: Internal GeneratePrettyNames: True TrimLeadingUnderscores : true *@ -@using Hangfire.Dashboard +@using DotNetCore.CAP.Dashboard @inherits RazorPage