diff --git a/horizon/static/angular/wizard/wizard.scss b/horizon/static/angular/wizard/wizard.scss index 8b4183802a..516e6ac05e 100644 --- a/horizon/static/angular/wizard/wizard.scss +++ b/horizon/static/angular/wizard/wizard.scss @@ -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; + } +} diff --git a/openstack_dashboard/static/dashboard/cloud-services/cloud-services.js b/openstack_dashboard/static/dashboard/cloud-services/cloud-services.js index 0a3bea86ef..88ea084019 100644 --- a/openstack_dashboard/static/dashboard/cloud-services/cloud-services.js +++ b/openstack_dashboard/static/dashboard/cloud-services/cloud-services.js @@ -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 extensions A list of extension's names. + * @param Array 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
- ``` + ``` */ .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 + + + + ``` + */ + + .directive('settingsService', ['createDirectiveSpec', + function (createDirectiveSpec) { + return createDirectiveSpec('settingsService', 'requiredSettings'); } ]) diff --git a/openstack_dashboard/static/dashboard/cloud-services/cloud-services.spec.js b/openstack_dashboard/static/dashboard/cloud-services/cloud-services.spec.js index 858ae810d4..1d167c19a1 100644 --- a/openstack_dashboard/static/dashboard/cloud-services/cloud-services.spec.js +++ b/openstack_dashboard/static/dashboard/cloud-services/cloud-services.spec.js @@ -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 = [ + '', + '
', + '
', + '
' + ].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); + }); + }); + }) ;})(); diff --git a/openstack_dashboard/static/dashboard/launch-instance/source/source.html b/openstack_dashboard/static/dashboard/launch-instance/source/source.html index ae35d774c9..d4e9056720 100644 --- a/openstack_dashboard/static/dashboard/launch-instance/source/source.html +++ b/openstack_dashboard/static/dashboard/launch-instance/source/source.html @@ -95,15 +95,31 @@
-
- + +
+
+ +
-
+ +
+
+ + +
+
+
+ +
@@ -122,11 +138,14 @@
-
- + +
+
+ +
@@ -136,14 +155,19 @@
+
-
- + +
+
+ +
+
diff --git a/openstack_dashboard/static/dashboard/launch-instance/source/source.js b/openstack_dashboard/static/dashboard/launch-instance/source/source.js index 56a409989f..64dc33374d 100644 --- a/openstack_dashboard/static/dashboard/launch-instance/source/source.js +++ b/openstack_dashboard/static/dashboard/launch-instance/source/source.js @@ -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 // diff --git a/openstack_dashboard/static/dashboard/launch-instance/source/source.scss b/openstack_dashboard/static/dashboard/launch-instance/source/source.scss index eb5a30e247..4dd356713a 100644 --- a/openstack_dashboard/static/dashboard/launch-instance/source/source.scss +++ b/openstack_dashboard/static/dashboard/launch-instance/source/source.scss @@ -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; } }