[Launch Instance Fix] Show volume device name

When setting a volume, allow setting the volume device name if supported

Note: To test, you need to add the following to local_settings.py

REST_API_REQUIRED_SETTINGS = ['OPENSTACK_HYPERVISOR_FEATURES']

OPENSTACK_HYPERVISOR_FEATURES = {
    'can_set_mount_point': True
}

Change can_set_mount_point to False to see it not show up.
Must restart the server.

Co-Authored-By: Brian Tully <brian.tully@hp.com>
Change-Id: Ied8b1e92ea361fa5806128a71d6815e4b1494272
Closes-Bug: #1439906
This commit is contained in:
Shaoquan Chen 2015-04-07 15:28:06 -07:00
parent 3a59bec6a7
commit 93eb310037
6 changed files with 268 additions and 106 deletions

View File

@ -304,3 +304,49 @@
color: $placeholder-text-color;
}
}
.form-group .required label:after {
content: " *";
color: red;
}
.btn-toggle {
color: #333;
background-color: #fff;
border-color: #adadad;
&:hover,
&:focus,
&:active {
background-color: #ebebeb;
}
&.active {
background-color: #0077b3;
border-color: #006699;
color: #fff !important;
}
&.disabled.active,
&[disabled].active {
background-color: rgba(0, 119, 179, 0.65);
border-color: rgba(0, 102, 153, 0.65);
color: #fff;
}
&.disabled,
&.disabled:hover,
&.disabled:focus,
&.disabled:active,
&[disabled]:hover,
&[disabled]:focus,
&[disabled]:active,
fieldset[disabled] &:hover,
fieldset[disabled] &:focus,
fieldset[disabled] &:active,
fieldset[disabled] &.active {
background-color: #fafafa;
border-color: #ccc;
color: #999;
}
}

View File

