From 1ac07b8c5f647c60c6987b1628a9b30173c775b7 Mon Sep 17 00:00:00 2001
From: Dmytro Bogatov <dmytro@dbogatov.org>
Date: Sun, 6 Aug 2017 16:26:07 +0300
Subject: [PATCH] Endpoint for resolve.

---
 client/ts/admin.ts                            | 42 ++++++--
 src/web/Controllers/View/AdminController.cs   | 67 ++++++++++++-
 .../DiscrepancyResolutionViewModel.cs         | 98 +++++++++++++++++++
 src/web/Views/Admin/Index.cshtml              | 38 ++++++-
 .../Components/DiscrepancyCard/Default.cshtml | 35 +++++--
 .../DiscrepancyCardViewComponent.cs           |  5 +-
 6 files changed, 268 insertions(+), 17 deletions(-)
 create mode 100644 src/web/ViewModels/DiscrepancyResolutionViewModel.cs

diff --git a/client/ts/admin.ts b/client/ts/admin.ts
index 50fc044..827a6f7 100644
--- a/client/ts/admin.ts
+++ b/client/ts/admin.ts
@@ -6,17 +6,47 @@ import "bootstrap-select"
 import * as Waves from "Waves"
 import "bootstrap"
 
-declare global {
-	interface JQuery {
-		jsonViewer(any): void;
-	}
-}
-
 $(async () => {
 
 	$('.selectpicker').selectpicker();
 
 	Utility.fixUtcTime();
 
+	setupDiscrepancyViewer();
+
 	document.dispatchEvent(new Event("page-ready"));
 });
+
+function setupDiscrepancyViewer() {
+	var resolvedLoaded = 10;
+
+	$("#load-more-resolved-btn").click(() => {
+
+		for (var index = resolvedLoaded + 1; index <= resolvedLoaded + 10; index++) {
+			$(`.discrepancy-card[data-number="${index}"]`).show();
+		}
+
+		resolvedLoaded += 10;
+
+		$("#load-less-resolved-btn").show();
+
+		if (resolvedLoaded >= $(".discrepancy-resolved").length) {
+			$("#load-more-resolved-btn").hide();
+		}
+	});
+
+	$("#load-less-resolved-btn").click(() => {
+
+		for (var index = resolvedLoaded; index > resolvedLoaded - 10; index--) {
+			$(`.discrepancy-card[data-number="${index}"]`).hide();
+		}
+
+		resolvedLoaded -= 10;
+
+		$("#load-more-resolved-btn").show();
+
+		if (resolvedLoaded <= 10) {
+			$("#load-less-resolved-btn").hide();
+		}
+	});
+}
diff --git a/src/web/Controllers/View/AdminController.cs b/src/web/Controllers/View/AdminController.cs
index b8862a9..599c3ef 100644
--- a/src/web/Controllers/View/AdminController.cs
+++ b/src/web/Controllers/View/AdminController.cs
@@ -34,6 +34,8 @@ namespace StatusMonitor.Web.Controllers.View
 		private readonly IServiceProvider _provider;
 		private readonly ICleanService _cleanService;
 		private readonly IDataContext _context;
+		private readonly INotificationService _notification;
+		private readonly IConfiguration _config;
 
 
 		public AdminController(
@@ -42,7 +44,9 @@ namespace StatusMonitor.Web.Controllers.View
 			IMetricService metricService,
 			IServiceProvider provider,
 			ICleanService cleanService,
-			IDataContext context
+			IDataContext context,
+			INotificationService notification,
+			IConfiguration congig
 		)
 		{
 			_metricService = metricService;
@@ -51,6 +55,8 @@ namespace StatusMonitor.Web.Controllers.View
 			_provider = provider;
 			_cleanService = cleanService;
 			_context = context;
+			_notification = notification;
+			_config = congig;
 		}
 
 		public async Task<IActionResult> Index()
@@ -84,6 +90,65 @@ namespace StatusMonitor.Web.Controllers.View
 			}
 		}
 
