[Launch Instance Fix] Enabling loading script from local file
In configuration step of Launch Instance work flow, we should allow for uploading script from local file. HTML5 provides a File API which makes it possible. - Browser support: IE10+, Chrome, FireFox and Safari (on Mac). - Max length of script is 16K. - Validation is added. Co-Authored-By: Brian Tully <brian.tully@hp.com> Closes-Bug: 1433438 Change-Id: I967b4f3e3dbecfc9a95988fdbaa603a731db0a7f
This commit is contained in:
parent
71cf0d21a7
commit
5bec804f2a
|
@ -37,6 +37,7 @@ ADD_JS_FILES = [
|
||||||
LAUNCH_INST + 'security-groups/security-groups.js',
|
LAUNCH_INST + 'security-groups/security-groups.js',
|
||||||
LAUNCH_INST + 'keypair/keypair.js',
|
LAUNCH_INST + 'keypair/keypair.js',
|
||||||
LAUNCH_INST + 'configuration/configuration.js',
|
LAUNCH_INST + 'configuration/configuration.js',
|
||||||
|
LAUNCH_INST + 'configuration/load-edit.js',
|
||||||
]
|
]
|
||||||
|
|
||||||
ADD_JS_SPEC_FILES = [
|
ADD_JS_SPEC_FILES = [
|
||||||
|
|
|
@ -1,57 +1,32 @@
|
||||||
<div ng-controller="LaunchInstanceConfigurationCtrl">
|
<div ng-controller="LaunchInstanceConfigurationCtrl as config">
|
||||||
<h1 clasa="title">{$ ::label.title $}</h1>
|
<h1 clasa="title">{$ ::config.label.title $}</h1>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="subtitle">{$ ::label.subtitle $}</div>
|
<div class="subtitle">{$ ::config.label.subtitle $}</div>
|
||||||
|
|
||||||
<div class="form-group customization-script-source">
|
<load-edit config="config"
|
||||||
<label for="launch-instance-customization-script-source">
|
user-input="model.newInstanceSpec"
|
||||||
{$ ::label.customizationScriptSource $}
|
key="user_data">
|
||||||
</label>
|
</load-edit>
|
||||||
<select class="form-control"
|
|
||||||
id="launch-instance-customization-script-source"
|
|
||||||
ng-model="model.newInstanceSpec.script_source"
|
|
||||||
ng-options="option.value as option.text for option in scriptSourceOptions">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group customization-script"
|
|
||||||
ng-show="model.newInstanceSpec.script_source === scriptSourceOptions[1].value">
|
|
||||||
<label for="launch-instance-customization-script">
|
|
||||||
{$ ::label.customizationScript $}</label>
|
|
||||||
<textarea class="form-control"
|
|
||||||
id="launch-instance-customization-script"
|
|
||||||
ng-model="model.newInstanceSpec.user_data">
|
|
||||||
</textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group script-file"
|
|
||||||
ng-show="model.newInstanceSpec.script_source === scriptSourceOptions[2].value">
|
|
||||||
<label for="launch-instance_script_upload">
|
|
||||||
{$ ::label.scriptFile $}
|
|
||||||
</label>
|
|
||||||
<input id="launch-instance_script_upload"
|
|
||||||
ng-model="model.newInstanceSpec.script_upload"
|
|
||||||
type="file">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="checkbox customization-script-source">
|
|
||||||
<label>
|
|
||||||
<input type="checkbox"
|
|
||||||
ng-model="model.newInstanceSpec.config_drive">
|
|
||||||
{$ ::label.configurationDrive $}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group disk-partition">
|
<div class="form-group disk-partition">
|
||||||
<label for="launch-instance-disk-partition">
|
<label for="launch-instance-disk-partition">
|
||||||
{$ ::label.diskPartition $}
|
{$ ::config.label.diskPartition $}
|
||||||
</label>
|
</label>
|
||||||
<select class="form-control"
|
<select class="form-control"
|
||||||
id="launch-instance-disk-partition"
|
id="launch-instance-disk-partition"
|
||||||
ng-model="model.newInstanceSpec.disk_config"
|
ng-model="model.newInstanceSpec.disk_config"
|
||||||
ng-options="option.value as option.text for option in diskConfigOptions">
|
ng-options="option.value as option.text for option in config.diskConfigOptions">
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox"
|
||||||
|
ng-model="model.newInstanceSpec.config_drive">
|
||||||
|
{$ ::config.label.configurationDrive $}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,44 +1,93 @@
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
var MAX_SCRIPT_SIZE = 16 * 1024,
|
||||||
|
DEFAULT_CONFIG_DRIVE = false,
|
||||||
|
DEFAULT_USER_DATA = '',
|
||||||
|
DEFAULT_DISK_CONFIG = 'AUTO';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc overview
|
||||||
|
* @name hz.dashboard.launch-instance
|
||||||
|
* @description
|
||||||
|
*
|
||||||
|
* # hz.dashboard.launch-instance
|
||||||
|
*
|
||||||
|
* The `hz.dashboard.launch-instance` module allows a user
|
||||||
|
* to launch an instance via the multi-step wizard framework
|
||||||
|
*
|
||||||
|
*/
|
||||||
var module = angular.module('hz.dashboard.launch-instance');
|
var module = angular.module('hz.dashboard.launch-instance');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc controller
|
||||||
|
* @name LaunchInstanceConfigurationCtrl
|
||||||
|
* @description
|
||||||
|
* The `LaunchInstanceConfigurationCtrl` controller is responsible for
|
||||||
|
* setting the following instance properties:
|
||||||
|
*
|
||||||
|
* @property {string} user_data, default to empty string.
|
||||||
|
* The maximum size of user_data is 16 * 1024.
|
||||||
|
* @property {string} disk_config, default to `AUTO`.
|
||||||
|
* @property {boolean} config_drive, default to false.
|
||||||
|
*/
|
||||||
module.controller('LaunchInstanceConfigurationCtrl', [
|
module.controller('LaunchInstanceConfigurationCtrl', [
|
||||||
'$scope',
|
'$scope',
|
||||||
|
'$element',
|
||||||
|
'$timeout',
|
||||||
|
'wizardEvents',
|
||||||
LaunchInstanceConfigurationCtrl
|
LaunchInstanceConfigurationCtrl
|
||||||
]);
|
]);
|
||||||
|
|
||||||
module.controller('LaunchInstanceConfigurationHelpCtrl', [
|
function LaunchInstanceConfigurationCtrl(
|
||||||
LaunchInstanceConfigurationHelpCtrl
|
$scope,
|
||||||
]);
|
$element,
|
||||||
|
$timeout,
|
||||||
|
wizardEvents) {
|
||||||
|
|
||||||
function LaunchInstanceConfigurationCtrl($scope) {
|
var config = this,
|
||||||
$scope.label = {
|
newInstanceSpec = $scope.model.newInstanceSpec;
|
||||||
|
|
||||||
|
newInstanceSpec.user_data = DEFAULT_USER_DATA;
|
||||||
|
newInstanceSpec.disk_config = DEFAULT_DISK_CONFIG;
|
||||||
|
newInstanceSpec.config_drive = DEFAULT_CONFIG_DRIVE;
|
||||||
|
|
||||||
|
config.MAX_SCRIPT_SIZE = MAX_SCRIPT_SIZE;
|
||||||
|
|
||||||
|
config.label = {
|
||||||
title: gettext('Configuration'),
|
title: gettext('Configuration'),
|
||||||
subtitle: gettext(''),
|
subtitle: gettext(''),
|
||||||
customizationScriptSource: gettext('Customization Script Source'),
|
|
||||||
customizationScript: gettext('Customization Script'),
|
customizationScript: gettext('Customization Script'),
|
||||||
|
customizationScriptMax: gettext('(Max: 16Kb)'),
|
||||||
|
loadScriptFromFile: gettext('Load script from a file'),
|
||||||
configurationDrive: gettext('Configuration Drive'),
|
configurationDrive: gettext('Configuration Drive'),
|
||||||
diskPartition: gettext('Disk Partition'),
|
diskPartition: gettext('Disk Partition'),
|
||||||
scriptFile: gettext('Script File')
|
scriptSize: gettext('Script size'),
|
||||||
|
scriptModified: gettext('Modified'),
|
||||||
|
scriptSizeWarningMsg: gettext('Script size > 16Kb'),
|
||||||
|
bytes: gettext('bytes'),
|
||||||
|
scriptSizeHoverWarningMsg: gettext('The maximum script size is 16Kb.')
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.scriptSourceOptions = [
|
config.diskConfigOptions = [
|
||||||
{ value: 'selected', text: gettext('Select Script Source') },
|
|
||||||
{ value: 'raw', text: gettext('Direct Input') },
|
|
||||||
{ value: 'file', text: gettext('File') }
|
|
||||||
];
|
|
||||||
|
|
||||||
$scope.model.newInstanceSpec.script_source = $scope.scriptSourceOptions[0].value;
|
|
||||||
|
|
||||||
$scope.diskConfigOptions = [
|
|
||||||
{ value: 'AUTO', text: gettext('Automatic') },
|
{ value: 'AUTO', text: gettext('Automatic') },
|
||||||
{ value: 'MANUAL', text: gettext('Manual') }
|
{ value: 'MANUAL', text: gettext('Manual') }
|
||||||
];
|
];
|
||||||
|
|
||||||
$scope.model.newInstanceSpec.disk_config = $scope.diskConfigOptions[0].value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc controller
|
||||||
|
* @name LaunchInstanceConfigurationHelpCtrl
|
||||||
|
* @description
|
||||||
|
* The `LaunchInstanceConfigurationHelpCtrl` controller provides functions for
|
||||||
|
* configuring the help text used within the configuration step of the
|
||||||
|
* Launch Instance Wizard.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
module.controller('LaunchInstanceConfigurationHelpCtrl', [
|
||||||
|
LaunchInstanceConfigurationHelpCtrl
|
||||||
|
]);
|
||||||
|
|
||||||
function LaunchInstanceConfigurationHelpCtrl() {
|
function LaunchInstanceConfigurationHelpCtrl() {
|
||||||
var ctrl = this;
|
var ctrl = this;
|
||||||
|
|
||||||
|
@ -49,9 +98,9 @@
|
||||||
|
|
||||||
ctrl.paragraphs = [
|
ctrl.paragraphs = [
|
||||||
interpolate(customScriptText, customScriptMap, true),
|
interpolate(customScriptText, customScriptMap, true),
|
||||||
gettext('The <b>Customization Script Source</b> field determines how the script information is delivered. Use <b>Direct Input</b> if you want to type the script directly into the <b>Customization Script</b> field.'),
|
gettext('Type your script directly into the Customization Script field. If your browser supports the HTML5 File API, you may choose to load your script from a file. The size of your script should not exceed 16 Kb.'),
|
||||||
gettext('Check the <b>Configuration Drive</b> box if you want to write metadata to a special configuration drive. When the instance boots, it attaches to the <b>Configuration Drive</b> and accesses the metadata.'),
|
gettext('An advanced option available when launching an instance is disk partitioning. There are two disk partition options. Selecting <b>Automatic</b> resizes the disk and sets it to a single partition. Selecting <b>Manual</b> allows you to create multiple partitions on the disk.'),
|
||||||
gettext('An advanced option available when launching an instance is disk partitioning. There are two disk partition options. Selecting <b>Automatic</b> resizes the disk and sets it to a single partition. Selecting <b>Manual</b> allows you to create multiple partitions on the disk.')
|
gettext('Check the <b>Configuration Drive</b> box if you want to write metadata to a special configuration drive. When the instance boots, it attaches to the <b>Configuration Drive</b> and accesses the metadata.')
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,65 @@
|
||||||
[ng-controller="LaunchInstanceConfigurationCtrl"] {
|
[ng-controller="LaunchInstanceConfigurationCtrl as config"] {
|
||||||
|
|
||||||
.customization-script-source select,
|
select {
|
||||||
.disk-partition select {
|
|
||||||
width: 250px;
|
width: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customization-script textarea {
|
textarea {
|
||||||
width: 480px;
|
width: 100%;
|
||||||
height: 280px;
|
height: 20em;
|
||||||
font-family: Menlo, Monaco, Consolas, 'Courier New';
|
font-family: $code-font-family;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-file {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
input[type=file] {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
min-width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
font-size: 100px;
|
||||||
|
text-align: right;
|
||||||
|
filter: alpha(opacity=0);
|
||||||
|
opacity: 0;
|
||||||
|
outline: none;
|
||||||
|
background: white;
|
||||||
|
cursor: inherit;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.script-modified {
|
||||||
|
font-width: normal;
|
||||||
|
font-style: italic;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa.invalid {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size-indicator.warning {
|
||||||
|
color: $WizardValidationErrorColor;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
.fa.invalid {
|
||||||
|
display: inline;
|
||||||
|
color: $invalid-color;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.script-file:after,
|
||||||
|
.disk-partition:after {
|
||||||
|
content: ' ';
|
||||||
|
display: block;
|
||||||
|
clear: both;
|
||||||
|
margin-bottom: 2.5em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
<div class="form-group customization-script">
|
||||||
|
<label>
|
||||||
|
{$ ::config.label.customizationScript $}
|
||||||
|
<span class="script-modified"
|
||||||
|
ng-show="scriptModified">
|
||||||
|
({$ ::config.label.scriptModified $})</span>
|
||||||
|
</label>
|
||||||
|
<span class="size-indicator pull-right clearfix"
|
||||||
|
ng-class="{warning: scriptLength >= config.MAX_SCRIPT_SIZE}">
|
||||||
|
<span class="invalid fa fa-exclamation-triangle"
|
||||||
|
popover="{$ ::config.label.scriptSizeHoverWarningMsg $}"
|
||||||
|
popover-placement="top"
|
||||||
|
popover-append-to-body="true"
|
||||||
|
popover-trigger="hover">
|
||||||
|
</span>
|
||||||
|
{$ ::config.label.scriptSize $}:
|
||||||
|
{$ (scriptLength || 0) | number $}
|
||||||
|
{$ ::config.label.bytes $}
|
||||||
|
<span>{$ ::config.label.customizationScriptMax $}</span>
|
||||||
|
</span>
|
||||||
|
<textarea class="form-control"
|
||||||
|
name="customization-script"
|
||||||
|
ng-maxlength="config.MAX_SCRIPT_SIZE"
|
||||||
|
ng-model="textContent">
|
||||||
|
</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group script-file" ng-show="config.fileApiSupported">
|
||||||
|
<span class="file-input btn btn-primary btn-file">
|
||||||
|
<span class="fa fa-upload"></span>
|
||||||
|
{$ ::config.label.loadScriptFromFile $}
|
||||||
|
<input type="file">
|
||||||
|
</span>
|
||||||
|
</div>
|
|
@ -0,0 +1,92 @@
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
angular.module('hz.dashboard.launch-instance')
|
||||||
|
|
||||||
|
.directive('loadEdit', ['dashboardBasePath', '$timeout',
|
||||||
|
function (path, $timeout) {
|
||||||
|
|
||||||
|
function link($scope, $element) {
|
||||||
|
var textarea = $element.find('textarea'),
|
||||||
|
fileInput = $element.find('input[type="file"]'),
|
||||||
|
userInput = $scope.userInput;
|
||||||
|
|
||||||
|
$scope.textContent = '';
|
||||||
|
|
||||||
|
// HTML5 file API is supported by IE10+, Chrome, FireFox and Safari (on Mac).
|
||||||
|
//
|
||||||
|
// If HTML5 file API is not supported by user's browser, remove the option
|
||||||
|
// to upload a script via file upload.
|
||||||
|
$scope.config.fileApiSupported = !!FileReader;
|
||||||
|
|
||||||
|
function onTextareaChange() {
|
||||||
|
$scope.$applyAsync(function () {
|
||||||
|
// Angular model won't provide the value of the <textarea> when it is in
|
||||||
|
// invalid status, so we have to use jQuery or jqLite to get the length
|
||||||
|
// of the <textarea> content.
|
||||||
|
|
||||||
|
$scope.scriptLength = textarea.val().length;
|
||||||
|
$scope.userInput[$scope.key] = $scope.textContent;
|
||||||
|
if ($scope.scriptLength > 0) {
|
||||||
|
$scope.scriptModified = true;
|
||||||
|
} else {
|
||||||
|
$scope.scriptModified = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Angular won't fire change events when the <textarea> is in invalid
|
||||||
|
// status, so we have to use jQuery/jqLite to watch for <textarea> changes.
|
||||||
|
// If there are changes, we call the onScriptChange function to update the
|
||||||
|
// size stats and perform validation.
|
||||||
|
textarea.on('input propertychange', onTextareaChange);
|
||||||
|
|
||||||
|
function onFileLoad(event) {
|
||||||
|
var file = event.originalEvent.target.files[0];
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
var reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onloadend = function (e) {
|
||||||
|
$scope.$applyAsync(function () {
|
||||||
|
var charArray = new Uint8Array(e.target.result);
|
||||||
|
|
||||||
|
$scope.textContent = [].map.call(charArray,
|
||||||
|
function (char) {
|
||||||
|
return String.fromCharCode(char);
|
||||||
|
}
|
||||||
|
).join('');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsArrayBuffer(file.slice(0, file.size));
|
||||||
|
|
||||||
|
// Once the DOM manipulation is done, update the scriptLength, so that
|
||||||
|
// user knows the length of the script loaded into the <textarea>.
|
||||||
|
$timeout(function () {
|
||||||
|
onTextareaChange();
|
||||||
|
$scope.scriptModified = false;
|
||||||
|
}, 250, false);
|
||||||
|
|
||||||
|
// Focus the <textarea> element after injecting the code into it.
|
||||||
|
textarea.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileInput.on('change', onFileLoad);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
scope: {
|
||||||
|
config: '=',
|
||||||
|
userInput: '=',
|
||||||
|
key: '@'
|
||||||
|
},
|
||||||
|
link: link,
|
||||||
|
templateUrl: path + 'launch-instance/configuration/load-edit.html'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
;})();
|
|
@ -98,16 +98,6 @@
|
||||||
availabilityZones: [],
|
availabilityZones: [],
|
||||||
flavors: [],
|
flavors: [],
|
||||||
allowedBootSources: [],
|
allowedBootSources: [],
|
||||||
allowedDiskConfigOptions:[
|
|
||||||
{
|
|
||||||
id:'AUTO',
|
|
||||||
label: gettext('Automatic')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id:'MANUAL',
|
|
||||||
label: gettext('Manual')
|
|
||||||
}
|
|
||||||
],
|
|
||||||
images: [],
|
images: [],
|
||||||
allowCreateVolumeFromImage: false,
|
allowCreateVolumeFromImage: false,
|
||||||
arePortProfilesSupported: false,
|
arePortProfilesSupported: false,
|
||||||
|
|
|
@ -16,6 +16,7 @@ $gray-light: #cccccc;
|
||||||
|
|
||||||
/* Horizon Custom Variables */
|
/* Horizon Custom Variables */
|
||||||
|
|
||||||
|
$code-font-family: Menlo, Monaco, Consolas, 'Courier New' !default;
|
||||||
$body-min-width: 900px !default;
|
$body-min-width: 900px !default;
|
||||||
$sidebar-background-color: #f9f9f9 !default;
|
$sidebar-background-color: #f9f9f9 !default;
|
||||||
$sidebar-width: 220px !default;
|
$sidebar-width: 220px !default;
|
||||||
|
@ -120,6 +121,7 @@ $WizardFinishBtnDisabledBgColor: #ccc !default;
|
||||||
$WizardFinishBtnDisabledBdColor: #ccc !default;
|
$WizardFinishBtnDisabledBdColor: #ccc !default;
|
||||||
$WizardToolbarBgColor: #f5f5f5 !default;
|
$WizardToolbarBgColor: #f5f5f5 !default;
|
||||||
$WizardToolbarVerticalSeparatorBdColor: #e3e3e3 !default;
|
$WizardToolbarVerticalSeparatorBdColor: #e3e3e3 !default;
|
||||||
|
$WizardValidationErrorColor: #d43f3a !default;
|
||||||
|
|
||||||
$wizard-textarea-border-color: #eee;
|
$wizard-textarea-border-color: #eee;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue