diff --git a/openstack_dashboard/dashboards/admin/trunks/__init__.py b/openstack_dashboard/dashboards/admin/trunks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstack_dashboard/dashboards/admin/trunks/panel.py b/openstack_dashboard/dashboards/admin/trunks/panel.py new file mode 100644 index 0000000000..a5d375b54e --- /dev/null +++ b/openstack_dashboard/dashboards/admin/trunks/panel.py @@ -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 diff --git a/openstack_dashboard/dashboards/admin/trunks/urls.py b/openstack_dashboard/dashboards/admin/trunks/urls.py new file mode 100644 index 0000000000..7cc3d1763f --- /dev/null +++ b/openstack_dashboard/dashboards/admin/trunks/urls.py @@ -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[^/]+)/$', + AngularIndexView.as_view(title=title), name='detail'), +] diff --git a/openstack_dashboard/enabled/_2340_admin_trunks_panel.py b/openstack_dashboard/enabled/_2340_admin_trunks_panel.py new file mode 100644 index 0000000000..ec828cca10 --- /dev/null +++ b/openstack_dashboard/enabled/_2340_admin_trunks_panel.py @@ -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' diff --git a/openstack_dashboard/static/app/core/openstack-service-api/neutron.service.spec.js b/openstack_dashboard/static/app/core/openstack-service-api/neutron.service.spec.js index 3daa0b1134..f6d2d167c9 100644 --- a/openstack_dashboard/static/app/core/openstack-service-api/neutron.service.spec.js +++ b/openstack_dashboard/static/app/core/openstack-service-api/neutron.service.spec.js @@ -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 = [ { diff --git a/openstack_dashboard/static/app/core/trunks/actions/create.action.service.js b/openstack_dashboard/static/app/core/trunks/actions/create.action.service.js index ea65d5edf6..445684638d 100644 --- a/openstack_dashboard/static/app/core/trunks/actions/create.action.service.js +++ b/openstack_dashboard/static/app/core/trunks/actions/create.action.service.js @@ -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() { diff --git a/openstack_dashboard/static/app/core/trunks/actions/create.action.service.spec.js b/openstack_dashboard/static/app/core/trunks/actions/create.action.service.spec.js index c105278f88..88bbf620c5 100644 --- a/openstack_dashboard/static/app/core/trunks/actions/create.action.service.spec.js +++ b/openstack_dashboard/static/app/core/trunks/actions/create.action.service.spec.js @@ -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() { diff --git a/openstack_dashboard/static/app/core/trunks/actions/edit.action.service.js b/openstack_dashboard/static/app/core/trunks/actions/edit.action.service.js index 0087568356..3fa9954d63 100644 --- a/openstack_dashboard/static/app/core/trunks/actions/edit.action.service.js +++ b/openstack_dashboard/static/app/core/trunks/actions/edit.action.service.js @@ -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}; } diff --git a/openstack_dashboard/static/app/core/trunks/actions/edit.action.service.spec.js b/openstack_dashboard/static/app/core/trunks/actions/edit.action.service.spec.js index 3f7701be45..ffb7e9d475 100644 --- a/openstack_dashboard/static/app/core/trunks/actions/edit.action.service.spec.js +++ b/openstack_dashboard/static/app/core/trunks/actions/edit.action.service.spec.js @@ -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() { diff --git a/openstack_dashboard/static/app/core/trunks/trunks.module.js b/openstack_dashboard/static/app/core/trunks/trunks.module.js index d7c533aee7..4b0656715c 100644 --- a/openstack_dashboard/static/app/core/trunks/trunks.module.js +++ b/openstack_dashboard/static/app/core/trunks/trunks.module.js @@ -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; } diff --git a/openstack_dashboard/static/app/core/trunks/trunks.service.js b/openstack_dashboard/static/app/core/trunks/trunks.service.js index 30fe0941a8..a215bc8696 100644 --- a/openstack_dashboard/static/app/core/trunks/trunks.service.js +++ b/openstack_dashboard/static/app/core/trunks/trunks.service.js @@ -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; } } diff --git a/openstack_dashboard/static/app/core/trunks/trunks.service.spec.js b/openstack_dashboard/static/app/core/trunks/trunks.service.spec.js index 8308ce0d2f..c96c357977 100644 --- a/openstack_dashboard/static/app/core/trunks/trunks.service.spec.js +++ b/openstack_dashboard/static/app/core/trunks/trunks.service.spec.js @@ -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); })); diff --git a/openstack_dashboard/test/settings.py b/openstack_dashboard/test/settings.py index 8e2b3f9fa8..7e6a82bd97 100644 --- a/openstack_dashboard/test/settings.py +++ b/openstack_dashboard/test/settings.py @@ -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'), diff --git a/releasenotes/notes/bp-neutron-trunk-ui-queens-1d59df887b9a079a.yaml b/releasenotes/notes/bp-neutron-trunk-ui-queens-1d59df887b9a079a.yaml index 4b5261d89c..da73d0c937 100644 --- a/releasenotes/notes/bp-neutron-trunk-ui-queens-1d59df887b9a079a.yaml +++ b/releasenotes/notes/bp-neutron-trunk-ui-queens-1d59df887b9a079a.yaml @@ -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.