@ -50,6 +50,7 @@
'novaExtensions',
'securityGroup',
'serviceCatalog',
'settingsService',
function (cinderAPI,
glanceAPI,
@ -58,7 +59,8 @@
novaAPI,
novaExtensions,
securityGroup,
serviceCatalog) {
serviceCatalog,
settingsService) {
return {
cinder: cinderAPI,
@ -68,35 +70,39 @@
nova: novaAPI,
novaExtensions: novaExtensions,
securityGroup: securityGroup,
serviceCatalog: serviceCatalog
serviceCatalog: serviceCatalog,
settingsService: settingsService
};
}
])
/**
* @ngdoc factory
* @name hz.dashboard:factory:ifExtensionsEnabled
* @name hz.dashboard:factory:ifFeaturesEnabled
* @module hz.dashboard
* @kind function
* @description
*
* Check to see if all the listed extensions are enabled on a certain service,
* Check to see if all the listed features are enabled on a certain service,
* which is described by the service name.
*
* This is an asynchronous operation.
*
* @param String serviceName The name of the service, e.g. `novaExtensions`.
* @param Array<String> extensions A list of extension's names.
* @param Array<String> features A list of feature's names.
* @return Promise the promise of the deferred task that gets resolved
* when all the sub-tasks are resolved.
*/
.factory('ifExtensionsEnabled', ['$q', 'cloudServices',
.factory('ifFeaturesEnabled', ['$q', 'cloudServices',
function ($q, cloudServices) {
return function ifExtensionsEnabled(serviceName, extensions) {
return function ifFeaturesEnabled(serviceName, features) {
// each cloudServices[serviceName].ifEnabled(feature) is an asynchronous
// operation which returns a promise, thus requiring the use of $q.all
// to defer.
return $q.all(
extensions.map(function (extension) {
return cloudServices[serviceName].ifEnabled(extension);
features.map(function (feature) {
return cloudServices[serviceName].ifEnabled(feature);
})
);//return
};//return
@ -114,35 +120,41 @@
* based on `serviceName`.
*
* @param String serviceName The name of the service, e.g. `novaExtensions`.
* @param String attrName The name of the attribute in the service.
* @return Object a directive specification object that can be used to
* create an angular directive.
*/
.factory('createDirectiveSpec', ['ifExtensionsEnabled',
function (ifExtensionsEnabled) {
return function createDirectiveSpec(serviceName) {
.factory('createDirectiveSpec', ['ifFeaturesEnabled',
function (ifFeaturesEnabled) {
return function createDirectiveSpec(serviceName, attrName) {
function link(scope, element, attrs, ctrl, transclude) {
element.addClass('ng-hide');
var features = fromJson(attrs[attrName]);
if (isArray(features)) {
ifFeaturesEnabled(serviceName, features).then(
// if the feature is enabled:
function () {
element.removeClass('ng-hide');
},
// if the feature is not enabled:
function () {
element.remove();
}
);
}
transclude(scope, function (clone) {
element.append(clone);
});
}
return {
link: link,
restrict: 'E',
transclude: true,
link: function link(scope, element, attrs, ctrl, transclude) {
element.addClass('ng-hide');
var extensions = fromJson(attrs.requiredExtensions);
if (isArray(extensions)) {
ifExtensionsEnabled(serviceName, extensions).then(
function () {
element.removeClass('ng-hide');
},
function () {
element.remove();
}
);//if-then
}
transclude(scope, function (clone) {
element.append(clone);
});
}//link
};//return
};//return
transclude: true
};
};
}
])
@ -158,7 +170,7 @@
*
* @example
*
```html
```html
<nova-extension required-extensions='["config_drive"]'>
<div class="checkbox customization-script-source">
<label>
@ -181,12 +193,37 @@
</select>
</div>
</nova-extension>
```
```
*/
.directive('novaExtension', ['createDirectiveSpec',
function (createDirectiveSpec) {
return createDirectiveSpec('novaExtensions');
return createDirectiveSpec('novaExtensions', 'requiredExtensions');
}
])
/**
* @ngdoc directive
* @name hz.dashboard:directive:settingsService
* @module hz.dashboard
* @description
*
* This is to enable specifying conditional UI in a declarative way.
* Some UI components should be showing only when some certain settings
* are enabled on `settingsService` service.
*
* @example
*
```html
<settings-service required-settings='["something"]'>
<!-- ui code here -->
</settings-service>
```
*/
.directive('settingsService', ['createDirectiveSpec',
function (createDirectiveSpec) {
return createDirectiveSpec('settingsService', 'requiredSettings');
}
])

View File

@ -35,6 +35,7 @@
$provide.value('novaExtensions', {});
$provide.value('securityGroup', {});
$provide.value('serviceCatalog', {});
$provide.value('settingsService', {});
}));
beforeEach(inject(function ($injector) {
@ -69,14 +70,18 @@
expect(cloudServices.novaExtensions).toBeDefined();
});
it('should have `cloudServices.settingsService` defined.', function () {
expect(cloudServices.settingsService).toBeDefined();
});
});
//
// factory:ifExtensionsEnabled
// factory:ifFeaturesEnabled
//
describe('factory:ifExtensionsEnabled', function () {
var ifExtensionsEnabled,
describe('factory:ifFeaturesEnabled', function () {
var ifFeaturesEnabled,
$q,
cloudServices;
@ -103,44 +108,44 @@
}));
beforeEach(inject(function ($injector) {
ifExtensionsEnabled = $injector.get('ifExtensionsEnabled');
ifFeaturesEnabled = $injector.get('ifFeaturesEnabled');
}));
it('should have `ifExtensionsEnabled` defined as a function.', function () {
expect(ifExtensionsEnabled).toBeDefined();
expect(angular.isFunction(ifExtensionsEnabled)).toBe(true);
it('should have `ifFeaturesEnabled` defined as a function.', function () {
expect(ifFeaturesEnabled).toBeDefined();
expect(angular.isFunction(ifFeaturesEnabled)).toBe(true);
});
it('should call $q.all() and someService.ifEnabled() when invoking ifExtensionsEnabled().', function () {
it('should call $q.all() and someService.ifEnabled() when invoking ifFeaturesEnabled().', function () {
var extensions = ['ext1', 'ext2'];
ifExtensionsEnabled('someService', extensions);
ifFeaturesEnabled('someService', extensions);
expect($q.all).toHaveBeenCalled();
expect(cloudServices.someService.ifEnabled).toHaveBeenCalled();
});
it('should not throw when passing in an empty extensions list.', function () {
expect(function () {
ifExtensionsEnabled('someService', []);
ifFeaturesEnabled('someService', []);
}).not.toThrow();
});
it('should throw when extensions is null or undefined or not an array', function () {
expect(function () {
ifExtensionsEnabled('someService', null);
ifFeaturesEnabled('someService', null);
}).toThrow();
expect(function () {
ifExtensionsEnabled('someService');
ifFeaturesEnabled('someService');
}).toThrow();
expect(function () {
ifExtensionsEnabled('123');
ifFeaturesEnabled('123');
}).toThrow();
});
it('should not throw when the provided serviceName is not a key in the services hash table', function () {
expect(function () {
ifExtensionsEnabled('invlidServiceName', []);
ifFeaturesEnabled('invlidServiceName', []);
}).not.toThrow();
});
});
@ -151,16 +156,16 @@
describe('factory:createDirectiveSpec', function () {
var createDirectiveSpec,
ifExtensionsEnabled;
ifFeaturesEnabled;
beforeEach(module('hz.dashboard', function ($provide) {
ifExtensionsEnabled = function () {
ifFeaturesEnabled = function () {
return {
then: function (successCallback, errorCallback) {
}
};
};
$provide.value('ifExtensionsEnabled', ifExtensionsEnabled);
$provide.value('ifFeaturesEnabled', ifFeaturesEnabled);
}));
beforeEach(inject(function ($injector) {
@ -176,7 +181,7 @@
var directiveSpec;
beforeEach(function () {
directiveSpec = createDirectiveSpec('someService');
directiveSpec = createDirectiveSpec('someService', 'someFeature');
});
it('should be defined.', function () {
@ -215,7 +220,7 @@
element;
beforeEach(module('hz.dashboard', function ($provide) {
$provide.value('ifExtensionsEnabled', function () {
$provide.value('ifFeaturesEnabled', function () {
return {
then: function (successCallback, errorCallback) {
$timeout(successCallback);
@ -252,6 +257,58 @@
});
//
// directive:settingsService
//
describe('directive:settingsService', function () {
var $timeout,
$scope,
html = [
'<settings-service required-settings=\'["something"]\'>',
'<div class="child-element">',
'</div>',
'</settings-service>'
].join(''),
element;
beforeEach(module('hz.dashboard', function ($provide) {
$provide.value('ifFeaturesEnabled', function () {
return {
then: function (successCallback, errorCallback) {
$timeout(successCallback);
}
};
});
}));
beforeEach(inject(function ($injector) {
var $compile = $injector.get('$compile');
$scope = $injector.get('$rootScope').$new();
$timeout = $injector.get('$timeout');
element = $compile(html)($scope);
}));
it('should be compiled.', function () {
expect(element.hasClass('ng-scope')).toBe(true);
});
it('should have class name `ng-hide` by default.', function () {
expect(element.hasClass('ng-hide')).toBe(true);
});
it('should have no class name `ng-hide` after an asyncs callback.', function () {
$timeout(function () {
expect(element.hasClass('ng-hide')).toBe(false);
});
$timeout.flush();
});
it('should have the right child element.', function () {
expect(element.children().first().hasClass('child-element')).toBe(true);
});
});
})
;})();

View File

@ -95,15 +95,31 @@
<div class="col-xs-12 col-sm-3">
<div class="form-group create-volume">
<div class="checkbox">
<label class="on-top">
<input type="checkbox" ng-model="model.newInstanceSpec.vol_create">
{$ ::label.volumeCreate $}</label>
<label class="on-top">{$ ::label.volumeCreate $}</label>
<div class="form-field">
<div class="btn-group">
<label class="btn btn-toggle"
ng-repeat="option in toggleButtonOptions"
ng-model="model.newInstanceSpec.vol_create"
btn-radio="option.value">{$ ::option.label $}</label>
</div>
</div>
</div>
</div>
<div class="col-xs-12 col-sm-4 volume-size-wrapper" ng-if="model.newInstanceSpec.vol_create == true">
<settings-service required-settings='["OPENSTACK_HYPERVISOR_FEATURES.can_set_mount_point"]'
ng-if="model.newInstanceSpec.vol_create === true">
<div class="col-xs-12 col-sm-3">
<div class="form-field">
<label>{$ ::label.volumeDeviceName $}</label>
<input class="form-control input-sm"
ng-model="model.newInstanceSpec.vol_device_name"
type="text">
</div>
</div>
</settings-service>
<div class="col-xs-12 col-sm-2 volume-size-wrapper" ng-if="model.newInstanceSpec.vol_create == true">
<div class="form-field volume-size"
ng-class="{ 'has-warning': launchInstanceSourceForm['volume-size'].$invalid }">
<label class="on-top">{$ ::label.volumeSize $}</label>
@ -122,11 +138,14 @@
<div class="col-xs-12 col-sm-4" ng-if="model.newInstanceSpec.vol_create == true">
<div class="form-group delete-volume">
<div class="checkbox">
<label class="on-top">
<input type="checkbox"
ng-model="model.newInstanceSpec.vol_delete_on_terminate">
{$ ::label.deleteVolumeOnTerminate $}</label>
<label class="on-top">{$ ::label.deleteVolumeOnTerminate $}</label>
<div class="form-field">
<div class="btn-group">
<label class="btn btn-toggle"
ng-repeat="option in toggleButtonOptions"
ng-model="model.newInstanceSpec.vol_delete_on_terminate"
btn-radio="option.value">{$ ::option.label $}</label>
</div>
</div>
</div>
</div>
@ -136,14 +155,19 @@
<div class="col-xs-12 col-sm-9"
ng-if="model.newInstanceSpec.source_type.type == 'volume' || model.newInstanceSpec.source_type.type == 'volume_snapshot'">
<div class="col-xs-12 col-sm-6">
<div class="form-group delete-volume">
<div class="checkbox">
<label class="on-top">
<input type="checkbox"
ng-model="model.newInstanceSpec.vol_delete_on_terminate">
{$ ::label.deleteVolumeOnTerminate $}</label>
<label class="on-top">{$ ::label.deleteVolumeOnTerminate $}</label>
<div class="form-field">
<div class="btn-group">
<label class="btn btn-toggle"
ng-repeat="option in toggleButtonOptions"
ng-model="model.newInstanceSpec.vol_delete_on_terminate"
btn-radio="option.value">{$ ::option.label $}</label>
</div>
</div>
</div>
</div>
</div><!-- end volume select options -->

View File

@ -83,9 +83,9 @@
instanceSourceTitle: gettext('Instance Source'),
instanceSourceSubTitle: gettext('Instance source is the template used to create an instance. You can use a snapshot of an existing instance, an image, or a volume (if enabled). You can also choose to use persistent storage by creating a new volume.'),
bootSource: gettext('Select Boot Source'),
deviceSize: gettext('Device Size (GB)'),
volumeSize: gettext('Volume Size (GB)'),
volumeSize: gettext('Size (GB)'),
volumeCreate: gettext('Create New Volume'),
volumeDeviceName: gettext('Device Name'),
deleteVolumeOnTerminate: gettext('Delete Volume on Terminate'),
id: gettext('ID'),
min_ram: gettext('Min Ram'),
@ -99,6 +99,13 @@
$scope.instanceCountError = gettext('Instance count is required and must be an integer of at least 1');
$scope.volumeSizeError = gettext('Volume size is required and must be an integer');
// toggle button label/value defaults
$scope.toggleButtonOptions = [
{ label: gettext('Yes'), value: true },
{ label: gettext('No'), value: false }
];
//
// Boot Sources
//

View File

@ -1,26 +1,3 @@
.form-group .required label:after {
content: " *";
color: red;
}
.selected-source {
background: #eee;
padding: 12px 18px;
margin-top: 20px;
margin-bottom: 20px;
.chart {
width: 99%;
margin-bottom: 0;
padding: 10px;
@media (min-width: 768px) {
border-left: 1px solid #ccc;
padding-left: 20px;
}
}
}
[ng-controller="LaunchInstanceSourceCtrl"] {
td.hi-light {
@ -32,22 +9,36 @@
text-align: right;
padding-right: 30px;
}
}
.instance-source {
margin-top: 18px;
margin-bottom: 40px;
.selected-source {
background: #eee;
padding: 12px 18px;
margin-top: 20px;
margin-bottom: 20px;
.image select {
width: 99%;
.chart {
width: 99%;
margin-bottom: 0;
padding: 10px;
@media (min-width: 768px) {
border-left: 1px solid #ccc;
padding-left: 20px;
}
}
}
.volume-size input[type="number"]{
width: 90%;
}
.instance-source {
margin-top: 18px;
margin-bottom: 40px;
.image select {
width: 99%;
}
.volume-size input[type="number"]{
width: 90%;
}
.create-volume,
.delete-volume {
margin-top: 1.7em;
}
}