More user control for server side settings update when dialog is open

The user will now be offered a dialog with two choices if a server side update
of the settings is detected while the user has the settings dialog opened and made
local changes: Either reload all changes, effectively overwriting any local changes,
or only set those settings changed on the server that are not also changed locally.
This commit is contained in:
Gina Häußge 2015-08-14 14:34:41 +02:00
parent 7518b44e6d
commit 833a618dbe
3 changed files with 279 additions and 169 deletions

View file

@ -461,3 +461,53 @@ function splitTextToArray(text, sep, stripEmpty, filter) {
function(item) { return (stripEmpty ? item : true) && (filter ? filter(item) : true); }
);
}
function hasDataChanged(data, oldData) {
if (data == undefined) {
return false;
}
if (oldData == undefined) {
return true;
}
if (_.isPlainObject(data)) {
return _.any(_.keys(data), function(key) {return hasDataChanged(data[key], oldData[key]);});
} else {
return !_.isEqual(data, oldData);
}
};
function getOnlyChangedData(data, oldData) {
if (data == undefined) {
return {};
}
if (oldData == undefined) {
return data;
}
var f = function(root, oldRoot) {
if (!_.isPlainObject(root)) {
return root;
}
var retval = {};
_.forOwn(root, function(value, key) {
var oldValue = oldRoot[key];
if (_.isPlainObject(value)) {
if (hasDataChanged(value, oldValue)) {
retval[key] = f(value, oldValue);
}
} else {
if (!_.isEqual(value, oldValue)) {
retval[key] = value;
}
}
});
return retval;
};
return f(data, oldData);
};

View file

