diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cf331cd..5210bd66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ ## 1.3.0 (unreleased) +### New Features + +* A new wizard dialog for system setups that can also be extended by plugins. Replaces the first run dialog + for setting up access control and can also be triggered in other cases than only the first run, e.g. + if Plugins necessitate user input to function properly. +* An About dialog including licenses, authors, the changelog and more, extendable by plugins if necessary. +* New features within the plugin system (TODO): + * New plugin mixin `UiPlugin` for plugins that want to provide an alternative web interface delivered by the + server. +* Extracted a Javascript client library for utilizing the server's API, can be reused by `UiPlugin`s. (TODO) + +(TODO) = needs to be further described and documented + ### Improvements * Upgraded versioneer, generated version numbers are now PEP440 compatible (relevant @@ -41,6 +54,8 @@ but a target temperature is set - this way the graph should be more responsive while monitoring a manual heatup. * Documentation improvements +* Test buttons for webcam snapshot & stream URL, ffmpeg path and some other settings + (see also [#183](https://github.com/foosel/OctoPrint/issues/183)). ### Bug Fixes diff --git a/src/octoprint/plugins/corewizard/static/js/corewizard.js b/src/octoprint/plugins/corewizard/static/js/corewizard.js index f909057e..c4b587ed 100644 --- a/src/octoprint/plugins/corewizard/static/js/corewizard.js +++ b/src/octoprint/plugins/corewizard/static/js/corewizard.js @@ -53,26 +53,22 @@ $(function() { }; self._sendData = function(data, callback) { - $.ajax({ - url: BASEURL + "plugin/corewizard/acl", - type: "POST", - dataType: "json", - data: data, - success: function() { + OctoPrint.postJson("plugin/corewizard/acl", data) + .done(function() { self.setup(true); self.decision(data.ac); if (data.ac) { // we now log the user in var user = data.user; var pass = data.pass1; - self.loginStateViewModel.login(user, pass, true, function() { - if (callback) callback(); - }); + self.loginStateViewModel.login(user, pass, true) + .done(function() { + if (callback) callback(); + }); } else { if (callback) callback(); } - } - }); + }); }; self.onWizardTabChange = function(current, next) { diff --git a/src/octoprint/plugins/cura/static/js/cura.js b/src/octoprint/plugins/cura/static/js/cura.js index fb551689..d99db545 100644 --- a/src/octoprint/plugins/cura/static/js/cura.js +++ b/src/octoprint/plugins/cura/static/js/cura.js @@ -70,6 +70,7 @@ $(function() { dataType: "json", maxNumberOfFiles: 1, autoUpload: false, + headers: OctoPrint.getRequestHeaders(), add: function(e, data) { if (data.files.length == 0) { return false; @@ -131,14 +132,11 @@ $(function() { return (item.key == data.key); }); - $.ajax({ - url: data.resource(), - type: "DELETE", - success: function() { + OctoPrint.slicing.deleteProfileForSlicer("cura", data.key, {url: data.resource}) + .done(function() { self.requestData(); self.slicingViewModel.requestData(); - } - }); + }); }; self.makeProfileDefault = function(data) { @@ -156,16 +154,10 @@ $(function() { item.isdefault(true); } - $.ajax({ - url: data.resource(), - type: "PATCH", - dataType: "json", - data: JSON.stringify({default: true}), - contentType: "application/json; charset=UTF-8", - success: function() { + OctoPrint.slicing.updateProfileForSlicer("cura", data.key, {default: true}, {url: data.resource()}) + .done(function() { self.requestData(); - } - }); + }); }; self.showImportProfileDialog = function(makeDefault) { @@ -176,13 +168,9 @@ $(function() { $("#settings_plugin_cura_import").modal("show"); }; - self.testEnginePath = function(successCallback) { - self.sendTestRequest(self.settings.plugins.cura.cura_engine()); - }; - - self.sendTestRequest = function(enginePath, successCallback) { - if (successCallback == undefined) { - successCallback = function(response) { + self.testEnginePath = function() { + OctoPrint.util.testExecutable(self.settings.plugins.cura.cura_engine()) + .done(function(response) { if (!response.result) { if (!response.exists) { self.pathText(gettext("The path doesn't exist")); @@ -196,31 +184,12 @@ $(function() { } self.pathOk(response.result); self.pathBroken(!response.result); - } - } - - $.ajax({ - url: API_BASEURL + "util/test", - type: "POST", - dataType: "json", - data: JSON.stringify({ - command: "path", - path: enginePath, - check_type: "file", - check_access: "x" - }), - contentType: "application/json; charset=UTF-8", - success: successCallback - }) + }); }; self.requestData = function() { - $.ajax({ - url: API_BASEURL + "slicing/cura/profiles", - type: "GET", - dataType: "json", - success: self.fromResponse - }); + OctoPrint.slicing.listProfilesForSlicer("cura") + .done(self.fromResponse); }; self.fromResponse = function(data) { diff --git a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js index 8767c9ea..5e3baf7e 100644 --- a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js +++ b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js @@ -1,3 +1,70 @@ +(function (global, factory) { + if (typeof define === "function" && define.amd) { + define(["OctoPrint"], factory); + } else { + factory(window.OctoPrint); + } +})(window || this, function(OctoPrint) { + var exports = {}; + + exports.get = function(refresh, opts) { + return OctoPrint.get(OctoPrint.getSimpleApiUrl("pluginmanager") + ((refresh) ? "?refresh_repository=true" : ""), opts); + }; + + exports.getWithRefresh = function(opts) { + return exports.get(true, opts); + }; + + exports.getWithoutRefresh = function(opts) { + return exports.get(false, opts); + }; + + exports.install = function(pluginUrl, dependencyLinks, opts) { + var data = { + url: pluginUrl, + dependency_links: !!dependencyLinks + }; + return OctoPrint.simpleApiCommand("pluginmanager", "install", data, opts); + }; + + exports.reinstall = function(plugin, pluginUrl, dependencyLinks, opts) { + var data = { + url: pluginUrl, + dependency_links: !!dependencyLinks, + reinstall: plugin, + force: true + }; + return OctoPrint.simpleApiCommand("pluginmanager", "install", data, opts); + }; + + exports.uninstall = function(plugin, opts) { + var data = { + plugin: plugin + }; + return OctoPrint.simpleApiCommand("pluginmanager", "uninstall", data, opts); + }; + + exports.enable = function(plugin, opts) { + var data = { + plugin: plugin + }; + return OctoPrint.simpleApiCommand("pluginmanager", "enable", data, opts); + }; + + exports.disable = function(plugin, opts) { + var data = { + plugin: plugin + }; + return OctoPrint.simpleApiCommand("pluginmanager", "disable", data, opts); + }; + + exports.upload = function(file) { + return OctoPrint.upload(OctoPrint.getBlueprintUrl("pluginmanager") + "upload_archive", file); + }; + + OctoPrint.plugins.pluginmanager = exports; +}); + $(function() { function PluginManagerViewModel(parameters) { var self = this; @@ -5,6 +72,7 @@ $(function() { self.loginState = parameters[0]; self.settingsViewModel = parameters[1]; self.printerState = parameters[2]; + self.systemViewModel = parameters[3]; self.config_repositoryUrl = ko.observable(); self.config_repositoryTtl = ko.observable(); @@ -104,6 +172,20 @@ $(function() { self.workingDialog = undefined; self.workingOutput = undefined; + self.restartCommandSpec = undefined; + self.systemViewModel.systemActions.subscribe(function() { + var lastResponse = self.systemViewModel.lastCommandResponse; + if (!lastResponse || !lastResponse.core) { + self.restartCommandSpec = undefined; + return; + } + + var restartSpec = _.filter(lastResponse.core, function(spec) { return spec.action == "restart" }); + self.restartCommandSpec = restartSpec != undefined && restartSpec.length > 0 ? restartSpec[0] : undefined; + }); + + self.notifications = []; + self.enableManagement = ko.computed(function() { return !self.printerState.isPrinting(); }); @@ -247,12 +329,8 @@ $(function() { return; } - $.ajax({ - url: API_BASEURL + "plugin/pluginmanager" + ((includeRepo) ? "?refresh_repository=true" : ""), - type: "GET", - dataType: "json", - success: self.fromResponse - }); + OctoPrint.plugins.pluginmanager.get(includeRepo) + .done(self.fromResponse); }; self.togglePlugin = function(data) { @@ -266,19 +344,25 @@ $(function() { if (data.key == "pluginmanager") return; - var command = self._getToggleCommand(data); + var onSuccess = self.requestData, + onError = function() { + new PNotify({ + title: gettext("Something went wrong"), + text: gettext("Please consult octoprint.log for details"), + type: "error", + hide: false + }) + }; - 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 - }) - }); + if (self._getToggleCommand(data) == "enable") { + OctoPrint.plugins.pluginmanager.enable(data.key) + .done(onSuccess) + .fail(onError); + } else { + OctoPrint.plugins.pluginmanager.disable(data.key) + .done(onSuccess) + .fail(onError); + } }; self.showRepository = function() { @@ -298,11 +382,7 @@ $(function() { return; } - if (self.installed(data)) { - self.installPlugin(data.archive, data.title, data.id, data.follow_dependency_links || self.followDependencyLinks()); - } else { - self.installPlugin(data.archive, data.title, undefined, data.follow_dependency_links || self.followDependencyLinks()); - } + self.installPlugin(data.archive, data.title, (self.installed(data) ? data.id : undefined), data.follow_dependency_links || self.followDependencyLinks()); }; self.installPlugin = function(url, name, reinstall, followDependencyLinks) { @@ -337,26 +417,33 @@ $(function() { } self._markWorking(workTitle, workText); - var command = "install"; - var payload = {url: url, dependency_links: followDependencyLinks}; - if (reinstall) { - payload["plugin"] = reinstall; - payload["force"] = true; - } + var onSuccess = function() { + self.requestData(); + self.installUrl(""); + }, + onError = function() { + new PNotify({ + title: gettext("Something went wrong"), + text: gettext("Please consult octoprint.log for details"), + type: "error", + hide: false + }); + }, + onAlways = function() { + self._markDone(); + }; - 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(); - }); + if (reinstall) { + OctoPrint.plugins.pluginmanager.reinstall(reinstall, url, followDependencyLinks) + .done(onSuccess) + .fail(onError) + .always(onAlways); + } else { + OctoPrint.plugins.pluginmanager.install(url, followDependencyLinks) + .done(onSuccess) + .fail(onError) + .always(onAlways); + } }; self.uninstallPlugin = function(data) { @@ -373,20 +460,19 @@ $(function() { 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 + OctoPrint.plugins.pluginmanager.uninstall(data.key) + .done(self.requestData) + .fail(function() { + new PNotify({ + title: gettext("Something went wrong"), + text: gettext("Please consult octoprint.log for details"), + type: "error", + hide: false + }); + }) + .always(function() { + self._markDone(); }); - self._markDone(); - }); }; self.refreshRepository = function() { @@ -464,15 +550,59 @@ $(function() { }; self._displayNotification = function(response, titleSuccess, textSuccess, textRestart, textReload, titleError, textError) { + var notification; + + var beforeClose = function(notification) { + self.notifications = _.without(self.notifications, notification); + }; + if (response.result) { if (response.needs_restart) { - new PNotify({ + var options = { title: titleSuccess, text: textRestart, + buttons: { + closer: false, + sticker: false + }, + callbacks: { + before_close: beforeClose + }, hide: false - }); + }; + + if (self.restartCommandSpec) { + options.confirm = { + confirm: true, + buttons: [{ + text: gettext("Restart now"), + click: function () { + showConfirmationDialog({ + message: gettext("This will restart your OctoPrint server."), + onproceed: function() { + OctoPrint.system.executeCommand("core", "restart") + .done(function() { + new PNotify({ + title: gettext("Restart in progress"), + text: gettext("The server is now being restarted in the background") + }) + }) + .fail(function() { + new PNotify({ + title: gettext("Something went wrong"), + text: gettext("Trying to restart the server produced an error, please check octoprint.log for details. You'll have to restart manually.") + }) + }); + } + }); + } + }] + } + } + + notification = PNotify.singleButtonNotify(options); } else if (response.needs_refresh) { - new PNotify({ + notification = PNotify.singleButtonNotify({ title: titleSuccess, text: textReload, confirm: { @@ -488,51 +618,35 @@ $(function() { closer: false, sticker: false }, + callbacks: { + before_close: beforeClose + }, hide: false }) } else { - new PNotify({ + notification = new PNotify({ title: titleSuccess, text: textSuccess, type: "success", + callbacks: { + before_close: beforeClose + }, hide: false }) } } else { - new PNotify({ + notification = new PNotify({ title: titleError, text: textError, type: "error", + callbacks: { + before_close: beforeClose + }, 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.notifications.push(notification); }; self._markWorking = function(title, line) { @@ -577,6 +691,16 @@ $(function() { self.onUserLoggedIn = function(user) { if (user.admin) { self.requestData(); + } else { + self.onUserLoggedOut(); + } + }; + + self.onUserLoggedOut = function() { + if (self.notifications) { + _.each(self.notifications, function(notification) { + notification.remove(); + }); } }; @@ -730,5 +854,9 @@ $(function() { } // view model class, parameters for constructor, container to bind to - ADDITIONAL_VIEWMODELS.push([PluginManagerViewModel, ["loginStateViewModel", "settingsViewModel", "printerStateViewModel"], "#settings_plugin_pluginmanager"]); + ADDITIONAL_VIEWMODELS.push([ + PluginManagerViewModel, + ["loginStateViewModel", "settingsViewModel", "printerStateViewModel", "systemViewModel"], + "#settings_plugin_pluginmanager" + ]); }); diff --git a/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js b/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js index b9223ddf..8747c25e 100644 --- a/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js +++ b/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js @@ -1,3 +1,43 @@ +(function (global, factory) { + if (typeof define === "function" && define.amd) { + define(["OctoPrint"], factory); + } else { + factory(window.OctoPrint); + } +})(window || this, function(OctoPrint) { + var exports = {}; + + var url = OctoPrint.getBlueprintUrl("softwareupdate"); + var checkUrl = url + "check"; + var updateUrl = url + "update"; + + exports.check = function(force, opts) { + return OctoPrint.get(checkUrl + ((!!force) ? "?force=true" : ""), opts); + }; + + exports.update = function(entries, force, opts) { + entries = entries || []; + if (typeof entries == "string") { + entries = [entries]; + } + + var data = { + entries: entries, + force: !!force + }; + return OctoPrint.postJson(updateUrl, data, opts); + }; + + exports.updateAll = function(force, opts) { + var data = { + force: !!force + }; + return OctoPrint.postJson(updateUrl, data, opts); + }; + + OctoPrint.plugins.softwareupdate = exports; +}); + $(function() { function SoftwareUpdateViewModel(parameters) { var self = this; @@ -221,20 +261,10 @@ $(function() { self.performCheck = function(showIfNothingNew, force, ignoreSeen) { if (!self.loginState.isUser()) return; - - var url = PLUGIN_BASEURL + "softwareupdate/check"; - if (force) { - url += "?force=true"; - } - - $.ajax({ - url: url, - type: "GET", - dataType: "json", - success: function(data) { + OctoPrint.plugins.softwareupdate.check(force) + .done(function(data) { self.fromCheckResponse(data, ignoreSeen, showIfNothingNew); - } - }); + }); }; self._markNotificationAsSeen = function(data) { @@ -285,13 +315,12 @@ $(function() { }; self._showPopup(options); - $.ajax({ - url: PLUGIN_BASEURL + "softwareupdate/update", - type: "POST", - dataType: "json", - contentType: "application/json; charset=UTF-8", - data: JSON.stringify({force: (force == true)}), - error: function() { + OctoPrint.plugins.softwareupdate.updateAll(force) + .done(function(data) { + self.currentlyBeingUpdated = data.checks; + self._markWorking(gettext("Updating..."), gettext("Updating, please wait.")); + }) + .fail(function() { self.updateInProgress = false; self._showPopup({ title: gettext("Update not started!"), @@ -302,12 +331,7 @@ $(function() { sticker: false } }); - }, - success: function(data) { - self.currentlyBeingUpdated = data.checks; - self._markWorking(gettext("Updating..."), gettext("Updating, please wait.")); - } - }); + }); }; self.update = function(force) { diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index d1be80d0..26468cf5 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -912,6 +912,26 @@ class Server(object): "js/lib/loglevel.min.js", "js/lib/sockjs-0.3.4.min.js" ] + js_client = [ + "js/app/client/base.js", + "js/app/client/socket.js", + "js/app/client/browser.js", + "js/app/client/connection.js", + "js/app/client/control.js", + "js/app/client/files.js", + "js/app/client/job.js", + "js/app/client/languages.js", + "js/app/client/logs.js", + "js/app/client/printer.js", + "js/app/client/printerprofiles.js", + "js/app/client/settings.js", + "js/app/client/slicing.js", + "js/app/client/system.js", + "js/app/client/timelapse.js", + "js/app/client/users.js", + "js/app/client/util.js", + "js/app/client/wizard.js" + ] js_app = dynamic_assets["js"] + [ "js/app/dataupdater.js", "js/app/helpers.js", @@ -966,8 +986,10 @@ class Server(object): js_libs_bundle = Bundle(*js_libs, output="webassets/packed_libs.js", filters="js_delimiter_bundler") if minify: + js_client_bundle = Bundle(*js_client, output="webassets/packed_client.js", filters="rjsmin, js_delimiter_bundler") js_app_bundle = Bundle(*js_app, output="webassets/packed_app.js", filters="rjsmin, js_delimiter_bundler") else: + js_client_bundle = Bundle(*js_client, output="webassets/packed_client.js", filters="js_delimiter_bundler") js_app_bundle = Bundle(*js_app, output="webassets/packed_app.js", filters="js_delimiter_bundler") css_libs_bundle = Bundle(*css_libs, output="webassets/packed_libs.css") @@ -976,6 +998,7 @@ class Server(object): all_less_bundle = Bundle(*less_app, output="webassets/packed_app.less", filters="cssrewrite, less_importrewrite") assets.register("js_libs", js_libs_bundle) + assets.register("js_client", js_client_bundle) assets.register("js_app", js_app_bundle) assets.register("css_libs", css_libs_bundle) assets.register("css_app", css_app_bundle) diff --git a/src/octoprint/server/api/__init__.py b/src/octoprint/server/api/__init__.py index 7bb87bc4..4b106cdd 100644 --- a/src/octoprint/server/api/__init__.py +++ b/src/octoprint/server/api/__init__.py @@ -38,6 +38,7 @@ from . import log as api_logs from . import slicing as api_slicing from . import printer_profiles as api_printer_profiles from . import languages as api_languages +from . import system as api_system VERSION = "0.1" @@ -177,51 +178,21 @@ def apiVersion(): "api": VERSION }) -#~~ system control - - -@api.route("/system", methods=["POST"]) -@restricted_access -@admin_permission.require(403) -def performSystemAction(): - logger = logging.getLogger(__name__) - if "action" in request.values.keys(): - action = request.values["action"] - available_actions = s().get(["system", "actions"]) - for availableAction in available_actions: - if availableAction["action"] == action: - async = availableAction["async"] if "async" in availableAction else False - ignore = availableAction["ignore"] if "ignore" in availableAction else False - logger.info("Performing command: %s" % availableAction["command"]) - try: - # we run this with shell=True since we have to trust whatever - # our admin configured as command and since we want to allow - # shell-alike handling here... - p = sarge.run(availableAction["command"], stderr=sarge.Capture(), shell=True, async=async) - if not async: - if not ignore and p.returncode != 0: - returncode = p.returncode - stderr_text = p.stderr.text - logger.warn("Command failed with return code %i: %s" % (returncode, stderr_text)) - return make_response(("Command failed with return code %i: %s" % (returncode, stderr_text), 500, [])) - except Exception, e: - if not ignore: - logger.warn("Command failed: %s" % e) - return make_response(("Command failed: %s" % e, 500, [])) - break - return NO_CONTENT - #~~ Login/user handling @api.route("/login", methods=["POST"]) def login(): - if octoprint.server.userManager is not None and "user" in request.values.keys() and "pass" in request.values.keys(): - username = request.values["user"] - password = request.values["pass"] + data = request.values + if hasattr(request, "json") and request.json: + data = request.json - if "remember" in request.values.keys() and request.values["remember"] == "true": + if octoprint.server.userManager is not None and "user" in data and "pass" in data: + username = data["user"] + password = data["pass"] + + if "remember" in data and data["remember"] in valid_boolean_trues: remember = True else: remember = False @@ -241,7 +212,7 @@ def login(): return jsonify(user.asDict()) return make_response(("User unknown or password incorrect", 401, [])) - elif "passive" in request.values: + elif "passive" in data: return passive_login() return NO_CONTENT diff --git a/src/octoprint/server/api/system.py b/src/octoprint/server/api/system.py new file mode 100644 index 00000000..fba71f43 --- /dev/null +++ b/src/octoprint/server/api/system.py @@ -0,0 +1,176 @@ +# coding=utf-8 +from __future__ import absolute_import + +__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 collections +import logging +import sarge + +from flask import request, make_response, jsonify, url_for +from flask.ext.babel import gettext + +from octoprint.settings import settings as s + +from octoprint.server import admin_permission, NO_CONTENT +from octoprint.server.api import api +from octoprint.server.util.flask import restricted_access, get_remote_address + + +@api.route("/system", methods=["POST"]) +@restricted_access +@admin_permission.require(403) +def performSystemAction(): + logging.getLogger(__name__).warn("Deprecated API call to /api/system made by {}, should be migrated to use /system/commands/custom/".format(get_remote_address(request))) + + data = request.values + if hasattr(request, "json") and request.json: + data = request.json + + if not "action" in data: + return make_response("action for perform is not defined", 400) + + return executeSystemCommand("custom", data["action"]) + + +@api.route("/system/commands", methods=["GET"]) +@restricted_access +@admin_permission.require(403) +def retrieveSystemCommands(): + return jsonify(core=_to_client_specs(_get_core_command_specs()), + custom=_to_client_specs(_get_custom_command_specs())) + + +@api.route("/system/commands/", methods=["GET"]) +@restricted_access +@admin_permission.require(403) +def retrieveSystemCommandsForSource(source): + if source == "core": + specs = _get_core_command_specs() + elif source == "custom": + specs = _get_custom_command_specs() + else: + return make_response("Unknown system command source: {}".format(source), 400) + + return jsonify(_to_client_specs(specs)) + + +@api.route("/system/commands//", methods=["POST"]) +@restricted_access +@admin_permission.require(403) +def executeSystemCommand(source, command): + logger = logging.getLogger(__name__) + + command_spec = _get_command_spec(source, command) + if not command_spec: + return make_response("Command {}:{} not found".format(source, command), 404) + + if not "command" in command_spec: + return make_response("Command {}:{} does not define a command to execute, can't proceed".format(source, command), 500) + + async = command_spec["async"] if "async" in command_spec else False + ignore = command_spec["ignore"] if "ignore" in command_spec else False + logger.info("Performing command for {}:{}: {}".format(source, command, command_spec["command"])) + try: + # we run this with shell=True since we have to trust whatever + # our admin configured as command and since we want to allow + # shell-alike handling here... + p = sarge.run(command_spec["command"], + stdout=sarge.Capture(), + stderr=sarge.Capture(), + shell=True, + async=async) + if not async: + if not ignore and p.returncode != 0: + returncode = p.returncode + stdout_text = p.stdout.text + stderr_text = p.stderr.text + + error = "Command failed with return code {}:\nSTDOUT: {}\nSTDERR: {}".format(returncode, stdout_text, stderr_text) + logger.warn(error) + return make_response(error, 500) + except Exception, e: + if not ignore: + error = "Command failed: {}".format(str(e)) + logger.warn(error) + return make_response(error, 500) + + return NO_CONTENT + + +def _to_client_specs(specs): + result = list() + for spec in specs.values(): + if not "action" in spec or not "source" in spec: + continue + copied = dict((k, v) for k, v in spec.items() if k in ("source", "action", "name", "confirm")) + copied["resource"] = url_for(".executeSystemCommand", + source=spec["source"], + command=spec["action"], + _external=True) + result.append(copied) + return result + + +def _get_command_spec(source, action): + if source == "core": + return _get_core_command_spec(action) + elif source == "custom": + return _get_custom_command_spec(action) + else: + return None + + +def _get_core_command_specs(): + commands = collections.OrderedDict( + shutdown=dict( + command=s().get(["server", "commands", "systemShutdownCommand"]), + name=gettext("Shutdown"), + confirm=gettext("You are about to shutdown the system.")), + reboot=dict( + command=s().get(["server", "commands", "systemRestartCommand"]), + name=gettext("Reboot"), + confirm=gettext("You are about to reboot the system.")), + restart=dict( + command=s().get(["server", "commands", "serverRestartCommand"]), + name="Restart OctoPrint", + confirm="You are about to restart the OctoPrint server.") + ) + + available_commands = dict() + for action, spec in commands.items(): + if not spec["command"]: + continue + spec.update(dict(action=action, source="core", async=True, ignore=True)) + available_commands[action] = spec + return available_commands + + +def _get_core_command_spec(action): + available_actions = _get_core_command_specs() + if not action in available_actions: + logging.getLogger(__name__).warn("Command for core action {} is not configured, you need to configure the command before it can be used".format(action)) + return None + + return available_actions[action] + + +def _get_custom_command_specs(): + specs = collections.OrderedDict() + for spec in s().get(["system", "actions"]): + if not "action" in spec: + continue + copied = dict(spec) + copied["source"] = "custom" + specs[spec["action"]] = copied + return specs + + +def _get_custom_command_spec(action): + available_actions = _get_custom_command_specs() + if not action in available_actions: + return None + + return available_actions[action] + diff --git a/src/octoprint/server/api/timelapse.py b/src/octoprint/server/api/timelapse.py index 63b5be34..fc37bdf8 100644 --- a/src/octoprint/server/api/timelapse.py +++ b/src/octoprint/server/api/timelapse.py @@ -68,52 +68,56 @@ def deleteTimelapse(filename): @api.route("/timelapse", methods=["POST"]) @restricted_access def setTimelapseConfig(): - if "type" in request.values: + data = request.values + if hasattr(request, "json") and request.json: + data = request.json + + if "type" in data: config = { - "type": request.values["type"], + "type": data["type"], "postRoll": 0, "fps": 25, "options": {} } - if "postRoll" in request.values: + if "postRoll" in data: try: - postRoll = int(request.values["postRoll"]) + postRoll = int(data["postRoll"]) except ValueError: - return make_response("Invalid value for postRoll: %r" % request.values["postRoll"], 400) + return make_response("Invalid value for postRoll: %r" % data["postRoll"], 400) else: if postRoll >= 0: config["postRoll"] = postRoll else: return make_response("Invalid value for postRoll: %d" % postRoll, 400) - if "fps" in request.values: + if "fps" in data: try: - fps = int(request.values["fps"]) + fps = int(data["fps"]) except ValueError: - return make_response("Invalid value for fps: %r" % request.values["fps"], 400) + return make_response("Invalid value for fps: %r" % data["fps"], 400) else: if fps > 0: config["fps"] = fps else: return make_response("Invalid value for fps: %d" % fps, 400) - if "interval" in request.values: + if "interval" in data: config["options"] = { "interval": 10 } try: - interval = int(request.values["interval"]) + interval = int(data["interval"]) except ValueError: - return make_response("Invalid value for interval: %r" % request.values["interval"]) + return make_response("Invalid value for interval: %r" % data["interval"]) else: if interval > 0: config["options"]["interval"] = interval else: return make_response("Invalid value for interval: %d" % interval) - if admin_permission.can() and "save" in request.values and request.values["save"] in valid_boolean_trues: + if admin_permission.can() and "save" in data and data["save"] in valid_boolean_trues: octoprint.timelapse.configureTimelapse(config, True) else: octoprint.timelapse.configureTimelapse(config) diff --git a/src/octoprint/server/util/flask.py b/src/octoprint/server/util/flask.py index 4fb4d6cb..b1c386af 100644 --- a/src/octoprint/server/util/flask.py +++ b/src/octoprint/server/util/flask.py @@ -631,6 +631,7 @@ def collect_plugin_assets(enable_gcodeviewer=True, preferred_stylesheet="css"): 'js/app/viewmodels/printerprofiles.js', 'js/app/viewmodels/settings.js', 'js/app/viewmodels/slicing.js', + 'js/app/viewmodels/system.js', 'js/app/viewmodels/temperature.js', 'js/app/viewmodels/terminal.js', 'js/app/viewmodels/timelapse.js', diff --git a/src/octoprint/server/views.py b/src/octoprint/server/views.py index 961ba445..0f1b8c70 100644 --- a/src/octoprint/server/views.py +++ b/src/octoprint/server/views.py @@ -573,4 +573,3 @@ def localeJs(locale, domain): def plugin_assets(name, filename): return redirect(url_for("plugin." + name + ".static", filename=filename)) - diff --git a/src/octoprint/static/js/app/client/base.js b/src/octoprint/static/js/app/client/base.js new file mode 100644 index 00000000..1637c0eb --- /dev/null +++ b/src/octoprint/static/js/app/client/base.js @@ -0,0 +1,249 @@ +(function (global, factory) { + if (typeof define === "function" && define.amd) { + define("OctoPrint", ["jquery", "lodash"], factory); + } else { + global.OctoPrint = factory(window.$, window._); + } +})(window || this, function($, _) { + var OctoPrint = {}; + + var noCache = function(opts) { + opts = opts || {}; + + var params = $.extend({}, opts); + params.headers = $.extend({}, params.headers || {}); + params.headers["Cache-Control"] = "no-cache"; + + return params; + }; + + var contentTypeJson = function(opts) { + opts = opts || {}; + + var params = $.extend({}, opts); + params.contentType = "application/json; charset=UTF-8"; + + return params; + }; + + OctoPrint.options = { + "baseurl": undefined, + "apikey": undefined + }; + + OctoPrint.plugins = {}; + + OctoPrint.getBaseUrl = function() { + var url = OctoPrint.options.baseurl; + if (!_.endsWith(url, "/")) { + url = url + "/"; + } + return url; + }; + + OctoPrint.getRequestHeaders = function(additional) { + additional = additional || {}; + + var headers = $.extend({}, additional); + headers["X-Api-Key"] = OctoPrint.options.apikey; + + return headers; + }; + + OctoPrint.ajax = function(method, url, opts) { + opts = opts || {}; + + method = opts.method || method || "GET"; + url = opts.url || url || ""; + + var urlToCall = url; + if (!_.startsWith(url, "http://") && !_.startsWith(url, "https://")) { + urlToCall = OctoPrint.getBaseUrl() + url; + } + + var headers = OctoPrint.getRequestHeaders(opts.headers); + + var params = $.extend({}, opts); + params.type = method; + params.headers = headers; + params.dataType = params.dataType || "json"; + + return $.ajax(urlToCall, params); + }; + + OctoPrint.ajaxWithData = function(method, url, data, opts) { + opts = opts || {}; + + var params = $.extend({}, opts); + params.data = data; + + return OctoPrint.ajax(method, url, params); + }; + + OctoPrint.get = function(url, opts) { + return OctoPrint.ajax("GET", url, opts); + }; + + OctoPrint.post = function(url, data, opts) { + return OctoPrint.ajaxWithData("POST", url, data, noCache(opts)); + }; + + OctoPrint.postJson = function(url, data, opts) { + return OctoPrint.post(url, JSON.stringify(data), contentTypeJson(opts)); + }; + + OctoPrint.put = function(url, data, opts) { + return OctoPrint.ajaxWithData("PUT", url, data, noCache(opts)); + }; + + OctoPrint.putJson = function(url, data, opts) { + return OctoPrint.put(url, data, contentTypeJson(opts)); + }; + + OctoPrint.patch = function(url, data, opts) { + return OctoPrint.ajaxWithData("PATCH", url, data, noCache(opts)); + }; + + OctoPrint.patchJson = function(url, data, opts) { + return OctoPrint.patch(url, JSON.stringify(data), contentTypeJson(opts)); + }; + + OctoPrint.delete = function(url, opts) { + return OctoPrint.ajax("DELETE", url, opts); + }; + + OctoPrint.download = function(url, opts) { + var params = $.extend({}, opts || {}); + params.dataType = "text"; + return OctoPrint.get(url, params); + }; + + OctoPrint.upload = function(url, file, filename, additional) { + additional = additional || {}; + + var fileData; + if (file instanceof jQuery) { + fileData = file[0].files[0]; + } else if (typeof file == "string") { + fileData = $(file)[0].files[0]; + } else { + fileData = file; + } + + filename = filename || fileData.name; + + var form = new FormData(); + form.append("file", fileData, filename); + + _.each(additional, function(value, key) { + form.append(key, value); + }); + + var deferred = $.Deferred(); + + var request = new XMLHttpRequest(); + request.onreadystatechange = function() { + if (request.readyState == 4) { + deferred.notify({loaded: filesize, total: filesize}); + + var success = request.status >= 200 && request.status < 300 + || request.status === 304; + var error, json, statusText; + + try { + json = JSON.parse(request.response); + statusText = "success"; + } catch (e) { + success = false; + error = e; + statusText = "parsererror"; + } + + if (success) { + deferred.resolve([json, statusText, request]); + } else { + if (!statusText) { + statusText = request.statusText; + } + deferred.reject([request, statusText, error]); + } + } + }; + request.ontimeout = function() { + deferred.reject([request, "timeout", "Timeout"]); + }; + request.upload.addEventListener("loadstart", function(e) { + deferred.notify({loaded: e.loaded, total: e.total}); + }); + request.upload.addEventListener("progress", function(e) { + deferred.notify({loaded: e.loaded, total: e.total}); + }); + request.upload.addEventListener("loadend", function(e) { + deferred.notify({loaded: e.loaded, total: e.total}); + }); + + var headers = OctoPrint.getRequestHeaders(); + + request.open("POST", OctoPrint.getBaseUrl() + url); + _.each(headers, function(value, key) { + request.setRequestHeader(key, value); + }); + request.send(form); + + return deferred.promise(); + }; + + OctoPrint.issueCommand = function(url, command, payload, opts) { + payload = payload || {}; + + var data = $.extend({}, payload); + data.command = command; + + return OctoPrint.postJson(url, data, opts); + }; + + OctoPrint.getSimpleApiUrl = function(plugin) { + return "api/plugin/" + plugin; + }; + + OctoPrint.simpleApiGet = function(plugin, opts) { + return OctoPrint.get(OctoPrint.getSimpleApiUrl(plugin), opts); + }; + + OctoPrint.simpleApiCommand = function(plugin, command, payload, opts) { + return OctoPrint.issueCommand(OctoPrint.getSimpleApiUrl(plugin), command, payload, opts); + }; + + OctoPrint.getBlueprintUrl = function(plugin) { + return "plugin/" + plugin + "/"; + }; + + OctoPrint.createRejectedDeferred = function() { + var deferred = $.Deferred(); + deferred.reject(arguments); + return deferred; + }; + + OctoPrint.createCustomException = function(name) { + var constructor; + + if (_.isFunction(name)) { + constructor = name; + } else { + constructor = function(message) { + this.name = name; + this.message = message; + this.stack = (new Error()).stack; + }; + } + + constructor.prototype = Object.create(Error.prototype); + constructor.prototype.constructor = constructor; + + return constructor; + }; + + OctoPrint.InvalidArgumentError = OctoPrint.createCustomException("InvalidArgumentError"); + + return OctoPrint; +}); diff --git a/src/octoprint/static/js/app/client/browser.js b/src/octoprint/static/js/app/client/browser.js new file mode 100644 index 00000000..ac791b6f --- /dev/null +++ b/src/octoprint/static/js/app/client/browser.js @@ -0,0 +1,29 @@ +(function (global, factory) { + if (typeof define === "function" && define.amd) { + define(["OctoPrint"], factory); + } else { + factory(window.OctoPrint); + } +})(window || this, function(OctoPrint) { + var loginUrl = "api/login"; + var logoutUrl = "api/logout"; + + OctoPrint.browser = { + login: function(username, password, remember, opts) { + var data = { + user: username, + pass: password, + remember: !!remember + }; + return OctoPrint.postJson(loginUrl, data, opts); + }, + + passiveLogin: function(opts) { + return OctoPrint.postJson(loginUrl, {passive: true}, opts); + }, + + logout: function(opts) { + return OctoPrint.postJson(logoutUrl, {}, opts); + } + }; +}); diff --git a/src/octoprint/static/js/app/client/connection.js b/src/octoprint/static/js/app/client/connection.js new file mode 100644 index 00000000..98ca4c67 --- /dev/null +++ b/src/octoprint/static/js/app/client/connection.js @@ -0,0 +1,27 @@ +(function (global, factory) { + if (typeof define === "function" && define.amd) { + define(["OctoPrint"], factory); + } else { + factory(window.OctoPrint); + } +})(window || this, function(OctoPrint) { + var url = "api/connection"; + + OctoPrint.connection = { + getSettings: function(opts) { + return OctoPrint.get(url, opts); + }, + + connect: function(data, opts) { + return OctoPrint.issueCommand(url, "connect", data || {}, opts); + }, + + disconnect: function(opts) { + return OctoPrint.issueCommand(url, "disconnect", {}, opts); + }, + + fakeAck: function(opts) { + return OctoPrint.issueCommand(url, "fake_ack", {}, opts); + } + } +}); diff --git a/src/octoprint/static/js/app/client/control.js b/src/octoprint/static/js/app/client/control.js new file mode 100644 index 00000000..d1a0eae7 --- /dev/null +++ b/src/octoprint/static/js/app/client/control.js @@ -0,0 +1,46 @@ +(function (global, factory) { + if (typeof define === "function" && define.amd) { + define(["OctoPrint"], factory); + } else { + factory(window.OctoPrint); + } +})(window || this, function(OctoPrint) { + var customUrl = "api/printer/command/custom"; + var commandUrl = "api/printer/command"; + + var sendGcodeWithParameters = function(commands, parameters, opts) { + commands = commands || []; + parameters = parameters || {}; + + if (typeof commands === "string") { + commands = [commands]; + } + + return OctoPrint.postJson(commandUrl, { + commands: commands, + parameters: parameters + }, opts); + }; + + OctoPrint.control = { + sendGcodeWithParameters: sendGcodeWithParameters, + + getCustomControls: function (opts) { + return OctoPrint.get(customUrl, opts); + }, + + sendGcode: function (commands, opts) { + return sendGcodeWithParameters(commands, undefined, opts); + }, + + sendGcodeScript: function (script, context, opts) { + script = script || ""; + context = context || {}; + + return OctoPrint.postJson(commandUrl, { + script: script, + context: context + }, opts); + } + } +}); diff --git a/src/octoprint/static/js/app/client/files.js b/src/octoprint/static/js/app/client/files.js new file mode 100644 index 00000000..84d26f17 --- /dev/null +++ b/src/octoprint/static/js/app/client/files.js @@ -0,0 +1,85 @@ +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(["OctoPrint", "jquery"], factory); + } else { + // Browser globals: + factory(window.OctoPrint, window.jQuery); + } +})(function(OctoPrint, $) { + var url = "api/files"; + + var resourceForLocation = function(location) { + return url + "/" + location; + }; + + var resourceForFile = function(location, filename) { + return resourceForLocation(location) + "/" + filename; + }; + + var issueFileCommand = function(location, filename, command, data, opts) { + var url = resourceForFile(location, filename); + return OctoPrint.issueCommand(url, command, data, opts); + }; + + var getFile = function(location, filename, opts) { + return OctoPrint.get(resourceForFile(location, filename), opts); + }; + + OctoPrint.files = { + get: getFile, + + list: function (opts) { + return OctoPrint.get(url, opts); + }, + + listForLocation: function (location, opts) { + return OctoPrint.get(resourceForLocation(location), opts); + }, + + select: function (location, filename, print, opts) { + print = print || false; + + var data = { + print: print + }; + + return issueFileCommand(location, filename, "select", data, opts); + }, + + slice: function (location, filename, parameters, opts) { + return issueFileCommand(location, filename, "slice", + parameters || {}, opts); + }, + + delete: function (location, filename, opts) { + return OctoPrint.delete(resourceForFile(location, filename), opts); + }, + + upload: function (location, file, data) { + data = data || {}; + + var filename = data.filename || undefined; + return OctoPrint.upload(resourceForLocation(location), file, filename, data); + }, + + download: function (location, filename, opts) { + var deferred = $.Deferred(); + getFile(location, filename, opts) + .done(function (response) { + OctoPrint.download(response.refs.download, opts) + .done(function () { + deferred.resolve.apply(null, arguments); + }) + .fail(function () { + deferred.reject.apply(null, arguments); + }); + }) + .fail(function () { + deferred.reject.apply(null, arguments); + }); + return deferred.promise(); + } + } +}); diff --git a/src/octoprint/static/js/app/client/job.js b/src/octoprint/static/js/app/client/job.js new file mode 100644 index 00000000..133058cb --- /dev/null +++ b/src/octoprint/static/js/app/client/job.js @@ -0,0 +1,31 @@ +(function (global, factory) { + if (typeof define === "function" && define.amd) { + define(["OctoPrint"], factory); + } else { + factory(window.OctoPrint); + } +})(window || this, function(OctoPrint) { + var url = "api/job"; + + var issueCommand = function(command, opts) { + return OctoPrint.issueCommand(url, command, {}, opts); + }; + + OctoPrint.job = { + get: function(opts) { + return OctoPrint.get(url, opts); + }, + start: function(opts) { + return issueCommand("start", opts); + }, + restart: function(opts) { + return issueCommand("restart", opts); + }, + pause: function(opts) { + return issueCommand("pause", opts); + }, + cancel: function(opts) { + return issueCommand("cancel", opts); + } + } +}); diff --git a/src/octoprint/static/js/app/client/languages.js b/src/octoprint/static/js/app/client/languages.js new file mode 100644 index 00000000..2ebec0d5 --- /dev/null +++ b/src/octoprint/static/js/app/client/languages.js @@ -0,0 +1,22 @@ +(function (global, factory) { + if (typeof define === "function" && define.amd) { + define(["OctoPrint"], factory); + } else { + factory(window.OctoPrint); + } +})(window || this, function(OctoPrint) { + var url = "api/languages"; + + OctoPrint.languages = { + list: function(opts) { + return OctoPrint.get(url, opts); + }, + upload: function(file) { + return OctoPrint.upload(url, file); + }, + delete: function(locale, pack, opts) { + var packUrl = url + "/" + locale + "/" + pack; + return OctoPrint.delete(packUrl, opts); + } + }; +}); diff --git a/src/octoprint/static/js/app/client/logs.js b/src/octoprint/static/js/app/client/logs.js new file mode 100644 index 00000000..a1e652e4 --- /dev/null +++ b/src/octoprint/static/js/app/client/logs.js @@ -0,0 +1,25 @@ +(function (global, factory) { + if (typeof define === "function" && define.amd) { + define(["OctoPrint"], factory); + } else { + factory(window.OctoPrint); + } +})(window || this, function(OctoPrint) { + var url = "api/logs"; + + OctoPrint.logs = { + list: function(opts) { + return OctoPrint.get(url, opts); + }, + + delete: function(file, opts) { + var fileUrl = url + "/" + file; + return OctoPrint.delete(fileUrl, opts); + }, + + download: function(file, opts) { + var fileUrl = url + "/" + file; + return OctoPrint.download(fileUrl, opts); + } + } +}); diff --git a/src/octoprint/static/js/app/client/printer.js b/src/octoprint/static/js/app/client/printer.js new file mode 100644 index 00000000..844a438a --- /dev/null +++ b/src/octoprint/static/js/app/client/printer.js @@ -0,0 +1,207 @@ +(function (global, factory) { + if (typeof define === "function" && define.amd) { + define(["OctoPrint"], factory); + } else { + factory(window.OctoPrint); + } +})(window || this, function(OctoPrint) { + var url = "api/printer"; + var printheadUrl = url + "/printhead"; + var toolUrl = url + "/tool"; + var bedUrl = url + "/bed"; + var sdUrl = url + "/sd"; + + var issuePrintheadCommand = function (command, payload, opts) { + return OctoPrint.issueCommand(printheadUrl, command, payload, opts); + }; + + var issueToolCommand = function (command, payload, opts) { + return OctoPrint.issueCommand(toolUrl, command, payload, opts); + }; + + var issueBedCommand = function (command, payload, opts) { + return OctoPrint.issueCommand(bedUrl, command, payload, opts); + }; + + var issueSdCommand = function (command, payload, opts) { + return OctoPrint.issueCommand(sdUrl, command, payload, opts); + }; + + OctoPrint.printer = { + getFullState: function (data, opts) { + data = data || {}; + + var history = data.history || undefined; + var limit = data.limit || undefined; + var exclude = data.exclude || undefined; + + var getUrl = url; + if (history || exclude) { + getUrl += "?"; + if (history) { + getUrl += "history=true&"; + if (limit) { + getUrl += "limit=" + limit + "&"; + } + } + + if (exclude) { + getUrl += "exclude=" + exclude.join(",") + "&"; + } + } + + return OctoPrint.get(getUrl, opts); + }, + + getToolState: function (data, opts) { + data = data || {}; + + var history = data.history || undefined; + var limit = data.limit || undefined; + + var getUrl = toolUrl; + if (history) { + getUrl += "?history=true"; + if (limit) { + getUrl += "&limit=" + limit; + } + } + + return OctoPrint.get(getUrl, opts); + }, + + getBedState: function (data, opts) { + data = data || {}; + + var history = data.history || undefined; + var limit = data.limit || undefined; + + var getUrl = bedUrl; + if (history) { + getUrl += "?history=true"; + if (limit) { + getUrl += "&limit=" + limit; + } + } + + return OctoPrint.get(getUrl, opts); + }, + + getSdState: function (opts) { + return OctoPrint.get(sdUrl, opts); + }, + + jog: function (data, opts) { + data = data || {}; + + var payload = {}; + if (data.x) payload.x = data.x; + if (data.y) payload.y = data.y; + if (data.z) payload.z = data.z; + + return issuePrintheadCommand("jog", payload, opts); + }, + + home: function (axes, opts) { + axes = axes || []; + + var payload = { + axes: axes + }; + + return issuePrintheadCommand("home", payload, opts); + }, + + setFeedrate: function (factor, opts) { + factor = factor || 100; + + var payload = { + factor: factor + }; + + return issuePrintheadCommand("feedrate", payload, opts); + }, + + setToolTargetTemperatures: function (targets, opts) { + targets = targets || {}; + + var payload = { + targets: targets + }; + + return issueToolCommand("target", payload, opts); + }, + + setToolTemperatureOffsets: function (offsets, opts) { + offsets = offsets || {}; + + var payload = { + offsets: offsets + }; + + return issueToolCommand("offset", payload, opts); + }, + + selectTool: function (tool, opts) { + tool = tool || undefined; + + var payload = { + tool: tool + }; + + return issueToolCommand("select", payload, opts); + }, + + extrude: function (amount, opts) { + amount = amount || undefined; + + var payload = { + amount: amount + }; + + return issueToolCommand("extrude", payload, opts); + }, + + setFlowrate: function (factor, opts) { + factor = factor || 100; + + var payload = { + factor: factor + }; + + return issueToolCommand("flowrate", payload, opts); + }, + + setBedTargetTemperature: function (temperature, opts) { + temperature = temperature || 0; + + var payload = { + target: temperature + }; + + return issueBedCommand("target", payload, opts); + }, + + setBedTemperatureOffset: function (offset, opts) { + offset = offset || 0; + + var payload = { + offset: offset + }; + + return issueBedCommand("offset", payload, opts); + }, + + initSd: function (opts) { + return issueSdCommand("init", {}, opts); + }, + + refreshSd: function (opts) { + return issueSdCommand("refresh", {}, opts); + }, + + releaseSd: function (opts) { + return issueSdCommand("release", {}, opts); + } + } +}); diff --git a/src/octoprint/static/js/app/client/printerprofiles.js b/src/octoprint/static/js/app/client/printerprofiles.js new file mode 100644 index 00000000..d03efb3e --- /dev/null +++ b/src/octoprint/static/js/app/client/printerprofiles.js @@ -0,0 +1,43 @@ +(function (global, factory) { + if (typeof define === "function" && define.amd) { + define(["OctoPrint", "jquery"], factory); + } else { + factory(window.OctoPrint, window.$); + } +})(window || this, function(OctoPrint, $) { + var url = "api/printerprofiles"; + + var profileUrl = function(profile) { + return url + "/" + profile; + }; + + OctoPrint.printerprofiles = { + get: function (opts) { + return OctoPrint.get(url, opts); + }, + + add: function (profile, additional, opts) { + profile = profile || {}; + additional = additional || {}; + + var data = $.extend({}, additional); + data.profile = profile; + + return OctoPrint.postJson(url, data, opts); + }, + + update: function (id, profile, additional, opts) { + profile = profile || {}; + additional = addtional || {}; + + var data = $.extend({}, additional); + data.profile = profile; + + return OctoPrint.patchJson(profileUrl(id), data, opts); + }, + + delete: function (id, opts) { + return OctoPrint.delete(profileUrl(id), opts); + } + } +}); diff --git a/src/octoprint/static/js/app/client/settings.js b/src/octoprint/static/js/app/client/settings.js new file mode 100644 index 00000000..d8d15fe2 --- /dev/null +++ b/src/octoprint/static/js/app/client/settings.js @@ -0,0 +1,43 @@ +(function (global, factory) { + if (typeof define === "function" && define.amd) { + define(["OctoPrint", "jquery"], factory); + } else { + factory(window.OctoPrint, window.$); + } +})(window || this, function(OctoPrint, $) { + var url = "api/settings"; + + var get = function(opts) { + return OctoPrint.get(url, opts); + }; + + var save = function(settings, opts) { + settings = settings || {}; + return OctoPrint.postJson(url, settings, opts); + }; + + OctoPrint.settings = { + get: get, + save: save, + + getPluginSettings: function (plugin, opts) { + return get(opts) + .then(function (settings, statusText, request) { + if (!settings.plugins || !settings.plugins[plugin]) { + return $.Deferred() + .reject(request, "dataerror", "No settings for plugin " + plugin) + .promise(); + } else { + return settings.plugins[plugin]; + } + }); + }, + + savePluginSettings: function (plugin, settings, opts) { + var data = {}; + data["plugins"] = {}; + data["plugins"][plugin] = settings; + return save(data, opts); + } + } +}); diff --git a/src/octoprint/static/js/app/client/slicing.js b/src/octoprint/static/js/app/client/slicing.js new file mode 100644 index 00000000..268ca361 --- /dev/null +++ b/src/octoprint/static/js/app/client/slicing.js @@ -0,0 +1,45 @@ +(function (global, factory) { + if (typeof define === "function" && define.amd) { + define(["OctoPrint"], factory); + } else { + factory(window.OctoPrint); + } +})(window || this, function(OctoPrint) { + var url = "api/slicing"; + + var slicerUrl = function(slicer) { + return url + "/" + slicer; + }; + + var profileUrl = function(slicer, profileId) { + return slicerUrl(slicer) + "/profiles/" + profileId; + }; + + OctoPrint.slicing = { + listAllSlicersAndProfiles: function(opts) { + return OctoPrint.get(url, opts); + }, + + listProfilesForSlicer: function(slicer, opts) { + return OctoPrint.get(slicerUrl(slicer) + "/profiles", opts); + }, + + getProfileForSlicer: function(slicer, profileId, opts) { + return OctoPrint.get(profileUrl(slicer, profileId), opts); + }, + + addProfileForSlicer: function(slicer, profileId, profile, opts) { + profile = profile || {}; + return OctoPrint.putJson(profileUrl(slicer, profileId), profile, opts); + }, + + updateProfileForSlicer: function(slicer, profileId, profile, opts) { + profile = profile || {}; + return OctoPrint.patchJson(profileUrl(slicer, profileId), profile, opts); + }, + + deleteProfileForSlicer: function(slicer, profileId, opts) { + return OctoPrint.delete(profileUrl(slicer, profileId), opts); + } + } +}); diff --git a/src/octoprint/static/js/app/client/socket.js b/src/octoprint/static/js/app/client/socket.js new file mode 100644 index 00000000..9777777f --- /dev/null +++ b/src/octoprint/static/js/app/client/socket.js @@ -0,0 +1,112 @@ +(function (global, factory) { + if (typeof define === "function" && define.amd) { + define(["OctoPrint", "jquery", "lodash", "sockjs"], factory); + } else { + factory(window.OctoPrint, window.$, window._, window.SockJS); + } +})(window || this, function(OctoPrint, $, _, SockJS) { + var exports = {}; + + exports.options = { + timeouts: [0, 1, 1, 2, 3, 5, 8, 13, 20, 40, 100] + }; + + var normalClose = 1000; + + var socket = undefined; + var reconnecting = false; + var reconnectTrial = 0; + var registeredHandlers = {}; + + var onOpen = function() { + reconnecting = false; + reconnectTrial = 0; + }; + + var onClose = function(e) { + if (e.code == normalClose) { + return; + } + + if (exports.onReconnectAttempt(reconnectTrial)) { + return; + } + + if (reconnectTrial < exports.options.timeouts.length) { + var timeout = exports.options.timeouts[reconnectTrial]; + setTimeout(exports.reconnect, timeout * 1000); + reconnectTrial++; + } else { + exports.onReconnectFailed(); + } + }; + + var onMessage = function(msg) { + _.each(msg.data, function(data, key) { + propagateMessage(key, data); + }); + }; + + var propagateMessage = function(event, data) { + if (!registeredHandlers.hasOwnProperty(event)) { + return; + } + + var eventObj = {event: event, data: data}; + + var catchAllHandlers = registeredHandlers["*"]; + if (catchAllHandlers && catchAllHandlers.length) { + _.each(catchAllHandlers, function(handler) { + handler({event: eventObj}) + }); + } + + var handlers = registeredHandlers[event]; + if (handlers && handlers.length) { + _.each(handlers, function(handler) { + handler(eventObj); + }); + } + }; + + exports.connect = function(opts) { + opts = opts || {}; + + exports.disconnect(); + + var url = OctoPrint.options.baseurl; + if (!_.endsWith(url, "/")) { + url += "/"; + } + + socket = new SockJS(url + "sockjs", undefined, opts); + socket.onopen = onOpen; + socket.onclose = onClose; + socket.onmessage = onMessage; + }; + + exports.reconnect = function() { + exports.disconnect(); + socket = undefined; + exports.connect(); + }; + + exports.disconnect = function() { + if (socket != undefined) { + socket.close(); + } + }; + + exports.onMessage = function(message, handler) { + if (!registeredHandlers.hasOwnProperty(message)) { + registeredHandlers[message] = []; + } + registeredHandlers[message].push(handler); + return exports; + }; + + exports.onReconnectAttempt = function(trial) {}; + exports.onReconnectFailed = function() {}; + + OctoPrint.socket = exports; +}); diff --git a/src/octoprint/static/js/app/client/system.js b/src/octoprint/static/js/app/client/system.js new file mode 100644 index 00000000..1a86419a --- /dev/null +++ b/src/octoprint/static/js/app/client/system.js @@ -0,0 +1,24 @@ +(function (global, factory) { + if (typeof define === "function" && define.amd) { + define(["OctoPrint"], factory); + } else { + factory(window.OctoPrint); + } +})(window || this, function(OctoPrint) { + var url = "api/system"; + var commandUrl = "api/system/commands"; + + OctoPrint.system = { + getCommands: function (opts) { + return OctoPrint.get(commandUrl, opts); + }, + + getCommandsForSource: function (source, opts) { + return OctoPrint.get(commandUrl + "/" + source, opts); + }, + + executeCommand: function (source, action, opts) { + return OctoPrint.postJson(commandUrl + "/" + source + "/" + action, {}, opts); + } + }; +}); diff --git a/src/octoprint/static/js/app/client/timelapse.js b/src/octoprint/static/js/app/client/timelapse.js new file mode 100644 index 00000000..5acaeef8 --- /dev/null +++ b/src/octoprint/static/js/app/client/timelapse.js @@ -0,0 +1,60 @@ +(function (global, factory) { + if (typeof define === "function" && define.amd) { + define(["OctoPrint", "jquery"], factory); + } else { + factory(window.OctoPrint, window.$); + } +})(window || this, function(OctoPrint, $) { + var url = "api/timelapse"; + + var timelapseUrl = function(filename) { + return url + "/" + filename; + }; + + var getTimelapseData = function (opts) { + return OctoPrint.get(url, opts); + }; + + OctoPrint.timelapse = { + get: getTimelapseData, + + list: function (opts) { + var deferred = $.Deferred(); + + getTimelapseData(opts) + .done(function (response, status, request) { + deferred.resolve(response.files, status, request); + }) + .fail(function () { + deferred.reject.apply(null, arguments); + }); + + return deferred.promise(); + }, + + download: function (filename, opts) { + return OctoPrint.download(timelapseUrl(filename), opts); + }, + + delete: function (filename, opts) { + return OctoPrint.delete(timelapseUrl(filename), opts); + }, + + getConfig: function (opts) { + var deferred = $.Deferred(); + getTimelapseData(opts) + .done(function (response, status, request) { + deferred.resolve(response.config, status, request); + }) + .fail(function () { + deferred.reject.apply(null, arguments); + }); + return deferred.promise(); + }, + + saveConfig: function (config, opts) { + config = config || {}; + return OctoPrint.postJson(url, config, opts); + } + } +}); diff --git a/src/octoprint/static/js/app/client/users.js b/src/octoprint/static/js/app/client/users.js new file mode 100644 index 00000000..4c715839 --- /dev/null +++ b/src/octoprint/static/js/app/client/users.js @@ -0,0 +1,110 @@ +(function (global, factory) { + if (typeof define === "function" && define.amd) { + define(["OctoPrint"], factory); + } else { + factory(window.OctoPrint); + } +})(window || this, function(OctoPrint) { + var baseUrl = "api/users"; + + var url = function() { + if (arguments.length) { + return baseUrl + "/" + Array.prototype.join.call(arguments, "/"); + } else { + return baseUrl; + } + }; + + OctoPrint.users = { + list: function (opts) { + return OctoPrint.get(url(), opts); + }, + + add: function (user, opts) { + if (!user.name || !user.password) { + throw new OctoPrint.InvalidArgumentError("Both user's name and password need to be set"); + } + + var data = { + name: user.name, + password: user.password, + active: user.hasOwnProperty("active") ? !!user.active : true, + admin: user.hasOwnProperty("admin") ? !!user.admin : false + }; + + return OctoPrint.postJson(url(), data, opts); + }, + + get: function (name, opts) { + if (!name) { + throw new OctoPrint.InvalidArgumentError("user name must be set"); + } + + return OctoPrint.get(url(name), opts); + }, + + update: function (name, active, admin, opts) { + if (!name) { + throw new OctoPrint.InvalidArgumentError("user name must be set"); + } + + var data = { + active: !!active, + admin: !!admin + }; + return OctoPrint.putJson(url(name), data, opts); + }, + + delete: function (name, opts) { + if (!name) { + throw new OctoPrint.InvalidArgumentError("user name must be set"); + } + + return OctoPrint.delete(url(name), opts); + }, + + changePassword: function (name, password, opts) { + if (!name || !password) { + throw new OctoPrint.InvalidArgumentError("user name and password must be set"); + } + + var data = { + password: password + }; + return OctoPrint.putJson(url(name, "password"), data, opts); + }, + + generateApiKey: function (name, opts) { + if (!name) { + throw new OctoPrint.InvalidArgumentError("user name must be set"); + } + + return OctoPrint.postJson(url(name, "apikey"), opts); + }, + + resetApiKey: function (name, opts) { + if (!name) { + throw new OctoPrint.InvalidArgumentError("user name must be set"); + } + + return OctoPrint.delete(url(name, "apikey"), opts); + }, + + getSettings: function (name, opts) { + if (!name) { + throw new OctoPrint.InvalidArgumentError("user name must be set"); + } + + return OctoPrint.get(url(name, "settings"), opts); + }, + + saveSettings: function (name, settings, opts) { + if (!name) { + throw new OctoPrint.InvalidArgumentError("user name must be set"); + } + + settings = settings || {}; + return OctoPrint.patchJson(url(name, "settings"), settings, opts); + } + }; +}); diff --git a/src/octoprint/static/js/app/client/util.js b/src/octoprint/static/js/app/client/util.js new file mode 100644 index 00000000..b06259fc --- /dev/null +++ b/src/octoprint/static/js/app/client/util.js @@ -0,0 +1,47 @@ +(function (global, factory) { + if (typeof define === "function" && define.amd) { + define(["OctoPrint", "jquery"], factory); + } else { + factory(window.OctoPrint, window.$); + } +})(window || this, function(OctoPrint, $) { + var url = "api/util"; + var testUrl = url + "/test"; + + var test = function(command, data, opts) { + return OctoPrint.issueCommand(testUrl, command, data, opts); + }; + + OctoPrint.util = { + test: test, + + testPath: function(path, additional, opts) { + additional = additional || {}; + + var data = $.extend({}, additional); + data.path = path; + + return test("path", data, opts); + }, + + testExecutable: function(path, additional, opts) { + additional = additional || {}; + + var data = $.extend({}, additional); + data.path = path; + data.check_type = "file"; + data.check_access = "x"; + + return test("path", data, opts); + }, + + testUrl: function(url, additional, opts) { + additional = additional || {}; + + var data = $.extend({}, additional); + data.url = url; + + return test("url", data, opts); + } + }; +}); diff --git a/src/octoprint/static/js/app/client/wizard.js b/src/octoprint/static/js/app/client/wizard.js new file mode 100644 index 00000000..e848e511 --- /dev/null +++ b/src/octoprint/static/js/app/client/wizard.js @@ -0,0 +1,18 @@ +(function (global, factory) { + if (typeof define === "function" && define.amd) { + define(["OctoPrint"], factory); + } else { + factory(window.OctoPrint); + } +})(window || this, function(OctoPrint) { + var url = "api/setup/wizard"; + + OctoPrint.wizard = { + get: function(opts) { + return OctoPrint.get(url, opts); + }, + finish: function(handled, opts) { + return OctoPrint.postJson(url, {handled: handled || []}, opts); + } + }; +}); diff --git a/src/octoprint/static/js/app/dataupdater.js b/src/octoprint/static/js/app/dataupdater.js index 52293417..443f657d 100644 --- a/src/octoprint/static/js/app/dataupdater.js +++ b/src/octoprint/static/js/app/dataupdater.js @@ -3,12 +3,6 @@ function DataUpdater(allViewModels) { self.allViewModels = allViewModels; - self._socket = undefined; - self._autoReconnecting = false; - self._autoReconnectTrial = 0; - self._autoReconnectTimeouts = [0, 1, 1, 2, 3, 5, 8, 13, 20, 40, 100]; - self._autoReconnectDialogIndex = 1; - self._pluginHash = undefined; self._configHash = undefined; @@ -16,65 +10,39 @@ function DataUpdater(allViewModels) { $("#reloadui_overlay_reload").click(function() { location.reload(true); }); self.connect = function() { - var options = {}; - if (SOCKJS_DEBUG) { - options["debug"] = true; - } - - self._socket = new SockJS(SOCKJS_URI, undefined, options); - self._socket.onopen = self._onconnect; - self._socket.onclose = self._onclose; - self._socket.onmessage = self._onmessage; + OctoPrint.socket.connect({debug: !!SOCKJS_DEBUG}); }; self.reconnect = function() { - self._socket.close(); - delete self._socket; - self.connect(); + OctoPrint.socket.reconnect(); }; - self._onconnect = function() { - self._autoReconnecting = false; - self._autoReconnectTrial = 0; - }; - - self._onclose = function(e) { - if (e.code == SOCKJS_CLOSE_NORMAL) { + self._onReconnectAttempt = function(trial) { + if (trial <= 0) { + // Only consider it a real disconnect if the trial number has exceeded our threshold. return; } - if (self._autoReconnectTrial >= self._autoReconnectDialogIndex) { - // Only consider it a real disconnect if the trial number has exceeded our threshold. - var handled = false; - callViewModelsIf( - self.allViewModels, - "onServerDisconnect", - function() { return !handled; }, - function(method) { handled = !method() || handled; } - ); + var handled = false; + callViewModelsIf( + self.allViewModels, + "onServerDisconnect", + function() { return !handled; }, + function(method) { handled = !method() || handled; } + ); - if (handled) { - return; - } - - showOfflineOverlay( - gettext("Server is offline"), - gettext("The server appears to be offline, at least I'm not getting any response from it. I'll try to reconnect automatically over the next couple of minutes, however you are welcome to try a manual reconnect anytime using the button below."), - self.reconnect - ); + if (handled) { + return true; } - if (self._autoReconnectTrial < self._autoReconnectTimeouts.length) { - var timeout = self._autoReconnectTimeouts[self._autoReconnectTrial]; - log.info("Reconnect trial #" + self._autoReconnectTrial + ", waiting " + timeout + "s"); - setTimeout(self.reconnect, timeout * 1000); - self._autoReconnectTrial++; - } else { - self._onreconnectfailed(); - } + showOfflineOverlay( + gettext("Server is offline"), + gettext("The server appears to be offline, at least I'm not getting any response from it. I'll try to reconnect automatically over the next couple of minutes, however you are welcome to try a manual reconnect anytime using the button below."), + self.reconnect + ); }; - self._onreconnectfailed = function() { + self._onReconnectFailed = function() { var handled = false; callViewModelsIf( self.allViewModels, @@ -91,182 +59,189 @@ function DataUpdater(allViewModels) { $("#offline_overlay_message").html(gettext("The server appears to be offline, at least I'm not getting any response from it. I could not reconnect automatically, but you may try a manual reconnect using the button below.")); }; - self._onmessage = function(e) { - for (var prop in e.data) { - if (!e.data.hasOwnProperty(prop)) { - continue; + self._onConnected = function(event) { + var data = event.data; + + // update version information + var oldVersion = VERSION; + VERSION = data["version"]; + DISPLAY_VERSION = data["display_version"]; + BRANCH = data["branch"]; + $("span.version").text(DISPLAY_VERSION); + + // update plugin hash + var oldPluginHash = self._pluginHash; + self._pluginHash = data["plugin_hash"]; + + // update config hash + var oldConfigHash = self._configHash; + self._configHash = data["config_hash"]; + + // if the offline overlay is still showing, now's a good time to + // hide it, plus reload the camera feed if it's currently displayed + if ($("#offline_overlay").is(":visible")) { + hideOfflineOverlay(); + callViewModels(self.allViewModels, "onDataUpdaterReconnect"); + + if ($('#tabs li[class="active"] a').attr("href") == "#control") { + $("#webcam_image").attr("src", CONFIG_WEBCAM_STREAM + "?" + new Date().getTime()); } + } - var data = e.data[prop]; - - var gcodeUploadProgress = $("#gcode_upload_progress"); - var gcodeUploadProgressBar = $(".bar", gcodeUploadProgress); - - switch (prop) { - case "connected": { - // update the current UI API key and send it with any request - UI_API_KEY = data["apikey"]; - $.ajaxSetup({ - headers: {"X-Api-Key": UI_API_KEY} - }); - - var oldVersion = VERSION; - VERSION = data["version"]; - DISPLAY_VERSION = data["display_version"]; - BRANCH = data["branch"]; - $("span.version").text(DISPLAY_VERSION); - - var oldPluginHash = self._pluginHash; - self._pluginHash = data["plugin_hash"]; - - var oldConfigHash = self._configHash; - self._configHash = data["config_hash"]; - - if ($("#offline_overlay").is(":visible")) { - hideOfflineOverlay(); - callViewModels(self.allViewModels, "onDataUpdaterReconnect"); - - if ($('#tabs li[class="active"] a').attr("href") == "#control") { - $("#webcam_image").attr("src", CONFIG_WEBCAM_STREAM + "?" + new Date().getTime()); - } - } - - var versionChanged = oldVersion != VERSION; - var pluginsChanged = oldPluginHash != undefined && oldPluginHash != self._pluginHash; - var configChanged = oldConfigHash != undefined && oldConfigHash != self._configHash; - if (versionChanged || pluginsChanged || configChanged) { - self.reloadOverlay.show(); - } - - break; - } - case "history": { - callViewModels(self.allViewModels, "fromHistoryData", [data]); - break; - } - case "current": { - callViewModels(self.allViewModels, "fromCurrentData", [data]); - break; - } - case "slicingProgress": { - gcodeUploadProgressBar.text(_.sprintf(gettext("Slicing ... (%(percentage)d%%)"), {percentage: Math.round(data["progress"])})); - - callViewModels(self.allViewModels, "onSlicingProgress", [ - data["slicer"], - data["model_path"], - data["machinecode_path"], - data["progress"] - ]); - break; - } - case "event": { - var type = data["type"]; - var payload = data["payload"]; - var html = ""; - var format = {}; - - log.debug("Got event " + type + " with payload: " + JSON.stringify(payload)); - - if (type == "SettingsUpdated") { - if (payload && payload.hasOwnProperty("config_hash")) { - self._configHash = payload.config_hash; - } - } else if (type == "MovieRendering") { - new PNotify({title: gettext("Rendering timelapse"), text: _.sprintf(gettext("Now rendering timelapse %(movie_basename)s"), payload)}); - } else if (type == "MovieDone") { - new PNotify({title: gettext("Timelapse ready"), text: _.sprintf(gettext("New timelapse %(movie_basename)s is done rendering."), payload)}); - } else if (type == "MovieFailed") { - html = "

