[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; 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', 'novaExtensions',
'securityGroup', 'securityGroup',
'serviceCatalog', 'serviceCatalog',
'settingsService',
function (cinderAPI, function (cinderAPI,
glanceAPI, glanceAPI,
@ -58,7 +59,8 @@
novaAPI, novaAPI,
novaExtensions, novaExtensions,
securityGroup, securityGroup,
serviceCatalog) { serviceCatalog,
settingsService) {
return { return {
cinder: cinderAPI, cinder: cinderAPI,
@ -68,35 +70,39 @@
nova: novaAPI, nova: novaAPI,
novaExtensions: novaExtensions, novaExtensions: novaExtensions,
securityGroup: securityGroup, securityGroup: securityGroup,
serviceCatalog: serviceCatalog serviceCatalog: serviceCatalog,
settingsService: settingsService
}; };
} }
]) ])
/** /**
* @ngdoc factory * @ngdoc factory
* @name hz.dashboard:factory:ifExtensionsEnabled * @name hz.dashboard:factory:ifFeaturesEnabled
* @module hz.dashboard * @module hz.dashboard
* @kind function * @kind function
* @description * @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. * which is described by the service name.
* *
* This is an asynchronous operation. * This is an asynchronous operation.
* *
* @param String serviceName The name of the service, e.g. `novaExtensions`. * @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 * @return Promise the promise of the deferred task that gets resolved
* when all the sub-tasks are resolved. * when all the sub-tasks are resolved.
*/ */
.factory('ifExtensionsEnabled', ['$q', 'cloudServices', .factory('ifFeaturesEnabled', ['$q', 'cloudServices',
function ($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( return $q.all(
extensions.map(function (extension) { features.map(function (feature) {
return cloudServices[serviceName].ifEnabled(extension); return cloudServices[serviceName].ifEnabled(feature);
}) })
);//return );//return
};//return };//return
@ -114,35 +120,41 @@
* based on `serviceName`. * based on `serviceName`.
* *
* @param String serviceName The name of the service, e.g. `novaExtensions`. * @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 * @return Object a directive specification object that can be used to
* create an angular directive. * create an angular directive.
*/ */
.factory('createDirectiveSpec', ['ifExtensionsEnabled', .factory('createDirectiveSpec', ['ifFeaturesEnabled',
function (ifExtensionsEnabled) { function (ifFeaturesEnabled) {
return function createDirectiveSpec(serviceName) { 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 { return {
link: link,
restrict: 'E', restrict: 'E',
transclude: true, 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
} }
]) ])
@ -158,7 +170,7 @@
* *
* @example * @example
* *
```html ```html
<nova-extension required-extensions='["config_drive"]'> <nova-extension required-extensions='["config_drive"]'>
<div class="checkbox customization-script-source"> <div class="checkbox customization-script-source">
<label> <label>
@ -181,12 +193,37 @@
</select> </select>
</div> </div>
</nova-extension> </nova-extension>
``` ```
*/ */
.directive('novaExtension', ['createDirectiveSpec', .directive('novaExtension', ['createDirectiveSpec',
function (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('novaExtensions', {});
$provide.value('securityGroup', {}); $provide.value('securityGroup', {});
$provide.value('serviceCatalog', {}); $provide.value('serviceCatalog', {});
$provide.value('settingsService', {});
})); }));
beforeEach(inject(function ($injector) { beforeEach(inject(function ($injector) {
@ -69,14 +70,18 @@
expect(cloudServices.novaExtensions).toBeDefined(); expect(cloudServices.novaExtensions).toBeDefined();
}); });
it('should have `cloudServices.settingsService` defined.', function () {
expect(cloudServices.settingsService).toBeDefined();
});
}); });
// //
// factory:ifExtensionsEnabled // factory:ifFeaturesEnabled
// //
describe('factory:ifExtensionsEnabled', function () { describe('factory:ifFeaturesEnabled', function () {
var ifExtensionsEnabled, var ifFeaturesEnabled,
$q, $q,
cloudServices; cloudServices;
@ -103,44 +108,44 @@
})); }));
beforeEach(inject(function ($injector) { beforeEach(inject(function ($injector) {
ifExtensionsEnabled = $injector.get('ifExtensionsEnabled'); ifFeaturesEnabled = $injector.get('ifFeaturesEnabled');
})); }));
it('should have `ifExtensionsEnabled` defined as a function.', function () { it('should have `ifFeaturesEnabled` defined as a function.', function () {
expect(ifExtensionsEnabled).toBeDefined(); expect(ifFeaturesEnabled).toBeDefined();
expect(angular.isFunction(ifExtensionsEnabled)).toBe(true); 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']; var extensions = ['ext1', 'ext2'];
ifExtensionsEnabled('someService', extensions); ifFeaturesEnabled('someService', extensions);
expect($q.all).toHaveBeenCalled(); expect($q.all).toHaveBeenCalled();
expect(cloudServices.someService.ifEnabled).toHaveBeenCalled(); expect(cloudServices.someService.ifEnabled).toHaveBeenCalled();
}); });
it('should not throw when passing in an empty extensions list.', function () { it('should not throw when passing in an empty extensions list.', function () {
expect(function () { expect(function () {
ifExtensionsEnabled('someService', []); ifFeaturesEnabled('someService', []);
}).not.toThrow(); }).not.toThrow();
}); });
it('should throw when extensions is null or undefined or not an array', function () { it('should throw when extensions is null or undefined or not an array', function () {
expect(function () { expect(function () {
ifExtensionsEnabled('someService', null); ifFeaturesEnabled('someService', null);
}).toThrow(); }).toThrow();
expect(function () { expect(function () {
ifExtensionsEnabled('someService'); ifFeaturesEnabled('someService');
}).toThrow(); }).toThrow();
expect(function () { expect(function () {
ifExtensionsEnabled('123'); ifFeaturesEnabled('123');
}).toThrow(); }).toThrow();
}); });
it('should not throw when the provided serviceName is not a key in the services hash table', function () { it('should not throw when the provided serviceName is not a key in the services hash table', function () {
expect(function () { expect(function () {
ifExtensionsEnabled('invlidServiceName', []); ifFeaturesEnabled('invlidServiceName', []);
}).not.toThrow(); }).not.toThrow();
}); });
}); });
@ -151,16 +156,16 @@
describe('factory:createDirectiveSpec', function () { describe('factory:createDirectiveSpec', function () {
var createDirectiveSpec, var createDirectiveSpec,
ifExtensionsEnabled; ifFeaturesEnabled;
beforeEach(module('hz.dashboard', function ($provide) { beforeEach(module('hz.dashboard', function ($provide) {
ifExtensionsEnabled = function () { ifFeaturesEnabled = function () {
return { return {
then: function (successCallback, errorCallback) { then: function (successCallback, errorCallback) {
} }
}; };
}; };
$provide.value('ifExtensionsEnabled', ifExtensionsEnabled); $provide.value('ifFeaturesEnabled', ifFeaturesEnabled);
})); }));
beforeEach(inject(function ($injector) { beforeEach(inject(function ($injector) {
@ -176,7 +181,7 @@
var directiveSpec; var directiveSpec;
beforeEach(function () { beforeEach(function () {
directiveSpec = createDirectiveSpec('someService'); directiveSpec = createDirectiveSpec('someService', 'someFeature');
}); });
it('should be defined.', function () { it('should be defined.', function () {
@ -215,7 +220,7 @@
element; element;
beforeEach(module('hz.dashboard', function ($provide) { beforeEach(module('hz.dashboard', function ($provide) {
$provide.value('ifExtensionsEnabled', function () { $provide.value('ifFeaturesEnabled', function () {
return { return {
then: function (successCallback, errorCallback) { then: function (successCallback, errorCallback) {
$timeout(successCallback); $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="col-xs-12 col-sm-3">
<div class="form-group create-volume"> <div class="form-group create-volume">
<div class="checkbox"> <label class="on-top">{$ ::label.volumeCreate $}</label>
<label class="on-top"> <div class="form-field">
<input type="checkbox" ng-model="model.newInstanceSpec.vol_create"> <div class="btn-group">
{$ ::label.volumeCreate $}</label> <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>
</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" <div class="form-field volume-size"
ng-class="{ 'has-warning': launchInstanceSourceForm['volume-size'].$invalid }"> ng-class="{ 'has-warning': launchInstanceSourceForm['volume-size'].$invalid }">
<label class="on-top">{$ ::label.volumeSize $}</label> <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="col-xs-12 col-sm-4" ng-if="model.newInstanceSpec.vol_create == true">
<div class="form-group delete-volume"> <div class="form-group delete-volume">
<div class="checkbox"> <label class="on-top">{$ ::label.deleteVolumeOnTerminate $}</label>
<label class="on-top"> <div class="form-field">
<input type="checkbox" <div class="btn-group">
ng-model="model.newInstanceSpec.vol_delete_on_terminate"> <label class="btn btn-toggle"
{$ ::label.deleteVolumeOnTerminate $}</label> 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>
</div> </div>
@ -136,14 +155,19 @@
<div class="col-xs-12 col-sm-9" <div class="col-xs-12 col-sm-9"
ng-if="model.newInstanceSpec.source_type.type == 'volume' || model.newInstanceSpec.source_type.type == 'volume_snapshot'"> 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="col-xs-12 col-sm-6">
<div class="form-group delete-volume"> <div class="form-group delete-volume">
<div class="checkbox"> <label class="on-top">{$ ::label.deleteVolumeOnTerminate $}</label>
<label class="on-top"> <div class="form-field">
<input type="checkbox" <div class="btn-group">
ng-model="model.newInstanceSpec.vol_delete_on_terminate"> <label class="btn btn-toggle"
{$ ::label.deleteVolumeOnTerminate $}</label> 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>
</div> </div>
</div><!-- end volume select options --> </div><!-- end volume select options -->

View File

@ -83,9 +83,9 @@
instanceSourceTitle: gettext('Instance Source'), 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.'), 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'), bootSource: gettext('Select Boot Source'),
deviceSize: gettext('Device Size (GB)'), volumeSize: gettext('Size (GB)'),
volumeSize: gettext('Volume Size (GB)'),
volumeCreate: gettext('Create New Volume'), volumeCreate: gettext('Create New Volume'),
volumeDeviceName: gettext('Device Name'),
deleteVolumeOnTerminate: gettext('Delete Volume on Terminate'), deleteVolumeOnTerminate: gettext('Delete Volume on Terminate'),
id: gettext('ID'), id: gettext('ID'),
min_ram: gettext('Min Ram'), 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.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'); $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 // 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"] { [ng-controller="LaunchInstanceSourceCtrl"] {
td.hi-light { td.hi-light {
@ -32,22 +9,36 @@
text-align: right; text-align: right;
padding-right: 30px; padding-right: 30px;
} }
}
.instance-source { .selected-source {
margin-top: 18px; background: #eee;
margin-bottom: 40px; padding: 12px 18px;
margin-top: 20px;
margin-bottom: 20px;
.image select { .chart {
width: 99%; width: 99%;
margin-bottom: 0;
padding: 10px;
@media (min-width: 768px) {
border-left: 1px solid #ccc;
padding-left: 20px;
}
}
} }
.volume-size input[type="number"]{ .instance-source {
width: 90%; 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;
} }
} }