Merge "[Launch Instance Fix] Conditionally enable UI"
This commit is contained in:
commit
c6b2bc7e22
@ -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')
|
||||
|
@ -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',
|
||||
|
@ -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');
|
||||
}
|
||||
])
|
||||
|
||||
;})();
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
;})();
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user