" + _.sprintf(gettext("Rendering of timelapse %(movie_basename)s failed with return code %(returncode)s"), payload) + "

"; - html += pnotifyAdditionalInfo('
' + payload.error + '
'); - new PNotify({ - title: gettext("Rendering failed"), - text: html, - type: "error", - hide: false - }); - } else if (type == "PostRollStart") { - if (payload.postroll_duration > 60) { - format = {duration: _.sprintf(gettext("%(minutes)d min"), {minutes: payload.postroll_duration / 60})}; - } else { - format = {duration: _.sprintf(gettext("%(seconds)d sec"), {seconds: payload.postroll_duration})}; - } - - new PNotify({ - title: gettext("Capturing timelapse postroll"), - text: _.sprintf(gettext("Now capturing timelapse post roll, this will take approximately %(duration)s..."), format) - }); - } else if (type == "SlicingStarted") { - gcodeUploadProgress.addClass("progress-striped").addClass("active"); - gcodeUploadProgressBar.css("width", "100%"); - if (payload.progressAvailable) { - gcodeUploadProgressBar.text(_.sprintf(gettext("Slicing ... (%(percentage)d%%)"), {percentage: 0})); - } else { - gcodeUploadProgressBar.text(gettext("Slicing ...")); - } - } else if (type == "SlicingDone") { - gcodeUploadProgress.removeClass("progress-striped").removeClass("active"); - gcodeUploadProgressBar.css("width", "0%"); - gcodeUploadProgressBar.text(""); - new PNotify({title: gettext("Slicing done"), text: _.sprintf(gettext("Sliced %(stl)s to %(gcode)s, took %(time).2f seconds"), payload), type: "success"}); - } else if (type == "SlicingCancelled") { - gcodeUploadProgress.removeClass("progress-striped").removeClass("active"); - gcodeUploadProgressBar.css("width", "0%"); - gcodeUploadProgressBar.text(""); - } else if (type == "SlicingFailed") { - gcodeUploadProgress.removeClass("progress-striped").removeClass("active"); - gcodeUploadProgressBar.css("width", "0%"); - gcodeUploadProgressBar.text(""); - - html = _.sprintf(gettext("Could not slice %(stl)s to %(gcode)s: %(reason)s"), payload); - new PNotify({title: gettext("Slicing failed"), text: html, type: "error", hide: false}); - } else if (type == "TransferStarted") { - gcodeUploadProgress.addClass("progress-striped").addClass("active"); - gcodeUploadProgressBar.css("width", "100%"); - gcodeUploadProgressBar.text(gettext("Streaming ...")); - } else if (type == "TransferDone") { - gcodeUploadProgress.removeClass("progress-striped").removeClass("active"); - gcodeUploadProgressBar.css("width", "0%"); - gcodeUploadProgressBar.text(""); - new PNotify({ - title: gettext("Streaming done"), - text: _.sprintf(gettext("Streamed %(local)s to %(remote)s on SD, took %(time).2f seconds"), payload), - type: "success" - }); - gcodeFilesViewModel.requestData(payload.remote, "sdcard"); - } - - var legacyEventHandlers = { - "UpdatedFiles": "onUpdatedFiles", - "MetadataStatisticsUpdated": "onMetadataStatisticsUpdated", - "MetadataAnalysisFinished": "onMetadataAnalysisFinished", - "SlicingDone": "onSlicingDone", - "SlicingCancelled": "onSlicingCancelled", - "SlicingFailed": "onSlicingFailed" - }; - _.each(self.allViewModels, function(viewModel) { - if (viewModel.hasOwnProperty("onEvent" + type)) { - viewModel["onEvent" + type](payload); - } else if (legacyEventHandlers.hasOwnProperty(type) && viewModel.hasOwnProperty(legacyEventHandlers[type])) { - // there might still be code that uses the old callbacks, make sure those still get called - // but log a warning - log.warn("View model " + viewModel.name + " is using legacy event handler " + legacyEventHandlers[type] + ", new handler is called " + legacyEventHandlers[type]); - viewModel[legacyEventHandlers[type]](payload); - } - }); - - break; - } - case "timelapse": { - callViewModels(self.allViewModels, "fromTimelapseData", [data]); - break; - } - case "plugin": { - callViewModels(self.allViewModels, "onDataUpdaterPluginMessage", [data.plugin, data.data]); - break; - } - } + // if the version, the plugin hash or the config hash changed, we + // want the user to reload the UI since it might be stale now + var versionChanged = oldVersion != VERSION; + var pluginsChanged = oldPluginHash != undefined && oldPluginHash != self._pluginHash; + var configChanged = oldConfigHash != undefined && oldConfigHash != self._configHash; + if (versionChanged || pluginsChanged || configChanged) { + self.reloadOverlay.show(); } }; + self._onHistoryData = function(event) { + callViewModels(self.allViewModels, "fromHistoryData", [event.data]); + }; + + self._onCurrentData = function(event) { + callViewModels(self.allViewModels, "fromCurrentData", [event.data]); + }; + + self._onSlicingProgress = function(event) { + $("#gcode_upload_progress").find(".bar").text(_.sprintf(gettext("Slicing ... (%(percentage)d%%)"), {percentage: Math.round(event.data["progress"])})); + + callViewModels(self.allViewModels, "onSlicingProgress", [ + data["slicer"], + data["model_path"], + data["machinecode_path"], + data["progress"] + ]); + }; + + self._onEvent = function(event) { + var gcodeUploadProgress = $("#gcode_upload_progress"); + var gcodeUploadProgressBar = $(".bar", gcodeUploadProgress); + + var type = event.data["type"]; + var payload = event.data["payload"]; + var html = ""; + var format = {}; + + log.debug("Got event " + type + " with payload: " + JSON.stringify(payload)); + + if (type == "SettingsUpdated") { + if (payload && payload.hasOwnProperty("config_hash")) { + self._configHash = payload.config_hash; + } + } else if (type == "MovieRendering") { + new PNotify({title: gettext("Rendering timelapse"), text: _.sprintf(gettext("Now rendering timelapse %(movie_basename)s"), payload)}); + } else if (type == "MovieDone") { + new PNotify({title: gettext("Timelapse ready"), text: _.sprintf(gettext("New timelapse %(movie_basename)s is done rendering."), payload)}); + } else if (type == "MovieFailed") { + html = "

