mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2024-11-19 18:11:36 +01:00
511 lines
26 KiB
Plaintext
511 lines
26 KiB
Plaintext
@using BTCPayServer.Configuration
|
|
@using BTCPayServer.Plugins
|
|
@using BTCPayServer.Abstractions.Contracts
|
|
@model BTCPayServer.Controllers.UIServerController.ListPluginsViewModel
|
|
@inject BTCPayServerOptions BTCPayServerOptions
|
|
@{
|
|
Layout = "_Layout";
|
|
ViewData.SetActivePage(ServerNavPages.Plugins);
|
|
var installed = Model.Installed.ToDictionary(plugin => plugin.Identifier.ToLowerInvariant(), plugin => plugin.Version);
|
|
|
|
var availableAndNotInstalledx = Model.Available
|
|
.Where(plugin => !installed.ContainsKey(plugin.Identifier.ToLowerInvariant()))
|
|
.GroupBy(plugin => plugin.Identifier)
|
|
.ToList();
|
|
|
|
var availableAndNotInstalled = new List<PluginService.AvailablePlugin>();
|
|
foreach (var availableAndNotInstalledItem in availableAndNotInstalledx)
|
|
{
|
|
var ordered = availableAndNotInstalledItem.OrderByDescending(plugin => plugin.Version).ToArray();
|
|
availableAndNotInstalled.Add(ordered.FirstOrDefault(availablePlugin => DependenciesMet(availablePlugin.Dependencies)) ?? ordered.FirstOrDefault());
|
|
}
|
|
|
|
bool DependentOn(string plugin)
|
|
{
|
|
foreach (var installedPlugin in Model.Installed)
|
|
{
|
|
if (installedPlugin.Dependencies.Any(dep => dep.Identifier.Equals(plugin, StringComparison.InvariantCultureIgnoreCase)))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
var pendingInstalls = Model.Commands.Where(tuple => tuple.command != "uninstall").Select(tuple => tuple.plugin).Distinct();
|
|
foreach (var pendingInstall in pendingInstalls)
|
|
{
|
|
if (Model.Available.Any(availablePlugin => availablePlugin.Identifier.Equals(pendingInstall, StringComparison.InvariantCultureIgnoreCase) &&
|
|
availablePlugin.Dependencies.Any(dep => dep.Identifier.Equals(plugin, StringComparison.InvariantCultureIgnoreCase))))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool DependencyMet(IBTCPayServerPlugin.PluginDependency dependency)
|
|
{
|
|
var plugin = dependency.Identifier.ToLowerInvariant();
|
|
var versionReq = dependency.Condition;
|
|
if (!installed.ContainsKey(plugin) && !versionReq.Equals("!"))
|
|
{
|
|
return false;
|
|
}
|
|
if (installed.ContainsKey(plugin) && versionReq.Equals("!"))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var versionConditions = versionReq.Split("||", StringSplitOptions.RemoveEmptyEntries);
|
|
return versionConditions.Any(s =>
|
|
{
|
|
s = s.Trim();
|
|
var v = s.Substring(1);
|
|
if (s[1] == '=')
|
|
{
|
|
v = s.Substring(2);
|
|
}
|
|
var parsedV = Version.Parse(v);
|
|
switch (s)
|
|
{
|
|
case { } xx when xx.StartsWith(">="):
|
|
return installed[plugin] >= parsedV;
|
|
case { } xx when xx.StartsWith("<="):
|
|
return installed[plugin] <= parsedV;
|
|
case { } xx when xx.StartsWith(">"):
|
|
return installed[plugin] > parsedV;
|
|
case { } xx when xx.StartsWith("<"):
|
|
return installed[plugin] >= parsedV;
|
|
case { } xx when xx.StartsWith("^"):
|
|
return installed[plugin] >= parsedV && installed[plugin].Major == parsedV.Major;
|
|
case { } xx when xx.StartsWith("~"):
|
|
return installed[plugin] >= parsedV && installed[plugin].Major == parsedV.Major && installed[plugin].Minor == parsedV.Minor;
|
|
case { } xx when xx.StartsWith("!="):
|
|
return installed[plugin] != parsedV;
|
|
case { } xx when xx.StartsWith("=="):
|
|
default:
|
|
return installed[plugin] == parsedV;
|
|
}
|
|
});
|
|
}
|
|
|
|
bool DependenciesMet(IBTCPayServerPlugin.PluginDependency[] dependencies)
|
|
{
|
|
foreach (var dependency in dependencies)
|
|
{
|
|
if (!DependencyMet(dependency))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
<style>
|
|
.version-switch .nav-link {
|
|
display: inline;
|
|
}
|
|
|
|
.version-switch .nav-link.active {
|
|
display: none;
|
|
}
|
|
</style>
|
|
|
|
<partial name="_StatusMessage" />
|
|
|
|
@if (Model.Disabled.Any())
|
|
{
|
|
<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.
|
|
<button class="btn btn-danger" data-bs-toggle="collapse" data-bs-target="#disabled-plugins">View disabled plugins</button>
|
|
</div>
|
|
}
|
|
@if (Model.Commands.Any())
|
|
{
|
|
<div class="alert alert-info mb-4 d-flex align-items-center justify-content-between">
|
|
You need to restart BTCPay Server in order to update your active plugins.
|
|
@if (Model.CanShowRestart)
|
|
{
|
|
<form method="post" asp-action="Maintenance" class="mt-2">
|
|
<button type="submit" name="command" value="soft-restart" class="btn btn-info" asp-action="Maintenance">Restart now</button>
|
|
</form>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
@if (Model.Installed.Any())
|
|
{
|
|
<h3 class="mb-4">Installed Plugins</h3>
|
|
<div class="row mb-4">
|
|
@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 x = matchedAvailable.FirstOrDefault(availablePlugin => DependenciesMet(availablePlugin.Dependencies)) ?? matchedAvailable.FirstOrDefault();
|
|
var updateAvailable = matchedAvailable.Any();
|
|
var tabId = plugin.Identifier.ToLowerInvariant().Replace(".", "_");
|
|
<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-body">
|
|
<div class="d-flex flex-wrap align-items-baseline justify-content-between gap-2 mb-3">
|
|
<h4 class="card-title mb-0" data-bs-toggle="tooltip" title="@plugin.Identifier">@plugin.Name</h4>
|
|
@if (!string.IsNullOrEmpty(downloadInfo?.Author))
|
|
{
|
|
<span class="text-muted text-nowrap">
|
|
by
|
|
<a href="@downloadInfo.AuthorLink" rel="noreferrer noopener" target="_blank">
|
|
@downloadInfo.Author
|
|
</a>
|
|
</span>
|
|
}
|
|
</div>
|
|
<div class="d-flex flex-wrap align-items-center mb-2 gap-3">
|
|
<h5 class="text-muted d-flex align-items-center mt-1 gap-3">
|
|
@plugin.Version
|
|
@if (updateAvailable && x != null)
|
|
{
|
|
<div class="badge bg-info">
|
|
@x.Version available
|
|
</div>
|
|
}
|
|
</h5>
|
|
@if (updateAvailable)
|
|
{
|
|
<span class="nav version-switch mt-n1" role="tablist">
|
|
<a data-bs-toggle="tab" href="#@tabId-current" class="nav-link text-info p-0 show active">Show current info</a>
|
|
<a data-bs-toggle="tab" href="#@tabId-update" class="nav-link text-info p-0">Show update info</a>
|
|
</span>
|
|
}
|
|
</div>
|
|
|
|
<div class="tab-content">
|
|
<div class="tab-pane active" id="@tabId-current">
|
|
<p class="card-text">@plugin.Description</p>
|
|
@if (plugin.Dependencies.Any())
|
|
{
|
|
<h6 class="text-muted fw-semibold">Dependencies</h6>
|
|
<ul class="list-group list-group-flush">
|
|
@foreach (var dependency in plugin.Dependencies)
|
|
{
|
|
<li class="list-group-item p-2 d-inline-flex align-items-center gap-2">
|
|
@dependency
|
|
@if (!DependencyMet(dependency))
|
|
{
|
|
<span title="Dependency not met." data-bs-toggle="tooltip" class="text-danger">
|
|
<vc:icon symbol="warning" />
|
|
</span>
|
|
}
|
|
</li>
|
|
}
|
|
</ul>
|
|
}
|
|
</div>
|
|
|
|
@if (updateAvailable && x != null)
|
|
{
|
|
<div class="tab-pane" id="@tabId-update">
|
|
<p class="card-text">@x.Description</p>
|
|
@if (x.Dependencies.Any())
|
|
{
|
|
<h6 class="text-muted fw-semibold">Dependencies</h6>
|
|
<ul class="list-group list-group-flush">
|
|
@foreach (var dependency in x.Dependencies)
|
|
{
|
|
<li class="list-group-item p-2 d-inline-flex align-items-center gap-2">
|
|
@dependency
|
|
@if (!DependencyMet(dependency))
|
|
{
|
|
<span title="Dependency not met." data-bs-toggle="tooltip" class="text-danger">
|
|
<vc:icon symbol="warning" />
|
|
</span>
|
|
}
|
|
</li>
|
|
}
|
|
</ul>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
@if (plugin != null)
|
|
{
|
|
<h6 class="text-muted fw-semibold mt-4">Resources</h6>
|
|
<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>
|
|
@{
|
|
var pendingAction = Model.Commands.Any(tuple => tuple.plugin.Equals(plugin.Identifier, StringComparison.InvariantCultureIgnoreCase));
|
|
}
|
|
@if (pendingAction || (updateAvailable && x != null && DependenciesMet(x.Dependencies)) || !DependentOn(plugin.Identifier))
|
|
{
|
|
<div class="card-footer border-0 pb-3 d-flex">
|
|
@if (pendingAction)
|
|
{
|
|
<form asp-action="CancelPluginCommands" asp-route-plugin="@plugin.Identifier">
|
|
<button type="submit" class="btn btn-outline-secondary">Cancel pending action</button>
|
|
</form>
|
|
}
|
|
else
|
|
{
|
|
@if (updateAvailable && x != null && DependenciesMet(x.Dependencies))
|
|
{
|
|
<form asp-action="InstallPlugin" asp-route-plugin="@plugin.Identifier" asp-route-version="@x.Version" asp-route-update="true" class="me-3">
|
|
<button type="submit" class="btn btn-secondary">Update</button>
|
|
</form>
|
|
}
|
|
@if (DependentOn(plugin.Identifier))
|
|
{
|
|
<button type="button" class="btn btn-outline-danger" data-bs-toggle="tooltip" title="This plugin cannot be uninstalled as it is depended on by other plugins.">Uninstall <span class="fa fa-exclamation"></span></button>
|
|
}
|
|
else
|
|
{
|
|
<form asp-action="UnInstallPlugin" asp-route-plugin="@plugin.Identifier">
|
|
<button type="submit" class="btn btn-outline-danger">Uninstall</button>
|
|
</form>
|
|
}
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
@if (availableAndNotInstalled.Any())
|
|
{
|
|
<h3 class="mb-4">Available Plugins</h3>
|
|
<div class="row mb-4">
|
|
@foreach (var plugin in availableAndNotInstalled)
|
|
{
|
|
var recommended = BTCPayServerOptions.RecommendedPlugins.Contains(plugin.Identifier.ToLowerInvariant());
|
|
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="card h-100" id="@plugin.Identifier">
|
|
<div class="card-body">
|
|
<div class="d-flex flex-wrap align-items-baseline justify-content-between gap-2 mb-3">
|
|
<h4 class="card-title mb-0" data-bs-toggle="tooltip" title="@plugin.Identifier">@plugin.Name</h4>
|
|
@if (!string.IsNullOrEmpty(plugin.Author))
|
|
{
|
|
<span class="text-muted text-nowrap">
|
|
by
|
|
<a href="@plugin.AuthorLink" rel="noreferrer noopener" target="_blank">
|
|
@plugin.Author
|
|
</a>
|
|
</span>
|
|
}
|
|
</div>
|
|
<h5 class="text-muted d-flex align-items-center mt-1 gap-2">
|
|
@plugin.Version
|
|
@if (disabled)
|
|
{
|
|
<div class="badge bg-light">Disabled</div>
|
|
}
|
|
else if (recommended)
|
|
{
|
|
<div class="badge bg-light text-nowrap" data-bs-toggle="tooltip" title="This plugin has been recommended to be installed by your deployment method.">Recommended <vc:icon symbol="info" /></div>
|
|
}
|
|
</h5>
|
|
<p class="card-text">@plugin.Description</p>
|
|
@if (plugin.Dependencies?.Any() is true)
|
|
{
|
|
<h6 class="text-muted fw-semibold">Dependencies</h6>
|
|
<ul class="list-group list-group-flush">
|
|
@foreach (var dependency in plugin.Dependencies)
|
|
{
|
|
<li class="list-group-item p-2 d-inline-flex align-items-center gap-2">
|
|
@dependency
|
|
@if (!DependencyMet(dependency))
|
|
{
|
|
<span title="Dependency not met." data-bs-toggle="tooltip" class="text-danger">
|
|
<vc:icon symbol="warning" />
|
|
</span>
|
|
}
|
|
</li>
|
|
}
|
|
</ul>
|
|
}
|
|
|
|
@if (plugin != null)
|
|
{
|
|
<h6 class="text-muted fw-semibold mt-4">Resources</h6>
|
|
<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 class="card-footer border-0 pb-3">
|
|
@{
|
|
var pending = Model.Commands.LastOrDefault(tuple => tuple.plugin.Equals(plugin.Identifier, StringComparison.InvariantCultureIgnoreCase));
|
|
}
|
|
@if (!pending.Equals(default))
|
|
{
|
|
<form asp-action="CancelPluginCommands" asp-route-plugin="@plugin.Identifier">
|
|
<button type="submit" class="btn btn-outline-secondary">Cancel pending @pending.command</button>
|
|
</form>
|
|
}
|
|
else if (DependenciesMet(plugin.Dependencies))
|
|
{
|
|
@* Don't show the "Install" button if plugin has been disabled *@
|
|
@if (!disabled)
|
|
{
|
|
<form asp-action="InstallPlugin" asp-route-plugin="@plugin.Identifier" asp-route-version="@plugin.Version">
|
|
<button type="submit" class="btn btn-primary">Install</button>
|
|
</form>
|
|
}
|
|
}
|
|
else
|
|
{
|
|
<div class="text-danger">
|
|
Cannot install until dependencies are met.
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
<div class="mb-4">
|
|
<h3 class="mb-4">Upload Plugin</h3>
|
|
<button class="btn btn-secondary mb-4" type="button" data-bs-toggle="collapse" data-bs-target="#manual-upload">
|
|
Upload Plugin
|
|
</button>
|
|
<div class="row collapse" id="manual-upload">
|
|
<div class="col col-xl-6 mb-4">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<h4 class="card-title">Add plugin manually</h4>
|
|
<div class="alert alert-warning my-3">
|
|
<h6 class="me-1">This is an extremely dangerous operation!</h6>
|
|
Only upload plugins from trusted sources.
|
|
</div>
|
|
<form method="post" enctype="multipart/form-data" asp-action="UploadPlugin">
|
|
<input type="file" class="form-control mb-3" required name="files" accept=".btcpay" id="files">
|
|
<button class="btn btn-primary" type="submit">Upload</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@if (Model.Commands.Any())
|
|
{
|
|
<div class="mb-4">
|
|
<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">
|
|
Pending Actions
|
|
</button>
|
|
<div class="row collapse" id="pending-actions">
|
|
<div class="col col-12 col-lg-6 mb-4">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<h4 class="card-title">Pending actions</h4>
|
|
<ul class="list-group list-group-flush">
|
|
@foreach (var extComm in Model.Commands.GroupBy(tuple => tuple.plugin))
|
|
{
|
|
<li class="list-group-item p-2">
|
|
<div class="d-flex flex-wrap align-items-center justify-content-between">
|
|
<span class="my-2 me-3">@extComm.Key</span>
|
|
<form asp-action="CancelPluginCommands" asp-route-plugin="@extComm.Key">
|
|
<button type="submit" class="btn btn-outline-secondary">Cancel pending @extComm.Last().command</button>
|
|
</form>
|
|
</div>
|
|
</li>
|
|
}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
@if (Model.Disabled.Any())
|
|
{
|
|
<div class="mb-4">
|
|
<h3 class="mb-4">Disabled Plugins</h3>
|
|
<button class="btn btn-secondary mb-4" type="button" data-bs-toggle="collapse" data-bs-target="#disabled-plugins">
|
|
Disabled Plugins
|
|
</button>
|
|
<div class="row collapse" id="disabled-plugins">
|
|
<div class="col col-12 col-lg-6 mb-4">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<h4 class="card-title">Disabled plugins</h4>
|
|
<ul class="list-group list-group-flush">
|
|
@foreach (var d in Model.Disabled)
|
|
{
|
|
<li class="list-group-item px-0">
|
|
<div class="d-flex flex-wrap align-items-center justify-content-between mx-3">
|
|
<span class="my-2 me-3">@d</span>
|
|
<form asp-action="UnInstallPlugin" asp-route-plugin="@d">
|
|
<button type="submit" class="btn btn-outline-secondary">Uninstall</button>
|
|
</form>
|
|
</div>
|
|
</li>
|
|
}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|