mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-09 05:14:31 +01:00
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:
parent
4aedf76f1f
commit
73a4ac599c
8 changed files with 171 additions and 10 deletions
7
BTCPayServer/Blazor/_Imports.razor
Normal file
7
BTCPayServer/Blazor/_Imports.razor
Normal 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
|
|
@ -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 =>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
<div class="sticky-header">
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3 mb-3">
|
||||
<h2 class="mb-0">@ViewData["Title"]</h2>
|
||||
<h2 class="mb-0">@ViewData["Title"]</h2>
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
<a cheat-mode="true" class="btn btn-outline-info text-nowrap" asp-action="StoreReports" asp-route-fakeData="true" asp-route-viewName="@Model.Request?.ViewName">Create fake date</a>
|
||||
<button id="exportCSV" class="btn btn-primary text-nowrap" type="button">
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue