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