Redesign plugin list items (#4528)

* Redesign plugin list items

* Update icon and format code

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
This commit is contained in:
Nicolas Dorier
2023-01-16 20:12:51 +09:00
committed by GitHub
parent ee70fe85c0
commit 785cf597ad
4 changed files with 188 additions and 55 deletions

View File

@@ -9,6 +9,7 @@ using BTCPayServer.Configuration;
using BTCPayServer.Plugins; using BTCPayServer.Plugins;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using static BTCPayServer.Plugins.PluginService;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
@@ -33,9 +34,9 @@ namespace BTCPayServer.Controllers
}); });
availablePlugins = Array.Empty<PluginService.AvailablePlugin>(); availablePlugins = Array.Empty<PluginService.AvailablePlugin>();
} }
var docsByIdentifier = new Dictionary<string, string>(); var availablePluginsByIdentifier = new Dictionary<string, AvailablePlugin>();
foreach (var p in availablePlugins.Where(p => !string.IsNullOrEmpty(p.Documentation))) foreach (var p in availablePlugins)
docsByIdentifier.TryAdd(p.Identifier, p.Documentation); availablePluginsByIdentifier.TryAdd(p.Identifier, p);
var res = new ListPluginsViewModel() var res = new ListPluginsViewModel()
{ {
Installed = pluginService.LoadedPlugins, Installed = pluginService.LoadedPlugins,
@@ -43,7 +44,7 @@ namespace BTCPayServer.Controllers
Commands = pluginService.GetPendingCommands(), Commands = pluginService.GetPendingCommands(),
Disabled = pluginService.GetDisabledPlugins(), Disabled = pluginService.GetDisabledPlugins(),
CanShowRestart = btcPayServerOptions.DockerDeployment, CanShowRestart = btcPayServerOptions.DockerDeployment,
DocsByIdentifier = docsByIdentifier DownloadedPluginsByIdentifier = availablePluginsByIdentifier
}; };
return View(res); return View(res);
} }
@@ -55,7 +56,7 @@ namespace BTCPayServer.Controllers
public (string command, string plugin)[] Commands { get; set; } public (string command, string plugin)[] Commands { get; set; }
public bool CanShowRestart { get; set; } public bool CanShowRestart { get; set; }
public string[] Disabled { get; set; } public string[] Disabled { get; set; }
public Dictionary<string, string> DocsByIdentifier { get; set; } = new Dictionary<string, string>(); public Dictionary<string, AvailablePlugin> DownloadedPluginsByIdentifier { get; set; } = new Dictionary<string, AvailablePlugin>();
} }
[HttpPost("server/plugins/uninstall")] [HttpPost("server/plugins/uninstall")]

View File

@@ -1,16 +1,49 @@
using System; using System;
using System.Collections.Generic;
using System.Net.Http; using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using ExchangeSharp;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using static System.Net.WebRequestMethods;
namespace BTCPayServer.Plugins namespace BTCPayServer.Plugins
{ {
public class PublishedVersion public class PublishedVersion
{ {
public class BuildInfoClass
{
public string gitCommit { get; set; }
public string pluginDir { get; set; }
public string gitRepository { get; set; }
#nullable enable
static Regex GithubRepositoryRegex = new Regex("^https://(www\\.)?github\\.com/([^/]+)/([^/]+)/?");
public record GithubRepository(string Owner, string RepositoryName)
{
public string? GetSourceUrl(string commit, string pluginDir)
{
if (commit is null)
return null;
return $"https://github.com/{Owner}/{RepositoryName}/tree/{commit}/{pluginDir}";
}
}
public GithubRepository? GetGithubRepository()
{
if (gitRepository is null)
return null;
var match = GithubRepositoryRegex.Match(gitRepository);
if (!match.Success)
return null;
return new GithubRepository(match.Groups[2].Value, match.Groups[3].Value);
}
#nullable restore
[JsonExtensionData]
public IDictionary<string, JToken> AdditionalData { get; set; }
}
public string ProjectSlug { get; set; } public string ProjectSlug { get; set; }
public long BuildId { get; set; } public long BuildId { get; set; }
public JObject BuildInfo { get; set; } public BuildInfoClass BuildInfo { get; set; }
public JObject ManifestInfo { get; set; } public JObject ManifestInfo { get; set; }
public string Documentation { get; set; } public string Documentation { get; set; }
} }

View File

@@ -5,14 +5,17 @@ using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Configuration; using BTCPayServer.Configuration;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Services; using BTCPayServer.Services;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileSystemGlobbing;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using NBitcoin.DataEncoders; using NBitcoin.DataEncoders;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -52,6 +55,13 @@ namespace BTCPayServer.Plugins
{ {
var p = v.ManifestInfo.ToObject<AvailablePlugin>(); var p = v.ManifestInfo.ToObject<AvailablePlugin>();
p.Documentation = v.Documentation; p.Documentation = v.Documentation;
var github = v.BuildInfo.GetGithubRepository();
if (github != null)
{
p.Source = github.GetSourceUrl(v.BuildInfo.gitCommit, v.BuildInfo.pluginDir);
p.Author = github.Owner;
p.AuthorLink = $"https://github.com/{github.Owner}";
}
p.SystemPlugin = false; p.SystemPlugin = false;
return p; return p;
}).ToArray(); }).ToArray();
@@ -108,6 +118,9 @@ namespace BTCPayServer.Plugins
public IBTCPayServerPlugin.PluginDependency[] Dependencies { get; set; } = Array.Empty<IBTCPayServerPlugin.PluginDependency>(); public IBTCPayServerPlugin.PluginDependency[] Dependencies { get; set; } = Array.Empty<IBTCPayServerPlugin.PluginDependency>();
public string Documentation { get; set; } public string Documentation { get; set; }
public string Source { get; set; }
public string Author { get; set; }
public string AuthorLink { get; set; }
public void Execute(IApplicationBuilder applicationBuilder, public void Execute(IApplicationBuilder applicationBuilder,
IServiceProvider applicationBuilderApplicationServices) IServiceProvider applicationBuilderApplicationServices)

