Dashboard: Add Lightning balances and services (#3838)

* Update Lightning lib

* Refactoring: Move Lightning methods and props to ExternalServices

* Rename Lightning services

* Add Lightning balance to dashboard

* Split Lightning dashboard tiles

* View updates
This commit is contained in:
d11n
2022-06-14 07:36:22 +02:00
committed by GitHub
parent 4a0f10ea99
commit 479f21f4f3
14 changed files with 451 additions and 40 deletions

View File

@@ -28,7 +28,7 @@
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.3.4" />
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.3.6" />
<PackageReference Include="NBitcoin" Version="7.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>

View File

@@ -48,7 +48,7 @@
<ItemGroup>
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.3.8" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.3.10" />
<PackageReference Include="BuildBundlerMinifier" Version="3.2.449" />
<PackageReference Include="BundlerMinifier.Core" Version="3.2.435" />
<PackageReference Include="BundlerMinifier.TagHelpers" Version="3.2.435" />
@@ -232,5 +232,9 @@
</Content>
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="Components\StoreLightningBalance\Default.cshtml" />
</ItemGroup>
<ProjectExtensions><VisualStudio><UserProperties wwwroot_4swagger_4v1_4swagger_1template_1invoices_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1misc_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1pull-payments_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1serverinfo_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1stores_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1users_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1webhooks_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" /></VisualStudio></ProjectExtensions>
</Project>

View File

@@ -0,0 +1,92 @@
@using BTCPayServer.Lightning
@model BTCPayServer.Components.StoreLightningBalance.StoreLightningBalanceViewModel
<div id="StoreLightningBalance-@Model.Store.Id" class="widget store-lightning-balance">
<header class="mb-3">
<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)
{
<p>@Model.ProblemDescription</p>
}
else
{
<div class="balances d-flex flex-wrap">
<div class="balance me-3">
<h3 class="d-inline-block me-1">@Model.TotalOffchain</h3>
<span class="text-secondary fw-semibold text-nowrap">@Model.CryptoCode in channels</span>
@if (Model.TotalOffchain != LightMoney.Zero && Model.Balance.OffchainBalance != null)
{
<div class="balance-details collapse" id="balanceDetailsOffchain">
@if (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>
</div>
}
<div class="mt-2">
<span class="fw-semibold">@Model.Balance.OffchainBalance.Local</span>
<span class="text-secondary text-nowrap">@Model.CryptoCode local balance</span>
</div>
<div class="mt-2">
<span class="fw-semibold">@Model.Balance.OffchainBalance.Remote</span>
<span class="text-secondary text-nowrap">@Model.CryptoCode remote balance</span>
</div>
@if (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>
</div>
}
</div>
}
</div>
<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>
@if (Model.TotalOnchain != LightMoney.Zero && Model.Balance.OnchainBalance != null)
{
<div class="balance-details collapse" id="balanceDetailsOnchain">
@if (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>
</div>
}
@if (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>
</div>
}
@if (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>
</div>
}
</div>
}
</div>
</div>
@if (Model.Balance.OffchainBalance != null && Model.Balance.OnchainBalance != null && (Model.TotalOffchain != LightMoney.Zero || Model.TotalOnchain != LightMoney.Zero))
{
<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>
}
}
</div>

View File

@@ -0,0 +1,108 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.Lightning;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace BTCPayServer.Components.StoreLightningBalance;
public class StoreLightningBalance : ViewComponent
{
private string _cryptoCode;
private readonly StoreRepository _storeRepo;
private readonly BTCPayNetworkBase _network;
private readonly BTCPayServerOptions _btcpayServerOptions;
private readonly BTCPayNetworkProvider _networkProvider;
private readonly LightningClientFactoryService _lightningClientFactory;
private readonly IOptions<LightningNetworkOptions> _lightningNetworkOptions;
private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
public StoreLightningBalance(
StoreRepository storeRepo,
BTCPayNetworkProvider networkProvider,
BTCPayServerOptions btcpayServerOptions,
LightningClientFactoryService lightningClientFactory,
IOptions<LightningNetworkOptions> lightningNetworkOptions,
IOptions<ExternalServicesOptions> externalServiceOptions)
{
_storeRepo = storeRepo;
_networkProvider = networkProvider;
_btcpayServerOptions = btcpayServerOptions;
_externalServiceOptions = externalServiceOptions;
_lightningClientFactory = lightningClientFactory;
_lightningNetworkOptions = lightningNetworkOptions;
_network = _networkProvider.DefaultNetwork;
_cryptoCode = _network.CryptoCode;
}
public async Task<IViewComponentResult> InvokeAsync(StoreData store)
{
var walletId = new WalletId(store.Id, _cryptoCode);
var lightningClient = GetLightningClient(store);
var vm = new StoreLightningBalanceViewModel
{
Store = store,
CryptoCode = _cryptoCode,
WalletId = walletId
};
if (lightningClient != null)
{
try
{
var balance = await lightningClient.GetBalance();
vm.Balance = balance;
vm.TotalOnchain = balance.OnchainBalance != null
? balance.OnchainBalance.Confirmed + balance.OnchainBalance.Reserved +
balance.OnchainBalance.Unconfirmed
: LightMoney.Zero;
vm.TotalOffchain = balance.OffchainBalance != null
? balance.OffchainBalance.Opening + balance.OffchainBalance.Local +
balance.OffchainBalance.Closing
: LightMoney.Zero;
}
catch (NotSupportedException)
{
// not all implementations support balance fetching
vm.ProblemDescription = "Your node does not support balance fetching.";
}
}
else
{
vm.ProblemDescription = "Cannot instantiate Lightning client.";
}
return View(vm);
}
private ILightningClient GetLightningClient(StoreData store)
{
var network = _networkProvider.GetNetwork<BTCPayNetwork>(_cryptoCode);
var id = new PaymentMethodId(_cryptoCode, PaymentTypes.LightningLike);
var existing = store.GetSupportedPaymentMethods(_networkProvider)
.OfType<LightningSupportedPaymentMethod>()
.FirstOrDefault(d => d.PaymentId == id);
if (existing == null) return null;
if (existing.GetExternalLightningUrl() is {} connectionString)
{
return _lightningClientFactory.Create(connectionString, network);
}
if (existing.IsInternalNode && _lightningNetworkOptions.Value.InternalLightningByCryptoCode.TryGetValue(_cryptoCode, out var internalLightningNode))
{
return _lightningClientFactory.Create(internalLightningNode, network);
}
return null;
}
}

View File

@@ -0,0 +1,18 @@
using System.Collections.Generic;
using BTCPayServer.Data;
using BTCPayServer.Lightning;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
namespace BTCPayServer.Components.StoreLightningBalance;
public class StoreLightningBalanceViewModel
{
public string CryptoCode { 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; }
}

View File

@@ -0,0 +1,36 @@
@using Microsoft.AspNetCore.Mvc.TagHelpers
@model BTCPayServer.Components.StoreLightningServices.StoreLightningServicesViewModel
@if (Model.Services != null && Model.Services.Any())
{
<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>
</header>
<div id="Services" class="services-list">
@foreach (var service in Model.Services)
{
@if (!string.IsNullOrEmpty(service.Error))
{
<div class="service" id="@($"Service-{service.ServiceName}")">
<img src="@($"~/img/{service.Type.ToLower()}.png")" asp-append-version="true" alt="@service.DisplayName" />
<small class="d-block mt-3 lh-sm fw-semibold text-danger">@service.Error</small>
</div>
}
else if (string.IsNullOrEmpty(service.Link))
{
<a asp-controller="UIServer" asp-action="Service" asp-route-serviceName="@service.ServiceName" asp-route-cryptoCode="@service.CryptoCode" class="service" id="@($"Service-{service.ServiceName}")">
<img src="@($"~/img/{service.Type.ToLower()}.png")" asp-append-version="true" alt="@service.DisplayName" />
</a>
}
else
{
<a href="@service.Link" target="_blank" rel="noreferrer noopener" class="service" id="@($"Service-{service.ServiceName}")">
<img src="@($"~/img/{service.Type.ToLower()}.png")" asp-append-version="true" alt="@service.DisplayName" />
</a>
}
}
</div>
</div>
}

