Commit 7eddbc03 authored by Dmytro Bogatov's avatar Dmytro Bogatov 💕

Merge branch '32-show-discrepancies-in-ui' into 'master'

Resolve "Show discrepancies in UI"

Closes #32

See merge request !17
parents bd3ab2f7 1c5da3d3
Pipeline #594 passed with stages
in 5 minutes and 54 seconds
......@@ -6,17 +6,52 @@ 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"));
});
/**
* Controls "load more" and "load less" buttons for
* resolved discrepancies list
*
*/
function setupDiscrepancyViewer() : void {
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();
}
});
}
......@@ -161,16 +161,24 @@ export class Utility {
);
});
$(".utc-date").each(function () {
$(this).text(
Utility.toLocalTimezone(
new Date($(this).text())
).toString()
);
});
}
public static toUtcDate(date : Date): number {
public static toUtcDate(date: Date): number {
return Date.UTC(
date.getUTCFullYear(),
date.getUTCMonth(),
date.getUTCDate(),
date.getUTCHours(),
date.getUTCMinutes(),
date.getUTCSeconds(),
date.getUTCFullYear(),
date.getUTCMonth(),
date.getUTCDate(),
date.getUTCHours(),
date.getUTCMinutes(),
date.getUTCSeconds(),
date.getUTCMilliseconds()
);
}
......
......@@ -43,6 +43,26 @@ namespace StatusMonitor.Shared.Models.Entities
}
public override string ToString() => ToStringWithTimeZone();
/// <summary>
/// Human readable description of the discrepancy.
/// </summary>
/// <returns>Human readable description of the discrepancy</returns>
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
......
......@@ -154,7 +154,7 @@ namespace StatusMonitor.Shared.Services
return
unresolved == 0 ?
"There are no outstanding issues. Well done." :
$"There {(unresolved == 1 ? "is" : "are")} still outstanding {unresolved} issue{(unresolved == 1 ? "" : "s")}."
$"There {(unresolved == 1 ? "is" : "are")} still outstanding {unresolved} issue{(unresolved == 1 ? "" : "s")}. See admin panel."
;
}
......
......@@ -33,6 +33,9 @@ namespace StatusMonitor.Web.Controllers.View
private readonly IMetricService _metricService;
private readonly IServiceProvider _provider;
private readonly ICleanService _cleanService;
private readonly IDataContext _context;
private readonly INotificationService _notification;
private readonly IConfiguration _config;
public AdminController(
......@@ -40,7 +43,10 @@ namespace StatusMonitor.Web.Controllers.View
ILogger<AdminController> logger,
IMetricService metricService,
IServiceProvider provider,
ICleanService cleanService
ICleanService cleanService,
IDataContext context,
INotificationService notification,
IConfiguration congig
)
{
_metricService = metricService;
......@@ -48,6 +54,9 @@ namespace StatusMonitor.Web.Controllers.View
_loggingService = loggingService;
_provider = provider;
_cleanService = cleanService;
_context = context;
_notification = notification;
_config = congig;
}
public async Task<IActionResult> Index()
......@@ -55,6 +64,11 @@ namespace StatusMonitor.Web.Controllers.View
ViewBag.Metrics = await _metricService
.GetMetricsAsync();
ViewBag.Discrepancies = await _context
.Discrepancies
.OrderByDescending(d => d.DateFirstOffense)
.ToListAsync();
return View();
}
......@@ -76,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)
......
......@@ -12,6 +12,12 @@ namespace StatusMonitor.Web.TagHelpers
/// </summary>
public class UtcTimeTagHelper : TagHelper
{
/// <summary>
/// If true, the full date should be rendered
/// Otherwise, only time part should be rendered
/// </summary>
public bool ShowDate { get; set; } = false;
public DateTime Time { get; set; }
[ViewContext]
......@@ -26,7 +32,8 @@ namespace StatusMonitor.Web.TagHelpers
output.TagMode = TagMode.StartTagAndEndTag;
output.Attributes.Clear();
output.Attributes.Add("class", "utc-time");
output.Attributes.Add("class", ShowDate ? "utc-date" : "utc-time");
output.Content.SetHtmlContent(Time.ToString());
......
using System;
using System.ComponentModel.DataAnnotations;
using StatusMonitor.Shared.Extensions;
using StatusMonitor.Shared.Models.Entities;
namespace StatusMonitor.Web.ViewModels
{
public class DiscrepancyResolutionViewModel
{
/// <summary>
/// Timestamp when the discrepancy starts.
/// </summary>
public DateTime DateFirstOffense { get; set; }
/// <summary>
/// Alias for DateFirstOffense.
/// Should represent the number of ticks (eq. DateTime.UtcNow.Ticks)
/// </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}.";
}
}
}
@using StatusMonitor.Shared.Models.Entities
@using Microsoft.Extensions.Configuration
@inject IConfiguration Config
@{
ViewData["Title"] = "Admin panel";
......@@ -11,91 +14,214 @@
</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>
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h2>Discrepancies
<small>
@{
var discrepancyDataStartDate = DateTime.UtcNow - new TimeSpan(0, 0, Convert.ToInt32(Config["ServiceManager:CleanService:MaxAge"]));
}
</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>
View the list of resolved and unresolved dicrepancies from <utc-time time="@discrepancyDataStartDate" show-date=true /> 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
number=0
hidden=false
></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) )
{
var number = 1;
@foreach (var dicrepancy in ((IEnumerable<Discrepancy>)ViewBag.Discrepancies).Where(d => d.Resolved))
{
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
{
@: <h3>No discrepancies have been noticed.</h3>
}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
......
<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"
style="color: @(Model.Resolved ? "green" : "red");"
>
</i>
</div>
@if (!Model.Resolved)
{
<div class="pull-right">
<div class="actions">
<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>
}