[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:
Brian Tully 2015-04-01 20:41:29 -04:00 committed by Travis Tripp
parent 71cf0d21a7
commit 5bec804f2a
8 changed files with 276 additions and 81 deletions

View File

@ -37,6 +37,7 @@ ADD_JS_FILES = [
LAUNCH_INST + 'security-groups/security-groups.js',
LAUNCH_INST + 'keypair/keypair.js',
LAUNCH_INST + 'configuration/configuration.js',
LAUNCH_INST + 'configuration/load-edit.js',
]
ADD_JS_SPEC_FILES = [

View File

@ -1,57 +1,32 @@
<div ng-controller="LaunchInstanceConfigurationCtrl">
<h1 clasa="title">{$ ::label.title $}</h1>
<div ng-controller="LaunchInstanceConfigurationCtrl as config">
<h1 clasa="title">{$ ::config.label.title $}</h1>
<div class="content">
<div class="subtitle">{$ ::label.subtitle $}</div>
<div class="subtitle">{$ ::config.label.subtitle $}</div>
<div class="form-group customization-script-source">
<label for="launch-instance-customization-script-source">
{$ ::label.customizationScriptSource $}
</label>
<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>
<load-edit config="config"
user-input="model.newInstanceSpec"
key="user_data">
</load-edit>
<div class="form-group disk-partition">
<label for="launch-instance-disk-partition">
{$ ::label.diskPartition $}
{$ ::config.label.diskPartition $}
</label>
<select class="form-control"
id="launch-instance-disk-partition"
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>
</div>
<div class="checkbox">
<label>
<input type="checkbox"
ng-model="model.newInstanceSpec.config_drive">
{$ ::config.label.configurationDrive $}
</label>
</div>
</div>
</div>

View File

@ -1,44 +1,93 @@
(function () {
'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');
/**
* @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', [
'$scope',
'$element',
'$timeout',
'wizardEvents',
LaunchInstanceConfigurationCtrl
]);
module.controller('LaunchInstanceConfigurationHelpCtrl', [
LaunchInstanceConfigurationHelpCtrl
]);
function LaunchInstanceConfigurationCtrl(
$scope,
$element,
$timeout,
wizardEvents) {
function LaunchInstanceConfigurationCtrl($scope) {
$scope.label = {
var config = this,
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'),
subtitle: gettext(''),
customizationScriptSource: gettext('Customization Script Source'),
customizationScript: gettext('Customization Script'),
customizationScriptMax: gettext('(Max: 16Kb)'),
loadScriptFromFile: gettext('Load script from a file'),
configurationDrive: gettext('Configuration Drive'),
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 = [
{ 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 = [
config.diskConfigOptions = [
{ value: 'AUTO', text: gettext('Automatic') },
{ 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() {
var ctrl = this;
@ -49,9 +98,9 @@
ctrl.paragraphs = [
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('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('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('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.')
];
}

View File

@ -1,13 +1,65 @@
[ng-controller="LaunchInstanceConfigurationCtrl"] {
[ng-controller="LaunchInstanceConfigurationCtrl as config"] {
.customization-script-source select,
.disk-partition select {
select {
width: 250px;
}
.customization-script textarea {
width: 480px;
height: 280px;
font-family: Menlo, Monaco, Consolas, 'Courier New';
textarea {
width: 100%;
height: 20em;
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;
}
}

View File

@ -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>

View File

@ -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'
};
}
])
;})();

View File

@ -98,16 +98,6 @@
availabilityZones: [],
flavors: [],
allowedBootSources: [],
allowedDiskConfigOptions:[
{
id:'AUTO',
label: gettext('Automatic')
},
{
id:'MANUAL',
label: gettext('Manual')
}
],
images: [],
allowCreateVolumeFromImage: false,
arePortProfilesSupported: false,

View File

@ -16,6 +16,7 @@ $gray-light: #cccccc;
/* Horizon Custom Variables */
$code-font-family: Menlo, Monaco, Consolas, 'Courier New' !default;
$body-min-width: 900px !default;
$sidebar-background-color: #f9f9f9 !default;
$sidebar-width: 220px !default;
@ -120,6 +121,7 @@ $WizardFinishBtnDisabledBgColor: #ccc !default;
$WizardFinishBtnDisabledBdColor: #ccc !default;
$WizardToolbarBgColor: #f5f5f5 !default;
$WizardToolbarVerticalSeparatorBdColor: #e3e3e3 !default;
$WizardValidationErrorColor: #d43f3a !default;
$wizard-textarea-border-color: #eee;