From 4fc0197ad92dd72115405561b8737f56862f8d5b Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Sun, 6 Oct 2024 10:06:31 +0200 Subject: [PATCH] Add time selection to balance graph Prerequisite btcpayserver/btcpayserver#6217. Closes #99. --- BTCPayApp.UI/Components/WalletOverview.razor | 2 +- BTCPayApp.UI/Features/StoreState.cs | 25 ++++++++++++++++--- BTCPayApp.UI/Features/UIState.cs | 23 ++++++++++++++++- BTCPayApp.UI/Pages/DashboardPage.razor | 22 ++++++++++++++++ BTCPayApp.UI/Pages/DashboardPage.razor.css | 10 +++++++- .../chartist/chartist-plugin-tooltip.css | 9 ++++--- .../chartist/chartist-plugin-tooltip.js | 18 +++++++------ BTCPayApp.UI/wwwroot/js/global.js | 15 ++++++----- 8 files changed, 101 insertions(+), 23 deletions(-) diff --git a/BTCPayApp.UI/Components/WalletOverview.razor b/BTCPayApp.UI/Components/WalletOverview.razor index d9c7b74..11dae8e 100644 --- a/BTCPayApp.UI/Components/WalletOverview.razor +++ b/BTCPayApp.UI/Components/WalletOverview.razor @@ -73,7 +73,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { if (Histogram != null) - await JS.InvokeVoidAsync("Chart.renderLineChart", "#Histogram", Histogram.Labels, Histogram.Series, "BTC", BitcoinUnit, null, Currency); + await JS.InvokeVoidAsync("Chart.renderLineChart", "#Histogram", Histogram.Labels, Histogram.Series, Histogram.Type.ToString(), "BTC", BitcoinUnit, null, Currency); } public void Dispose() diff --git a/BTCPayApp.UI/Features/StoreState.cs b/BTCPayApp.UI/Features/StoreState.cs index 44a0441..baf60ff 100644 --- a/BTCPayApp.UI/Features/StoreState.cs +++ b/BTCPayApp.UI/Features/StoreState.cs @@ -28,12 +28,13 @@ public record StoreState private static string[] RateFetchExcludes = ["BTC", "SATS"]; public record SetStoreInfo(AppUserStoreInfo? StoreInfo); - public record SetHistogramType(HistogramType? HistogramType); + public record SetHistogramType(HistogramType Type); public record FetchStore(string StoreId); public record FetchOnchainBalance(string StoreId); public record FetchLightningBalance(string StoreId); public record FetchOnchainHistogram(string StoreId, HistogramType? Type = null); public record FetchLightningHistogram(string StoreId, HistogramType? Type = null); + public record FetchHistograms(string StoreId, HistogramType? Type = null); public record FetchBalances(string StoreId, HistogramType? Type = null); public record FetchNotifications(string StoreId); public record UpdateNotification(string NotificationId, bool Seen); @@ -97,7 +98,7 @@ public override StoreState Reduce(StoreState state, SetHistogramType action) { return state with { - HistogramType = action.HistogramType, + HistogramType = action.Type, }; } } @@ -553,7 +554,7 @@ public override StoreState Reduce(StoreState state, SetPosSalesStats action) return histogram; } - public class StoreEffects(IAccountManager accountManager) + public class StoreEffects(IState state, IState uiState, IAccountManager accountManager) { [EffectMethod] public Task SetStoreInfoEffect(SetStoreInfo action, IDispatcher dispatcher) @@ -563,8 +564,9 @@ public Task SetStoreInfoEffect(SetStoreInfo action, IDispatcher dispatcher) { var storeId = store.Id; var posId = store.PosAppId!; + var histogramType = state.Value.HistogramType ?? uiState.Value.HistogramType; dispatcher.Dispatch(new FetchStore(storeId)); - dispatcher.Dispatch(new FetchBalances(storeId)); + dispatcher.Dispatch(new FetchBalances(storeId, histogramType)); dispatcher.Dispatch(new FetchNotifications(storeId)); dispatcher.Dispatch(new FetchInvoices(storeId)); dispatcher.Dispatch(new FetchRates(store)); @@ -609,6 +611,13 @@ public Task FetchBalancesEffect(FetchBalances action, IDispatcher dispatcher) { dispatcher.Dispatch(new FetchOnchainBalance(action.StoreId)); dispatcher.Dispatch(new FetchLightningBalance(action.StoreId)); + dispatcher.Dispatch(new FetchHistograms(action.StoreId, action.Type)); + return Task.CompletedTask; + } + + [EffectMethod] + public Task FetchHistogramsEffect(FetchHistograms action, IDispatcher dispatcher) + { dispatcher.Dispatch(new FetchOnchainHistogram(action.StoreId, action.Type)); dispatcher.Dispatch(new FetchLightningHistogram(action.StoreId, action.Type)); return Task.CompletedTask; @@ -833,6 +842,14 @@ public async Task FetchInvoicePaymentMethodsEffect(FetchInvoicePaymentMethods ac dispatcher.Dispatch(new SetInvoicePaymentMethods(null, error, action.InvoiceId)); } } + + [EffectMethod] + public async Task SetHistogramTypeEffect(SetHistogramType action, IDispatcher dispatcher) + { + var storeInfo = state.Value.StoreInfo; + if (storeInfo != null) + dispatcher.Dispatch(new FetchHistograms(storeInfo.Id, action.Type)); + } } } diff --git a/BTCPayApp.UI/Features/UIState.cs b/BTCPayApp.UI/Features/UIState.cs index 0463d43..dc07c93 100644 --- a/BTCPayApp.UI/Features/UIState.cs +++ b/BTCPayApp.UI/Features/UIState.cs @@ -1,6 +1,7 @@ using System.Text.Json.Serialization; using BTCPayApp.CommonServer.Models; using BTCPayApp.Core; +using BTCPayServer.Client.Models; using Fluxor; using Microsoft.JSInterop; @@ -24,7 +25,9 @@ public record UIState { public string SelectedTheme { get; set; } = Themes.System; public string SystemTheme { get; set; } = Themes.Light; - public string BitcoinUnit{ get; set; } = CurrencyUnit.SATS; + public string BitcoinUnit { get; set; } = CurrencyUnit.SATS; + public HistogramType HistogramType { get; set; } = HistogramType.Week; + [JsonIgnore] public RemoteData? Instance; @@ -33,6 +36,7 @@ public record SetUserTheme(string Theme); public record FetchInstanceInfo(string? Url); public record SetInstanceInfo(AppInstanceInfo? Instance, string? Error); public record ToggleBitcoinUnit(string? BitcoinUnit = null); + public record SetHistogramType(HistogramType Type); protected class SetUserThemeReducer : Reducer { @@ -92,6 +96,17 @@ public override UIState Reduce(UIState state, ToggleBitcoinUnit action) } } + protected class SetHistogramTypeReducer : Reducer + { + public override UIState Reduce(UIState state, SetHistogramType action) + { + return state with + { + HistogramType = action.Type + }; + } + } + public class UIEffects(IJSRuntime jsRuntime, IHttpClientFactory httpClientFactory, IState state) { [EffectMethod] @@ -137,5 +152,11 @@ public async Task ToggleBitcoinUnitEffect(ToggleBitcoinUnit action, IDispatcher { await jsRuntime.InvokeVoidAsync("Interop.setBitcoinUnit", state.Value.BitcoinUnit); } + + [EffectMethod] + public async Task SetHistogramTypeEffect(SetHistogramType action, IDispatcher dispatcher) + { + dispatcher.Dispatch(new StoreState.SetHistogramType(action.Type)); + } } } diff --git a/BTCPayApp.UI/Pages/DashboardPage.razor b/BTCPayApp.UI/Pages/DashboardPage.razor index cb086d7..395786d 100644 --- a/BTCPayApp.UI/Pages/DashboardPage.razor +++ b/BTCPayApp.UI/Pages/DashboardPage.razor @@ -31,6 +31,22 @@ +
+ + + + + + + + + + + + + + +
@if (TotalBalance is > 0) {
@@ -104,6 +120,12 @@ : (OnchainConfirmedBalance ?? 0) + (OnchainUnconfirmedBalance ?? 0) + (LightningOnchainBalance ?? 0) + (LightningOffchainBalance ?? 0); private decimal? Rate => StoreState.Value.Rates?.Data?.FirstOrDefault()?.Rate; private string BitcoinUnit => UIState.Value.BitcoinUnit; + private HistogramType HistogramPeriod + { + get => UIState.Value.HistogramType; + set => Dispatcher.Dispatch(new UIState.SetHistogramType(value)); + } + private HistogramData? Histogram => StoreState.Value.UnifiedHistogram; private MoneyUnit UnitMoney => BitcoinUnit == CurrencyUnit.BTC ? MoneyUnit.BTC : MoneyUnit.Satoshi; private LightMoneyUnit UnitLightMoney => BitcoinUnit == CurrencyUnit.BTC ? LightMoneyUnit.BTC : LightMoneyUnit.Satoshi; diff --git a/BTCPayApp.UI/Pages/DashboardPage.razor.css b/BTCPayApp.UI/Pages/DashboardPage.razor.css index 2329895..30edab7 100644 --- a/BTCPayApp.UI/Pages/DashboardPage.razor.css +++ b/BTCPayApp.UI/Pages/DashboardPage.razor.css @@ -1,4 +1,12 @@ ::deep #WalletOverview { margin-top: var(--btcpay-space-m); - margin-bottom: var(--btcpay-space-l); + margin-bottom: var(--btcpay-space-s); +} + +.btn-group label { + --btcpay-btn-color: var(--btcpay-body-text-muted); + --btcpay-btn-active-color: var(--btcpay-body-text); + --btcpay-btn-padding-x: var(--btcpay-space-s); + font-weight: var(--btcpay-font-weight-semibold); + max-width: 4rem; } diff --git a/BTCPayApp.UI/wwwroot/chartist/chartist-plugin-tooltip.css b/BTCPayApp.UI/wwwroot/chartist/chartist-plugin-tooltip.css index a1c9238..e88f13d 100644 --- a/BTCPayApp.UI/wwwroot/chartist/chartist-plugin-tooltip.css +++ b/BTCPayApp.UI/wwwroot/chartist/chartist-plugin-tooltip.css @@ -11,11 +11,14 @@ gap: 4px; } .chartist-tooltip .chartist-tooltip-value { - color: var(--btcpay-body-text-muted); background-color: var(--btcpay-bg-tile); border-radius: var(--btcpay-border-radius); - padding: 0 var(--btcpay-space-s); + padding: var(--btcpay-space-xs) var(--btcpay-space-s); font-size: var(--btcpay-font-size-s); + text-align: center; +} +.chartist-tooltip .chartist-tooltip-value-date { + color: var(--btcpay-body-text-muted); } .chartist-tooltip .chartist-tooltip-line { width: 1px; @@ -41,7 +44,7 @@ opacity: 1; } -/* Tooltip arrow +/* Tooltip arrow .chartist-tooltip::before { content: '\25BC'; position: absolute; diff --git a/BTCPayApp.UI/wwwroot/chartist/chartist-plugin-tooltip.js b/BTCPayApp.UI/wwwroot/chartist/chartist-plugin-tooltip.js index 5431fd4..0ca5bdd 100644 --- a/BTCPayApp.UI/wwwroot/chartist/chartist-plugin-tooltip.js +++ b/BTCPayApp.UI/wwwroot/chartist/chartist-plugin-tooltip.js @@ -317,14 +317,16 @@ */ function setTooltipPosition(relativeElement, ignoreClasses) { containerRect = chart.container.getBoundingClientRect(); - var positionData = getTooltipPosition(relativeElement); - + var isLine = tooltipElement.innerHTML.match('chartist-tooltip-line'); + var positionData = getTooltipPosition(relativeElement, isLine); tooltipElement.style.transform = 'translate(' + positionData.left + 'px, ' + positionData.top + 'px)'; - tooltipElement.style.height = containerRect.height + options.offset.y + 'px'; + if (isLine) { + var tooltipRect = tooltipElement.querySelector('.chartist-tooltip-value').getBoundingClientRect(); + tooltipElement.style.height = containerRect.height + options.offset.y + options.offset.lineY + tooltipRect.height + 'px'; + } - if (ignoreClasses) { + if (ignoreClasses) return; - } tooltipElement.classList.remove(options.cssClass + '--right'); tooltipElement.classList.remove(options.cssClass + '--left'); @@ -336,7 +338,7 @@ * @param Element relativeElement * @return Object positionData */ - function getTooltipPosition(relativeElement) { + function getTooltipPosition(relativeElement, isLine) { var positionData = { alignment: 'center', }; @@ -345,7 +347,9 @@ var boxData = relativeElement.getBoundingClientRect(); var left = boxData.left + window.scrollX + options.offset.x - width / 2 + boxData.width / 2; - var top = containerRect.top + window.scrollY + options.offset.y; + var top = isLine + ? containerRect.top + window.scrollY + options.offset.y + : boxData.top + window.scrollY - height + options.offset.y; // Minimum horizontal collision detection if (left + width > document.body.clientWidth) { diff --git a/BTCPayApp.UI/wwwroot/js/global.js b/BTCPayApp.UI/wwwroot/js/global.js index afcdc2d..11ec0ea 100644 --- a/BTCPayApp.UI/wwwroot/js/global.js +++ b/BTCPayApp.UI/wwwroot/js/global.js @@ -85,7 +85,7 @@ Interop = { } Chart = { - renderLineChart (selector, labels, series, seriesUnit, displayUnit, rate, defaultCurrency, divisibility) { + renderLineChart (selector, labels, series, type, seriesUnit, displayUnit, rate, defaultCurrency, divisibility) { const $el = document.querySelector(selector); if (!$el) return; const valueTransform = (value, fromUnit, toUnit) =>{ @@ -94,13 +94,15 @@ Chart = { if (fromUnit === 'SATS' && toUnit === 'BTC') return value / 100000000; else return value; } + const labelCount = 6 + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat + const dateFormatter = new Intl.DateTimeFormat('default', type.toLowerCase() === 'day' ? { hour: 'numeric', minute: 'numeric' } : { month: 'short', day: 'numeric' }) + const dateFormatterDetails = new Intl.DateTimeFormat('default', { month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' }) Chart.lineChartTooltipValueTransform = (value, label) => { + const date = dateFormatterDetails.format(new Date(label)) const val = valueTransform(value, seriesUnit, displayUnit) - return displayCurrency(val, rate, displayUnit, divisibility) + ' ' + (displayUnit === 'SATS' ? 'sats' : displayUnit) + return `
${displayCurrency(val, rate, displayUnit, divisibility) + ' ' + (displayUnit === 'SATS' ? 'sats' : displayUnit)}
${date}
` } - const labelCount = 6 - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat - const dateFormatter = new Intl.DateTimeFormat('default', { month: 'short', day: 'numeric' }) const min = Math.min(...series); const max = Math.max(...series); const low = Math.max(min - ((max - min) / 5), 0); @@ -127,7 +129,8 @@ Chart = { template: '
{{value}}
', offset: { x: 0, - y: -16 + y: -32, + lineY: -17.5 }, valueTransformFunction(value, label) { return Chart.lineChartTooltipValueTransform(value, label)