Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f360785
Feat: wwwroot JavaScript integration
Lorefist5 Mar 21, 2025
2ca5ec9
fix: script as a string changed to MarkupString
Lorefist5 Mar 21, 2025
2748df1
Feat: Signal flag created
Lorefist5 Mar 21, 2025
f595196
Enhance report management and JavaScript integration
Lorefist5 Mar 21, 2025
e7e2fa7
Feat: Changing Blazor reports to await for a js signal rather than po…
Lorefist5 Mar 21, 2025
fb8f516
Fix: changed signals to be transmited from javascript and awaited in …
Lorefist5 Mar 22, 2025
3509cb8
fix: Included an extra bracket by mistake.
Lorefist5 Mar 22, 2025
e8337f4
Enhance report generation and JavaScript timeout handling
Lorefist5 Mar 22, 2025
02427ab
remove:Remove the hello js data from .js to avoid dupplicated logic.
Lorefist5 Mar 22, 2025
56b242d
chore:fixed formating
Lorefist5 Mar 22, 2025
d5d0543
CodeFormating
Lorefist5 Mar 22, 2025
8acd01a
Code clean up
Lorefist5 Mar 22, 2025
75d063a
CSharpier formatting
Lorefist5 Mar 22, 2025
4fd1925
remove:HelloReport.razor changes removed.
Lorefist5 Mar 22, 2025
12fdfa4
removed:Removed custom options from AddBlazorReports, reverting to de…
Lorefist5 Mar 23, 2025
5135ce0
refactor: Renamed SimpleJsTimeoutReport to SimpleJsAsyncReport
Lorefist5 Mar 23, 2025
23cf4e3
removed:string for the ReportName removed.
Lorefist5 Mar 23, 2025
7ce06c7
remove: Eliminated reduntant Output Format specification.
Lorefist5 Mar 23, 2025
e25e0fc
remove: removed report specific timeout setting.
Lorefist5 Mar 23, 2025
a13704e
chore: improve inline documentation in report script
Lorefist5 Mar 23, 2025
380fc59
rememove: removal of redundant "!" operator.
Lorefist5 Mar 23, 2025
728d478
refactor: shouldReportAwaitJavascript moved closer to if statment.
Lorefist5 Mar 23, 2025
c96d89f
rename: JavascriptTimedoutProblem to CompletedSignalTimeoutProblem
Lorefist5 Mar 23, 2025
3708f09
fix:script should be below body to access the DOM
Lorefist5 Mar 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
<PackageVersion Include="Microsoft.Extensions.Http.Polly" Version="8.0.0" />
<PackageVersion Include="BlazorMonaco" Version="3.3.0" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="8.0.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="7.2.0" />
</ItemGroup>
</Project>
1 change: 1 addition & 0 deletions examples/SimpleReportServer/HelloReport.razor
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

<h3>Hello @Data.Name</h3>

