Adding identity domains table

This patch adds the domain table to the identity domains panel
adn domain detail page.
Actions will be added in subsequent patches.

To test this patch, it needs to show domain panel and enable angular
feature for domain panel.

To show domain panel (after installing OpenStack with latest Devstack).
 - using backends except signed cookie as SESSION_ENGINE in local_settings.py.
   e.g memcached
----
CACHES = {
   'default': {
       'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
       'LOCATION': '127.0.0.1:11211',
   }
}
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
----

 - enable OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT in local_settings.py
----
OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT = True
----

After that, if admin user logged in, the user could see Domain panel and
could operate actions for domains like Create.

To test this patch, after above, set 'domains_panel': True in 'ANGULAR_FEATURES'.

Change-Id: Ib15e25a4cebe6de83f1c2f427490d5850d36b908
Partially-Implements: blueprint angularize-identity-tables
This commit is contained in:
Kenji Ishii 2016-10-18 11:57:27 +09:00
parent 9524bb14eb
commit cc94b0f202
15 changed files with 583 additions and 8 deletions

View File

@ -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:

View File

@ -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<domain_id>[^/]+)/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<domain_id>[^/]+)/update/$',
legacyView.UpdateDomainView.as_view(), name='update')
]

View File

@ -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);
}
}
})();

View File

@ -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');
});
});
})();

View File

@ -0,0 +1,5 @@
<hz-resource-property-list
resource-type-name="OS::Keystone::Domain"
item="item"
property-groups="[['id', 'name'], ['description', 'enabled']]">
</hz-resource-property-list>

View File

@ -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;
}
}
})();

View File

@ -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();
}));
});
})();

View File

@ -0,0 +1,14 @@
<div ng-controller="DomainOverviewController as ctrl">
<div class="row">
<div class="col-md-6 detail">
<h3 translate>Domain</h3>
<hr>
<hz-resource-property-list
resource-type-name="OS::Keystone::Domain"
cls="dl-horizontal"
item="ctrl.domain"
property-groups="[['id', 'name', 'description', 'enabled']]">
</hz-resource-property-list>
</div>
</div>
</div>

View File

@ -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'
});
}
})();

View File

@ -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();
});
});
})();

View File

@ -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);
}
}
})();

View File

@ -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');
}));
});
});
})();

View File

@ -0,0 +1,4 @@
<hz-resource-panel resource-type-name="OS::Keystone::Domain">
<hz-resource-table resource-type-name="OS::Keystone::Domain">
</hz-resource-table>
</hz-resource-panel>

View File

@ -26,6 +26,7 @@
*/
angular
.module('horizon.dashboard.identity', [
'horizon.dashboard.identity.domains',
'horizon.dashboard.identity.users',
'horizon.dashboard.identity.projects'
]);

View File

@ -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