diff --git a/BTCPayServer/Components/AppSales/AppSales.cs b/BTCPayServer/Components/AppSales/AppSales.cs index 86511e5ac..890308c02 100644 --- a/BTCPayServer/Components/AppSales/AppSales.cs +++ b/BTCPayServer/Components/AppSales/AppSales.cs @@ -1,4 +1,5 @@ using System; +using System.Security.AccessControl; using System.Threading.Tasks; using BTCPayServer.Data; using BTCPayServer.Models.AppViewModels; @@ -24,15 +25,20 @@ public class AppSales : ViewComponent _appService = appService; } - public async Task InvokeAsync(AppSalesViewModel vm) + public async Task InvokeAsync(string appId, string appType) { - if (vm.App == null) - throw new ArgumentNullException(nameof(vm.App)); + var vm = new AppSalesViewModel() + { + Id = appId, + AppType = appType, + Url = Url.Action("AppSales", "UIApps", new { appId = appId }), + InitialRendering = HttpContext.GetAppData()?.Id != appId + }; if (vm.InitialRendering) return View(vm); - - var stats = await _appService.GetSalesStats(vm.App); - + var app = HttpContext.GetAppData(); + vm.AppType = app.AppType; + var stats = await _appService.GetSalesStats(HttpContext.GetAppData()); vm.SalesCount = stats.SalesCount; vm.Series = stats.Series; diff --git a/BTCPayServer/Components/AppSales/AppSalesViewModel.cs b/BTCPayServer/Components/AppSales/AppSalesViewModel.cs index 36902ebfa..c0490383e 100644 --- a/BTCPayServer/Components/AppSales/AppSalesViewModel.cs +++ b/BTCPayServer/Components/AppSales/AppSalesViewModel.cs @@ -6,9 +6,12 @@ namespace BTCPayServer.Components.AppSales; public class AppSalesViewModel { - public AppData App { get; set; } - public AppSalesPeriod Period { get; set; } = AppSalesPeriod.Week; - public int SalesCount { get; set; } + public string Id { get; set; } + public string Name { get; set; } + public string AppType { get; set; } + public AppSalesPeriod Period { get; set; } + public string Url { get; set; } + public long SalesCount { get; set; } public IEnumerable Series { get; set; } public bool InitialRendering { get; set; } } diff --git a/BTCPayServer/Components/AppSales/Default.cshtml b/BTCPayServer/Components/AppSales/Default.cshtml index 7508dcce5..bee4f8fd3 100644 --- a/BTCPayServer/Components/AppSales/Default.cshtml +++ b/BTCPayServer/Components/AppSales/Default.cshtml @@ -1,17 +1,17 @@ @using BTCPayServer.Services.Apps @using BTCPayServer.Components.AppSales +@inject BTCPayServer.Security.ContentSecurityPolicies Csp @model BTCPayServer.Components.AppSales.AppSalesViewModel - @{ - var controller = $"UI{Model.App.AppType}"; - var action = $"Update{Model.App.AppType}"; - var label = Model.App.AppType == nameof(AppType.Crowdfund) ? "Contributions" : "Sales"; + var controller = $"UI{Model.AppType}"; + var action = $"Update{Model.AppType}"; + var label = Model.AppType == "Crowdfund" ? "Contributions" : "Sales"; } -
+
-

@Model.App.Name @label

- Manage +

@Model.Name @label

+ Manage
@if (Model.InitialRendering) { @@ -20,15 +20,16 @@ Loading...
+ @@ -40,54 +41,15 @@ @Model.SalesCount Total @label
- - - - + + + +
- + } diff --git a/BTCPayServer/Components/AppSales/Default.cshtml.js b/BTCPayServer/Components/AppSales/Default.cshtml.js new file mode 100644 index 000000000..49ebd2716 --- /dev/null +++ b/BTCPayServer/Components/AppSales/Default.cshtml.js @@ -0,0 +1,45 @@ +if (!window.appSales) { + window.appSales = + { + dataLoaded: function (model) { + const id = "AppSales-" + model.id; + const appId = model.id; + const period = model.period; + const baseUrl = model.url; + const data = model; + + const render = (data, period) => { + const series = data.series.map(s => s.salesCount); + const labels = data.series.map((s, i) => period === model.period ? s.label : (i % 5 === 0 ? s.label : '')); + const min = Math.min(...series); + const max = Math.max(...series); + const low = min === max ? 0 : Math.max(min - ((max - min) / 5), 0); + + document.querySelectorAll(`#${id} .sales-count`).innerText = data.salesCount; + + new Chartist.Bar(`#${id} .ct-chart`, { + labels, + series: [series] + }, { + low, + }); + }; + + render(data, period); + + const update = async period => { + const url = `${baseUrl}/${period}`; + const response = await fetch(url); + if (response.ok) { + const data = await response.json(); + render(data, period); + } + }; + + delegate('change', `#${id} [name="AppSalesPeriod-${appId}"]`, async e => { + const type = e.target.value; + await update(type); + }); + } + }; +} diff --git a/BTCPayServer/Components/AppTopItems/AppTopItems.cs b/BTCPayServer/Components/AppTopItems/AppTopItems.cs index 6cb71fdf3..e985caaa8 100644 --- a/BTCPayServer/Components/AppTopItems/AppTopItems.cs +++ b/BTCPayServer/Components/AppTopItems/AppTopItems.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Components.AppSales; using BTCPayServer.Data; using BTCPayServer.Services.Apps; using BTCPayServer.Services.Stores; @@ -18,17 +19,25 @@ public class AppTopItems : ViewComponent _appService = appService; } - public async Task InvokeAsync(AppTopItemsViewModel vm) + public async Task InvokeAsync(string appId, string appType = null) { - if (vm.App == null) - throw new ArgumentNullException(nameof(vm.App)); + var vm = new AppTopItemsViewModel() + { + Id = appId, + AppType = appType, + Url = Url.Action("AppTopItems", "UIApps", new { appId = appId }), + InitialRendering = HttpContext.GetAppData()?.Id != appId + }; if (vm.InitialRendering) return View(vm); - var entries = Enum.Parse(vm.App.AppType) == AppType.Crowdfund - ? await _appService.GetPerkStats(vm.App) - : await _appService.GetItemStats(vm.App); + var app = HttpContext.GetAppData(); + vm.AppType = app.AppType; + var entries = Enum.Parse(vm.AppType) == AppType.Crowdfund + ? await _appService.GetPerkStats(app) + : await _appService.GetItemStats(app); + vm.SalesCount = entries.Select(e => e.SalesCount).ToList(); vm.Entries = entries.ToList(); return View(vm); diff --git a/BTCPayServer/Components/AppTopItems/AppTopItemsViewModel.cs b/BTCPayServer/Components/AppTopItems/AppTopItemsViewModel.cs index 60bacb425..18620ede1 100644 --- a/BTCPayServer/Components/AppTopItems/AppTopItemsViewModel.cs +++ b/BTCPayServer/Components/AppTopItems/AppTopItemsViewModel.cs @@ -6,7 +6,11 @@ namespace BTCPayServer.Components.AppTopItems; public class AppTopItemsViewModel { - public AppData App { get; set; } + public string Id { get; set; } + public string Name { get; set; } + public string AppType { get; set; } + public string Url { get; set; } public List Entries { get; set; } + public List SalesCount { get; set; } public bool InitialRendering { get; set; } } diff --git a/BTCPayServer/Components/AppTopItems/Default.cshtml b/BTCPayServer/Components/AppTopItems/Default.cshtml index 3b97d13dd..b8ad1707d 100644 --- a/BTCPayServer/Components/AppTopItems/Default.cshtml +++ b/BTCPayServer/Components/AppTopItems/Default.cshtml @@ -1,76 +1,65 @@ @using BTCPayServer.Services.Apps +@inject BTCPayServer.Security.ContentSecurityPolicies Csp @model BTCPayServer.Components.AppTopItems.AppTopItemsViewModel - @{ - var controller = $"UI{Model.App.AppType}"; - var action = $"Update{Model.App.AppType}"; - var label = Model.App.AppType == nameof(AppType.Crowdfund) ? "contribution" : "sale"; + var controller = $"UI{Model.AppType}"; + var action = $"Update{Model.AppType}"; + var label = Model.AppType == nameof(AppType.Crowdfund) ? "contribution" : "sale"; } -
-
-

Top @(Model.App.AppType == nameof(AppType.Crowdfund) ? "Perks" : "Items")

- View All -
- @if (Model.InitialRendering) - { -
-
- Loading... -
-
- - } - else if (Model.Entries.Any()) - { -
- -
- @for (var i = 0; i < Model.Entries.Count; i++) - { - var entry = Model.Entries[i]; -
- - - @entry.Title - - - @entry.SalesCount @($"{label}{(entry.SalesCount == 1 ? "" : "s")}"), - @entry.TotalFormatted - -
- } -
- } - else - { -

- No @($"{label}s") have been made yet. -

- } +
+
+

Top @(Model.AppType == nameof(AppType.Crowdfund) ? "Perks" : "Items")

+ View All +
+ @if (Model.InitialRendering) + { +
+
+ Loading... +
+
+ + + } + else if (Model.Entries.Any()) + { +
+ +
+ @for (var i = 0; i < Model.Entries.Count; i++) + { + var entry = Model.Entries[i]; +
+ + + @entry.Title + + + @entry.SalesCount @($"{label}{(entry.SalesCount == 1 ? "" : "s")}"), + @entry.TotalFormatted + +
+ } +
+ } + else + { +

+ No @($"{label}s") have been made yet. +

+ }
diff --git a/BTCPayServer/Components/AppTopItems/Default.cshtml.js b/BTCPayServer/Components/AppTopItems/Default.cshtml.js new file mode 100644 index 000000000..8eb3adf99 --- /dev/null +++ b/BTCPayServer/Components/AppTopItems/Default.cshtml.js @@ -0,0 +1,18 @@ +if (!window.appTopItems) { + window.appTopItems = + { + dataLoaded: function (model) { + const id = "AppTopItems-" + model.id; + const series = model.salesCount; + new Chartist.Bar(`#${id} .ct-chart`, { series }, { + distributeSeries: true, + horizontalBars: true, + showLabel: false, + stackBars: true, + axisY: { + offset: 0 + } + }); + } + }; +} diff --git a/BTCPayServer/Controllers/UIAppsController.Dashboard.cs b/BTCPayServer/Controllers/UIAppsController.Dashboard.cs index 15ddf66b2..b4a7d5299 100644 --- a/BTCPayServer/Controllers/UIAppsController.Dashboard.cs +++ b/BTCPayServer/Controllers/UIAppsController.Dashboard.cs @@ -21,8 +21,7 @@ namespace BTCPayServer.Controllers app.StoreData = GetCurrentStore(); - var vm = new AppTopItemsViewModel { App = app }; - return ViewComponent("AppTopItems", new { vm }); + return ViewComponent("AppTopItems", new { appId = app.Id }); } [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] @@ -34,9 +33,7 @@ namespace BTCPayServer.Controllers return NotFound(); app.StoreData = GetCurrentStore(); - - var vm = new AppSalesViewModel { App = app }; - return ViewComponent("AppSales", new { vm }); + return ViewComponent("AppSales", new { appId = app.Id }); } [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] diff --git a/BTCPayServer/Hosting/Startup.cs b/BTCPayServer/Hosting/Startup.cs index a0796ec13..56d8cd536 100644 --- a/BTCPayServer/Hosting/Startup.cs +++ b/BTCPayServer/Hosting/Startup.cs @@ -29,6 +29,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -274,6 +275,19 @@ namespace BTCPayServer.Hosting } }); + // The framework during publish automatically publish the js files into + // wwwroot, so this shouldn't be needed. + // But somehow during debug the collocated js files, are error 404! + var componentsFolder = Path.Combine(env.ContentRootPath, "Components"); + if (Directory.Exists(componentsFolder)) + { + app.UseStaticFiles(new StaticFileOptions() + { + FileProvider = new PhysicalFileProvider(componentsFolder), + RequestPath = "/Components" + }); + } + app.UseProviderStorage(dataDirectories); app.UseAuthentication(); app.UseAuthorization(); @@ -296,5 +310,16 @@ namespace BTCPayServer.Hosting }); app.UsePlugins(); } + + private static Action NewMethod() + { + return 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; + }; + } } } diff --git a/BTCPayServer/Views/UIStores/Dashboard.cshtml b/BTCPayServer/Views/UIStores/Dashboard.cshtml index 3d9c9226c..cf22aca16 100644 --- a/BTCPayServer/Views/UIStores/Dashboard.cshtml +++ b/BTCPayServer/Views/UIStores/Dashboard.cshtml @@ -118,8 +118,8 @@ @foreach (var app in Model.Apps) { - - + + }
}