" + _.sprintf(gettext("Rendering of timelapse %(movie_basename)s failed with return code %(returncode)s"), payload) + "

"; + html += pnotifyAdditionalInfo('
' + payload.error + '
'); + new PNotify({ + title: gettext("Rendering failed"), + text: html, + type: "error", + hide: false + }); + } else if (type == "PostRollStart") { + var title = gettext("Capturing timelapse postroll"); + + var text; + if (!payload.postroll_duration) { + text = _.sprintf(gettext("Now capturing timelapse post roll, this will take only a moment..."), format); + } else { + if (payload.postroll_duration > 60) { + format = {duration: _.sprintf(gettext("%(minutes)d min"), {minutes: payload.postroll_duration / 60})}; + } else { + format = {duration: _.sprintf(gettext("%(seconds)d sec"), {seconds: payload.postroll_duration})}; + } + text = _.sprintf(gettext("Now capturing timelapse post roll, this will take approximately %(duration)s..."), format); + } + + new PNotify({ + title: title, + text: text + }); + } else if (type == "SlicingStarted") { + gcodeUploadProgress.addClass("progress-striped").addClass("active"); + gcodeUploadProgressBar.css("width", "100%"); + if (payload.progressAvailable) { + gcodeUploadProgressBar.text(_.sprintf(gettext("Slicing ... (%(percentage)d%%)"), {percentage: 0})); + } else { + gcodeUploadProgressBar.text(gettext("Slicing ...")); + } + } else if (type == "SlicingDone") { + gcodeUploadProgress.removeClass("progress-striped").removeClass("active"); + gcodeUploadProgressBar.css("width", "0%"); + gcodeUploadProgressBar.text(""); + new PNotify({title: gettext("Slicing done"), text: _.sprintf(gettext("Sliced %(stl)s to %(gcode)s, took %(time).2f seconds"), payload), type: "success"}); + } else if (type == "SlicingCancelled") { + gcodeUploadProgress.removeClass("progress-striped").removeClass("active"); + gcodeUploadProgressBar.css("width", "0%"); + gcodeUploadProgressBar.text(""); + } else if (type == "SlicingFailed") { + gcodeUploadProgress.removeClass("progress-striped").removeClass("active"); + gcodeUploadProgressBar.css("width", "0%"); + gcodeUploadProgressBar.text(""); + + html = _.sprintf(gettext("Could not slice %(stl)s to %(gcode)s: %(reason)s"), payload); + new PNotify({title: gettext("Slicing failed"), text: html, type: "error", hide: false}); + } else if (type == "TransferStarted") { + gcodeUploadProgress.addClass("progress-striped").addClass("active"); + gcodeUploadProgressBar.css("width", "100%"); + gcodeUploadProgressBar.text(gettext("Streaming ...")); + } else if (type == "TransferDone") { + gcodeUploadProgress.removeClass("progress-striped").removeClass("active"); + gcodeUploadProgressBar.css("width", "0%"); + gcodeUploadProgressBar.text(""); + new PNotify({ + title: gettext("Streaming done"), + text: _.sprintf(gettext("Streamed %(local)s to %(remote)s on SD, took %(time).2f seconds"), payload), + type: "success" + }); + gcodeFilesViewModel.requestData(payload.remote, "sdcard"); + } + + var legacyEventHandlers = { + "UpdatedFiles": "onUpdatedFiles", + "MetadataStatisticsUpdated": "onMetadataStatisticsUpdated", + "MetadataAnalysisFinished": "onMetadataAnalysisFinished", + "SlicingDone": "onSlicingDone", + "SlicingCancelled": "onSlicingCancelled", + "SlicingFailed": "onSlicingFailed" + }; + _.each(self.allViewModels, function(viewModel) { + if (viewModel.hasOwnProperty("onEvent" + type)) { + viewModel["onEvent" + type](payload); + } else if (legacyEventHandlers.hasOwnProperty(type) && viewModel.hasOwnProperty(legacyEventHandlers[type])) { + // there might still be code that uses the old callbacks, make sure those still get called + // but log a warning + log.warn("View model " + viewModel.name + " is using legacy event handler " + legacyEventHandlers[type] + ", new handler is called " + legacyEventHandlers[type]); + viewModel[legacyEventHandlers[type]](payload); + } + }); + }; + + self._onTimelapse = function(event) { + callViewModels(self.allViewModels, "fromTimelapseData", [event.data]); + }; + + self._onPluginMessage = function(event) { + callViewModels(self.allViewModels, "onDataUpdaterPluginMessage", [event.data.plugin, event.data.data]); + }; + + OctoPrint.socket.onReconnectAttempt = self._onReconnectAttempt; + OctoPrint.socket.onReconnectFailed = self._onReconnectFailed; + OctoPrint.socket + .onMessage("connected", self._onConnected) + .onMessage("history", self._onHistoryData) + .onMessage("current", self._onCurrentData) + .onMessage("slicingProgress", self._onSlicingProgress) + .onMessage("event", self._onEvent) + .onMessage("timelapse", self._onTimelapse) + .onMessage("plugin", self._onPluginMessage); + self.connect(); } diff --git a/src/octoprint/static/js/app/main.js b/src/octoprint/static/js/app/main.js index a0627f4a..90d32ead 100644 --- a/src/octoprint/static/js/app/main.js +++ b/src/octoprint/static/js/app/main.js @@ -1,4 +1,6 @@ $(function() { + OctoPrint = window.OctoPrint; + //~~ Lodash setup _.mixin({"sprintf": sprintf, "vsprintf": vsprintf}); @@ -7,6 +9,23 @@ $(function() { log.setLevel(CONFIG_DEBUG ? "debug" : "info"); + //~~ OctoPrint client setup + OctoPrint.options.baseurl = BASEURL; + OctoPrint.options.apikey = UI_API_KEY; + + OctoPrint.socket.onMessage("connected", function(data) { + var payload = data.data; + OctoPrint.options.apikey = payload.apikey; + + // update the API key directly in jquery's ajax options too, + // to ensure the fileupload plugin and any plugins still using + // $.ajax directly still work fine too + UI_API_KEY = payload["apikey"]; + $.ajaxSetup({ + headers: {"X-Api-Key": UI_API_KEY} + }); + }); + //~~ AJAX setup // work around a stupid iOS6 bug where ajax requests get cached and only work once, as described at @@ -62,6 +81,26 @@ $(function() { PNotify.prototype.options.styling = "bootstrap2"; PNotify.prototype.options.mouse_reset = false; + PNotify.singleButtonNotify = function(options) { + if (!options.confirm || !options.confirm.buttons || !options.confirm.buttons.length) { + return new PNotify(options); + } + + var autoDisplay = options.auto_display != false; + + var params = $.extend(true, {}, options); + params.auto_display = false; + + var notify = new PNotify(params); + notify.options.confirm.buttons = [notify.options.confirm.buttons[0]]; + notify.modules.confirm.makeDialog(notify, notify.options.confirm); + + if (autoDisplay) { + notify.open(); + } + return notify; + }; + //~~ Initialize view models // the view model map is our basic look up table for dependencies that may be injected into other view models @@ -487,7 +526,8 @@ $(function() { if (!_.has(viewModelMap, "settingsViewModel")) { throw new Error("settingsViewModel is missing, can't run UI") } - viewModelMap["settingsViewModel"].requestData(bindViewModels); + viewModelMap["settingsViewModel"].requestData() + .done(bindViewModels); } ); diff --git a/src/octoprint/static/js/app/viewmodels/connection.js b/src/octoprint/static/js/app/viewmodels/connection.js index 7f55d607..d501e7ec 100644 --- a/src/octoprint/static/js/app/viewmodels/connection.js +++ b/src/octoprint/static/js/app/viewmodels/connection.js @@ -47,14 +47,8 @@ $(function() { self.previousIsOperational = undefined; self.requestData = function() { - $.ajax({ - url: API_BASEURL + "connection", - method: "GET", - dataType: "json", - success: function(response) { - self.fromResponse(response); - } - }) + OctoPrint.connection.getSettings() + .done(self.fromResponse); }; self.fromResponse = function(response) { @@ -93,7 +87,7 @@ $(function() { } else if (!self.isOperational() && !connectionTab.hasClass("in")) { connectionTab.collapse("show"); } - } + }; self._processStateData = function(data) { self.previousIsOperational = self.isOperational(); @@ -116,7 +110,6 @@ $(function() { self.connect = function() { if (self.isErrorOrClosed()) { var data = { - "command": "connect", "port": self.selectedPort() || "AUTO", "baudrate": self.selectedBaudrate() || 0, "printerProfile": self.selectedPrinter(), @@ -126,26 +119,14 @@ $(function() { if (self.saveSettings()) data["save"] = true; - $.ajax({ - url: API_BASEURL + "connection", - type: "POST", - dataType: "json", - contentType: "application/json; charset=UTF-8", - data: JSON.stringify(data), - success: function(response) { + OctoPrint.connection.connect(data) + .done(function() { self.settings.requestData(); self.settings.printerProfiles.requestData(); - } - }); + }); } else { self.requestData(); - $.ajax({ - url: API_BASEURL + "connection", - type: "POST", - dataType: "json", - contentType: "application/json; charset=UTF-8", - data: JSON.stringify({"command": "disconnect"}) - }) + OctoPrint.connection.disconnect(); } }; diff --git a/src/octoprint/static/js/app/viewmodels/control.js b/src/octoprint/static/js/app/viewmodels/control.js index d8ab3457..a8680e87 100644 --- a/src/octoprint/static/js/app/viewmodels/control.js +++ b/src/octoprint/static/js/app/viewmodels/control.js @@ -108,14 +108,10 @@ $(function() { }; self.requestData = function () { - $.ajax({ - url: API_BASEURL + "printer/command/custom", - method: "GET", - dataType: "json", - success: function (response) { + OctoPrint.control.getCustomControls() + .done(function(response) { self._fromResponse(response); - } - }); + }); }; self._fromResponse = function (response) { @@ -254,26 +250,17 @@ $(function() { multiplier *= -1; } - var data = { - "command": "jog" - }; + var data = {}; data[axis] = distance * multiplier; - - self.sendPrintHeadCommand(data); + OctoPrint.printer.jog(data); }; self.sendHomeCommand = function (axis) { - self.sendPrintHeadCommand({ - "command": "home", - "axes": axis - }); + OctoPrint.printer.home(axis); }; self.sendFeedRateCommand = function () { - self.sendPrintHeadCommand({ - "command": "feedrate", - "factor": self.feedRate() - }); + OctoPrint.printer.setFeedrate(self.feedRate()); }; self.sendExtrudeCommand = function () { @@ -285,90 +272,46 @@ $(function() { }; self.sendFlowRateCommand = function () { - self.sendToolCommand({ - "command": "flowrate", - "factor": self.flowRate() - }); + OctoPrint.printer.setFlowrate(self.flowRate()); }; self._sendECommand = function (dir) { - var length = self.extrusionAmount(); - if (!length) length = self.settings.printer_defaultExtrusionLength(); - - self.sendToolCommand({ - command: "extrude", - amount: length * dir - }); + var length = self.extrusionAmount() || self.settings.printer_defaultExtrusionLength(); + OctoPrint.printer.extrude(length * dir); }; self.sendSelectToolCommand = function (data) { if (!data || !data.key()) return; - self.sendToolCommand({ - command: "select", - tool: data.key() - }); - }; - - self.sendPrintHeadCommand = function (data) { - $.ajax({ - url: API_BASEURL + "printer/printhead", - type: "POST", - dataType: "json", - contentType: "application/json; charset=UTF-8", - data: JSON.stringify(data) - }); - }; - - self.sendToolCommand = function (data) { - $.ajax({ - url: API_BASEURL + "printer/tool", - type: "POST", - dataType: "json", - contentType: "application/json; charset=UTF-8", - data: JSON.stringify(data) - }); + OctoPrint.printer.selectTool(data.key()); }; self.sendCustomCommand = function (command) { if (!command) return; - var data = undefined; - if (command.hasOwnProperty("command")) { - // single command - data = {"command": command.command}; - } else if (command.hasOwnProperty("commands")) { - // multi command - data = {"commands": command.commands}; - } else if (command.hasOwnProperty("script")) { - data = {"script": command.script}; - if (command.hasOwnProperty("context")) { - data["context"] = command.context; + if (command.hasOwnProperty("command") || command.hasOwnProperty("commands")) { + var commands = command.commands || [command.command]; + + if (commands.hasOwnProperty("input")) { + var parameters = {}; + _.each(command.input, function(input) { + if (!input.hasOwnProperty("parameter") || !input.hasOwnProperty("value")) { + return; + } + + parameters[input.parameter] = input.value(); + }); + OctoPrint.control.sendGcodeWithParameters(commands, parameters); + } else { + OctoPrint.control.sendGcode(commands); } - } else { - return; + } else if (command.hasOwnProperty("script")) { + var script = command.script; + var context = command.context || {}; + + OctoPrint.control.sendGcodeScript(script, context); } - - if (command.hasOwnProperty("input")) { - // parametric command(s) - data["parameters"] = {}; - _.each(command.input, function(input) { - if (!input.hasOwnProperty("parameter") || !input.hasOwnProperty("value")) { - return; - } - - data["parameters"][input.parameter] = input.value(); - }); - } - - $.ajax({ - url: API_BASEURL + "printer/command", - type: "POST", - dataType: "json", - contentType: "application/json; charset=UTF-8", - data: JSON.stringify(data) - }) }; self.displayMode = function (customControl) { @@ -414,7 +357,7 @@ $(function() { } else { $("#webcam_rotator").css("height", ""); } - } + }; self.onSettingsBeforeSave = self.updateRotatorWidth; diff --git a/src/octoprint/static/js/app/viewmodels/files.js b/src/octoprint/static/js/app/viewmodels/files.js index 677a88d6..603023e9 100644 --- a/src/octoprint/static/js/app/viewmodels/files.js +++ b/src/octoprint/static/js/app/viewmodels/files.js @@ -165,19 +165,13 @@ $(function() { if (self._otherRequestInProgress) return; self._otherRequestInProgress = true; - $.ajax({ - url: API_BASEURL + "files", - method: "GET", - dataType: "json", - data: {"recursive": true}, - success: function(response) { + OctoPrint.files.list({ data: { recursive: true} }) + .done(function(response) { self.fromResponse(response, filenameToFocus, locationToFocus, switchToPath); + }) + .always(function() { self._otherRequestInProgress = false; - }, - error: function() { - self._otherRequestInProgress = false; - } - }); + }); }; self.fromResponse = function(response, filenameToFocus, locationToFocus, switchToPath) { @@ -244,7 +238,7 @@ $(function() { self.currentPath(""); self.listHelper.updateItems(self.allItems()); } - } + }; self.pathByElement = function(element) { if (!element || element.parent == undefined) @@ -277,20 +271,22 @@ $(function() { }; self.loadFile = function(file, printAfterLoad) { - if (!file || !file.refs || !file.refs.hasOwnProperty("resource")) return; - - $.ajax({ - url: file.refs.resource, - type: "POST", - dataType: "json", - contentType: "application/json; charset=UTF-8", - data: JSON.stringify({command: "select", print: printAfterLoad}) - }); + if (!file) { + return; + } + OctoPrint.files.select(file.origin, file.name) + .done(function() { + if (printAfterLoad) { + OctoPrint.job.start(); + } + }); }; self.removeFile = function(file) { - if (!file || !file.refs || !file.refs.hasOwnProperty("resource")) return; - + if (!file) { + return; + } + var index = self.listHelper.paginatedItems().indexOf(file) + 1; if (index >= self.listHelper.paginatedItems().length) index = index - 2; @@ -301,42 +297,31 @@ $(function() { var fileToFocus = self.listHelper.paginatedItems()[index]; if (fileToFocus) filenameToFocus = fileToFocus.name; - - $.ajax({ - url: file.refs.resource, - type: "DELETE", - success: function() { + + OctoPrint.files.delete(file.origin, file.name) + .done(function() { self.requestData(undefined, filenameToFocus, self.pathByElement(file.parent)); - } - }); + }) }; self.sliceFile = function(file) { - if (!file) return; + if (!file) { + return; + } self.slicing.show(file.origin, file.name, true); }; self.initSdCard = function() { - self._sendSdCommand("init"); + OctoPrint.printer.initSd(); }; self.releaseSdCard = function() { - self._sendSdCommand("release"); + OctoPrint.printer.releaseSd(); }; self.refreshSdFiles = function() { - self._sendSdCommand("refresh"); - }; - - self._sendSdCommand = function(command) { - $.ajax({ - url: API_BASEURL + "printer/sd", - type: "POST", - dataType: "json", - contentType: "application/json; charset=UTF-8", - data: JSON.stringify({command: command}) - }); + OctoPrint.printer.refreshSd(); }; self.downloadLink = function(data) { diff --git a/src/octoprint/static/js/app/viewmodels/gcode.js b/src/octoprint/static/js/app/viewmodels/gcode.js index eb8c358d..b16fd9d1 100644 --- a/src/octoprint/static/js/app/viewmodels/gcode.js +++ b/src/octoprint/static/js/app/viewmodels/gcode.js @@ -29,6 +29,7 @@ $(function() { self.ui_modelInfo = ko.observable(""); self.ui_layerInfo = ko.observable(""); + self.tabActive = false; self.enableReload = ko.observable(false); self.waitForApproval = ko.observable(false); @@ -231,19 +232,20 @@ $(function() { self._configureLayerSlider(layerSliderElement); self._configureLayerCommandSlider(commandSliderElement); - self.settings.requestData(function() { - GCODE.ui.init({ - container: "#gcode_canvas", - onProgress: self._onProgress, - onModelLoaded: self._onModelLoaded, - onLayerSelected: self._onLayerSelected, - bed: self._retrieveBedDimensions(), - toolOffsets: self._retrieveToolOffsets(), - invertAxes: self._retrieveAxesConfiguration() + self.settings.requestData() + .done(function() { + GCODE.ui.init({ + container: "#gcode_canvas", + onProgress: self._onProgress, + onModelLoaded: self._onModelLoaded, + onLayerSelected: self._onLayerSelected, + bed: self._retrieveBedDimensions(), + toolOffsets: self._retrieveToolOffsets(), + invertAxes: self._retrieveAxesConfiguration() + }); + self.synchronizeOptions(); + self.enabled = true; }); - self.synchronizeOptions(); - self.enabled = true; - }); }; self.reset = function() { @@ -289,11 +291,8 @@ $(function() { self.enableReload(false); if (self.status == "idle" && self.errorCount < 3) { self.status = "request"; - $.ajax({ - url: BASEURL + "downloads/files/local/" + filename, - data: { "ctime": date }, - type: "GET", - success: function(response, rstatus) { + OctoPrint.files.download("local", filename) + .done(function(response, rstatus) { if(rstatus === 'success'){ self.showGCodeViewer(response, rstatus); self.loadedFilename = filename; @@ -301,12 +300,11 @@ $(function() { self.status = "idle"; self.enableReload(true); } - }, - error: function() { + }) + .fail(function() { self.status = "idle"; self.errorCount++; - } - }); + }); } }; @@ -358,7 +356,7 @@ $(function() { if(self.loadedFilename && self.loadedFilename == data.job.file.name && self.loadedFileDate == data.job.file.date) { - if (self.currentlyPrinting && self.renderer_syncProgress() && !self.waitForApproval()) { + if (self.tabActive && self.currentlyPrinting && self.renderer_syncProgress() && !self.waitForApproval()) { var cmdIndex = GCODE.gCodeReader.getCmdIndexForPercentage(data.progress.completion); if(cmdIndex){ GCODE.renderer.render(cmdIndex.layer, 0, cmdIndex.cmd); @@ -506,6 +504,10 @@ $(function() { self.initialize(); } + self.onTabChange = function(current, previous) { + self.tabActive = current == "#gcode"; + + } } OCTOPRINT_VIEWMODELS.push([ diff --git a/src/octoprint/static/js/app/viewmodels/log.js b/src/octoprint/static/js/app/viewmodels/log.js index fa42aeed..872eb288 100644 --- a/src/octoprint/static/js/app/viewmodels/log.js +++ b/src/octoprint/static/js/app/viewmodels/log.js @@ -36,12 +36,8 @@ $(function() { ); self.requestData = function() { - $.ajax({ - url: API_BASEURL + "logs", - type: "GET", - dataType: "json", - success: self.fromResponse - }); + OctoPrint.logs.list() + .done(self.fromResponse); }; self.fromResponse = function(response) { @@ -53,12 +49,8 @@ $(function() { }; self.removeFile = function(filename) { - $.ajax({ - url: API_BASEURL + "logs/" + filename, - type: "DELETE", - dataType: "json", - success: self.requestData - }); + OctoPrint.logs.delete(filename) + .done(self.requestData); }; self.onUserLoggedIn = function(user) { @@ -73,4 +65,4 @@ $(function() { ["loginStateViewModel"], "#logs" ]); -}); \ No newline at end of file +}); diff --git a/src/octoprint/static/js/app/viewmodels/loginstate.js b/src/octoprint/static/js/app/viewmodels/loginstate.js index 661b1549..a5dfaf38 100644 --- a/src/octoprint/static/js/app/viewmodels/loginstate.js +++ b/src/octoprint/static/js/app/viewmodels/loginstate.js @@ -32,20 +32,13 @@ $(function() { return; } - $.ajax({ - url: API_BASEURL + "users/" + self.currentUser().name, - type: "GET", - success: self.fromResponse - }) + OctoPrint.users.get(self.currentUser().name) + .done(self.fromResponse); }; self.requestData = function() { - $.ajax({ - url: API_BASEURL + "login", - type: "POST", - data: {"passive": true}, - success: self.fromResponse - }) + OctoPrint.browser.passiveLogin() + .done(self.fromResponse); }; self.fromResponse = function(response) { @@ -70,7 +63,7 @@ $(function() { } }; - self.login = function(u, p, r, callback) { + self.login = function(u, p, r) { var username = u || self.loginUser(); var password = p || self.loginPass(); var remember = (r != undefined ? r : self.loginRemember()); @@ -79,30 +72,22 @@ $(function() { self.loginPass(""); self.loginRemember(false); - $.ajax({ - url: API_BASEURL + "login", - type: "POST", - data: {"user": username, "pass": password, "remember": remember}, - success: function(response) { + return OctoPrint.browser.login(username, password, remember) + .done(function(response) { new PNotify({title: gettext("Login successful"), text: _.sprintf(gettext('You are now logged in as "%(username)s"'), {username: response.name}), type: "success"}); self.fromResponse(response); - if (callback) callback(response); - }, - error: function(jqXHR, textStatus, errorThrown) { + }) + .fail(function() { new PNotify({title: gettext("Login failed"), text: gettext("User unknown or wrong password"), type: "error"}); - } - }) + }); }; self.logout = function() { - $.ajax({ - url: API_BASEURL + "logout", - type: "POST", - success: function(response) { + OctoPrint.browser.logout() + .done(function(response) { new PNotify({title: gettext("Logout successful"), text: gettext("You are now logged out"), type: "success"}); self.fromResponse(response); - } - }) + }); }; self.onLoginUserKeyup = function(data, event) { diff --git a/src/octoprint/static/js/app/viewmodels/navigation.js b/src/octoprint/static/js/app/viewmodels/navigation.js index 736fc949..37bebbe8 100644 --- a/src/octoprint/static/js/app/viewmodels/navigation.js +++ b/src/octoprint/static/js/app/viewmodels/navigation.js @@ -6,8 +6,7 @@ $(function() { self.appearance = parameters[1]; self.settings = parameters[2]; self.usersettings = parameters[3]; - - self.systemActions = self.settings.system_actions; + self.system = parameters[4]; self.appearanceClasses = ko.computed(function() { var classes = self.appearance.color(); @@ -17,41 +16,11 @@ $(function() { return classes; }); - self.triggerAction = function(action) { - var callback = function() { - $.ajax({ - url: API_BASEURL + "system", - type: "POST", - dataType: "json", - data: "action=" + action.action, - success: function() { - new PNotify({title: "Success", text: _.sprintf(gettext("The command \"%(command)s\" executed successfully"), {command: action.name}), type: "success"}); - }, - error: function(jqXHR, textStatus, errorThrown) { - if (!action.hasOwnProperty("ignore") || !action.ignore) { - var error = "

