Skip to content

Commit

Permalink
TNO-2072 Enhance charts (#1458)
Browse files Browse the repository at this point in the history
Add DB migration 1.0.115
Publish tno-core:0.1.21
Update Reporting Service
Update Editor app
Update Subscriber app
Update API
  • Loading branch information
Fosol authored Jan 25, 2024
1 parent 4f023be commit bd7fc8d
Show file tree
Hide file tree
Showing 82 changed files with 9,206 additions and 156 deletions.
34 changes: 27 additions & 7 deletions api/net/Areas/Admin/Controllers/ChartTemplateController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
using TNO.API.Helpers;
using TNO.API.Models;
using TNO.Core.Exceptions;
using TNO.Core.Extensions;
using TNO.DAL.Services;
using TNO.Entities;
using TNO.Keycloak;
using TNO.Core.Extensions;

namespace TNO.API.Areas.Admin.Controllers;

Expand Down Expand Up @@ -150,8 +150,17 @@ public async Task<IActionResult> PreviewJsonAsync(ChartPreviewRequestModel model
Template = model.Template,
SectionSettings = model.Settings
};
var chartTemplate = new TemplateEngine.Models.Reports.ChartEngineContentModel("test", chart, model.Content?.Select(c => new TemplateEngine.Models.ContentModel(c)));
var preview = await _reportHelper.GenerateJsonAsync(new TemplateEngine.Models.Charts.ChartRequestModel(chartTemplate, model.ChartData), model.Index, model.Filter, true);

// Merge content passed to request and also get content from Elasticsearch if a filter is provided.
var content = model.Content?.Select(c => new TemplateEngine.Models.ContentModel(c)) ?? Array.Empty<TemplateEngine.Models.ContentModel>();
if (model.Filter != null && model.Filter.ToJson() != "{}")
content = content.AppendRange(await _reportHelper.FindContentAsync(model.Filter, model.Index));

// If this chart pulls data from a linked report add this content.
var sections = model.LinkedReportId.HasValue ? _reportHelper.GetLinkedReportContent(model.LinkedReportId.Value, null).Result : new();

var chartTemplate = new TemplateEngine.Models.Reports.ChartEngineContentModel("test", chart, sections, content);
var preview = await _reportHelper.GenerateJsonAsync(new TemplateEngine.Models.Charts.ChartRequestModel(chartTemplate, model.ChartData), true);
return new JsonResult(preview);
}

