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:
parent
f1eba8073d
commit
aa669f0d02
45
openstack_dashboard/dashboards/admin/trunks/panel.py
Normal file
45
openstack_dashboard/dashboards/admin/trunks/panel.py
Normal 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
|
26
openstack_dashboard/dashboards/admin/trunks/urls.py
Normal file
26
openstack_dashboard/dashboards/admin/trunks/urls.py
Normal 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'),
|
||||
]
|
9
openstack_dashboard/enabled/_2340_admin_trunks_panel.py
Normal file
9
openstack_dashboard/enabled/_2340_admin_trunks_panel.py
Normal 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'
|
@ -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 = [
|
||||
|
||||
{
|
||||
|
@ -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() {
|
||||
|
@ -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() {
|
||||
|
@ -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};
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}));
|
||||
|
@ -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'),
|
||||
|
@ -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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user