Skip to content

Commit

Permalink
Merge pull request #19 from davewalker5/FR-67-Job-Status-Report
Browse files Browse the repository at this point in the history
Added Job Status report and export
  • Loading branch information
davewalker5 committed Oct 30, 2023
2 parents d11d64d + a139435 commit d96d0a7
Show file tree
Hide file tree
Showing 12 changed files with 332 additions and 12 deletions.
4 changes: 2 additions & 2 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/aspnet:latest AS runtime
COPY flightrecorder.mvc-1.5.0.0 /opt/flightrecorder.mvc-1.5.0.0
WORKDIR /opt/flightrecorder.mvc-1.5.0.0/bin
COPY flightrecorder.mvc-1.6.0.0 /opt/flightrecorder.mvc-1.6.0.0
WORKDIR /opt/flightrecorder.mvc-1.6.0.0/bin
ENTRYPOINT [ "./FlightRecorder.Mvc" ]
23 changes: 17 additions & 6 deletions src/FlightRecorder.Mvc/Api/ReportsClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public ReportsClient(HttpClient client, IOptions<AppSettings> settings, IHttpCon
/// <param name="pageSize"></param>
/// <returns></returns>
public async Task<List<AirlineStatistics>> AirlineStatisticsAsync(DateTime? from, DateTime? to, int pageNumber, int pageSize)
=> await DateBasedStatisticsReportAsync<AirlineStatistics>("AirlineStatistics", from, to, pageNumber, pageSize);
=> await DateBasedReportAsync<AirlineStatistics>("AirlineStatistics", from, to, pageNumber, pageSize);

/// <summary>
/// Return the location statistics report
Expand All @@ -39,7 +39,7 @@ public async Task<List<AirlineStatistics>> AirlineStatisticsAsync(DateTime? from
/// <param name="pageSize"></param>
/// <returns></returns>
public async Task<List<LocationStatistics>> LocationStatisticsAsync(DateTime? from, DateTime? to, int pageNumber, int pageSize)
=> await DateBasedStatisticsReportAsync<LocationStatistics>("LocationStatistics", from, to, pageNumber, pageSize);
=> await DateBasedReportAsync<LocationStatistics>("LocationStatistics", from, to, pageNumber, pageSize);

/// <summary>
/// Return the manufacturer statistics report
Expand All @@ -50,7 +50,7 @@ public async Task<List<LocationStatistics>> LocationStatisticsAsync(DateTime? fr
/// <param name="pageSize"></param>
/// <returns></returns>
public async Task<List<ManufacturerStatistics>> ManufacturerStatisticsAsync(DateTime? from, DateTime? to, int pageNumber, int pageSize)
=> await DateBasedStatisticsReportAsync<ManufacturerStatistics>("ManufacturerStatistics", from, to, pageNumber, pageSize);
=> await DateBasedReportAsync<ManufacturerStatistics>("ManufacturerStatistics", from, to, pageNumber, pageSize);

/// <summary>
/// Return the model statistics report
Expand All @@ -61,7 +61,7 @@ public async Task<List<ManufacturerStatistics>> ManufacturerStatisticsAsync(Date
/// <param name="pageSize"></param>
/// <returns></returns>
public async Task<List<ModelStatistics>> ModelStatisticsAsync(DateTime? from, DateTime? to, int pageNumber, int pageSize)
=> await DateBasedStatisticsReportAsync<ModelStatistics>("ModelStatistics", from, to, pageNumber, pageSize);
=> await DateBasedReportAsync<ModelStatistics>("ModelStatistics", from, to, pageNumber, pageSize);

/// <summary>
/// Return the flights by month report
Expand All @@ -72,7 +72,18 @@ public async Task<List<ModelStatistics>> ModelStatisticsAsync(DateTime? from, Da
/// <param name="pageSize"></param>
/// <returns></returns>
public async Task<List<FlightsByMonth>> FlightsByMonthAsync(DateTime? from, DateTime? to, int pageNumber, int pageSize)
=> await DateBasedStatisticsReportAsync<FlightsByMonth>("FlightsByMonth", from, to, pageNumber, pageSize);
=> await DateBasedReportAsync<FlightsByMonth>("FlightsByMonth", from, to, pageNumber, pageSize);

/// <summary>
/// Return the job status report
/// </summary>
/// <param name="from"></param>
/// <param name="to"></param>
/// <param name="pageNumber"></param>
/// <param name="pageSize"></param>
/// <returns></returns>
public async Task<List<JobStatus>> JobStatusAsync(DateTime? from, DateTime? to, int pageNumber, int pageSize)
=> await DateBasedReportAsync<JobStatus>("JobStatus", from, to, pageNumber, pageSize);

/// <summary>
/// Return a date-based statistics report
Expand All @@ -84,7 +95,7 @@ public async Task<List<FlightsByMonth>> FlightsByMonthAsync(DateTime? from, Date
/// <param name="pageNumber"></param>
/// <param name="pageSize"></param>
/// <returns></returns>
private async Task<List<T>> DateBasedStatisticsReportAsync<T>(string routeName, DateTime? from, DateTime? to, int pageNumber, int pageSize)
private async Task<List<T>> DateBasedReportAsync<T>(string routeName, DateTime? from, DateTime? to, int pageNumber, int pageSize)
{
// URL encode the dates
string fromRouteSegment = (from ?? DateTime.MinValue).ToString(Settings.Value.DateTimeFormat);
Expand Down
101 changes: 101 additions & 0 deletions src/FlightRecorder.Mvc/Controllers/JobStatusController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using FlightRecorder.Mvc.Api;
using FlightRecorder.Mvc.Configuration;
using FlightRecorder.Mvc.Entities;
using FlightRecorder.Mvc.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace FlightRecorder.Mvc.Controllers
{
[Authorize]
public class JobStatusController : Controller
{
private readonly ReportsClient _reportsClient;
private readonly ExportClient _exportClient;
private readonly IOptions<AppSettings> _settings;

public JobStatusController(
ReportsClient reportsClient,
ExportClient exportsClient,
IOptions<AppSettings> settings)
{
_reportsClient = reportsClient;
_exportClient = exportsClient;
_settings = settings;
}

/// <summary>
/// Serve the empty report page
/// </summary>
/// <returns></returns>
[HttpGet]
public IActionResult Index()
{
JobStatusViewModel model = new JobStatusViewModel
{
PageNumber = 1
};
return View(model);
}

/// <summary>
/// Respond to a POST event triggering the report generation
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Index(JobStatusViewModel model)
{
if (ModelState.IsValid)
{
int page = model.PageNumber;
switch (model.Action)
{
case ControllerActions.ActionPreviousPage:
page -= 1;
break;
case ControllerActions.ActionNextPage:
page += 1;
break;
case ControllerActions.ActionSearch:
page = 1;
break;
default:
break;
}

// Need to clear model state here or the page number that was posted
// is returned and page navigation doesn't work correctly. So, capture
// and amend the page number, above, then apply it, below
ModelState.Clear();

// Get the date and time
DateTime start = !string.IsNullOrEmpty(model.From) ? DateTime.Parse(model.From) : DateTime.MinValue;
DateTime end = !string.IsNullOrEmpty(model.To) ? DateTime.Parse(model.To) : DateTime.MaxValue;

// Retrieve the matching report records
List<JobStatus> records = await _reportsClient.JobStatusAsync(start, end, page, _settings.Value.SearchPageSize);
model.SetRecords(records, page, _settings.Value.SearchPageSize);
}

return View(model);
}

/// <summary>
/// Request export of the report
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> Export([FromBody] JobStatusViewModel model)
{
await _exportClient.ExportReport<JobStatus>(model);
return Ok();
}
}
}
18 changes: 18 additions & 0 deletions src/FlightRecorder.Mvc/Entities/JobStatus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System;

namespace FlightRecorder.Mvc.Entities
{
[ExcludeFromCodeCoverage]
public class JobStatus
{
[Key]
public long Id { get; set; }
public string Name { get; set; }
public string Parameters { get; set; }
public DateTime Start { get; set; }
public DateTime? End { get; set; }
public string Error { get; set; }
}
}
3 changes: 2 additions & 1 deletion src/FlightRecorder.Mvc/Entities/ReportDefinitions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ public static class ReportDefinitions
new ReportDefinition(ReportType.LocationStatistics, typeof(LocationStatistics), "Location Statistics"),
new ReportDefinition(ReportType.ManufacturerStatistics, typeof(ManufacturerStatistics), "Manufacturer Statistics"),
new ReportDefinition(ReportType.ModelStatistics, typeof(ModelStatistics), "Model Statistics"),
new ReportDefinition(ReportType.FlightsByMonth, typeof(FlightsByMonth), "Flights By Month")
new ReportDefinition(ReportType.FlightsByMonth, typeof(FlightsByMonth), "Flights By Month"),
new ReportDefinition(ReportType.JobStatus, typeof(JobStatus), "Job Status")
};
}
}
3 changes: 2 additions & 1 deletion src/FlightRecorder.Mvc/Entities/ReportType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ public enum ReportType
LocationStatistics = 1,
ManufacturerStatistics = 2,
ModelStatistics = 3,
FlightsByMonth = 4
FlightsByMonth = 4,
JobStatus = 5
}
}
2 changes: 1 addition & 1 deletion src/FlightRecorder.Mvc/FlightRecorder.Mvc.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ReleaseVersion>1.5.0.0</ReleaseVersion>
<ReleaseVersion>1.6.0.0</ReleaseVersion>
</PropertyGroup>


Expand Down
8 changes: 8 additions & 0 deletions src/FlightRecorder.Mvc/Models/JobStatusViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using FlightRecorder.Mvc.Entities;

namespace FlightRecorder.Mvc.Models
{
public class JobStatusViewModel : DateBasedReportViewModelBase<JobStatus>
{
}
}
163 changes: 163 additions & 0 deletions src/FlightRecorder.Mvc/Views/JobStatus/Index.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
@using FlightRecorder.Mvc.Entities
@model FlightRecorder.Mvc.Models.JobStatusViewModel

@{
ViewData["Title"] = "Job Status Report";
string previousDisabled = (Model.PreviousEnabled) ? "" : "disabled";
string nextDisabled = (Model.NextEnabled) ? "" : "disabled";
}

<p class="text-center font-weight-bold">
<span style="font-size: 1.2rem">
Job Status
</span>
<br />
<small class="text-muted">
<em>
Report the status of background jobs
</em>
</small>
</p>

<div id="MessageContainer" style="display:none">
<div class="row">
<div class="col-md-12 message">
<span id="MessageText"></span>
</div>
</div>
<hr />
</div>

<div id="ErrorContainer" style="display:none">
<div class="row">
<div class="col-md-12 error">
<span id="ErrorText"></span>
</div>
</div>
<hr />
</div>

<div class="container-fluid">
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()

<div class="row">
<div class="col">
<strong>@Html.LabelFor(m => m.From)</strong>
<div class="input-group">
@Html.EditorFor(m => m.From, new { @class = "form-control" })
<script type="text/javascript">
new GijgoDatePicker(document.getElementById("From"), { calendarWeeks: false, uiLibrary: "bootstrap4" });
</script>
</div>
<span>@Html.ValidationMessageFor(m => m.From, "", new { @class = "text-danger" })</span>
</div>
<div class="col">
<strong>@Html.LabelFor(m => m.To)</strong>
<div class="input-group">
@Html.EditorFor(m => m.To, new { @class = "form-control" })
<script type="text/javascript">
new GijgoDatePicker(document.getElementById("To"), { calendarWeeks: false, uiLibrary: "bootstrap4" });
</script>
</div>
<span>@Html.ValidationMessageFor(m => m.To, "", new { @class = "text-danger" })</span>
</div>
<div class="col">
<strong><label>&nbsp;</label></strong>
<div class="input-group">
@if (Model.Records != null)
{
<button id="Export" class="btn btn-secondary">Export</button>
<strong><label>&nbsp;</label></strong>
}
<button type="submit" name="Action" value="@ControllerActions.ActionSearch" class="btn btn-primary">Search</button>
</div>
</div>
</div>
<br />

@Html.HiddenFor(m => m.PageNumber)

@if (Model.Records != null)
{
<table class="table">
<tr>
<th>Job Name</th>
<th>Parameters</th>
<th>Started</th>
<th>Completed</th>
<th>Errors</th>
</tr>
@foreach (var record in Model.Records)
{
<tr>
<td valign="center">@record.Name</td>
<td valign="center">@record.Parameters</td>
<td valign="center">@record.Start</td>
<td valign="center">@record.End</td>
<td valign="center">@record.Error</td>
</tr>
}
</table>
<hr />
<p class="text-right">
<button type="submit" name="Action" value="@ControllerActions.ActionPreviousPage" class="btn btn-secondary" @previousDisabled>Previous</button>
<button type="submit" name="Action" value="@ControllerActions.ActionNextPage" class="btn btn-primary" @nextDisabled>Next</button>
</p>
}
else if (Model.HasNoMatchingResults)
{
<hr />
@Html.Raw("Airline statistics are not available")
}
}
</div>

<script type="text/javascript">
function ExportReport(url) {
// Hide the error and message elements
$("#MessageContainer").hide();
$("#WarningContainer").hide();
$("#ErrorContainer").hide();

// Construct the model holding the export parameters to POST to the controller
const model = {
"From": $("#From").val(),
"To": $("#To").val()
};

// Set a busy cursor
$('html, body').css("cursor", "wait");

// POST the data to the controller
$.ajax({
url: url,
method: "POST",
data: JSON.stringify(model),
contentType: "application/json",
cache: false,
success: function (result) {
// Worked, so show an "OK" message
$("#MessageText").text("The report export has been queued in the background");
$("#MessageContainer").show();
$('html, body').css("cursor", "auto");
},
error: function (xhr, status, error) {
// Failed, so show an error message
var errorMessage = xhr.status + ': ' + xhr.statusText
$("#ErrorText").text(errorMessage);
$("#ErrorContainer").show();
$('html, body').css("cursor", "auto");
}
});
}

$(document).ready(function () {
// Respond to a click on the "Export" button by POSTing an export request back
$("#Export").click(function (e) {
e.preventDefault();
ExportReport("/JobStatus/Export");
});
});
</script>
Loading

0 comments on commit d96d0a7

Please sign in to comment.