[Launch Instance Fix] Conditionally enable UI

In Launch Instance work flow Configuration step, `DiskConfig`
and `ConfigDrive` should be handle only when certain extension
on navaExtension are enbled respectively.

Created a generic way to make this happen.

Co-Authored-By: Travis Tripp <travis.tripp@hp.com>
Change-Id: I98a91ff9e1ae78310fb7e568cce909e76e610cf8
Closes-Bug: #1435155
This commit is contained in:
Shaoquan Chen 2015-03-22 23:34:59 -07:00 committed by Travis Tripp
parent 5bec804f2a
commit 43f2917cbe
6 changed files with 519 additions and 49 deletions

View File

@ -270,40 +270,55 @@ limitations under the License.
angular.module('hz.api')
.service('novaAPI', ['apiService', NovaAPI]);
/**
* @ngdoc service
* @name hz.api.novaExtensions
* @description
* Provides cached access to Nova Extensions with utilities to help
* with asynchronous data loading. The cache may be reset at any time
* by accessing the cache and calling removeAll. The next call to any
* function will retrieve fresh results.
*
* The enabled extensions do not change often, so using cached data will
* speed up results. Even on a local devstack in informal testing,
* this saved between 30 - 100 ms per request.
*/
/**
* @ngdoc service
* @name hz.api.novaExtensions
* @description
* Provides cached access to Nova Extensions with utilities to help
* with asynchronous data loading. The cache may be reset at any time
* by accessing the cache and calling removeAll. The next call to any
* function will retrieve fresh results.
*
* The enabled extensions do not change often, so using cached data will
* speed up results. Even on a local devstack in informal testing,
* this saved between 30 - 100 ms per request.
*/
function NovaExtensions($cacheFactory, $q, novaAPI) {
var service = {};
service.cache = $cacheFactory('hz.api.novaExtensions', {capacity: 1});
var service = {};
service.cache = $cacheFactory('hz.api.novaExtensions', {capacity: 1});
service.get = function () {
return novaAPI.getExtensions({cache: service.cache})
.then(function (data) {
return data.data.items;
});
};
service.get = function() {
return novaAPI.getExtensions({cache: service.cache})
.then(function(data){
return data.data.items;
service.ifNameEnabled = function(desired) {
var deferred = $q.defer();
service.get().then(onDataLoaded, onDataFailure);
function onDataLoaded(extensions) {
if (enabled(extensions, 'name', desired)) {
deferred.resolve();
} else {
deferred.reject(interpolate(
gettext('Extension is not enabled: %(extension)s'),
{extension: desired},
true));
}
);
}
function onDataFailure() {
deferred.reject(gettext('Cannot get nova extension list.'));
}
return deferred.promise;
};
service.ifNameEnabled = function(desired, doThis) {
return service.get().then(function(extensions){
if (enabled(extensions, 'name', desired)){
return $q.when(doThis());
}
}
);
};
// This is an alias to support the extension directive default interface
service.ifEnabled = service.ifNameEnabled;
function enabled(resources, key, desired) {
if(resources) {
@ -315,7 +330,7 @@ limitations under the License.
}
}
return service;
return service;
}
angular.module('hz.api')

View File

@ -28,6 +28,7 @@ LAUNCH_INST = 'dashboard/launch-instance/'
ADD_JS_FILES = [
'dashboard/dashboard.module.js',
'dashboard/workflow/workflow.js',
'dashboard/cloud-services/cloud-services.js',
LAUNCH_INST + 'launch-instance.js',
LAUNCH_INST + 'launch-instance.model.js',
LAUNCH_INST + 'source/source.js',
@ -43,6 +44,7 @@ ADD_JS_FILES = [
ADD_JS_SPEC_FILES = [
'dashboard/dashboard.module.spec.js',
'dashboard/workflow/workflow.spec.js',
'dashboard/cloud-services/cloud-services.spec.js',
LAUNCH_INST + 'launch-instance.spec.js',
LAUNCH_INST + 'launch-instance.model.spec.js',
LAUNCH_INST + 'source/source.spec.js',

View File

@ -0,0 +1,193 @@
/*
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function () {
'use strict';
var fromJson = angular.fromJson,
isArray = angular.isArray;
angular.module('hz.dashboard')
/**
* @ngdoc factory
* @name hz.dashboard:factory:cloudServices
* @module hz.dashboard
* @kind hash table
* @description
*
* Provides a hash table contains all the cloud services so that:
*
* 1) Easy to inject all the services since they are injected with one dependency.
* 2) Provides a way to look up a service by name programmatically.
*
* The use of this is currently limited to existing API services. Use at
* your own risk for extensibility purposes at this time. The API will
* be evolving in the coming release and backward compatibility is not
* guaranteed. This also makes no guarantee that the back-end service
* is actually enabled.
*/
.factory('cloudServices', [
'cinderAPI',
'glanceAPI',
'keystoneAPI',
'neutronAPI',
'novaAPI',
'novaExtensions',
'securityGroup',
'serviceCatalog',
function (cinderAPI,
glanceAPI,
keystoneAPI,
neutronAPI,
novaAPI,
novaExtensions,
securityGroup,
serviceCatalog) {
return {
cinder: cinderAPI,
glance: glanceAPI,
keystone: keystoneAPI,
neutron: neutronAPI,
nova: novaAPI,
novaExtensions: novaExtensions,
securityGroup: securityGroup,
serviceCatalog: serviceCatalog
};
}
])
/**
* @ngdoc factory
* @name hz.dashboard:factory:ifExtensionsEnabled
* @module hz.dashboard
* @kind function
* @description
*
* Check to see if all the listed extensions 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.
* @return Promise the promise of the deferred task that gets resolved
* when all the sub-tasks are resolved.
*/
.factory('ifExtensionsEnabled', ['$q', 'cloudServices',
function ($q, cloudServices) {
return function ifExtensionsEnabled(serviceName, extensions) {
return $q.all(
extensions.map(function (extension) {
return cloudServices[serviceName].ifEnabled(extension);
})
);//return
};//return
}
])
/**
* @ngdoc factory
* @name hz.dashboard:factory:createDirectiveSpec
* @module hz.dashboard
* @kind function
* @description
*
* A normalized function that can create a directive specification object
* based on `serviceName`.
*
* @param String serviceName The name of the service, e.g. `novaExtensions`.
* @return Object a directive specification object that can be used to
* create an angular directive.
*/
.factory('createDirectiveSpec', ['ifExtensionsEnabled',
function (ifExtensionsEnabled) {
return function createDirectiveSpec(serviceName) {
return {
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
}
])
/**
* @ngdoc directive
* @name hz.dashboard:directive:novaExtension
* @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 extensions
* are enabled on `novaExtensions` service.
*
* @example
*
```html
<nova-extension required-extensions='["config_drive"]'>
<div class="checkbox customization-script-source">
<label>
<input type="checkbox"
ng-model="model.newInstanceSpec.config_drive">
{$ ::label.configurationDrive $}
</label>
</div>
</nova-extension>
<nova-extension required-extensions='["disk_config"]'>
<div class="form-group disk-partition">
<label for="launch-instance-disk-partition">
{$ ::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">
</select>
</div>
</nova-extension>
```
*/
.directive('novaExtension', ['createDirectiveSpec',
function (createDirectiveSpec) {
return createDirectiveSpec('novaExtensions');
}
])
;})();

View File

@ -0,0 +1,257 @@
/*
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function () {
'use strict';
describe('hz.dashboard', function () {
//
// factory:cloudServices
//
describe('factory:cloudServices', function () {
var cloudServices;
beforeEach(module('hz.dashboard', function ($provide) {
$provide.value('cinderAPI', {});
$provide.value('glanceAPI', {});
$provide.value('keystoneAPI', {});
$provide.value('neutronAPI', {});
$provide.value('novaAPI', {});
$provide.value('novaExtensions', {});
$provide.value('securityGroup', {});
$provide.value('serviceCatalog', {});
}));
beforeEach(inject(function ($injector) {
cloudServices = $injector.get('cloudServices');
}));
it('should have `cloudServices` defined.', function () {
expect(cloudServices).toBeDefined();
});
it('should have `cloudServices.cinder` defined.', function () {
expect(cloudServices.cinder).toBeDefined();
});
it('should have `cloudServices.glance` defined.', function () {
expect(cloudServices.glance).toBeDefined();
});
it('should have `cloudServices.keystone` defined.', function () {
expect(cloudServices.keystone).toBeDefined();
});
it('should have `cloudServices.neutron` defined.', function () {
expect(cloudServices.neutron).toBeDefined();
});
it('should have `cloudServices.nova` defined.', function () {
expect(cloudServices.nova).toBeDefined();
});
it('should have `cloudServices.novaExtensions` defined.', function () {
expect(cloudServices.novaExtensions).toBeDefined();
});
});
//
// factory:ifExtensionsEnabled
//
describe('factory:ifExtensionsEnabled', function () {
var ifExtensionsEnabled,
$q,
cloudServices;
beforeEach(module('hz.dashboard', function ($provide) {
$q = {
all: function () {
return {
then: function () {}
};
}
};
cloudServices = {
'someService': {
ifEnabled: function () {}
}
};
spyOn(cloudServices.someService, 'ifEnabled');
spyOn($q, 'all');
$provide.value('$q', $q);
$provide.value('cloudServices', cloudServices);
}));
beforeEach(inject(function ($injector) {
ifExtensionsEnabled = $injector.get('ifExtensionsEnabled');
}));
it('should have `ifExtensionsEnabled` defined as a function.', function () {
expect(ifExtensionsEnabled).toBeDefined();
expect(angular.isFunction(ifExtensionsEnabled)).toBe(true);
});
it('should call $q.all() and someService.ifEnabled() when invoking ifExtensionsEnabled().', function () {
var extensions = ['ext1', 'ext2'];
ifExtensionsEnabled('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', []);
}).not.toThrow();
});
it('should throw when extensions is null or undefined or not an array', function () {
expect(function () {
ifExtensionsEnabled('someService', null);
}).toThrow();
expect(function () {
ifExtensionsEnabled('someService');
}).toThrow();
expect(function () {
ifExtensionsEnabled('123');
}).toThrow();
});
it('should not throw when the provided serviceName is not a key in the services hash table', function () {
expect(function () {
ifExtensionsEnabled('invlidServiceName', []);
}).not.toThrow();
});
});
//
// factory:createDirectiveSpec
//
describe('factory:createDirectiveSpec', function () {
var createDirectiveSpec,
ifExtensionsEnabled;
beforeEach(module('hz.dashboard', function ($provide) {
ifExtensionsEnabled = function () {
return {
then: function (successCallback, errorCallback) {
}
};
};
$provide.value('ifExtensionsEnabled', ifExtensionsEnabled);
}));
beforeEach(inject(function ($injector) {
createDirectiveSpec = $injector.get('createDirectiveSpec');
}));
it('should have `createDirectiveSpec` defined as a function.', function () {
expect(createDirectiveSpec).toBeDefined();
expect(angular.isFunction(createDirectiveSpec)).toBe(true);
});
describe('When called, the returned object', function () {
var directiveSpec;
beforeEach(function () {
directiveSpec = createDirectiveSpec('someService');
});
it('should be defined.', function () {
expect(directiveSpec).toBeDefined();
});
it('should have "restrict" property "E".', function () {
expect(directiveSpec.restrict).toBe('E');
});
it('should have "transclude" property true.', function () {
expect(directiveSpec.transclude).toBe(true);
});
it('should have "link" property as a function.', function () {
expect(directiveSpec.link).toEqual(jasmine.any(Function));
});
});
});
//
// directive:novaExtension
//
describe('directive:novaExtension', function () {
var $timeout,
$scope,
html = [
'<nova-extension required-extensions=\'["config_drive"]\'>',
'<div class="child-element">',
'</div>',
'</nova-extension>'
].join(''),
element;
beforeEach(module('hz.dashboard', function ($provide) {
$provide.value('ifExtensionsEnabled', 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

@ -9,24 +9,28 @@
key="user_data">
</load-edit>
<div class="form-group disk-partition">
<label for="launch-instance-disk-partition">
{$ ::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 config.diskConfigOptions">
</select>
</div>
<nova-extension required-extensions='["DiskConfig"]'>
<div class="form-group disk-partition">
<label for="launch-instance-disk-partition">
{$ ::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 config.diskConfigOptions">
</select>
</div>
</nova-extension>
<div class="checkbox">
<label>
<input type="checkbox"
ng-model="model.newInstanceSpec.config_drive">
{$ ::config.label.configurationDrive $}
</label>
</div>
<nova-extension required-extensions='["ConfigDrive"]'>
<div class="checkbox customization-script-source">
<label>
<input type="checkbox"
ng-model="model.newInstanceSpec.config_drive">
{$ ::config.label.configurationDrive $}
</label>
</div>
</nova-extension>
</div>
</div>

View File

@ -387,9 +387,8 @@
volumePromises.push(cinderAPI.getVolumeSnapshots({ status: 'available' }).then(onGetVolumeSnapshots));
// Can only boot image to volume if the Nova extension is enabled.
novaExtensions.ifNameEnabled('BlockDeviceMappingV2Boot', function(){
model.allowCreateVolumeFromImage = true;
});
novaExtensions.ifNameEnabled('BlockDeviceMappingV2Boot')
.then(function(){ model.allowCreateVolumeFromImage = true; });
return $q.all(volumePromises);
}