New corewizard plugin taking care of first run setup
Also introduced wizard finish backend method and remove old firstrun dialog remnants. TODO: bound check won't work this way, needs to be redone differently. Goal is to only call onWizardFinish for wizards which are actually involved...
This commit is contained in:
parent
1bba5ca788
commit
b1e4a1d9dc
11 changed files with 257 additions and 98 deletions
137
src/octoprint/plugins/corewizard/__init__.py
Normal file
137
src/octoprint/plugins/corewizard/__init__.py
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
__copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms of the AGPLv3 License"
|
||||
|
||||
|
||||
import octoprint.plugin
|
||||
|
||||
|
||||
class CoreWizardPlugin(octoprint.plugin.AssetPlugin,
|
||||
octoprint.plugin.TemplatePlugin,
|
||||
octoprint.plugin.WizardPlugin,
|
||||
octoprint.plugin.SettingsPlugin,
|
||||
octoprint.plugin.BlueprintPlugin):
|
||||
|
||||
#~~ TemplatePlugin API
|
||||
|
||||
def get_template_configs(self):
|
||||
required = self._get_subwizard_attrs("_is_", "_wizard_required")
|
||||
names = self._get_subwizard_attrs("_get_", "_wizard_name")
|
||||
additional = self._get_subwizard_attrs("_get_", "_additional_wizard_template_data")
|
||||
|
||||
result = list()
|
||||
for key, method in required.iteritems():
|
||||
if not method():
|
||||
continue
|
||||
|
||||
if not key in names:
|
||||
continue
|
||||
|
||||
name = names[key]()
|
||||
if not name:
|
||||
continue
|
||||
|
||||
config = dict(type="wizard", name=name, template="corewizard_{}_wizard.jinja2".format(key), div="wizard_plugin_corewizard_{}".format(key))
|
||||
if key in additional:
|
||||
additional_result = additional[key]()
|
||||
if additional_result:
|
||||
config.update(additional_result)
|
||||
result.append(config)
|
||||
|
||||
return result
|
||||
|
||||
#~~ AssetPlugin API
|
||||
|
||||
def get_assets(self):
|
||||
return dict(
|
||||
js=["js/corewizard.js"]
|
||||
)
|
||||
|
||||
#~~ WizardPlugin API
|
||||
|
||||
def is_wizard_required(self):
|
||||
methods = self._get_subwizard_attrs("_is_", "_wizard_required")
|
||||
return self._settings.global_get(["server", "firstRun"]) and any(map(lambda m: m(), methods.values()))
|
||||
|
||||
def get_wizard_details(self):
|
||||
result = dict()
|
||||
|
||||
def add_result(key, method):
|
||||
result[key] = method()
|
||||
self._get_subwizard_attrs("_get_", "_wizard_details", add_result)
|
||||
|
||||
return result
|
||||
|
||||
#~~ ACL subwizard
|
||||
|
||||
def _is_acl_wizard_required(self):
|
||||
return self._user_manager is not None and not self._user_manager.hasBeenCustomized()
|
||||
|
||||
def _get_acl_wizard_details(self):
|
||||
return dict()
|
||||
|
||||
def _get_acl_wizard_name(self):
|
||||
return "Access Control"
|
||||
|
||||
@octoprint.plugin.BlueprintPlugin.route("/acl", methods=["POST"])
|
||||
def acl_wizard_api(self):
|
||||
from flask import request
|
||||
from octoprint.server.api import valid_boolean_trues
|
||||
|
||||
if "ac" in request.values and request.values["ac"] in valid_boolean_trues and \
|
||||
"user" in request.values.keys() and "pass1" in request.values.keys() and \
|
||||
"pass2" in request.values.keys() and request.values["pass1"] == request.values["pass2"]:
|
||||
# configure access control
|
||||
self._settings.global_set_boolean(["accessControl", "enabled"], True)
|
||||
octoprint.server.userManager.addUser(request.values["user"], request.values["pass1"], True, ["user", "admin"], overwrite=True)
|
||||
elif "ac" in request.values.keys() and not request.values["ac"] in valid_boolean_trues:
|
||||
# disable access control
|
||||
self._settings.global_set_boolean(["accessControl", "enabled"], False)
|
||||
|
||||
octoprint.server.loginManager.anonymous_user = octoprint.users.DummyUser
|
||||
octoprint.server.principals.identity_loaders.appendleft(octoprint.users.dummy_identity_loader)
|
||||
|
||||
self._settings.save()
|
||||
|
||||
#~~ Webcam subwizard
|
||||
|
||||
def _is_webcam_wizard_required(self):
|
||||
webcam_snapshot_url = self._settings.global_get(["webcam", "snapshotUrl"])
|
||||
webcam_stream_url = self._settings.global_get(["webcam", "streamUrl"])
|
||||
ffmpeg_path = self._settings.global_get(["webcam", "ffmpeg"])
|
||||
|
||||
return not (webcam_snapshot_url and webcam_stream_url and ffmpeg_path)
|
||||
|
||||
def _get_webcam_wizard_details(self):
|
||||
return dict()
|
||||
|
||||
def _get_webcam_wizard_name(self):
|
||||
return "Webcam & Timelapse"
|
||||
|
||||
#~~ helpers
|
||||
|
||||
def _get_subwizard_attrs(self, start, end, callback=None):
|
||||
result = dict()
|
||||
|
||||
for item in dir(self):
|
||||
if not item.startswith(start) or not item.endswith(end):
|
||||
continue
|
||||
|
||||
key = item[len(start):-len(end)]
|
||||
if not key:
|
||||
continue
|
||||
|
||||
attr = getattr(self, item)
|
||||
if callable(callback):
|
||||
callback(key, attr)
|
||||
result[key] = attr
|
||||
|
||||
return result
|
||||
|
||||
|
||||
__plugin_name__ = "Core Wizard"
|
||||
__plugin_description__ = "Provides wizard dialogs for core components"
|
||||
__plugin_implementation__ = CoreWizardPlugin()
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
$(function() {
|
||||
function AclWizardViewModel() {
|
||||
function CoreWizardAclViewModel() {
|
||||
var self = this;
|
||||
|
||||
self.username = ko.observable(undefined);
|
||||
|
|
@ -52,7 +52,7 @@ $(function() {
|
|||
|
||||
self._sendData = function(data, callback) {
|
||||
$.ajax({
|
||||
url: API_BASEURL + "setup",
|
||||
url: API_BASEURL + "plugin/corewizard/acl",
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
data: data,
|
||||
|
|
@ -65,7 +65,7 @@ $(function() {
|
|||
};
|
||||
|
||||
self.onWizardTabChange = function(current, next) {
|
||||
if (!current || !_.startsWith(current, "wizard_firstrun_acl") || self.setup()) {
|
||||
if (!current || !_.startsWith(current, "wizard_plugin_corewizard_acl_") || self.setup()) {
|
||||
return true;
|
||||
}
|
||||
showMessageDialog({
|
||||
|
|
@ -75,26 +75,37 @@ $(function() {
|
|||
return false;
|
||||
};
|
||||
|
||||
self.onWizardFinished = function() {
|
||||
self.onWizardFinish = function() {
|
||||
if (!self.decision()) {
|
||||
return "reload";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function WebcamWizardViewModel(parameters) {
|
||||
function CoreWizardWebcamViewModel(parameters) {
|
||||
var self = this;
|
||||
|
||||
self.settingsViewModel = parameters[0];
|
||||
|
||||
self.onWizardFinish = function() {
|
||||
if (self.unbound) return;
|
||||
|
||||
self.settingsViewModel.enqueueForSaving({
|
||||
webcam: {
|
||||
streamUrl: self.settingsViewModel.webcam_streamUrl(),
|
||||
snapshotUrl: self.settingsViewModel.webcam_snapshotUrl()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
OCTOPRINT_VIEWMODELS.push([
|
||||
AclWizardViewModel,
|
||||
CoreWizardAclViewModel,
|
||||
[],
|
||||
"#wizard_firstrun_acl"
|
||||
"#wizard_plugin_corewizard_acl"
|
||||
], [
|
||||
WebcamWizardViewModel,
|
||||
CoreWizardWebcamViewModel,
|
||||
["settingsViewModel"],
|
||||
"#wizard_firstrun_webcam"
|
||||
"#wizard_plugin_corewizard_webcam"
|
||||
]);
|
||||
});
|
||||
|
|
@ -4,13 +4,13 @@
|
|||
<strong>Please read the following, it is very important for your printer's health!</strong>
|
||||
</p>
|
||||
<p>
|
||||
OctoPrint by default now ships with Access Control enabled, meaning you won't be able to do anything with the
|
||||
OctoPrint by default ships with Access Control enabled, meaning you won't be able to do anything with the
|
||||
printer unless you login first as a configured user. This is to <strong>prevent strangers - possibly with
|
||||
malicious intent - to gain access to your printer</strong> via the internet or another untrustworthy network
|
||||
and using it in such a way that it is damaged or worse (i.e. causes a fire).
|
||||
</p>
|
||||
<p>
|
||||
It looks like you haven't configured access control yet. Please <strong>set up an username and password</strong> for the
|
||||
It looks like you haven't configured access control yet. Please <strong>set up a username and password</strong> for the
|
||||
initial administrator account who will have full access to both the printer and OctoPrint's settings, then click
|
||||
on "Keep Access Control Enabled":
|
||||
</p>{% endtrans %}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<h3>{{ _('Webcam & Timelapse Recordings') }}</h3>
|
||||
|
||||
{% trans %}<p>
|
||||
If you have a webcam, you may now configure it here.
|
||||
</p>{% endtrans %}
|
||||
|
||||
<h4>{{ _('Webcam') }}</h4>
|
||||
|
||||
{% trans %}<p>
|
||||
The <strong>Stream URL</strong> is the URL OctoPrint uses to embed the
|
||||
actual webcam stream (which should be an MJPG stream) into the web
|
||||
interface. This needs to be reachable <strong>from your browser</strong>.
|
||||
</p><p>
|
||||
It may be
|
||||
<ul>
|
||||
<li>a fully qualified URL (<code>http://host:port/path</code>),</li>
|
||||
<li>a URL defaulting to the protocol used for accessing the web
|
||||
interface (e.g. <code>//host:port/path</code>),</li>
|
||||
<li>an absolute path on the same host as OctoPrint (e.g. <code>/absolute/path</code>) or</li>
|
||||
<li>a relative path on the same host as OctoPrint (e.g. <code>relative/path</code>)</li>
|
||||
</ul>
|
||||
</p><p>
|
||||
The <strong>Snapshot URL</strong> is the URL OctoPrint uses to fetch single
|
||||
images from the webcam for creating timelapse recordings. This needs to be
|
||||
reachable <strong>from the OctoPrint server</strong>.
|
||||
</p>{% endtrans %}
|
||||
|
||||
<form class="form-horizontal" data-bind="with: settingsViewModel">
|
||||
{% include "dialogs/_snippets/configurewebcamurls.jinja2" %}
|
||||
</form>
|
||||
|
||||
<h4>{{ _('Timelapse Recordings') }}</h4>
|
||||
|
||||
{% trans %}<p>
|
||||
To render the snapshots into timelapse recordings, OctoPrint also needs to
|
||||
know the correct <strong>path to FFMPEG</strong>.
|
||||
</p>{% endtrans %}
|
||||
|
||||
<form class="form-horizontal" data-bind="with: settingsViewModel">
|
||||
{% include "dialogs/_snippets/configureffmpeg.jinja2" %}
|
||||
</form>
|
||||
|
|
@ -100,7 +100,7 @@ def pluginCommand(name):
|
|||
|
||||
@api.route("/setup/wizard", methods=["GET"])
|
||||
def wizardState():
|
||||
if not s().getBoolean(["server", "firstRun"]) and not admin_permission.can():
|
||||
if not admin_permission.can():
|
||||
abort(403)
|
||||
|
||||
result = dict()
|
||||
|
|
@ -117,29 +117,28 @@ def wizardState():
|
|||
|
||||
return jsonify(result)
|
||||
|
||||
@api.route("/setup", methods=["POST"])
|
||||
def firstRunSetup():
|
||||
if not s().getBoolean(["server", "firstRun"]):
|
||||
|
||||
@api.route("/setup/wizard", methods=["POST"])
|
||||
def wizardFinish():
|
||||
if not admin_permission.can():
|
||||
abort(403)
|
||||
|
||||
if "ac" in request.values.keys() and request.values["ac"] in valid_boolean_trues and \
|
||||
"user" in request.values.keys() and "pass1" in request.values.keys() and \
|
||||
"pass2" in request.values.keys() and request.values["pass1"] == request.values["pass2"]:
|
||||
# configure access control
|
||||
s().setBoolean(["accessControl", "enabled"], True)
|
||||
octoprint.server.userManager.addUser(request.values["user"], request.values["pass1"], True, ["user", "admin"], overwrite=True)
|
||||
s().setBoolean(["server", "firstRun"], False)
|
||||
elif "ac" in request.values.keys() and not request.values["ac"] in valid_boolean_trues:
|
||||
# disable access control
|
||||
s().setBoolean(["accessControl", "enabled"], False)
|
||||
if s().getBoolean(["server", "firstRun"]):
|
||||
s().setBoolean(["server", "firstRun"], False)
|
||||
|
||||
octoprint.server.loginManager.anonymous_user = octoprint.users.DummyUser
|
||||
octoprint.server.principals.identity_loaders.appendleft(octoprint.users.dummy_identity_loader)
|
||||
wizard_plugins = octoprint.server.pluginManager.get_implementations(octoprint.plugin.WizardPlugin)
|
||||
for implementation in wizard_plugins:
|
||||
name = implementation._identifier
|
||||
try:
|
||||
implementation.on_wizard_finish()
|
||||
except:
|
||||
logging.getLogger(__name__).exceptino("There was an error finishing the wizard for {}, ignoring".format(name))
|
||||
|
||||
s().save()
|
||||
|
||||
return NO_CONTENT
|
||||
|
||||
|
||||
#~~ system state
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -613,7 +613,6 @@ def collect_plugin_assets(enable_gcodeviewer=True, enable_timelapse=True, prefer
|
|||
'js/app/viewmodels/appearance.js',
|
||||
'js/app/viewmodels/connection.js',
|
||||
'js/app/viewmodels/control.js',
|
||||
'js/app/viewmodels/firstrun_wizard.js',
|
||||
'js/app/viewmodels/files.js',
|
||||
'js/app/viewmodels/loginstate.js',
|
||||
'js/app/viewmodels/navigation.js',
|
||||
|
|
|
|||
|
|
@ -182,7 +182,6 @@ def index():
|
|||
templates["wizard"]["entries"] = dict(
|
||||
firstrunstart=(gettext("Start"), dict(template="dialogs/wizard/firstrun_start.jinja2", _div="wizard_firstrun_start")),
|
||||
firstrunend=(gettext("Finish"), dict(template="dialogs/wizard/firstrun_end.jinja2", _div="wizard_firstrun_end")),
|
||||
access=(gettext("Access Control"), dict(template="dialogs/wizard/firstrun_acl.jinja2", _div="wizard_firstrun_acl", custom_bindings=True))
|
||||
)
|
||||
|
||||
# extract data from template plugins
|
||||
|
|
|
|||
|
|
@ -446,12 +446,14 @@ $(function() {
|
|||
|
||||
if (object == undefined || !object.length) {
|
||||
log.info("Did not bind view model", viewModel.constructor.name, "to target", target, "since it does not exist");
|
||||
viewModel.unbound = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var element = object.get(0);
|
||||
if (element == undefined) {
|
||||
log.info("Did not bind view model", viewModel.constructor.name, "to target", target, "since it does not exist");
|
||||
viewModel.unbound = true;
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -460,6 +462,7 @@ $(function() {
|
|||
log.debug("View model", viewModel.constructor.name, "bound to", target);
|
||||
} catch (exc) {
|
||||
log.error("Could not bind view model", viewModel.constructor.name, "to target", target, ":", (exc.stack || exc));
|
||||
viewModel.unbound = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ $(function() {
|
|||
|
||||
self.allViewModels = undefined;
|
||||
|
||||
self.finishing = false;
|
||||
|
||||
self.isDialogActive = function() {
|
||||
return self.wizardDialog.is(":visible");
|
||||
};
|
||||
|
|
@ -18,17 +20,19 @@ $(function() {
|
|||
|
||||
self.getWizardDetails(function(response) {
|
||||
_.each(self.allViewModels, function(viewModel) {
|
||||
if (viewModel.hasOwnProperty("onWizardDetails")) {
|
||||
if (!viewModel.unbound && viewModel.hasOwnProperty("onWizardDetails")) {
|
||||
viewModel.onWizardDetails(response);
|
||||
}
|
||||
});
|
||||
|
||||
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); }
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -73,7 +77,7 @@ $(function() {
|
|||
var active = tab[0].id;
|
||||
if (active != undefined) {
|
||||
_.each(allViewModels, function(viewModel) {
|
||||
if (viewModel.hasOwnProperty("onAfterWizardTabChange")) {
|
||||
if (!viewModel.unbound && viewModel.hasOwnProperty("onAfterWizardTabChange")) {
|
||||
viewModel.onAfterWizardTabChange(active);
|
||||
}
|
||||
});
|
||||
|
|
@ -96,7 +100,7 @@ $(function() {
|
|||
if (current != undefined && next != undefined) {
|
||||
var result = true;
|
||||
_.each(allViewModels, function(viewModel) {
|
||||
if (viewModel.hasOwnProperty("onWizardTabChange")) {
|
||||
if (!viewModel.unbound && viewModel.hasOwnProperty("onWizardTabChange")) {
|
||||
result = result && (viewModel.onWizardTabChange(current, next) !== false);
|
||||
}
|
||||
});
|
||||
|
|
@ -106,19 +110,20 @@ $(function() {
|
|||
onFinish: function(tab, navigation, index) {
|
||||
var closeDialog = true;
|
||||
_.each(allViewModels, function(viewModel) {
|
||||
if (viewModel.hasOwnProperty("onBeforeWizardFinish")) {
|
||||
if (!viewModel.unbound && viewModel.hasOwnProperty("onBeforeWizardFinish")) {
|
||||
closeDialog = closeDialog && (viewModel.onBeforeWizardFinish() !== false);
|
||||
}
|
||||
});
|
||||
|
||||
if (closeDialog) {
|
||||
_.each(allViewModels, function(viewModel) {
|
||||
if (viewModel.hasOwnProperty("onWizardFinish")) {
|
||||
if (!viewModel.unbound && viewModel.hasOwnProperty("onWizardFinish")) {
|
||||
viewModel.onWizardFinish();
|
||||
}
|
||||
});
|
||||
self.settingsViewModel.saveEnqueued();
|
||||
self.closeDialog();
|
||||
self.finishWizard(function() {
|
||||
self.closeDialog();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -136,8 +141,28 @@ $(function() {
|
|||
});
|
||||
};
|
||||
|
||||
self.finishWizard = function(callback) {
|
||||
self.finishing = true;
|
||||
|
||||
self.settingsViewModel.saveEnqueued();
|
||||
$.ajax({
|
||||
url: API_BASEURL + "setup/wizard",
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
contentType: "application/json; charset=UTF-8",
|
||||
success: function() {
|
||||
self.finishing = false;
|
||||
callback();
|
||||
},
|
||||
failure: function() {
|
||||
self.finishing = false;
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
self.onSettingsPreventRefresh = function() {
|
||||
if (self.isDialogActive() && hasDataChanged(self.settingsViewModel.getLocalData(), self.settingsViewModel.lastReceivedSettings)) {
|
||||
if (!self.finishing && self.isDialogActive()
|
||||
&& hasDataChanged(self.settingsViewModel.getLocalData(), self.settingsViewModel.lastReceivedSettings)) {
|
||||
// we have local changes, show update dialog
|
||||
self.settingsViewModel.settingsUpdatedDialog.modal("show");
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
<div id="first_run_dialog" class="modal hide fade" data-backdrop="static" data-keyboard="false">
|
||||
<div class="modal-header">
|
||||
<h3><i class="icon-warning-sign"></i> {{ _('Configure Access Control') }}</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{% trans %}<p>
|
||||
<strong>Please read the following, it is very important for your printer's health!</strong>
|
||||
</p>
|
||||
<p>
|
||||
OctoPrint by default now ships with Access Control enabled, meaning you won't be able to do anything with the
|
||||
printer unless you login first as a configured user. This is to <strong>prevent strangers - possibly with
|
||||
malicious intent - to gain access to your printer</strong> via the internet or another untrustworthy network
|
||||
and using it in such a way that it is damaged or worse (i.e. causes a fire).
|
||||
</p>
|
||||
<p>
|
||||
It looks like you haven't configured access control yet. Please <strong>set up an username and password</strong> for the
|
||||
initial administrator account who will have full access to both the printer and OctoPrint's settings, then click
|
||||
on "Keep Access Control Enabled":
|
||||
</p>{% endtrans %}
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group" data-bind="css: {success: validUsername()}">
|
||||
<label class="control-label" for="first_run_username">{{ _('Username') }}</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-medium" data-bind="value: username, valueUpdate: 'afterkeydown'">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group" data-bind="css: {success: validPassword()}">
|
||||
<label class="control-label" for="first_run_username">{{ _('Password') }}</label>
|
||||
<div class="controls">
|
||||
<input type="password" class="input-medium" data-bind="value: password, valueUpdate: 'afterkeydown'">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group" data-bind="css: {error: passwordMismatch(), success: validPassword() && !passwordMismatch()}">
|
||||
<label class="control-label" for="first_run_username">{{ _('Confirm Password') }}</label>
|
||||
<div class="controls">
|
||||
<input type="password" class="input-medium" data-bind="value: confirmedPassword, valueUpdate: 'afterkeydown'">
|
||||
<span class="help-inline" data-bind="visible: passwordMismatch()">{{ _('Passwords do not match') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% trans %}<p>
|
||||
<strong>Note:</strong> In case that your OctoPrint installation is only accessible from within a trustworthy network and you don't
|
||||
need Access Control for other reasons, you may alternatively disable Access Control. You should only
|
||||
do this if you are absolutely certain that only people you know and trust will be able to connect to it.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Do NOT underestimate the risk of an unsecured access from the internet to your printer!</strong>
|
||||
</p>{% endtrans %}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="#" class="btn btn-danger" data-bind="click: disableAccessControl">{{ _('Disable Access Control') }}</a>
|
||||
<a href="#" class="btn btn-primary" data-bind="click: keepAccessControl, enable: validData(), css: {disabled: !validData()}">{{ _('Keep Access Control Enabled') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -124,7 +124,6 @@
|
|||
</div>
|
||||
|
||||
<!-- Dialogs -->
|
||||
{% include 'dialogs/firstrun.jinja2' %}
|
||||
{% include 'dialogs/settings.jinja2' %}
|
||||
{% include 'dialogs/slicing.jinja2' %}
|
||||
{% include 'dialogs/usersettings.jinja2' %}
|
||||
|
|
|
|||
Loading…
Reference in a new issue