+		[HttpPost]
+		[ServiceFilter(typeof(ModelValidation))]
+		[AutoValidateAntiforgeryToken]
+		public async Task<IActionResult> ResolveDiscrepancy(DiscrepancyResolutionViewModel model)
+		{
+			if (
+				await _context
+					.Discrepancies
+					.AnyAsync(
+						d =>
+							d.DateFirstOffense == model.DateFirstOffense &&
+							d.MetricSource == model.Source &&
+							d.MetricType == model.EnumMetricType &&
+							d.Type == model.EnumDiscrepancyType
+					)
+			)
+			{
+
+				var discrepancy =
+					await _context
+						.Discrepancies
+						.SingleAsync(
+							d =>
+								d.DateFirstOffense == model.DateFirstOffense &&
+								d.MetricSource == model.Source &&
+								d.MetricType == model.EnumMetricType &&
+								d.Type == model.EnumDiscrepancyType
+						);
+
+				if (discrepancy.Resolved)
+				{
+					TempData["MessageSeverity"] = "warning";
+					TempData["MessageContent"] = $"Discrepancy {model.EnumDiscrepancyType} from {model.EnumMetricType} of {model.Source} at {model.DateFirstOffense} (UTC) has been already resolved.";
+				}
+				else
+				{
+					discrepancy.Resolved = true;
+					discrepancy.DateResolved = DateTime.UtcNow;
+
+					await _notification.ScheduleNotificationAsync(
+						$"Discrepancy \"{discrepancy.ToStringWithTimeZone(_config["ServiceManager:NotificationService:TimeZone"])}\" has been resolved!",
+						NotificationSeverity.Medium
+					);
+
+					await _context.SaveChangesAsync();
+
+					TempData["MessageSeverity"] = "success";
+					TempData["MessageContent"] = $"Discrepancy {model.EnumDiscrepancyType} from {model.EnumMetricType} of {model.Source} at {model.DateFirstOffense} (UTC) has been resolved.";
+				}
+
+				return RedirectToAction("Index", "Admin");
+			}
+			else
+			{
+				return NotFound($"Discrepancy for the following request not found. {model}");
+			}
+
+		}
+
 		public IActionResult Metric([FromQuery] string metric)
 		{
 			if (metric == null)
diff --git a/src/web/ViewModels/DiscrepancyResolutionViewModel.cs b/src/web/ViewModels/DiscrepancyResolutionViewModel.cs
new file mode 100644
index 0000000..57707d8
--- /dev/null
+++ b/src/web/ViewModels/DiscrepancyResolutionViewModel.cs
@@ -0,0 +1,98 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using StatusMonitor.Shared.Extensions;
+using StatusMonitor.Shared.Models.Entities;
+
+namespace StatusMonitor.Web.ViewModels
+{
+	public class DiscrepancyResolutionViewModel
+	{
+		public DateTime DateFirstOffense { get; set; }
+
+		/// <summary>
+		/// Alias for DateFirstOffense.
+		/// </summary>
+		[Required]
+		public string Date
+		{
+			get
+			{
+				return DateFirstOffense.ToString();
+			}
+			set
+			{
+				try
+				{
+					DateFirstOffense = new DateTime(Convert.ToInt64(value));
+				}
+				catch (System.Exception)
+				{
+					throw new ArgumentException("Invalid Date parameter.");
+				}
+			}
+		}
+
+		public DiscrepancyType EnumDiscrepancyType { get; set; }
+
+		/// <summary>
+		/// Alias for EnumDiscrepancyType.
+		/// </summary>
+		[Required]
+		public string DiscrepancyType
+		{
+			get
+			{
+				return EnumDiscrepancyType.ToString();
+			}
+			set
+			{
+				try
+				{
+					EnumDiscrepancyType = value.ToEnum<DiscrepancyType>();
+				}
+				catch (System.Exception)
+				{
+					throw new ArgumentException("Invalid DiscrepancyType parameter.");
+				}
+			}
+		}
+
+		public Metrics EnumMetricType { get; set; }
+
+		/// <summary>
+		/// Alias for EnumMetricType.
+		/// </summary>
+		[Required]
+		public string MetricType
+		{
+			get
+			{
+				return EnumMetricType.ToString();
+			}
+			set
+			{
+				try
+				{
+					EnumMetricType = value.ToEnum<Metrics>();
+				}
+				catch (System.Exception)
+				{
+					throw new ArgumentException("Invalid MetricType parameter.");
+				}
+			}
+		}
+
+		/// <summary>
+		/// Source identifier. May be server id or website URL.
+		/// </summary>
+		[Required]
+		[StringLength(32)]
+		[RegularExpression("[a-z0-9\\.\\-]+")]
+		public string Source { get; set; }
+
+		public override string ToString()
+		{
+			return $"Discrepancy removal model: type {DiscrepancyType} from {MetricType} of {Source} at {DateFirstOffense}.";
+		}
+	}
+}
diff --git a/src/web/Views/Admin/Index.cshtml b/src/web/Views/Admin/Index.cshtml
index b57c0a0..bc134a6 100644
--- a/src/web/Views/Admin/Index.cshtml
+++ b/src/web/Views/Admin/Index.cshtml
@@ -149,7 +149,11 @@
 										{
 											@foreach (var dicrepancy in ((IEnumerable<Discrepancy>)ViewBag.Discrepancies).Where(d => !d.Resolved))
 											{
-												<vc:discrepancy-card model=dicrepancy></vc:discrepancy-card>
+												<vc:discrepancy-card 
+													model=dicrepancy
+													number=0
+													hidden=false
+												></vc:discrepancy-card>
 											}
 										}
 										else
@@ -160,10 +164,40 @@
                                     <div role="tabpanel" class="tab-pane" id="resovled">
                                         @if ( ((IEnumerable<Discrepancy>)ViewBag.Discrepancies).Any(d => d.Resolved) )
 										{
+											var number = 1;
+
 											@foreach (var dicrepancy in ((IEnumerable<Discrepancy>)ViewBag.Discrepancies).Where(d => d.Resolved))
 											{
-												<vc:discrepancy-card model=dicrepancy></vc:discrepancy-card>
+												var hidden = number > 10;
+
+												<vc:discrepancy-card 
+													model=dicrepancy 
+													number=number
+													hidden=hidden
+												></vc:discrepancy-card>
+
+												number++;
 											}
+											<div class="w-100 text-center p-t-10">
+
+												<button 
+													type="type" 
+													id="load-more-resolved-btn" 
+													class="btn btn-primary btn-md waves-effect"
+													style="@( ((IEnumerable<Discrepancy>)ViewBag.Discrepancies).Count(d => d.Resolved) <= 10 ? "display: none;" : "")"
+												>
+													Show another 10
+												</button>
+
+												<button 
+													type="type" 
+													id="load-less-resolved-btn" 
+													class="btn btn-warning btn-md waves-effect" 
+													style="display: none;"
+												>
+													Hide another 10
+												</button>
+											</div>
 										}
 										else
 										{
diff --git a/src/web/Views/Shared/Components/DiscrepancyCard/Default.cshtml b/src/web/Views/Shared/Components/DiscrepancyCard/Default.cshtml
index 5df9a58..f2623af 100644
--- a/src/web/Views/Shared/Components/DiscrepancyCard/Default.cshtml
+++ b/src/web/Views/Shared/Components/DiscrepancyCard/Default.cshtml
@@ -1,19 +1,40 @@
-<div class="list-group-item media">
+<div 
+	class="list-group-item media discrepancy-card discrepancy-@(Model.Resolved ? "resolved" : "unresolved")" 
+	data-number="@ViewBag.Number" 
+	style="@(ViewBag.Hidden ? "display: none;" : "")"
+>
 	<div class="pull-left">
-		<i class="zmdi zmdi-@(Model.Resolved ? "check-circle" : "alert-circle-o") zmdi-hc-3x"></i>
+		<i 
+			class="zmdi zmdi-@(Model.Resolved ? "check-circle" : "alert-circle-o") zmdi-hc-3x"
+			style="color: @(Model.Resolved ? "green" : "red");"
+		>
+		</i>
 	</div>
 
 	@if (!Model.Resolved)
 	{
 		<div class="pull-right">
 			<div class="actions">
-				<!-- TODO form -->
-				<button type="submit" class="btn btn-success btn-md waves-effect">
-					Resolve
-				</button>
+				<form asp-controller="Admin" asp-action="ResolveDiscrepancy" asp-anti-forgery="true">
+
+					<input type="text" hidden name="Date" value="@Model.DateFirstOffense.Ticks">
+					<input type="text" hidden name="DiscrepancyType" value="@Model.Type">
+					<input type="text" hidden name="MetricType" value="@Model.MetricType">
+					<input type="text" hidden name="Source" value="@Model.MetricSource">
+
+					<button type="submit" class="btn btn-success btn-md waves-effect">
+						Resolve
+					</button>
+				</form>
 			</div>
 		</div>
 	}
+	else
+	{
+		<div class="pull-right">
+			<p>#@ViewBag.Number
+		</div>
+	}
 
 	<div class="media-body">
 		<div class="lgi-heading">
@@ -22,7 +43,7 @@
 		<small class="lgi-text">@Model.Description()</small>
 
 		<ul class="lgi-attrs">
-			<li>Date first offense: <utc-time time="@Model.DateFirstOffense" /></li>
+			<li>Date started: <utc-time time="@Model.DateFirstOffense" /></li>
 			@if (Model.Resolved)
 			{
 				<li>Date resolved: <utc-time time="@Model.DateResolved" /></li>
diff --git a/src/web/Views/ViewComponents/DiscrepancyCardViewComponent.cs b/src/web/Views/ViewComponents/DiscrepancyCardViewComponent.cs
index 0b1f277..e5ca31a 100644
--- a/src/web/Views/ViewComponents/DiscrepancyCardViewComponent.cs
+++ b/src/web/Views/ViewComponents/DiscrepancyCardViewComponent.cs
@@ -13,10 +13,13 @@ namespace StatusMonitor.Web.Views.ViewComponents
 	[ViewComponent(Name = "DiscrepancyCard")]
 	public class DiscrepancyCardViewComponent : ViewComponent
 	{
-		public async Task<IViewComponentResult> InvokeAsync(Discrepancy model)
+		public async Task<IViewComponentResult> InvokeAsync(Discrepancy model, int number = 0, bool hidden = false)
 		{
 			await Task.CompletedTask;
 
+			ViewBag.Number = number;
+			ViewBag.Hidden = hidden;
+
 			return View(model);
 		}
 	}
-- 
GitLab