From 331ec3c63479ff74a871a8222b4db5eb720ab76e Mon Sep 17 00:00:00 2001
From: Dmytro Bogatov <dmytro@dbogatov.org>
Date: Sun, 6 Aug 2017 01:37:37 +0300
Subject: [PATCH] Add discrepancy tabs.

---
 src/shared/Models/Entities/Discrepancies.cs   |  16 ++
 src/web/Controllers/View/AdminController.cs   |  10 +-
 src/web/Views/Admin/Index.cshtml              | 236 ++++++++++++------
 .../Components/DiscrepancyCard/Default.cshtml |  33 +++
 .../DiscrepancyCardViewComponent.cs           |  23 ++
 .../AdminController/AdminControllerTest.cs    |  17 +-
 6 files changed, 258 insertions(+), 77 deletions(-)
 create mode 100644 src/web/Views/Shared/Components/DiscrepancyCard/Default.cshtml
 create mode 100644 src/web/Views/ViewComponents/DiscrepancyCardViewComponent.cs

diff --git a/src/shared/Models/Entities/Discrepancies.cs b/src/shared/Models/Entities/Discrepancies.cs
index e69692d..184b75d 100644
--- a/src/shared/Models/Entities/Discrepancies.cs
+++ b/src/shared/Models/Entities/Discrepancies.cs
@@ -43,6 +43,22 @@ namespace StatusMonitor.Shared.Models.Entities
 		}
 
 		public override string ToString() => ToStringWithTimeZone();
+
+		public string Description() {
+			switch (Type)
+			{
+				case DiscrepancyType.GapInData:
+					return "Occurs if difference in timestamps between two consecutive data points exceeds certain value.";
+				case DiscrepancyType.HighLoad:
+					return "Occurs if CPU load exceeds certain value for some number of consecutive data points.";
+				case DiscrepancyType.LowHealth:
+					return "Occurs if system health value drops below certain value for some number of consecutive health reports.";
+				case DiscrepancyType.PingFailedNTimes:
+					return "Occurs if ping fails certain number of consecutive readings.";
+				default:
+					return "Unknown type";
+			}
+		}
 	}
 
 	public enum DiscrepancyType
diff --git a/src/web/Controllers/View/AdminController.cs b/src/web/Controllers/View/AdminController.cs
index a5bf362..b8862a9 100644
--- a/src/web/Controllers/View/AdminController.cs
+++ b/src/web/Controllers/View/AdminController.cs
@@ -33,6 +33,7 @@ namespace StatusMonitor.Web.Controllers.View
 		private readonly IMetricService _metricService;
 		private readonly IServiceProvider _provider;
 		private readonly ICleanService _cleanService;
+		private readonly IDataContext _context;
 
 
 		public AdminController(
@@ -40,7 +41,8 @@ namespace StatusMonitor.Web.Controllers.View
 			ILogger<AdminController> logger,
 			IMetricService metricService,
 			IServiceProvider provider,
-			ICleanService cleanService
+			ICleanService cleanService,
+			IDataContext context
 		)
 		{
 			_metricService = metricService;
@@ -48,6 +50,7 @@ namespace StatusMonitor.Web.Controllers.View
 			_loggingService = loggingService;
 			_provider = provider;
 			_cleanService = cleanService;
+			_context = context;
 		}
 
 		public async Task<IActionResult> Index()
@@ -55,6 +58,11 @@ namespace StatusMonitor.Web.Controllers.View
 			ViewBag.Metrics = await _metricService
 				.GetMetricsAsync();
 
+			ViewBag.Discrepancies = await _context
+				.Discrepancies
+				.OrderByDescending(d => d.DateFirstOffense)
+				.ToListAsync();
+
 			return View();
 		}
 
diff --git a/src/web/Views/Admin/Index.cshtml b/src/web/Views/Admin/Index.cshtml
index a01a95b..b57c0a0 100644
--- a/src/web/Views/Admin/Index.cshtml
+++ b/src/web/Views/Admin/Index.cshtml
@@ -11,91 +11,177 @@
 	</div>
 
 	<div class="row">