" + _.sprintf(gettext("The command \"%(command)s\" could not be executed."), {command: action.name}) + "

"; - error += pnotifyAdditionalInfo("
" + jqXHR.responseText + "
"); - new PNotify({title: gettext("Error"), text: error, type: "error", hide: false}); - } - } - }) - }; - if (action.confirm) { - showConfirmationDialog({ - message: action.confirm, - onproceed: function(e) { - callback(); - } - }); - } else { - callback(); - } - } } OCTOPRINT_VIEWMODELS.push([ NavigationViewModel, - ["loginStateViewModel", "appearanceViewModel", "settingsViewModel", "userSettingsViewModel"], + ["loginStateViewModel", "appearanceViewModel", "settingsViewModel", "userSettingsViewModel", "systemViewModel"], "#navbar" ]); }); diff --git a/src/octoprint/static/js/app/viewmodels/printerprofiles.js b/src/octoprint/static/js/app/viewmodels/printerprofiles.js index 79612465..922ddd8f 100644 --- a/src/octoprint/static/js/app/viewmodels/printerprofiles.js +++ b/src/octoprint/static/js/app/viewmodels/printerprofiles.js @@ -194,12 +194,8 @@ $(function() { }; self.requestData = function() { - $.ajax({ - url: API_BASEURL + "printerprofiles", - type: "GET", - dataType: "json", - success: self.fromResponse - }) + OctoPrint.printerprofiles.get() + .done(self.fromResponse); }; self.fromResponse = function(data) { @@ -228,43 +224,35 @@ $(function() { self.addProfile = function(callback) { var profile = self._editorData(); self.requestInProgress(true); - $.ajax({ - url: API_BASEURL + "printerprofiles", - type: "POST", - dataType: "json", - contentType: "application/json; charset=UTF-8", - data: JSON.stringify({profile: profile}), - success: function() { - self.requestInProgress(false); + OctoPrint.printerprofiles.add(profile) + .done(function() { if (callback !== undefined) { callback(); } self.requestData(); - }, - error: function() { - self.requestInProgress(false); + }) + .fail(function() { var text = gettext("There was unexpected error while saving the printer profile, please consult the logs."); new PNotify({title: gettext("Saving failed"), text: text, type: "error", hide: false}); - } - }); + }) + .always(function() { + self.requestInProgress(false); + }); }; self.removeProfile = function(data) { self.requestInProgress(true); - $.ajax({ - url: data.resource, - type: "DELETE", - dataType: "json", - success: function() { - self.requestInProgress(false); + OctoPrint.printerprofiles.delete(data.id, {url: data.resource}) + .done(function() { self.requestData(); - }, - error: function() { - self.requestInProgress(false); + }) + .fail(function() { var text = gettext("There was unexpected error while removing the printer profile, please consult the logs."); new PNotify({title: gettext("Saving failed"), text: text, type: "error", hide: false}); - } - }) + }) + .always(function() { + self.requestInProgress(false); + }); }; self.updateProfile = function(profile, callback) { @@ -273,26 +261,20 @@ $(function() { } self.requestInProgress(true); - - $.ajax({ - url: API_BASEURL + "printerprofiles/" + profile.id, - type: "PATCH", - dataType: "json", - contentType: "application/json; charset=UTF-8", - data: JSON.stringify({profile: profile}), - success: function() { - self.requestInProgress(false); + OctoPrint.printerprofiles.update(profile.key, profile) + .done(function() { if (callback !== undefined) { callback(); } self.requestData(); - }, - error: function() { - self.requestInProgress(false); + }) + .fail(function() { var text = gettext("There was unexpected error while updating the printer profile, please consult the logs."); new PNotify({title: gettext("Saving failed"), text: text, type: "error", hide: false}); - } - }); + }) + .always(function() { + self.requestInProgress(false); + }); }; self.showEditProfileDialog = function(data) { diff --git a/src/octoprint/static/js/app/viewmodels/printerstate.js b/src/octoprint/static/js/app/viewmodels/printerstate.js index e764a003..d1bfa10a 100644 --- a/src/octoprint/static/js/app/viewmodels/printerstate.js +++ b/src/octoprint/static/js/app/viewmodels/printerstate.js @@ -198,45 +198,25 @@ $(function() { }; self.print = function() { - var restartCommand = function() { - self._jobCommand("restart"); - }; - if (self.isPaused()) { showConfirmationDialog({ message: gettext("This will restart the print job from the beginning."), - onproceed: function(e) { - restartCommand(); + onproceed: function() { + OctoPrint.job.restart(); } }); } else { - self._jobCommand("start"); + OctoPrint.job.start(); } - }; self.pause = function() { - self._jobCommand("pause"); + OctoPrint.job.pause(); }; self.cancel = function() { - self._jobCommand("cancel"); + OctoPrint.job.cancel(); }; - - self._jobCommand = function(command, callback) { - $.ajax({ - url: API_BASEURL + "job", - type: "POST", - dataType: "json", - contentType: "application/json; charset=UTF-8", - data: JSON.stringify({command: command}), - success: function(response) { - if (callback != undefined) { - callback(); - } - } - }); - } } OCTOPRINT_VIEWMODELS.push([ diff --git a/src/octoprint/static/js/app/viewmodels/settings.js b/src/octoprint/static/js/app/viewmodels/settings.js index 797e3e05..f3d0ed20 100644 --- a/src/octoprint/static/js/app/viewmodels/settings.js +++ b/src/octoprint/static/js/app/viewmodels/settings.js @@ -10,7 +10,7 @@ $(function() { self.receiving = ko.observable(false); self.sending = ko.observable(false); - self.callbacks = []; + self.outstanding = []; self.settingsDialog = undefined; self.settings_dialog_update_detected = undefined; @@ -225,18 +225,9 @@ $(function() { var errorText = gettext("Could not retrieve snapshot URL, please double check the URL"); var errorTitle = gettext("Snapshot test failed"); - $.ajax({ - url: API_BASEURL + "util/test", - type: "POST", - dataType: "json", - data: JSON.stringify({ - command: "url", - url: self.webcam_snapshotUrl(), - method: "GET", - response: true - }), - contentType: "application/json; charset=UTF-8", - success: function(response) { + + OctoPrint.util.testUrl(self.webcam_snapshotUrl(), {method: "GET", response: true}) + .done(function(response) { $("i.icon-spinner", target).remove(); if (!response.result) { @@ -260,15 +251,14 @@ $(function() { title: gettext("Snapshot test"), message: $('