View File

@@ -0,0 +1,91 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.Lightning;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace BTCPayServer.Components.StoreLightningServices;
public class StoreLightningServices : ViewComponent
{
private readonly string _cryptoCode;
private readonly BTCPayServerOptions _btcpayServerOptions;
private readonly BTCPayNetworkProvider _networkProvider;
private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
public StoreLightningServices(
BTCPayNetworkProvider networkProvider,
BTCPayServerOptions btcpayServerOptions,
IOptions<ExternalServicesOptions> externalServiceOptions)
{
_networkProvider = networkProvider;
_btcpayServerOptions = btcpayServerOptions;
_externalServiceOptions = externalServiceOptions;
_cryptoCode = _networkProvider.DefaultNetwork.CryptoCode;
}
public IViewComponentResult Invoke(StoreData store)
{
var vm = new StoreLightningServicesViewModel
{
Store = store,
CryptoCode = _cryptoCode,
};
if (vm.LightningNodeType == LightningNodeType.Internal)
{
var services = _externalServiceOptions.Value.ExternalServices.ToList()
.Where(service => ExternalServices.LightningServiceTypes.Contains(service.Type))
.Select(async service =>
{
var model = new AdditionalServiceViewModel
{
DisplayName = service.DisplayName,
ServiceName = service.ServiceName,
CryptoCode = service.CryptoCode,
Type = service.Type.ToString()
};
try
{
model.Link = await service.GetLink(Request.GetAbsoluteUriNoPathBase(), _btcpayServerOptions.NetworkType);
}
catch (Exception exception)
{
model.Error = exception.Message;
}
return model;
})
.Select(t => t.Result)
.ToList();
// other services
foreach ((string key, Uri value) in _externalServiceOptions.Value.OtherExternalServices)
{
if (ExternalServices.LightningServiceNames.Contains(key))
{
services.Add(new AdditionalServiceViewModel
{
DisplayName = key,
ServiceName = key,
Type = key.Replace(" ", ""),
Link = Request.GetAbsoluteUriNoPathBase(value).AbsoluteUri
});
}
}
vm.Services = services;
}
return View(vm);
}
}

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
using BTCPayServer.Data;
using BTCPayServer.Lightning;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
namespace BTCPayServer.Components.StoreLightningServices;
public class StoreLightningServicesViewModel
{
public string CryptoCode { get; set; }
public StoreData Store { get; set; }
public LightningNodeType LightningNodeType { get; set; }
public List<AdditionalServiceViewModel> Services { get; set; }
}

View File