-		<div class="col-md-3">
-			<div class="card">
-				<div class="card-header">
-					<h2>
-						Manual cleaunup
-						<small>
-							Run clean service manually with a specified max age
-						</small>
-					</h2>
-				</div>
-				<div class="card-body card-padding">
-					<div class="row">
-						<form 
-							role="form" 
-							asp-controller="Admin" 
-							asp-action="Clean" 
-							method="post"
-							novalidate
-						>
-							<div class="col-md-8 col-sm-12">
-
-								<h5>Set max age</h5>
-
-								<select class="selectpicker" name="maxAge">
-									<option value="0">Everything</option>
-									<option value="1">1 minutes</option>
-									<option value="10">10 minutes</option>
-									<option value="20">30 minutes</option>
-									<option value="60">1 hour</option>
-									<option value="240">4 hours</option>
-									<option value="720">12 hours</option>
-									<option value="1440">1 day</option>
-									<option value="4320">3 days</option>
-									<option value="10080">1 week</option>
-									<option value="43200">1 month</option>
-								</select>
+
+		<div class="col-md-6">
+
+			<div class="row">
+
+				<div class="col-md-6">
+					<div class="card">
+						<div class="card-header">
+							<h2>
+								Manual cleaunup
+								<small>
+									Run clean service manually with a specified max age
+								</small>
+							</h2>
+						</div>
+						<div class="card-body card-padding">
+							<div class="row">
+								<form 
+									role="form" 
+									asp-controller="Admin" 
+									asp-action="Clean" 
+									method="post"
+									novalidate
+								>
+									<div class="col-md-8 col-sm-12">
+
+										<h5>Set max age</h5>
+
+										<select class="selectpicker" name="maxAge">
+											<option value="0">Everything</option>
+											<option value="1">1 minutes</option>
+											<option value="10">10 minutes</option>
+											<option value="20">30 minutes</option>
+											<option value="60">1 hour</option>
+											<option value="240">4 hours</option>
+											<option value="720">12 hours</option>
+											<option value="1440">1 day</option>
+											<option value="4320">3 days</option>
+											<option value="10080">1 week</option>
+											<option value="43200">1 month</option>
+										</select>
+									</div>
+									<div class="col-md-4 col-sm-12">
+										<button type="submit" class="btn btn-danger btn-md waves-effect">Clean</button>
+									</div>
+								</form>
 							</div>
-							<div class="col-md-4 col-sm-12">
-								<button type="submit" class="btn btn-danger btn-md waves-effect">Clean</button>
+						</div>
+					</div>
+				</div>
+
+				<div class="col-md-6">
+					<div class="card">
+						<div class="card-header">
+							<h2>
+								Go to metric
+								<small>
+									Go to the metric's page
+								</small>
+							</h2>
+						</div>
+						<div class="card-body card-padding">
+							<div class="row">
+								<form 
+									role="form" 
+									asp-controller="Admin" 
+									asp-action="Metric" 
+									method="get"
+									novalidate
+								>
+									<div class="col-md-8 col-sm-12">
+
+										<h5>Metric</h5>
+
+										<select class="selectpicker" name="metric" data-live-search="true">
+											@foreach (var metric in ViewBag.Metrics)
+											{
+												<option value="@((Metrics)metric.Type)@@@(metric.Source)">@(metric.Title) from @(metric.Source)</option>
+											}
+										</select>
+									</div>
+									<div class="col-md-4 col-sm-12">
+										<button type="submit" class="btn btn-primary btn-md waves-effect">Go</button>
+									</div>
+								</form>
 							</div>
-						</form>
+						</div>
 					</div>
 				</div>
+
 			</div>
-		</div>
 
-		<div class="col-md-3">
-			<div class="card">
-				<div class="card-header">
-					<h2>
-						Go to metric
-						<small>
-							Go to the metric's page
-						</small>
-					</h2>
-				</div>
-				<div class="card-body card-padding">
-					<div class="row">
-						<form 
-							role="form" 
-							asp-controller="Admin" 
-							asp-action="Metric" 
-							method="get"
-							novalidate
-						>
-							<div class="col-md-8 col-sm-12">
-
-								<h5>Metric</h5>
-
-								<select class="selectpicker" name="metric" data-live-search="true">
-									@foreach (var metric in ViewBag.Metrics)
-									{
-										<option value="@((Metrics)metric.Type)@@@(metric.Source)">@(metric.Title) from @(metric.Source)</option>
-									}
-								</select>
-							</div>
-							<div class="col-md-4 col-sm-12">
-								<button type="submit" class="btn btn-primary btn-md waves-effect">Go</button>
-							</div>
-						</form>
+			<div class="row">
+
+				<div class="col-md-12">
+
+					<div class="card">
+                        <div class="card-header">
+                            <h2>Discrepancies
+                                <small>
+									View the list of resolved and unresolved dicrepancies from TODO until now
+                                </small>
+                            </h2>
+                        </div>
+
+                        <div class="card-body card-padding">
+                            <div role="tabpanel">
+                                <ul class="tab-nav" role="tablist">
+                                    <li class="active">
+										<a 
+											href="#unresolved" 
+											aria-controls="unresolved" 
+											role="tab" 
+											data-toggle="tab" 
+											aria-expanded="true"
+										>
+											Unresolved ( @(((IEnumerable<Discrepancy>)ViewBag.Discrepancies).Count(d => !d.Resolved)) )
+										</a>
+									</li>
+                                    <li class="">
+										<a 
+											href="#resovled" 
+											aria-controls="resovled" 
+											role="tab" 
+											data-toggle="tab" 
+											aria-expanded="false"
+										>
+											Resolved ( @(((IEnumerable<Discrepancy>)ViewBag.Discrepancies).Count(d => d.Resolved)) )
+										</a>
+                                    </li>
+                                </ul>
+
+                                <div class="tab-content">
+                                    <div role="tabpanel" class="tab-pane active" id="unresolved">
+										@if ( ((IEnumerable<Discrepancy>)ViewBag.Discrepancies).Any(d => !d.Resolved) )
+										{
+											@foreach (var dicrepancy in ((IEnumerable<Discrepancy>)ViewBag.Discrepancies).Where(d => !d.Resolved))
+											{
+												<vc:discrepancy-card model=dicrepancy></vc:discrepancy-card>
+											}
+										}
+										else
+										{
+                                        	@: <h3>No outstanding issues! Well done!</h3>
+										}
+                                    </div>
+                                    <div role="tabpanel" class="tab-pane" id="resovled">
+                                        @if ( ((IEnumerable<Discrepancy>)ViewBag.Discrepancies).Any(d => d.Resolved) )
+										{
+											@foreach (var dicrepancy in ((IEnumerable<Discrepancy>)ViewBag.Discrepancies).Where(d => d.Resolved))
+											{
+												<vc:discrepancy-card model=dicrepancy></vc:discrepancy-card>
+											}
+										}
+										else
+										{
+                                        	@: <h3>No discrepancies have been noticed.</h3>
+										}
+                                    </div>
+                                </div>
+                            </div>
+
+                        </div>
+
 					</div>
