Implemented more user-friendly editing for advanced options in ngax

This commit is contained in:
Kenneth Skovhede
2015-12-07 22:03:40 +01:00
parent 2ffdf582ad
commit 42376b44d6
12 changed files with 522 additions and 26 deletions
+2
View File
@@ -70,12 +70,14 @@
<script type="text/javascript" src="scripts/directives/targetFolderPicker.js"></script>
<script type="text/javascript" src="scripts/directives/parseFilterType.js"></script>
<script type="text/javascript" src="scripts/directives/parseSizeNumber.js"></script>
<script type="text/javascript" src="scripts/directives/parseAdvancedOption.js"></script>
<script type="text/javascript" src="scripts/directives/stringArrayAsText.js"></script>
<script type="text/javascript" src="scripts/directives/waitArea.js"></script>
<script type="text/javascript" src="scripts/directives/restoreFilePicker.js"></script>
<script type="text/javascript" src="scripts/directives/keyboardHandlers.js"></script>
<script type="text/javascript" src="scripts/directives/notificationArea.js"></script>
<script type="text/javascript" src="scripts/directives/progressBar.js"></script>
<script type="text/javascript" src="scripts/directives/advancedOptionsEditor.js"></script>
<script type="text/javascript" src="scripts/oem.js"></script>
<script type="text/javascript" src="../customized/customized.js"></script>
@@ -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) {
@@ -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('/');
},
@@ -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;
}
});
}
};
});
@@ -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() {
@@ -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;
}
});
}
};
});
@@ -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;
};
});
@@ -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;
}
@@ -249,11 +249,19 @@
<option value="">custom</option>
</select>
</div>
<div class="input textarea">
<label for="backupOptions"><a href id="backupOptionsPopup" data-remodal-target="backupOptions">Options</a><span class="info">i</span></label>
<textarea id="backupOptions" ng-model="ExtendedOptions" placeholder="Enter one option per line in command-line format, eg. --dblock-size=100MB"></textarea>
<div class="input textarea" ng-show="ShowAdvancedTextArea">
<label for="backupOptions">Advanced options</label>
<a href ng-click="ShowAdvancedTextArea = false" class="advanced-toggle">Edit as list</a>
<textarea id="backupOptions" ng-model="ExtendedOptions" string-array-as-text placeholder="Enter one option per line in command-line format, eg. --dblock-size=100MB"></textarea>
</div>
<div class="input" ng-hide="ShowAdvancedTextArea">
<label for="backupOptions">Advanced options</label>
<a href ng-click="ShowAdvancedTextArea = true" class="advanced-toggle">Edit as text</a>
<advanced-options-editor ng-option-list="ExtendedOptionList" ng-model="ExtendedOptions"></advanced-options-editor>
</div>
<div class="buttons">
<input class="submit save" type="submit" id="save" ng-click="save()" value="Save" />
<input class="submit prev" type="button" id="prevStep4" ng-click="prevPage()" value="&lt; Previous" />
@@ -0,0 +1,46 @@
<ul class="advancedoptions">
<li ng-repeat="item in ngModel | orderBy: 'item' as nn track by $index" class="advancedentry">
<div>
<label class="shortname">{{getShortName(item)}}</label>
<select ng-if="getInputType(item) == 'enum'" ng-options="n for n in getEnumerations(item)" ng-model="$parent.ngModel[$index]" parse-advanced-option ></select>
<input ng-if="getInputType(item) == 'bool'" type="checkbox" ng-model="$parent.ngModel[$index]" parse-advanced-option-bool ></input>
<input ng-if="getInputType(item) == 'password'" type="password" ng-model="$parent.ngModel[$index]" parse-advanced-option ></input>
<input ng-if="getInputType(item) == 'text'" type="text" ng-model="$parent.ngModel[$index]" parse-advanced-option ></input>
<input ng-if="getInputType(item) == 'int'" type="number" ng-model="$parent.ngModel[$index]" parse-advanced-option-integer ></input>
<div ng-if="getInputType(item) == 'size'" class="input type multiple">
<input type="number" parse-advanced-option-size-number ng-model="$parent.ngModel[$index]" />
<select parse-advanced-option-size-multiplier ng-model="$parent.ngModel[$index]">
<option value="kb">KByte</option>
<option value="mb">MByte</option>
<option value="gb">GByte</option>
<option value="tb">TByte</option>
</select>
</div>
<div ng-if="getInputType(item) == 'timespan'" class="input type multiple">
<input parse-advanced-option-size-number type="number" ng-model="$parent.ngModel[$index]" />
<select parse-advanced-option-size-multiplier ng-model="$parent.ngModel[$index]">
<option value="D">Days</option>
<option value="W">Weeks</option>
<option value="M">Months</option>
<option value="Y">Years</option>
<option value="">custom</option>
</select>
</div>
</div>
<a href ng-click="deleteItem(item)" title="Remove option">x</a>
<div class="longdescription">{{getLongDescription(item)}}</div>
</li>
<li>
<label>Add advanced option</label>
<select ng-options="getDisplayName(opt.Name) group by opt.Category for opt in ngOptionList | orderBy: 'Name' " ng-model="NewItem">
<option value="">- pick an option -</option>
</select>
</li>
</ul>
@@ -17,10 +17,18 @@
<h3>Chose a storage type to get started</h3>
</div>
<div class="input textarea">
<label for="targetOptions">Options<span class="info">i</span></label>
<textarea id="targetOptions" name="targetOptions" ng-model="AdvancedOptions"></textarea>
<div class="input textarea" ng-show="ShowAdvancedTextArea">
<label for="targetOptions">Options</label>
<a href ng-click="ShowAdvancedTextArea = false" class="advanced-toggle">Edit as list</a>
<textarea id="targetOptions" name="targetOptions" string-array-as-text ng-model="AdvancedOptions"></textarea>
</div>
<div class="input" ng-hide="ShowAdvancedTextArea">
<label for="backupOptions">Options</label>
<a href ng-click="ShowAdvancedTextArea = true" class="advanced-toggle">Edit as text</a>
<advanced-options-editor ng-option-list="Backend.Options" ng-model="AdvancedOptions"></advanced-options-editor>
</div>
<div class="buttons" ng-hide="Testing">
<a href ng-click="save()" class="save">OK</a>
<a href ng-click="testConnection()" class="test-connection" >Test connection</a>
@@ -12,8 +12,6 @@
<input type="checkbox" class="checkbox" id="remote" name="remote" ng-model="allowRemoteAccess" />
<label for="remote">Allow remote access (requires restart)</label>
</div>
<h2>Pause after startup or hibernation<span class="info">i</span></h2>
<div class="input mixed multiple">
@@ -36,10 +34,17 @@
</select>
</div>
<h2>Advanced Options</h2>
<div class="input textarea">
<label for="options">Options<span class="info">i</span></label>
<textarea id="options" name="options" ng-model="advancedOptions"></textarea>
<h2>Default options</h2>
<div class="input textarea" ng-show="ShowAdvancedTextArea">
<label for="backupOptions">Options</label>
<a href ng-click="ShowAdvancedTextArea = false" class="advanced-toggle">Edit as list</a>
<textarea id="backupOptions" ng-model="advancedOptions" string-array-as-text placeholder="Enter one option per line in command-line format, eg. --dblock-size=100MB"></textarea>
</div>
<div class="input" ng-hide="ShowAdvancedTextArea">
<label for="backupOptions">Options</label>
<a href ng-click="ShowAdvancedTextArea = true" class="advanced-toggle">Edit as text</a>
<advanced-options-editor ng-option-list="advancedOptionList" ng-model="advancedOptions"></advanced-options-editor>
</div>
<div class="buttons">