View File

@@ -1,7 +1,7 @@
@using BTCPayServer.Configuration @using BTCPayServer.Configuration
@using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Plugins @using BTCPayServer.Plugins
@model BTCPayServer.Controllers.UIServerController.ListPluginsViewModel @using BTCPayServer.Abstractions.Contracts
@model BTCPayServer.Controllers.UIServerController.ListPluginsViewModel
@inject BTCPayServerOptions BTCPayServerOptions @inject BTCPayServerOptions BTCPayServerOptions
@{ @{
Layout = "_Layout"; Layout = "_Layout";
@@ -19,7 +19,7 @@
var ordered = availableAndNotInstalledItem.OrderByDescending(plugin => plugin.Version).ToArray(); var ordered = availableAndNotInstalledItem.OrderByDescending(plugin => plugin.Version).ToArray();
availableAndNotInstalled.Add(ordered.FirstOrDefault(availablePlugin => DependenciesMet(availablePlugin.Dependencies)) ?? ordered.FirstOrDefault()); availableAndNotInstalled.Add(ordered.FirstOrDefault(availablePlugin => DependenciesMet(availablePlugin.Dependencies)) ?? ordered.FirstOrDefault());
} }
bool DependentOn(string plugin) bool DependentOn(string plugin)
{ {
foreach (var installedPlugin in Model.Installed) foreach (var installedPlugin in Model.Installed)
@@ -102,8 +102,13 @@
} }
<style> <style>
.version-switch .nav-link { display: inline; } .version-switch .nav-link {
.version-switch .nav-link.active { display: none; } display: inline;
}
.version-switch .nav-link.active {
display: none;
}
</style> </style>
<partial name="_StatusMessage" /> <partial name="_StatusMessage" />
@@ -112,7 +117,7 @@
{ {
<div class="alert alert-danger mb-4 d-flex align-items-center justify-content-between"> <div class="alert alert-danger mb-4 d-flex align-items-center justify-content-between">
Some plugins were disabled due to fatal errors. They may be incompatible with this version of BTCPay Server. Some plugins were disabled due to fatal errors. They may be incompatible with this version of BTCPay Server.
<button class="btn btn-danger" data-bs-toggle="collapse" data-bs-target="#disabled-plugins">View disabled plugins</button> <button class="btn btn-danger" data-bs-toggle="collapse" data-bs-target="#disabled-plugins">View disabled plugins</button>
</div> </div>
} }
@if (Model.Commands.Any()) @if (Model.Commands.Any())
@@ -134,6 +139,7 @@
<div class="row mb-4"> <div class="row mb-4">
@foreach (var plugin in Model.Installed.Where(i => !i.SystemPlugin)) @foreach (var plugin in Model.Installed.Where(i => !i.SystemPlugin))
{ {
Model.DownloadedPluginsByIdentifier.TryGetValue(plugin.Identifier, out var downloadInfo);
var matchedAvailable = Model.Available.Where(availablePlugin => availablePlugin.Identifier == plugin.Identifier && availablePlugin.Version > plugin.Version).OrderByDescending(availablePlugin => availablePlugin.Version).ToArray(); var matchedAvailable = Model.Available.Where(availablePlugin => availablePlugin.Identifier == plugin.Identifier && availablePlugin.Version > plugin.Version).OrderByDescending(availablePlugin => availablePlugin.Version).ToArray();
var x = matchedAvailable.FirstOrDefault(availablePlugin => DependenciesMet(availablePlugin.Dependencies)) ?? matchedAvailable.FirstOrDefault(); var x = matchedAvailable.FirstOrDefault(availablePlugin => DependenciesMet(availablePlugin.Dependencies)) ?? matchedAvailable.FirstOrDefault();
var updateAvailable = matchedAvailable.Any(); var updateAvailable = matchedAvailable.Any();
@@ -141,13 +147,18 @@
<div class="col col-12 col-md-6 col-lg-12 col-xl-6 col-xxl-4 mb-4"> <div class="col col-12 col-md-6 col-lg-12 col-xl-6 col-xxl-4 mb-4">
<div class="card h-100" id="@plugin.Identifier"> <div class="card h-100" id="@plugin.Identifier">
<div class="card-body"> <div class="card-body">
<div class="d-flex align-items-baseline justify-content-between gap-2"> <div class="d-flex align-items-baseline justify-content-between gap-2">
<h4 class="card-title" title="@plugin.Identifier" data-bs-toggle="tooltip">@plugin.Name</h4> <h4 class="card-title" data-bs-toggle="tooltip" title="@plugin.Identifier">@plugin.Name</h4>
@if (Model.DocsByIdentifier.ContainsKey(plugin.Identifier)) @if (!string.IsNullOrEmpty(downloadInfo.Author))
{ {
<a href="@Model.DocsByIdentifier[plugin.Identifier]" rel="noreferrer noopener" target="_blank">Documentation</a> <span class="text-muted">
} by
</div> <a href="@downloadInfo.AuthorLink" rel="noreferrer noopener" target="_blank">
<span>@downloadInfo.Author</span>
</a>
</span>
}
</div>
<div class="d-flex flex-wrap align-items-center mb-2"> <div class="d-flex flex-wrap align-items-center mb-2">
<h5 class="text-muted d-flex align-items-center mt-1 gap-3"> <h5 class="text-muted d-flex align-items-center mt-1 gap-3">
@plugin.Version @plugin.Version
@@ -210,6 +221,40 @@
} }
</div> </div>
} }
@if (plugin != null)
{
<h5 class="text-muted mt-4">Resources</h5>
<ul class="list-group list-group-flush list-unstyled">
@if (downloadInfo.Source is not null)
{
<li>
<a href="@downloadInfo.Source" rel="noreferrer noopener" class="d-flex align-items-center" target="_blank">
<vc:icon symbol="github" />
<span style="margin-left:.4rem">Sources</span>
</a>
</li>
}
@if (!string.IsNullOrEmpty(downloadInfo.Documentation))
{
<li>
<a href="@downloadInfo.Documentation" rel="noreferrer noopener" class="d-flex align-items-center gap-2" target="_blank">
<vc:icon symbol="docs" />
<span>Documentation</span>
</a>
</li>
}
else
{
<li>
<span rel="noreferrer noopener" class="d-flex align-items-center gap-2 text-danger" target="_blank">
<vc:icon symbol="docs" />
<span>No documentation</span>
</span>
</li>
}
</ul>
}
</div> </div>
</div> </div>
@{ @{
@@ -259,20 +304,25 @@
{ {
var recommended = BTCPayServerOptions.RecommendedPlugins.Contains(plugin.Identifier.ToLowerInvariant()); var recommended = BTCPayServerOptions.RecommendedPlugins.Contains(plugin.Identifier.ToLowerInvariant());
var disabled = Model.Disabled?.Contains(plugin.Identifier) ?? false; var disabled = Model.Disabled?.Contains(plugin.Identifier) ?? false;
<div class="col col-12 col-md-6 col-lg-12 col-xl-6 col-xxl-4 mb-4"> <div class="col col-12 col-md-6 col-lg-12 col-xl-6 col-xxl-4 mb-4">
<div class="card h-100" id="@plugin.Identifier"> <div class="card h-100" id="@plugin.Identifier">
<div class="card-body"> <div class="card-body">
<div class="d-flex align-items-baseline justify-content-between gap-2"> <div class="d-flex align-items-baseline justify-content-between gap-2">
<h4 class="card-title" data-bs-toggle="tooltip" title="@plugin.Identifier">@plugin.Name</h4> <h4 class="card-title" data-bs-toggle="tooltip" title="@plugin.Identifier">@plugin.Name</h4>
@if (!string.IsNullOrEmpty(plugin.Documentation)) @if (!string.IsNullOrEmpty(plugin.Author))
{ {
<a href="@plugin.Documentation" rel="noreferrer noopener" target="_blank">Documentation</a> <span class="text-muted">
} by
</div> <a href="@plugin.AuthorLink" rel="noreferrer noopener" target="_blank">
<span>@plugin.Author</span>
</a>
</span>
}
</div>
<h5 class="text-muted d-flex align-items-center mt-1 gap-2"> <h5 class="text-muted d-flex align-items-center mt-1 gap-2">
@plugin.Version @plugin.Version
@if (disabled) @if (disabled)
{ {
<div class="badge bg-light">Disabled</div> <div class="badge bg-light">Disabled</div>
} }
@@ -298,9 +348,45 @@
} }
</ul> </ul>
} }
@if (plugin != null)
{
<h5 class="text-muted mt-4">Resources</h5>
<ul class="list-group list-group-flush list-unstyled">
@if (plugin.Source is not null)
{
<li>
<a href="@plugin.Source" rel="noreferrer noopener" class="d-flex align-items-center" target="_blank">
<vc:icon symbol="github" />
<span style="margin-left:.4rem">Sources</span>
</a>
</li>
}
@if (!string.IsNullOrEmpty(plugin.Documentation))
{
<li>
<a href="@plugin.Documentation" rel="noreferrer noopener" class="d-flex align-items-center gap-2" target="_blank">
<vc:icon symbol="docs" />
<span>Documentation</span>
</a>
</li>
}
else
{
<li>
<span rel="noreferrer noopener" class="d-flex align-items-center gap-2 text-danger" target="_blank">
<vc:icon symbol="docs" />
<span>No documentation</span>
</span>
</li>
}
</ul>
}
</div> </div>
<div class="card-footer border-0 pb-3"> <div class="card-footer border-0 pb-3">
@{ var pending = Model.Commands.LastOrDefault(tuple => tuple.plugin.Equals(plugin.Identifier, StringComparison.InvariantCultureIgnoreCase)); } @{
var pending = Model.Commands.LastOrDefault(tuple => tuple.plugin.Equals(plugin.Identifier, StringComparison.InvariantCultureIgnoreCase));
}
@if (!pending.Equals(default)) @if (!pending.Equals(default))
{ {
<form asp-action="CancelPluginCommands" asp-route-plugin="@plugin.Identifier"> <form asp-action="CancelPluginCommands" asp-route-plugin="@plugin.Identifier">
@@ -309,7 +395,7 @@
} }
else if (DependenciesMet(plugin.Dependencies)) else if (DependenciesMet(plugin.Dependencies))
{ {
@* Don't show the "Install" button if plugin has been disabled *@ @* Don't show the "Install" button if plugin has been disabled *@
@if (!disabled) @if (!disabled)
{ {
<form asp-action="InstallPlugin" asp-route-plugin="@plugin.Identifier" asp-route-version="@plugin.Version"> <form asp-action="InstallPlugin" asp-route-plugin="@plugin.Identifier" asp-route-version="@plugin.Version">
@@ -323,7 +409,7 @@
Cannot install until dependencies are met. Cannot install until dependencies are met.
</div> </div>
} }
</div> </div>
</div> </div>
</div> </div>
} }
@@ -356,34 +442,34 @@
@if (Model.Commands.Any()) @if (Model.Commands.Any())
{ {
<div class="mb-4"> <div class="mb-4">
<h3 class="mb-4">Pending Action</h3> <h3 class="mb-4">Pending Action</h3>
<button class="btn btn-secondary mb-4" type="button" data-bs-toggle="collapse" data-bs-target="#pending-actions"> <button class="btn btn-secondary mb-4" type="button" data-bs-toggle="collapse" data-bs-target="#pending-actions">
Pending Actions Pending Actions
</button> </button>
<div class="row collapse" id="pending-actions"> <div class="row collapse" id="pending-actions">
<div class="col col-12 col-lg-6 mb-4"> <div class="col col-12 col-lg-6 mb-4">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<h4 class="card-title">Pending actions</h4> <h4 class="card-title">Pending actions</h4>
<ul class="list-group list-group-flush"> <ul class="list-group list-group-flush">
@foreach (var extComm in Model.Commands.GroupBy(tuple => tuple.plugin)) @foreach (var extComm in Model.Commands.GroupBy(tuple => tuple.plugin))
{ {
<li class="list-group-item p-2"> <li class="list-group-item p-2">
<div class="d-flex flex-wrap align-items-center justify-content-between"> <div class="d-flex flex-wrap align-items-center justify-content-between">
<span class="my-2 me-3">@extComm.Key</span> <span class="my-2 me-3">@extComm.Key</span>
<form asp-action="CancelPluginCommands" asp-route-plugin="@extComm.Key"> <form asp-action="CancelPluginCommands" asp-route-plugin="@extComm.Key">
<button type="submit" class="btn btn-outline-secondary">Cancel pending @extComm.Last().command</button> <button type="submit" class="btn btn-outline-secondary">Cancel pending @extComm.Last().command</button>
</form> </form>
</div> </div>
</li> </li>
} }
</ul> </ul>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
} }
@if (Model.Disabled.Any()) @if (Model.Disabled.Any())
{ {