' + text + '

') }); - }, - error: function() { + }) + .fail(function() { $("i.icon-spinner", target).remove(); showMessageDialog({ title: errorTitle, message: errorText }); - } - }); + }); }; self.testWebcamFfmpegPath = function() { @@ -276,36 +266,22 @@ $(function() { return; } - var successCallback = function(response) { - if (!response.result) { - if (!response.exists) { - self.webcam_ffmpegPathText(gettext("The path doesn't exist")); - } else if (!response.typeok) { - self.webcam_ffmpegPathText(gettext("The path is not a file")); - } else if (!response.access) { - self.webcam_ffmpegPathText(gettext("The path is not an executable")); + OctoPrint.util.testExecutable(self.webcam_ffmpegPath()) + .done(function(response) { + if (!response.result) { + if (!response.exists) { + self.webcam_ffmpegPathText(gettext("The path doesn't exist")); + } else if (!response.typeok) { + self.webcam_ffmpegPathText(gettext("The path is not a file")); + } else if (!response.access) { + self.webcam_ffmpegPathText(gettext("The path is not an executable")); + } + } else { + self.webcam_ffmpegPathText(gettext("The path is valid")); } - } else { - self.webcam_ffmpegPathText(gettext("The path is valid")); - } - self.webcam_ffmpegPathOk(response.result); - self.webcam_ffmpegPathBroken(!response.result); - }; - - var path = self.webcam_ffmpegPath(); - $.ajax({ - url: API_BASEURL + "util/test", - type: "POST", - dataType: "json", - data: JSON.stringify({ - command: "path", - path: path, - check_type: "file", - check_access: "x" - }), - contentType: "application/json; charset=UTF-8", - success: successCallback - }) + self.webcam_ffmpegPathOk(response.result); + self.webcam_ffmpegPathBroken(!response.result); + }); }; self.onSettingsShown = function() { @@ -331,6 +307,7 @@ $(function() { dataType: "json", maxNumberOfFiles: 1, autoUpload: false, + headers: OctoPrint.getRequestHeaders(), add: function(e, data) { if (data.files.length == 0) { return false; @@ -409,57 +386,83 @@ $(function() { return false; }; - self.requestData = function(callback, local) { - if (self.receiving()) { - if (callback) { - self.callbacks.push(callback); + self.requestData = function(local) { + // handle old parameter format + var callback = undefined; + if (arguments.length == 2 || _.isFunction(local)) { + var exc = new Error(); + log.warn("The callback parameter of SettingsViewModel.requestData is deprecated, the method now returns a promise, please use that instead. Stacktrace:", (exc.stack || exc.stacktrace || "")); + + if (arguments.length == 2) { + callback = arguments[0]; + local = arguments[1]; + } else { + callback = local; + local = false; } - return; } - self.receiving(true); - $.ajax({ - url: API_BASEURL + "settings", - type: "GET", - dataType: "json", - success: function(response) { - if (callback) { - self.callbacks.push(callback); - } - - try { - self.fromResponse(response, local); - - var cb; - while (self.callbacks.length) { - cb = self.callbacks.shift(); - try { - cb(); - } catch(exc) { - log.error("Error calling settings callback", cb, ":", (exc.stack || exc)); - } - } - } finally { - self.receiving(false); - self.callbacks = []; - } - }, - error: function(xhr) { - self.receiving(false); + // handler for any explicitely provided callbacks + var callbackHandler = function() { + if (!callback) return; + try { + callback(); + } catch (exc) { + log.error("Error calling settings callback", callback, ":", (exc.stack || exc.stacktrace || exc)); } - }); + }; + + // if a request is already active, create a new deferred and return + // its promise, it will be resolved in the response handler of the + // current request + if (self.receiving()) { + var deferred = $.Deferred(); + self.outstanding.push(deferred); + + if (callback) { + // if we have a callback, we need to make sure it will + // get called when the deferred is resolved + deferred.done(callbackHandler); + } + + return deferred.promise(); + } + + // perform the request + self.receiving(true); + return OctoPrint.settings.get() + .done(function(response) { + self.fromResponse(response, local); + + if (callback) { + var deferred = $.Deferred(); + deferred.done(callbackHandler); + self.outstanding.push(deferred); + } + + // resolve all promises + var args = arguments; + _.each(self.outstanding, function(deferred) { + deferred.resolve(args); + }); + self.outstanding = []; + }) + .fail(function() { + // reject all promises + var args = arguments; + _.each(self.outstanding, function(deferred) { + deferred.reject(args); + }); + self.outstanding = []; + }) + .always(function() { + self.receiving(false); + }); }; - self.requestTranslationData = function(callback) { - $.ajax({ - url: API_BASEURL + "languages", - type: "GET", - dataType: "json", - success: function(response) { - self.fromTranslationResponse(response); - if (callback) callback(); - } - }) + self.requestTranslationData = function() { + return OctoPrint.languages.list() + .done(self.fromTranslationResponse); }; self.fromTranslationResponse = function(response) { @@ -509,14 +512,8 @@ $(function() { }); self.deleteLanguagePack = function(locale, pack) { - $.ajax({ - url: API_BASEURL + "languages/" + locale + "/" + pack, - type: "DELETE", - dataType: "json", - success: function(response) { - self.fromTranslationResponse(response); - } - }) + OctoPrint.languages.delete(locale, pack) + .done(self.fromTranslationResponse); }; /** @@ -652,7 +649,10 @@ $(function() { longRunningCommands: function(value) { self.serial_longRunningCommands(value.join(", "))}, checksumRequiringCommands: function(value) { self.serial_checksumRequiringCommands(value.join(", "))} }, - terminalFilters: function(value) { self.terminalFilters.removeAll(); _.each(value, function(item) {self.terminalFilters.push(item)}); } + terminalFilters: function(value) { self.terminalFilters($.extend(true, [], value)) }, + temperature: { + profiles: function(value) { self.temperature_profiles($.extend(true, [], value)); } + } }; var mapToObservables = function(data, mapping, local, keyPrefix) { @@ -705,13 +705,8 @@ $(function() { data = getOnlyChangedData(self.getLocalData(), self.lastReceivedSettings); } - $.ajax({ - url: API_BASEURL + "settings", - type: "POST", - dataType: "json", - contentType: "application/json; charset=UTF-8", - data: JSON.stringify(data), - success: function(data, status, xhr) { + OctoPrint.settings.save(data) + .done(function(data, status, xhr) { self.receiving(true); self.sending(false); try { @@ -720,15 +715,14 @@ $(function() { } finally { self.receiving(false); } - }, - error: function(xhr, status, error) { + }) + .fail(function(xhr, status, error) { self.sending(false); if (options.error) options.error(xhr, status, error); - }, - complete: function(xhr, status) { + }) + .always(function(xhr, status) { if (options.complete) options.complete(xhr, status); - } - }); + }); }; self.onEventSettingsUpdated = function() { diff --git a/src/octoprint/static/js/app/viewmodels/slicing.js b/src/octoprint/static/js/app/viewmodels/slicing.js index eba0581a..ffbb21f9 100644 --- a/src/octoprint/static/js/app/viewmodels/slicing.js +++ b/src/octoprint/static/js/app/viewmodels/slicing.js @@ -64,18 +64,11 @@ $(function() { && self.profile() != undefined; }); - self.requestData = function(callback) { - $.ajax({ - url: API_BASEURL + "slicing", - type: "GET", - dataType: "json", - success: function(data) { + self.requestData = function() { + return OctoPrint.slicing.listAllSlicersAndProfiles() + .done(function(data) { self.fromResponse(data); - if (callback !== undefined) { - callback(); - } - } - }); + }); }; self.fromResponse = function(data) { @@ -150,7 +143,6 @@ $(function() { } var data = { - command: "slice", slicer: self.slicer(), profile: self.profile(), printerProfile: self.printerProfile(), @@ -163,19 +155,14 @@ $(function() { data["select"] = true; } - $.ajax({ - url: API_BASEURL + "files/" + self.target + "/" + self.file, - type: "POST", - dataType: "json", - contentType: "application/json; charset=UTF-8", - data: JSON.stringify(data) - }); + OctoPrint.files.slice(self.target, self.file, data) + .done(function() { + $("#slicing_configuration_dialog").modal("hide"); - $("#slicing_configuration_dialog").modal("hide"); - - self.gcodeFilename(undefined); - self.slicer(self.defaultSlicer); - self.profile(self.defaultProfile); + self.gcodeFilename(undefined); + self.slicer(self.defaultSlicer); + self.profile(self.defaultProfile); + }); }; self._sanitize = function(name) { @@ -196,4 +183,4 @@ $(function() { ["loginStateViewModel", "printerProfilesViewModel"], "#slicing_configuration_dialog" ]); -}); \ No newline at end of file +}); diff --git a/src/octoprint/static/js/app/viewmodels/system.js b/src/octoprint/static/js/app/viewmodels/system.js new file mode 100644 index 00000000..63b916c6 --- /dev/null +++ b/src/octoprint/static/js/app/viewmodels/system.js @@ -0,0 +1,100 @@ +$(function() { + function SystemViewModel(parameters) { + var self = this; + + self.loginState = parameters[0]; + + self.lastCommandResponse = undefined; + self.systemActions = ko.observableArray([]); + + self.requestData = function() { + self.requestCommandData(); + }; + + self.requestCommandData = function() { + if (!self.loginState.isAdmin()) { + return $.Deferred().reject().promise(); + } + + return OctoPrint.system.getCommands() + .done(self.fromCommandResponse); + }; + + self.fromCommandResponse = function(response) { + var actions = []; + if (response.core && response.core.length) { + _.each(response.core, function(data) { + var action = _.extend({}, data); + action.actionSource = "core"; + actions.push(action); + }); + actions.push({action: "divider"}); + } + _.each(response.custom, function(data) { + var action = _.extend({}, data); + action.actionSource = "custom"; + actions.push(action); + }); + self.lastCommandResponse = response; + self.systemActions(actions); + }; + + self.triggerCommand = function(commandSpec) { + var deferred = $.Deferred(); + + var callback = function() { + OctoPrint.system.executeCommand(commandSpec.actionSource, commandSpec.action) + .done(function() { + new PNotify({title: "Success", text: _.sprintf(gettext("The command \"%(command)s\" executed successfully"), {command: commandSpec.name}), type: "success"}); + deferred.resolve(["success", arguments]); + }) + .fail(function(jqXHR, textStatus, errorThrown) { + if (!commandSpec.hasOwnProperty("ignore") || !commandSpec.ignore) { + var error = "

