mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-01-19 05:33:31 +01:00
Dashboard: Load Lightning balance async, display default currency (#3907)
* Dashboard: Load Lightning balance async, display default currency * Simplify approach, improve views and scripts * Remove LightMoney converters
This commit is contained in:
parent
2e2c6aef83
commit
2c3b8d8925
@ -1,60 +1,78 @@
|
||||
@using BTCPayServer.Lightning
|
||||
@using BTCPayServer.TagHelpers
|
||||
@model BTCPayServer.Components.StoreLightningBalance.StoreLightningBalanceViewModel
|
||||
|
||||
<div id="StoreLightningBalance-@Model.Store.Id" class="widget store-lightning-balance">
|
||||
<header class="mb-3">
|
||||
<div class="d-flex gap-3 align-items-center justify-content-between mb-2">
|
||||
<h6>Lightning Balance</h6>
|
||||
<a
|
||||
asp-controller="UIPublicLightningNodeInfo"
|
||||
asp-action="ShowLightningNodeInfo"app-top-items
|
||||
asp-route-cryptoCode="@Model.CryptoCode"
|
||||
asp-route-storeId="@Model.Store.Id"
|
||||
target="_blank"
|
||||
id="PublicNodeInfo">
|
||||
Node Info
|
||||
</a>
|
||||
</header>
|
||||
|
||||
@if (Model.Balance == null)
|
||||
@if (Model.CryptoCode != Model.DefaultCurrency && Model.Balance != null)
|
||||
{
|
||||
<div class="btn-group btn-group-sm gap-0 currency-toggle" role="group">
|
||||
<input type="radio" class="btn-check" name="StoreLightningBalance-currency" id="StoreLightningBalance-currency_@Model.CryptoCode" value="@Model.CryptoCode" autocomplete="off" checked>
|
||||
<label class="btn btn-outline-secondary px-2 py-1" for="StoreLightningBalance-currency_@Model.CryptoCode">@Model.CryptoCode</label>
|
||||
<input type="radio" class="btn-check" name="StoreLightningBalance-currency" id="StoreLightningBalance-currency_@Model.DefaultCurrency" value="@Model.DefaultCurrency" autocomplete="off">
|
||||
<label class="btn btn-outline-secondary px-2 py-1" for="StoreLightningBalance-currency_@Model.DefaultCurrency">@Model.DefaultCurrency</label>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(Model.ProblemDescription))
|
||||
{
|
||||
<p>@Model.ProblemDescription</p>
|
||||
}
|
||||
else
|
||||
else if (Model.Balance != null)
|
||||
{
|
||||
<div class="balances d-flex flex-wrap gap-3">
|
||||
<div class="balances d-flex flex-wrap">
|
||||
@if (Model.TotalOffchain != LightMoney.Zero && Model.Balance.OffchainBalance != null)
|
||||
{
|
||||
<div class="balance">
|
||||
<h3 class="d-inline-block me-1">@Model.TotalOffchain</h3>
|
||||
<span class="text-secondary fw-semibold text-nowrap">@Model.CryptoCode in channels</span>
|
||||
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOffchain">@Model.TotalOffchain</h3>
|
||||
<span class="text-secondary fw-semibold text-nowrap">
|
||||
<span class="currency">@Model.CryptoCode</span> in channels
|
||||
</span>
|
||||
|
||||
<div class="balance-details collapse" id="balanceDetailsOffchain">
|
||||
@if (Model.Balance.OffchainBalance.Opening != null && Model.Balance.OffchainBalance.Opening != LightMoney.Zero)
|
||||
{
|
||||
<div class="mt-2">
|
||||
<span class="fw-semibold">@Model.Balance.OffchainBalance.Opening</span>
|
||||
<span class="text-secondary text-nowrap">@Model.CryptoCode opening channels</span>
|
||||
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Opening">
|
||||
@Model.Balance.OffchainBalance.Opening
|
||||
</span>
|
||||
<span class="text-secondary text-nowrap">
|
||||
<span class="currency">@Model.CryptoCode</span> opening channels
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@if (Model.Balance.OffchainBalance.Local != null)
|
||||
{
|
||||
<div class="mt-2">
|
||||
<span class="fw-semibold">@Model.Balance.OffchainBalance.Local</span>
|
||||
<span class="text-secondary text-nowrap">@Model.CryptoCode local balance</span>
|
||||
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Local">
|
||||
@Model.Balance.OffchainBalance.Local
|
||||
</span>
|
||||
<span class="text-secondary text-nowrap">
|
||||
<span class="currency">@Model.CryptoCode</span> local balance
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@if (Model.Balance.OffchainBalance.Remote != null)
|
||||
{
|
||||
<div class="mt-2">
|
||||
<span class="fw-semibold">@Model.Balance.OffchainBalance.Remote</span>
|
||||
<span class="text-secondary text-nowrap">@Model.CryptoCode remote balance</span>
|
||||
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Remote">
|
||||
@Model.Balance.OffchainBalance.Remote
|
||||
</span>
|
||||
<span class="text-secondary text-nowrap">
|
||||
<span class="currency">@Model.CryptoCode</span> remote balance
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@if (Model.Balance.OffchainBalance.Closing != null && Model.Balance.OffchainBalance.Closing != LightMoney.Zero)
|
||||
{
|
||||
<div class="mt-2">
|
||||
<span class="fw-semibold">@Model.Balance.OffchainBalance.Closing</span>
|
||||
<span class="text-secondary text-nowrap">@Model.CryptoCode closing channels</span>
|
||||
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Closing">
|
||||
@Model.Balance.OffchainBalance.Closing
|
||||
</span>
|
||||
<span class="text-secondary text-nowrap">
|
||||
<span class="currency">@Model.CryptoCode</span> closing channels
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@ -63,28 +81,42 @@
|
||||
@if (Model.TotalOnchain != LightMoney.Zero && Model.Balance.OnchainBalance != null)
|
||||
{
|
||||
<div class="balance">
|
||||
<h3 class="d-inline-block me-1">@Model.TotalOnchain</h3>
|
||||
<span class="text-secondary fw-semibold text-nowrap">@Model.CryptoCode on-chain</span>
|
||||
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOnchain">@Model.TotalOnchain</h3>
|
||||
<span class="text-secondary fw-semibold text-nowrap">
|
||||
<span class="currency">@Model.CryptoCode</span> on-chain
|
||||
</span>
|
||||
<div class="balance-details collapse" id="balanceDetailsOnchain">
|
||||
@if (Model.Balance.OnchainBalance.Confirmed != null && Model.Balance.OnchainBalance.Confirmed != LightMoney.Zero)
|
||||
{
|
||||
<div class="mt-2">
|
||||
<span class="fw-semibold">@Model.Balance.OnchainBalance.Confirmed</span>
|
||||
<span class="text-secondary text-nowrap">@Model.CryptoCode confirmed</span>
|
||||
<span class="fw-semibold" data-balance="@Model.Balance.OnchainBalance.Confirmed">
|
||||
@Model.Balance.OnchainBalance.Confirmed
|
||||
</span>
|
||||
<span class="text-secondary text-nowrap">
|
||||
<span class="currency">@Model.CryptoCode</span> confirmed
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@if (Model.Balance.OnchainBalance.Unconfirmed != null && Model.Balance.OnchainBalance.Unconfirmed != LightMoney.Zero)
|
||||
{
|
||||
<div class="mt-2">
|
||||
<span class="fw-semibold">@Model.Balance.OnchainBalance.Unconfirmed</span>
|
||||
<span class="text-secondary text-nowrap">@Model.CryptoCode unconfirmed</span>
|
||||
<span class="fw-semibold" data-balance="@Model.Balance.OnchainBalance.Unconfirmed">
|
||||
@Model.Balance.OnchainBalance.Unconfirmed
|
||||
</span>
|
||||
<span class="text-secondary text-nowrap">
|
||||
<span class="currency">@Model.CryptoCode</span> unconfirmed
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@if (Model.Balance.OnchainBalance.Reserved != null && Model.Balance.OnchainBalance.Reserved != LightMoney.Zero)
|
||||
{
|
||||
<div class="mt-2">
|
||||
<span class="fw-semibold">@Model.Balance.OnchainBalance.Reserved</span>
|
||||
<span class="text-secondary text-nowrap">@Model.CryptoCode reserved</span>
|
||||
<span class="fw-semibold" data-balance="@Model.Balance.OnchainBalance.Reserved">
|
||||
@Model.Balance.OnchainBalance.Reserved
|
||||
</span>
|
||||
<span class="text-secondary text-nowrap">
|
||||
<span class="currency">@Model.CryptoCode</span> reserved
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@ -96,4 +128,55 @@
|
||||
<a class="d-inline-block mt-3" role="button" data-bs-toggle="collapse" data-bs-target=".balance-details" aria-expanded="false" aria-controls="balanceDetailsOffchain balanceDetailsOnchain">Show details</a>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="loading d-flex justify-content-center p-3">
|
||||
<div class="spinner-border text-light" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(async () => {
|
||||
const url = @Safe.Json(Url.Action("LightningBalance", "UIStores", new { storeId = Model.Store.Id, cryptoCode = Model.CryptoCode }));
|
||||
const storeId = @Safe.Json(Model.Store.Id);
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
document.getElementById(`StoreLightningBalance-${storeId}`).outerHTML = await response.text();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
const storeId = @Safe.Json(Model.Store.Id);
|
||||
const cryptoCode = @Safe.Json(Model.CryptoCode);
|
||||
const defaultCurrency = @Safe.Json(Model.DefaultCurrency);
|
||||
const divisibility = @Safe.Json(Model.CurrencyData.Divisibility);
|
||||
const id = `StoreLightningBalance-${storeId}`;
|
||||
|
||||
const render = rate => {
|
||||
const currency = rate ? defaultCurrency : cryptoCode;
|
||||
document.querySelectorAll(`#${id} .currency`).forEach(c => c.innerText = currency)
|
||||
document.querySelectorAll(`#${id} [data-balance]`).forEach(c => {
|
||||
const value = Number.parseFloat(c.dataset.balance);
|
||||
c.innerText = rate
|
||||
? DashboardUtils.displayDefaultCurrency(value, rate, currency, divisibility)
|
||||
: value
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
delegate('change', `#${id} .currency-toggle input`, async e => {
|
||||
const { target } = e;
|
||||
if (target.value === defaultCurrency) {
|
||||
const rate = await DashboardUtils.fetchRate(`${cryptoCode}_${defaultCurrency}`);
|
||||
if (rate) render(rate);
|
||||
} else {
|
||||
render(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
@ -10,6 +10,7 @@ using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
@ -20,6 +21,7 @@ public class StoreLightningBalance : ViewComponent
|
||||
{
|
||||
private string _cryptoCode;
|
||||
private readonly StoreRepository _storeRepo;
|
||||
private readonly CurrencyNameTable _currencies;
|
||||
private readonly BTCPayServerOptions _btcpayServerOptions;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private readonly LightningClientFactoryService _lightningClientFactory;
|
||||
@ -28,6 +30,7 @@ public class StoreLightningBalance : ViewComponent
|
||||
|
||||
public StoreLightningBalance(
|
||||
StoreRepository storeRepo,
|
||||
CurrencyNameTable currencies,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
BTCPayServerOptions btcpayServerOptions,
|
||||
LightningClientFactoryService lightningClientFactory,
|
||||
@ -35,6 +38,7 @@ public class StoreLightningBalance : ViewComponent
|
||||
IOptions<ExternalServicesOptions> externalServiceOptions)
|
||||
{
|
||||
_storeRepo = storeRepo;
|
||||
_currencies = currencies;
|
||||
_networkProvider = networkProvider;
|
||||
_btcpayServerOptions = btcpayServerOptions;
|
||||
_externalServiceOptions = externalServiceOptions;
|
||||
@ -43,48 +47,40 @@ public class StoreLightningBalance : ViewComponent
|
||||
_cryptoCode = _networkProvider.DefaultNetwork.CryptoCode;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(StoreData store)
|
||||
public async Task<IViewComponentResult> InvokeAsync(StoreLightningBalanceViewModel vm)
|
||||
{
|
||||
var walletId = new WalletId(store.Id, _cryptoCode);
|
||||
var lightningClient = GetLightningClient(store);
|
||||
var vm = new StoreLightningBalanceViewModel
|
||||
{
|
||||
Store = store,
|
||||
CryptoCode = _cryptoCode,
|
||||
WalletId = walletId
|
||||
};
|
||||
if (vm.Store == null) throw new ArgumentNullException(nameof(vm.Store));
|
||||
|
||||
if (lightningClient != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var balance = await lightningClient.GetBalance();
|
||||
vm.Balance = balance;
|
||||
vm.TotalOnchain = balance.OnchainBalance != null
|
||||
? (balance.OnchainBalance.Confirmed?? 0) + (balance.OnchainBalance.Reserved ?? 0) +
|
||||
(balance.OnchainBalance.Unconfirmed ?? 0)
|
||||
: null;
|
||||
vm.TotalOffchain = balance.OffchainBalance != null
|
||||
? (balance.OffchainBalance.Opening?? 0) + (balance.OffchainBalance.Local?? 0) +
|
||||
(balance.OffchainBalance.Closing?? 0)
|
||||
: null;
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
// not all implementations support balance fetching
|
||||
vm.ProblemDescription = "Your node does not support balance fetching.";
|
||||
}
|
||||
catch
|
||||
{
|
||||
// general error
|
||||
vm.ProblemDescription = "Could not fetch Lightning balance.";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
vm.ProblemDescription = "Cannot instantiate Lightning client.";
|
||||
}
|
||||
vm.CryptoCode = _cryptoCode;
|
||||
vm.DefaultCurrency = vm.Store.GetStoreBlob().DefaultCurrency;
|
||||
vm.CurrencyData = _currencies.GetCurrencyData(vm.DefaultCurrency, true);
|
||||
|
||||
if (vm.InitialRendering) return View(vm);
|
||||
|
||||
try
|
||||
{
|
||||
var lightningClient = GetLightningClient(vm.Store);
|
||||
var balance = await lightningClient.GetBalance();
|
||||
vm.Balance = balance;
|
||||
vm.TotalOnchain = balance.OnchainBalance != null
|
||||
? (balance.OnchainBalance.Confirmed?? 0) + (balance.OnchainBalance.Reserved ?? 0) +
|
||||
(balance.OnchainBalance.Unconfirmed ?? 0)
|
||||
: null;
|
||||
vm.TotalOffchain = balance.OffchainBalance != null
|
||||
? (balance.OffchainBalance.Opening?? 0) + (balance.OffchainBalance.Local?? 0) +
|
||||
(balance.OffchainBalance.Closing?? 0)
|
||||
: null;
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
// not all implementations support balance fetching
|
||||
vm.ProblemDescription = "Your node does not support balance fetching.";
|
||||
}
|
||||
catch
|
||||
{
|
||||
// general error
|
||||
vm.ProblemDescription = "Could not fetch Lightning balance.";
|
||||
}
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
|
@ -1,15 +1,20 @@
|
||||
using BTCPayServer.Client.JsonConverters;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Components.StoreLightningBalance;
|
||||
|
||||
public class StoreLightningBalanceViewModel
|
||||
{
|
||||
public string CryptoCode { get; set; }
|
||||
public string DefaultCurrency { get; set; }
|
||||
public CurrencyData CurrencyData { get; set; }
|
||||
public StoreData Store { get; set; }
|
||||
public WalletId WalletId { get; set; }
|
||||
public LightMoney TotalOnchain { get; set; }
|
||||
public LightMoney TotalOffchain { get; set; }
|
||||
public LightningNodeBalance Balance { get; set; }
|
||||
public string ProblemDescription { get; set; }
|
||||
public bool InitialRendering { get; set; }
|
||||
}
|
||||
|
@ -6,7 +6,15 @@
|
||||
<div id="StoreLightningServices-@Model.Store.Id" class="widget store-lightning-services">
|
||||
<header class="mb-4">
|
||||
<h6>Lightning Services</h6>
|
||||
<a class asp-controller="UIStores" asp-action="Lightning" asp-route-storeId="@Model.Store.Id" asp-route-cryptoCode="@Model.CryptoCode">Details</a>
|
||||
<a
|
||||
asp-controller="UIPublicLightningNodeInfo"
|
||||
asp-action="ShowLightningNodeInfo"app-top-items
|
||||
asp-route-cryptoCode="@Model.CryptoCode"
|
||||
asp-route-storeId="@Model.Store.Id"
|
||||
target="_blank"
|
||||
id="PublicNodeInfo">
|
||||
Node Info
|
||||
</a>
|
||||
</header>
|
||||
<div id="Services" class="services-list">
|
||||
@foreach (var service in Model.Services)
|
||||
|
@ -1,43 +1,26 @@
|
||||
@using BTCPayServer.Services.Wallets
|
||||
@model BTCPayServer.Components.StoreWalletBalance.StoreWalletBalanceViewModel
|
||||
|
||||
<style>
|
||||
#DefaultCurrencyToggle .btn {
|
||||
background-color: var(--btcpay-bg-tile);
|
||||
border-color: var(--btcpay-body-border-light);
|
||||
}
|
||||
|
||||
#DefaultCurrencyToggle input:not(:checked) + .btn {
|
||||
color: var(--btcpay-body-text-muted);
|
||||
}
|
||||
</style>
|
||||
<div id="StoreWalletBalance-@Model.Store.Id" class="widget store-wallet-balance">
|
||||
<div class="d-flex gap-3 align-items-center justify-content-between mb-2">
|
||||
<h6>Wallet Balance</h6>
|
||||
@if (Model.CryptoCode != Model.DefaultCurrency)
|
||||
{
|
||||
<div class="btn-group btn-group-sm gap-0" role="group" id="DefaultCurrencyToggle">
|
||||
<input type="radio" class="btn-check" name="currency" id="currency_@Model.CryptoCode" value="@Model.CryptoCode" autocomplete="off" checked>
|
||||
<label class="btn btn-outline-secondary px-2 py-1" for="currency_@Model.CryptoCode">@Model.CryptoCode</label>
|
||||
<input type="radio" class="btn-check" name="currency" id="currency_@Model.DefaultCurrency" value="@Model.DefaultCurrency" autocomplete="off">
|
||||
<label class="btn btn-outline-secondary px-2 py-1" for="currency_@Model.DefaultCurrency">@Model.DefaultCurrency</label>
|
||||
<div class="btn-group btn-group-sm gap-0 currency-toggle" role="group">
|
||||
<input type="radio" class="btn-check" name="StoreWalletBalance-currency" id="StoreWalletBalance-currency_@Model.CryptoCode" value="@Model.CryptoCode" autocomplete="off" checked>
|
||||
<label class="btn btn-outline-secondary px-2 py-1" for="StoreWalletBalance-currency_@Model.CryptoCode">@Model.CryptoCode</label>
|
||||
<input type="radio" class="btn-check" name="StoreWalletBalance-currency" id="StoreWalletBalance-currency_@Model.DefaultCurrency" value="@Model.DefaultCurrency" autocomplete="off">
|
||||
<label class="btn btn-outline-secondary px-2 py-1" for="StoreWalletBalance-currency_@Model.DefaultCurrency">@Model.DefaultCurrency</label>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<header class="mb-3">
|
||||
@if (Model.Balance != null)
|
||||
{
|
||||
<div class="balance" id="Balance-@Model.CryptoCode">
|
||||
<h3 class="d-inline-block me-1">@Model.Balance</h3>
|
||||
<span class="text-secondary fw-semibold">@Model.CryptoCode</span>
|
||||
<div class="balance">
|
||||
<h3 class="d-inline-block me-1" data-balance="@Model.Balance">@Model.Balance</h3>
|
||||
<span class="text-secondary fw-semibold currency">@Model.CryptoCode</span>
|
||||
</div>
|
||||
@if (Model.CryptoCode != Model.DefaultCurrency)
|
||||
{
|
||||
<div class="balance" id="Balance-@Model.DefaultCurrency">
|
||||
<h3 class="d-inline-block" id="DefaultCurrencyBalance"></h3>
|
||||
<span class="text-secondary fw-semibold">@Model.DefaultCurrency</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if (Model.Series != null)
|
||||
{
|
||||
@ -65,19 +48,13 @@
|
||||
}
|
||||
<script>
|
||||
(function () {
|
||||
const balance = @Safe.Json(Model.Balance);
|
||||
const storeId = @Safe.Json(Model.Store.Id);
|
||||
const cryptoCode = @Safe.Json(Model.CryptoCode);
|
||||
const defaultCurrency = @Safe.Json(Model.DefaultCurrency);
|
||||
const divisibility = @Safe.Json(Model.CurrencyData.Divisibility);
|
||||
const pathBase = @Safe.Json(Context.Request.PathBase);
|
||||
let data = { series: @Safe.Json(Model.Series), labels: @Safe.Json(Model.Labels), balance: @Safe.Json(Model.Balance) };
|
||||
let rate = null;
|
||||
|
||||
const $cryptoBalance = document.getElementById(`Balance-${cryptoCode}`);
|
||||
const $defaultBalance = document.getElementById(`Balance-${defaultCurrency}`);
|
||||
const $defaultCurrencyBalance = document.getElementById('DefaultCurrencyBalance');
|
||||
|
||||
const id = `StoreWalletBalance-${storeId}`;
|
||||
const baseUrl = @Safe.Json(Url.Action("WalletHistogram", "UIWallets", new { walletId = Model.WalletId, type = WalletHistogramType.Week }));
|
||||
const chartOpts = {
|
||||
@ -85,23 +62,23 @@
|
||||
showArea: true,
|
||||
axisY: {
|
||||
labelInterpolationFnc: value => rate
|
||||
? displayDefaultCurrency(value, defaultCurrency).toString()
|
||||
? DashboardUtils.displayDefaultCurrency(value, rate, defaultCurrency, divisibility).toString()
|
||||
: value
|
||||
}
|
||||
};
|
||||
|
||||
const render = data => {
|
||||
let { series, labels, balance } = data;
|
||||
if (balance)
|
||||
document.querySelector(`#${id} h3`).innerText = balance;
|
||||
if (cryptoCode !== defaultCurrency) {
|
||||
$cryptoBalance.style.display = rate ? 'none' : 'block';
|
||||
$defaultBalance.style.display = rate ? 'block' : 'none';
|
||||
}
|
||||
let { series, labels } = data;
|
||||
const currency = rate ? defaultCurrency : cryptoCode;
|
||||
document.querySelectorAll(`#${id} .currency`).forEach(c => c.innerText = currency)
|
||||
document.querySelectorAll(`#${id} [data-balance]`).forEach(c => {
|
||||
const value = Number.parseFloat(c.dataset.balance);
|
||||
c.innerText = rate
|
||||
? DashboardUtils.displayDefaultCurrency(value, rate, currency, divisibility)
|
||||
: value
|
||||
});
|
||||
if (!series) return;
|
||||
|
||||
if (rate)
|
||||
series = data.series.map(i => toDefaultCurrency(i, rate));
|
||||
const min = Math.min(...series);
|
||||
const max = Math.max(...series);
|
||||
const low = Math.max(min - ((max - min) / 5), 0);
|
||||
@ -110,6 +87,7 @@
|
||||
labels,
|
||||
series: [series]
|
||||
}, renderOpts);
|
||||
|
||||
// prevent y-axis labels from getting cut off
|
||||
window.setTimeout(() => {
|
||||
const yLabels = [...document.querySelectorAll('.ct-label.ct-vertical.ct-start')];
|
||||
@ -133,16 +111,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
const toDefaultCurrency = (value, rate) => {
|
||||
return Math.round((value * rate) * 100) / 100;
|
||||
};
|
||||
|
||||
const displayDefaultCurrency = (value, currency) => {
|
||||
const locale = currency === "USD" ? 'en-US' : navigator.language;
|
||||
const opts = { currency, style: 'decimal', minimumFractionDigits: divisibility };
|
||||
return new Intl.NumberFormat(locale, opts).format(value);
|
||||
};
|
||||
|
||||
render(data);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
@ -150,20 +118,11 @@
|
||||
const type = e.target.value;
|
||||
await update(type);
|
||||
})
|
||||
delegate('change', '#DefaultCurrencyToggle input', async e => {
|
||||
delegate('change', `#${id} .currency-toggle input`, async e => {
|
||||
const { target } = e;
|
||||
if (target.value === defaultCurrency) {
|
||||
const currencyPair = `${cryptoCode}_${defaultCurrency}`;
|
||||
const response = await fetch(`${pathBase}/api/rates?storeId=${storeId}¤cyPairs=${currencyPair}`);
|
||||
const json = await response.json();
|
||||
rate = json[0] && json[0].rate;
|
||||
if (rate) {
|
||||
const value = toDefaultCurrency(balance, rate);
|
||||
$defaultCurrencyBalance.innerText = displayDefaultCurrency(value, defaultCurrency);
|
||||
render(data);
|
||||
} else {
|
||||
console.warn(`Fetching rate for ${currencyPair} failed.`);
|
||||
}
|
||||
rate = await DashboardUtils.fetchRate(`${cryptoCode}_${defaultCurrency}`);
|
||||
if (rate) render(data);
|
||||
} else {
|
||||
rate = null;
|
||||
render(data);
|
||||
|
74
BTCPayServer/Controllers/UIStoresController.Dashboard.cs
Normal file
74
BTCPayServer/Controllers/UIStoresController.Dashboard.cs
Normal file
@ -0,0 +1,74 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Components.StoreLightningBalance;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class UIStoresController
|
||||
{
|
||||
[HttpGet("{storeId}")]
|
||||
public async Task<IActionResult> Dashboard()
|
||||
{
|
||||
var store = CurrentStore;
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
|
||||
AddPaymentMethods(store, storeBlob,
|
||||
out var derivationSchemes, out var lightningNodes);
|
||||
|
||||
var walletEnabled = derivationSchemes.Any(scheme => !string.IsNullOrEmpty(scheme.Value) && scheme.Enabled);
|
||||
var lightningEnabled = lightningNodes.Any(ln => !string.IsNullOrEmpty(ln.Address) && ln.Enabled);
|
||||
var vm = new StoreDashboardViewModel
|
||||
{
|
||||
WalletEnabled = walletEnabled,
|
||||
LightningEnabled = lightningEnabled,
|
||||
StoreId = CurrentStore.Id,
|
||||
StoreName = CurrentStore.StoreName,
|
||||
IsSetUp = walletEnabled || lightningEnabled
|
||||
};
|
||||
|
||||
// Widget data
|
||||
if (vm.WalletEnabled || vm.LightningEnabled)
|
||||
{
|
||||
var userId = GetUserId();
|
||||
var apps = await _appService.GetAllApps(userId, false, store.Id);
|
||||
vm.Apps = apps
|
||||
.Select(a =>
|
||||
{
|
||||
var appData = _appService.GetAppDataIfOwner(userId, a.Id).Result;
|
||||
appData.StoreData = store;
|
||||
return appData;
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/lightning/{cryptoCode}/balance")]
|
||||
public async Task<IActionResult> LightningBalance(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var vm = new StoreLightningBalanceViewModel { Store = store };
|
||||
return ViewComponent("StoreLightningBalance", new { vm });
|
||||
}
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Components.StoreLightningBalance;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Lightning;
|
||||
@ -13,6 +14,7 @@ using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@ -20,6 +22,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class UIStoresController
|
||||
{
|
||||
|
||||
[HttpGet("{storeId}/lightning/{cryptoCode}")]
|
||||
public IActionResult Lightning(string storeId, string cryptoCode)
|
||||
{
|
||||
|
@ -132,44 +132,6 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
public StoreData CurrentStore => HttpContext.GetStoreData();
|
||||
|
||||
[HttpGet("{storeId}")]
|
||||
public async Task<IActionResult> Dashboard()
|
||||
{
|
||||
var store = CurrentStore;
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
|
||||
AddPaymentMethods(store, storeBlob,
|
||||
out var derivationSchemes, out var lightningNodes);
|
||||
|
||||
var walletEnabled = derivationSchemes.Any(scheme => !string.IsNullOrEmpty(scheme.Value) && scheme.Enabled);
|
||||
var lightningEnabled = lightningNodes.Any(ln => !string.IsNullOrEmpty(ln.Address) && ln.Enabled);
|
||||
var vm = new StoreDashboardViewModel
|
||||
{
|
||||
WalletEnabled = walletEnabled,
|
||||
LightningEnabled = lightningEnabled,
|
||||
StoreId = CurrentStore.Id,
|
||||
StoreName = CurrentStore.StoreName,
|
||||
IsSetUp = walletEnabled || lightningEnabled
|
||||
};
|
||||
|
||||
// Widget data
|
||||
if (vm.WalletEnabled || vm.LightningEnabled)
|
||||
{
|
||||
var userId = GetUserId();
|
||||
var apps = await _appService.GetAllApps(userId, false, store.Id);
|
||||
vm.Apps = apps
|
||||
.Select(a =>
|
||||
{
|
||||
var appData = _appService.GetAppDataIfOwner(userId, a.Id).Result;
|
||||
appData.StoreData = store;
|
||||
return appData;
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/users")]
|
||||
public async Task<IActionResult> StoreUsers(StoreUsersViewModel vm)
|
||||
|
@ -348,11 +348,6 @@ namespace BTCPayServer.Controllers
|
||||
: Json(data);
|
||||
}
|
||||
|
||||
private static string GetLabelTarget(WalletId walletId, uint256 txId)
|
||||
{
|
||||
return $"{walletId}:{txId}";
|
||||
}
|
||||
|
||||
[HttpGet("{walletId}/receive")]
|
||||
public IActionResult WalletReceive([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId)
|
||||
{
|
||||
|
@ -1,3 +1,8 @@
|
||||
@using BTCPayServer.Components.StoreLightningBalance
|
||||
@using BTCPayServer.Components.StoreNumbers
|
||||
@using BTCPayServer.Components.StoreWalletBalance
|
||||
@using BTCPayServer.TagHelpers
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@model StoreDashboardViewModel;
|
||||
|
||||
@inject BTCPayNetworkProvider networkProvider
|
||||
@ -56,6 +61,28 @@
|
||||
/* include chart library inline so that it instantly renders */
|
||||
<link rel="stylesheet" href="~/vendor/chartist/chartist.css" asp-append-version="true">
|
||||
<script src="~/vendor/chartist/chartist.min.js" asp-append-version="true"></script>
|
||||
<script>
|
||||
const DashboardUtils = {
|
||||
toDefaultCurrency(amount, rate) {
|
||||
return Math.round((amount * rate) * 100) / 100;
|
||||
},
|
||||
displayDefaultCurrency(amount, rate, currency, divisibility) {
|
||||
const value = DashboardUtils.toDefaultCurrency(amount, rate);
|
||||
const locale = currency === 'USD' ? 'en-US' : navigator.language;
|
||||
const opts = { currency, style: 'decimal', minimumFractionDigits: divisibility };
|
||||
return new Intl.NumberFormat(locale, opts).format(value);
|
||||
},
|
||||
async fetchRate(currencyPair) {
|
||||
const storeId = @Safe.Json(Context.GetRouteValue("storeId"));
|
||||
const pathBase = @Safe.Json(Context.Request.PathBase);
|
||||
const response = await fetch(`${pathBase}/api/rates?storeId=${storeId}¤cyPairs=${currencyPair}`);
|
||||
const json = await response.json();
|
||||
const rate = json[0] && json[0].rate;
|
||||
if (rate) return rate;
|
||||
else console.warn(`Fetching rate for ${currencyPair} failed.`);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<div id="Dashboard" class="mt-4">
|
||||
@if (Model.WalletEnabled)
|
||||
{
|
||||
@ -87,7 +114,7 @@
|
||||
<vc:store-numbers store="@store"/>
|
||||
@if (Model.LightningEnabled)
|
||||
{
|
||||
<vc:store-lightning-balance store="@store"/>
|
||||
<vc:store-lightning-balance vm="@(new StoreLightningBalanceViewModel { Store = store, InitialRendering = true })"/>
|
||||
<vc:store-lightning-services store="@store"/>
|
||||
}
|
||||
@if (Model.WalletEnabled)
|
||||
|
@ -330,6 +330,15 @@ svg.icon-note {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.widget .currency-toggle .btn {
|
||||
background-color: var(--btcpay-bg-tile);
|
||||
border-color: var(--btcpay-body-border-light);
|
||||
}
|
||||
|
||||
.widget .currency-toggle input:not(:checked) + .btn {
|
||||
color: var(--btcpay-body-text-muted);
|
||||
}
|
||||
|
||||
.widget .btn-group {
|
||||
display: inline-flex;
|
||||
gap: var(--btcpay-space-m);
|
||||
|
Loading…
Reference in New Issue
Block a user