diff --git a/src/shared/Models/Entities/Discrepancies.cs b/src/shared/Models/Entities/Discrepancies.cs index e69692d0f0bdd7c5dfb32e10918a2b837ec9fbfd..184b75de62ae841303b4ee9d1506a6f81b1c08c6 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 a5bf362a3f48b55b50f2ba099c8663d5f86be44d..b8862a943849706e1e770680aa8d9e70a46715ed 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 a01a95b2b3a899954dbbde05ce2691325decef4d..b57c0a0509f8c1bf322cc825dcbbbb185940b805 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 0000000000000000000000000000000000000000..5df9a581ba7ca5224719177e0b4b409a938bc593 --- /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 0000000000000000000000000000000000000000..0b1f27798277c03ec808ab41f01141b9c587f84a --- /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 db3922b63c1c4ae076958c621799cf5d568470b6..cb6cee19ef1049b076e2474d4c59d234722b31d3 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;