" + _.sprintf(gettext("The command \"%(command)s\" could not be executed."), {command: commandSpec.name}) + "

"; + error += pnotifyAdditionalInfo("
" + jqXHR.responseText + "
"); + new PNotify({title: gettext("Error"), text: error, type: "error", hide: false}); + deferred.reject(["error", arguments]); + } else { + deferred.resolve(["ignored", arguments]); + } + }); + }; + + if (commandSpec.confirm) { + showConfirmationDialog({ + message: commandSpec.confirm, + onproceed: function() { + callback(); + }, + oncancel: function() { + deferred.reject("cancelled", arguments); + } + }); + } else { + callback(); + } + + return deferred.promise(); + }; + + self.onUserLoggedIn = function(user) { + if (user.admin) { + self.requestData(); + } else { + self.onUserLoggedOut(); + } + }; + + self.onUserLoggedOut = function() { + self.lastCommandResponse = undefined; + self.systemActions([]); + } + } + + // view model class, parameters for constructor, container to bind to + ADDITIONAL_VIEWMODELS.push([ + SystemViewModel, + ["loginStateViewModel"], + [] + ]); +}); diff --git a/src/octoprint/static/js/app/viewmodels/temperature.js b/src/octoprint/static/js/app/viewmodels/temperature.js index 9b58d2ec..e9f51ece 100644 --- a/src/octoprint/static/js/app/viewmodels/temperature.js +++ b/src/octoprint/static/js/app/viewmodels/temperature.js @@ -268,87 +268,84 @@ $(function() { var value = item.newTarget(); if (!value) return; - self._sendToolCommand("target", - item.key(), - item.newTarget(), - function() {item.newTarget("");} - ); + var onSuccess = function() { + item.newTarget(""); + }; + + if (item.key() == "bed") { + self._setBedTemperature(value) + .done(onSuccess); + } else { + self._setToolTemperature(item.key(), value) + .done(onSuccess); + } }; self.setTargetFromProfile = function(item, profile) { if (!profile) return; - var value = undefined; - if (item.key() == "bed") { - value = profile.bed; - } else { - value = profile.extruder; - } + var onSuccess = function() { + item.newTarget(""); + }; - self._sendToolCommand("target", - item.key(), - value, - function() {item.newTarget("");} - ); + if (item.key() == "bed") { + self._setBedTemperature(profile.bed) + .done(onSuccess); + } else { + self._setToolTemperature(item.key(), profile.extruder) + .done(onSuccess); + } }; self.setTargetToZero = function(item) { - self._sendToolCommand("target", - item.key(), - 0, - function() {item.newTarget("");} - ); + var onSuccess = function() { + item.newTarget(""); + }; + + if (item.key() == "bed") { + self._setBedTemperature(0) + .done(onSuccess); + } else { + self._setToolTemperature(item.key(), 0) + .done(onSuccess); + } }; self.setOffset = function(item) { - self._sendToolCommand("offset", - item.key(), - item.newOffset(), - function() {item.newOffset("");} - ); - }; + var value = item.newOffset(); + if (!value) return; - self._sendToolCommand = function(command, type, temp, successCb, errorCb) { - var data = { - command: command + var onSuccess = function() { + item.newOffset(""); }; - var endpoint; - if (type == "bed") { - if ("target" == command) { - data["target"] = parseInt(temp); - } else if ("offset" == command) { - data["offset"] = parseInt(temp); - } else { - return; - } - - endpoint = "bed"; + if (item.key() == "bed") { + self._setBedOffset(value) + .done(onSuccess); } else { - var group; - if ("target" == command) { - group = "targets"; - } else if ("offset" == command) { - group = "offsets"; - } else { - return; - } - data[group] = {}; - data[group][type] = parseInt(temp); - - endpoint = "tool"; + self._setToolOffset(item.key(), value) + .done(onSuccess); } + }; - $.ajax({ - url: API_BASEURL + "printer/" + endpoint, - type: "POST", - dataType: "json", - contentType: "application/json; charset=UTF-8", - data: JSON.stringify(data), - success: function() { if (successCb !== undefined) successCb(); }, - error: function() { if (errorCb !== undefined) errorCb(); } - }); + self._setToolTemperature = function(tool, temperature) { + var data = {}; + data[tool] = parseInt(temperature); + return OctoPrint.printer.setToolTargetTemperatures(data); + }; + self._setToolOffset = function(tool, offset) { + var data = {}; + data[tool] = parseInt(offset); + return OctoPrint.printer.setToolTemperatureOffsets(data); + }; + + self._setBedTemperature = function(temperature) { + return OctoPrint.printer.setBedTargetTemperature(parseInt(temperature)); + }; + + self._setBedOffset = function(offset) { + return OctoPrint.printer.setBedTemperatureOffset(parseInt(offset)); }; self.handleEnter = function(event, type, item) { @@ -375,4 +372,4 @@ $(function() { ["loginStateViewModel", "settingsViewModel"], "#temp" ]); -}); \ No newline at end of file +}); diff --git a/src/octoprint/static/js/app/viewmodels/terminal.js b/src/octoprint/static/js/app/viewmodels/terminal.js index d7ec494d..3da817a0 100644 --- a/src/octoprint/static/js/app/viewmodels/terminal.js +++ b/src/octoprint/static/js/app/viewmodels/terminal.js @@ -158,29 +158,18 @@ $(function() { } if (command) { - $.ajax({ - url: API_BASEURL + "printer/command", - type: "POST", - dataType: "json", - contentType: "application/json; charset=UTF-8", - data: JSON.stringify({"command": command}) - }); - - self.cmdHistory.push(command); - self.cmdHistory.slice(-300); // just to set a sane limit to how many manually entered commands will be saved... - self.cmdHistoryIdx = self.cmdHistory.length; - self.command(""); + OctoPrint.control.sendGcode(command) + .done(function() { + self.cmdHistory.push(command); + self.cmdHistory.slice(-300); // just to set a sane limit to how many manually entered commands will be saved... + self.cmdHistoryIdx = self.cmdHistory.length; + self.command(""); + }); } }; self.fakeAck = function() { - $.ajax({ - url: API_BASEURL + "connection", - type: "POST", - dataType: "json", - contentType: "application/json; charset=UTF-8", - data: JSON.stringify({"command": "fake_ack"}) - }); + OctoPrint.printer.fakeAck(); }; self.handleKeyDown = function(event) { @@ -232,4 +221,4 @@ $(function() { ["loginStateViewModel", "settingsViewModel"], "#term" ]); -}); \ No newline at end of file +}); diff --git a/src/octoprint/static/js/app/viewmodels/timelapse.js b/src/octoprint/static/js/app/viewmodels/timelapse.js index ed4188b5..0b3a0950 100644 --- a/src/octoprint/static/js/app/viewmodels/timelapse.js +++ b/src/octoprint/static/js/app/viewmodels/timelapse.js @@ -34,20 +34,20 @@ $(function() { return self.isDirty() && self.isOperational() && !self.isPrinting() && self.loginState.isUser(); }); - self.isOperational.subscribe(function(newValue) { + self.isOperational.subscribe(function() { self.requestData(); }); - self.timelapseType.subscribe(function(newValue) { + self.timelapseType.subscribe(function() { self.isDirty(true); }); - self.timelapseTimedInterval.subscribe(function(newValue) { + self.timelapseTimedInterval.subscribe(function() { self.isDirty(true); }); - self.timelapsePostRoll.subscribe(function(newValue) { + self.timelapsePostRoll.subscribe(function() { self.isDirty(true); }); - self.timelapseFps.subscribe(function(newValue) { + self.timelapseFps.subscribe(function() { self.isDirty(true); }); @@ -83,12 +83,8 @@ $(function() { ); self.requestData = function() { - $.ajax({ - url: API_BASEURL + "timelapse", - type: "GET", - dataType: "json", - success: self.fromResponse - }); + OctoPrint.timelapse.get() + .done(self.fromResponse); }; self.fromResponse = function(response) { @@ -141,15 +137,11 @@ $(function() { }; self.removeFile = function(filename) { - $.ajax({ - url: API_BASEURL + "timelapse/" + filename, - type: "DELETE", - dataType: "json", - success: self.requestData - }); + OctoPrint.timelapse.delete(filename) + .done(self.requestData); }; - self.save = function(data, event) { + self.save = function() { var payload = { "type": self.timelapseType(), "postRoll": self.timelapsePostRoll(), @@ -161,20 +153,15 @@ $(function() { payload["interval"] = self.timelapseTimedInterval(); } - $.ajax({ - url: API_BASEURL + "timelapse", - type: "POST", - dataType: "json", - data: payload, - success: self.fromResponse - }); + OctoPrint.timelapse.saveConfig(payload) + .done(self.fromResponse); }; self.onDataUpdaterReconnect = function() { self.requestData(); }; - self.onEventMovieDone = function(payload) { + self.onEventMovieDone = function() { self.requestData(); }; diff --git a/src/octoprint/static/js/app/viewmodels/users.js b/src/octoprint/static/js/app/viewmodels/users.js index c341a067..235dd28d 100644 --- a/src/octoprint/static/js/app/viewmodels/users.js +++ b/src/octoprint/static/js/app/viewmodels/users.js @@ -60,12 +60,8 @@ $(function() { self.requestData = function() { if (!CONFIG_ACCESS_CONTROL) return; - $.ajax({ - url: API_BASEURL + "users", - type: "GET", - dataType: "json", - success: self.fromResponse - }); + OctoPrint.users.list() + .done(self.fromResponse); }; self.fromResponse = function(response) { @@ -83,12 +79,19 @@ $(function() { self.confirmAddUser = function() { if (!CONFIG_ACCESS_CONTROL) return; - var user = {name: self.editorUsername(), password: self.editorPassword(), admin: self.editorAdmin(), active: self.editorActive()}; - self.addUser(user, function() { - // close dialog - self.currentUser(undefined); - self.addUserDialog.modal("hide"); - }); + var user = { + name: self.editorUsername(), + password: self.editorPassword(), + admin: self.editorAdmin(), + active: self.editorActive() + }; + + self.addUser(user) + .done(function() { + // close dialog + self.currentUser(undefined); + self.addUserDialog.modal("hide"); + }); }; self.showEditUserDialog = function(user) { @@ -105,12 +108,12 @@ $(function() { user.active = self.editorActive(); user.admin = self.editorAdmin(); - // make AJAX call - self.updateUser(user, function() { - // close dialog - self.currentUser(undefined); - self.editUserDialog.modal("hide"); - }); + self.updateUser(user) + .done(function() { + // close dialog + self.currentUser(undefined); + self.editUserDialog.modal("hide"); + }); }; self.showChangePasswordDialog = function(user) { @@ -123,27 +126,30 @@ $(function() { self.confirmChangePassword = function() { if (!CONFIG_ACCESS_CONTROL) return; - self.updatePassword(self.currentUser().name, self.editorPassword(), function() { - // close dialog - self.currentUser(undefined); - self.changePasswordDialog.modal("hide"); - }); + self.updatePassword(self.currentUser().name, self.editorPassword()) + .done(function() { + // close dialog + self.currentUser(undefined); + self.changePasswordDialog.modal("hide"); + }); }; self.confirmGenerateApikey = function() { if (!CONFIG_ACCESS_CONTROL) return; - self.generateApikey(self.currentUser().name, function(response) { - self._updateApikey(response.apikey); - }) + self.generateApikey(self.currentUser().name) + .done(function(response) { + self._updateApikey(response.apikey); + }); }; self.confirmDeleteApikey = function() { if (!CONFIG_ACCESS_CONTROL) return; - self.deleteApikey(self.currentUser().name, function() { - self._updateApikey(undefined); - }) + self.deleteApikey(self.currentUser().name) + .done(function() { + self._updateApikey(undefined); + }); }; self._updateApikey = function(apikey) { @@ -159,108 +165,55 @@ $(function() { self.changePasswordDialog = $("#settings-usersDialogChangePassword"); }; - //~~ AJAX calls + //~~ API calls - self.addUser = function(user, callback) { - if (!CONFIG_ACCESS_CONTROL) return; - if (user === undefined) return; + self.addUser = function(user) { + if (!user) { + throw OctoPrint.InvalidArgumentError("user must be set"); + } - $.ajax({ - url: API_BASEURL + "users", - type: "POST", - contentType: "application/json; charset=UTF-8", - data: JSON.stringify(user), - success: function(response) { - self.fromResponse(response); - if (callback) { - callback(response); - } - } - }); + return OctoPrint.users.add(user) + .done(self.fromResponse); }; - self.removeUser = function(user, callback) { - if (!CONFIG_ACCESS_CONTROL) return; - if (user === undefined) return; + self.removeUser = function(user) { + if (!user) { + throw OctoPrint.InvalidArgumentError("user must be set"); + } if (user.name == self.loginState.username()) { // we do not allow to delete ourselves - new PNotify({title: "Not possible", text: "You may not delete your own account.", type: "error"}); - return; + new PNotify({ + title: gettext("Not possible"), + text: gettext("You may not delete your own account."), + type: "error" + }); + return $.Deferred().reject("You may not delete your own account").promise(); } - $.ajax({ - url: API_BASEURL + "users/" + user.name, - type: "DELETE", - success: function(response) { - self.fromResponse(response); - if (callback) { - callback(response); - } - } - }); + return OctoPrint.users.delete(user.name) + .done(self.fromResponse); }; - self.updateUser = function(user, callback) { - if (!CONFIG_ACCESS_CONTROL) return; - if (user === undefined) return; + self.updateUser = function(user) { + if (!user) { + throw OctoPrint.InvalidArgumentError("user must be set"); + } - $.ajax({ - url: API_BASEURL + "users/" + user.name, - type: "PUT", - contentType: "application/json; charset=UTF-8", - data: JSON.stringify(user), - success: function(response) { - self.fromResponse(response); - if (callback) { - callback(response); - } - } - }); + return OctoPrint.users.update(user.name, user.active, user.admin) + .done(self.fromResponse); }; - self.updatePassword = function(username, password, callback) { - if (!CONFIG_ACCESS_CONTROL) return; - - $.ajax({ - url: API_BASEURL + "users/" + username + "/password", - type: "PUT", - contentType: "application/json; charset=UTF-8", - data: JSON.stringify({password: password}), - success: function(response) { - if (callback) { - callback(response); - } - } - }); + self.updatePassword = function(username, password) { + return OctoPrint.users.changePassword(username, password); }; - self.generateApikey = function(username, callback) { - if (!CONFIG_ACCESS_CONTROL) return; - - $.ajax({ - url: API_BASEURL + "users/" + username + "/apikey", - type: "POST", - success: function(response) { - if (callback) { - callback(response); - } - } - }); + self.generateApikey = function(username) { + return OctoPrint.users.generateApiKey(username); }; - self.deleteApikey = function(username, callback) { - if (!CONFIG_ACCESS_CONTROL) return; - - $.ajax({ - url: API_BASEURL + "users/" + username + "/apikey", - type: "DELETE", - success: function(response) { - if (callback) { - callback(response); - } - } - }); + self.deleteApikey = function(username) { + return OctoPrint.users.resetApiKey(username); }; self.onUserLoggedIn = function(user) { @@ -275,4 +228,4 @@ $(function() { ["loginStateViewModel"], [] ]); -}); \ No newline at end of file +}); diff --git a/src/octoprint/static/js/app/viewmodels/usersettings.js b/src/octoprint/static/js/app/viewmodels/usersettings.js index ed8d0a6a..99b79631 100644 --- a/src/octoprint/static/js/app/viewmodels/usersettings.js +++ b/src/octoprint/static/js/app/viewmodels/usersettings.js @@ -60,24 +60,17 @@ $(function() { "language": self.interface_language() } }; - self.updateSettings(self.currentUser().name, settings, function() { - // close dialog - self.currentUser(undefined); - self.userSettingsDialog.modal("hide"); - self.loginState.reloadUser(); - }); + self.updateSettings(self.currentUser().name, settings) + .done(function() { + // close dialog + self.currentUser(undefined); + self.userSettingsDialog.modal("hide"); + self.loginState.reloadUser(); + }); }; - self.updateSettings = function(username, settings, callback) { - if (!CONFIG_ACCESS_CONTROL) return; - - $.ajax({ - url: API_BASEURL + "users/" + username + "/settings", - type: "PATCH", - contentType: "application/json; charset=UTF-8", - data: JSON.stringify(settings), - success: callback - }); + self.updateSettings = function(username, settings) { + return OctoPrint.users.saveSettings(username, settings); }; self.saveEnabled = function() { diff --git a/src/octoprint/static/js/app/viewmodels/wizard.js b/src/octoprint/static/js/app/viewmodels/wizard.js index c15b11a3..54ca62e4 100644 --- a/src/octoprint/static/js/app/viewmodels/wizard.js +++ b/src/octoprint/static/js/app/viewmodels/wizard.js @@ -19,18 +19,19 @@ $(function() { self.showDialog = function() { if (!CONFIG_WIZARD || !(CONFIG_FIRST_RUN || self.loginState.isAdmin())) return; - self.getWizardDetails(function(response) { - callViewModels(self.allViewModels, "onWizardDetails", [response]); + self.getWizardDetails() + .done(function(response) { + callViewModels(self.allViewModels, "onWizardDetails", [response]); - if (!self.isDialogActive()) { - self.wizardDialog.modal({ - minHeight: function() { return Math.max($.fn.modal.defaults.maxHeight() - 80, 250); } - }).css({ - width: 'auto', - 'margin-left': function() { return -($(this).width() /2); } - }); - } - }); + if (!self.isDialogActive()) { + self.wizardDialog.modal({ + minHeight: function() { return Math.max($.fn.modal.defaults.maxHeight() - 80, 250); } + }).css({ + width: 'auto', + 'margin-left': function() { return -($(this).width() /2); } + }); + } + }); }; self.closeDialog = function() { @@ -121,53 +122,34 @@ $(function() { // then reload = (method() == "reload") || reload; }); - self.finishWizard(function() { - self.closeDialog(); - if (reload) { - log.info("Wizard requested reloading"); - location.reload(true); - } - }); + self.finishWizard() + .done(function() { + self.closeDialog(); + if (reload) { + log.info("Wizard requested reloading"); + location.reload(true); + } + }); } } }); self.showDialog(); }; - self.getWizardDetails = function(callback) { - if (!callback) return; - - $.ajax({ - url: API_BASEURL + "setup/wizard", - type: "GET", - dataType: "json", - success: function(response) { + self.getWizardDetails = function() { + return OctoPrint.wizard.get() + .done(function(response) { self.wizards = _.filter(_.keys(response), function(key) { return response[key] && response[key]["required"] && !response[key]["ignored"]; }); - if (callback) { - callback(response); - } - } - }); + }); }; - self.finishWizard = function(callback) { + self.finishWizard = function() { self.finishing = true; - self.settingsViewModel.saveData(); - $.ajax({ - url: API_BASEURL + "setup/wizard", - type: "POST", - dataType: "json", - data: JSON.stringify({handled: self.wizards}), - contentType: "application/json; charset=UTF-8", - success: function() { + return OctoPrint.wizard.finish(self.wizards) + .always(function() { self.finishing = false; - callback(); - }, - failure: function() { - self.finishing = false; - } - }) + }); }; self.onSettingsPreventRefresh = function() { diff --git a/src/octoprint/templates/javascripts.jinja2 b/src/octoprint/templates/javascripts.jinja2 index acad1952..f69b158c 100644 --- a/src/octoprint/templates/javascripts.jinja2 +++ b/src/octoprint/templates/javascripts.jinja2 @@ -2,6 +2,10 @@ {% endassets %} +{% assets "js_client" %} + +{% endassets %} + {% assets "js_app" %} {% endassets %} diff --git a/src/octoprint/templates/navbar/systemmenu.jinja2 b/src/octoprint/templates/navbar/systemmenu.jinja2 index a2ee3f3b..603d2a09 100644 --- a/src/octoprint/templates/navbar/systemmenu.jinja2 +++ b/src/octoprint/templates/navbar/systemmenu.jinja2 @@ -2,11 +2,11 @@ {{ _('System') }} -