+
 				</div>
+
 			</div>
-		</div>
 
+		</div>
 
 		<div class="col-md-6">
 			<div class="card">
diff --git a/src/web/Views/Shared/Components/DiscrepancyCard/Default.cshtml b/src/web/Views/Shared/Components/DiscrepancyCard/Default.cshtml
new file mode 100644
index 0000000..5df9a58
--- /dev/null
+++ b/src/web/Views/Shared/Components/DiscrepancyCard/Default.cshtml
@@ -0,0 +1,33 @@
+<div class="list-group-item media">
+	<div class="pull-left">
+		<i class="zmdi zmdi-@(Model.Resolved ? "check-circle" : "alert-circle-o") zmdi-hc-3x"></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>
+			</div>
+		</div>
+	}
+
+	<div class="media-body">
+		<div class="lgi-heading">
+			Discrepancy of type <strong>@Model.Type</strong> from <em>@Model.MetricType</em> of <em>@Model.MetricSource</em>.
+		</div>
+		<small class="lgi-text">@Model.Description()</small>
+
+		<ul class="lgi-attrs">
+			<li>Date first offense: <utc-time time="@Model.DateFirstOffense" /></li>
+			@if (Model.Resolved)
+			{
+				<li>Date resolved: <utc-time time="@Model.DateResolved" /></li>
+				<li>Duration: @( Math.Round((@Model.DateResolved - @Model.DateFirstOffense).TotalSeconds) ) seconds</li>
+			}
+		</ul>
+	</div>
+</div>
diff --git a/src/web/Views/ViewComponents/DiscrepancyCardViewComponent.cs b/src/web/Views/ViewComponents/DiscrepancyCardViewComponent.cs
new file mode 100644
index 0000000..0b1f277
--- /dev/null
+++ b/src/web/Views/ViewComponents/DiscrepancyCardViewComponent.cs
@@ -0,0 +1,23 @@
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
+using StatusMonitor.Shared.Extensions;
+using StatusMonitor.Shared.Models.Entities;
+
+namespace StatusMonitor.Web.Views.ViewComponents
+{
+	/// <summary>
+	/// View component responsible for rendering discrepancy card.
+	/// </summary>
+	[ViewComponent(Name = "DiscrepancyCard")]
+	public class DiscrepancyCardViewComponent : ViewComponent
+	{
+		public async Task<IViewComponentResult> InvokeAsync(Discrepancy model)
+		{
+			await Task.CompletedTask;
+
+			return View(model);
+		}
+	}
+}
diff --git a/test/ControllerTests/AdminController/AdminControllerTest.cs b/test/ControllerTests/AdminController/AdminControllerTest.cs
index db3922b..cb6cee1 100644
--- a/test/ControllerTests/AdminController/AdminControllerTest.cs
+++ b/test/ControllerTests/AdminController/AdminControllerTest.cs
@@ -33,6 +33,20 @@ namespace StatusMonitor.Tests.ControllerTests
 
 		public AdminControllerTest()
 		{
+			var services = new ServiceCollection();
+
+			var mockEnv = new Mock<IHostingEnvironment>();
+			mockEnv
+				.SetupGet(environment => environment.EnvironmentName)
+				.Returns("Testing");
+			var env = mockEnv.Object;
+
+			services.RegisterSharedServices(env, new Mock<IConfiguration>().Object);
+
+			var context = services
+				.BuildServiceProvider()
+				.GetRequiredService<IDataContext>();
+
 			var mockServiceProvider = new Mock<IServiceProvider>();
 			mockServiceProvider
 				.Setup(provider => provider.GetService(typeof(IApiController)))
@@ -43,7 +57,8 @@ namespace StatusMonitor.Tests.ControllerTests
 				new Mock<ILogger<AdminController>>().Object,
 				_mockMetricService.Object,
 				mockServiceProvider.Object,
-				_mockCleanService.Object
+				_mockCleanService.Object,
+				context
 			);
 			_controller.ControllerContext.HttpContext = new DefaultHttpContext();
 			_controller.TempData = new Mock<ITempDataDictionary>().Object;
-- 
GitLab