2020-11-05 10:21:09 +01:00
@using BTCPayServer.Configuration
2020-11-17 13:46:23 +01:00
@using BTCPayServer.Abstractions.Contracts
2020-10-21 14:02:20 +02:00
@model BTCPayServer.Controllers.ServerController.ListPluginsViewModel
2020-11-05 10:21:09 +01:00
@inject BTCPayServerOptions BTCPayServerOptions
2020-10-21 14:02:20 +02:00
@{
ViewData.SetActivePageAndTitle(ServerNavPages.Plugins);
2020-11-05 15:43:14 +01:00
var installed = Model.Installed.ToDictionary(plugin => plugin.Identifier.ToLowerInvariant(), plugin => plugin.Version);
2020-11-06 08:06:37 +01:00
var availableAndNotInstalled = Model.Available.Where(plugin => !installed.ContainsKey(plugin.Identifier.ToLowerInvariant())).Select(plugin => (plugin, BTCPayServerOptions.RecommendedPlugins.Contains(plugin.Identifier.ToLowerInvariant()))).OrderBy(tuple => tuple.Item1);
2020-11-05 15:43:14 +01:00
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;
}
2020-10-21 14:02:20 +02:00
}
2020-10-22 10:58:22 +02:00
<partial name="_StatusMessage"/>
2020-11-06 08:06:37 +01:00
<style>
.version-switch .nav-link { display: inline; }
.version-switch .nav-link.active { display: none; }
</style>
2020-10-21 14:02:20 +02:00
@if (Model.Commands.Any())
{
2020-10-22 10:58:22 +02:00
<div class="alert alert-info mb-5">
2020-10-21 14:02:20 +02:00
You need to restart BTCPay Server in order to update your active plugins.
@if (Model.CanShowRestart)
{
2020-10-22 10:58:22 +02:00
<form method="post" asp-action="Maintenance" class="mt-2">
<button type="submit" name="command" value="restart" class="btn btn-info" asp-action="Maintenance">Restart now</button>
2020-10-21 14:02:20 +02:00
</form>
}
</div>
}
@if (Model.Installed.Any())
{
2020-10-22 10:58:22 +02:00
<h3 class="mb-3">Installed Plugins</h3>
<div class="row mb-4">
2020-10-21 14:02:20 +02:00
@foreach (var plugin in Model.Installed)
{
var matchedAvailable = Model.Available.SingleOrDefault(availablePlugin => availablePlugin.Identifier == plugin.Identifier);
2020-11-05 15:43:14 +01:00
var updateAvailable = !plugin.SystemPlugin && matchedAvailable != null && plugin.Version < matchedAvailable.Version;
2020-11-06 08:06:37 +01:00
var tabId = plugin.Identifier.ToLowerInvariant().Replace(".", "_");
2020-10-22 10:58:22 +02:00
<div class="col col-12 col-lg-6 mb-4">
<div class="card h-100">
<div class="card-body">
2020-11-06 08:06:37 +01:00
<h4 class="card-title d-inline-block" title="@plugin.Identifier" data-toggle="tooltip">@plugin.Name</h4>
<div class="d-flex flex-wrap align-items-center mb-3">
<h5 class="card-subtitle text-muted d-flex align-items-center mr-3">
@plugin.Version
@if (plugin.SystemPlugin)
{
<div class="badge badge-secondary ml-2">System plugin</div>
}
else if (updateAvailable)
{
<div class="badge badge-info ml-2">
@matchedAvailable.Version available
</div>
}
</h5>
2020-11-05 15:43:14 +01:00
@if (updateAvailable)
2020-10-22 10:58:22 +02:00
{
2020-11-06 08:06:37 +01:00
<span class="nav version-switch mt-n1" role="tablist">
<a data-toggle="tab" href="#@tabId-current" class="nav-link px-0 small text-info show active">Show current version</a>
<a data-toggle="tab" href="#@tabId-update" class="nav-link px-0 small text-info">Show update information</a>
</span>
2020-10-22 10:58:22 +02:00
}
2020-11-05 15:43:14 +01:00
</div>
<div class="tab-content">
2020-11-06 08:06:37 +01:00
<div class="tab-pane active" id="@tabId-current">
2020-11-05 15:43:14 +01:00
<p class="card-text">@plugin.Description</p>
@if (plugin.Dependencies.Any())
{
2020-11-06 08:06:37 +01:00
<h5 class="text-muted">Dependencies</h5>
2020-11-05 15:43:14 +01:00
<ul class="list-group list-group-flush">
@foreach (var dependency in plugin.Dependencies)
{
2020-11-06 08:06:37 +01:00
<li class="list-group-item px-0">
2020-11-05 15:43:14 +01:00
@dependency
@if (!DependencyMet(dependency))
{
<span title="Dependency not met." data-toggle="tooltip" class="fa fa-warning text-danger"></span>
}
</li>
}
</ul>
}
</div>
@if (updateAvailable)
2020-10-22 10:58:22 +02:00
{
2020-11-06 08:06:37 +01:00
<div class="tab-pane" id="@tabId-update">
2020-11-05 15:43:14 +01:00
<p class="card-text">@matchedAvailable.Description</p>
@if (matchedAvailable.Dependencies.Any())
{
2020-11-06 08:06:37 +01:00
<h5 class="text-muted">Dependencies</h5>
2020-11-05 15:43:14 +01:00
<ul class="list-group list-group-flush">
@foreach (var dependency in matchedAvailable.Dependencies)
{
2020-11-06 08:06:37 +01:00
<li class="list-group-item px-0">
2020-11-05 15:43:14 +01:00
@dependency
@if (!DependencyMet(dependency))
{
<span title="Dependency not met." data-toggle="tooltip" class="fa fa-warning text-danger"></span>
}
</li>
}
</ul>
}
</div>
2020-10-22 10:58:22 +02:00
}
2020-11-05 15:43:14 +01:00
</div>
2020-10-22 10:58:22 +02:00
</div>
2020-11-05 15:43:14 +01:00
@{
var pendingAction = Model.Commands.Any(tuple => tuple.plugin.Equals(plugin.Identifier, StringComparison.InvariantCultureIgnoreCase));
}
@if (!plugin.SystemPlugin && (pendingAction || (updateAvailable && DependenciesMet(matchedAvailable.Dependencies)) || !DependentOn(plugin.Identifier)))
2020-10-22 10:58:22 +02:00
{
<div class="card-footer border-0 pb-3 d-flex">
2020-11-05 15:43:14 +01:00
@if (pendingAction)
2020-10-22 10:58:22 +02:00
{
2020-10-21 14:02:20 +02:00
<form asp-action="CancelPluginCommands" asp-route-plugin="@plugin.Identifier">
2020-10-22 10:58:22 +02:00
<button type="submit" class="btn btn-outline-secondary">Cancel pending action</button>
2020-10-21 14:02:20 +02:00
</form>
2020-10-22 10:58:22 +02:00
}
else
2020-10-21 14:02:20 +02:00
{
2020-11-05 15:43:14 +01:00
@if (updateAvailable && DependenciesMet(matchedAvailable.Dependencies))
2020-10-22 10:58:22 +02:00
{
2020-11-05 15:43:14 +01:00
<form asp-action="InstallPlugin" asp-route-plugin="@plugin.Identifier" asp-route-update="true" class="mr-3">
2020-10-22 10:58:22 +02:00
<button type="submit" class="btn btn-secondary">Update</button>
</form>
}
2020-11-05 15:43:14 +01:00
@if (DependentOn(plugin.Identifier))
{
<button type="button" class="btn btn-outline-danger" data-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>
}
2020-10-21 14:02:20 +02:00
}
</div>
2020-10-22 10:58:22 +02:00
}
</div>
2020-10-21 14:02:20 +02:00
</div>
}
</div>
}
@if (availableAndNotInstalled.Any())
{
2020-10-22 10:58:22 +02:00
<h3 class="mb-3">Available Plugins</h3>
<div class="row mb-4">
2020-11-05 10:21:09 +01:00
@foreach (var pluginT in availableAndNotInstalled)
2020-10-21 14:02:20 +02:00
{
2020-11-05 10:21:09 +01:00
var plugin = pluginT.Item1;
2020-10-22 10:58:22 +02:00
<div class="col col-12 col-lg-6 mb-4">
<div class="card h-100">
<div class="card-body">
2020-11-06 08:06:37 +01:00
<h4 class="card-title d-inline-block" data-toggle="tooltip" title="@plugin.Identifier">@plugin.Name</h4>
<h5 class="card-subtitle mb-3 text-muted d-flex align-items-center">
@plugin.Version
2020-11-05 10:21:09 +01:00
@if (pluginT.Item2)
{
2020-11-06 08:06:37 +01:00
<div class="badge badge-light ml-2" data-toggle="tooltip" title="This plugin has been recommended to be installed by your deployment method." class="text-nowrap">Recommended <span class="fa fa-question-circle-o"></span></div>
2020-11-05 10:21:09 +01:00
}
</h5>
2020-10-22 10:58:22 +02:00
<p class="card-text">@plugin.Description</p>
2020-11-05 15:43:14 +01:00
@if (plugin.Dependencies?.Any() is true)
{
2020-11-06 08:06:37 +01:00
<h5 class="text-muted">Dependencies</h5>
2020-11-05 15:43:14 +01:00
<ul class="list-group list-group-flush">
@foreach (var dependency in plugin.Dependencies)
{
2020-11-06 08:06:37 +01:00
<li class="list-group-item px-0">
2020-11-05 15:43:14 +01:00
@dependency
@if (!DependencyMet(dependency))
{
<span title="Dependency not met." data-toggle="tooltip" class="fa fa-warning text-danger"></span>
}
</li>
}
</ul>
}
2020-10-22 10:58:22 +02:00
</div>
2020-11-05 15:43:14 +01:00
@{
var pending = Model.Commands.Any(tuple => tuple.plugin.Equals(plugin.Identifier, StringComparison.InvariantCultureIgnoreCase));
}
2020-10-22 10:58:22 +02:00
<div class="card-footer border-0 pb-3">
2020-11-05 15:43:14 +01:00
@if (pending)
2020-10-22 10:58:22 +02:00
{
2020-10-21 14:02:20 +02:00
<form asp-action="CancelPluginCommands" asp-route-plugin="@plugin.Identifier">
2020-10-22 10:58:22 +02:00
<button type="submit" class="btn btn-outline-secondary">Cancel pending install</button>
2020-10-21 14:02:20 +02:00
</form>
2020-10-22 10:58:22 +02:00
}
2020-11-05 15:43:14 +01:00
else if (DependenciesMet(plugin.Dependencies))
2020-10-22 10:58:22 +02:00
{
2020-11-05 10:21:09 +01:00
<form asp-action="InstallPlugin" asp-route-plugin="@plugin.Identifier">
2020-10-22 10:58:22 +02:00
<button type="submit" class="btn btn-primary">Install</button>
</form>
}
2020-11-05 15:43:14 +01:00
else
{
2020-11-06 08:06:37 +01:00
<div class="text-danger">
Cannot install until dependencies are met.
</div>
2020-11-05 15:43:14 +01:00
}
2020-10-22 10:58:22 +02:00
</div>
2020-10-21 14:02:20 +02:00
</div>
</div>
}
</div>
}
2020-10-22 10:58:22 +02:00
<div class="mb-4">
<button class="btn btn-link text-secondary mb-2" type="button" data-toggle="collapse" data-target="#manual-upload">
Upload plugin
</button>
<div class="row collapse" id="manual-upload">
<div class="col col-12 col-lg-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="mr-1">This is an extremely dangerous operation!</h6>
Only upload plugins from trusted sources.
2020-10-21 14:02:20 +02:00
</div>
2020-10-22 10:58:22 +02:00
<form method="post" enctype="multipart/form-data" asp-action="UploadPlugin">
<div class="form-group">
<div class="custom-file">
<input type="file" class="custom-file-input form-control-file" required name="files" accept=".btcpay" id="files">
<label class="custom-file-label" for="files">Choose file</label>
</div>
</div>
<button class="btn btn-primary" type="submit">Upload</button>
</form>
2020-10-21 14:02:20 +02:00
</div>
2020-10-22 10:58:22 +02:00
</div>
2020-10-21 14:02:20 +02:00
</div>
</div>
</div>
2020-10-22 10:58:22 +02:00
2020-10-21 14:02:20 +02:00
@if (Model.Commands.Any())
{
2020-10-22 10:58:22 +02:00
<div class="mb-4">
<button class="btn btn-link text-secondary mb-2" type="button" data-toggle="collapse" data-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 px-0">
<div class="d-flex flex-wrap align-items-center justify-content-between">
<span class="my-2 mr-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>
2020-10-21 14:02:20 +02:00
</div>
</div>
</div>
}
@section Scripts {
<script>
2020-10-22 10:58:22 +02:00
$(document).ready(function () {
$(".custom-file-input").on("change", function () {
var label = $(this).next("label");
var el = $(this).get(0);
if (el.files.length > 0) {
var fileName = el.files[0].name;
label.addClass("selected").html(fileName);
} else {
label.removeClass("selected").html("Choose file");
}
2020-10-21 14:02:20 +02:00
});
2020-10-22 10:58:22 +02:00
});
</script>
2020-10-21 14:02:20 +02:00
}