diff --git a/doc/source/topics/settings.rst b/doc/source/topics/settings.rst index 284bb6c087..3e8c1470b2 100644 --- a/doc/source/topics/settings.rst +++ b/doc/source/topics/settings.rst @@ -435,11 +435,17 @@ Default:: 'images_panel': True, 'flavors_panel': False, 'users_panel': False, + 'domains_panel': False } A dictionary of currently available AngularJS features. This allows simple toggling of legacy or rewritten features, such as new panels, workflows etc. +.. note:: + + If you toggle 'domains_panel' to True, you also need to enable the setting + of OPENSTACK_KEYSTONE_DEFAULT_DOMAIN and add OPENSTACK_KEYSTONE_DEFAULT_DOMAIN + to REST_API_REQUIRED_SETTINGS. .. _available_themes: diff --git a/openstack_dashboard/dashboards/identity/domains/urls.py b/openstack_dashboard/dashboards/identity/domains/urls.py index dbf521b48b..d536e4a3f7 100644 --- a/openstack_dashboard/dashboards/identity/domains/urls.py +++ b/openstack_dashboard/dashboards/identity/domains/urls.py @@ -12,14 +12,24 @@ # License for the specific language governing permissions and limitations # under the License. +from django.conf import settings from django.conf.urls import url +from django.utils.translation import ugettext_lazy as _ -from openstack_dashboard.dashboards.identity.domains import views +from horizon.browsers import views + +from openstack_dashboard.dashboards.identity.domains import views as legacyView -urlpatterns = [ - url(r'^$', views.IndexView.as_view(), name='index'), - url(r'^create$', views.CreateDomainView.as_view(), name='create'), - url(r'^(?P[^/]+)/update/$', - views.UpdateDomainView.as_view(), name='update') -] +if settings.ANGULAR_FEATURES.get('domains_panel'): + title = _("Domains") + urlpatterns = [ + url('', views.AngularIndexView.as_view(title=title), name='index'), + ] +else: + urlpatterns = [ + url(r'^$', legacyView.IndexView.as_view(), name='index'), + url(r'^create$', legacyView.CreateDomainView.as_view(), name='create'), + url(r'^(?P[^/]+)/update/$', + legacyView.UpdateDomainView.as_view(), name='update') + ] diff --git a/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/details/details.module.js b/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/details/details.module.js new file mode 100644 index 0000000000..df3304c9cf --- /dev/null +++ b/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/details/details.module.js @@ -0,0 +1,54 @@ +/* + * Copyright 2016 NEC Corporation. + * + * 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'; + + /** + * @ngdoc overview + * @ngname horizon.cluster.receivers.details + * + * @description + * Provides details features for domain. + */ + angular.module('horizon.dashboard.identity.domains.details', [ + 'horizon.framework.conf', + 'horizon.app.core' + ]) + .run(registerDomainDetails); + + registerDomainDetails.$inject = [ + 'horizon.dashboard.identity.domains.basePath', + 'horizon.dashboard.identity.domains.resourceType', + 'horizon.app.core.openstack-service-api.keystone', + 'horizon.framework.conf.resource-type-registry.service' + ]; + + function registerDomainDetails(basePath, domainResourceType, keystone, registry) { + registry.getResourceType(domainResourceType) + .setLoadFunction(loadFunction) + .detailsViews.append({ + id: 'domainDetailsOverview', + name: gettext('Overview'), + template: basePath + 'details/overview.html' + }); + + function loadFunction(identifier) { + return keystone.getDomain(identifier); + } + } + +})(); diff --git a/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/details/details.module.spec.js b/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/details/details.module.spec.js new file mode 100644 index 0000000000..2b5d6cca86 --- /dev/null +++ b/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/details/details.module.spec.js @@ -0,0 +1,36 @@ +/** + * 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('Identity domains details module', function() { + it('should exist', function() { + expect(angular.module('horizon.dashboard.identity.domains.details')).toBeDefined(); + }); + + var registry, resource; + beforeEach(module('horizon.framework')); + beforeEach(module('horizon.dashboard.identity.domains')); + beforeEach(inject(function($injector) { + registry = $injector.get('horizon.framework.conf.resource-type-registry.service'); + })); + + it('should be loaded', function() { + resource = registry.getResourceType('OS::Keystone::Domain'); + expect(resource.detailsViews[0].id).toBe('domainDetailsOverview'); + }); + }); +})(); + diff --git a/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/details/drawer.html b/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/details/drawer.html new file mode 100644 index 0000000000..6a8c00bd4a --- /dev/null +++ b/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/details/drawer.html @@ -0,0 +1,5 @@ + + diff --git a/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/details/overview.controller.js b/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/details/overview.controller.js new file mode 100644 index 0000000000..3381c33fb0 --- /dev/null +++ b/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/details/overview.controller.js @@ -0,0 +1,48 @@ +/* + * Copyright 2016 NEC Corporation. + * + * 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"; + + angular + .module('horizon.dashboard.identity.domains') + .controller('DomainOverviewController', DomainOverviewController); + + DomainOverviewController.$inject = [ + 'horizon.dashboard.identity.domains.resourceType', + 'horizon.framework.conf.resource-type-registry.service', + '$scope' + ]; + + function DomainOverviewController( + domainResourceType, + registry, + $scope + ) { + var ctrl = this; + + ctrl.domain = {}; + ctrl.resourceType = registry.getResourceType(domainResourceType); + + // assign a controller attribute once the RoutedDetailsViewController + // has loaded the domain for us + $scope.context.loadPromise.then(onGetDomain); + + function onGetDomain(domain) { + ctrl.domain = domain.data; + } + } + +})(); diff --git a/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/details/overview.controller.spec.js b/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/details/overview.controller.spec.js new file mode 100644 index 0000000000..b650b28ff0 --- /dev/null +++ b/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/details/overview.controller.spec.js @@ -0,0 +1,50 @@ +/** + * 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('Identity domains details module', function() { + it('should exist', function() { + expect(angular.module('horizon.dashboard.identity.domains.details')).toBeDefined(); + }); + }); + + describe('domain overview controller', function() { + var ctrl, $scope; + + beforeEach(module('horizon.dashboard.identity.domains')); + beforeEach(module('horizon.framework.conf')); + beforeEach(inject(function($controller, $injector, $q, _$rootScope_) { + var deferred = $q.defer(); + deferred.resolve({data: {id: '1234', 'name': 'test'}}); + ctrl = $controller('DomainOverviewController', + {'$scope': {context: {loadPromise: deferred.promise}}} + ); + $scope = _$rootScope_.$new(); + })); + + it('sets ctrl.resourceType', function() { + expect(ctrl.resourceType).toBeDefined(); + }); + + it('sets ctrl.domain', inject(function() { + $scope.$apply(); + expect(ctrl.domain).toBeDefined(); + })); + + }); + +})(); + diff --git a/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/details/overview.html b/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/details/overview.html new file mode 100644 index 0000000000..d2743fe6aa --- /dev/null +++ b/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/details/overview.html @@ -0,0 +1,14 @@ +
+
+
+

