mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-11-03 20:15:59 +01:00
Experiment of plugin installation from the remote repository
This commit is contained in:
@@ -322,21 +322,16 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
|||||||
get("/admin/plugins")(adminOnly {
|
get("/admin/plugins")(adminOnly {
|
||||||
// Installed plugins
|
// Installed plugins
|
||||||
val enabledPlugins = PluginRegistry().getPlugins()
|
val enabledPlugins = PluginRegistry().getPlugins()
|
||||||
|
val gitbucketVersion = GitBucketCoreModule.getVersions.asScala.last.getVersion
|
||||||
|
|
||||||
val gitbucketVersion = Semver.valueOf(GitBucketCoreModule.getVersions.asScala.last.getVersion)
|
// Plugins in the remote repository
|
||||||
|
|
||||||
// Plugins in the local repository
|
|
||||||
val repositoryPlugins = PluginRepository
|
val repositoryPlugins = PluginRepository
|
||||||
.getPlugins()
|
.getPlugins()
|
||||||
.filterNot { meta =>
|
|
||||||
enabledPlugins.exists { plugin =>
|
|
||||||
plugin.pluginId == meta.id &&
|
|
||||||
Semver.valueOf(plugin.pluginVersion).greaterThanOrEqualTo(Semver.valueOf(meta.latestVersion.version))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.map { meta =>
|
.map { meta =>
|
||||||
(meta, meta.versions.reverse.find { version =>
|
(meta, meta.versions.reverse.find { version =>
|
||||||
gitbucketVersion.satisfies(version.range)
|
gitbucketVersion == version.gitbucketVersion && !enabledPlugins.exists { plugin =>
|
||||||
|
plugin.pluginId == meta.id && plugin.pluginVersion == version.version
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
.collect {
|
.collect {
|
||||||
@@ -345,12 +340,20 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
|||||||
pluginId = meta.id,
|
pluginId = meta.id,
|
||||||
pluginName = meta.name,
|
pluginName = meta.name,
|
||||||
pluginVersion = version.version,
|
pluginVersion = version.version,
|
||||||
|
gitbucketVersion = Some(version.gitbucketVersion),
|
||||||
description = meta.description
|
description = meta.description
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge
|
// Merge
|
||||||
val plugins = enabledPlugins.map((_, true)) ++ repositoryPlugins.map((_, false))
|
val plugins = (enabledPlugins.map((_, true)) ++ repositoryPlugins.map((_, false)))
|
||||||
|
.groupBy(_._1.pluginId)
|
||||||
|
.map {
|
||||||
|
case (pluginId, plugins) =>
|
||||||
|
val (plugin, enabled) = plugins.head
|
||||||
|
(plugin, enabled, if (plugins.length > 1) plugins.last._1.pluginVersion else "")
|
||||||
|
}
|
||||||
|
.toList
|
||||||
|
|
||||||
html.plugins(plugins, flash.get("info"))
|
html.plugins(plugins, flash.get("info"))
|
||||||
})
|
})
|
||||||
@@ -378,21 +381,20 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
|||||||
post("/admin/plugins/:pluginId/:version/_install")(adminOnly {
|
post("/admin/plugins/:pluginId/:version/_install")(adminOnly {
|
||||||
val pluginId = params("pluginId")
|
val pluginId = params("pluginId")
|
||||||
val version = params("version")
|
val version = params("version")
|
||||||
/// TODO!!!!
|
|
||||||
PluginRepository
|
PluginRepository
|
||||||
.getPlugins()
|
.getPlugins()
|
||||||
.collect { case meta if meta.id == pluginId => (meta, meta.versions.find(_.version == version)) }
|
.collect { case meta if meta.id == pluginId => (meta, meta.versions.find(_.version == version)) }
|
||||||
.foreach {
|
.foreach {
|
||||||
case (meta, version) =>
|
case (meta, version) =>
|
||||||
version.foreach { version =>
|
version.foreach { version =>
|
||||||
// TODO Install version!
|
|
||||||
PluginRegistry.install(
|
PluginRegistry.install(
|
||||||
new java.io.File(PluginHome, s".repository/${version.file}"),
|
new java.net.URL(version.url),
|
||||||
request.getServletContext,
|
request.getServletContext,
|
||||||
loadSystemSettings(),
|
loadSystemSettings(),
|
||||||
request2Session(request).conn
|
request2Session(request).conn
|
||||||
)
|
)
|
||||||
flash += "info" -> s"${pluginId} was installed."
|
flash += "info" -> s"${pluginId}:${version.version} was installed."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
redirect("/admin/plugins")
|
redirect("/admin/plugins")
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import java.util.concurrent.ConcurrentLinkedQueue
|
|||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import javax.servlet.ServletContext
|
import javax.servlet.ServletContext
|
||||||
|
|
||||||
|
import com.github.zafarkhaja.semver.Version
|
||||||
|
import gitbucket.core.GitBucketCoreModule
|
||||||
import gitbucket.core.controller.{Context, ControllerBase}
|
import gitbucket.core.controller.{Context, ControllerBase}
|
||||||
import gitbucket.core.model.{Account, Issue}
|
import gitbucket.core.model.{Account, Issue}
|
||||||
import gitbucket.core.service.ProtectedBranchService.ProtectedBranchReceiveHook
|
import gitbucket.core.service.ProtectedBranchService.ProtectedBranchReceiveHook
|
||||||
@@ -202,7 +204,7 @@ object PluginRegistry {
|
|||||||
|
|
||||||
private var watcher: PluginWatchThread = null
|
private var watcher: PluginWatchThread = null
|
||||||
private var extraWatcher: PluginWatchThread = null
|
private var extraWatcher: PluginWatchThread = null
|
||||||
private val initializing = new AtomicBoolean(false)
|
//private val initializing = new AtomicBoolean(false)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the PluginRegistry singleton instance.
|
* Returns the PluginRegistry singleton instance.
|
||||||
@@ -234,7 +236,15 @@ object PluginRegistry {
|
|||||||
// logger.error(s"Error during uninstalling plugin: ${plugin.pluginJar.getName}", e)
|
// logger.error(s"Error during uninstalling plugin: ${plugin.pluginJar.getName}", e)
|
||||||
// }
|
// }
|
||||||
shutdown(context, settings)
|
shutdown(context, settings)
|
||||||
plugin.pluginJar.delete()
|
|
||||||
|
new File(PluginHome)
|
||||||
|
.listFiles((_: File, name: String) => {
|
||||||
|
name.startsWith(s"gitbucket-${pluginId}-plugin") && name.endsWith(".jar")
|
||||||
|
})
|
||||||
|
.foreach { file =>
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
|
||||||
instance = new PluginRegistry()
|
instance = new PluginRegistry()
|
||||||
initialize(context, settings, conn)
|
initialize(context, settings, conn)
|
||||||
}
|
}
|
||||||
@@ -243,10 +253,11 @@ object PluginRegistry {
|
|||||||
/**
|
/**
|
||||||
* Install a plugin from a specified jar file.
|
* Install a plugin from a specified jar file.
|
||||||
*/
|
*/
|
||||||
def install(file: File, context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit =
|
def install(url: java.net.URL, context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit =
|
||||||
synchronized {
|
synchronized {
|
||||||
shutdown(context, settings)
|
shutdown(context, settings)
|
||||||
FileUtils.copyFile(file, new File(PluginHome, file.getName))
|
val in = url.openStream()
|
||||||
|
FileUtils.copyToFile(in, new File(PluginHome, new File(url.getFile).getName))
|
||||||
instance = new PluginRegistry()
|
instance = new PluginRegistry()
|
||||||
initialize(context, settings, conn)
|
initialize(context, settings, conn)
|
||||||
}
|
}
|
||||||
@@ -257,12 +268,27 @@ object PluginRegistry {
|
|||||||
override def accept(dir: File, name: String): Boolean = name.endsWith(".jar")
|
override def accept(dir: File, name: String): Boolean = name.endsWith(".jar")
|
||||||
})
|
})
|
||||||
.toSeq
|
.toSeq
|
||||||
.sortBy(_.getName)
|
.sortBy(x => Version.valueOf(getPluginVersion(x.getName)))
|
||||||
.reverse
|
.reverse
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy val extraPluginDir: Option[String] = Option(System.getProperty("gitbucket.pluginDir"))
|
lazy val extraPluginDir: Option[String] = Option(System.getProperty("gitbucket.pluginDir"))
|
||||||
|
|
||||||
|
def getGitBucketVersion(pluginJarFileName: String): Option[String] = {
|
||||||
|
val regex = ".+-gitbucket\\_(\\d+\\.\\d+\\.\\d+)-.+".r
|
||||||
|
pluginJarFileName match {
|
||||||
|
case regex(x) => Some(x)
|
||||||
|
case _ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getPluginVersion(pluginJarFileName: String): String = {
|
||||||
|
val regex = ".+-(\\d+\\.\\d+\\.\\d+)\\.jar$".r
|
||||||
|
pluginJarFileName match {
|
||||||
|
case regex(x) => x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes all installed plugins.
|
* Initializes all installed plugins.
|
||||||
*/
|
*/
|
||||||
@@ -278,6 +304,7 @@ object PluginRegistry {
|
|||||||
installedDir.mkdirs()
|
installedDir.mkdirs()
|
||||||
|
|
||||||
val pluginJars = listPluginJars(pluginDir)
|
val pluginJars = listPluginJars(pluginDir)
|
||||||
|
|
||||||
val extraJars = extraPluginDir
|
val extraJars = extraPluginDir
|
||||||
.map { extraDir =>
|
.map { extraDir =>
|
||||||
listPluginJars(new File(extraDir))
|
listPluginJars(new File(extraDir))
|
||||||
@@ -288,9 +315,9 @@ object PluginRegistry {
|
|||||||
val installedJar = new File(installedDir, pluginJar.getName)
|
val installedJar = new File(installedDir, pluginJar.getName)
|
||||||
|
|
||||||
FileUtils.copyFile(pluginJar, installedJar)
|
FileUtils.copyFile(pluginJar, installedJar)
|
||||||
|
|
||||||
logger.info(s"Initialize ${pluginJar.getName}")
|
logger.info(s"Initialize ${pluginJar.getName}")
|
||||||
val classLoader = new URLClassLoader(Array(installedJar.toURI.toURL), Thread.currentThread.getContextClassLoader)
|
val classLoader =
|
||||||
|
new URLClassLoader(Array(installedJar.toURI.toURL), Thread.currentThread.getContextClassLoader)
|
||||||
try {
|
try {
|
||||||
val plugin = classLoader.loadClass("Plugin").getDeclaredConstructor().newInstance().asInstanceOf[Plugin]
|
val plugin = classLoader.loadClass("Plugin").getDeclaredConstructor().newInstance().asInstanceOf[Plugin]
|
||||||
val pluginId = plugin.pluginId
|
val pluginId = plugin.pluginId
|
||||||
@@ -304,7 +331,12 @@ object PluginRegistry {
|
|||||||
// Migration
|
// Migration
|
||||||
val solidbase = new Solidbase()
|
val solidbase = new Solidbase()
|
||||||
solidbase
|
solidbase
|
||||||
.migrate(conn, classLoader, DatabaseConfig.liquiDriver, new Module(plugin.pluginId, plugin.versions: _*))
|
.migrate(
|
||||||
|
conn,
|
||||||
|
classLoader,
|
||||||
|
DatabaseConfig.liquiDriver,
|
||||||
|
new Module(plugin.pluginId, plugin.versions: _*)
|
||||||
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
// Check database version
|
// Check database version
|
||||||
@@ -323,6 +355,7 @@ object PluginRegistry {
|
|||||||
pluginId = plugin.pluginId,
|
pluginId = plugin.pluginId,
|
||||||
pluginName = plugin.pluginName,
|
pluginName = plugin.pluginName,
|
||||||
pluginVersion = plugin.versions.last.getVersion,
|
pluginVersion = plugin.versions.last.getVersion,
|
||||||
|
gitbucketVersion = getGitBucketVersion(installedJar.getName),
|
||||||
description = plugin.description,
|
description = plugin.description,
|
||||||
pluginClass = plugin,
|
pluginClass = plugin,
|
||||||
pluginJar = pluginJar,
|
pluginJar = pluginJar,
|
||||||
@@ -334,6 +367,7 @@ object PluginRegistry {
|
|||||||
} catch {
|
} catch {
|
||||||
case e: Throwable => logger.error(s"Error during plugin initialization: ${pluginJar.getName}", e)
|
case e: Throwable => logger.error(s"Error during plugin initialization: ${pluginJar.getName}", e)
|
||||||
}
|
}
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (watcher == null) {
|
if (watcher == null) {
|
||||||
@@ -384,6 +418,7 @@ class PluginInfoBase(
|
|||||||
val pluginId: String,
|
val pluginId: String,
|
||||||
val pluginName: String,
|
val pluginName: String,
|
||||||
val pluginVersion: String,
|
val pluginVersion: String,
|
||||||
|
val gitbucketVersion: Option[String],
|
||||||
val description: String
|
val description: String
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -391,11 +426,12 @@ case class PluginInfo(
|
|||||||
override val pluginId: String,
|
override val pluginId: String,
|
||||||
override val pluginName: String,
|
override val pluginName: String,
|
||||||
override val pluginVersion: String,
|
override val pluginVersion: String,
|
||||||
|
override val gitbucketVersion: Option[String],
|
||||||
override val description: String,
|
override val description: String,
|
||||||
pluginClass: Plugin,
|
pluginClass: Plugin,
|
||||||
pluginJar: File,
|
pluginJar: File,
|
||||||
classLoader: URLClassLoader
|
classLoader: URLClassLoader
|
||||||
) extends PluginInfoBase(pluginId, pluginName, pluginVersion, description)
|
) extends PluginInfoBase(pluginId, pluginName, pluginVersion, gitbucketVersion, description)
|
||||||
|
|
||||||
class PluginWatchThread(context: ServletContext, dir: String) extends Thread with SystemSettingsService {
|
class PluginWatchThread(context: ServletContext, dir: String) extends Thread with SystemSettingsService {
|
||||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package gitbucket.core.plugin
|
|||||||
|
|
||||||
import org.json4s._
|
import org.json4s._
|
||||||
import gitbucket.core.util.Directory._
|
import gitbucket.core.util.Directory._
|
||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.{FileUtils, IOUtils}
|
||||||
|
|
||||||
object PluginRepository {
|
object PluginRepository {
|
||||||
implicit val formats = DefaultFormats
|
implicit val formats = DefaultFormats
|
||||||
@@ -15,9 +15,10 @@ object PluginRepository {
|
|||||||
lazy val LocalRepositoryIndexFile = new java.io.File(LocalRepositoryDir, "plugins.json")
|
lazy val LocalRepositoryIndexFile = new java.io.File(LocalRepositoryDir, "plugins.json")
|
||||||
|
|
||||||
def getPlugins(): Seq[PluginMetadata] = {
|
def getPlugins(): Seq[PluginMetadata] = {
|
||||||
if (LocalRepositoryIndexFile.exists) {
|
// TODO Pre-load the plugin list in background
|
||||||
parsePluginJson(FileUtils.readFileToString(LocalRepositoryIndexFile, "UTF-8"))
|
val url = new java.net.URL("https://plugins.gitbucket-community.org/releases/plugins.json")
|
||||||
} else Nil
|
val str = IOUtils.toString(url, "UTF-8")
|
||||||
|
parsePluginJson(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -36,7 +37,5 @@ case class PluginMetadata(
|
|||||||
case class VersionDef(
|
case class VersionDef(
|
||||||
version: String,
|
version: String,
|
||||||
url: String,
|
url: String,
|
||||||
range: String
|
gitbucketVersion: String
|
||||||
) {
|
)
|
||||||
lazy val file = url.substring(url.lastIndexOf("/") + 1)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -136,46 +136,46 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
|
|||||||
}
|
}
|
||||||
|
|
||||||
private def extractBundledPlugins(gitbucketVersion: String): Unit = {
|
private def extractBundledPlugins(gitbucketVersion: String): Unit = {
|
||||||
logger.info("Extract bundled plugins")
|
// logger.info("Extract bundled plugins")
|
||||||
val cl = Thread.currentThread.getContextClassLoader
|
// val cl = Thread.currentThread.getContextClassLoader
|
||||||
try {
|
// try {
|
||||||
using(cl.getResourceAsStream("plugins/plugins.json")) { pluginsFile =>
|
// using(cl.getResourceAsStream("plugins/plugins.json")) { pluginsFile =>
|
||||||
if (pluginsFile != null) {
|
// if (pluginsFile != null) {
|
||||||
val pluginsJson = IOUtils.toString(pluginsFile, "UTF-8")
|
// val pluginsJson = IOUtils.toString(pluginsFile, "UTF-8")
|
||||||
|
//
|
||||||
FileUtils.forceMkdir(PluginRepository.LocalRepositoryDir)
|
// FileUtils.forceMkdir(PluginRepository.LocalRepositoryDir)
|
||||||
FileUtils.write(PluginRepository.LocalRepositoryIndexFile, pluginsJson, "UTF-8")
|
// FileUtils.write(PluginRepository.LocalRepositoryIndexFile, pluginsJson, "UTF-8")
|
||||||
|
//
|
||||||
val plugins = PluginRepository.parsePluginJson(pluginsJson)
|
// val plugins = PluginRepository.parsePluginJson(pluginsJson)
|
||||||
plugins.foreach { plugin =>
|
// plugins.foreach { plugin =>
|
||||||
plugin.versions
|
// plugin.versions
|
||||||
.sortBy { x =>
|
// .sortBy { x =>
|
||||||
Semver.valueOf(x.version)
|
// Semver.valueOf(x.version)
|
||||||
}
|
// }
|
||||||
.reverse
|
// .reverse
|
||||||
.zipWithIndex
|
// .zipWithIndex
|
||||||
.foreach {
|
// .foreach {
|
||||||
case (version, i) =>
|
// case (version, i) =>
|
||||||
val file = new File(PluginRepository.LocalRepositoryDir, version.file)
|
// val file = new File(PluginRepository.LocalRepositoryDir, version.file)
|
||||||
if (!file.exists) {
|
// if (!file.exists) {
|
||||||
logger.info(s"Copy ${plugin} to ${file.getAbsolutePath}")
|
// logger.info(s"Copy ${plugin} to ${file.getAbsolutePath}")
|
||||||
FileUtils.forceMkdirParent(file)
|
// FileUtils.forceMkdirParent(file)
|
||||||
using(cl.getResourceAsStream("plugins/" + version.file), new FileOutputStream(file)) {
|
// using(cl.getResourceAsStream("plugins/" + version.file), new FileOutputStream(file)) {
|
||||||
case (in, out) => IOUtils.copy(in, out)
|
// case (in, out) => IOUtils.copy(in, out)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if (plugin.default && i == 0) {
|
// if (plugin.default && i == 0) {
|
||||||
logger.info(s"Enable ${file.getName} in default")
|
// logger.info(s"Enable ${file.getName} in default")
|
||||||
FileUtils.copyFile(file, new File(PluginHome, version.file))
|
// FileUtils.copyFile(file, new File(PluginHome, version.file))
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
} catch {
|
// } catch {
|
||||||
case e: Exception => logger.error("Error in extracting bundled plugin", e)
|
// case e: Exception => logger.error("Error in extracting bundled plugin", e)
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
override def contextDestroyed(event: ServletContextEvent): Unit = {
|
override def contextDestroyed(event: ServletContextEvent): Unit = {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@(plugins: List[(gitbucket.core.plugin.PluginInfoBase, Boolean)], info: Option[Any])(implicit context: gitbucket.core.controller.Context)
|
@(plugins: List[(gitbucket.core.plugin.PluginInfoBase, Boolean, String)], info: Option[Any])(implicit context: gitbucket.core.controller.Context)
|
||||||
@gitbucket.core.html.main("Plugins"){
|
@gitbucket.core.html.main("Plugins"){
|
||||||
@gitbucket.core.admin.html.menu("plugins") {
|
@gitbucket.core.admin.html.menu("plugins") {
|
||||||
@gitbucket.core.helper.html.information(info)
|
@gitbucket.core.helper.html.information(info)
|
||||||
@@ -8,18 +8,24 @@
|
|||||||
<h1 class="system-settings-title">Plugins</h1>
|
<h1 class="system-settings-title">Plugins</h1>
|
||||||
@if(plugins.size > 0) {
|
@if(plugins.size > 0) {
|
||||||
<ul>
|
<ul>
|
||||||
@plugins.map { case (plugin, enabled) =>
|
@plugins.map { case (plugin, enabled, updatableVersion) =>
|
||||||
<li><a href="#@plugin.pluginId">@plugin.pluginId:@plugin.pluginVersion</a></li>
|
<li><a href="#@plugin.pluginId">@plugin.pluginId:@plugin.pluginVersion</a></li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@plugins.map { case (plugin, enabled) =>
|
@plugins.map { case (plugin, enabled, updatableVersion) =>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading strong" id="@plugin.pluginId">
|
<div class="panel-heading strong" id="@plugin.pluginId">
|
||||||
@if(enabled){
|
@if(enabled){
|
||||||
|
@if(updatableVersion.isEmpty){
|
||||||
<form action="@{context.path}/admin/plugins/@{plugin.pluginId}/@{plugin.pluginVersion}/_uninstall" method="POST" class="pull-right uninstall-form">
|
<form action="@{context.path}/admin/plugins/@{plugin.pluginId}/@{plugin.pluginVersion}/_uninstall" method="POST" class="pull-right uninstall-form">
|
||||||
<input type="submit" value="Uninstall" class="btn btn-danger btn-sm" style="position: relative; top: -5px; left: 10px;" data-name="@plugin.pluginName">
|
<input type="submit" value="Uninstall" class="btn btn-danger btn-sm" style="position: relative; top: -5px; left: 10px;" data-name="@plugin.pluginName">
|
||||||
</form>
|
</form>
|
||||||
|
} else {
|
||||||
|
<form action="@{context.path}/admin/plugins/@{plugin.pluginId}/@{updatableVersion}/_install" method="POST" class="pull-right install-form">
|
||||||
|
<input type="submit" value="Update" class="btn btn-primary btn-sm" style="position: relative; top: -5px; left: 10px;" data-name="@plugin.pluginName">
|
||||||
|
</form>
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
<form action="@{context.path}/admin/plugins/@{plugin.pluginId}/@{plugin.pluginVersion}/_install" method="POST" class="pull-right install-form">
|
<form action="@{context.path}/admin/plugins/@{plugin.pluginId}/@{plugin.pluginVersion}/_install" method="POST" class="pull-right install-form">
|
||||||
<input type="submit" value="Install" class="btn btn-success btn-sm" style="position: relative; top: -5px; left: 10px;" data-name="@plugin.pluginName">
|
<input type="submit" value="Install" class="btn btn-success btn-sm" style="position: relative; top: -5px; left: 10px;" data-name="@plugin.pluginName">
|
||||||
|
|||||||
Reference in New Issue
Block a user