diff --git a/Directory.Packages.props b/Directory.Packages.props index 54ec2f0..0ccc88e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,6 +10,6 @@ - + \ No newline at end of file diff --git a/examples/SimpleReportServer/HelloReport.razor b/examples/SimpleReportServer/HelloReport.razor index c0dc338..95399b9 100644 --- a/examples/SimpleReportServer/HelloReport.razor +++ b/examples/SimpleReportServer/HelloReport.razor @@ -1,3 +1,4 @@ +

Hello @Data.Name

@code { diff --git a/examples/SimpleReportServer/Program.cs b/examples/SimpleReportServer/Program.cs index d7aefb9..4425dea 100644 --- a/examples/SimpleReportServer/Program.cs +++ b/examples/SimpleReportServer/Program.cs @@ -2,6 +2,7 @@ using BlazorReports.Models; using ExampleTemplates.Reports; using SimpleReportServer; +using SimpleReportServer.Reports; var builder = WebApplication.CreateSlimBuilder(args); @@ -19,6 +20,10 @@ var reportsGroup = app.MapGroup("reports"); +reportsGroup.MapBlazorReport(opts => +{ + opts.JavascriptSettings.WaitForJavascriptCompletedSignal = true; +}); reportsGroup.MapBlazorReport(); reportsGroup.MapBlazorReport(opts => { diff --git a/examples/SimpleReportServer/Reports/SimpleJsAsyncReport.razor b/examples/SimpleReportServer/Reports/SimpleJsAsyncReport.razor new file mode 100644 index 0000000..46eb0d0 --- /dev/null +++ b/examples/SimpleReportServer/Reports/SimpleJsAsyncReport.razor @@ -0,0 +1,22 @@ + +

+ +@code { + [Parameter] + public required SimpleJsAsyncReportData Data { get; set; } + +} diff --git a/examples/SimpleReportServer/Reports/SimpleJsAsyncReportData.cs b/examples/SimpleReportServer/Reports/SimpleJsAsyncReportData.cs new file mode 100644 index 0000000..1e74068 --- /dev/null +++ b/examples/SimpleReportServer/Reports/SimpleJsAsyncReportData.cs @@ -0,0 +1,6 @@ +namespace SimpleReportServer.Reports; + +public record SimpleJsAsyncReportData(int TimeoutInSeconds) +{ + public TimeSpan TimeSpan => TimeSpan.FromSeconds(TimeoutInSeconds); +}; diff --git a/examples/SimpleReportServer/wwwroot/js/HelloJavascriptData.js b/examples/SimpleReportServer/wwwroot/js/HelloJavascriptData.js new file mode 100644 index 0000000..e69de29 diff --git a/src/BlazorReports.Components/BlazorReportsTemplate.razor b/src/BlazorReports.Components/BlazorReportsTemplate.razor index d0c4e6d..4c524ca 100644 --- a/src/BlazorReports.Components/BlazorReportsTemplate.razor +++ b/src/BlazorReports.Components/BlazorReportsTemplate.razor @@ -1,29 +1,90 @@ - + + - + + + @foreach (var script in Scripts) + { + + } - @code { - /// - /// The base styles to be applied to the component. - /// - [Parameter] - public string BaseStyles { get; set; } = ""; - /// - /// The type of the component to be rendered in the html body. - /// - [Parameter] - public Type ChildComponentType { get; set; } = null!; - /// - /// The parameters to be passed to the child component. - /// - [Parameter] - public IDictionary ChildComponentParameters { get; set; } = null!; + /// + /// The base styles to be applied to the component. + /// + [Parameter] + public string BaseStyles { get; set; } = ""; + + /// + /// The type of the component to be rendered in the html body. + /// + [Parameter] + public Type ChildComponentType { get; set; } = null!; + + /// + /// The parameters to be passed to the child component. + /// + [Parameter] + public IDictionary ChildComponentParameters { get; set; } = null!; + + /// + /// The scripts to be included in the html body. + /// + [Parameter] + public Dictionary Scripts { get; set; } = new Dictionary(); + + /// + /// The name of the JavaScript signal used to indicate when the report is ready. + /// + [Parameter] + public string ReportIsReadySignal { get; set; } = "reportIsReady"; + } diff --git a/src/BlazorReports/Extensions/ReportExtensions.cs b/src/BlazorReports/Extensions/ReportExtensions.cs index 3d360a6..e82ac78 100644 --- a/src/BlazorReports/Extensions/ReportExtensions.cs +++ b/src/BlazorReports/Extensions/ReportExtensions.cs @@ -110,7 +110,8 @@ CancellationToken token _ => (int?)null, _ => StatusCodes.Status503ServiceUnavailable, _ => StatusCodes.Status499ClientClosedRequest, - _ => StatusCodes.Status500InternalServerError + _ => StatusCodes.Status500InternalServerError, + _ => StatusCodes.Status408RequestTimeout ); if (errorStatusCode is not null) @@ -121,6 +122,7 @@ CancellationToken token } ) .Produces(200, blazorReport.GetContentType()) + .Produces(StatusCodes.Status408RequestTimeout) .Produces(StatusCodes.Status503ServiceUnavailable); } @@ -174,7 +176,8 @@ CancellationToken token _ => (int?)null, _ => StatusCodes.Status503ServiceUnavailable, _ => StatusCodes.Status499ClientClosedRequest, - _ => StatusCodes.Status500InternalServerError + _ => StatusCodes.Status500InternalServerError, + _ => StatusCodes.Status408RequestTimeout ); if (errorStatusCode is not null) @@ -185,6 +188,7 @@ CancellationToken token } ) .Produces(200, blazorReport.GetContentType()) + .Produces(StatusCodes.Status408RequestTimeout) .Produces(StatusCodes.Status503ServiceUnavailable); } diff --git a/src/BlazorReports/Extensions/ServiceCollectionExtensions.cs b/src/BlazorReports/Extensions/ServiceCollectionExtensions.cs index 3eda7c2..8ba7892 100644 --- a/src/BlazorReports/Extensions/ServiceCollectionExtensions.cs +++ b/src/BlazorReports/Extensions/ServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ using BlazorReports.Services; using BlazorReports.Services.BrowserServices; using BlazorReports.Services.BrowserServices.Factories; +using BlazorReports.Services.JavascriptServices; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Constraints; using Microsoft.Extensions.DependencyInjection; @@ -26,6 +27,8 @@ public static IServiceCollection AddBlazorReports( { services.Configure(options ?? (_ => { })); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/BlazorReports/Models/BlazorReport.cs b/src/BlazorReports/Models/BlazorReport.cs index 5359173..caaaeb6 100644 --- a/src/BlazorReports/Models/BlazorReport.cs +++ b/src/BlazorReports/Models/BlazorReport.cs @@ -44,4 +44,9 @@ public class BlazorReport /// The page settings to use for the report. /// public BlazorReportsPageSettings? PageSettings { get; set; } + + /// + /// The current report javascript settings. + /// + public required BlazorReportCurrentReportJavascriptSettings CurrentReportJavascriptSettings { get; set; } } diff --git a/src/BlazorReports/Models/BlazorReportCurrentReportJavascriptSettings.cs b/src/BlazorReports/Models/BlazorReportCurrentReportJavascriptSettings.cs new file mode 100644 index 0000000..5f77e4a --- /dev/null +++ b/src/BlazorReports/Models/BlazorReportCurrentReportJavascriptSettings.cs @@ -0,0 +1,32 @@ +namespace BlazorReports.Models; + +/// +/// Settings for the internal javascript api +/// +public class BlazorReportCurrentReportJavascriptSettings +{ + /// + /// The signal that the report is ready + /// + public string ReportIsReadySignal { get; set; } = default!; + + /// + /// Decides if the report should wait for the complete signal in the javascript + /// + public bool WaitForJavascriptCompletedSignal { get; set; } + + /// + /// The amount of time a reports javascript can take until it is considered to have timed out + /// + public TimeSpan? WaitForCompletedSignalTimeout { get; set; } + + /// + /// The default settings for the current report javascript settings + /// + public static BlazorReportCurrentReportJavascriptSettings Default => + new BlazorReportCurrentReportJavascriptSettings + { + WaitForJavascriptCompletedSignal = false, + WaitForCompletedSignalTimeout = TimeSpan.FromSeconds(3), + }; +} diff --git a/src/BlazorReports/Models/BlazorReportGlobalJavascriptSettings.cs b/src/BlazorReports/Models/BlazorReportGlobalJavascriptSettings.cs new file mode 100644 index 0000000..af807f5 --- /dev/null +++ b/src/BlazorReports/Models/BlazorReportGlobalJavascriptSettings.cs @@ -0,0 +1,24 @@ +namespace BlazorReports.Models; + +/// +/// +/// +public class BlazorReportGlobalJavascriptSettings +{ + /// + /// The signal that the report is ready + /// + public string ReportIsReadySignal { get; set; } = "reportIsReady"; + + /// + /// Decides if the report should wait for the complete signal in the javascript. + /// (Default: false) + /// + public bool WaitForJavascriptCompletedSignal { get; set; } + + /// + /// The amount of time a reports javascript can take until it is considered to have timed out + /// (Default: null) + /// + public TimeSpan WaitForCompletedSignalTimeout { get; set; } = TimeSpan.FromSeconds(3); +} diff --git a/src/BlazorReports/Models/BlazorReportRegistrationOptions.cs b/src/BlazorReports/Models/BlazorReportRegistrationOptions.cs index 726416b..da55af3 100644 --- a/src/BlazorReports/Models/BlazorReportRegistrationOptions.cs +++ b/src/BlazorReports/Models/BlazorReportRegistrationOptions.cs @@ -29,4 +29,9 @@ public class BlazorReportRegistrationOptions /// Settings for generating a PDF /// public BlazorReportsPageSettings PageSettings { get; set; } = new(); + + /// + /// Javascript api settings + /// + public BlazorReportCurrentReportJavascriptSettings JavascriptSettings { get; set; } = new(); } diff --git a/src/BlazorReports/Models/BlazorReportRegistry.cs b/src/BlazorReports/Models/BlazorReportRegistry.cs index fec0360..9e24b46 100644 --- a/src/BlazorReports/Models/BlazorReportRegistry.cs +++ b/src/BlazorReports/Models/BlazorReportRegistry.cs @@ -85,6 +85,8 @@ public BlazorReport AddReport(BlazorReportRegistrationOptions? options = null Component = typeof(T), Data = null, PageSettings = options?.PageSettings, + CurrentReportJavascriptSettings = + options?.JavascriptSettings ?? BlazorReportCurrentReportJavascriptSettings.Default, }; if (!string.IsNullOrEmpty(options?.BaseStylesPath)) { @@ -137,6 +139,8 @@ public BlazorReport AddReport(BlazorReportRegistrationOptions? options = Component = typeof(T), Data = typeof(TD), PageSettings = options?.PageSettings, + CurrentReportJavascriptSettings = + options?.JavascriptSettings ?? BlazorReportCurrentReportJavascriptSettings.Default, }; if (!string.IsNullOrEmpty(options?.BaseStylesPath)) { diff --git a/src/BlazorReports/Models/BlazorReportsOptions.cs b/src/BlazorReports/Models/BlazorReportsOptions.cs index 86dbebc..4a864c4 100644 --- a/src/BlazorReports/Models/BlazorReportsOptions.cs +++ b/src/BlazorReports/Models/BlazorReportsOptions.cs @@ -24,4 +24,9 @@ public class BlazorReportsOptions /// Settings for generating a PDF /// public BlazorReportsPageSettings PageSettings { get; set; } = new(); + + /// + /// Javascript api settings + /// + public BlazorReportGlobalJavascriptSettings GlobalJavascriptSettings { get; set; } = new(); } diff --git a/src/BlazorReports/Services/BrowserServices/Browser.cs b/src/BlazorReports/Services/BrowserServices/Browser.cs index ed94142..fef96e1 100644 --- a/src/BlazorReports/Services/BrowserServices/Browser.cs +++ b/src/BlazorReports/Services/BrowserServices/Browser.cs @@ -21,6 +21,7 @@ internal sealed class Browser( DirectoryInfo dataDirectory, Connection connection, BlazorReportsBrowserOptions browserOptions, + BlazorReportGlobalJavascriptSettings globalJavascriptSettings, ILogger logger, IBrowserPageFactory browserPageFactory ) : IAsyncDisposable @@ -30,11 +31,18 @@ IBrowserPageFactory browserPageFactory private readonly SemaphoreSlim _poolLock = new(1, 1); public async ValueTask< - OneOf + OneOf< + Success, + ServerBusyProblem, + OperationCancelledProblem, + BrowserProblem, + CompletedSignalTimeoutProblem + > > GenerateReport( PipeWriter pipeWriter, string html, BlazorReportsPageSettings pageSettings, + BlazorReportCurrentReportJavascriptSettings currentReportJavascriptSettings, CancellationToken cancellationToken ) { @@ -92,6 +100,24 @@ out browserPage try { await browserPage.DisplayHtml(html, cancellationToken); + var shouldReportAwaitJavascript = + currentReportJavascriptSettings.WaitForJavascriptCompletedSignal + || globalJavascriptSettings.WaitForJavascriptCompletedSignal; + if (shouldReportAwaitJavascript) + { + TimeSpan globalTimeout = globalJavascriptSettings.WaitForCompletedSignalTimeout; + TimeSpan? currentReportTimeout = + currentReportJavascriptSettings.WaitForCompletedSignalTimeout; + + TimeSpan timeout = currentReportTimeout ?? globalTimeout; + + var didNotHitTimeOut = await browserPage.WaitForJsFlagAsync(timeout, cancellationToken); + if (!didNotHitTimeOut) + { + return new CompletedSignalTimeoutProblem(); + } + } + await browserPage.ConvertPageToPdf(pipeWriter, pageSettings, cancellationToken); } catch (Exception e) diff --git a/src/BlazorReports/Services/BrowserServices/BrowserPage.cs b/src/BlazorReports/Services/BrowserServices/BrowserPage.cs index e2df393..852f1cf 100644 --- a/src/BlazorReports/Services/BrowserServices/BrowserPage.cs +++ b/src/BlazorReports/Services/BrowserServices/BrowserPage.cs @@ -70,6 +70,145 @@ await connection.SendAsync( ); } + /// + /// Checks if a JavaScript flag is set + /// + /// Flag to be checked + /// cancellation token + /// Returns the flag current value + internal async Task CheckIfFlagIsSetAsync( + string flagName, + CancellationToken stoppingToken = default + ) + { + await connection.ConnectAsync(stoppingToken); + + BrowserMessage evaluateMessage = new("Runtime.evaluate"); + evaluateMessage.Parameters.Add("expression", $"window.{flagName} === true"); + evaluateMessage.Parameters.Add("returnByValue", true); + evaluateMessage.Parameters.Add("awaitPromise", true); + + bool isFlagSet = false; + + await connection.SendAsync( + evaluateMessage, + RuntimeEvaluateResponseSerializationContext + .Default + .BrowserResultResponseRuntimeEvaluateResponse, + evaluateResponse => + { + if (!evaluateResponse.Result.WasThrown && evaluateResponse.Result.Result?.Type == "boolean") + { + isFlagSet = evaluateResponse.Result.Result?.Value?.GetBoolean() ?? false; + } + }, + stoppingToken + ); + + return isFlagSet; + } + + /// + /// Waits for a JavaScript flag to be set + /// + /// + /// + /// + internal async Task WaitForJsFlagAsync(TimeSpan timeout, CancellationToken stoppingToken) + { + string script = $"waitForSignal({timeout.TotalMilliseconds})"; + + var result = await EvaluateJavaScriptAsync(script, stoppingToken); + + return result == "Signal received"; + } + + /// + /// Checks if a JavaScript flag exists + /// + /// + /// + /// + internal async Task DoesJsFlagExistAsync( + string flagName, + CancellationToken stoppingToken = default + ) + { + await connection.ConnectAsync(stoppingToken); + + // JavaScript Expression: Check if the flag exists in the window object + string jsCheck = $"typeof window.{flagName} !== 'undefined'"; + + BrowserMessage evaluateMessage = new("Runtime.evaluate"); + evaluateMessage.Parameters.Add("expression", jsCheck); + evaluateMessage.Parameters.Add("returnByValue", true); + evaluateMessage.Parameters.Add("awaitPromise", true); + + bool flagExists = false; + + await connection.SendAsync( + evaluateMessage, + RuntimeEvaluateResponseSerializationContext + .Default + .BrowserResultResponseRuntimeEvaluateResponse, + evaluateResponse => + { + // If JavaScript returns "true", it means the flag exists + if (evaluateResponse.Result.Result?.Type == "boolean") + { + flagExists = evaluateResponse.Result.Result?.Value?.GetBoolean() ?? false; + } + }, + stoppingToken + ); + + return flagExists; + } + + /// + /// Evaluates JavaScript code + /// + /// + /// + /// + internal async Task EvaluateJavaScriptAsync( + string script, + CancellationToken stoppingToken + ) + { + var evaluateMessage = new BrowserMessage("Runtime.evaluate"); + evaluateMessage.Parameters.Add("expression", script); + evaluateMessage.Parameters.Add("returnByValue", true); + evaluateMessage.Parameters.Add("awaitPromise", true); + + string? evaluatedValue = null; + + await connection.SendAsync( + evaluateMessage, + RuntimeEvaluateResponseSerializationContext + .Default + .BrowserResultResponseRuntimeEvaluateResponse, + evaluateResponse => + { + if ( + evaluateResponse.Result.WasThrown && evaluateResponse.Result.ExceptionDetails is not null + ) + { + // Handle JS error + return; + } + + if (evaluateResponse.Result.Result?.Type == "string") + { + evaluatedValue = evaluateResponse.Result.Result?.Value?.GetString(); + } + }, + stoppingToken + ); + + return evaluatedValue; + } + internal async ValueTask ConvertPageToPdf( PipeWriter pipeWriter, BlazorReportsPageSettings pageSettings, diff --git a/src/BlazorReports/Services/BrowserServices/BrowserService.cs b/src/BlazorReports/Services/BrowserServices/BrowserService.cs index 39f3ade..26df2ea 100644 --- a/src/BlazorReports/Services/BrowserServices/BrowserService.cs +++ b/src/BlazorReports/Services/BrowserServices/BrowserService.cs @@ -48,14 +48,22 @@ IBrowserFactory browserFactory /// The pipe writer to write the report to /// The HTML to use in the report /// The page settings to use in the report + /// The current report javascript settings /// The cancellation token /// The result of the operation public async ValueTask< - OneOf + OneOf< + Success, + ServerBusyProblem, + OperationCancelledProblem, + BrowserProblem, + CompletedSignalTimeoutProblem + > > GenerateReport( PipeWriter pipeWriter, string html, BlazorReportsPageSettings pageSettings, + BlazorReportCurrentReportJavascriptSettings currentReportJavascriptSettings, CancellationToken cancellationToken ) { @@ -80,7 +88,13 @@ out var browser return browserStartProblem; } - return await browser.GenerateReport(pipeWriter, html, pageSettings, cancellationToken); + return await browser.GenerateReport( + pipeWriter, + html, + pageSettings, + currentReportJavascriptSettings, + cancellationToken + ); } /// diff --git a/src/BlazorReports/Services/BrowserServices/Factories/BrowserFactory.cs b/src/BlazorReports/Services/BrowserServices/Factories/BrowserFactory.cs index 4472519..06a0df3 100644 --- a/src/BlazorReports/Services/BrowserServices/Factories/BrowserFactory.cs +++ b/src/BlazorReports/Services/BrowserServices/Factories/BrowserFactory.cs @@ -30,7 +30,7 @@ IBrowserPageFactory browserPageFactory public async ValueTask> CreateBrowser() { var browserOptions = options.Value.BrowserOptions; - + var globalJavascriptSettings = options.Value.GlobalJavascriptSettings; var browserExecutableLocation = browserOptions.BrowserExecutableLocation is not null ? browserOptions.BrowserExecutableLocation.FullName : BrowserFinder.Find(browserOptions.Browser); @@ -93,6 +93,7 @@ public async ValueTask> CreateBrowser() devToolsActivePortDirectory, connection, browserOptions, + globalJavascriptSettings, browserLogger, browserPageFactory ); diff --git a/src/BlazorReports/Services/BrowserServices/IBrowserService.cs b/src/BlazorReports/Services/BrowserServices/IBrowserService.cs index 6ec10f6..ddf19b2 100644 --- a/src/BlazorReports/Services/BrowserServices/IBrowserService.cs +++ b/src/BlazorReports/Services/BrowserServices/IBrowserService.cs @@ -17,14 +17,22 @@ public interface IBrowserService /// The pipe writer to write the report to /// The HTML to use in the report /// The page settings to use in the report + /// The current report javascript settings /// The cancellation token /// The result of the operation ValueTask< - OneOf + OneOf< + Success, + ServerBusyProblem, + OperationCancelledProblem, + BrowserProblem, + CompletedSignalTimeoutProblem + > > GenerateReport( PipeWriter pipeWriter, string html, BlazorReportsPageSettings pageSettings, + BlazorReportCurrentReportJavascriptSettings currentReportJavascriptSettings, CancellationToken cancellationToken ); } diff --git a/src/BlazorReports/Services/BrowserServices/Problems/CompletedSignalTimeoutProblem.cs b/src/BlazorReports/Services/BrowserServices/Problems/CompletedSignalTimeoutProblem.cs new file mode 100644 index 0000000..e2e68e0 --- /dev/null +++ b/src/BlazorReports/Services/BrowserServices/Problems/CompletedSignalTimeoutProblem.cs @@ -0,0 +1,6 @@ +namespace BlazorReports.Services.BrowserServices.Problems; + +/// +/// Represents a problem where the javascript has timed out +/// +public readonly record struct CompletedSignalTimeoutProblem; diff --git a/src/BlazorReports/Services/BrowserServices/Responses/RuntimeEvaluateResponse.cs b/src/BlazorReports/Services/BrowserServices/Responses/RuntimeEvaluateResponse.cs new file mode 100644 index 0000000..f6663dc --- /dev/null +++ b/src/BlazorReports/Services/BrowserServices/Responses/RuntimeEvaluateResponse.cs @@ -0,0 +1,44 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace BlazorReports.Services.BrowserServices.Responses; + +/// +/// Response returned from the "Runtime.evaluate" DevTools command +/// +/// The evaluation result object if the command succeeded +/// Details about any exception thrown in JS +/// Whether an exception was thrown +public sealed record RuntimeEvaluateResponse( + [property: JsonPropertyName("result")] RuntimeEvaluateResult? Result, + [property: JsonPropertyName("exceptionDetails")] + RuntimeEvaluateExceptionDetails? ExceptionDetails, + [property: JsonPropertyName("wasThrown")] bool WasThrown +); + +/// +/// Contains the result of a "Runtime.evaluate" call +/// +/// The type of the result (e.g., "string", "object", "undefined") +/// The raw JSON value (if returnByValue was used) +/// A textual description (especially for objects/functions) +public sealed record RuntimeEvaluateResult( + [property: JsonPropertyName("type")] string? Type, + [property: JsonPropertyName("value")] JsonElement? Value, + [property: JsonPropertyName("description")] string? Description +); + +/// +/// Contains exception details if JS threw an error +/// +/// The error message text +public sealed record RuntimeEvaluateExceptionDetails( + [property: JsonPropertyName("text")] string? Text +); + +/// +/// JSON serialization context for the "Runtime.evaluate" DevTools command +/// +[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +[JsonSerializable(typeof(BrowserResultResponse))] +internal sealed partial class RuntimeEvaluateResponseSerializationContext : JsonSerializerContext; diff --git a/src/BlazorReports/Services/IReportService.cs b/src/BlazorReports/Services/IReportService.cs index 3f8d66f..d1f5c78 100644 --- a/src/BlazorReports/Services/IReportService.cs +++ b/src/BlazorReports/Services/IReportService.cs @@ -17,14 +17,22 @@ public interface IReportService /// /// The pipe writer to write the report to /// The data to use in the report + /// The report to generate /// The cancellation token /// The component to use in the report /// The type of data to use in the report /// The generated report ValueTask< - OneOf + OneOf< + Success, + ServerBusyProblem, + OperationCancelledProblem, + BrowserProblem, + CompletedSignalTimeoutProblem + > > GenerateReport( PipeWriter pipeWriter, + BlazorReport report, TD data, CancellationToken cancellationToken = default ) @@ -41,7 +49,13 @@ public interface IReportService /// The type of data to use in the report /// The generated report ValueTask< - OneOf + OneOf< + Success, + ServerBusyProblem, + OperationCancelledProblem, + BrowserProblem, + CompletedSignalTimeoutProblem + > > GenerateReport( PipeWriter pipeWriter, BlazorReport blazorReport, @@ -58,7 +72,13 @@ public interface IReportService /// The cancellation token /// The generated report ValueTask< - OneOf + OneOf< + Success, + ServerBusyProblem, + OperationCancelledProblem, + BrowserProblem, + CompletedSignalTimeoutProblem + > > GenerateReport( PipeWriter pipeWriter, BlazorReport blazorReport, diff --git a/src/BlazorReports/Services/JavascriptServices/JavascriptContainer.cs b/src/BlazorReports/Services/JavascriptServices/JavascriptContainer.cs new file mode 100644 index 0000000..976f2b0 --- /dev/null +++ b/src/BlazorReports/Services/JavascriptServices/JavascriptContainer.cs @@ -0,0 +1,44 @@ +using System.Text; +using Microsoft.AspNetCore.Hosting; + +namespace BlazorReports.Services.JavascriptServices; + +internal sealed class JavascriptContainer +{ + /// + /// A dictionary of (FileName -> FileContent) for all .js files. + /// + public Dictionary Scripts { get; } = new(); + + /// + /// Scans the wwwroot/js folder (or any folder you specify) and reads + /// all .js file contents into memory. + /// + public JavascriptContainer(IWebHostEnvironment env) + { + // 1. Get the physical path to the wwwroot folder + var webRootPath = env.WebRootPath; + + // 2. Build path to the "js" subfolder + var jsDir = Path.Combine(webRootPath, "js"); + + if (Directory.Exists(jsDir)) + { + // 3. Get all *.js files recursively + var allJsFiles = Directory.GetFiles(jsDir, "*.js", SearchOption.AllDirectories); + + foreach (var filePath in allJsFiles) + { + // For example: "C:\MyApp\wwwroot\js\somefolder\file.js" + // We'll use the file name (e.g. "file.js") or a relative path as key + var fileName = Path.GetFileName(filePath); + + // 4. Read the file text content + var content = File.ReadAllText(filePath, Encoding.UTF8); + + // 5. Store in the dictionary + Scripts[fileName] = content; + } + } + } +} diff --git a/src/BlazorReports/Services/ReportService.cs b/src/BlazorReports/Services/ReportService.cs index 6ba3aae..40dfb0e 100644 --- a/src/BlazorReports/Services/ReportService.cs +++ b/src/BlazorReports/Services/ReportService.cs @@ -3,10 +3,12 @@ using BlazorReports.Models; using BlazorReports.Services.BrowserServices; using BlazorReports.Services.BrowserServices.Problems; +using BlazorReports.Services.JavascriptServices; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using OneOf; using OneOf.Types; @@ -31,15 +33,23 @@ IBrowserService browserService /// Generates a report using the specified component and data /// /// The pipe writer to write the report to + /// The report to generate /// The data to use in the report /// The cancellation token /// The component to use in the report /// The type of data to use in the report /// The generated report public async ValueTask< - OneOf + OneOf< + Success, + ServerBusyProblem, + OperationCancelledProblem, + BrowserProblem, + CompletedSignalTimeoutProblem + > > GenerateReport( PipeWriter pipeWriter, + BlazorReport report, TD data, CancellationToken cancellationToken = default ) @@ -74,6 +84,7 @@ public async ValueTask< pipeWriter, html, reportRegistry.DefaultPageSettings, + report.CurrentReportJavascriptSettings, cancellationToken ); } @@ -88,7 +99,13 @@ public async ValueTask< /// The type of data to use in the report /// The generated report public async ValueTask< - OneOf + OneOf< + Success, + ServerBusyProblem, + OperationCancelledProblem, + BrowserProblem, + CompletedSignalTimeoutProblem + > > GenerateReport( PipeWriter pipeWriter, BlazorReport blazorReport, @@ -99,6 +116,9 @@ public async ValueTask< { using var scope = serviceProvider.CreateScope(); var loggerFactory = scope.ServiceProvider.GetRequiredService(); + var javascriptContainer = scope.ServiceProvider.GetRequiredService(); + var options = scope.ServiceProvider.GetRequiredService>(); + var javascriptInternalSettings = options.Value.GlobalJavascriptSettings; var baseStyles = string.Empty; if (!string.IsNullOrEmpty(blazorReport.BaseStyles)) @@ -134,6 +154,25 @@ public async ValueTask< { baseComponentParameters.Add("BaseStyles", baseStyles); } + if (javascriptContainer.Scripts.Count > 0) + { + baseComponentParameters.Add("Scripts", javascriptContainer.Scripts); + } + //Checks if the report has a custom signal if not use the global one + if (blazorReport.CurrentReportJavascriptSettings.ReportIsReadySignal is not null) + { + baseComponentParameters.Add( + "ReportIsReadySignal", + blazorReport.CurrentReportJavascriptSettings.ReportIsReadySignal + ); + } + else + { + baseComponentParameters.Add( + "ReportIsReadySignal", + javascriptInternalSettings.ReportIsReadySignal + ); + } baseComponentParameters.Add("ChildComponentType", blazorReport.Component); baseComponentParameters.Add("ChildComponentParameters", childComponentParameters); @@ -151,7 +190,13 @@ public async ValueTask< var pageSettings = blazorReport.PageSettings ?? reportRegistry.DefaultPageSettings; - return await browserService.GenerateReport(pipeWriter, html, pageSettings, cancellationToken); + return await browserService.GenerateReport( + pipeWriter, + html, + pageSettings, + blazorReport.CurrentReportJavascriptSettings, + cancellationToken + ); } else { @@ -175,7 +220,13 @@ public async ValueTask< /// The cancellation token /// The generated report public ValueTask< - OneOf + OneOf< + Success, + ServerBusyProblem, + OperationCancelledProblem, + BrowserProblem, + CompletedSignalTimeoutProblem + > > GenerateReport( PipeWriter pipeWriter, BlazorReport blazorReport,