@@ -2,7 +2,9 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using NBitcoin;
namespace BTCPayServer.Configuration
{
@@ -15,13 +17,13 @@ namespace BTCPayServer.Configuration
"lnd server: 'server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
"lnd server: 'server=https://lnd.example.com;macaroondirectorypath=/root/.lnd;certthumbprint=2abdf302...'" + Environment.NewLine +
"Error: {1}",
"LND (gRPC server)");
"LND (gRPC)");
Load(configuration, cryptoCode, "lndrest", ExternalServiceTypes.LNDRest, "Invalid setting {0}, " + Environment.NewLine +
"lnd server: 'server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
"lnd server: 'server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
"lnd server: 'server=https://lnd.example.com;macaroondirectorypath=/root/.lnd;certthumbprint=2abdf302...'" + Environment.NewLine +
"Error: {1}",
"LND (REST server)");
"LND (REST)");
Load(configuration, cryptoCode, "lndseedbackup", ExternalServiceTypes.LNDSeedBackup, "Invalid setting {0}, " + Environment.NewLine +
"lnd seed backup: /etc/merchant_lnd/data/chain/bitcoin/regtest/walletunlock.json'" + Environment.NewLine +
"Error: {1}",
@@ -29,11 +31,11 @@ namespace BTCPayServer.Configuration
Load(configuration, cryptoCode, "spark", ExternalServiceTypes.Spark, "Invalid setting {0}, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/spark/btc/;cookiefile=/etc/clightning_bitcoin_spark/.cookie'" + Environment.NewLine +
"Error: {1}",
"C-Lightning (Spark server)");
"Core Lightning (Spark)");
Load(configuration, cryptoCode, "rtl", ExternalServiceTypes.RTL, "Invalid setting {0}, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/rtl/btc/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'" + Environment.NewLine +
"Error: {1}",
"Ride the Lightning server");
"Ride The Lightning");
Load(configuration, cryptoCode, "thunderhub", ExternalServiceTypes.ThunderHub, "Invalid setting {0}, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/thub/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'" + Environment.NewLine +
"Error: {1}",
@@ -41,12 +43,12 @@ namespace BTCPayServer.Configuration
Load(configuration, cryptoCode, "clightningrest", ExternalServiceTypes.CLightningRest, "Invalid setting {0}, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/clightning-rest/btc/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'" + Environment.NewLine +
"Error: {1}",
"C-Lightning REST");
"Core Lightning (REST)");
Load(configuration, cryptoCode, "charge", ExternalServiceTypes.Charge, "Invalid setting {0}, " + Environment.NewLine +
$"lightning charge server: 'type=charge;server=https://charge.example.com;api-token=2abdf302...'" + Environment.NewLine +
$"lightning charge server: 'type=charge;server=https://charge.example.com;cookiefilepath=/root/.charge/.cookie'" + Environment.NewLine +
"Error: {1}",
"C-Lightning (Charge server)");
"Core Lightning (Charge)");
}
@@ -86,6 +88,18 @@ namespace BTCPayServer.Configuration
&&
o.ServiceName.Equals(serviceName, StringComparison.OrdinalIgnoreCase));
}
public static readonly ExternalServiceTypes[] LightningServiceTypes =
{
ExternalServiceTypes.Spark,
ExternalServiceTypes.RTL,
ExternalServiceTypes.ThunderHub
};
public static readonly string[] LightningServiceNames =
{
"Lightning Terminal"
};
}
public class ExternalService
@@ -95,6 +109,13 @@ namespace BTCPayServer.Configuration
public ExternalConnectionString ConnectionString { get; set; }
public string CryptoCode { get; set; }
public string ServiceName { get; set; }
public async Task<string> GetLink(Uri absoluteUriNoPathBase, ChainName networkType)
{
var connectionString = await ConnectionString.Expand(absoluteUriNoPathBase, Type, networkType);
var tokenParam = Type == ExternalServiceTypes.ThunderHub ? "token" : "access-key";
return $"{connectionString.Server}?{tokenParam}={connectionString.AccessKey}";
}
}
public enum ExternalServiceTypes

View File