@ -20,6 +20,7 @@ $(function() {
self.appearance_defaultLanguage = ko.observable();
self.settingsDialog = undefined;
self.settings_dialog_update_detected = undefined;
self.translationManagerDialog = undefined;
self.translationUploadElement = $("#settings_appearance_managelanguagesdialog_upload");
self.translationUploadButton = $("#settings_appearance_managelanguagesdialog_upload_start");
@ -162,6 +163,7 @@ $(function() {
self.server_commands_serverRestartCommand = ko.observable(undefined);
self.settings = undefined;
self.lastReceivedSettings = undefined;
self.addTemperatureProfile = function() {
self.temperature_profiles.push({name: "New", extruder:0, bed:0});
@ -180,11 +182,16 @@ $(function() {
};
self.onSettingsShown = function() {
self.requestData();
self.requestData();
};
self.isDialogActive = function() {
return self.settingsDialog.is(":visible");
};
self.onStartup = function() {
self.settingsDialog = $('#settings_dialog');
self.settingsUpdatedDialog = $('#settings_dialog_update_detected');
self.translationManagerDialog = $('#settings_appearance_managelanguagesdialog');
self.translationUploadElement = $("#settings_appearance_managelanguagesdialog_upload");
self.translationUploadButton = $("#settings_appearance_managelanguagesdialog_upload_start");
@ -245,6 +252,19 @@ $(function() {
}
});
});
$(".reload_all", self.settingsUpdatedDialog).click(function(e) {
e.preventDefault();
self.settingsUpdatedDialog.modal("hide");
self.requestData();
return false;
});
$(".reload_nonconflicts", self.settingsUpdatedDialog).click(function(e) {
e.preventDefault();
self.settingsUpdatedDialog.modal("hide");
self.requestData(undefined, true);
return false;
});
};
self.show = function() {
@ -268,7 +288,7 @@ $(function() {
return false;
};
self.requestData = function(callback) {
self.requestData = function(callback, local) {
if (self.receiving()) {
if (callback) {
self.callbacks.push(callback);
@ -287,7 +307,7 @@ $(function() {
}
try {
self.fromResponse(response);
self.fromResponse(response, local);
var cb;
while (self.callbacks.length) {
@ -378,92 +398,185 @@ $(function() {
})
};
self.fromResponse = function(response) {
self._getLocalData = function() {
var data = {};
if (self.settings != undefined) {
data = ko.mapping.toJS(self.settings);
}
data = _.extend(data, {
"api" : {
"enabled": self.api_enabled(),
"key": self.api_key(),
"allowCrossOrigin": self.api_allowCrossOrigin()
},
"appearance" : {
"name": self.appearance_name(),
"color": self.appearance_color(),
"colorTransparent": self.appearance_colorTransparent(),
"defaultLanguage": self.appearance_defaultLanguage()
},
"printer": {
"defaultExtrusionLength": self.printer_defaultExtrusionLength()
},
"webcam": {
"streamUrl": self.webcam_streamUrl(),
"snapshotUrl": self.webcam_snapshotUrl(),
"ffmpegPath": self.webcam_ffmpegPath(),
"bitrate": self.webcam_bitrate(),
"ffmpegThreads": self.webcam_ffmpegThreads(),
"watermark": self.webcam_watermark(),
"flipH": self.webcam_flipH(),
"flipV": self.webcam_flipV(),
"rotate90": self.webcam_rotate90()
},
"feature": {
"gcodeViewer": self.feature_gcodeViewer(),
"temperatureGraph": self.feature_temperatureGraph(),
"waitForStart": self.feature_waitForStart(),
"alwaysSendChecksum": self.feature_alwaysSendChecksum(),
"sdSupport": self.feature_sdSupport(),
"sdAlwaysAvailable": self.feature_sdAlwaysAvailable(),
"swallowOkAfterResend": self.feature_swallowOkAfterResend(),
"repetierTargetTemp": self.feature_repetierTargetTemp(),
"externalHeatupDetection": !self.feature_disableExternalHeatupDetection(),
"keyboardControl": self.feature_keyboardControl(),
"pollWatched": self.feature_pollWatched()
},
"serial": {
"port": self.serial_port(),
"baudrate": self.serial_baudrate(),
"autoconnect": self.serial_autoconnect(),
"timeoutConnection": self.serial_timeoutConnection(),
"timeoutDetection": self.serial_timeoutDetection(),
"timeoutCommunication": self.serial_timeoutCommunication(),
"timeoutTemperature": self.serial_timeoutTemperature(),
"timeoutSdStatus": self.serial_timeoutSdStatus(),
"log": self.serial_log(),
"additionalPorts": commentableLinesToArray(self.serial_additionalPorts()),
"additionalBaudrates": _.map(splitTextToArray(self.serial_additionalBaudrates(), ",", true, function(item) { return !isNaN(parseInt(item)); }), function(item) { return parseInt(item); }),
"longRunningCommands": splitTextToArray(self.serial_longRunningCommands(), ",", true),
"checksumRequiringCommands": splitTextToArray(self.serial_checksumRequiringCommands(), ",", true),
"helloCommand": self.serial_helloCommand()
},
"folder": {
"uploads": self.folder_uploads(),
"timelapse": self.folder_timelapse(),
"timelapseTmp": self.folder_timelapseTmp(),
"logs": self.folder_logs(),
"watched": self.folder_watched()
},
"temperature": {
"profiles": self.temperature_profiles(),
"cutoff": self.temperature_cutoff()
},
"system": {
"actions": self.system_actions()
},
"terminalFilters": self.terminalFilters(),
"scripts": {
"gcode": {
"beforePrintStarted": self.scripts_gcode_beforePrintStarted(),
"afterPrintDone": self.scripts_gcode_afterPrintDone(),
"afterPrintCancelled": self.scripts_gcode_afterPrintCancelled(),
"afterPrintPaused": self.scripts_gcode_afterPrintPaused(),
"beforePrintResumed": self.scripts_gcode_beforePrintResumed(),
"afterPrinterConnected": self.scripts_gcode_afterPrinterConnected(),
"beforePrinterDisconnected": self.scripts_gcode_beforePrinterDisconnected()
}
},
"server": {
"commands": {
"systemShutdownCommand": self.server_commands_systemShutdownCommand(),
"systemRestartCommand": self.server_commands_systemRestartCommand(),
"serverRestartCommand": self.server_commands_serverRestartCommand()
}
}
});
return data;
};
self.fromResponse = function(response, local) {
// server side changes to set
var serverChangedData;
// client side changes to keep
var clientChangedData;
if (local) {
// local is true, so we'll keep all local changes and only update what's been updated server side
serverChangedData = getOnlyChangedData(response, self.lastReceivedSettings);
clientChangedData = getOnlyChangedData(self._getLocalData(), self.lastReceivedSettings);
} else {
// local is false or unset, so we'll forcefully update with the settings from the server
serverChangedData = response;
clientChangedData = undefined;
}
// last received settings reset to response
self.lastReceivedSettings = response;
if (self.settings === undefined) {
self.settings = ko.mapping.fromJS(response);
self.settings = ko.mapping.fromJS(serverChangedData);
} else {
ko.mapping.fromJS(response, self.settings);
ko.mapping.fromJS(serverChangedData, self.settings);
}
self.api_enabled(response.api.enabled);
self.api_key(response.api.key);
self.api_allowCrossOrigin(response.api.allowCrossOrigin);
// some special apply functions for various observables
var specialMappings = {
appearance: {
defaultLanguage: function(value) {
self.appearance_defaultLanguage("_default");
if (_.includes(self.locale_languages, value)) {
self.appearance_defaultLanguage(value);
}
}
},
feature: {
externalHeatupDetection: function(value) { self.feature_disableExternalHeatupDetection(!value) }
},
serial: {
additionalPorts : function(value) { self.serial_additionalPorts(value.join("\n"))},
additionalBaudrates: function(value) { self.serial_additionalBaudrates(value.join(", "))},
longRunningCommands: function(value) { self.serial_longRunningCommands(value.join(", "))},
checksumRequiringCommands: function(value) { self.serial_checksumRequiringCommands(value.join(", "))}
}
};
self.appearance_name(response.appearance.name);
self.appearance_color(response.appearance.color);
self.appearance_colorTransparent(response.appearance.colorTransparent);
self.appearance_defaultLanguage("_default");
if (_.includes(self.locale_languages, response.appearance.defaultLanguage)) {
self.appearance_defaultLanguage(response.appearance.defaultLanguage);
}
var mapToObservables = function(data, mapping, local, keyPrefix) {
if (!_.isPlainObject(data)) {
return;
}
self.printer_defaultExtrusionLength(response.printer.defaultExtrusionLength);
_.forOwn(data, function(value, key) {
var observable = key;
if (keyPrefix != undefined) {
observable = keyPrefix + "_" + observable;
}
self.webcam_streamUrl(response.webcam.streamUrl);
self.webcam_snapshotUrl(response.webcam.snapshotUrl);
self.webcam_ffmpegPath(response.webcam.ffmpegPath);
self.webcam_bitrate(response.webcam.bitrate);
self.webcam_ffmpegThreads(response.webcam.ffmpegThreads);
self.webcam_watermark(response.webcam.watermark);
self.webcam_flipH(response.webcam.flipH);
self.webcam_flipV(response.webcam.flipV);
self.webcam_rotate90(response.webcam.rotate90);
if (_.isPlainObject(value)) {
// value is another object, we'll dive deeper
mapToObservables(value, (mapping && mapping[key]) ? mapping[key] : undefined, (local && local[key]) ? local[key] : undefined, observable);
} else {
// if we have a local version of this field, we'll not override it
if (local && local.hasOwnProperty(key)) {
return;
}
self.feature_gcodeViewer(response.feature.gcodeViewer);
self.feature_temperatureGraph(response.feature.temperatureGraph);
self.feature_waitForStart(response.feature.waitForStart);
self.feature_alwaysSendChecksum(response.feature.alwaysSendChecksum);
self.feature_sdSupport(response.feature.sdSupport);
self.feature_sdAlwaysAvailable(response.feature.sdAlwaysAvailable);
self.feature_swallowOkAfterResend(response.feature.swallowOkAfterResend);
self.feature_repetierTargetTemp(response.feature.repetierTargetTemp);
self.feature_disableExternalHeatupDetection(!response.feature.externalHeatupDetection);
self.feature_keyboardControl(response.feature.keyboardControl);
self.feature_pollWatched(response.feature.pollWatched);
// applying the value usually means we'll call the observable with it
var apply = function(v) { if (self.hasOwnProperty(observable)) { self[observable](v) } };
self.serial_port(response.serial.port);
self.serial_baudrate(response.serial.baudrate);
self.serial_portOptions(response.serial.portOptions);
self.serial_baudrateOptions(response.serial.baudrateOptions);
self.serial_autoconnect(response.serial.autoconnect);
self.serial_timeoutConnection(response.serial.timeoutConnection);
self.serial_timeoutDetection(response.serial.timeoutDetection);
self.serial_timeoutCommunication(response.serial.timeoutCommunication);
self.serial_timeoutTemperature(response.serial.timeoutTemperature);
self.serial_timeoutSdStatus(response.serial.timeoutSdStatus);
self.serial_log(response.serial.log);
self.serial_additionalPorts(response.serial.additionalPorts.join("\n"));
self.serial_additionalBaudrates(response.serial.additionalBaudrates.join(", "));
self.serial_longRunningCommands(response.serial.longRunningCommands.join(", "));
self.serial_checksumRequiringCommands(response.serial.checksumRequiringCommands.join(", "));
self.serial_helloCommand(response.serial.helloCommand);
// if we have a custom apply function for this, we'll use it instead
if (mapping && mapping[key] && _.isFunction(mapping[key])) {
apply = mapping[key];
}
self.folder_uploads(response.folder.uploads);
self.folder_timelapse(response.folder.timelapse);
self.folder_timelapseTmp(response.folder.timelapseTmp);
self.folder_logs(response.folder.logs);
self.folder_watched(response.folder.watched);
apply(value);
}
});
};
self.temperature_profiles(response.temperature.profiles);
self.scripts_gcode_beforePrintStarted(response.scripts.gcode.beforePrintStarted);
self.scripts_gcode_afterPrintDone(response.scripts.gcode.afterPrintDone);
self.scripts_gcode_afterPrintCancelled(response.scripts.gcode.afterPrintCancelled);
self.scripts_gcode_afterPrintPaused(response.scripts.gcode.afterPrintPaused);
self.scripts_gcode_beforePrintResumed(response.scripts.gcode.beforePrintResumed);
self.scripts_gcode_afterPrinterConnected(response.scripts.gcode.afterPrinterConnected);
self.scripts_gcode_beforePrinterDisconnected(response.scripts.gcode.beforePrinterDisconnected);
self.temperature_profiles(response.temperature.profiles);
self.temperature_cutoff(response.temperature.cutoff);
self.system_actions(response.system.actions);
self.terminalFilters(response.terminalFilters);
self.server_commands_systemShutdownCommand(response.server.commands.systemShutdownCommand);
self.server_commands_systemRestartCommand(response.server.commands.systemRestartCommand);
self.server_commands_serverRestartCommand(response.server.commands.serverRestartCommand);
mapToObservables(serverChangedData, specialMappings, clientChangedData);
};
self.saveData = function (data, successCallback) {
@ -472,97 +585,9 @@ $(function() {
if (data == undefined) {
// we only set sending to true when we didn't include data
self.sending(true);
data = ko.mapping.toJS(self.settings);
data = _.extend(data, {
"api" : {
"enabled": self.api_enabled(),
"key": self.api_key(),
"allowCrossOrigin": self.api_allowCrossOrigin()
},
"appearance" : {
"name": self.appearance_name(),
"color": self.appearance_color(),
"colorTransparent": self.appearance_colorTransparent(),
"defaultLanguage": self.appearance_defaultLanguage()
},
"printer": {
"defaultExtrusionLength": self.printer_defaultExtrusionLength()
},
"webcam": {
"streamUrl": self.webcam_streamUrl(),
"snapshotUrl": self.webcam_snapshotUrl(),
"ffmpegPath": self.webcam_ffmpegPath(),
"bitrate": self.webcam_bitrate(),
"ffmpegThreads": self.webcam_ffmpegThreads(),
"watermark": self.webcam_watermark(),
"flipH": self.webcam_flipH(),
"flipV": self.webcam_flipV(),
"rotate90": self.webcam_rotate90()
},
"feature": {
"gcodeViewer": self.feature_gcodeViewer(),
"temperatureGraph": self.feature_temperatureGraph(),
"waitForStart": self.feature_waitForStart(),
"alwaysSendChecksum": self.feature_alwaysSendChecksum(),
"sdSupport": self.feature_sdSupport(),
"sdAlwaysAvailable": self.feature_sdAlwaysAvailable(),
"swallowOkAfterResend": self.feature_swallowOkAfterResend(),
"repetierTargetTemp": self.feature_repetierTargetTemp(),
"externalHeatupDetection": !self.feature_disableExternalHeatupDetection(),
"keyboardControl": self.feature_keyboardControl(),
"pollWatched": self.feature_pollWatched()
},
"serial": {
"port": self.serial_port(),
"baudrate": self.serial_baudrate(),
"autoconnect": self.serial_autoconnect(),
"timeoutConnection": self.serial_timeoutConnection(),
"timeoutDetection": self.serial_timeoutDetection(),
"timeoutCommunication": self.serial_timeoutCommunication(),
"timeoutTemperature": self.serial_timeoutTemperature(),
"timeoutSdStatus": self.serial_timeoutSdStatus(),
"log": self.serial_log(),
"additionalPorts": commentableLinesToArray(self.serial_additionalPorts()),
"additionalBaudrates": _.map(splitTextToArray(self.serial_additionalBaudrates(), ",", true, function(item) { return !isNaN(parseInt(item)); }), function(item) { return parseInt(item); }),
"longRunningCommands": splitTextToArray(self.serial_longRunningCommands(), ",", true),
"checksumRequiringCommands": splitTextToArray(self.serial_checksumRequiringCommands(), ",", true),
"helloCommand": self.serial_helloCommand()
},
"folder": {
"uploads": self.folder_uploads(),
"timelapse": self.folder_timelapse(),
"timelapseTmp": self.folder_timelapseTmp(),
"logs": self.folder_logs(),
"watched": self.folder_watched()
},
"temperature": {
"profiles": self.temperature_profiles(),
"cutoff": self.temperature_cutoff()
},
"system": {
"actions": self.system_actions()
},
"terminalFilters": self.terminalFilters(),
"scripts": {
"gcode": {
"beforePrintStarted": self.scripts_gcode_beforePrintStarted(),
"afterPrintDone": self.scripts_gcode_afterPrintDone(),
"afterPrintCancelled": self.scripts_gcode_afterPrintCancelled(),
"afterPrintPaused": self.scripts_gcode_afterPrintPaused(),
"beforePrintResumed": self.scripts_gcode_beforePrintResumed(),
"afterPrinterConnected": self.scripts_gcode_afterPrinterConnected(),
"beforePrinterDisconnected": self.scripts_gcode_beforePrinterDisconnected()
}
},
"server": {
"commands": {
"systemShutdownCommand": self.server_commands_systemShutdownCommand(),
"systemRestartCommand": self.server_commands_systemRestartCommand(),
"serverRestartCommand": self.server_commands_serverRestartCommand()
}
}
});
// we also only sent data that actually changed when no data is specified
data = getOnlyChangedData(self._getLocalData(), self.lastReceivedSettings);
}
$.ajax({
@ -588,7 +613,20 @@ $(function() {
};
self.onEventSettingsUpdated = function() {
self.requestData();
if (self.isDialogActive()) {
if (self.sending() || self.receiving()) {
return;
}
if (!hasDataChanged(self._getLocalData(), self.lastReceivedSettings)) {
self.requestData();
return;
}
self.settingsUpdatedDialog.modal("show");
} else {
self.requestData();
}
};
}

View file

@ -52,3 +52,25 @@
<button class="btn btn-primary" data-bind="click: function() { saveData(undefined, $root.hide) }, enable: !sending(), css: {disabled: sending()}"><i class="icon-spinner icon-spin" data-bind="visible: sending"></i> {{ _('Save') }}</button>
</div>
</div>
<div id="settings_dialog_update_detected" class="modal hide fade" data-keyboard="false">
<div class="modal-header">
<h3>{{ _('Settings update detected') }}</h3>
</div>
<div class="modal-body">
<p>{% trans %}
The settings have been updated on the server. You may reload all settings,
overwriting any changes you might have done locally, or alternatively
only reload those settings you haven't changed locally.
{% endtrans %}</p>
<p>{% trans %}
How do you want to proceed?
{% endtrans %}</p>
</div>
<div class="modal-footer">
<div class="row-fluid">
<a href="#" class="btn btn-danger span6 reload_all">{{ _('Reload all') }}</a>
<a href="#" class="btn btn-primary span6 reload_nonconflicts">{{ _('Reload only non-conflicting changes') }}</a>
</div>
</div>
</div>