Domain

+
+ + +
+
+
diff --git a/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/domains.module.js b/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/domains.module.js new file mode 100644 index 0000000000..34ed76363e --- /dev/null +++ b/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/domains.module.js @@ -0,0 +1,102 @@ +/* + * Copyright 2016 NEC Corporation. + * + * 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'; + + /** + * @ngdoc overview + * @ngname horizon.dashboard.identity.domains + * + * @description + * Provides all of the services and widgets required + * to support and display domains related content. + */ + angular + .module('horizon.dashboard.identity.domains', [ + 'ngRoute', + 'horizon.dashboard.identity.domains.details' + ]) + .constant('horizon.dashboard.identity.domains.resourceType', 'OS::Keystone::Domain') + .run(run) + .config(config); + + run.$inject = [ + 'horizon.framework.conf.resource-type-registry.service', + 'horizon.dashboard.identity.domains.service', + 'horizon.dashboard.identity.domains.basePath', + 'horizon.dashboard.identity.domains.resourceType' + ]; + + function run(registry, domainService, basePath, domainResourceType) { + registry.getResourceType(domainResourceType) + .setNames(gettext('Domain'), gettext('Domains')) + .setSummaryTemplateUrl(basePath + 'details/drawer.html') + .setProperties(domainProperties()) + .setListFunction(domainService.listDomains) + .tableColumns + .append({ + id: 'name', + priority: 1, + sortDefault: true, + urlFunction: domainService.getDetailsPath + }) + .append({ + id: 'description', + priority: 1 + }) + .append({ + id: 'id', + priority: 1 + }) + .append({ + id: 'enabled', + priority: 1 + }); + } + + function domainProperties() { + return { + name: { label: gettext('Name'), filters: ['noName'] }, + description: { label: gettext('Description'), filters: ['noValue'] }, + id: { label: gettext('ID'), filters: ['noValue'] }, + enabled: { label: gettext('Enabled'), filters: ['yesno'] } + }; + } + + config.$inject = [ + '$provide', + '$windowProvider', + '$routeProvider' + ]; + + /** + * @name config + * @param {Object} $provide + * @param {Object} $windowProvider + * @param {Object} $routeProvider + * @description Routes used by this module. + * @returns {undefined} Returns nothing + */ + function config($provide, $windowProvider, $routeProvider) { + var path = $windowProvider.$get().STATIC_URL + 'dashboard/identity/domains/'; + $provide.constant('horizon.dashboard.identity.domains.basePath', path); + + $routeProvider.when('/identity/domains', { + templateUrl: path + 'panel.html' + }); + } +})(); diff --git a/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/domains.module.spec.js b/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/domains.module.spec.js new file mode 100644 index 0000000000..7327b0427c --- /dev/null +++ b/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/domains.module.spec.js @@ -0,0 +1,24 @@ +/** + * 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('Identity domains module', function() { + it('should exist', function() { + expect(angular.module('horizon.dashboard.identity.domains')).toBeDefined(); + }); + }); + +})(); diff --git a/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/domains.service.js b/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/domains.service.js new file mode 100644 index 0000000000..177994d142 --- /dev/null +++ b/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/domains.service.js @@ -0,0 +1,128 @@ +/* + * Copyright 2016 NEC Corporation. + * + * 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"; + + angular.module('horizon.dashboard.identity.domains') + .factory('horizon.dashboard.identity.domains.service', domainService); + + domainService.$inject = [ + '$q', + 'horizon.app.core.openstack-service-api.keystone', + 'horizon.app.core.openstack-service-api.policy', + 'horizon.app.core.openstack-service-api.settings' + ]; + + /* + * @ngdoc factory + * @name horizon.dashboard.identity.domains.service + * + * @description + * This service provides functions that are used through the Domains + * features. These are primarily used in the module registrations + * but do not need to be restricted to such use. Each exposed function + * is documented below. + */ + function domainService($q, keystone, policy, settingsService) { + return { + getDetailsPath: getDetailsPath, + getDomainPromise: getDomainPromise, + listDomains: listDomains + }; + + /* + * @ngdoc function + * @name getDetailsPath + * @param item {Object} - The domain object + * @description + * Given an Domain object, returns the relative path to the details + * view. + */ + function getDetailsPath(item) { + return 'project/ngdetails/OS::Keystone::Domain/' + item.id; + } + + /* + * @ngdoc function + * @name listDomains + * @description + * Returns list of domains. This is used in displaying lists of Domains. + * In this case, we need to modify the API's response by adding a + * composite value called 'trackBy' to assist the display mechanism + * when updating rows. + */ + function listDomains() { + var defaultDomain = null; + var KEYSTONE_DEFAULT_DOMAIN = null; + + return $q.all([ + keystone.getDomain('default'), + settingsService.getSetting('OPENSTACK_KEYSTONE_DEFAULT_DOMAIN') + ]).then(allowed); + + function allowed(results) { + defaultDomain = results[0].data; + KEYSTONE_DEFAULT_DOMAIN = results[1]; + + var rules = [['identity', 'identity:list_domains']]; + return policy.ifAllowed({ rules: rules }).then(policySuccess, policyFailed); + } + + function policySuccess() { + if (isDefaultDomain()) { + // In case that a user is cloud admin and context is Keystone default domain. + return keystone.getDomains().then(getDomainSuccess); + } else { + // In case that a user is cloud admin but has a specific domain scope. + return keystone.getDomain(defaultDomain.id).then(getDomainSuccess); + } + } + + function policyFailed() { + // In case that a user doesn't have a privilege of list_domains. + return keystone.getDomain(defaultDomain.id).then(getDomainSuccess); + } + + function getDomainSuccess(response) { + if (!angular.isArray(response.data.items)) { + // the result of getDomain is not array. + response.data.items = [response.data]; + } + return {data: {items: response.data.items.map(modifyDomain)}}; + + function modifyDomain(domain) { + domain.trackBy = domain.id; + return domain; + } + } + + function isDefaultDomain() { + return defaultDomain.name === KEYSTONE_DEFAULT_DOMAIN; + } + } + + /* + * @ngdoc function + * @name getDomainPromise + * @description + * Given an id, returns a promise for the domain data. + */ + function getDomainPromise(identifier) { + return keystone.getDomain(identifier); + } + } + +})(); diff --git a/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/domains.service.spec.js b/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/domains.service.spec.js new file mode 100644 index 0000000000..4db1f12947 --- /dev/null +++ b/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/domains.service.spec.js @@ -0,0 +1,92 @@ +/* + * 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('domain service', function() { + var service, $scope; + beforeEach(module('horizon.dashboard.identity.domains')); + beforeEach(inject(function($injector) { + service = $injector.get('horizon.dashboard.identity.domains.service'); + })); + + it("getDetailsPath creates urls using the item's ID", function() { + var myItem = {id: "1234"}; + expect(service.getDetailsPath(myItem)).toBe('project/ngdetails/OS::Keystone::Domain/1234'); + }); + + describe('listDomains', function() { + var keystone, setting, policy; + beforeEach(inject(function($injector) { + keystone = $injector.get('horizon.app.core.openstack-service-api.keystone'); + setting = $injector.get('horizon.app.core.openstack-service-api.settings'); + policy = $injector.get('horizon.app.core.openstack-service-api.policy'); + })); + + it("allowed list_domain and default domain scope", inject(function($q, _$rootScope_) { + $scope = _$rootScope_.$new(); + var deferredGetDomain = $q.defer(); + var deferredGetDomains = $q.defer(); + var deferredSetting = $q.defer(); + var deferredPolicy = $q.defer(); + spyOn(keystone, 'getDomain').and.returnValue(deferredGetDomain.promise); + spyOn(keystone, 'getDomains').and.returnValue(deferredGetDomains.promise); + spyOn(setting, 'getSetting').and.returnValue(deferredSetting.promise); + spyOn(policy, 'ifAllowed').and.returnValue(deferredPolicy.promise); + + var result = service.listDomains({}); + + deferredGetDomain.resolve({data: {id: 'default', name: 'Default'}}); + deferredGetDomains.resolve({data: {items: [{id: '1234', name: 'test_domain1'}]}}); + deferredSetting.resolve("Default"); + deferredPolicy.resolve({"allowed": true}); + + $scope.$apply(); + expect(result.$$state.value.data.items[0].trackBy).toBe('1234'); + })); + + it("allowed list_domain and not domain scope", inject(function($q, _$rootScope_) { + $scope = _$rootScope_.$new(); + var deferredGetDomain = $q.defer(); + var deferredSetting = $q.defer(); + var deferredPolicy = $q.defer(); + spyOn(keystone, 'getDomain').and.returnValue(deferredGetDomain.promise); + spyOn(setting, 'getSetting').and.returnValue(deferredSetting.promise); + spyOn(policy, 'ifAllowed').and.returnValue(deferredPolicy.promise); + + var result = service.listDomains({}); + + deferredGetDomain.resolve({data: {id: '1234', name: 'test_domain1'}}); + deferredSetting.resolve("Default"); + deferredPolicy.resolve({"allowed": true}); + + $scope.$apply(); + expect(result.$$state.value.data.items[0].trackBy).toBe('1234'); + })); + }); + + describe('getDomainPromise', function() { + it("provides a promise", inject(function($q, $injector) { + var keystone = $injector.get('horizon.app.core.openstack-service-api.keystone'); + var deferred = $q.defer(); + spyOn(keystone, 'getDomain').and.returnValue(deferred.promise); + var result = service.getDomainPromise({}); + deferred.resolve({id: 1, name: 'test_domain'}); + expect(keystone.getDomain).toHaveBeenCalled(); + expect(result.$$state.value.name).toBe('test_domain'); + })); + }); + }); + +})(); diff --git a/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/panel.html b/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/panel.html new file mode 100644 index 0000000000..e757dd6568 --- /dev/null +++ b/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/panel.html @@ -0,0 +1,4 @@ + + + + diff --git a/openstack_dashboard/dashboards/identity/static/dashboard/identity/identity.module.js b/openstack_dashboard/dashboards/identity/static/dashboard/identity/identity.module.js index bedb1a1f45..36ac9b39ba 100644 --- a/openstack_dashboard/dashboards/identity/static/dashboard/identity/identity.module.js +++ b/openstack_dashboard/dashboards/identity/static/dashboard/identity/identity.module.js @@ -26,6 +26,7 @@ */ angular .module('horizon.dashboard.identity', [ + 'horizon.dashboard.identity.domains', 'horizon.dashboard.identity.users', 'horizon.dashboard.identity.projects' ]); diff --git a/openstack_dashboard/local/local_settings.py.example b/openstack_dashboard/local/local_settings.py.example index ed8e6b7553..5c4a147e84 100644 --- a/openstack_dashboard/local/local_settings.py.example +++ b/openstack_dashboard/local/local_settings.py.example @@ -747,7 +747,8 @@ SECURITY_GROUP_RULES = { # See: https://wiki.openstack.org/wiki/Horizon/RESTAPI REST_API_REQUIRED_SETTINGS = ['OPENSTACK_HYPERVISOR_FEATURES', 'LAUNCH_INSTANCE_DEFAULTS', - 'OPENSTACK_IMAGE_FORMATS'] + 'OPENSTACK_IMAGE_FORMATS', + 'OPENSTACK_KEYSTONE_DEFAULT_DOMAIN'] # Additional settings can be made available to the client side for # extensibility by specifying them in REST_API_ADDITIONAL_SETTINGS