[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 + 'keypair/keypair.js',
|
||||
LAUNCH_INST + 'configuration/configuration.js',
|
||||
LAUNCH_INST + 'configuration/load-edit.js',
|
||||
]
|
||||
|
||||
ADD_JS_SPEC_FILES = [
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.')
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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: [],
|
||||
flavors: [],
|
||||
allowedBootSources: [],
|
||||
allowedDiskConfigOptions:[
|
||||
{
|
||||
id:'AUTO',
|
||||
label: gettext('Automatic')
|
||||
},
|
||||
{
|
||||
id:'MANUAL',
|
||||
label: gettext('Manual')
|
||||
}
|
||||
],
|
||||
images: [],
|
||||
allowCreateVolumeFromImage: false,
|
||||
arePortProfilesSupported: false,
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
Loading…
Reference in New Issue