@@ -20,17 +20,6 @@ namespace BTCPayServer.Controllers
{
public partial class UIStoresController
{
private readonly ExternalServiceTypes[] _externalServiceTypes =
{
ExternalServiceTypes.Spark,
ExternalServiceTypes.RTL,
ExternalServiceTypes.ThunderHub
};
private readonly string[] _externalServiceNames =
{
"Lightning Terminal"
};
[HttpGet("{storeId}/lightning/{cryptoCode}")]
public IActionResult Lightning(string storeId, string cryptoCode)
{
@@ -48,7 +37,7 @@ namespace BTCPayServer.Controllers
if (vm.LightningNodeType == LightningNodeType.Internal)
{
var services = _externalServiceOptions.Value.ExternalServices.ToList()
.Where(service => _externalServiceTypes.Contains(service.Type))
.Where(service => ExternalServices.LightningServiceTypes.Contains(service.Type))
.Select(async service =>
{
var model = new AdditionalServiceViewModel
@@ -60,7 +49,7 @@ namespace BTCPayServer.Controllers
};
try
{
model.Link = await GetServiceLink(service);
model.Link = await service.GetLink(Request.GetAbsoluteUriNoPathBase(), _BtcpayServerOptions.NetworkType);
}
catch (Exception exception)
{
@@ -74,7 +63,7 @@ namespace BTCPayServer.Controllers
// other services
foreach ((string key, Uri value) in _externalServiceOptions.Value.OtherExternalServices)
{
if (_externalServiceNames.Contains(key))
if (ExternalServices.LightningServiceNames.Contains(key))
{
services.Add(new AdditionalServiceViewModel
{
@@ -398,12 +387,5 @@ namespace BTCPayServer.Controllers
.FirstOrDefault(d => d.PaymentId == id);
return existing;
}
private async Task<string> GetServiceLink(ExternalService service)
{
var connectionString = await service.ConnectionString.Expand(Request.GetAbsoluteUriNoPathBase(), service.Type, _BtcpayServerOptions.NetworkType);
var tokenParam = service.Type == ExternalServiceTypes.ThunderHub ? "token" : "access-key";
return $"{connectionString.Server}?{tokenParam}={connectionString.AccessKey}";
}
}
}

View File

@@ -22,16 +22,12 @@ namespace BTCPayServer.Payments.Lightning
public LightningConnectionString? GetExternalLightningUrl()
{
#pragma warning disable CS0618 // Type or member is obsolete
if (!string.IsNullOrEmpty(LightningConnectionString))
{
if (string.IsNullOrEmpty(LightningConnectionString)) return null;
if (!BTCPayServer.Lightning.LightningConnectionString.TryParse(LightningConnectionString, false, out var connectionString, out var error))
{
throw new FormatException(error);
}
return connectionString;
}
else
return null;
#pragma warning restore CS0618 // Type or member is obsolete
}

View File

@@ -85,6 +85,11 @@
</div>
}
<vc:store-numbers store="@store"/>
@if (Model.LightningEnabled)
{
<vc:store-lightning-balance store="@store"/>
<vc:store-lightning-services store="@store"/>
}
@if (Model.WalletEnabled)
{
<vc:store-recent-transactions store="@store"/>

View File

@@ -40,7 +40,7 @@
@if (Model.Services != null && Model.Services.Any())
{
<div permission="@Policies.CanModifyServerSettings" class="mt-4">
<div permission="@Policies.CanModifyServerSettings" class="mt-5">
<h3 class="mb-3">Services</h3>
<div id="Services" class="services-list">
@foreach (var service in Model.Services)

View File

@@ -233,12 +233,13 @@ svg.icon-note {
/* Services */
.services-list {
display: flex;
flex-wrap: wrap;
gap: var(--btcpay-space-l);
}
.services-list .service {
--service-width: 100px;
flex: 0 0 var(--service-width);
margin: 0 var(--btcpay-space-l) var(--btcpay-space-l) 0;
text-align: center;
}
@@ -334,6 +335,18 @@ svg.icon-note {
color: var(--btcpay-body-link-accent);
}
.widget.store-lightning-balance .balances {
gap: 1.5rem 2.25rem;
}
.widget.store-lightning-services .services-list {
gap: var(--btcpay-space-m);
}
.widget.store-lightning-services .services-list .service {
--service-width: 3rem;
}
.widget.store-numbers {
display: flex;
flex-wrap: wrap;
@@ -390,6 +403,10 @@ svg.icon-note {
}
@media (max-width: 575px) {
.widget.store-lightning-services .services-list .service {
--service-width: 3rem;
}
.widget .store-number {
flex: 0 1 100%;
}
@@ -401,15 +418,41 @@ svg.icon-note {
}
}
@media (min-width: 576px) and (max-width: 1199px) {
.widget.store-lightning-services .services-list {
gap: 1.5rem;
}
.widget.store-lightning-services .services-list .service {
--service-width: 4rem;
}
}
@media (max-width: 1199px) {
/* Reorder so that Lightning is below the wallet balance */
.widget.store-wallet-balance {
order: -3;
}
.widget.store-lightning-balance {
order: -2;
}
.widget.store-lightning-services {
order: -1;
}
}
@media (min-width: 1200px) {
.widget.app-sales,
.widget.setup-guide,
.widget.store-wallet-balance {
.widget.store-wallet-balance,
.widget.store-lightning-balance {
--widget-chart-width: 80vw;
grid-column-start: 1;
grid-column-end: 9;
}
.widget.store-lightning-services,
.widget.app-top-items,
.widget.store-numbers {
grid-column-start: 9;