[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 + '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 = [

View File

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

View File

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

View File

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

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: [], 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,

View File

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