The Plugin Manager is now bundled with OctoPrint
This commit is contained in:
parent
a42868d7eb
commit
caef322b65
10 changed files with 1340 additions and 7 deletions
|
|
@ -17,7 +17,9 @@
|
|||
* You can now define a folder (default: `~/.octoprint/watched`) to be watched for newly added GCODE (or -- if slicing
|
||||
support is enabled -- STL) files to automatically add.
|
||||
* OctoPrint now has a [plugin system](http://docs.octoprint.org/en/devel/plugins/index.html) which allows extending its
|
||||
core functionality.
|
||||
core functionality. Plugins may be installed through the new Plugin Manager available in OctoPrint's settings. This
|
||||
Plugin Manager also allows browsing and easy installation of plugins registered on the official
|
||||
[OctoPrint Plugin Repository](http://plugins.octoprint.org).
|
||||
* New type of API key: [App Session Keys](http://docs.octoprint.org/en/devel/api/apps.html) for trusted applications
|
||||
* Printer Profiles: Printer properties like print volume, extruder offsets etc are now managed via Printer Profiles. A
|
||||
connection to a printer will always have a printer profile associated.
|
||||
|
|
|
|||
|
|
@ -26,11 +26,17 @@ and on the `OctoPrint organization Github page <https://github.com/OctoPrint>`_.
|
|||
Installing Plugins
|
||||
==================
|
||||
|
||||
Plugins can be installed either by unpacking them into one of the configured plugin folders (regularly those are
|
||||
``<octoprint source root>/plugins`` and ``<octoprint config folder>/plugins`` [#f1]_ or by installing them as regular python
|
||||
modules via ``pip`` [#f2]_.
|
||||
Plugins can be installed through the bundled Plugin Manager, which allows installing plugins available in the
|
||||
`OctoPrint Plugin Repository <http://plugins.octoprint.org>`_, from a web address or from an uploaded file archive.
|
||||
|
||||
Please refer to the documentation of the plugin for installations instructions.
|
||||
Please refer to the documentation of the plugin for additional installations instructions.
|
||||
|
||||
Manual Installation
|
||||
-------------------
|
||||
|
||||
If you don't want or can't use the Plugin Manager, plugins may also be installed manually either by copying and
|
||||
unpacking them into one of the configured plugin folders (regularly those are ``<octoprint source root>/plugins`` and
|
||||
``<octoprint config folder>/plugins`` [#f1]_ or by installing them as regular python modules via ``pip`` [#f2]_.
|
||||
|
||||
For a plugin available on the Python Package Index (PyPi), the process is as simple as issuing a
|
||||
|
||||
|
|
|
|||
3
setup.py
3
setup.py
|
|
@ -28,7 +28,8 @@ INSTALL_REQUIRES = [
|
|||
"netifaces",
|
||||
"pylru",
|
||||
"rsa",
|
||||
"pkginfo"
|
||||
"pkginfo",
|
||||
"requests"
|
||||
]
|
||||
|
||||
# Additional requirements for optional install options
|
||||
|
|
|
|||
524
src/octoprint/plugins/pluginmanager/__init__.py
Normal file
524
src/octoprint/plugins/pluginmanager/__init__.py
Normal file
|
|
@ -0,0 +1,524 @@
|
|||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
__copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms of the AGPLv3 License"
|
||||
|
||||
|
||||
import octoprint.plugin
|
||||
import octoprint.plugin.core
|
||||
|
||||
from octoprint.settings import valid_boolean_trues
|
||||
from octoprint.server.util.flask import restricted_access
|
||||
from octoprint.server import admin_permission
|
||||
|
||||
from flask import jsonify, make_response
|
||||
|
||||
import logging
|
||||
import sarge
|
||||
import sys
|
||||
import requests
|
||||
import re
|
||||
|
||||
class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
||||
octoprint.plugin.TemplatePlugin,
|
||||
octoprint.plugin.AssetPlugin,
|
||||
octoprint.plugin.SettingsPlugin,
|
||||
octoprint.plugin.StartupPlugin,
|
||||
octoprint.plugin.BlueprintPlugin):
|
||||
|
||||
def __init__(self):
|
||||
self._pending_enable = set()
|
||||
self._pending_disable = set()
|
||||
self._pending_install = set()
|
||||
self._pending_uninstall = set()
|
||||
|
||||
self._repository_available = False
|
||||
self._repository_plugins = []
|
||||
|
||||
def initialize(self):
|
||||
self._console_logger = logging.getLogger("octoprint.plugins.pluginmanager.console")
|
||||
|
||||
##~~ StartupPlugin
|
||||
|
||||
def on_startup(self, host, port):
|
||||
console_logging_handler = logging.handlers.RotatingFileHandler(self._settings.get_plugin_logfile_path(postfix="console"), maxBytes=2*1024*1024)
|
||||
console_logging_handler.setFormatter(logging.Formatter("%(asctime)s %(message)s"))
|
||||
console_logging_handler.setLevel(logging.DEBUG)
|
||||
|
||||
self._console_logger.addHandler(console_logging_handler)
|
||||
self._console_logger.setLevel(logging.DEBUG)
|
||||
self._console_logger.propagate = False
|
||||
|
||||
self._repository_available = self._refresh_repository()
|
||||
|
||||
##~~ SettingsPlugin
|
||||
|
||||
def get_settings_defaults(self):
|
||||
return dict(
|
||||
repository="http://plugins.octoprint.org/plugins.json",
|
||||
pip=None
|
||||
)
|
||||
|
||||
##~~ AssetPlugin
|
||||
|
||||
def get_assets(self):
|
||||
return dict(
|
||||
js=["js/pluginmanager.js"],
|
||||
css=["css/pluginmanager.css"],
|
||||
less=["less/pluginmanager.less"]
|
||||
)
|
||||
|
||||
##~~ TemplatePlugin
|
||||
|
||||
def get_template_configs(self):
|
||||
return [
|
||||
dict(type="settings", template="pluginmanager_settings.jinja2", custom_bindings=True)
|
||||
]
|
||||
|
||||
##~~ BlueprintPlugin
|
||||
|
||||
@octoprint.plugin.BlueprintPlugin.route("/upload_archive", methods=["POST"])
|
||||
@restricted_access
|
||||
@admin_permission.require(403)
|
||||
def upload_archive(self):
|
||||
import flask
|
||||
|
||||
input_name = "file"
|
||||
input_upload_path = input_name + "." + self._settings.global_get(["server", "uploads", "pathSuffix"])
|
||||
input_upload_name = input_name + "." + self._settings.global_get(["server", "uploads", "nameSuffix"])
|
||||
|
||||
if input_upload_path not in flask.request.values or input_upload_name not in flask.request.values:
|
||||
return flask.make_response("No file included", 400)
|
||||
upload_path = flask.request.values[input_upload_path]
|
||||
upload_name = flask.request.values[input_upload_name]
|
||||
|
||||
import tempfile
|
||||
import shutil
|
||||
import os
|
||||
|
||||
archive = tempfile.NamedTemporaryFile(delete=False, suffix="-{upload_name}".format(**locals()))
|
||||
try:
|
||||
archive.close()
|
||||
shutil.copy(upload_path, archive.name)
|
||||
return self.command_install(path=archive.name, force="force" in flask.request.values and flask.request.values["force"] in valid_boolean_trues)
|
||||
finally:
|
||||
try:
|
||||
os.remove(archive.name)
|
||||
except Exception as e:
|
||||
self._logger.warn("Could not remove temporary file {path} again: {message}".format(path=archive.name, message=str(e)))
|
||||
|
||||
##~~ SimpleApiPlugin
|
||||
|
||||
def get_api_commands(self):
|
||||
return {
|
||||
"install": ["url"],
|
||||
"uninstall": ["plugin"],
|
||||
"enable": ["plugin"],
|
||||
"disable": ["plugin"],
|
||||
"refresh_repository": []
|
||||
}
|
||||
|
||||
def on_api_get(self, request):
|
||||
if not admin_permission.can():
|
||||
return make_response("Insufficient rights", 403)
|
||||
|
||||
plugins = self._plugin_manager.plugins
|
||||
|
||||
result = []
|
||||
for name, plugin in plugins.items():
|
||||
result.append(self._to_external_representation(plugin))
|
||||
|
||||
if "refresh_repository" in request.values and request.values["refresh_repository"] in valid_boolean_trues:
|
||||
self._refresh_repository()
|
||||
|
||||
return jsonify(plugins=result, repository=dict(available=self._repository_available, plugins=self._repository_plugins), os=self._get_os(), octoprint=self._get_octoprint_version())
|
||||
|
||||
def on_api_command(self, command, data):
|
||||
if not admin_permission.can():
|
||||
return make_response("Insufficient rights", 403)
|
||||
|
||||
if command == "install":
|
||||
url = data["url"]
|
||||
plugin_name = data["plugin"] if "plugin" in data else None
|
||||
return self.command_install(url=url, force="force" in data and data["force"] in valid_boolean_trues, reinstall=plugin_name)
|
||||
|
||||
elif command == "uninstall":
|
||||
plugin_name = data["plugin"]
|
||||
if not plugin_name in self._plugin_manager.plugins:
|
||||
return make_response("Unknown plugin: %s" % plugin_name, 404)
|
||||
|
||||
plugin = self._plugin_manager.plugins[plugin_name]
|
||||
return self.command_uninstall(plugin)
|
||||
|
||||
elif command == "enable" or command == "disable":
|
||||
plugin_name = data["plugin"]
|
||||
if not plugin_name in self._plugin_manager.plugins:
|
||||
return make_response("Unknown plugin: %s" % plugin_name, 404)
|
||||
|
||||
plugin = self._plugin_manager.plugins[plugin_name]
|
||||
return self.command_toggle(plugin, command)
|
||||
|
||||
elif command == "refresh_repository":
|
||||
self._repository_available = self._refresh_repository()
|
||||
return jsonify(repository=dict(available=self._repository_available, plugins=self._repository_plugins))
|
||||
|
||||
def command_install(self, url=None, path=None, force=False, reinstall=None):
|
||||
if url is not None:
|
||||
pip_args = ["install", sarge.shell_quote(url)]
|
||||
elif path is not None:
|
||||
pip_args = ["install", path]
|
||||
else:
|
||||
raise ValueError("Either url or path must be provided")
|
||||
|
||||
all_plugins_before = self._plugin_manager.find_plugins()
|
||||
|
||||
try:
|
||||
returncode, stdout, stderr = self._call_pip(pip_args)
|
||||
except:
|
||||
self._logger.exception("Could not install plugin from %s" % url)
|
||||
return make_response("Could not install plugin from url, see the log for more details", 500)
|
||||
else:
|
||||
if force:
|
||||
pip_args += ["--ignore-installed", "--force-reinstall", "--no-deps"]
|
||||
try:
|
||||
returncode, stdout, stderr = self._call_pip(pip_args)
|
||||
except:
|
||||
self._logger.exception("Could not install plugin from %s" % url)
|
||||
return make_response("Could not install plugin from url, see the log for more details", 500)
|
||||
|
||||
try:
|
||||
last_line = filter(lambda x: x.strip() != "", stdout)[-1]
|
||||
except IndexError:
|
||||
result = dict(result=False, reason="Could not parse output from pip")
|
||||
self._send_result_notification("install", result)
|
||||
return jsonify(result)
|
||||
|
||||
# The last line of a pip install command looks something like this:
|
||||
#
|
||||
# Successfully installed OctoPrint-Plugin-1.0 Dependency-One-0.1 Dependency-Two-9.3
|
||||
#
|
||||
# So we'll first strip the first part, then split by whitespace and remove the version to get the name of
|
||||
# the installed package. By then matching up the package names stored for all our entry point plugins against
|
||||
# the list of installed packages from our pip install run, we'll find the plugin that was installed.
|
||||
|
||||
last_line = last_line.strip()
|
||||
if not last_line.startswith("Successfully installed "):
|
||||
result = dict(result=False, reason="Pip did not report successful installation")
|
||||
self._send_result_notification("install", result)
|
||||
return jsonify(result)
|
||||
|
||||
installed = map(lambda x: x.strip().rsplit("-", 1)[0], last_line[len("Successfully installed "):].split(" "))
|
||||
all_plugins_after = self._plugin_manager.find_plugins(existing=dict(), ignore_uninstalled=False)
|
||||
for key, plugin in all_plugins_after.items():
|
||||
if plugin.origin is None or plugin.origin[0] != "entry_point":
|
||||
continue
|
||||
|
||||
if plugin.origin[3] in installed:
|
||||
new_plugin_key = key
|
||||
new_plugin = plugin
|
||||
break
|
||||
else:
|
||||
return make_response("Could not find plugin that was installed", 500)
|
||||
|
||||
self._plugin_manager.mark_plugin(new_plugin_key, uninstalled=False)
|
||||
self._plugin_manager.reload_plugins()
|
||||
|
||||
needs_restart = self._plugin_manager.is_restart_needing_plugin(new_plugin) or new_plugin_key in all_plugins_before or reinstall is not None
|
||||
needs_refresh = new_plugin.implementation and isinstance(new_plugin.implementation, octoprint.plugin.ReloadNeedingPlugin)
|
||||
|
||||
self._plugin_manager.log_all_plugins()
|
||||
|
||||
result = dict(result=True, url=url, needs_restart=needs_restart, needs_refresh=needs_refresh, was_reinstalled=new_plugin_key in all_plugins_before or reinstall is not None, plugin=self._to_external_representation(new_plugin))
|
||||
self._send_result_notification("install", result)
|
||||
return jsonify(result)
|
||||
|
||||
def command_uninstall(self, plugin):
|
||||
if plugin.key == "pluginmanager":
|
||||
return make_response("Can't uninstall Plugin Manager", 400)
|
||||
|
||||
if plugin.bundled:
|
||||
return make_response("Bundled plugins cannot be uninstalled", 400)
|
||||
|
||||
if plugin.origin[0] == "entry_point":
|
||||
# plugin is installed through entry point, need to use pip to uninstall it
|
||||
origin = plugin.origin[3]
|
||||
if origin is None:
|
||||
origin = plugin.origin[2]
|
||||
|
||||
pip_args = ["uninstall", "--yes", origin]
|
||||
try:
|
||||
self._call_pip(pip_args)
|
||||
except:
|
||||
self._logger.exception(u"Could not uninstall plugin via pip")
|
||||
return make_response("Could not uninstall plugin via pip, see the log for more details", 500)
|
||||
|
||||
elif plugin.origin[0] == "folder":
|
||||
import os
|
||||
import shutil
|
||||
full_path = os.path.realpath(plugin.location)
|
||||
|
||||
if os.path.isdir(full_path):
|
||||
# plugin is installed via a plugin folder, need to use rmtree to get rid of it
|
||||
self._log_stdout(u"Deleting plugin from {folder}".format(folder=plugin.location))
|
||||
shutil.rmtree(full_path)
|
||||
elif os.path.isfile(full_path):
|
||||
self._log_stdout(u"Deleting plugin from {file}".format(file=plugin.location))
|
||||
os.remove(full_path)
|
||||
|
||||
if full_path.endswith(".py"):
|
||||
pyc_file = "{full_path}c".format(**locals())
|
||||
if os.path.isfile(pyc_file):
|
||||
os.remove(pyc_file)
|
||||
|
||||
needs_restart = self._plugin_manager.is_restart_needing_plugin(plugin)
|
||||
needs_refresh = plugin.implementation and isinstance(plugin.implementation, octoprint.plugin.ReloadNeedingPlugin)
|
||||
|
||||
self._plugin_manager.mark_plugin(plugin.key, uninstalled=True)
|
||||
|
||||
if not needs_restart:
|
||||
try:
|
||||
self._plugin_manager.disable_plugin(plugin.key, plugin=plugin)
|
||||
except octoprint.plugin.core.PluginLifecycleException as e:
|
||||
self._logger.exception(u"Problem disabling plugin {name}".format(name=plugin.key))
|
||||
result = dict(result=False, uninstalled=True, disabled=False, unloaded=False, reason=e.reason)
|
||||
self._send_result_notification("uninstall", result)
|
||||
return jsonify(result)
|
||||
|
||||
try:
|
||||
self._plugin_manager.unload_plugin(plugin.key)
|
||||
except octoprint.plugin.core.PluginLifecycleException as e:
|
||||
self._logger.exception(u"Problem unloading plugin {name}".format(name=plugin.key))
|
||||
result = dict(result=False, uninstalled=True, disabled=True, unloaded=False, reason=e.reason)
|
||||
self._send_result_notification("uninstall", result)
|
||||
return jsonify(result)
|
||||
|
||||
self._plugin_manager.reload_plugins()
|
||||
|
||||
result = dict(result=True, needs_restart=needs_restart, needs_refresh=needs_refresh, plugin=self._to_external_representation(plugin))
|
||||
self._send_result_notification("uninstall", result)
|
||||
return jsonify(result)
|
||||
|
||||
def command_toggle(self, plugin, command):
|
||||
if plugin.key == "pluginmanager":
|
||||
return make_response("Can't enable/disable Plugin Manager", 400)
|
||||
|
||||
needs_restart = self._plugin_manager.is_restart_needing_plugin(plugin)
|
||||
needs_refresh = plugin.implementation and isinstance(plugin.implementation, octoprint.plugin.ReloadNeedingPlugin)
|
||||
|
||||
pending = ((command == "disable" and plugin.key in self._pending_enable) or (command == "enable" and plugin.key in self._pending_disable))
|
||||
needs_restart_api = needs_restart and not pending
|
||||
needs_refresh_api = needs_refresh and not pending
|
||||
|
||||
try:
|
||||
if command == "disable":
|
||||
self._mark_plugin_disabled(plugin, needs_restart=needs_restart)
|
||||
elif command == "enable":
|
||||
self._mark_plugin_enabled(plugin, needs_restart=needs_restart)
|
||||
except octoprint.plugin.core.PluginLifecycleException as e:
|
||||
self._logger.exception(u"Problem toggling enabled state of {name}: {reason}".format(name=plugin.key, reason=e.reason))
|
||||
result = dict(result=False, reason=e.reason)
|
||||
except octoprint.plugin.core.PluginNeedsRestart:
|
||||
result = dict(result=True, needs_restart=True, needs_refresh=True, plugin=self._to_external_representation(plugin))
|
||||
else:
|
||||
result = dict(result=True, needs_restart=needs_restart_api, needs_refresh=needs_refresh_api, plugin=self._to_external_representation(plugin))
|
||||
|
||||
self._send_result_notification(command, result)
|
||||
return jsonify(result)
|
||||
|
||||
def _send_result_notification(self, action, result):
|
||||
notification = dict(type="result", action=action)
|
||||
notification.update(result)
|
||||
self._plugin_manager.send_plugin_message(self._identifier, notification)
|
||||
|
||||
def _call_pip(self, args):
|
||||
pip_command = self._settings.get(["pip"])
|
||||
if pip_command is None:
|
||||
import os
|
||||
python_command = sys.executable
|
||||
binary_dir = os.path.dirname(python_command)
|
||||
|
||||
pip_command = os.path.join(binary_dir, "pip")
|
||||
if sys.platform == "win32":
|
||||
# Windows is a bit special... first of all the file will be called pip.exe, not just pip, and secondly
|
||||
# for a non-virtualenv install (e.g. global install) the pip binary will not be located in the
|
||||
# same folder as python.exe, but in a subfolder Scripts, e.g.
|
||||
#
|
||||
# C:\Python2.7\
|
||||
# |- python.exe
|
||||
# `- Scripts
|
||||
# `- pip.exe
|
||||
|
||||
# virtual env?
|
||||
pip_command = os.path.join(binary_dir, "pip.exe")
|
||||
|
||||
if not os.path.isfile(pip_command):
|
||||
# nope, let's try the Scripts folder then
|
||||
scripts_dir = os.path.join(binary_dir, "Scripts")
|
||||
if os.path.isdir(scripts_dir):
|
||||
pip_command = os.path.join(scripts_dir, "pip.exe")
|
||||
|
||||
if not os.path.isfile(pip_command) or not os.access(pip_command, os.X_OK):
|
||||
raise RuntimeError(u"No pip path configured and {pip_command} does not exist or is not executable, can't install".format(**locals()))
|
||||
|
||||
command = [pip_command] + args
|
||||
|
||||
self._logger.debug(u"Calling: {}".format(" ".join(command)))
|
||||
|
||||
p = sarge.run(" ".join(command), shell=True, async=True, stdout=sarge.Capture(), stderr=sarge.Capture())
|
||||
p.wait_events()
|
||||
|
||||
all_stdout = []
|
||||
all_stderr = []
|
||||
try:
|
||||
while p.returncode is None:
|
||||
line = p.stderr.readline(timeout=0.5)
|
||||
if line:
|
||||
self._log_stderr(line)
|
||||
all_stderr.append(line)
|
||||
|
||||
line = p.stdout.readline(timeout=0.5)
|
||||
if line:
|
||||
self._log_stdout(line)
|
||||
all_stdout.append(line)
|
||||
|
||||
p.commands[0].poll()
|
||||
|
||||
finally:
|
||||
p.close()
|
||||
|
||||
stderr = p.stderr.text
|
||||
if stderr:
|
||||
split_lines = stderr.split("\n")
|
||||
self._log_stderr(*split_lines)
|
||||
all_stderr += split_lines
|
||||
|
||||
stdout = p.stdout.text
|
||||
if stdout:
|
||||
split_lines = stdout.split("\n")
|
||||
self._log_stdout(*split_lines)
|
||||
all_stdout += split_lines
|
||||
|
||||
return p.returncode, all_stdout, all_stderr
|
||||
|
||||
def _log_stdout(self, *lines):
|
||||
self._log(lines, prefix=">", stream="stdout")
|
||||
|
||||
def _log_stderr(self, *lines):
|
||||
self._log(lines, prefix="!", stream="stderr")
|
||||
|
||||
def _log(self, lines, prefix=None, stream=None, strip=True):
|
||||
if strip:
|
||||
lines = map(lambda x: x.strip(), lines)
|
||||
|
||||
self._plugin_manager.send_plugin_message(self._identifier, dict(type="loglines", loglines=[dict(line=line, stream=stream) for line in lines]))
|
||||
for line in lines:
|
||||
self._console_logger.debug(u"{prefix} {line}".format(**locals()))
|
||||
|
||||
def _mark_plugin_enabled(self, plugin, needs_restart=False):
|
||||
disabled_list = list(self._settings.global_get(["plugins", "_disabled"]))
|
||||
if plugin.key in disabled_list:
|
||||
disabled_list.remove(plugin.key)
|
||||
self._settings.global_set(["plugins", "_disabled"], disabled_list)
|
||||
self._settings.save(force=True)
|
||||
|
||||
if not needs_restart:
|
||||
self._plugin_manager.enable_plugin(plugin.key)
|
||||
else:
|
||||
if plugin.key in self._pending_disable:
|
||||
self._pending_disable.remove(plugin.key)
|
||||
elif not plugin.enabled and plugin.key not in self._pending_enable:
|
||||
self._pending_enable.add(plugin.key)
|
||||
|
||||
def _mark_plugin_disabled(self, plugin, needs_restart=False):
|
||||
disabled_list = list(self._settings.global_get(["plugins", "_disabled"]))
|
||||
if not plugin.key in disabled_list:
|
||||
disabled_list.append(plugin.key)
|
||||
self._settings.global_set(["plugins", "_disabled"], disabled_list)
|
||||
self._settings.save(force=True)
|
||||
|
||||
if not needs_restart:
|
||||
self._plugin_manager.disable_plugin(plugin.key)
|
||||
else:
|
||||
if plugin.key in self._pending_enable:
|
||||
self._pending_enable.remove(plugin.key)
|
||||
elif plugin.enabled and plugin.key not in self._pending_disable:
|
||||
self._pending_disable.add(plugin.key)
|
||||
|
||||
def _refresh_repository(self):
|
||||
import requests
|
||||
repository_url = self._settings.get(["repository"])
|
||||
try:
|
||||
r = requests.get(repository_url)
|
||||
except Exception as e:
|
||||
self._logger.warn("Could not fetch plugins from repository at {repository_url}: {message}".format(repository_url=repository_url, message=str(e)))
|
||||
return False
|
||||
|
||||
current_os = self._get_os()
|
||||
octoprint_version = self._get_octoprint_version()
|
||||
if "-" in octoprint_version:
|
||||
octoprint_version = octoprint_version[:octoprint_version.find("-")]
|
||||
|
||||
def map_repository_entry(entry):
|
||||
result = dict(entry)
|
||||
result["is_compatible"] = dict(
|
||||
octoprint=True,
|
||||
os=True
|
||||
)
|
||||
|
||||
if "compatibility" in entry:
|
||||
if "octoprint" in entry["compatibility"]:
|
||||
import semantic_version
|
||||
for octo_compat in entry["compatibility"]["octoprint"]:
|
||||
s = semantic_version.Spec("=={}".format(octo_compat))
|
||||
if semantic_version.Version(octoprint_version) in s:
|
||||
break
|
||||
else:
|
||||
result["is_compatible"]["octoprint"] = False
|
||||
|
||||
if "os" in entry["compatibility"]:
|
||||
result["is_compatible"]["os"] = current_os in entry["compatibility"]["os"]
|
||||
|
||||
return result
|
||||
|
||||
self._repository_plugins = map(map_repository_entry, r.json())
|
||||
return True
|
||||
|
||||
def _get_os(self):
|
||||
if sys.platform == "win32":
|
||||
return "windows"
|
||||
elif sys.platform == "linux2":
|
||||
return "linux"
|
||||
elif sys.platform == "darwin":
|
||||
return "macos"
|
||||
else:
|
||||
return "unknown"
|
||||
|
||||
def _get_octoprint_version(self):
|
||||
from octoprint._version import get_versions
|
||||
return get_versions()["version"]
|
||||
|
||||
def _to_external_representation(self, plugin):
|
||||
return dict(
|
||||
key=plugin.key,
|
||||
name=plugin.name,
|
||||
description=plugin.description,
|
||||
author=plugin.author,
|
||||
version=plugin.version,
|
||||
url=plugin.url,
|
||||
license=plugin.license,
|
||||
bundled=plugin.bundled,
|
||||
enabled=plugin.enabled,
|
||||
pending_enable=(not plugin.enabled and plugin.key in self._pending_enable),
|
||||
pending_disable=(plugin.enabled and plugin.key in self._pending_disable),
|
||||
pending_install=(plugin.key in self._pending_install),
|
||||
pending_uninstall=(plugin.key in self._pending_uninstall)
|
||||
)
|
||||
|
||||
__plugin_name__ = "Plugin Manager"
|
||||
__plugin_author__ = "Gina Häußge"
|
||||
__plugin_url__ = "https://github.com/foosel/OctoPrint/wiki/Plugin:-Plugin-Manager"
|
||||
__plugin_description__ = "Allows installing and managing OctoPrint plugins"
|
||||
__plugin_license__ = "AGPLv3"
|
||||
__plugin_implementation__ = PluginManagerPlugin()
|
||||
|
|
@ -0,0 +1 @@
|
|||
table th.settings_plugin_plugin_manager_plugins_name,table td.settings_plugin_plugin_manager_plugins_name{text-overflow:ellipsis;text-align:left}table th.settings_plugin_plugin_manager_plugins_actions,table td.settings_plugin_plugin_manager_plugins_actions{text-align:center;width:80px}table th.settings_plugin_plugin_manager_plugins_actions a,table td.settings_plugin_plugin_manager_plugins_actions a{text-decoration:none;color:#000}table th.settings_plugin_plugin_manager_plugins_actions a.disabled,table td.settings_plugin_plugin_manager_plugins_actions a.disabled{color:#ccc;cursor:default}#settings_plugin_pluginmanager_repositorydialog .slimScrollDiv{margin-bottom:20px}#settings_plugin_pluginmanager_repositorydialog h4{position:relative}#settings_plugin_pluginmanager_repositorydialog h4 a.dropdown-toggle{color:inherit;text-decoration:none}#settings_plugin_pluginmanager_repositorydialog .form-search{text-align:center;margin-bottom:5px!important}#settings_plugin_pluginmanager_repositorydialog .form-inline{padding:5px;padding-right:10px;margin-bottom:0}#settings_plugin_pluginmanager_repositorydialog .form-inline .help-block{margin-bottom:0;font-size:85%}#settings_plugin_pluginmanager_repositorydialog #settings_plugin_pluginmanager_repositorydialog_unavailable{overflow:hidden;width:100%;height:300px;background-image:url("../img/repo_unavailable.png");text-align:center;display:table}#settings_plugin_pluginmanager_repositorydialog #settings_plugin_pluginmanager_repositorydialog_unavailable div{display:table-cell;vertical-align:middle}#settings_plugin_pluginmanager_repositorydialog #settings_plugin_pluginmanager_repositorydialog_list{overflow:hidden;width:auto;height:300px}#settings_plugin_pluginmanager_repositorydialog #settings_plugin_pluginmanager_repositorydialog_list .entry{border-bottom:1px solid #ddd;padding:5px;padding-right:10px}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
573
src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js
Normal file
573
src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js
Normal file
|
|
@ -0,0 +1,573 @@
|
|||
$(function() {
|
||||
function PluginManagerViewModel(parameters) {
|
||||
var self = this;
|
||||
|
||||
self.loginState = parameters[0];
|
||||
self.settingsViewModel = parameters[1];
|
||||
|
||||
self.plugins = new ItemListHelper(
|
||||
"plugin.pluginmanager.installedplugins",
|
||||
{
|
||||
"name": function (a, b) {
|
||||
// sorts ascending
|
||||
if (a["name"].toLocaleLowerCase() < b["name"].toLocaleLowerCase()) return -1;
|
||||
if (a["name"].toLocaleLowerCase() > b["name"].toLocaleLowerCase()) return 1;
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
{
|
||||
},
|
||||
"name",
|
||||
[],
|
||||
[],
|
||||
5
|
||||
);
|
||||
|
||||
self.repositoryplugins = new ItemListHelper(
|
||||
"plugin.pluginmanager.repositoryplugins",
|
||||
{
|
||||
"title": function (a, b) {
|
||||
// sorts ascending
|
||||
if (a["title"].toLocaleLowerCase() < b["title"].toLocaleLowerCase()) return -1;
|
||||
if (a["title"].toLocaleLowerCase() > b["title"].toLocaleLowerCase()) return 1;
|
||||
return 0;
|
||||
},
|
||||
"published": function (a, b) {
|
||||
// sorts descending
|
||||
if (a["published"].toLocaleLowerCase() > b["published"].toLocaleLowerCase()) return -1;
|
||||
if (a["published"].toLocaleLowerCase() < b["published"].toLocaleLowerCase()) return 1;
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
{
|
||||
"filter_installed": function(plugin) {
|
||||
return !self.installed(plugin);
|
||||
},
|
||||
"filter_incompatible": function(plugin) {
|
||||
return plugin.is_compatible.octoprint && plugin.is_compatible.os;
|
||||
}
|
||||
},
|
||||
"title",
|
||||
["filter_installed", "filter_incompatible"],
|
||||
[],
|
||||
0
|
||||
);
|
||||
|
||||
self.uploadElement = $("#settings_plugin_pluginmanager_repositorydialog_upload");
|
||||
self.uploadButton = $("#settings_plugin_pluginmanager_repositorydialog_upload_start");
|
||||
|
||||
self.repositoryAvailable = ko.observable(false);
|
||||
|
||||
self.repositorySearchQuery = ko.observable();
|
||||
self.repositorySearchQuery.subscribe(function() {
|
||||
self.performRepositorySearch();
|
||||
});
|
||||
|
||||
self.installUrl = ko.observable();
|
||||
self.uploadFilename = ko.observable();
|
||||
|
||||
self.loglines = ko.observableArray([]);
|
||||
self.installedPlugins = ko.observableArray([]);
|
||||
|
||||
self.working = ko.observable(false);
|
||||
self.workingTitle = ko.observable();
|
||||
self.workingDialog = undefined;
|
||||
self.workingOutput = undefined;
|
||||
|
||||
self.invalidUrl = ko.computed(function() {
|
||||
var url = self.installUrl();
|
||||
return url !== undefined && url.trim() != "" && !(_.startsWith(url.toLocaleLowerCase(), "http://") || _.startsWith(url.toLocaleLowerCase(), "https://"));
|
||||
});
|
||||
|
||||
self.enableUrlInstall = ko.computed(function() {
|
||||
var url = self.installUrl();
|
||||
return url !== undefined && url.trim() != "" && !self.invalidUrl();
|
||||
});
|
||||
|
||||
self.invalidArchive = ko.computed(function() {
|
||||
var name = self.uploadFilename();
|
||||
return name !== undefined && !(_.endsWith(name.toLocaleLowerCase(), ".zip") || _.endsWith(name.toLocaleLowerCase(), ".tar.gz") || _.endsWith(name.toLocaleLowerCase(), ".tgz") || _.endsWith(name.toLocaleLowerCase(), ".tar"));
|
||||
});
|
||||
|
||||
self.enableArchiveInstall = ko.computed(function() {
|
||||
var name = self.uploadFilename();
|
||||
return name !== undefined && name.trim() != "" && !self.invalidArchive();
|
||||
});
|
||||
|
||||
self.uploadElement.fileupload({
|
||||
dataType: "json",
|
||||
maxNumberOfFiles: 1,
|
||||
autoUpload: false,
|
||||
add: function(e, data) {
|
||||
if (data.files.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.uploadFilename(data.files[0].name);
|
||||
|
||||
self.uploadButton.unbind("click");
|
||||
self.uploadButton.bind("click", function() {
|
||||
self._markWorking(gettext("Installing plugin..."), gettext("Installing plugin from uploaded archive..."));
|
||||
data.submit();
|
||||
return false;
|
||||
});
|
||||
},
|
||||
done: function(e, data) {
|
||||
self._markDone();
|
||||
self.uploadButton.unbind("click");
|
||||
self.uploadFilename("");
|
||||
},
|
||||
fail: function(e, data) {
|
||||
new PNotify({
|
||||
title: gettext("Something went wrong"),
|
||||
text: gettext("Please consult octoprint.log for details"),
|
||||
type: "error",
|
||||
hide: false
|
||||
});
|
||||
self._markDone();
|
||||
self.uploadButton.unbind("click");
|
||||
self.uploadFilename("");
|
||||
}
|
||||
});
|
||||
|
||||
self.performRepositorySearch = function() {
|
||||
var query = self.repositorySearchQuery();
|
||||
if (query !== undefined && query.trim() != "") {
|
||||
self.repositoryplugins.changeSearchFunction(function(entry) {
|
||||
return entry && (entry["title"].toLocaleLowerCase().indexOf(query) > -1 || entry["description"].toLocaleLowerCase().indexOf(query) > -1);
|
||||
});
|
||||
} else {
|
||||
self.repositoryplugins.resetSearch();
|
||||
}
|
||||
};
|
||||
|
||||
self.fromResponse = function(data) {
|
||||
self._fromPluginsResponse(data.plugins);
|
||||
self._fromRepositoryResponse(data.repository)
|
||||
};
|
||||
|
||||
self._fromPluginsResponse = function(data) {
|
||||
var installedPlugins = [];
|
||||
_.each(data, function(plugin) {
|
||||
installedPlugins.push(plugin.key);
|
||||
});
|
||||
self.installedPlugins(installedPlugins);
|
||||
self.plugins.updateItems(data);
|
||||
};
|
||||
|
||||
self._fromRepositoryResponse = function(data) {
|
||||
self.repositoryAvailable(data.available);
|
||||
if (data.available) {
|
||||
self.repositoryplugins.updateItems(data.plugins);
|
||||
} else {
|
||||
self.repositoryplugins.updateItems([]);
|
||||
}
|
||||
};
|
||||
|
||||
self.requestData = function(includeRepo) {
|
||||
if (!self.loginState.isAdmin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: API_BASEURL + "plugin/pluginmanager" + ((includeRepo) ? "?refresh_repository=true" : ""),
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: self.fromResponse
|
||||
});
|
||||
};
|
||||
|
||||
self.togglePlugin = function(data) {
|
||||
if (!self.loginState.isAdmin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.key == "pluginmanager") return;
|
||||
|
||||
var command = self._getToggleCommand(data);
|
||||
|
||||
var payload = {plugin: data.key};
|
||||
self._postCommand(command, payload, function(response) {
|
||||
self.requestData();
|
||||
}, function() {
|
||||
new PNotify({
|
||||
title: gettext("Something went wrong"),
|
||||
text: gettext("Please consult octoprint.log for details"),
|
||||
type: "error",
|
||||
hide: false
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
self.showRepository = function() {
|
||||
self.repositoryDialog.modal("show");
|
||||
};
|
||||
|
||||
self.pluginDetails = function(data) {
|
||||
window.open(data.page);
|
||||
};
|
||||
|
||||
self.installFromRepository = function(data) {
|
||||
if (!self.loginState.isAdmin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.installed(data)) {
|
||||
self.installPlugin(data.archive, data.title, data.id);
|
||||
} else {
|
||||
self.installPlugin(data.archive, data.title);
|
||||
}
|
||||
};
|
||||
|
||||
self.installPlugin = function(url, name, reinstall) {
|
||||
if (!self.loginState.isAdmin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (url === undefined) {
|
||||
url = self.installUrl();
|
||||
}
|
||||
if (!url) return;
|
||||
|
||||
var workTitle, workText;
|
||||
if (!reinstall) {
|
||||
workTitle = gettext("Installing plugin...");
|
||||
if (name) {
|
||||
workText = _.sprintf(gettext("Installing plugin \"%(name)s\" from %(url)s..."), {url: url, name: name});
|
||||
} else {
|
||||
workText = _.sprintf(gettext("Installing plugin from %(url)s..."), {url: url});
|
||||
}
|
||||
} else {
|
||||
workTitle = gettext("Reinstalling plugin...");
|
||||
workText = _.sprintf(gettext("Reinstalling plugin \"%(name)s\" from %(url)s..."), {url: url, name: name});
|
||||
}
|
||||
self._markWorking(workTitle, workText);
|
||||
|
||||
var command = "install";
|
||||
var payload = {url: url};
|
||||
if (reinstall) {
|
||||
payload["plugin"] = reinstall;
|
||||
payload["force"] = true;
|
||||
}
|
||||
|
||||
self._postCommand(command, payload, function(response) {
|
||||
self.requestData();
|
||||
self._markDone();
|
||||
self.installUrl("");
|
||||
}, function() {
|
||||
new PNotify({
|
||||
title: gettext("Something went wrong"),
|
||||
text: gettext("Please consult octoprint.log for details"),
|
||||
type: "error",
|
||||
hide: false
|
||||
});
|
||||
self._markDone();
|
||||
});
|
||||
};
|
||||
|
||||
self.uninstallPlugin = function(data) {
|
||||
if (!self.loginState.isAdmin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.bundled) return;
|
||||
if (data.key == "pluginmanager") return;
|
||||
|
||||
self._markWorking(gettext("Uninstalling plugin..."), _.sprintf(gettext("Uninstalling plugin \"%(name)s\""), {name: data.name}));
|
||||
|
||||
var command = "uninstall";
|
||||
var payload = {plugin: data.key};
|
||||
self._postCommand(command, payload, function(response) {
|
||||
self.requestData();
|
||||
self._markDone();
|
||||
}, function() {
|
||||
new PNotify({
|
||||
title: gettext("Something went wrong"),
|
||||
text: gettext("Please consult octoprint.log for details"),
|
||||
type: "error",
|
||||
hide: false
|
||||
});
|
||||
self._markDone();
|
||||
});
|
||||
};
|
||||
|
||||
self.refreshRepository = function() {
|
||||
if (!self.loginState.isAdmin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
self._postCommand("refresh_repository", {}, function(data) {
|
||||
self._fromRepositoryResponse(data.repository);
|
||||
})
|
||||
};
|
||||
|
||||
self.installed = function(data) {
|
||||
return _.includes(self.installedPlugins(), data.id);
|
||||
};
|
||||
|
||||
self.isCompatible = function(data) {
|
||||
return data.is_compatible.octoprint && data.is_compatible.os;
|
||||
};
|
||||
|
||||
self.installButtonText = function(data) {
|
||||
return self.isCompatible(data) ? (self.installed(data) ? gettext("Reinstall") : gettext("Install")) : gettext("Incompatible");
|
||||
};
|
||||
|
||||
self._displayNotification = function(response, titleSuccess, textSuccess, textRestart, textReload, titleError, textError) {
|
||||
if (response.result) {
|
||||
if (response.needs_restart) {
|
||||
new PNotify({
|
||||
title: titleSuccess,
|
||||
text: textRestart,
|
||||
hide: false
|
||||
});
|
||||
} else if (response.needs_refresh) {
|
||||
new PNotify({
|
||||
title: titleSuccess,
|
||||
text: textReload,
|
||||
confirm: {
|
||||
confirm: true,
|
||||
buttons: [{
|
||||
text: gettext("Reload now"),
|
||||
click: function () {
|
||||
location.reload(true);
|
||||
}
|
||||
}]
|
||||
},
|
||||
buttons: {
|
||||
closer: false,
|
||||
sticker: false
|
||||
},
|
||||
hide: false
|
||||
})
|
||||
} else {
|
||||
new PNotify({
|
||||
title: titleSuccess,
|
||||
text: textSuccess,
|
||||
type: "success",
|
||||
hide: false
|
||||
})
|
||||
}
|
||||
} else {
|
||||
new PNotify({
|
||||
title: titleError,
|
||||
text: textError,
|
||||
type: "error",
|
||||
hide: false
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
self._postCommand = function (command, data, successCallback, failureCallback, alwaysCallback, timeout) {
|
||||
var payload = _.extend(data, {command: command});
|
||||
|
||||
var params = {
|
||||
url: API_BASEURL + "plugin/pluginmanager",
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
data: JSON.stringify(payload),
|
||||
contentType: "application/json; charset=UTF-8",
|
||||
success: function(response) {
|
||||
if (successCallback) successCallback(response);
|
||||
},
|
||||
error: function() {
|
||||
if (failureCallback) failureCallback();
|
||||
},
|
||||
complete: function() {
|
||||
if (alwaysCallback) alwaysCallback();
|
||||
}
|
||||
};
|
||||
|
||||
if (timeout != undefined) {
|
||||
params.timeout = timeout;
|
||||
}
|
||||
|
||||
$.ajax(params);
|
||||
};
|
||||
|
||||
self._markWorking = function(title, line) {
|
||||
self.working(true);
|
||||
self.workingTitle(title);
|
||||
|
||||
self.loglines.removeAll();
|
||||
self.loglines.push({line: line, stream: "message"});
|
||||
|
||||
self.workingDialog.modal("show");
|
||||
};
|
||||
|
||||
self._markDone = function() {
|
||||
self.working(false);
|
||||
self.loglines.push({line: gettext("Done!"), stream: "message"});
|
||||
self._scrollWorkingOutputToEnd();
|
||||
};
|
||||
|
||||
self._scrollWorkingOutputToEnd = function() {
|
||||
self.workingOutput.scrollTop(self.workingOutput[0].scrollHeight - self.workingOutput.height());
|
||||
};
|
||||
|
||||
self._getToggleCommand = function(data) {
|
||||
return ((!data.enabled || data.pending_disable) && !data.pending_enable) ? "enable" : "disable";
|
||||
};
|
||||
|
||||
self.toggleButtonCss = function(data) {
|
||||
var icon = self._getToggleCommand(data) == "enable" ? "icon-circle-blank" : "icon-circle";
|
||||
var disabled = (data.key == "pluginmanager") ? " disabled" : "";
|
||||
|
||||
return icon + disabled;
|
||||
};
|
||||
|
||||
self.toggleButtonTitle = function(data) {
|
||||
return self._getToggleCommand(data) == "enable" ? gettext("Enable Plugin") : gettext("Disable Plugin");
|
||||
};
|
||||
|
||||
self.onBeforeBinding = function() {
|
||||
self.settings = self.settingsViewModel.settings;
|
||||
};
|
||||
|
||||
self.onUserLoggedIn = function(user) {
|
||||
if (user.admin) {
|
||||
self.requestData();
|
||||
}
|
||||
};
|
||||
|
||||
self.onStartup = function() {
|
||||
self.workingDialog = $("#settings_plugin_pluginmanager_workingdialog");
|
||||
self.workingOutput = $("#settings_plugin_pluginmanager_workingdialog_output");
|
||||
self.repositoryDialog = $("#settings_plugin_pluginmanager_repositorydialog");
|
||||
|
||||
$("#settings_plugin_pluginmanager_repositorydialog_list").slimScroll({
|
||||
height: "306px",
|
||||
size: "5px",
|
||||
distance: "0",
|
||||
railVisible: true,
|
||||
alwaysVisible: true,
|
||||
scrollBy: "102px"
|
||||
});
|
||||
};
|
||||
|
||||
self.onDataUpdaterPluginMessage = function(plugin, data) {
|
||||
if (plugin != "pluginmanager") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!self.loginState.isAdmin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.hasOwnProperty("type")) {
|
||||
return;
|
||||
}
|
||||
|
||||
var messageType = data.type;
|
||||
|
||||
if (messageType == "loglines" && self.working()) {
|
||||
_.each(data.loglines, function(line) {
|
||||
self.loglines.push(line);
|
||||
});
|
||||
self._scrollWorkingOutputToEnd();
|
||||
} else if (messageType == "result") {
|
||||
var titleSuccess, textSuccess, textRestart, textReload, titleError, textError;
|
||||
var action = data.action;
|
||||
|
||||
var name = "Unknown";
|
||||
if (action == "install") {
|
||||
if (data.hasOwnProperty("plugin")) {
|
||||
name = data.plugin.name;
|
||||
}
|
||||
|
||||
if (data.was_reinstalled) {
|
||||
titleSuccess = _.sprintf(gettext("Plugin \"%(name)s\" reinstalled"), {name: name});
|
||||
textSuccess = gettext("The plugin was reinstalled successfully");
|
||||
textRestart = gettext("The plugin was reinstalled successfully, however a restart of OctoPrint is needed for that to take effect.");
|
||||
textReload = gettext("The plugin was reinstalled successfully, however a reload of the page is needed for that to take effect.");
|
||||
} else {
|
||||
titleSuccess = _.sprintf(gettext("Plugin \"%(name)s\" installed"), {name: name});
|
||||
textSuccess = gettext("The plugin was installed successfully");
|
||||
textRestart = gettext("The plugin was installed successfully, however a restart of OctoPrint is needed for that to take effect.");
|
||||
textReload = gettext("The plugin was installed successfully, however a reload of the page is needed for that to take effect.");
|
||||
}
|
||||
|
||||
titleError = gettext("Something went wrong");
|
||||
var url = "unknown";
|
||||
if (data.hasOwnProperty("url")) {
|
||||
url = data.url;
|
||||
}
|
||||
|
||||
if (data.hasOwnProperty("reason")) {
|
||||
if (data.was_reinstalled) {
|
||||
textError = _.sprintf(gettext("Reinstalling the plugin from URL \"%(url)s\" failed: %(reason)s"), {reason: data.reason, url: url});
|
||||
} else {
|
||||
textError = _.sprintf(gettext("Installing the plugin from URL \"%(url)s\" failed: %(reason)s"), {reason: data.reason, url: url});
|
||||
}
|
||||
} else {
|
||||
if (data.was_reinstalled) {
|
||||
textError = _.sprintf(gettext("Reinstalling the plugin from URL \"%(url)s\" failed, please see the log for details."), {url: url});
|
||||
} else {
|
||||
textError = _.sprintf(gettext("Installing the plugin from URL \"%(url)s\" failed, please see the log for details."), {url: url});
|
||||
}
|
||||
}
|
||||
|
||||
} else if (action == "uninstall") {
|
||||
if (data.hasOwnProperty("plugin")) {
|
||||
name = data.plugin.name;
|
||||
}
|
||||
|
||||
titleSuccess = _.sprintf(gettext("Plugin \"%(name)s\" uninstalled"), {name: name});
|
||||
textSuccess = gettext("The plugin was uninstalled successfully");
|
||||
textRestart = gettext("The plugin was uninstalled successfully, however a restart of OctoPrint is needed for that to take effect.");
|
||||
textReload = gettext("The plugin was uninstalled successfully, however a reload of the page is needed for that to take effect.");
|
||||
|
||||
titleError = gettext("Something went wrong");
|
||||
if (data.hasOwnProperty("reason")) {
|
||||
textError = _.sprintf(gettext("Uninstalling the plugin failed: %(reason)s"), {reason: data.reason});
|
||||
} else {
|
||||
textError = gettext("Uninstalling the plugin failed, please see the log for details.");
|
||||
}
|
||||
|
||||
} else if (action == "enable") {
|
||||
if (data.hasOwnProperty("plugin")) {
|
||||
name = data.plugin.name;
|
||||
}
|
||||
|
||||
titleSuccess = _.sprintf(gettext("Plugin \"%(name)s\" enabled"), {name: name});
|
||||
textSuccess = gettext("The plugin was enabled successfully.");
|
||||
textRestart = gettext("The plugin was enabled successfully, however a restart of OctoPrint is needed for that to take effect.");
|
||||
textReload = gettext("The plugin was enabled successfully, however a reload of the page is needed for that to take effect.");
|
||||
|
||||
titleError = gettext("Something went wrong");
|
||||
if (data.hasOwnProperty("reason")) {
|
||||
textError = _.sprintf(gettext("Toggling the plugin failed: %(reason)s"), {reason: data.reason});
|
||||
} else {
|
||||
textError = gettext("Toggling the plugin failed, please see the log for details.");
|
||||
}
|
||||
|
||||
} else if (action == "disable") {
|
||||
if (data.hasOwnProperty("plugin")) {
|
||||
name = data.plugin.name;
|
||||
}
|
||||
|
||||
titleSuccess = _.sprintf(gettext("Plugin \"%(name)s\" disabled"), {name: name});
|
||||
textSuccess = gettext("The plugin was disabled successfully.");
|
||||
textRestart = gettext("The plugin was disabled successfully, however a restart of OctoPrint is needed for that to take effect.");
|
||||
textReload = gettext("The plugin was disabled successfully, however a reload of the page is needed for that to take effect.");
|
||||
|
||||
titleError = gettext("Something went wrong");
|
||||
if (data.hasOwnProperty("reason")) {
|
||||
textError = _.sprintf(gettext("Toggling the plugin failed: %(reason)s"), {reason: data.reason});
|
||||
} else {
|
||||
textError = gettext("Toggling the plugin failed, please see the log for details.");
|
||||
}
|
||||
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
self._displayNotification(data, titleSuccess, textSuccess, textRestart, textReload, titleError, textError);
|
||||
self.requestData();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// view model class, parameters for constructor, container to bind to
|
||||
ADDITIONAL_VIEWMODELS.push([PluginManagerViewModel, ["loginStateViewModel", "settingsViewModel"], "#settings_plugin_pluginmanager"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
table {
|
||||
th, td {
|
||||
&.settings_plugin_plugin_manager_plugins_name {
|
||||
text-overflow: ellipsis;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&.settings_plugin_plugin_manager_plugins_actions {
|
||||
text-align: center;
|
||||
width: 80px;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #000;
|
||||
|
||||
&.disabled {
|
||||
color: #ccc;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#settings_plugin_pluginmanager_repositorydialog {
|
||||
.slimScrollDiv {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
position: relative;
|
||||
|
||||
a.dropdown-toggle {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.form-search {
|
||||
text-align: center;
|
||||
margin-bottom: 5px !important;
|
||||
}
|
||||
|
||||
.form-inline {
|
||||
padding: 5px;
|
||||
padding-right: 10px;
|
||||
margin-bottom: 0;
|
||||
|
||||
.help-block {
|
||||
margin-bottom: 0;
|
||||
font-size: 85%;
|
||||
}
|
||||
}
|
||||
|
||||
#settings_plugin_pluginmanager_repositorydialog_unavailable {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
background-image: url("../img/repo_unavailable.png");
|
||||
text-align: center;
|
||||
display: table;
|
||||
|
||||
div {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
#settings_plugin_pluginmanager_repositorydialog_list {
|
||||
overflow: hidden;
|
||||
width: auto;
|
||||
height: 300px;
|
||||
|
||||
.entry {
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 5px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
<h3>{{ _('Installed Plugins') }}</h3>
|
||||
|
||||
<table class="table table-striped table-hover table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="settings_plugin_plugin_manager_plugins_name">{{ _('Name') }}</th>
|
||||
<th class="settings_plugin_plugin_manager_plugins_actions">{{ _('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-bind="foreach: plugins.paginatedItems">
|
||||
<tr>
|
||||
<td class="settings_plugin_plugin_manager_plugins_name">
|
||||
<div data-bind="css: {muted: !enabled}"><span data-bind="text: name"></span> <span data-bind="visible: version">(<span data-bind="text: version"></span>)</span> <i title="{{ _('Bundled') }}" class="icon-th-large" data-bind="visible: bundled"></i> <i title="Restart needed" class="icon-refresh" data-bind="visible: pending_enable || pending_disable || pending_uninstall"></i> <i title="Uninstalled" class="icon-remove" data-bind="visible: pending_uninstall"></i></div>
|
||||
<div><small class="muted" data-bind="text: description"> </small></div>
|
||||
<div data-bind="css: {muted: !enabled}">
|
||||
<small data-bind="visible: url"><i class="icon-home"></i> <a data-bind="attr: {href: url}">{{ _('Homepage') }}</a></small>
|
||||
<small data-bind="visible: license"><i class="icon-legal"></i> <span data-bind="text: license"></span></small>
|
||||
<small data-bind="visible: author"><i class="icon-user"></i> <span data-bind="text: author"></span></small>
|
||||
<small> </small>
|
||||
</div>
|
||||
</td>
|
||||
<td class="settings_plugin_plugin_manager_plugins_actions">
|
||||
<a href="#" data-bind="css: $root.toggleButtonCss($data), attr: {title: $root.toggleButtonTitle($data)}, enable: key != 'pluginmanager', click: function() { $root.togglePlugin($data) }"></a> | <a href="#" class="icon-trash" title="{{ _('Uninstall Plugin') }}" data-bind="css: {disabled: bundled || key == 'pluginmanager' || pending_uninstall}, enable: !bundled && key != 'pluginmanager' && !pending_uninstall, click: function() { $root.uninstallPlugin($data) }"></a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pagination pagination-mini pagination-centered">
|
||||
<ul>
|
||||
<li data-bind="css: {disabled: plugins.currentPage() === 0}"><a href="#" data-bind="click: plugins.prevPage">«</a></li>
|
||||
</ul>
|
||||
<ul data-bind="foreach: plugins.pages">
|
||||
<li data-bind="css: { active: $data.number === $root.plugins.currentPage(), disabled: $data.number === -1 }"><a href="#" data-bind="text: $data.text, click: function() { $root.plugins.changePage($data.number); }"></a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li data-bind="css: {disabled: plugins.currentPage() === plugins.lastPage()}"><a href="#" data-bind="click: plugins.nextPage">»</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-block" data-bind="click: $root.showRepository">{{ _('Get More...') }}</button>
|
||||
|
||||
<div id="settings_plugin_pluginmanager_workingdialog" class="modal hide fade">
|
||||
<div class="modal-header">
|
||||
<a href="#" class="close" data-dismiss="modal" aria-hidden="true">×</a>
|
||||
<h3 data-bind="text: workingTitle"></h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pre id="settings_plugin_pluginmanager_workingdialog_output" class="terminal pre-scrollable" style="height: 170px" data-bind="foreach: loglines"><span data-bind="text: line, css: {stdout: stream == 'stdout', stderr: stream == 'stderr'}"></span><br></pre>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" data-dismiss="modal" data-bind="enable: !$root.working()" aria-hidden="true">{{ _('Close') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="settings_plugin_pluginmanager_repositorydialog" class="modal hide fade">
|
||||
<div class="modal-header">
|
||||
<a href="#" class="close" data-dismiss="modal" aria-hidden="true">×</a>
|
||||
<h3>{{ _('Install new Plugins...') }}</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h4 style="position: relative">
|
||||
{{ _('... from the <a href="%(url)s" target="_blank">Plugin Repository</a>', url='http://plugins.octoprint.org') }}
|
||||
<a class="dropdown-toggle pull-right" data-toggle="dropdown" href="#">
|
||||
<span class="icon-wrench"></span>
|
||||
<ul class="dropdown-menu pull-right">
|
||||
<li><a href="#" data-bind="click: function() { repositoryplugins.changeSorting('title'); }"><i class="icon-ok" data-bind="style: {visibility: repositoryplugins.currentSorting() == 'title' ? 'visible' : 'hidden'}"></i> {{ _('Sort by title') }} ({{ _('ascending') }})</a></li>
|
||||
<li><a href="#" data-bind="click: function() { repositoryplugins.changeSorting('published'); }"><i class="icon-ok" data-bind="style: {visibility: repositoryplugins.currentSorting() == 'published' ? 'visible' : 'hidden'}"></i> {{ _('Sort by publication date') }} ({{ _('descending') }})</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="#" data-bind="click: function() { repositoryplugins.toggleFilter('filter_installed'); }"><i class="icon-ok" data-bind="style: {visibility: _.contains(repositoryplugins.currentFilters(), 'filter_installed') ? 'visible' : 'hidden'}"></i> {{ _('Only show uninstalled plugins') }}</a></li>
|
||||
<li><a href="#" data-bind="click: function() { repositoryplugins.toggleFilter('filter_incompatible'); }"><i class="icon-ok" data-bind="style: {visibility: _.contains(repositoryplugins.currentFilters(), 'filter_incompatible') ? 'visible' : 'hidden'}"></i> {{ _('Only show compatible plugins') }}</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="#" data-bind="click: function() { refreshRepository(); }"><i class="icon-refresh"></i> {{ _('Refresh list from repository') }}</a></li>
|
||||
</ul>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
<form class="form-search">
|
||||
<input type="text" class="input-block search-query" data-bind="value: repositorySearchQuery, valueUpdate: 'input'" placeholder="{{ _('Search...') }}">
|
||||
</form>
|
||||
|
||||
<div data-bind="visible: repositoryAvailable()">
|
||||
<div id="settings_plugin_pluginmanager_repositorydialog_list" data-bind="slimScrolledForeach: repositoryplugins.paginatedItems">
|
||||
<div class="entry">
|
||||
<div class="row-fluid">
|
||||
<div class="span9">
|
||||
<div><span data-bind="text: title"></span></div>
|
||||
<div><small class="muted" data-bind="text: description"> </small></div>
|
||||
<div>
|
||||
<small data-bind="visible: page"><i class="icon-info"></i> <a data-bind="attr: {href: page}" target="_blank">{{ _('Details') }}</a></small>
|
||||
<small data-bind="visible: homepage"><i class="icon-home"></i> <a data-bind="attr: {href: homepage}" target="_blank">{{ _('Homepage') }}</a></small>
|
||||
<small data-bind="visible: license"><i class="icon-legal"></i> <span data-bind="text: license"></span></small>
|
||||
<small data-bind="visible: author"><i class="icon-user"></i> <span data-bind="text: author"></span></small>
|
||||
<small> </small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="span3">
|
||||
<button class="btn btn-primary btn-block" data-bind="enable: $root.isCompatible($data), css: {disabled: !$root.isCompatible($data)}, click: function() { if ($root.isCompatible($data)) { $root.installFromRepository($data); } else { return false; } }"><i class="icon-add"></i> <span data-bind="text: $root.installButtonText($data)"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="settings_plugin_pluginmanager_repositorydialog_unavailable" data-bind="visible: !repositoryAvailable()">
|
||||
<div>
|
||||
<p>
|
||||
<strong>{{ _('Sadly the repository is currently not available') }}</strong>
|
||||
</p>
|
||||
<p>
|
||||
<small>{{ _('Is your OctoPrint installation connected to the internet?') }}</small>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>{{ _('... from URL') }}</h4>
|
||||
|
||||
<form class="form-inline">
|
||||
<div class="control-group row-fluid" data-bind="css: {error: invalidUrl}">
|
||||
<div class="span9">
|
||||
<input type="text" class="input-block-level" data-bind="value: installUrl, valueUpdate: 'input'" placeholder="{{ _('Enter URL...') }}" >
|
||||
</div>
|
||||
<button class="btn btn-primary span3" data-bind="enable: enableUrlInstall, css: {disabled: !enableUrlInstall()}, click: function() { if (enableUrlInstall()) { $root.installPlugin(); } }">{{ _('Install') }}</button>
|
||||
</div>
|
||||
<span class="help-block" data-bind="visible: invalidUrl">{{ _('This does not look like a valid "http://" or "https://" URL.') }}</span>
|
||||
</form>
|
||||
|
||||
<h4>{{ _('... from an uploaded archive') }}</h4>
|
||||
|
||||
<form class="form-inline">
|
||||
<div class="control-group row-fluid" data-bind="css: {error: invalidArchive}">
|
||||
<div class="input-prepend span9">
|
||||
<span class="btn fileinput-button">
|
||||
<span>{{ _('Browse...') }}</span>
|
||||
<input id="settings_plugin_pluginmanager_repositorydialog_upload" type="file" name="file" data-url="{{ url_for("plugin.pluginmanager.upload_archive") }}">
|
||||
</span>
|
||||
<span class="add-on add-on-limited text-left" data-bind="text: uploadFilename, attr: {title: uploadFilename}"></span>
|
||||
</div>
|
||||
<button id="settings_plugin_pluginmanager_repositorydialog_upload_start" class="btn btn-primary span3" data-bind="enable: enableArchiveInstall, css: {disabled: !enableArchiveInstall()}, click: function(){}">{{ _('Install') }}</button>
|
||||
</div>
|
||||
<span class="help-block" data-bind="visible: invalidArchive">{{ _('This does not look like a valid plugin archive. Valid plugin archives should be either zip files or tarballs and have the extension ".zip", ".tar.gz", ".tgz" or ".tar"') }}</span>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" data-dismiss="modal" aria-hidden="true">{{ _('Close') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -183,7 +183,7 @@ default_settings = {
|
|||
"settings": [
|
||||
"section_printer", "serial", "printerprofiles", "temperatures", "terminalfilters", "gcodescripts",
|
||||
"section_features", "features", "webcam", "accesscontrol", "api",
|
||||
"section_octoprint", "folders", "appearance", "logs"
|
||||
"section_octoprint", "folders", "appearance", "logs", "plugin_pluginmanager"
|
||||
],
|
||||
"usersettings": ["access", "interface"],
|
||||
"generic": []
|
||||
|
|
|
|||
Loading…
Reference in a new issue