mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-09 16:04:43 +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.HttpOverrides;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Rewrite;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
@ -56,7 +57,6 @@ namespace BTCPayServer.Hosting
|
||||||
}
|
}
|
||||||
public ILoggerFactory LoggerFactory { get; }
|
public ILoggerFactory LoggerFactory { get; }
|
||||||
public Logs Logs { get; }
|
public Logs Logs { get; }
|
||||||
|
|
||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddMemoryCache();
|
services.AddMemoryCache();
|
||||||
|
@ -146,6 +146,8 @@ namespace BTCPayServer.Hosting
|
||||||
.AddPlugins(services, Configuration, LoggerFactory)
|
.AddPlugins(services, Configuration, LoggerFactory)
|
||||||
.AddControllersAsServices();
|
.AddControllersAsServices();
|
||||||
|
|
||||||
|
services.AddServerSideBlazor();
|
||||||
|
|
||||||
LowercaseTransformer.Register(services);
|
LowercaseTransformer.Register(services);
|
||||||
ValidateControllerNameTransformer.Register(services);
|
ValidateControllerNameTransformer.Register(services);
|
||||||
|
|
||||||
|
@ -248,6 +250,13 @@ namespace BTCPayServer.Hosting
|
||||||
rateLimits.SetZone($"zone={ZoneLimits.ForgotPassword} rate=5r/d burst=5 nodelay");
|
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();
|
app.UseHeadersOverride();
|
||||||
var forwardingOptions = new ForwardedHeadersOptions()
|
var forwardingOptions = new ForwardedHeadersOptions()
|
||||||
{
|
{
|
||||||
|
@ -264,15 +273,18 @@ namespace BTCPayServer.Hosting
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
app.UseCors();
|
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
|
app.UseStaticFiles(new StaticFileOptions
|
||||||
{
|
{
|
||||||
OnPrepareResponse = ctx =>
|
OnPrepareResponse = LongCache
|
||||||
{
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// The framework during publish automatically publish the js files into
|
// The framework during publish automatically publish the js files into
|
||||||
|
@ -300,10 +312,12 @@ namespace BTCPayServer.Hosting
|
||||||
HttpOnly = Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy.Always,
|
HttpOnly = Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy.Always,
|
||||||
Secure = Microsoft.AspNetCore.Http.CookieSecurePolicy.SameAsRequest
|
Secure = Microsoft.AspNetCore.Http.CookieSecurePolicy.SameAsRequest
|
||||||
});
|
});
|
||||||
|
|
||||||
app.UseEndpoints(endpoints =>
|
app.UseEndpoints(endpoints =>
|
||||||
{
|
{
|
||||||
AppHub.Register(endpoints);
|
AppHub.Register(endpoints);
|
||||||
PaymentRequestHub.Register(endpoints);
|
PaymentRequestHub.Register(endpoints);
|
||||||
|
endpoints.MapBlazorHub().RequireAuthorization();
|
||||||
endpoints.MapRazorPages();
|
endpoints.MapRazorPages();
|
||||||
endpoints.MapControllers();
|
endpoints.MapControllers();
|
||||||
endpoints.MapControllerRoute("default", "{controller:validate=UIHome}/{action:lowercase=Index}/{id?}");
|
endpoints.MapControllerRoute("default", "{controller:validate=UIHome}/{action:lowercase=Index}/{id?}");
|
||||||
|
@ -311,6 +325,14 @@ namespace BTCPayServer.Hosting
|
||||||
app.UsePlugins();
|
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()
|
private static Action<Microsoft.AspNetCore.StaticFiles.StaticFileResponseContext> NewMethod()
|
||||||
{
|
{
|
||||||
return ctx =>
|
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/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/moment/moment.min.js" asp-append-version="true"></script>
|
||||||
<script src="~/vendor/flatpickr/flatpickr.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="~/js/copy-to-clipboard.js" asp-append-version="true"></script>
|
||||||
<script src="~/main/utils.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>
|
<script src="~/main/site.js" asp-append-version="true"></script>
|
||||||
|
|
|
@ -48,3 +48,19 @@
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</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="sticky-header">
|
||||||
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3 mb-3">
|
<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">
|
<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>
|
<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">
|
<button id="exportCSV" class="btn btn-primary text-nowrap" type="button">
|
||||||
|
|
|
@ -444,6 +444,22 @@
|
||||||
top: .125rem;
|
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) {
|
@media (max-width: 991px) {
|
||||||
:root {
|
:root {
|
||||||
--header-height: var(--mobile-header-height);
|
--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) {
|
@media (max-width: 449px) {
|
||||||
#StoreSelector {
|
#StoreSelector {
|
||||||
max-width: 40vw;
|
max-width: 40vw;
|
||||||
|
|
|
@ -1064,3 +1064,9 @@ input.ts-wrapper.form-control:not(.ts-hidden-accessible,.ts-inline) {
|
||||||
.btn[data-clipboard-confirming] {
|
.btn[data-clipboard-confirming] {
|
||||||
color: var(--btcpay-success) !important;
|
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