Add Blazor server (#5312)

* Add Blazor server

* Improve Blazor status UI

* Improve UX

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
This commit is contained in:
Nicolas Dorier 2023-09-13 13:13:15 +09:00 committed by GitHub
parent 4aedf76f1f
commit 73a4ac599c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 171 additions and 10 deletions

View file

@ -0,0 +1,7 @@
@using System.Net.Http
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.JSInterop

View file

@ -25,6 +25,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Rewrite;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@ -56,7 +57,6 @@ namespace BTCPayServer.Hosting
}
public ILoggerFactory LoggerFactory { get; }
public Logs Logs { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMemoryCache();
@ -146,6 +146,8 @@ namespace BTCPayServer.Hosting
.AddPlugins(services, Configuration, LoggerFactory)
.AddControllersAsServices();
services.AddServerSideBlazor();
LowercaseTransformer.Register(services);
ValidateControllerNameTransformer.Register(services);
@ -248,6 +250,13 @@ namespace BTCPayServer.Hosting
rateLimits.SetZone($"zone={ZoneLimits.ForgotPassword} rate=5r/d burst=5 nodelay");
}
// HACK: blazor server js hard code some path, making it works only on root path. This fix it.
// Workaround this bug https://github.com/dotnet/aspnetcore/issues/43191
var rewriteOptions = new RewriteOptions();
rewriteOptions.AddRewrite("_blazor/(negotiate|initializers|disconnect)$", "/_blazor/$1", skipRemainingRules: true);
rewriteOptions.AddRewrite("_blazor$", "/_blazor", skipRemainingRules: true);
app.UseRewriter(rewriteOptions);
app.UseHeadersOverride();
var forwardingOptions = new ForwardedHeadersOptions()
{
@ -264,15 +273,18 @@ namespace BTCPayServer.Hosting
app.UseRouting();
app.UseCors();
// HACK: Make blazor js available on: ~/_blazorfiles/_framework/blazor.server.js
// Workaround this bug https://github.com/dotnet/aspnetcore/issues/19578
app.UseStaticFiles(new StaticFileOptions()
{
RequestPath = "/_blazorfiles",
FileProvider = new ManifestEmbeddedFileProvider(typeof(ComponentServiceCollectionExtensions).Assembly),
OnPrepareResponse = LongCache
});
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
// Cache static assets for one year, set asp-append-version="true" on references to update on change.
// https://andrewlock.net/adding-cache-control-headers-to-static-files-in-asp-net-core/
const int durationInSeconds = 60 * 60 * 24 * 365;
ctx.Context.Response.Headers[HeaderNames.CacheControl] = "public,max-age=" + durationInSeconds;
}
OnPrepareResponse = LongCache
});
// The framework during publish automatically publish the js files into
@ -300,10 +312,12 @@ namespace BTCPayServer.Hosting
HttpOnly = Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy.Always,
Secure = Microsoft.AspNetCore.Http.CookieSecurePolicy.SameAsRequest
});
app.UseEndpoints(endpoints =>
{
AppHub.Register(endpoints);
PaymentRequestHub.Register(endpoints);
endpoints.MapBlazorHub().RequireAuthorization();
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.MapControllerRoute("default", "{controller:validate=UIHome}/{action:lowercase=Index}/{id?}");
@ -311,6 +325,14 @@ namespace BTCPayServer.Hosting
app.UsePlugins();
}
private static void LongCache(Microsoft.AspNetCore.StaticFiles.StaticFileResponseContext ctx)
{
// Cache static assets for one year, set asp-append-version="true" on references to update on change.
// https://andrewlock.net/adding-cache-control-headers-to-static-files-in-asp-net-core/
const int durationInSeconds = 60 * 60 * 24 * 365;
ctx.Context.Response.Headers[HeaderNames.CacheControl] = "public,max-age=" + durationInSeconds;
}
private static Action<Microsoft.AspNetCore.StaticFiles.StaticFileResponseContext> NewMethod()
{
return ctx =>

View file

@ -1,7 +1,11 @@
<script src="~/vendor/jquery/jquery.min.js" asp-append-version="true"></script>
<script src="~/vendor/jquery/jquery.min.js" asp-append-version="true"></script>
<script src="~/vendor/bootstrap/bootstrap.bundle.min.js" asp-append-version="true"></script>
<script src="~/vendor/moment/moment.min.js" asp-append-version="true"></script>
<script src="~/vendor/flatpickr/flatpickr.js" asp-append-version="true"></script>
<script src="~/js/copy-to-clipboard.js" asp-append-version="true"></script>
<script src="~/main/utils.js" asp-append-version="true"></script>
@if (User.Identity.IsAuthenticated)
{
<script src="~/_blazorfiles/_framework/blazor.server.js" autostart="false" asp-append-version="true"></script>
}
<script src="~/main/site.js" asp-append-version="true"></script>

View file

@ -48,3 +48,19 @@
}
</div>
</footer>
@if (User.Identity.IsAuthenticated)
{
<div id="StatusUpdates" class="toast-container">
<div id="BlazorStatus" class="toast blazor-status" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
<span class="blazor-status__state btcpay-status"></span>
<h6 class="blazor-status__title ms-2 mb-0 me-auto"></h6>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close">
<vc:icon symbol="close" />
</button>
</div>
<div class="toast-body blazor-status__body text-secondary pt-0"></div>
</div>
</div>
}

View file

@ -444,6 +444,22 @@
top: .125rem;
}
#StatusUpdates {
position: fixed;
top: var(--content-padding-top);
right: var(--content-padding-horizontal);
}
#StatusUpdates .blazor-status {
width: 16rem;
border-color: var(--btcpay-border-color);
}
#StatusUpdates .blazor-status__title {
color: var(--btcpay-body-text);
}
#StatusUpdates .blazor-status__body {
padding-left: 2.45rem;
}
@media (max-width: 991px) {
:root {
--header-height: var(--mobile-header-height);
@ -692,6 +708,12 @@
}
}
@media (min-width: 992px) and (min-height: 800px) {
#StatusUpdates {
top: 1.25rem;
}
}
@media (max-width: 449px) {
#StoreSelector {
max-width: 40vw;

View file

@ -1064,3 +1064,9 @@ input.ts-wrapper.form-control:not(.ts-hidden-accessible,.ts-inline) {
.btn[data-clipboard-confirming] {
color: var(--btcpay-success) !important;
}
.blazor-status .btn-close .icon {
width: .75rem;
height: .75rem;
}

View file

@ -338,4 +338,88 @@ document.addEventListener("DOMContentLoaded", () => {
}
});
// Initialize Blazor
if (window.Blazor) {
let isUnloading = false;
window.addEventListener("beforeunload", () => { isUnloading = true; });
class BlazorReconnectionHandler {
reconnecting = false;
async onConnectionDown(options, _error) {
if (this.reconnecting)
return;
this.setBlazorStatus(false);
this.reconnecting = true;
console.warn('Blazor hub connection lost');
await this.reconnect();
}
async reconnect() {
let delays = [500, 1000, 2000, 4000, 8000, 16000, 20000];
let i = 0;
const lastDelay = delays.length - 1;
while (true) {
await this.delay(delays[i]);
try {
if (await Blazor.reconnect())
break;
var refresh = document.createElement('span');
refresh.appendChild(document.createTextNode('Please '));
var refreshLink = document.createElement('a');
refreshLink.href = "#";
refreshLink.textContent = "refresh";
refreshLink.addEventListener('click', (event) => { event.preventDefault(); window.location.reload(); });
refresh.appendChild(refreshLink);
refresh.appendChild(document.createTextNode(' the page.'));
this.setBlazorStatus(false, refresh);
console.warn('Error while reconnecting to Blazor hub (Broken circuit)');
}
catch (err) {
this.setBlazorStatus(false, err + '. Reconnecting...');
console.warn(`Error while reconnecting to Blazor hub (${err})`);
}
i++;
if (i > lastDelay)
i = lastDelay;
}
}
onConnectionUp() {
this.reconnecting = false;
console.debug('Blazor hub connected');
this.setBlazorStatus(true);
}
setBlazorStatus(isConnected, text) {
document.querySelectorAll('.blazor-status').forEach($status => {
const $state = $status.querySelector('.blazor-status__state');
const $title = $status.querySelector('.blazor-status__title');
const $body = $status.querySelector('.blazor-status__body');
$state.classList.remove('btcpay-status--enabled');
$state.classList.remove('btcpay-status--disabled');
$state.classList.add('btcpay-status--' + (isConnected ? 'enabled' : 'disabled'));
$title.textContent = `Backend ${isConnected ? 'connected' : 'disconnected'}`;
if (text instanceof Node) {
$body.innerHTML = '';
$body.appendChild(text);
} else
$body.textContent = text || '';
$body.classList.toggle('d-none', !text);
if (!isConnected && !isUnloading) {
const toast = new bootstrap.Toast($status, { autohide: false });
if (!toast.isShown())
toast.show();
}
});
}
delay(durationMilliseconds) {
return new Promise(resolve => setTimeout(resolve, durationMilliseconds));
}
}
const handler = new BlazorReconnectionHandler();
handler.setBlazorStatus(true);
Blazor.start({
reconnectionHandler: handler
});
}