From 42376b44d6d1a18211d4e947e5cc28cdbd5e5fc4 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Mon, 7 Dec 2015 22:03:40 +0100 Subject: [PATCH] Implemented more user-friendly editing for advanced options in ngax --- Duplicati/Server/webroot/ngax/index.html | 2 + .../controllers/EditBackupController.js | 25 ++- .../controllers/SystemSettingsController.js | 17 +- .../directives/advancedOptionsEditor.js | 127 ++++++++++++ .../ngax/scripts/directives/backupEditUri.js | 4 +- .../scripts/directives/parseAdvancedOption.js | 183 ++++++++++++++++++ .../webroot/ngax/scripts/services/AppUtils.js | 69 ++++++- .../Server/webroot/ngax/styles/styles2.css | 30 +++ .../webroot/ngax/templates/addoredit.html | 14 +- .../ngax/templates/advancedoptionseditor.html | 46 +++++ .../webroot/ngax/templates/edituri.html | 14 +- .../webroot/ngax/templates/settings.html | 17 +- 12 files changed, 522 insertions(+), 26 deletions(-) create mode 100644 Duplicati/Server/webroot/ngax/scripts/directives/advancedOptionsEditor.js create mode 100644 Duplicati/Server/webroot/ngax/scripts/directives/parseAdvancedOption.js create mode 100644 Duplicati/Server/webroot/ngax/templates/advancedoptionseditor.html diff --git a/Duplicati/Server/webroot/ngax/index.html b/Duplicati/Server/webroot/ngax/index.html index 97c24db31..84121a6aa 100644 --- a/Duplicati/Server/webroot/ngax/index.html +++ b/Duplicati/Server/webroot/ngax/index.html @@ -70,12 +70,14 @@ + + diff --git a/Duplicati/Server/webroot/ngax/scripts/controllers/EditBackupController.js b/Duplicati/Server/webroot/ngax/scripts/controllers/EditBackupController.js index 99dee13a1..5b8861df4 100644 --- a/Duplicati/Server/webroot/ngax/scripts/controllers/EditBackupController.js +++ b/Duplicati/Server/webroot/ngax/scripts/controllers/EditBackupController.js @@ -1,4 +1,4 @@ -backupApp.controller('EditBackupController', function ($scope, $routeParams, $location, AppService, AppUtils, SystemInfo) { +backupApp.controller('EditBackupController', function ($scope, $routeParams, $location, $timeout, AppService, AppUtils, SystemInfo) { $scope.SystemInfo = SystemInfo.watch($scope); $scope.AppUtils = AppUtils; @@ -233,7 +233,7 @@ backupApp.controller('EditBackupController', function ($scope, $routeParams, $lo delete extopts['--exclude-files-attributes']; delete extopts['--no-encryption']; - $scope.ExtendedOptions = AppUtils.serializeAdvancedOptions(extopts); + $scope.ExtendedOptions = AppUtils.serializeAdvancedOptionsToArray(extopts); var now = new Date(); if ($scope.Schedule != null) { @@ -266,6 +266,27 @@ backupApp.controller('EditBackupController', function ($scope, $routeParams, $lo } } + function reloadOptionsList() + { + if ($scope.Options == null) + return; + + var encmodule = $scope.Options['encryption-module'] || ''; + var compmodule = $scope.Options['compression-module'] || $scope.Options['--compression-module'] || 'zip'; + var backmodule = $scope.Backup.TargetURL || ''; + var ix = backmodule.indexOf(':'); + if (ix > 0) + backmodule = backmodule.substr(0, ix); + + $scope.ExtendedOptionList = AppUtils.buildOptionList($scope.SystemInfo, encmodule, compmodule, backmodule); + }; + + $scope.$watch("Options['encryption-module']", reloadOptionsList); + $scope.$watch("Options['compression-module']", reloadOptionsList); + $scope.$watch("Options['--compression-module']", reloadOptionsList); + $scope.$watch("Backup.TargetURL", reloadOptionsList); + $scope.$on('systeminfochanged', reloadOptionsList); + if ($routeParams.backupid == null) { AppService.get('/backupdefaults').then(function(data) { diff --git a/Duplicati/Server/webroot/ngax/scripts/controllers/SystemSettingsController.js b/Duplicati/Server/webroot/ngax/scripts/controllers/SystemSettingsController.js index 68cf510a8..fb5cc3a20 100644 --- a/Duplicati/Server/webroot/ngax/scripts/controllers/SystemSettingsController.js +++ b/Duplicati/Server/webroot/ngax/scripts/controllers/SystemSettingsController.js @@ -1,4 +1,14 @@ -backupApp.controller('SystemSettingsController', function($scope, $location, AppService, AppUtils, Localization) { +backupApp.controller('SystemSettingsController', function($scope, $location, AppService, AppUtils, Localization, SystemInfo) { + + $scope.SystemInfo = SystemInfo.watch($scope); + + function reloadOptionsList() { + $scope.advancedOptionList = AppUtils.buildOptionList($scope.SystemInfo, false, false, false); + }; + + reloadOptionsList(); + + $scope.$on('systeminfochanged', reloadOptionsList); AppService.get('/serversettings').then(function(data) { @@ -9,7 +19,8 @@ backupApp.controller('SystemSettingsController', function($scope, $location, App $scope.allowRemoteAccess = data.data['server-listen-interface'] != 'loopback'; $scope.startupDelayDurationValue = data.data['startup-delay'].substr(0, data.data['startup-delay'].length - 1); $scope.startupDelayDurationMultiplier = data.data['startup-delay'].substr(-1); - $scope.advancedOptions = AppUtils.serializeAdvancedOptions(data.data); + $scope.advancedOptions = AppUtils.serializeAdvancedOptionsToArray(data.data); + }, AppUtils.connectionError); @@ -38,7 +49,7 @@ backupApp.controller('SystemSettingsController', function($scope, $location, App AppUtils.mergeAdvancedOptions($scope.advancedOptions, patchdata, $scope.rawdata); - AppService.patch('/serversettings', patchdata).then( + AppService.patch('/serversettings', patchdata, {headers: {'Content-Type': 'application/json; charset=utf-8'}}).then( function() { $location.path('/'); }, diff --git a/Duplicati/Server/webroot/ngax/scripts/directives/advancedOptionsEditor.js b/Duplicati/Server/webroot/ngax/scripts/directives/advancedOptionsEditor.js new file mode 100644 index 000000000..5a12426ad --- /dev/null +++ b/Duplicati/Server/webroot/ngax/scripts/directives/advancedOptionsEditor.js @@ -0,0 +1,127 @@ +backupApp.directive('advancedOptionsEditor', function() { + return { + restrict: 'E', + scope: { + ngModel: '=', + ngOptionList: '=' + }, + templateUrl: 'templates/advancedoptionseditor.html', + controller: function($scope, $timeout) { + $scope.NewItem = null; + + var optionmap = null; + + function rebuildOptionMap() { + optionmap = {}; + + for(var n in $scope.ngOptionList) + optionmap[$scope.ngOptionList[n].Name.toLowerCase()] = $scope.ngOptionList[n]; + } + + function coreName(name) { + if (name == null) + return ''; + if (typeof(name) != typeof('')) + name = name.Name; + + name = name || ''; + if (name.indexOf('--') == 0) + name = name.substr(2); + if (name.indexOf('=') >= 0) + name = name.substr(0, name.indexOf('=')); + return name; + } + + $scope.getEntry = function(key) { + if (optionmap == null) + return null; + + return optionmap[coreName(key)]; + }; + + $scope.getDisplayName = function(name) { + var item = $scope.getEntry(name); + if (item == null) + return coreName(name); + + return item.Name + ': ' + item.ShortDescription; + }; + + $scope.getInputType = function(item) { + var item = $scope.getEntry(item); + if (item == null) + return 'text'; + + if (item.Type == 'Enumeration') + return 'enum'; + else if (item.Type == 'Boolean') + return 'bool'; + else if (item.Type == 'Password') + return 'password'; + else if (item.Type == 'Integer') + return 'int'; + else if (item.Type == 'Size') + return 'size'; + else if (item.Type == 'Timespan') + return 'timespan'; + else + return 'text'; + }; + + $scope.getShortName = function(name) { + var item = $scope.getEntry(name); + if (item == null) + return coreName(name); + + return item.Name; + }; + + $scope.getShortDescription = function(item) { + var item = $scope.getEntry(item); + if (item == null) + return null; + + return item.ShortDescription; + }; + + $scope.getLongDescription = function(item) { + var item = $scope.getEntry(item); + if (item == null) + return null; + + return item.LongDescription; + }; + + $scope.getEnumerations = function(item) { + var item = $scope.getEntry(item); + if (item == null) + return null; + + return item.ValidValues; + }; + + $scope.deleteItem = function(item) { + for (var i = $scope.ngModel.length - 1; i >= 0; i--) { + if ($scope.ngModel[i] == item) { + $scope.ngModel.splice(i, 1); + return; + } + }; + }; + + $scope.$watch('ngOptionList', rebuildOptionMap); + $scope.$watch('NewItem', function() { + if ($scope.NewItem != null) { + var opt = '--' + $scope.NewItem.Name; + var itm = $scope.getEntry(opt); + if (itm != null && itm.DefaultValue != null) + opt += '=' + itm.DefaultValue; + + $scope.ngModel.push(opt); + $scope.NewItem = null; + } + }); + + } + }; +}); \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/scripts/directives/backupEditUri.js b/Duplicati/Server/webroot/ngax/scripts/directives/backupEditUri.js index 9c1966f8f..1c993cfbc 100644 --- a/Duplicati/Server/webroot/ngax/scripts/directives/backupEditUri.js +++ b/Duplicati/Server/webroot/ngax/scripts/directives/backupEditUri.js @@ -69,7 +69,7 @@ backupApp.directive('backupEditUri', function() { if (confirm('The server certificate could not be validated.\nDo you want to approve the SSL certificate with the hash: ' + hash + '?' )) { hasTriedCert = true; - scope.AdvancedOptions += '\r\n--accept-specified-ssl-hash=' + hash; + scope.AdvancedOptions.push('--accept-specified-ssl-hash=' + hash); testConnection(); } @@ -141,7 +141,7 @@ backupApp.directive('backupEditUri', function() { delete parts['--auth-username']; delete parts['--auth-password']; delete parts['--use-ssl']; - scope.AdvancedOptions = AppUtils.serializeAdvancedOptions(parts); + scope.AdvancedOptions = AppUtils.serializeAdvancedOptionsToArray(parts); }; $scope.SystemInfo = SystemInfo.watch($scope, function() { diff --git a/Duplicati/Server/webroot/ngax/scripts/directives/parseAdvancedOption.js b/Duplicati/Server/webroot/ngax/scripts/directives/parseAdvancedOption.js new file mode 100644 index 000000000..eb22dd9b2 --- /dev/null +++ b/Duplicati/Server/webroot/ngax/scripts/directives/parseAdvancedOption.js @@ -0,0 +1,183 @@ +backupApp.directive('parseAdvancedOption', function(AppUtils) { + return { + restrict: 'A', + require: ['ngModel'], + link: function(scope, element, attr, ctrl) { + + var name = null; + + ctrl[0].$parsers.push(function(txt) { + if (name == null) + return null; + + return name + '=' + txt; + }); + + ctrl[0].$formatters.push(function(src) { + src = src || ''; + var ix = src.indexOf('='); + if (ix >= 0) { + name = src.substr(0, ix); + return src.substr(ix + 1); + } + else + { + name = src; + return null; + } + }); + } + }; +}); + +backupApp.directive('parseAdvancedOptionBool', function(AppUtils) { + return { + restrict: 'A', + require: ['ngModel'], + link: function(scope, element, attr, ctrl) { + + var name = null; + + ctrl[0].$parsers.push(function(txt) { + if (name == null) + return null; + + return name + '=' + txt; + }); + + ctrl[0].$formatters.push(function(src) { + src = src || ''; + var ix = src.indexOf('='); + if (ix >= 0) { + name = src.substr(0, ix); + return AppUtils.parseBoolString(src.substr(ix + 1), true); + } + else + { + name = src; + return null; + } + }); + } + }; +}); + +backupApp.directive('parseAdvancedOptionInteger', function(AppUtils) { + return { + restrict: 'A', + require: ['ngModel'], + link: function(scope, element, attr, ctrl) { + + var name = null; + + ctrl[0].$parsers.push(function(txt) { + if (name == null) + return null; + + return name + '=' + txt; + }); + + ctrl[0].$formatters.push(function(src) { + src = src || ''; + var ix = src.indexOf('='); + if (ix >= 0) { + name = src.substr(0, ix); + return parseInt(src.substr(ix + 1), 10); + } + else + { + name = src; + return null; + } + }); + } + }; +}); + +backupApp.directive('parseAdvancedOptionSizeNumber', function(AppUtils) { + return { + restrict: 'A', + require: ['ngModel'], + link: function(scope, element, attr, ctrl) { + + var name = null; + var multiplier = null; + + ctrl[0].$parsers.push(function(txt) { + if (name == null) + return null; + + return name + '=' + txt + multiplier; + }); + + ctrl[0].$formatters.push(function(src) { + src = src || ''; + var ix = src.indexOf('='); + if (ix >= 0) { + name = src.substr(0, ix); + var parts = AppUtils.splitSizeString(src.substr(ix + 1)); + if (parts == null) + { + multiplier = null; + return null; + } + else + { + multiplier = parts[1]; + return parseInt(parts[0], 10); + } + } + else + { + name = src; + multiplier = ''; + return null; + } + }); + } + }; +}); + +backupApp.directive('parseAdvancedOptionSizeMultiplier', function(AppUtils) { + return { + restrict: 'A', + require: ['ngModel'], + link: function(scope, element, attr, ctrl) { + + var name = null; + var number = null; + + ctrl[0].$parsers.push(function(txt) { + if (name == null) + return null; + + return name + '=' + (number || '') + txt; + }); + + ctrl[0].$formatters.push(function(src) { + src = src || ''; + var ix = src.indexOf('='); + if (ix >= 0) { + name = src.substr(0, ix); + var parts = AppUtils.splitSizeString(src.substr(ix + 1)); + if (parts == null) + { + number = null; + return null; + } + else + { + number = parseInt(parts[0]); + return parts[1]; + } + } + else + { + name = src; + number = 0; + return null; + } + }); + } + }; +}); \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/scripts/services/AppUtils.js b/Duplicati/Server/webroot/ngax/scripts/services/AppUtils.js index 2d26c2198..f8ea1da59 100644 --- a/Duplicati/Server/webroot/ngax/scripts/services/AppUtils.js +++ b/Duplicati/Server/webroot/ngax/scripts/services/AppUtils.js @@ -40,7 +40,17 @@ backupApp.service('AppUtils', function() { {name: 'Fri', value: 'fri'}, {name: 'Sat', value: 'sat'}, {name: 'Sun', value: 'sun'} - ]; + ]; + + this.parseBoolString = function(txt, def) { + txt = (txt || '').toLowerCase(); + if (txt == '0' || txt == 'false' || txt == 'off' || txt == 'no' || txt == 'f') + return false; + else if (txt == '1' || txt == 'true' || txt == 'on' || txt == 'yes' || txt == 't') + return true; + else + return def === undefined ? false : def; + }; this.splitSizeString = function(val) { @@ -83,7 +93,14 @@ backupApp.service('AppUtils', function() { this.parseOptionStrings = function(val, dict, validateCallback) { dict = dict || {}; - var lines = this.replace_all(val || '', '\r', '\n').split('\n'); + + var lines = null; + + if (val != null && typeof(val) == typeof([])) + lines = val; + else + lines = this.replace_all(val || '', '\r', '\n').split('\n'); + for(var i in lines) { var line = lines[i].trim(); if (line != '' && line[0] != '#') { @@ -124,12 +141,16 @@ backupApp.service('AppUtils', function() { }; this.serializeAdvancedOptions = function(opts) { - var advopts = ''; + return this.serializeAdvancedOptionsToArray(opts).join('\n'); + }; + + this.serializeAdvancedOptionsToArray = function(opts) { + var res = []; for(var n in opts) if (n.indexOf('--') == 0) - advopts += n + '=' + opts[n] + '\n'; + res.push(n + '=' + opts[n]); - return advopts; + return res; }; this.mergeAdvancedOptions = function(advStr, target, source) { @@ -478,7 +499,7 @@ backupApp.service('AppUtils', function() { } return res; - } + }; this.evalFilter = function(path, filters, include) { for(var i = 0; i < filters.length; i++) { @@ -488,6 +509,40 @@ backupApp.service('AppUtils', function() { } return include === undefined ? true : include; - } + }; + + this.buildOptionList = function(sysinfo, encmodule, compmodule, backmodule) { + if (sysinfo == null || sysinfo.Options == null) + return null; + + var items = angular.copy(sysinfo.Options); + for(var n in items) + items[n].Category = 'Core options'; + + function copyToList(lst, key) { + for(var n in lst) + { + if (key == null || key.toLowerCase() == lst[n].Key.toLowerCase()) + { + var m = angular.copy(lst[n].Options); + for(var i in m) + m[i].Category = lst[n].DisplayName; + items.push.apply(items, m); + } + } + } + + copyToList(sysinfo.GenericModules); + copyToList(sysinfo.ConnectionModules); + + if (encmodule !== false) + copyToList(sysinfo.EncryptionModules, encmodule); + if (compmodule !== false) + copyToList(sysinfo.CompressionModules, compmodule); + if (backmodule !== false) + copyToList(sysinfo.BackendModules, backmodule); + + return items; + }; }); \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/styles/styles2.css b/Duplicati/Server/webroot/ngax/styles/styles2.css index 6754b65e7..47fb9f20e 100644 --- a/Duplicati/Server/webroot/ngax/styles/styles2.css +++ b/Duplicati/Server/webroot/ngax/styles/styles2.css @@ -582,3 +582,33 @@ ul.notification { .resizable { margin-bottom: 6px; } + +.advanced-toggle { + float: right; + margin-right: 25px; +} + +.advancedoptions li { + clear: both; +} + +.advancedoptions .advancedentry { + margin-bottom: 10px; + padding-bottom: 4px; + border-bottom: 1px lightgray solid; +} + +.advancedentry .multiple { + display: inline; +} + +.advancedentry .shortname { + font-weight: bold; +} + +.advancedentry .longdescription { + margin-left: 190px; + clear: both; + font-style: italic; +} + diff --git a/Duplicati/Server/webroot/ngax/templates/addoredit.html b/Duplicati/Server/webroot/ngax/templates/addoredit.html index 377ef65c0..1efc3172e 100644 --- a/Duplicati/Server/webroot/ngax/templates/addoredit.html +++ b/Duplicati/Server/webroot/ngax/templates/addoredit.html @@ -249,11 +249,19 @@ -
- - + +
+ + Edit as list +
+
+ + Edit as text + +
+
diff --git a/Duplicati/Server/webroot/ngax/templates/advancedoptionseditor.html b/Duplicati/Server/webroot/ngax/templates/advancedoptionseditor.html new file mode 100644 index 000000000..995315480 --- /dev/null +++ b/Duplicati/Server/webroot/ngax/templates/advancedoptionseditor.html @@ -0,0 +1,46 @@ +
    +
  • +
    + + + + + + + + +
    + + +
    + +
    + + +
    + +
    + + x + +
    {{getLongDescription(item)}}
    +
  • + +
  • + + +
  • +
\ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/templates/edituri.html b/Duplicati/Server/webroot/ngax/templates/edituri.html index 805b05709..99305ee3b 100644 --- a/Duplicati/Server/webroot/ngax/templates/edituri.html +++ b/Duplicati/Server/webroot/ngax/templates/edituri.html @@ -17,10 +17,18 @@

Chose a storage type to get started

-
- - +
+ + Edit as list +
+ +
+ + Edit as text + +
+
OK Test connection diff --git a/Duplicati/Server/webroot/ngax/templates/settings.html b/Duplicati/Server/webroot/ngax/templates/settings.html index e139e6a8f..474ed0929 100644 --- a/Duplicati/Server/webroot/ngax/templates/settings.html +++ b/Duplicati/Server/webroot/ngax/templates/settings.html @@ -12,8 +12,6 @@
- -

Pause after startup or hibernationi

@@ -36,10 +34,17 @@
-

Advanced Options

-
- - +

Default options

+
+ + Edit as list + +
+ +
+ + Edit as text +