@code {
Expand Down
5 changes: 5 additions & 0 deletions examples/SimpleReportServer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using BlazorReports.Models;
using ExampleTemplates.Reports;
using SimpleReportServer;
using SimpleReportServer.Reports;

var builder = WebApplication.CreateSlimBuilder(args);

Expand All @@ -19,6 +20,10 @@

var reportsGroup = app.MapGroup("reports");

reportsGroup.MapBlazorReport<SimpleJsAsyncReport, SimpleJsAsyncReportData>(opts =>
{
opts.JavascriptSettings.WaitForJavascriptCompletedSignal = true;
});
reportsGroup.MapBlazorReport<HelloReport, HelloReportData>();
reportsGroup.MapBlazorReport<HelloReport, HelloReportData>(opts =>
{
Expand Down
22 changes: 22 additions & 0 deletions examples/SimpleReportServer/Reports/SimpleJsAsyncReport.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

<p id="HelloWorld"></p>
<script>

var element = document.getElementById("HelloWorld");


if (element) {
element.innerHTML = "<h1>Hello from JavaScript</h1>";
}

// Call the globally defined blazorReport.completed() to let BlazorReports know the report's contents have finished generating.
setTimeout(() => {
blazorReport.completed();
}, @Data.TimeSpan.TotalMilliseconds)

</script>
@code {
[Parameter]
public required SimpleJsAsyncReportData Data { get; set; }

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace SimpleReportServer.Reports;

public record SimpleJsAsyncReportData(int TimeoutInSeconds)
{
public TimeSpan TimeSpan => TimeSpan.FromSeconds(TimeoutInSeconds);
};
Empty file.
97 changes: 79 additions & 18 deletions src/BlazorReports.Components/BlazorReportsTemplate.razor
Original file line number Diff line number Diff line change
@@ -1,29 +1,90 @@
<!DOCTYPE html>
<html lang="en">
<head>
<style>@((MarkupString)BaseStyles)</style>
<style>@((MarkupString)BaseStyles)</style>
<script>
// Define BlazorReport class
class BlazorReport {
constructor(signalName) {
this.signalName = signalName;
}

notReady() {
window[this.signalName] = "not-ready";
}

completed() {
window[this.signalName] = "ready";
}
}

// Create global instance with the provided signal name
window.blazorReport = new BlazorReport("@ReportIsReadySignal");

// Function to wait for signal change
window.waitForSignal = (timeoutMs = 5000, intervalMs = 50) => {
var expectedValue = 'ready'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change to blazorReportsSignalReady

return new Promise((resolve, reject) => {
const start = Date.now();

const check = () => {
if (window["@ReportIsReadySignal"] === expectedValue) {
clearInterval(interval);
resolve('Signal received');
} else if (Date.now() - start > timeoutMs) {
clearInterval(interval);
reject(new Error('Timeout waiting for signal'));
}
};

const interval = setInterval(check, intervalMs);
check(); // In case it's already ready
});
};
</script>
</head>
<body>
<DynamicComponent Type="@ChildComponentType" Parameters="@ChildComponentParameters" />
<DynamicComponent Type="@ChildComponentType" Parameters="@ChildComponentParameters" />

@foreach (var script in Scripts)
{
<script>
@((MarkupString)@script.Value)
</script>
}
</body>
</html>


@code {
/// <summary>
/// The base styles to be applied to the component.
/// </summary>
[Parameter]
public string BaseStyles { get; set; } = "";
/// <summary>
/// The type of the component to be rendered in the html body.
/// </summary>
[Parameter]
public Type ChildComponentType { get; set; } = null!;
/// <summary>
/// The parameters to be passed to the child component.
/// </summary>
[Parameter]
public IDictionary<string, object> ChildComponentParameters { get; set; } = null!;
/// <summary>
/// The base styles to be applied to the component.
/// </summary>
[Parameter]
public string BaseStyles { get; set; } = "";

/// <summary>
/// The type of the component to be rendered in the html body.
/// </summary>
[Parameter]
public Type ChildComponentType { get; set; } = null!;

/// <summary>
/// The parameters to be passed to the child component.
/// </summary>
[Parameter]
public IDictionary<string, object> ChildComponentParameters { get; set; } = null!;

/// <summary>
/// The scripts to be included in the html body.
/// </summary>
[Parameter]
public Dictionary<string, string> Scripts { get; set; } = new Dictionary<string, string>();

/// <summary>
/// The name of the JavaScript signal used to indicate when the report is ready.
/// </summary>
[Parameter]
public string ReportIsReadySignal { get; set; } = "reportIsReady";


}
8 changes: 6 additions & 2 deletions src/BlazorReports/Extensions/ReportExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ CancellationToken token
_ => (int?)null,
_ => StatusCodes.Status503ServiceUnavailable,
_ => StatusCodes.Status499ClientClosedRequest,
_ => StatusCodes.Status500InternalServerError
_ => StatusCodes.Status500InternalServerError,
_ => StatusCodes.Status408RequestTimeout
);

if (errorStatusCode is not null)
Expand All @@ -121,6 +122,7 @@ CancellationToken token
}
)
.Produces<FileStreamHttpResult>(200, blazorReport.GetContentType())
.Produces(StatusCodes.Status408RequestTimeout)
.Produces(StatusCodes.Status503ServiceUnavailable);
}