Expand All @@ -168,9 +177,11 @@ public async Task<IActionResult> PreviewJsonAsync(ChartPreviewRequestModel model
public async Task<IActionResult> GenerateBase64Async(ChartPreviewRequestModel model)
{
// pie charts should never have scales
if (model.Settings.ChartType.Equals("pie")) {
if (model.Settings.ChartType.Equals("pie"))
{
var json = JsonNode.Parse(model.Settings.Options.ToJson())?.AsObject();
if (json != null) {
if (json != null)
{
json.Remove("scales");
model.Settings.Options = JsonDocument.Parse(json.ToJsonString());
}
Expand All @@ -181,8 +192,17 @@ public async Task<IActionResult> GenerateBase64Async(ChartPreviewRequestModel mo
Template = model.Template,
SectionSettings = model.Settings
};
var chartTemplate = new TemplateEngine.Models.Reports.ChartEngineContentModel("test", chart, model.Content?.Select(c => new TemplateEngine.Models.ContentModel(c)));
var base64Image = await _reportHelper.GenerateBase64ImageAsync(new TemplateEngine.Models.Charts.ChartRequestModel(chartTemplate, model.ChartData), model.Index, model.Filter, true);

// Merge content passed to request and also get content from Elasticsearch if a filter is provided.
var content = model.Content?.Select(c => new TemplateEngine.Models.ContentModel(c)) ?? Array.Empty<TemplateEngine.Models.ContentModel>();
if (model.Filter != null && model.Filter.ToJson() != "{}")
content = content.AppendRange(await _reportHelper.FindContentAsync(model.Filter, model.Index));

// If this chart pulls data from a linked report add this content.
var sections = model.LinkedReportId.HasValue ? _reportHelper.GetLinkedReportContent(model.LinkedReportId.Value, null).Result : new();

var chartTemplate = new TemplateEngine.Models.Reports.ChartEngineContentModel("test", chart, sections, content);
var base64Image = await _reportHelper.GenerateBase64ImageAsync(new TemplateEngine.Models.Charts.ChartRequestModel(chartTemplate, model.ChartData), true);
return Ok(base64Image);
}
#endregion
Expand Down
5 changes: 2 additions & 3 deletions api/net/Areas/Admin/Controllers/ReportController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
using TNO.Kafka;
using TNO.Kafka.Models;
using TNO.Keycloak;
using TNO.TemplateEngine.Models.Reports;

namespace TNO.API.Areas.Admin.Controllers;

Expand Down Expand Up @@ -259,7 +258,7 @@ public async Task<IActionResult> Publish(int id)
/// <returns></returns>
[HttpPost("preview")]
[Produces(MediaTypeNames.Application.Json)]
[ProducesResponseType(typeof(ReportResultModel), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(TNO.TemplateEngine.Models.Reports.ReportResultModel), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]
[SwaggerOperation(Tags = new[] { "Report" })]
public async Task<IActionResult> Preview(ReportModel model)
Expand All @@ -279,7 +278,7 @@ public async Task<IActionResult> Preview(ReportModel model)
/// <returns></returns>
[HttpPost("preview/prime")]
[Produces(MediaTypeNames.Application.Json)]
[ProducesResponseType(typeof(ReportResultModel), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(TNO.TemplateEngine.Models.Reports.ReportResultModel), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]
[SwaggerOperation(Tags = new[] { "Report" })]
public IActionResult PrimeReportCache(ReportModel model)
Expand Down
3 changes: 1 addition & 2 deletions api/net/Areas/Editor/Controllers/ReportController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
using TNO.Kafka.Models;
using TNO.Keycloak;
using TNO.Models.Filters;
using TNO.TemplateEngine.Models.Reports;

namespace TNO.API.Areas.Editor.Controllers;

Expand Down Expand Up @@ -186,7 +185,7 @@ public IActionResult Delete(ReportModel model)
/// <returns></returns>
[HttpPost("{id}/preview")]
[Produces(MediaTypeNames.Application.Json)]
[ProducesResponseType(typeof(ReportResultModel), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(TNO.TemplateEngine.Models.Reports.ReportResultModel), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]
[SwaggerOperation(Tags = new[] { "Report" })]
public async Task<IActionResult> Preview(int id)
Expand Down
24 changes: 18 additions & 6 deletions api/net/Areas/Helpers/IReportHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,39 @@ public interface IReportHelper
#endregion

#region Methods
/// <summary>
/// If a filter is provided make a request to Elasticsearch for content.
/// </summary>
/// <param name="filter"></param>
/// <param name="index"></param>
/// <returns></returns>
Task<IEnumerable<TNO.TemplateEngine.Models.ContentModel>> FindContentAsync(JsonDocument filter, string? index);

/// <summary>
/// Get the content from the current instance of the specified 'reportId' and 'ownerId'.
/// </summary>
/// <param name="reportId"></param>
/// <param name="ownerId"></param>
/// <returns></returns>
Task<Dictionary<string, ReportSectionModel>> GetLinkedReportContent(int reportId, int? ownerId = null);

/// <summary>
/// Makes a request to Elasticsearch if required to fetch content.
/// Generate the Chart JSON for the specified 'model' containing a template and content.
/// </summary>
/// <param name="model"></param>
/// <param name="index"></param>
/// <param name="filter"></param>
/// <param name="isPreview"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
Task<ChartResultModel> GenerateJsonAsync(ChartRequestModel model, string? index, JsonDocument? filter, bool isPreview = false);
Task<ChartResultModel> GenerateJsonAsync(ChartRequestModel model, bool isPreview = false);

/// <summary>
/// Executes the chart template provided to generate JSON, which is then sent with a request to the Charts API to generate a base 64 image.
/// </summary>
/// <param name="model"></param>
/// <param name="index"></param>
/// <param name="filter"></param>
/// <param name="isPreview"></param>
/// <returns>Returns the base64 image from the Charts API.</returns>
Task<string> GenerateBase64ImageAsync(ChartRequestModel model, string? index, JsonDocument? filter, bool isPreview = false);
Task<string> GenerateBase64ImageAsync(ChartRequestModel model, bool isPreview = false);

/// <summary>
/// Generate an instance of the report.
Expand Down
58 changes: 37 additions & 21 deletions api/net/Areas/Helpers/ReportHelper.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@

using System.Text.Json;
using Microsoft.Extensions.Options;
using TNO.Core.Exceptions;
using TNO.Core.Extensions;
using TNO.DAL.Config;
using TNO.DAL.Services;
Expand Down Expand Up @@ -63,54 +64,69 @@ public ReportHelper(
#endregion

#region Methods
/// <summary>
/// If a filter is provided make a request to Elasticsearch for content.
/// </summary>
/// <param name="filter"></param>
/// <param name="index"></param>
/// <returns></returns>
public async Task<IEnumerable<ContentModel>> FindContentAsync(JsonDocument filter, string? index)
{
var content = new List<ContentModel>();
var searchResults = await _contentService.FindWithElasticsearchAsync(index ?? _elasticOptions.PublishedIndex, filter);
content.AddRange(searchResults.Hits.Hits.Select(h => new ContentModel(h.Source)).ToArray());
return content;
}

/// <summary>
/// Get the content from the current instance of the specified 'reportId' and 'ownerId'.
/// </summary>
/// <param name="reportId"></param>
/// <param name="ownerId"></param>
/// <returns></returns>
public Task<Dictionary<string, ReportSectionModel>> GetLinkedReportContent(int reportId, int? ownerId = null)
{
var report = _reportService.FindById(reportId) ?? throw new NoContentException($"Report does not exist: ${reportId}");
var instance = _reportService.GetCurrentReportInstance(reportId, ownerId, true);
var sections = report.Sections.ToDictionary(s => s.Name, s =>
{
var content = instance?.ContentManyToMany.Where(c => c.Content != null && c.SectionName == s.Name) ?? Array.Empty<Entities.ReportInstanceContent>();
var section = new ReportSectionModel(s, content, _serializerOptions);
return section;
});
return Task.FromResult(sections);
}

/// <summary>
/// Makes a request to Elasticsearch if required to fetch content.
/// Generate the Chart JSON for the specified 'model' containing a template and content.
/// If the model includes a Filter it will make a request to Elasticsearch.
/// </summary>
/// <param name="model"></param>
/// <param name="index"></param>
/// <param name="filter"></param>
/// <param name="isPreview"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public async Task<ChartResultModel> GenerateJsonAsync(
ChartRequestModel model,
string? index = null,
JsonDocument? filter = null,
bool isPreview = false)
{
var chart = model.ChartTemplate;
SearchResultModel<Areas.Services.Models.Content.ContentModel>? searchResults = null;
var content = new List<ContentModel>(chart.Content ?? Array.Empty<ContentModel>());
if (filter != null)
{
searchResults = await _contentService.FindWithElasticsearchAsync(index ?? _elasticOptions.PublishedIndex, filter);
content.AddRange(searchResults.Hits.Hits.Select(h => new ContentModel(h.Source)).ToArray());
}
chart.Content = content.ToArray();

var result = await _reportEngine.GenerateJsonAsync(model, isPreview);
result.Results = searchResults != null ? JsonDocument.Parse(JsonSerializer.Serialize(searchResults)) : null;
result.Results = JsonDocument.Parse(JsonSerializer.Serialize(model.ChartTemplate.Content));
return result;
}

/// <summary>
/// Execute the chart template provided to generate JSON, which is then sent with a request to the Charts API to generate a base64 image.
/// </summary>
/// <param name="model"></param>
/// <param name="index"></param>
/// <param name="filter"></param>
/// <param name="isPreview"></param>
/// <returns>Returns the base64 image from the Charts API.</returns>
public async Task<string> GenerateBase64ImageAsync(
ChartRequestModel model,
string? index = null,
JsonDocument? filter = null,
bool isPreview = false)
{
// Get the Chart JSON data.
model.ChartData ??= (await this.GenerateJsonAsync(model, index, filter, isPreview)).Json;
model.ChartData ??= (await this.GenerateJsonAsync(model, isPreview)).Json;
return await _reportEngine.GenerateBase64ImageAsync(model);
}

Expand Down Expand Up @@ -249,7 +265,7 @@ private async Task<ReportResultModel> GenerateReportAsync(
bool isPreview = false)
{
var subject = await _reportEngine.GenerateReportSubjectAsync(report, sections, viewOnWebOnly, isPreview);
var body = await _reportEngine.GenerateReportBodyAsync(report, sections, _storageOptions.GetUploadPath(), viewOnWebOnly, isPreview);
var body = await _reportEngine.GenerateReportBodyAsync(report, sections, GetLinkedReportContent, _storageOptions.GetUploadPath(), viewOnWebOnly, isPreview);

return new ReportResultModel(subject, body);
}
Expand Down
22 changes: 21 additions & 1 deletion api/net/Areas/Services/Controllers/ReportController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
using TNO.API.Models;
using TNO.Core.Exceptions;
using TNO.DAL.Services;
using TNO.Elastic;
using TNO.Entities.Models;
using TNO.Keycloak;

Expand Down Expand Up @@ -99,6 +98,27 @@ public async Task<IActionResult> FindContentForReportIdAsync(int id, int? reques
return new JsonResult(results);
}

/// <summary>
/// Get the current instance for the specified report 'id'.
/// </summary>
/// <param name="id"></param>
/// <param name="requestorId"></param>
/// <returns></returns>
[HttpGet("{id}/instance")]
[Produces(MediaTypeNames.Application.Json)]
[ProducesResponseType(typeof(ReportInstanceModel), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
[SwaggerOperation(Tags = new[] { "Report" })]
public IActionResult GetCurrentInstance(int id, int? requestorId)
{
var instance = _service.GetCurrentReportInstance(id, requestorId, true);
if (instance == null) return NoContent();
var report = _service.FindById(id);
instance.Report = report;
return new JsonResult(new ReportInstanceModel(instance, _serializerOptions));
}

/// <summary>
/// Clears all content from folders within this report.
/// </summary>
Expand Down
46 changes: 29 additions & 17 deletions api/net/Areas/Subscriber/Controllers/ReportController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,9 @@
using TNO.Core.Extensions;
using TNO.DAL.Services;
using TNO.Kafka;
using TNO.Kafka.Models;
using TNO.Kafka.SignalR;
using TNO.Keycloak;
using TNO.Models.Filters;
using TNO.TemplateEngine.Models.Reports;

namespace TNO.API.Areas.Subscriber.Controllers;

Expand Down Expand Up @@ -278,7 +276,7 @@ public IActionResult Delete(ReportModel model)
/// <returns></returns>
[HttpPost("{id}/preview")]
[Produces(MediaTypeNames.Application.Json)]
[ProducesResponseType(typeof(ReportResultModel), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(TNO.TemplateEngine.Models.Reports.ReportResultModel), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]
[SwaggerOperation(Tags = new[] { "Report" })]
public async Task<IActionResult> Preview(int id)
Expand Down Expand Up @@ -316,9 +314,35 @@ public async Task<IActionResult> Generate(int id, [FromQuery] bool regenerate =

var instances = _reportService.GetLatestInstances(id, user.Id);
var currentInstance = instances.FirstOrDefault();
if (regenerate && currentInstance != null && currentInstance.SentOn.HasValue == false)
if (currentInstance == null)
{
// Generate a new instance of this report because the request asked for it, and the last instance was already sent.
// Generate a new instance of this report if there is no current instance, or if it has already been sent.
currentInstance = await _reportHelper.GenerateReportInstanceAsync(new Services.Models.Report.ReportModel(report, _serializerOptions), user.Id);
currentInstance = _reportInstanceService.AddAndSave(currentInstance);
instances = _reportService.GetLatestInstances(id, user.Id);
currentInstance.ContentManyToMany.Clear();
currentInstance.ContentManyToMany.AddRange(_reportInstanceService.GetContentForInstance(currentInstance.Id));
report.Instances.Clear();
report.Instances.Add(currentInstance);
report.Instances.AddRange(instances.Where(i => i.Id != currentInstance.Id));
}
else if (regenerate
&& (currentInstance.SentOn.HasValue
|| new[] { Entities.ReportStatus.Accepted, Entities.ReportStatus.Completed, Entities.ReportStatus.Failed, Entities.ReportStatus.Cancelled }.Contains(currentInstance.Status)))
{
// Generate a new instance because the prior was sent to CHES.
currentInstance = await _reportHelper.GenerateReportInstanceAsync(new Services.Models.Report.ReportModel(report, _serializerOptions), user.Id);
currentInstance = _reportInstanceService.AddAndSave(currentInstance);
instances = _reportService.GetLatestInstances(id, user.Id);
currentInstance.ContentManyToMany.Clear();
currentInstance.ContentManyToMany.AddRange(_reportInstanceService.GetContentForInstance(currentInstance.Id));
report.Instances.Clear();
report.Instances.Add(currentInstance);
report.Instances.AddRange(instances.Where(i => i.Id != currentInstance.Id));
}
else if (regenerate && currentInstance.SentOn.HasValue == false)
{
// Regenerate the current instance, but do not create a new instance.
var regeneratedInstance = await _reportHelper.GenerateReportInstanceAsync(new Services.Models.Report.ReportModel(report, _serializerOptions), user.Id, currentInstance.Id);
_reportInstanceService.ClearChangeTracker();
currentInstance.ContentManyToMany.Clear();
Expand All @@ -338,18 +362,6 @@ public async Task<IActionResult> Generate(int id, [FromQuery] bool regenerate =
report.Instances.Add(currentInstance);
report.Instances.AddRange(instances.Where(i => i.Id != currentInstance.Id));
}
else if (currentInstance == null || currentInstance.SentOn.HasValue)
{
// Generate a new instance of this report if there is no current instance, or if it has already been sent.
currentInstance = await _reportHelper.GenerateReportInstanceAsync(new Services.Models.Report.ReportModel(report, _serializerOptions), user.Id);
currentInstance = _reportInstanceService.AddAndSave(currentInstance);
instances = _reportService.GetLatestInstances(id, user.Id);
currentInstance.ContentManyToMany.Clear();
currentInstance.ContentManyToMany.AddRange(_reportInstanceService.GetContentForInstance(currentInstance.Id));
report.Instances.Clear();
report.Instances.Add(currentInstance);
report.Instances.AddRange(instances.Where(i => i.Id != currentInstance.Id));
}
else
{
// Get the content for the current instance.
Expand Down
Binary file not shown.
Binary file modified app/editor/.yarn/install-state.gz
Binary file not shown.
2 changes: 1 addition & 1 deletion app/editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
"react-tooltip": "5.10.0",
"redux-logger": "3.0.6",
"styled-components": "5.3.9",
"tno-core": "0.1.20"
"tno-core": "0.1.21"
},
"devDependencies": {
"@simbathesailor/use-what-changed": "2.0.0",
Expand Down
Loading

0 comments on commit bd7fc8d

Please sign in to comment.