[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:
parent
3a59bec6a7
commit
93eb310037
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
])
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
;})();
|
||||
|
|
|
@ -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 -->
|
||||
|
||||
|
|
|
@ -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
|
||||
//
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue