Trunks panel: admin panel

Enable admin panel for trunks. For the admin panel the delete action and
the readonly operations are enabled (table view and details view).

Change-Id: Icfc01612cc60798e4b0ff7379a9c8b83d3f1d60b
Implements: blueprint neutron-trunk-ui
This commit is contained in:
Lajos Katona 2018-01-25 07:54:17 +01:00
parent f1eba8073d
commit aa669f0d02
14 changed files with 270 additions and 61 deletions

View File

@ -0,0 +1,45 @@
# Copyright 2017 Ericsson
#
# 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.
import logging
from django.utils.translation import ugettext_lazy as _
import horizon
from openstack_dashboard.api import neutron
LOG = logging.getLogger(__name__)
class Trunks(horizon.Panel):
name = _("Trunks")
slug = "trunks"
permissions = ('openstack.services.network',)
policy_rules = (("trunk", "context_is_admin"),)
def allowed(self, context):
request = context['request']
try:
return (
super(Trunks, self).allowed(context)
and request.user.has_perms(self.permissions)
and neutron.is_extension_supported(request,
extension_alias='trunk')
)
except Exception:
LOG.error("Call to list enabled services failed. This is likely "
"due to a problem communicating with the Neutron "
"endpoint. Trunks admin panel will not be displayed.")
return False

View File

@ -0,0 +1,26 @@
# Copyright 2017 Ericsson
#
# 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.
from django.conf.urls import url
from django.utils.translation import ugettext_lazy as _
from horizon.browsers.views import AngularIndexView
title = _("Trunks")
urlpatterns = [
url(r'^$', AngularIndexView.as_view(title=title), name='index'),
url(r'^(?P<trunk_id>[^/]+)/$',
AngularIndexView.as_view(title=title), name='detail'),
]

View File

@ -0,0 +1,9 @@
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'trunks'
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = 'admin'
# The slug of the panel group the PANEL is associated with.
PANEL_GROUP = 'network'
# Python panel class of the PANEL to be added.
ADD_PANEL = 'openstack_dashboard.dashboards.admin.trunks.panel.Trunks'

View File

@ -90,6 +90,26 @@
});
});
it('can suppress errors in case of deleting trunks', function() {
spyOn(apiService, 'delete').and.callFake(function() {
return {
success: function(c) {
c();
return this;
},
error: function(c) {
c();
return this;
}
};
});
spyOn(toastService, 'add').and.callThrough();
service.deleteTrunk('42', true).error(function() {
expect(toastService.add).not.toHaveBeenCalled();
});
});
var tests = [
{

View File

@ -34,7 +34,8 @@
// angular-schema-form would have made many things easier, but it wasn't
// really an option because it does not have a transfer-table widget.
'horizon.framework.widgets.modal.wizard-modal.service',
'horizon.framework.widgets.toast.service'
'horizon.framework.widgets.toast.service',
'$location'
];
/**
@ -52,7 +53,8 @@
resourceType,
actionResultService,
wizardModalService,
toast
toast,
$location
) {
var service = {
perform: perform,
@ -64,11 +66,24 @@
////////////
function allowed() {
return policy.ifAllowed(
// NOTE(lajos katona): in case of admin let's disable create action.
// TODO(lajos katona): make possible to create/edit from admin panel
var fromNonAdminUrl = ($location.url().indexOf('admin') === -1);
var deferred = $q.defer();
policy.ifAllowed(
{rules: [
['network', 'create_trunk']
]}
);
).then(function(result) {
if (fromNonAdminUrl) {
deferred.resolve(result);
} else {
deferred.reject();
}
});
return deferred.promise;
}
function perform() {

View File

@ -21,13 +21,15 @@
var $q, $scope, service, modalWaitSpinnerService, deferred, $timeout;
var location = {
url: function() {
return "project/trunks";
}
};
var policyAPI = {
ifAllowed: function() {
return {
success: function(callback) {
callback({allowed: true});
}
};
return $q.when({allowed: true});
}
};
@ -80,6 +82,7 @@
neutronAPI);
$provide.value('horizon.app.core.openstack-service-api.userSession',
userSession);
$provide.value('$location', location);
}));
beforeEach(inject(function($injector, $rootScope, _$q_, _$timeout_) {
@ -93,13 +96,31 @@
);
}));
it('should check the policy if the user is allowed to create trunks', function() {
it('should check the policy if the user is allowed to create trunks', function(done) {
spyOn(policyAPI, 'ifAllowed').and.callThrough();
var allowed = service.allowed();
expect(allowed).toBeTruthy();
expect(policyAPI.ifAllowed).toHaveBeenCalledWith(
{ rules: [['network', 'create_trunk']] }
);
spyOn(location, 'url').and.callThrough();
service.allowed().then(function(result) {
expect(result).toBeTruthy();
expect(policyAPI.ifAllowed).toHaveBeenCalledWith(
{ rules: [['network', 'create_trunk']] }
);
done();
});
$scope.$digest();
});
it('Allowed should be rejected in case of admin', function(done) {
spyOn(policyAPI, 'ifAllowed').and.callThrough();
spyOn(location, 'url').and.returnValue('admin/trunks');
service.allowed().then(null, function(result) {
expect(result).toBeUndefined();
done();
});
$scope.$digest();
});
it('open the modal with the correct parameters', function() {

View File

@ -32,7 +32,8 @@
'horizon.app.core.trunks.resourceType',
'horizon.framework.util.actions.action-result.service',
'horizon.framework.widgets.modal.wizard-modal.service',
'horizon.framework.widgets.toast.service'
'horizon.framework.widgets.toast.service',
'$rootScope'
];
/**
@ -51,8 +52,17 @@
resourceType,
actionResultService,
wizardModalService,
toast
toast,
$rootScope
) {
// Note(lajos katona): To have a workaround for the fact that on the details
// page there is no way to find out if we are in the project or the admin
// dashboard, try to fetch the previous url by catching the locationChangesucces
// event.
var urlFromLocationChangeNonAdmin = true;
$rootScope.$on('$locationChangeSuccess', function(event, newUrl, oldUrl) {
urlFromLocationChangeNonAdmin = (oldUrl.indexOf('admin') === -1);
});
var service = {
perform: perform,
allowed: allowed
@ -62,18 +72,31 @@
////////////
function allowed() {
return policy.ifAllowed(
// NOTE(lajos katona): in case of admin let's disable edit action.
// TODO(lajos katona): make possible to create/edit from admin panel
var fromNonAdminUrl = ($location.url().indexOf('admin') === -1);
var deferred = $q.defer();
policy.ifAllowed(
{rules: [
['network', 'add_subports'],
['network', 'remove_subports']
]}
);
).then(function(result) {
if (fromNonAdminUrl && urlFromLocationChangeNonAdmin) {
deferred.resolve(result);
} else {
deferred.reject();
}
});
return deferred.promise;
}
function perform(selected) {
var params = {};
if ($location.url().indexOf('admin') === -1) {
if (($location.url().indexOf('admin') === -1) && urlFromLocationChangeNonAdmin) {
params = {project_id: userSession.project_id};
}

View File

@ -21,13 +21,15 @@
var $q, $scope, service, modalWaitSpinnerService, deferred, $timeout;
var location = {
url: function() {
return "project/trunks";
}
};
var policyAPI = {
ifAllowed: function() {
return {
success: function(callback) {
callback({allowed: true});
}
};
return $q.when({allowed: true});
}
};
@ -85,6 +87,7 @@
neutronAPI);
$provide.value('horizon.app.core.openstack-service-api.userSession',
userSession);
$provide.value('$location', location);
}));
beforeEach(inject(function($injector, $rootScope, _$q_, _$timeout_) {
@ -94,17 +97,35 @@
deferred = $q.defer();
service = $injector.get('horizon.app.core.trunks.actions.edit.service');
modalWaitSpinnerService = $injector.get(
'horizon.framework.widgets.modal-wait-spinner.service'
);
'horizon.framework.widgets.modal-wait-spinner.service'
);
}));
it('should check the policy if the user is allowed to update trunks', function() {
it('should check the policy if the user is allowed to update trunks', function(done) {
spyOn(policyAPI, 'ifAllowed').and.callThrough();
var allowed = service.allowed();
expect(allowed).toBeTruthy();
expect(policyAPI.ifAllowed).toHaveBeenCalledWith(
{ rules: [['network', 'add_subports'], ['network', 'remove_subports']] }
);
spyOn(location, 'url').and.callThrough();
service.allowed().then(function(result) {
expect(result).toBeTruthy();
expect(policyAPI.ifAllowed).toHaveBeenCalledWith(
{ rules: [['network', 'add_subports'], ['network', 'remove_subports']] }
);
done();
});
$scope.$digest();
});
it('Allowed should be rejected in case of admin', function(done) {
spyOn(policyAPI, 'ifAllowed').and.callThrough();
spyOn(location, 'url').and.returnValue('admin/trunks');
service.allowed().then(null, function(result) {
expect(result).toBeUndefined();
done();
});
$scope.$digest();
});
it('open the modal with the correct parameters', function() {

View File

@ -184,6 +184,14 @@
redirectTo: goToAngularDetails
});
$routeProvider.when('/admin/trunks', {
templateUrl: path + 'panel.html'
});
$routeProvider.when('/admin/trunk/:id/detail', {
redirectTo: goToAngularDetails
});
function goToAngularDetails(params) {
return detailRoute + 'OS::Neutron::Trunk/' + params.id;
}

View File

@ -24,7 +24,8 @@
'horizon.app.core.openstack-service-api.neutron',
'horizon.app.core.openstack-service-api.userSession',
'horizon.app.core.detailRoute',
'$location'
'$location',
'$window'
];
/*
@ -37,7 +38,7 @@
* but do not need to be restricted to such use. Each exposed function
* is documented below.
*/
function trunksService(neutron, userSession, detailRoute, $location) {
function trunksService(neutron, userSession, detailRoute, $location, $window) {
return {
getDetailsPath: getDetailsPath,
@ -68,7 +69,17 @@
return userSession.get().then(getTrunksForProject);
function getTrunksForProject(userSession) {
params.project_id = userSession.project_id;
var locationURLNotAdmin = ($location.url().indexOf('admin') === -1);
// Note(lajoskatona): To list all trunks in case of
// the listing is for the Admin panel, check here the
// location.url.
// there should be a better way to check for admin or project panel??
if (locationURLNotAdmin) {
params.project_id = userSession.project_id;
} else {
delete params.project_id;
}
return neutron.getTrunks(params).then(addTrackBy);
}
@ -112,10 +123,10 @@
function getTrunkError(trunk) {
// TODO(bence romsics): When you delete a trunk from the details
// view then it cannot be re-read (of course) and we handle that
// by a hard-coded redirect to the project panel. This is okay
// for now. But when we want this panel to work for admin too,
// we should not hard-code this anymore.
$location.url('project/trunks');
// by window.histoy.back(). This is a workaround and must be deleted
// as soon as there is a final solution for the promels with ngDetails
// pages.
$window.history.back();
return trunk;
}
}

View File

@ -18,19 +18,20 @@
'use strict';
describe('trunks service', function() {
var service, _location_;
var service, neutron, session, _location_;
beforeEach(module('horizon.framework.util'));
beforeEach(module('horizon.framework.conf'));
beforeEach(module('horizon.app.core.trunks'));
beforeEach(inject(function($injector, $location) {
service = $injector.get('horizon.app.core.trunks.service');
neutron = $injector.get('horizon.app.core.openstack-service-api.neutron');
session = $injector.get('horizon.app.core.openstack-service-api.userSession');
_location_ = $location;
}));
describe('getTrunkPromise', function() {
it('provides a promise', inject(function($q, $injector, $timeout) {
var neutron = $injector.get('horizon.app.core.openstack-service-api.neutron');
it('provides a promise', inject(function($q, $timeout) {
var deferred = $q.defer();
spyOn(neutron, 'getTrunk').and.returnValue(deferred.promise);
var result = service.getTrunkPromise({});
@ -40,21 +41,7 @@
expect(result.$$state.value.data.updated_at).toBe('May29');
}));
it('redirects back to panel on failure', inject(function($q, $injector, $timeout) {
var neutron = $injector.get('horizon.app.core.openstack-service-api.neutron');
var deferred = $q.defer();
spyOn(neutron, 'getTrunk').and.returnValue(deferred.promise);
spyOn(_location_, 'url');
service.getTrunkPromise({});
deferred.reject();
$timeout.flush();
expect(neutron.getTrunk).toHaveBeenCalled();
expect(_location_.url).toHaveBeenCalledWith('project/trunks');
}));
it('provides a promise that gets translated', inject(function($q, $injector, $timeout) {
var neutron = $injector.get('horizon.app.core.openstack-service-api.neutron');
var session = $injector.get('horizon.app.core.openstack-service-api.userSession');
it('provides a promise that gets translated', inject(function($q, $timeout) {
var deferred = $q.defer();
var deferredSession = $q.defer();
var updatedAt = new Date('November 15, 2017');
@ -64,7 +51,24 @@
deferred.resolve({data: {items: [{id: 1, updated_at: updatedAt}]}});
deferredSession.resolve({project_id: '42'});
$timeout.flush();
expect(neutron.getTrunks).toHaveBeenCalled();
expect(neutron.getTrunks).toHaveBeenCalledWith({project_id: '42'});
expect(result.$$state.value.data.items[0].updated_at).toBe(updatedAt);
expect(result.$$state.value.data.items[0].id).toBe(1);
}));
it('removes project_id in case of calling from admin panel',
inject(function($q, $timeout) {
var deferred = $q.defer();
var deferredSession = $q.defer();
var updatedAt = new Date('November 15, 2017');
spyOn(neutron, 'getTrunks').and.returnValue(deferred.promise);
spyOn(session, 'get').and.returnValue(deferredSession.promise);
spyOn(_location_, 'url').and.returnValue('/admin/trunks');
var result = service.getTrunksPromise({project_id: '43'});
deferred.resolve({data: {items: [{id: 1, updated_at: updatedAt}]}});
deferredSession.resolve({project_id: '42'});
$timeout.flush();
expect(neutron.getTrunks).toHaveBeenCalledWith({});
expect(result.$$state.value.data.items[0].updated_at).toBe(updatedAt);
expect(result.$$state.value.data.items[0].id).toBe(1);
}));

View File

@ -297,11 +297,16 @@ TEST_GLOBAL_MOCKS_ON_PANELS = {
'.aggregates.panel.Aggregates.can_access'),
'return_value': True,
},
'trunk': {
'trunk-project': {
'method': ('openstack_dashboard.dashboards.project'
'.trunks.panel.Trunks.can_access'),
'return_value': True,
},
'trunk-admin': {
'method': ('openstack_dashboard.dashboards.admin'
'.trunks.panel.Trunks.can_access'),
'return_value': True,
},
'qos': {
'method': ('openstack_dashboard.dashboards.project'
'.network_qos.panel.NetworkQoS.can_access'),

View File

@ -2,7 +2,8 @@
features:
- |
[:blueprint:`neutron-trunk-ui`]
Neutron trunk feature is now supported in the project dashboard.
Neutron trunk feature is now supported. It is supported in both the
project and admin dashboards.
The panel will be displayed if Neutron API extension 'trunk' is available.
It displays information about trunks. The details page for each trunk also
shows information about subports of that trunk.