Expand Down Expand Up @@ -174,7 +176,8 @@ CancellationToken token
_ => (int?)null,
_ => StatusCodes.Status503ServiceUnavailable,
_ => StatusCodes.Status499ClientClosedRequest,
_ => StatusCodes.Status500InternalServerError
_ => StatusCodes.Status500InternalServerError,
_ => StatusCodes.Status408RequestTimeout
);

if (errorStatusCode is not null)
Expand All @@ -185,6 +188,7 @@ CancellationToken token
}
)
.Produces<FileStreamHttpResult>(200, blazorReport.GetContentType())
.Produces(StatusCodes.Status408RequestTimeout)
.Produces(StatusCodes.Status503ServiceUnavailable);
}

Expand Down
3 changes: 3 additions & 0 deletions src/BlazorReports/Extensions/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,6 +27,8 @@ public static IServiceCollection AddBlazorReports(
{
services.Configure(options ?? (_ => { }));
services.AddSingleton<BlazorReportRegistry>();
services.AddSingleton<JavascriptContainer>();

services.AddSingleton<IConnectionFactory, ConnectionFactory>();
services.AddSingleton<IBrowserFactory, BrowserFactory>();
services.AddSingleton<IBrowserPageFactory, BrowserPageFactory>();
Expand Down
5 changes: 5 additions & 0 deletions src/BlazorReports/Models/BlazorReport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,9 @@ public class BlazorReport
/// The page settings to use for the report.
/// </summary>
public BlazorReportsPageSettings? PageSettings { get; set; }

/// <summary>
/// The current report javascript settings.
/// </summary>
public required BlazorReportCurrentReportJavascriptSettings CurrentReportJavascriptSettings { get; set; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To mantain consistency use BlazorReportsJavascriptSettings

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace BlazorReports.Models;

/// <summary>
/// Settings for the internal javascript api
/// </summary>
public class BlazorReportCurrentReportJavascriptSettings
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now one single configuration obect structure might suffice?

Basically joining the global and current report into on single BlazorReportJavascriptSettings

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, makes sense

Copy link
Collaborator Author

@andres-m-rodriguez andres-m-rodriguez Mar 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question, if the person does WaitForJavascriptCompletedSignal what should be the default timeout value?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably let's leave it at 1s if it's not completed, to not affect the performance too much if they forget. But this will throw an error right?

{
/// <summary>
/// The signal that the report is ready
/// </summary>
public string ReportIsReadySignal { get; set; } = default!;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be internal for the time being.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I remove it from both the settings?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes


/// <summary>
/// Decides if the report should wait for the complete signal in the javascript
/// </summary>
public bool WaitForJavascriptCompletedSignal { get; set; }

/// <summary>
/// The amount of time a reports javascript can take until it is considered to have timed out
/// </summary>
public TimeSpan? WaitForCompletedSignalTimeout { get; set; }

/// <summary>
/// The default settings for the current report javascript settings
/// </summary>
public static BlazorReportCurrentReportJavascriptSettings Default =>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The defaults can be added to each property

new BlazorReportCurrentReportJavascriptSettings
{
WaitForJavascriptCompletedSignal = false,
WaitForCompletedSignalTimeout = TimeSpan.FromSeconds(3),
};
}
24 changes: 24 additions & 0 deletions src/BlazorReports/Models/BlazorReportGlobalJavascriptSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace BlazorReports.Models;

/// <summary>
///
/// </summary>
public class BlazorReportGlobalJavascriptSettings
{
/// <summary>
/// The signal that the report is ready
/// </summary>
public string ReportIsReadySignal { get; set; } = "reportIsReady";

/// <summary>
/// Decides if the report should wait for the complete signal in the javascript.
/// (Default: false)
/// </summary>
public bool WaitForJavascriptCompletedSignal { get; set; }

/// <summary>
/// The amount of time a reports javascript can take until it is considered to have timed out
/// (Default: null)
/// </summary>
public TimeSpan WaitForCompletedSignalTimeout { get; set; } = TimeSpan.FromSeconds(3);
}
5 changes: 5 additions & 0 deletions src/BlazorReports/Models/BlazorReportRegistrationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,9 @@ public class BlazorReportRegistrationOptions
/// Settings for generating a PDF
/// </summary>
public BlazorReportsPageSettings PageSettings { get; set; } = new();

/// <summary>
/// Javascript api settings
/// </summary>
public BlazorReportCurrentReportJavascriptSettings JavascriptSettings { get; set; } = new();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What other options would be placed here? Thinking about moving the signal and timeout options as a regular setting instead of nesting another object

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As reading it like this, the options don't necesarrily configure Javascript but will just wait for the signal, what do you think?

Copy link
Collaborator Author

@andres-m-rodriguez andres-m-rodriguez Mar 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could either rename the object to JavascriptReportSignalSettings or something along those lines...
or we could have something like a js configuration that would have JavascriptReportSignalSettings inside of it if we ever want to give users more flexibility on how to handle the js files in their project, for example

Solution 1

public class JavascriptReportOptions
{
    /// <summary>
    /// Whether to include the razor.js code-behind file associated with the component (e.g., MyReport.razor.js).
    /// </summary>
    public bool IncludeCodeBehindScript { get; set; } = true;

    /// <summary>
    /// Whether to include all global scripts registered in the wwwroot or shared locations for this specific report.
    /// </summary>
    public bool IncludeGlobalScripts { get; set; } = true;

    /// <summary>
    /// Settings related to how the JS signals the report is ready.
    /// </summary>
    public JavascriptReportSignalSettings Signal { get; set; } = new();

    /// <summary>
    /// Whether to defer script execution until the page is fully loaded so it wouldn't matter if they were placed at the 
    /// 'head' tag or below the body tag
    /// </summary>
    public bool DeferScripts { get; set; } = true;

    /// <summary>
    /// Custom script URLs to include before rendering.
    /// </summary>
    public List<string> CustomScriptUrls { get; set; } = new();

    /// <summary>
    /// Custom inline scripts to inject directly into the page.
    /// </summary>
    public List<string> InlineScripts { get; set; } = new();
}

The JavascriptReportOptions would go inside the BlazorReportRegistrationOptions


Solution 2

The other option would be to just do

public class BlazorReportRegistrationOptions
{
    /// <summary>
    /// Output format for the report. Defaults to PDF.
    /// </summary>
    public ReportOutputFormat OutputFormat { get; set; } = ReportOutputFormat.Pdf;

    /// <summary>
    /// The name of the report. This is utilized to generate the route for the report.
    /// </summary>
    public string? ReportName { get; set; }

    /// <summary>
    /// Base styles path for the report.
    /// </summary>
    public string? BaseStylesPath { get; set; }

    /// <summary>
    /// Assets path for the report.
    /// </summary>
    public string? AssetsPath { get; set; }

    /// <summary>
    /// Settings for generating a PDF
    /// </summary>
    public BlazorReportsPageSettings PageSettings { get; set; } = new();

    // --- JavaScript options defined directly here ---

    /// <summary>
    /// Whether to include the report's `.razor.js` code-behind file.
    /// </summary>
    public bool IncludeCodeBehindScript { get; set; } = true;

    /// <summary>
    /// Whether to include globally registered JS scripts.
    /// </summary>
    public bool IncludeGlobalScripts { get; set; } = true;

    /// <summary>
    /// Name of the global JS signal to watch for report readiness.
    /// </summary>
    public string? ReportIsReadySignal { get; set; }

    /// <summary>
    /// Whether to wait for the `.completed()` signal from JavaScript.
    /// </summary>
    public bool WaitForJavascriptCompletedSignal { get; set; } = false;

    /// <summary>
    /// Timeout to wait for JS `.completed()` before failing.
    /// </summary>
    public TimeSpan WaitForCompletedSignalTimeout { get; set; } = TimeSpan.FromSeconds(10);
}

I think I prefer the first one pero both sound good honestly

}
4 changes: 4 additions & 0 deletions src/BlazorReports/Models/BlazorReportRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ public BlazorReport AddReport<T>(BlazorReportRegistrationOptions? options = null
Component = typeof(T),
Data = null,
PageSettings = options?.PageSettings,
CurrentReportJavascriptSettings =
options?.JavascriptSettings ?? BlazorReportCurrentReportJavascriptSettings.Default,
};
if (!string.IsNullOrEmpty(options?.BaseStylesPath))
{
Expand Down Expand Up @@ -137,6 +139,8 @@ public BlazorReport AddReport<T, TD>(BlazorReportRegistrationOptions? options =
Component = typeof(T),
Data = typeof(TD),
PageSettings = options?.PageSettings,
CurrentReportJavascriptSettings =
options?.JavascriptSettings ?? BlazorReportCurrentReportJavascriptSettings.Default,
};
if (!string.IsNullOrEmpty(options?.BaseStylesPath))
{
Expand Down
5 changes: 5 additions & 0 deletions src/BlazorReports/Models/BlazorReportsOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,9 @@ public class BlazorReportsOptions
/// Settings for generating a PDF
/// </summary>
public BlazorReportsPageSettings PageSettings { get; set; } = new();

/// <summary>
/// Javascript api settings
/// </summary>
public BlazorReportGlobalJavascriptSettings GlobalJavascriptSettings { get; set; } = new();
}
28 changes: 27 additions & 1 deletion src/BlazorReports/Services/BrowserServices/Browser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ internal sealed class Browser(
DirectoryInfo dataDirectory,
Connection connection,
BlazorReportsBrowserOptions browserOptions,
BlazorReportGlobalJavascriptSettings globalJavascriptSettings,
ILogger logger,
IBrowserPageFactory browserPageFactory
) : IAsyncDisposable
Expand All @@ -30,11 +31,18 @@ IBrowserPageFactory browserPageFactory
private readonly SemaphoreSlim _poolLock = new(1, 1);

public async ValueTask<
OneOf<Success, ServerBusyProblem, OperationCancelledProblem, BrowserProblem>
OneOf<
Success,
ServerBusyProblem,
OperationCancelledProblem,
BrowserProblem,
CompletedSignalTimeoutProblem
>
> GenerateReport(
PipeWriter pipeWriter,
string html,
BlazorReportsPageSettings pageSettings,
BlazorReportCurrentReportJavascriptSettings currentReportJavascriptSettings,
CancellationToken cancellationToken
)
{
Expand Down Expand Up @@ -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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What value do you get out of this variable?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If set to true, it means the report did not hit the timeout—e.g., if the timeout was set 2 seconds and the report finished rendering (including any JavaScript execution) in 1 second, it successfully completed within the allowed time therefore the value will be true (it did not hit the time out).

If set to false, it means the report exceeded the timeout—e.g., if it took 3 seconds to finish due to long-running JavaScript or other dependencies, the timeout would have been hit (did not hit the time out will be false).

Should I rename the variable?
I could also rename the method or reverse the method so that the variable could be
var reportHitTimeout = await browserPage.WaitForJsFlagAsync(timeout, cancellationToken); or something

if (!didNotHitTimeOut)
{
return new CompletedSignalTimeoutProblem();
}
}

await browserPage.ConvertPageToPdf(pipeWriter, pageSettings, cancellationToken);
}
catch (Exception e)
Expand Down
Loading
Loading